Compare commits

..

No commits in common. "c45d1b20ed5dd39a7d40b48f3dfee511b1d6b48f" and "5d0a5ab3c3e939368fa3446aa16611f7d8ab9472" have entirely different histories.

4 changed files with 377 additions and 509 deletions

View File

@ -1,542 +1,411 @@
import { import {
Box, Box, Typography, Button, Paper, TextField, FormControlLabel,
Button, Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress
Checkbox,
CircularProgress,
Divider,
FormControl,
FormControlLabel,
InputLabel,
MenuItem,
Paper,
Select,
TextField,
Typography,
} from "@mui/material"; } from "@mui/material";
import MuiAlert from "@mui/material/Alert"; import { useState, useEffect } from "react";
import Snackbar from "@mui/material/Snackbar";
import { useEffect, useState } from "react";
import type { Kennzahl } from "../types/kpi"; import type { Kennzahl } from "../types/kpi";
import { typeDisplayMapping } from "../types/kpi"; import { typeDisplayMapping } from "../types/kpi";
import { API_HOST } from "../util/api"; import Snackbar from "@mui/material/Snackbar";
import MuiAlert from "@mui/material/Alert";
interface KPIFormProps { interface KPIFormProps {
mode: "add" | "edit"; mode: 'add' | 'edit';
initialData?: Kennzahl | null; initialData?: Kennzahl | null;
onSave: (data: Partial<Kennzahl>) => Promise<void>; onSave: (data: Partial<Kennzahl>) => Promise<void>;
onCancel: () => void; onCancel: () => void;
loading?: boolean; loading?: boolean;
resetTrigger?: number; resetTrigger?: number;
} }
const createEmptyKPI = (): Partial<Kennzahl> => ({ const emptyKPI: Partial<Kennzahl> = {
name: "", name: '',
mandatory: false, mandatory: false,
type: "string", type: 'string',
active: true, active: true,
examples: [{ sentence: "", value: "" }], examples: [{ sentence: '', value: '' }],
}); };
export function KPIForm({
mode,
initialData,
onSave,
onCancel,
loading = false,
}: KPIFormProps) {
const [formData, setFormData] = useState<Partial<Kennzahl>>(createEmptyKPI());
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(() => { export function KPIForm({ mode, initialData, onSave, onCancel, loading = false, resetTrigger }: KPIFormProps) {
if (mode === "edit" && initialData) { const [formData, setFormData] = useState<Partial<Kennzahl>>(emptyKPI);
setOriginalExamples(initialData.examples || []); const [isSaving, setIsSaving] = useState(false);
setFormData({ const [snackbarOpen, setSnackbarOpen] = useState(false);
...initialData, const [snackbarMessage, setSnackbarMessage] = useState("");
examples: [{ sentence: "", value: "" }], const [snackbarSeverity, setSnackbarSeverity] = useState<'success' | 'error' | 'info'>("success");
});
} else if (mode === "add") {
setOriginalExamples([]);
setFormData(createEmptyKPI());
}
}, [mode, initialData]);
const handleSave = async () => {
if (!formData.name?.trim()) { useEffect(() => {
setSnackbarMessage("Name ist erforderlich"); if (mode === 'edit' && initialData) {
setSnackbarSeverity("error"); setFormData(initialData);
setSnackbarOpen(true); } else if (mode === 'add') {
return; setFormData(emptyKPI);
} }
}, [mode, initialData]);
if (mode === "add") { useEffect(() => {
if (!formData.examples || formData.examples.length === 0) { if (mode === 'add') {
setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich"); setFormData(emptyKPI);
setSnackbarSeverity("error"); }
setSnackbarOpen(true); }, [resetTrigger]);
return;
}
const newExamples = formData.examples.filter(
(ex) => ex.sentence?.trim() && ex.value?.trim(),
);
if (newExamples.length === 0) {
setSnackbarMessage(
"Mindestens ein vollständiger Beispielsatz ist erforderlich.",
);
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
}
const newExamples = (formData.examples || []).filter( const handleSave = async () => {
(ex) => ex.sentence?.trim() && ex.value?.trim(), if (!formData.name?.trim()) {
); setSnackbarMessage("Name ist erforderlich");
setSnackbarSeverity("error");
setSnackbarOpen(true);
if (formData.examples && formData.examples.length > 0) { return;
for (const ex of formData.examples) { }
if (!ex.sentence?.trim() && !ex.value?.trim()) continue;
if (!ex.sentence?.trim() || !ex.value?.trim()) {
setSnackbarMessage("Alle Beispielsätze müssen vollständig sein oder leer gelassen werden.");
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
}
}
setIsSaving(true); if (!formData.examples || formData.examples.length === 0) {
try { setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich");
if (newExamples.length > 0) { setSnackbarSeverity("error");
const spacyEntries = generateSpacyEntries({ setSnackbarOpen(true);
...formData,
examples: newExamples,
});
// Für jeden einzelnen Beispielsatz: return;
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 for (const ex of formData.examples) {
const response = await fetch( if (!ex.sentence?.trim() || !ex.value?.trim()) {
`${API_HOST}/api/spacy/append-training-entry`, setSnackbarMessage('Alle Beispielsätze müssen vollständig sein.');
{ setSnackbarSeverity("error");
method: "POST", setSnackbarOpen(true);
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(entry),
},
);
const data = await response.json(); return;
}
}
if (!response.ok) {
throw new Error(
data.error || "Fehler beim Aufruf von append-training-entry",
);
}
console.log("SpaCy-Eintrag gespeichert:", data); setIsSaving(true);
} try {
} const spacyEntries = generateSpacyEntries(formData);
const allExamples = // Für jeden einzelnen Beispielsatz:
mode === "edit" ? [...originalExamples, ...newExamples] : newExamples; 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));
// Dann in die DB speichern // POST Request an das Flask-Backend
await onSave({ const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", {
name: formData.name ?? "", method: "POST",
mandatory: formData.mandatory ?? false, headers: {
type: formData.type || "string", "Content-Type": "application/json"
position: formData.position ?? 0, },
active: formData.active ?? true, body: JSON.stringify(entry)
examples: allExamples, });
is_trained: false,
});
// Formular zurücksetzen: const data = await response.json();
if (mode === "add") {
setFormData(createEmptyKPI());
} else {
setFormData((prev) => ({
...prev,
examples: [{ sentence: "", value: "" }],
}));
}
const successMessage = newExamples.length > 0 if (!response.ok) {
? "Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder weitere Kennzahlen hinzufügen." throw new Error(data.error || "Fehler beim Aufruf von append-training-entry");
: mode === "edit" }
? "Kennzahl erfolgreich aktualisiert."
: "Kennzahl erfolgreich erstellt.";
setSnackbarMessage(successMessage); console.log("SpaCy-Eintrag gespeichert:", data);
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 = () => { // Dann in die DB speichern
setFormData(createEmptyKPI()); await onSave({
onCancel(); 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);
const updateField = (field: keyof Kennzahl, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const updateExample = ( setSnackbarMessage("Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder weitere Kennzahlen hinzufügen.");
index: number, setSnackbarSeverity("success");
field: "sentence" | "value", setSnackbarOpen(true);
value: string, } catch (e: any) {
) => { // Prüfe auf 409-Fehler
const newExamples = [...(formData.examples || [])]; if (e?.message?.includes("409") || e?.response?.status === 409) {
newExamples[index][field] = value; setSnackbarMessage("Diese Kennzahl existiert bereits. Sie können sie unter -Konfiguration- bearbeiten.");
updateField("examples", newExamples); setSnackbarSeverity("info");
}; setSnackbarOpen(true);
} else {
setSnackbarMessage(e.message || "Fehler beim Speichern.");
setSnackbarSeverity("error");
setSnackbarOpen(true);
}
console.error(e);
} finally {
setIsSaving(false);
}
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) { const handleCancel = () => {
return ( setFormData(emptyKPI);
<Box onCancel();
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 ( const updateField = (field: keyof Kennzahl, value: any) => {
<> setFormData(prev => ({ ...prev, [field]: value }));
<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>
<Divider sx={{ my: 3 }} /> const updateExample = (index: number, field: 'sentence' | 'value', value: string) => {
const newExamples = [...(formData.examples || [])];
newExamples[index][field] = value;
updateField('examples', newExamples);
};
<Box mb={4}> const addExample = () => {
<Typography variant="h6" fontWeight="bold" mb={2}> const newExamples = [...(formData.examples || []), { sentence: '', value: '' }];
Format:{" "} updateField('examples', newExamples);
{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>
<Divider sx={{ my: 3 }} /> const removeExample = (index: number) => {
const newExamples = [...(formData.examples || [])];
newExamples.splice(index, 1);
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>
<Divider sx={{ my: 3 }} /> 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 wie viele Beispielsätzen vorhanden sind*/} return (
{mode === "edit" && originalExamples.length > 0 && ( <>
<Box <Paper
mb={2} elevation={2}
p={2} sx={{
sx={{ width: "90%",
backgroundColor: "#e3f2fd", maxWidth: 800,
border: "1px solid #90caf9", p: 4,
borderRadius: 2, borderRadius: 2,
}} backgroundColor: "white"
> }}
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}> >
Vorhandene Beispielsätze: {originalExamples.length} <Box mb={4}>
</Typography> <Typography variant="h6" fontWeight="bold" mb={2}>
<Typography variant="body2"> Kennzahl
Diese Kennzahl hat bereits {originalExamples.length}{" "} </Typography>
Beispielsätze. Neue Beispielsätze werden zu den vorhandenen <TextField
hinzugefügt. fullWidth
</Typography> label="Name *"
</Box> 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>
{/* Hinweistext vor Beispielsätzen */} <Divider sx={{ my: 3 }} />
<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.
</Typography>
</Box>
<Box mb={4}> <Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}> <Typography variant="h6" fontWeight="bold" mb={2}>
Beispielsätze Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type}
</Typography> </Typography>
{(formData.examples || []).map((ex, idx) => ( <FormControl fullWidth sx={{ mb: 2 }}>
<Box <InputLabel>Typ</InputLabel>
key={idx} <Select
sx={{ mb: 2, border: "1px solid #ccc", p: 2, borderRadius: 1 }} value={formData.type || 'string'}
> label="Typ"
<TextField onChange={(e) => updateField('type', e.target.value)}
fullWidth >
multiline <MenuItem value="string">Text</MenuItem>
label={`Beispielsatz ${idx + 1}`} <MenuItem value="number">Zahl</MenuItem>
placeholder="z.B. Die IRR beträgt 7,8 %" <MenuItem value="date">Datum</MenuItem>
value={ex.sentence} <MenuItem value="boolean">Ja/Nein</MenuItem>
onChange={(e) => updateExample(idx, "sentence", e.target.value)} <MenuItem value="array">Liste (mehrfach)</MenuItem>
required </Select>
sx={{ mb: 1 }} </FormControl>
/> </Box>
<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}> {mode === 'add' && (
+ Beispielsatz hinzufügen <>
</Button> <Divider sx={{ my: 3 }} />
</Box> <Box mb={4}>
<FormControlLabel
control={
<Checkbox
checked={formData.active !== false}
onChange={(e) => updateField('active', e.target.checked)}
sx={{ color: '#383838' }}
/>
}
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: '#383838' }}
/>
}
label="Erforderlich"
/>
<Typography variant="body2" color="text.secondary" ml={4}>
Die Kennzahl erlaubt keine leeren Werte
</Typography>
</Box>
</>
)}
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}> <Divider sx={{ my: 3 }} />
<Button
variant="outlined" {/* Hinweistext vor Beispielsätzen */}
onClick={handleCancel} <Box mb={2} p={2} sx={{ backgroundColor: '#fff8e1', border: '1px solid #ffe082', borderRadius: 2 }}>
disabled={isSaving} <Typography variant="body1" sx={{ fontWeight: 'bold', mb: 1 }}>
sx={{ Hinweis zur Trainingsqualität
borderColor: "#383838", </Typography>
color: "#383838", <Typography variant="body2">
"&:hover": { borderColor: "#2e2e2e", backgroundColor: "#f5f5f5" }, 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}>
Abbrechen <strong>Wichtig:</strong> Neue Kennzahlen werden erst in PDF-Dokumenten erkannt, wenn Sie den Button <em>"Neu trainieren"</em> auf der Konfigurationsseite ausführen.
</Button> </Typography>
<Button <Typography variant="body2" mt={1}>
variant="contained" <strong>Tipp:</strong> Sie können jederzeit weitere Beispielsätze hinzufügen oder vorhandene in der Kennzahlenverwaltung bearbeiten.
onClick={handleSave} </Typography>
disabled={isSaving || !formData.name?.trim()} </Box>
sx={{
backgroundColor: "#383838", <Box mb={4}>
"&:hover": { backgroundColor: "#2e2e2e" },
}} <Typography variant="h6" fontWeight="bold" mb={2}>
> Beispielsätze
{isSaving ? ( </Typography>
<> {(formData.examples || []).map((ex, idx) => (
<CircularProgress size={16} sx={{ mr: 1, color: "white" }} /> <Box key={idx} sx={{ mb: 2, border: '1px solid #ccc', p: 2, borderRadius: 1 }}>
{mode === "add" ? "Hinzufügen..." : "Speichern..."} <TextField
</> fullWidth
) : mode === "add" ? ( multiline
"Hinzufügen" label={`Beispielsatz ${idx + 1}`}
) : ( placeholder="z.B. Die IRR beträgt 7,8 %"
"Speichern" value={ex.sentence}
)} onChange={(e) => updateExample(idx, 'sentence', e.target.value)}
</Button> required
</Box> sx={{ mb: 1 }}
</Paper> />
<Snackbar
open={snackbarOpen} <TextField
autoHideDuration={5000} fullWidth
onClose={() => setSnackbarOpen(false)} label="Bezeichneter Wert im Satz"
anchorOrigin={{ vertical: "top", horizontal: "center" }} placeholder="z.B. 7,8 %"
> value={ex.value}
<MuiAlert onChange={(e) => updateExample(idx, 'value', e.target.value)}
elevation={6} required
variant="filled" />
onClose={() => setSnackbarOpen(false)} {(formData.examples?.length || 0) > 1 && (
severity={snackbarSeverity} <Button onClick={() => removeExample(idx)} sx={{ mt: 1 }} color="error">
sx={{ Entfernen
width: "100%", </Button>
display: "flex", )}
justifyContent: "space-between", </Box>
alignItems: "center", ))}
}}
> <Button variant="outlined" onClick={addExample}>
<span>{snackbarMessage}</span> + Beispielsatz hinzufügen
<Button </Button>
color="inherit" </Box>
size="small"
onClick={() => setSnackbarOpen(false)} <Box display="flex" justifyContent="flex-end" gap={2} mt={4}>
> <Button
OK variant="outlined"
</Button> onClick={handleCancel}
</MuiAlert> disabled={isSaving}
</Snackbar> 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>) { function generateSpacyEntries(formData: Partial<Kennzahl>) {
const label = formData.name?.trim().toUpperCase() || ""; const label = formData.name?.trim().toUpperCase() || "";
return (formData.examples || []).map(({ sentence, value }) => { return (formData.examples || []).map(({ sentence, value }) => {
const trimmedValue = value.trim(); const trimmedValue = value.trim();
const start = sentence.indexOf(trimmedValue); const start = sentence.indexOf(trimmedValue);
if (start === -1) { if (start === -1) {
throw new Error( throw new Error(`"${trimmedValue}" nicht gefunden in Satz: "${sentence}"`);
`"${trimmedValue}" nicht gefunden in Satz: "${sentence}"`, }
); return {
} text: sentence,
return { entities: [[start, start + trimmedValue.length, label]]
text: sentence, };
entities: [[start, start + trimmedValue.length, label]], });
};
});
} }

View File

@ -188,10 +188,6 @@ export default function PDFViewer({
); );
}, []); }, []);
const contentWidth = baseWidth ? baseWidth * 0.98 * zoomLevel : 0;
const containerWidth = baseWidth ? baseWidth : 0;
const willOverflow = contentWidth > containerWidth;
return ( return (
<Box <Box
display="flex" display="flex"
@ -201,6 +197,11 @@ export default function PDFViewer({
width="100%" width="100%"
maxWidth="850px" maxWidth="850px"
margin="0 auto" margin="0 auto"
sx={{
backgroundColor: "#f5f5f5",
borderRadius: 2,
boxShadow: 2,
}}
> >
<Box <Box
ref={containerRef} ref={containerRef}
@ -212,10 +213,11 @@ export default function PDFViewer({
borderRadius: 0, borderRadius: 0,
boxShadow: "none", boxShadow: "none",
overflow: "auto", overflow: "auto",
display: willOverflow ? "block" : "flex", display: "flex",
justifyContent: willOverflow ? "flex-start" : "center", justifyContent: "center",
alignItems: willOverflow ? "flex-start" : "center", alignItems: "center",
padding: willOverflow ? `${Math.max(0, (500 - (contentWidth * (500 / containerWidth))) / 2)}px ${Math.max(0, (containerWidth - contentWidth) / 2)}px` : 0, marginTop: 2,
marginBottom: 2,
}} }}
> >
<Document <Document

View File

@ -1,6 +1,6 @@
import CssBaseline from "@mui/material/CssBaseline"; import CssBaseline from "@mui/material/CssBaseline";
import { createTheme, ThemeProvider } from "@mui/material/styles"; import { ThemeProvider, createTheme } from "@mui/material/styles";
import { createRouter, RouterProvider } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import { StrictMode } from "react"; import { StrictMode } from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import "react-pdf/dist/Page/TextLayer.css"; import "react-pdf/dist/Page/TextLayer.css";
@ -10,9 +10,10 @@ import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css"; import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css"; import "@fontsource/roboto/700.css";
import { pdfjs } from "react-pdf";
import * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx"; import * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx";
import { pdfjs } from "react-pdf";
// Import the generated route tree // Import the generated route tree
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
@ -26,7 +27,6 @@ const router = createRouter({
scrollRestoration: true, scrollRestoration: true,
defaultStructuralSharing: true, defaultStructuralSharing: true,
defaultPreloadStaleTime: 0, defaultPreloadStaleTime: 0,
basepath: "/ff",
}); });
// Register the router instance for type safety // Register the router instance for type safety

View File

@ -4,7 +4,4 @@ import { API_HOST } from "./util/api";
// "undefined" means the URL will be computed from the `window.location` object // "undefined" means the URL will be computed from the `window.location` object
// const URL = process.env.NODE_ENV === 'production' ? undefined : 'http://localhost:4000'; // const URL = process.env.NODE_ENV === 'production' ? undefined : 'http://localhost:4000';
const url = new URL(API_HOST); export const socket = io(`${API_HOST}`);
export const socket = io(`${url.host}`, {
path: `${url.pathname.replace(/^\/+/, "")}/socket.io`,
});