dashboard / frontend /src /selected_tools /SelectedToolsApp.tsx
timchen0618's picture
Show selected step indices in header
2b7fb4d verified
import { useEffect, useState, useMemo } from "react";
// ── Types ────────────────────────────────────────────────────────────────
interface Row {
query_id: string;
rationale: string;
selected_indices: number[];
k_requested: number;
k_effective: number;
excerpt: string;
new_trajectory: string;
direct_answer: boolean;
tool_call_counts: Record<string, number>;
total_tool_calls: number;
status: string;
}
// ── Block parsers ────────────────────────────────────────────────────────
type BlockType = "reasoning" | "tool_call" | "tool_result" | "final_answer" | "unknown";
interface Block { type: BlockType; label?: string; content: string }
const BLOCK_STYLES: Record<BlockType, { border: string; labelColor: string; bg: string }> = {
reasoning: { border: "border-purple-700", labelColor: "text-purple-400", bg: "bg-purple-950/30" },
tool_call: { border: "border-blue-700", labelColor: "text-blue-400", bg: "bg-blue-950/30" },
tool_result: { border: "border-gray-600", labelColor: "text-gray-400", bg: "bg-gray-800/30" },
final_answer: { border: "border-green-700", labelColor: "text-green-400", bg: "bg-green-950/30" },
unknown: { border: "border-gray-700", labelColor: "text-gray-500", bg: "" },
};
/**
* Parse the `excerpt` column (reference trajectory format):
* [Reasoning]: text
* [Tool call] tool_name\narguments:\n{...}
* [Tool result]:\n[...]
*/
function parseExcerpt(text: string): Block[] {
if (!text) return [];
const blocks: Block[] = [];
const parts = text.split(/\n\n(?=\[)/);
for (const part of parts) {
const p = part.trim();
if (p.startsWith("[Reasoning]:")) {
blocks.push({ type: "reasoning", label: "Reasoning", content: p.slice("[Reasoning]:".length).trim() });
} else if (p.startsWith("[Tool call]")) {
blocks.push({ type: "tool_call", label: "Tool Call", content: p.slice("[Tool call]".length).trim() });
} else if (p.startsWith("[Tool result]:")) {
blocks.push({ type: "tool_result", label: "Tool Result", content: p.slice("[Tool result]:".length).trim() });
} else if (p.startsWith("[Final answer]:")) {
blocks.push({ type: "final_answer", label: "Final Answer", content: p.slice("[Final answer]:".length).trim() });
} else if (p) {
blocks.push({ type: "unknown", label: "β€”", content: p });
}
}
return blocks;
}
/**
* Parse the `new_trajectory` column (our formatted output):
* [Reasoning]\ntext
* [Tool Call: name]\nArguments:\n{...}\n\n[Tool Result]\n{...}
* [Final Answer]\ntext
* Blocks separated by \n\n---\n\n
*/
function parseNewTrajectory(text: string): Block[] {
if (!text) return [];
const blocks: Block[] = [];
const parts = text.split("\n\n---\n\n");
for (const part of parts) {
const p = part.trim();
if (p.startsWith("[Reasoning]\n")) {
blocks.push({ type: "reasoning", label: "Reasoning", content: p.slice("[Reasoning]\n".length).trim() });
} else if (p.startsWith("[Tool Call:")) {
// Split into call and result at the embedded [Tool Result] marker
const resultMarker = "\n\n[Tool Result]\n";
const resultIdx = p.indexOf(resultMarker);
const headerEnd = p.indexOf("]\n");
const toolName = headerEnd >= 0 ? p.slice("[Tool Call:".length, headerEnd).trim() : "unknown";
if (resultIdx >= 0) {
const callContent = p.slice(0, resultIdx).replace(/^\[Tool Call:[^\]]*\]\n/, "").trim();
const resultContent = p.slice(resultIdx + resultMarker.length).trim();
blocks.push({ type: "tool_call", label: `Tool Call: ${toolName}`, content: callContent });
blocks.push({ type: "tool_result", label: "Tool Result", content: resultContent });
} else {
const callContent = p.replace(/^\[Tool Call:[^\]]*\]\n/, "").trim();
blocks.push({ type: "tool_call", label: `Tool Call: ${toolName}`, content: callContent });
}
} else if (p.startsWith("[Final Answer]\n")) {
blocks.push({ type: "final_answer", label: "Final Answer", content: p.slice("[Final Answer]\n".length).trim() });
} else if (p) {
blocks.push({ type: "unknown", label: "β€”", content: p });
}
}
return blocks;
}
// ── Trajectory renderer ──────────────────────────────────────────────────
function TrajectoryView({ blocks }: { blocks: Block[] }) {
if (blocks.length === 0) return <div className="text-gray-500 text-xs italic">No steps.</div>;
return (
<div className="space-y-2">
{blocks.map((b, i) => {
const s = BLOCK_STYLES[b.type];
return (
<div key={i} className={`border-l-2 ${s.border} ${s.bg} pl-3 py-1.5 rounded-r`}>
<div className={`text-[10px] font-bold uppercase tracking-widest mb-1 ${s.labelColor}`}>
{b.label ?? b.type}
</div>
<pre className="text-xs text-gray-300 whitespace-pre-wrap font-mono leading-relaxed">{b.content}</pre>
</div>
);
})}
</div>
);
}
// ── Filter type ──────────────────────────────────────────────────────────
type FilterMode = "all" | "direct" | "searched";
// ── Main component ───────────────────────────────────────────────────────
export default function SelectedToolsApp() {
const [data, setData] = useState<Row[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedIdx, setSelectedIdx] = useState(0);
const [search, setSearch] = useState("");
const [filter, setFilter] = useState<FilterMode>("all");
useEffect(() => {
setLoading(true);
fetch("/api/selected-tools/")
.then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); })
.then((d: { rows: Row[] }) => { setData(d.rows); setLoading(false); })
.catch(e => { setError(e.message); setLoading(false); });
}, []);
const filtered = useMemo(() => {
let rows = data;
if (filter === "direct") rows = rows.filter(r => r.direct_answer);
if (filter === "searched") rows = rows.filter(r => !r.direct_answer);
if (search.trim()) {
const q = search.toLowerCase();
rows = rows.filter(r => r.query_id.includes(q));
}
return rows;
}, [data, search, filter]);
const current = filtered[selectedIdx] ?? null;
const excerptBlocks = useMemo(() => current ? parseExcerpt(current.excerpt) : [], [current]);
const trajectoryBlocks = useMemo(() => current ? parseNewTrajectory(current.new_trajectory) : [], [current]);
// Stats
const directCount = data.filter(r => r.direct_answer).length;
const directPct = data.length ? Math.round(100 * directCount / data.length) : 0;
if (loading) return <div className="h-full flex items-center justify-center text-gray-400">Loading from HuggingFace…</div>;
if (error) return <div className="h-full flex items-center justify-center text-red-400">Error: {error}</div>;
return (
<div className="h-full flex overflow-hidden bg-gray-950 text-gray-100">
{/* ── Sidebar ──────────────────────────────────────────────── */}
<div className="w-64 shrink-0 flex flex-col border-r border-gray-800 bg-gray-900">
{/* Stats banner */}
<div className="px-3 py-2 border-b border-gray-800 bg-gray-900/80">
<div className="text-[10px] text-gray-500 uppercase tracking-widest mb-1">Direct-answer rate</div>
<div className="text-lg font-bold text-emerald-400">{directPct}%</div>
<div className="text-[10px] text-gray-600">{directCount} / {data.length} no tool calls</div>
</div>
{/* Filter toggles */}
<div className="flex gap-1 px-2 py-2 border-b border-gray-800">
{(["all", "direct", "searched"] as FilterMode[]).map(m => (
<button
key={m}
onClick={() => { setFilter(m); setSelectedIdx(0); }}
className={`flex-1 text-[10px] py-1 rounded border transition-colors capitalize ${
filter === m
? m === "direct" ? "bg-emerald-900/60 border-emerald-600 text-emerald-300"
: m === "searched" ? "bg-blue-900/60 border-blue-600 text-blue-300"
: "bg-gray-700 border-gray-500 text-gray-200"
: "bg-gray-800/50 border-gray-700 text-gray-500 hover:border-gray-500"
}`}
>{m}</button>
))}
</div>
{/* Search */}
<div className="px-2 py-1.5 border-b border-gray-800">
<input
type="text"
placeholder="Search query ID…"
value={search}
onChange={e => { setSearch(e.target.value); setSelectedIdx(0); }}
className="w-full bg-gray-800 border border-gray-700 text-gray-200 text-xs rounded px-2 py-1.5 placeholder-gray-600"
/>
<div className="text-[10px] text-gray-600 mt-1">{filtered.length} / {data.length}</div>
</div>
{/* Query list */}
<div className="flex-1 overflow-y-auto">
{filtered.map((row, i) => (
<button
key={row.query_id}
onClick={() => setSelectedIdx(i)}
className={`w-full text-left px-3 py-2 border-b border-gray-800/50 text-xs transition-colors ${
selectedIdx === i
? "bg-blue-900/40 text-blue-200 border-l-2 border-l-blue-500"
: "text-gray-400 hover:bg-gray-800"
}`}
>
<div className="flex items-center justify-between">
<span className="font-medium text-gray-200">#{row.query_id}</span>
{row.direct_answer
? <span className="text-[9px] px-1.5 py-0.5 rounded-full bg-emerald-900/60 text-emerald-400 border border-emerald-800">direct</span>
: <span className="text-[9px] px-1.5 py-0.5 rounded-full bg-blue-900/60 text-blue-400 border border-blue-800">{row.total_tool_calls} calls</span>
}
</div>
<div className="text-[10px] text-gray-600 mt-0.5">k={row.k_effective} selected steps</div>
</button>
))}
</div>
</div>
{/* ── Main: two-column side-by-side ──────────────────────── */}
{current ? (
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
{/* Header */}
<div className="px-4 py-2 bg-gray-900/60 border-b border-gray-800 shrink-0">
<div className="flex items-center gap-3 flex-wrap">
<span className="text-sm font-medium text-gray-100">Query #{current.query_id}</span>
{current.direct_answer
? <span className="text-xs px-2 py-0.5 rounded-full bg-emerald-900/50 text-emerald-300 border border-emerald-800">Direct answer</span>
: <span className="text-xs px-2 py-0.5 rounded-full bg-blue-900/50 text-blue-300 border border-blue-800">{current.total_tool_calls} tool calls</span>
}
<span className="text-xs text-gray-500">k={current.k_effective} steps selected</span>
<span className={`text-xs px-2 py-0.5 rounded-full ${current.status === "completed" ? "bg-gray-800 text-gray-400" : "bg-amber-900/50 text-amber-300"}`}>
{current.status}
</span>
</div>
{/* Selected indices */}
<div className="mt-1.5 flex items-center gap-1.5 flex-wrap">
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-500">Selected steps</span>
{current.selected_indices.map(idx => (
<span key={idx} className="text-[10px] px-1.5 py-0.5 rounded bg-gray-800 border border-gray-700 text-gray-300 font-mono">
#{idx}
</span>
))}
</div>
{/* Rationale */}
<div className="mt-1.5 text-xs text-gray-400 leading-snug bg-gray-800/50 rounded px-2 py-1.5 border border-gray-700">
<span className="text-[10px] font-bold uppercase tracking-widest text-amber-500 mr-2">Rationale</span>
{current.rationale}
</div>
</div>
{/* Side-by-side columns */}
<div className="flex-1 flex overflow-hidden min-w-0">
{/* Left: excerpt (selected tool calls from reference trajectory) */}
<div className="flex-1 flex flex-col min-w-0 border-r border-gray-800 overflow-hidden">
<div className="px-3 py-1.5 bg-gray-900/40 border-b border-gray-800 shrink-0">
<span className="text-[11px] font-semibold text-amber-400 uppercase tracking-widest">Selected Tool Calls</span>
<span className="text-[10px] text-gray-600 ml-2">reference trajectory Β· {current.k_effective} steps</span>
</div>
<div className="flex-1 overflow-y-auto p-3">
<TrajectoryView blocks={excerptBlocks} />
</div>
</div>
{/* Right: new trajectory */}
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
<div className="px-3 py-1.5 bg-gray-900/40 border-b border-gray-800 shrink-0">
<span className="text-[11px] font-semibold text-sky-400 uppercase tracking-widest">New Trajectory</span>
<span className="text-[10px] text-gray-600 ml-2">gpt-oss-120b Β· conditioned on selected steps</span>
</div>
<div className="flex-1 overflow-y-auto p-3">
<TrajectoryView blocks={trajectoryBlocks} />
</div>
</div>
</div>
</div>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500">No query selected.</div>
)}
</div>
);
}