({ message, userQuery, onOpenRef, onOpenNode, streaming }: Props)
| 78 | } |
| 79 | |
| 80 | function FlowGraphInner({ message, userQuery, onOpenRef, onOpenNode, streaming }: Props) { |
| 81 | const t = useT(); |
| 82 | const { locale } = useLocale(); |
| 83 | // 结构签名作为 memo 依赖(而非 message 本身)——流式逐字增量不重建图,保持稳定。 |
| 84 | const signature = graphSignature(message, userQuery); |
| 85 | const { nodes, edges } = useMemo(() => { |
| 86 | const built = buildFlowGraph(message, userQuery, locale); |
| 87 | const positioned = layoutGraph(built.nodes, built.edges, "LR"); |
| 88 | const toolNodes = positioned.filter( |
| 89 | (n) => n.data.kind === "file" || n.data.kind === "search" || n.data.kind === "list", |
| 90 | ); |
| 91 | const pending = toolNodes.find((n) => n.data.status === "pending"); |
| 92 | const latest = pending?.data || toolNodes[toolNodes.length - 1]?.data || null; |
| 93 | const enriched: GraphNode[] = positioned.map((n) => ({ |
| 94 | ...n, |
| 95 | data: { |
| 96 | ...n.data, |
| 97 | onOpenRef, |
| 98 | isLatestActive: streaming && latest && n.data.key === latest.key, |
| 99 | }, |
| 100 | })); |
| 101 | return { nodes: enriched, edges: built.edges }; |
| 102 | // 依赖用 signature(结构指纹)+ streaming/locale/onOpenRef;message/userQuery 已被 signature 概括。 |
| 103 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 104 | }, [signature, streaming, onOpenRef, locale]); |
| 105 | |
| 106 | const rf = useReactFlow(); |
| 107 | |
| 108 | // 自动聚焦(fitView):每当出现新卡片(nodes.length 变化)就平滑取景到全景, |
| 109 | // 让新卡片入画、整张图整体可见。这不会像以前那样「一直晃」——图已由结构签名 memo 把关, |
| 110 | // nodes.length 只在「真有新卡片 / 结构变化」时才变(逐字流式增量不触发),所以是 |
| 111 | // 「每张新卡片聚焦一次」而非每帧重排。streaming 变化(如刚结束补齐答案卡)也触发一次, |
| 112 | // 保证最终把全景框好。 |
| 113 | useEffect(() => { |
| 114 | if (nodes.length === 0) return; |
| 115 | const id = setTimeout(() => { |
| 116 | rf.fitView({ padding: 0.2, duration: 400 }); |
| 117 | }, 80); |
| 118 | return () => clearTimeout(id); |
| 119 | }, [nodes.length, streaming, rf]); |
| 120 | |
| 121 | // styledEdges 用 memo(依赖 edges)——edges 只在结构变化时变,流式逐字增量不会产生新的 |
| 122 | // 连线数组引用,react-flow 不会每帧重渲染连线 → 蚂蚁线 CSS 动画的 DOM 持续存在、不被打断, |
| 123 | // 流动既不闪烁也不卡顿。注意 useMemo 必须在下面的提前 return 之前(hooks 规则)。 |
| 124 | const styledEdges: Edge[] = useMemo( |
| 125 | () => |
| 126 | edges.map((e) => { |
| 127 | const color = edgeColor(e.data.stepType, e.data.isGhost, e.data.isSynthetic); |
| 128 | return { |
| 129 | ...e, |
| 130 | style: { |
| 131 | stroke: color, |
| 132 | strokeWidth: e.data.isGhost |
| 133 | ? 1.5 |
| 134 | : e.data.isSynthetic |
| 135 | ? 2.5 |
| 136 | : e.data.isFanIn |
| 137 | ? 2.5 |
nothing calls this directly
no test coverage detected