App.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { useEffect, useMemo, useRef, useState } from "react";
  2. import { TopBar } from "./components/TopBar/TopBar";
  3. import { MainContent } from "./components/MainContent/MainContent";
  4. import { DetailPanel } from "./components/DetailPanel/DetailPanel";
  5. import type { Goal } from "./types/goal";
  6. import type { Edge, Message } from "./types/message";
  7. import type { FlowChartRef } from "./components/FlowChart/FlowChart";
  8. import { useFlowChartData } from "./components/FlowChart/hooks/useFlowChartData";
  9. import "./styles/global.css";
  10. function App() {
  11. const [selectedTraceId, setSelectedTraceId] = useState<string | null>(null);
  12. const [selectedTraceTitle, setSelectedTraceTitle] = useState("流程图可视化系统");
  13. const [selectedNode, setSelectedNode] = useState<Goal | Message | null>(null);
  14. const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
  15. const [rightWidth, setRightWidth] = useState(360);
  16. const [isDragging, setIsDragging] = useState(false);
  17. const [refreshTrigger, setRefreshTrigger] = useState(0);
  18. const [messageRefreshTrigger, setMessageRefreshTrigger] = useState(0);
  19. const bodyRef = useRef<HTMLDivElement | null>(null);
  20. const flowChartRef = useRef<FlowChartRef>(null);
  21. // 获取数据以传递给 DetailPanel
  22. const { msgGroups } = useFlowChartData(selectedTraceId, messageRefreshTrigger);
  23. const handleNodeClick = (node: Goal | Message, edge?: Edge) => {
  24. setSelectedNode(node);
  25. setSelectedEdge(edge || null);
  26. };
  27. const handleCloseDetail = () => {
  28. setSelectedNode(null);
  29. setSelectedEdge(null);
  30. };
  31. const handleNavigateToMessage = (goalId: string, sequence: number) => {
  32. // 先在对应 goal 的 group 里找
  33. let target = (msgGroups[goalId] ?? []).find((m) => m.sequence === sequence);
  34. // 兜底:跨所有 group 搜索
  35. if (!target) {
  36. for (const msgs of Object.values(msgGroups)) {
  37. target = msgs.find((m) => m.sequence === sequence);
  38. if (target) break;
  39. }
  40. }
  41. if (target) handleNodeClick(target);
  42. // 顺滑滚动到节点
  43. flowChartRef.current?.scrollToSequence(sequence);
  44. };
  45. const isGoalNode = (node: Goal | Message): node is Goal => "status" in node && "created_at" in node;
  46. // 根据选中的节点获取对应的消息
  47. const selectedMessages = useMemo(() => {
  48. if (selectedNode) {
  49. if (isGoalNode(selectedNode)) {
  50. return msgGroups[selectedNode.id] || [];
  51. }
  52. return [selectedNode as Message];
  53. }
  54. // 如果点击的是边,且该边是主链上的边,通常我们显示源节点的消息(如果需要)
  55. // 但根据用户需求 "边里面的信息,显示对应msgGroup里面的所有的子集的描述"
  56. // 如果 selectedEdge 存在且 selectedNode 为空(虽然目前逻辑是联动),这里可以做处理
  57. // 目前 handleNodeClick 会设置 selectedNode 为 link.source
  58. // 所以 selectedNode.id 就是边的源节点 ID,直接取 msgGroups 即可
  59. return [];
  60. }, [selectedNode, msgGroups]);
  61. useEffect(() => {
  62. if (!isDragging) return;
  63. const handleMove = (event: MouseEvent) => {
  64. const rect = bodyRef.current?.getBoundingClientRect();
  65. if (!rect) return;
  66. const next = rect.right - event.clientX;
  67. const clamped = Math.min(800, Math.max(240, next));
  68. setRightWidth(clamped);
  69. };
  70. const handleUp = () => {
  71. setIsDragging(false);
  72. };
  73. window.addEventListener("mousemove", handleMove);
  74. window.addEventListener("mouseup", handleUp);
  75. return () => {
  76. window.removeEventListener("mousemove", handleMove);
  77. window.removeEventListener("mouseup", handleUp);
  78. };
  79. }, [isDragging]);
  80. return (
  81. <div className="app">
  82. <div className="app-top">
  83. <TopBar
  84. selectedTraceId={selectedTraceId}
  85. selectedNode={selectedNode}
  86. title={selectedTraceTitle}
  87. onTraceSelect={(id, title) => {
  88. setSelectedTraceId(id);
  89. if (title) setSelectedTraceTitle(title);
  90. }}
  91. onTraceCreated={() => setRefreshTrigger((t) => t + 1)}
  92. onMessageInserted={() => setMessageRefreshTrigger((t) => t + 1)}
  93. onNavigateToMessage={handleNavigateToMessage}
  94. />
  95. </div>
  96. <div
  97. className="app-body"
  98. ref={bodyRef}
  99. style={{ userSelect: isDragging ? "none" : "auto" }}
  100. >
  101. <div className="app-main">
  102. <MainContent
  103. traceId={selectedTraceId}
  104. onNodeClick={handleNodeClick}
  105. onTraceChange={(id, title) => {
  106. setSelectedTraceId(id);
  107. if (title) setSelectedTraceTitle(title);
  108. }}
  109. refreshTrigger={refreshTrigger}
  110. messageRefreshTrigger={messageRefreshTrigger}
  111. flowChartRef={flowChartRef}
  112. />
  113. </div>
  114. {(selectedNode || selectedEdge) && (
  115. <>
  116. <div
  117. className="app-splitter"
  118. onMouseDown={() => setIsDragging(true)}
  119. role="separator"
  120. aria-orientation="vertical"
  121. aria-valuenow={rightWidth}
  122. aria-valuemin={240}
  123. aria-valuemax={800}
  124. />
  125. <div
  126. className="app-right"
  127. style={{ width: rightWidth }}
  128. >
  129. <DetailPanel
  130. node={selectedNode}
  131. edge={selectedEdge}
  132. messages={selectedMessages as Message[]}
  133. onClose={handleCloseDetail}
  134. traceId={selectedTraceId ?? undefined}
  135. />
  136. </div>
  137. </>
  138. )}
  139. </div>
  140. </div>
  141. );
  142. }
  143. export default App;