Просмотр исходного кода

Merge remote-tracking branch 'refs/remotes/origin/main'

Talegorithm 1 неделя назад
Родитель
Сommit
82d23fc8f3

+ 19 - 6
frontend/react-template/src/App.tsx

@@ -3,17 +3,18 @@ import { TopBar } from "./components/TopBar/TopBar";
 import { MainContent } from "./components/MainContent/MainContent";
 import { DetailPanel } from "./components/DetailPanel/DetailPanel";
 import type { Goal } from "./types/goal";
-import type { Edge } from "./types/message";
+import type { Edge, Message } from "./types/message";
 import { useFlowChartData } from "./components/FlowChart/hooks/useFlowChartData";
 import { useTrace } from "./hooks/useTrace";
 import "./styles/global.css";
 
 function App() {
   const [selectedTraceId, setSelectedTraceId] = useState<string | null>(null);
-  const [selectedNode, setSelectedNode] = useState<Goal | null>(null);
+  const [selectedNode, setSelectedNode] = useState<Goal | Message | null>(null);
   const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
   const [rightWidth, setRightWidth] = useState(360);
   const [isDragging, setIsDragging] = useState(false);
+  const [refreshTrigger, setRefreshTrigger] = useState(0);
   const bodyRef = useRef<HTMLDivElement | null>(null);
 
   // 获取数据以传递给 DetailPanel
@@ -21,7 +22,7 @@ function App() {
   const initialGoals = useMemo(() => trace?.goal_tree?.goals ?? [], [trace]);
   const { msgGroups } = useFlowChartData(selectedTraceId, initialGoals);
 
-  const handleNodeClick = (node: Goal, edge?: Edge) => {
+  const handleNodeClick = (node: Goal | Message, edge?: Edge) => {
     setSelectedNode(node);
     setSelectedEdge(edge || null);
   };
@@ -31,10 +32,15 @@ function App() {
     setSelectedEdge(null);
   };
 
+  const isGoalNode = (node: Goal | Message): node is Goal => "status" in node && "created_at" in node;
+
   // 根据选中的节点获取对应的消息
   const selectedMessages = useMemo(() => {
     if (selectedNode) {
-      return msgGroups[selectedNode.id] || [];
+      if (isGoalNode(selectedNode)) {
+        return msgGroups[selectedNode.id] || [];
+      }
+      return [selectedNode as Message];
     }
     // 如果点击的是边,且该边是主链上的边,通常我们显示源节点的消息(如果需要)
     // 但根据用户需求 "边里面的信息,显示对应msgGroup里面的所有的子集的描述"
@@ -72,10 +78,17 @@ function App() {
         style={{ userSelect: isDragging ? "none" : "auto" }}
       >
         <div className="app-main">
-          <TopBar onTraceSelect={setSelectedTraceId} />
+          <TopBar
+            selectedTraceId={selectedTraceId}
+            selectedNode={selectedNode}
+            onTraceSelect={setSelectedTraceId}
+            onTraceCreated={() => setRefreshTrigger((t) => t + 1)}
+          />
           <MainContent
             traceId={selectedTraceId}
             onNodeClick={handleNodeClick}
+            onTraceChange={setSelectedTraceId}
+            refreshTrigger={refreshTrigger}
           />
         </div>
         {(selectedNode || selectedEdge) && (
@@ -94,7 +107,7 @@ function App() {
               style={{ width: rightWidth }}
             >
               <DetailPanel
-                node={selectedNode}
+                node={selectedNode && isGoalNode(selectedNode) ? (selectedNode as Goal) : null}
                 edge={selectedEdge}
                 messages={selectedMessages}
                 onClose={handleCloseDetail}

+ 24 - 0
frontend/react-template/src/api/traceApi.ts

@@ -13,4 +13,28 @@ export const traceApi = {
   fetchTraceDetail(traceId: string) {
     return request<TraceDetailResponse>(`/api/traces/${traceId}`);
   },
+  createTrace(data: { system_prompt: string; user_prompt: string }) {
+    return request<{ trace_id: string }>("/api/traces", {
+      method: "POST",
+      data,
+    });
+  },
+  runTrace(messageId: string) {
+    return request<void>(`/api/traces/${messageId}/run`, {
+      method: "POST",
+    });
+  },
+  stopTrace(traceId: string) {
+    return request<void>(`/api/traces/${traceId}/stop`, {
+      method: "POST",
+    });
+  },
+  reflectTrace(traceId: string) {
+    return request<void>(`/api/traces/${traceId}/reflect`, {
+      method: "POST",
+    });
+  },
+  getExperiences() {
+    return request<string>("/api/experiences");
+  },
 };

+ 4 - 1
frontend/react-template/src/components/FlowChart/FlowChart.tsx

@@ -23,7 +23,7 @@ import { Tooltip } from "@douyinfe/semi-ui";
 interface FlowChartProps {
   goals: Goal[]; // 目标节点列表
   msgGroups?: Record<string, Message[]>; // 消息组,key 是 goal_id
-  onNodeClick?: (node: Goal, edge?: EdgeType) => void; // 节点点击回调
+  onNodeClick?: (node: Goal | Message, edge?: EdgeType) => void; // 节点点击回调
   onSubTraceClick?: (parentGoal: Goal, entry: SubTraceEntry) => void; // 子追踪点击回调
 }
 
@@ -744,6 +744,9 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
         // 主链节点,触发 onNodeClick
         setSelectedNodeId(node.id);
         onNodeClick?.(node.data as Goal);
+      } else if (node.type === "message") {
+        setSelectedNodeId(node.id);
+        onNodeClick?.(node.data as Message);
       }
     },
     [onNodeClick, onSubTraceClick, layoutData],

+ 33 - 4
frontend/react-template/src/components/MainContent/MainContent.tsx

@@ -1,24 +1,43 @@
-import { useMemo, useRef, useState } from "react";
+import { useMemo, useRef, useState, useEffect } from "react";
 import type { FC } from "react";
+import { Select } from "@douyinfe/semi-ui";
 import { FlowChart } from "../FlowChart/FlowChart";
 import type { FlowChartRef } from "../FlowChart/FlowChart";
 import { useTrace } from "../../hooks/useTrace";
 import { useFlowChartData } from "../FlowChart/hooks/useFlowChartData";
+import { traceApi } from "../../api/traceApi";
 import type { Goal } from "../../types/goal";
-import type { Edge } from "../../types/message";
+import type { Edge, Message } from "../../types/message";
+import type { TraceListItem } from "../../types/trace";
 import styles from "./MainContent.module.css";
 
 interface MainContentProps {
   traceId: string | null;
-  onNodeClick?: (node: Goal, edge?: Edge) => void;
+  onNodeClick?: (node: Goal | Message, edge?: Edge) => void;
+  onTraceChange?: (traceId: string) => void;
+  refreshTrigger?: number;
 }
 
-export const MainContent: FC<MainContentProps> = ({ traceId, onNodeClick }) => {
+export const MainContent: FC<MainContentProps> = ({ traceId, onNodeClick, onTraceChange, refreshTrigger }) => {
   const flowChartRef = useRef<FlowChartRef>(null);
   const [isAllExpanded, setIsAllExpanded] = useState(true);
+  const [traceList, setTraceList] = useState<TraceListItem[]>([]);
   const { trace, loading } = useTrace(traceId);
   const initialGoals = useMemo(() => trace?.goal_tree?.goals ?? [], [trace]);
   const { goals, connected, msgGroups } = useFlowChartData(traceId, initialGoals);
+
+  useEffect(() => {
+    const fetchTraces = async () => {
+      try {
+        const data = await traceApi.fetchTraces({ limit: 100 });
+        setTraceList(data.traces);
+      } catch (error) {
+        console.error("Failed to load traces:", error);
+      }
+    };
+    fetchTraces();
+  }, [refreshTrigger]);
+
   console.log("%c [ MainContent-goals ]-19", "font-size:13px; background:pink; color:#bf2c9f;", goals);
 
   if (!traceId && !loading) {
@@ -40,6 +59,16 @@ export const MainContent: FC<MainContentProps> = ({ traceId, onNodeClick }) => {
       <div className={styles.header}>
         <div className={styles.title}>{connected ? "WebSocket 已连接" : "WebSocket 未连接"}</div>
         <div className={styles.headerRight}>
+          <Select
+            value={traceId}
+            onChange={(value: unknown) => onTraceChange?.(value as string)}
+            style={{ width: 200 }}
+            placeholder="选择 Trace"
+            optionList={traceList.map((t) => ({
+              label: t.task || t.trace_id,
+              value: t.trace_id,
+            }))}
+          />
           {/* <div className={styles.status}>{connected ? "WebSocket 已连接" : "WebSocket 未连接"}</div> */}
           {/* <div className={styles.legend}>
             <div className={styles.legendItem}>

+ 153 - 8
frontend/react-template/src/components/TopBar/TopBar.tsx

@@ -1,14 +1,32 @@
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useState, useRef } from "react";
 import type { FC } from "react";
+import { Modal, Form, Toast } from "@douyinfe/semi-ui";
 import { traceApi } from "../../api/traceApi";
+import type { Goal } from "../../types/goal";
+import type { Message } from "../../types/message";
 import styles from "./TopBar.module.css";
 
 interface TopBarProps {
+  selectedTraceId: string | null;
+  selectedNode: Goal | Message | null;
   onTraceSelect: (traceId: string) => void;
+  onTraceCreated?: () => void;
 }
 
-export const TopBar: FC<TopBarProps> = ({ onTraceSelect }) => {
+export const TopBar: FC<TopBarProps> = ({ selectedTraceId, selectedNode, onTraceSelect, onTraceCreated }) => {
   const [title, setTitle] = useState("流程图可视化系统");
+  const [isModalVisible, setIsModalVisible] = useState(false);
+  const [isExperienceModalVisible, setIsExperienceModalVisible] = useState(false);
+  const [experienceContent, setExperienceContent] = useState("");
+  const formApiRef = useRef<{ getValues: () => { system_prompt: string; user_prompt: string } } | null>(null);
+
+  const isMessageNode = (node: Goal | Message): node is Message =>
+    "message_id" in node || "role" in node || "content" in node || "goal_id" in node || "tokens" in node;
+  const getMessageId = (node: Message) => {
+    if (typeof node.message_id === "string" && node.message_id) return node.message_id;
+    if (typeof node.id === "string" && node.id) return node.id;
+    return null;
+  };
 
   const loadTraces = useCallback(
     async (status?: string) => {
@@ -19,7 +37,6 @@ export const TopBar: FC<TopBarProps> = ({ onTraceSelect }) => {
         });
         const firstTrace = data.traces[0];
         const traceId = firstTrace?.parent_trace_id || firstTrace.trace_id;
-        console.log("%c [ firstTrace ]-24", "font-size:13px; background:pink; color:#bf2c9f;", firstTrace);
         if (firstTrace) {
           setTitle(firstTrace.task);
           onTraceSelect(traceId);
@@ -38,6 +55,99 @@ export const TopBar: FC<TopBarProps> = ({ onTraceSelect }) => {
     loadTraces();
   }, [loadTraces]);
 
+  const handleNewTask = () => {
+    setIsModalVisible(true);
+  };
+
+  const handleConfirm = async () => {
+    try {
+      const values = formApiRef.current?.getValues();
+      if (!values) return;
+
+      await traceApi.createTrace(values);
+      await loadTraces();
+      onTraceCreated?.();
+      setIsModalVisible(false);
+      Toast.success("创建成功");
+    } catch (error) {
+      console.error("Failed to create trace:", error);
+      Toast.error("创建失败");
+    }
+  };
+
+  const handleRun = async () => {
+    console.log("%c [ selectedNode ]-72", "font-size:13px; background:pink; color:#bf2c9f;", selectedNode);
+
+    if (!selectedNode) {
+      Toast.warning("请选择插入节点");
+      return;
+    }
+
+    if (!isMessageNode(selectedNode)) {
+      Toast.warning("插入位置错误");
+      return;
+    }
+
+    const messageId = getMessageId(selectedNode);
+    if (!messageId) {
+      Toast.error("消息ID缺失");
+      return;
+    }
+
+    try {
+      await traceApi.runTrace(messageId);
+      Toast.success("已开始运行");
+    } catch (error) {
+      console.error("Failed to run trace:", error);
+      Toast.error("运行请求失败");
+    }
+  };
+
+  const handleStop = () => {
+    if (!selectedTraceId) {
+      Toast.warning("请先选择一个 Trace");
+      return;
+    }
+    Modal.confirm({
+      title: "确认停止",
+      content: "确定要停止当前运行的 Trace 吗?",
+      onOk: async () => {
+        try {
+          await traceApi.stopTrace(selectedTraceId);
+          Toast.success("已停止");
+        } catch (error) {
+          console.error("Failed to stop trace:", error);
+          Toast.error("停止失败");
+        }
+      },
+    });
+  };
+
+  const handleReflect = async () => {
+    if (!selectedTraceId) {
+      Toast.warning("请先选择一个 Trace");
+      return;
+    }
+    try {
+      await traceApi.reflectTrace(selectedTraceId);
+      Toast.success("已触发反思");
+    } catch (error) {
+      console.error("Failed to reflect trace:", error);
+      Toast.error("反思请求失败");
+    }
+  };
+
+  const handleExperience = async () => {
+    try {
+      const content = await traceApi.getExperiences();
+      setExperienceContent(typeof content === "string" ? content : JSON.stringify(content, null, 2));
+      setIsExperienceModalVisible(true);
+    } catch (error) {
+      console.error("Failed to get experiences:", error);
+      Toast.error("获取经验失败");
+    }
+  };
+
   return (
     <header className={styles.topbar}>
       <div className={styles.title}>
@@ -46,35 +156,70 @@ export const TopBar: FC<TopBarProps> = ({ onTraceSelect }) => {
       <div className={styles.filters}>
         <button
           className={styles.button}
-          onClick={() => console.log("新任务")}
+          onClick={handleNewTask}
         >
           新任务
         </button>
         <button
           className={styles.buttonPrimary}
-          onClick={() => console.log("运行")}
+          onClick={handleRun}
         >
           运行
         </button>
         <button
           className={styles.buttonDanger}
-          onClick={() => console.log("停止")}
+          onClick={handleStop}
         >
           停止
         </button>
         <button
           className={styles.button}
-          onClick={() => console.log("反思")}
+          onClick={handleReflect}
         >
           反思
         </button>
         <button
           className={styles.button}
-          onClick={() => console.log("经验")}
+          onClick={handleExperience}
         >
           经验
         </button>
       </div>
+      <Modal
+        title="新建任务"
+        visible={isModalVisible}
+        onOk={handleConfirm}
+        onCancel={() => setIsModalVisible(false)}
+        centered
+        style={{ width: 600 }}
+      >
+        {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
+        <Form getFormApi={(api: any) => (formApiRef.current = api)}>
+          <Form.TextArea
+            field="system_prompt"
+            label="System Prompt"
+            placeholder="请输入 System Prompt"
+            autosize={{ minRows: 3, maxRows: 6 }}
+          />
+          <Form.TextArea
+            field="user_prompt"
+            label="User Prompt"
+            placeholder="请输入 User Prompt"
+            autosize={{ minRows: 3, maxRows: 6 }}
+          />
+        </Form>
+      </Modal>
+      <Modal
+        title="经验列表"
+        visible={isExperienceModalVisible}
+        onCancel={() => setIsExperienceModalVisible(false)}
+        footer={null}
+        centered
+        style={{ width: 800 }}
+        bodyStyle={{ maxHeight: "70vh", overflow: "auto" }}
+      >
+        <pre style={{ whiteSpace: "pre-wrap", wordWrap: "break-word" }}>{experienceContent || "暂无经验数据"}</pre>
+      </Modal>
     </header>
   );
 };