pull/22629/merge
GuanMu 7 months ago committed by GitHub
commit 6f304ab8b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -204,6 +204,7 @@ const WorkflowPreview = () => {
<TracingPanel
className='bg-background-section-burn'
list={workflowRunningData?.tracing || []}
enableSearch={true}
/>
)}
{currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && (

@ -0,0 +1,133 @@
import { useCallback, useMemo, useState } from 'react'
import type { NodeTracing } from '@/types/workflow'
type UseTracingSearchProps = {
treeNodes: NodeTracing[]
}
type UseTracingSearchReturn = {
searchQuery: string
setSearchQuery: (query: string) => void
filteredNodes: NodeTracing[]
clearSearch: () => void
searchStats: {
matched: number
total: number
}
}
export const useTracingSearch = ({ treeNodes }: UseTracingSearchProps): UseTracingSearchReturn => {
const [searchQuery, setSearchQuery] = useState('')
// Recursively count all nodes including children
const countNodesRecursively = useCallback((nodes: NodeTracing[]): number => {
return nodes.reduce((count, node) => {
let nodeCount = 1
if (node.parallelDetail?.children)
nodeCount += countNodesRecursively(node.parallelDetail.children)
return count + nodeCount
}, 0)
}, [])
// Deep recursive search filtering logic
const filteredNodes = useMemo(() => {
if (!searchQuery.trim()) return treeNodes
const query = searchQuery.toLowerCase().trim()
// Deep search object content with proper typing
const searchInObject = (obj: unknown): boolean => {
if (!obj) return false
if (typeof obj === 'string') return obj.toLowerCase().includes(query)
if (typeof obj === 'number') return obj.toString().includes(query)
if (typeof obj === 'boolean') return obj.toString().includes(query)
if (Array.isArray(obj))
return obj.some(item => searchInObject(item))
if (typeof obj === 'object' && obj !== null)
return Object.values(obj as Record<string, unknown>).some(value => searchInObject(value))
return false
}
// Search all content in a single node with safe property access
const searchInNode = (node: NodeTracing): boolean => {
// Safe string search with nullish coalescing
const titleMatch = node.title?.toLowerCase().includes(query) ?? false
const nodeTypeMatch = node.node_type?.toLowerCase().includes(query) ?? false
const statusMatch = (node as NodeTracing & { status?: string }).status?.toLowerCase().includes(query) ?? false
// Search in node data with proper type checking
const inputsMatch = searchInObject(node.inputs)
const outputsMatch = searchInObject(node.outputs)
const processDataMatch = searchInObject((node as NodeTracing & { process_data?: unknown }).process_data)
const metadataMatch = searchInObject((node as NodeTracing & { execution_metadata?: unknown }).execution_metadata)
return titleMatch || nodeTypeMatch || statusMatch || inputsMatch || outputsMatch || processDataMatch || metadataMatch
}
// Recursively search node and all its children
const searchNodeRecursively = (node: NodeTracing): boolean => {
// Search current node
if (searchInNode(node)) return true
// Search parallel branch children with safe access
if (node.parallelDetail?.children)
return node.parallelDetail.children.some((child: NodeTracing) => searchNodeRecursively(child))
return false
}
// Recursively filter node tree while maintaining hierarchy
const filterNodesRecursively = (nodes: NodeTracing[]): NodeTracing[] => {
return nodes.reduce((acc: NodeTracing[], node: NodeTracing) => {
const nodeMatches = searchInNode(node)
const hasMatchingChildren = node.parallelDetail?.children
? node.parallelDetail.children.some((child: NodeTracing) => searchNodeRecursively(child))
: false
if (nodeMatches || hasMatchingChildren) {
const filteredNode = { ...node }
// If has parallel children, recursively filter them
if (node.parallelDetail?.children) {
const filteredChildren = filterNodesRecursively(node.parallelDetail.children)
if (filteredChildren.length > 0) {
filteredNode.parallelDetail = {
...node.parallelDetail,
children: filteredChildren,
}
}
}
acc.push(filteredNode)
}
return acc
}, [])
}
return filterNodesRecursively(treeNodes)
}, [treeNodes, searchQuery])
// Clear search function
const clearSearch = useCallback(() => {
setSearchQuery('')
}, [])
// Calculate search statistics
const searchStats = useMemo(() => ({
matched: countNodesRecursively(filteredNodes),
total: countNodesRecursively(treeNodes),
}), [filteredNodes, treeNodes, countNodesRecursively])
return {
searchQuery,
setSearchQuery,
filteredNodes,
clearSearch,
searchStats,
}
}

@ -162,6 +162,7 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
<TracingPanel
className='bg-background-section-burn'
list={list}
enableSearch={true}
/>
)}
</div>

@ -9,7 +9,9 @@ React,
import cn from 'classnames'
import {
RiArrowDownSLine,
RiCloseLine,
RiMenu4Line,
RiSearchLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useLogs } from './hooks'
@ -17,12 +19,14 @@ import NodePanel from './node'
import SpecialResultPanel from './special-result-panel'
import type { NodeTracing } from '@/types/workflow'
import formatNodeList from '@/app/components/workflow/run/utils/format-log'
import { useTracingSearch } from './hooks/useTracingSearch'
type TracingPanelProps = {
list: NodeTracing[]
className?: string
hideNodeInfo?: boolean
hideNodeProcessDetail?: boolean
enableSearch?: boolean
}
const TracingPanel: FC<TracingPanelProps> = ({
@ -30,9 +34,21 @@ const TracingPanel: FC<TracingPanelProps> = ({
className,
hideNodeInfo = false,
hideNodeProcessDetail = false,
enableSearch = false,
}) => {
const { t } = useTranslation()
const treeNodes = formatNodeList(list, t)
// Search functionality using custom hook
const {
searchQuery,
setSearchQuery,
filteredNodes,
clearSearch,
searchStats,
} = useTracingSearch({ treeNodes })
const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set())
const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)
@ -185,13 +201,62 @@ const TracingPanel: FC<TracingPanelProps> = ({
return (
<div
className={cn('py-2', className)}
className={cn('flex h-full flex-col', className)}
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
}}
>
{treeNodes.map(renderNode)}
{/* Search input */}
{enableSearch && (
<div className="border-b border-divider-subtle px-4 py-3">
<div className="flex h-8 items-center rounded-lg bg-components-input-bg-normal px-2 hover:bg-components-input-bg-hover">
<RiSearchLine className="mr-1.5 h-4 w-4 shrink-0 text-components-input-text-placeholder" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder={t('workflow.common.searchNodes') || 'Search nodes, types, inputs, outputs...'}
className="system-sm-regular block h-[18px] grow appearance-none border-0 bg-transparent text-components-input-text-filled outline-none placeholder:text-components-input-text-placeholder"
autoComplete="off"
/>
{searchQuery && (
<div className="ml-1 cursor-pointer" onClick={clearSearch}>
<RiCloseLine className="h-4 w-4 text-components-input-text-placeholder hover:text-text-tertiary" />
</div>
)}
</div>
</div>
)}
{/* Search results statistics */}
{enableSearch && searchQuery && (
<div className="border-b border-divider-subtle px-4 py-2">
<div className="system-xs-regular text-text-tertiary">
{filteredNodes.length === 0
? t('workflow.common.noSearchResults')
: t('workflow.common.searchResults', {
matched: searchStats.matched,
total: searchStats.total,
})}
</div>
</div>
)}
{/* Empty search results hint */}
{enableSearch && searchQuery && filteredNodes.length === 0 && (
<div className="flex flex-1 flex-col items-center justify-center text-text-tertiary">
<div className="system-sm-medium mb-2">{t('workflow.common.noSearchResults')}</div>
<div className="system-xs-regular">
{t('workflow.common.searchHint', { query: searchQuery })}
</div>
</div>
)}
{/* Tracing content */}
<div className={cn('flex-1 overflow-y-auto py-2', { 'py-0': enableSearch })}>
{filteredNodes.length > 0 && filteredNodes.map(renderNode)}
</div>
</div>
)
}

@ -40,6 +40,10 @@ const translation = {
maxTreeDepth: 'Maximales Limit von {{depth}} Knoten pro Ast',
workflowProcess: 'Arbeitsablauf',
notRunning: 'Noch nicht ausgeführt',
searchNodes: 'Knoten, Typen, Eingaben, Ausgaben suchen...',
noSearchResults: 'Keine Suchergebnisse',
searchResults: '{{matched}} von {{total}} Knoten',
searchHint: 'Versuchen Sie andere Schlüsselwörter für "{{query}}"',
previewPlaceholder: 'Geben Sie den Inhalt in das Feld unten ein, um das Debuggen des Chatbots zu starten',
effectVarConfirm: {
title: 'Variable entfernen',

@ -50,6 +50,10 @@ const translation = {
needAnswerNode: 'The Answer node must be added',
workflowProcess: 'Workflow Process',
notRunning: 'Not running yet',
searchNodes: 'Search nodes, types, inputs, outputs...',
noSearchResults: 'No search results',
searchResults: '{{matched}} of {{total}} nodes',
searchHint: 'Try different keywords for "{{query}}"',
previewPlaceholder: 'Enter content in the box below to start debugging the Chatbot',
effectVarConfirm: {
title: 'Remove Variable',

@ -40,6 +40,10 @@ const translation = {
maxTreeDepth: 'Límite máximo de {{depth}} nodos por rama',
workflowProcess: 'Proceso de flujo de trabajo',
notRunning: 'Aún no se está ejecutando',
searchNodes: 'Buscar nodos, tipos, entradas, salidas...',
noSearchResults: 'Sin resultados de búsqueda',
searchResults: '{{matched}} de {{total}} nodos',
searchHint: 'Prueba con diferentes palabras clave para "{{query}}"',
previewPlaceholder: 'Ingrese contenido en el cuadro de abajo para comenzar a depurar el Chatbot',
effectVarConfirm: {
title: 'Eliminar variable',

@ -40,6 +40,10 @@ const translation = {
maxTreeDepth: 'Limite maximale de {{depth}} nœuds par branche',
workflowProcess: 'Processus de flux de travail',
notRunning: 'Pas encore en cours d\'exécution',
searchNodes: 'Rechercher nœuds, types, entrées, sorties...',
noSearchResults: 'Aucun résultat de recherche',
searchResults: '{{matched}} sur {{total}} nœuds',
searchHint: 'Essayez des mots-clés différents pour "{{query}}"',
previewPlaceholder: 'Entrez le contenu dans la boîte ci-dessous pour commencer à déboguer le Chatbot',
effectVarConfirm: {
title: 'Supprimer la variable',

@ -50,6 +50,10 @@ const translation = {
needAnswerNode: '回答ブロックを追加する必要があります',
workflowProcess: 'ワークフロー処理',
notRunning: 'まだ実行されていません',
searchNodes: 'ノード、タイプ、入力、出力を検索...',
noSearchResults: '検索結果がありません',
searchResults: '{{total}}件中{{matched}}件のノード',
searchHint: '"{{query}}"について別のキーワードを試してください',
previewPlaceholder: '入力欄にテキストを入力してチャットボットのデバッグを開始',
effectVarConfirm: {
title: '変数の削除',

@ -40,6 +40,10 @@ const translation = {
maxTreeDepth: '분기당 최대 {{depth}} 노드 제한',
workflowProcess: '워크플로우 과정',
notRunning: '아직 실행되지 않음',
searchNodes: '노드, 유형, 입력, 출력 검색...',
noSearchResults: '검색 결과 없음',
searchResults: '{{total}}개 중 {{matched}}개 노드',
searchHint: '"{{query}}"에 대해 다른 키워드를 시도해 보세요',
previewPlaceholder: '디버깅을 시작하려면 아래 상자에 내용을 입력하세요',
effectVarConfirm: {
title: '변수 제거',

@ -40,6 +40,10 @@ const translation = {
maxTreeDepth: 'Максимальный предел {{depth}} узлов на ветку',
workflowProcess: 'Процесс рабочего процесса',
notRunning: 'Еще не запущено',
searchNodes: 'Поиск узлов, типов, входов, выходов...',
noSearchResults: 'Нет результатов поиска',
searchResults: '{{matched}} из {{total}} узлов',
searchHint: 'Попробуйте другие ключевые слова для "{{query}}"',
previewPlaceholder: 'Введите текст в поле ниже, чтобы начать отладку чат-бота',
effectVarConfirm: {
title: 'Удалить переменную',

@ -49,6 +49,10 @@ const translation = {
needAnswerNode: '必须添加直接回复节点',
workflowProcess: '工作流',
notRunning: '尚未运行',
searchNodes: '搜索节点、类型、输入、输出...',
noSearchResults: '无搜索结果',
searchResults: '{{matched}} / {{total}} 个节点',
searchHint: '尝试使用不同的关键词搜索 "{{query}}"',
previewPlaceholder: '在下面的框中输入内容开始调试聊天机器人',
effectVarConfirm: {
title: '移除变量',

@ -42,6 +42,10 @@ const translation = {
needAnswerNode: '必須添加直接回覆節點',
workflowProcess: '工作流',
notRunning: '尚未運行',
searchNodes: '搜索節點、類型、輸入、輸出...',
noSearchResults: '無搜索結果',
searchResults: '{{matched}} / {{total}} 個節點',
searchHint: '嘗試使用不同的關鍵詞搜索 "{{query}}"',
previewPlaceholder: '在下面的框中輸入內容開始調試聊天機器人',
effectVarConfirm: {
title: '移除變量',

Loading…
Cancel
Save