diff --git a/src/common/stores/workspace/WorkspaceIdProvider.tsx b/src/common/stores/workspace/WorkspaceIdProvider.tsx index 799da3bc3..01014508a 100644 --- a/src/common/stores/workspace/WorkspaceIdProvider.tsx +++ b/src/common/stores/workspace/WorkspaceIdProvider.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import type { DConversationId } from '~/common/stores/chat/chat.conversation'; -import { workspaceForConversationIdentity, DWorkspaceId } from './workspace.types'; +import { DWorkspaceId, workspaceForConversationIdentity } from './workspace.types'; // The Context and the data it will prop-drill @@ -40,6 +40,5 @@ export function useContextWorkspaceId() { const value = React.useContext(WorkspaceContext); if (!value) throw new Error('Missing WorkspaceProvider'); - console.log('useContextWorkspaceId', value); return value.workspaceId; } diff --git a/src/common/stores/workspace/useWorkspaceContentsMetadata.ts b/src/common/stores/workspace/useWorkspaceContentsMetadata.ts new file mode 100644 index 000000000..15d52be02 --- /dev/null +++ b/src/common/stores/workspace/useWorkspaceContentsMetadata.ts @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { useShallow } from 'zustand/react/shallow'; + +import type { LiveFile, LiveFileId, LiveFileMetadata } from '~/common/livefile/liveFile.types'; +import { useLiveFileStore } from '~/common/livefile/store-live-file'; + +import type { DWorkspaceId } from './workspace.types'; +import { useClientWorkspaceStore } from './store-client-workspace'; + + +export interface WorkspaceContents { + workspaceId: DWorkspaceId; + liveFilesMetadata: LiveFileMetadata[]; +} + +export function useWorkspaceContentsMetadata(workspaceId: DWorkspaceId | null): WorkspaceContents { + + // stable reference to the LiveFileIds + // - w/out useShallow as updates to the array contents are real + const workspaceLiveFileIds: LiveFileId[] | null = useClientWorkspaceStore(state => { + if (!workspaceId) return null; + + // as we only have LiveFiles, stop if we don't have any + if (!state.liveFilesByWorkspace[workspaceId]?.length) + return null; + + // re-renders if the array changes at all + return state.liveFilesByWorkspace[workspaceId]; + }); + + // reactive stable reference to the LiveFiles + // - with useShallow as map recreates the array every time + const workspaceLiveFiles: LiveFile[] | null = useLiveFileStore(useShallow(state => { + // stop if we are not referencing any LiveFiles + if (!workspaceLiveFileIds?.length) + return null; + + // re-render if any LiveFile changes, is added or removed + // upcast liveFile.id -> liveFile, skipping missing, with a stable array check + return workspaceLiveFileIds.map(id => state.liveFiles[id]).filter(Boolean); + })); + + // re-renders (returns a new object) every time a dependency changes + return React.useMemo(() => { + // creation of the woekspace contents (stabilized thought the memo inputs) + const { metadataGet } = useLiveFileStore.getState(); + const liveFilesMetadata = (workspaceLiveFiles || []).map(lf => metadataGet(lf.id)).filter(Boolean) as LiveFileMetadata[]; + return { + workspaceId: workspaceId!, + liveFilesMetadata, + }; + }, [workspaceId, workspaceLiveFiles]); +} diff --git a/src/common/stores/workspace/workspace.hooks.ts b/src/common/stores/workspace/workspace.hooks.ts deleted file mode 100644 index a53e17629..000000000 --- a/src/common/stores/workspace/workspace.hooks.ts +++ /dev/null @@ -1,50 +0,0 @@ -// import * as React from 'react'; -// import { useShallow } from 'zustand/react/shallow'; -// -// import type { LiveFile, LiveFileId, LiveFileMetadata } from '~/common/livefile/liveFile.types'; -// import { useLiveFileStore } from '~/common/livefile/store-live-file'; -// -// import type { DWorkspaceId } from './workspace.types'; -// import { useClientWorkspaceStore } from './store-client-workspace'; -// -// -// export interface WorkspaceContents { -// workspaceId: DWorkspaceId; -// liveFilesMetadata: LiveFileMetadata[]; -// } -// -// // const stableWorkspace: WorkspaceContents = { -// // liveFilesMetadata: [], -// // }; -// -// export function useWorkspaceContents(workspaceId: DWorkspaceId | null): WorkspaceContents | null { -// -// // stable reference to the LiveFileIds -// // - w/out useShallow as updates to the array contents are real -// const workspaceLiveFileIds: LiveFileId[] | null = useClientWorkspaceStore(state => -// (!workspaceId || !state.liveFilesByWorkspace[workspaceId]?.length) ? null -// : state.liveFilesByWorkspace[workspaceId], -// ); -// -// // reactive stable reference to the LiveFiles -// // - with useShallow as map recreates the array every time -// const workspaceLiveFiles: LiveFile[] | null = useLiveFileStore(useShallow(state => -// !workspaceLiveFileIds?.length ? null -// : workspaceLiveFileIds.map(id => state.liveFiles[id]).filter(Boolean), -// )); -// -// return React.useMemo(() => { -// // stable out -// // if (!workspaceLiveFiles || !workspaceLiveFiles.length) -// // return null; // stableWorkspace; -// console.log('workspaceLiveFileIds', { workspaceId, workspaceLiveFiles }); -// -// // creation of the woekspace contents (stabilized thought the memo inputs) -// const { metadataGet } = useLiveFileStore.getState(); -// const liveFilesMetadata = (workspaceLiveFiles || []).map(lf => metadataGet(lf.id)).filter(Boolean) as LiveFileMetadata[]; -// return { -// workspaceId: workspaceId!, -// liveFilesMetadata, -// }; -// }, [workspaceId, workspaceLiveFiles]); -// } diff --git a/src/modules/blocks/enhanced-code/livefile-patch/useLiveFilePatch.tsx b/src/modules/blocks/enhanced-code/livefile-patch/useLiveFilePatch.tsx index b7c92a95d..613f189a8 100644 --- a/src/modules/blocks/enhanced-code/livefile-patch/useLiveFilePatch.tsx +++ b/src/modules/blocks/enhanced-code/livefile-patch/useLiveFilePatch.tsx @@ -1,11 +1,13 @@ import * as React from 'react'; -import { Typography } from '@mui/joy'; +import type { SxProps } from '@mui/joy/styles/types'; +import { Box, Option, Select, Typography } from '@mui/joy'; import { useContextWorkspaceId } from '~/common/stores/workspace/WorkspaceIdProvider'; +import { useWorkspaceContentsMetadata } from '~/common/stores/workspace/useWorkspaceContentsMetadata'; -import type { LiveFileId } from '~/common/livefile/liveFile.types'; -import { SxProps } from '@mui/joy/styles/types'; +import type { LiveFileId, LiveFileMetadata } from '~/common/livefile/liveFile.types'; +import { LiveFileIcon } from '~/common/livefile/liveFile.icons'; import { isLiveFileSupported } from '~/common/livefile/store-live-file'; import { useUXLabsStore } from '~/common/state/store-ux-labs'; @@ -17,31 +19,92 @@ const buttonContainerSx: SxProps = { export function useLiveFilePatch(title: string, code: string, isPartial: boolean, isMobile: boolean) { + // state /** - * (very) local state - * This will get wiped just on a component remount - so it's just a temporary 'solution'. + * Warning: very local. + * This will get wiped just on a component remount - so it's just a temporary solution. */ const [liveFileId, setLiveFileId] = React.useState(null); - // state - const isEnabled = useUXLabsStore((state) => state.labsEnhanceCodeLiveFile && isLiveFileSupported()); - console.log('isEnabled', isEnabled); + // external state + let isEnabled = useUXLabsStore((state) => state.labsEnhanceCodeLiveFile && isLiveFileSupported()); + const workspaceId = useContextWorkspaceId(); + const { liveFilesMetadata } = useWorkspaceContentsMetadata(isEnabled ? workspaceId : null); + + // [effect] auto-select a LiveFileId + React.useEffect(() => { + if (liveFilesMetadata.length === 1) { + // auto-select the only LiveFile + setLiveFileId(liveFilesMetadata[0].id); + } else { + // auto-select matching the title + const lfm = liveFilesMetadata.findLast(lfm => lfm.name === title); + if (lfm) + setLiveFileId(lfm.id); + } + }, [isEnabled, liveFilesMetadata, title]); + + + // reset enablement if no live files + if (!liveFilesMetadata?.length) + isEnabled = false; + + + // handlers + + const handleLiveFileChange = React.useCallback((event: unknown, value: LiveFileId | null) => { + setLiveFileId(value); + }, []); + // components + // const + const button = React.useMemo(() => !isEnabled ? null : ( - - {liveFileId || 'aaa'} - - ), [isEnabled, liveFileId]); + + + + ), [handleLiveFileChange, isEnabled, liveFileId, liveFilesMetadata]); - const actionBar = React.useMemo(() => (!isEnabled || !liveFileId) ? null : ( + const actionBar = React.useMemo(() => (!isEnabled || !liveFilesMetadata || true) ? null : ( - test + {JSON.stringify(liveFilesMetadata)} - ), [isEnabled, liveFileId]); + ), [liveFilesMetadata, isEnabled]); - const workspaceId = useContextWorkspaceId(); return { button,