mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-18 07:25:54 +08:00
feat: enhance JSON Schema visual editor with new components and translations
This commit is contained in:
parent
7a2c831ef3
commit
4333820aa6
@ -8,8 +8,9 @@ const textareaVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
regular: 'px-3 radius-md system-sm-regular',
|
small: 'py-1 rounded-md system-xs-regular',
|
||||||
large: 'px-4 radius-lg system-md-regular',
|
regular: 'px-3 rounded-md system-sm-regular',
|
||||||
|
large: 'px-4 rounded-lg system-md-regular',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { type FC, useCallback, useState } from 'react'
|
import React, { type FC, useCallback, useState } from 'react'
|
||||||
import Modal from '../../../../../base/modal'
|
import Modal from '../../../../../base/modal'
|
||||||
import { type StructuredOutput, Type } from '../../types'
|
import { type Field, Type } from '../../types'
|
||||||
import { RiBracesLine, RiCloseLine, RiExternalLinkLine, RiTimelineView } from '@remixicon/react'
|
import { RiBracesLine, RiCloseLine, RiExternalLinkLine, RiTimelineView } from '@remixicon/react'
|
||||||
import { SegmentedControl } from '../../../../../base/segmented-control'
|
import { SegmentedControl } from '../../../../../base/segmented-control'
|
||||||
import JsonSchemaGenerator from './json-schema-generator'
|
import JsonSchemaGenerator from './json-schema-generator'
|
||||||
@ -8,11 +8,12 @@ import Divider from '@/app/components/base/divider'
|
|||||||
import JsonImporter from './json-importer'
|
import JsonImporter from './json-importer'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
|
import VisualEditor from './visual-editor'
|
||||||
|
|
||||||
type JsonSchemaConfigModalProps = {
|
type JsonSchemaConfigModalProps = {
|
||||||
isShow: boolean
|
isShow: boolean
|
||||||
defaultSchema: StructuredOutput
|
defaultSchema?: Field
|
||||||
onSave: (schema: StructuredOutput) => void
|
onSave: (schema: Field) => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,18 +22,16 @@ enum SchemaView {
|
|||||||
JsonSchema = 'jsonSchema',
|
JsonSchema = 'jsonSchema',
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = [
|
const VIEW_TABS = [
|
||||||
{ Icon: RiTimelineView, text: 'Visual Editor', value: SchemaView.VisualEditor },
|
{ Icon: RiTimelineView, text: 'Visual Editor', value: SchemaView.VisualEditor },
|
||||||
{ Icon: RiBracesLine, text: 'JSON Schema', value: SchemaView.JsonSchema },
|
{ Icon: RiBracesLine, text: 'JSON Schema', value: SchemaView.JsonSchema },
|
||||||
]
|
]
|
||||||
|
|
||||||
const DEFAULT_SCHEMA = {
|
const DEFAULT_SCHEMA: Field = {
|
||||||
schema: {
|
|
||||||
type: Type.object,
|
type: Type.object,
|
||||||
properties: {},
|
properties: {},
|
||||||
required: [],
|
required: [],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
|
const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
|
||||||
@ -54,6 +53,10 @@ const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
|
|||||||
|
|
||||||
const handleSubmit = useCallback(() => {}, [])
|
const handleSubmit = useCallback(() => {}, [])
|
||||||
|
|
||||||
|
const handleUpdateSchema = useCallback((schema: Field) => {
|
||||||
|
setJsonSchema(schema)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleResetDefaults = useCallback(() => {
|
const handleResetDefaults = useCallback(() => {
|
||||||
setJsonSchema(defaultSchema || DEFAULT_SCHEMA)
|
setJsonSchema(defaultSchema || DEFAULT_SCHEMA)
|
||||||
}, [defaultSchema])
|
}, [defaultSchema])
|
||||||
@ -73,7 +76,7 @@ const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
|
|||||||
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'>
|
<div className='flex flex-col h-full'>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className='relative flex p-6 pr-14 pb-3'>
|
<div className='relative flex p-6 pr-14 pb-3'>
|
||||||
<div className='text-text-primary title-2xl-semi-bold grow truncate'>
|
<div className='text-text-primary title-2xl-semi-bold grow truncate'>
|
||||||
@ -87,7 +90,7 @@ const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
|
|||||||
<div className='flex items-center justify-between px-6 py-2'>
|
<div className='flex items-center justify-between px-6 py-2'>
|
||||||
{/* Tab */}
|
{/* Tab */}
|
||||||
<SegmentedControl<SchemaView>
|
<SegmentedControl<SchemaView>
|
||||||
options={options}
|
options={VIEW_TABS}
|
||||||
value={currentTab}
|
value={currentTab}
|
||||||
onChange={(value: SchemaView) => {
|
onChange={(value: SchemaView) => {
|
||||||
setCurrentTab(value)
|
setCurrentTab(value)
|
||||||
@ -108,8 +111,15 @@ const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='px-6 grow'>
|
<div className='px-6 grow'>
|
||||||
{currentTab === SchemaView.VisualEditor && <div className='h-full bg-components-input-bg-normal'>Visual Editor</div>}
|
{currentTab === SchemaView.VisualEditor && (
|
||||||
{currentTab === SchemaView.JsonSchema && <div className='h-full bg-components-input-bg-normal'>JSON Schema</div>}
|
<VisualEditor
|
||||||
|
schema={jsonSchema}
|
||||||
|
onChange={handleUpdateSchema}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{currentTab === SchemaView.JsonSchema && (
|
||||||
|
<div className='h-full rounded-xl bg-components-input-bg-normal'>JSON Schema</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className='flex items-center p-6 pt-5 gap-x-2'>
|
<div className='flex items-center p-6 pt-5 gap-x-2'>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { type FC, useCallback, useRef, useState } from 'react'
|
import React, { type FC, useCallback, useRef, useState } from 'react'
|
||||||
import type { StructuredOutput } from '../../../types'
|
import type { SchemaRoot } from '../../../types'
|
||||||
import { RiArrowLeftLine, RiClipboardLine, RiCloseLine, RiSparklingLine } from '@remixicon/react'
|
import { RiArrowLeftLine, RiClipboardLine, RiCloseLine, RiSparklingLine } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Editor from '@monaco-editor/react'
|
import Editor from '@monaco-editor/react'
|
||||||
@ -7,7 +7,7 @@ import copy from 'copy-to-clipboard'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
|
|
||||||
type GeneratedResultProps = {
|
type GeneratedResultProps = {
|
||||||
schema: StructuredOutput
|
schema: SchemaRoot
|
||||||
onBack: () => void
|
onBack: () => void
|
||||||
onRegenerate: () => void
|
onRegenerate: () => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { type FC, useCallback, useState } from 'react'
|
import React, { type FC, useCallback, useState } from 'react'
|
||||||
import { type StructuredOutput, Type } from '../../../types'
|
import { type SchemaRoot, Type } from '../../../types'
|
||||||
import {
|
import {
|
||||||
PortalToFollowElem,
|
PortalToFollowElem,
|
||||||
PortalToFollowElemContent,
|
PortalToFollowElemContent,
|
||||||
@ -13,7 +13,7 @@ import PromptEditor from './prompt-editor'
|
|||||||
import GeneratedResult from './generated-result'
|
import GeneratedResult from './generated-result'
|
||||||
|
|
||||||
type JsonSchemaGeneratorProps = {
|
type JsonSchemaGeneratorProps = {
|
||||||
onApply: (schema: StructuredOutput) => void
|
onApply: (schema: SchemaRoot) => void
|
||||||
crossAxisOffset?: number
|
crossAxisOffset?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const [view, setView] = useState(GeneratorView.promptEditor)
|
const [view, setView] = useState(GeneratorView.promptEditor)
|
||||||
const [instruction, setInstruction] = useState('')
|
const [instruction, setInstruction] = useState('')
|
||||||
const [schema, setSchema] = useState<StructuredOutput | null>(null)
|
const [schema, setSchema] = useState<SchemaRoot | null>(null)
|
||||||
const SchemaGenerator = theme === Theme.light ? SchemaGeneratorLight : SchemaGeneratorDark
|
const SchemaGenerator = theme === Theme.light ? SchemaGeneratorLight : SchemaGeneratorDark
|
||||||
|
|
||||||
const handleTrigger = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
const handleTrigger = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||||
@ -47,7 +47,6 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
|||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSchema({
|
setSchema({
|
||||||
schema: {
|
|
||||||
type: Type.object,
|
type: Type.object,
|
||||||
properties: {
|
properties: {
|
||||||
string_field_1: {
|
string_field_1: {
|
||||||
@ -63,7 +62,6 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
|||||||
'string_field_1',
|
'string_field_1',
|
||||||
],
|
],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
|
||||||
})
|
})
|
||||||
resolve()
|
resolve()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import React, { type FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
type CardProps = {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
required: boolean
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Card: FC<CardProps> = ({
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
required,
|
||||||
|
description,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col py-0.5'>
|
||||||
|
<div className='flex items-center gap-x-1 p-0.5 pl-1'>
|
||||||
|
<div className='px-1 py-0.5 text-text-primary system-sm-semibold'>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
<div className='px-1 py-0.5 text-text-tertiary system-xs-medium'>
|
||||||
|
{type}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
required && (
|
||||||
|
<div className='px-1 py-0.5 text-text-warning system-2xs-medium-uppercase'>
|
||||||
|
{t('workflow.nodes.llm.jsonSchema.required')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<div className='px-2 pb-1 text-text-tertiary system-xs-regular'>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card
|
@ -0,0 +1,56 @@
|
|||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
import { RiAddCircleLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
type ActionsProps = {
|
||||||
|
disableAddBtn: boolean
|
||||||
|
onAddChildField: () => void
|
||||||
|
onEdit: () => void
|
||||||
|
onDelete: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Actions: FC<ActionsProps> = ({
|
||||||
|
disableAddBtn,
|
||||||
|
onAddChildField,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center gap-x-0.5'>
|
||||||
|
<Tooltip popupContent={t('workflow.nodes.llm.jsonSchema.addChildField')}>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='flex items-center justify-center w-6 h-6 rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled'
|
||||||
|
onClick={onAddChildField}
|
||||||
|
disabled={disableAddBtn}
|
||||||
|
>
|
||||||
|
<RiAddCircleLine className='w-4 h-4'/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip popupContent={t('common.operation.edit')}>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='flex items-center justify-center w-6 h-6 rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
|
||||||
|
onClick={onEdit}
|
||||||
|
>
|
||||||
|
<RiEditLine className='w-4 h-4' />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip popupContent={t('common.operation.remove')}>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='flex items-center justify-center w-6 h-6 rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive'
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<RiDeleteBinLine className='w-4 h-4' />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Actions)
|
@ -0,0 +1,28 @@
|
|||||||
|
import React, { type FC } from 'react'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
type AdvancedActionsProps = {
|
||||||
|
onCancel: () => void
|
||||||
|
onConfirm: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdvancedActions: FC<AdvancedActionsProps> = ({
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center gap-x-1'>
|
||||||
|
<Button size='small' variant='secondary' onClick={onCancel}>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button size='small' variant='primary' onClick={onConfirm}>
|
||||||
|
{t('common.operation.confirm')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(AdvancedActions)
|
@ -0,0 +1,83 @@
|
|||||||
|
import React, { type FC, useCallback, useState } from 'react'
|
||||||
|
import { RiArrowDownDoubleLine } from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Divider from '@/app/components/base/divider'
|
||||||
|
import Textarea from '@/app/components/base/textarea'
|
||||||
|
|
||||||
|
export type AdvancedOptionsType = {
|
||||||
|
enum: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdvancedOptionsProps = {
|
||||||
|
options: AdvancedOptionsType
|
||||||
|
onChange: (options: AdvancedOptionsType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdvancedOptions: FC<AdvancedOptionsProps> = ({
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false)
|
||||||
|
const [enumValue, setEnumValue] = useState(options.enum)
|
||||||
|
|
||||||
|
const handleEnumChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setEnumValue(e.target.value)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleEnumBlur = useCallback((e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||||
|
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)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='border-t border-divider-subtle'>
|
||||||
|
{showAdvancedOptions ? (
|
||||||
|
<div className='flex flex-col px-2 py-1.5 gap-y-1'>
|
||||||
|
<div className='flex items-center gap-x-2 w-full'>
|
||||||
|
<span className='text-text-tertiary system-2xs-medium-uppercase'>
|
||||||
|
{t('workflow.nodes.llm.jsonSchema.stringValidations')}
|
||||||
|
</span>
|
||||||
|
<div className='grow'>
|
||||||
|
<Divider type='horizontal' className='h-px my-0 bg-line-divider-bg' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='flex items-center h-6 text-text-secondary system-xs-medium'>
|
||||||
|
Enum
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
size='small'
|
||||||
|
className='min-h-6'
|
||||||
|
value={enumValue}
|
||||||
|
onChange={handleEnumChange}
|
||||||
|
onBlur={handleEnumBlur}
|
||||||
|
placeholder={'\'abcd\', 1, 1.5, \'etc\''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='flex items-center pl-1.5 pt-2 pr-2 pb-1 gap-x-0.5'
|
||||||
|
onClick={handleToggleAdvancedOptions}
|
||||||
|
>
|
||||||
|
<RiArrowDownDoubleLine className='w-3 h-3 text-text-tertiary' />
|
||||||
|
<span className='text-text-tertiary system-xs-regular'>
|
||||||
|
{t('workflow.nodes.llm.jsonSchema.showAdvancedOptions')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdvancedOptions
|
@ -0,0 +1,179 @@
|
|||||||
|
import React, { type FC, useCallback, useMemo, useState } from 'react'
|
||||||
|
import type { SchemaEnumType } from '../../../../types'
|
||||||
|
import { ArrayType, Type } from '../../../../types'
|
||||||
|
import type { TypeItem } from './type-selector'
|
||||||
|
import TypeSelector from './type-selector'
|
||||||
|
import RequiredSwitch from './required-switch'
|
||||||
|
import Divider from '@/app/components/base/divider'
|
||||||
|
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'
|
||||||
|
|
||||||
|
export type EditData = {
|
||||||
|
name: string
|
||||||
|
type: Type | ArrayType
|
||||||
|
required: boolean
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE_OPTIONS = [
|
||||||
|
{ value: Type.string, text: 'string' },
|
||||||
|
{ value: Type.number, text: 'number' },
|
||||||
|
{ value: Type.boolean, text: 'boolean' },
|
||||||
|
{ value: Type.object, text: 'object' },
|
||||||
|
{ value: ArrayType.string, text: 'array[string]' },
|
||||||
|
{ value: ArrayType.number, text: 'array[number]' },
|
||||||
|
{ value: ArrayType.boolean, text: 'array[boolean]' },
|
||||||
|
{ value: ArrayType.object, text: 'array[object]' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const EditCard: FC<EditCardProps> = ({
|
||||||
|
fields,
|
||||||
|
onPropertyNameChange,
|
||||||
|
onTypeChange,
|
||||||
|
onRequiredChange,
|
||||||
|
onDescriptionChange,
|
||||||
|
onAdvancedOptionsChange,
|
||||||
|
onDelete,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [propertyName, setPropertyName] = useState(fields.name)
|
||||||
|
const [description, setDescription] = useState(fields.description)
|
||||||
|
const [AdvancedEditing, setAdvancedEditing] = useState(!fields)
|
||||||
|
|
||||||
|
const handlePropertyNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPropertyName(e.target.value)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handlePropertyNameBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
onPropertyNameChange(e.target.value)
|
||||||
|
}, [onPropertyNameChange])
|
||||||
|
|
||||||
|
const handleTypeChange = useCallback((item: TypeItem) => {
|
||||||
|
onTypeChange(item.value)
|
||||||
|
}, [onTypeChange])
|
||||||
|
|
||||||
|
const toggleRequired = useCallback(() => {
|
||||||
|
onRequiredChange(propertyName)
|
||||||
|
}, [onRequiredChange, propertyName])
|
||||||
|
|
||||||
|
const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setDescription(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])
|
||||||
|
|
||||||
|
const handleAdvancedOptionsChange = useCallback((options: AdvancedOptionsType) => {
|
||||||
|
onAdvancedOptionsChange(options)
|
||||||
|
}, [onAdvancedOptionsChange])
|
||||||
|
|
||||||
|
const handleConfirm = useCallback(() => {
|
||||||
|
setAdvancedEditing(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
onDelete(propertyName)
|
||||||
|
}, [onDelete, propertyName])
|
||||||
|
|
||||||
|
const handleEdit = useCallback(() => {
|
||||||
|
setAdvancedEditing(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useUnmount(() => {
|
||||||
|
onPropertyNameChange(propertyName)
|
||||||
|
})
|
||||||
|
|
||||||
|
const disableAddBtn = fields.type !== Type.object && fields.type !== ArrayType.object
|
||||||
|
const hasAdvancedOptions = fields.type === Type.string || fields.type === Type.number
|
||||||
|
|
||||||
|
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}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
<TypeSelector
|
||||||
|
currentValue={fields.type}
|
||||||
|
items={TYPE_OPTIONS}
|
||||||
|
onSelect={handleTypeChange}
|
||||||
|
popupClassName={'z-[1000]'}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
fields.required && (
|
||||||
|
<div className='px-1 py-0.5 text-text-warning system-2xs-medium-uppercase'>
|
||||||
|
{t('workflow.nodes.llm.jsonSchema.required')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<RequiredSwitch
|
||||||
|
defaultValue={fields.required}
|
||||||
|
toggleRequired={toggleRequired}
|
||||||
|
/>
|
||||||
|
<Divider type='vertical' className='h-3' />
|
||||||
|
{AdvancedEditing ? (
|
||||||
|
<AdvancedActions
|
||||||
|
onCancel={() => { }}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Actions
|
||||||
|
disableAddBtn={disableAddBtn}
|
||||||
|
onAddChildField={() => { }}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onEdit={handleEdit}/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(description || AdvancedEditing) && (
|
||||||
|
<div className={classNames(AdvancedEditing ? 'p-2 pt-1' : 'px-2 pb-1')}>
|
||||||
|
<input
|
||||||
|
value={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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{AdvancedEditing && hasAdvancedOptions && (
|
||||||
|
<AdvancedOptions
|
||||||
|
options={advancedOptions}
|
||||||
|
onChange={handleAdvancedOptionsChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditCard
|
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import Switch from '@/app/components/base/switch'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
type RequiredSwitchProps = {
|
||||||
|
defaultValue: boolean
|
||||||
|
toggleRequired: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RequiredSwitch: FC<RequiredSwitchProps> = ({
|
||||||
|
defaultValue,
|
||||||
|
toggleRequired,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center gap-x-1 px-1.5 py-1 rounded-[5px] border border-divider-subtle bg-background-default-lighter'>
|
||||||
|
<span className='text-text-secondary system-2xs-medium-uppercase'>{t('workflow.nodes.llm.jsonSchema.required')}</span>
|
||||||
|
<Switch size='xs' defaultValue={defaultValue} onChange={toggleRequired} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(RequiredSwitch)
|
@ -0,0 +1,69 @@
|
|||||||
|
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import type { ArrayType, Type } from '../../../../types'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
export type TypeItem = {
|
||||||
|
value: Type | ArrayType
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeSelectorProps = {
|
||||||
|
items: TypeItem[]
|
||||||
|
currentValue: Type | ArrayType
|
||||||
|
onSelect: (item: TypeItem) => void
|
||||||
|
popupClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const TypeSelector: FC<TypeSelectorProps> = ({
|
||||||
|
items,
|
||||||
|
currentValue,
|
||||||
|
onSelect,
|
||||||
|
popupClassName,
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
placement='bottom-start'
|
||||||
|
offset={{
|
||||||
|
mainAxis: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||||
|
<div className={cn(
|
||||||
|
'flex items-center p-0.5 pl-1 rounded-[5px] hover:bg-state-base-hover',
|
||||||
|
open && 'bg-state-base-hover',
|
||||||
|
)}>
|
||||||
|
<span className='text-text-tertiary system-xs-medium'>{currentValue}</span>
|
||||||
|
<RiArrowDownSLine className='w-4 h-4 text-text-tertiary' />
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className={popupClassName}>
|
||||||
|
<div className='w-40 p-1 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg shadow-shadow-shadow-5'>
|
||||||
|
{items.map((item) => {
|
||||||
|
const isSelected = item.value === currentValue
|
||||||
|
return (<div
|
||||||
|
key={item.value}
|
||||||
|
className={'flex items-center gap-x-1 px-2 py-1 rounded-lg hover:bg-state-base-hover'}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(item)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className='px-1 text-text-secondary system-sm-medium'>{item.text}</span>
|
||||||
|
{isSelected && <RiCheckLine className='w-4 h-4 text-text-accent' />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TypeSelector
|
@ -0,0 +1,27 @@
|
|||||||
|
import type { FC } from 'react'
|
||||||
|
import type { Field } from '../../../types'
|
||||||
|
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'>
|
||||||
|
<SchemaNode
|
||||||
|
name='structured_output'
|
||||||
|
schema={schema}
|
||||||
|
required={false}
|
||||||
|
onChange={onChange}
|
||||||
|
depth={0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VisualEditor
|
@ -0,0 +1,375 @@
|
|||||||
|
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 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'
|
||||||
|
|
||||||
|
type SchemaNodeProps = {
|
||||||
|
name: string
|
||||||
|
required: boolean
|
||||||
|
schema: Field
|
||||||
|
depth: number
|
||||||
|
onChange: (schema: Field) => void
|
||||||
|
onPropertyNameChange?: (name: string) => void
|
||||||
|
onRequiredChange?: (name: string) => void
|
||||||
|
onNodeDelete?: (name: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support 10 levels of indentation
|
||||||
|
const indentPadding: Record<number, string> = {
|
||||||
|
0: 'pl-0',
|
||||||
|
1: 'pl-[20px]',
|
||||||
|
2: 'pl-[40px]',
|
||||||
|
3: 'pl-[60px]',
|
||||||
|
4: 'pl-[80px]',
|
||||||
|
5: 'pl-[100px]',
|
||||||
|
6: 'pl-[120px]',
|
||||||
|
7: 'pl-[140px]',
|
||||||
|
8: 'pl-[160px]',
|
||||||
|
9: 'pl-[180px]',
|
||||||
|
}
|
||||||
|
|
||||||
|
const indentLeft: Record<number, string> = {
|
||||||
|
1: 'left-0',
|
||||||
|
2: 'left-[20px]',
|
||||||
|
3: 'left-[40px]',
|
||||||
|
4: 'left-[60px]',
|
||||||
|
5: 'left-[80px]',
|
||||||
|
6: 'left-[100px]',
|
||||||
|
7: 'left-[120px]',
|
||||||
|
8: 'left-[140px]',
|
||||||
|
9: 'left-[160px]',
|
||||||
|
}
|
||||||
|
|
||||||
|
const SchemaNode: FC<SchemaNodeProps> = ({
|
||||||
|
name,
|
||||||
|
required,
|
||||||
|
schema,
|
||||||
|
onChange,
|
||||||
|
onPropertyNameChange,
|
||||||
|
onRequiredChange,
|
||||||
|
onNodeDelete,
|
||||||
|
depth,
|
||||||
|
}) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(true)
|
||||||
|
const [isHovering, setIsHovering] = useState(false)
|
||||||
|
const hoverTimer = useRef<any>(null)
|
||||||
|
|
||||||
|
const hasChildren = getHasChildren(schema)
|
||||||
|
const isEditing = isHovering && depth > 0
|
||||||
|
const type = getFieldType(schema)
|
||||||
|
|
||||||
|
const handleExpand = () => {
|
||||||
|
setIsExpanded(!isExpanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
hoverTimer.current = setTimeout(() => {
|
||||||
|
setIsHovering(true)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
clearTimeout(hoverTimer.current)
|
||||||
|
setIsHovering(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
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])}>
|
||||||
|
{depth > 0 && hasChildren && (
|
||||||
|
<div className={classNames(
|
||||||
|
'flex items-center absolute top-0 w-5 h-7 px-0.5 z-10 bg-background-section-burn',
|
||||||
|
indentLeft[depth],
|
||||||
|
)}>
|
||||||
|
<button
|
||||||
|
onClick={handleExpand}
|
||||||
|
className='py-0.5 text-text-tertiary hover:text-text-accent'
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isExpanded
|
||||||
|
? <RiArrowDropDownLine className='w-4 h-4' />
|
||||||
|
: <RiArrowDropRightLine className='w-4 h-4' />
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
{isEditing ? (
|
||||||
|
<EditCard
|
||||||
|
fields={{
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
required,
|
||||||
|
description: schema.description || '',
|
||||||
|
enum: schema.enum || [],
|
||||||
|
}}
|
||||||
|
onPropertyNameChange={onPropertyNameChange!}
|
||||||
|
onTypeChange={handleTypeChange}
|
||||||
|
onRequiredChange={onRequiredChange!}
|
||||||
|
onDescriptionChange={handleDescriptionChange}
|
||||||
|
onAdvancedOptionsChange={handleAdvancedOptionsChange}
|
||||||
|
onDelete={onNodeDelete!}
|
||||||
|
onCancel={() => {}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Card
|
||||||
|
name={name}
|
||||||
|
type={type}
|
||||||
|
required={required}
|
||||||
|
description={schema.description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classNames(
|
||||||
|
'flex justify-center w-5 h-[calc(100%-1.75rem)] absolute top-7 z-0',
|
||||||
|
indentLeft[depth + 1],
|
||||||
|
)}>
|
||||||
|
<Divider type='vertical' className='bg-divider-subtle mx-0' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && hasChildren && (
|
||||||
|
<>
|
||||||
|
{schema.type === Type.object && schema.properties && (
|
||||||
|
Object.entries(schema.properties).map(([key, childSchema]) => (
|
||||||
|
<SchemaNode
|
||||||
|
key={key}
|
||||||
|
name={key}
|
||||||
|
required={!!schema.required?.includes(key)}
|
||||||
|
schema={childSchema}
|
||||||
|
onChange={handlePropertyChange.bind(null, key)}
|
||||||
|
onPropertyNameChange={handlePropertyNameChange.bind(null, key)}
|
||||||
|
onRequiredChange={toggleRequired}
|
||||||
|
onNodeDelete={handleNodeDelete}
|
||||||
|
depth={depth + 1}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
|
{schema.type === Type.array
|
||||||
|
&& schema.items
|
||||||
|
&& schema.items.type === Type.object
|
||||||
|
&& schema.items.properties
|
||||||
|
&& (
|
||||||
|
Object.entries(schema.items.properties).map(([key, childSchema]) => (
|
||||||
|
<SchemaNode
|
||||||
|
key={key}
|
||||||
|
name={key}
|
||||||
|
required={!!schema.items?.required?.includes(key)}
|
||||||
|
schema={childSchema}
|
||||||
|
onChange={handleItemsPropertyChange.bind(null, key)}
|
||||||
|
onPropertyNameChange={handlePropertyNameChange.bind(null, key)}
|
||||||
|
onRequiredChange={toggleRequired}
|
||||||
|
onNodeDelete={handleNodeDelete}
|
||||||
|
depth={depth + 1}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(SchemaNode)
|
@ -35,6 +35,9 @@ export enum ArrayType {
|
|||||||
export type TypeWithArray = Type | ArrayType
|
export type TypeWithArray = Type | ArrayType
|
||||||
|
|
||||||
type ArrayItemType = Exclude<Type, Type.array>
|
type ArrayItemType = Exclude<Type, Type.array>
|
||||||
|
export type ArrayItems = Omit<Field, 'type'> & { type: ArrayItemType }
|
||||||
|
|
||||||
|
export type SchemaEnumType = string[] | number[]
|
||||||
|
|
||||||
export type Field = {
|
export type Field = {
|
||||||
type: Type
|
type: Type
|
||||||
@ -43,18 +46,18 @@ export type Field = {
|
|||||||
}
|
}
|
||||||
required?: string[] // Key of required properties in object
|
required?: string[] // Key of required properties in object
|
||||||
description?: string
|
description?: string
|
||||||
items?: { // Array has items. Define the item type
|
items?: ArrayItems // Array has items. Define the item type
|
||||||
type: ArrayItemType
|
enum?: SchemaEnumType // Enum values
|
||||||
}
|
|
||||||
enum?: string[] // Enum values
|
|
||||||
additionalProperties?: false // Required in object by api. Just set false
|
additionalProperties?: false // Required in object by api. Just set false
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StructuredOutput = {
|
export type StructuredOutput = {
|
||||||
schema: {
|
schema: SchemaRoot
|
||||||
type: Type.object,
|
}
|
||||||
|
|
||||||
|
export type SchemaRoot = {
|
||||||
|
type: Type.object
|
||||||
properties: Record<string, Field>
|
properties: Record<string, Field>
|
||||||
required: string[]
|
required?: string[]
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -12,3 +12,13 @@ export const getFieldType = (field: Field) => {
|
|||||||
|
|
||||||
return ArrayType[items.type]
|
return ArrayType[items.type]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getHasChildren = (schema: Field) => {
|
||||||
|
const complexTypes = [Type.object, Type.array]
|
||||||
|
if (!complexTypes.includes(schema.type))
|
||||||
|
return false
|
||||||
|
if (schema.type === Type.object)
|
||||||
|
return schema.properties && Object.keys(schema.properties).length > 0
|
||||||
|
if (schema.type === Type.array)
|
||||||
|
return schema.items && schema.items.type === Type.object && schema.items.properties && Object.keys(schema.items.properties).length > 0
|
||||||
|
}
|
||||||
|
@ -1,19 +1,77 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { ToolTipContent } from '../components/base/tooltip/content'
|
import { useState } from 'react'
|
||||||
import { SwitchPluginVersion } from '../components/workflow/nodes/_base/components/switch-plugin-version'
|
import { type Field, Type } from '../components/workflow/nodes/llm/types'
|
||||||
import { useTranslation } from 'react-i18next'
|
import JsonSchemaConfigModal from '../components/workflow/nodes/llm/components/json-schema-config-modal'
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { t } = useTranslation()
|
const [show, setShow] = useState(false)
|
||||||
return <div className="p-20">
|
const [schema, setSchema] = useState<Field>({
|
||||||
<SwitchPluginVersion
|
type: Type.object,
|
||||||
uniqueIdentifier={'langgenius/openai:12'}
|
properties: {
|
||||||
tooltip={<ToolTipContent
|
userId: {
|
||||||
title={t('workflow.nodes.agent.unsupportedStrategy')}
|
type: Type.number,
|
||||||
>
|
description: 'The user ID',
|
||||||
{t('workflow.nodes.agent.strategyNotFoundDescAndSwitchVersion')}
|
},
|
||||||
</ToolTipContent>}
|
id: {
|
||||||
/>
|
type: Type.number,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: Type.string,
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
type: Type.boolean,
|
||||||
|
},
|
||||||
|
locations: {
|
||||||
|
type: Type.array,
|
||||||
|
items: {
|
||||||
|
type: Type.object,
|
||||||
|
properties: {
|
||||||
|
x: {
|
||||||
|
type: Type.object,
|
||||||
|
properties: {
|
||||||
|
x1: {
|
||||||
|
type: Type.array,
|
||||||
|
items: {
|
||||||
|
type: Type.number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'x1',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: Type.number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'x',
|
||||||
|
'y',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'userId',
|
||||||
|
'id',
|
||||||
|
'title',
|
||||||
|
],
|
||||||
|
additionalProperties: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
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)}
|
||||||
|
/>}
|
||||||
|
<pre className='bg-gray-50 p-4 rounded-lg overflow-auto grow'>
|
||||||
|
{JSON.stringify(schema, null, 2)}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -426,6 +426,13 @@ const translation = {
|
|||||||
apply: 'Apply',
|
apply: 'Apply',
|
||||||
doc: 'Learn more about structured output',
|
doc: 'Learn more about structured output',
|
||||||
resetDefaults: 'Reset Defaults',
|
resetDefaults: 'Reset Defaults',
|
||||||
|
required: 'required',
|
||||||
|
addField: 'Add Field',
|
||||||
|
addChildField: 'Add Child Field',
|
||||||
|
showAdvancedOptions: 'Show advanced options',
|
||||||
|
stringValidations: 'String Validations',
|
||||||
|
fieldNamePlaceholder: 'Field Name',
|
||||||
|
descriptionPlaceholder: 'Add description',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
|
@ -426,6 +426,13 @@ const translation = {
|
|||||||
apply: '应用',
|
apply: '应用',
|
||||||
doc: '了解有关结构化输出的更多信息',
|
doc: '了解有关结构化输出的更多信息',
|
||||||
resetDefaults: '恢复默认值',
|
resetDefaults: '恢复默认值',
|
||||||
|
required: '必填',
|
||||||
|
addField: '添加字段',
|
||||||
|
addChildField: '添加子字段',
|
||||||
|
showAdvancedOptions: '显示高级选项',
|
||||||
|
stringValidations: '字符串验证',
|
||||||
|
fieldNamePlaceholder: '字段名',
|
||||||
|
descriptionPlaceholder: '添加描述',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
knowledgeRetrieval: {
|
knowledgeRetrieval: {
|
||||||
|
@ -113,6 +113,7 @@ const config = {
|
|||||||
'dataset-option-card-purple-gradient': 'var(--color-dataset-option-card-purple-gradient)',
|
'dataset-option-card-purple-gradient': 'var(--color-dataset-option-card-purple-gradient)',
|
||||||
'dataset-option-card-orange-gradient': 'var(--color-dataset-option-card-orange-gradient)',
|
'dataset-option-card-orange-gradient': 'var(--color-dataset-option-card-orange-gradient)',
|
||||||
'dataset-chunk-list-mask-bg': 'var(--color-dataset-chunk-list-mask-bg)',
|
'dataset-chunk-list-mask-bg': 'var(--color-dataset-chunk-list-mask-bg)',
|
||||||
|
'line-divider-bg': 'var(--color-line-divider-bg)',
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'spin-slow': 'spin 2s linear infinite',
|
'spin-slow': 'spin 2s linear infinite',
|
||||||
|
@ -33,7 +33,7 @@ html[data-theme="dark"] {
|
|||||||
rgba(240, 68, 56, 0.3) 0%,
|
rgba(240, 68, 56, 0.3) 0%,
|
||||||
rgba(0, 0, 0, 0) 100%);
|
rgba(0, 0, 0, 0) 100%);
|
||||||
--color-toast-info-bg: linear-gradient(92deg,
|
--color-toast-info-bg: linear-gradient(92deg,
|
||||||
rgba(11, 165, 236, 0.3) 0%),
|
rgba(11, 165, 236, 0.3) 0%);
|
||||||
--color-account-teams-bg: linear-gradient(271deg,
|
--color-account-teams-bg: linear-gradient(271deg,
|
||||||
rgba(34, 34, 37, 0.9) -0.1%,
|
rgba(34, 34, 37, 0.9) -0.1%,
|
||||||
rgba(29, 29, 32, 0.9) 98.26%
|
rgba(29, 29, 32, 0.9) 98.26%
|
||||||
@ -61,4 +61,5 @@ html[data-theme="dark"] {
|
|||||||
180deg,
|
180deg,
|
||||||
rgba(24, 24, 27, 0.08) 0%,
|
rgba(24, 24, 27, 0.08) 0%,
|
||||||
rgba(0, 0, 0, 0) 100%);
|
rgba(0, 0, 0, 0) 100%);
|
||||||
|
--color-line-divider-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.14) 0%, rgba(0, 0, 0, 0) 100%,);
|
||||||
}
|
}
|
@ -33,7 +33,7 @@ html[data-theme="light"] {
|
|||||||
rgba(240, 68, 56, 0.25) 0%,
|
rgba(240, 68, 56, 0.25) 0%,
|
||||||
rgba(255, 255, 255, 0) 100%);
|
rgba(255, 255, 255, 0) 100%);
|
||||||
--color-toast-info-bg: linear-gradient(92deg,
|
--color-toast-info-bg: linear-gradient(92deg,
|
||||||
rgba(11, 165, 236, 0.25) 0%),
|
rgba(11, 165, 236, 0.25) 0%);
|
||||||
--color-account-teams-bg: linear-gradient(271deg,
|
--color-account-teams-bg: linear-gradient(271deg,
|
||||||
rgba(249, 250, 251, 0.9) -0.1%,
|
rgba(249, 250, 251, 0.9) -0.1%,
|
||||||
rgba(242, 244, 247, 0.9) 98.26%
|
rgba(242, 244, 247, 0.9) 98.26%
|
||||||
@ -61,4 +61,5 @@ html[data-theme="light"] {
|
|||||||
180deg,
|
180deg,
|
||||||
rgba(200, 206, 218, 0.2) 0%,
|
rgba(200, 206, 218, 0.2) 0%,
|
||||||
rgba(255, 255, 255, 0) 100%);
|
rgba(255, 255, 255, 0) 100%);
|
||||||
|
--color-line-divider-bg: linear-gradient(90deg, rgba(16, 24, 40, 0.08) 0%, rgba(255, 255, 255, 0) 100%);
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user