diff --git a/frontend/src/routes/documents/+page.svelte b/frontend/src/routes/documents/+page.svelte index ad3744f5..00abe3cd 100644 --- a/frontend/src/routes/documents/+page.svelte +++ b/frontend/src/routes/documents/+page.svelte @@ -8,23 +8,9 @@ import DocumentList from '../DocumentList.svelte'; import Pagination from '$lib/shared/primitives/Pagination.svelte'; import BulkSelectionBar from '$lib/document/BulkSelectionBar.svelte'; import TimelineDensityFilter from '$lib/document/TimelineDensityFilter.svelte'; -import SmartSearchStatus from '../search/SmartSearchStatus.svelte'; -import InterpretationChipRow from '../search/InterpretationChipRow.svelte'; -import type { ChipType } from '../search/chip-types.js'; -import { buildThemeRemovalUrl } from './theme-chip-removal.js'; -import DisambiguationPicker from '../search/DisambiguationPicker.svelte'; import { bulkSelectionStore } from '$lib/document/bulkSelection.svelte'; import { getErrorMessage, parseBackendError } from '$lib/shared/errors'; -import { csrfFetch } from '$lib/shared/cookies'; import * as m from '$lib/paraglide/messages.js'; -import { getLocale } from '$lib/paraglide/runtime'; -import type { components } from '$lib/generated/api'; - -type NlQueryInterpretation = components['schemas']['NlQueryInterpretation']; -type NlSearchResponse = components['schemas']['NlSearchResponse']; -type DocumentSearchResult = components['schemas']['DocumentSearchResult']; -type PersonHint = components['schemas']['PersonHint']; -type SmartSearchErrorCode = 'SMART_SEARCH_UNAVAILABLE' | 'SMART_SEARCH_RATE_LIMITED'; let { data } = $props(); @@ -48,27 +34,6 @@ let tagQ = $state(untrack(() => data.tagQ || '')); let tagOperator = $state<'AND' | 'OR'>(untrack(() => (data.tagOp as 'AND' | 'OR') || 'AND')); let undated = $state(untrack(() => data.undated ?? false)); -// Smart (NL) search — UI-local state, resets on real page navigation (away + back). -let smartMode = $state(false); -let nlSubmitted = $state(false); -let nlLoading = $state(false); -let nlError = $state(null); -let nlInterpretation = $state(null); -let nlResult = $state(null); - -const showNlView = $derived(smartMode && nlSubmitted); -const nlHasResults = $derived((nlResult?.items.length ?? 0) > 0); -const ambiguousPersons = $derived(nlInterpretation?.ambiguousPersons ?? []); -const nlIsAmbiguous = $derived(ambiguousPersons.length > 0); -// A 1-item picker is always a "did you mean …?" suggestion (a single direct match auto-selects -// and never reaches the picker); ≥2 keeps the "choose a person" framing and the action cue. -const disambiguationHeading = $derived( - ambiguousPersons.length === 1 - ? m.search_disambiguation_did_you_mean({ name: ambiguousPersons[0].displayName }) - : m.search_disambiguation_heading() -); -const showDisambiguationCue = $derived(ambiguousPersons.length >= 2); - function hasAdvancedFilters() { return ( (data.tags?.length ?? 0) > 0 || @@ -199,124 +164,6 @@ function handleImmediateSearch() { triggerSearchKeepZoom(); } -function resetNlState() { - nlSubmitted = false; - nlLoading = false; - nlError = null; - nlInterpretation = null; - nlResult = null; -} - -/** Toggling the mode (either direction) always clears any prior NL interpretation. */ -function onModeToggle() { - resetNlState(); -} - -/** Submit the natural-language query to the server-side parser. */ -async function runSmartSearch() { - const query = q.trim(); - if (query.length < 3) return; - nlSubmitted = true; - nlLoading = true; - nlError = null; - nlInterpretation = null; - nlResult = null; - try { - const res = await csrfFetch('/api/search/nl', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query, lang: getLocale() }) - }); - if (!res.ok) { - const backend = await parseBackendError(res); - nlError = - backend?.code === 'SMART_SEARCH_RATE_LIMITED' - ? 'SMART_SEARCH_RATE_LIMITED' - : 'SMART_SEARCH_UNAVAILABLE'; - return; - } - const body: NlSearchResponse = await res.json(); - nlInterpretation = body.interpretation; - nlResult = body.result; - } catch { - nlError = 'SMART_SEARCH_UNAVAILABLE'; - } finally { - nlLoading = false; - } -} - -/** Option A empty/error fallback: drop NL mode, keep the raw query, run a keyword search. */ -function switchToKeywordMode() { - resetNlState(); - smartMode = false; - handleImmediateSearch(); -} - -/** Applies a resolved param set to the keyword filters and re-runs via GET. */ -function applyResolvedAndSearch(p: { - senderId: string; - receiverId: string; - from: string; - to: string; - q: string; -}) { - resetNlState(); - smartMode = false; - senderId = p.senderId; - receiverId = p.receiverId; - from = p.from; - to = p.to; - q = p.q; - handleImmediateSearch(); -} - -function paramsFromInterpretation(interp: NlQueryInterpretation) { - const resolved = interp.resolvedPersons; - return { - senderId: resolved.length >= 1 ? resolved[0].id : '', - receiverId: resolved.length >= 2 ? resolved[1].id : '', - from: interp.dateFrom ?? '', - to: interp.dateTo ?? '', - q: interp.keywordsApplied ? interp.keywords.join(' ') : '' - }; -} - -function removeChip(type: ChipType, value?: string) { - if (!nlInterpretation) return; - const p = paramsFromInterpretation(nlInterpretation); - if (type === 'sender') { - p.senderId = ''; - } else if (type === 'directional') { - p.senderId = ''; - p.receiverId = ''; - } else if (type === 'date') { - p.from = ''; - p.to = ''; - } else if (type === 'keyword' && value) { - const remaining = nlInterpretation.keywords.filter((keyword) => keyword !== value); - p.q = remaining.join(' '); - } else if (type === 'theme' && value) { - const url = buildThemeRemovalUrl(nlInterpretation, value); - resetNlState(); - goto(url, { keepFocus: true, noScroll: true }); - return; - } - applyResolvedAndSearch(p); -} - -/** Single-select disambiguation: resolved person becomes sender, chosen becomes receiver. */ -function selectDisambiguated(person: PersonHint) { - if (!nlInterpretation) return; - const resolved = nlInterpretation.resolvedPersons; - applyResolvedAndSearch({ - senderId: resolved.length >= 1 ? resolved[0].id : person.id, - receiverId: resolved.length >= 1 ? person.id : '', - from: nlInterpretation.dateFrom ?? '', - to: nlInterpretation.dateTo ?? '', - q: nlInterpretation.keywordsApplied ? nlInterpretation.keywords.join(' ') : '' - }); -} - // Trigger search reactively when the tag list changes. let prevTagStr = untrack(() => tagNames.map((t) => t.name).join(',')); $effect(() => { @@ -421,7 +268,6 @@ $effect(() => { bind:tagQ={tagQ} bind:tagOperator={tagOperator} bind:undated={undated} - bind:smartMode={smartMode} undatedCount={data.undatedCount ?? 0} initialSenderName={initialSenderName} initialReceiverName={initialReceiverName} @@ -429,71 +275,20 @@ $effect(() => { isLoading={navigating.to !== null} onSearch={handleTextSearch} onSearchImmediate={handleImmediateSearch} - onSmartSearch={runSmartSearch} - onModeToggle={onModeToggle} onfocus={() => (qFocused = true)} onblur={() => (qFocused = false)} /> - {#if showNlView} - -
- {#if nlLoading} - - {:else if nlError} - - {:else if nlInterpretation} - {#key nlInterpretation} -
- {#if nlIsAmbiguous} - - {:else} - - {/if} -
- - {#if !nlIsAmbiguous} - {#if nlHasResults} -

- {m.docs_result_count({ count: nlResult?.totalElements ?? 0 })} -

- - {:else} -
-

{m.search_empty_nl()}

- -
- {/if} - {/if} - {/key} - {/if} -
- {:else} - -
-

- {#if data.totalElements > 0}{m.docs_result_count({ count: data.totalElements })}{/if} -

- {#if data.canWrite} -
-
- {#if data.totalElements > 0} - - {/if} - +

+ {#if data.totalElements > 0}{m.docs_result_count({ count: data.totalElements })}{/if} +

+ {#if data.canWrite} +
+ - {#if editAllError} - + {m.bulk_edit_all_x({ count: data.totalElements })} + {/if} + + + {m.docs_btn_new()} +
- {/if} -
+ {#if editAllError} + + {/if} +
+ {/if} +
- + - - {/if} +