From 8f016a0e9d14badc39dffd67ed6fb862f9d08496 Mon Sep 17 00:00:00 2001 From: soryu Date: Wed, 24 Dec 2025 02:21:19 +0000 Subject: Update overwrite mechanism to show diff --- .../src/components/files/UpdateNotification.tsx | 216 ++++++++++++++++++--- 1 file changed, 193 insertions(+), 23 deletions(-) (limited to 'makima/frontend/src/components') 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 ( -
-
-
i
-
-

- File Updated -

-

- This file was updated by {source}. -

-
- - +
+ {/* Header */} +
+
+
i
+
+

+ Remote Changes Detected +

+

+ This file was updated by {source}. + {hasChanges + ? ` ${diffs.length} change${diffs.length > 1 ? "s" : ""} found.` + : " No content changes detected."} +

+ + {/* Diff Content */} + {hasChanges && ( +
+
+ Changes +
+ {diffs.map((diff, index) => ( + + ))} +
+ )} + + {/* Actions */} +
+ + +
); } + +function DiffItemView({ diff }: { diff: DiffItem }) { + if (diff.type === "added") { + return ( +
+
+ + ADDED + + [{getElementTypeLabel(diff.remoteElement!)}] + +
+
+ {truncateText(getElementText(diff.remoteElement!), 150)} +
+
+ ); + } + + if (diff.type === "removed") { + return ( +
+
+ - REMOVED + + [{getElementTypeLabel(diff.localElement!)}] + +
+
+ {truncateText(getElementText(diff.localElement!), 150)} +
+
+ ); + } + + if (diff.type === "modified") { + return ( +
+
+ ~ MODIFIED + + [{getElementTypeLabel(diff.localElement!)}] + +
+
+
+
Your version:
+
+ {truncateText(getElementText(diff.localElement!), 120)} +
+
+
+
Remote version:
+
+ {truncateText(getElementText(diff.remoteElement!), 120)} +
+
+
+
+ ); + } + + return null; +} + +function truncateText(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + return text.slice(0, maxLength) + "..."; +} -- cgit v1.2.3