summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/files/UpdateNotification.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/files/UpdateNotification.tsx')
-rw-r--r--makima/frontend/src/components/files/UpdateNotification.tsx118
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>
);
}