App.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  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 { Terminal } from "./components/Terminal/Terminal";
  6. import type { Goal } from "./types/goal";
  7. import type { Edge, Message } from "./types/message";
  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 [showTerminal, setShowTerminal] = useState(false);
  18. const [refreshTrigger, setRefreshTrigger] = useState(0);
  19. const [messageRefreshTrigger, setMessageRefreshTrigger] = useState(0);
  20. const bodyRef = useRef<HTMLDivElement | null>(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 isGoalNode = (node: Goal | Message): node is Goal => "status" in node && "created_at" in node;
  32. // 根据选中的节点获取对应的消息
  33. const selectedMessages = useMemo(() => {
  34. if (selectedNode) {
  35. if (isGoalNode(selectedNode)) {
  36. return msgGroups[selectedNode.id] || [];
  37. }
  38. return [selectedNode as Message];
  39. }
  40. // 如果点击的是边,且该边是主链上的边,通常我们显示源节点的消息(如果需要)
  41. // 但根据用户需求 "边里面的信息,显示对应msgGroup里面的所有的子集的描述"
  42. // 如果 selectedEdge 存在且 selectedNode 为空(虽然目前逻辑是联动),这里可以做处理
  43. // 目前 handleNodeClick 会设置 selectedNode 为 link.source
  44. // 所以 selectedNode.id 就是边的源节点 ID,直接取 msgGroups 即可
  45. return [];
  46. }, [selectedNode, msgGroups]);
  47. useEffect(() => {
  48. if (!isDragging) return;
  49. const handleMove = (event: MouseEvent) => {
  50. const rect = bodyRef.current?.getBoundingClientRect();
  51. if (!rect) return;
  52. const next = rect.right - event.clientX;
  53. const clamped = Math.min(800, Math.max(240, next));
  54. setRightWidth(clamped);
  55. };
  56. const handleUp = () => {
  57. setIsDragging(false);
  58. };
  59. window.addEventListener("mousemove", handleMove);
  60. window.addEventListener("mouseup", handleUp);
  61. return () => {
  62. window.removeEventListener("mousemove", handleMove);
  63. window.removeEventListener("mouseup", handleUp);
  64. };
  65. }, [isDragging]);
  66. return (
  67. <div className="app">
  68. <div className="app-top">
  69. <TopBar
  70. selectedTraceId={selectedTraceId}
  71. selectedNode={selectedNode}
  72. title={selectedTraceTitle}
  73. onTraceSelect={(id, title) => {
  74. setSelectedTraceId(id);
  75. if (title) setSelectedTraceTitle(title);
  76. }}
  77. onTraceCreated={() => setRefreshTrigger((t) => t + 1)}
  78. onMessageInserted={() => setMessageRefreshTrigger((t) => t + 1)}
  79. />
  80. </div>
  81. <div
  82. className="app-body"
  83. ref={bodyRef}
  84. style={{ userSelect: isDragging ? "none" : "auto" }}
  85. >
  86. <div className="app-main">
  87. <MainContent
  88. traceId={selectedTraceId}
  89. onNodeClick={handleNodeClick}
  90. onTraceChange={(id, title) => {
  91. setSelectedTraceId(id);
  92. if (title) setSelectedTraceTitle(title);
  93. }}
  94. refreshTrigger={refreshTrigger}
  95. messageRefreshTrigger={messageRefreshTrigger}
  96. />
  97. </div>
  98. {(selectedNode || selectedEdge) && (
  99. <>
  100. <div
  101. className="app-splitter"
  102. onMouseDown={() => setIsDragging(true)}
  103. role="separator"
  104. aria-orientation="vertical"
  105. aria-valuenow={rightWidth}
  106. aria-valuemin={240}
  107. aria-valuemax={800}
  108. />
  109. <div
  110. className="app-right"
  111. style={{ width: rightWidth }}
  112. >
  113. <DetailPanel
  114. node={selectedNode}
  115. edge={selectedEdge}
  116. messages={selectedMessages as Message[]}
  117. onClose={handleCloseDetail}
  118. />
  119. </div>
  120. </>
  121. )}
  122. </div>
  123. {showTerminal && (
  124. <div className="app-terminal-float">
  125. <Terminal onClose={() => setShowTerminal(false)} />
  126. </div>
  127. )}
  128. {!showTerminal && (
  129. <button
  130. className="app-terminal-toggle"
  131. onClick={() => setShowTerminal(true)}
  132. title="打开控制台"
  133. >
  134. ▶ 控制台
  135. </button>
  136. )}
  137. </div>
  138. );
  139. }
  140. export default App;