diff --git a/src/components/tree-browsers/project-tree-browser/index.tsx b/src/components/editors/EntityEditor/EntityTree/index.tsx similarity index 50% rename from src/components/tree-browsers/project-tree-browser/index.tsx rename to src/components/editors/EntityEditor/EntityTree/index.tsx index 94d1bb84..66030894 100644 --- a/src/components/tree-browsers/project-tree-browser/index.tsx +++ b/src/components/editors/EntityEditor/EntityTree/index.tsx @@ -1,15 +1,20 @@ 'use client' import { cn } from '@/lib/utils' -import { isClapEntity } from '../utils/isSomething' -import { useProjectLibrary } from '../stores/useProjectLibrary' -import { LibraryNodeItem, LibraryNodeType } from '../types' +import { isClapEntity } from '@/components/tree-browsers/utils/isSomething' +import { TreeNodeItem, LibraryNodeType } from '@/components/tree-browsers/types' import { Tree } from '@/components/core/tree' -export function ProjectTreeBrowser() { - const libraryTreeRoot = useProjectLibrary((s) => s.libraryTreeRoot) - const selectTreeNode = useProjectLibrary((s) => s.selectTreeNode) - const selectedTreeNodeId = useProjectLibrary((s) => s.selectedTreeNodeId) +import { useEntityTree } from './useEntityTree' + +export function EntityTree({ + className = '', +}: { + className?: string +} = {}) { + const libraryTreeRoot = useEntityTree((s) => s.libraryTreeRoot) + const selectTreeNode = useEntityTree((s) => s.selectTreeNode) + const selectedTreeNodeId = useEntityTree((s) => s.selectedTreeNodeId) /** * handle click on tree node @@ -23,7 +28,7 @@ export function ProjectTreeBrowser() { const handleOnChange = async ( id: string | null, nodeType?: LibraryNodeType, - nodeItem?: LibraryNodeItem + nodeItem?: TreeNodeItem ) => { console.log(`calling selectTreeNodeById(id)`) selectTreeNode(id, nodeType, nodeItem) @@ -44,17 +49,15 @@ export function ProjectTreeBrowser() { } return ( -
- - value={selectedTreeNodeId} - onChange={handleOnChange} - className="not-prose h-full w-full px-2 pt-8" - label="Project Library" - > - {libraryTreeRoot.map((node) => ( - - ))} - -
+ + value={selectedTreeNodeId} + onChange={handleOnChange} + className={cn(`not-prose h-full w-full px-2 pt-2`, className)} + label="Entities" + > + {libraryTreeRoot.map((node) => ( + + ))} + ) } diff --git a/src/components/editors/EntityEditor/EntityTree/useEntityTree.ts b/src/components/editors/EntityEditor/EntityTree/useEntityTree.ts new file mode 100644 index 00000000..d8f3fe8c --- /dev/null +++ b/src/components/editors/EntityEditor/EntityTree/useEntityTree.ts @@ -0,0 +1,207 @@ +'use client' + +import { create } from 'zustand' +import { ClapEntity, UUID } from '@aitube/clap' +import { + LibraryTreeNode, + TreeNodeItem, + LibraryNodeType, +} from '@/components/tree-browsers/types' +import { icons } from '@/components/icons' +import { getAppropriateIcon } from '@/components/icons/getAppropriateIcon' +import { + collectionClassName, + libraryClassName, +} from '@/components/tree-browsers/style/treeNodeStyles' + +export const useEntityTree = create<{ + // project entities stored in the .clap + projectLibraryTreeNodeId: string + + // in the future, we are going to put + // + + // entities stored on the public database (Hugging Face datasets, tagged) + communityLibraryTreeNodeId: string + + libraryTreeRoot: LibraryTreeNode[] + init: () => void + + /** + * Load entity collections (characters, locations..) from the clap project into the tree + * + * @param collections + * @returns + */ + // setProjectLibrary: (collections: ProjectEntityCollection[]) => void + + /** + * Load entity collections (characters, locations..) from the Clapper community into the tree + * + * @param collections + * @returns + */ + // setCommunityLibrary: (collections: CommunityEntityCollection[]) => void + + // we support those all selection modes for convenience - please keep them! + selectedNodeItem?: TreeNodeItem + selectedNodeType?: LibraryNodeType + selectTreeNode: ( + treeNodeId?: string | null, + nodeType?: LibraryNodeType, + nodeItem?: TreeNodeItem + ) => void + selectedTreeNodeId: string | null +}>((set, get) => ({ + // project entities stored in the .clap + projectLibraryTreeNodeId: '', + + // in the future, we are going to put + // + + // entities stored on the public database (Hugging Face datasets, tagged) + communityLibraryTreeNodeId: '', + libraryTreeRoot: [], + init: () => { + const projectLibrary: LibraryTreeNode = { + id: UUID(), + nodeType: 'TREE_ROOT_PROJECT', + label: 'Project entities', + icon: icons.project, + className: libraryClassName, + isExpanded: true, + children: [ + { + id: UUID(), + nodeType: 'DEFAULT_TREE_NODE_EMPTY', + label: 'Empty', + icon: icons.project, + className: collectionClassName, + }, + ], + } + + const communityLibrary: LibraryTreeNode = { + id: UUID(), + nodeType: 'TREE_ROOT_COMMUNITY', + label: 'Community entities', + icon: icons.community, + className: libraryClassName, + children: [ + { + id: UUID(), + nodeType: 'DEFAULT_TREE_NODE_EMPTY', + label: 'Empty', + icon: icons.community, + className: collectionClassName, + }, + ], + } + + const libraryTreeRoot = [projectLibrary, communityLibrary] + + set({ + projectLibraryTreeNodeId: projectLibrary.id, + communityLibraryTreeNodeId: communityLibrary.id, + libraryTreeRoot, + selectedNodeItem: undefined, + selectedTreeNodeId: null, + }) + }, + + /* + setProjectEntities: async (entities: ClapEntity[]) => { + const characters: LibraryTreeNode = { + id: UUID(), + nodeType: 'LIB_NODE_GENERIC_COLLECTION', + data: undefined, + label: 'Characters', + icon: icons.characters, + className: collectionClassName, + isExpanded: true, // This node is expanded by default + children: [], + } + + const locations: LibraryTreeNode = { + id: UUID(), + nodeType: 'LIB_NODE_GENERIC_COLLECTION', + data: undefined, + label: 'Locations', + icon: icons.location, + className: collectionClassName, + isExpanded: false, // This node is expanded by default + children: [], + } + + const misc: LibraryTreeNode = { + id: UUID(), + nodeType: 'LIB_NODE_GENERIC_COLLECTION', + data: undefined, + label: 'Misc', + icon: icons.misc, + className: collectionClassName, + isExpanded: false, // This node is expanded by default + children: [], + } + + entities.forEach((entity) => { + const node: LibraryTreeNode = { + nodeType: TreeNodeEntityItem, + id: entity.id, + data: entity, + label: entity.label, + icon: icons.misc, + className: itemClassName, + } + if (entity.category === ClapSegmentCategory.CHARACTER) { + node.icon = icons.character + characters.children!.push(node) + } else if (entity.category === ClapSegmentCategory.LOCATION) { + node.icon = icons.location + locations.children!.push(node) + } else { + misc.children!.push(node) + } + }) + }, + + setCommunityCollections: (collections: CommunityEntityCollection[]) => { + // TODO: implement this + + }, + */ + + selectedNodeItem: undefined, + selectEntity: (entity?: ClapEntity) => { + if (entity) { + console.log( + 'TODO julian: change this code to search in the entity collections' + ) + const selectedTreeNode = get().libraryTreeRoot.find( + (node) => node.data?.id === entity.id + ) + + // set({ selectedTreeNode }) + set({ selectedTreeNodeId: selectedTreeNode?.id || null }) + set({ selectedNodeItem: entity }) + } else { + // set({ selectedTreeNode: undefined }) + set({ selectedTreeNodeId: null }) + set({ selectedNodeItem: undefined }) + } + }, + + // selectedTreeNode: undefined, + selectedTreeNodeId: null, + selectTreeNode: ( + treeNodeId?: string | null, + nodeType?: LibraryNodeType, + nodeItem?: TreeNodeItem + ) => { + set({ selectedTreeNodeId: treeNodeId ? treeNodeId : undefined }) + set({ selectedNodeType: nodeType ? nodeType : undefined }) + set({ selectedNodeItem: nodeItem ? nodeItem : undefined }) + }, +})) + +useEntityTree.getState().init() diff --git a/src/components/editors/EntityEditor/EntityViewer/EntityList.tsx b/src/components/editors/EntityEditor/EntityViewer/EntityList.tsx new file mode 100644 index 00000000..5671b93a --- /dev/null +++ b/src/components/editors/EntityEditor/EntityViewer/EntityList.tsx @@ -0,0 +1,65 @@ +import { ClapEntity, ClapSegmentCategory, newEntity } from '@aitube/clap' +import { useTimeline } from '@aitube/timeline' + +import { Button } from '@/components/ui/button' +import { useEntityEditor, useIO } from '@/services' + +export function EntityList({ + onSelectEntity, +}: { + onSelectEntity: (entityId: string) => void +}) { + const entities = useTimeline((s) => s.entities) + const setCurrent = useEntityEditor((s) => s.setCurrent) + const addEntity = useEntityEditor((s) => s.addEntity) + const removeEntity = useEntityEditor((s) => s.removeEntity) + + const handleAddEntity = () => { + const entity: ClapEntity = newEntity({ + id: Date.now().toString(), + label: 'NEW_ENTITY', + category: ClapSegmentCategory.CHARACTER, + description: '', + appearance: '', + }) // ignoring some fields for now + addEntity(entity) + } + + return ( +
+
+

Entities

+ +
+
    + {entities.map((entity: ClapEntity) => ( +
  • + + +
  • + ))} +
+
+ ) +} diff --git a/src/components/editors/EntityEditor/EntityViewer/index.tsx b/src/components/editors/EntityEditor/EntityViewer/index.tsx new file mode 100644 index 00000000..a1d8f359 --- /dev/null +++ b/src/components/editors/EntityEditor/EntityViewer/index.tsx @@ -0,0 +1,192 @@ +import { useEffect } from 'react' +import { ClapEntity, ClapSegmentCategory } from '@aitube/clap' +import { useTimeline } from '@aitube/timeline' + +import { FormFile } from '@/components/forms/FormFile' +import { FormInput } from '@/components/forms/FormInput' +import { FormSection } from '@/components/forms/FormSection' +import { FormSelect } from '@/components/forms/FormSelect' +import { Button } from '@/components/ui/button' +import { useEntityEditor, useIO } from '@/services' +import { cn } from '@/lib/utils' + +import { EntityList } from './EntityList' + +export function EntityViewer({ + className = '', +}: { + className?: string +} = {}) { + const entities = useTimeline((s) => s.entities) + const updateEntities = useTimeline((s) => s.updateEntities) + + const saveEntitiesToClap = useIO((s) => s.saveEntitiesToClap) + const openEntitiesFromClap = useIO((s) => s.openEntitiesFromClap) + + const current = useEntityEditor((s) => s.current) + const setCurrent = useEntityEditor((s) => s.setCurrent) + + const draft = useEntityEditor((s) => s.draft) + const setDraft = useEntityEditor((s) => s.setDraft) + + const showEntityList = useEntityEditor((s) => s.showEntityList) + const setShowEntityList = useEntityEditor((s) => s.setShowEntityList) + + useEffect(() => { + setCurrent(entities.at(0)) + }, [entities, setCurrent]) + + useEffect(() => { + setDraft(current) + }, [current, setDraft]) + + const handleInputChange = ( + field: keyof ClapEntity, + value: string | number | undefined + ) => { + if (!draft) { + return + } + let updatedValue = value + if (field === 'age') { + updatedValue = value === '' ? undefined : parseInt(value as string) + } + if (field === 'label') { + updatedValue = value?.toString().toUpperCase() + } + + setDraft({ ...draft, [field]: updatedValue }) + } + + const handleSave = () => { + if (!draft) { + return + } + updateEntities([draft]) + } + + const handleFileUpload = async (field: 'imageId' | 'audioId', file: File) => { + if (!draft) { + return + } + const dataUrl = await new Promise((resolve) => { + const reader = new FileReader() + reader.onload = (e) => resolve(e.target?.result as string) + reader.readAsDataURL(file) + }) + setDraft({ ...draft, [field]: dataUrl }) + } + + const handleExport = async () => { + if (!draft) { + return + } + await saveEntitiesToClap([draft]) + } + + const handleImport = async (file: File) => { + await openEntitiesFromClap(file) + } + + const handleBack = () => { + setShowEntityList(true) + } + + const handleSelectEntity = (entityId: string) => { + setShowEntityList(false) + } + + return ( +
+ {showEntityList ? ( +
+ +
+ ) : ( +
+ {draft && ( + + + handleInputChange('label', value)} + /> + + label="Category" + selectedItemId={draft.category} + items={Object.values(ClapSegmentCategory).map( + (category: ClapSegmentCategory) => ({ + id: category, + label: category, + value: category, + }) + )} + onSelect={(value) => handleInputChange('category', value)} + /> + {/* ... form fields ... */} + + files[0] && handleFileUpload('imageId', files[0]) + } + /> + {draft.imageId && ( +
+ Entity Preview +
+ )} + + files[0] && handleFileUpload('audioId', files[0]) + } + /> + {draft.audioId && ( +
+
+ )} + handleInputChange('description', value)} + /> + handleInputChange('appearance', value)} + /> + handleInputChange('age', value)} + /> + handleInputChange('gender', value)} + /> +
+ + +
+
+ files[0] && handleImport(files[0])} + /> +
+
+ )} +
+ )} +
+ ) +} diff --git a/src/components/editors/EntityEditor/index.tsx b/src/components/editors/EntityEditor/index.tsx index 5787833a..026957a6 100644 --- a/src/components/editors/EntityEditor/index.tsx +++ b/src/components/editors/EntityEditor/index.tsx @@ -1,243 +1,17 @@ -import { useEffect } from 'react' -import { ClapEntity, ClapSegmentCategory, newEntity } from '@aitube/clap' -import { useTimeline } from '@aitube/timeline' - -import { FormFile } from '@/components/forms/FormFile' -import { FormInput } from '@/components/forms/FormInput' -import { FormSection } from '@/components/forms/FormSection' -import { FormSelect } from '@/components/forms/FormSelect' -import { Button } from '@/components/ui/button' -import { useEntityEditor, useIO } from '@/services' - -function EntityList({ - onSelectEntity, -}: { - onSelectEntity: (entityId: string) => void -}) { - const entities = useTimeline((s) => s.entities) - const setCurrent = useEntityEditor((s) => s.setCurrent) - const addEntity = useEntityEditor((s) => s.addEntity) - const removeEntity = useEntityEditor((s) => s.removeEntity) - - const handleAddEntity = () => { - const entity: ClapEntity = newEntity({ - id: Date.now().toString(), - label: 'NEW_ENTITY', - category: ClapSegmentCategory.CHARACTER, - description: '', - appearance: '', - }) // ignoring some fields for now - addEntity(entity) - } - - return ( -
-
-

Entities

- -
-
    - {entities.map((entity: ClapEntity) => ( -
  • - - -
  • - ))} -
-
- ) -} +import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex' +import { EntityTree } from './EntityTree' +import { EntityViewer } from './EntityViewer' export function EntityEditor() { - const entities = useTimeline((s) => s.entities) - const updateEntities = useTimeline((s) => s.updateEntities) - - const saveEntitiesToClap = useIO((s) => s.saveEntitiesToClap) - const openEntitiesFromClap = useIO((s) => s.openEntitiesFromClap) - - const current = useEntityEditor((s) => s.current) - const setCurrent = useEntityEditor((s) => s.setCurrent) - - const draft = useEntityEditor((s) => s.draft) - const setDraft = useEntityEditor((s) => s.setDraft) - - const showEntityList = useEntityEditor((s) => s.showEntityList) - const setShowEntityList = useEntityEditor((s) => s.setShowEntityList) - - useEffect(() => { - setCurrent(entities.at(0)) - }, [entities, setCurrent]) - - useEffect(() => { - setDraft(current) - }, [current, setDraft]) - - const handleInputChange = ( - field: keyof ClapEntity, - value: string | number | undefined - ) => { - if (!draft) { - return - } - let updatedValue = value - if (field === 'age') { - updatedValue = value === '' ? undefined : parseInt(value as string) - } - if (field === 'label') { - updatedValue = value?.toString().toUpperCase() - } - - setDraft({ ...draft, [field]: updatedValue }) - } - - const handleSave = () => { - if (!draft) { - return - } - updateEntities([draft]) - } - - const handleFileUpload = async (field: 'imageId' | 'audioId', file: File) => { - if (!draft) { - return - } - const dataUrl = await new Promise((resolve) => { - const reader = new FileReader() - reader.onload = (e) => resolve(e.target?.result as string) - reader.readAsDataURL(file) - }) - setDraft({ ...draft, [field]: dataUrl }) - } - - const handleExport = async () => { - if (!draft) { - return - } - await saveEntitiesToClap([draft]) - } - - const handleImport = async (file: File) => { - await openEntitiesFromClap(file) - } - - const handleBack = () => { - setShowEntityList(true) - } - - const handleSelectEntity = (entityId: string) => { - setShowEntityList(false) - } - return ( -
- {showEntityList ? ( -
- -
- ) : ( -
- {draft && ( - - - handleInputChange('label', value)} - /> - - label="Category" - selectedItemId={draft.category} - items={Object.values(ClapSegmentCategory).map( - (category: ClapSegmentCategory) => ({ - id: category, - label: category, - value: category, - }) - )} - onSelect={(value) => handleInputChange('category', value)} - /> - {/* ... form fields ... */} - - files[0] && handleFileUpload('imageId', files[0]) - } - /> - {draft.imageId && ( -
- Entity Preview -
- )} - - files[0] && handleFileUpload('audioId', files[0]) - } - /> - {draft.audioId && ( -
-
- )} - handleInputChange('description', value)} - /> - handleInputChange('appearance', value)} - /> - handleInputChange('age', value)} - /> - handleInputChange('gender', value)} - /> -
- - -
-
- files[0] && handleImport(files[0])} - /> -
-
- )} -
- )} -
+ + + + + + + + + ) } diff --git a/src/components/editors/WorkflowEditor/WorkflowTree/index.tsx b/src/components/editors/WorkflowEditor/WorkflowTree/index.tsx new file mode 100644 index 00000000..69ed1cca --- /dev/null +++ b/src/components/editors/WorkflowEditor/WorkflowTree/index.tsx @@ -0,0 +1,63 @@ +'use client' + +import { cn } from '@/lib/utils' +import { isClapEntity } from '@/components/tree-browsers/utils/isSomething' +import { TreeNodeItem, LibraryNodeType } from '@/components/tree-browsers/types' +import { Tree } from '@/components/core/tree' + +import { useWorkflowTree } from './useWorkflowTree' + +export function WorkflowTree({ + className = '', +}: { + className?: string +} = {}) { + const libraryTreeRoot = useWorkflowTree((s) => s.libraryTreeRoot) + const selectTreeNode = useWorkflowTree((s) => s.selectTreeNode) + const selectedTreeNodeId = useWorkflowTree((s) => s.selectedTreeNodeId) + + /** + * handle click on tree node + * yes, this is where the magic happens! + * + * @param id + * @param nodeType + * @param node + * @returns + */ + const handleOnChange = async ( + id: string | null, + nodeType?: LibraryNodeType, + nodeItem?: TreeNodeItem + ) => { + console.log(`calling selectTreeNodeById(id)`) + selectTreeNode(id, nodeType, nodeItem) + + if (!nodeType || !nodeItem) { + console.log('tree-browser: clicked on an undefined node') + return + } + if (isClapEntity(nodeType, nodeItem)) { + // ClapEntity + } else { + console.log( + `tree-browser: no action attached to ${nodeType}, so skipping` + ) + return + } + console.log(`tree-browser: clicked on a ${nodeType}`, nodeItem) + } + + return ( + + value={selectedTreeNodeId} + onChange={handleOnChange} + className={cn(`not-prose h-full w-full px-2 pt-2`, className)} + label="Workflows" + > + {libraryTreeRoot.map((node) => ( + + ))} + + ) +} diff --git a/src/components/tree-browsers/stores/useWorkflowLibrary.ts b/src/components/editors/WorkflowEditor/WorkflowTree/useWorkflowTree.ts similarity index 73% rename from src/components/tree-browsers/stores/useWorkflowLibrary.ts rename to src/components/editors/WorkflowEditor/WorkflowTree/useWorkflowTree.ts index 363adcba..cb8e927b 100644 --- a/src/components/tree-browsers/stores/useWorkflowLibrary.ts +++ b/src/components/editors/WorkflowEditor/WorkflowTree/useWorkflowTree.ts @@ -2,12 +2,20 @@ import { create } from 'zustand' import { ClapEntity, UUID } from '@aitube/clap' -import { LibraryNodeItem, LibraryNodeType, LibraryTreeNode } from '../types' + import { icons } from '@/components/icons' -import { collectionClassName, libraryClassName } from './treeNodeStyles' +import { + TreeNodeItem, + LibraryNodeType, + LibraryTreeNode, +} from '@/components/tree-browsers/types' +import { + collectionClassName, + libraryClassName, +} from '@/components/tree-browsers/style/treeNodeStyles' -export const useWorkflowLibrary = create<{ - builtInLibraryTreeNodeId: string +export const useWorkflowTree = create<{ + builtinLibraryTreeNodeId: string communityLibraryTreeNodeId: string libraryTreeRoot: LibraryTreeNode[] init: () => void @@ -29,32 +37,32 @@ export const useWorkflowLibrary = create<{ //setCommunityCollections: (collections: WorkflowCollection[]) => void // we support those all selection modes for convenience - please keep them! - selectedNodeItem?: LibraryNodeItem + selectedNodeItem?: TreeNodeItem selectedNodeType?: LibraryNodeType selectTreeNode: ( treeNodeId?: string | null, nodeType?: LibraryNodeType, - nodeItem?: LibraryNodeItem + nodeItem?: TreeNodeItem ) => void selectedTreeNodeId: string | null }>((set, get) => ({ - builtInLibraryTreeNodeId: '', + builtinLibraryTreeNodeId: '', communityLibraryTreeNodeId: '', libraryTreeRoot: [], init: () => { - const builtInLibrary: LibraryTreeNode = { + const builtinLibrary: LibraryTreeNode = { id: UUID(), - nodeType: 'LIB_NODE_WORKFLOWS', - label: 'Built-in workflows', - icon: icons.project, + nodeType: 'TREE_ROOT_BUILTIN', + label: 'Default workflows', + icon: icons.community, className: libraryClassName, isExpanded: true, children: [ { id: UUID(), - nodeType: 'LIB_NODE_GENERIC_EMPTY', - label: 'A - 2', - icon: icons.project, + nodeType: 'DEFAULT_TREE_NODE_EMPTY', + label: 'Empty', + icon: icons.community, className: collectionClassName, }, ], @@ -62,25 +70,25 @@ export const useWorkflowLibrary = create<{ const communityLibrary: LibraryTreeNode = { id: UUID(), - nodeType: 'LIB_NODE_COMMUNITY_COLLECTION', + nodeType: 'TREE_ROOT_COMMUNITY', label: 'Community workflows', icon: icons.community, className: libraryClassName, children: [ { id: UUID(), - nodeType: 'LIB_NODE_GENERIC_EMPTY', - label: 'A - 2', + nodeType: 'DEFAULT_TREE_NODE_EMPTY', + label: 'Empty', icon: icons.community, className: collectionClassName, }, ], } - const libraryTreeRoot = [builtInLibrary, communityLibrary] + const libraryTreeRoot = [builtinLibrary, communityLibrary] set({ - builtInLibraryTreeNodeId: builtInLibrary.id, + builtinLibraryTreeNodeId: builtinLibrary.id, communityLibraryTreeNodeId: communityLibrary.id, libraryTreeRoot, selectedNodeItem: undefined, @@ -113,7 +121,7 @@ export const useWorkflowLibrary = create<{ selectTreeNode: ( treeNodeId?: string | null, nodeType?: LibraryNodeType, - nodeItem?: LibraryNodeItem + nodeItem?: TreeNodeItem ) => { set({ selectedTreeNodeId: treeNodeId ? treeNodeId : undefined }) set({ selectedNodeType: nodeType ? nodeType : undefined }) @@ -121,4 +129,4 @@ export const useWorkflowLibrary = create<{ }, })) -useWorkflowLibrary.getState().init() +useWorkflowTree.getState().init() diff --git a/src/components/editors/WorkflowEditor/viewer/NodeView.tsx b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/NodeView.tsx similarity index 97% rename from src/components/editors/WorkflowEditor/viewer/NodeView.tsx rename to src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/NodeView.tsx index 5f9cb6ad..723ff7b6 100644 --- a/src/components/editors/WorkflowEditor/viewer/NodeView.tsx +++ b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/NodeView.tsx @@ -1,9 +1,9 @@ import React, { memo } from 'react' import { Handle, Position } from '@xyflow/react' -import { ReactWorkflowNode } from '../types' import { useTheme } from '@/services' import { cn } from '@/lib/utils' +import { ReactWorkflowNode } from './types' function NodeComponent({ data }: ReactWorkflowNode) { const theme = useTheme() diff --git a/src/components/editors/WorkflowEditor/clapWorkflowToReactWorkflow.ts b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/clapWorkflowToReactWorkflow.ts similarity index 82% rename from src/components/editors/WorkflowEditor/clapWorkflowToReactWorkflow.ts rename to src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/clapWorkflowToReactWorkflow.ts index 03278e5c..41349883 100644 --- a/src/components/editors/WorkflowEditor/clapWorkflowToReactWorkflow.ts +++ b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/clapWorkflowToReactWorkflow.ts @@ -1,7 +1,7 @@ import { ClapWorkflow, ClapWorkflowEngine } from '@aitube/clap' import { ReactWorkflow } from './types' -import { glifToReactWorkflow } from './specialized/glif/glifToReactWorkflow' +import { glifToReactWorkflow } from './formats/glif/glifToReactWorkflow' export function clapWorkflowToReactWorkflow( clapWorkflow: ClapWorkflow diff --git a/src/components/editors/WorkflowEditor/specialized/comfyui/types.ts b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/formats/comfyui/types.ts similarity index 100% rename from src/components/editors/WorkflowEditor/specialized/comfyui/types.ts rename to src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/formats/comfyui/types.ts diff --git a/src/components/editors/WorkflowEditor/specialized/falai/types.ts b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/formats/falai/types.ts similarity index 100% rename from src/components/editors/WorkflowEditor/specialized/falai/types.ts rename to src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/formats/falai/types.ts diff --git a/src/components/editors/WorkflowEditor/specialized/glif/glifToReactWorkflow.ts b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/formats/glif/glifToReactWorkflow.ts similarity index 100% rename from src/components/editors/WorkflowEditor/specialized/glif/glifToReactWorkflow.ts rename to src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/formats/glif/glifToReactWorkflow.ts diff --git a/src/components/editors/WorkflowEditor/specialized/glif/types.ts b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/formats/glif/types.ts similarity index 100% rename from src/components/editors/WorkflowEditor/specialized/glif/types.ts rename to src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/formats/glif/types.ts diff --git a/src/components/editors/WorkflowEditor/viewer/WorkflowView.tsx b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/index.tsx similarity index 56% rename from src/components/editors/WorkflowEditor/viewer/WorkflowView.tsx rename to src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/index.tsx index a0f0583c..2ecc73ef 100644 --- a/src/components/editors/WorkflowEditor/viewer/WorkflowView.tsx +++ b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/index.tsx @@ -14,18 +14,18 @@ import { import '@xyflow/react/dist/base.css' import { NodeView } from './NodeView' -import { ReactWorkflowEdge, ReactWorkflowNode } from '../types' +import { ReactWorkflowEdge, ReactWorkflowNode } from './types' import { useWorkflowEditor } from '@/services/editors' -import { glifs } from '../samples/glif' -import { glifToReactWorkflow } from '../specialized/glif/glifToReactWorkflow' +import { glifs } from './samples/glif' +import { glifToReactWorkflow } from './formats/glif/glifToReactWorkflow' import { useTheme } from '@/services' const nodeTypes = { custom: NodeView, } -export function WorkflowView() { +export function ReactFlowCanvas() { const theme = useTheme() const current = useWorkflowEditor((s) => s.current) const [nodes, setNodes, onNodesChange] = useNodesState([]) @@ -59,8 +59,27 @@ export function WorkflowView() { backgroundColor: theme.workflow.bgColor || theme.defaultBgColor || '#000000', }} + proOptions={{ + // Since a workflow can be shown inside a tiny panel, we need to free up visual space. + // As a consequence I have hidden the React Flow attribution. + // Doing so is acceptable by their terms, since at the time of writing + // Clapper is a non-commercial project that doesn't belong to any organization. + // + // for more information about React Flow licensing and attribution: + // + // "If you start making money using React Flow or use it in an organization in the future, + // we would ask that you re-add the attribution or sign up for one of our subscriptions." + // + // https://reactflow.dev/learn/troubleshooting/remove-attribution + hideAttribution: true, + }} > - + ) diff --git a/src/components/editors/WorkflowEditor/samples/glif.ts b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/samples/glif.ts similarity index 99% rename from src/components/editors/WorkflowEditor/samples/glif.ts rename to src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/samples/glif.ts index fbf3175e..5c53501f 100644 --- a/src/components/editors/WorkflowEditor/samples/glif.ts +++ b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/samples/glif.ts @@ -1,4 +1,4 @@ -import { GlifWorkflow } from '../specialized/glif/types' +import { GlifWorkflow } from '../formats/glif/types' export const glifs: GlifWorkflow[] = [ { diff --git a/src/components/editors/WorkflowEditor/types.ts b/src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/types.ts similarity index 100% rename from src/components/editors/WorkflowEditor/types.ts rename to src/components/editors/WorkflowEditor/WorkflowViewer/ReactFlowCanvas/types.ts diff --git a/src/components/editors/WorkflowEditor/WorkflowViewer/index.tsx b/src/components/editors/WorkflowEditor/WorkflowViewer/index.tsx new file mode 100644 index 00000000..6f10f2a9 --- /dev/null +++ b/src/components/editors/WorkflowEditor/WorkflowViewer/index.tsx @@ -0,0 +1,31 @@ +import { FormSection } from '@/components/forms/FormSection' +import { useWorkflowEditor } from '@/services/editors' +import { useUI } from '@/services' +import { ReactFlowCanvas } from './ReactFlowCanvas' +import { cn } from '@/lib/utils' + +export function WorkflowViewer({ + className = '', +}: { + className?: string +} = {}) { + const current = useWorkflowEditor((s) => s.current) + const hasBetaAccess = useUI((s) => s.hasBetaAccess) + + const content = hasBetaAccess ? ( + + ) : !current ? ( + + Workflows are not implemented yet. + + ) : ( + +
Should be a form to edit the parameters.
+
+ We can also display a link or an iframe with the actual workflow graph. +
+
+ ) + + return
{content}
+} diff --git a/src/components/editors/WorkflowEditor/index.tsx b/src/components/editors/WorkflowEditor/index.tsx index f9f1d3e1..eac6401b 100644 --- a/src/components/editors/WorkflowEditor/index.tsx +++ b/src/components/editors/WorkflowEditor/index.tsx @@ -1,37 +1,17 @@ -import { useEffect } from 'react' - -import { FormInput } from '@/components/forms/FormInput' -import { FormSection } from '@/components/forms/FormSection' -import { useWorkflowEditor } from '@/services/editors' -import { useUI } from '@/services' -import { WorkflowView } from './viewer/WorkflowView' +import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex' +import { WorkflowTree } from './WorkflowTree' +import { WorkflowViewer } from './WorkflowViewer' export function WorkflowEditor() { - const current = useWorkflowEditor((s) => s.current) - const setCurrent = useWorkflowEditor((s) => s.setCurrent) - const history = useWorkflowEditor((s) => s.history) - const undo = useWorkflowEditor((s) => s.undo) - const redo = useWorkflowEditor((s) => s.redo) - - const hasBetaAccess = useUI((s) => s.hasBetaAccess) - - if (hasBetaAccess) { - return - } - if (!current) { - return ( - - Workflows are not implemented yet. - - ) - } - return ( - -
Should be a form to edit the parameters.
-
- We can also display a link or an iframe with the actual workflow graph. -
-
+ + + + + + + + + ) } diff --git a/src/components/editors/WorkflowEditor/viewer/README.md b/src/components/editors/WorkflowEditor/viewer/README.md deleted file mode 100644 index dd049b13..00000000 --- a/src/components/editors/WorkflowEditor/viewer/README.md +++ /dev/null @@ -1 +0,0 @@ -# Workflow diff --git a/src/components/forms/index.ts b/src/components/forms/index.ts new file mode 100644 index 00000000..d4190c06 --- /dev/null +++ b/src/components/forms/index.ts @@ -0,0 +1,9 @@ +export { FormDir } from './FormDir' +export { FormField } from './FormField' +export { FormFile } from './FormFile' +export { FormInput } from './FormInput' +export { FormLabel } from './FormLabel' +export { FormRadio } from './FormRadio' +export { FormSection } from './FormSection' +export { FormSelect } from './FormSelect' +export { FormSwitch } from './FormSwitch' diff --git a/src/components/toolbars/top-bar/index.tsx b/src/components/toolbars/top-bar/index.tsx index 417dd6b1..ec407e85 100644 --- a/src/components/toolbars/top-bar/index.tsx +++ b/src/components/toolbars/top-bar/index.tsx @@ -1,24 +1,31 @@ -import { ClapProject } from '@aitube/clap' -import { useTimeline } from '@aitube/timeline' +'use client' +import { useTheme } from '@/services' import { cn } from '@/lib/utils' import { TopMenu } from '../top-menu' export function TopBar() { - const clap: ClapProject = useTimeline((s) => s.clap) + const theme = useTheme() return (
- +
+ +
) } diff --git a/src/components/toolbars/top-menu/index.tsx b/src/components/toolbars/top-menu/index.tsx index 2c08acd3..87b719ff 100644 --- a/src/components/toolbars/top-menu/index.tsx +++ b/src/components/toolbars/top-menu/index.tsx @@ -40,7 +40,7 @@ export function TopMenu() { return ( { setIsTopMenuOpen(!!value) }} diff --git a/src/components/tree-browsers/model-tree-browser/index.tsx b/src/components/tree-browsers/model-tree-browser/index.tsx deleted file mode 100644 index 47705a93..00000000 --- a/src/components/tree-browsers/model-tree-browser/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -'use client' - -import { useEffect } from 'react' - -import { cn } from '@/lib/utils' -import { useEntityLibrary } from '../stores/useEntityLibrary' -import { LibraryNodeItem, LibraryNodeType } from '../types' -import { Tree } from '@/components/core/tree' - -import { isClapEntity, isReplicateCollection } from '../utils/isSomething' -import { useCivitaiCollections } from '../stores/useCivitaiCollections' -import { useReplicateCollections } from '../stores/useReplicateCollections' - -export function ModelTreeBrowser() { - const libraryTreeRoot = useEntityLibrary((s) => s.libraryTreeRoot) - const selectTreeNode = useEntityLibrary((s) => s.selectTreeNode) - const selectedTreeNodeId = useEntityLibrary((s) => s.selectedTreeNodeId) - const setReplicateCollections = useEntityLibrary( - (s) => s.setReplicateCollections - ) - const setCivitaiCollections = useEntityLibrary((s) => s.setCivitaiCollections) - - // TODO: we are forced to do this because the api "endpoint" is a server action - // however we could rewrite it so that we can pull the collections directly - // from the Zustand store - - const newReplicateCollections = useReplicateCollections() - useEffect(() => { - setReplicateCollections(newReplicateCollections) - // eslint-disable-next-line - }, [JSON.stringify(newReplicateCollections)]) // ... yeah, I know, I know.. - - const newCivitaiCollections = useCivitaiCollections() - useEffect(() => { - setCivitaiCollections(newCivitaiCollections) - // eslint-disable-next-line - }, [JSON.stringify(newCivitaiCollections)]) // ... yeah, I know, I know.. - - /** - * handle click on tree node - * yes, this is where the magic happens! - * - * @param id - * @param nodeType - * @param node - * @returns - */ - const handleOnChange = async ( - id: string | null, - nodeType?: LibraryNodeType, - nodeItem?: LibraryNodeItem - ) => { - console.log(`calling selectTreeNodeById(id)`) - selectTreeNode(id, nodeType, nodeItem) - - if (!nodeType || !nodeItem) { - console.log('tree-browser: clicked on an undefined node') - return - } - - if (isReplicateCollection(nodeType, nodeItem)) { - // ReplicateCollection - } else if (isClapEntity(nodeType, nodeItem)) { - // ClapEntity - } else { - console.log( - `tree-browser: no action attached to ${nodeType}, so skipping` - ) - return - } - console.log(`tree-browser: clicked on a ${nodeType}`, nodeItem) - } - - return ( -
- - value={selectedTreeNodeId} - onChange={handleOnChange} - className="not-prose h-full w-full px-2 pt-8" - label="Model Library" - > - {libraryTreeRoot.map((node) => ( - - ))} - -
- ) -} diff --git a/src/components/tree-browsers/model-tree-browser/tree-item-viewer.tsx b/src/components/tree-browsers/model-tree-browser/tree-item-viewer.tsx deleted file mode 100644 index d35e4752..00000000 --- a/src/components/tree-browsers/model-tree-browser/tree-item-viewer.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client' - -import { useEntityLibrary } from '../stores/useEntityLibrary' - -export function TreeItemViewer() { - const selectedNodeItem = useEntityLibrary((s) => s.selectedNodeItem) - const selectedNodeType = useEntityLibrary((s) => s.selectedNodeType) - - const nodeType = selectedNodeType - const data = selectedNodeItem - - if (!nodeType || !data) { - return null - } - - return
The selected item cannot be preview.
-} diff --git a/src/components/tree-browsers/project-tree-browser/tree-item-viewer.tsx b/src/components/tree-browsers/project-tree-browser/tree-item-viewer.tsx deleted file mode 100644 index e0d8d324..00000000 --- a/src/components/tree-browsers/project-tree-browser/tree-item-viewer.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client' - -import { useEntityLibrary } from '../stores/useEntityLibrary' - -export function TreeItemViewer() { - const selectedNodeItem = useEntityLibrary((s) => s.selectedNodeItem) - const selectedNodeType = useEntityLibrary((s) => s.selectedNodeType) - - const nodeType = selectedNodeType - const data = selectedNodeItem - - if (!nodeType || !data) { - return null - } - - return
TODO
-} diff --git a/src/components/tree-browsers/stores/treeNodeStyles.ts b/src/components/tree-browsers/stores/treeNodeStyles.ts deleted file mode 100644 index 86468745..00000000 --- a/src/components/tree-browsers/stores/treeNodeStyles.ts +++ /dev/null @@ -1,8 +0,0 @@ -// TODO: this isn't the best place for this as this is style, -// and we are in a state manager -export const libraryClassName = 'text-base font-semibold' - -export const collectionClassName = `text-base font-normal` - -export const itemClassName = - 'text-sm font-light text-gray-200/60 hover:text-gray-200/100' diff --git a/src/components/tree-browsers/stores/useCivitaiCollections.ts b/src/components/tree-browsers/stores/useCivitaiCollections.ts deleted file mode 100644 index 8b9e0998..00000000 --- a/src/components/tree-browsers/stores/useCivitaiCollections.ts +++ /dev/null @@ -1,24 +0,0 @@ -'use client' - -import { useEffect, useState, useTransition } from 'react' -import { CivitaiCollection } from '../types' - -export function useCivitaiCollections(): CivitaiCollection[] { - const [_pending, startTransition] = useTransition() - const [collections, setCollections] = useState([]) - // const [models, setModels] = useState([]) - - useEffect(() => { - startTransition(async () => { - // TODO @Julian: this was something we did in the ligacy - // Clapper, but I'm not sure we want to support Civitai - // again just yet, we probably require other arch changes - // const collections = await listCollections() - const collections: CivitaiCollection[] = [] - setCollections(collections) - // setModels(models) - }) - }, []) - - return collections -} diff --git a/src/components/tree-browsers/stores/useEntityLibrary.ts b/src/components/tree-browsers/stores/useEntityLibrary.ts deleted file mode 100644 index 53d241c1..00000000 --- a/src/components/tree-browsers/stores/useEntityLibrary.ts +++ /dev/null @@ -1,336 +0,0 @@ -'use client' - -import { create } from 'zustand' -import { ClapEntity, ClapSegmentCategory, UUID } from '@aitube/clap' -import { - CivitaiCollection, - LibraryNodeItem, - LibraryNodeType, - LibraryTreeNode, - ReplicateCollection, -} from '../types' -import { icons } from '@/components/icons' -import { getAppropriateIcon } from '@/components/icons/getAppropriateIcon' -import { - collectionClassName, - itemClassName, - libraryClassName, -} from './treeNodeStyles' - -export const useEntityLibrary = create<{ - teamLibraryTreeNodeId: string - communityLibraryTreeNodeId: string - civitaiLibraryTreeNodeId: string - huggingfaceLibraryTreeNodeId: string - replicateLibraryTreeNodeId: string - libraryTreeRoot: LibraryTreeNode[] - init: () => void - - /** - * Load Replicate collections (API models) into the tree - * - * @param collections - * @returns - */ - setReplicateCollections: (collections: ReplicateCollection[]) => void - - /** - * Load Replicate collections (LoRA models) into the tree - * - * @param collections - * @returns - */ - setCivitaiCollections: (collections: CivitaiCollection[]) => void - - // we support those all selection modes for convenience - please keep them! - selectedNodeItem?: LibraryNodeItem - selectedNodeType?: LibraryNodeType - selectTreeNode: ( - treeNodeId?: string | null, - nodeType?: LibraryNodeType, - nodeItem?: LibraryNodeItem - ) => void - selectedTreeNodeId: string | null -}>((set, get) => ({ - localUserLibraryTreeNodeId: '', - huggingfaceUserLibraryTreeNodeId: '', - teamLibraryTreeNodeId: '', - communityLibraryTreeNodeId: '', - civitaiLibraryTreeNodeId: '', - huggingfaceLibraryTreeNodeId: '', - replicateLibraryTreeNodeId: '', - libraryTreeRoot: [], - init: () => { - const teamLibrary: LibraryTreeNode = { - id: UUID(), - nodeType: 'LIB_NODE_TEAM_COLLECTION', - label: 'Team models', - icon: icons.team, - className: libraryClassName, - isExpanded: true, - children: [ - { - id: UUID(), - nodeType: 'LIB_NODE_GENERIC_EMPTY', - label: 'A - 2', - icon: icons.team, - className: collectionClassName, - }, - ], - } - - const civitaiLibrary: LibraryTreeNode = { - id: UUID(), - nodeType: 'LIB_NODE_CIVITAI_COLLECTION', - label: 'Civitai models', - icon: icons.community, - className: libraryClassName, - children: [], - } - - const communityLibrary: LibraryTreeNode = { - id: UUID(), - nodeType: 'LIB_NODE_COMMUNITY_COLLECTION', - label: 'Community models', - icon: icons.community, - className: libraryClassName, - children: [ - { - id: UUID(), - nodeType: 'LIB_NODE_GENERIC_EMPTY', - label: 'A - 2', - icon: icons.community, - className: collectionClassName, - }, - ], - } - - const huggingfaceLibrary: LibraryTreeNode = { - id: UUID(), - nodeType: 'LIB_NODE_HUGGINGFACE_COLLECTION', - label: 'Hugging Face', - icon: icons.vendor, - className: libraryClassName, - children: [], - } - - const replicateLibrary: LibraryTreeNode = { - id: UUID(), - nodeType: 'LIB_NODE_REPLICATE_COLLECTION', - label: 'Replicate', - icon: icons.vendor, - isExpanded: false, // This node is expanded by default - className: libraryClassName, - children: [], - } - - const libraryTreeRoot = [ - // teamLibrary, - // communityLibrary, - civitaiLibrary, - // huggingfaceLibrary, - replicateLibrary, - ] - - set({ - teamLibraryTreeNodeId: teamLibrary.id, - civitaiLibraryTreeNodeId: civitaiLibrary.id, - communityLibraryTreeNodeId: communityLibrary.id, - huggingfaceLibraryTreeNodeId: huggingfaceLibrary.id, - replicateLibraryTreeNodeId: replicateLibrary.id, - libraryTreeRoot, - selectedNodeItem: undefined, - selectedTreeNodeId: null, - }) - }, - - setProjectEntities: async (entities: ClapEntity[]) => { - const characters: LibraryTreeNode = { - id: UUID(), - nodeType: 'LIB_NODE_GENERIC_COLLECTION', - data: undefined, - label: 'Characters', - icon: icons.characters, - className: collectionClassName, - isExpanded: true, // This node is expanded by default - children: [], - } - - const locations: LibraryTreeNode = { - id: UUID(), - nodeType: 'LIB_NODE_GENERIC_COLLECTION', - data: undefined, - label: 'Locations', - icon: icons.location, - className: collectionClassName, - isExpanded: false, // This node is expanded by default - children: [], - } - - const misc: LibraryTreeNode = { - id: UUID(), - nodeType: 'LIB_NODE_GENERIC_COLLECTION', - data: undefined, - label: 'Misc', - icon: icons.misc, - className: collectionClassName, - isExpanded: false, // This node is expanded by default - children: [], - } - - entities.forEach((entity) => { - const node: LibraryTreeNode = { - nodeType: 'LIB_NODE_REPLICATE_MODEL', - id: entity.id, - data: entity, - label: entity.label, - icon: icons.misc, - className: itemClassName, - } - if (entity.category === ClapSegmentCategory.CHARACTER) { - node.icon = icons.character - characters.children!.push(node) - } else if (entity.category === ClapSegmentCategory.LOCATION) { - node.icon = icons.location - locations.children!.push(node) - } else { - misc.children!.push(node) - } - }) - }, - - setReplicateCollections: (collections: ReplicateCollection[]) => { - const { replicateLibraryTreeNodeId, libraryTreeRoot } = get() - - set({ - libraryTreeRoot: libraryTreeRoot.map((node) => { - if (node.id !== replicateLibraryTreeNodeId) { - return node - } - - return { - ...node, - - children: - // only keep non-empty models - collections - .filter((c) => c.models.length) - - // only visual or sound oriented models - .filter((c) => { - const name = c.name.toLowerCase() - - // ignore captioning models, we don't need this right now - if (name.includes('to text') || name.includes('to-text')) { - return false - } - - if ( - name.includes('image') || - name.includes('video') || - name.includes('style') || - name.includes('audio') || - name.includes('sound') || - name.includes('music') || - name.includes('speech') || - name.includes('voice') || - name.includes('resolution') || - name.includes('upscale') || - name.includes('upscaling') || - name.includes('interpolate') || - name.includes('interpolation') - ) { - return true - } - return false - }) - .map((c) => ({ - id: UUID(), - data: c, - nodeType: 'LIB_NODE_REPLICATE_COLLECTION', - label: c.name, - icon: getAppropriateIcon(c.name), - className: collectionClassName, // `${collectionClassName} ${getCollectionColor(c.name)}`, - isExpanded: false, // This node is expanded by default - children: c.models.map((m) => ({ - nodeType: 'LIB_NODE_REPLICATE_MODEL', - id: m.id, - data: m, - label: m.label, - icon: getAppropriateIcon(m.label, getAppropriateIcon(c.name)), - className: itemClassName, // `${itemClassName} ${getItemColor(m.label, getItemColor(c.name))}`, - })), - })), - } - }), - }) - }, - - setCivitaiCollections: (collections: CivitaiCollection[]) => { - const { civitaiLibraryTreeNodeId, libraryTreeRoot } = get() - - set({ - libraryTreeRoot: libraryTreeRoot.map((node) => { - if (node.id !== civitaiLibraryTreeNodeId) { - return node - } - - return { - ...node, - - children: collections.map((c) => ({ - id: UUID(), - data: c, - nodeType: 'LIB_NODE_CIVITAI_COLLECTION', - label: c.name, - icon: getAppropriateIcon(c.name), - className: collectionClassName, // `${collectionClassName} ${getCollectionColor(c.name)}`, - isExpanded: false, // This node is expanded by default - children: c.models.map((m) => ({ - nodeType: 'LIB_NODE_CIVITAI_MODEL', - id: m.id, - data: m, - label: m.label, - icon: getAppropriateIcon(m.label, getAppropriateIcon(c.name)), - className: itemClassName, // `${itemClassName} ${getItemColor(m.label, getItemColor(c.name))}`, - })), - })), - } - }), - }) - }, - - selectedNodeItem: undefined, - selectEntity: (entity?: ClapEntity) => { - if (entity) { - console.log( - 'TODO julian: change this code to search in the entity collections' - ) - const selectedTreeNode = get().libraryTreeRoot.find( - (node) => node.data?.id === entity.id - ) - - // set({ selectedTreeNode }) - set({ selectedTreeNodeId: selectedTreeNode?.id || null }) - set({ selectedNodeItem: entity }) - } else { - // set({ selectedTreeNode: undefined }) - set({ selectedTreeNodeId: null }) - set({ selectedNodeItem: undefined }) - } - }, - - // selectedTreeNode: undefined, - selectedTreeNodeId: null, - selectTreeNode: ( - treeNodeId?: string | null, - nodeType?: LibraryNodeType, - nodeItem?: LibraryNodeItem - ) => { - set({ selectedTreeNodeId: treeNodeId ? treeNodeId : undefined }) - set({ selectedNodeType: nodeType ? nodeType : undefined }) - set({ selectedNodeItem: nodeItem ? nodeItem : undefined }) - }, -})) - -useEntityLibrary.getState().init() diff --git a/src/components/tree-browsers/stores/useFileLibrary.txt b/src/components/tree-browsers/stores/useFileLibrary.txt deleted file mode 100644 index da935382..00000000 --- a/src/components/tree-browsers/stores/useFileLibrary.txt +++ /dev/null @@ -1,204 +0,0 @@ -"use client" - -import { create } from "zustand" - -import { ClapEntity, UUID } from "@aitube/clap" -import { HuggingFaceUserCollection, LibraryNodeItem, LibraryNodeType, LibraryTreeNode, LocalUserCollection } from "../types" -import { icons } from "@/components/icons" -import { getAppropriateIcon } from "@/components/icons/getAppropriateIcon" -import { className } from "@/app/fonts" -import { getCollectionItemTextColor } from "../utils/getCollectionItemTextColor" -import { collectionClassName, itemClassName, libraryClassName } from './treeNodeStyles' - -export const useFileLibrary = create<{ - localUserLibraryTreeNodeId: string - huggingfaceUserLibraryTreeNodeId: string - libraryTreeRoot: LibraryTreeNode[] - init: () => void - - /** - * Load local user collections (projects, assets) into the tree - * - * @param collections - * @returns - */ - setLocalUserCollections: (collections: LocalUserCollection[]) => void - - /** - * Load Hugging Face user collections (projects, assets) into the tree - * - * @param collections - * @returns - */ - setHuggingFaceUserCollections: (collections: HuggingFaceUserCollection[]) => void - - // we support those all selection modes for convenience - please keep them! - selectedNodeItem?: LibraryNodeItem - selectedNodeType?: LibraryNodeType - selectTreeNode: (treeNodeId?: string | null, nodeType?: LibraryNodeType, nodeItem?: LibraryNodeItem) => void - selectedTreeNodeId: string | null -}>((set, get) => ({ - localUserLibraryTreeNodeId: "", - huggingfaceUserLibraryTreeNodeId: "", - libraryTreeRoot: [], - init: () => { - - const localUserLibrary: LibraryTreeNode = { - id: UUID(), - nodeType: "LIB_NODE_LOCAL_USER_COLLECTION", - label: "My Computer", - icon: icons.computer, - className: libraryClassName, - isExpanded: true, // This node is expanded by default - children: [ - { - id: UUID(), - nodeType: "LIB_NODE_GENERIC_EMPTY", - label: "(No files to display)", - icon: icons.misc, - className: `${collectionClassName} text-gray-100/30`, - isExpanded: false, // This node is expanded by default - children: [] - } - ] - } - - const huggingfaceUserLibrary: LibraryTreeNode = { - id: UUID(), - nodeType: "LIB_NODE_HUGGINGFACE_USER_COLLECTION", - label: "My HF Cloud", - icon: icons.cloud, - className: libraryClassName, - isExpanded: true, // This node is expanded by default - children: [ - { - id: UUID(), - nodeType: "LIB_NODE_GENERIC_EMPTY", - label: "(No files to display)", - icon: icons.misc, - className: `${collectionClassName} text-gray-100/30`, - isExpanded: false, // This node is expanded by default - children: [] - } - ] - } - - const libraryTreeRoot = [ - localUserLibrary, - huggingfaceUserLibrary, - ] - - set({ - localUserLibraryTreeNodeId: localUserLibrary.id, - huggingfaceUserLibraryTreeNodeId: huggingfaceUserLibrary.id, - libraryTreeRoot, - selectedNodeItem: undefined, - selectedTreeNodeId: null, - }) - }, - - setLocalUserCollections: (collections: LocalUserCollection[]) => { - const { localUserLibraryTreeNodeId, libraryTreeRoot } = get() - - console.log("setLocalUserCollections:", collections) - - set({ - libraryTreeRoot: libraryTreeRoot.map(node => { - if (node.id !== localUserLibraryTreeNodeId) { return node } - - return { - ...node, - - children: collections.map(c => ({ - id: UUID(), - nodeType: "LIB_NODE_LOCAL_USER_FOLDER", - data: c, - label: c.name, // file directory name - icon: getAppropriateIcon(c.name), - className: collectionClassName, // `${collectionClassName} ${getCollectionColor(c.name)}`, - isExpanded: false, // This node is expanded by default - children: c.items.map(m => ({ - nodeType: "LIB_NODE_LOCAL_USER_FILE", - id: m.id, - data: m, - label: <>{ - m.fileName.split(".").slice(0, -1) - }{ - `.${m.fileName.split(".").pop()}` - }, - icon: getAppropriateIcon(m.fileName, getAppropriateIcon(c.name)), - className: `${itemClassName} ${ - getCollectionItemTextColor(m.fileName) - }`, // `${itemClassName} ${getItemColor(m.label, getItemColor(c.name))}`, - })) - })) - } - }) - }) - }, - - setHuggingFaceUserCollections: (collections: HuggingFaceUserCollection[]) => { - const { huggingfaceUserLibraryTreeNodeId, libraryTreeRoot } = get() - - set({ - libraryTreeRoot: libraryTreeRoot.map(node => { - if (node.id !== huggingfaceUserLibraryTreeNodeId) { return node } - - return { - ...node, - - children: collections.map(c => ({ - id: c.id, - nodeType: "LIB_NODE_HUGGINGFACE_USER_DATASET", - data: c, - label: c.name, // file directory name - icon: getAppropriateIcon(c.name), - className: collectionClassName, // `${collectionClassName} ${getCollectionColor(c.name)}`, - isExpanded: false, // This node is expanded by default - children: c.items.map(m => ({ - nodeType: "LIB_NODE_HUGGINGFACE_USER_FILE", - id: m.id, - data: m, - label: <>{ - m.fileName.split(".").slice(0, -1) - }{ - `.${m.fileName.split(".").pop()}` - }, - icon: getAppropriateIcon(m.fileName, getAppropriateIcon(c.name)), - className: `${itemClassName} ${ - getCollectionItemTextColor(m.fileName) - }`, // `${itemClassName} ${getItemColor(m.label, getItemColor(c.name))}`, - })) - })) - } - }) - }) - }, - - selectedNodeItem: undefined, - selectEntity: (entity?: ClapEntity) => { - if (entity) { - console.log("TODO julian: change this code to search in the entity collections") - const selectedTreeNode = - get().libraryTreeRoot - .find(node => node.data?.id === entity.id) - - // set({ selectedTreeNode }) - set({ selectedTreeNodeId: selectedTreeNode?.id || null }) - set({ selectedNodeItem: entity }) - } else { - // set({ selectedTreeNode: undefined }) - set({ selectedTreeNodeId: null }) - set({ selectedNodeItem: undefined }) - } - }, - // selectedTreeNode: undefined, - selectedTreeNodeId: null, - selectTreeNode: (treeNodeId?: string | null, nodeType?: LibraryNodeType, nodeItem?: LibraryNodeItem) => { - set({ selectedTreeNodeId: treeNodeId ? treeNodeId : undefined }) - set({ selectedNodeType: nodeType ? nodeType : undefined }) - set({ selectedNodeItem: nodeItem ? nodeItem : undefined }) - } -})) - -useFileLibrary.getState().init() \ No newline at end of file diff --git a/src/components/tree-browsers/stores/useProjectLibrary.ts b/src/components/tree-browsers/stores/useProjectLibrary.ts deleted file mode 100644 index 3bf532ed..00000000 --- a/src/components/tree-browsers/stores/useProjectLibrary.ts +++ /dev/null @@ -1,140 +0,0 @@ -'use client' - -import { create } from 'zustand' -import { ClapEntity, ClapSegmentCategory, UUID } from '@aitube/clap' - -import { icons } from '@/components/icons' - -import { LibraryNodeItem, LibraryNodeType, LibraryTreeNode } from '../types' -import { - collectionClassName, - itemClassName, - libraryClassName, -} from './treeNodeStyles' - -export const useProjectLibrary = create<{ - libraryTreeRoot: LibraryTreeNode[] - init: () => void - setProjectEntities: (entities: ClapEntity[]) => Promise - selectedNodeItem?: LibraryNodeItem - selectedNodeType?: LibraryNodeType - selectEntity: (entity?: ClapEntity) => void - selectTreeNode: ( - treeNodeId?: string | null, - nodeType?: LibraryNodeType, - nodeItem?: LibraryNodeItem - ) => void - selectedTreeNodeId: string | null -}>((set, get) => ({ - libraryTreeRoot: [], - init: () => { - set({ - libraryTreeRoot: [], - selectedNodeItem: undefined, - selectedTreeNodeId: null, - // selectedTreeNode: undefined, - }) - }, - - setProjectEntities: async (entities: ClapEntity[]) => { - const { libraryTreeRoot } = get() - - const characters: LibraryTreeNode = { - id: UUID(), - nodeType: 'LIB_NODE_GENERIC_COLLECTION', - data: undefined, - label: 'Characters', - icon: icons.characters, - className: libraryClassName, - isExpanded: true, // This node is expanded by default - children: [], - } - - const locations: LibraryTreeNode = { - id: UUID(), - nodeType: 'LIB_NODE_GENERIC_COLLECTION', - data: undefined, - label: 'Locations', - icon: icons.location, - className: libraryClassName, - isExpanded: true, // This node is expanded by default - children: [], - } - - const misc: LibraryTreeNode = { - id: UUID(), - nodeType: 'LIB_NODE_GENERIC_COLLECTION', - data: undefined, - label: 'Misc', - icon: icons.misc, - className: libraryClassName, - isExpanded: true, // This node is expanded by default - children: [], - } - - entities.forEach((entity) => { - const node: LibraryTreeNode = { - nodeType: 'LIB_NODE_PROJECT_ENTITY_GENERIC', - id: entity.id, - data: entity, - label: entity.label, - icon: icons.misc, - className: collectionClassName, - } - if (entity.category === ClapSegmentCategory.CHARACTER) { - node.icon = icons.character - node.nodeType = 'LIB_NODE_PROJECT_ENTITY_CHARACTER' - characters.children!.push(node) - } else if (entity.category === ClapSegmentCategory.LOCATION) { - node.icon = icons.location - node.nodeType = 'LIB_NODE_PROJECT_ENTITY_LOCATION' - locations.children!.push(node) - } else { - misc.children!.push(node) - } - }) - - set({ - libraryTreeRoot: [ - characters, - locations, - misc, - // displaying an empty collection isn't very useful, - // so let's just clean them out - ].filter((node) => node.children?.length), - }) - }, - - selectedNodeItem: undefined, - selectEntity: (entity?: ClapEntity) => { - if (entity) { - console.log( - 'TODO julian: change this code to search in the model collections' - ) - const selectedTreeNode = get().libraryTreeRoot.find( - (node) => node.data?.id === entity.id - ) - - // set({ selectedTreeNode }) - set({ selectedTreeNodeId: selectedTreeNode?.id || null }) - set({ selectedNodeItem: entity }) - } else { - // set({ selectedTreeNode: undefined }) - set({ selectedTreeNodeId: null }) - set({ selectedNodeItem: undefined }) - } - }, - // selectedTreeNode: undefined, - selectedTreeNodeId: null, - selectTreeNode: ( - treeNodeId?: string | null, - nodeType?: LibraryNodeType, - nodeItem?: LibraryNodeItem - ) => { - set({ selectedTreeNodeId: treeNodeId ? treeNodeId : undefined }) - set({ selectedNodeType: nodeType ? nodeType : undefined }) - set({ selectedNodeItem: nodeItem ? nodeItem : undefined }) - }, -})) - -useProjectLibrary.getState().init() diff --git a/src/components/tree-browsers/stores/useReplicateCollections.ts b/src/components/tree-browsers/stores/useReplicateCollections.ts deleted file mode 100644 index c07e25a8..00000000 --- a/src/components/tree-browsers/stores/useReplicateCollections.ts +++ /dev/null @@ -1,24 +0,0 @@ -'use client' - -import { useEffect, useState, useTransition } from 'react' -import { ReplicateCollection } from '../types' - -export function useReplicateCollections(): ReplicateCollection[] { - const [_pending, startTransition] = useTransition() - const [collections, setCollections] = useState([]) - // const [models, setModels] = useState([]) - - useEffect(() => { - startTransition(async () => { - // TODO @Julian: this was something we did in the ligacy - // Clapper, but I'm not sure we want to support Replicate - // again just yet, we probably require other arch changes - // const collections = await listCollections() - const collections: ReplicateCollection[] = [] - setCollections(collections) - // setModels(models) - }) - }, []) - - return collections -} diff --git a/src/components/tree-browsers/style/treeNodeStyles.ts b/src/components/tree-browsers/style/treeNodeStyles.ts new file mode 100644 index 00000000..d1af4c70 --- /dev/null +++ b/src/components/tree-browsers/style/treeNodeStyles.ts @@ -0,0 +1,10 @@ +// TODO: this isn't the best place for this as this is style, +// and we are in a state manager +export const libraryClassName = + 'text-sm font-semibold text-white/80 hover:text-white/90' + +export const collectionClassName = + 'text-sm font-normal text-white/70 hover:text-white/90' + +export const itemClassName = + 'text-xs font-light text-white/60 hover:text-white/90' diff --git a/src/components/tree-browsers/types.ts b/src/components/tree-browsers/types.ts index b4e29639..efeba2c3 100644 --- a/src/components/tree-browsers/types.ts +++ b/src/components/tree-browsers/types.ts @@ -4,67 +4,72 @@ import { ScreenplaySequence } from '@aitube/broadway' import { ClapEntity, ClapSegment, ClapWorkflow } from '@aitube/clap' import { TreeNodeType } from '../core/tree/types' -// not sure if we should also sort them into data type categories, -// as vendors like to be on multiple kind of models - -export type LibraryNodeHuggingFaceType = - | 'LIB_NODE_HUGGINGFACE_USER_COLLECTION' - | 'LIB_NODE_HUGGINGFACE_USER_DATASET' - -export type LibraryNodeLocalFileType = - | 'LIB_NODE_LOCAL_USER_FILE' - | 'LIB_NODE_LOCAL_USER_FOLDER' - -export type LibraryNodeRemoteFileType = - | 'LIB_NODE_HUGGINGFACE_USER_FOLDER' - | 'LIB_NODE_HUGGINGFACE_USER_FILE' - -export type LibraryNodeFileType = - | LibraryNodeLocalFileType - | LibraryNodeRemoteFileType - -export type LibraryNodeWorkflowType = 'LIB_NODE_WORKFLOWS' | 'LIB_NODE_WORKFLOW' - -export type LibraryNodeProjectType = - | 'LIB_NODE_PROJECT_COLLECTION' - | 'LIB_NODE_PROJECT_ARCHIVE' - | 'LIB_NODE_PROJECT_ASSET' // image, sound file.. - | 'LIB_NODE_PROJECT_ENTITY_GENERIC' - | 'LIB_NODE_PROJECT_ENTITY_CHARACTER' - | 'LIB_NODE_PROJECT_ENTITY_LOCATION' - -export type LibraryNodeGenericType = - | 'LIB_NODE_GENERIC_COLLECTION' - | 'LIB_NODE_GENERIC_MODEL' - | 'LIB_NODE_GENERIC_ITEM' - | 'LIB_NODE_GENERIC_EMPTY' - -/** - * we could use "LIB_NODE_GENERIC_COLLECTION", - * but I think it can also be useful to keep specific types, - * that way we can show a custom collection UI panel on the right of the explorer - */ -export type LibraryNodeType = - | 'LIB_NODE_LOCAL_USER_COLLECTION' - | LibraryNodeLocalFileType - | LibraryNodeHuggingFaceType - | LibraryNodeRemoteFileType - | LibraryNodeWorkflowType - | LibraryNodeProjectType - | 'LIB_NODE_TEAM_COLLECTION' - | 'LIB_NODE_TEAM_MODEL' - | 'LIB_NODE_COMMUNITY_COLLECTION' - | 'LIB_NODE_COMMUNITY_MODEL' - | 'LIB_NODE_HUGGINGFACE_COLLECTION' - | 'LIB_NODE_HUGGINGFACE_MODEL' - | 'LIB_NODE_REPLICATE_COLLECTION' - | 'LIB_NODE_REPLICATE_MODEL' - | 'LIB_NODE_CIVITAI_COLLECTION' - | 'LIB_NODE_CIVITAI_MODEL' - | LibraryNodeGenericType +export type DefaultTreeNodeList = 'DEFAULT_TREE_NODE_LIST' +export type DefaultTreeNodeItem = 'DEFAULT_TREE_NODE_ITEM' +export type DefaultTreeNodeEmpty = 'DEFAULT_TREE_NODE_EMPTY' + +export type DefaultTreeNode = + | DefaultTreeNodeList + | DefaultTreeNodeItem + | DefaultTreeNodeEmpty + +// ------------------------------------------ +export type CommunityTreeNodeDataset = 'COMMUNITY_TREE_NODE_LIST_DATASET' +export type CommunityTreeNodeList = 'COMMUNITY_TREE_NODE_LIST_FOLDER' +export type CommunityTreeNodeItem = 'COMMUNITY_TREE_NODE_ITEM_FILE' + +export type CommunityTreeNode = + | CommunityTreeNodeDataset + | CommunityTreeNodeList + | CommunityTreeNodeItem + +// ------------------------------------------ + +export type DeviceTreeNodeList = 'DEVICE_TREE_NODE_LIST_FOLDER' +export type DeviceTreeNodeItem = 'DEVICE_TREE_NODE_ITEM_FILE' + +export type DeviceTreeNode = DeviceTreeNodeList | DeviceTreeNodeItem + +// ------------------------------------------ + +export type FlowTreeNodeList = 'FLOW_TREE_NODE_LIST_WORKFLOWS' +export type FlowTreeNodeItem = 'FLOW_TREE_NODE_ITEM_WORKFLOW' + +export type FlowTreeNode = FlowTreeNodeList | FlowTreeNodeItem + +// ------------------------------------------ + +export type EntityTreeNodeList = 'ENTITY_TREE_NODE_LIST_ENTITIES' +export type EntityTreeNodeItem = 'ENTITY_TREE_NODE_ITEM_ENTITY' + +export type EntityTreeNode = EntityTreeNodeList | EntityTreeNodeItem + +// ------------------------------------------ + +// some specialized types + +// TODO + +// ------------------------------------------ + +// Root elements +export type TreeRootProject = 'TREE_ROOT_PROJECT' +export type TreeRootDevice = 'TREE_ROOT_DEVICE' +export type TreeRootTeam = 'TREE_ROOT_TEAM' +export type TreeRootCommunity = 'TREE_ROOT_COMMUNITY' +export type TreeRootBuiltin = 'TREE_ROOT_BUILTIN' + +export type TreeRoot = + | TreeRootProject + | TreeRootDevice + | TreeRootTeam + | TreeRootCommunity + | TreeRootBuiltin + +// ------------------------------------------ // can be a file or folder -export type LocalUserItem = { +export type DeviceFileOrFolder = { id: string // can be the path for now path: string // path to the file content fileName: string @@ -73,19 +78,19 @@ export type LocalUserItem = { extension: string mimetype: string isDirectory: boolean - items: LocalUserItem[] + items: DeviceFileOrFolder[] } -export type LocalUserCollection = { +export type DeviceCollection = { id: string name: string description: string path: string // path to the root directory (eg "~/Clapper") - items: LocalUserItem[] // files or folders + items: DeviceFileOrFolder[] // files or folders } // can be a file or folder -export type HuggingFaceUserItem = { +export type CommunityFileOrFolder = { id: string // can be the path for now (so not a uuid) assetUrl: string // full URL to download the content (eg. to use with wget) datasetName: string @@ -99,18 +104,25 @@ export type HuggingFaceUserItem = { mimeType: string isDirectory: boolean isPrivate: boolean - items: HuggingFaceUserItem[] + items: CommunityFileOrFolder[] } -export type HuggingFaceUserCollection = { - id: string // huggingface ud (not a uuid) +export type CommunityEntityCollection = { + id: string // huggingface id (not a uuid) name: string description: string userName: string datasetName: string repository: string url: string - items: HuggingFaceUserItem[] // files or folders + items: ClapEntity[] +} + +export type ProjectEntityCollection = { + id: string + name: string + description: string + items: ClapEntity[] } export type ReplicateModel = { @@ -158,24 +170,38 @@ export type WorkflowCollection = { } // TODO unify this a bit, at least in the naming scheme -export type LibraryNodeFileItem = LocalUserItem | HuggingFaceUserItem +export type LibraryNodeFileItem = DeviceFileOrFolder | CommunityFileOrFolder + +// ------------------------------------------ + +export type LibraryNodeType = + | DefaultTreeNode + | CommunityTreeNode + | DeviceTreeNode + | FlowTreeNode + | EntityTreeNode + | TreeRoot // TODO unify this a bit, at least in the naming scheme -export type LibraryNodeItem = +export type TreeNodeItem = | ClapEntity - | ReplicateCollection - | ReplicateModel - | CivitaiCollection - | CivitaiModel - | LocalUserCollection - | LocalUserItem - | HuggingFaceUserCollection - | HuggingFaceUserItem - | ScreenplaySequence | ClapSegment - | WorkflowCollection + | ScreenplaySequence | ClapWorkflow + | WorkflowCollection + + // TODO: if we keep them, then rename those to things like + // ReplicateWorkflowCollection, + // CivitaiWorkflowCollection etc + // | ReplicateCollection + // | ReplicateModel + // | CivitaiCollection + // | CivitaiModel + | DeviceCollection + | DeviceFileOrFolder + | CommunityEntityCollection + | CommunityFileOrFolder // a model library is a collection of models // this collection can itself include sub-models -export type LibraryTreeNode = TreeNodeType +export type LibraryTreeNode = TreeNodeType diff --git a/src/components/tree-browsers/utils/isSomething.ts b/src/components/tree-browsers/utils/isSomething.ts index 6dc441cc..90e2c2e9 100644 --- a/src/components/tree-browsers/utils/isSomething.ts +++ b/src/components/tree-browsers/utils/isSomething.ts @@ -6,63 +6,51 @@ import { ClapEntity } from '@aitube/clap' import { - HuggingFaceUserCollection, - HuggingFaceUserItem, - LibraryNodeItem, + CommunityEntityCollection, + CommunityFileOrFolder, + TreeNodeItem, LibraryNodeType, - LocalUserCollection, - LocalUserItem, - ReplicateCollection, + DeviceCollection, + DeviceFileOrFolder, } from '../types' -export const isLocalUserCollection = ( +export const isFSCollection = ( nodeType: LibraryNodeType, - data: LibraryNodeItem -): data is LocalUserCollection => { - return nodeType === 'LIB_NODE_LOCAL_USER_COLLECTION' + data: TreeNodeItem +): data is DeviceCollection => { + return nodeType === 'TREE_ROOT_DEVICE' } -export const isLocalUserItem = ( +export const isFSFileOrFolder = ( nodeType: LibraryNodeType, - data: LibraryNodeItem -): data is LocalUserItem => { + data: TreeNodeItem +): data is DeviceFileOrFolder => { return ( - nodeType === 'LIB_NODE_LOCAL_USER_FILE' || - nodeType === 'LIB_NODE_LOCAL_USER_FOLDER' + nodeType === 'DEVICE_TREE_NODE_LIST_FOLDER' || + nodeType === 'DEVICE_TREE_NODE_ITEM_FILE' ) } -export const isHuggingFaceUserCollection = ( +export const isCommunityEntityCollection = ( nodeType: LibraryNodeType, - data: LibraryNodeItem -): data is HuggingFaceUserCollection => { - return nodeType === 'LIB_NODE_HUGGINGFACE_USER_COLLECTION' + data: TreeNodeItem +): data is CommunityEntityCollection => { + return nodeType === 'COMMUNITY_TREE_NODE_LIST_DATASET' } -export const isHuggingFaceUserItem = ( +export const isCommunityFileOrFolder = ( nodeType: LibraryNodeType, - data: LibraryNodeItem -): data is HuggingFaceUserItem => { + data: TreeNodeItem +): data is CommunityFileOrFolder => { return ( - nodeType === 'LIB_NODE_HUGGINGFACE_USER_FILE' || - nodeType === 'LIB_NODE_HUGGINGFACE_USER_FOLDER' + nodeType === 'COMMUNITY_TREE_NODE_LIST_FOLDER' || + nodeType === 'COMMUNITY_TREE_NODE_ITEM_FILE' ) } -export const isReplicateCollection = ( - nodeType: LibraryNodeType, - data: LibraryNodeItem -): data is ReplicateCollection => { - return nodeType === 'LIB_NODE_REPLICATE_COLLECTION' -} - export const isClapEntity = ( nodeType: LibraryNodeType, - data: LibraryNodeItem + data: TreeNodeItem ): data is ClapEntity => { - return ( - nodeType === 'LIB_NODE_REPLICATE_MODEL' || - nodeType === 'LIB_NODE_HUGGINGFACE_MODEL' || - nodeType === 'LIB_NODE_GENERIC_MODEL' - ) + return nodeType === 'ENTITY_TREE_NODE_ITEM_ENTITY' } diff --git a/src/components/tree-browsers/workflow-tree-browser/index.tsx b/src/components/tree-browsers/workflow-tree-browser/index.tsx deleted file mode 100644 index 578a8c45..00000000 --- a/src/components/tree-browsers/workflow-tree-browser/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -'use client' - -import { useEffect } from 'react' - -import { cn } from '@/lib/utils' -import { useEntityLibrary } from '../stores/useEntityLibrary' -import { LibraryNodeItem, LibraryNodeType } from '../types' -import { Tree } from '@/components/core/tree' - -import { isClapEntity, isReplicateCollection } from '../utils/isSomething' - -export function WorkflowTreeBrowser() { - const libraryTreeRoot = useEntityLibrary((s) => s.libraryTreeRoot) - const selectTreeNode = useEntityLibrary((s) => s.selectTreeNode) - const selectedTreeNodeId = useEntityLibrary((s) => s.selectedTreeNodeId) - - /** - * handle click on tree node - * yes, this is where the magic happens! - * - * @param id - * @param nodeType - * @param node - * @returns - */ - const handleOnChange = async ( - id: string | null, - nodeType?: LibraryNodeType, - nodeItem?: LibraryNodeItem - ) => { - console.log(`calling selectTreeNodeById(id)`) - selectTreeNode(id, nodeType, nodeItem) - - if (!nodeType || !nodeItem) { - console.log('tree-browser: clicked on an undefined node') - return - } - - if (isReplicateCollection(nodeType, nodeItem)) { - // ReplicateCollection - } else if (isClapEntity(nodeType, nodeItem)) { - // ClapEntity - } else { - console.log( - `tree-browser: no action attached to ${nodeType}, so skipping` - ) - return - } - console.log(`tree-browser: clicked on a ${nodeType}`, nodeItem) - } - - return ( -
- - value={selectedTreeNodeId} - onChange={handleOnChange} - className="not-prose h-full w-full px-2 pt-8" - label="Model Library" - > - {libraryTreeRoot.map((node) => ( - - ))} - -
- ) -} diff --git a/src/components/ui/menubar.tsx b/src/components/ui/menubar.tsx index 07761414..2a40ef85 100644 --- a/src/components/ui/menubar.tsx +++ b/src/components/ui/menubar.tsx @@ -23,7 +23,7 @@ const Menubar = React.forwardRef< (({ className, ...props }, ref) => ( )) @@ -207,7 +210,7 @@ const MenubarShortcut = ({ return ( ((set, get) => ({ if (!textModel) { return } - // we need to update the model - textModel?.setValue(draft) + + try { + // we need to update the model + textModel?.setValue(draft) + } catch (err) { + // to catch the "Error: Model is disposed!" + // this can happen if the timing isn't right, + // the model might already (or still) be disposed of + return + } // and highlight the text again - highlightElements() + try { + highlightElements() + } catch (err) {} }, publishDraftToTimeline: async (): Promise => { const { draft } = get()