App.tsx 4.9 KB

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