MainContent.tsx 6.9 KB

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