Spaces:
Running on Zero
Running on Zero
| import React from "react"; | |
| import { Eye, EyeOff } from "lucide-react"; | |
| import { C, FD, FM, SEV, turnSeverity, toolBucket } from "../theme.js"; | |
| // MODE A left legend: typed entities with counts + severity colour dot + a | |
| // show/hide eye per type, then tool-type tallies and the Direct/Indirect | |
| // causality tally. (UI-SPEC: "Left legend panel: typed entities with counts, | |
| // a severity colour dot, and a show/hide eye per type.") | |
| function LegendRow({ color, label, count, visible, onToggle, dot = "circle" }) { | |
| return ( | |
| <div className="row" onClick={onToggle} style={{ display: "flex", alignItems: "center", gap: 9, padding: "6px 9px", borderRadius: 7, cursor: "pointer", opacity: visible ? 1 : 0.42, userSelect: "none" }}> | |
| <span style={{ width: 9, height: 9, borderRadius: dot === "square" ? 2 : 999, background: color, boxShadow: visible ? `0 0 6px ${color}` : "none", flexShrink: 0 }} /> | |
| <span style={{ fontSize: 12, color: C.text2, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{label}</span> | |
| <span style={{ fontFamily: FM, fontSize: 11, color: C.muted, minWidth: 22, textAlign: "right" }}>{count}</span> | |
| {visible ? <Eye size={13} color={C.muted} /> : <EyeOff size={13} color={C.muted} />} | |
| </div> | |
| ); | |
| } | |
| function GroupLabel({ text }) { | |
| return ( | |
| <div style={{ padding: "12px 9px 4px" }}> | |
| <span style={{ fontFamily: FM, fontSize: 9.5, letterSpacing: 0.9, color: C.muted, textTransform: "uppercase" }}>{text}</span> | |
| </div> | |
| ); | |
| } | |
| export default function Legend({ turns, vis, setVis }) { | |
| // Severity tallies across turns. | |
| const sevCounts = { heavy: 0, tip: 0, clean: 0 }; | |
| turns.forEach((t) => { | |
| sevCounts[turnSeverity(t)]++; | |
| }); | |
| // Tool-type tallies + causality tally across the whole session. | |
| const tool = { Read: 0, Bash: 0, Edit: 0, MCP: 0, Task: 0, Other: 0 }; | |
| let direct = 0, | |
| indirect = 0; | |
| turns.forEach((t) => | |
| (t.tools || []).forEach((tl) => { | |
| const b = toolBucket(tl.name); | |
| tool[b] = (tool[b] || 0) + 1; | |
| if (tl.provenance === "indirect") indirect++; | |
| else direct++; | |
| }) | |
| ); | |
| const toggle = (k) => setVis((v) => ({ ...v, [k]: !v[k] })); | |
| return ( | |
| <div style={{ overflowY: "auto", flex: 1, padding: "4px 6px 14px" }}> | |
| <GroupLabel text="Turn type" /> | |
| <LegendRow color={SEV.heavy.color} label={SEV.heavy.label} count={sevCounts.heavy} visible={vis.heavy} onToggle={() => toggle("heavy")} /> | |
| <LegendRow color={SEV.tip.color} label={SEV.tip.label} count={sevCounts.tip} visible={vis.tip} onToggle={() => toggle("tip")} /> | |
| <LegendRow color={SEV.clean.color} label={SEV.clean.label} count={sevCounts.clean} visible={vis.clean} onToggle={() => toggle("clean")} /> | |
| <GroupLabel text="Tool types" /> | |
| <LegendRow color={C.blue} label="Read" count={tool.Read} visible={vis.tRead} onToggle={() => toggle("tRead")} dot="square" /> | |
| <LegendRow color={C.text2} label="Bash" count={tool.Bash} visible={vis.tBash} onToggle={() => toggle("tBash")} dot="square" /> | |
| <LegendRow color={C.amber} label="Edit" count={tool.Edit} visible={vis.tEdit} onToggle={() => toggle("tEdit")} dot="square" /> | |
| <LegendRow color={C.cyan} label="MCP" count={tool.MCP} visible={vis.tMCP} onToggle={() => toggle("tMCP")} dot="square" /> | |
| <LegendRow color={C.muted} label="Task" count={tool.Task} visible={vis.tTask} onToggle={() => toggle("tTask")} dot="square" /> | |
| <GroupLabel text="Causality" /> | |
| <div style={{ padding: "2px 9px" }}> | |
| <div style={{ display: "flex", height: 22, borderRadius: 6, overflow: "hidden", border: `1px solid ${C.borderSoft}` }}> | |
| <div style={{ width: `${(100 * direct) / Math.max(1, direct + indirect)}%`, background: C.border, display: "flex", alignItems: "center", justifyContent: "center" }}> | |
| <span style={{ fontFamily: FM, fontSize: 9.5, color: C.text }}>{direct}</span> | |
| </div> | |
| <div style={{ width: `${(100 * indirect) / Math.max(1, direct + indirect)}%`, background: `linear-gradient(90deg,${C.orange},${C.orangeHi})`, display: "flex", alignItems: "center", justifyContent: "center" }}> | |
| <span style={{ fontFamily: FM, fontSize: 9.5, color: "#fff" }}>{indirect}</span> | |
| </div> | |
| </div> | |
| <div style={{ display: "flex", justifyContent: "space-between", marginTop: 5 }}> | |
| <span style={{ fontFamily: FM, fontSize: 9.5, color: C.text2 }}>Direct · you</span> | |
| <span style={{ fontFamily: FM, fontSize: 9.5, color: C.orange }}>Indirect · agent</span> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Default visibility map for the legend toggles. | |
| export const defaultVis = { | |
| heavy: true, tip: true, clean: true, | |
| tRead: true, tBash: true, tEdit: true, tMCP: true, tTask: true, | |
| }; | |