From 87c46c25c6a4b63f93f5b2cb7da3b5a0f84f0625 Mon Sep 17 00:00:00 2001 From: dtoro Date: Tue, 10 Mar 2026 21:34:39 +0100 Subject: [PATCH] feat: add node calssification --- frontend/src/app/canvas/CanvasPage.tsx | 32 +++++++++------ frontend/src/app/platform/AppSidebar.tsx | 4 +- .../src/app/platform/NewProjectDialog.tsx | 2 +- frontend/src/app/platform/ProjectsPage.tsx | 6 +-- .../base/NodeFooterEdgeIndicators.tsx | 9 ++++- .../platform/icon-map.tsx => lib/iconMap.tsx} | 2 +- frontend/src/lib/nodeRegistry.ts | 39 +++++++++++++++++++ frontend/src/lib/registerBuiltinNodes.tsx | 4 ++ 8 files changed, 79 insertions(+), 19 deletions(-) rename frontend/src/{app/platform/icon-map.tsx => lib/iconMap.tsx} (91%) diff --git a/frontend/src/app/canvas/CanvasPage.tsx b/frontend/src/app/canvas/CanvasPage.tsx index 6ddbebc..3be1507 100644 --- a/frontend/src/app/canvas/CanvasPage.tsx +++ b/frontend/src/app/canvas/CanvasPage.tsx @@ -30,6 +30,7 @@ import { ContextMenu, ContextMenuContent, ContextMenuItem, + ContextMenuLabel, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, @@ -52,6 +53,7 @@ import { ClipboardPaste, FolderOpen, FileStack, CircleDotDashed } from 'lucide-r import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '@/lib/flowUtils' import { getRegisteredNodeTypes, + getRegisteredNodeTypesGroupedByClassification, getRegisteredNodeTypeIds, getDefaultStyle, isConnectionAllowed, @@ -594,17 +596,25 @@ export function CanvasPage({ projectId }: CanvasPageProps) { Create Node - - {getRegisteredNodeTypes().map((desc, index) => ( - - {index === 2 ? : null} - createNode(desc.id)}> - {desc.menuIcon} - {desc.menuLabel} - - - ))} - + {getRegisteredNodeTypesGroupedByClassification().map( + (group, groupIndex) => + group.types.length > 0 && ( + + {groupIndex > 0 && } + + + {group.label} + + {group.types.map((desc) => ( + createNode(desc.id)}> + {desc.menuIcon} + {desc.menuLabel} + + ))} + + + ) + )} diff --git a/frontend/src/app/platform/AppSidebar.tsx b/frontend/src/app/platform/AppSidebar.tsx index c5411b4..059a6b4 100644 --- a/frontend/src/app/platform/AppSidebar.tsx +++ b/frontend/src/app/platform/AppSidebar.tsx @@ -21,7 +21,7 @@ import { Plus, ListTodo } from 'lucide-react' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useSidebar } from '@/components/ui/sidebar' import { usePlatform } from './platformContext' -import { getProjectIcon } from './icon-map' +import { getProjectIcon } from '../../lib/iconMap' import { NewProjectDialog } from './NewProjectDialog' import type { Project } from './types' @@ -132,7 +132,7 @@ export function AppSidebar() { - All projects + Pleroma diff --git a/frontend/src/app/platform/NewProjectDialog.tsx b/frontend/src/app/platform/NewProjectDialog.tsx index 64f810e..ef033ee 100644 --- a/frontend/src/app/platform/NewProjectDialog.tsx +++ b/frontend/src/app/platform/NewProjectDialog.tsx @@ -17,7 +17,7 @@ import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Plus } from 'lucide-react' import { PROJECT_ICON_IDS, type Project, type ProjectIconId } from './types' -import { getProjectIcon } from './icon-map' +import { getProjectIcon } from '../../lib/iconMap' type NewProjectDialogProps = { onCreate: (project: Project) => void diff --git a/frontend/src/app/platform/ProjectsPage.tsx b/frontend/src/app/platform/ProjectsPage.tsx index e8aa01c..902b89f 100644 --- a/frontend/src/app/platform/ProjectsPage.tsx +++ b/frontend/src/app/platform/ProjectsPage.tsx @@ -49,7 +49,7 @@ import { Network, } from 'lucide-react' import { usePlatform } from './platformContext' -import { getProjectIcon } from './icon-map' +import { getProjectIcon } from '../../lib/iconMap' import { loadGraphFromStorage, removeGraphFromStorage, @@ -319,7 +319,7 @@ export function ProjectsPage() {

- Hi, David + Hi, Demiurge

@@ -393,7 +393,7 @@ export function ProjectsPage() { onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() - ;(e.currentTarget as HTMLElement).click() + ; (e.currentTarget as HTMLElement).click() } }} aria-label="Create new project" diff --git a/frontend/src/components/base/NodeFooterEdgeIndicators.tsx b/frontend/src/components/base/NodeFooterEdgeIndicators.tsx index efd4194..8ed2cd6 100644 --- a/frontend/src/components/base/NodeFooterEdgeIndicators.tsx +++ b/frontend/src/components/base/NodeFooterEdgeIndicators.tsx @@ -2,7 +2,7 @@ import React, { useContext, useMemo } from 'react' import FlowContext from '../../lib/flowContext' import { ArrowDownLeft, ArrowUpRight } from 'lucide-react' import { NodeHelpPopover } from './NodeHelpPopover' -import { getNodeType } from '../../lib/nodeRegistry' +import { getNodeType, getNodeClassificationLabel } from '../../lib/nodeRegistry' type Props = { nodeId: string @@ -27,6 +27,7 @@ export function NodeFooterEdgeIndicators({ nodeId, nodeType, children }: Props) const descriptor = getNodeType(nodeType) const showInput = descriptor?.hasInput ?? false const showOutput = descriptor?.hasOutput ?? false + const classificationLabel = getNodeClassificationLabel(nodeType) return (
@@ -48,6 +49,12 @@ export function NodeFooterEdgeIndicators({ nodeId, nodeType, children }: Props) {children} )} + {classificationLabel != null && ( + <> + {(showInput || showOutput || children != null) && |} + {classificationLabel} + + )} diff --git a/frontend/src/app/platform/icon-map.tsx b/frontend/src/lib/iconMap.tsx similarity index 91% rename from frontend/src/app/platform/icon-map.tsx rename to frontend/src/lib/iconMap.tsx index 3f89596..728cec0 100644 --- a/frontend/src/app/platform/icon-map.tsx +++ b/frontend/src/lib/iconMap.tsx @@ -18,7 +18,7 @@ import { Turtle, type LucideIcon, } from 'lucide-react' -import type { ProjectIconId } from './types' +import type { ProjectIconId } from '../app/platform/types' export const PROJECT_ICON_MAP: Record = { bird: Bird, diff --git a/frontend/src/lib/nodeRegistry.ts b/frontend/src/lib/nodeRegistry.ts index cee556c..50b2a76 100644 --- a/frontend/src/lib/nodeRegistry.ts +++ b/frontend/src/lib/nodeRegistry.ts @@ -7,6 +7,15 @@ import type React from 'react' import type { Node } from '@xyflow/react' +/** Node classification for UI: Psyche (Form/Config, Variable, Behavior/Function), Pneuma (Rendering), Physis (TBD). */ +export type NodeClassification = 'psyche' | 'pneuma' | 'physis' + +export const NODE_CLASSIFICATION_LABELS: Record = { + psyche: 'Psyche', + pneuma: 'Pneuma', + physis: 'Physis', +} + export type NodeHelpEntry = { title: string content: React.ReactNode @@ -20,6 +29,8 @@ export type NodeTypeDescriptor = { idPrefix: string hasInput: boolean hasOutput: boolean + /** Classification for creation menu and node footer: Psyche, Pneuma, Physis. */ + classification?: NodeClassification /** When this type is the connection target, which source types are allowed. Omit = all. */ allowedSourceTypes?: string[] /** When this type is the connection source, which target types are allowed. Omit = all. */ @@ -48,10 +59,31 @@ export function getNodeType(id: string): NodeTypeDescriptor | undefined { return registry.get(id) } +const CLASSIFICATION_ORDER: NodeClassification[] = ['psyche', 'pneuma', 'physis'] + export function getRegisteredNodeTypes(): NodeTypeDescriptor[] { return Array.from(registry.values()) } +/** Node types grouped by classification for the Create Node menu. Order: Psyche, Pneuma, Physis. */ +export function getRegisteredNodeTypesGroupedByClassification(): { + classification: NodeClassification + label: string + types: NodeTypeDescriptor[] +}[] { + const byClass = new Map() + for (const c of CLASSIFICATION_ORDER) byClass.set(c, []) + for (const desc of registry.values()) { + const c = desc.classification ?? 'physis' + byClass.get(c)!.push(desc) + } + return CLASSIFICATION_ORDER.map((classification) => ({ + classification, + label: NODE_CLASSIFICATION_LABELS[classification], + types: byClass.get(classification)!, + })) +} + export function getRegisteredNodeTypeIds(): string[] { return Array.from(registry.keys()) } @@ -114,3 +146,10 @@ export function getNodeHelp(nodeType: string): NodeHelpEntry { const desc = getNodeType(nodeType) return desc?.help ?? { title: nodeType, content: null } } + +/** Classification label for a node type (e.g. "Psyche"). Shown in node footer before help button. */ +export function getNodeClassificationLabel(nodeType: string): string | null { + const desc = getNodeType(nodeType) + const c = desc?.classification + return c ? NODE_CLASSIFICATION_LABELS[c] : null +} diff --git a/frontend/src/lib/registerBuiltinNodes.tsx b/frontend/src/lib/registerBuiltinNodes.tsx index d0e748d..7686ee7 100644 --- a/frontend/src/lib/registerBuiltinNodes.tsx +++ b/frontend/src/lib/registerBuiltinNodes.tsx @@ -23,6 +23,7 @@ export function registerBuiltinNodes(): void { idPrefix: 'cfg_', hasInput: true, hasOutput: true, + classification: 'psyche', allowedSourceTypes: ['config', 'variable', 'function'], allowedTargetTypes: ['config', 'render'], help: NODE_HELP.config, @@ -49,6 +50,7 @@ export function registerBuiltinNodes(): void { idPrefix: 'rnd_', hasInput: true, hasOutput: false, + classification: 'pneuma', allowedSourceTypes: ['config'], help: NODE_HELP.render, menuLabel: 'Renderer', @@ -64,6 +66,7 @@ export function registerBuiltinNodes(): void { idPrefix: 'var_', hasInput: false, hasOutput: true, + classification: 'psyche', allowedTargetTypes: ['config', 'function'], help: NODE_HELP.variable, menuLabel: 'Variable', @@ -82,6 +85,7 @@ export function registerBuiltinNodes(): void { idPrefix: 'fn_', hasInput: true, hasOutput: true, + classification: 'psyche', allowedSourceTypes: ['config', 'variable', 'function'], allowedTargetTypes: ['config', 'function'], help: NODE_HELP.function,