feat: last run frontend (#21369)
The frontend of feat: Persist Variables for Enhanced Debugging Workflow (#20699). Co-authored-by: jZonG <jzongcode@gmail.com>
This commit is contained in:
28
web/app/components/workflow/variable-inspect/empty.tsx
Normal file
28
web/app/components/workflow/variable-inspect/empty.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
|
||||
const Empty: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col gap-3 rounded-xl bg-background-section p-8'>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur-sm'>
|
||||
<Variable02 className='h-5 w-5 text-text-accent' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='system-sm-semibold text-text-secondary'>{t('workflow.debug.variableInspect.title')}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{t('workflow.debug.variableInspect.emptyTip')}</div>
|
||||
<a
|
||||
className='system-xs-regular cursor-pointer text-text-accent'
|
||||
href='https://docs.dify.ai/guides/workflow/debug-and-preview/variable-inspect'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'>
|
||||
{t('workflow.debug.variableInspect.emptyLink')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Empty
|
174
web/app/components/workflow/variable-inspect/group.tsx
Normal file
174
web/app/components/workflow/variable-inspect/group.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiDeleteBinLine,
|
||||
RiFileList3Line,
|
||||
RiLoader2Line,
|
||||
// RiErrorWarningFill,
|
||||
} from '@remixicon/react'
|
||||
// import Button from '@/app/components/base/button'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import {
|
||||
BubbleX,
|
||||
Env,
|
||||
} from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import type { currentVarType } from './panel'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useToolIcon } from '../hooks'
|
||||
|
||||
type Props = {
|
||||
nodeData?: NodeWithVar
|
||||
currentVar?: currentVarType
|
||||
varType: VarInInspectType
|
||||
varList: VarInInspect[]
|
||||
handleSelect: (state: any) => void
|
||||
handleView?: () => void
|
||||
handleClear?: () => void
|
||||
}
|
||||
|
||||
const Group = ({
|
||||
nodeData,
|
||||
currentVar,
|
||||
varType,
|
||||
varList,
|
||||
handleSelect,
|
||||
handleView,
|
||||
handleClear,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||
|
||||
const toolIcon = useToolIcon(nodeData?.nodePayload as any)
|
||||
|
||||
const isEnv = varType === VarInInspectType.environment
|
||||
const isChatVar = varType === VarInInspectType.conversation
|
||||
const isSystem = varType === VarInInspectType.system
|
||||
|
||||
const visibleVarList = isEnv ? varList : varList.filter(v => v.visible)
|
||||
|
||||
const handleSelectVar = (varItem: any, type?: string) => {
|
||||
if (type === VarInInspectType.environment) {
|
||||
handleSelect({
|
||||
nodeId: VarInInspectType.environment,
|
||||
title: VarInInspectType.environment,
|
||||
nodeType: VarInInspectType.environment,
|
||||
var: {
|
||||
...varItem,
|
||||
type: VarInInspectType.environment,
|
||||
...(varItem.value_type === 'secret' ? { value: '******************' } : {}),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (type === VarInInspectType.conversation) {
|
||||
handleSelect({
|
||||
nodeId: VarInInspectType.conversation,
|
||||
nodeType: VarInInspectType.conversation,
|
||||
title: VarInInspectType.conversation,
|
||||
var: {
|
||||
...varItem,
|
||||
type: VarInInspectType.conversation,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (type === VarInInspectType.system) {
|
||||
handleSelect({
|
||||
nodeId: VarInInspectType.system,
|
||||
nodeType: VarInInspectType.system,
|
||||
title: VarInInspectType.system,
|
||||
var: {
|
||||
...varItem,
|
||||
type: VarInInspectType.system,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!nodeData) return
|
||||
handleSelect({
|
||||
nodeId: nodeData.nodeId,
|
||||
nodeType: nodeData.nodeType,
|
||||
title: nodeData.title,
|
||||
var: varItem,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='p-0.5'>
|
||||
{/* node item */}
|
||||
<div className='group flex h-6 items-center gap-0.5'>
|
||||
<div className='h-3 w-3 shrink-0'>
|
||||
{nodeData?.isSingRunRunning && (
|
||||
<RiLoader2Line className='h-3 w-3 animate-spin text-text-accent' />
|
||||
)}
|
||||
{(!nodeData || !nodeData.isSingRunRunning) && visibleVarList.length > 0 && (
|
||||
<RiArrowRightSLine className={cn('h-3 w-3 text-text-tertiary', !isCollapsed && 'rotate-90')} onClick={() => setIsCollapsed(!isCollapsed)} />
|
||||
)}
|
||||
</div>
|
||||
<div className='flex grow cursor-pointer items-center gap-1' onClick={() => setIsCollapsed(!isCollapsed)}>
|
||||
{nodeData && (
|
||||
<>
|
||||
<BlockIcon
|
||||
className='shrink-0'
|
||||
type={nodeData.nodeType}
|
||||
toolIcon={toolIcon || ''}
|
||||
size='xs'
|
||||
/>
|
||||
<div className='system-xs-medium-uppercase truncate text-text-tertiary'>{nodeData.title}</div>
|
||||
</>
|
||||
)}
|
||||
{!nodeData && (
|
||||
<div className='system-xs-medium-uppercase truncate text-text-tertiary'>
|
||||
{isEnv && t('workflow.debug.variableInspect.envNode')}
|
||||
{isChatVar && t('workflow.debug.variableInspect.chatNode')}
|
||||
{isSystem && t('workflow.debug.variableInspect.systemNode')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{nodeData && !nodeData.isSingRunRunning && (
|
||||
<div className='hidden shrink-0 items-center group-hover:flex'>
|
||||
<Tooltip popupContent={t('workflow.debug.variableInspect.view')}>
|
||||
<ActionButton onClick={handleView}>
|
||||
<RiFileList3Line className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip popupContent={t('workflow.debug.variableInspect.clearNode')}>
|
||||
<ActionButton onClick={handleClear}>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* var item list */}
|
||||
{!isCollapsed && !nodeData?.isSingRunRunning && (
|
||||
<div className='px-0.5'>
|
||||
{visibleVarList.length > 0 && visibleVarList.map(varItem => (
|
||||
<div
|
||||
key={varItem.id}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer items-center gap-1 rounded-md px-3 py-1 hover:bg-state-base-hover',
|
||||
varItem.id === currentVar?.var?.id && 'bg-state-base-hover-alt hover:bg-state-base-hover-alt',
|
||||
)}
|
||||
onClick={() => handleSelectVar(varItem, varType)}
|
||||
>
|
||||
{isEnv && <Env className='h-4 w-4 shrink-0 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />}
|
||||
{(isSystem || nodeData) && <Variable02 className={cn('h-4 w-4 shrink-0 text-text-accent', ['error_type', 'error_message'].includes(varItem.name) && 'text-text-warning')} />}
|
||||
<div className='system-sm-medium grow truncate text-text-secondary'>{varItem.name}</div>
|
||||
<div className='system-xs-regular shrink-0 text-text-tertiary'>{varItem.value_type}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Group
|
61
web/app/components/workflow/variable-inspect/index.tsx
Normal file
61
web/app/components/workflow/variable-inspect/index.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { useStore } from '../store'
|
||||
import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel'
|
||||
import Panel from './panel'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const VariableInspectPanel: FC = () => {
|
||||
const showVariableInspectPanel = useStore(s => s.showVariableInspectPanel)
|
||||
const workflowCanvasHeight = useStore(s => s.workflowCanvasHeight)
|
||||
const variableInspectPanelHeight = useStore(s => s.variableInspectPanelHeight)
|
||||
const setVariableInspectPanelHeight = useStore(s => s.setVariableInspectPanelHeight)
|
||||
|
||||
const maxHeight = useMemo(() => {
|
||||
if (!workflowCanvasHeight)
|
||||
return 480
|
||||
return workflowCanvasHeight - 60
|
||||
}, [workflowCanvasHeight])
|
||||
|
||||
const handleResize = useCallback((width: number, height: number) => {
|
||||
localStorage.setItem('workflow-variable-inpsect-panel-height', `${height}`)
|
||||
setVariableInspectPanelHeight(height)
|
||||
}, [setVariableInspectPanelHeight])
|
||||
|
||||
const {
|
||||
triggerRef,
|
||||
containerRef,
|
||||
} = useResizePanel({
|
||||
direction: 'vertical',
|
||||
triggerDirection: 'top',
|
||||
minHeight: 120,
|
||||
maxHeight,
|
||||
onResize: debounce(handleResize),
|
||||
})
|
||||
|
||||
if (!showVariableInspectPanel)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className={cn('relative pb-1')}>
|
||||
<div
|
||||
ref={triggerRef}
|
||||
className='absolute -top-1 left-0 flex h-1 w-full cursor-row-resize resize-y items-center justify-center'>
|
||||
<div className='h-0.5 w-10 rounded-sm bg-state-base-handle hover:w-full hover:bg-state-accent-solid active:w-full active:bg-state-accent-solid'></div>
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl')}
|
||||
style={{ height: `${variableInspectPanelHeight}px` }}
|
||||
>
|
||||
<Panel />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VariableInspectPanel
|
111
web/app/components/workflow/variable-inspect/left.tsx
Normal file
111
web/app/components/workflow/variable-inspect/left.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
// import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useStore } from '../store'
|
||||
import Button from '@/app/components/base/button'
|
||||
// import ActionButton from '@/app/components/base/action-button'
|
||||
// import Tooltip from '@/app/components/base/tooltip'
|
||||
import Group from './group'
|
||||
import useCurrentVars from '../hooks/use-inspect-vars-crud'
|
||||
import { useNodesInteractions } from '../hooks/use-nodes-interactions'
|
||||
import type { currentVarType } from './panel'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
currentNodeVar?: currentVarType
|
||||
handleVarSelect: (state: any) => void
|
||||
}
|
||||
|
||||
const Left = ({
|
||||
currentNodeVar,
|
||||
handleVarSelect,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const environmentVariables = useStore(s => s.environmentVariables)
|
||||
const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId)
|
||||
|
||||
const {
|
||||
conversationVars,
|
||||
systemVars,
|
||||
nodesWithInspectVars,
|
||||
deleteAllInspectorVars,
|
||||
deleteNodeInspectorVars,
|
||||
} = useCurrentVars()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
|
||||
const showDivider = environmentVariables.length > 0 || conversationVars.length > 0 || systemVars.length > 0
|
||||
|
||||
const handleClearAll = () => {
|
||||
deleteAllInspectorVars()
|
||||
setCurrentFocusNodeId('')
|
||||
}
|
||||
|
||||
const handleClearNode = (nodeId: string) => {
|
||||
deleteNodeInspectorVars(nodeId)
|
||||
setCurrentFocusNodeId('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col')}>
|
||||
{/* header */}
|
||||
<div className='flex shrink-0 items-center justify-between gap-1 pl-4 pr-1 pt-2'>
|
||||
<div className='system-sm-semibold-uppercase truncate text-text-primary'>{t('workflow.debug.variableInspect.title')}</div>
|
||||
<Button variant='ghost' size='small' className='shrink-0' onClick={handleClearAll}>{t('workflow.debug.variableInspect.clearAll')}</Button>
|
||||
</div>
|
||||
{/* content */}
|
||||
<div className='grow overflow-y-auto py-1'>
|
||||
{/* group ENV */}
|
||||
{environmentVariables.length > 0 && (
|
||||
<Group
|
||||
varType={VarInInspectType.environment}
|
||||
varList={environmentVariables as VarInInspect[]}
|
||||
currentVar={currentNodeVar}
|
||||
handleSelect={handleVarSelect}
|
||||
/>
|
||||
)}
|
||||
{/* group CHAT VAR */}
|
||||
{conversationVars.length > 0 && (
|
||||
<Group
|
||||
varType={VarInInspectType.conversation}
|
||||
varList={conversationVars}
|
||||
currentVar={currentNodeVar}
|
||||
handleSelect={handleVarSelect}
|
||||
/>
|
||||
)}
|
||||
{/* group SYSTEM VAR */}
|
||||
{systemVars.length > 0 && (
|
||||
<Group
|
||||
varType={VarInInspectType.system}
|
||||
varList={systemVars}
|
||||
currentVar={currentNodeVar}
|
||||
handleSelect={handleVarSelect}
|
||||
/>
|
||||
)}
|
||||
{/* divider */}
|
||||
{showDivider && (
|
||||
<div className='px-4 py-1'>
|
||||
<div className='h-px bg-divider-subtle'></div>
|
||||
</div>
|
||||
)}
|
||||
{/* group nodes */}
|
||||
{nodesWithInspectVars.length > 0 && nodesWithInspectVars.map(group => (
|
||||
<Group
|
||||
key={group.nodeId}
|
||||
varType={VarInInspectType.node}
|
||||
varList={group.vars}
|
||||
nodeData={group}
|
||||
currentVar={currentNodeVar}
|
||||
handleSelect={handleVarSelect}
|
||||
handleView={() => handleNodeSelect(group.nodeId, false, true)}
|
||||
handleClear={() => handleClearNode(group.nodeId)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Left
|
188
web/app/components/workflow/variable-inspect/panel.tsx
Normal file
188
web/app/components/workflow/variable-inspect/panel.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
import { useStore } from '../store'
|
||||
import useCurrentVars from '../hooks/use-inspect-vars-crud'
|
||||
import Empty from './empty'
|
||||
import Left from './left'
|
||||
import Right from './right'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type currentVarType = {
|
||||
nodeId: string
|
||||
nodeType: string
|
||||
title: string
|
||||
isValueFetched?: boolean
|
||||
var: VarInInspect
|
||||
}
|
||||
|
||||
const Panel: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const bottomPanelWidth = useStore(s => s.bottomPanelWidth)
|
||||
const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel)
|
||||
const [showLeftPanel, setShowLeftPanel] = useState(true)
|
||||
|
||||
const environmentVariables = useStore(s => s.environmentVariables)
|
||||
const currentFocusNodeId = useStore(s => s.currentFocusNodeId)
|
||||
const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId)
|
||||
const [currentVarId, setCurrentVarId] = useState('')
|
||||
|
||||
const {
|
||||
conversationVars,
|
||||
systemVars,
|
||||
nodesWithInspectVars,
|
||||
fetchInspectVarValue,
|
||||
} = useCurrentVars()
|
||||
|
||||
const isEmpty = useMemo(() => {
|
||||
const allVars = [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars]
|
||||
return allVars.length === 0
|
||||
}, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars])
|
||||
|
||||
const currentNodeInfo = useMemo(() => {
|
||||
if (!currentFocusNodeId) return
|
||||
if (currentFocusNodeId === VarInInspectType.environment) {
|
||||
const currentVar = environmentVariables.find(v => v.id === currentVarId)
|
||||
const res = {
|
||||
nodeId: VarInInspectType.environment,
|
||||
title: VarInInspectType.environment,
|
||||
nodeType: VarInInspectType.environment,
|
||||
}
|
||||
if (currentVar) {
|
||||
return {
|
||||
...res,
|
||||
var: {
|
||||
...currentVar,
|
||||
type: VarInInspectType.environment,
|
||||
visible: true,
|
||||
...(currentVar.value_type === 'secret' ? { value: '******************' } : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
if (currentFocusNodeId === VarInInspectType.conversation) {
|
||||
const currentVar = conversationVars.find(v => v.id === currentVarId)
|
||||
const res = {
|
||||
nodeId: VarInInspectType.conversation,
|
||||
title: VarInInspectType.conversation,
|
||||
nodeType: VarInInspectType.conversation,
|
||||
}
|
||||
if (currentVar) {
|
||||
return {
|
||||
...res,
|
||||
var: {
|
||||
...currentVar,
|
||||
type: VarInInspectType.conversation,
|
||||
},
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
if (currentFocusNodeId === VarInInspectType.system) {
|
||||
const currentVar = systemVars.find(v => v.id === currentVarId)
|
||||
const res = {
|
||||
nodeId: VarInInspectType.system,
|
||||
title: VarInInspectType.system,
|
||||
nodeType: VarInInspectType.system,
|
||||
}
|
||||
if (currentVar) {
|
||||
return {
|
||||
...res,
|
||||
var: {
|
||||
...currentVar,
|
||||
type: VarInInspectType.system,
|
||||
},
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId)
|
||||
if (!targetNode) return
|
||||
const currentVar = targetNode.vars.find(v => v.id === currentVarId)
|
||||
return {
|
||||
nodeId: targetNode.nodeId,
|
||||
nodeType: targetNode.nodeType,
|
||||
title: targetNode.title,
|
||||
isSingRunRunning: targetNode.isSingRunRunning,
|
||||
isValueFetched: targetNode.isValueFetched,
|
||||
...(currentVar ? { var: currentVar } : {}),
|
||||
}
|
||||
}, [currentFocusNodeId, currentVarId, environmentVariables, conversationVars, systemVars, nodesWithInspectVars])
|
||||
|
||||
const isCurrentNodeVarValueFetching = useMemo(() => {
|
||||
if (!currentNodeInfo) return false
|
||||
const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentNodeInfo.nodeId)
|
||||
if (!targetNode) return false
|
||||
return !targetNode.isValueFetched
|
||||
}, [currentNodeInfo, nodesWithInspectVars])
|
||||
|
||||
const handleNodeVarSelect = useCallback((node: currentVarType) => {
|
||||
setCurrentFocusNodeId(node.nodeId)
|
||||
setCurrentVarId(node.var.id)
|
||||
}, [setCurrentFocusNodeId, setCurrentVarId])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentFocusNodeId && currentVarId) {
|
||||
const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId)
|
||||
if (targetNode && !targetNode.isValueFetched)
|
||||
fetchInspectVarValue([currentFocusNodeId])
|
||||
}
|
||||
}, [currentFocusNodeId, currentVarId, nodesWithInspectVars, fetchInspectVarValue])
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col')}>
|
||||
<div className='flex shrink-0 items-center justify-between pl-4 pr-2 pt-2'>
|
||||
<div className='system-sm-semibold-uppercase text-text-primary'>{t('workflow.debug.variableInspect.title')}</div>
|
||||
<ActionButton onClick={() => setShowVariableInspectPanel(false)}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className='grow p-2'>
|
||||
<Empty />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative flex h-full')}>
|
||||
{/* left */}
|
||||
{bottomPanelWidth < 488 && showLeftPanel && <div className='absolute left-0 top-0 h-full w-full' onClick={() => setShowLeftPanel(false)}></div>}
|
||||
<div
|
||||
className={cn(
|
||||
'w-60 shrink-0 border-r border-divider-burn',
|
||||
bottomPanelWidth < 488
|
||||
? showLeftPanel
|
||||
? 'absolute left-0 top-0 z-10 h-full w-[217px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-sm'
|
||||
: 'hidden'
|
||||
: 'block',
|
||||
)}
|
||||
>
|
||||
<Left
|
||||
currentNodeVar={currentNodeInfo as currentVarType}
|
||||
handleVarSelect={handleNodeVarSelect}
|
||||
/>
|
||||
</div>
|
||||
{/* right */}
|
||||
<div className='w-0 grow'>
|
||||
<Right
|
||||
isValueFetching={isCurrentNodeVarValueFetching}
|
||||
currentNodeVar={currentNodeInfo as currentVarType}
|
||||
handleOpenMenu={() => setShowLeftPanel(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Panel
|
150
web/app/components/workflow/variable-inspect/right.tsx
Normal file
150
web/app/components/workflow/variable-inspect/right.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowGoBackLine,
|
||||
RiCloseLine,
|
||||
RiMenuLine,
|
||||
} from '@remixicon/react'
|
||||
import { useStore } from '../store'
|
||||
import type { BlockEnum } from '../types'
|
||||
import useCurrentVars from '../hooks/use-inspect-vars-crud'
|
||||
import Empty from './empty'
|
||||
import ValueContent from './value-content'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import type { currentVarType } from './panel'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
currentNodeVar?: currentVarType
|
||||
handleOpenMenu: () => void
|
||||
isValueFetching?: boolean
|
||||
}
|
||||
|
||||
const Right = ({
|
||||
currentNodeVar,
|
||||
handleOpenMenu,
|
||||
isValueFetching,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const bottomPanelWidth = useStore(s => s.bottomPanelWidth)
|
||||
const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel)
|
||||
const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId)
|
||||
|
||||
const {
|
||||
resetConversationVar,
|
||||
resetToLastRunVar,
|
||||
editInspectVarValue,
|
||||
} = useCurrentVars()
|
||||
|
||||
const handleValueChange = (varId: string, value: any) => {
|
||||
if (!currentNodeVar) return
|
||||
editInspectVarValue(currentNodeVar.nodeId, varId, value)
|
||||
}
|
||||
|
||||
const resetValue = () => {
|
||||
if (!currentNodeVar) return
|
||||
resetToLastRunVar(currentNodeVar.nodeId, currentNodeVar.var.id)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setShowVariableInspectPanel(false)
|
||||
setCurrentFocusNodeId('')
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
if (!currentNodeVar) return
|
||||
resetConversationVar(currentNodeVar.var.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col')}>
|
||||
{/* header */}
|
||||
<div className='flex shrink-0 items-center justify-between gap-1 px-2 pt-2'>
|
||||
{bottomPanelWidth < 488 && (
|
||||
<ActionButton className='shrink-0' onClick={handleOpenMenu}>
|
||||
<RiMenuLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
<div className='flex w-0 grow items-center gap-1'>
|
||||
{currentNodeVar && (
|
||||
<>
|
||||
{currentNodeVar.nodeType === VarInInspectType.environment && (
|
||||
<Env className='h-4 w-4 shrink-0 text-util-colors-violet-violet-600' />
|
||||
)}
|
||||
{currentNodeVar.nodeType === VarInInspectType.conversation && (
|
||||
<BubbleX className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />
|
||||
)}
|
||||
{currentNodeVar.nodeType === VarInInspectType.system && (
|
||||
<Variable02 className='h-4 w-4 shrink-0 text-text-accent' />
|
||||
)}
|
||||
{currentNodeVar.nodeType !== VarInInspectType.environment && currentNodeVar.nodeType !== VarInInspectType.conversation && currentNodeVar.nodeType !== VarInInspectType.system && (
|
||||
<>
|
||||
<BlockIcon
|
||||
className='shrink-0'
|
||||
type={currentNodeVar.nodeType as BlockEnum}
|
||||
size='xs'
|
||||
/>
|
||||
<div className='system-sm-regular shrink-0 text-text-secondary'>{currentNodeVar.title}</div>
|
||||
<div className='system-sm-regular shrink-0 text-text-quaternary'>/</div>
|
||||
</>
|
||||
)}
|
||||
<div title={currentNodeVar.var.name} className='system-sm-semibold truncate text-text-secondary'>{currentNodeVar.var.name}</div>
|
||||
<div className='system-xs-medium ml-1 shrink-0 text-text-tertiary'>{currentNodeVar.var.value_type}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex shrink-0 items-center gap-1'>
|
||||
{currentNodeVar && (
|
||||
<>
|
||||
{currentNodeVar.var.edited && (
|
||||
<Badge>
|
||||
<span className='ml-[2.5px] mr-[4.5px] h-[3px] w-[3px] rounded bg-text-accent-secondary'></span>
|
||||
<span className='system-2xs-semibold-uupercase'>{t('workflow.debug.variableInspect.edited')}</span>
|
||||
</Badge>
|
||||
)}
|
||||
{currentNodeVar.var.edited && currentNodeVar.var.type !== VarInInspectType.conversation && (
|
||||
<Tooltip popupContent={t('workflow.debug.variableInspect.reset')}>
|
||||
<ActionButton onClick={resetValue}>
|
||||
<RiArrowGoBackLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentNodeVar.var.edited && currentNodeVar.var.type === VarInInspectType.conversation && (
|
||||
<Tooltip popupContent={t('workflow.debug.variableInspect.resetConversationVar')}>
|
||||
<ActionButton onClick={handleClear}>
|
||||
<RiArrowGoBackLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentNodeVar.var.value_type !== 'secret' && (
|
||||
<CopyFeedback content={currentNodeVar.var.value ? JSON.stringify(currentNodeVar.var.value) : ''} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<ActionButton onClick={handleClose}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
{/* content */}
|
||||
<div className='grow p-2'>
|
||||
{!currentNodeVar && <Empty />}
|
||||
{isValueFetching && (
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
{currentNodeVar && !isValueFetching && <ValueContent currentVar={currentNodeVar.var} handleValueChange={handleValueChange} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Right
|
113
web/app/components/workflow/variable-inspect/trigger.tsx
Normal file
113
web/app/components/workflow/variable-inspect/trigger.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { FC } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiLoader2Line, RiStopCircleFill } from '@remixicon/react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useStore } from '../store'
|
||||
import useCurrentVars from '../hooks/use-inspect-vars-crud'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const VariableInspectTrigger: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
const showVariableInspectPanel = useStore(s => s.showVariableInspectPanel)
|
||||
const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel)
|
||||
|
||||
const environmentVariables = useStore(s => s.environmentVariables)
|
||||
const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId)
|
||||
const {
|
||||
conversationVars,
|
||||
systemVars,
|
||||
nodesWithInspectVars,
|
||||
deleteAllInspectorVars,
|
||||
} = useCurrentVars()
|
||||
const currentVars = useMemo(() => {
|
||||
const allVars = [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars]
|
||||
return allVars
|
||||
}, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars])
|
||||
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const isStepRunning = useMemo(() => nodes.some(node => node.data._singleRunningStatus === NodeRunningStatus.Running), [nodes])
|
||||
const isPreviewRunning = useMemo(() => {
|
||||
if (!workflowRunningData)
|
||||
return false
|
||||
return workflowRunningData.result.status === WorkflowRunningStatus.Running
|
||||
}, [workflowRunningData])
|
||||
const isRunning = useMemo(() => isPreviewRunning || isStepRunning, [isPreviewRunning, isStepRunning])
|
||||
|
||||
const handleStop = () => {
|
||||
eventEmitter?.emit({
|
||||
type: EVENT_WORKFLOW_STOP,
|
||||
} as any)
|
||||
}
|
||||
|
||||
const handleClearAll = () => {
|
||||
deleteAllInspectorVars()
|
||||
setCurrentFocusNodeId('')
|
||||
}
|
||||
|
||||
if (showVariableInspectPanel)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1')}>
|
||||
{!isRunning && !currentVars.length && (
|
||||
<div
|
||||
className='system-2xs-semibold-uppercase flex h-5 cursor-pointer items-center gap-1 rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-2 text-text-tertiary shadow-lg backdrop-blur-sm hover:bg-background-default-hover'
|
||||
onClick={() => setShowVariableInspectPanel(true)}
|
||||
>
|
||||
{t('workflow.debug.variableInspect.trigger.normal')}
|
||||
</div>
|
||||
)}
|
||||
{!isRunning && currentVars.length > 0 && (
|
||||
<>
|
||||
<div
|
||||
className='system-xs-medium flex h-6 cursor-pointer items-center gap-1 rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-2 text-text-accent shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent'
|
||||
onClick={() => setShowVariableInspectPanel(true)}
|
||||
>
|
||||
{t('workflow.debug.variableInspect.trigger.cached')}
|
||||
</div>
|
||||
<div
|
||||
className='system-xs-medium flex h-6 cursor-pointer items-center rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-1 text-text-tertiary shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent hover:text-text-accent'
|
||||
onClick={handleClearAll}
|
||||
>
|
||||
{t('workflow.debug.variableInspect.trigger.clear')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isRunning && (
|
||||
<>
|
||||
<div
|
||||
className='system-xs-medium flex h-6 cursor-pointer items-center gap-1 rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-2 text-text-accent shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent'
|
||||
onClick={() => setShowVariableInspectPanel(true)}
|
||||
>
|
||||
<RiLoader2Line className='h-4 w-4' />
|
||||
<span className='text-text-accent'>{t('workflow.debug.variableInspect.trigger.running')}</span>
|
||||
</div>
|
||||
{isPreviewRunning && (
|
||||
<Tooltip
|
||||
popupContent={t('workflow.debug.variableInspect.trigger.stop')}
|
||||
>
|
||||
<div
|
||||
className='flex h-6 cursor-pointer items-center rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-1 shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent'
|
||||
onClick={handleStop}
|
||||
>
|
||||
<RiStopCircleFill className='h-4 w-4 text-text-accent' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VariableInspectTrigger
|
1
web/app/components/workflow/variable-inspect/types.ts
Normal file
1
web/app/components/workflow/variable-inspect/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const EVENT_WORKFLOW_STOP = 'WORKFLOW_STOP'
|
33
web/app/components/workflow/variable-inspect/utils.tsx
Normal file
33
web/app/components/workflow/variable-inspect/utils.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const arrayStringSchemaParttern = z.array(z.string())
|
||||
const arrayNumberSchemaParttern = z.array(z.number())
|
||||
|
||||
// # jsonSchema from https://zod.dev/?id=json-type
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()])
|
||||
type Literal = z.infer<typeof literalSchema>
|
||||
type Json = Literal | { [key: string]: Json } | Json[]
|
||||
const jsonSchema: z.ZodType<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]))
|
||||
const arrayJsonSchema: z.ZodType<Json[]> = z.lazy(() => z.array(jsonSchema))
|
||||
|
||||
export const validateJSONSchema = (schema: any, type: string) => {
|
||||
if (type === 'array[string]') {
|
||||
const result = arrayStringSchemaParttern.safeParse(schema)
|
||||
return result
|
||||
}
|
||||
else if (type === 'array[number]') {
|
||||
const result = arrayNumberSchemaParttern.safeParse(schema)
|
||||
return result
|
||||
}
|
||||
else if (type === 'object') {
|
||||
const result = jsonSchema.safeParse(schema)
|
||||
return result
|
||||
}
|
||||
else if (type === 'array[object]') {
|
||||
const result = arrayJsonSchema.safeParse(schema)
|
||||
return result
|
||||
}
|
||||
else {
|
||||
return { success: true } as any
|
||||
}
|
||||
}
|
225
web/app/components/workflow/variable-inspect/value-content.tsx
Normal file
225
web/app/components/workflow/variable-inspect/value-content.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message'
|
||||
import {
|
||||
checkJsonSchemaDepth,
|
||||
getValidationErrorMessage,
|
||||
validateSchemaAgainstDraft7,
|
||||
} from '@/app/components/workflow/nodes/llm/utils'
|
||||
import {
|
||||
validateJSONSchema,
|
||||
} from '@/app/components/workflow/variable-inspect/utils'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
currentVar: VarInInspect
|
||||
handleValueChange: (varId: string, value: any) => void
|
||||
}
|
||||
|
||||
const ValueContent = ({
|
||||
currentVar,
|
||||
handleValueChange,
|
||||
}: Props) => {
|
||||
const contentContainerRef = useRef<HTMLDivElement>(null)
|
||||
const errorMessageRef = useRef<HTMLDivElement>(null)
|
||||
const [editorHeight, setEditorHeight] = useState(0)
|
||||
const showTextEditor = currentVar.value_type === 'secret' || currentVar.value_type === 'string' || currentVar.value_type === 'number'
|
||||
const isSysFiles = currentVar.type === VarInInspectType.system && currentVar.name === 'files'
|
||||
const showJSONEditor = !isSysFiles && (currentVar.value_type === 'object' || currentVar.value_type === 'array[string]' || currentVar.value_type === 'array[number]' || currentVar.value_type === 'array[object]' || currentVar.value_type === 'array[any]')
|
||||
const showFileEditor = isSysFiles || currentVar.value_type === 'file' || currentVar.value_type === 'array[file]'
|
||||
const textEditorDisabled = currentVar.type === VarInInspectType.environment || (currentVar.type === VarInInspectType.system && currentVar.name !== 'query' && currentVar.name !== 'files')
|
||||
const JSONEditorDisabled = currentVar.value_type === 'array[any]'
|
||||
|
||||
const formatFileValue = (value: VarInInspect) => {
|
||||
if (value.value_type === 'file')
|
||||
return value.value ? getProcessedFilesFromResponse([value.value]) : []
|
||||
if (value.value_type === 'array[file]' || (value.type === VarInInspectType.system && currentVar.name === 'files'))
|
||||
return value.value && value.value.length > 0 ? getProcessedFilesFromResponse(value.value) : []
|
||||
return []
|
||||
}
|
||||
|
||||
const [value, setValue] = useState<any>()
|
||||
const [json, setJson] = useState('')
|
||||
const [parseError, setParseError] = useState<Error | null>(null)
|
||||
const [validationError, setValidationError] = useState<string>('')
|
||||
const fileFeature = useFeatures(s => s.features.file)
|
||||
const [fileValue, setFileValue] = useState<any>(formatFileValue(currentVar))
|
||||
|
||||
const { run: debounceValueChange } = useDebounceFn(handleValueChange, { wait: 500 })
|
||||
|
||||
// update default value when id changed
|
||||
useEffect(() => {
|
||||
if (showTextEditor) {
|
||||
if (currentVar.value_type === 'number')
|
||||
return setValue(JSON.stringify(currentVar.value))
|
||||
if (!currentVar.value)
|
||||
return setValue('')
|
||||
setValue(currentVar.value)
|
||||
}
|
||||
if (showJSONEditor)
|
||||
setJson(currentVar.value ? JSON.stringify(currentVar.value, null, 2) : '')
|
||||
|
||||
if (showFileEditor)
|
||||
setFileValue(formatFileValue(currentVar))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentVar.id, currentVar.value])
|
||||
|
||||
const handleTextChange = (value: string) => {
|
||||
if (currentVar.value_type === 'string')
|
||||
setValue(value)
|
||||
|
||||
if (currentVar.value_type === 'number') {
|
||||
if (/^-?\d+(\.)?(\d+)?$/.test(value))
|
||||
setValue(Number.parseFloat(value))
|
||||
}
|
||||
const newValue = currentVar.value_type === 'number' ? Number.parseFloat(value) : value
|
||||
debounceValueChange(currentVar.id, newValue)
|
||||
}
|
||||
|
||||
const jsonValueValidate = (value: string, type: string) => {
|
||||
try {
|
||||
const newJSONSchema = JSON.parse(value)
|
||||
setParseError(null)
|
||||
const result = validateJSONSchema(newJSONSchema, type)
|
||||
if (!result.success) {
|
||||
setValidationError(result.error.message)
|
||||
return false
|
||||
}
|
||||
if (type === 'object' || type === 'array[object]') {
|
||||
const schemaDepth = checkJsonSchemaDepth(newJSONSchema)
|
||||
if (schemaDepth > JSON_SCHEMA_MAX_DEPTH) {
|
||||
setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
|
||||
return false
|
||||
}
|
||||
const validationErrors = validateSchemaAgainstDraft7(newJSONSchema)
|
||||
if (validationErrors.length > 0) {
|
||||
setValidationError(getValidationErrorMessage(validationErrors))
|
||||
return false
|
||||
}
|
||||
}
|
||||
setValidationError('')
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
setValidationError('')
|
||||
if (error instanceof Error) {
|
||||
setParseError(error)
|
||||
return false
|
||||
}
|
||||
else {
|
||||
setParseError(new Error('Invalid JSON'))
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditorChange = (value: string) => {
|
||||
setJson(value)
|
||||
if (jsonValueValidate(value, currentVar.value_type)) {
|
||||
const parsed = JSON.parse(value)
|
||||
debounceValueChange(currentVar.id, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
const fileValueValidate = (fileList: any[]) => fileList.every(file => file.upload_file_id)
|
||||
|
||||
const handleFileChange = (value: any[]) => {
|
||||
setFileValue(value)
|
||||
// check every file upload progress
|
||||
// invoke update api after every file uploaded
|
||||
if (!fileValueValidate(value))
|
||||
return
|
||||
if (currentVar.value_type === 'file')
|
||||
debounceValueChange(currentVar.id, value[0])
|
||||
if (currentVar.value_type === 'array[file]' || isSysFiles)
|
||||
debounceValueChange(currentVar.id, value)
|
||||
}
|
||||
|
||||
// get editor height
|
||||
useEffect(() => {
|
||||
if (contentContainerRef.current && errorMessageRef.current) {
|
||||
const errorMessageObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { inlineSize } = entry.borderBoxSize[0]
|
||||
const height = (contentContainerRef.current as any).clientHeight - inlineSize
|
||||
setEditorHeight(height)
|
||||
}
|
||||
})
|
||||
errorMessageObserver.observe(errorMessageRef.current)
|
||||
return () => {
|
||||
errorMessageObserver.disconnect()
|
||||
}
|
||||
}
|
||||
}, [setEditorHeight])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentContainerRef}
|
||||
className='flex h-full flex-col'
|
||||
>
|
||||
<div className={cn('grow')} style={{ height: `${editorHeight}px` }}>
|
||||
{showTextEditor && (
|
||||
<Textarea
|
||||
readOnly={textEditorDisabled}
|
||||
disabled={textEditorDisabled}
|
||||
className='h-full'
|
||||
value={value as any}
|
||||
onChange={e => handleTextChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{showJSONEditor && (
|
||||
<SchemaEditor
|
||||
readonly={JSONEditorDisabled}
|
||||
className='overflow-y-auto'
|
||||
hideTopMenu
|
||||
schema={json}
|
||||
onUpdate={handleEditorChange}
|
||||
/>
|
||||
)}
|
||||
{showFileEditor && (
|
||||
<div className='max-w-[460px]'>
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={fileValue}
|
||||
onChange={files => handleFileChange(getProcessedFiles(files))}
|
||||
fileConfig={{
|
||||
allowed_file_types: [
|
||||
SupportUploadFileTypes.image,
|
||||
SupportUploadFileTypes.document,
|
||||
SupportUploadFileTypes.audio,
|
||||
SupportUploadFileTypes.video,
|
||||
],
|
||||
allowed_file_extensions: [
|
||||
...FILE_EXTS[SupportUploadFileTypes.image],
|
||||
...FILE_EXTS[SupportUploadFileTypes.document],
|
||||
...FILE_EXTS[SupportUploadFileTypes.audio],
|
||||
...FILE_EXTS[SupportUploadFileTypes.video],
|
||||
],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
number_limits: currentVar.value_type === 'file' ? 1 : (fileFeature as any).fileUploadConfig?.workflow_file_upload_limit || 5,
|
||||
fileUploadConfig: (fileFeature as any).fileUploadConfig,
|
||||
}}
|
||||
isDisabled={textEditorDisabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div ref={errorMessageRef} className='shrink-0'>
|
||||
{parseError && <ErrorMessage className='mt-1' message={parseError.message} />}
|
||||
{validationError && <ErrorMessage className='mt-1' message={validationError} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ValueContent
|
Reference in New Issue
Block a user