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 "?"; } } // Word-level diff for showing inline changes interface WordDiff { type: "same" | "added" | "removed"; text: string; } function computeWordDiff(oldText: string, newText: string): WordDiff[] { const oldWords = oldText.split(/(\s+)/); const newWords = newText.split(/(\s+)/); const result: WordDiff[] = []; // Simple LCS-based diff const m = oldWords.length; const n = newWords.length; // Build LCS table const dp: number[][] = Array(m + 1) .fill(null) .map(() => Array(n + 1).fill(0)); for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (oldWords[i - 1] === newWords[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } } // Backtrack to find diff let i = m, j = n; const diffStack: WordDiff[] = []; while (i > 0 || j > 0) { if (i > 0 && j > 0 && oldWords[i - 1] === newWords[j - 1]) { diffStack.push({ type: "same", text: oldWords[i - 1] }); i--; j--; } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { diffStack.push({ type: "added", text: newWords[j - 1] }); j--; } else { diffStack.push({ type: "removed", text: oldWords[i - 1] }); i--; } } // Reverse and merge consecutive same-type diffs for (let k = diffStack.length - 1; k >= 0; k--) { const item = diffStack[k]; if (result.length > 0 && result[result.length - 1].type === item.type) { result[result.length - 1].text += item.text; } else { result.push({ ...item }); } } return result; } interface DiffItem { type: "added" | "removed" | "modified" | "unchanged"; localElement?: BodyElement; remoteElement?: BodyElement; localIndex?: number; remoteIndex?: number; wordDiff?: WordDiff[]; } // 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 - compute word-level diff const wordDiff = computeWordDiff(localText, remoteText); diffs.push({ type: "modified", localElement: local, remoteElement: remote, localIndex: i, remoteIndex: i, wordDiff, }); } // 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 (
This file was updated by {source}. {hasChanges ? ` ${diffs.length} change${diffs.length > 1 ? "s" : ""} found.` : " No content changes detected."}