feat: handle tool output ui

This commit is contained in:
Joel 2025-03-31 16:13:06 +08:00
parent 1193ab12fc
commit 722d35e9e7
6 changed files with 92 additions and 40 deletions

View File

@ -3,6 +3,8 @@ import type { FC, ReactNode } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
import TreeIndentLine from './variable/object-child-tree-panel/tree-indent-line'
import cn from '@/utils/classnames'
type Props = {
className?: string
@ -41,6 +43,7 @@ type VarItemProps = {
type: string
description: string
}[]
isIndent?: boolean
}
export const VarItem: FC<VarItemProps> = ({
@ -48,29 +51,33 @@ export const VarItem: FC<VarItemProps> = ({
type,
description,
subItems,
isIndent,
}) => {
return (
<div className='py-1'>
<div className='flex justify-between'>
<div className='flex items-center leading-[18px]'>
<div className='code-sm-semibold text-text-secondary'>{name}</div>
<div className='system-xs-regular ml-2 text-text-tertiary'>{type}</div>
</div>
</div>
<div className='system-xs-regular mt-0.5 text-text-tertiary'>
{description}
{subItems && (
<div className='ml-2 border-l border-gray-200 pl-2'>
{subItems.map((item, index) => (
<VarItem
key={index}
name={item.name}
type={item.type}
description={item.description}
/>
))}
<div className={cn('flex', isIndent && 'relative left-[-7px]')}>
{isIndent && <TreeIndentLine depth={1} />}
<div className='py-1'>
<div className='flex'>
<div className='flex items-center leading-[18px]'>
<div className='code-sm-semibold text-text-secondary'>{name}</div>
<div className='system-xs-regular ml-2 text-text-tertiary'>{type}</div>
</div>
)}
</div>
<div className='system-xs-regular mt-0.5 text-text-tertiary'>
{description}
{subItems && (
<div className='ml-2 border-l border-gray-200 pl-2'>
{subItems.map((item, index) => (
<VarItem
key={index}
name={item.name}
type={item.type}
description={item.description}
/>
))}
</div>
)}
</div>
</div>
</div>
)

View File

@ -15,6 +15,7 @@ type Props = {
payload: FieldType,
required: boolean,
depth?: number,
rootClassName?: string
}
const Field: FC<Props> = ({
@ -22,8 +23,10 @@ const Field: FC<Props> = ({
payload,
depth = 1,
required,
rootClassName,
}) => {
const { t } = useTranslation()
const isRoot = depth === 1
const hasChildren = payload.type === Type.object && payload.properties
const [fold, {
toggle: toggleFold,
@ -40,7 +43,7 @@ const Field: FC<Props> = ({
onClick={toggleFold}
/>
)}
<div className='system-sm-medium h-6 truncate leading-6 text-text-secondary'>{name}</div>
<div className={cn('system-sm-medium h-6 truncate leading-6 text-text-secondary', isRoot && rootClassName)}>{name}</div>
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>{getFieldType(payload)}</div>
{required && <div className='system-2xs-medium-uppercase ml-3 leading-6 text-text-warning'>{t('app.structOutput.required')}</div>}
</div>

View File

@ -7,10 +7,12 @@ import { useTranslation } from 'react-i18next'
type Props = {
payload: StructuredOutput
rootClassName?: string
}
const ShowPanel: FC<Props> = ({
payload,
rootClassName,
}) => {
const { t } = useTranslation()
const schema = {
@ -28,6 +30,7 @@ const ShowPanel: FC<Props> = ({
name={name}
payload={schema.schema.properties![name]}
required={!!schema.schema.required?.includes(name)}
rootClassName={rootClassName}
/>
))}
</div>

View File

@ -4,15 +4,17 @@ import React from 'react'
import cn from '@/utils/classnames'
type Props = {
depth?: number
depth?: number,
className?: string,
}
const TreeIndentLine: FC<Props> = ({
depth = 1,
className,
}) => {
const depthArray = Array.from({ length: depth }, (_, index) => index)
return (
<div className='ml-2.5 mr-2.5 flex space-x-[12px]'>
<div className={cn('ml-2.5 mr-2.5 flex space-x-[12px]', className)}>
{depthArray.map(d => (
<div key={d} className={cn('w-px bg-divider-regular')}></div>
))}

View File

@ -18,6 +18,7 @@ import { useToolIcon } from '@/app/components/workflow/hooks'
import { useLogs } from '@/app/components/workflow/run/hooks'
import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log'
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { Type } from '../llm/types'
const i18nPrefix = 'workflow.nodes.tool'
@ -52,6 +53,7 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
handleStop,
runResult,
outputSchema,
hasObjectOutput,
} = useConfig(id, data)
const toolIcon = useToolIcon(data)
const logsParams = useLogs()
@ -135,27 +137,45 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
<>
<VarItem
name='text'
type='String'
type='string'
description={t(`${i18nPrefix}.outputVars.text`)}
isIndent={hasObjectOutput}
/>
<VarItem
name='files'
type='Array[File]'
type='array[file]'
description={t(`${i18nPrefix}.outputVars.files.title`)}
isIndent={hasObjectOutput}
/>
<VarItem
name='json'
type='Array[Object]'
type='array[object]'
description={t(`${i18nPrefix}.outputVars.json`)}
isIndent={hasObjectOutput}
/>
{outputSchema.map(outputItem => (
// <VarItem
// key={outputItem.name}
// name={outputItem.name}
// type={outputItem.type}
// description={outputItem.description}
// />
<StructureOutputItem payload={outputItem} />
<div key={outputItem.name}>
{outputItem.value?.type === 'object' ? (
<StructureOutputItem
rootClassName='code-sm-semibold text-text-secondary'
payload={{
schema: {
type: Type.object,
properties: {
[outputItem.name]: outputItem.value,
},
additionalProperties: false,
},
}} />
) : (
<VarItem
name={outputItem.name}
type={outputItem.type.toLocaleLowerCase()}
description={outputItem.description}
isIndent={hasObjectOutput}
/>
)}
</div>
))}
</>
</OutputVars>

View File

@ -262,17 +262,33 @@ const useConfig = (id: string, payload: ToolNodeType) => {
return []
Object.keys(output_schema.properties).forEach((outputKey) => {
const output = output_schema.properties[outputKey]
res.push({
name: outputKey,
type: output.type === 'array'
? `Array[${output.items?.type.slice(0, 1).toLocaleUpperCase()}${output.items?.type.slice(1)}]`
: `${output.type.slice(0, 1).toLocaleUpperCase()}${output.type.slice(1)}`,
description: output.description,
})
const type = output.type
if (type === 'object') {
res.push({
name: outputKey,
value: output,
})
}
else {
res.push({
name: outputKey,
type: output.type === 'array'
? `Array[${output.items?.type.slice(0, 1).toLocaleUpperCase()}${output.items?.type.slice(1)}]`
: `${output.type.slice(0, 1).toLocaleUpperCase()}${output.type.slice(1)}`,
description: output.description,
})
}
})
return res
}, [output_schema])
const hasObjectOutput = useMemo(() => {
if (!output_schema)
return false
const properties = output_schema.properties
return Object.keys(properties).some(key => properties[key].type === 'object')
}, [output_schema])
return {
readOnly,
inputs,
@ -302,6 +318,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
handleStop,
runResult,
outputSchema,
hasObjectOutput,
}
}