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:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user