import { useEffect, useState, useMemo } from "react";
// ── Types ─────────────────────────────────────────────────────────────────
type Template = "gpt-oss" | "qwen";
interface Message {
role: "user" | "assistant";
content: string;
}
interface Row {
query_id: string;
excerpt: string;
messages_gpt: Message[] | null;
messages_qwen: Message[] | null;
}
// ── Color palette ─────────────────────────────────────────────────────────
const COLORS = {
user: { bg: "bg-amber-950/40", border: "border-amber-600", label: "text-amber-400", tag: "USER MESSAGE" },
reasoning: { bg: "bg-purple-950/40", border: "border-purple-600", label: "text-purple-400", tag: "REASONING" },
tool_call: { bg: "bg-blue-950/40", border: "border-blue-600", label: "text-blue-400", tag: "TOOL CALL" },
tool_resp: { bg: "bg-gray-800/60", border: "border-gray-600", label: "text-gray-400", tag: "TOOL RESPONSE" },
};
// ── Block component ───────────────────────────────────────────────────────
function Block({ kind, label, children }: {
kind: keyof typeof COLORS;
label?: string;
children: React.ReactNode;
}) {
const c = COLORS[kind];
return (
{label ?? c.tag}
{children}
);
}
// ── Left panel: parse excerpt (newline-separated JSON objects) ────────────
interface ExcerptItem {
type: "reasoning" | "function_call" | "function_call_output" | string;
[key: string]: unknown;
}
function parseExcerptItems(excerpt: string): ExcerptItem[] {
return excerpt
.split(/\n\n/)
.map(s => s.trim())
.filter(Boolean)
.flatMap(chunk => {
try {
const obj = JSON.parse(chunk);
return typeof obj === "object" && obj !== null && "type" in obj ? [obj as ExcerptItem] : [];
} catch {
return [];
}
});
}
function ExcerptPanel({ excerpt, userContent }: { excerpt: string; userContent: string }) {
const items = useMemo(() => parseExcerptItems(excerpt), [excerpt]);
return (
{userContent}
{items.map((item, i) => {
const raw = JSON.stringify(item, null, 2);
if (item.type === "reasoning") {
return (
{raw}
);
}
if (item.type === "function_call") {
return (
{raw}
);
}
if (item.type === "function_call_output") {
return (
{raw}
);
}
return (
);
})}
);
}
// ── Right panel: split assistant content into typed segments ──────────────
type SegKind = "reasoning" | "tool_call";
/**
* For gpt-oss: split at ....
* Segments outside tags are reasoning; tags themselves are tool_call.
*/
function splitGptContent(content: string): { kind: SegKind; text: string }[] {
const segs: { kind: SegKind; text: string }[] = [];
const re = /([\s\S]*?<\/tool_call>)/g;
let last = 0;
let m: RegExpExecArray | null;
while ((m = re.exec(content)) !== null) {
const pre = content.slice(last, m.index).trim();
if (pre) segs.push({ kind: "reasoning", text: pre });
segs.push({ kind: "tool_call", text: m[1] });
last = m.index + m[1].length;
}
const tail = content.slice(last).trim();
if (tail) segs.push({ kind: "reasoning", text: tail });
return segs;
}
/**
* For qwen: reasoning is inside ..., tool calls in ....
* Both may appear in one assistant message; extract all in order.
*/
function splitQwenContent(content: string): { kind: SegKind; text: string }[] {
const segs: { kind: SegKind; text: string }[] = [];
const re = /([\s\S]*?<\/think>|[\s\S]*?<\/tool_call>)/g;
let last = 0;
let m: RegExpExecArray | null;
while ((m = re.exec(content)) !== null) {
const gap = content.slice(last, m.index).trim();
if (gap) segs.push({ kind: "reasoning", text: gap });
if (m[1].startsWith("")) {
segs.push({ kind: "reasoning", text: m[1] });
} else {
segs.push({ kind: "tool_call", text: m[1] });
}
last = m.index + m[1].length;
}
const tail = content.slice(last).trim();
if (tail) segs.push({ kind: "reasoning", text: tail });
return segs;
}
function AssistantBlock({ content, template }: { content: string; template: Template }) {
const segs = template === "qwen" ? splitQwenContent(content) : splitGptContent(content);
const hasToolCall = segs.some(s => s.kind === "tool_call");
const outerColor = COLORS.reasoning;
return (
{hasToolCall ? "REASONING + TOOL CALL" : "REASONING"}
{segs.map((seg, j) => {
if (seg.kind === "tool_call") {
const tc = COLORS.tool_call;
return (
);
}
return (
{seg.text}
);
})}
);
}
function MessagesPanel({ messages, template }: { messages: Message[]; template: Template }) {
return (
{messages.map((msg, i) => {
if (msg.role === "user" && i === 0) {
return (
{msg.content}
);
}
if (msg.role === "user") {
return (
{msg.content}
);
}
return
;
})}
);
}
// ── Legend ────────────────────────────────────────────────────────────────
function Legend() {
return (
Legend
{(Object.entries(COLORS) as [keyof typeof COLORS, typeof COLORS[keyof typeof COLORS]][]).map(([k, c]) => (
{c.tag}
))}
);
}
// ── Template dropdown ─────────────────────────────────────────────────────
function TemplateDropdown({ value, onChange }: { value: Template; onChange: (t: Template) => void }) {
return (
);
}
// ── Main component ────────────────────────────────────────────────────────
export default function SftDiffApp() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedIdx, setSelectedIdx] = useState(0);
const [search, setSearch] = useState("");
const [template, setTemplate] = useState("gpt-oss");
useEffect(() => {
setLoading(true);
fetch("/api/sft-diff/")
.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(() => {
if (!search.trim()) return data;
const q = search.toLowerCase();
return data.filter(r =>
r.query_id.toLowerCase().includes(q) ||
(r.messages_gpt?.[0]?.content ?? r.messages_qwen?.[0]?.content ?? "").toLowerCase().includes(q)
);
}, [data, search]);
const current = filtered[selectedIdx] ?? null;
const activeMessages: Message[] | null = current
? (template === "qwen" ? current.messages_qwen : current.messages_gpt)
: null;
// User content for the left panel — prefer gpt-oss, fall back to qwen (same content)
const userContent = current?.messages_gpt?.[0]?.content ?? current?.messages_qwen?.[0]?.content ?? "";
if (loading) return Loading…
;
if (error) return Error: {error}
;
return (
{/* ── Sidebar ──────────────────────────────────────────────── */}
{filtered.map((row, i) => {
const content = row.messages_gpt?.[0]?.content ?? row.messages_qwen?.[0]?.content ?? "";
const qMatch = content.match(/Question:\s*([\s\S]{0,120})/);
const preview = qMatch ? qMatch[1].trim().replace(/\n/g, " ") : content.slice(0, 80);
return (
);
})}
{/* ── Main area ────────────────────────────────────────────── */}
{current ? (
{/* Left: original excerpt */}
Original
excerpt field — raw JSON items
{/* Right: converted messages */}
Converted
{activeMessages && (
{activeMessages.length} messages
)}
{activeMessages ? (
) : (
Not available for {template} template.
)}
) : (
No record selected.
)}
);
}