Merge branch 'feat/plugins' of https://github.com/langgenius/dify into feat/plugins
This commit is contained in:
commit
a562e6db89
@ -59,14 +59,12 @@ const PluginDetailPanel: FC<Props> = ({
|
||||
{!!detail.declaration.agent_strategy && <AgentStrategyList detail={detail} />}
|
||||
{!!detail.declaration.endpoint && <EndpointList detail={detail} />}
|
||||
{!!detail.declaration.model && <ModelList detail={detail} />}
|
||||
{false && (
|
||||
<div className='px-4 py-2'>
|
||||
<ToolSelector
|
||||
value={value}
|
||||
onSelect={item => testChange(item)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='px-4 py-2'>
|
||||
<ToolSelector
|
||||
value={value}
|
||||
onSelect={item => testChange(item)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
@ -3,6 +3,7 @@ import type { FC } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
RiArrowRightUpLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
@ -39,19 +40,19 @@ import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
value?: {
|
||||
provider: string
|
||||
provider_name: string
|
||||
tool_name: string
|
||||
description?: string
|
||||
parameters?: Record<string, any>
|
||||
extra?: Record<string, any>
|
||||
}
|
||||
disabled?: boolean
|
||||
placement?: Placement
|
||||
offset?: OffsetOptions
|
||||
onSelect: (tool: {
|
||||
provider: string
|
||||
provider_name: string
|
||||
tool_name: string
|
||||
description?: string
|
||||
parameters?: Record<string, any>
|
||||
extra?: Record<string, any>
|
||||
}) => void
|
||||
supportAddCustomTool?: boolean
|
||||
scope?: string
|
||||
@ -79,7 +80,7 @@ const ToolSelector: FC<Props> = ({
|
||||
const currentProvider = useMemo(() => {
|
||||
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
|
||||
return mergedTools.find((toolWithProvider) => {
|
||||
return toolWithProvider.id === value?.provider && toolWithProvider.tools.some(tool => tool.name === value?.tool_name)
|
||||
return toolWithProvider.id === value?.provider_name && toolWithProvider.tools.some(tool => tool.name === value?.tool_name)
|
||||
})
|
||||
}, [value, buildInTools, customTools, workflowTools])
|
||||
|
||||
@ -87,10 +88,12 @@ const ToolSelector: FC<Props> = ({
|
||||
const handleSelectTool = (tool: ToolDefaultValue) => {
|
||||
const paramValues = addDefaultValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas as any))
|
||||
const toolValue = {
|
||||
provider: tool.provider_id,
|
||||
provider_name: tool.provider_id,
|
||||
tool_name: tool.tool_name,
|
||||
description: '',
|
||||
parameters: paramValues,
|
||||
extra: {
|
||||
description: '',
|
||||
},
|
||||
}
|
||||
onSelect(toolValue)
|
||||
setIsShowChooseTool(false)
|
||||
@ -101,7 +104,10 @@ const ToolSelector: FC<Props> = ({
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onSelect({
|
||||
...value,
|
||||
description: e.target.value || '',
|
||||
extra: {
|
||||
...value?.extra,
|
||||
description: e.target.value || '',
|
||||
},
|
||||
} as any)
|
||||
}
|
||||
|
||||
@ -157,75 +163,122 @@ const ToolSelector: FC<Props> = ({
|
||||
/>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className="relative w-[361px] min-h-20 max-h-[642px] overflow-y-auto pb-2 rounded-xl backdrop-blur-sm bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg">
|
||||
<div className='px-4 pt-3.5 pb-1 text-text-primary system-xl-semibold'>{t('plugin.detailPanel.toolSelector.title')}</div>
|
||||
{/* base form */}
|
||||
<div className='px-4 py-2 flex flex-col gap-3'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='h-6 flex items-center system-sm-semibold text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div>
|
||||
<ToolPicker
|
||||
placement='bottom'
|
||||
offset={offset}
|
||||
trigger={
|
||||
<ToolTrigger
|
||||
open={isShowChooseTool}
|
||||
value={value}
|
||||
provider={currentProvider}
|
||||
<div className={cn('relative w-[361px] min-h-20 max-h-[642px] pb-4 rounded-xl backdrop-blur-sm bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg', !isShowSettingAuth && 'overflow-y-auto pb-2')}>
|
||||
{!isShowSettingAuth && (
|
||||
<>
|
||||
<div className='px-4 pt-3.5 pb-1 text-text-primary system-xl-semibold'>{t('plugin.detailPanel.toolSelector.title')}</div>
|
||||
{/* base form */}
|
||||
<div className='px-4 py-2 flex flex-col gap-3'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='h-6 flex items-center system-sm-semibold text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div>
|
||||
<ToolPicker
|
||||
placement='bottom'
|
||||
offset={offset}
|
||||
trigger={
|
||||
<ToolTrigger
|
||||
open={isShowChooseTool}
|
||||
value={value}
|
||||
provider={currentProvider}
|
||||
/>
|
||||
}
|
||||
isShow={isShowChooseTool}
|
||||
onShowChange={setIsShowChooseTool}
|
||||
disabled={false}
|
||||
supportAddCustomTool
|
||||
onSelect={handleSelectTool}
|
||||
scope={scope}
|
||||
/>
|
||||
}
|
||||
isShow={isShowChooseTool}
|
||||
onShowChange={setIsShowChooseTool}
|
||||
disabled={false}
|
||||
supportAddCustomTool
|
||||
onSelect={handleSelectTool}
|
||||
scope={scope}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='h-6 flex items-center system-sm-semibold text-text-secondary'>{t('plugin.detailPanel.toolSelector.descriptionLabel')}</div>
|
||||
<Textarea
|
||||
className='resize-none'
|
||||
placeholder={t('plugin.detailPanel.toolSelector.descriptionPlaceholder')}
|
||||
value={value?.description || ''}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* authorization */}
|
||||
{!isShowSettingAuth && currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && (
|
||||
<div className='px-4 pt-3 flex flex-col'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('plugin.detailPanel.toolSelector.auth')}</div>
|
||||
<Divider bgStyle='gradient' className='grow' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='h-6 flex items-center system-sm-semibold text-text-secondary'>{t('plugin.detailPanel.toolSelector.descriptionLabel')}</div>
|
||||
<Textarea
|
||||
className='resize-none'
|
||||
placeholder={t('plugin.detailPanel.toolSelector.descriptionPlaceholder')}
|
||||
value={value?.extra?.description || ''}
|
||||
onChange={handleDescriptionChange}
|
||||
disabled={!value?.provider_name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='py-2'>
|
||||
{!currentProvider.is_team_authorization && (
|
||||
<Button
|
||||
variant='primary'
|
||||
className={cn('shrink-0 w-full')}
|
||||
onClick={() => setShowSettingAuth(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('tools.auth.unauthorized')}
|
||||
</Button>
|
||||
)}
|
||||
{currentProvider.is_team_authorization && (
|
||||
<Button
|
||||
variant='secondary'
|
||||
className={cn('shrink-0 w-full')}
|
||||
onClick={() => setShowSettingAuth(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className='mr-2' color={'green'} />
|
||||
{t('tools.auth.authorized')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* authorization */}
|
||||
{currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && (
|
||||
<div className='px-4 pt-3 flex flex-col'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('plugin.detailPanel.toolSelector.auth')}</div>
|
||||
<Divider bgStyle='gradient' className='grow' />
|
||||
</div>
|
||||
<div className='py-2'>
|
||||
{!currentProvider.is_team_authorization && (
|
||||
<Button
|
||||
variant='primary'
|
||||
className={cn('shrink-0 w-full')}
|
||||
onClick={() => setShowSettingAuth(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('tools.auth.unauthorized')}
|
||||
</Button>
|
||||
)}
|
||||
{currentProvider.is_team_authorization && (
|
||||
<Button
|
||||
variant='secondary'
|
||||
className={cn('shrink-0 w-full')}
|
||||
onClick={() => setShowSettingAuth(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className='mr-2' color={'green'} />
|
||||
{t('tools.auth.authorized')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* tool settings */}
|
||||
{currentToolParams.length > 0 && currentProvider?.is_team_authorization && (
|
||||
<div className='px-4 pt-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('plugin.detailPanel.toolSelector.settings')}</div>
|
||||
<Divider bgStyle='gradient' className='grow' />
|
||||
</div>
|
||||
<div className='py-2'>
|
||||
<Form
|
||||
value={value?.parameters || {}}
|
||||
onChange={handleFormChange}
|
||||
formSchemas={formSchemas as any}
|
||||
isEditMode={true}
|
||||
showOnVariableMap={{}}
|
||||
validating={false}
|
||||
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
|
||||
fieldMoreInfo={item => item.url
|
||||
? (<a
|
||||
href={item.url}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='inline-flex items-center text-xs text-text-accent'
|
||||
>
|
||||
{t('tools.howToGet')}
|
||||
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
|
||||
</a>)
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* authorization panel */}
|
||||
{isShowSettingAuth && currentProvider && (
|
||||
<div className='px-4 pb-4 border-t border-divider-subtle'>
|
||||
<>
|
||||
<div className='relative pt-3.5 flex flex-col gap-1'>
|
||||
<div className='absolute -top-2 left-2 w-[345px] pt-2 rounded-t-xl backdrop-blur-sm bg-components-panel-bg-blur border-[0.5px] border-components-panel-border'></div>
|
||||
<div
|
||||
className='px-3 h-6 flex items-center gap-1 text-text-accent-secondary system-xs-semibold-uppercase cursor-pointer'
|
||||
onClick={() => setShowSettingAuth(false)}
|
||||
>
|
||||
<RiArrowLeftLine className='w-4 h-4' />
|
||||
BACK
|
||||
</div>
|
||||
<div className='px-4 text-text-primary system-xl-semibold'>{t('tools.auth.setupModalTitle')}</div>
|
||||
<div className='px-4 text-text-tertiary system-xs-regular'>{t('tools.auth.setupModalTitleDescription')}</div>
|
||||
</div>
|
||||
<ToolCredentialForm
|
||||
collection={currentProvider}
|
||||
onCancel={() => setShowSettingAuth(false)}
|
||||
@ -234,37 +287,7 @@ const ToolSelector: FC<Props> = ({
|
||||
credentials: value,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* tool settings */}
|
||||
{currentToolParams.length > 0 && currentProvider?.is_team_authorization && (
|
||||
<div className='px-4 pt-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('plugin.detailPanel.toolSelector.settings')}</div>
|
||||
<Divider bgStyle='gradient' className='grow' />
|
||||
</div>
|
||||
<div className='py-2'>
|
||||
<Form
|
||||
value={value?.parameters || {}}
|
||||
onChange={handleFormChange}
|
||||
formSchemas={formSchemas as any}
|
||||
isEditMode={true}
|
||||
showOnVariableMap={{}}
|
||||
validating={false}
|
||||
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
|
||||
fieldMoreInfo={item => item.url
|
||||
? (<a
|
||||
href={item.url}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='inline-flex items-center text-xs text-text-accent'
|
||||
>
|
||||
{t('tools.howToGet')}
|
||||
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
|
||||
</a>)
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
|
@ -53,33 +53,35 @@ const ToolCredentialForm: FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='h-full'>
|
||||
<>
|
||||
{!credentialSchema
|
||||
? <div className='pt-3'><Loading type='app' /></div>
|
||||
: (
|
||||
<>
|
||||
<Form
|
||||
value={tempCredential}
|
||||
onChange={(v) => {
|
||||
setTempCredential(v)
|
||||
}}
|
||||
formSchemas={credentialSchema}
|
||||
isEditMode={true}
|
||||
showOnVariableMap={{}}
|
||||
validating={false}
|
||||
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
|
||||
fieldMoreInfo={item => item.url
|
||||
? (<a
|
||||
href={item.url}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='inline-flex items-center text-xs text-primary-600'
|
||||
>
|
||||
{t('tools.howToGet')}
|
||||
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
|
||||
</a>)
|
||||
: null}
|
||||
/>
|
||||
<div className={cn('mt-1 flex justify-end')} >
|
||||
<div className='px-4 max-h-[464px] overflow-y-auto'>
|
||||
<Form
|
||||
value={tempCredential}
|
||||
onChange={(v) => {
|
||||
setTempCredential(v)
|
||||
}}
|
||||
formSchemas={credentialSchema}
|
||||
isEditMode={true}
|
||||
showOnVariableMap={{}}
|
||||
validating={false}
|
||||
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
|
||||
fieldMoreInfo={item => item.url
|
||||
? (<a
|
||||
href={item.url}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='inline-flex items-center text-xs text-text-accent'
|
||||
>
|
||||
{t('tools.howToGet')}
|
||||
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
|
||||
</a>)
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn('mt-1 flex justify-end px-4')} >
|
||||
<div className='flex space-x-2'>
|
||||
<Button onClick={onCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
@ -89,7 +91,7 @@ const ToolCredentialForm: FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
</div >
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(ToolCredentialForm)
|
||||
|
@ -14,7 +14,7 @@ type Props = {
|
||||
open: boolean
|
||||
provider?: ToolWithProvider
|
||||
value?: {
|
||||
provider: string
|
||||
provider_name: string
|
||||
tool_name: string
|
||||
}
|
||||
isConfigure?: boolean
|
||||
@ -31,9 +31,9 @@ const ToolTrigger = ({
|
||||
<div className={cn(
|
||||
'group flex items-center p-2 pl-3 bg-components-input-bg-normal rounded-lg cursor-pointer hover:bg-state-base-hover-alt',
|
||||
open && 'bg-state-base-hover-alt',
|
||||
value?.provider && 'pl-1.5 py-1.5',
|
||||
value?.provider_name && 'pl-1.5 py-1.5',
|
||||
)}>
|
||||
{value?.provider && provider && (
|
||||
{value?.provider_name && provider && (
|
||||
<div className='shrink-0 mr-1 p-px rounded-lg bg-components-panel-bg border border-components-panel-border'>
|
||||
<BlockIcon
|
||||
className='!w-4 !h-4'
|
||||
@ -45,7 +45,7 @@ const ToolTrigger = ({
|
||||
{value?.tool_name && (
|
||||
<div className='grow system-sm-medium text-components-input-text-filled'>{value.tool_name}</div>
|
||||
)}
|
||||
{!value?.provider && (
|
||||
{!value?.provider_name && (
|
||||
<div className='grow text-components-input-text-placeholder system-sm-regular'>
|
||||
{!isConfigure ? t('plugin.detailPanel.toolSelector.placeholder') : t('plugin.detailPanel.configureTool')}
|
||||
</div>
|
||||
|
@ -202,7 +202,7 @@ const ViewHistory = ({
|
||||
{`Test ${isChatMode ? 'Chat' : 'Run'}#${item.sequence_number}`}
|
||||
</div>
|
||||
<div className='flex items-center text-xs text-gray-500 leading-[18px]'>
|
||||
{item.created_by_account.name} · {formatTimeFromNow((item.finished_at || item.created_at) * 1000)}
|
||||
{item.created_by_account?.name} · {formatTimeFromNow((item.finished_at || item.created_at) * 1000)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -61,7 +61,7 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
|
||||
const { data: nodeList } = await fetchTracingList({
|
||||
url: `/apps/${appID}/workflow-runs/${runID}/node-executions`,
|
||||
})
|
||||
setList(formatNodeList(nodeList))
|
||||
setList(formatNodeList(nodeList, t))
|
||||
}
|
||||
catch (err) {
|
||||
notify({
|
||||
|
@ -11,13 +11,9 @@ import {
|
||||
RiArrowDownSLine,
|
||||
RiMenu4Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLogs } from './hooks'
|
||||
import NodePanel from './node'
|
||||
import SpecialResultPanel from './special-result-panel'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
type TracingPanelProps = {
|
||||
@ -27,145 +23,14 @@ type TracingPanelProps = {
|
||||
hideNodeProcessDetail?: boolean
|
||||
}
|
||||
|
||||
type TracingNodeProps = {
|
||||
id: string
|
||||
uniqueId: string
|
||||
isParallel: boolean
|
||||
data: NodeTracing | null
|
||||
children: TracingNodeProps[]
|
||||
parallelTitle?: string
|
||||
branchTitle?: string
|
||||
hideNodeInfo?: boolean
|
||||
hideNodeProcessDetail?: boolean
|
||||
}
|
||||
|
||||
function buildLogTree(nodes: NodeTracing[], t: (key: string) => string): TracingNodeProps[] {
|
||||
const rootNodes: TracingNodeProps[] = []
|
||||
const parallelStacks: { [key: string]: TracingNodeProps } = {}
|
||||
const levelCounts: { [key: string]: number } = {}
|
||||
const parallelChildCounts: { [key: string]: Set<string> } = {}
|
||||
let uniqueIdCounter = 0
|
||||
const getUniqueId = () => {
|
||||
uniqueIdCounter++
|
||||
return `unique-${uniqueIdCounter}`
|
||||
}
|
||||
|
||||
const getParallelTitle = (parentId: string | null): string => {
|
||||
const levelKey = parentId || 'root'
|
||||
if (!levelCounts[levelKey])
|
||||
levelCounts[levelKey] = 0
|
||||
|
||||
levelCounts[levelKey]++
|
||||
|
||||
const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
|
||||
const levelNumber = parentTitle ? Number.parseInt(parentTitle.split('-')[1]) + 1 : 1
|
||||
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
|
||||
return `${t('workflow.common.parallel')}-${levelNumber}${letter}`
|
||||
}
|
||||
|
||||
const getBranchTitle = (parentId: string | null, branchNum: number): string => {
|
||||
const levelKey = parentId || 'root'
|
||||
const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
|
||||
const levelNumber = parentTitle ? Number.parseInt(parentTitle.split('-')[1]) + 1 : 1
|
||||
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
|
||||
const branchLetter = String.fromCharCode(64 + branchNum)
|
||||
return `${t('workflow.common.branch')}-${levelNumber}${letter}-${branchLetter}`
|
||||
}
|
||||
|
||||
// Count parallel children (for figuring out if we need to use letters)
|
||||
for (const node of nodes) {
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
|
||||
if (parallel_id) {
|
||||
const parentKey = parent_parallel_id || 'root'
|
||||
if (!parallelChildCounts[parentKey])
|
||||
parallelChildCounts[parentKey] = new Set()
|
||||
|
||||
parallelChildCounts[parentKey].add(parallel_id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
const parent_parallel_start_node_id = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null
|
||||
|
||||
if (!parallel_id || node.node_type === BlockEnum.End) {
|
||||
rootNodes.push({
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
})
|
||||
}
|
||||
else {
|
||||
if (!parallelStacks[parallel_id]) {
|
||||
const newParallelGroup: TracingNodeProps = {
|
||||
id: parallel_id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: true,
|
||||
data: null,
|
||||
children: [],
|
||||
parallelTitle: '',
|
||||
}
|
||||
parallelStacks[parallel_id] = newParallelGroup
|
||||
|
||||
if (parent_parallel_id && parallelStacks[parent_parallel_id]) {
|
||||
const sameBranchIndex = parallelStacks[parent_parallel_id].children.findLastIndex(c =>
|
||||
c.data?.execution_metadata?.parallel_start_node_id === parent_parallel_start_node_id || c.data?.parallel_start_node_id === parent_parallel_start_node_id,
|
||||
)
|
||||
parallelStacks[parent_parallel_id].children.splice(sameBranchIndex + 1, 0, newParallelGroup)
|
||||
newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
|
||||
}
|
||||
else {
|
||||
newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
|
||||
rootNodes.push(newParallelGroup)
|
||||
}
|
||||
}
|
||||
const branchTitle = parallel_start_node_id === node.node_id ? getBranchTitle(parent_parallel_id, parallelStacks[parallel_id].children.length + 1) : ''
|
||||
if (branchTitle) {
|
||||
parallelStacks[parallel_id].children.push({
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
branchTitle,
|
||||
})
|
||||
}
|
||||
else {
|
||||
let sameBranchIndex = parallelStacks[parallel_id].children.findLastIndex(c =>
|
||||
c.data?.execution_metadata?.parallel_start_node_id === parallel_start_node_id || c.data?.parallel_start_node_id === parallel_start_node_id,
|
||||
)
|
||||
if (parallelStacks[parallel_id].children[sameBranchIndex + 1]?.isParallel)
|
||||
sameBranchIndex++
|
||||
|
||||
parallelStacks[parallel_id].children.splice(sameBranchIndex + 1, 0, {
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
branchTitle,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootNodes
|
||||
}
|
||||
|
||||
const TracingPanel: FC<TracingPanelProps> = ({
|
||||
list,
|
||||
className,
|
||||
hideNodeInfo = false,
|
||||
hideNodeProcessDetail = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const treeNodes = buildLogTree(list, t)
|
||||
const treeNodes = list
|
||||
console.log(treeNodes)
|
||||
const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set())
|
||||
const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)
|
||||
|
||||
@ -219,13 +84,16 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
setAgentResultList,
|
||||
} = useLogs()
|
||||
|
||||
const renderNode = (node: TracingNodeProps) => {
|
||||
if (node.isParallel) {
|
||||
|
||||
const renderNode = (node: NodeTracing) => {
|
||||
const isParallelFirstNode = !!node.parallelDetail?.isParallelStartNode
|
||||
if (isParallelFirstNode) {
|
||||
const parallelDetail = node.parallelDetail!
|
||||
const isCollapsed = collapsedNodes.has(node.id)
|
||||
const isHovered = hoveredParallel === node.id
|
||||
return (
|
||||
<div
|
||||
key={node.uniqueId}
|
||||
key={node.id}
|
||||
className="ml-4 mb-2 relative"
|
||||
data-parallel-id={node.id}
|
||||
onMouseEnter={() => handleParallelMouseEnter(node.id)}
|
||||
@ -242,7 +110,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
{isHovered ? <RiArrowDownSLine className="w-3 h-3" /> : <RiMenu4Line className="w-3 h-3 text-text-tertiary" />}
|
||||
</button>
|
||||
<div className="system-xs-semibold-uppercase text-text-secondary flex items-center">
|
||||
<span>{node.parallelTitle}</span>
|
||||
<span>{parallelDetail.parallelTitle}</span>
|
||||
</div>
|
||||
<div
|
||||
className="mx-2 grow h-px bg-divider-subtle"
|
||||
@ -254,7 +122,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
'absolute top-0 bottom-0 left-[5px] w-[2px]',
|
||||
isHovered ? 'bg-text-accent-secondary' : 'bg-divider-subtle',
|
||||
)}></div>
|
||||
{node.children.map(renderNode)}
|
||||
{parallelDetail.children!.map(renderNode)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -262,12 +130,12 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
else {
|
||||
const isHovered = hoveredParallel === node.id
|
||||
return (
|
||||
<div key={node.uniqueId}>
|
||||
<div key={node.id}>
|
||||
<div className={cn('pl-4 -mb-1.5 system-2xs-medium-uppercase', isHovered ? 'text-text-tertiary' : 'text-text-quaternary')}>
|
||||
{node.branchTitle}
|
||||
{node?.parallelDetail?.branchTitle}
|
||||
</div>
|
||||
<NodePanel
|
||||
nodeInfo={node.data!}
|
||||
nodeInfo={node!}
|
||||
onShowIterationDetail={handleShowIterationResultList}
|
||||
onShowRetryDetail={handleShowRetryResultList}
|
||||
onShowAgentResultList={setAgentResultList}
|
||||
|
@ -1,15 +1,22 @@
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatIterationNode from './iteration'
|
||||
import formatParallelNode from './parallel'
|
||||
import formatRetryNode from './retry'
|
||||
import formatAgentNode from './agent'
|
||||
|
||||
const formatToTracingNodeList = (list: NodeTracing[]) => {
|
||||
const formatToTracingNodeList = (list: NodeTracing[], t: any) => {
|
||||
const allItems = [...list].reverse()
|
||||
const formattedIterationList = formatIterationNode(allItems)
|
||||
const formattedRetryList = formatRetryNode(formattedIterationList)
|
||||
const formattedAgentList = formatAgentNode(formattedRetryList)
|
||||
/*
|
||||
* First handle not change list structure node
|
||||
* Because Handle struct node will put the node in different
|
||||
*/
|
||||
const formattedAgentList = formatAgentNode(allItems)
|
||||
const formattedRetryList = formatRetryNode(formattedAgentList) // retry one node
|
||||
// would change the structure of the list. Iteration and parallel can include each other.
|
||||
const formattedIterationList = formatIterationNode(formattedRetryList)
|
||||
const formattedParallelList = formatParallelNode(formattedIterationList, t)
|
||||
|
||||
const result = formattedAgentList
|
||||
const result = formattedParallelList
|
||||
// console.log(allItems)
|
||||
// console.log(result)
|
||||
|
||||
|
@ -0,0 +1,132 @@
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
type TracingNodeProps = {
|
||||
id: string
|
||||
uniqueId: string
|
||||
isParallel: boolean
|
||||
data: NodeTracing | null
|
||||
children: TracingNodeProps[]
|
||||
parallelTitle?: string
|
||||
branchTitle?: string
|
||||
hideNodeInfo?: boolean
|
||||
hideNodeProcessDetail?: boolean
|
||||
}
|
||||
|
||||
function buildLogTree(nodes: NodeTracing[], t: (key: string) => string): TracingNodeProps[] {
|
||||
const rootNodes: TracingNodeProps[] = []
|
||||
const parallelStacks: { [key: string]: TracingNodeProps } = {}
|
||||
const levelCounts: { [key: string]: number } = {}
|
||||
const parallelChildCounts: { [key: string]: Set<string> } = {}
|
||||
let uniqueIdCounter = 0
|
||||
const getUniqueId = () => {
|
||||
uniqueIdCounter++
|
||||
return `unique-${uniqueIdCounter}`
|
||||
}
|
||||
|
||||
const getParallelTitle = (parentId: string | null): string => {
|
||||
const levelKey = parentId || 'root'
|
||||
if (!levelCounts[levelKey])
|
||||
levelCounts[levelKey] = 0
|
||||
|
||||
levelCounts[levelKey]++
|
||||
|
||||
const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
|
||||
const levelNumber = parentTitle ? Number.parseInt(parentTitle.split('-')[1]) + 1 : 1
|
||||
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
|
||||
return `${t('workflow.common.parallel')}-${levelNumber}${letter}`
|
||||
}
|
||||
|
||||
const getBranchTitle = (parentId: string | null, branchNum: number): string => {
|
||||
const levelKey = parentId || 'root'
|
||||
const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
|
||||
const levelNumber = parentTitle ? Number.parseInt(parentTitle.split('-')[1]) + 1 : 1
|
||||
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
|
||||
const branchLetter = String.fromCharCode(64 + branchNum)
|
||||
return `${t('workflow.common.branch')}-${levelNumber}${letter}-${branchLetter}`
|
||||
}
|
||||
|
||||
// Count parallel children (for figuring out if we need to use letters)
|
||||
for (const node of nodes) {
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
|
||||
if (parallel_id) {
|
||||
const parentKey = parent_parallel_id || 'root'
|
||||
if (!parallelChildCounts[parentKey])
|
||||
parallelChildCounts[parentKey] = new Set()
|
||||
|
||||
parallelChildCounts[parentKey].add(parallel_id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
const parent_parallel_start_node_id = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null
|
||||
|
||||
if (!parallel_id || node.node_type === BlockEnum.End) {
|
||||
rootNodes.push({
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
})
|
||||
}
|
||||
else {
|
||||
if (!parallelStacks[parallel_id]) {
|
||||
const newParallelGroup: TracingNodeProps = {
|
||||
id: parallel_id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: true,
|
||||
data: null,
|
||||
children: [],
|
||||
parallelTitle: '',
|
||||
}
|
||||
parallelStacks[parallel_id] = newParallelGroup
|
||||
|
||||
if (parent_parallel_id && parallelStacks[parent_parallel_id]) {
|
||||
const sameBranchIndex = parallelStacks[parent_parallel_id].children.findLastIndex(c =>
|
||||
c.data?.execution_metadata?.parallel_start_node_id === parent_parallel_start_node_id || c.data?.parallel_start_node_id === parent_parallel_start_node_id,
|
||||
)
|
||||
parallelStacks[parent_parallel_id].children.splice(sameBranchIndex + 1, 0, newParallelGroup)
|
||||
newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
|
||||
}
|
||||
else {
|
||||
newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
|
||||
rootNodes.push(newParallelGroup)
|
||||
}
|
||||
}
|
||||
const branchTitle = parallel_start_node_id === node.node_id ? getBranchTitle(parent_parallel_id, parallelStacks[parallel_id].children.length + 1) : ''
|
||||
if (branchTitle) {
|
||||
parallelStacks[parallel_id].children.push({
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
branchTitle,
|
||||
})
|
||||
}
|
||||
else {
|
||||
let sameBranchIndex = parallelStacks[parallel_id].children.findLastIndex(c =>
|
||||
c.data?.execution_metadata?.parallel_start_node_id === parallel_start_node_id || c.data?.parallel_start_node_id === parallel_start_node_id,
|
||||
)
|
||||
if (parallelStacks[parallel_id].children[sameBranchIndex + 1]?.isParallel)
|
||||
sameBranchIndex++
|
||||
|
||||
parallelStacks[parallel_id].children.splice(sameBranchIndex + 1, 0, {
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
branchTitle,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootNodes
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
function addTitle({
|
||||
list, level, parallelNumRecord,
|
||||
}: {
|
||||
list: NodeTracing[], level: number, parallelNumRecord: Record<string, number>
|
||||
}, t: any) {
|
||||
let branchIndex = 0
|
||||
list.forEach((node) => {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
|
||||
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
|
||||
if (isNotInParallel)
|
||||
return
|
||||
|
||||
const isParallelStartNode = node.parallelDetail?.isParallelStartNode
|
||||
if (isParallelStartNode)
|
||||
parallelNumRecord.num++
|
||||
|
||||
const letter = parallelNumRecord.num > 1 ? String.fromCharCode(64 + level) : ''
|
||||
const parallelLevelInfo = `${parallelNumRecord.num}${letter}`
|
||||
|
||||
if (isParallelStartNode) {
|
||||
node.parallelDetail!.isParallelStartNode = true
|
||||
node.parallelDetail!.parallelTitle = `${t('workflow.common.parallel')}-${parallelLevelInfo}`
|
||||
}
|
||||
|
||||
const isBrachStartNode = parallel_start_node_id === node.node_id
|
||||
if (isBrachStartNode) {
|
||||
branchIndex++
|
||||
const branchLetter = String.fromCharCode(64 + branchIndex)
|
||||
if (!node.parallelDetail) {
|
||||
node.parallelDetail = {
|
||||
branchTitle: '',
|
||||
}
|
||||
}
|
||||
|
||||
node.parallelDetail!.branchTitle = `${t('workflow.common.branch')}-${parallelLevelInfo}-${branchLetter}`
|
||||
}
|
||||
|
||||
if (node.parallelDetail?.children && node.parallelDetail.children.length > 0) {
|
||||
addTitle({
|
||||
list: node.parallelDetail.children,
|
||||
level: level + 1,
|
||||
parallelNumRecord,
|
||||
}, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// list => group by parallel_id(parallel tree).
|
||||
const format = (list: NodeTracing[], t: any): NodeTracing[] => {
|
||||
const result: NodeTracing[] = [...list]
|
||||
const parallelFirstNodeMap: Record<string, string> = {}
|
||||
// list to tree by parent_parallel_start_node_id and parallel_start_node_id
|
||||
result.forEach((node) => {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
const parent_parallel_start_node_id = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null
|
||||
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
|
||||
if (isNotInParallel)
|
||||
return
|
||||
|
||||
const isParallelStartNode = !parallelFirstNodeMap[parallel_id]
|
||||
if (isParallelStartNode) {
|
||||
const selfNode = { ...node }
|
||||
node.parallelDetail = {
|
||||
isParallelStartNode: true,
|
||||
children: [selfNode],
|
||||
}
|
||||
parallelFirstNodeMap[parallel_id] = node.node_id
|
||||
const isRootLevel = !parent_parallel_id
|
||||
if (isRootLevel)
|
||||
return
|
||||
|
||||
const parentParallelStartNode = result.find(item => item.node_id === parent_parallel_start_node_id)
|
||||
// append to parent parallel start node
|
||||
if (parentParallelStartNode) {
|
||||
if (!parentParallelStartNode?.parallelDetail) {
|
||||
parentParallelStartNode!.parallelDetail = {
|
||||
children: [],
|
||||
}
|
||||
}
|
||||
if (parentParallelStartNode!.parallelDetail.children)
|
||||
parentParallelStartNode!.parallelDetail.children.push(node)
|
||||
}
|
||||
}
|
||||
|
||||
// append to parallel start node
|
||||
const parallelStartNode = result.find(item => item.node_id === parallelFirstNodeMap[parallel_id])
|
||||
if (parallelStartNode && parallelStartNode.parallelDetail && parallelStartNode!.parallelDetail!.children)
|
||||
parallelStartNode!.parallelDetail!.children.push(node)
|
||||
})
|
||||
|
||||
const filteredInParallelSubNodes = result.filter((node) => {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
|
||||
if (isNotInParallel)
|
||||
return true
|
||||
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
|
||||
if (parent_parallel_id)
|
||||
return false
|
||||
|
||||
const isParallelStartNode = node.parallelDetail?.isParallelStartNode
|
||||
if (!isParallelStartNode)
|
||||
return false
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
const parallelNumRecord: Record<string, number> = {
|
||||
num: 0,
|
||||
}
|
||||
|
||||
addTitle({
|
||||
list: filteredInParallelSubNodes,
|
||||
level: 1,
|
||||
parallelNumRecord,
|
||||
}, t)
|
||||
|
||||
return filteredInParallelSubNodes
|
||||
}
|
||||
export default format
|
@ -68,12 +68,18 @@ export type NodeTracing = {
|
||||
expand?: boolean // for UI
|
||||
details?: NodeTracing[][] // iteration detail
|
||||
retryDetail?: NodeTracing[] // retry detail
|
||||
agentLog?: AgentLogItemWithChildren[]
|
||||
retry_index?: number
|
||||
parallelDetail?: { // parallel detail. if is in parallel, this field will be set
|
||||
isParallelStartNode?: boolean
|
||||
parallelTitle?: string
|
||||
branchTitle?: string
|
||||
children?: NodeTracing[]
|
||||
}
|
||||
parallel_id?: string
|
||||
parallel_start_node_id?: string
|
||||
parent_parallel_id?: string
|
||||
parent_parallel_start_node_id?: string
|
||||
retry_index?: number
|
||||
agentLog?: AgentLogItemWithChildren[] // agent log
|
||||
}
|
||||
|
||||
export type FetchWorkflowDraftResponse = {
|
||||
|
Loading…
Reference in New Issue
Block a user