import React, { useEffect, useState } from "react"; import { Card, CardContent } from "@/src/components/ui/card"; import { cn } from "@/src/utils/tailwind"; import { diffLines as calculateDiffLines, diffWords } from "diff"; type DiffSegmentPart = { value: string; type?: "unchanged" | "removed" | "added" | "empty"; }; type DiffSegment = { text: string; type: "unchanged" | "removed" | "added" | "empty"; parts?: DiffSegmentPart[]; }; type DiffViewerProps = { oldString: string; newString: string; oldLabel?: string; newLabel?: string; oldSubLabel?: string; newSubLabel?: string; className?: string; }; const DIFF_COLORS = { added: { text: "bg-green-500/30", line: "bg-green-500/10", }, removed: { text: "bg-destructive/60", line: "bg-destructive/10", }, unchanged: { text: "bg-muted", line: "bg-muted", }, empty: { text: "bg-muted", line: "bg-muted", }, } as const; /** * Calculates the diff between two segments, word by word. * @param oldString - The old string to compare * @param newString - The new string to compare * @returns The diff between the two strings */ const calculateSegmentDiff = (oldString: string, newString: string) => { const segmentChanges = diffWords(oldString, newString, {}); const leftWords: DiffSegmentPart[] = []; const rightWords: DiffSegmentPart[] = []; for (let charIndex = 0; charIndex < segmentChanges.length; charIndex++) { const change = segmentChanges[charIndex]; if (!change.added && !change.removed) { // not added or removed, so it's unchanged. leftWords.push({ value: change.value, type: "unchanged" }); rightWords.push({ value: change.value, type: "unchanged" }); } else if (change.removed) { // removed, so we need to check if there is an addition next. const nextChange = segmentChanges[charIndex + 1]; const areThereMoreCharacterChanges = nextChange !== undefined; const addsCharacterNext = areThereMoreCharacterChanges && segmentChanges[charIndex + 1].added; if (addsCharacterNext) { // there is addition next so we can show it as an update. leftWords.push({ value: change.value, type: "removed" }); rightWords.push({ value: nextChange.value, type: "added" }); // skip the next change since we've already processed it. charIndex++; } else { // no addition next, so we can show it as a removal. leftWords.push({ value: change.value, type: "removed" }); rightWords.push({ value: "", type: "empty" }); } } else { // added, so we can show it as an addition. leftWords.push({ value: "", type: "empty" }); rightWords.push({ value: change.value, type: "added" }); } } return { leftWords, rightWords }; }; const DiffViewer: React.FC = ({ oldString, newString, oldLabel = "Original Version", newLabel = "New Version", oldSubLabel, newSubLabel, className, }) => { const [diffLines, setDiffLines] = useState<{ left: DiffSegment[]; right: DiffSegment[]; }>({ left: [], right: [] }); useEffect(() => { const left: DiffSegment[] = []; const right: DiffSegment[] = []; const lineChanges = calculateDiffLines(oldString, newString, {}); for (let diffIndex = 0; diffIndex < lineChanges.length; diffIndex++) { const part = lineChanges[diffIndex]; // No changes if (!part.added && !part.removed) { left.push({ text: part.value, type: "unchanged" }); right.push({ text: part.value, type: "unchanged" }); } else if (part.removed) { // removed, so we need to check if there is an addition next. const areThereMoreChanges = diffIndex < lineChanges.length - 1; const isThereAnAdditionNext = areThereMoreChanges && lineChanges[diffIndex + 1].added; if (isThereAnAdditionNext) { // there is another change and it's an addition, meaning there is a change in the segment. const { leftWords, rightWords } = calculateSegmentDiff( part.value, lineChanges[diffIndex + 1].value, ); left.push({ parts: leftWords, text: "", type: "removed" }); right.push({ parts: rightWords, text: "", type: "added" }); diffIndex++; } else { // No addition next, meaning it's a removal of the part. left.push({ text: part.value, type: "removed" }); right.push({ text: "", type: "empty" }); } } else { // No removal before this part, meaning it's a new part. left.push({ text: "", type: "empty" }); right.push({ text: part.value, type: "added" }); } } setDiffLines({ left, right }); }, [oldString, newString]); const DiffRow: React.FC<{ leftLine: DiffSegment; rightLine: DiffSegment; }> = ({ leftLine, rightLine }) => { const typeClasses = { unchanged: "", removed: DIFF_COLORS.removed.line, added: DIFF_COLORS.added.line, empty: DIFF_COLORS.empty, }; const renderContent = (line: DiffSegment) => line.parts ? line.parts.map((part, idx) => ( {part.value} )) : line.text || "\u00A0"; return (
{renderContent(leftLine)}
{renderContent(rightLine)}
); }; if (oldString === newString) { return
No changes
; } return (
{oldLabel} {oldSubLabel && (
{oldSubLabel}
)}
{newLabel} {newSubLabel && (
{newSubLabel}
)}
{diffLines.left.map((leftLine, idx) => ( ))}
); }; export default DiffViewer;