Feat/frontend canvas - #11
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enhances the frontend workflow-canvas experience by adding interaction features (undo/redo, delete/save controls, drag-to-trash deletion) and making the sidebar/config/output panels collapsible and resizable for better screen utilization.
Changes:
- Added undo/redo state management and node/edge deletion support in the pipeline store, plus selection tracking for nodes and edges.
- Updated the canvas and sidebar UX (edge selection, double-click delete, drag-to-trash delete, double-click-to-add from sidebar).
- Reworked the main page layout to support collapsible/resizable sidebar/config/output panels and introduced topbar controls (undo/redo/delete/save/run) with toast notifications.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/store/pipelineStore.ts | Adds history (past/future), selection for edges, deletion APIs, and undo/redo logic for canvas state. |
| frontend/src/components/TrashBin.tsx | New UI component used as a visual drop target for drag-to-delete. |
| frontend/src/components/Sidebar.tsx | Adds double-click-to-add behavior and refines node list presentation for the collapsible sidebar. |
| frontend/src/components/output/OutputPanel.tsx | UI refinements, save button UX state, and simplified header/layout to fit the resizable output panel container. |
| frontend/src/components/ConfigPanel.tsx | Adjusts styling/layout for the new resizable config panel and improves node type display formatting. |
| frontend/src/components/Canvas.tsx | Adds edge selection, double-click delete, drag-to-trash delete, and drag-stop history integration. |
| frontend/app/page.tsx | Implements new topbar controls, collapsible/resizable panels, and toast notifications around run/save actions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
92
to
+111
| updateNodeConfig: (nodeId, config) => | ||
| set((state) => ({nodes: state.nodes.map((node) => node.id === nodeId ? {...node, data: {...node.data,config: {...(((node.data as Record<string, unknown>).config as Record<string, unknown>) ?? {}), ...config,},},} : node),})), | ||
| })); No newline at end of file | ||
| set((state) => ({ | ||
| nodes: state.nodes.map((node) => | ||
| node.id === nodeId | ||
| ? { | ||
| ...node, | ||
| data: { | ||
| ...node.data, | ||
| config: { | ||
| ...(((node.data as Record<string, unknown>).config as Record< | ||
| string, | ||
| unknown | ||
| >) ?? {}), | ||
| ...config, | ||
| }, | ||
| }, | ||
| } | ||
| : node, | ||
| ), | ||
| })), |
Comment on lines
+149
to
+170
| undo: () => | ||
| set((state) => { | ||
| if (state.past.length === 0) return state; | ||
| const previous = state.past[state.past.length - 1]; | ||
| return { | ||
| past: state.past.slice(0, -1), | ||
| future: [{ nodes: state.nodes, edges: state.edges }, ...state.future], | ||
| nodes: previous.nodes, | ||
| edges: previous.edges, | ||
| }; | ||
| }), | ||
|
|
||
| redo: () => | ||
| set((state) => { | ||
| if (state.future.length === 0) return state; | ||
| const next = state.future[0]; | ||
| return { | ||
| past: [...state.past, { nodes: state.nodes, edges: state.edges }], | ||
| future: state.future.slice(1), | ||
| nodes: next.nodes, | ||
| edges: next.edges, | ||
| }; |
Comment on lines
60
to
+65
| onConnect: (connection) => | ||
| set((state) => ({ edges: addEdge(connection, state.edges) })), | ||
| set((state) => ({ | ||
| past: [...state.past, { nodes: state.nodes, edges: state.edges }], | ||
| future: [], | ||
| edges: addEdge(connection, state.edges), | ||
| })), |
| }, [editingName]); | ||
|
|
||
| useEffect(() => { | ||
| if (selectedNodeId && !configOpen) { |
Comment on lines
+167
to
+179
| const buildSavedRun = (result: any) => { | ||
| const output = result.output; | ||
| if (!output) return null; | ||
| return { | ||
| id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, | ||
| timestamp: new Date().toISOString(), | ||
| modelName: output.model_name ?? output.run_summary?.model ?? "unknown", | ||
| taskType: output.run_summary?.task_type ?? "unknown", | ||
| metrics: output.metrics ?? {}, | ||
| configUsed: output.config_used ?? {}, | ||
| executionTime: result.execution_time ?? 0, | ||
| }; | ||
| }; |
Comment on lines
131
to
138
| } catch (e: unknown) { | ||
| setExecutionError(e instanceof Error ? e.message : "Something went wrong"); | ||
| const errorMessage = | ||
| e instanceof Error ? e.message : "Something went wrong"; | ||
| setExecutionError(errorMessage); | ||
| setToast({ message: errorMessage, type: "error" }); | ||
| setToastVisible(true); | ||
| setTimeout(() => setToastVisible(false), 3000); | ||
| } |
Comment on lines
+439
to
+455
| <button | ||
| onClick={() => setSidebarOpen(false)} | ||
| className="w-5 h-5 flex items-center justify-center text-white/30 hover:text-white/60 transition-colors" | ||
| > | ||
| {/* Arrow points LEFT = collapse to left */} | ||
| <svg | ||
| width="11" | ||
| height="11" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2.5" | ||
| strokeLinecap="round" | ||
| > | ||
| <polyline points="15 18 9 12 15 6" /> | ||
| </svg> | ||
| </button> |
Comment on lines
+493
to
+514
| <button | ||
| onClick={() => setOutputOpen((v) => !v)} | ||
| className="w-5 h-5 flex items-center justify-center text-white/30 hover:text-white/60 transition-colors" | ||
| > | ||
| <svg | ||
| width="11" | ||
| height="11" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2.5" | ||
| strokeLinecap="round" | ||
| style={{ | ||
| transform: outputOpen ? "rotate(0deg)" : "rotate(180deg)", | ||
| transition: "transform 0.2s", | ||
| }} | ||
| > | ||
| {/* Points down when open (to collapse), up when closed (to expand) */} | ||
| <polyline points="6 9 12 15 18 9" /> | ||
| </svg> | ||
| </button> | ||
| </div> |
Comment on lines
433
to
+438
| <button | ||
| onClick={handleCopy} | ||
| onClick={async () => { | ||
| await navigator.clipboard.writeText(code); | ||
| setToast("Copied!"); | ||
| setTimeout(() => setToast(null), 2000); | ||
| }} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Added small features: undo/redo, delete/save buttons and resizing/minimizing option for configuration and output panel.
Type of Change
Related Issues
none
Checklist