summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/files/UpdateNotification.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2025-12-24 02:21:19 +0000
committersoryu <soryu@soryu.co>2025-12-24 02:21:19 +0000
commit8f016a0e9d14badc39dffd67ed6fb862f9d08496 (patch)
treec5ad00f89f838e2e7b6fd61184b8b6565f3296bc /makima/frontend/src/components/files/UpdateNotification.tsx
parentaa62bb8578d48598297e60b253e29a1957c5f51a (diff)
downloadsoryu-8f016a0e9d14badc39dffd67ed6fb862f9d08496.tar.gz
soryu-8f016a0e9d14badc39dffd67ed6fb862f9d08496.zip
Update overwrite mechanism to show diff
Diffstat (limited to 'makima/frontend/src/components/files/UpdateNotification.tsx')
-rw-r--r--makima/frontend/src/components/files/UpdateNotification.tsx216
1 files changed, 193 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) + "...";
+}