feat: improvements

This commit is contained in:
2026-03-15 22:08:31 +01:00
parent 4d673ab556
commit 45c387c9e6
12 changed files with 598 additions and 420 deletions

View File

@@ -4,7 +4,6 @@
*/
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import { useLocation } from 'react-router-dom'
import {
ReactFlow,
ReactFlowProvider,
@@ -37,8 +36,8 @@ import { ContextMenu, ContextMenuTrigger } from '@/components/ui/context-menu'
import { CanvasContextMenuContent } from '@/app/canvas/CanvasContextMenuContent'
import { useCanvasConnectionPathFromStore } from '@/app/canvas/useCanvasConnectionPathFromStore'
import { dispatchCanvasCommand } from '@/app/canvas/canvasStore'
import { useRecollectionTitleData } from '@/app/recollections/RecollectionTitleDataContext'
import { FluxMenubarContent } from '@/app/recollections/flux/FluxMenubarContent'
import { useRecollectionActions } from '@/app/recollections/RecollectionActionsContext'
import type { StoredGraphState } from '@/app/recollections/recollectionStore'
import { createContextualNode } from '@/app/canvas/ContextualZoomNode'
import { ViewportDisplayProvider } from '@/app/canvas/ViewportDisplayContext'
import { FlowKeyboardShortcuts } from '@/components/graph/FlowKeyboardShortcuts'
@@ -68,7 +67,7 @@ import {
} from '@/lib/graph/nodeRegistry'
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
import { toast } from 'sonner'
import { RECOLLECTION_FILE_EXT, RECOLLECTION_VERSION } from '@/app/recollections/recollectionGraphStorage'
import { RECOLLECTION_VERSION } from '@/app/recollections/recollectionGraphStorage'
const SNAP_GRID: [number, number] = [15, 15]
const DUPLICATE_OFFSET = { x: 30, y: 30 }
@@ -176,7 +175,6 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
saveStatus,
} = useCanvasGraph(recollectionId)
const importInputRef = useRef<HTMLInputElement | null>(null)
const [rfInstance, setRfInstance] = React.useState<unknown>(null)
const [renamingNodeId, setRenamingNodeId] = React.useState<string | null>(null)
const [connectionFrom, setConnectionFrom] = React.useState<{ nodeId: string; sourceHandle?: string } | null>(null)
@@ -325,57 +323,12 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
const onNodeDragStart = useCallback(() => saveForDragEnd(), [saveForDragEnd])
const onNodeDragStop = useCallback(() => commitDragEnd(), [commitDragEnd])
const handleExportRecollection = useCallback(() => {
const state = { version: RECOLLECTION_VERSION, nodes, edges }
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `recollection${RECOLLECTION_FILE_EXT}`
a.click()
URL.revokeObjectURL(url)
toast.success('Recollection exported')
}, [nodes, edges])
const handleImportRecollection = useCallback(() => importInputRef.current?.click(), [])
const handleLoadExample = useCallback(() => {
const { nodes: exampleNodes, edges: exampleEdges } = getExampleGraph()
setStateImmediate({ nodes: exampleNodes, edges: exampleEdges })
toast.success('Example loaded')
}, [setStateImmediate])
const onImportFileChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
e.target.value = ''
if (!file) return
const reader = new FileReader()
reader.onload = () => {
try {
const text = reader.result as string
const state = JSON.parse(text) as { version?: number; nodes?: unknown[]; edges?: unknown[] }
if (!state || !Array.isArray(state.nodes) || !Array.isArray(state.edges)) {
toast.error('Invalid file: expected nodes and edges arrays')
return
}
const nodes = state.nodes as AppNode[]
const edges = backfillEdgeTargetTypes(nodes, state.edges as AppEdge[])
setStateImmediate({ nodes, edges })
if (state.version != null && state.version > RECOLLECTION_VERSION) {
toast.error('Recollection was created with a newer app version')
} else {
toast.success('Recollection loaded')
}
} catch {
toast.error('Invalid file: not valid JSON')
}
}
reader.readAsText(file)
},
[setStateImmediate]
)
const selectedNodes = useMemo(
() => nodes.filter((n) => (n as Node & { selected?: boolean }).selected),
[nodes]
@@ -429,56 +382,56 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
flowActionsRef.current?.pasteAtViewportCenter?.()
}, [])
const { pathname } = useLocation()
const isFluxActive = pathname.endsWith('/flux')
const { setTitleData } = useRecollectionTitleData()
const { setFluxSlot, onImport } = useRecollectionActions()
const onRefreshFromStore = useCallback(
(graph: StoredGraphState) => {
const nodes = graph.nodes as AppNode[]
const edges = backfillEdgeTargetTypes(nodes, graph.edges as AppEdge[])
setStateImmediate({ nodes, edges })
},
[setStateImmediate]
)
useEffect(() => {
setTitleData({
const slot = {
saveStatus,
onSave: recollectionId
? () => {
save()
toast.success('Saved')
}
: undefined,
: () => {},
canSave: Boolean(recollectionId),
onImport: handleImportRecollection,
onExport: handleExportRecollection,
})
return () => setTitleData(null)
}, [
setTitleData,
saveStatus,
recollectionId,
save,
handleImportRecollection,
handleExportRecollection,
])
const fluxMenubarProps = useMemo(
() => ({
undo,
redo,
canUndo,
canRedo,
onRefreshFromStore,
onDuplicate: handleDuplicate,
onCopy: handleCopy,
onPaste: handlePaste,
canDuplicate: selectedNodes.length > 0,
canCopy: selectedNodes.length === 1,
onFitView: () => flowActionsRef.current?.fitView?.(),
}),
[
canUndo,
canRedo,
selectedNodes.length,
undo,
redo,
handleDuplicate,
handleCopy,
handlePaste,
]
)
}
setFluxSlot(slot)
return () => setFluxSlot(null)
}, [
setFluxSlot,
saveStatus,
recollectionId,
save,
undo,
redo,
canUndo,
canRedo,
onRefreshFromStore,
handleDuplicate,
handleCopy,
handlePaste,
selectedNodes.length,
])
const graphContextValue = useMemo(
() => ({ setNodes, setEdges, graphRef, edges }),
@@ -688,18 +641,9 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
onContextMenu={onCanvasContextMenu}
style={{ position: 'relative' }}
>
{isFluxActive && <FluxMenubarContent {...fluxMenubarProps} />}
<div role="status" aria-live="polite" aria-atomic className="sr-only">
{ariaAnnouncement}
</div>
<input
ref={importInputRef}
type="file"
accept=".json,.zui.json,application/json"
className="hidden"
onChange={onImportFileChange}
aria-hidden
/>
<div className="flex-1 min-h-0 relative flex flex-col">
<div className="flex-1 min-h-0 flex flex-col">
<GraphContext.Provider value={graphContextValue}>
@@ -729,7 +673,7 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
</EmptyDescription>
</EmptyHeader>
<EmptyContent className="flex-row flex-wrap justify-center gap-2">
<Button onClick={handleImportRecollection} variant="outline" size="sm">
<Button onClick={onImport} variant="outline" size="sm">
<FolderOpen className="size-4" />
Import
</Button>