diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/context.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/context.tsx deleted file mode 100644 index 787e4354d2..0000000000 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/context.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { - createContext, - useContext, - useRef, -} from 'react' -import { createJsonSchemaConfigStore } from './store' -import { useMitt } from '@/hooks/use-mitt' - -type JsonSchemaConfigStore = ReturnType - -type JsonSchemaConfigContextType = JsonSchemaConfigStore | null - -type JsonSchemaConfigProviderProps = { - children: React.ReactNode -} - -export const JsonSchemaConfigContext = createContext(null) - -export const JsonSchemaConfigContextProvider = ({ children }: JsonSchemaConfigProviderProps) => { - const storeRef = useRef() - - if (!storeRef.current) - storeRef.current = createJsonSchemaConfigStore() - - return ( - - {children} - - ) -} - -export const MittContext = createContext>({ - emit: () => {}, - useSubscribe: () => {}, -}) - -export const MittProvider = ({ children }: { children: React.ReactNode }) => { - const mitt = useMitt() - - return ( - - {children} - - ) -} - -export const useMittContext = () => { - return useContext(MittContext) -} diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/index.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/index.tsx index 485fe80e24..864aade83f 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/index.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/index.tsx @@ -2,7 +2,6 @@ import React, { type FC } from 'react' import Modal from '../../../../../base/modal' import type { SchemaRoot } from '../../types' import JsonSchemaConfig from './json-schema-config' -import { JsonSchemaConfigContextProvider, MittProvider } from './context' type JsonSchemaConfigModalProps = { isShow: boolean @@ -23,15 +22,11 @@ const JsonSchemaConfigModal: FC = ({ onClose={onClose} className='max-w-[960px] h-[800px] p-0' > - - - - - + ) } diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx index 6a020cec0f..1a67aae14f 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx @@ -1,5 +1,5 @@ import React, { type FC, useCallback, useState } from 'react' -import { ArrayType, type Field, type SchemaRoot, Type } from '../../types' +import { type SchemaRoot, Type } from '../../types' import { RiBracesLine, RiCloseLine, RiExternalLinkLine, RiTimelineView } from '@remixicon/react' import { SegmentedControl } from '../../../../../base/segmented-control' import JsonSchemaGenerator from './json-schema-generator' @@ -9,12 +9,8 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import VisualEditor from './visual-editor' import SchemaEditor from './schema-editor' -import { useJsonSchemaConfigStore } from './store' -import { useMittContext } from './context' -import type { EditData } from './visual-editor/edit-card' -import produce from 'immer' -import Toast from '@/app/components/base/toast' import { jsonToSchema } from '../../utils' +import { MittProvider, VisualEditorContextProvider } from './visual-editor/context' type JsonSchemaConfigProps = { defaultSchema?: SchemaRoot @@ -39,435 +35,17 @@ const DEFAULT_SCHEMA: SchemaRoot = { additionalProperties: false, } -type ChangeEventParams = { - path: string[], - parentPath: string[], - oldFields: EditData, - fields: EditData, -} - -type AddEventParams = { - path: string[] -} - -const findPropertyWithPath = (target: any, path: string[]) => { - let current = target - for (const key of path) - current = current[key] - return current -} - const JsonSchemaConfig: FC = ({ defaultSchema, onSave, onClose, }) => { const { t } = useTranslation() - const backupSchema = useJsonSchemaConfigStore(state => state.backupSchema) - const setBackupSchema = useJsonSchemaConfigStore(state => state.setBackupSchema) - const setIsAddingNewField = useJsonSchemaConfigStore(state => state.setIsAddingNewField) - const setHoveringProperty = useJsonSchemaConfigStore(state => state.setHoveringProperty) - const { emit, useSubscribe } = useMittContext() const [currentTab, setCurrentTab] = useState(SchemaView.VisualEditor) const [jsonSchema, setJsonSchema] = useState(defaultSchema || DEFAULT_SCHEMA) const [json, setJson] = useState(JSON.stringify(jsonSchema, null, 2)) const [btnWidth, setBtnWidth] = useState(0) - useSubscribe('restoreSchema', () => { - if (backupSchema) { - setJsonSchema(backupSchema) - setBackupSchema(null) - } - }) - - useSubscribe('propertyNameChange', (params) => { - const { parentPath, oldFields, fields } = params as ChangeEventParams - const { name: oldName } = oldFields - const { name: newName } = fields - const newSchema = produce(jsonSchema, (draft) => { - if (oldName === newName) return - const schema = findPropertyWithPath(draft, parentPath) as Field - - if (schema.type === Type.object) { - const properties = schema.properties || {} - if (properties[newName]) { - Toast.notify({ - type: 'error', - message: 'Property name already exists', - }) - emit('restorePropertyName') - return - } - - const newProperties = Object.entries(properties).reduce((acc, [key, value]) => { - acc[key === oldName ? newName : key] = value - return acc - }, {} as Record) - - const required = schema.required || [] - const newRequired = produce(required, (draft) => { - const index = draft.indexOf(oldName) - if (index !== -1) - draft.splice(index, 1, newName) - }) - - schema.properties = newProperties - schema.required = newRequired - } - - if (schema.type === Type.array && schema.items && schema.items.type === Type.object) { - const properties = schema.items.properties || {} - if (properties[newName]) { - Toast.notify({ - type: 'error', - message: 'Property name already exists', - }) - emit('restorePropertyName') - return - } - - const newProperties = Object.entries(properties).reduce((acc, [key, value]) => { - acc[key === oldName ? newName : key] = value - return acc - }, {} as Record) - const required = schema.items.required || [] - const newRequired = produce(required, (draft) => { - const index = draft.indexOf(oldName) - if (index !== -1) - draft.splice(index, 1, newName) - }) - - schema.items.properties = newProperties - schema.items.required = newRequired - } - }) - setJsonSchema(newSchema) - }) - - useSubscribe('propertyTypeChange', (params) => { - const { path, oldFields, fields } = params as ChangeEventParams - const { type: oldType } = oldFields - const { type: newType } = fields - if (oldType === newType) return - const newSchema = produce(jsonSchema, (draft) => { - const schema = findPropertyWithPath(draft, path) as Field - - if (schema.type === Type.object) { - delete schema.properties - delete schema.required - } - if (schema.type === Type.array) - delete schema.items - switch (newType) { - case Type.object: - schema.type = Type.object - schema.properties = {} - schema.required = [] - break - case ArrayType.string: - schema.type = Type.array - schema.items = { - type: Type.string, - } - break - case ArrayType.number: - schema.type = Type.array - schema.items = { - type: Type.number, - } - break - case ArrayType.boolean: - schema.type = Type.array - schema.items = { - type: Type.boolean, - } - break - case ArrayType.object: - schema.type = Type.array - schema.items = { - type: Type.object, - properties: {}, - required: [], - } - break - default: - schema.type = newType as Type - } - }) - setJsonSchema(newSchema) - }) - - useSubscribe('propertyRequiredToggle', (params) => { - const { parentPath, fields } = params as ChangeEventParams - const { name } = fields - const newSchema = produce(jsonSchema, (draft) => { - const schema = findPropertyWithPath(draft, parentPath) as Field - - if (schema.type === Type.object) { - const required = schema.required || [] - const newRequired = required.includes(name) - ? required.filter(item => item !== name) - : [...required, name] - schema.required = newRequired - } - if (schema.type === Type.array && schema.items && schema.items.type === Type.object) { - const required = schema.items.required || [] - const newRequired = required.includes(name) - ? required.filter(item => item !== name) - : [...required, name] - schema.items.required = newRequired - } - }) - setJsonSchema(newSchema) - }) - - useSubscribe('propertyOptionsChange', (params) => { - const { path, fields } = params as ChangeEventParams - const newSchema = produce(jsonSchema, (draft) => { - const schema = findPropertyWithPath(draft, path) as Field - schema.description = fields.description - schema.enum = fields.enum - }) - setJsonSchema(newSchema) - }) - - useSubscribe('propertyDelete', (params) => { - const { parentPath, fields } = params as ChangeEventParams - const { name } = fields - const newSchema = produce(jsonSchema, (draft) => { - const schema = findPropertyWithPath(draft, parentPath) as Field - if (schema.type === Type.object && schema.properties) { - delete schema.properties[name] - schema.required = schema.required?.filter(item => item !== name) - } - if (schema.type === Type.array && schema.items?.properties && schema.items?.type === Type.object) { - delete schema.items.properties[name] - schema.items.required = schema.items.required?.filter(item => item !== name) - } - }) - setJsonSchema(newSchema) - }) - - useSubscribe('addField', (params) => { - setBackupSchema(jsonSchema) - const { path } = params as AddEventParams - setIsAddingNewField(true) - const newSchema = produce(jsonSchema, (draft) => { - const schema = findPropertyWithPath(draft, path) as Field - if (schema.type === Type.object) { - schema.properties = { - ...(schema.properties || {}), - '': { - type: Type.string, - description: '', - enum: [], - }, - } - setHoveringProperty([...path, 'properties', ''].join('.')) - } - if (schema.type === Type.array && schema.items && schema.items.type === Type.object) { - schema.items.properties = { - ...(schema.items.properties || {}), - '': { - type: Type.string, - description: '', - enum: [], - }, - } - setHoveringProperty([...path, 'items', 'properties', ''].join('.')) - } - }) - setJsonSchema(newSchema) - }) - - useSubscribe('fieldChange', (params) => { - let samePropertyNameError = false - const { parentPath, oldFields, fields } = params as ChangeEventParams - const newSchema = produce(jsonSchema, (draft) => { - const parentSchema = findPropertyWithPath(draft, parentPath) as Field - const { name: oldName, type: oldType, required: oldRequired } = oldFields - const { name: newName, type: newType, required: newRequired } = fields - if (parentSchema.type === Type.object && parentSchema.properties) { - // name change - if (oldName !== newName) { - const properties = parentSchema.properties - if (properties[newName]) { - Toast.notify({ - type: 'error', - message: 'Property name already exists', - }) - samePropertyNameError = true - } - - const newProperties = Object.entries(properties).reduce((acc, [key, value]) => { - acc[key === oldName ? newName : key] = value - return acc - }, {} as Record) - - const requiredProperties = parentSchema.required || [] - const newRequiredProperties = produce(requiredProperties, (draft) => { - const index = draft.indexOf(oldName) - if (index !== -1) - draft.splice(index, 1, newName) - }) - - parentSchema.properties = newProperties - parentSchema.required = newRequiredProperties - } - - // required change - if (oldRequired !== newRequired) { - const required = parentSchema.required || [] - const newRequired = required.includes(newName) - ? required.filter(item => item !== newName) - : [...required, newName] - parentSchema.required = newRequired - } - - const schema = parentSchema.properties[newName] - - // type change - if (oldType !== newType) { - if (schema.type === Type.object) { - delete schema.properties - delete schema.required - } - if (schema.type === Type.array) - delete schema.items - switch (newType) { - case Type.object: - schema.type = Type.object - schema.properties = {} - schema.required = [] - break - case ArrayType.string: - schema.type = Type.array - schema.items = { - type: Type.string, - } - break - case ArrayType.number: - schema.type = Type.array - schema.items = { - type: Type.number, - } - break - case ArrayType.boolean: - schema.type = Type.array - schema.items = { - type: Type.boolean, - } - break - case ArrayType.object: - schema.type = Type.array - schema.items = { - type: Type.object, - properties: {}, - required: [], - } - break - default: - schema.type = newType as Type - } - } - - // other options change - schema.description = fields.description - schema.enum = fields.enum - } - - if (parentSchema.type === Type.array && parentSchema.items && parentSchema.items.type === Type.object && parentSchema.items.properties) { - // name change - if (oldName !== newName) { - const properties = parentSchema.items.properties || {} - if (properties[newName]) { - Toast.notify({ - type: 'error', - message: 'Property name already exists', - }) - samePropertyNameError = true - } - - const newProperties = Object.entries(properties).reduce((acc, [key, value]) => { - acc[key === oldName ? newName : key] = value - return acc - }, {} as Record) - const required = parentSchema.items.required || [] - const newRequired = produce(required, (draft) => { - const index = draft.indexOf(oldName) - if (index !== -1) - draft.splice(index, 1, newName) - }) - - parentSchema.items.properties = newProperties - parentSchema.items.required = newRequired - } - - // required change - if (oldRequired !== newRequired) { - const required = parentSchema.items.required || [] - const newRequired = required.includes(newName) - ? required.filter(item => item !== newName) - : [...required, newName] - parentSchema.items.required = newRequired - } - - const schema = parentSchema.items.properties[newName] - // type change - if (oldType !== newType) { - if (schema.type === Type.object) { - delete schema.properties - delete schema.required - } - if (schema.type === Type.array) - delete schema.items - switch (newType) { - case Type.object: - schema.type = Type.object - schema.properties = {} - schema.required = [] - break - case ArrayType.string: - schema.type = Type.array - schema.items = { - type: Type.string, - } - break - case ArrayType.number: - schema.type = Type.array - schema.items = { - type: Type.number, - } - break - case ArrayType.boolean: - schema.type = Type.array - schema.items = { - type: Type.boolean, - } - break - case ArrayType.object: - schema.type = Type.array - schema.items = { - type: Type.object, - properties: {}, - required: [], - } - break - default: - schema.type = newType as Type - } - } - - // other options change - schema.description = fields.description - schema.enum = fields.enum - } - }) - if (samePropertyNameError) return - setJsonSchema(newSchema) - emit('fieldChangeSuccess') - }) - const updateBtnWidth = useCallback((width: number) => { setBtnWidth(width + 32) }, []) @@ -481,6 +59,10 @@ const JsonSchemaConfig: FC = ({ setJsonSchema(jsonSchema) }, []) + const handleVisualEditorUpdate = useCallback((schema: SchemaRoot) => { + setJsonSchema(schema) + }, []) + const handleSchemaEditorUpdate = useCallback((schema: string) => { setJson(schema) }, []) @@ -535,7 +117,14 @@ const JsonSchemaConfig: FC = ({
{currentTab === SchemaView.VisualEditor && ( - + + + + + )} {currentTab === SchemaView.JsonSchema && ( { const { t } = useTranslation() - const setIsAddingNewField = useJsonSchemaConfigStore(state => state.setIsAddingNewField) + const setIsAddingNewField = useVisualEditorStore(state => state.setIsAddingNewField) const { emit } = useMittContext() const handleAddField = useCallback(() => { diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx new file mode 100644 index 0000000000..13ec53686f --- /dev/null +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx @@ -0,0 +1,49 @@ +import { + createContext, + useContext, + useRef, +} from 'react' +import { createVisualEditorStore } from './store' +import { useMitt } from '@/hooks/use-mitt' + +type VisualEditorStore = ReturnType + +type VisualEditorContextType = VisualEditorStore | null + +type VisualEditorProviderProps = { + children: React.ReactNode +} + +export const VisualEditorContext = createContext(null) + +export const VisualEditorContextProvider = ({ children }: VisualEditorProviderProps) => { + const storeRef = useRef() + + if (!storeRef.current) + storeRef.current = createVisualEditorStore() + + return ( + + {children} + + ) +} + +export const MittContext = createContext>({ + emit: () => {}, + useSubscribe: () => {}, +}) + +export const MittProvider = ({ children }: { children: React.ReactNode }) => { + const mitt = useMitt() + + return ( + + {children} + + ) +} + +export const useMittContext = () => { + return useContext(MittContext) +} diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/index.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/index.tsx index 7e6fd8ec93..309fbeda4f 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/index.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/index.tsx @@ -10,8 +10,8 @@ import AdvancedActions from './advanced-actions' import AdvancedOptions, { type AdvancedOptionsType } from './advanced-options' import { useTranslation } from 'react-i18next' import classNames from '@/utils/classnames' -import { useJsonSchemaConfigStore } from '../../store' -import { useMittContext } from '../../context' +import { useVisualEditorStore } from '../store' +import { useMittContext } from '../context' import produce from 'immer' import { useUnmount } from 'ahooks' import { JSON_SCHEMA_MAX_DEPTH } from '@/config' @@ -56,10 +56,10 @@ const EditCard: FC = ({ const { t } = useTranslation() const [currentFields, setCurrentFields] = useState(fields) const [backupFields, setBackupFields] = useState(null) - const isAddingNewField = useJsonSchemaConfigStore(state => state.isAddingNewField) - const setIsAddingNewField = useJsonSchemaConfigStore(state => state.setIsAddingNewField) - const advancedEditing = useJsonSchemaConfigStore(state => state.advancedEditing) - const setAdvancedEditing = useJsonSchemaConfigStore(state => state.setAdvancedEditing) + const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField) + const setIsAddingNewField = useVisualEditorStore(state => state.setIsAddingNewField) + const advancedEditing = useVisualEditorStore(state => state.advancedEditing) + const setAdvancedEditing = useVisualEditorStore(state => state.setAdvancedEditing) const { emit, useSubscribe } = useMittContext() const blurWithActions = useRef(false) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts new file mode 100644 index 0000000000..beaa02689c --- /dev/null +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts @@ -0,0 +1,423 @@ +import produce from 'immer' +import type { VisualEditorProps } from '.' +import { useMittContext } from './context' +import { useVisualEditorStore } from './store' +import type { EditData } from './edit-card' +import { ArrayType, type Field, Type } from '../../../types' +import Toast from '@/app/components/base/toast' +import { findPropertyWithPath } from '../../../utils' + +type ChangeEventParams = { + path: string[], + parentPath: string[], + oldFields: EditData, + fields: EditData, +} + +type AddEventParams = { + path: string[] +} + +export const useSchemaNodeOperations = (props: VisualEditorProps) => { + const { schema: jsonSchema, onChange } = props + const backupSchema = useVisualEditorStore(state => state.backupSchema) + const setBackupSchema = useVisualEditorStore(state => state.setBackupSchema) + const setIsAddingNewField = useVisualEditorStore(state => state.setIsAddingNewField) + const setHoveringProperty = useVisualEditorStore(state => state.setHoveringProperty) + const { emit, useSubscribe } = useMittContext() + + useSubscribe('restoreSchema', () => { + if (backupSchema) { + onChange(backupSchema) + setBackupSchema(null) + } + }) + + useSubscribe('propertyNameChange', (params) => { + const { parentPath, oldFields, fields } = params as ChangeEventParams + const { name: oldName } = oldFields + const { name: newName } = fields + const newSchema = produce(jsonSchema, (draft) => { + if (oldName === newName) return + const schema = findPropertyWithPath(draft, parentPath) as Field + + if (schema.type === Type.object) { + const properties = schema.properties || {} + if (properties[newName]) { + Toast.notify({ + type: 'error', + message: 'Property name already exists', + }) + emit('restorePropertyName') + return + } + + const newProperties = Object.entries(properties).reduce((acc, [key, value]) => { + acc[key === oldName ? newName : key] = value + return acc + }, {} as Record) + + const required = schema.required || [] + const newRequired = produce(required, (draft) => { + const index = draft.indexOf(oldName) + if (index !== -1) + draft.splice(index, 1, newName) + }) + + schema.properties = newProperties + schema.required = newRequired + } + + if (schema.type === Type.array && schema.items && schema.items.type === Type.object) { + const properties = schema.items.properties || {} + if (properties[newName]) { + Toast.notify({ + type: 'error', + message: 'Property name already exists', + }) + emit('restorePropertyName') + return + } + + const newProperties = Object.entries(properties).reduce((acc, [key, value]) => { + acc[key === oldName ? newName : key] = value + return acc + }, {} as Record) + const required = schema.items.required || [] + const newRequired = produce(required, (draft) => { + const index = draft.indexOf(oldName) + if (index !== -1) + draft.splice(index, 1, newName) + }) + + schema.items.properties = newProperties + schema.items.required = newRequired + } + }) + onChange(newSchema) + }) + + useSubscribe('propertyTypeChange', (params) => { + const { path, oldFields, fields } = params as ChangeEventParams + const { type: oldType } = oldFields + const { type: newType } = fields + if (oldType === newType) return + const newSchema = produce(jsonSchema, (draft) => { + const schema = findPropertyWithPath(draft, path) as Field + + if (schema.type === Type.object) { + delete schema.properties + delete schema.required + } + if (schema.type === Type.array) + delete schema.items + switch (newType) { + case Type.object: + schema.type = Type.object + schema.properties = {} + schema.required = [] + break + case ArrayType.string: + schema.type = Type.array + schema.items = { + type: Type.string, + } + break + case ArrayType.number: + schema.type = Type.array + schema.items = { + type: Type.number, + } + break + case ArrayType.boolean: + schema.type = Type.array + schema.items = { + type: Type.boolean, + } + break + case ArrayType.object: + schema.type = Type.array + schema.items = { + type: Type.object, + properties: {}, + required: [], + } + break + default: + schema.type = newType as Type + } + }) + onChange(newSchema) + }) + + useSubscribe('propertyRequiredToggle', (params) => { + const { parentPath, fields } = params as ChangeEventParams + const { name } = fields + const newSchema = produce(jsonSchema, (draft) => { + const schema = findPropertyWithPath(draft, parentPath) as Field + + if (schema.type === Type.object) { + const required = schema.required || [] + const newRequired = required.includes(name) + ? required.filter(item => item !== name) + : [...required, name] + schema.required = newRequired + } + if (schema.type === Type.array && schema.items && schema.items.type === Type.object) { + const required = schema.items.required || [] + const newRequired = required.includes(name) + ? required.filter(item => item !== name) + : [...required, name] + schema.items.required = newRequired + } + }) + onChange(newSchema) + }) + + useSubscribe('propertyOptionsChange', (params) => { + const { path, fields } = params as ChangeEventParams + const newSchema = produce(jsonSchema, (draft) => { + const schema = findPropertyWithPath(draft, path) as Field + schema.description = fields.description + schema.enum = fields.enum + }) + onChange(newSchema) + }) + + useSubscribe('propertyDelete', (params) => { + const { parentPath, fields } = params as ChangeEventParams + const { name } = fields + const newSchema = produce(jsonSchema, (draft) => { + const schema = findPropertyWithPath(draft, parentPath) as Field + if (schema.type === Type.object && schema.properties) { + delete schema.properties[name] + schema.required = schema.required?.filter(item => item !== name) + } + if (schema.type === Type.array && schema.items?.properties && schema.items?.type === Type.object) { + delete schema.items.properties[name] + schema.items.required = schema.items.required?.filter(item => item !== name) + } + }) + onChange(newSchema) + }) + + useSubscribe('addField', (params) => { + setBackupSchema(jsonSchema) + const { path } = params as AddEventParams + setIsAddingNewField(true) + const newSchema = produce(jsonSchema, (draft) => { + const schema = findPropertyWithPath(draft, path) as Field + if (schema.type === Type.object) { + schema.properties = { + ...(schema.properties || {}), + '': { + type: Type.string, + description: '', + enum: [], + }, + } + setHoveringProperty([...path, 'properties', ''].join('.')) + } + if (schema.type === Type.array && schema.items && schema.items.type === Type.object) { + schema.items.properties = { + ...(schema.items.properties || {}), + '': { + type: Type.string, + description: '', + enum: [], + }, + } + setHoveringProperty([...path, 'items', 'properties', ''].join('.')) + } + }) + onChange(newSchema) + }) + + useSubscribe('fieldChange', (params) => { + let samePropertyNameError = false + const { parentPath, oldFields, fields } = params as ChangeEventParams + const newSchema = produce(jsonSchema, (draft) => { + const parentSchema = findPropertyWithPath(draft, parentPath) as Field + const { name: oldName, type: oldType, required: oldRequired } = oldFields + const { name: newName, type: newType, required: newRequired } = fields + if (parentSchema.type === Type.object && parentSchema.properties) { + // name change + if (oldName !== newName) { + const properties = parentSchema.properties + if (properties[newName]) { + Toast.notify({ + type: 'error', + message: 'Property name already exists', + }) + samePropertyNameError = true + } + + const newProperties = Object.entries(properties).reduce((acc, [key, value]) => { + acc[key === oldName ? newName : key] = value + return acc + }, {} as Record) + + const requiredProperties = parentSchema.required || [] + const newRequiredProperties = produce(requiredProperties, (draft) => { + const index = draft.indexOf(oldName) + if (index !== -1) + draft.splice(index, 1, newName) + }) + + parentSchema.properties = newProperties + parentSchema.required = newRequiredProperties + } + + // required change + if (oldRequired !== newRequired) { + const required = parentSchema.required || [] + const newRequired = required.includes(newName) + ? required.filter(item => item !== newName) + : [...required, newName] + parentSchema.required = newRequired + } + + const schema = parentSchema.properties[newName] + + // type change + if (oldType !== newType) { + if (schema.type === Type.object) { + delete schema.properties + delete schema.required + } + if (schema.type === Type.array) + delete schema.items + switch (newType) { + case Type.object: + schema.type = Type.object + schema.properties = {} + schema.required = [] + break + case ArrayType.string: + schema.type = Type.array + schema.items = { + type: Type.string, + } + break + case ArrayType.number: + schema.type = Type.array + schema.items = { + type: Type.number, + } + break + case ArrayType.boolean: + schema.type = Type.array + schema.items = { + type: Type.boolean, + } + break + case ArrayType.object: + schema.type = Type.array + schema.items = { + type: Type.object, + properties: {}, + required: [], + } + break + default: + schema.type = newType as Type + } + } + + // other options change + schema.description = fields.description + schema.enum = fields.enum + } + + if (parentSchema.type === Type.array && parentSchema.items && parentSchema.items.type === Type.object && parentSchema.items.properties) { + // name change + if (oldName !== newName) { + const properties = parentSchema.items.properties || {} + if (properties[newName]) { + Toast.notify({ + type: 'error', + message: 'Property name already exists', + }) + samePropertyNameError = true + } + + const newProperties = Object.entries(properties).reduce((acc, [key, value]) => { + acc[key === oldName ? newName : key] = value + return acc + }, {} as Record) + const required = parentSchema.items.required || [] + const newRequired = produce(required, (draft) => { + const index = draft.indexOf(oldName) + if (index !== -1) + draft.splice(index, 1, newName) + }) + + parentSchema.items.properties = newProperties + parentSchema.items.required = newRequired + } + + // required change + if (oldRequired !== newRequired) { + const required = parentSchema.items.required || [] + const newRequired = required.includes(newName) + ? required.filter(item => item !== newName) + : [...required, newName] + parentSchema.items.required = newRequired + } + + const schema = parentSchema.items.properties[newName] + // type change + if (oldType !== newType) { + if (schema.type === Type.object) { + delete schema.properties + delete schema.required + } + if (schema.type === Type.array) + delete schema.items + switch (newType) { + case Type.object: + schema.type = Type.object + schema.properties = {} + schema.required = [] + break + case ArrayType.string: + schema.type = Type.array + schema.items = { + type: Type.string, + } + break + case ArrayType.number: + schema.type = Type.array + schema.items = { + type: Type.number, + } + break + case ArrayType.boolean: + schema.type = Type.array + schema.items = { + type: Type.boolean, + } + break + case ArrayType.object: + schema.type = Type.array + schema.items = { + type: Type.object, + properties: {}, + required: [], + } + break + default: + schema.type = newType as Type + } + } + + // other options change + schema.description = fields.description + schema.enum = fields.enum + } + }) + if (samePropertyNameError) return + onChange(newSchema) + emit('fieldChangeSuccess') + }) +} diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx index 425146039e..b5dafe2058 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx @@ -1,14 +1,17 @@ import type { FC } from 'react' import type { SchemaRoot } from '../../../types' import SchemaNode from './schema-node' +import { useSchemaNodeOperations } from './hooks' -type VisualEditorProps = { +export type VisualEditorProps = { schema: SchemaRoot + onChange: (schema: SchemaRoot) => void } -const VisualEditor: FC = ({ - schema, -}) => { +const VisualEditor: FC = (props) => { + const { schema } = props + useSchemaNodeOperations(props) + return (
= ({ depth, }) => { const [isExpanded, setIsExpanded] = useState(true) - const hoveringProperty = useJsonSchemaConfigStore(state => state.hoveringProperty) - const setHoveringProperty = useJsonSchemaConfigStore(state => state.setHoveringProperty) - const isAddingNewField = useJsonSchemaConfigStore(state => state.isAddingNewField) - const advancedEditing = useJsonSchemaConfigStore(state => state.advancedEditing) + const hoveringProperty = useVisualEditorStore(state => state.hoveringProperty) + const setHoveringProperty = useVisualEditorStore(state => state.setHoveringProperty) + const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField) + const advancedEditing = useVisualEditorStore(state => state.advancedEditing) const { run: setHoveringPropertyDebounced } = useDebounceFn((path: string) => { setHoveringProperty(path) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/store.ts b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/store.ts similarity index 66% rename from web/app/components/workflow/nodes/llm/components/json-schema-config-modal/store.ts rename to web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/store.ts index 3036dbc6cc..e1317e8c59 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/store.ts +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/store.ts @@ -1,9 +1,9 @@ import { useContext } from 'react' import { createStore, useStore } from 'zustand' -import type { SchemaRoot } from '../../types' -import { JsonSchemaConfigContext } from './context' +import type { SchemaRoot } from '../../../types' +import { VisualEditorContext } from './context' -type JsonSchemaConfigStore = { +type VisualEditorStore = { hoveringProperty: string | '' setHoveringProperty: (propertyPath: string) => void isAddingNewField: boolean @@ -14,7 +14,7 @@ type JsonSchemaConfigStore = { setBackupSchema: (schema: SchemaRoot | null) => void } -export const createJsonSchemaConfigStore = () => createStore(set => ({ +export const createVisualEditorStore = () => createStore(set => ({ hoveringProperty: '', setHoveringProperty: (propertyPath: string) => set({ hoveringProperty: propertyPath }), isAddingNewField: false, @@ -25,10 +25,10 @@ export const createJsonSchemaConfigStore = () => createStore set({ backupSchema: schema }), })) -export const useJsonSchemaConfigStore = (selector: (state: JsonSchemaConfigStore) => T): T => { - const store = useContext(JsonSchemaConfigContext) +export const useVisualEditorStore = (selector: (state: VisualEditorStore) => T): T => { + const store = useContext(VisualEditorContext) if (!store) - throw new Error('Missing JsonSchemaConfigContext.Provider in the tree') + throw new Error('Missing VisualEditorContext.Provider in the tree') return useStore(store, selector) } diff --git a/web/app/components/workflow/nodes/llm/utils.ts b/web/app/components/workflow/nodes/llm/utils.ts index f0184b0738..3fa7243865 100644 --- a/web/app/components/workflow/nodes/llm/utils.ts +++ b/web/app/components/workflow/nodes/llm/utils.ts @@ -72,3 +72,10 @@ export const checkDepth = (json: any, currentDepth = 1) => { } return maxDepth } + +export const findPropertyWithPath = (target: any, path: string[]) => { + let current = target + for (const key of path) + current = current[key] + return current +}