fix host url and prepare for deployment

pull/110/head
Jaronim Pracht 2025-07-01 18:58:24 +02:00
parent 3d6458ffb0
commit 810827e0bb
3 changed files with 485 additions and 415 deletions

View File

@ -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]],
};
});
}

View File

@ -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

View File

@ -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`,
});