From 66515b4ec6aba080deff4333688914c0e5a0595d Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:08:14 +0100 Subject: [PATCH] feat(aichat): better diff viewer for inputs in flow mode (#7490) * better diff viewer for inputs * nit * fix * better * nit * fix undefined access * fix order * cleaning --- frontend/src/lib/components/SchemaForm.svelte | 45 +++-- .../components/flows/content/FlowInput.svelte | 162 +++++++++++++++--- 2 files changed, 167 insertions(+), 40 deletions(-) diff --git a/frontend/src/lib/components/SchemaForm.svelte b/frontend/src/lib/components/SchemaForm.svelte index fefe6b4656..cb8828286c 100644 --- a/frontend/src/lib/components/SchemaForm.svelte +++ b/frontend/src/lib/components/SchemaForm.svelte @@ -289,6 +289,7 @@ {#if keys.length > 0 && args} {#each fields as item, i (item.id)} {@const argName = item.value} + {@const prop = schema?.properties?.[argName]} - {#if args && typeof args == 'object' && schema?.properties[argName]} + {#if args && typeof args == 'object' && prop} @@ -382,38 +383,44 @@ {prettifyHeader} autofocus={i == 0 && autofocus ? true : null} label={argName} - description={schema.properties[argName].description} + description={prop?.description} bind:value={args[argName]} - type={schema.properties[argName].type} - oneOf={schema.properties[argName].oneOf} + type={prop?.type} + oneOf={prop?.oneOf} required={schema?.required?.includes(argName)} - pattern={schema.properties[argName].pattern} + pattern={prop?.pattern} bind:valid={inputCheck[argName]} defaultValue={defaultValues?.[argName] ?? - structuredClone($state.snapshot(schema.properties[argName].default))} - enum_={dynamicEnums?.[argName] ?? schema.properties[argName].enum} - format={schema.properties[argName].format} - contentEncoding={schema.properties[argName].contentEncoding} - customErrorMessage={schema.properties[argName].customErrorMessage} - bind:properties={schema.properties[argName].properties} - bind:order={schema.properties[argName].order} - nestedRequired={schema.properties[argName]?.required} - itemsType={schema.properties[argName].items} + structuredClone($state.snapshot(prop?.default))} + enum_={dynamicEnums?.[argName] ?? prop?.enum} + format={prop?.format} + contentEncoding={prop?.contentEncoding} + customErrorMessage={prop?.customErrorMessage} + bind:properties={ + () => prop?.properties, + (v) => { if (prop) prop.properties = v } + } + bind:order={ + () => prop?.order, + (v) => { if (prop) prop.order = v } + } + nestedRequired={prop?.required} + itemsType={prop?.items} disabled={disabledArgs.includes(argName) || disabled || - schema.properties[argName].disabled} + prop?.disabled} {compact} {variableEditor} {itemPicker} bind:pickForField password={linkedSecret == argName} - extra={schema.properties[argName]} + extra={prop} {showSchemaExplorer} simpleTooltip={schemaFieldTooltip[argName]} {onlyMaskPassword} - nullable={schema.properties[argName].nullable} - title={schema.properties[argName].title} - placeholder={schema.properties[argName].placeholder} + nullable={prop?.nullable} + title={prop?.title} + placeholder={prop?.placeholder} orderEditable={dndConfig != undefined} otherArgs={{ ...args, [argName]: undefined }} {helperScript} diff --git a/frontend/src/lib/components/flows/content/FlowInput.svelte b/frontend/src/lib/components/flows/content/FlowInput.svelte index 0c53df1b8a..a3d21ace66 100644 --- a/frontend/src/lib/components/flows/content/FlowInput.svelte +++ b/frontend/src/lib/components/flows/content/FlowInput.svelte @@ -49,6 +49,7 @@ import { nextId } from '../flowModuleNextId' import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte' import FlowChat from '../conversations/FlowChat.svelte' + import { SPECIAL_MODULE_IDS } from '$lib/components/copilot/chat/shared' interface Props { noEditor: boolean @@ -81,6 +82,11 @@ // Use pending schema from diffManager when in diff mode, otherwise use flowStore const effectiveSchema = $derived(diffManager?.currentInputSchema ?? flowStore.val.schema) + // Detect if we're in "review mode" (AI has made schema changes that are pending) + const hasAiSchemaChanges = $derived( + Boolean(diffManager?.moduleActions[SPECIAL_MODULE_IDS.INPUT]?.pending && diffManager?.beforeFlow?.schema) + ) + let chatInputEnabled = $state(Boolean(flowStore.val.value?.chat_input_enabled)) let shouldUseStreaming = $derived.by(() => { const modules = flowStore.val.value?.modules @@ -134,6 +140,34 @@ chatInputEnabled = Boolean(flowStore.val.value?.chat_input_enabled) }) + // Set up review mode when AI has made schema changes that are pending + $effect(() => { + hasAiSchemaChanges + diffManager?.beforeFlow?.schema + flowStore.val.schema + untrack(() => { + if (hasAiSchemaChanges) { + // In review mode, selectedSchema = beforeSchema (what we might revert to) + selectedSchema = structuredClone($state.snapshot(diffManager!.beforeFlow!.schema)) + diff = computeDiff(flowStore.val.schema, selectedSchema) + previewSchema = schemaFromDiff(diff, selectedSchema) + runDisabled = true + if (Object.values(diff).every((d) => d.diff === 'same')) { + diffManager?.acceptModule(SPECIAL_MODULE_IDS.INPUT, flowStore) + } + } + }) + }) + + $effect(() => { + if (!hasAiSchemaChanges) { + selectedSchema = undefined + previewSchema = undefined + diff = {} + runDisabled = false + } + }) + const getDropdownItems = () => { return [ { @@ -282,13 +316,19 @@ } async function applySchemaAndArgs() { - flowStore.val.schema = applyDiff(flowStore.val.schema, diff) - if (previewArgs.val) { - savedPreviewArgs = structuredClone($state.snapshot(previewArgs.val)) - } - updatePreviewSchemaAndArgs(undefined) - if ($flowInputEditorState) { - $flowInputEditorState.selectedTab = undefined + if (hasAiSchemaChanges) { + // Review mode: Accept all AI changes + diffManager?.acceptModule(SPECIAL_MODULE_IDS.INPUT, flowStore) + } else { + // Preview mode: Apply the diff to flowStore + flowStore.val.schema = applyDiff(flowStore.val.schema, diff) + if (previewArgs.val) { + savedPreviewArgs = structuredClone($state.snapshot(previewArgs.val)) + } + updatePreviewSchemaAndArgs(undefined) + if ($flowInputEditorState) { + $flowInputEditorState.selectedTab = undefined + } } } @@ -327,16 +367,28 @@ let preventEnter = $state(false) async function acceptChange(arg: { label: string; nestedParent: any | undefined }) { - handleChange(arg, flowStore.val.schema, diff, (newSchema) => { - flowStore.val.schema = newSchema - }) + if (hasAiSchemaChanges) { + // Review mode: Accept = finalize the AI change (keep current value) + handleChangeInReviewMode(arg, 'accept') + } else { + // Preview mode: Accept = apply the change to flowStore + handleChange(arg, flowStore.val.schema, diff, (newSchema) => { + flowStore.val.schema = newSchema + }) + } } async function rejectChange(arg: { label: string; nestedParent: any | undefined }) { - const revertDiff = computeDiff(flowStore.val.schema, selectedSchema) - handleChange(arg, selectedSchema, revertDiff, (newSchema) => { - selectedSchema = newSchema - }) + if (hasAiSchemaChanges) { + // Review mode: Reject = revert flowStore field to beforeSchema value + handleChangeInReviewMode(arg, 'reject') + } else { + // Preview mode: Reject = remove proposal from selectedSchema + const revertDiff = computeDiff(flowStore.val.schema, selectedSchema) + handleChange(arg, selectedSchema, revertDiff, (newSchema) => { + selectedSchema = newSchema + }) + } } function handleChange( @@ -367,6 +419,58 @@ previewSchema = schemaFromDiff(diff, flowStore.val.schema) } + function handleChangeInReviewMode( + arg: { label: string; nestedParent: any | undefined }, + action: 'accept' | 'reject' + ) { + // Accept: source=flowStore.val.schema (current), target=beforeSchema + // Reject: source=beforeSchema, target=flowStore.val.schema (current) + const beforeSchema = diffManager?.beforeFlow?.schema + const sourceSchema = action === 'accept' ? flowStore.val.schema : beforeSchema + const targetSchema = action === 'accept' ? beforeSchema : flowStore.val.schema + + if (!beforeSchema || !sourceSchema || !targetSchema) return + + const path = getFullPath(arg) + const parentPath = path.slice(0, -1) + + const getSchemaAtPath = (schema: Record) => + parentPath.length === 0 + ? schema + : getNestedProperty(schema, parentPath, 'properties') + + const getProperties = (schema: Record) => getSchemaAtPath(schema)?.properties + + const sourceProperties = getProperties(sourceSchema) + const targetProperties = getProperties(targetSchema) + const targetSchemaAtPath = getSchemaAtPath(targetSchema) + const sourceValue = sourceProperties?.[arg.label] + + if (sourceValue !== undefined) { + if (targetProperties) { + targetProperties[arg.label] = structuredClone($state.snapshot(sourceValue)) + if (targetSchemaAtPath?.order && !targetSchemaAtPath.order.includes(arg.label)) { + targetSchemaAtPath.order.push(arg.label) + } + } + } else { + if (targetProperties && arg.label in targetProperties) { + delete targetProperties[arg.label] + if (targetSchemaAtPath?.order) { + targetSchemaAtPath.order = targetSchemaAtPath.order.filter( + (x: string) => x !== arg.label + ) + } + } + } + + if (action === 'accept') { + diffManager.beforeFlow.schema = { ...beforeSchema } + } else { + flowStore.val.schema = { ...flowStore.val.schema } + } + } + function resetArgs() { if (!previewSchema) { savedPreviewArgs = undefined @@ -540,7 +644,7 @@ { + on:change={() => { handleToggleChatMode() }} options={{ @@ -591,13 +695,19 @@ {diff} disableDnd={!!previewSchema} on:rejectChange={(e) => { + const isAiChange = hasAiSchemaChanges rejectChange(e.detail).then(() => { - updatePreviewSchema(selectedSchema) + if (!isAiChange) { + updatePreviewSchema(selectedSchema) + } }) }} on:acceptChange={(e) => { acceptChange(e.detail).then(() => { - updatePreviewSchema(selectedSchema) + const isAiChange = hasAiSchemaChanges + if (!isAiChange) { + updatePreviewSchema(selectedSchema) + } }) }} shouldDispatchChanges={true} @@ -651,9 +761,13 @@ connectFirstNode() }} > - {Object.values(diff).every((el) => el.diff === 'same') - ? 'Apply args' - : 'Update schema'} + {#if hasAiSchemaChanges} + Accept all changes + {:else if Object.values(diff).every((el) => el.diff === 'same')} + Apply args + {:else} + Update schema + {/if}