Kaynağa Gözat

refactor: 更新工具调用类型并优化详情面板显示

调整 ToolCall 接口结构以支持新的工具调用格式,移除不再使用的 onSubTraceClick 回调,并优化详情面板中工具调用和图片的渲染逻辑
max_liu 3 gün önce
ebeveyn
işleme
61427dc3a6

+ 36 - 21
frontend/react-template/src/components/DetailPanel/DetailPanel.tsx

@@ -47,15 +47,34 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
     if (content.tool_calls && content.tool_calls.length > 0) {
       return (
         <div className={styles.toolCalls}>
-          {content.tool_calls.map((call) => (
-            <div
-              key={call.id}
-              className={styles.toolCall}
-            >
-              <div className={styles.toolName}>工具调用: {call.name}</div>
-              <pre className={styles.toolArgs}>{JSON.stringify(call.arguments, null, 2)}</pre>
-            </div>
-          ))}
+          {content.tool_calls.map((call, idx) => {
+            const anyCall = call as unknown as Record<string, unknown>;
+            const fn = anyCall.function as Record<string, unknown> | undefined;
+            const name =
+              (fn && (fn.name as string)) ||
+              (anyCall.name as string) ||
+              ((content as unknown as Record<string, unknown>).tool_name as string) ||
+              `tool_${idx}`;
+            let args: unknown =
+              (fn && fn.arguments) || anyCall.arguments || (content as unknown as Record<string, unknown>).arguments;
+            if (typeof args === "string") {
+              try {
+                args = JSON.parse(args);
+              } catch {
+                // keep as string if JSON.parse fails
+              }
+            }
+            const key = (anyCall.id as string) || `${name}-${idx}`;
+            return (
+              <div
+                key={key}
+                className={styles.toolCall}
+              >
+                <div className={styles.toolName}>工具调用: {name}</div>
+                <pre className={styles.toolArgs}>{JSON.stringify(args, null, 2)}</pre>
+              </div>
+            );
+          })}
         </div>
       );
     }
@@ -139,10 +158,12 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
               <div className={styles.label}>ID</div>
               <div className={styles.value}>{isMessageNode(node) ? node.message_id || node.id : node.id}</div>
             </div>
-            <div className={styles.section}>
-              <div className={styles.label}>图片</div>
-              {isMessageNode(node) && renderImages(node)}
-            </div>
+            {isMessageNode(node) && extractImagesFromMessage(node).length > 0 && (
+              <div className={styles.section}>
+                <div className={styles.label}>图片</div>
+                {renderImages(node)}
+              </div>
+            )}
 
             {isGoal(node) ? (
               <>
@@ -189,10 +210,7 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
                 )}
                 <div className={styles.section}>
                   <div className={styles.label}>内容</div>
-                  {/* <div className={styles.value}>
-                    {node.content && renderMessageContent(node.content)}
-                    {renderImages(node)}
-                  </div> */}
+                  <div className={styles.value}>{node.content && renderMessageContent(node.content)}</div>
                 </div>
                 {node.goal_id && (
                   <div className={styles.section}>
@@ -224,10 +242,7 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
                 </div>
                 <div className={styles.section}>
                   <div className={styles.label}>内容</div>
-                  <div className={styles.value}>
-                    {msg.content && renderMessageContent(msg.content)}
-                    {renderImages(msg)}
-                  </div>
+                  <div className={styles.value}>{msg.content && renderMessageContent(msg.content)}</div>
                 </div>
               </div>
             ))}

+ 24 - 23
frontend/react-template/src/components/FlowChart/FlowChart.tsx

@@ -75,7 +75,7 @@ interface LayoutEdge {
 }
 
 const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps> = (
-  { goals, msgGroups = {}, invalidBranches, onNodeClick, onSubTraceClick },
+  { goals, msgGroups = {}, invalidBranches, onNodeClick },
   ref,
 ) => {
   // 过滤掉有父节点的 goals,只保留主链节点
@@ -820,25 +820,25 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
   const handleNodeClick = useCallback(
     (node: LayoutNode) => {
       if (node.type === "goal") {
-        const goalData = node.data as Goal;
-
-        // 只有具有 sub_trace_ids 的子目标节点(agent 委托执行)才触发 trace 切换
-        // 普通的 sub_goal 节点(蓝色节点)没有 sub_trace_ids,应该打开 DetailPanel
-        const hasSubTraces = goalData.sub_trace_ids && goalData.sub_trace_ids.length > 0;
-
-        if (node.parentId && onSubTraceClick && hasSubTraces) {
-          const parentNode = layoutData.nodes.find((n) => n.id === node.parentId);
-          if (parentNode && parentNode.type === "goal") {
-            // 取第一个 sub_trace_id 作为跳转目标(使用 trace_id,而非 goal.id)
-            const firstEntry = goalData.sub_trace_ids![0];
-            const entry: SubTraceEntry =
-              typeof firstEntry === "string"
-                ? { id: firstEntry }
-                : { id: firstEntry.trace_id, mission: firstEntry.mission };
-            onSubTraceClick(parentNode.data as Goal, entry);
-            return;
-          }
-        }
+        // const goalData = node.data as Goal;
+
+        // // 只有具有 sub_trace_ids 的子目标节点(agent 委托执行)才触发 trace 切换
+        // // 普通的 sub_goal 节点(蓝色节点)没有 sub_trace_ids,应该打开 DetailPanel
+        // const hasSubTraces = goalData.sub_trace_ids && goalData.sub_trace_ids.length > 0;
+
+        // if (node.parentId && onSubTraceClick && hasSubTraces) {
+        //   const parentNode = layoutData.nodes.find((n) => n.id === node.parentId);
+        //   if (parentNode && parentNode.type === "goal") {
+        //     // 取第一个 sub_trace_id 作为跳转目标(使用 trace_id,而非 goal.id)
+        //     const firstEntry = goalData.sub_trace_ids![0];
+        //     const entry: SubTraceEntry =
+        //       typeof firstEntry === "string"
+        //         ? { id: firstEntry }
+        //         : { id: firstEntry.trace_id, mission: firstEntry.mission };
+        //     onSubTraceClick(parentNode.data as Goal, entry);
+        //     return;
+        //   }
+        // }
 
         // 主链节点 或 没有 sub_trace_ids 的普通子目标节点 → 打开 DetailPanel
         setSelectedNodeId(node.id);
@@ -848,7 +848,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
         onNodeClick?.(node.data as Message);
       }
     },
-    [onNodeClick, onSubTraceClick, layoutData],
+    [onNodeClick],
   );
 
   if (!layoutData) return <div>Loading...</div>;
@@ -969,8 +969,9 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
                     {/* 折叠状态提示徽章 */}
                     {edge.collapsed && (
                       <g
-                        transform={`translate(${(edge.source.x + edge.target.x) / 2},${(edge.source.y + edge.target.y) / 2
-                          })`}
+                        transform={`translate(${(edge.source.x + edge.target.x) / 2},${
+                          (edge.source.y + edge.target.y) / 2
+                        })`}
                         onClick={(e) => {
                           e.stopPropagation();
                           toggleCollapse(edge.id);

+ 5 - 1
frontend/react-template/src/types/message.ts

@@ -1,7 +1,11 @@
 export interface ToolCall {
   id: string;
+  type?: string;
+  function?: {
+    name: string;
+    arguments: string;
+  };
   name: string;
-  arguments: Record<string, unknown>;
 }
 export interface MsgResult {
   type?: string;