mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-17 22:35:56 +08:00
feat: implement JSON Schema configuration context and store for advanced editing options
This commit is contained in:
parent
183edf0fd5
commit
d4185c2d91
@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useRef,
|
||||||
|
} from 'react'
|
||||||
|
import { createJsonSchemaConfigStore } from './store'
|
||||||
|
import { useMitt } from '@/hooks/use-mitt'
|
||||||
|
|
||||||
|
type JsonSchemaConfigStore = ReturnType<typeof createJsonSchemaConfigStore>
|
||||||
|
|
||||||
|
type JsonSchemaConfigContextType = JsonSchemaConfigStore | null
|
||||||
|
|
||||||
|
type JsonSchemaConfigProviderProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const JsonSchemaConfigContext = createContext<JsonSchemaConfigContextType>(null)
|
||||||
|
|
||||||
|
export const JsonSchemaConfigContextProvider = ({ children }: JsonSchemaConfigProviderProps) => {
|
||||||
|
const storeRef = useRef<JsonSchemaConfigStore>()
|
||||||
|
|
||||||
|
if (!storeRef.current)
|
||||||
|
storeRef.current = createJsonSchemaConfigStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JsonSchemaConfigContext.Provider value={storeRef.current}>
|
||||||
|
{children}
|
||||||
|
</JsonSchemaConfigContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MittContext = createContext<ReturnType<typeof useMitt>>({
|
||||||
|
emit: () => {},
|
||||||
|
useSubscribe: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const MittProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const mitt = useMitt()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MittContext.Provider value={mitt}>
|
||||||
|
{children}
|
||||||
|
</MittContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMittContext = () => {
|
||||||
|
return useContext(MittContext)
|
||||||
|
}
|
@ -1,164 +1,37 @@
|
|||||||
import React, { type FC, useCallback, useState } from 'react'
|
import React, { type FC } from 'react'
|
||||||
import Modal from '../../../../../base/modal'
|
import Modal from '../../../../../base/modal'
|
||||||
import { type Field, Type } from '../../types'
|
import type { SchemaRoot } from '../../types'
|
||||||
import { RiBracesLine, RiCloseLine, RiExternalLinkLine, RiTimelineView } from '@remixicon/react'
|
import JsonSchemaConfig from './json-schema-config'
|
||||||
import { SegmentedControl } from '../../../../../base/segmented-control'
|
import { JsonSchemaConfigContextProvider, MittProvider } from './context'
|
||||||
import JsonSchemaGenerator from './json-schema-generator'
|
|
||||||
import Divider from '@/app/components/base/divider'
|
|
||||||
import JsonImporter from './json-importer'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import Button from '@/app/components/base/button'
|
|
||||||
import VisualEditor from './visual-editor'
|
|
||||||
import SchemaEditor from './schema-editor'
|
|
||||||
|
|
||||||
type JsonSchemaConfigModalProps = {
|
type JsonSchemaConfigModalProps = {
|
||||||
isShow: boolean
|
isShow: boolean
|
||||||
defaultSchema?: Field
|
defaultSchema?: SchemaRoot
|
||||||
onSave: (schema: Field) => void
|
onSave: (schema: SchemaRoot) => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SchemaView {
|
|
||||||
VisualEditor = 'visualEditor',
|
|
||||||
JsonSchema = 'jsonSchema',
|
|
||||||
}
|
|
||||||
|
|
||||||
const VIEW_TABS = [
|
|
||||||
{ Icon: RiTimelineView, text: 'Visual Editor', value: SchemaView.VisualEditor },
|
|
||||||
{ Icon: RiBracesLine, text: 'JSON Schema', value: SchemaView.JsonSchema },
|
|
||||||
]
|
|
||||||
|
|
||||||
const DEFAULT_SCHEMA: Field = {
|
|
||||||
type: Type.object,
|
|
||||||
properties: {},
|
|
||||||
required: [],
|
|
||||||
additionalProperties: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
|
const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
|
||||||
isShow,
|
isShow,
|
||||||
defaultSchema,
|
defaultSchema,
|
||||||
onSave,
|
onSave,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
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)
|
|
||||||
|
|
||||||
const updateBtnWidth = useCallback((width: number) => {
|
|
||||||
setBtnWidth(width + 32)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleApplySchema = useCallback(() => {}, [])
|
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {}, [])
|
|
||||||
|
|
||||||
const handleUpdateSchema = useCallback((schema: Field) => {
|
|
||||||
setJsonSchema(schema)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleSchemaEditorUpdate = useCallback((schema: string) => {
|
|
||||||
setJson(schema)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleResetDefaults = useCallback(() => {
|
|
||||||
setJsonSchema(defaultSchema || DEFAULT_SCHEMA)
|
|
||||||
}, [defaultSchema])
|
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
|
||||||
onClose()
|
|
||||||
}, [onClose])
|
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
|
||||||
onSave(jsonSchema)
|
|
||||||
onClose()
|
|
||||||
}, [jsonSchema, onSave, onClose])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isShow={isShow}
|
isShow={isShow}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
className='max-w-[960px] h-[800px] p-0'
|
className='max-w-[960px] h-[800px] p-0'
|
||||||
>
|
>
|
||||||
<div className='flex flex-col h-full'>
|
<MittProvider>
|
||||||
{/* Header */}
|
<JsonSchemaConfigContextProvider>
|
||||||
<div className='relative flex p-6 pr-14 pb-3'>
|
<JsonSchemaConfig
|
||||||
<div className='text-text-primary title-2xl-semi-bold grow truncate'>
|
defaultSchema={defaultSchema}
|
||||||
{t('workflow.nodes.llm.jsonSchema.title')}
|
onSave={onSave}
|
||||||
</div>
|
onClose={onClose}
|
||||||
<div className='absolute right-5 top-5 w-8 h-8 flex justify-center items-center p-1.5' onClick={() => onClose()}>
|
|
||||||
<RiCloseLine className='w-[18px] h-[18px] text-text-tertiary' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Content */}
|
|
||||||
<div className='flex items-center justify-between px-6 py-2'>
|
|
||||||
{/* Tab */}
|
|
||||||
<SegmentedControl<SchemaView>
|
|
||||||
options={VIEW_TABS}
|
|
||||||
value={currentTab}
|
|
||||||
onChange={(value: SchemaView) => {
|
|
||||||
setCurrentTab(value)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className='flex items-center gap-x-0.5'>
|
</JsonSchemaConfigContextProvider>
|
||||||
{/* JSON Schema Generator */}
|
</MittProvider >
|
||||||
<JsonSchemaGenerator
|
|
||||||
crossAxisOffset={btnWidth}
|
|
||||||
onApply={handleApplySchema}
|
|
||||||
/>
|
|
||||||
<Divider type='vertical' className='h-3' />
|
|
||||||
{/* JSON Schema Importer */}
|
|
||||||
<JsonImporter
|
|
||||||
updateBtnWidth={updateBtnWidth}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='px-6 grow'>
|
|
||||||
{currentTab === SchemaView.VisualEditor && (
|
|
||||||
<VisualEditor
|
|
||||||
schema={jsonSchema}
|
|
||||||
onChange={handleUpdateSchema}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{currentTab === SchemaView.JsonSchema && (
|
|
||||||
<SchemaEditor
|
|
||||||
schema={json}
|
|
||||||
onUpdate={handleSchemaEditorUpdate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* Footer */}
|
|
||||||
<div className='flex items-center p-6 pt-5 gap-x-2'>
|
|
||||||
<a
|
|
||||||
className='flex items-center gap-x-1 grow text-text-accent'
|
|
||||||
href='https://json-schema.org/'
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
<span className='system-xs-regular'>{t('workflow.nodes.llm.jsonSchema.doc')}</span>
|
|
||||||
<RiExternalLinkLine className='w-3 h-3' />
|
|
||||||
</a>
|
|
||||||
<div className='flex items-center gap-x-3'>
|
|
||||||
<div className='flex items-center gap-x-2'>
|
|
||||||
<Button variant='secondary' onClick={handleResetDefaults}>
|
|
||||||
{t('workflow.nodes.llm.jsonSchema.resetDefaults')}
|
|
||||||
</Button>
|
|
||||||
<Divider type='vertical' className='h-4 ml-1 mr-0' />
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-x-2'>
|
|
||||||
<Button variant='secondary' onClick={handleCancel}>
|
|
||||||
{t('common.operation.cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button variant='primary' onClick={handleSave}>
|
|
||||||
{t('common.operation.save')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,572 @@
|
|||||||
|
import React, { type FC, useCallback, useState } from 'react'
|
||||||
|
import { ArrayType, type Field, 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'
|
||||||
|
import Divider from '@/app/components/base/divider'
|
||||||
|
import JsonImporter from './json-importer'
|
||||||
|
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'
|
||||||
|
|
||||||
|
type JsonSchemaConfigProps = {
|
||||||
|
defaultSchema?: SchemaRoot
|
||||||
|
onSave: (schema: SchemaRoot) => void
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SchemaView {
|
||||||
|
VisualEditor = 'visualEditor',
|
||||||
|
JsonSchema = 'jsonSchema',
|
||||||
|
}
|
||||||
|
|
||||||
|
const VIEW_TABS = [
|
||||||
|
{ Icon: RiTimelineView, text: 'Visual Editor', value: SchemaView.VisualEditor },
|
||||||
|
{ Icon: RiBracesLine, text: 'JSON Schema', value: SchemaView.JsonSchema },
|
||||||
|
]
|
||||||
|
|
||||||
|
const DEFAULT_SCHEMA: SchemaRoot = {
|
||||||
|
type: Type.object,
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
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<JsonSchemaConfigProps> = ({
|
||||||
|
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<string, Field>)
|
||||||
|
|
||||||
|
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<string, Field>)
|
||||||
|
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) => {
|
||||||
|
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',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
||||||
|
acc[key === oldName ? newName : key] = value
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, Field>)
|
||||||
|
|
||||||
|
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',
|
||||||
|
})
|
||||||
|
emit('restorePropertyName')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
||||||
|
acc[key === oldName ? newName : key] = value
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, Field>)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setJsonSchema(newSchema)
|
||||||
|
emit('fieldChangeSuccess')
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateBtnWidth = useCallback((width: number) => {
|
||||||
|
setBtnWidth(width + 32)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleApplySchema = useCallback(() => {}, [])
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {}, [])
|
||||||
|
|
||||||
|
const handleSchemaEditorUpdate = useCallback((schema: string) => {
|
||||||
|
setJson(schema)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleResetDefaults = useCallback(() => {
|
||||||
|
setJsonSchema(defaultSchema || DEFAULT_SCHEMA)
|
||||||
|
}, [defaultSchema])
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
onClose()
|
||||||
|
}, [onClose])
|
||||||
|
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
onSave(jsonSchema)
|
||||||
|
onClose()
|
||||||
|
}, [jsonSchema, onSave, onClose])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col h-full'>
|
||||||
|
{/* Header */}
|
||||||
|
<div className='relative flex p-6 pr-14 pb-3'>
|
||||||
|
<div className='text-text-primary title-2xl-semi-bold grow truncate'>
|
||||||
|
{t('workflow.nodes.llm.jsonSchema.title')}
|
||||||
|
</div>
|
||||||
|
<div className='absolute right-5 top-5 w-8 h-8 flex justify-center items-center p-1.5' onClick={onClose}>
|
||||||
|
<RiCloseLine className='w-[18px] h-[18px] text-text-tertiary' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Content */}
|
||||||
|
<div className='flex items-center justify-between px-6 py-2'>
|
||||||
|
{/* Tab */}
|
||||||
|
<SegmentedControl<SchemaView>
|
||||||
|
options={VIEW_TABS}
|
||||||
|
value={currentTab}
|
||||||
|
onChange={(value: SchemaView) => {
|
||||||
|
setCurrentTab(value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className='flex items-center gap-x-0.5'>
|
||||||
|
{/* JSON Schema Generator */}
|
||||||
|
<JsonSchemaGenerator
|
||||||
|
crossAxisOffset={btnWidth}
|
||||||
|
onApply={handleApplySchema}
|
||||||
|
/>
|
||||||
|
<Divider type='vertical' className='h-3' />
|
||||||
|
{/* JSON Schema Importer */}
|
||||||
|
<JsonImporter
|
||||||
|
updateBtnWidth={updateBtnWidth}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='px-6 grow overflow-hidden'>
|
||||||
|
{currentTab === SchemaView.VisualEditor && (
|
||||||
|
<VisualEditor schema={jsonSchema} />
|
||||||
|
)}
|
||||||
|
{currentTab === SchemaView.JsonSchema && (
|
||||||
|
<SchemaEditor
|
||||||
|
schema={json}
|
||||||
|
onUpdate={handleSchemaEditorUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Footer */}
|
||||||
|
<div className='flex items-center p-6 pt-5 gap-x-2'>
|
||||||
|
<a
|
||||||
|
className='flex items-center gap-x-1 grow text-text-accent'
|
||||||
|
href='https://json-schema.org/'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<span className='system-xs-regular'>{t('workflow.nodes.llm.jsonSchema.doc')}</span>
|
||||||
|
<RiExternalLinkLine className='w-3 h-3' />
|
||||||
|
</a>
|
||||||
|
<div className='flex items-center gap-x-3'>
|
||||||
|
<div className='flex items-center gap-x-2'>
|
||||||
|
<Button variant='secondary' onClick={handleResetDefaults}>
|
||||||
|
{t('workflow.nodes.llm.jsonSchema.resetDefaults')}
|
||||||
|
</Button>
|
||||||
|
<Divider type='vertical' className='h-4 ml-1 mr-0' />
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-x-2'>
|
||||||
|
<Button variant='secondary' onClick={handleCancel}>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button variant='primary' onClick={handleSave}>
|
||||||
|
{t('common.operation.save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JsonSchemaConfig
|
@ -0,0 +1,34 @@
|
|||||||
|
import { useContext } from 'react'
|
||||||
|
import { createStore, useStore } from 'zustand'
|
||||||
|
import type { SchemaRoot } from '../../types'
|
||||||
|
import { JsonSchemaConfigContext } from './context'
|
||||||
|
|
||||||
|
type JsonSchemaConfigStore = {
|
||||||
|
hoveringProperty: string | ''
|
||||||
|
setHoveringProperty: (propertyPath: string) => void
|
||||||
|
isAddingNewField: boolean
|
||||||
|
setIsAddingNewField: (isAdding: boolean) => void
|
||||||
|
advancedEditing: boolean
|
||||||
|
setAdvancedEditing: (isEditing: boolean) => void
|
||||||
|
backupSchema: SchemaRoot | null
|
||||||
|
setBackupSchema: (schema: SchemaRoot | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createJsonSchemaConfigStore = () => createStore<JsonSchemaConfigStore>(set => ({
|
||||||
|
hoveringProperty: '',
|
||||||
|
setHoveringProperty: (propertyPath: string) => set({ hoveringProperty: propertyPath }),
|
||||||
|
isAddingNewField: false,
|
||||||
|
setIsAddingNewField: (isAdding: boolean) => set({ isAddingNewField: isAdding }),
|
||||||
|
advancedEditing: false,
|
||||||
|
setAdvancedEditing: (isEditing: boolean) => set({ advancedEditing: isEditing }),
|
||||||
|
backupSchema: null,
|
||||||
|
setBackupSchema: (schema: SchemaRoot | null) => set({ backupSchema: schema }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const useJsonSchemaConfigStore = <T>(selector: (state: JsonSchemaConfigStore) => T): T => {
|
||||||
|
const store = useContext(JsonSchemaConfigContext)
|
||||||
|
if (!store)
|
||||||
|
throw new Error('Missing JsonSchemaConfigContext.Provider in the tree')
|
||||||
|
|
||||||
|
return useStore(store, selector)
|
||||||
|
}
|
@ -1,30 +1,32 @@
|
|||||||
import { type FC, useState } from 'react'
|
|
||||||
import type { Field } from '../../../types'
|
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import { RiAddCircleFill } from '@remixicon/react'
|
import { RiAddCircleFill } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useJsonSchemaConfigStore } from '../store'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useMittContext } from '../context'
|
||||||
|
|
||||||
type AddToRootProps = {
|
const AddField = () => {
|
||||||
addField: (path: string[], updates: Field) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddField: FC<AddToRootProps> = ({
|
|
||||||
addField,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const setIsAddingNewField = useJsonSchemaConfigStore(state => state.setIsAddingNewField)
|
||||||
|
const { emit } = useMittContext()
|
||||||
|
|
||||||
const handleAddField = () => {
|
const handleAddField = useCallback(() => {
|
||||||
setIsEditing(true)
|
setIsAddingNewField(true)
|
||||||
}
|
emit('addField', { path: [] })
|
||||||
|
}, [setIsAddingNewField, emit])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='pl-5 py-2'>
|
||||||
<Button size='small' className='flex items-center gap-x-[1px]' onClick={handleAddField}>
|
<Button
|
||||||
|
size='small'
|
||||||
|
variant='secondary-accent'
|
||||||
|
className='flex items-center gap-x-[1px]'
|
||||||
|
onClick={handleAddField}
|
||||||
|
>
|
||||||
<RiAddCircleFill className='w-3.5 h-3.5'/>
|
<RiAddCircleFill className='w-3.5 h-3.5'/>
|
||||||
<span className='px-[3px]'>{t('workflow.nodes.llm.jsonSchema.addField')}</span>
|
<span className='px-[3px]'>{t('workflow.nodes.llm.jsonSchema.addField')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,11 +3,13 @@ import Button from '@/app/components/base/button'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
type AdvancedActionsProps = {
|
type AdvancedActionsProps = {
|
||||||
|
isConfirmDisabled: boolean
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
onConfirm: () => void
|
onConfirm: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AdvancedActions: FC<AdvancedActionsProps> = ({
|
const AdvancedActions: FC<AdvancedActionsProps> = ({
|
||||||
|
isConfirmDisabled,
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}) => {
|
}) => {
|
||||||
@ -18,7 +20,12 @@ const AdvancedActions: FC<AdvancedActionsProps> = ({
|
|||||||
<Button size='small' variant='secondary' onClick={onCancel}>
|
<Button size='small' variant='secondary' onClick={onCancel}>
|
||||||
{t('common.operation.cancel')}
|
{t('common.operation.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size='small' variant='primary' onClick={onConfirm}>
|
<Button
|
||||||
|
disabled={isConfirmDisabled}
|
||||||
|
size='small'
|
||||||
|
variant='primary'
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
{t('common.operation.confirm')}
|
{t('common.operation.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,11 +29,6 @@ const AdvancedOptions: FC<AdvancedOptionsProps> = ({
|
|||||||
onChange({ enum: e.target.value })
|
onChange({ enum: e.target.value })
|
||||||
}, [onChange])
|
}, [onChange])
|
||||||
|
|
||||||
// const handleEnumChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
// const value = e.target.value
|
|
||||||
// onChange({ enum: value })
|
|
||||||
// }, [onChange])
|
|
||||||
|
|
||||||
const handleToggleAdvancedOptions = useCallback(() => {
|
const handleToggleAdvancedOptions = useCallback(() => {
|
||||||
setShowAdvancedOptions(prev => !prev)
|
setShowAdvancedOptions(prev => !prev)
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -9,8 +9,10 @@ import Actions from './actions'
|
|||||||
import AdvancedActions from './advanced-actions'
|
import AdvancedActions from './advanced-actions'
|
||||||
import AdvancedOptions, { type AdvancedOptionsType } from './advanced-options'
|
import AdvancedOptions, { type AdvancedOptionsType } from './advanced-options'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useUnmount } from 'ahooks'
|
|
||||||
import classNames from '@/utils/classnames'
|
import classNames from '@/utils/classnames'
|
||||||
|
import { useJsonSchemaConfigStore } from '../../store'
|
||||||
|
import { useMittContext } from '../../context'
|
||||||
|
import produce from 'immer'
|
||||||
|
|
||||||
export type EditData = {
|
export type EditData = {
|
||||||
name: string
|
name: string
|
||||||
@ -20,15 +22,16 @@ export type EditData = {
|
|||||||
enum?: SchemaEnumType
|
enum?: SchemaEnumType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
description: string
|
||||||
|
enum?: SchemaEnumType
|
||||||
|
}
|
||||||
|
|
||||||
type EditCardProps = {
|
type EditCardProps = {
|
||||||
fields: EditData
|
fields: EditData
|
||||||
onPropertyNameChange: (name: string) => void
|
depth: number
|
||||||
onTypeChange: (type: Type | ArrayType) => void
|
path: string[]
|
||||||
onRequiredChange: (name: string) => void
|
parentPath: string[]
|
||||||
onDescriptionChange: (description: string) => void
|
|
||||||
onAdvancedOptionsChange: (options: AdvancedOptionsType) => void
|
|
||||||
onDelete: (name: string) => void
|
|
||||||
onCancel: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TYPE_OPTIONS = [
|
const TYPE_OPTIONS = [
|
||||||
@ -42,93 +45,165 @@ const TYPE_OPTIONS = [
|
|||||||
{ value: ArrayType.object, text: 'array[object]' },
|
{ value: ArrayType.object, text: 'array[object]' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const DEPTH_LIMIT = 10
|
||||||
|
|
||||||
const EditCard: FC<EditCardProps> = ({
|
const EditCard: FC<EditCardProps> = ({
|
||||||
fields,
|
fields,
|
||||||
onPropertyNameChange,
|
depth,
|
||||||
onTypeChange,
|
path,
|
||||||
onRequiredChange,
|
parentPath,
|
||||||
onDescriptionChange,
|
|
||||||
onAdvancedOptionsChange,
|
|
||||||
onDelete,
|
|
||||||
onCancel,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [propertyName, setPropertyName] = useState(fields.name)
|
const [currentFields, setCurrentFields] = useState(fields)
|
||||||
const [description, setDescription] = useState(fields.description)
|
const [backupFields, setBackupFields] = useState<EditData | null>(null)
|
||||||
const [AdvancedEditing, setAdvancedEditing] = useState(!fields)
|
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 { emit, useSubscribe } = useMittContext()
|
||||||
|
|
||||||
|
const disableAddBtn = fields.type !== Type.object && fields.type !== ArrayType.object && depth < DEPTH_LIMIT
|
||||||
|
const hasAdvancedOptions = fields.type === Type.string || fields.type === Type.number
|
||||||
|
const isAdvancedEditing = advancedEditing || isAddingNewField
|
||||||
|
|
||||||
|
const advancedOptions = useMemo(() => {
|
||||||
|
return { enum: (currentFields.enum || []).join(', ') }
|
||||||
|
}, [currentFields.enum])
|
||||||
|
|
||||||
|
useSubscribe('restorePropertyName', () => {
|
||||||
|
setCurrentFields(prev => ({ ...prev, name: fields.name }))
|
||||||
|
})
|
||||||
|
|
||||||
|
useSubscribe('fieldChangeSuccess', () => {
|
||||||
|
if (isAddingNewField) {
|
||||||
|
setIsAddingNewField(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setAdvancedEditing(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
const emitPropertyNameChange = useCallback((name: string) => {
|
||||||
|
const newFields = produce(fields, (draft) => {
|
||||||
|
draft.name = name
|
||||||
|
})
|
||||||
|
emit('propertyNameChange', { path, parentPath, oldFields: fields, fields: newFields })
|
||||||
|
}, [fields, path, parentPath, emit])
|
||||||
|
|
||||||
|
const emitPropertyTypeChange = useCallback((type: Type | ArrayType) => {
|
||||||
|
const newFields = produce(fields, (draft) => {
|
||||||
|
draft.type = type
|
||||||
|
})
|
||||||
|
emit('propertyTypeChange', { path, parentPath, oldFields: fields, fields: newFields })
|
||||||
|
}, [fields, path, parentPath, emit])
|
||||||
|
|
||||||
|
const emitPropertyRequiredToggle = useCallback(() => {
|
||||||
|
emit('propertyRequiredToggle', { path, parentPath, oldFields: fields, fields: currentFields })
|
||||||
|
}, [emit, path, parentPath, fields, currentFields])
|
||||||
|
|
||||||
|
const emitPropertyOptionsChange = useCallback((options: Options) => {
|
||||||
|
emit('propertyOptionsChange', { path, parentPath, oldFields: fields, fields: { ...currentFields, ...options } })
|
||||||
|
}, [emit, path, parentPath, fields, currentFields])
|
||||||
|
|
||||||
|
const emitPropertyDelete = useCallback(() => {
|
||||||
|
emit('propertyDelete', { path, parentPath, oldFields: fields, fields: currentFields })
|
||||||
|
}, [emit, path, parentPath, fields, currentFields])
|
||||||
|
|
||||||
|
const emitPropertyAdd = useCallback(() => {
|
||||||
|
emit('addField', { path })
|
||||||
|
}, [emit, path])
|
||||||
|
|
||||||
|
const emitFieldChange = useCallback(() => {
|
||||||
|
emit('fieldChange', { path, parentPath, oldFields: fields, fields: currentFields })
|
||||||
|
}, [emit, path, parentPath, fields, currentFields])
|
||||||
|
|
||||||
const handlePropertyNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handlePropertyNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setPropertyName(e.target.value)
|
setCurrentFields(prev => ({ ...prev, name: e.target.value }))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handlePropertyNameBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
|
const handlePropertyNameBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
onPropertyNameChange(e.target.value)
|
if (isAdvancedEditing) return
|
||||||
}, [onPropertyNameChange])
|
emitPropertyNameChange(e.target.value)
|
||||||
|
}, [isAdvancedEditing, emitPropertyNameChange])
|
||||||
|
|
||||||
const handleTypeChange = useCallback((item: TypeItem) => {
|
const handleTypeChange = useCallback((item: TypeItem) => {
|
||||||
onTypeChange(item.value)
|
setCurrentFields(prev => ({ ...prev, type: item.value }))
|
||||||
}, [onTypeChange])
|
if (isAdvancedEditing) return
|
||||||
|
emitPropertyTypeChange(item.value)
|
||||||
|
}, [isAdvancedEditing, emitPropertyTypeChange])
|
||||||
|
|
||||||
const toggleRequired = useCallback(() => {
|
const toggleRequired = useCallback(() => {
|
||||||
onRequiredChange(propertyName)
|
setCurrentFields(prev => ({ ...prev, required: !prev.required }))
|
||||||
}, [onRequiredChange, propertyName])
|
if (isAdvancedEditing) return
|
||||||
|
emitPropertyRequiredToggle()
|
||||||
|
}, [isAdvancedEditing, emitPropertyRequiredToggle])
|
||||||
|
|
||||||
const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setDescription(e.target.value)
|
setCurrentFields(prev => ({ ...prev, description: e.target.value }))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDescriptionBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
|
const handleDescriptionBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
onDescriptionChange(e.target.value)
|
if (isAdvancedEditing) return
|
||||||
}, [onDescriptionChange])
|
emitPropertyOptionsChange({ description: e.target.value, enum: fields.enum })
|
||||||
|
}, [isAdvancedEditing, emitPropertyOptionsChange, fields])
|
||||||
const advancedOptions = useMemo(() => {
|
|
||||||
return { enum: (fields.enum || []).join(', ') }
|
|
||||||
}, [fields.enum])
|
|
||||||
|
|
||||||
const handleAdvancedOptionsChange = useCallback((options: AdvancedOptionsType) => {
|
const handleAdvancedOptionsChange = useCallback((options: AdvancedOptionsType) => {
|
||||||
onAdvancedOptionsChange(options)
|
if (isAdvancedEditing) return
|
||||||
}, [onAdvancedOptionsChange])
|
const enumValue = options.enum.replace(' ', '').split(',')
|
||||||
|
emitPropertyOptionsChange({ description: fields.description, enum: enumValue })
|
||||||
const handleConfirm = useCallback(() => {
|
}, [isAdvancedEditing, emitPropertyOptionsChange, fields])
|
||||||
setAdvancedEditing(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
onDelete(propertyName)
|
emitPropertyDelete()
|
||||||
}, [onDelete, propertyName])
|
}, [emitPropertyDelete])
|
||||||
|
|
||||||
const handleEdit = useCallback(() => {
|
const handleAdvancedEdit = useCallback(() => {
|
||||||
|
setBackupFields({ ...currentFields })
|
||||||
setAdvancedEditing(true)
|
setAdvancedEditing(true)
|
||||||
}, [])
|
}, [currentFields, setAdvancedEditing])
|
||||||
|
|
||||||
useUnmount(() => {
|
const handleAddChildField = useCallback(() => {
|
||||||
onPropertyNameChange(propertyName)
|
emitPropertyAdd()
|
||||||
})
|
}, [emitPropertyAdd])
|
||||||
|
|
||||||
const disableAddBtn = fields.type !== Type.object && fields.type !== ArrayType.object
|
const handleConfirm = useCallback(() => {
|
||||||
const hasAdvancedOptions = fields.type === Type.string || fields.type === Type.number
|
emitFieldChange()
|
||||||
|
}, [emitFieldChange])
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
if (isAddingNewField) {
|
||||||
|
emit('restoreSchema')
|
||||||
|
setIsAddingNewField(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (backupFields) {
|
||||||
|
setCurrentFields(backupFields)
|
||||||
|
setBackupFields(null)
|
||||||
|
}
|
||||||
|
setAdvancedEditing(false)
|
||||||
|
}, [isAddingNewField, emit, setIsAddingNewField, setAdvancedEditing, backupFields])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col py-0.5 rounded-lg bg-components-panel-bg shadow-sm shadow-shadow-shadow-4'>
|
<div className='flex flex-col py-0.5 rounded-lg bg-components-panel-bg shadow-sm shadow-shadow-shadow-4'>
|
||||||
<div className='flex items-center pl-1 pr-0.5'>
|
<div className='flex items-center pl-1 pr-0.5'>
|
||||||
<div className='flex items-center gap-x-1 grow'>
|
<div className='flex items-center gap-x-1 grow'>
|
||||||
<input
|
<input
|
||||||
value={propertyName}
|
value={currentFields.name}
|
||||||
className='max-w-20 h-5 rounded-[5px] px-1 py-0.5 text-text-primary system-sm-semibold placeholder:text-text-placeholder
|
className='max-w-20 h-5 rounded-[5px] px-1 py-0.5 text-text-primary system-sm-semibold placeholder:text-text-placeholder
|
||||||
placeholder:system-sm-semibold hover:bg-state-base-hover border border-transparent focus:border-components-input-border-active
|
placeholder:system-sm-semibold hover:bg-state-base-hover border border-transparent focus:border-components-input-border-active
|
||||||
focus:bg-components-input-bg-active focus:shadow-xs shadow-shadow-shadow-3 caret-[#295EFF] outline-none'
|
focus:bg-components-input-bg-active focus:shadow-xs shadow-shadow-shadow-3 caret-[#295EFF] outline-none'
|
||||||
placeholder={t('workflow.nodes.llm.jsonSchema.fieldNamePlaceholder')}
|
placeholder={t('workflow.nodes.llm.jsonSchema.fieldNamePlaceholder')}
|
||||||
onChange={handlePropertyNameChange}
|
onChange={handlePropertyNameChange}
|
||||||
onBlur={handlePropertyNameBlur}
|
onBlur={handlePropertyNameBlur}
|
||||||
|
onKeyUp={e => e.key === 'Enter' && e.currentTarget.blur()}
|
||||||
/>
|
/>
|
||||||
<TypeSelector
|
<TypeSelector
|
||||||
currentValue={fields.type}
|
currentValue={currentFields.type}
|
||||||
items={TYPE_OPTIONS}
|
items={TYPE_OPTIONS}
|
||||||
onSelect={handleTypeChange}
|
onSelect={handleTypeChange}
|
||||||
popupClassName={'z-[1000]'}
|
popupClassName={'z-[1000]'}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
fields.required && (
|
currentFields.required && (
|
||||||
<div className='px-1 py-0.5 text-text-warning system-2xs-medium-uppercase'>
|
<div className='px-1 py-0.5 text-text-warning system-2xs-medium-uppercase'>
|
||||||
{t('workflow.nodes.llm.jsonSchema.required')}
|
{t('workflow.nodes.llm.jsonSchema.required')}
|
||||||
</div>
|
</div>
|
||||||
@ -136,37 +211,40 @@ const EditCard: FC<EditCardProps> = ({
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<RequiredSwitch
|
<RequiredSwitch
|
||||||
defaultValue={fields.required}
|
defaultValue={currentFields.required}
|
||||||
toggleRequired={toggleRequired}
|
toggleRequired={toggleRequired}
|
||||||
/>
|
/>
|
||||||
<Divider type='vertical' className='h-3' />
|
<Divider type='vertical' className='h-3' />
|
||||||
{AdvancedEditing ? (
|
{isAdvancedEditing ? (
|
||||||
<AdvancedActions
|
<AdvancedActions
|
||||||
onCancel={() => { }}
|
isConfirmDisabled={currentFields.name === ''}
|
||||||
|
onCancel={handleCancel}
|
||||||
onConfirm={handleConfirm}
|
onConfirm={handleConfirm}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Actions
|
<Actions
|
||||||
disableAddBtn={disableAddBtn}
|
disableAddBtn={disableAddBtn}
|
||||||
onAddChildField={() => { }}
|
onAddChildField={handleAddChildField}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onEdit={handleEdit}/>
|
onEdit={handleAdvancedEdit}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(description || AdvancedEditing) && (
|
{(currentFields.description || isAdvancedEditing) && (
|
||||||
<div className={classNames(AdvancedEditing ? 'p-2 pt-1' : 'px-2 pb-1')}>
|
<div className={classNames(isAdvancedEditing ? 'p-2 pt-1' : 'px-2 pb-1')}>
|
||||||
<input
|
<input
|
||||||
value={description}
|
value={currentFields.description}
|
||||||
className='w-full h-4 p-0 text-text-tertiary system-xs-regular placeholder:text-text-placeholder placeholder:system-xs-regular caret-[#295EFF] outline-none'
|
className='w-full h-4 p-0 text-text-tertiary system-xs-regular placeholder:text-text-placeholder placeholder:system-xs-regular caret-[#295EFF] outline-none'
|
||||||
placeholder={t('workflow.nodes.llm.jsonSchema.descriptionPlaceholder')}
|
placeholder={t('workflow.nodes.llm.jsonSchema.descriptionPlaceholder')}
|
||||||
onChange={handleDescriptionChange}
|
onChange={handleDescriptionChange}
|
||||||
onBlur={handleDescriptionBlur}
|
onBlur={handleDescriptionBlur}
|
||||||
|
onKeyUp={e => e.key === 'Enter' && e.currentTarget.blur()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{AdvancedEditing && hasAdvancedOptions && (
|
{isAdvancedEditing && hasAdvancedOptions && (
|
||||||
<AdvancedOptions
|
<AdvancedOptions
|
||||||
options={advancedOptions}
|
options={advancedOptions}
|
||||||
onChange={handleAdvancedOptionsChange}
|
onChange={handleAdvancedOptionsChange}
|
||||||
|
@ -4,20 +4,18 @@ import SchemaNode from './schema-node'
|
|||||||
|
|
||||||
type VisualEditorProps = {
|
type VisualEditorProps = {
|
||||||
schema: Field
|
schema: Field
|
||||||
onChange: (schema: Field) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const VisualEditor: FC<VisualEditorProps> = ({
|
const VisualEditor: FC<VisualEditorProps> = ({
|
||||||
schema,
|
schema,
|
||||||
onChange,
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className='h-full rounded-xl p-1 pl-2 bg-background-section-burn'>
|
<div className='h-full rounded-xl p-1 pl-2 bg-background-section-burn overflow-auto'>
|
||||||
<SchemaNode
|
<SchemaNode
|
||||||
name='structured_output'
|
name='structured_output'
|
||||||
schema={schema}
|
schema={schema}
|
||||||
required={false}
|
required={false}
|
||||||
onChange={onChange}
|
path={[]}
|
||||||
depth={0}
|
depth={0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,25 +1,23 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React, { useCallback, useRef, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { ArrayType } from '../../../types'
|
import { type Field, Type } from '../../../types'
|
||||||
import { type ArrayItems, type Field, Type } from '../../../types'
|
|
||||||
import type { AdvancedOptionsType } from './edit-card/advanced-options'
|
|
||||||
import classNames from '@/utils/classnames'
|
import classNames from '@/utils/classnames'
|
||||||
import { RiArrowDropDownLine, RiArrowDropRightLine } from '@remixicon/react'
|
import { RiArrowDropDownLine, RiArrowDropRightLine } from '@remixicon/react'
|
||||||
import { getFieldType, getHasChildren } from '../../../utils'
|
import { getFieldType, getHasChildren } from '../../../utils'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import EditCard from './edit-card'
|
import EditCard from './edit-card'
|
||||||
import Card from './card'
|
import Card from './card'
|
||||||
import produce from 'immer'
|
import { useJsonSchemaConfigStore } from '../store'
|
||||||
|
import { useDebounceFn } from 'ahooks'
|
||||||
|
import AddField from './add-field'
|
||||||
|
|
||||||
type SchemaNodeProps = {
|
type SchemaNodeProps = {
|
||||||
name: string
|
name: string
|
||||||
required: boolean
|
required: boolean
|
||||||
schema: Field
|
schema: Field
|
||||||
|
path: string[]
|
||||||
|
parentPath?: string[]
|
||||||
depth: number
|
depth: number
|
||||||
onChange: (schema: Field) => void
|
|
||||||
onPropertyNameChange?: (name: string) => void
|
|
||||||
onRequiredChange?: (name: string) => void
|
|
||||||
onNodeDelete?: (name: string) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Support 10 levels of indentation
|
// Support 10 levels of indentation
|
||||||
@ -52,222 +50,38 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
name,
|
name,
|
||||||
required,
|
required,
|
||||||
schema,
|
schema,
|
||||||
onChange,
|
path,
|
||||||
onPropertyNameChange,
|
parentPath,
|
||||||
onRequiredChange,
|
|
||||||
onNodeDelete,
|
|
||||||
depth,
|
depth,
|
||||||
}) => {
|
}) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(true)
|
const [isExpanded, setIsExpanded] = useState(true)
|
||||||
const [isHovering, setIsHovering] = useState(false)
|
const hoveringProperty = useJsonSchemaConfigStore(state => state.hoveringProperty)
|
||||||
const hoverTimer = useRef<any>(null)
|
const setHoveringProperty = useJsonSchemaConfigStore(state => state.setHoveringProperty)
|
||||||
|
const isAddingNewField = useJsonSchemaConfigStore(state => state.isAddingNewField)
|
||||||
|
const advancedEditing = useJsonSchemaConfigStore(state => state.advancedEditing)
|
||||||
|
|
||||||
|
const { run: setHoveringPropertyDebounced } = useDebounceFn((path: string) => {
|
||||||
|
setHoveringProperty(path)
|
||||||
|
}, { wait: 50 })
|
||||||
|
|
||||||
const hasChildren = getHasChildren(schema)
|
const hasChildren = getHasChildren(schema)
|
||||||
const isEditing = isHovering && depth > 0
|
|
||||||
const type = getFieldType(schema)
|
const type = getFieldType(schema)
|
||||||
|
const isHovering = hoveringProperty === path.join('.') && depth > 0
|
||||||
|
|
||||||
const handleExpand = () => {
|
const handleExpand = () => {
|
||||||
setIsExpanded(!isExpanded)
|
setIsExpanded(!isExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
hoverTimer.current = setTimeout(() => {
|
if (advancedEditing || isAddingNewField) return
|
||||||
setIsHovering(true)
|
setHoveringPropertyDebounced(path.join('.'))
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
clearTimeout(hoverTimer.current)
|
if (advancedEditing || isAddingNewField) return
|
||||||
setIsHovering(false)
|
setHoveringPropertyDebounced('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePropertyNameChange = useCallback((oldName: string, newName: string) => {
|
|
||||||
if (oldName === newName) return
|
|
||||||
|
|
||||||
if (schema.type === Type.object) {
|
|
||||||
const properties = schema.properties || {}
|
|
||||||
if (properties[newName]) {
|
|
||||||
// TODO: Show error message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
|
||||||
acc[key === oldName ? newName : key] = value
|
|
||||||
return acc
|
|
||||||
}, {} as Record<string, Field>)
|
|
||||||
|
|
||||||
const required = schema.required || []
|
|
||||||
const newRequired = produce(required, (draft) => {
|
|
||||||
const index = draft.indexOf(oldName)
|
|
||||||
if (index !== -1)
|
|
||||||
draft.splice(index, 1, newName)
|
|
||||||
})
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
...schema,
|
|
||||||
properties: newProperties,
|
|
||||||
required: newRequired,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema.type === Type.array && schema.items && schema.items.type === Type.object) {
|
|
||||||
const properties = schema.items.properties || {}
|
|
||||||
if (properties[newName]) {
|
|
||||||
// TODO: Show error message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
|
|
||||||
acc[key === oldName ? newName : key] = value
|
|
||||||
return acc
|
|
||||||
}, {} as Record<string, Field>)
|
|
||||||
const required = schema.items.required || []
|
|
||||||
const newRequired = produce(required, (draft) => {
|
|
||||||
const index = draft.indexOf(oldName)
|
|
||||||
if (index !== -1)
|
|
||||||
draft.splice(index, 1, newName)
|
|
||||||
})
|
|
||||||
onChange({
|
|
||||||
...schema,
|
|
||||||
items: {
|
|
||||||
...schema.items,
|
|
||||||
properties: newProperties,
|
|
||||||
required: newRequired,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [onChange, schema])
|
|
||||||
|
|
||||||
const handleTypeChange = useCallback((newType: Type | ArrayType) => {
|
|
||||||
if (schema.type === newType) return
|
|
||||||
const newSchema = produce(schema, (draft) => {
|
|
||||||
if (draft.type === Type.object) {
|
|
||||||
delete draft.properties
|
|
||||||
delete draft.required
|
|
||||||
}
|
|
||||||
if (draft.type === Type.array)
|
|
||||||
delete draft.items
|
|
||||||
switch (newType) {
|
|
||||||
case Type.object:
|
|
||||||
draft.type = Type.object
|
|
||||||
draft.properties = {}
|
|
||||||
draft.required = []
|
|
||||||
break
|
|
||||||
case ArrayType.string:
|
|
||||||
draft.type = Type.array
|
|
||||||
draft.items = {
|
|
||||||
type: Type.string,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.number:
|
|
||||||
draft.type = Type.array
|
|
||||||
draft.items = {
|
|
||||||
type: Type.number,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.boolean:
|
|
||||||
draft.type = Type.array
|
|
||||||
draft.items = {
|
|
||||||
type: Type.boolean,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ArrayType.object:
|
|
||||||
draft.type = Type.array
|
|
||||||
draft.items = {
|
|
||||||
type: Type.object,
|
|
||||||
properties: {},
|
|
||||||
required: [],
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
draft.type = newType as Type
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onChange(newSchema)
|
|
||||||
}, [onChange, schema])
|
|
||||||
|
|
||||||
const toggleRequired = useCallback((name: string) => {
|
|
||||||
if (schema.type === Type.object) {
|
|
||||||
const required = schema.required || []
|
|
||||||
const newRequired = required.includes(name)
|
|
||||||
? required.filter(item => item !== name)
|
|
||||||
: [...required, name]
|
|
||||||
onChange({
|
|
||||||
...schema,
|
|
||||||
required: newRequired,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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]
|
|
||||||
onChange({
|
|
||||||
...schema,
|
|
||||||
items: {
|
|
||||||
...schema.items,
|
|
||||||
required: newRequired,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [onChange, schema])
|
|
||||||
|
|
||||||
const handleDescriptionChange = useCallback((description: string) => {
|
|
||||||
onChange({
|
|
||||||
...schema,
|
|
||||||
description,
|
|
||||||
})
|
|
||||||
}, [onChange, schema])
|
|
||||||
|
|
||||||
const handleAdvancedOptionsChange = useCallback((advancedOptions: AdvancedOptionsType) => {
|
|
||||||
const newAdvancedOptions = {
|
|
||||||
enum: advancedOptions.enum.replace(' ', '').split(','),
|
|
||||||
}
|
|
||||||
onChange({
|
|
||||||
...schema,
|
|
||||||
...newAdvancedOptions,
|
|
||||||
})
|
|
||||||
}, [onChange, schema])
|
|
||||||
|
|
||||||
const handleNodeDelete = useCallback((name: string) => {
|
|
||||||
const newSchema = produce(schema, (draft) => {
|
|
||||||
if (draft.type === Type.object && draft.properties) {
|
|
||||||
delete draft.properties[name]
|
|
||||||
draft.required = draft.required?.filter(item => item !== name)
|
|
||||||
}
|
|
||||||
if (draft.type === Type.array && draft.items?.properties && draft.items?.type === Type.object) {
|
|
||||||
delete draft.items.properties[name]
|
|
||||||
draft.items.required = draft.items.required?.filter(item => item !== name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onChange(newSchema)
|
|
||||||
}, [onChange, schema])
|
|
||||||
|
|
||||||
const handlePropertyChange = useCallback((name: string, propertySchema: Field) => {
|
|
||||||
onChange({
|
|
||||||
...schema,
|
|
||||||
properties: {
|
|
||||||
...(schema.properties || {}),
|
|
||||||
[name]: propertySchema,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [onChange, schema])
|
|
||||||
|
|
||||||
const handleItemsPropertyChange = useCallback((name: string, itemsSchema: Field) => {
|
|
||||||
onChange({
|
|
||||||
...schema,
|
|
||||||
items: {
|
|
||||||
...schema.items,
|
|
||||||
properties: {
|
|
||||||
...(schema.items?.properties || {}),
|
|
||||||
[name]: itemsSchema as ArrayItems,
|
|
||||||
},
|
|
||||||
} as ArrayItems,
|
|
||||||
})
|
|
||||||
}, [onChange, schema])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<div className={classNames('relative z-10', indentPadding[depth])}>
|
<div className={classNames('relative z-10', indentPadding[depth])}>
|
||||||
@ -293,7 +107,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
{isEditing ? (
|
{isHovering ? (
|
||||||
<EditCard
|
<EditCard
|
||||||
fields={{
|
fields={{
|
||||||
name,
|
name,
|
||||||
@ -302,13 +116,9 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
description: schema.description || '',
|
description: schema.description || '',
|
||||||
enum: schema.enum || [],
|
enum: schema.enum || [],
|
||||||
}}
|
}}
|
||||||
onPropertyNameChange={onPropertyNameChange!}
|
path={path}
|
||||||
onTypeChange={handleTypeChange}
|
parentPath={parentPath!}
|
||||||
onRequiredChange={onRequiredChange!}
|
depth={depth}
|
||||||
onDescriptionChange={handleDescriptionChange}
|
|
||||||
onAdvancedOptionsChange={handleAdvancedOptionsChange}
|
|
||||||
onDelete={onNodeDelete!}
|
|
||||||
onCancel={() => {}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Card
|
<Card
|
||||||
@ -319,11 +129,11 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={classNames(
|
<div className={classNames(
|
||||||
'flex justify-center w-5 h-[calc(100%-1.75rem)] absolute top-7 z-0',
|
'flex justify-center w-5 absolute top-7 z-0',
|
||||||
|
schema.description ? 'h-[calc(100%-3rem)]' : 'h-[calc(100%-1.75rem)]',
|
||||||
indentLeft[depth + 1],
|
indentLeft[depth + 1],
|
||||||
)}>
|
)}>
|
||||||
<Divider type='vertical' className='bg-divider-subtle mx-0' />
|
<Divider type='vertical' className='bg-divider-subtle mx-0' />
|
||||||
@ -338,10 +148,8 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
name={key}
|
name={key}
|
||||||
required={!!schema.required?.includes(key)}
|
required={!!schema.required?.includes(key)}
|
||||||
schema={childSchema}
|
schema={childSchema}
|
||||||
onChange={handlePropertyChange.bind(null, key)}
|
path={[...path, 'properties', key]}
|
||||||
onPropertyNameChange={handlePropertyNameChange.bind(null, key)}
|
parentPath={path}
|
||||||
onRequiredChange={toggleRequired}
|
|
||||||
onNodeDelete={handleNodeDelete}
|
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@ -358,16 +166,20 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
name={key}
|
name={key}
|
||||||
required={!!schema.items?.required?.includes(key)}
|
required={!!schema.items?.required?.includes(key)}
|
||||||
schema={childSchema}
|
schema={childSchema}
|
||||||
onChange={handleItemsPropertyChange.bind(null, key)}
|
path={[...path, 'items', 'properties', key]}
|
||||||
onPropertyNameChange={handlePropertyNameChange.bind(null, key)}
|
parentPath={path}
|
||||||
onRequiredChange={toggleRequired}
|
|
||||||
onNodeDelete={handleNodeDelete}
|
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{
|
||||||
|
depth === 0 && !isAddingNewField && (
|
||||||
|
<AddField />
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { type Field, Type } from '../components/workflow/nodes/llm/types'
|
import { type SchemaRoot, Type } from '../components/workflow/nodes/llm/types'
|
||||||
import JsonSchemaConfigModal from '../components/workflow/nodes/llm/components/json-schema-config-modal'
|
import JsonSchemaConfigModal from '../components/workflow/nodes/llm/components/json-schema-config-modal'
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [show, setShow] = useState(false)
|
const [show, setShow] = useState(false)
|
||||||
const [schema, setSchema] = useState<Field>({
|
const [schema, setSchema] = useState<SchemaRoot>({
|
||||||
type: Type.object,
|
type: Type.object,
|
||||||
properties: {
|
properties: {
|
||||||
userId: {
|
userId: {
|
||||||
@ -19,9 +19,6 @@ export default function Page() {
|
|||||||
title: {
|
title: {
|
||||||
type: Type.string,
|
type: Type.string,
|
||||||
},
|
},
|
||||||
completed: {
|
|
||||||
type: Type.boolean,
|
|
||||||
},
|
|
||||||
locations: {
|
locations: {
|
||||||
type: Type.array,
|
type: Type.array,
|
||||||
items: {
|
items: {
|
||||||
@ -51,6 +48,9 @@ export default function Page() {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
completed: {
|
||||||
|
type: Type.boolean,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [
|
required: [
|
||||||
'userId',
|
'userId',
|
||||||
@ -62,14 +62,16 @@ export default function Page() {
|
|||||||
|
|
||||||
return <div className='flex flex-col p-20 h-full w-full overflow-hidden'>
|
return <div className='flex flex-col p-20 h-full w-full overflow-hidden'>
|
||||||
<button onClick={() => setShow(true)} className='shrink-0'>Open Json Schema Config</button>
|
<button onClick={() => setShow(true)} className='shrink-0'>Open Json Schema Config</button>
|
||||||
{show && <JsonSchemaConfigModal
|
{show && (
|
||||||
|
<JsonSchemaConfigModal
|
||||||
isShow={show}
|
isShow={show}
|
||||||
defaultSchema={schema}
|
defaultSchema={schema}
|
||||||
onSave={(schema) => {
|
onSave={(schema) => {
|
||||||
setSchema(schema)
|
setSchema(schema)
|
||||||
}}
|
}}
|
||||||
onClose={() => setShow(false)}
|
onClose={() => setShow(false)}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
<pre className='bg-gray-50 p-4 rounded-lg overflow-auto grow'>
|
<pre className='bg-gray-50 p-4 rounded-lg overflow-auto grow'>
|
||||||
{JSON.stringify(schema, null, 2)}
|
{JSON.stringify(schema, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
|
@ -10,7 +10,7 @@ const merge = <T extends Record<string, any>>(
|
|||||||
|
|
||||||
export type _Events = Record<EventType, unknown>
|
export type _Events = Record<EventType, unknown>
|
||||||
|
|
||||||
export type UseSubcribeOption = {
|
export type UseSubscribeOption = {
|
||||||
/**
|
/**
|
||||||
* Whether the subscription is enabled.
|
* Whether the subscription is enabled.
|
||||||
* @default true
|
* @default true
|
||||||
@ -22,21 +22,21 @@ export type ExtendedOn<Events extends _Events> = {
|
|||||||
<Key extends keyof Events>(
|
<Key extends keyof Events>(
|
||||||
type: Key,
|
type: Key,
|
||||||
handler: Handler<Events[Key]>,
|
handler: Handler<Events[Key]>,
|
||||||
options?: UseSubcribeOption,
|
options?: UseSubscribeOption,
|
||||||
): void;
|
): void;
|
||||||
(
|
(
|
||||||
type: '*',
|
type: '*',
|
||||||
handler: WildcardHandler<Events>,
|
handler: WildcardHandler<Events>,
|
||||||
option?: UseSubcribeOption,
|
option?: UseSubscribeOption,
|
||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UseMittReturn<Events extends _Events> = {
|
export type UseMittReturn<Events extends _Events> = {
|
||||||
useSubcribe: ExtendedOn<Events>;
|
useSubscribe: ExtendedOn<Events>;
|
||||||
emit: Emitter<Events>['emit'];
|
emit: Emitter<Events>['emit'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSubcribeOption: UseSubcribeOption = {
|
const defaultSubscribeOption: UseSubscribeOption = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,12 +52,12 @@ function useMitt<Events extends _Events>(
|
|||||||
emitterRef.current = mitt
|
emitterRef.current = mitt
|
||||||
}
|
}
|
||||||
const emitter = emitterRef.current
|
const emitter = emitterRef.current
|
||||||
const useSubcribe: ExtendedOn<Events> = (
|
const useSubscribe: ExtendedOn<Events> = (
|
||||||
type: string,
|
type: string,
|
||||||
handler: any,
|
handler: any,
|
||||||
option?: UseSubcribeOption,
|
option?: UseSubscribeOption,
|
||||||
) => {
|
) => {
|
||||||
const { enabled } = merge(defaultSubcribeOption, option)
|
const { enabled } = merge(defaultSubscribeOption, option)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
emitter.on(type, handler)
|
emitter.on(type, handler)
|
||||||
@ -67,7 +67,7 @@ function useMitt<Events extends _Events>(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
emit: emitter.emit,
|
emit: emitter.emit,
|
||||||
useSubcribe,
|
useSubscribe,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user