import { Box, Typography, Button, Paper, TextField, FormControlLabel, Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress } from "@mui/material"; import { useState, useEffect } from "react"; import type { Kennzahl } from "../types/kpi"; import { typeDisplayMapping } from "../types/kpi"; import Snackbar from "@mui/material/Snackbar"; import MuiAlert from "@mui/material/Alert"; interface KPIFormProps { mode: 'add' | 'edit'; initialData?: Kennzahl | null; onSave: (data: Partial) => Promise; onCancel: () => void; loading?: boolean; resetTrigger?: number; } const emptyKPI: Partial = { name: '', mandatory: false, type: 'string', active: true, examples: [{ sentence: '', value: '' }], }; export function KPIForm({ mode, initialData, onSave, onCancel, loading = false, resetTrigger }: KPIFormProps) { const [formData, setFormData] = useState>(emptyKPI); const [isSaving, setIsSaving] = useState(false); const [snackbarOpen, setSnackbarOpen] = useState(false); const [snackbarMessage, setSnackbarMessage] = useState(""); const [snackbarSeverity, setSnackbarSeverity] = useState<'success' | 'error' | 'info'>("success"); useEffect(() => { if (mode === 'edit' && initialData) { setFormData(initialData); } else if (mode === 'add') { setFormData(emptyKPI); } }, [mode, initialData]); useEffect(() => { if (mode === 'add') { setFormData(emptyKPI); } }, [resetTrigger]); const handleSave = async () => { if (!formData.name?.trim()) { setSnackbarMessage("Name ist erforderlich"); setSnackbarSeverity("error"); setSnackbarOpen(true); return; } if (!formData.examples || formData.examples.length === 0) { setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich"); setSnackbarSeverity("error"); setSnackbarOpen(true); return; } for (const ex of formData.examples) { if (!ex.sentence?.trim() || !ex.value?.trim()) { setSnackbarMessage('Alle Beispielsätze müssen vollständig sein.'); setSnackbarSeverity("error"); setSnackbarOpen(true); return; } } setIsSaving(true); try { const spacyEntries = generateSpacyEntries(formData); // Für jeden einzelnen Beispielsatz: for (const entry of spacyEntries) { // im localStorage speichern (zum Debuggen oder Vorschau) const stored = localStorage.getItem("spacyData"); const existingData = stored ? JSON.parse(stored) : []; const updated = [...existingData, entry]; localStorage.setItem("spacyData", JSON.stringify(updated)); // POST Request an das Flask-Backend const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(entry) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "Fehler beim Aufruf von append-training-entry"); } console.log("SpaCy-Eintrag gespeichert:", data); } // Dann in die DB speichern await onSave({ name: formData.name!, mandatory: formData.mandatory ?? false, type: formData.type || 'string', position: formData.position ?? 0, active: formData.active ?? true, examples: formData.examples ?? [], is_trained: false, }); // Formular zurücksetzen: setFormData(emptyKPI); setSnackbarMessage("Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder weitere Kennzahlen hinzufügen."); setSnackbarSeverity("success"); setSnackbarOpen(true); } catch (e: any) { // Prüfe auf 409-Fehler if (e?.message?.includes("409") || e?.response?.status === 409) { setSnackbarMessage("Diese Kennzahl existiert bereits. Sie können sie unter -Konfiguration- bearbeiten."); setSnackbarSeverity("info"); setSnackbarOpen(true); } else { setSnackbarMessage(e.message || "Fehler beim Speichern."); setSnackbarSeverity("error"); setSnackbarOpen(true); } console.error(e); } finally { setIsSaving(false); } }; const handleCancel = () => { setFormData(emptyKPI); onCancel(); }; const updateField = (field: keyof Kennzahl, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); }; const updateExample = (index: number, field: 'sentence' | 'value', value: string) => { const newExamples = [...(formData.examples || [])]; newExamples[index][field] = value; updateField('examples', newExamples); }; const addExample = () => { const newExamples = [...(formData.examples || []), { sentence: '', value: '' }]; updateField('examples', newExamples); }; const removeExample = (index: number) => { const newExamples = [...(formData.examples || [])]; newExamples.splice(index, 1); updateField('examples', newExamples); }; if (loading) { return ( {mode === 'edit' ? 'Lade KPI Details...' : 'Laden...'} ); } return ( <> Kennzahl updateField('name', e.target.value)} sx={{ mb: 2 }} required error={!formData.name?.trim()} helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''} /> Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type} Typ {mode === 'add' && ( <> updateField('active', e.target.checked)} sx={{ color: '#383838' }} /> } label="Aktiv" /> Die Kennzahl ist aktiv und wird angezeigt updateField('mandatory', e.target.checked)} sx={{ color: '#383838' }} /> } label="Erforderlich" /> Die Kennzahl erlaubt keine leeren Werte )} {/* Hinweistext vor Beispielsätzen */} Hinweis zur Trainingsqualität Damit das System neue Kennzahlen zuverlässig erkennen kann, empfehlen wir mindestens 5 Beispielsätze zu erstellen – je mehr, desto besser. Wichtig: Neue Kennzahlen werden erst in PDF-Dokumenten erkannt, wenn Sie den Button "Neu trainieren" auf der Konfigurationsseite ausführen. Tipp: Sie können jederzeit weitere Beispielsätze hinzufügen oder vorhandene in der Kennzahlenverwaltung bearbeiten. Beispielsätze {(formData.examples || []).map((ex, idx) => ( updateExample(idx, 'sentence', e.target.value)} required sx={{ mb: 1 }} /> updateExample(idx, 'value', e.target.value)} required /> {(formData.examples?.length || 0) > 1 && ( )} ))} setSnackbarOpen(false)} anchorOrigin={{ vertical: 'top', horizontal: 'center' }} > setSnackbarOpen(false)} severity={snackbarSeverity} sx={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }} > {snackbarMessage} ); } function generateSpacyEntries(formData: Partial) { const label = formData.name?.trim().toUpperCase() || ""; return (formData.examples || []).map(({ sentence, value }) => { const trimmedValue = value.trim(); const start = sentence.indexOf(trimmedValue); if (start === -1) { throw new Error(`"${trimmedValue}" nicht gefunden in Satz: "${sentence}"`); } return { text: sentence, entities: [[start, start + trimmedValue.length, label]] }; }); }