From 830548984a063ab53fd6d3debc23936a5706aecf Mon Sep 17 00:00:00 2001 From: changliuchang777 <1551617642@qq.com> Date: Sun, 5 Jul 2026 23:44:44 +0800 Subject: [PATCH 1/3] feat: add response diff utility --- src/utils/diff.ts | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/utils/diff.ts diff --git a/src/utils/diff.ts b/src/utils/diff.ts new file mode 100644 index 0000000..45a8a97 --- /dev/null +++ b/src/utils/diff.ts @@ -0,0 +1,57 @@ +export type DiffLineType = 'same' | 'added' | 'removed'; + +export type DiffLine = { + type: DiffLineType; + text: string; +}; + +function formatJsonLines(value: unknown) { + return JSON.stringify(value ?? {}, null, 2).split('\n'); +} + +export function buildJsonDiff(before: unknown, after: unknown): DiffLine[] { + const previous = formatJsonLines(before); + const next = formatJsonLines(after); + const lengths = Array.from({ length: previous.length + 1 }, () => + Array(next.length + 1).fill(0) + ); + + for (let i = previous.length - 1; i >= 0; i -= 1) { + for (let j = next.length - 1; j >= 0; j -= 1) { + lengths[i][j] = + previous[i] === next[j] + ? lengths[i + 1][j + 1] + 1 + : Math.max(lengths[i + 1][j], lengths[i][j + 1]); + } + } + + const diff: DiffLine[] = []; + let i = 0; + let j = 0; + + while (i < previous.length && j < next.length) { + if (previous[i] === next[j]) { + diff.push({ type: 'same', text: previous[i] }); + i += 1; + j += 1; + } else if (lengths[i + 1][j] >= lengths[i][j + 1]) { + diff.push({ type: 'removed', text: previous[i] }); + i += 1; + } else { + diff.push({ type: 'added', text: next[j] }); + j += 1; + } + } + + while (i < previous.length) { + diff.push({ type: 'removed', text: previous[i] }); + i += 1; + } + + while (j < next.length) { + diff.push({ type: 'added', text: next[j] }); + j += 1; + } + + return diff; +} From c254a0256869b6676f607b38d9da29d739b62cc0 Mon Sep 17 00:00:00 2001 From: changliuchang777 <1551617642@qq.com> Date: Sun, 5 Jul 2026 23:44:59 +0800 Subject: [PATCH 2/3] feat: show response diff in call history --- src/components/CallHistoryRow.tsx | 34 ++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/components/CallHistoryRow.tsx b/src/components/CallHistoryRow.tsx index 523fb87..a6de951 100644 --- a/src/components/CallHistoryRow.tsx +++ b/src/components/CallHistoryRow.tsx @@ -1,6 +1,6 @@ -import { useState } from 'react'; import { CheckCircle, XCircle } from 'lucide-react'; import { formatPrice } from '../utils/format'; +import { buildJsonDiff } from '../utils/diff'; export type CallRecord = { id: string; @@ -11,6 +11,7 @@ export type CallRecord = { cost: number; request?: unknown; response?: unknown; + compareResponse?: unknown; }; type Props = { @@ -55,6 +56,34 @@ function StatusIcon({ status }: { status: 'success' | 'error' }) { ); } +function ResponseDiff({ before, after }: { before: unknown; after: unknown }) { + const lines = buildJsonDiff(before, after); + + return ( +
+

Response diff

+
+        {lines.map((line, index) => {
+          const prefix =
+            line.type === 'added' ? '+ ' : line.type === 'removed' ? '- ' : '  ';
+
+          return (
+            
+              {prefix}
+              {line.text}
+              {'\n'}
+            
+          );
+        })}
+      
+
+ ); +} + export default function CallHistoryRow({ call, expanded, onToggleExpand }: Props) { return ( <> @@ -97,6 +126,9 @@ export default function CallHistoryRow({ call, expanded, onToggleExpand }: Props

Response

{JSON.stringify(call.response ?? {}, null, 2)}
+ {call.compareResponse !== undefined && ( + + )} )} From bd8562c2114383cadfbf2b38f97e8bf90854985d Mon Sep 17 00:00:00 2001 From: changliuchang777 <1551617642@qq.com> Date: Sun, 5 Jul 2026 23:45:14 +0800 Subject: [PATCH 3/3] test: cover response diff rendering --- src/components/CallHistoryRow.test.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/components/CallHistoryRow.test.tsx b/src/components/CallHistoryRow.test.tsx index 246795f..5146ecc 100644 --- a/src/components/CallHistoryRow.test.tsx +++ b/src/components/CallHistoryRow.test.tsx @@ -114,6 +114,25 @@ describe('CallHistoryRow', () => { expect(screen.queryByRole('region', { name: 'Call details' })).toBeNull(); }); + it('renders response diff lines when a comparison response is available', () => { + const callWithDiff: CallRecord = { + ...successCall, + compareResponse: { name: 'Alice', plan: 'Free' }, + response: { name: 'Alice', plan: 'Pro' }, + }; + + renderRow({ call: callWithDiff, expanded: true, onToggleExpand: () => {} }); + + expect(screen.getByLabelText('Response diff')).toBeTruthy(); + expect(screen.getByText(/-.*"plan": "Free"/)).toBeTruthy(); + expect(screen.getByText(/\+.*"plan": "Pro"/)).toBeTruthy(); + }); + + it('does not render response diff when no comparison response is available', () => { + renderRow({ call: successCall, expanded: true, onToggleExpand: () => {} }); + expect(screen.queryByLabelText('Response diff')).toBeNull(); + }); + // ── Data rendering ─────────────────────────────────────────────────────── it('displays the endpoint path', () => {