Merge remote-tracking branch 'origin/main' into feat/support-agent-sandbox

This commit is contained in:
yyh
2026-03-25 17:49:36 +08:00
93 changed files with 13582 additions and 2337 deletions

View File

@ -0,0 +1,195 @@
import type { ChangeEvent } from 'react'
import { act, renderHook } from '@testing-library/react'
import { ChatVarType } from '../../type'
import { useVariableModalState } from '../use-variable-modal-state'
vi.mock('uuid', () => ({
v4: () => 'generated-id',
}))
const createOptions = (overrides: Partial<Parameters<typeof useVariableModalState>[0]> = {}) => ({
chatVar: undefined,
conversationVariables: [],
notify: vi.fn(),
onClose: vi.fn(),
onSave: vi.fn(),
t: (key: string) => key,
...overrides,
})
describe('useVariableModalState', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should build initial state from an existing array object variable', () => {
const { result } = renderHook(() => useVariableModalState(createOptions({
chatVar: {
id: 'var-1',
name: 'payload',
description: 'desc',
value_type: ChatVarType.ArrayObject,
value: [{ enabled: true }],
},
})))
expect(result.current.name).toBe('payload')
expect(result.current.description).toBe('desc')
expect(result.current.type).toBe(ChatVarType.ArrayObject)
expect(result.current.editInJSON).toBe(true)
expect(result.current.editorContent).toBe(JSON.stringify([{ enabled: true }]))
})
it('should update state when changing types and editing scalar values', () => {
const { result } = renderHook(() => useVariableModalState(createOptions()))
act(() => {
result.current.handleTypeChange(ChatVarType.Object)
})
expect(result.current.type).toBe(ChatVarType.Object)
expect(result.current.objectValue).toHaveLength(1)
act(() => {
result.current.handleTypeChange(ChatVarType.Number)
result.current.handleStringOrNumberChange([12])
})
expect(result.current.value).toBe(12)
act(() => {
result.current.setDescription('note')
result.current.setValue(true)
})
expect(result.current.description).toBe('note')
expect(result.current.value).toBe(true)
})
it('should toggle object values between form and json modes', () => {
const { result } = renderHook(() => useVariableModalState(createOptions({
chatVar: {
id: 'var-2',
name: 'config',
description: '',
value_type: ChatVarType.Object,
value: { timeout: 30 },
},
})))
act(() => {
result.current.handleEditorChange(true)
})
expect(result.current.editInJSON).toBe(true)
expect(result.current.editorContent).toBe(JSON.stringify({ timeout: 30 }))
act(() => {
result.current.handleEditorValueChange('{"timeout":45}')
result.current.handleEditorChange(false)
})
expect(result.current.editInJSON).toBe(false)
expect(result.current.objectValue).toEqual([
{ key: 'timeout', type: ChatVarType.Number, value: 45 },
])
})
it('should reset object form values when leaving empty json mode', () => {
const { result } = renderHook(() => useVariableModalState(createOptions({
chatVar: {
id: 'var-3',
name: 'config',
description: '',
value_type: ChatVarType.Object,
value: {},
},
})))
act(() => {
result.current.handleEditorChange(true)
result.current.handleEditorValueChange('')
result.current.handleEditorChange(false)
})
expect(result.current.objectValue).toHaveLength(1)
expect(result.current.value).toBeUndefined()
})
it('should handle array editor toggles and invalid json safely', () => {
const { result } = renderHook(() => useVariableModalState(createOptions()))
act(() => {
result.current.handleTypeChange(ChatVarType.ArrayString)
result.current.setValue(['a', '', 'b'])
result.current.handleEditorChange(true)
})
expect(result.current.editInJSON).toBe(true)
expect(result.current.value).toEqual(['a', 'b'])
act(() => {
result.current.handleEditorValueChange('[invalid')
})
expect(result.current.editorContent).toBe('[invalid')
expect(result.current.value).toEqual(['a', 'b'])
act(() => {
result.current.handleEditorChange(false)
})
expect(result.current.value).toEqual(['a', 'b'])
act(() => {
result.current.handleTypeChange(ChatVarType.ArrayBoolean)
result.current.setValue([true, false])
result.current.handleEditorChange(true)
})
expect(result.current.editorContent).toBe(JSON.stringify(['True', 'False']))
})
it('should notify and stop saving when object keys are invalid', () => {
const notify = vi.fn()
const onSave = vi.fn()
const onClose = vi.fn()
const { result } = renderHook(() => useVariableModalState(createOptions({
notify,
onClose,
onSave,
})))
act(() => {
result.current.handleVarNameChange({ target: { value: 'config' } } as ChangeEvent<HTMLInputElement>)
result.current.handleTypeChange(ChatVarType.Object)
result.current.setObjectValue([{ key: '', type: ChatVarType.String, value: 'secret' }])
})
act(() => {
result.current.handleSave()
})
expect(notify).toHaveBeenCalledWith({ type: 'error', message: 'chatVariable.modal.objectKeyRequired' })
expect(onSave).not.toHaveBeenCalled()
expect(onClose).not.toHaveBeenCalled()
})
it('should save a new variable and close when state is valid', () => {
const onSave = vi.fn()
const onClose = vi.fn()
const { result } = renderHook(() => useVariableModalState(createOptions({
onClose,
onSave,
})))
act(() => {
result.current.handleVarNameChange({ target: { value: 'greeting' } } as ChangeEvent<HTMLInputElement>)
result.current.handleStringOrNumberChange(['hello'])
})
act(() => {
result.current.handleSave()
})
expect(onSave).toHaveBeenCalledWith({
description: '',
id: 'generated-id',
name: 'greeting',
value: 'hello',
value_type: ChatVarType.String,
})
expect(onClose).toHaveBeenCalled()
})
})

View File

@ -0,0 +1,123 @@
import { ChatVarType } from '../../type'
import {
buildObjectValueItems,
formatChatVariableValue,
formatObjectValueFromList,
getEditorMinHeight,
getEditorToggleLabelKey,
getPlaceholderByType,
getTypeChangeState,
parseEditorContent,
validateVariableName,
} from '../variable-modal.helpers'
describe('variable-modal helpers', () => {
it('should build object items from a conversation variable value', () => {
expect(buildObjectValueItems()).toHaveLength(1)
expect(buildObjectValueItems({
id: 'var-1',
name: 'config',
description: '',
value_type: ChatVarType.Object,
value: { apiKey: 'secret', timeout: 30 },
})).toEqual([
{ key: 'apiKey', type: ChatVarType.String, value: 'secret' },
{ key: 'timeout', type: ChatVarType.Number, value: 30 },
])
})
it('should format object and array values for saving', () => {
expect(formatObjectValueFromList([
{ key: 'apiKey', type: ChatVarType.String, value: 'secret' },
{ key: '', type: ChatVarType.Number, value: 1 },
])).toEqual({ apiKey: 'secret' })
expect(formatChatVariableValue({
editInJSON: false,
objectValue: [{ key: 'enabled', type: ChatVarType.String, value: 'true' }],
type: ChatVarType.Object,
value: undefined,
})).toEqual({ enabled: 'true' })
expect(formatChatVariableValue({
editInJSON: true,
objectValue: [],
type: ChatVarType.Object,
value: { count: 1 },
})).toEqual({ count: 1 })
expect(formatChatVariableValue({
editInJSON: false,
objectValue: [],
type: ChatVarType.ArrayString,
value: ['a', '', 'b'],
})).toEqual(['a', 'b'])
expect(formatChatVariableValue({
editInJSON: false,
objectValue: [],
type: ChatVarType.Number,
value: undefined,
})).toBe(0)
expect(formatChatVariableValue({
editInJSON: false,
objectValue: [],
type: ChatVarType.Boolean,
value: undefined,
})).toBe(true)
expect(formatChatVariableValue({
editInJSON: false,
objectValue: [],
type: ChatVarType.ArrayBoolean,
value: undefined,
})).toEqual([])
})
it('should derive placeholders, editor defaults, and editor toggle labels', () => {
expect(getEditorMinHeight(ChatVarType.ArrayObject)).toBe('240px')
expect(getEditorMinHeight(ChatVarType.Object)).toBe('120px')
expect(getPlaceholderByType(ChatVarType.ArrayBoolean)).toBeTruthy()
expect(getTypeChangeState(ChatVarType.Boolean).value).toBe(false)
expect(getTypeChangeState(ChatVarType.ArrayBoolean).value).toEqual([false])
expect(getTypeChangeState(ChatVarType.Object).objectValue).toHaveLength(1)
expect(getTypeChangeState(ChatVarType.ArrayObject).editInJSON).toBe(true)
expect(getEditorToggleLabelKey(ChatVarType.Object, true)).toBe('chatVariable.modal.editInForm')
expect(getEditorToggleLabelKey(ChatVarType.ArrayString, false)).toBe('chatVariable.modal.editInJSON')
})
it('should parse boolean arrays from JSON editor content', () => {
expect(parseEditorContent({
content: '["True","false",true,false,"invalid"]',
type: ChatVarType.ArrayBoolean,
})).toEqual([true, false, true, false])
expect(parseEditorContent({
content: '{"enabled":true}',
type: ChatVarType.Object,
})).toEqual({ enabled: true })
})
it('should validate variable names and notify when invalid', () => {
const notify = vi.fn()
const t = (key: string) => key
expect(validateVariableName({
name: 'valid_name',
notify,
t,
})).toBe(true)
expect(validateVariableName({
name: '1invalid',
notify,
t,
})).toBe(false)
expect(notify).toHaveBeenCalledWith(expect.objectContaining({
type: 'error',
}))
})
})

View File

@ -0,0 +1,198 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { toast } from '@/app/components/base/ui/toast'
import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
import { ChatVarType } from '../../type'
import VariableModal from '../variable-modal'
vi.mock('uuid', () => ({
v4: () => 'generated-id',
}))
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
error: vi.fn(),
info: vi.fn(),
success: vi.fn(),
warning: vi.fn(),
},
}))
const renderVariableModal = (props?: Partial<React.ComponentProps<typeof VariableModal>>) => {
const onClose = vi.fn()
const onSave = vi.fn()
const result = renderWorkflowComponent(
React.createElement(
VariableModal,
{
onClose,
onSave,
...props,
},
),
)
return { ...result, onClose, onSave }
}
describe('variable-modal', () => {
const mockToastError = vi.mocked(toast.error)
beforeEach(() => {
vi.clearAllMocks()
})
it('should create a new string variable and close after saving', async () => {
const user = userEvent.setup()
const { onClose, onSave } = renderVariableModal()
await user.type(screen.getByPlaceholderText('workflow.chatVariable.modal.namePlaceholder'), 'greeting')
await user.type(screen.getByPlaceholderText('workflow.chatVariable.modal.valuePlaceholder'), 'hello')
await user.type(screen.getByPlaceholderText('workflow.chatVariable.modal.descriptionPlaceholder'), 'demo variable')
await user.click(screen.getByText('common.operation.save'))
expect(onSave).toHaveBeenCalledWith({
id: 'generated-id',
name: 'greeting',
value_type: ChatVarType.String,
value: 'hello',
description: 'demo variable',
})
expect(onClose).toHaveBeenCalled()
})
it('should reject duplicate variable names from the workflow store', async () => {
const user = userEvent.setup()
const { onSave, store } = renderVariableModal()
store.setState({
conversationVariables: [{
id: 'var-1',
name: 'existing_name',
description: '',
value_type: ChatVarType.String,
value: '',
}],
})
await user.type(screen.getByPlaceholderText('workflow.chatVariable.modal.namePlaceholder'), 'existing_name')
await user.click(screen.getByText('common.operation.save'))
expect(mockToastError.mock.calls.at(-1)?.[0]).toBe('name is existed')
expect(onSave).not.toHaveBeenCalled()
})
it('should load an existing object variable and save object values edited in form mode', async () => {
const user = userEvent.setup()
const { onSave } = renderVariableModal({
chatVar: {
id: 'var-2',
name: 'config',
description: 'settings',
value_type: ChatVarType.Object,
value: { apiKey: 'secret', timeout: 30 },
},
})
expect(screen.getByDisplayValue('config')).toBeInTheDocument()
expect(screen.getByDisplayValue('secret')).toBeInTheDocument()
expect(screen.getByDisplayValue('30')).toBeInTheDocument()
await user.clear(screen.getByDisplayValue('secret'))
await user.type(screen.getByDisplayValue('30'), '5')
await user.click(screen.getByText('common.operation.save'))
expect(onSave).toHaveBeenCalledWith({
id: 'var-2',
name: 'config',
value_type: ChatVarType.Object,
value: {
apiKey: null,
timeout: 305,
},
description: 'settings',
})
})
it('should switch types and use default values for boolean arrays', async () => {
const user = userEvent.setup()
const { onSave } = renderVariableModal()
await user.type(screen.getByPlaceholderText('workflow.chatVariable.modal.namePlaceholder'), 'flags')
await user.click(screen.getByText('string'))
await user.click(screen.getByText('array[boolean]'))
await user.click(screen.getByText('common.operation.save'))
expect(onSave).toHaveBeenCalledWith({
id: 'generated-id',
name: 'flags',
value_type: ChatVarType.ArrayBoolean,
value: [false],
description: '',
})
})
it('should toggle object editing modes without changing behavior', async () => {
const user = userEvent.setup()
renderVariableModal({
chatVar: {
id: 'var-3',
name: 'payload',
description: '',
value_type: ChatVarType.Object,
value: { enabled: 1 },
},
})
await user.click(screen.getByText('workflow.chatVariable.modal.editInJSON'))
await waitFor(() => {
expect(screen.getByText('Loading...')).toBeInTheDocument()
})
await user.click(screen.getByText('workflow.chatVariable.modal.editInForm'))
expect(screen.getByDisplayValue('enabled')).toBeInTheDocument()
})
it('should validate variable names on blur and preserve underscore replacement', () => {
renderVariableModal()
const input = screen.getByPlaceholderText('workflow.chatVariable.modal.namePlaceholder')
fireEvent.change(input, { target: { value: 'bad name' } })
fireEvent.blur(input)
expect((input as HTMLInputElement).value).toBe('bad_name')
expect(mockToastError).not.toHaveBeenCalled()
})
it('should stop invalid variable names before they are stored in local state', async () => {
const { onSave } = renderVariableModal()
const input = screen.getByPlaceholderText('workflow.chatVariable.modal.namePlaceholder') as HTMLInputElement
fireEvent.change(input, { target: { value: '1bad' } })
await userEvent.click(screen.getByText('common.operation.save'))
expect(input.value).toBe('')
expect(mockToastError).toHaveBeenCalled()
expect(onSave).not.toHaveBeenCalled()
})
it('should edit number variables through the value input', async () => {
const user = userEvent.setup()
const { onSave } = renderVariableModal()
await user.type(screen.getByPlaceholderText('workflow.chatVariable.modal.namePlaceholder'), 'timeout')
await user.click(screen.getByText('string'))
await user.click(screen.getByText('number'))
await user.type(screen.getByPlaceholderText('workflow.chatVariable.modal.valuePlaceholder'), '3')
await user.click(screen.getByText('common.operation.save'))
expect(onSave).toHaveBeenCalledWith({
id: 'generated-id',
name: 'timeout',
value_type: ChatVarType.Number,
value: 3,
description: '',
})
})
})

View File

@ -0,0 +1,234 @@
import type { ObjectValueItem, ToastPayload } from './variable-modal.helpers'
import type { ConversationVariable } from '@/app/components/workflow/types'
import { useMemo, useState } from 'react'
import { v4 as uuid4 } from 'uuid'
import { DEFAULT_OBJECT_VALUE } from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-item'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import {
buildObjectValueItems,
formatChatVariableValue,
formatObjectValueFromList,
getEditorMinHeight,
getPlaceholderByType,
getTypeChangeState,
parseEditorContent,
validateVariableName,
} from './variable-modal.helpers'
type UseVariableModalStateOptions = {
chatVar?: ConversationVariable
conversationVariables: ConversationVariable[]
notify: (props: ToastPayload) => void
onClose: () => void
onSave: (chatVar: ConversationVariable) => void
t: (key: string, options?: Record<string, unknown>) => string
}
type VariableModalState = {
description: string
editInJSON: boolean
editorContent?: string
name: string
objectValue: ObjectValueItem[]
type: ChatVarType
value: unknown
}
const buildObjectValueListFromRecord = (record: Record<string, string | number>) => {
return Object.keys(record).map(key => ({
key,
type: typeof record[key] === 'string' ? ChatVarType.String : ChatVarType.Number,
value: record[key],
}))
}
const buildInitialState = (chatVar?: ConversationVariable): VariableModalState => {
if (!chatVar) {
return {
description: '',
editInJSON: false,
editorContent: undefined,
name: '',
objectValue: [DEFAULT_OBJECT_VALUE],
type: ChatVarType.String,
value: undefined,
}
}
return {
description: chatVar.description,
editInJSON: chatVar.value_type === ChatVarType.ArrayObject,
editorContent: chatVar.value_type === ChatVarType.ArrayObject ? JSON.stringify(chatVar.value) : undefined,
name: chatVar.name,
objectValue: buildObjectValueItems(chatVar),
type: chatVar.value_type,
value: chatVar.value,
}
}
export const useVariableModalState = ({
chatVar,
conversationVariables,
notify,
onClose,
onSave,
t,
}: UseVariableModalStateOptions) => {
const [state, setState] = useState<VariableModalState>(() => buildInitialState(chatVar))
const editorMinHeight = useMemo(() => getEditorMinHeight(state.type), [state.type])
const placeholder = useMemo(() => getPlaceholderByType(state.type), [state.type])
const handleVarNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setState(prev => ({ ...prev, name: e.target.value || '' }))
}
const handleTypeChange = (nextType: ChatVarType) => {
const nextState = getTypeChangeState(nextType)
setState(prev => ({
...prev,
editInJSON: nextState.editInJSON,
editorContent: nextState.editorContent,
objectValue: nextState.objectValue ?? prev.objectValue,
type: nextType,
value: nextState.value,
}))
}
const handleStringOrNumberChange = (nextValue: Array<string | number | undefined>) => {
setState(prev => ({ ...prev, value: nextValue[0] }))
}
const handleEditorChange = (nextEditInJSON: boolean) => {
setState((prev) => {
const nextState: VariableModalState = {
...prev,
editInJSON: nextEditInJSON,
}
if (prev.type === ChatVarType.Object) {
if (nextEditInJSON) {
const nextValue = !prev.objectValue[0].key ? undefined : formatObjectValueFromList(prev.objectValue)
nextState.value = nextValue
nextState.editorContent = JSON.stringify(nextValue)
return nextState
}
if (!prev.editorContent) {
nextState.value = undefined
nextState.objectValue = [DEFAULT_OBJECT_VALUE]
return nextState
}
try {
const nextValue = JSON.parse(prev.editorContent) as Record<string, string | number>
nextState.value = nextValue
nextState.objectValue = buildObjectValueListFromRecord(nextValue)
}
catch {
// ignore JSON.parse errors
}
return nextState
}
if (prev.type === ChatVarType.ArrayString || prev.type === ChatVarType.ArrayNumber) {
if (nextEditInJSON) {
const nextValue = (Array.isArray(prev.value) && prev.value.length && prev.value.filter(Boolean).length)
? prev.value.filter(Boolean)
: undefined
nextState.value = nextValue
if (!prev.editorContent)
nextState.editorContent = JSON.stringify(nextValue)
return nextState
}
nextState.value = Array.isArray(prev.value) && prev.value.length ? prev.value : [undefined]
return nextState
}
if (prev.type === ChatVarType.ArrayBoolean && Array.isArray(prev.value) && nextEditInJSON)
nextState.editorContent = JSON.stringify(prev.value.map(item => item ? 'True' : 'False'))
return nextState
})
}
const handleEditorValueChange = (content: string) => {
setState((prev) => {
const nextState: VariableModalState = {
...prev,
editorContent: content,
}
if (!content) {
nextState.value = undefined
return nextState
}
try {
nextState.value = parseEditorContent({ content, type: prev.type })
}
catch {
// ignore JSON.parse errors
}
return nextState
})
}
const handleSave = () => {
if (!validateVariableName({ name: state.name, notify, t }))
return
if (!chatVar && conversationVariables.some(item => item.name === state.name)) {
notify({
type: 'error',
message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: t('chatVariable.modal.name', { ns: 'workflow' }) }),
})
return
}
if (state.type === ChatVarType.Object && state.objectValue.some(item => !item.key && !!item.value)) {
notify({
type: 'error',
message: t('chatVariable.modal.objectKeyRequired', { ns: 'workflow' }),
})
return
}
onSave({
description: state.description,
id: chatVar ? chatVar.id : uuid4(),
name: state.name,
value: formatChatVariableValue({
editInJSON: state.editInJSON,
objectValue: state.objectValue,
type: state.type,
value: state.value,
}),
value_type: state.type,
})
onClose()
}
return {
description: state.description,
editInJSON: state.editInJSON,
editorContent: state.editorContent,
editorMinHeight,
handleEditorChange,
handleEditorValueChange,
handleSave,
handleStringOrNumberChange,
handleTypeChange,
handleVarNameChange,
name: state.name,
objectValue: state.objectValue,
placeholder,
setDescription: (description: string) => setState(prev => ({ ...prev, description })),
setObjectValue: (objectValue: ObjectValueItem[]) => setState(prev => ({ ...prev, objectValue })),
setValue: (value: unknown) => setState(prev => ({ ...prev, value })),
type: state.type,
value: state.value,
}
}

View File

@ -57,6 +57,7 @@ const VariableModalTrigger = ({
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[11]">
<VariableModal
key={chatVar?.id ?? 'new'}
chatVar={chatVar}
onSave={onSave}
onClose={() => {

View File

@ -0,0 +1,170 @@
import type { ReactNode } from 'react'
import type { ChatVarType } from '../type'
import type { ConversationVariable } from '@/app/components/workflow/types'
import { checkKeys } from '@/utils/var'
import { ChatVarType as ChatVarTypeEnum } from '../type'
import {
arrayBoolPlaceholder,
arrayNumberPlaceholder,
arrayObjectPlaceholder,
arrayStringPlaceholder,
objectPlaceholder,
} from '../utils'
import { DEFAULT_OBJECT_VALUE } from './object-value-item'
export type ObjectValueItem = {
key: string
type: ChatVarType
value: string | number | undefined
}
export type ToastPayload = {
type?: 'success' | 'error' | 'warning' | 'info'
size?: 'md' | 'sm'
duration?: number
message: string
children?: ReactNode
onClose?: () => void
className?: string
customComponent?: ReactNode
}
export const typeList = [
ChatVarTypeEnum.String,
ChatVarTypeEnum.Number,
ChatVarTypeEnum.Boolean,
ChatVarTypeEnum.Object,
ChatVarTypeEnum.ArrayString,
ChatVarTypeEnum.ArrayNumber,
ChatVarTypeEnum.ArrayBoolean,
ChatVarTypeEnum.ArrayObject,
]
export const getEditorMinHeight = (type: ChatVarType) =>
type === ChatVarTypeEnum.ArrayObject ? '240px' : '120px'
export const getPlaceholderByType = (type: ChatVarType) => {
if (type === ChatVarTypeEnum.ArrayString)
return arrayStringPlaceholder
if (type === ChatVarTypeEnum.ArrayNumber)
return arrayNumberPlaceholder
if (type === ChatVarTypeEnum.ArrayObject)
return arrayObjectPlaceholder
if (type === ChatVarTypeEnum.ArrayBoolean)
return arrayBoolPlaceholder
return objectPlaceholder
}
export const buildObjectValueItems = (chatVar?: ConversationVariable): ObjectValueItem[] => {
if (!chatVar || !chatVar.value || Object.keys(chatVar.value).length === 0)
return [DEFAULT_OBJECT_VALUE]
return Object.keys(chatVar.value).map((key) => {
const itemValue = chatVar.value[key]
return {
key,
type: typeof itemValue === 'string' ? ChatVarTypeEnum.String : ChatVarTypeEnum.Number,
value: itemValue,
}
})
}
export const formatObjectValueFromList = (list: ObjectValueItem[]) => {
return list.reduce<Record<string, string | number | null>>((acc, curr) => {
if (curr.key)
acc[curr.key] = curr.value || null
return acc
}, {})
}
export const formatChatVariableValue = ({
editInJSON,
objectValue,
type,
value,
}: {
editInJSON: boolean
objectValue: ObjectValueItem[]
type: ChatVarType
value: unknown
}) => {
switch (type) {
case ChatVarTypeEnum.String:
return value || ''
case ChatVarTypeEnum.Number:
return value || 0
case ChatVarTypeEnum.Boolean:
return value === undefined ? true : value
case ChatVarTypeEnum.Object:
return editInJSON ? value : formatObjectValueFromList(objectValue)
case ChatVarTypeEnum.ArrayString:
case ChatVarTypeEnum.ArrayNumber:
case ChatVarTypeEnum.ArrayObject:
return Array.isArray(value) ? value.filter(Boolean) : []
case ChatVarTypeEnum.ArrayBoolean:
return value || []
}
}
export const validateVariableName = ({
name,
notify,
t,
}: {
name: string
notify: (props: ToastPayload) => void
t: (key: string, options?: Record<string, unknown>) => string
}) => {
const { isValid, errorMessageKey } = checkKeys([name], false)
if (!isValid) {
notify({
type: 'error',
message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: t('env.modal.name', { ns: 'workflow' }) }),
})
return false
}
return true
}
export const getTypeChangeState = (nextType: ChatVarType) => {
return {
editInJSON: nextType === ChatVarTypeEnum.ArrayObject,
editorContent: undefined as string | undefined,
objectValue: nextType === ChatVarTypeEnum.Object ? [DEFAULT_OBJECT_VALUE] : undefined,
value:
nextType === ChatVarTypeEnum.Boolean
? false
: nextType === ChatVarTypeEnum.ArrayBoolean
? [false]
: undefined,
}
}
export const parseEditorContent = ({
content,
type,
}: {
content: string
type: ChatVarType
}) => {
const parsed = JSON.parse(content)
if (type !== ChatVarTypeEnum.ArrayBoolean)
return parsed
return parsed
.map((item: string | boolean) => {
if (item === 'True' || item === 'true' || item === true)
return true
if (item === 'False' || item === 'false' || item === false)
return false
return undefined
})
.filter((item?: boolean) => item !== undefined)
}
export const getEditorToggleLabelKey = (type: ChatVarType, editInJSON: boolean) => {
if (type === ChatVarTypeEnum.Object)
return editInJSON ? 'chatVariable.modal.editInForm' : 'chatVariable.modal.editInJSON'
return editInJSON ? 'chatVariable.modal.oneByOne' : 'chatVariable.modal.editInJSON'
}

View File

@ -0,0 +1,217 @@
import type { ReactNode } from 'react'
import type { ObjectValueItem } from './variable-modal.helpers'
import { RiDraftLine, RiInputField } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { ChatVarType } from '../type'
import ArrayBoolList from './array-bool-list'
import ArrayValueList from './array-value-list'
import BoolValue from './bool-value'
import ObjectValueList from './object-value-list'
import VariableTypeSelector from './variable-type-select'
type SectionTitleProps = {
children: ReactNode
}
export const SectionTitle = ({ children }: SectionTitleProps) => (
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-semibold">{children}</div>
)
type NameSectionProps = {
name: string
onBlur: (value: string) => void
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
placeholder: string
title: string
}
export const NameSection = ({
name,
onBlur,
onChange,
placeholder,
title,
}: NameSectionProps) => (
<div className="mb-4">
<SectionTitle>{title}</SectionTitle>
<div className="flex">
<Input
placeholder={placeholder}
value={name}
onChange={onChange}
onBlur={e => onBlur(e.target.value)}
type="text"
/>
</div>
</div>
)
type TypeSectionProps = {
list: ChatVarType[]
onSelect: (value: ChatVarType) => void
title: string
type: ChatVarType
}
export const TypeSection = ({
list,
onSelect,
title,
type,
}: TypeSectionProps) => (
<div className="mb-4">
<SectionTitle>{title}</SectionTitle>
<div className="flex">
<VariableTypeSelector
value={type}
list={list}
onSelect={onSelect}
popupClassName="w-[327px]"
/>
</div>
</div>
)
type ValueSectionProps = {
editorContent?: string
editorMinHeight: string
editInJSON: boolean
objectValue: ObjectValueItem[]
onArrayBoolChange: (value: boolean[]) => void
onArrayChange: (value: Array<string | number | undefined>) => void
onEditorChange: (nextEditInJson: boolean) => void
onEditorValueChange: (content: string) => void
onObjectChange: (value: ObjectValueItem[]) => void
onValueChange: (value: boolean) => void
placeholder: ReactNode
t: (key: string, options?: Record<string, unknown>) => string
toggleLabelKey?: string
type: ChatVarType
value: unknown
}
export const ValueSection = ({
editorContent,
editorMinHeight,
editInJSON,
objectValue,
onArrayBoolChange,
onArrayChange,
onEditorChange,
onEditorValueChange,
onObjectChange,
onValueChange,
placeholder,
t,
toggleLabelKey,
type,
value,
}: ValueSectionProps) => (
<div className="mb-4">
<div className="mb-1 flex h-6 items-center justify-between text-text-secondary system-sm-semibold">
<div>{t('chatVariable.modal.value', { ns: 'workflow' })}</div>
{toggleLabelKey && (
<Button
variant="ghost"
size="small"
className="text-text-tertiary"
onClick={() => onEditorChange(!editInJSON)}
>
{editInJSON ? <RiInputField className="mr-1 h-3.5 w-3.5" /> : <RiDraftLine className="mr-1 h-3.5 w-3.5" />}
{t(toggleLabelKey, { ns: 'workflow' })}
</Button>
)}
</div>
<div className="flex">
{type === ChatVarType.String && (
<textarea
className="block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none system-sm-regular placeholder:text-components-input-text-placeholder placeholder:system-sm-regular hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs"
value={(value as string) || ''}
placeholder={t('chatVariable.modal.valuePlaceholder', { ns: 'workflow' }) || ''}
onChange={e => onArrayChange([e.target.value])}
/>
)}
{type === ChatVarType.Number && (
<Input
placeholder={t('chatVariable.modal.valuePlaceholder', { ns: 'workflow' }) || ''}
value={value as number | undefined}
onChange={e => onArrayChange([Number(e.target.value)])}
type="number"
/>
)}
{type === ChatVarType.Boolean && (
<BoolValue
value={value as boolean}
onChange={onValueChange}
/>
)}
{type === ChatVarType.Object && !editInJSON && (
<ObjectValueList
list={objectValue}
onChange={onObjectChange}
/>
)}
{type === ChatVarType.ArrayString && !editInJSON && (
<ArrayValueList
isString
list={(value as Array<string | undefined>) || [undefined]}
onChange={onArrayChange}
/>
)}
{type === ChatVarType.ArrayNumber && !editInJSON && (
<ArrayValueList
isString={false}
list={(value as Array<number | undefined>) || [undefined]}
onChange={onArrayChange}
/>
)}
{type === ChatVarType.ArrayBoolean && !editInJSON && (
<ArrayBoolList
list={(value as boolean[]) || [true]}
onChange={onArrayBoolChange}
/>
)}
{editInJSON && (
<div className="w-full rounded-[10px] bg-components-input-bg-normal py-2 pl-3 pr-1" style={{ height: editorMinHeight }}>
<CodeEditor
isExpand
noWrapper
language={CodeLanguage.json}
value={editorContent}
placeholder={<div className="whitespace-pre">{placeholder}</div>}
onChange={onEditorValueChange}
/>
</div>
)}
</div>
</div>
)
type DescriptionSectionProps = {
description: string
onChange: (value: string) => void
placeholder: string
title: string
}
export const DescriptionSection = ({
description,
onChange,
placeholder,
title,
}: DescriptionSectionProps) => (
<div>
<SectionTitle>{title}</SectionTitle>
<div className="flex">
<textarea
className="block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none system-sm-regular placeholder:text-components-input-text-placeholder placeholder:system-sm-regular hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs"
value={description}
placeholder={placeholder}
onChange={e => onChange(e.target.value)}
/>
</div>
</div>
)

View File

@ -1,31 +1,26 @@
import type { ToastPayload } from './variable-modal.helpers'
import type { ConversationVariable } from '@/app/components/workflow/types'
import { RiCloseLine, RiDraftLine, RiInputField } from '@remixicon/react'
import { RiCloseLine } from '@remixicon/react'
import * as React from 'react'
import { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { v4 as uuid4 } from 'uuid'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { toast } from '@/app/components/base/ui/toast'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import ArrayValueList from '@/app/components/workflow/panel/chat-variable-panel/components/array-value-list'
import { DEFAULT_OBJECT_VALUE } from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-item'
import ObjectValueList from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-list'
import VariableTypeSelector from '@/app/components/workflow/panel/chat-variable-panel/components/variable-type-select'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import {
arrayBoolPlaceholder,
arrayNumberPlaceholder,
arrayObjectPlaceholder,
arrayStringPlaceholder,
objectPlaceholder,
} from '@/app/components/workflow/panel/chat-variable-panel/utils'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { cn } from '@/utils/classnames'
import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
import ArrayBoolList from './array-bool-list'
import BoolValue from './bool-value'
import { replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
import { useVariableModalState } from './use-variable-modal-state'
import {
getEditorToggleLabelKey,
typeList,
validateVariableName,
} from './variable-modal.helpers'
import {
DescriptionSection,
NameSection,
TypeSection,
ValueSection,
} from './variable-modal.sections'
export type ModalPropsType = {
chatVar?: ConversationVariable
@ -33,23 +28,6 @@ export type ModalPropsType = {
onSave: (chatVar: ConversationVariable) => void
}
type ObjectValueItem = {
key: string
type: ChatVarType
value: string | number | undefined
}
const typeList = [
ChatVarType.String,
ChatVarType.Number,
ChatVarType.Boolean,
ChatVarType.Object,
ChatVarType.ArrayString,
ChatVarType.ArrayNumber,
ChatVarType.ArrayBoolean,
ChatVarType.ArrayObject,
]
const ChatVariableModal = ({
chatVar,
onClose,
@ -57,215 +35,43 @@ const ChatVariableModal = ({
}: ModalPropsType) => {
const { t } = useTranslation()
const workflowStore = useWorkflowStore()
const [name, setName] = React.useState('')
const [type, setType] = React.useState<ChatVarType>(ChatVarType.String)
const [value, setValue] = React.useState<any>()
const [objectValue, setObjectValue] = React.useState<ObjectValueItem[]>([DEFAULT_OBJECT_VALUE])
const [editorContent, setEditorContent] = React.useState<string>()
const [editInJSON, setEditInJSON] = React.useState(false)
const [description, setDescription] = React.useState<string>('')
const editorMinHeight = useMemo(() => {
if (type === ChatVarType.ArrayObject)
return '240px'
return '120px'
}, [type])
const placeholder = useMemo(() => {
if (type === ChatVarType.ArrayString)
return arrayStringPlaceholder
if (type === ChatVarType.ArrayNumber)
return arrayNumberPlaceholder
if (type === ChatVarType.ArrayObject)
return arrayObjectPlaceholder
if (type === ChatVarType.ArrayBoolean)
return arrayBoolPlaceholder
return objectPlaceholder
}, [type])
const getObjectValue = useCallback(() => {
const raw = chatVar?.value
if (!chatVar || raw === null || typeof raw !== 'object' || Array.isArray(raw) || Object.keys(raw).length === 0)
return [DEFAULT_OBJECT_VALUE]
return Object.keys(raw).map((key) => {
const v = raw[key]
const isStr = typeof v === 'string'
const isNum = typeof v === 'number'
return {
key,
type: isStr ? ChatVarType.String : ChatVarType.Number,
value: isStr || isNum ? v : undefined,
}
})
}, [chatVar])
const formatValueFromObject = useCallback((list: ObjectValueItem[]) => {
return list.reduce((acc: any, curr) => {
if (curr.key)
acc[curr.key] = curr.value || null
return acc
}, {})
const notify = React.useCallback(({ children, message, type = 'info' }: ToastPayload) => {
toast[type](message, children ? { description: children } : undefined)
}, [])
const {
description,
editInJSON,
editorContent,
editorMinHeight,
handleEditorChange,
handleEditorValueChange,
handleSave,
handleStringOrNumberChange,
handleTypeChange,
handleVarNameChange,
name,
objectValue,
placeholder,
setDescription,
setObjectValue,
setValue,
type,
value,
} = useVariableModalState({
chatVar,
conversationVariables: workflowStore.getState().conversationVariables,
notify,
onClose,
onSave,
t,
})
const formatValue = (value: any) => {
switch (type) {
case ChatVarType.String:
return value || ''
case ChatVarType.Number:
return value || 0
case ChatVarType.Boolean:
return value === undefined ? true : value
case ChatVarType.Object:
return editInJSON ? value : formatValueFromObject(objectValue)
case ChatVarType.ArrayString:
case ChatVarType.ArrayNumber:
case ChatVarType.ArrayObject:
return value?.filter(Boolean) || []
case ChatVarType.ArrayBoolean:
return value || []
}
}
const checkVariableName = (value: string) => {
const { isValid, errorMessageKey } = checkKeys([value], false)
if (!isValid) {
toast.error(t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: t('env.modal.name', { ns: 'workflow' }) }))
return false
}
return true
}
const handleVarNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
replaceSpaceWithUnderscoreInVarNameInput(e.target)
if (!!e.target.value && !checkVariableName(e.target.value))
if (e.target.value && !validateVariableName({ name: e.target.value, notify, t }))
return
setName(e.target.value || '')
handleVarNameChange(e)
}
const handleTypeChange = (v: ChatVarType) => {
setValue(undefined)
setEditorContent(undefined)
if (v === ChatVarType.ArrayObject)
setEditInJSON(true)
if (v === ChatVarType.String || v === ChatVarType.Number || v === ChatVarType.Object)
setEditInJSON(false)
if (v === ChatVarType.Boolean)
setValue(false)
if (v === ChatVarType.ArrayBoolean)
setValue([false])
setType(v)
}
const handleEditorChange = (editInJSON: boolean) => {
if (type === ChatVarType.Object) {
if (editInJSON) {
const newValue = !objectValue[0].key ? undefined : formatValueFromObject(objectValue)
setValue(newValue)
setEditorContent(JSON.stringify(newValue))
}
else {
if (!editorContent) {
setValue(undefined)
setObjectValue([DEFAULT_OBJECT_VALUE])
}
else {
try {
const newValue = JSON.parse(editorContent)
setValue(newValue)
const newObjectValue = Object.keys(newValue).map((key) => {
return {
key,
type: typeof newValue[key] === 'string' ? ChatVarType.String : ChatVarType.Number,
value: newValue[key],
}
})
setObjectValue(newObjectValue)
}
catch {
// ignore JSON.parse errors
}
}
}
}
if (type === ChatVarType.ArrayString || type === ChatVarType.ArrayNumber) {
if (editInJSON) {
const newValue = (value?.length && value.filter(Boolean).length) ? value.filter(Boolean) : undefined
setValue(newValue)
if (!editorContent)
setEditorContent(JSON.stringify(newValue))
}
else {
setValue(value?.length ? value : [undefined])
}
}
if (type === ChatVarType.ArrayBoolean) {
if (editInJSON)
setEditorContent(JSON.stringify(value.map((item: boolean) => item ? 'True' : 'False')))
}
setEditInJSON(editInJSON)
}
const handleEditorValueChange = (content: string) => {
if (!content) {
setEditorContent(content)
return setValue(undefined)
}
else {
setEditorContent(content)
try {
let newValue = JSON.parse(content)
if (type === ChatVarType.ArrayBoolean) {
newValue = newValue.map((item: string | boolean) => {
if (item === 'True' || item === 'true' || item === true)
return true
if (item === 'False' || item === 'false' || item === false)
return false
return undefined
}).filter((item?: boolean) => item !== undefined)
}
setValue(newValue)
}
catch {
// ignore JSON.parse errors
}
}
}
const handleSave = () => {
if (!checkVariableName(name))
return
const varList = workflowStore.getState().conversationVariables
if (!chatVar && varList.some(chatVar => chatVar.name === name))
return toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: t('chatVariable.modal.name', { ns: 'workflow' }) }))
if (type === ChatVarType.Object && objectValue.some(item => !item.key && !!item.value))
return toast.error(t('chatVariable.modal.objectKeyRequired', { ns: 'workflow' }))
onSave({
id: chatVar ? chatVar.id : uuid4(),
name,
value_type: type,
value: formatValue(value),
description,
})
onClose()
}
useEffect(() => {
if (chatVar) {
setName(chatVar.name)
setType(chatVar.value_type)
setValue(chatVar.value)
setDescription(chatVar.description)
setObjectValue(getObjectValue())
if (chatVar.value_type === ChatVarType.ArrayObject) {
setEditorContent(JSON.stringify(chatVar.value))
setEditInJSON(true)
}
else {
setEditInJSON(false)
}
}
}, [chatVar, getObjectValue])
return (
<div
className={cn('flex h-full w-[360px] flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-2xl', type === ChatVarType.Object && 'w-[480px]')}
@ -282,135 +88,49 @@ const ChatVariableModal = ({
</div>
</div>
<div className="max-h-[480px] overflow-y-auto px-4 py-2">
{/* name */}
<div className="mb-4">
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-semibold">{t('chatVariable.modal.name', { ns: 'workflow' })}</div>
<div className="flex">
<Input
placeholder={t('chatVariable.modal.namePlaceholder', { ns: 'workflow' }) || ''}
value={name}
onChange={handleVarNameChange}
onBlur={e => checkVariableName(e.target.value)}
type="text"
/>
</div>
</div>
{/* type */}
<div className="mb-4">
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-semibold">{t('chatVariable.modal.type', { ns: 'workflow' })}</div>
<div className="flex">
<VariableTypeSelector
value={type}
list={typeList}
onSelect={handleTypeChange}
popupClassName="w-[327px]"
/>
</div>
</div>
{/* default value */}
<div className="mb-4">
<div className="mb-1 flex h-6 items-center justify-between text-text-secondary system-sm-semibold">
<div>{t('chatVariable.modal.value', { ns: 'workflow' })}</div>
{(type === ChatVarType.ArrayString || type === ChatVarType.ArrayNumber || type === ChatVarType.ArrayBoolean) && (
<Button
variant="ghost"
size="small"
className="text-text-tertiary"
onClick={() => handleEditorChange(!editInJSON)}
>
{editInJSON ? <RiInputField className="mr-1 h-3.5 w-3.5" /> : <RiDraftLine className="mr-1 h-3.5 w-3.5" />}
{editInJSON ? t('chatVariable.modal.oneByOne', { ns: 'workflow' }) : t('chatVariable.modal.editInJSON', { ns: 'workflow' })}
</Button>
)}
{type === ChatVarType.Object && (
<Button
variant="ghost"
size="small"
className="text-text-tertiary"
onClick={() => handleEditorChange(!editInJSON)}
>
{editInJSON ? <RiInputField className="mr-1 h-3.5 w-3.5" /> : <RiDraftLine className="mr-1 h-3.5 w-3.5" />}
{editInJSON ? t('chatVariable.modal.editInForm', { ns: 'workflow' }) : t('chatVariable.modal.editInJSON', { ns: 'workflow' })}
</Button>
)}
</div>
<div className="flex">
{type === ChatVarType.String && (
// Input will remove \n\r, so use Textarea just like description area
<textarea
className="block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none system-sm-regular placeholder:text-components-input-text-placeholder placeholder:system-sm-regular hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs"
value={value}
placeholder={t('chatVariable.modal.valuePlaceholder', { ns: 'workflow' }) || ''}
onChange={e => setValue(e.target.value)}
/>
)}
{type === ChatVarType.Number && (
<Input
placeholder={t('chatVariable.modal.valuePlaceholder', { ns: 'workflow' }) || ''}
value={value}
onChange={e => setValue(Number(e.target.value))}
type="number"
/>
)}
{type === ChatVarType.Boolean && (
<BoolValue
value={value}
onChange={setValue}
/>
)}
{type === ChatVarType.Object && !editInJSON && (
<ObjectValueList
list={objectValue}
onChange={setObjectValue}
/>
)}
{type === ChatVarType.ArrayString && !editInJSON && (
<ArrayValueList
isString
list={value || [undefined]}
onChange={setValue}
/>
)}
{type === ChatVarType.ArrayNumber && !editInJSON && (
<ArrayValueList
isString={false}
list={value || [undefined]}
onChange={setValue}
/>
)}
{type === ChatVarType.ArrayBoolean && !editInJSON && (
<ArrayBoolList
list={value || [true]}
onChange={setValue}
/>
)}
{editInJSON && (
<div className="w-full rounded-[10px] bg-components-input-bg-normal py-2 pl-3 pr-1" style={{ height: editorMinHeight }}>
<CodeEditor
isExpand
noWrapper
language={CodeLanguage.json}
value={editorContent}
placeholder={<div className="whitespace-pre">{placeholder}</div>}
onChange={handleEditorValueChange}
/>
</div>
)}
</div>
</div>
{/* description */}
<div className="">
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-semibold">{t('chatVariable.modal.description', { ns: 'workflow' })}</div>
<div className="flex">
<textarea
className="block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none system-sm-regular placeholder:text-components-input-text-placeholder placeholder:system-sm-regular hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs"
value={description}
placeholder={t('chatVariable.modal.descriptionPlaceholder', { ns: 'workflow' }) || ''}
onChange={e => setDescription(e.target.value)}
/>
</div>
</div>
<NameSection
name={name}
onBlur={nextName => validateVariableName({ name: nextName, notify, t })}
onChange={handleNameChange}
placeholder={t('chatVariable.modal.namePlaceholder', { ns: 'workflow' }) || ''}
title={t('chatVariable.modal.name', { ns: 'workflow' })}
/>
<TypeSection
type={type}
list={typeList}
onSelect={handleTypeChange}
title={t('chatVariable.modal.type', { ns: 'workflow' })}
/>
<ValueSection
type={type}
value={value}
objectValue={objectValue}
editInJSON={editInJSON}
editorContent={editorContent}
editorMinHeight={editorMinHeight}
onArrayBoolChange={setValue}
onArrayChange={type === ChatVarType.String || type === ChatVarType.Number ? handleStringOrNumberChange : setValue}
onEditorChange={handleEditorChange}
onEditorValueChange={handleEditorValueChange}
onObjectChange={setObjectValue}
onValueChange={setValue}
placeholder={placeholder}
t={t}
toggleLabelKey={
type === ChatVarType.Object
|| type === ChatVarType.ArrayString
|| type === ChatVarType.ArrayNumber
|| type === ChatVarType.ArrayBoolean
? getEditorToggleLabelKey(type, editInJSON)
: undefined
}
/>
<DescriptionSection
description={description}
onChange={setDescription}
placeholder={t('chatVariable.modal.descriptionPlaceholder', { ns: 'workflow' }) || ''}
title={t('chatVariable.modal.description', { ns: 'workflow' })}
/>
</div>
<div className="flex flex-row-reverse rounded-b-2xl p-4 pt-2">
<div className="flex gap-2">