From 04d2ef419dfe64c99da55ecead545cb1cc5cf185 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:17:56 +0100 Subject: [PATCH] feat(flow): Add graph diff visualizer (#6948) * graph mode * show colors * show module diff viewer button * better diff logic * small width merge graph diff * invert logic * simplify * better logic * put removed modules in initial position * nit * nit * fix conflicting ids * fix * add shadowed for after * better position logic * fix * cleaning * use splitpanes * add toggle * fix * sync move * icons * handle zoom * left header snippet * cleaning * cleaning * remove stats * big cleaning * fix * fix * fix * remove not working logic * invert logic * nit * use in deploymentui * fix typo * no custom style * handle nested * simpler logic * fix * fix --- .../src/lib/components/DeployWorkspace.svelte | 4 +- frontend/src/lib/components/DiffDrawer.svelte | 68 ++- .../lib/components/FlowGraphDiffViewer.svelte | 288 +++++++++ .../lib/components/copilot/chat/flow/core.ts | 2 +- .../lib/components/copilot/chat/flow/utils.ts | 6 + frontend/src/lib/components/flows/flowDiff.ts | 573 ++++++++++++++++++ .../flows/map/FlowModuleSchemaItem.svelte | 24 +- .../lib/components/flows/map/MapItem.svelte | 13 + .../components/flows/map/VirtualItem.svelte | 4 +- .../lib/components/graph/FlowGraphV2.svelte | 120 ++-- .../graph/ViewportSynchronizer.svelte | 63 ++ .../components/graph/graphBuilder.svelte.ts | 14 +- .../graph/renderers/nodes/InputNode.svelte | 16 +- .../graph/renderers/nodes/ModuleNode.svelte | 2 + .../flows/edit/[...path]/+page.svelte | 2 +- 15 files changed, 1137 insertions(+), 62 deletions(-) create mode 100644 frontend/src/lib/components/FlowGraphDiffViewer.svelte create mode 100644 frontend/src/lib/components/flows/flowDiff.ts create mode 100644 frontend/src/lib/components/graph/ViewportSynchronizer.svelte diff --git a/frontend/src/lib/components/DeployWorkspace.svelte b/frontend/src/lib/components/DeployWorkspace.svelte index d56a9819e1..9cccd0cb29 100644 --- a/frontend/src/lib/components/DeployWorkspace.svelte +++ b/frontend/src/lib/components/DeployWorkspace.svelte @@ -60,6 +60,7 @@ let diffDrawer: DiffDrawer | undefined = $state(undefined) let notSet: boolean | undefined = $state(undefined) + let isFlow: boolean | undefined = $state(undefined) async function reload(path: string) { try { @@ -638,7 +639,7 @@ {:else if seeTarget == true}

All related deployable items

- +
{#each dependencies ?? [] as { kind, path, include }, i} {@const statusPath = computeStatusPath(kind, path)} @@ -683,6 +684,7 @@ class="text-blue-600 font-normal mt-1" onclick={() => { showDiff(kind, path) + isFlow = kind === 'flow' }}>diff {/if}
{ if (!data || !diffType) return undefined @@ -30,14 +31,16 @@ ? 'metadata' : undefined }) + let diffViewer: Drawer | undefined = $state(undefined) interface Props { restoreDeployed?: () => Promise restoreDraft?: () => Promise + isFlow?: boolean } - let { restoreDeployed = async () => {}, restoreDraft = async () => {} }: Props = $props() + let { restoreDeployed = undefined, restoreDraft = undefined, isFlow = false }: Props = $props() let data: | { @@ -158,7 +161,7 @@ unifiedSize="md" variant="default" wrapperClasses="self-start" - on:click={restoreDraft} + onClick={restoreDraft} disabled={orderedJsonStringify(data.draft) === orderedJsonStringify(data.current)} >Restore to latest saved draft @@ -167,7 +170,7 @@ unifiedSize="md" variant="default" wrapperClasses="self-start" - on:click={restoreDeployed} + onClick={restoreDeployed} disabled={!data.draft && orderedJsonStringify(data.deployed) === orderedJsonStringify(data.current)} > @@ -228,19 +231,50 @@ /> {/await} {:else if contentType === 'metadata'} - {#await import('$lib/components/DiffEditor.svelte')} - - {:then Module} - - {/await} + {#if isFlow} + + + + + {#if flowdiffMode === 'yaml'} + {#await import('$lib/components/DiffEditor.svelte')} + + {:then Module} + + {/await} + {:else if flowdiffMode === 'graph'} + {#await import('$lib/components/FlowGraphDiffViewer.svelte')} + + {:then Module} + + {/await} + {/if} + {:else} + {#await import('$lib/components/DiffEditor.svelte')} + + {:then Module} + + {/await} + {/if} {/if} {/key} @@ -264,7 +298,7 @@ {#if data?.button} + {/if}
void annotation?: string | undefined nodeState?: FlowNodeState moving?: string | undefined @@ -53,6 +56,8 @@ moduleId, mod = $bindable(), insertable, + moduleAction = undefined, + onShowModuleDiff = undefined, annotation = undefined, nodeState, moving = undefined, @@ -148,6 +153,8 @@ onSelect(mod.id)} on:delete diff --git a/frontend/src/lib/components/flows/map/VirtualItem.svelte b/frontend/src/lib/components/flows/map/VirtualItem.svelte index 3c6cb8d236..b6e1f535bb 100644 --- a/frontend/src/lib/components/flows/map/VirtualItem.svelte +++ b/frontend/src/lib/components/flows/map/VirtualItem.svelte @@ -28,6 +28,7 @@ cache?: boolean earlyStop?: boolean editMode?: boolean + action?: 'added' | 'removed' | 'modified' | 'shadowed' | undefined icon?: import('svelte').Snippet onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void onEditInput?: (moduleId: string, key: string) => void @@ -57,6 +58,7 @@ cache = false, earlyStop = false, editMode = false, + action: actionProp = undefined, icon, onUpdateMock, onEditInput, @@ -75,7 +77,7 @@ (nodeKind || (inputJson && Object.keys(inputJson).length > 0)) && editMode ) - let action = $derived(label === 'Input' ? getAiModuleAction(label) : undefined) + let action = $derived(actionProp ?? (label === 'Input' ? getAiModuleAction(label) : undefined)) let hoverButton = $state(false) const outputType = $derived( diff --git a/frontend/src/lib/components/graph/FlowGraphV2.svelte b/frontend/src/lib/components/graph/FlowGraphV2.svelte index d9d8a5d0ae..8b16261e0d 100644 --- a/frontend/src/lib/components/graph/FlowGraphV2.svelte +++ b/frontend/src/lib/components/graph/FlowGraphV2.svelte @@ -1,7 +1,7 @@ {#if insertable} @@ -576,10 +605,20 @@ {:else} + {#if sharedViewport && onViewportChange} + + {/if} { document.dispatchEvent(new Event('focus')) }} + onmove={(event, viewport) => { + viewportSynchronizer?.handleLocalViewportChange(event, viewport) + }} {nodes} {edges} {edgeTypes} @@ -597,41 +636,48 @@ nodesDraggable={false} --background-color={false} > -
- - {#if download} - { - try { - localStorage.setItem( - 'svelvet', - encodeState({ modules, failureModule, preprocessorModule }) - ) - } catch (e) { - console.error('error interacting with local storage', e) - } - window.open('/view_graph', '_blank') - }} - class="!bg-surface" - > - - - {/if} - +
+ {#if leftHeader} +
+ {@render leftHeader()} +
+ {:else} + + {#if download} + { + try { + localStorage.setItem( + 'svelvet', + encodeState({ modules, failureModule, preprocessorModule }) + ) + } catch (e) { + console.error('error interacting with local storage', e) + } + window.open('/view_graph', '_blank') + }} + class="!bg-surface" + > + + + {/if} + - - - {#if showDataflow} - - {/if} - + + + {#if showDataflow} + + {/if} + + {/if}
{/if} diff --git a/frontend/src/lib/components/graph/ViewportSynchronizer.svelte b/frontend/src/lib/components/graph/ViewportSynchronizer.svelte new file mode 100644 index 0000000000..0458f6679e --- /dev/null +++ b/frontend/src/lib/components/graph/ViewportSynchronizer.svelte @@ -0,0 +1,63 @@ + diff --git a/frontend/src/lib/components/graph/graphBuilder.svelte.ts b/frontend/src/lib/components/graph/graphBuilder.svelte.ts index 829f42aefa..a6f0ebdd6a 100644 --- a/frontend/src/lib/components/graph/graphBuilder.svelte.ts +++ b/frontend/src/lib/components/graph/graphBuilder.svelte.ts @@ -7,6 +7,7 @@ import type { GraphModuleState } from './model' import { getFlowModuleAssets, type AssetWithAltAccessType } from '../assets/lib' import { assetDisplaysAsOutputInFlowGraph } from './renderers/nodes/AssetNode.svelte' import type { ModulesTestStates, ModuleTestState } from '../modulesTest.svelte' +import { type AIModuleAction } from '../copilot/chat/flow/core' export type InsertKind = | 'script' @@ -126,6 +127,8 @@ export type InputN = { showJobStatus: boolean flowHasChanged: boolean chatInputEnabled: boolean + inputSchemaModified?: boolean + onShowModuleDiff?: (moduleId: string) => void assets?: AssetWithAltAccessType[] | undefined } } @@ -146,6 +149,8 @@ export type ModuleN = { flowJob: Job | undefined isOwner: boolean assets: AssetWithAltAccessType[] | undefined + moduleAction: AIModuleAction | undefined + onShowModuleDiff?: (moduleId: string) => void } } @@ -364,6 +369,8 @@ export function graphBuilder( insertable: boolean flowModuleStates: Record | undefined testModuleStates: ModulesTestStates | undefined + moduleActions?: Record + inputSchemaModified?: boolean selectedId: string | undefined path: string | undefined newFlow: boolean @@ -378,6 +385,7 @@ export function graphBuilder( suspendStatus: Record flowHasChanged: boolean chatInputEnabled: boolean + onShowModuleDiff?: (moduleId: string) => void additionalAssetsMap?: Record }, failureModule: FlowModule | undefined, @@ -435,7 +443,9 @@ export function graphBuilder( editMode: extra.editMode, isOwner: extra.isOwner, flowJob: extra.flowJob, - assets: getFlowModuleAssets(module, extra.additionalAssetsMap) + assets: getFlowModuleAssets(module, extra.additionalAssetsMap), + moduleAction: extra.moduleActions?.[module.id], + onShowModuleDiff: extra.onShowModuleDiff }, type: 'module' }) @@ -553,6 +563,8 @@ export function graphBuilder( showJobStatus: extra.showJobStatus, flowHasChanged: extra.flowHasChanged, chatInputEnabled: extra.chatInputEnabled, + inputSchemaModified: extra.inputSchemaModified, + onShowModuleDiff: extra.onShowModuleDiff, ...(inputAssets ? { assets: inputAssets } : {}) } } diff --git a/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte index 6db83a4a0d..4bf2bd980a 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte @@ -9,7 +9,8 @@ import { schemaToObject } from '$lib/schema' import type { Schema } from '$lib/common' import type { FlowEditorContext } from '$lib/components/flows/types' - import { MessageSquare } from 'lucide-svelte' + import { MessageSquare, DiffIcon } from 'lucide-svelte' + import { Button } from '$lib/components/common' interface Props { data: InputN['data'] @@ -33,6 +34,18 @@ let inputLabel = $derived(data.chatInputEnabled ? 'Chat message' : 'Input') +{#if data.inputSchemaModified && data.onShowModuleDiff} +
+ +
+{/if} + {#snippet children({ darkMode })} {#if data.insertable && !data.hasPreprocessor} @@ -82,6 +95,7 @@ cache={data.cache} earlyStop={data.earlyStop} editMode={data.editMode} + action={data.inputSchemaModified ? 'modified' : undefined} onEditInput={data.eventHandlers.editInput} onTestFlow={() => { data.eventHandlers.testFlow() diff --git a/frontend/src/lib/components/graph/renderers/nodes/ModuleNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/ModuleNode.svelte index 27390981eb..aeaafbb189 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/ModuleNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/ModuleNode.svelte @@ -44,6 +44,8 @@ mod={data.module} insertable={data.insertable} editMode={data.editMode} + moduleAction={data.moduleAction} + onShowModuleDiff={data.onShowModuleDiff} annotation={flowJobs && (data.module.value.type === 'forloopflow' || data.module.value.type === 'whileloopflow') ? 'Iteration: ' + diff --git a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte index 76d2fca707..45df378067 100644 --- a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte @@ -266,7 +266,7 @@ - + {#if notFound}

Flow not found at path {page.params.path}