feat: implement JSON Schema configuration context and store for advanced editing options

This commit is contained in:
twwu 2025-03-17 16:42:05 +08:00
parent 183edf0fd5
commit d4185c2d91
12 changed files with 896 additions and 474 deletions

View File

@ -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)
}

View File

@ -1,164 +1,37 @@
import React, { type FC, useCallback, useState } from 'react'
import React, { type FC } from 'react'
import Modal from '../../../../../base/modal'
import { type Field, 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 type { SchemaRoot } from '../../types'
import JsonSchemaConfig from './json-schema-config'
import { JsonSchemaConfigContextProvider, MittProvider } from './context'
type JsonSchemaConfigModalProps = {
isShow: boolean
defaultSchema?: Field
onSave: (schema: Field) => void
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: Field = {
type: Type.object,
properties: {},
required: [],
additionalProperties: false,
}
const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
isShow,
defaultSchema,
onSave,
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 (
<Modal
isShow={isShow}
onClose={onClose}
className='max-w-[960px] h-[800px] p-0'
>
<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)
}}
<MittProvider>
<JsonSchemaConfigContextProvider>
<JsonSchemaConfig
defaultSchema={defaultSchema}
onSave={onSave}
onClose={onClose}
/>
<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'>
{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>
</JsonSchemaConfigContextProvider>
</MittProvider >
</Modal>
)
}

View File

@ -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

View File

@ -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)
}

View File

@ -1,30 +1,32 @@
import { type FC, useState } from 'react'
import type { Field } from '../../../types'
import Button from '@/app/components/base/button'
import { RiAddCircleFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useJsonSchemaConfigStore } from '../store'
import { useCallback } from 'react'
import { useMittContext } from '../context'
type AddToRootProps = {
addField: (path: string[], updates: Field) => void
}
const AddField: FC<AddToRootProps> = ({
addField,
}) => {
const AddField = () => {
const { t } = useTranslation()
const [isEditing, setIsEditing] = useState(false)
const setIsAddingNewField = useJsonSchemaConfigStore(state => state.setIsAddingNewField)
const { emit } = useMittContext()
const handleAddField = () => {
setIsEditing(true)
}
const handleAddField = useCallback(() => {
setIsAddingNewField(true)
emit('addField', { path: [] })
}, [setIsAddingNewField, emit])
return (
<>
<Button size='small' className='flex items-center gap-x-[1px]' onClick={handleAddField}>
<div className='pl-5 py-2'>
<Button
size='small'
variant='secondary-accent'
className='flex items-center gap-x-[1px]'
onClick={handleAddField}
>
<RiAddCircleFill className='w-3.5 h-3.5'/>
<span className='px-[3px]'>{t('workflow.nodes.llm.jsonSchema.addField')}</span>
</Button>
</>
</div>
)
}

View File

@ -3,11 +3,13 @@ import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
type AdvancedActionsProps = {
isConfirmDisabled: boolean
onCancel: () => void
onConfirm: () => void
}
const AdvancedActions: FC<AdvancedActionsProps> = ({
isConfirmDisabled,
onCancel,
onConfirm,
}) => {
@ -18,7 +20,12 @@ const AdvancedActions: FC<AdvancedActionsProps> = ({
<Button size='small' variant='secondary' onClick={onCancel}>
{t('common.operation.cancel')}
</Button>
<Button size='small' variant='primary' onClick={onConfirm}>
<Button
disabled={isConfirmDisabled}
size='small'
variant='primary'
onClick={onConfirm}
>
{t('common.operation.confirm')}
</Button>
</div>

View File

@ -29,11 +29,6 @@ const AdvancedOptions: FC<AdvancedOptionsProps> = ({
onChange({ enum: e.target.value })
}, [onChange])
// const handleEnumChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
// const value = e.target.value
// onChange({ enum: value })
// }, [onChange])
const handleToggleAdvancedOptions = useCallback(() => {
setShowAdvancedOptions(prev => !prev)
}, [])

View File

@ -9,8 +9,10 @@ import Actions from './actions'
import AdvancedActions from './advanced-actions'
import AdvancedOptions, { type AdvancedOptionsType } from './advanced-options'
import { useTranslation } from 'react-i18next'
import { useUnmount } from 'ahooks'
import classNames from '@/utils/classnames'
import { useJsonSchemaConfigStore } from '../../store'
import { useMittContext } from '../../context'
import produce from 'immer'
export type EditData = {
name: string
@ -20,15 +22,16 @@ export type EditData = {
enum?: SchemaEnumType
}
type Options = {
description: string
enum?: SchemaEnumType
}
type EditCardProps = {
fields: EditData
onPropertyNameChange: (name: string) => void
onTypeChange: (type: Type | ArrayType) => void
onRequiredChange: (name: string) => void
onDescriptionChange: (description: string) => void
onAdvancedOptionsChange: (options: AdvancedOptionsType) => void
onDelete: (name: string) => void
onCancel: () => void
depth: number
path: string[]
parentPath: string[]
}
const TYPE_OPTIONS = [
@ -42,93 +45,165 @@ const TYPE_OPTIONS = [
{ value: ArrayType.object, text: 'array[object]' },
]
const DEPTH_LIMIT = 10
const EditCard: FC<EditCardProps> = ({
fields,
onPropertyNameChange,
onTypeChange,
onRequiredChange,
onDescriptionChange,
onAdvancedOptionsChange,
onDelete,
onCancel,
depth,
path,
parentPath,
}) => {
const { t } = useTranslation()
const [propertyName, setPropertyName] = useState(fields.name)
const [description, setDescription] = useState(fields.description)
const [AdvancedEditing, setAdvancedEditing] = useState(!fields)
const [currentFields, setCurrentFields] = useState(fields)
const [backupFields, setBackupFields] = useState<EditData | null>(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 { 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>) => {
setPropertyName(e.target.value)
setCurrentFields(prev => ({ ...prev, name: e.target.value }))
}, [])
const handlePropertyNameBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
onPropertyNameChange(e.target.value)
}, [onPropertyNameChange])
if (isAdvancedEditing) return
emitPropertyNameChange(e.target.value)
}, [isAdvancedEditing, emitPropertyNameChange])
const handleTypeChange = useCallback((item: TypeItem) => {
onTypeChange(item.value)
}, [onTypeChange])
setCurrentFields(prev => ({ ...prev, type: item.value }))
if (isAdvancedEditing) return
emitPropertyTypeChange(item.value)
}, [isAdvancedEditing, emitPropertyTypeChange])
const toggleRequired = useCallback(() => {
onRequiredChange(propertyName)
}, [onRequiredChange, propertyName])
setCurrentFields(prev => ({ ...prev, required: !prev.required }))
if (isAdvancedEditing) return
emitPropertyRequiredToggle()
}, [isAdvancedEditing, emitPropertyRequiredToggle])
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>) => {
onDescriptionChange(e.target.value)
}, [onDescriptionChange])
const advancedOptions = useMemo(() => {
return { enum: (fields.enum || []).join(', ') }
}, [fields.enum])
if (isAdvancedEditing) return
emitPropertyOptionsChange({ description: e.target.value, enum: fields.enum })
}, [isAdvancedEditing, emitPropertyOptionsChange, fields])
const handleAdvancedOptionsChange = useCallback((options: AdvancedOptionsType) => {
onAdvancedOptionsChange(options)
}, [onAdvancedOptionsChange])
const handleConfirm = useCallback(() => {
setAdvancedEditing(false)
}, [])
if (isAdvancedEditing) return
const enumValue = options.enum.replace(' ', '').split(',')
emitPropertyOptionsChange({ description: fields.description, enum: enumValue })
}, [isAdvancedEditing, emitPropertyOptionsChange, fields])
const handleDelete = useCallback(() => {
onDelete(propertyName)
}, [onDelete, propertyName])
emitPropertyDelete()
}, [emitPropertyDelete])
const handleEdit = useCallback(() => {
const handleAdvancedEdit = useCallback(() => {
setBackupFields({ ...currentFields })
setAdvancedEditing(true)
}, [])
}, [currentFields, setAdvancedEditing])
useUnmount(() => {
onPropertyNameChange(propertyName)
})
const handleAddChildField = useCallback(() => {
emitPropertyAdd()
}, [emitPropertyAdd])
const disableAddBtn = fields.type !== Type.object && fields.type !== ArrayType.object
const hasAdvancedOptions = fields.type === Type.string || fields.type === Type.number
const handleConfirm = useCallback(() => {
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 (
<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 gap-x-1 grow'>
<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
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'
placeholder={t('workflow.nodes.llm.jsonSchema.fieldNamePlaceholder')}
onChange={handlePropertyNameChange}
onBlur={handlePropertyNameBlur}
onKeyUp={e => e.key === 'Enter' && e.currentTarget.blur()}
/>
<TypeSelector
currentValue={fields.type}
currentValue={currentFields.type}
items={TYPE_OPTIONS}
onSelect={handleTypeChange}
popupClassName={'z-[1000]'}
/>
{
fields.required && (
currentFields.required && (
<div className='px-1 py-0.5 text-text-warning system-2xs-medium-uppercase'>
{t('workflow.nodes.llm.jsonSchema.required')}
</div>
@ -136,37 +211,40 @@ const EditCard: FC<EditCardProps> = ({
}
</div>
<RequiredSwitch
defaultValue={fields.required}
defaultValue={currentFields.required}
toggleRequired={toggleRequired}
/>
<Divider type='vertical' className='h-3' />
{AdvancedEditing ? (
{isAdvancedEditing ? (
<AdvancedActions
onCancel={() => { }}
isConfirmDisabled={currentFields.name === ''}
onCancel={handleCancel}
onConfirm={handleConfirm}
/>
) : (
<Actions
disableAddBtn={disableAddBtn}
onAddChildField={() => { }}
onAddChildField={handleAddChildField}
onDelete={handleDelete}
onEdit={handleEdit}/>
onEdit={handleAdvancedEdit}
/>
)}
</div>
{(description || AdvancedEditing) && (
<div className={classNames(AdvancedEditing ? 'p-2 pt-1' : 'px-2 pb-1')}>
{(currentFields.description || isAdvancedEditing) && (
<div className={classNames(isAdvancedEditing ? 'p-2 pt-1' : 'px-2 pb-1')}>
<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'
placeholder={t('workflow.nodes.llm.jsonSchema.descriptionPlaceholder')}
onChange={handleDescriptionChange}
onBlur={handleDescriptionBlur}
onKeyUp={e => e.key === 'Enter' && e.currentTarget.blur()}
/>
</div>
)}
{AdvancedEditing && hasAdvancedOptions && (
{isAdvancedEditing && hasAdvancedOptions && (
<AdvancedOptions
options={advancedOptions}
onChange={handleAdvancedOptionsChange}

View File

@ -4,20 +4,18 @@ import SchemaNode from './schema-node'
type VisualEditorProps = {
schema: Field
onChange: (schema: Field) => void
}
const VisualEditor: FC<VisualEditorProps> = ({
schema,
onChange,
}) => {
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
name='structured_output'
schema={schema}
required={false}
onChange={onChange}
path={[]}
depth={0}
/>
</div>

View File

@ -1,25 +1,23 @@
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { ArrayType } from '../../../types'
import { type ArrayItems, type Field, Type } from '../../../types'
import type { AdvancedOptionsType } from './edit-card/advanced-options'
import React, { useState } from 'react'
import { type Field, Type } from '../../../types'
import classNames from '@/utils/classnames'
import { RiArrowDropDownLine, RiArrowDropRightLine } from '@remixicon/react'
import { getFieldType, getHasChildren } from '../../../utils'
import Divider from '@/app/components/base/divider'
import EditCard from './edit-card'
import Card from './card'
import produce from 'immer'
import { useJsonSchemaConfigStore } from '../store'
import { useDebounceFn } from 'ahooks'
import AddField from './add-field'
type SchemaNodeProps = {
name: string
required: boolean
schema: Field
path: string[]
parentPath?: string[]
depth: number
onChange: (schema: Field) => void
onPropertyNameChange?: (name: string) => void
onRequiredChange?: (name: string) => void
onNodeDelete?: (name: string) => void
}
// Support 10 levels of indentation
@ -52,222 +50,38 @@ const SchemaNode: FC<SchemaNodeProps> = ({
name,
required,
schema,
onChange,
onPropertyNameChange,
onRequiredChange,
onNodeDelete,
path,
parentPath,
depth,
}) => {
const [isExpanded, setIsExpanded] = useState(true)
const [isHovering, setIsHovering] = useState(false)
const hoverTimer = useRef<any>(null)
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 { run: setHoveringPropertyDebounced } = useDebounceFn((path: string) => {
setHoveringProperty(path)
}, { wait: 50 })
const hasChildren = getHasChildren(schema)
const isEditing = isHovering && depth > 0
const type = getFieldType(schema)
const isHovering = hoveringProperty === path.join('.') && depth > 0
const handleExpand = () => {
setIsExpanded(!isExpanded)
}
const handleMouseEnter = () => {
hoverTimer.current = setTimeout(() => {
setIsHovering(true)
}, 100)
if (advancedEditing || isAddingNewField) return
setHoveringPropertyDebounced(path.join('.'))
}
const handleMouseLeave = () => {
clearTimeout(hoverTimer.current)
setIsHovering(false)
if (advancedEditing || isAddingNewField) return
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 (
<div className='relative'>
<div className={classNames('relative z-10', indentPadding[depth])}>
@ -293,7 +107,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{isEditing ? (
{isHovering ? (
<EditCard
fields={{
name,
@ -302,13 +116,9 @@ const SchemaNode: FC<SchemaNodeProps> = ({
description: schema.description || '',
enum: schema.enum || [],
}}
onPropertyNameChange={onPropertyNameChange!}
onTypeChange={handleTypeChange}
onRequiredChange={onRequiredChange!}
onDescriptionChange={handleDescriptionChange}
onAdvancedOptionsChange={handleAdvancedOptionsChange}
onDelete={onNodeDelete!}
onCancel={() => {}}
path={path}
parentPath={parentPath!}
depth={depth}
/>
) : (
<Card
@ -319,11 +129,11 @@ const SchemaNode: FC<SchemaNodeProps> = ({
/>
)}
</div>
</div>
<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],
)}>
<Divider type='vertical' className='bg-divider-subtle mx-0' />
@ -338,10 +148,8 @@ const SchemaNode: FC<SchemaNodeProps> = ({
name={key}
required={!!schema.required?.includes(key)}
schema={childSchema}
onChange={handlePropertyChange.bind(null, key)}
onPropertyNameChange={handlePropertyNameChange.bind(null, key)}
onRequiredChange={toggleRequired}
onNodeDelete={handleNodeDelete}
path={[...path, 'properties', key]}
parentPath={path}
depth={depth + 1}
/>
))
@ -358,16 +166,20 @@ const SchemaNode: FC<SchemaNodeProps> = ({
name={key}
required={!!schema.items?.required?.includes(key)}
schema={childSchema}
onChange={handleItemsPropertyChange.bind(null, key)}
onPropertyNameChange={handlePropertyNameChange.bind(null, key)}
onRequiredChange={toggleRequired}
onNodeDelete={handleNodeDelete}
path={[...path, 'items', 'properties', key]}
parentPath={path}
depth={depth + 1}
/>
))
)}
</>
)}
{
depth === 0 && !isAddingNewField && (
<AddField />
)
}
</div>
)
}

View File

@ -1,12 +1,12 @@
'use client'
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'
export default function Page() {
const [show, setShow] = useState(false)
const [schema, setSchema] = useState<Field>({
const [schema, setSchema] = useState<SchemaRoot>({
type: Type.object,
properties: {
userId: {
@ -19,9 +19,6 @@ export default function Page() {
title: {
type: Type.string,
},
completed: {
type: Type.boolean,
},
locations: {
type: Type.array,
items: {
@ -51,6 +48,9 @@ export default function Page() {
],
},
},
completed: {
type: Type.boolean,
},
},
required: [
'userId',
@ -62,14 +62,16 @@ export default function Page() {
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>
{show && <JsonSchemaConfigModal
isShow={show}
defaultSchema={schema}
onSave={(schema) => {
setSchema(schema)
}}
onClose={() => setShow(false)}
/>}
{show && (
<JsonSchemaConfigModal
isShow={show}
defaultSchema={schema}
onSave={(schema) => {
setSchema(schema)
}}
onClose={() => setShow(false)}
/>
)}
<pre className='bg-gray-50 p-4 rounded-lg overflow-auto grow'>
{JSON.stringify(schema, null, 2)}
</pre>

View File

@ -10,7 +10,7 @@ const merge = <T extends Record<string, any>>(
export type _Events = Record<EventType, unknown>
export type UseSubcribeOption = {
export type UseSubscribeOption = {
/**
* Whether the subscription is enabled.
* @default true
@ -22,21 +22,21 @@ export type ExtendedOn<Events extends _Events> = {
<Key extends keyof Events>(
type: Key,
handler: Handler<Events[Key]>,
options?: UseSubcribeOption,
options?: UseSubscribeOption,
): void;
(
type: '*',
handler: WildcardHandler<Events>,
option?: UseSubcribeOption,
option?: UseSubscribeOption,
): void;
}
export type UseMittReturn<Events extends _Events> = {
useSubcribe: ExtendedOn<Events>;
useSubscribe: ExtendedOn<Events>;
emit: Emitter<Events>['emit'];
}
const defaultSubcribeOption: UseSubcribeOption = {
const defaultSubscribeOption: UseSubscribeOption = {
enabled: true,
}
@ -52,12 +52,12 @@ function useMitt<Events extends _Events>(
emitterRef.current = mitt
}
const emitter = emitterRef.current
const useSubcribe: ExtendedOn<Events> = (
const useSubscribe: ExtendedOn<Events> = (
type: string,
handler: any,
option?: UseSubcribeOption,
option?: UseSubscribeOption,
) => {
const { enabled } = merge(defaultSubcribeOption, option)
const { enabled } = merge(defaultSubscribeOption, option)
useEffect(() => {
if (enabled) {
emitter.on(type, handler)
@ -67,7 +67,7 @@ function useMitt<Events extends _Events>(
}
return {
emit: emitter.emit,
useSubcribe,
useSubscribe,
}
}