diff --git a/frontend/src/pages/TextDiffChecker.jsx b/frontend/src/pages/TextDiffChecker.jsx
deleted file mode 100644
index 288bb02..0000000
--- a/frontend/src/pages/TextDiffChecker.jsx
+++ /dev/null
@@ -1,227 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import * as Diff from 'diff';
-import { Grid, Column, Button, ContentSwitcher, Switch } from '@carbon/react';
-import { Compare, Renew } from '@carbon/icons-react';
-import { ToolHeader, ToolControls, ToolPane, ToolSplitPane } from '../components/ToolUI';
-import useLayoutToggle from '../hooks/useLayoutToggle';
-
-const DiffView = ({ diffs, mode }) => {
- const renderDiff = () => {
- if (diffs.length === 0) {
- return No differences;
- }
-
- return diffs.map((part, index) => {
- const isAdded = part.added;
- const isRemoved = part.removed;
-
- let style = {
- display: 'block',
- padding: '1px 4px',
- margin: '1px 0',
- borderRadius: '2px',
- };
-
- if (isAdded) {
- style = {
- ...style,
- backgroundColor: 'var(--cds-support-success-inverse)',
- color: 'var(--cds-text-on-color)',
- };
- } else if (isRemoved) {
- style = {
- ...style,
- backgroundColor: 'var(--cds-support-error-inverse)',
- color: 'var(--cds-text-on-color)',
- };
- }
-
- const prefix = isAdded ? '+ ' : isRemoved ? '- ' : ' ';
-
- return (
-
- {prefix}
- {part.value}
-
- );
- });
- };
-
- return (
-
-
-
-
-
- {renderDiff()}
-
-
- );
-};
-
-export default function TextDiffChecker() {
- const [oldText, setOldText] = useState('');
- const [newText, setNewText] = useState('');
- const [diffs, setDiffs] = useState([]);
- const [diffMode, setDiffMode] = useState(0); // 0 = lines, 1 = words, 2 = chars
-
- const layout = useLayoutToggle({
- toolKey: 'text-diff-layout',
- defaultDirection: 'horizontal',
- showToggle: true,
- persist: true,
- });
-
- const compare = () => {
- let d;
- switch (diffMode) {
- case 0:
- d = Diff.diffLines(oldText, newText, { newlineIsToken: true });
- break;
- case 1:
- d = Diff.diffWords(oldText, newText);
- break;
- case 2:
- d = Diff.diffChars(oldText, newText);
- break;
- default:
- d = Diff.diffLines(oldText, newText, { newlineIsToken: true });
- }
- setDiffs(d);
- };
-
- const clearAll = () => {
- setOldText('');
- setNewText('');
- setDiffs([]);
- };
-
- // Auto-compare when inputs change
- useEffect(() => {
- if (oldText || newText) {
- compare();
- } else {
- setDiffs([]);
- }
- }, [oldText, newText, diffMode]);
-
- return (
-
-
-
-
-
-
-
-
-
-
- setDiffMode(index)}
- size="sm"
- style={{ width: 'auto', minWidth: '150px' }}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- setOldText(e.target.value)}
- placeholder="Paste original text..."
- />
- setNewText(e.target.value)}
- placeholder="Paste new text..."
- />
-
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/TextDiffChecker/components/DiffModeToggle.jsx b/frontend/src/pages/TextDiffChecker/components/DiffModeToggle.jsx
new file mode 100644
index 0000000..f6e70fe
--- /dev/null
+++ b/frontend/src/pages/TextDiffChecker/components/DiffModeToggle.jsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { TextAlignLeft, TextAlignCenter, LetterAa } from '@carbon/icons-react';
+
+const modes = [
+ { label: 'Lines', icon: TextAlignLeft },
+ { label: 'Words', icon: TextAlignCenter },
+ { label: 'Chars', icon: LetterAa },
+];
+
+export default function DiffModeToggle({ activeMode, onChange }) {
+ return (
+
+ {modes.map((mode, idx) => {
+ const isActive = activeMode === idx;
+ const Icon = mode.icon;
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend/src/pages/TextDiffChecker/components/DiffView.jsx b/frontend/src/pages/TextDiffChecker/components/DiffView.jsx
new file mode 100644
index 0000000..d2b889b
--- /dev/null
+++ b/frontend/src/pages/TextDiffChecker/components/DiffView.jsx
@@ -0,0 +1,95 @@
+import React from 'react';
+
+export default function DiffView({ diffs }) {
+ const renderDiff = () => {
+ if (diffs.length === 0) {
+ return No differences;
+ }
+
+ return diffs.map((part, index) => {
+ const isAdded = part.added;
+ const isRemoved = part.removed;
+
+ let style = {
+ display: 'block',
+ padding: '1px 4px',
+ margin: '1px 0',
+ };
+
+ if (isAdded) {
+ style = {
+ ...style,
+ backgroundColor: 'var(--cds-support-success-inverse)',
+ color: 'var(--cds-text-on-color)',
+ };
+ } else if (isRemoved) {
+ style = {
+ ...style,
+ backgroundColor: 'var(--cds-support-error-inverse)',
+ color: 'var(--cds-text-on-color)',
+ };
+ }
+
+ const prefix = isAdded ? '+ ' : isRemoved ? '- ' : ' ';
+
+ return (
+
+ {prefix}
+ {part.value}
+
+ );
+ });
+ };
+
+ return (
+
+
+
+
+
+ {renderDiff()}
+
+
+ );
+}
diff --git a/frontend/src/pages/TextDiffChecker/constants.js b/frontend/src/pages/TextDiffChecker/constants.js
new file mode 100644
index 0000000..2441863
--- /dev/null
+++ b/frontend/src/pages/TextDiffChecker/constants.js
@@ -0,0 +1,8 @@
+export const algorithmOptions = [
+ { id: 'lines', label: 'Lines', description: 'Compare line by line' },
+ { id: 'words', label: 'Words', description: 'Compare word by word' },
+ { id: 'wordsWithSpace', label: 'Words with Space', description: 'Words including whitespace' },
+ { id: 'chars', label: 'Characters', description: 'Compare character by character' },
+ { id: 'sentences', label: 'Sentences', description: 'Compare sentence by sentence' },
+ { id: 'json', label: 'JSON', description: 'Semantic JSON comparison' },
+];
diff --git a/frontend/src/pages/TextDiffChecker/diffUtils.js b/frontend/src/pages/TextDiffChecker/diffUtils.js
new file mode 100644
index 0000000..3def29f
--- /dev/null
+++ b/frontend/src/pages/TextDiffChecker/diffUtils.js
@@ -0,0 +1,36 @@
+import * as Diff from 'diff';
+
+export const computeDiffResult = (oldText, newText, algorithm, ignoreWhitespace) => {
+ if (!oldText && !newText) {
+ return [];
+ }
+
+ const options = {};
+
+ if (ignoreWhitespace && (algorithm === 'lines' || algorithm === 'words')) {
+ options.ignoreWhitespace = true;
+ }
+
+ switch (algorithm) {
+ case 'lines':
+ return Diff.diffLines(oldText, newText, { ...options, newlineIsToken: true });
+ case 'words':
+ return Diff.diffWords(oldText, newText, options);
+ case 'wordsWithSpace':
+ return Diff.diffWordsWithSpace(oldText, newText);
+ case 'chars':
+ return Diff.diffChars(oldText, newText);
+ case 'sentences':
+ return Diff.diffSentences(oldText, newText);
+ case 'json':
+ try {
+ const oldObj = oldText ? JSON.parse(oldText) : {};
+ const newObj = newText ? JSON.parse(newText) : {};
+ return Diff.diffJson(oldObj, newObj);
+ } catch {
+ return Diff.diffLines(oldText, newText, { newlineIsToken: true });
+ }
+ default:
+ return Diff.diffLines(oldText, newText, { newlineIsToken: true });
+ }
+};
diff --git a/frontend/src/pages/TextDiffChecker/index.jsx b/frontend/src/pages/TextDiffChecker/index.jsx
new file mode 100644
index 0000000..d912536
--- /dev/null
+++ b/frontend/src/pages/TextDiffChecker/index.jsx
@@ -0,0 +1,111 @@
+import React, { useState, useEffect } from 'react';
+import { Grid, Column, Checkbox } from '@carbon/react';
+import { Compare } from '@carbon/icons-react';
+import { ToolHeader, ToolPane, ToolSplitPane } from '../../components/ToolUI';
+import useLayoutToggle from '../../hooks/useLayoutToggle';
+import DiffView from './components/DiffView';
+import DiffModeToggle from './components/DiffModeToggle';
+import { computeDiffResult } from './diffUtils';
+
+export default function TextDiffChecker() {
+ const [oldText, setOldText] = useState('');
+ const [newText, setNewText] = useState('');
+ const [diffs, setDiffs] = useState([]);
+ const [algorithm, setAlgorithm] = useState('lines');
+ const [ignoreWhitespace, setIgnoreWhitespace] = useState(false);
+
+ const layout = useLayoutToggle({
+ toolKey: 'text-diff-layout',
+ defaultDirection: 'horizontal',
+ showToggle: true,
+ persist: true,
+ });
+
+ // Auto-compare when inputs or options change
+ useEffect(() => {
+ const result = computeDiffResult(oldText, newText, algorithm, ignoreWhitespace);
+ setDiffs(result);
+ }, [oldText, newText, algorithm, ignoreWhitespace]);
+
+ const granularityIndex = ['lines', 'words', 'chars'].indexOf(algorithm);
+
+ return (
+
+
+
+
+
+ {/* Controls Row */}
+
+
+ {/* Granularity Toggle */}
+
+
+ = 0 ? granularityIndex : 0}
+ onChange={(mode) => {
+ const algoMap = ['lines', 'words', 'chars'];
+ setAlgorithm(algoMap[mode]);
+ }}
+ />
+
+
+ {/* Ignore Whitespace Checkbox */}
+
setIgnoreWhitespace(checked)}
+ />
+
+
+
+ {/* Inputs Row */}
+
+
+
+ setOldText(e.target.value)}
+ placeholder="Paste original text..."
+ />
+ setNewText(e.target.value)}
+ placeholder="Paste new text..."
+ />
+
+
+
+
+
+
+
+ );
+}