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 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) + "...";
}