diff --git a/web/app/components/workflow/panel/workflow-preview.tsx b/web/app/components/workflow/panel/workflow-preview.tsx index 2c797e05d6..49ca3c4544 100644 --- a/web/app/components/workflow/panel/workflow-preview.tsx +++ b/web/app/components/workflow/panel/workflow-preview.tsx @@ -204,6 +204,7 @@ const WorkflowPreview = () => { )} {currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && ( diff --git a/web/app/components/workflow/run/index.tsx b/web/app/components/workflow/run/index.tsx index 8b996032e2..d87529d7cf 100644 --- a/web/app/components/workflow/run/index.tsx +++ b/web/app/components/workflow/run/index.tsx @@ -162,6 +162,7 @@ const RunPanel: FC = ({ hideResult, activeTab = 'RESULT', runID, getRe )} diff --git a/web/app/components/workflow/run/tracing-panel.tsx b/web/app/components/workflow/run/tracing-panel.tsx index a6e9bf9dd4..3ba4c30f5f 100644 --- a/web/app/components/workflow/run/tracing-panel.tsx +++ b/web/app/components/workflow/run/tracing-panel.tsx @@ -4,12 +4,15 @@ import React, { useCallback, + useMemo, useState, } from 'react' import cn from 'classnames' import { RiArrowDownSLine, + RiCloseLine, RiMenu4Line, + RiSearchLine, } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { useLogs } from './hooks' @@ -23,6 +26,7 @@ type TracingPanelProps = { className?: string hideNodeInfo?: boolean hideNodeProcessDetail?: boolean + enableSearch?: boolean } const TracingPanel: FC = ({ @@ -30,9 +34,96 @@ const TracingPanel: FC = ({ className, hideNodeInfo = false, hideNodeProcessDetail = false, + enableSearch = false, }) => { const { t } = useTranslation() + const [searchQuery, setSearchQuery] = useState('') + const treeNodes = formatNodeList(list, t) + + // 递归计算节点总数(包括子节点) + const countNodesRecursively = useCallback((nodes: any[]): number => { + return nodes.reduce((count, node) => { + let nodeCount = 1 + if (node.parallelDetail?.children) + nodeCount += countNodesRecursively(node.parallelDetail.children) + return count + nodeCount + }, 0) + }, []) + + // 深度递归搜索过滤逻辑 + const filteredNodes = useMemo(() => { + if (!searchQuery.trim()) return treeNodes + + const query = searchQuery.toLowerCase().trim() + + // 深度搜索对象内容 + const searchInObject = (obj: any): boolean => { + if (!obj) return false + if (typeof obj === 'string') return obj.toLowerCase().includes(query) + if (typeof obj === 'number') return obj.toString().includes(query) + if (Array.isArray(obj)) return obj.some(item => searchInObject(item)) + if (typeof obj === 'object') + return Object.values(obj).some(value => searchInObject(value)) + return false + } + + // 搜索单个节点的所有内容 + const searchInNode = (node: any): boolean => { + return ( + node.title?.toLowerCase().includes(query) + || node.node_type?.toLowerCase().includes(query) + || node.status?.toLowerCase().includes(query) + || searchInObject(node.inputs) + || searchInObject(node.outputs) + || searchInObject(node.process_data) + || searchInObject(node.execution_metadata) + ) + } + + // 递归搜索节点及其所有子节点 + const searchNodeRecursively = (node: any): boolean => { + // 搜索当前节点 + if (searchInNode(node)) return true + + // 搜索并行分支子节点 + if (node.parallelDetail?.children) + return node.parallelDetail.children.some((child: any) => searchNodeRecursively(child)) + + return false + } + + // 递归过滤节点树,保持层级结构 + const filterNodesRecursively = (nodes: any[]): any[] => { + return nodes.reduce((acc: any[], node: any) => { + const nodeMatches = searchInNode(node) + const hasMatchingChildren = node.parallelDetail?.children + ? node.parallelDetail.children.some((child: any) => searchNodeRecursively(child)) + : false + + if (nodeMatches || hasMatchingChildren) { + const filteredNode = { ...node } + + // 如果有并行子节点,递归过滤它们 + 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]) const [collapsedNodes, setCollapsedNodes] = useState>(new Set()) const [hoveredParallel, setHoveredParallel] = useState(null) @@ -185,13 +276,62 @@ const TracingPanel: FC = ({ return (
{ e.stopPropagation() e.nativeEvent.stopImmediatePropagation() }} > - {treeNodes.map(renderNode)} + {/* 搜索框 */} + {enableSearch && ( +
+
+ + 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 && ( +
setSearchQuery('')}> + +
+ )} +
+
+ )} + + {/* 搜索结果统计 */} + {enableSearch && searchQuery && ( +
+
+ {filteredNodes.length === 0 + ? t('workflow.common.noSearchResults') + : t('workflow.common.searchResults', { + matched: countNodesRecursively(filteredNodes), + total: countNodesRecursively(treeNodes), + })} +
+
+ )} + + {/* 无搜索结果提示 */} + {enableSearch && searchQuery && filteredNodes.length === 0 && ( +
+
{t('workflow.common.noSearchResults')}
+
+ {t('workflow.common.searchHint', { query: searchQuery })} +
+
+ )} + + {/* 追踪内容 */} +
+ {filteredNodes.length > 0 && filteredNodes.map(renderNode)} +
) } diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts index ba49f72b69..33222f3426 100644 --- a/web/i18n/de-DE/workflow.ts +++ b/web/i18n/de-DE/workflow.ts @@ -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', diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index c56b497ac2..f5440cc96d 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -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', diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts index 44516317e8..81c74d6906 100644 --- a/web/i18n/es-ES/workflow.ts +++ b/web/i18n/es-ES/workflow.ts @@ -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', diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts index 8c8180abff..21aa09ec86 100644 --- a/web/i18n/fr-FR/workflow.ts +++ b/web/i18n/fr-FR/workflow.ts @@ -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', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 04702194f8..38414e5051 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -50,6 +50,10 @@ const translation = { needAnswerNode: '回答ブロックを追加する必要があります', workflowProcess: 'ワークフロー処理', notRunning: 'まだ実行されていません', + searchNodes: 'ノード、タイプ、入力、出力を検索...', + noSearchResults: '検索結果がありません', + searchResults: '{{total}}件中{{matched}}件のノード', + searchHint: '"{{query}}"について別のキーワードを試してください', previewPlaceholder: '入力欄にテキストを入力してチャットボットのデバッグを開始', effectVarConfirm: { title: '変数の削除', diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts index 6a9f97862e..90f3bc2b83 100644 --- a/web/i18n/ko-KR/workflow.ts +++ b/web/i18n/ko-KR/workflow.ts @@ -40,6 +40,10 @@ const translation = { maxTreeDepth: '분기당 최대 {{depth}} 노드 제한', workflowProcess: '워크플로우 과정', notRunning: '아직 실행되지 않음', + searchNodes: '노드, 유형, 입력, 출력 검색...', + noSearchResults: '검색 결과 없음', + searchResults: '{{total}}개 중 {{matched}}개 노드', + searchHint: '"{{query}}"에 대해 다른 키워드를 시도해 보세요', previewPlaceholder: '디버깅을 시작하려면 아래 상자에 내용을 입력하세요', effectVarConfirm: { title: '변수 제거', diff --git a/web/i18n/ru-RU/workflow.ts b/web/i18n/ru-RU/workflow.ts index aecd9e652c..470dcd57ad 100644 --- a/web/i18n/ru-RU/workflow.ts +++ b/web/i18n/ru-RU/workflow.ts @@ -40,6 +40,10 @@ const translation = { maxTreeDepth: 'Максимальный предел {{depth}} узлов на ветку', workflowProcess: 'Процесс рабочего процесса', notRunning: 'Еще не запущено', + searchNodes: 'Поиск узлов, типов, входов, выходов...', + noSearchResults: 'Нет результатов поиска', + searchResults: '{{matched}} из {{total}} узлов', + searchHint: 'Попробуйте другие ключевые слова для "{{query}}"', previewPlaceholder: 'Введите текст в поле ниже, чтобы начать отладку чат-бота', effectVarConfirm: { title: 'Удалить переменную', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 6bd202d58f..68e55514df 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -49,6 +49,10 @@ const translation = { needAnswerNode: '必须添加直接回复节点', workflowProcess: '工作流', notRunning: '尚未运行', + searchNodes: '搜索节点、类型、输入、输出...', + noSearchResults: '无搜索结果', + searchResults: '{{matched}} / {{total}} 个节点', + searchHint: '尝试使用不同的关键词搜索 "{{query}}"', previewPlaceholder: '在下面的框中输入内容开始调试聊天机器人', effectVarConfirm: { title: '移除变量', diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index 1d29d2f5ab..dae115db08 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -42,6 +42,10 @@ const translation = { needAnswerNode: '必須添加直接回覆節點', workflowProcess: '工作流', notRunning: '尚未運行', + searchNodes: '搜索節點、類型、輸入、輸出...', + noSearchResults: '無搜索結果', + searchResults: '{{matched}} / {{total}} 個節點', + searchHint: '嘗試使用不同的關鍵詞搜索 "{{query}}"', previewPlaceholder: '在下面的框中輸入內容開始調試聊天機器人', effectVarConfirm: { title: '移除變量',