diff --git a/frontend/src/routes/search/InterpretationChipRow.svelte b/frontend/src/routes/search/InterpretationChipRow.svelte
new file mode 100644
index 00000000..8e9cdbc0
--- /dev/null
+++ b/frontend/src/routes/search/InterpretationChipRow.svelte
@@ -0,0 +1,133 @@
+
+
+
+ {#each chips as chip (chip.key)}
+ {#if chip.type === 'directional'}
+
+ {chip.from}
+ →
+ {chip.to}
+
+
+ {:else}
+
+ {chip.label}
+
+
+ {/if}
+ {/each}
+
+
+{#if showKeywordsNotApplied}
+ {m.smart_search_keywords_not_applied()}
+{/if}
diff --git a/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts b/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts
new file mode 100644
index 00000000..f3164389
--- /dev/null
+++ b/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts
@@ -0,0 +1,133 @@
+import { describe, expect, it, vi, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import InterpretationChipRow from './InterpretationChipRow.svelte';
+import type { components } from '$lib/generated/api';
+
+type NlQueryInterpretation = components['schemas']['NlQueryInterpretation'];
+type PersonHint = components['schemas']['PersonHint'];
+
+afterEach(() => cleanup());
+
+const makePerson = (id: string, displayName: string): PersonHint => ({ id, displayName });
+
+const makeInterpretation = (
+ overrides: Partial = {}
+): NlQueryInterpretation => ({
+ resolvedPersons: [],
+ ambiguousPersons: [],
+ keywords: [],
+ rawQuery: 'test',
+ keywordsApplied: true,
+ ...overrides
+});
+
+describe('InterpretationChipRow', () => {
+ it('renders type-prefixed labels for sender, date and keyword chips', async () => {
+ render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedPersons: [makePerson('p1', 'Walter Raddatz')],
+ dateFrom: '1914-01-01',
+ dateTo: '1918-12-31',
+ keywords: ['krieg']
+ }),
+ onRemoveChip: vi.fn()
+ });
+ await expect.element(page.getByText('Absender: Walter Raddatz')).toBeInTheDocument();
+ await expect.element(page.getByText('Zeitraum: 1914–1918')).toBeInTheDocument();
+ await expect.element(page.getByText('Stichwort: krieg')).toBeInTheDocument();
+ });
+
+ it('calls onRemoveChip with "sender" when the sender chip × is clicked', async () => {
+ const onRemoveChip = vi.fn();
+ render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedPersons: [makePerson('p1', 'Walter Raddatz')]
+ }),
+ onRemoveChip
+ });
+ await page.getByRole('button', { name: /Absender: Walter Raddatz/ }).click();
+ expect(onRemoveChip).toHaveBeenCalledWith('sender', undefined);
+ });
+
+ it('removes a chip from the DOM but keeps the rest when one × is clicked', async () => {
+ const { container } = render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedPersons: [makePerson('p1', 'Walter Raddatz')],
+ dateFrom: '1914-01-01',
+ dateTo: '1918-12-31',
+ keywords: ['krieg']
+ }),
+ onRemoveChip: vi.fn()
+ });
+ expect(container.querySelectorAll('[data-chip-type]')).toHaveLength(3);
+ await page.getByRole('button', { name: /Absender/ }).click();
+ await vi.waitFor(() => expect(container.querySelectorAll('[data-chip-type]')).toHaveLength(2));
+ });
+
+ it('renders a single directional chip with an arrow for a 2-name query', async () => {
+ const { container } = render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedPersons: [makePerson('p1', 'Walter Raddatz'), makePerson('p2', 'Emma Raddatz')]
+ }),
+ onRemoveChip: vi.fn()
+ });
+ expect(container.querySelectorAll('[data-chip-type="directional"]')).toHaveLength(1);
+ await expect.element(page.getByText(/→/)).toBeInTheDocument();
+ });
+
+ it('calls onRemoveChip with "directional" when the directional chip × is clicked', async () => {
+ const onRemoveChip = vi.fn();
+ render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedPersons: [makePerson('p1', 'Walter Raddatz'), makePerson('p2', 'Emma Raddatz')]
+ }),
+ onRemoveChip
+ });
+ await page.getByRole('button', { name: /Walter Raddatz/ }).click();
+ expect(onRemoveChip).toHaveBeenCalledWith('directional', undefined);
+ });
+
+ it('does not render keyword chips when keywordsApplied is false', async () => {
+ const { container } = render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ keywordsApplied: false,
+ keywords: ['krieg', 'brief']
+ }),
+ onRemoveChip: vi.fn()
+ });
+ expect(container.querySelectorAll('[data-chip-type="keyword"]')).toHaveLength(0);
+ });
+
+ it('renders no keyword chips when keywords is empty', async () => {
+ const { container } = render(InterpretationChipRow, {
+ interpretation: makeInterpretation({ keywordsApplied: true, keywords: [] }),
+ onRemoveChip: vi.fn()
+ });
+ expect(container.querySelectorAll('[data-chip-type="keyword"]')).toHaveLength(0);
+ });
+
+ it('renders exactly one keyword chip per keyword', async () => {
+ const { container } = render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ keywordsApplied: true,
+ keywords: ['krieg', 'brief', 'front']
+ }),
+ onRemoveChip: vi.fn()
+ });
+ expect(container.querySelectorAll('[data-chip-type="keyword"]')).toHaveLength(3);
+ });
+
+ it('keeps the × button in the DOM when a display name is 100 characters', async () => {
+ const longName = 'W'.repeat(100);
+ render(InterpretationChipRow, {
+ interpretation: makeInterpretation({
+ resolvedPersons: [makePerson('p1', longName)]
+ }),
+ onRemoveChip: vi.fn()
+ });
+ await expect
+ .element(page.getByRole('button', { name: new RegExp('Absender') }))
+ .toBeInTheDocument();
+ });
+});