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
This commit is contained in:
centdix
2026-01-06 11:08:14 +01:00
committed by GitHub
parent 939ac8d820
commit 66515b4ec6
2 changed files with 167 additions and 40 deletions

View File

@@ -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]}
<ResizeTransitionWrapper
vertical
class={twMerge(
@@ -361,7 +362,7 @@
dispatch('click', argName)
}}
>
{#if args && typeof args == 'object' && schema?.properties[argName]}
{#if args && typeof args == 'object' && prop}
<!-- {argName}
{args == undefined}
{JSON.stringify(args?.[argName])} -->
@@ -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}

View File

@@ -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<string, any>) =>
parentPath.length === 0
? schema
: getNestedProperty(schema, parentPath, 'properties')
const getProperties = (schema: Record<string, any>) => 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 @@
<Toggle
size="sm"
bind:checked={chatInputEnabled}
on:change={(e) => {
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}
</Button>
<Button
variant="default"
@@ -661,7 +775,13 @@
startIcon={{ icon: X }}
shortCut={{ key: 'esc', withoutModifier: true }}
on:click={() => {
resetSelected()
if (hasAiSchemaChanges) {
if (diffManager?.beforeFlow?.schema) {
diffManager.rejectModule(SPECIAL_MODULE_IDS.INPUT, flowStore)
}
} else {
resetSelected()
}
}}
/>
</div>