MainContent.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import { useRef, useState, useEffect } from "react";
  2. import type { FC, RefObject } from "react";
  3. import { Select } from "@douyinfe/semi-ui";
  4. import { FlowChart } from "../FlowChart/FlowChart";
  5. import type { FlowChartRef } from "../FlowChart/FlowChart";
  6. import { useFlowChartData } from "../FlowChart/hooks/useFlowChartData";
  7. import { traceApi } from "../../api/traceApi";
  8. import type { Goal } from "../../types/goal";
  9. import type { Edge, Message } from "../../types/message";
  10. import type { TraceListItem } from "../../types/trace";
  11. import styles from "./MainContent.module.css";
  12. interface MainContentProps {
  13. traceId: string | null;
  14. onNodeClick?: (node: Goal | Message, edge?: Edge) => void;
  15. onTraceChange?: (traceId: string, title?: string) => void;
  16. refreshTrigger?: number;
  17. messageRefreshTrigger?: number;
  18. flowChartRef?: RefObject<FlowChartRef>;
  19. }
  20. interface ConnectionStatusProps {
  21. isConnected: boolean;
  22. }
  23. const ConnectionStatus: FC<ConnectionStatusProps> = ({ isConnected }) => {
  24. return (
  25. <div
  26. className={`${styles.status} ${isConnected ? styles.connected : styles.disconnected}`}
  27. data-testid="connection-status"
  28. >
  29. <span className={styles.statusDot} />
  30. <span>{isConnected ? "WebSocket 已连接" : "WebSocket 未连接"}</span>
  31. </div>
  32. );
  33. };
  34. export const MainContent: FC<MainContentProps> = ({
  35. traceId,
  36. onNodeClick,
  37. onTraceChange,
  38. refreshTrigger,
  39. messageRefreshTrigger,
  40. flowChartRef: externalFlowChartRef,
  41. }) => {
  42. const internalFlowChartRef = useRef<FlowChartRef>(null);
  43. const flowChartRef = externalFlowChartRef ?? internalFlowChartRef;
  44. const [isAllExpanded, setIsAllExpanded] = useState(true);
  45. const [traceList, setTraceList] = useState<TraceListItem[]>([]);
  46. const [cachedGoals, setCachedGoals] = useState<Goal[]>([]);
  47. const [cachedMsgGroups, setCachedMsgGroups] = useState<Record<string, Message[]>>({});
  48. const [cachedInvalidBranches, setCachedInvalidBranches] = useState<Message[][]>([]);
  49. const { goals, connected, msgGroups, reloading, invalidBranches, traceCompleted } = useFlowChartData(
  50. traceId,
  51. messageRefreshTrigger,
  52. );
  53. console.log("%c [ msgGroups ]-34", "font-size:13px; background:pink; color:#bf2c9f;", msgGroups);
  54. const displayGoals = goals.length > 0 ? goals : cachedGoals;
  55. const displayMsgGroups = Object.keys(msgGroups).length > 0 ? msgGroups : cachedMsgGroups;
  56. const displayInvalidBranches =
  57. invalidBranches && invalidBranches.length > 0 ? invalidBranches : cachedInvalidBranches;
  58. useEffect(() => {
  59. const fetchTraces = async () => {
  60. try {
  61. const data = await traceApi.fetchTraces({ limit: 100 });
  62. setTraceList(data.traces);
  63. } catch (error) {
  64. console.error("Failed to load traces:", error);
  65. }
  66. };
  67. fetchTraces();
  68. }, [refreshTrigger]);
  69. useEffect(() => {
  70. // 移除 reload 调用,因为 useFlowChartData 内部会监听 messageRefreshTrigger 并重新加载
  71. }, [messageRefreshTrigger]);
  72. useEffect(() => {
  73. if (goals.length > 0) {
  74. setCachedGoals(goals);
  75. }
  76. }, [goals]);
  77. useEffect(() => {
  78. if (Object.keys(msgGroups).length > 0) {
  79. setCachedMsgGroups(msgGroups);
  80. }
  81. }, [msgGroups]);
  82. useEffect(() => {
  83. if (invalidBranches && invalidBranches.length > 0) {
  84. setCachedInvalidBranches(invalidBranches);
  85. }
  86. }, [invalidBranches]);
  87. useEffect(() => {
  88. setCachedGoals([]);
  89. setCachedMsgGroups({});
  90. setCachedInvalidBranches([]);
  91. }, [traceId]);
  92. if (!traceId && !reloading) {
  93. return (
  94. <div className={styles.main}>
  95. <div className={styles.header}>
  96. <div className={styles.title}>暂无 Trace</div>
  97. <ConnectionStatus isConnected={connected} />
  98. </div>
  99. <div className={styles.content}>
  100. <div className={styles.empty}>暂无可展示的数据</div>
  101. </div>
  102. </div>
  103. );
  104. }
  105. return (
  106. <div className={styles.main}>
  107. <div className={styles.header}>
  108. <div style={{ display: "flex", alignItems: "center", gap: 16 }}>
  109. <ConnectionStatus isConnected={connected} />
  110. {traceCompleted && (
  111. <div className={styles.completedBadge}>
  112. <span style={{ fontSize: 16 }}>✓</span>
  113. <span>执行完成</span>
  114. </div>
  115. )}
  116. </div>
  117. <div className={styles.headerRight}>
  118. <Select
  119. value={traceId}
  120. onChange={(value: unknown) => {
  121. const trace = traceList.find((t) => t.trace_id === value);
  122. onTraceChange?.(value as string, trace?.task || trace?.trace_id);
  123. }}
  124. style={{ width: 400 }}
  125. placeholder="选择 Trace"
  126. optionList={traceList.map((t) => {
  127. const taskDesc = t.task && t.task.length > 50 ? `${t.task.slice(0, 50)}...` : t.task;
  128. return {
  129. label: taskDesc || t.trace_id,
  130. value: t.trace_id,
  131. };
  132. })}
  133. />
  134. {/* <div className={styles.status}>{connected ? "WebSocket 已连接" : "WebSocket 未连接"}</div> */}
  135. {/* <div className={styles.legend}>
  136. <div className={styles.legendItem}>
  137. <span
  138. className={styles.legendDot}
  139. style={{ background: "#00c853" }}
  140. />
  141. 已完成
  142. </div>
  143. <div className={styles.legendItem}>
  144. <span
  145. className={styles.legendDot}
  146. style={{ background: "#f44336" }}
  147. />
  148. 失败
  149. </div>
  150. <div className={styles.legendItem}>
  151. <span
  152. className={styles.legendDot}
  153. style={{ background: "#ff9800" }}
  154. />
  155. 运行中
  156. </div>
  157. <div className={styles.legendItem}>
  158. <span
  159. className={styles.legendDot}
  160. style={{ background: "#4e79a7" }}
  161. />
  162. 默认
  163. </div>
  164. </div> */}
  165. <div className={styles.buttons}>
  166. <button
  167. className={styles.btn}
  168. onClick={() => {
  169. if (isAllExpanded) {
  170. flowChartRef.current?.collapseAll();
  171. } else {
  172. flowChartRef.current?.expandAll();
  173. }
  174. setIsAllExpanded(!isAllExpanded);
  175. }}
  176. >
  177. {isAllExpanded ? "全折叠" : "全展开"}
  178. </button>
  179. </div>
  180. </div>
  181. </div>
  182. <div className={styles.content}>
  183. {reloading ? (
  184. <div className={styles.loading}>加载中...</div>
  185. ) : displayGoals.length === 0 ? (
  186. <div className={styles.empty}>暂无数据</div>
  187. ) : (
  188. <FlowChart
  189. ref={flowChartRef}
  190. goals={displayGoals}
  191. msgGroups={displayMsgGroups}
  192. invalidBranches={displayInvalidBranches}
  193. onNodeClick={onNodeClick}
  194. onSubTraceClick={(_parentGoal, entry) => {
  195. onTraceChange?.(entry.id, entry.mission || entry.id);
  196. }}
  197. />
  198. )}
  199. </div>
  200. </div>
  201. );
  202. };