From 8b17a175c3e7e27b789812eba4e3cd760beadb10 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 6 Jan 2026 04:08:11 +0000 Subject: Initial Control system --- .../src/components/mesh/OverlayDiffViewer.tsx | 476 +++++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 makima/frontend/src/components/mesh/OverlayDiffViewer.tsx (limited to 'makima/frontend/src/components/mesh/OverlayDiffViewer.tsx') diff --git a/makima/frontend/src/components/mesh/OverlayDiffViewer.tsx b/makima/frontend/src/components/mesh/OverlayDiffViewer.tsx new file mode 100644 index 0000000..74059a0 --- /dev/null +++ b/makima/frontend/src/components/mesh/OverlayDiffViewer.tsx @@ -0,0 +1,476 @@ +import { useState, useMemo } from "react"; + +interface DiffLine { + type: "add" | "remove" | "context" | "header" | "hunk"; + content: string; + oldLineNumber?: number; + newLineNumber?: number; +} + +interface DiffFile { + path: string; + status: "added" | "modified" | "deleted" | "renamed"; + oldPath?: string; // For renames + additions: number; + deletions: number; + lines: DiffLine[]; +} + +interface OverlayDiffViewerProps { + diff: string; + changedFiles?: string[]; + loading?: boolean; + error?: string; + onClose?: () => void; + title?: string; +} + +function parseDiff(diffText: string): DiffFile[] { + if (!diffText.trim()) return []; + + const files: DiffFile[] = []; + const lines = diffText.split("\n"); + + let currentFile: DiffFile | null = null; + let oldLineNum = 0; + let newLineNum = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // File header (diff --git a/path b/path) + if (line.startsWith("diff --git")) { + if (currentFile) { + files.push(currentFile); + } + + // Extract paths + const match = line.match(/diff --git a\/(.+) b\/(.+)/); + const oldPath = match?.[1] || ""; + const newPath = match?.[2] || oldPath; + + currentFile = { + path: newPath, + oldPath: oldPath !== newPath ? oldPath : undefined, + status: "modified", + additions: 0, + deletions: 0, + lines: [], + }; + continue; + } + + if (!currentFile) continue; + + // New file indicator + if (line.startsWith("new file mode")) { + currentFile.status = "added"; + continue; + } + + // Deleted file indicator + if (line.startsWith("deleted file mode")) { + currentFile.status = "deleted"; + continue; + } + + // Rename indicator + if (line.startsWith("rename from") || line.startsWith("rename to")) { + currentFile.status = "renamed"; + continue; + } + + // Hunk header (@@ -1,3 +1,4 @@) + if (line.startsWith("@@")) { + const hunkMatch = line.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/); + if (hunkMatch) { + oldLineNum = parseInt(hunkMatch[1], 10); + newLineNum = parseInt(hunkMatch[2], 10); + } + currentFile.lines.push({ + type: "hunk", + content: line, + }); + continue; + } + + // Skip other headers (---, +++, index, etc.) + if ( + line.startsWith("---") || + line.startsWith("+++") || + line.startsWith("index ") || + line.startsWith("Binary files") + ) { + currentFile.lines.push({ + type: "header", + content: line, + }); + continue; + } + + // Diff content + if (line.startsWith("+")) { + currentFile.additions++; + currentFile.lines.push({ + type: "add", + content: line.substring(1), + newLineNumber: newLineNum++, + }); + } else if (line.startsWith("-")) { + currentFile.deletions++; + currentFile.lines.push({ + type: "remove", + content: line.substring(1), + oldLineNumber: oldLineNum++, + }); + } else if (line.startsWith(" ") || line === "") { + currentFile.lines.push({ + type: "context", + content: line.substring(1) || "", + oldLineNumber: oldLineNum++, + newLineNumber: newLineNum++, + }); + } + } + + if (currentFile) { + files.push(currentFile); + } + + return files; +} + +function DiffFileView({ file, collapsed, onToggle }: { file: DiffFile; collapsed: boolean; onToggle: () => void }) { + const statusColors: Record = { + added: "text-green-400 bg-green-400/10", + modified: "text-yellow-400 bg-yellow-400/10", + deleted: "text-red-400 bg-red-400/10", + renamed: "text-purple-400 bg-purple-400/10", + }; + + const statusLabels: Record = { + added: "A", + modified: "M", + deleted: "D", + renamed: "R", + }; + + return ( +
+ {/* File header */} + + + {/* File content */} + {!collapsed && ( +
+ + + {file.lines.map((line, i) => { + if (line.type === "header" || line.type === "hunk") { + return ( + + + + ); + } + + const bgColor = + line.type === "add" + ? "bg-green-400/10" + : line.type === "remove" + ? "bg-red-400/10" + : ""; + + const textColor = + line.type === "add" + ? "text-green-400" + : line.type === "remove" + ? "text-red-400" + : "text-[#9bc3ff]"; + + const prefix = + line.type === "add" + ? "+" + : line.type === "remove" + ? "-" + : " "; + + return ( + + {/* Old line number */} + + {/* New line number */} + + {/* Content */} + + + ); + })} + +
+ {line.content} +
+ {line.type !== "add" ? line.oldLineNumber : ""} + + {line.type !== "remove" ? line.newLineNumber : ""} + + + {prefix} + + {line.content} +
+
+ )} +
+ ); +} + +export function OverlayDiffViewer({ + diff, + changedFiles, + loading, + error, + onClose, + title = "Overlay Changes", +}: OverlayDiffViewerProps) { + const [collapsedFiles, setCollapsedFiles] = useState>(new Set()); + const [showFullDiff, setShowFullDiff] = useState(true); + + const parsedFiles = useMemo(() => parseDiff(diff), [diff]); + + const toggleFile = (path: string) => { + setCollapsedFiles((prev) => { + const next = new Set(prev); + if (next.has(path)) { + next.delete(path); + } else { + next.add(path); + } + return next; + }); + }; + + const expandAll = () => setCollapsedFiles(new Set()); + const collapseAll = () => setCollapsedFiles(new Set(parsedFiles.map((f) => f.path))); + + // Calculate totals + const totals = useMemo(() => { + return parsedFiles.reduce( + (acc, file) => ({ + additions: acc.additions + file.additions, + deletions: acc.deletions + file.deletions, + files: acc.files + 1, + }), + { additions: 0, deletions: 0, files: 0 } + ); + }, [parsedFiles]); + + if (loading) { + return ( +
+
+
+ Loading diff... +
+
+
+ ); + } + + if (error) { + return ( +
+
+
+ {title} +
+ {onClose && ( + + )} +
+
+ {error} +
+
+ ); + } + + if (!diff.trim() && (!changedFiles || changedFiles.length === 0)) { + return ( +
+
+
+ {title} +
+ {onClose && ( + + )} +
+
+ No changes detected in overlay +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ {title} +
+
+ {totals.files} file{totals.files !== 1 ? "s" : ""} changed + {totals.additions > 0 && ( + +{totals.additions} + )} + {totals.deletions > 0 && ( + -{totals.deletions} + )} +
+
+
+ + + + {onClose && ( + + )} +
+
+ + {/* Content */} +
+ {showFullDiff ? ( + // Full diff view + parsedFiles.length > 0 ? ( + parsedFiles.map((file) => ( + toggleFile(file.path)} + /> + )) + ) : ( + // Fallback to raw diff +
+              {diff}
+            
+ ) + ) : ( + // File list view +
+ {(changedFiles || parsedFiles.map((f) => f.path)).map((path) => { + const file = parsedFiles.find((f) => f.path === path); + return ( +
+ {file && ( + + {file.status.charAt(0).toUpperCase()} + + )} + + {path} + + {file && ( + + {file.additions > 0 && ( + +{file.additions} + )} + {file.deletions > 0 && ( + -{file.deletions} + )} + + )} +
+ ); + })} +
+ )} +
+
+ ); +} -- cgit v1.2.3