diff options
Diffstat (limited to 'makima/frontend/src/components/files/UpdateNotification.tsx')
| -rw-r--r-- | makima/frontend/src/components/files/UpdateNotification.tsx | 118 |
1 files changed, 106 insertions, 12 deletions
diff --git a/makima/frontend/src/components/files/UpdateNotification.tsx b/makima/frontend/src/components/files/UpdateNotification.tsx index c87d535..863f951 100644 --- a/makima/frontend/src/components/files/UpdateNotification.tsx +++ b/makima/frontend/src/components/files/UpdateNotification.tsx @@ -39,12 +39,75 @@ function getElementTypeLabel(element: BodyElement): string { } } +// 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 @@ -66,13 +129,15 @@ function computeDiff(localBody: BodyElement[], remoteBody: BodyElement[]): DiffI const localText = getElementText(local); const remoteText = getElementText(remote); if (localText !== remoteText || local.type !== remote.type) { - // Element modified + // 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 @@ -186,20 +251,49 @@ function DiffItemView({ diff }: { diff: DiffItem }) { [{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> + {diff.wordDiff ? ( + <div className="font-mono text-xs break-words leading-relaxed"> + {diff.wordDiff.map((word, idx) => { + if (word.type === "same") { + return ( + <span key={idx} className="text-white/60"> + {word.text} + </span> + ); + } else if (word.type === "removed") { + return ( + <span + key={idx} + className="bg-red-500/30 text-red-300 line-through" + > + {word.text} + </span> + ); + } else { + return ( + <span key={idx} className="bg-green-500/30 text-green-300"> + {word.text} + </span> + ); + } + })} </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 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"> + {getElementText(diff.localElement!)} + </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"> + {getElementText(diff.remoteElement!)} + </div> </div> </div> - </div> + )} </div> ); } |
