فهرست منبع

feat: 支持在消息节点后插入新指令并刷新流程图

扩展 traceApi 的 createTrace 和 runTrace 接口以支持更灵活的参数传递,包括消息数组和序列号。
在 TopBar 组件中新增“插入”按钮和模态框,允许用户在选定的消息节点后输入指令并执行。
添加 messageRefreshTrigger 状态以在插入新指令后触发流程图数据刷新。
移除调试用的 console.log 语句。
max_liu 1 هفته پیش
والد
کامیت
a955b913f7

+ 4 - 1
frontend/react-template/src/App.tsx

@@ -15,12 +15,13 @@ function App() {
   const [rightWidth, setRightWidth] = useState(360);
   const [rightWidth, setRightWidth] = useState(360);
   const [isDragging, setIsDragging] = useState(false);
   const [isDragging, setIsDragging] = useState(false);
   const [refreshTrigger, setRefreshTrigger] = useState(0);
   const [refreshTrigger, setRefreshTrigger] = useState(0);
+  const [messageRefreshTrigger, setMessageRefreshTrigger] = useState(0);
   const bodyRef = useRef<HTMLDivElement | null>(null);
   const bodyRef = useRef<HTMLDivElement | null>(null);
 
 
   // 获取数据以传递给 DetailPanel
   // 获取数据以传递给 DetailPanel
   const { trace } = useTrace(selectedTraceId);
   const { trace } = useTrace(selectedTraceId);
   const initialGoals = useMemo(() => trace?.goal_tree?.goals ?? [], [trace]);
   const initialGoals = useMemo(() => trace?.goal_tree?.goals ?? [], [trace]);
-  const { msgGroups } = useFlowChartData(selectedTraceId, initialGoals);
+  const { msgGroups } = useFlowChartData(selectedTraceId, initialGoals, messageRefreshTrigger);
 
 
   const handleNodeClick = (node: Goal | Message, edge?: Edge) => {
   const handleNodeClick = (node: Goal | Message, edge?: Edge) => {
     setSelectedNode(node);
     setSelectedNode(node);
@@ -83,12 +84,14 @@ function App() {
             selectedNode={selectedNode}
             selectedNode={selectedNode}
             onTraceSelect={setSelectedTraceId}
             onTraceSelect={setSelectedTraceId}
             onTraceCreated={() => setRefreshTrigger((t) => t + 1)}
             onTraceCreated={() => setRefreshTrigger((t) => t + 1)}
+            onMessageInserted={() => setMessageRefreshTrigger((t) => t + 1)}
           />
           />
           <MainContent
           <MainContent
             traceId={selectedTraceId}
             traceId={selectedTraceId}
             onNodeClick={handleNodeClick}
             onNodeClick={handleNodeClick}
             onTraceChange={setSelectedTraceId}
             onTraceChange={setSelectedTraceId}
             refreshTrigger={refreshTrigger}
             refreshTrigger={refreshTrigger}
+            messageRefreshTrigger={messageRefreshTrigger}
           />
           />
         </div>
         </div>
         {(selectedNode || selectedEdge) && (
         {(selectedNode || selectedEdge) && (

+ 17 - 2
frontend/react-template/src/api/traceApi.ts

@@ -13,15 +13,30 @@ export const traceApi = {
   fetchTraceDetail(traceId: string) {
   fetchTraceDetail(traceId: string) {
     return request<TraceDetailResponse>(`/api/traces/${traceId}`);
     return request<TraceDetailResponse>(`/api/traces/${traceId}`);
   },
   },
-  createTrace(data: { system_prompt: string; user_prompt: string }) {
+  createTrace(data: {
+    messages: Array<{ role: "system" | "user" | "assistant" | "tool"; content: unknown }>;
+    model?: string;
+    temperature?: number;
+    max_iterations?: number;
+    tools?: string[] | null;
+    name?: string;
+    uid?: string;
+  }) {
     return request<{ trace_id: string }>("/api/traces", {
     return request<{ trace_id: string }>("/api/traces", {
       method: "POST",
       method: "POST",
       data,
       data,
     });
     });
   },
   },
-  runTrace(messageId: string) {
+  runTrace(
+    messageId: string,
+    data?: {
+      messages?: Array<{ role: "system" | "user" | "assistant" | "tool"; content: unknown }>;
+      after_sequence?: number;
+    },
+  ) {
     return request<void>(`/api/traces/${messageId}/run`, {
     return request<void>(`/api/traces/${messageId}/run`, {
       method: "POST",
       method: "POST",
+      data,
     });
     });
   },
   },
   stopTrace(traceId: string) {
   stopTrace(traceId: string) {

+ 0 - 2
frontend/react-template/src/components/FlowChart/FlowChart.tsx

@@ -77,8 +77,6 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
   // 过滤掉有父节点的 goals,只保留主链节点
   // 过滤掉有父节点的 goals,只保留主链节点
   goals = goals.filter((g) => !g.parent_id);
   goals = goals.filter((g) => !g.parent_id);
 
 
-  console.log("%c [ FlowChart-goals ]-33", "font-size:13px; background:pink; color:#bf2c9f;", goals);
-
   // 确保 goals 中包含 END 节点,如果没有则自动添加
   // 确保 goals 中包含 END 节点,如果没有则自动添加
   const displayGoals = useMemo(() => {
   const displayGoals = useMemo(() => {
     if (!goals) return [];
     if (!goals) return [];

+ 20 - 5
frontend/react-template/src/components/FlowChart/hooks/useFlowChartData.ts

@@ -41,7 +41,7 @@ const buildSubGoals = (flatGoals: Goal[]): Goal[] => {
 };
 };
 
 
 // FlowChart 专用数据 Hook:处理实时事件并聚合消息组
 // FlowChart 专用数据 Hook:处理实时事件并聚合消息组
-export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) => {
+export const useFlowChartData = (traceId: string | null, initialGoals: Goal[], refreshTrigger?: number) => {
   const [goals, setGoals] = useState<Goal[]>(initialGoals);
   const [goals, setGoals] = useState<Goal[]>(initialGoals);
   const [messages, setMessages] = useState<Message[]>([]);
   const [messages, setMessages] = useState<Message[]>([]);
   const [msgGroups, setMsgGroups] = useState<Record<string, Message[]>>({});
   const [msgGroups, setMsgGroups] = useState<Record<string, Message[]>>({});
@@ -90,17 +90,21 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
 
 
   useEffect(() => {
   useEffect(() => {
     setGoals(initialGoals);
     setGoals(initialGoals);
+  }, [initialGoals]);
+
+  useEffect(() => {
     setMessages([]);
     setMessages([]);
     setMsgGroups({});
     setMsgGroups({});
     setSinceEventId(0);
     setSinceEventId(0);
     currentEventIdRef.current = 0;
     currentEventIdRef.current = 0;
     restReloadingRef.current = false;
     restReloadingRef.current = false;
-  }, [initialGoals, traceId]);
+  }, [traceId]);
 
 
   const reloadViaRest = useCallback(async () => {
   const reloadViaRest = useCallback(async () => {
     if (!traceId) return;
     if (!traceId) return;
     if (restReloadingRef.current) return;
     if (restReloadingRef.current) return;
     restReloadingRef.current = true;
     restReloadingRef.current = true;
+    let nextSinceEventId: number | null = null;
     try {
     try {
       const [traceRes, messagesRes] = await Promise.all([
       const [traceRes, messagesRes] = await Promise.all([
         fetch(`http://localhost:8000/api/traces/${traceId}`),
         fetch(`http://localhost:8000/api/traces/${traceId}`),
@@ -118,6 +122,7 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
         if (typeof lastEventId === "number") {
         if (typeof lastEventId === "number") {
           currentEventIdRef.current = Math.max(currentEventIdRef.current, lastEventId);
           currentEventIdRef.current = Math.max(currentEventIdRef.current, lastEventId);
           setSinceEventId(lastEventId);
           setSinceEventId(lastEventId);
+          nextSinceEventId = lastEventId;
         }
         }
 
 
         if (goalList.length > 0) {
         if (goalList.length > 0) {
@@ -172,13 +177,21 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
     } finally {
     } finally {
       restReloadingRef.current = false;
       restReloadingRef.current = false;
     }
     }
+    return nextSinceEventId;
   }, [messageSortKey, traceId]);
   }, [messageSortKey, traceId]);
 
 
+  useEffect(() => {
+    if (!traceId) return;
+    if (!refreshTrigger) return;
+    void reloadViaRest();
+  }, [refreshTrigger, reloadViaRest, traceId]);
+
   const handleWebSocketMessage = useCallback(
   const handleWebSocketMessage = useCallback(
     (payload: unknown) => {
     (payload: unknown) => {
       const raw = isRecord(payload) ? payload : {};
       const raw = isRecord(payload) ? payload : {};
       const event = (typeof raw.event === "string" && raw.event) || (typeof raw.type === "string" && raw.type) || "";
       const event = (typeof raw.event === "string" && raw.event) || (typeof raw.type === "string" && raw.type) || "";
       const data = isRecord(raw.data) ? raw.data : raw;
       const data = isRecord(raw.data) ? raw.data : raw;
+      console.log("%c [ data ]-182", "font-size:13px; background:pink; color:#bf2c9f;", data);
 
 
       const eventId = typeof raw.event_id === "number" ? raw.event_id : undefined;
       const eventId = typeof raw.event_id === "number" ? raw.event_id : undefined;
       if (typeof eventId === "number") {
       if (typeof eventId === "number") {
@@ -191,9 +204,11 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
           (typeof raw.message === "string" ? raw.message : undefined) ||
           (typeof raw.message === "string" ? raw.message : undefined) ||
           "";
           "";
         if (message.includes("Too many missed events")) {
         if (message.includes("Too many missed events")) {
-          void reloadViaRest();
-          const nextSince = currentEventIdRef.current;
-          if (nextSince > 0) setSinceEventId(nextSince);
+          void reloadViaRest().then((nextSince) => {
+            if (typeof nextSince === "number") return;
+            const fallbackSince = currentEventIdRef.current;
+            if (fallbackSince > 0) setSinceEventId(fallbackSince);
+          });
         }
         }
         return;
         return;
       }
       }

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

@@ -16,15 +16,22 @@ interface MainContentProps {
   onNodeClick?: (node: Goal | Message, edge?: Edge) => void;
   onNodeClick?: (node: Goal | Message, edge?: Edge) => void;
   onTraceChange?: (traceId: string) => void;
   onTraceChange?: (traceId: string) => void;
   refreshTrigger?: number;
   refreshTrigger?: number;
+  messageRefreshTrigger?: number;
 }
 }
 
 
-export const MainContent: FC<MainContentProps> = ({ traceId, onNodeClick, onTraceChange, refreshTrigger }) => {
+export const MainContent: FC<MainContentProps> = ({
+  traceId,
+  onNodeClick,
+  onTraceChange,
+  refreshTrigger,
+  messageRefreshTrigger,
+}) => {
   const flowChartRef = useRef<FlowChartRef>(null);
   const flowChartRef = useRef<FlowChartRef>(null);
   const [isAllExpanded, setIsAllExpanded] = useState(true);
   const [isAllExpanded, setIsAllExpanded] = useState(true);
   const [traceList, setTraceList] = useState<TraceListItem[]>([]);
   const [traceList, setTraceList] = useState<TraceListItem[]>([]);
   const { trace, loading } = useTrace(traceId);
   const { trace, loading } = useTrace(traceId);
   const initialGoals = useMemo(() => trace?.goal_tree?.goals ?? [], [trace]);
   const initialGoals = useMemo(() => trace?.goal_tree?.goals ?? [], [trace]);
-  const { goals, connected, msgGroups } = useFlowChartData(traceId, initialGoals);
+  const { goals, connected, msgGroups } = useFlowChartData(traceId, initialGoals, messageRefreshTrigger);
 
 
   useEffect(() => {
   useEffect(() => {
     const fetchTraces = async () => {
     const fetchTraces = async () => {
@@ -38,8 +45,6 @@ export const MainContent: FC<MainContentProps> = ({ traceId, onNodeClick, onTrac
     fetchTraces();
     fetchTraces();
   }, [refreshTrigger]);
   }, [refreshTrigger]);
 
 
-  console.log("%c [ MainContent-goals ]-19", "font-size:13px; background:pink; color:#bf2c9f;", goals);
-
   if (!traceId && !loading) {
   if (!traceId && !loading) {
     return (
     return (
       <div className={styles.main}>
       <div className={styles.main}>

+ 72 - 10
frontend/react-template/src/components/TopBar/TopBar.tsx

@@ -11,14 +11,23 @@ interface TopBarProps {
   selectedNode: Goal | Message | null;
   selectedNode: Goal | Message | null;
   onTraceSelect: (traceId: string) => void;
   onTraceSelect: (traceId: string) => void;
   onTraceCreated?: () => void;
   onTraceCreated?: () => void;
+  onMessageInserted?: () => void;
 }
 }
 
 
-export const TopBar: FC<TopBarProps> = ({ selectedTraceId, selectedNode, onTraceSelect, onTraceCreated }) => {
+export const TopBar: FC<TopBarProps> = ({
+  selectedTraceId,
+  selectedNode,
+  onTraceSelect,
+  onTraceCreated,
+  onMessageInserted,
+}) => {
   const [title, setTitle] = useState("流程图可视化系统");
   const [title, setTitle] = useState("流程图可视化系统");
   const [isModalVisible, setIsModalVisible] = useState(false);
   const [isModalVisible, setIsModalVisible] = useState(false);
+  const [isInsertModalVisible, setIsInsertModalVisible] = useState(false);
   const [isExperienceModalVisible, setIsExperienceModalVisible] = useState(false);
   const [isExperienceModalVisible, setIsExperienceModalVisible] = useState(false);
   const [experienceContent, setExperienceContent] = useState("");
   const [experienceContent, setExperienceContent] = useState("");
   const formApiRef = useRef<{ getValues: () => { system_prompt: string; user_prompt: string } } | null>(null);
   const formApiRef = useRef<{ getValues: () => { system_prompt: string; user_prompt: string } } | null>(null);
+  const insertFormApiRef = useRef<{ getValues: () => { insert_prompt: string } } | null>(null);
 
 
   const isMessageNode = (node: Goal | Message): node is Message =>
   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;
     "message_id" in node || "role" in node || "content" in node || "goal_id" in node || "tokens" in node;
@@ -64,7 +73,15 @@ export const TopBar: FC<TopBarProps> = ({ selectedTraceId, selectedNode, onTrace
       const values = formApiRef.current?.getValues();
       const values = formApiRef.current?.getValues();
       if (!values) return;
       if (!values) return;
 
 
-      await traceApi.createTrace(values);
+      const messages: Array<{ role: "system" | "user"; content: string }> = [];
+      if (values.system_prompt) {
+        messages.push({ role: "system", content: values.system_prompt });
+      }
+      if (values.user_prompt) {
+        messages.push({ role: "user", content: values.user_prompt });
+      }
+
+      await traceApi.createTrace({ messages });
       await loadTraces();
       await loadTraces();
       onTraceCreated?.();
       onTraceCreated?.();
       setIsModalVisible(false);
       setIsModalVisible(false);
@@ -75,9 +92,7 @@ export const TopBar: FC<TopBarProps> = ({ selectedTraceId, selectedNode, onTrace
     }
     }
   };
   };
 
 
-  const handleRun = async () => {
-    console.log("%c [ selectedNode ]-72", "font-size:13px; background:pink; color:#bf2c9f;", selectedNode);
-
+  const handleRun = () => {
     if (!selectedNode) {
     if (!selectedNode) {
       Toast.warning("请选择插入节点");
       Toast.warning("请选择插入节点");
       return;
       return;
@@ -88,18 +103,47 @@ export const TopBar: FC<TopBarProps> = ({ selectedTraceId, selectedNode, onTrace
       return;
       return;
     }
     }
 
 
-    const messageId = getMessageId(selectedNode);
+    setIsInsertModalVisible(true);
+  };
+
+  const handleInsertConfirm = async () => {
+    const node = selectedNode;
+    if (!node) {
+      Toast.warning("请选择插入节点");
+      return;
+    }
+
+    if (!isMessageNode(node)) {
+      Toast.warning("插入位置错误");
+      return;
+    }
+
+    const values = insertFormApiRef.current?.getValues();
+    const insertPrompt = values?.insert_prompt?.trim();
+    if (!insertPrompt) {
+      Toast.warning("请输入指令");
+      return;
+    }
+
+    const messageId = getMessageId(node);
     if (!messageId) {
     if (!messageId) {
       Toast.error("消息ID缺失");
       Toast.error("消息ID缺失");
       return;
       return;
     }
     }
 
 
     try {
     try {
-      await traceApi.runTrace(messageId);
-      Toast.success("已开始运行");
+      const sequence = (node as Message & { sequence?: number }).sequence;
+      const payload = {
+        messages: [{ role: "user" as const, content: insertPrompt }],
+        after_sequence: typeof sequence === "number" ? sequence : undefined,
+      };
+      await traceApi.runTrace(messageId, payload);
+      Toast.success("插入成功");
+      setIsInsertModalVisible(false);
+      onMessageInserted?.();
     } catch (error) {
     } catch (error) {
       console.error("Failed to run trace:", error);
       console.error("Failed to run trace:", error);
-      Toast.error("运行请求失败");
+      Toast.error("插入失败");
     }
     }
   };
   };
 
 
@@ -164,7 +208,7 @@ export const TopBar: FC<TopBarProps> = ({ selectedTraceId, selectedNode, onTrace
           className={styles.buttonPrimary}
           className={styles.buttonPrimary}
           onClick={handleRun}
           onClick={handleRun}
         >
         >
-          运行
+          插入
         </button>
         </button>
         <button
         <button
           className={styles.buttonDanger}
           className={styles.buttonDanger}
@@ -209,6 +253,24 @@ export const TopBar: FC<TopBarProps> = ({ selectedTraceId, selectedNode, onTrace
           />
           />
         </Form>
         </Form>
       </Modal>
       </Modal>
+      <Modal
+        title="插入指令"
+        visible={isInsertModalVisible}
+        onOk={handleInsertConfirm}
+        onCancel={() => setIsInsertModalVisible(false)}
+        centered
+        style={{ width: 600 }}
+      >
+        {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
+        <Form getFormApi={(api: any) => (insertFormApiRef.current = api)}>
+          <Form.TextArea
+            field="insert_prompt"
+            label="指令"
+            placeholder="请输入插入指令"
+            autosize={{ minRows: 3, maxRows: 6 }}
+          />
+        </Form>
+      </Modal>
       <Modal
       <Modal
         title="经验列表"
         title="经验列表"
         visible={isExperienceModalVisible}
         visible={isExperienceModalVisible}