diff options
| author | soryu <soryu@soryu.co> | 2025-12-24 02:21:19 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2025-12-24 02:21:19 +0000 |
| commit | 8f016a0e9d14badc39dffd67ed6fb862f9d08496 (patch) | |
| tree | c5ad00f89f838e2e7b6fd61184b8b6565f3296bc /makima | |
| parent | aa62bb8578d48598297e60b253e29a1957c5f51a (diff) | |
| download | soryu-8f016a0e9d14badc39dffd67ed6fb862f9d08496.tar.gz soryu-8f016a0e9d14badc39dffd67ed6fb862f9d08496.zip | |
Update overwrite mechanism to show diff
Diffstat (limited to 'makima')
| -rw-r--r-- | makima/frontend/src/components/files/UpdateNotification.tsx | 216 | ||||
| -rw-r--r-- | makima/frontend/src/routes/files.tsx | 8 |
2 files changed, 201 insertions, 23 deletions
diff --git a/makima/frontend/src/components/files/UpdateNotification.tsx b/makima/frontend/src/components/files/UpdateNotification.tsx index 92b2b15..c87d535 100644 --- a/makima/frontend/src/components/files/UpdateNotification.tsx +++ b/makima/frontend/src/components/files/UpdateNotification.tsx @@ -1,43 +1,213 @@ +import type { BodyElement } from "../../lib/api"; + interface UpdateNotificationProps { updatedBy: "user" | "llm" | "system"; + localBody: BodyElement[]; + remoteBody: BodyElement[]; onRefresh: () => void; onDismiss: () => void; } +// Get text content from a body element for comparison +function getElementText(element: BodyElement): string { + switch (element.type) { + case "heading": + case "paragraph": + return element.text; + case "chart": + return `[Chart: ${element.title || element.chartType}]`; + case "image": + return `[Image: ${element.alt || element.src}]`; + default: + return "[Unknown element]"; + } +} + +// Get element type label +function getElementTypeLabel(element: BodyElement): string { + switch (element.type) { + case "heading": + return `H${element.level}`; + case "paragraph": + return "P"; + case "chart": + return "Chart"; + case "image": + return "Image"; + default: + return "?"; + } +} + +interface DiffItem { + type: "added" | "removed" | "modified" | "unchanged"; + localElement?: BodyElement; + remoteElement?: BodyElement; + localIndex?: number; + remoteIndex?: number; +} + +// Simple diff algorithm - compares elements by their text content +function computeDiff(localBody: BodyElement[], remoteBody: BodyElement[]): DiffItem[] { + const diffs: DiffItem[] = []; + const maxLen = Math.max(localBody.length, remoteBody.length); + + for (let i = 0; i < maxLen; i++) { + const local = localBody[i]; + const remote = remoteBody[i]; + + if (!local && remote) { + // Element added remotely + diffs.push({ type: "added", remoteElement: remote, remoteIndex: i }); + } else if (local && !remote) { + // Element removed remotely (exists locally but not remotely) + diffs.push({ type: "removed", localElement: local, localIndex: i }); + } else if (local && remote) { + const localText = getElementText(local); + const remoteText = getElementText(remote); + if (localText !== remoteText || local.type !== remote.type) { + // Element modified + diffs.push({ + type: "modified", + localElement: local, + remoteElement: remote, + localIndex: i, + remoteIndex: i, + }); + } + // Skip unchanged elements + } + } + + return diffs; +} + export function UpdateNotification({ updatedBy, + localBody, + remoteBody, onRefresh, onDismiss, }: UpdateNotificationProps) { const source = updatedBy === "llm" ? "AI assistant" : "another session"; + const diffs = computeDiff(localBody, remoteBody); + const hasChanges = diffs.length > 0; return ( - <div className="fixed bottom-4 right-4 max-w-md p-4 bg-[#1a2332] border border-[#3f6fb3]/50 shadow-lg z-50"> - <div className="flex items-start gap-3"> - <div className="text-[#75aafc] text-xl font-bold">i</div> - <div className="flex-1"> - <h3 className="font-mono text-sm text-[#9bc3ff] font-semibold mb-1"> - File Updated - </h3> - <p className="font-mono text-xs text-white/70 mb-3"> - This file was updated by {source}. - </p> - <div className="flex gap-2"> - <button - onClick={onRefresh} - className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors" - > - Refresh Now - </button> - <button - onClick={onDismiss} - className="px-3 py-1 font-mono text-xs text-[#555] hover:text-white/70 transition-colors" - > - Dismiss - </button> + <div className="fixed bottom-4 right-4 w-[480px] max-h-[70vh] flex flex-col bg-[#1a2332] border border-[#3f6fb3]/50 shadow-lg z-50"> + {/* Header */} + <div className="p-4 border-b border-[#3f6fb3]/30"> + <div className="flex items-start gap-3"> + <div className="text-[#75aafc] text-xl font-bold shrink-0">i</div> + <div className="flex-1 min-w-0"> + <h3 className="font-mono text-sm text-[#9bc3ff] font-semibold mb-1"> + Remote Changes Detected + </h3> + <p className="font-mono text-xs text-white/70"> + This file was updated by {source}. + {hasChanges + ? ` ${diffs.length} change${diffs.length > 1 ? "s" : ""} found.` + : " No content changes detected."} + </p> </div> </div> </div> + + {/* Diff Content */} + {hasChanges && ( + <div className="flex-1 overflow-y-auto p-4 space-y-3 min-h-0"> + <div className="font-mono text-xs text-[#555] uppercase tracking-wider mb-2"> + Changes + </div> + {diffs.map((diff, index) => ( + <DiffItemView key={index} diff={diff} /> + ))} + </div> + )} + + {/* Actions */} + <div className="p-4 border-t border-[#3f6fb3]/30 flex gap-2"> + <button + onClick={onRefresh} + className="flex-1 px-3 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors" + > + Accept Remote Changes + </button> + <button + onClick={onDismiss} + className="px-3 py-2 font-mono text-xs text-[#555] border border-[#333] hover:text-white/70 hover:border-[#555] transition-colors" + > + Keep Local + </button> + </div> </div> ); } + +function DiffItemView({ diff }: { diff: DiffItem }) { + if (diff.type === "added") { + return ( + <div className="border border-green-500/30 bg-green-500/5 p-3"> + <div className="flex items-center gap-2 mb-1"> + <span className="text-green-400 text-xs font-mono font-bold">+ ADDED</span> + <span className="text-[#555] text-xs font-mono"> + [{getElementTypeLabel(diff.remoteElement!)}] + </span> + </div> + <div className="font-mono text-sm text-green-300/80 break-words"> + {truncateText(getElementText(diff.remoteElement!), 150)} + </div> + </div> + ); + } + + if (diff.type === "removed") { + return ( + <div className="border border-red-500/30 bg-red-500/5 p-3"> + <div className="flex items-center gap-2 mb-1"> + <span className="text-red-400 text-xs font-mono font-bold">- REMOVED</span> + <span className="text-[#555] text-xs font-mono"> + [{getElementTypeLabel(diff.localElement!)}] + </span> + </div> + <div className="font-mono text-sm text-red-300/80 break-words line-through"> + {truncateText(getElementText(diff.localElement!), 150)} + </div> + </div> + ); + } + + if (diff.type === "modified") { + return ( + <div className="border border-yellow-500/30 bg-yellow-500/5 p-3"> + <div className="flex items-center gap-2 mb-2"> + <span className="text-yellow-400 text-xs font-mono font-bold">~ MODIFIED</span> + <span className="text-[#555] text-xs font-mono"> + [{getElementTypeLabel(diff.localElement!)}] + </span> + </div> + <div className="space-y-2"> + <div> + <div className="text-[#555] text-[10px] font-mono uppercase mb-1">Your version:</div> + <div className="font-mono text-xs text-red-300/70 break-words bg-red-500/10 p-2 border-l-2 border-red-500/50"> + {truncateText(getElementText(diff.localElement!), 120)} + </div> + </div> + <div> + <div className="text-[#555] text-[10px] font-mono uppercase mb-1">Remote version:</div> + <div className="font-mono text-xs text-green-300/70 break-words bg-green-500/10 p-2 border-l-2 border-green-500/50"> + {truncateText(getElementText(diff.remoteElement!), 120)} + </div> + </div> + </div> + </div> + ); + } + + return null; +} + +function truncateText(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + return text.slice(0, maxLength) + "..."; +} diff --git a/makima/frontend/src/routes/files.tsx b/makima/frontend/src/routes/files.tsx index 037df7e..f398041 100644 --- a/makima/frontend/src/routes/files.tsx +++ b/makima/frontend/src/routes/files.tsx @@ -21,6 +21,7 @@ export default function FilesPage() { const [detailLoading, setDetailLoading] = useState(false); const [creating, setCreating] = useState(false); const [remoteUpdate, setRemoteUpdate] = useState<FileUpdateEvent | null>(null); + const [remoteFileData, setRemoteFileData] = useState<FileDetailType | null>(null); const [hasLocalChanges, setHasLocalChanges] = useState(false); const [isActivelyEditing, setIsActivelyEditing] = useState(false); const pendingUpdateRef = useRef(false); @@ -54,6 +55,9 @@ export default function FilesPage() { const detail = await fetchFile(event.fileId); setFileDetail(detail); } else { + // Fetch remote version for diff display + const remoteData = await fetchFile(event.fileId); + setRemoteFileData(remoteData); // Show notification about remote update setRemoteUpdate(event); } @@ -218,12 +222,14 @@ export default function FilesPage() { const detail = await fetchFile(id); setFileDetail(detail); setRemoteUpdate(null); + setRemoteFileData(null); setHasLocalChanges(false); } }, [id, fetchFile]); const handleRemoteUpdateDismiss = useCallback(() => { setRemoteUpdate(null); + setRemoteFileData(null); }, []); return ( @@ -285,6 +291,8 @@ export default function FilesPage() { {remoteUpdate && ( <UpdateNotification updatedBy={remoteUpdate.updatedBy} + localBody={fileDetail?.body || []} + remoteBody={remoteFileData?.body || []} onRefresh={handleRemoteUpdateRefresh} onDismiss={handleRemoteUpdateDismiss} /> |
