her / ui /src /components /Legend.jsx
geekwrestler's picture
Squash history (purge pre-scrub demo session blobs)
5f43c7d
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,
};