"use client"; import { useMemo, useState } from "react"; import type { DiffSummary } from "@/lib/api"; type Tab = "unified" | "split" | "semantic" | "raw"; interface Line { kind: "add" | "del" | "ctx" | "hunk" | "meta"; text: string; } function parseUnified(patch: string): Line[] { return patch.split("\n").map((l): Line => { if (l.startsWith("+++") || l.startsWith("---") || l.startsWith("Index:") || l.startsWith("====")) return { kind: "meta", text: l }; if (l.startsWith("@@")) return { kind: "hunk", text: l }; if (l.startsWith("+")) return { kind: "add", text: l.slice(1) }; if (l.startsWith("-")) return { kind: "del", text: l.slice(1) }; return { kind: "ctx", text: l.startsWith(" ") ? l.slice(1) : l }; }); } /** Pair deletions and additions inside each hunk into left/right rows. */ function splitRows(lines: Line[]): { left: Line | null; right: Line | null }[] { const rows: { left: Line | null; right: Line | null }[] = []; let i = 0; while (i < lines.length) { const l = lines[i]!; if (l.kind === "ctx" || l.kind === "hunk" || l.kind === "meta") { rows.push({ left: l, right: l }); i++; continue; } const dels: Line[] = []; const adds: Line[] = []; while (i < lines.length && lines[i]!.kind === "del") dels.push(lines[i++]!); while (i < lines.length && lines[i]!.kind === "add") adds.push(lines[i++]!); const n = Math.max(dels.length, adds.length); for (let k = 0; k < n; k++) rows.push({ left: dels[k] ?? null, right: adds[k] ?? null }); } return rows; } const cls = (k: Line["kind"] | undefined): string => (k === "add" ? "diff-line-add" : k === "del" ? "diff-line-del" : k === "hunk" ? "diff-line-hunk" : k === "meta" ? "diff-line-meta" : ""); export function DiffViewer({ unified, summary, defaultTab = "semantic" }: { unified: string | null; summary: DiffSummary | null | undefined; defaultTab?: Tab }) { const [tab, setTab] = useState(unified ? defaultTab : "semantic"); const lines = useMemo(() => (unified ? parseUnified(unified) : []), [unified]); const rows = useMemo(() => splitRows(lines), [lines]); const tabs: [Tab, string][] = [ ["unified", "Unified"], ["split", "Side-by-side"], ["semantic", "Semantic"], ["raw", "Raw"], ]; return (
{tabs.map(([k, label]) => ( ))} {summary?.stats && ( +{summary.stats.added} −{summary.stats.removed} ~{summary.stats.modified} )} {summary?.counts && ( +{summary.counts.added} −{summary.counts.removed} ~{summary.counts.modified} )}
{tab === "unified" && (lines.length ? lines.map((l, i) => (
{l.kind === "add" ? "+" : l.kind === "del" ? "−" : " "} {l.text}
)) : )} {tab === "split" && (rows.length ? (
{rows.map((r, i) =>
{r.left?.kind === "add" ? "" : r.left?.text ?? ""}
)}
{rows.map((r, i) =>
{r.right?.kind === "del" ? "" : r.right?.text ?? ""}
)}
) : )} {tab === "semantic" && } {tab === "raw" && (unified ?
{unified}
: )}
); } function NoPatch() { return
No textual patch stored for this change.
; } function Semantic({ summary }: { summary: DiffSummary | null | undefined }) { if (!summary) return
No structured summary.
; const str = (v: unknown): string => (typeof v === "string" ? v : v && typeof v === "object" ? String((v as { title?: unknown; url?: unknown; key?: unknown }).title ?? (v as { url?: unknown }).url ?? (v as { key?: unknown }).key ?? JSON.stringify(v)) : JSON.stringify(v)); if (summary.kind === "json") { return (
{(summary.changes ?? []).map((c, i) => (
{c.op} {c.path} {c.op !== "add" && {JSON.stringify(c.before)}} {c.op !== "remove" && {JSON.stringify(c.after)}}
))} {!summary.changes?.length && }
); } const mods = (summary.modified ?? []) as ({ before: unknown; after: unknown; fields?: string[]; key?: string })[]; return (
{(summary.added ?? []).length > 0 && (
{(summary.added ?? []).map((a, i) => ( {str(a)} {typeof a === "object" && a && (a as { summary?: string }).summary &&
{String((a as { summary?: string }).summary).slice(0, 400)}
} {typeof a === "object" && a && (a as { url?: string }).url && (a as { title?: string }).title &&
{String((a as { url?: string }).url)}
}
))}
)} {(summary.removed ?? []).length > 0 && (
{(summary.removed ?? []).map((a, i) => {str(a)})}
)} {mods.length > 0 && (
{mods.map((m, i) => (
{m.fields &&
{str(m.after)} · {m.fields.join(", ")}
}
{m.fields ? m.fields.map((f) => `${f}: ${JSON.stringify((m.before as Record)[f])}`).join(" · ") : str(m.before)}
{m.fields ? m.fields.map((f) => `${f}: ${JSON.stringify((m.after as Record)[f])}`).join(" · ") : str(m.after)}
))}
)} {summary.truncated &&
Summary truncated — see the Unified tab for the full patch.
} {!(summary.added ?? []).length && !(summary.removed ?? []).length && !mods.length && }
); } function Section({ title, tone, children }: { title: string; tone: "ok" | "danger" | "info"; children: React.ReactNode }) { return (
{title}
{children}
); } function Item({ tone, children }: { tone: "ok" | "danger"; children: React.ReactNode }) { return
{children}
; }