From c86ff8d1516ca8aae23661cb38fa16c4b12de115 Mon Sep 17 00:00:00 2001 From: "CTO (LegalAI)" Date: Thu, 9 Apr 2026 11:46:44 +0000 Subject: [PATCH] feat: Fliesstext-Import fuer Gesetze mit KI-Parsing Neuer /normen/import Bildschirm: Gesetzestext als Fliesstext einfuegen oder TXT-Datei hochladen, KI zerlegt automatisch in Paragraphen, Vorschau mit Bearbeitungsmoeglichkeit, dann Import ins Regelwerk. - POST /api/norms/parse: AI-gestuetztes Parsing von Gesetzestexten - /normen/import: Mehrstufiges Frontend (Eingabe -> Vorschau -> Import) - Link zum Fliesstext-Import auf der Normen-Uebersichtsseite Co-Authored-By: Paperclip --- src/app/(dashboard)/normen/import/page.tsx | 482 +++++++++++++++++++++ src/app/(dashboard)/normen/page.tsx | 20 +- src/app/api/norms/parse/route.ts | 147 +++++++ 3 files changed, 643 insertions(+), 6 deletions(-) create mode 100644 src/app/(dashboard)/normen/import/page.tsx create mode 100644 src/app/api/norms/parse/route.ts diff --git a/src/app/(dashboard)/normen/import/page.tsx b/src/app/(dashboard)/normen/import/page.tsx new file mode 100644 index 0000000..c0d0933 --- /dev/null +++ b/src/app/(dashboard)/normen/import/page.tsx @@ -0,0 +1,482 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; + +const NORM_TYPES = [ + { value: 'gesetz', label: 'Gesetz' }, + { value: 'tarifvertrag', label: 'Tarifvertrag' }, + { value: 'schiedsordnung', label: 'Schiedsordnung' }, + { value: 'verordnung', label: 'Verordnung' }, + { value: 'satzung', label: 'Satzung' }, +]; + +const SOURCE_RANKS = [ + { value: 'gesetz', label: 'Gesetz' }, + { value: 'tarif', label: 'Tarifvertrag' }, + { value: 'schiedsordnung', label: 'Schiedsordnung' }, + { value: 'praxis', label: 'Praxis' }, + { value: 'kommentar', label: 'Kommentar' }, +]; + +interface ParsedProvision { + paragraph: string; + title: string; + body: string; + validFrom: string; + included: boolean; +} + +type Step = 'input' | 'preview' | 'done'; + +export default function ImportNormPage() { + const router = useRouter(); + const [step, setStep] = useState('input'); + const [rawText, setRawText] = useState(''); + const [provisions, setProvisions] = useState([]); + const [parsing, setParsing] = useState(false); + const [importing, setImporting] = useState(false); + const [error, setError] = useState(''); + const [importResult, setImportResult] = useState<{ instrumentId: string; count: number } | null>(null); + + async function handleFileUpload(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + + if (file.type === 'text/plain' || file.name.endsWith('.txt')) { + const text = await file.text(); + setRawText(text); + } else { + setError('Nur TXT-Dateien werden aktuell unterstuetzt. Bitte fuegen Sie den Text manuell ein.'); + } + } + + async function handleParse(e: React.FormEvent) { + e.preventDefault(); + setError(''); + setParsing(true); + + try { + const res = await fetch('/api/norms/parse', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: rawText }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => null); + throw new Error(data?.error ?? 'Parsing fehlgeschlagen.'); + } + + const data = await res.json(); + const today = new Date().toISOString().split('T')[0]; + const parsed: ParsedProvision[] = data.provisions.map( + (p: { paragraph: string; title: string; body: string }) => ({ + ...p, + validFrom: today, + included: true, + }), + ); + + setProvisions(parsed); + setStep('preview'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten.'); + } finally { + setParsing(false); + } + } + + function updateProvision(index: number, field: keyof ParsedProvision, value: string | boolean) { + setProvisions((prev) => + prev.map((p, i) => (i === index ? { ...p, [field]: value } : p)), + ); + } + + async function handleImport(e: React.FormEvent) { + e.preventDefault(); + setError(''); + setImporting(true); + + const form = new FormData(e.currentTarget); + + const instrumentBody = { + type: form.get('type'), + sourceRank: form.get('sourceRank'), + abbreviation: form.get('abbreviation'), + fullTitle: form.get('fullTitle'), + enactedAt: form.get('enactedAt') || undefined, + issuingBody: form.get('issuingBody') || undefined, + }; + + const selectedProvisions = provisions + .filter((p) => p.included) + .map((p) => ({ + paragraph: p.paragraph, + title: p.title || undefined, + body: p.body, + validFrom: p.validFrom, + })); + + if (selectedProvisions.length === 0) { + setError('Bitte waehlen Sie mindestens einen Paragraphen aus.'); + setImporting(false); + return; + } + + try { + // Create instrument + const instrumentRes = await fetch('/api/norms', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(instrumentBody), + }); + + if (!instrumentRes.ok) { + const data = await instrumentRes.json().catch(() => null); + throw new Error(data?.error ?? 'Regelwerk konnte nicht angelegt werden.'); + } + + const instrumentData = await instrumentRes.json(); + const instrumentId = instrumentData.instrument.id; + + // Import provisions + const importRes = await fetch('/api/norms/import', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ instrumentId, provisions: selectedProvisions }), + }); + + if (!importRes.ok) { + const data = await importRes.json().catch(() => null); + throw new Error(data?.error ?? 'Import fehlgeschlagen.'); + } + + setImportResult({ instrumentId, count: selectedProvisions.length }); + setStep('done'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten.'); + } finally { + setImporting(false); + } + } + + const inputClass = + 'w-full rounded-lg border border-card-border px-3 py-2.5 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary'; + + return ( +
+
+ + Normen + + / + Fliesstext-Import +
+ +

Gesetzestext importieren

+

+ Fuegen Sie einen vollstaendigen Gesetzestext ein. Die KI zerlegt ihn automatisch in einzelne Paragraphen. +

+ + {error && ( +
+ {error} +
+ )} + + {/* Step 1: Input */} + {step === 'input' && ( +
+
+

Gesetzestext

+ +
+ + +
+ +
+ +