import { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { DocumentRevision } from "@penclipai/shared"; import { issuesApi } from "../api/issues"; import { queryKeys } from "../lib/queryKeys"; import { relativeTime } from "../lib/utils"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; function getRevisionLabel(revision: DocumentRevision) { const actor = revision.createdByUserId ? "board" : revision.createdByAgentId ? "agent" : "system"; return `rev ${revision.revisionNumber} — ${relativeTime(revision.createdAt)} • ${actor}`; } type DiffRow = { kind: "context" | "removed" | "added"; oldLineNumber: number | null; newLineNumber: number | null; text: string; }; function buildLineDiff(oldText: string, newText: string): DiffRow[] { const oldLines = oldText.split("\n"); const newLines = newText.split("\n"); const oldCount = oldLines.length; const newCount = newLines.length; const dp = Array.from({ length: oldCount + 1 }, () => Array(newCount + 1).fill(0)); for (let i = oldCount - 1; i >= 0; i -= 1) { for (let j = newCount - 1; j >= 0; j -= 1) { dp[i][j] = oldLines[i] === newLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]); } } const rows: DiffRow[] = []; let i = 0; let j = 0; let oldLineNumber = 1; let newLineNumber = 1; while (i < oldCount && j < newCount) { if (oldLines[i] === newLines[j]) { rows.push({ kind: "context", oldLineNumber, newLineNumber, text: oldLines[i], }); i += 1; j += 1; oldLineNumber += 1; newLineNumber += 1; continue; } if (dp[i + 1][j] >= dp[i][j + 1]) { rows.push({ kind: "removed", oldLineNumber, newLineNumber: null, text: oldLines[i], }); i += 1; oldLineNumber += 1; continue; } rows.push({ kind: "added", oldLineNumber: null, newLineNumber, text: newLines[j], }); j += 1; newLineNumber += 1; } while (i < oldCount) { rows.push({ kind: "removed", oldLineNumber, newLineNumber: null, text: oldLines[i], }); i += 1; oldLineNumber += 1; } while (j < newCount) { rows.push({ kind: "added", oldLineNumber: null, newLineNumber, text: newLines[j], }); j += 1; newLineNumber += 1; } return rows; } export function DocumentDiffModal({ issueId, documentKey, latestRevisionNumber, open, onOpenChange, }: { issueId: string; documentKey: string; latestRevisionNumber: number; open: boolean; onOpenChange: (open: boolean) => void; }) { const { data: revisions } = useQuery({ queryKey: queryKeys.issues.documentRevisions(issueId, documentKey), queryFn: () => issuesApi.listDocumentRevisions(issueId, documentKey), enabled: open, }); const sortedRevisions = useMemo(() => { if (!revisions) return []; return [...revisions].sort((a, b) => b.revisionNumber - a.revisionNumber); }, [revisions]); // Default: compare previous (latestRevisionNumber - 1) with current (latestRevisionNumber) const [leftRevisionId, setLeftRevisionId] = useState(null); const [rightRevisionId, setRightRevisionId] = useState(null); const effectiveLeftId = leftRevisionId ?? sortedRevisions.find( (r) => r.revisionNumber === latestRevisionNumber - 1, )?.id ?? null; const effectiveRightId = rightRevisionId ?? sortedRevisions.find( (r) => r.revisionNumber === latestRevisionNumber, )?.id ?? null; const leftRevision = sortedRevisions.find((r) => r.id === effectiveLeftId) ?? null; const rightRevision = sortedRevisions.find((r) => r.id === effectiveRightId) ?? null; const leftBody = leftRevision?.body ?? ""; const rightBody = rightRevision?.body ?? ""; const diffRows = useMemo(() => buildLineDiff(leftBody, rightBody), [leftBody, rightBody]); const lineClassesByKind: Record = { context: "bg-transparent", removed: "bg-red-500/10 text-red-100", added: "bg-green-500/10 text-green-100", }; const markerByKind: Record = { context: " ", removed: "-", added: "+", }; return (
Diff — {documentKey}
Old
New
{!revisions ? (
Loading revisions...
) : !leftRevision || !rightRevision ? (
Select two revisions to compare.
) : leftRevision.id === rightRevision.id ? (
Both sides are the same revision.
) : (
Old New Content
{diffRows.map((row, index) => (
{row.oldLineNumber ?? ""} {row.newLineNumber ?? ""} {markerByKind[row.kind]}
                    {row.text.length > 0 ? row.text : " "}
                  
))}
)}
); }