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 React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse' 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 = { type Props = {
className?: string className?: string
@ -41,6 +43,7 @@ type VarItemProps = {
type: string type: string
description: string description: string
}[] }[]
isIndent?: boolean
} }
export const VarItem: FC<VarItemProps> = ({ export const VarItem: FC<VarItemProps> = ({
@ -48,29 +51,33 @@ export const VarItem: FC<VarItemProps> = ({
type, type,
description, description,
subItems, subItems,
isIndent,
}) => { }) => {
return ( return (
<div className='py-1'> <div className={cn('flex', isIndent && 'relative left-[-7px]')}>
<div className='flex justify-between'> {isIndent && <TreeIndentLine depth={1} />}
<div className='flex items-center leading-[18px]'> <div className='py-1'>
<div className='code-sm-semibold text-text-secondary'>{name}</div> <div className='flex'>
<div className='system-xs-regular ml-2 text-text-tertiary'>{type}</div> <div className='flex items-center leading-[18px]'>
</div> <div className='code-sm-semibold text-text-secondary'>{name}</div>
</div> <div className='system-xs-regular ml-2 text-text-tertiary'>{type}</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 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>
</div> </div>
) )

View File

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

View File

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

View File

@ -4,15 +4,17 @@ import React from 'react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type Props = { type Props = {
depth?: number depth?: number,
className?: string,
} }
const TreeIndentLine: FC<Props> = ({ const TreeIndentLine: FC<Props> = ({
depth = 1, depth = 1,
className,
}) => { }) => {
const depthArray = Array.from({ length: depth }, (_, index) => index) const depthArray = Array.from({ length: depth }, (_, index) => index)
return ( 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 => ( {depthArray.map(d => (
<div key={d} className={cn('w-px bg-divider-regular')}></div> <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 { useLogs } from '@/app/components/workflow/run/hooks'
import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log' 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 StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { Type } from '../llm/types'
const i18nPrefix = 'workflow.nodes.tool' const i18nPrefix = 'workflow.nodes.tool'
@ -52,6 +53,7 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
handleStop, handleStop,
runResult, runResult,
outputSchema, outputSchema,
hasObjectOutput,
} = useConfig(id, data) } = useConfig(id, data)
const toolIcon = useToolIcon(data) const toolIcon = useToolIcon(data)
const logsParams = useLogs() const logsParams = useLogs()
@ -135,27 +137,45 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
<> <>
<VarItem <VarItem
name='text' name='text'
type='String' type='string'
description={t(`${i18nPrefix}.outputVars.text`)} description={t(`${i18nPrefix}.outputVars.text`)}
isIndent={hasObjectOutput}
/> />
<VarItem <VarItem
name='files' name='files'
type='Array[File]' type='array[file]'
description={t(`${i18nPrefix}.outputVars.files.title`)} description={t(`${i18nPrefix}.outputVars.files.title`)}
isIndent={hasObjectOutput}
/> />
<VarItem <VarItem
name='json' name='json'
type='Array[Object]' type='array[object]'
description={t(`${i18nPrefix}.outputVars.json`)} description={t(`${i18nPrefix}.outputVars.json`)}
isIndent={hasObjectOutput}
/> />
{outputSchema.map(outputItem => ( {outputSchema.map(outputItem => (
// <VarItem <div key={outputItem.name}>
// key={outputItem.name} {outputItem.value?.type === 'object' ? (
// name={outputItem.name} <StructureOutputItem
// type={outputItem.type} rootClassName='code-sm-semibold text-text-secondary'
// description={outputItem.description} payload={{
// /> schema: {
<StructureOutputItem payload={outputItem} /> 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> </OutputVars>

View File

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