fix host url and prepare for deployment
parent
3d6458ffb0
commit
810827e0bb
|
|
@ -1,460 +1,527 @@
|
|||
import {
|
||||
Box, Typography, Button, Paper, TextField, FormControlLabel,
|
||||
Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useState, useEffect } from "react";
|
||||
import MuiAlert from "@mui/material/Alert";
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import { useEffect, useState } 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";
|
||||
import { API_HOST } from "../util/api";
|
||||
|
||||
|
||||
interface KPIFormProps {
|
||||
mode: 'add' | 'edit';
|
||||
initialData?: Kennzahl | null;
|
||||
onSave: (data: Partial<Kennzahl>) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
loading?: boolean;
|
||||
resetTrigger?: number;
|
||||
mode: "add" | "edit";
|
||||
initialData?: Kennzahl | null;
|
||||
onSave: (data: Partial<Kennzahl>) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
loading?: boolean;
|
||||
resetTrigger?: number;
|
||||
}
|
||||
|
||||
const emptyKPI: Partial<Kennzahl> = {
|
||||
name: '',
|
||||
mandatory: false,
|
||||
type: 'string',
|
||||
active: true,
|
||||
examples: [{ sentence: '', value: '' }],
|
||||
name: "",
|
||||
mandatory: false,
|
||||
type: "string",
|
||||
active: true,
|
||||
examples: [{ sentence: "", value: "" }],
|
||||
};
|
||||
|
||||
export function KPIForm({
|
||||
mode,
|
||||
initialData,
|
||||
onSave,
|
||||
onCancel,
|
||||
loading = false,
|
||||
}: KPIFormProps) {
|
||||
const [formData, setFormData] = useState<Partial<Kennzahl>>(emptyKPI);
|
||||
const [originalExamples, setOriginalExamples] = useState<
|
||||
Array<{ sentence: string; value: string }>
|
||||
>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("");
|
||||
const [snackbarSeverity, setSnackbarSeverity] = useState<
|
||||
"success" | "error" | "info"
|
||||
>("success");
|
||||
|
||||
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false, resetTrigger }: KPIFormProps) {
|
||||
const [formData, setFormData] = useState<Partial<Kennzahl>>(emptyKPI);
|
||||
const [originalExamples, setOriginalExamples] = useState<Array<{ sentence: string; value: string }>>([]);
|
||||
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) {
|
||||
setOriginalExamples(initialData.examples || []);
|
||||
setFormData({
|
||||
...initialData,
|
||||
examples: [{ sentence: "", value: "" }],
|
||||
});
|
||||
} else if (mode === "add") {
|
||||
setOriginalExamples([]);
|
||||
setFormData(emptyKPI);
|
||||
}
|
||||
}, [mode, initialData]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === 'edit' && initialData) {
|
||||
setOriginalExamples(initialData.examples || []);
|
||||
setFormData({
|
||||
...initialData,
|
||||
examples: [{ sentence: '', value: '' }]
|
||||
});
|
||||
} else if (mode === 'add') {
|
||||
setOriginalExamples([]);
|
||||
setFormData(emptyKPI);
|
||||
}
|
||||
}, [mode, initialData]);
|
||||
const handleSave = async () => {
|
||||
if (!formData.name?.trim()) {
|
||||
setSnackbarMessage("Name ist erforderlich");
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === 'add') {
|
||||
setOriginalExamples([]);
|
||||
setFormData(emptyKPI);
|
||||
}
|
||||
}, [resetTrigger]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.examples || formData.examples.length === 0) {
|
||||
setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich");
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const newExamples = formData.examples.filter(
|
||||
(ex) => ex.sentence?.trim() && ex.value?.trim(),
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formData.name?.trim()) {
|
||||
setSnackbarMessage("Name ist erforderlich");
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
if (newExamples.length === 0) {
|
||||
setSnackbarMessage(
|
||||
"Mindestens ein vollständiger Beispielsatz ist erforderlich.",
|
||||
);
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
for (const ex of newExamples) {
|
||||
if (!ex.sentence?.trim() || !ex.value?.trim()) {
|
||||
setSnackbarMessage("Alle Beispielsätze müssen vollständig sein.");
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!formData.examples || formData.examples.length === 0) {
|
||||
setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich");
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const spacyEntries = generateSpacyEntries({
|
||||
...formData,
|
||||
examples: newExamples,
|
||||
});
|
||||
|
||||
const newExamples = formData.examples.filter(ex => ex.sentence?.trim() && ex.value?.trim());
|
||||
// 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));
|
||||
|
||||
if (newExamples.length === 0) {
|
||||
setSnackbarMessage('Mindestens ein vollständiger Beispielsatz ist erforderlich.');
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
// POST Request an das Flask-Backend
|
||||
const response = await fetch(
|
||||
`${API_HOST}/api/spacy/append-training-entry`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(entry),
|
||||
},
|
||||
);
|
||||
|
||||
for (const ex of newExamples) {
|
||||
if (!ex.sentence?.trim() || !ex.value?.trim()) {
|
||||
setSnackbarMessage('Alle Beispielsätze müssen vollständig sein.');
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const spacyEntries = generateSpacyEntries({ ...formData, examples: newExamples });
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.error || "Fehler beim Aufruf von append-training-entry",
|
||||
);
|
||||
}
|
||||
|
||||
// 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));
|
||||
console.log("SpaCy-Eintrag gespeichert:", data);
|
||||
}
|
||||
|
||||
// POST Request an das Flask-Backend
|
||||
const response = await fetch(`${API_HOST}/api/spacy/append-training-entry/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(entry)
|
||||
});
|
||||
const allExamples =
|
||||
mode === "edit" ? [...originalExamples, ...newExamples] : newExamples;
|
||||
|
||||
const data = await response.json();
|
||||
// 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: allExamples,
|
||||
is_trained: false,
|
||||
});
|
||||
// Formular zurücksetzen:
|
||||
if (mode === "add") {
|
||||
setFormData(emptyKPI);
|
||||
} else {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
examples: [{ sentence: "", value: "" }],
|
||||
}));
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || "Fehler beim Aufruf von append-training-entry");
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
console.log("SpaCy-Eintrag gespeichert:", data);
|
||||
}
|
||||
const handleCancel = () => {
|
||||
setFormData(emptyKPI);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const allExamples = mode === 'edit'
|
||||
? [...originalExamples, ...newExamples]
|
||||
: newExamples;
|
||||
const updateField = (field: keyof Kennzahl, value: any) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// 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: allExamples,
|
||||
is_trained: false,
|
||||
});
|
||||
// Formular zurücksetzen:
|
||||
if (mode === 'add') {
|
||||
setFormData(emptyKPI);
|
||||
} else {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
examples: [{ sentence: '', 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);
|
||||
};
|
||||
|
||||
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 removeExample = (index: number) => {
|
||||
const newExamples = [...(formData.examples || [])];
|
||||
newExamples.splice(index, 1);
|
||||
updateField("examples", newExamples);
|
||||
};
|
||||
|
||||
};
|
||||
if (loading) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
minHeight="400px"
|
||||
flexDirection="column"
|
||||
>
|
||||
<CircularProgress sx={{ color: "#383838", mb: 2 }} />
|
||||
<Typography>
|
||||
{mode === "edit" ? "Lade KPI Details..." : "Laden..."}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
width: "90%",
|
||||
maxWidth: 800,
|
||||
p: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
<Box mb={4}>
|
||||
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||
Kennzahl
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Name *"
|
||||
placeholder="z.B. IRR"
|
||||
value={formData.name || ""}
|
||||
onChange={(e) => updateField("name", e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
required
|
||||
error={!formData.name?.trim()}
|
||||
helperText={!formData.name?.trim() ? "Name ist erforderlich" : ""}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
const handleCancel = () => {
|
||||
setFormData(emptyKPI);
|
||||
onCancel();
|
||||
};
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
const updateField = (field: keyof Kennzahl, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
<Box mb={4}>
|
||||
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||
Format:{" "}
|
||||
{typeDisplayMapping[
|
||||
formData.type as keyof typeof typeDisplayMapping
|
||||
] || formData.type}
|
||||
</Typography>
|
||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||
<InputLabel>Typ</InputLabel>
|
||||
<Select
|
||||
value={formData.type || "string"}
|
||||
label="Typ"
|
||||
onChange={(e) => updateField("type", e.target.value)}
|
||||
>
|
||||
<MenuItem value="string">Text</MenuItem>
|
||||
<MenuItem value="number">Zahl</MenuItem>
|
||||
<MenuItem value="date">Datum</MenuItem>
|
||||
<MenuItem value="boolean">Ja/Nein</MenuItem>
|
||||
<MenuItem value="array">Liste (mehrfach)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
const updateExample = (index: number, field: 'sentence' | 'value', value: string) => {
|
||||
const newExamples = [...(formData.examples || [])];
|
||||
newExamples[index][field] = value;
|
||||
updateField('examples', newExamples);
|
||||
};
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
const addExample = () => {
|
||||
const newExamples = [...(formData.examples || []), { sentence: '', value: '' }];
|
||||
updateField('examples', newExamples);
|
||||
};
|
||||
<Box mb={4}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={formData.active !== false}
|
||||
onChange={(e) => updateField("active", e.target.checked)}
|
||||
sx={{
|
||||
color: "#666666",
|
||||
"&.Mui-checked": {
|
||||
color: "#333333",
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(102, 102, 102, 0.04)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Aktiv"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" ml={4}>
|
||||
Die Kennzahl ist aktiv und wird angezeigt
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box mt={3}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={formData.mandatory || false}
|
||||
onChange={(e) => updateField("mandatory", e.target.checked)}
|
||||
sx={{
|
||||
color: "#666666",
|
||||
"&.Mui-checked": {
|
||||
color: "#333333",
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(102, 102, 102, 0.04)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Erforderlich"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" ml={4}>
|
||||
Die Kennzahl erlaubt keine leeren Werte
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
const removeExample = (index: number) => {
|
||||
const newExamples = [...(formData.examples || [])];
|
||||
newExamples.splice(index, 1);
|
||||
updateField('examples', newExamples);
|
||||
};
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* Hinweistext wie viele Beispielsätzen vorhanden sind*/}
|
||||
{mode === "edit" && originalExamples.length > 0 && (
|
||||
<Box
|
||||
mb={2}
|
||||
p={2}
|
||||
sx={{
|
||||
backgroundColor: "#e3f2fd",
|
||||
border: "1px solid #90caf9",
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
|
||||
Vorhandene Beispielsätze: {originalExamples.length}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Diese Kennzahl hat bereits {originalExamples.length}{" "}
|
||||
Beispielsätze. Neue Beispielsätze werden zu den vorhandenen
|
||||
hinzugefügt.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
minHeight="400px"
|
||||
flexDirection="column"
|
||||
>
|
||||
<CircularProgress sx={{ color: '#383838', mb: 2 }} />
|
||||
<Typography>
|
||||
{mode === 'edit' ? 'Lade KPI Details...' : 'Laden...'}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
{/* Hinweistext vor Beispielsätzen */}
|
||||
<Box
|
||||
mb={2}
|
||||
p={2}
|
||||
sx={{
|
||||
backgroundColor: "#fff8e1",
|
||||
border: "1px solid #ffe082",
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
|
||||
Hinweis zur Trainingsqualität
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Damit das System neue Kennzahlen zuverlässig erkennen kann,
|
||||
empfehlen wir <strong>mindestens 5 Beispielsätze</strong> zu
|
||||
erstellen – je mehr, desto besser.
|
||||
</Typography>
|
||||
<Typography variant="body2" mt={1}>
|
||||
<strong>Wichtig:</strong> Neue Kennzahlen werden erst in
|
||||
PDF-Dokumenten erkannt, wenn Sie den Button{" "}
|
||||
<em>"Neu trainieren"</em> auf der Konfigurationsseite ausführen.
|
||||
</Typography>
|
||||
<Typography variant="body2" mt={1}>
|
||||
<strong>Tipp:</strong> Sie können jederzeit weitere Beispielsätze
|
||||
hinzufügen oder vorhandene in der Kennzahlenverwaltung bearbeiten.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
width: "90%",
|
||||
maxWidth: 800,
|
||||
p: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: "white"
|
||||
}}
|
||||
>
|
||||
<Box mb={4}>
|
||||
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||
Kennzahl
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Name *"
|
||||
placeholder="z.B. IRR"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => updateField('name', e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
required
|
||||
error={!formData.name?.trim()}
|
||||
helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''}
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={4}>
|
||||
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||
Beispielsätze
|
||||
</Typography>
|
||||
{(formData.examples || []).map((ex, idx) => (
|
||||
<Box
|
||||
key={idx}
|
||||
sx={{ mb: 2, border: "1px solid #ccc", p: 2, borderRadius: 1 }}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
label={`Beispielsatz ${idx + 1}`}
|
||||
placeholder="z.B. Die IRR beträgt 7,8 %"
|
||||
value={ex.sentence}
|
||||
onChange={(e) => updateExample(idx, "sentence", e.target.value)}
|
||||
required
|
||||
sx={{ mb: 1 }}
|
||||
/>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Bezeichneter Wert im Satz"
|
||||
placeholder="z.B. 7,8 %"
|
||||
value={ex.value}
|
||||
onChange={(e) => updateExample(idx, "value", e.target.value)}
|
||||
required
|
||||
/>
|
||||
{(formData.examples?.length || 0) > 1 && (
|
||||
<Button
|
||||
onClick={() => removeExample(idx)}
|
||||
sx={{ mt: 1 }}
|
||||
color="error"
|
||||
>
|
||||
Entfernen
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Box mb={4}>
|
||||
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||
Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type}
|
||||
</Typography>
|
||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||
<InputLabel>Typ</InputLabel>
|
||||
<Select
|
||||
value={formData.type || 'string'}
|
||||
label="Typ"
|
||||
onChange={(e) => updateField('type', e.target.value)}
|
||||
>
|
||||
<MenuItem value="string">Text</MenuItem>
|
||||
<MenuItem value="number">Zahl</MenuItem>
|
||||
<MenuItem value="date">Datum</MenuItem>
|
||||
<MenuItem value="boolean">Ja/Nein</MenuItem>
|
||||
<MenuItem value="array">Liste (mehrfach)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Button variant="outlined" onClick={addExample}>
|
||||
+ Beispielsatz hinzufügen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
<Box mb={4}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={formData.active !== false}
|
||||
onChange={(e) => updateField('active', e.target.checked)}
|
||||
sx={{
|
||||
color: '#666666',
|
||||
'&.Mui-checked': {
|
||||
color: '#333333',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(102, 102, 102, 0.04)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Aktiv"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" ml={4}>
|
||||
Die Kennzahl ist aktiv und wird angezeigt
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box mt={3}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={formData.mandatory || false}
|
||||
onChange={(e) => updateField('mandatory', e.target.checked)}
|
||||
sx={{
|
||||
color: '#666666',
|
||||
'&.Mui-checked': {
|
||||
color: '#333333',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(102, 102, 102, 0.04)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Erforderlich"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" ml={4}>
|
||||
Die Kennzahl erlaubt keine leeren Werte
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* Hinweistext wie viele Beispielsätzen vorhanden sind*/}
|
||||
{mode === 'edit' && originalExamples.length > 0 && (
|
||||
<Box mb={2} p={2} sx={{ backgroundColor: '#e3f2fd', border: '1px solid #90caf9', borderRadius: 2 }}>
|
||||
<Typography variant="body1" sx={{ fontWeight: 'bold', mb: 1 }}>
|
||||
Vorhandene Beispielsätze: {originalExamples.length}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Diese Kennzahl hat bereits {originalExamples.length} Beispielsätze. Neue Beispielsätze werden zu den vorhandenen hinzugefügt.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Hinweistext vor Beispielsätzen */}
|
||||
<Box mb={2} p={2} sx={{ backgroundColor: '#fff8e1', border: '1px solid #ffe082', borderRadius: 2 }}>
|
||||
<Typography variant="body1" sx={{ fontWeight: 'bold', mb: 1 }}>
|
||||
Hinweis zur Trainingsqualität
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Damit das System neue Kennzahlen zuverlässig erkennen kann, empfehlen wir <strong>mindestens 5 Beispielsätze</strong> zu erstellen – je mehr, desto besser.
|
||||
</Typography>
|
||||
<Typography variant="body2" mt={1}>
|
||||
<strong>Wichtig:</strong> Neue Kennzahlen werden erst in PDF-Dokumenten erkannt, wenn Sie den Button <em>"Neu trainieren"</em> auf der Konfigurationsseite ausführen.
|
||||
</Typography>
|
||||
<Typography variant="body2" mt={1}>
|
||||
<strong>Tipp:</strong> Sie können jederzeit weitere Beispielsätze hinzufügen oder vorhandene in der Kennzahlenverwaltung bearbeiten.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box mb={4}>
|
||||
|
||||
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||
Beispielsätze
|
||||
</Typography>
|
||||
{(formData.examples || []).map((ex, idx) => (
|
||||
<Box key={idx} sx={{ mb: 2, border: '1px solid #ccc', p: 2, borderRadius: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
label={`Beispielsatz ${idx + 1}`}
|
||||
placeholder="z.B. Die IRR beträgt 7,8 %"
|
||||
value={ex.sentence}
|
||||
onChange={(e) => updateExample(idx, 'sentence', e.target.value)}
|
||||
required
|
||||
sx={{ mb: 1 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Bezeichneter Wert im Satz"
|
||||
placeholder="z.B. 7,8 %"
|
||||
value={ex.value}
|
||||
onChange={(e) => updateExample(idx, 'value', e.target.value)}
|
||||
required
|
||||
/>
|
||||
{(formData.examples?.length || 0) > 1 && (
|
||||
<Button onClick={() => removeExample(idx)} sx={{ mt: 1 }} color="error">
|
||||
Entfernen
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Button variant="outlined" onClick={addExample}>
|
||||
+ Beispielsatz hinzufügen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleCancel}
|
||||
disabled={isSaving}
|
||||
sx={{
|
||||
borderColor: "#383838",
|
||||
color: "#383838",
|
||||
"&:hover": { borderColor: "#2e2e2e", backgroundColor: "#f5f5f5" }
|
||||
}}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !formData.name?.trim()}
|
||||
sx={{
|
||||
backgroundColor: "#383838",
|
||||
"&:hover": { backgroundColor: "#2e2e2e" },
|
||||
}}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
|
||||
{mode === 'add' ? 'Hinzufügen...' : 'Speichern...'}
|
||||
</>
|
||||
) : (
|
||||
mode === 'add' ? 'Hinzufügen' : 'Speichern'
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Snackbar
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={5000}
|
||||
onClose={() => setSnackbarOpen(false)}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
>
|
||||
<MuiAlert
|
||||
elevation={6}
|
||||
variant="filled"
|
||||
onClose={() => setSnackbarOpen(false)}
|
||||
severity={snackbarSeverity}
|
||||
sx={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
|
||||
>
|
||||
<span>{snackbarMessage}</span>
|
||||
<Button color="inherit" size="small" onClick={() => setSnackbarOpen(false)}>
|
||||
OK
|
||||
</Button>
|
||||
</MuiAlert>
|
||||
|
||||
</Snackbar>
|
||||
</>
|
||||
);
|
||||
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleCancel}
|
||||
disabled={isSaving}
|
||||
sx={{
|
||||
borderColor: "#383838",
|
||||
color: "#383838",
|
||||
"&:hover": { borderColor: "#2e2e2e", backgroundColor: "#f5f5f5" },
|
||||
}}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !formData.name?.trim()}
|
||||
sx={{
|
||||
backgroundColor: "#383838",
|
||||
"&:hover": { backgroundColor: "#2e2e2e" },
|
||||
}}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<CircularProgress size={16} sx={{ mr: 1, color: "white" }} />
|
||||
{mode === "add" ? "Hinzufügen..." : "Speichern..."}
|
||||
</>
|
||||
) : mode === "add" ? (
|
||||
"Hinzufügen"
|
||||
) : (
|
||||
"Speichern"
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Snackbar
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={5000}
|
||||
onClose={() => setSnackbarOpen(false)}
|
||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
>
|
||||
<MuiAlert
|
||||
elevation={6}
|
||||
variant="filled"
|
||||
onClose={() => setSnackbarOpen(false)}
|
||||
severity={snackbarSeverity}
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span>{snackbarMessage}</span>
|
||||
<Button
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setSnackbarOpen(false)}
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</MuiAlert>
|
||||
</Snackbar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function generateSpacyEntries(formData: Partial<Kennzahl>) {
|
||||
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]]
|
||||
};
|
||||
});
|
||||
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]],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||
import { StrictMode } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "react-pdf/dist/Page/TextLayer.css";
|
||||
|
|
@ -10,9 +10,8 @@ import "@fontsource/roboto/400.css";
|
|||
import "@fontsource/roboto/500.css";
|
||||
import "@fontsource/roboto/700.css";
|
||||
|
||||
import * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx";
|
||||
|
||||
import { pdfjs } from "react-pdf";
|
||||
import * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx";
|
||||
|
||||
// Import the generated route tree
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
|
@ -27,6 +26,7 @@ const router = createRouter({
|
|||
scrollRestoration: true,
|
||||
defaultStructuralSharing: true,
|
||||
defaultPreloadStaleTime: 0,
|
||||
basepath: "/ff",
|
||||
});
|
||||
|
||||
// Register the router instance for type safety
|
||||
|
|
|
|||
|
|
@ -4,4 +4,7 @@ import { API_HOST } from "./util/api";
|
|||
// "undefined" means the URL will be computed from the `window.location` object
|
||||
// const URL = process.env.NODE_ENV === 'production' ? undefined : 'http://localhost:4000';
|
||||
|
||||
export const socket = io(`${API_HOST}`);
|
||||
const url = new URL(API_HOST);
|
||||
export const socket = io(`${url.host}`, {
|
||||
path: `${url.pathname.replace(/^\/+/, "")}/socket.io`,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue