Skip to content

Commit

Permalink
add minimalist tree system
Browse files Browse the repository at this point in the history
  • Loading branch information
jbilcke-hf committed Jul 27, 2024
1 parent c1021fe commit 3420261
Show file tree
Hide file tree
Showing 37 changed files with 856 additions and 1,386 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -44,17 +49,15 @@ export function ProjectTreeBrowser() {
}

return (
<div className={cn()}>
<Tree.Root<LibraryNodeType, LibraryNodeItem>
value={selectedTreeNodeId}
onChange={handleOnChange}
className="not-prose h-full w-full px-2 pt-8"
label="Project Library"
>
{libraryTreeRoot.map((node) => (
<Tree.Node node={node} key={node.id} />
))}
</Tree.Root>
</div>
<Tree.Root<LibraryNodeType, TreeNodeItem>
value={selectedTreeNodeId}
onChange={handleOnChange}
className={cn(`not-prose h-full w-full px-2 pt-2`, className)}
label="Entities"
>
{libraryTreeRoot.map((node) => (
<Tree.Node node={node} key={node.id} />
))}
</Tree.Root>
)
}
207 changes: 207 additions & 0 deletions src/components/editors/EntityEditor/EntityTree/useEntityTree.ts
Original file line number Diff line number Diff line change
@@ -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
// <placeholder>

// 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
// <placeholder>

// 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()
65 changes: 65 additions & 0 deletions src/components/editors/EntityEditor/EntityViewer/EntityList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="pt-4">
<div className="mb-2">
<h1 className="mb-4 inline px-4 text-xl font-bold">Entities</h1>
<Button
onClick={handleAddEntity}
className="absolute right-2 top-2"
variant="secondary"
>
New +
</Button>
</div>
<ul>
{entities.map((entity: ClapEntity) => (
<li key={entity.id} className={`flex px-2 py-1`}>
<Button
onClick={() => {
setCurrent(entity)
onSelectEntity(entity.id)
}}
variant="ghost"
>
{entity.label} ({entity.category})
</Button>
<Button
onClick={() => removeEntity(entity.id)}
className={`ml-2 ml-auto`}
variant="destructive"
size="sm"
>
Remove
</Button>
</li>
))}
</ul>
</div>
)
}
Loading

0 comments on commit 3420261

Please sign in to comment.