diff --git a/ui/package.json b/ui/package.json index c8374200..7a798f7d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -52,7 +52,6 @@ "mermaid": "^11.12.0", "radix-ui": "^1.4.3", "react": "^19.0.0", - "react-diff-viewer-continued": "^4.2.0", "react-dom": "^19.0.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.1.5", diff --git a/ui/src/components/DocumentDiffModal.tsx b/ui/src/components/DocumentDiffModal.tsx index 875237ac..aaee6514 100644 --- a/ui/src/components/DocumentDiffModal.tsx +++ b/ui/src/components/DocumentDiffModal.tsx @@ -1,7 +1,6 @@ import { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { DocumentRevision } from "@paperclipai/shared"; -import ReactDiffViewer, { DiffMethod } from "react-diff-viewer-continued"; import { issuesApi } from "../api/issues"; import { queryKeys } from "../lib/queryKeys"; import { relativeTime } from "../lib/utils"; @@ -28,6 +27,96 @@ function getRevisionLabel(revision: DocumentRevision) { 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, @@ -69,6 +158,19 @@ export function DocumentDiffModal({ 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 ( @@ -128,50 +230,33 @@ export function DocumentDiffModal({ ) : 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 : " "}
+                  
+
+ ))} +
)}