import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import { MessageSquare, CornerDownRight, Lightbulb } from "lucide-react";
import { C, FD, FM, toolIcon, intentOf, intentIcon, toolTypeVisible } from "../theme.js";
import { EdgeLegend } from "./Primitives.jsx";
import { buildDataflow } from "../dataflow.js";
// MODE B centre: per-turn DATAFLOW graph. Root = the user query. Children = the
// tool calls along the spine. INDIRECT edges cross-link a tool_result to the
// later tool that consumed its value — the highlighted causal path.
//
// PROVEN (value-flow) edges = SOLID orange. HYPOTHESIS (proximity) = DOTTED muted.
// Always visually separated (NON-NEGOTIABLE #4).
const ROW_H = 40; // vertical spacing per tool row
const GUTTER = 64; // left gutter reserved for the edge SVG
const NODE_X = 22; // x of node centre inside the gutter
export default function TurnGraph({ turn, selected, onSelect, vis }) {
const { nodes, edges } = buildDataflow(turn);
// Tool-type legend filter: rows of a hidden tool type are dimmed and their
// value-flow arcs suppressed, so the eyes work here too.
const hidden = new Set();
(turn.tools || []).forEach((tl, idx) => {
if (vis && !toolTypeVisible(vis, tl.name)) hidden.add("t" + idx);
});
const containerRef = useRef(null);
const rowRefs = useRef({});
const [ys, setYs] = useState({});
// Measure each node's vertical centre so the SVG edges can connect to the
// real DOM rows (robust to wrapping / variable heights).
useLayoutEffect(() => {
const cont = containerRef.current;
if (!cont) return;
const base = cont.getBoundingClientRect().top;
const next = {};
nodes.forEach((n) => {
const el = rowRefs.current[n.key];
if (el) {
const r = el.getBoundingClientRect();
next[n.key] = r.top - base + r.height / 2;
}
});
setYs(next);
// re-run when the turn or selection changes layout
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [turn.i, selected, nodes.length]);
// When a tool is selected (e.g. opened from a chat citation), scroll it into
// view — a 71-tool turn can hide the cited tool far down the spine.
useEffect(() => {
if (selected == null) return;
const el = rowRefs.current["t" + selected];
if (el && el.scrollIntoView) el.scrollIntoView({ block: "center", behavior: "smooth" });
}, [selected, turn.i]);
const II = intentIcon(intentOf(turn));
// Focus-driven causal path: a turn with 49 value-flow links must NOT render as
// a permanent hairball. The clean grey spine is always on; an orange value-flow
// arc is drawn ONLY for the tool the user hovers or selects (UI-SPEC: "the
// highlighted causal path").
const [hover, setHover] = useState(null);
const activeIdx = hover != null ? hover : selected;
const activeKey = activeIdx != null ? "t" + activeIdx : null;
const provenCount = edges.filter((e) => e.type === "proven").length;
const tracedHere = edges.filter((e) => e.type === "proven" && e.from && e.to).length;
// Build SVG path data for each edge using measured y-centres.
function edgePath(e) {
const y1 = ys[e.from];
const y2 = ys[e.to];
if (y1 == null || y2 == null) return null;
if (e.type === "spine") {
// vertical backbone hugging the gutter
return `M ${NODE_X} ${y1} L ${NODE_X} ${y2}`;
}
// cross-link: ONE smooth cubic bow to the left — a clean causal jump.
const bow = Math.min(40, 16 + Math.abs(y2 - y1) * 0.12);
const cx = NODE_X - bow;
return `M ${NODE_X} ${y1} C ${cx} ${y1}, ${cx} ${y2}, ${NODE_X} ${y2}`;
}
return (
{/* query header */}
QUERY {String(turn.i).padStart(2, "0")}
{turn.origin === "system" && (
SYSTEM
)}
{turn.heavy && (
HEAVY
)}
{turn.prompt}
{turn.reply && (
REPLY {turn.reply.slice(0, 240)}…
)}
{/* guidance card — surfaces inline on flagged turns ONLY */}
{turn.guide && (