feat: Erweitere Volltextsuche über volltext_clean und KI-Zusammenfassungen

Suche durchsucht jetzt auch den bereinigten Volltext und die Begründungen
aus KI-Bewertungen (typ=zusammenfassung). Frontend zeigt Treffer-Highlighting.

Fixes #1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-03-31 16:46:49 +02:00
parent aa9e5699f0
commit 14d4be5d67
2 changed files with 29 additions and 5 deletions

View File

@ -45,8 +45,16 @@ def list_vorlagen(
params.append(typ) params.append(typ)
if suche: if suche:
where_clauses.append("(v.betreff LIKE ? OR v.aktenzeichen LIKE ?)") where_clauses.append(
params.extend([f"%{suche}%", f"%{suche}%"]) "(v.betreff LIKE ? OR v.aktenzeichen LIKE ?"
" OR v.volltext_clean LIKE ?"
" OR v.id IN ("
" SELECT kb.vorlage_id FROM ki_bewertungen kb"
" WHERE kb.typ = 'zusammenfassung' AND kb.begruendung LIKE ?"
"))"
)
like = f"%{suche}%"
params.extend([like, like, like, like])
where_sql = ("WHERE " + " AND ".join(where_clauses)) if where_clauses else "" where_sql = ("WHERE " + " AND ".join(where_clauses)) if where_clauses else ""

View File

@ -5,6 +5,12 @@
import { formatDate } from '$lib/status'; import { formatDate } from '$lib/status';
let data: Paginated<VorlageKurz> | null = $state(null); let data: Paginated<VorlageKurz> | null = $state(null);
function highlight(text: string | null, query: string): string {
if (!text || !query) return text || '-';
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return text.replace(new RegExp(`(${escaped})`, 'gi'), '<mark class="bg-yellow-200 rounded px-0.5">$1</mark>');
}
let error: string | null = $state(null); let error: string | null = $state(null);
let loading = $state(false); let loading = $state(false);
@ -71,7 +77,7 @@
<div class="flex flex-wrap gap-3 items-end"> <div class="flex flex-wrap gap-3 items-end">
<div> <div>
<label for="suche" class="block text-xs font-medium text-gray-500 mb-1">Suche</label> <label for="suche" class="block text-xs font-medium text-gray-500 mb-1">Suche</label>
<input id="suche" type="text" bind:value={filterSuche} placeholder="Betreff oder Aktenzeichen..." <input id="suche" type="text" bind:value={filterSuche} placeholder="Volltextsuche..."
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500" class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500"
onkeydown={(e) => { if (e.key === 'Enter') applyFilters(); }} /> onkeydown={(e) => { if (e.key === 'Enter') applyFilters(); }} />
</div> </div>
@ -117,10 +123,20 @@
<tr class="hover:bg-gray-50 transition-colors cursor-pointer" onclick={() => goto(`/vorlagen/${v.id}`)}> <tr class="hover:bg-gray-50 transition-colors cursor-pointer" onclick={() => goto(`/vorlagen/${v.id}`)}>
<td class="px-4 py-3"> <td class="px-4 py-3">
<a href="/vorlagen/{v.id}" class="font-mono text-sm font-medium text-green-700 hover:underline"> <a href="/vorlagen/{v.id}" class="font-mono text-sm font-medium text-green-700 hover:underline">
{#if filterSuche}
{@html highlight(v.aktenzeichen || `#${v.id}`, filterSuche)}
{:else}
{v.aktenzeichen || `#${v.id}`} {v.aktenzeichen || `#${v.id}`}
{/if}
</a> </a>
</td> </td>
<td class="px-4 py-3 text-sm text-gray-700 max-w-lg truncate">{v.betreff || '-'}</td> <td class="px-4 py-3 text-sm text-gray-700 max-w-lg truncate">
{#if filterSuche}
{@html highlight(v.betreff, filterSuche)}
{:else}
{v.betreff || '-'}
{/if}
</td>
<td class="px-4 py-3 text-sm text-gray-600 capitalize">{v.typ || '-'}</td> <td class="px-4 py-3 text-sm text-gray-600 capitalize">{v.typ || '-'}</td>
<td class="px-4 py-3 text-sm text-gray-500">{formatDate(v.datum_eingang)}</td> <td class="px-4 py-3 text-sm text-gray-500">{formatDate(v.datum_eingang)}</td>
</tr> </tr>