Parcourir la source

Merge remote-tracking branch 'refs/remotes/origin/main'

Talegorithm il y a 3 semaines
Parent
commit
357c3b0b96

+ 148 - 36
frontend/react-template/src/components/FlowChart/FlowChart.tsx

@@ -175,7 +175,7 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
     const nodes: LayoutNode[] = []; // 所有节点列表
     const edges: LayoutEdge[] = []; // 所有连接线列表
 
-    const NODE_HEIGHT = 80; // 节点间距(纵向)
+    const NODE_HEIGHT = 110; // 节点间距(纵向)
     const centerX = dimensions.width / 2; // 主链节点的 X 坐标(居中)
     const HORIZONTAL_OFFSET = 0; // 子节点水平偏移量
 
@@ -213,17 +213,7 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
       let lastNode = currentNode; // 记录最后一个节点,用于连接到下一个主链节点
       globalY += NODE_HEIGHT; // 更新全局 Y 坐标
 
-      // 递归展开 sub_goals
-      if (goal.sub_goals && goal.sub_goals.length > 0) {
-        goal.sub_goals.forEach((subGoal) => {
-          const subX = x + HORIZONTAL_OFFSET; // 子节点向右偏移
-          const result = expandNode(subGoal, subX, level + 1, nodeId);
-          localNodes.push(...result.nodes);
-          lastNode = result.lastNode; // 更新最后一个节点
-        });
-      }
-
-      // 展开 msgGroup(消息组)
+      // 1. 先展开 msgGroup(消息组)- 按照用户需求,消息显示在 sub_goals 之前
       const messages = msgGroups[nodeId];
       if (messages && messages.length > 0) {
         messages.forEach((msg) => {
@@ -242,6 +232,16 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
         });
       }
 
+      // 2. 再递归展开 sub_goals
+      if (goal.sub_goals && goal.sub_goals.length > 0) {
+        goal.sub_goals.forEach((subGoal) => {
+          const subX = x + HORIZONTAL_OFFSET; // 子节点向右偏移
+          const result = expandNode(subGoal, subX, level + 1, nodeId);
+          localNodes.push(...result.nodes);
+          lastNode = result.lastNode; // 更新最后一个节点
+        });
+      }
+
       return { nodes: localNodes, firstNode: currentNode, lastNode };
     };
 
@@ -308,6 +308,26 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
         const childNodes = current.allNodes.slice(1);
         const directChildren = childNodes.filter((n) => n.parentId === current.firstNode.id);
 
+        // Logic A: Messages Group Arc (A -> s1)
+        // 检查是否有消息节点且后面跟着子目标节点
+        // 如果有,绘制一条蓝色弧线从主节点到第一个子目标节点,包裹所有消息节点
+        const firstSubgoalIndex = directChildren.findIndex((n) => n.type === "goal");
+        if (firstSubgoalIndex > 0) {
+          const target = directChildren[firstSubgoalIndex];
+          const messages = directChildren.slice(0, firstSubgoalIndex);
+          const arcId = `arc-msg-${current.firstNode.id}-${target.id}`;
+          edges.push({
+            id: arcId,
+            source: current.firstNode,
+            target: target,
+            type: "arc",
+            level: 1,
+            collapsible: true,
+            collapsed: collapsedEdges.has(arcId),
+            children: messages,
+          });
+        }
+
         // 连接第一个节点到第一个子节点
         if (directChildren.length > 0) {
           edges.push({
@@ -407,6 +427,50 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
             collapsible: false,
             collapsed: false,
           });
+
+          // Logic C: Last Child Arc (s2 -> B)
+          // 如果最后一个直接子节点是 sub_goal 且有子节点,绘制弧线连接到下一个主节点
+          const lastChild = directChildren[directChildren.length - 1];
+          const lastChildChildren = nodes.filter((n) => n.parentId === lastChild.id);
+
+          if (lastChild.type === "goal" && lastChildChildren.length > 0) {
+            const arcId = `arc-${lastChild.id}-${next.firstNode.id}`;
+            edges.push({
+              id: arcId,
+              source: lastChild,
+              target: next.firstNode,
+              type: "arc",
+              level: 1,
+              collapsible: true,
+              collapsed: collapsedEdges.has(arcId),
+              children: lastChildChildren,
+            });
+
+            // 补充绘制最后一个子节点的内部连线(直线)
+            // 1. 连接 lastChild 到第一个孙节点
+            edges.push({
+              id: `line-${lastChild.id}-${lastChildChildren[0].id}`,
+              source: lastChild,
+              target: lastChildChildren[0],
+              type: "line",
+              level: lastChild.level + 1,
+              collapsible: false,
+              collapsed: false,
+            });
+
+            // 2. 连接孙节点之间
+            for (let k = 0; k < lastChildChildren.length - 1; k++) {
+              edges.push({
+                id: `line-${lastChildChildren[k].id}-${lastChildChildren[k + 1].id}`,
+                source: lastChildChildren[k],
+                target: lastChildChildren[k + 1],
+                type: "line",
+                level: lastChild.level + 1,
+                collapsible: false,
+                collapsed: false,
+              });
+            }
+          }
         }
       } else {
         // 情况 4:没有子节点,直接绘制直线
@@ -452,10 +516,20 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
    * 并且调整后续节点的位置,填补折叠后的空白
    */
   const visibleData = useMemo(() => {
-    const NODE_HEIGHT = 80;
+    const NODE_HEIGHT = 110;
 
     // 1. 找出所有折叠的边
-    const collapsedEdgesList = layoutData.edges.filter((e) => e.collapsed);
+    const allCollapsedEdges = layoutData.edges.filter((e) => e.collapsed);
+
+    // 筛选出“有效”的折叠边:如果一个折叠边被另一个更大的折叠边包含(即其节点被隐藏),则忽略它
+    // 避免重复计算 shiftY
+    const collapsedEdgesList = allCollapsedEdges.filter((edge) => {
+      return !allCollapsedEdges.some((other) => {
+        if (edge === other) return false;
+        // 如果当前边的源节点或目标节点在另一个折叠边的子节点列表中,说明当前边是被包裹在内部的
+        return other.children?.some((child) => child.id === edge.source.id || child.id === edge.target.id);
+      });
+    });
 
     // 2. 过滤节点:隐藏被折叠的节点
     const visibleNodesRaw = layoutData.nodes.filter((node) => {
@@ -582,9 +656,9 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
    */
   const getArcPath = (source: LayoutNode, target: LayoutNode, level: number) => {
     const sx = source.x;
-    const sy = source.y;
+    const sy = source.y + 25; // 从节点底部出发
     const tx = target.x;
-    const ty = target.y;
+    const ty = target.y - 25; // 到节点顶部结束
 
     // 弧线向右偏移,偏移量根据层级递减
     // 外层弧线偏移量大,内层弧线偏移量小
@@ -604,7 +678,7 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
    * @returns SVG 路径字符串
    */
   const getLinePath = (source: LayoutNode, target: LayoutNode) => {
-    return `M ${source.x},${source.y} L ${target.x},${target.y}`;
+    return `M ${source.x},${source.y + 25} L ${target.x},${target.y - 25}`;
   };
 
   /**
@@ -745,16 +819,47 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
               }
 
               return (
-                <path
-                  key={edge.id}
-                  d={path}
-                  fill="none"
-                  stroke={color}
-                  strokeWidth={strokeWidth}
-                  markerEnd="url(#arrow-default)" // 箭头
-                  style={{ cursor: edge.collapsible ? "pointer" : "default" }} // 可折叠的显示手型光标
-                  onClick={() => edge.collapsible && toggleCollapse(edge.id)} // 点击切换折叠状态
-                />
+                <g key={edge.id}>
+                  <path
+                    d={path}
+                    fill="none"
+                    stroke={color}
+                    strokeWidth={strokeWidth}
+                    markerEnd="url(#arrow-default)" // 箭头
+                    style={{ cursor: edge.collapsible ? "pointer" : "default" }} // 可折叠的显示手型光标
+                    onClick={() => edge.collapsible && toggleCollapse(edge.id)} // 点击切换折叠状态
+                  />
+                  {/* 折叠状态提示徽章 */}
+                  {edge.collapsed && (
+                    <g
+                      transform={`translate(${(edge.source.x + edge.target.x) / 2},${
+                        (edge.source.y + edge.target.y) / 2
+                      })`}
+                      onClick={(e) => {
+                        e.stopPropagation();
+                        toggleCollapse(edge.id);
+                      }}
+                      style={{ cursor: "pointer" }}
+                    >
+                      <circle
+                        r={10}
+                        fill="#FFFFFF"
+                        stroke={color}
+                        strokeWidth={1}
+                      />
+                      <text
+                        x={0}
+                        y={4}
+                        fontSize={10}
+                        fill={color}
+                        textAnchor="middle"
+                        fontWeight="bold"
+                      >
+                        {edge.children ? edge.children.length : "+"}
+                      </text>
+                    </g>
+                  )}
+                </g>
               );
             })}
           </g>
@@ -784,7 +889,7 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
                   <rect
                     x={-70}
                     y={-25}
-                    width={140}
+                    width={150}
                     height={50}
                     rx={8}
                     fill={isGoal ? "#E3F2FD" : "#F5F5F5"} // 目标节点浅蓝色,消息节点灰色
@@ -793,15 +898,22 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
                   />
                   {/* 节点文本(带 Tooltip) */}
                   <Tooltip content={text}>
-                    <text
-                      x={0}
-                      y={5}
-                      fontSize={12}
-                      fill={textColor}
-                      textAnchor="middle"
+                    <foreignObject
+                      x={-70}
+                      y={-25}
+                      width={150}
+                      height={50}
                     >
-                      {text.length > 10 ? text.slice(0, 10) + "..." : text}
-                    </text>
+                      <div
+                        className="w-full h-full flex items-center justify-center text-xs text-center leading-[1.2] px-1 box-border line-clamp-3"
+                        style={{
+                          color: textColor,
+                          WebkitBoxPack: "center", // 垂直居中
+                        }}
+                      >
+                        {text}
+                      </div>
+                    </foreignObject>
                   </Tooltip>
                 </g>
               );

+ 10 - 10
frontend/react-template/src/components/FlowChart/components/ArrowMarkers.tsx

@@ -8,8 +8,8 @@ export const ArrowMarkers: FC = () => (
       viewBox="0 0 10 10"
       refX="10"
       refY="5"
-      markerWidth="7"
-      markerHeight="7"
+      markerWidth="3"
+      markerHeight="3"
       orient="auto"
     >
       <path
@@ -22,8 +22,8 @@ export const ArrowMarkers: FC = () => (
       viewBox="0 0 10 10"
       refX="10"
       refY="5"
-      markerWidth="7"
-      markerHeight="7"
+      markerWidth="3"
+      markerHeight="3"
       orient="auto"
     >
       <path
@@ -36,8 +36,8 @@ export const ArrowMarkers: FC = () => (
       viewBox="0 0 10 10"
       refX="10"
       refY="5"
-      markerWidth="7"
-      markerHeight="7"
+      markerWidth="3"
+      markerHeight="3"
       orient="auto"
     >
       <path
@@ -50,8 +50,8 @@ export const ArrowMarkers: FC = () => (
       viewBox="0 0 10 10"
       refX="10"
       refY="5"
-      markerWidth="7"
-      markerHeight="7"
+      markerWidth="3"
+      markerHeight="3"
       orient="auto"
     >
       <path
@@ -64,8 +64,8 @@ export const ArrowMarkers: FC = () => (
       viewBox="0 0 10 10"
       refX="10"
       refY="5"
-      markerWidth="7"
-      markerHeight="7"
+      markerWidth="3"
+      markerHeight="3"
       orient="auto"
     >
       <path

+ 2 - 2
frontend/react-template/src/components/TopBar/TopBar.tsx

@@ -55,7 +55,7 @@ export const TopBar: FC<TopBarProps> = ({ onTraceSelect }) => {
         <h1>{title}</h1>
       </div>
       <div className={styles.filters}>
-        <select
+        {/* <select
           value={statusFilter}
           onChange={(e: ChangeEvent<HTMLSelectElement>) => handleStatusChange(e.target.value)}
           className={styles.select}
@@ -65,7 +65,7 @@ export const TopBar: FC<TopBarProps> = ({ onTraceSelect }) => {
           <option value="running">运行中</option>
           <option value="completed">已完成</option>
           <option value="failed">失败</option>
-        </select>
+        </select> */}
         {/* <button
           onClick={() => loadTraces(statusFilter)}
           className={styles.button}