MainContent.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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 } = useFlowChartData(traceId, messageRefreshTrigger);
  47. console.log("%c [ msgGroups ]-34", "font-size:13px; background:pink; color:#bf2c9f;", msgGroups);
  48. const displayGoals = goals.length > 0 ? goals : cachedGoals;
  49. const displayMsgGroups = Object.keys(msgGroups).length > 0 ? msgGroups : cachedMsgGroups;
  50. const displayInvalidBranches =
  51. invalidBranches && invalidBranches.length > 0 ? invalidBranches : cachedInvalidBranches;
  52. useEffect(() => {
  53. const fetchTraces = async () => {
  54. try {
  55. const data = await traceApi.fetchTraces({ limit: 100 });
  56. setTraceList(data.traces);
  57. } catch (error) {
  58. console.error("Failed to load traces:", error);
  59. }
  60. };
  61. fetchTraces();
  62. }, [refreshTrigger]);
  63. useEffect(() => {
  64. // 移除 reload 调用,因为 useFlowChartData 内部会监听 messageRefreshTrigger 并重新加载
  65. }, [messageRefreshTrigger]);
  66. useEffect(() => {
  67. if (goals.length > 0) {
  68. setCachedGoals(goals);
  69. }
  70. }, [goals]);
  71. useEffect(() => {
  72. if (Object.keys(msgGroups).length > 0) {
  73. setCachedMsgGroups(msgGroups);
  74. }
  75. }, [msgGroups]);
  76. useEffect(() => {
  77. if (invalidBranches && invalidBranches.length > 0) {
  78. setCachedInvalidBranches(invalidBranches);
  79. }
  80. }, [invalidBranches]);
  81. useEffect(() => {
  82. setCachedGoals([]);
  83. setCachedMsgGroups({});
  84. setCachedInvalidBranches([]);
  85. }, [traceId]);
  86. if (!traceId && !reloading) {
  87. return (
  88. <div className={styles.main}>
  89. <div className={styles.header}>
  90. <div className={styles.title}>暂无 Trace</div>
  91. <ConnectionStatus isConnected={connected} />
  92. </div>
  93. <div className={styles.content}>
  94. <div className={styles.empty}>暂无可展示的数据</div>
  95. </div>
  96. </div>
  97. );
  98. }
  99. return (
  100. <div className={styles.main}>
  101. <div className={styles.header}>
  102. <ConnectionStatus isConnected={connected} />
  103. <div className={styles.headerRight}>
  104. <Select
  105. value={traceId}
  106. onChange={(value: unknown) => {
  107. const trace = traceList.find((t) => t.trace_id === value);
  108. onTraceChange?.(value as string, trace?.task || trace?.trace_id);
  109. }}
  110. style={{ width: 200 }}
  111. placeholder="选择 Trace"
  112. optionList={traceList.map((t) => ({
  113. label: t.task?.length > 15 ? `${t.task.slice(0, 15)}...` : t.task || t.trace_id,
  114. value: t.trace_id,
  115. }))}
  116. />
  117. {/* <div className={styles.status}>{connected ? "WebSocket 已连接" : "WebSocket 未连接"}</div> */}
  118. {/* <div className={styles.legend}>
  119. <div className={styles.legendItem}>
  120. <span
  121. className={styles.legendDot}
  122. style={{ background: "#00c853" }}
  123. />
  124. 已完成
  125. </div>
  126. <div className={styles.legendItem}>
  127. <span
  128. className={styles.legendDot}
  129. style={{ background: "#f44336" }}
  130. />
  131. 失败
  132. </div>
  133. <div className={styles.legendItem}>
  134. <span
  135. className={styles.legendDot}
  136. style={{ background: "#ff9800" }}
  137. />
  138. 运行中
  139. </div>
  140. <div className={styles.legendItem}>
  141. <span
  142. className={styles.legendDot}
  143. style={{ background: "#4e79a7" }}
  144. />
  145. 默认
  146. </div>
  147. </div> */}
  148. <div className={styles.buttons}>
  149. <button
  150. className={styles.btn}
  151. onClick={() => {
  152. if (isAllExpanded) {
  153. flowChartRef.current?.collapseAll();
  154. } else {
  155. flowChartRef.current?.expandAll();
  156. }
  157. setIsAllExpanded(!isAllExpanded);
  158. }}
  159. >
  160. {isAllExpanded ? "全折叠" : "全展开"}
  161. </button>
  162. </div>
  163. </div>
  164. </div>
  165. <div className={styles.content}>
  166. {reloading ? (
  167. <div className={styles.loading}>加载中...</div>
  168. ) : displayGoals.length === 0 ? (
  169. <div className={styles.empty}>暂无数据</div>
  170. ) : (
  171. <FlowChart
  172. ref={flowChartRef}
  173. goals={displayGoals}
  174. msgGroups={displayMsgGroups}
  175. invalidBranches={displayInvalidBranches}
  176. onNodeClick={onNodeClick}
  177. onSubTraceClick={(_parentGoal, entry) => {
  178. onTraceChange?.(entry.id, entry.mission || entry.id);
  179. }}
  180. />
  181. )}
  182. </div>
  183. </div>
  184. );
  185. };