|
|
@@ -1,4 +1,4 @@
|
|
|
-import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
import { useWebSocket } from "../../../hooks/useWebSocket";
|
|
|
import type { Goal } from "../../../types/goal";
|
|
|
import type { Message } from "../../../types/message";
|
|
|
@@ -45,6 +45,9 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
|
|
|
const [goals, setGoals] = useState<Goal[]>(initialGoals);
|
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
|
const [msgGroups, setMsgGroups] = useState<Record<string, Message[]>>({});
|
|
|
+ const [sinceEventId, setSinceEventId] = useState(0);
|
|
|
+ const currentEventIdRef = useRef(0);
|
|
|
+ const restReloadingRef = useRef(false);
|
|
|
|
|
|
const messageSortKey = useCallback((message: Message): number => {
|
|
|
const mid =
|
|
|
@@ -89,15 +92,120 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
|
|
|
setGoals(initialGoals);
|
|
|
setMessages([]);
|
|
|
setMsgGroups({});
|
|
|
+ setSinceEventId(0);
|
|
|
+ currentEventIdRef.current = 0;
|
|
|
+ restReloadingRef.current = false;
|
|
|
}, [initialGoals, traceId]);
|
|
|
|
|
|
+ const reloadViaRest = useCallback(async () => {
|
|
|
+ if (!traceId) return;
|
|
|
+ if (restReloadingRef.current) return;
|
|
|
+ restReloadingRef.current = true;
|
|
|
+ try {
|
|
|
+ const [traceRes, messagesRes] = await Promise.all([
|
|
|
+ fetch(`http://localhost:8000/api/traces/${traceId}`),
|
|
|
+ fetch(`http://localhost:8000/api/traces/${traceId}/messages`),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (traceRes.ok) {
|
|
|
+ const json = (await traceRes.json()) as unknown;
|
|
|
+ const root = isRecord(json) ? json : {};
|
|
|
+ const trace = isRecord(root.trace) ? root.trace : undefined;
|
|
|
+ const goalTree = isRecord(root.goal_tree) ? root.goal_tree : undefined;
|
|
|
+ const goalList = goalTree && Array.isArray(goalTree.goals) ? (goalTree.goals as Goal[]) : [];
|
|
|
+
|
|
|
+ const lastEventId = trace && typeof trace.last_event_id === "number" ? trace.last_event_id : undefined;
|
|
|
+ if (typeof lastEventId === "number") {
|
|
|
+ currentEventIdRef.current = Math.max(currentEventIdRef.current, lastEventId);
|
|
|
+ setSinceEventId(lastEventId);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (goalList.length > 0) {
|
|
|
+ setGoals((prev) => {
|
|
|
+ const mergedFlat = goalList.map((ng) => {
|
|
|
+ const existing = prev.find((p) => p.id === ng.id);
|
|
|
+ if (!existing) return ng;
|
|
|
+ const merged: Goal = { ...existing, ...ng };
|
|
|
+ if (existing.sub_trace_ids && !merged.sub_trace_ids) {
|
|
|
+ merged.sub_trace_ids = existing.sub_trace_ids;
|
|
|
+ }
|
|
|
+ if (existing.agent_call_mode && !merged.agent_call_mode) {
|
|
|
+ merged.agent_call_mode = existing.agent_call_mode;
|
|
|
+ }
|
|
|
+ return merged;
|
|
|
+ });
|
|
|
+ return buildSubGoals(mergedFlat);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (messagesRes.ok) {
|
|
|
+ const json = (await messagesRes.json()) as unknown;
|
|
|
+ const root = isRecord(json) ? json : {};
|
|
|
+ const list = Array.isArray(root.messages) ? (root.messages as Message[]) : [];
|
|
|
+ const nextMessages = [...list].sort((a, b) => messageSortKey(a) - messageSortKey(b));
|
|
|
+ setMessages(nextMessages);
|
|
|
+ const grouped: Record<string, Message[]> = {};
|
|
|
+ nextMessages.forEach((message) => {
|
|
|
+ const groupKey = typeof message.goal_id === "string" && message.goal_id ? message.goal_id : "START";
|
|
|
+ if (!grouped[groupKey]) grouped[groupKey] = [];
|
|
|
+ grouped[groupKey].push(message);
|
|
|
+ });
|
|
|
+ Object.keys(grouped).forEach((key) => {
|
|
|
+ grouped[key].sort((a, b) => messageSortKey(a) - messageSortKey(b));
|
|
|
+ });
|
|
|
+ setMsgGroups(grouped);
|
|
|
+
|
|
|
+ if (grouped.START && grouped.START.length > 0) {
|
|
|
+ setGoals((prev) => {
|
|
|
+ if (prev.some((g) => g.id === "START")) return prev;
|
|
|
+ const startGoal: Goal = {
|
|
|
+ id: "START",
|
|
|
+ description: "START",
|
|
|
+ status: "completed",
|
|
|
+ created_at: "",
|
|
|
+ };
|
|
|
+ return [startGoal, ...prev];
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ restReloadingRef.current = false;
|
|
|
+ }
|
|
|
+ }, [messageSortKey, traceId]);
|
|
|
+
|
|
|
const handleWebSocketMessage = useCallback(
|
|
|
(payload: unknown) => {
|
|
|
const raw = isRecord(payload) ? payload : {};
|
|
|
const event = (typeof raw.event === "string" && raw.event) || (typeof raw.type === "string" && raw.type) || "";
|
|
|
const data = isRecord(raw.data) ? raw.data : raw;
|
|
|
|
|
|
+ const eventId = typeof raw.event_id === "number" ? raw.event_id : undefined;
|
|
|
+ if (typeof eventId === "number") {
|
|
|
+ currentEventIdRef.current = Math.max(currentEventIdRef.current, eventId);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (event === "error") {
|
|
|
+ const message =
|
|
|
+ (typeof data.message === "string" ? data.message : undefined) ||
|
|
|
+ (typeof raw.message === "string" ? raw.message : undefined) ||
|
|
|
+ "";
|
|
|
+ if (message.includes("Too many missed events")) {
|
|
|
+ void reloadViaRest();
|
|
|
+ const nextSince = currentEventIdRef.current;
|
|
|
+ if (nextSince > 0) setSinceEventId(nextSince);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
if (event === "connected") {
|
|
|
+ const currentEventId =
|
|
|
+ (typeof data.current_event_id === "number" ? data.current_event_id : undefined) ||
|
|
|
+ (typeof raw.current_event_id === "number" ? raw.current_event_id : undefined);
|
|
|
+ if (typeof currentEventId === "number") {
|
|
|
+ currentEventIdRef.current = Math.max(currentEventIdRef.current, currentEventId);
|
|
|
+ }
|
|
|
+
|
|
|
const trace = isRecord(data.trace) ? data.trace : undefined;
|
|
|
const rawTrace = isRecord(raw.trace) ? raw.trace : undefined;
|
|
|
const goalTree =
|
|
|
@@ -192,11 +300,14 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
- [messageSortKey, updateMessageGroups],
|
|
|
+ [messageSortKey, reloadViaRest, updateMessageGroups],
|
|
|
);
|
|
|
|
|
|
// 主 Trace 连接
|
|
|
- const wsOptions = useMemo(() => ({ onMessage: handleWebSocketMessage }), [handleWebSocketMessage]);
|
|
|
+ const wsOptions = useMemo(
|
|
|
+ () => ({ onMessage: handleWebSocketMessage, sinceEventId }),
|
|
|
+ [handleWebSocketMessage, sinceEventId],
|
|
|
+ );
|
|
|
const { connected } = useWebSocket(traceId, wsOptions);
|
|
|
|
|
|
return { goals, messages, msgGroups, connected };
|