Spaces:
Sleeping
Sleeping
| /** | |
| * CodeDiffViewer — Manus-style code diff viewer with tabs: | |
| * Diff / Original / Modified | |
| * Used in ActionTree when edit_file or write_file tool calls are shown. | |
| */ | |
| import { useState, useMemo } from "react"; | |
| import { cn } from "@/lib/utils"; | |
| type DiffTab = "diff" | "original" | "modified"; | |
| interface DiffLine { | |
| type: "added" | "removed" | "context"; | |
| lineOld?: number; | |
| lineNew?: number; | |
| content: string; | |
| } | |
| /** | |
| * Simple unified diff parser. | |
| * Takes old text and new text, produces a line-by-line diff. | |
| */ | |
| function computeDiff(oldText: string, newText: string): DiffLine[] { | |
| const oldLines = oldText.split("\n"); | |
| const newLines = newText.split("\n"); | |
| const result: DiffLine[] = []; | |
| // Simple LCS-based diff | |
| const m = oldLines.length; | |
| const n = newLines.length; | |
| // For large files, fall back to a simpler approach | |
| if (m * n > 1_000_000) { | |
| // Just show removed then added for very large files | |
| oldLines.forEach((line, i) => { | |
| result.push({ type: "removed", lineOld: i + 1, content: line }); | |
| }); | |
| newLines.forEach((line, i) => { | |
| result.push({ type: "added", lineNew: i + 1, content: line }); | |
| }); | |
| return result; | |
| } | |
| // Build LCS table | |
| const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); | |
| for (let i = 1; i <= m; i++) { | |
| for (let j = 1; j <= n; j++) { | |
| if (oldLines[i - 1] === newLines[j - 1]) { | |
| dp[i][j] = dp[i - 1][j - 1] + 1; | |
| } else { | |
| dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); | |
| } | |
| } | |
| } | |
| // Backtrack to produce diff | |
| const diffLines: DiffLine[] = []; | |
| let i = m, j = n; | |
| while (i > 0 || j > 0) { | |
| if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) { | |
| diffLines.unshift({ type: "context", lineOld: i, lineNew: j, content: oldLines[i - 1] }); | |
| i--; j--; | |
| } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { | |
| diffLines.unshift({ type: "added", lineNew: j, content: newLines[j - 1] }); | |
| j--; | |
| } else { | |
| diffLines.unshift({ type: "removed", lineOld: i, content: oldLines[i - 1] }); | |
| i--; | |
| } | |
| } | |
| return diffLines; | |
| } | |
| interface CodeDiffViewerProps { | |
| oldContent: string; | |
| newContent: string; | |
| fileName?: string; | |
| className?: string; | |
| } | |
| export function CodeDiffViewer({ oldContent, newContent, fileName, className }: CodeDiffViewerProps) { | |
| const [activeTab, setActiveTab] = useState<DiffTab>("diff"); | |
| const diffLines = useMemo( | |
| () => computeDiff(oldContent, newContent), | |
| [oldContent, newContent] | |
| ); | |
| const stats = useMemo(() => { | |
| const added = diffLines.filter(l => l.type === "added").length; | |
| const removed = diffLines.filter(l => l.type === "removed").length; | |
| return { added, removed }; | |
| }, [diffLines]); | |
| const tabs: { id: DiffTab; label: string }[] = [ | |
| { id: "diff", label: "Diff" }, | |
| { id: "original", label: "Original" }, | |
| { id: "modified", label: "Modified" }, | |
| ]; | |
| return ( | |
| <div className={cn("flex flex-col rounded-md border border-border overflow-hidden", className)}> | |
| {/* Header with tabs */} | |
| <div className="flex items-center justify-between bg-secondary/30 border-b border-border"> | |
| <div className="flex"> | |
| {tabs.map((tab) => ( | |
| <button | |
| key={tab.id} | |
| onClick={() => setActiveTab(tab.id)} | |
| className={cn( | |
| "px-3 py-1.5 text-[11px] font-medium transition-colors border-b-2 -mb-px", | |
| activeTab === tab.id | |
| ? "border-primary text-foreground bg-background/50" | |
| : "border-transparent text-muted-foreground hover:text-foreground" | |
| )} | |
| > | |
| {tab.label} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="flex items-center gap-2 px-2"> | |
| {fileName && ( | |
| <span className="text-[10px] font-mono text-muted-foreground"> | |
| {fileName} | |
| </span> | |
| )} | |
| <span className="text-[10px] font-mono text-green-500">+{stats.added}</span> | |
| <span className="text-[10px] font-mono text-red-500">-{stats.removed}</span> | |
| </div> | |
| </div> | |
| {/* Content */} | |
| <div className="overflow-auto max-h-[400px] bg-[#0d1117]"> | |
| {activeTab === "diff" && ( | |
| <table className="w-full text-[11px] font-mono border-collapse"> | |
| <tbody> | |
| {diffLines.map((line, idx) => ( | |
| <tr | |
| key={idx} | |
| className={cn( | |
| line.type === "added" && "bg-green-500/10", | |
| line.type === "removed" && "bg-red-500/10" | |
| )} | |
| > | |
| <td className="w-[1px] px-1 text-right text-muted-foreground/40 select-none border-r border-border/30 whitespace-nowrap"> | |
| {line.lineOld || ""} | |
| </td> | |
| <td className="w-[1px] px-1 text-right text-muted-foreground/40 select-none border-r border-border/30 whitespace-nowrap"> | |
| {line.lineNew || ""} | |
| </td> | |
| <td className="w-[1px] px-1 select-none font-bold"> | |
| <span className={cn( | |
| line.type === "added" && "text-green-500", | |
| line.type === "removed" && "text-red-500", | |
| line.type === "context" && "text-muted-foreground/30" | |
| )}> | |
| {line.type === "added" ? "+" : line.type === "removed" ? "-" : " "} | |
| </span> | |
| </td> | |
| <td className="px-2 whitespace-pre-wrap break-all"> | |
| <span className={cn( | |
| line.type === "added" && "text-green-400/90", | |
| line.type === "removed" && "text-red-400/90", | |
| line.type === "context" && "text-foreground/60" | |
| )}> | |
| {line.content} | |
| </span> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| )} | |
| {activeTab === "original" && ( | |
| <pre className="text-[11px] font-mono p-3 whitespace-pre-wrap break-all text-foreground/70"> | |
| {oldContent || "(empty)"} | |
| </pre> | |
| )} | |
| {activeTab === "modified" && ( | |
| <pre className="text-[11px] font-mono p-3 whitespace-pre-wrap break-all text-foreground/70"> | |
| {newContent || "(empty)"} | |
| </pre> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |