diff --git a/frontend/src/lib/components/graph/FlowGraphV2.svelte b/frontend/src/lib/components/graph/FlowGraphV2.svelte index 4788da7604..c141101c0c 100644 --- a/frontend/src/lib/components/graph/FlowGraphV2.svelte +++ b/frontend/src/lib/components/graph/FlowGraphV2.svelte @@ -34,7 +34,7 @@ import BaseEdge from './renderers/edges/BaseEdge.svelte' import EmptyEdge from './renderers/edges/EmptyEdge.svelte' import { sugiyama, dagStratify, coordCenter, decrossTwoLayer, decrossOpt } from 'd3-dag' - import { Expand } from 'lucide-svelte' + import { Expand, StickyNote } from 'lucide-svelte' import Toggle from '../Toggle.svelte' import DataflowEdge from './renderers/edges/DataflowEdge.svelte' import { encodeState, readFieldsRecursively } from '$lib/utils' @@ -55,6 +55,8 @@ import type { FlowGraphAssetContext } from '../flows/types' import AiToolNode, { computeAIToolNodes } from './renderers/nodes/AIToolNode.svelte' import NewAiToolNode from './renderers/nodes/NewAIToolNode.svelte' + import NoteNode from './renderers/nodes/NoteNode.svelte' + import NoteTool from './NoteTool.svelte' import { ChangeTracker } from '$lib/svelte5Utils.svelte' import type { ModulesTestStates } from '../modulesTest.svelte' import { deepEqual } from 'fast-equals' @@ -367,6 +369,19 @@ let height = $state(0) + // Note feature state + type NoteData = { + id: string + text: string + position: { x: number; y: number } + size: { width: number; height: number } + color: string + } + + let noteMode = $state(false) + let notes = $state([]) + let nextNoteId = $state(1) + function isSimplifiable(modules: FlowModule[] | undefined): boolean { if (!modules || modules?.length !== 2) { return false @@ -379,6 +394,56 @@ return false } + function toggleNoteMode() { + noteMode = !noteMode + } + + function onNoteAdded(newNoteFromTool: any) { + // Add the note to our separate notes array if a note was created + if (newNoteFromTool) { + const newNote: NoteData = { + id: `note-${nextNoteId}`, + text: '', + position: newNoteFromTool.position, + size: newNoteFromTool.size || { width: 200, height: 100 }, + color: 'oklch(96.2% 0.059 95.617)' + } + notes = [...notes, newNote] + nextNoteId += 1 + } + noteMode = false + updateStores() + } + + function updateNoteText(noteId: string, text: string) { + notes = notes.map((note) => (note.id === noteId ? { ...note, text } : note)) + } + + function deleteNote(noteId: string) { + notes = notes.filter((note) => note.id !== noteId) + updateStores() + } + + function convertNotesToNodes(): Node[] { + return notes.map((note) => ({ + id: note.id, + type: 'note', + position: note.position, + data: { + text: note.text, + color: note.color, + onUpdate: (text: string) => updateNoteText(note.id, text), + onDelete: () => deleteNote(note.id) + }, + style: `width: ${note.size.width}px; height: ${note.size.height}px;`, + width: note.size.width, + height: note.size.height, + zIndex: 1, + draggable: true, + selectable: true + })) + } + async function updateStores() { if (graph.error) { return @@ -409,7 +474,8 @@ nodes = [ ...newNodes.map((n) => ({ ...n, position: aiToolNodesResult.newNodePositions[n.id] })), ...assetNodesResult.newAssetNodes, - ...aiToolNodesResult.toolNodes + ...aiToolNodesResult.toolNodes, + ...convertNotesToNodes() ] edges = [...assetNodesResult.newAssetEdges, ...aiToolNodesResult.toolEdges, ...graph.edges] @@ -435,7 +501,8 @@ asset: AssetNode, assetsOverflowed: AssetsOverflowedNode, aiTool: AiToolNode, - newAiTool: NewAiToolNode + newAiTool: NewAiToolNode, + note: NoteNode } as any const edgeTypes = { @@ -565,7 +632,7 @@ { + onpaneclick={() => { document.dispatchEvent(new Event('focus')) }} {nodes} @@ -586,6 +653,10 @@ --background-color={false} >
+ + {#if noteMode} + + {/if} {#if download} {/if} + {#if editMode} + + + + {/if} + import { useSvelteFlow, type XYPosition } from '@xyflow/svelte' + + interface Props { + onNoteAdded?: (note: any) => void + } + + let { onNoteAdded }: Props = $props() + + const { screenToFlowPosition, getViewport } = useSvelteFlow() + + let isDrawing = $state(false) + let startPosition: XYPosition | null = $state(null) + let endPosition: XYPosition | null = $state(null) + let rect: DOMRect | null = $state(null) + + function onPointerDown(event: PointerEvent) { + // Capture pointer to continue tracking outside the element + const target = event.currentTarget as Element + target?.setPointerCapture?.(event.pointerId) + + // Use page coordinates as reference + rect = target.getBoundingClientRect() + startPosition = { + x: event.pageX - rect.left, + y: event.pageY - rect.top + } + isDrawing = true + } + + function onPointerMove(event: PointerEvent) { + console.log('NoteTool: pointer move', event) + if (event.buttons !== 1) return + + // Use page coordinates as reference + const target = event.currentTarget as Element + rect = target.getBoundingClientRect() + endPosition = { + x: event.pageX - rect.left, + y: event.pageY - rect.top + } + } + + function onPointerUp() { + if (!isDrawing || !startPosition || !endPosition || !rect) return + + // We need to convert the start and end positions to absolute positions to then convert to flow positions + const absoluteStartPosition = { + x: startPosition.x + rect.left, + y: startPosition.y + rect.top + } + const absoluteEndPosition = { + x: endPosition.x + rect.left, + y: endPosition.y + rect.top + } + + const position = screenToFlowPosition({ + x: Math.min(absoluteStartPosition.x, absoluteEndPosition.x), + y: Math.min(absoluteStartPosition.y, absoluteEndPosition.y) + }) + + const zoom = getViewport().zoom + const size = { + width: Math.abs(absoluteEndPosition.x - absoluteStartPosition.x) / zoom, + height: Math.abs(absoluteEndPosition.y - absoluteStartPosition.y) / zoom + } + + // Create the actual note with the calculated size and position + onNoteAdded?.({ + position, + size + }) + + // Reset state + isDrawing = false + startPosition = null + } + + const previewNote = $derived( + startPosition && endPosition + ? { + position: { + x: Math.min(startPosition['x'], endPosition['x']), + y: Math.min(startPosition['y'], endPosition['y']) + }, + size: { + width: Math.abs(endPosition['x'] - startPosition['x']), + height: Math.abs(endPosition['y'] - startPosition['y']) + } + } + : null + ) + + +
{ + if (e.key === 'Escape') { + if (isDrawing) { + // Cancel current drawing + isDrawing = false + startPosition = null + } else { + // Exit note mode + onNoteAdded?.(null) + } + } + }} +> + + {#if previewNote} +
+
+ {/if} +
+ + diff --git a/frontend/src/lib/components/graph/renderers/nodes/NoteNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/NoteNode.svelte new file mode 100644 index 0000000000..d0e4e5c537 --- /dev/null +++ b/frontend/src/lib/components/graph/renderers/nodes/NoteNode.svelte @@ -0,0 +1,107 @@ + + +
{ + dragging = false + }} + ondragstart={() => { + dragging = true + }} + ondragend={() => { + dragging = false + }} + role="note" +> + + + + +
+ +
+ + + +
+ +