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()) {
setSnackbarMessage("Name ist erforderlich");
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
if (mode === "add") { useEffect(() => {
if (!formData.examples || formData.examples.length === 0) { if (mode === 'edit' && initialData) {
setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich"); setFormData(initialData);
setSnackbarSeverity("error"); } else if (mode === 'add') {
setSnackbarOpen(true); setFormData(emptyKPI);
return; }
} }, [mode, initialData]);
const newExamples = formData.examples.filter( useEffect(() => {
(ex) => ex.sentence?.trim() && ex.value?.trim(), if (mode === 'add') {
); setFormData(emptyKPI);
}
}, [resetTrigger]);
if (newExamples.length === 0) {
setSnackbarMessage(
"Mindestens ein vollständiger Beispielsatz ist erforderlich.",
);
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
}
const newExamples = (formData.examples || []).filter(
(ex) => ex.sentence?.trim() && ex.value?.trim(),
);
if (formData.examples && formData.examples.length > 0) { const handleSave = async () => {
for (const ex of formData.examples) { if (!formData.name?.trim()) {
if (!ex.sentence?.trim() && !ex.value?.trim()) continue; setSnackbarMessage("Name ist erforderlich");
if (!ex.sentence?.trim() || !ex.value?.trim()) { setSnackbarSeverity("error");
setSnackbarMessage("Alle Beispielsätze müssen vollständig sein oder leer gelassen werden."); setSnackbarOpen(true);
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
}
}
setIsSaving(true); return;
try { }
if (newExamples.length > 0) {
const spacyEntries = generateSpacyEntries({
...formData,
examples: newExamples,
});
// Für jeden einzelnen Beispielsatz: if (!formData.examples || formData.examples.length === 0) {
for (const entry of spacyEntries) { setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich");
// im localStorage speichern (zum Debuggen oder Vorschau) setSnackbarSeverity("error");
const stored = localStorage.getItem("spacyData"); setSnackbarOpen(true);
const existingData = stored ? JSON.parse(stored) : [];
const updated = [...existingData, entry];
localStorage.setItem("spacyData", JSON.stringify(updated));
// POST Request an das Flask-Backend return;
const response = await fetch( }
`${API_HOST}/api/spacy/append-training-entry`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(entry),
},
);
const data = await response.json(); 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);
if (!response.ok) { return;
throw new Error( }
data.error || "Fehler beim Aufruf von append-training-entry", }
);
}
console.log("SpaCy-Eintrag gespeichert:", data);
}
}
const allExamples = setIsSaving(true);
mode === "edit" ? [...originalExamples, ...newExamples] : newExamples; try {
const spacyEntries = generateSpacyEntries(formData);
// Dann in die DB speichern // Für jeden einzelnen Beispielsatz:
await onSave({ for (const entry of spacyEntries) {
name: formData.name ?? "", // im localStorage speichern (zum Debuggen oder Vorschau)
mandatory: formData.mandatory ?? false, const stored = localStorage.getItem("spacyData");
type: formData.type || "string", const existingData = stored ? JSON.parse(stored) : [];
position: formData.position ?? 0, const updated = [...existingData, entry];
active: formData.active ?? true, localStorage.setItem("spacyData", JSON.stringify(updated));
examples: allExamples,
is_trained: false,
});
// Formular zurücksetzen: // POST Request an das Flask-Backend
if (mode === "add") { const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", {
setFormData(createEmptyKPI()); method: "POST",
} else { headers: {
setFormData((prev) => ({ "Content-Type": "application/json"
...prev, },
examples: [{ sentence: "", value: "" }], body: JSON.stringify(entry)
})); });
}
const successMessage = newExamples.length > 0 const data = await response.json();
? "Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder weitere Kennzahlen hinzufügen."
: mode === "edit"
? "Kennzahl erfolgreich aktualisiert."
: "Kennzahl erfolgreich erstellt.";
setSnackbarMessage(successMessage); if (!response.ok) {
setSnackbarSeverity("success"); throw new Error(data.error || "Fehler beim Aufruf von append-training-entry");
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 = () => { console.log("SpaCy-Eintrag gespeichert:", data);
setFormData(createEmptyKPI()); }
onCancel();
};
const updateField = (field: keyof Kennzahl, value: any) => { // Dann in die DB speichern
setFormData((prev) => ({ ...prev, [field]: value })); 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);
const updateExample = (
index: number,
field: "sentence" | "value",
value: string,
) => {
const newExamples = [...(formData.examples || [])];
newExamples[index][field] = value;
updateField("examples", newExamples);
};
const addExample = () => { setSnackbarMessage("Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder weitere Kennzahlen hinzufügen.");
const newExamples = [ setSnackbarSeverity("success");
...(formData.examples || []), setSnackbarOpen(true);
{ sentence: "", value: "" }, } catch (e: any) {
]; // Prüfe auf 409-Fehler
updateField("examples", newExamples); 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 ( const handleCancel = () => {
<> setFormData(emptyKPI);
<Paper onCancel();
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 updateField = (field: keyof Kennzahl, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
<Box mb={4}> const updateExample = (index: number, field: 'sentence' | 'value', value: string) => {
<Typography variant="h6" fontWeight="bold" mb={2}> const newExamples = [...(formData.examples || [])];
Format:{" "} newExamples[index][field] = value;
{typeDisplayMapping[ updateField('examples', newExamples);
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 addExample = () => {
const newExamples = [...(formData.examples || []), { sentence: '', value: '' }];
updateField('examples', newExamples);
};
<Box mb={4}> const removeExample = (index: number) => {
<FormControlLabel const newExamples = [...(formData.examples || [])];
control={ newExamples.splice(index, 1);
<Checkbox updateField('examples', newExamples);
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*/} if (loading) {
{mode === "edit" && originalExamples.length > 0 && ( return (
<Box <Box
mb={2} display="flex"
p={2} justifyContent="center"
sx={{ alignItems="center"
backgroundColor: "#e3f2fd", minHeight="400px"
border: "1px solid #90caf9", flexDirection="column"
borderRadius: 2, >
}} <CircularProgress sx={{ color: '#383838', mb: 2 }} />
> <Typography>
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}> {mode === 'edit' ? 'Lade KPI Details...' : 'Laden...'}
Vorhandene Beispielsätze: {originalExamples.length} </Typography>
</Typography> </Box>
<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 */} return (
<Box <>
mb={2} <Paper
p={2} elevation={2}
sx={{ sx={{
backgroundColor: "#fff8e1", width: "90%",
border: "1px solid #ffe082", maxWidth: 800,
borderRadius: 2, p: 4,
}} borderRadius: 2,
> backgroundColor: "white"
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}> }}
Hinweis zur Trainingsqualität >
</Typography> <Box mb={4}>
<Typography variant="body2"> <Typography variant="h6" fontWeight="bold" mb={2}>
Damit das System neue Kennzahlen zuverlässig erkennen kann, Kennzahl
empfehlen wir <strong>mindestens 5 Beispielsätze</strong> zu </Typography>
erstellen je mehr, desto besser. <TextField
</Typography> fullWidth
<Typography variant="body2" mt={1}> label="Name *"
<strong>Wichtig:</strong> Neue Kennzahlen werden erst in placeholder="z.B. IRR"
PDF-Dokumenten erkannt, wenn Sie den Button{" "} value={formData.name || ''}
<em>"Neu trainieren"</em> auf der Konfigurationsseite ausführen. onChange={(e) => updateField('name', e.target.value)}
</Typography> sx={{ mb: 2 }}
<Typography variant="body2" mt={1}> required
<strong>Tipp:</strong> Sie können jederzeit weitere Beispielsätze error={!formData.name?.trim()}
hinzufügen. helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''}
</Typography> />
</Box> </Box>
<Box mb={4}> <Divider sx={{ my: 3 }} />
<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 <Box mb={4}>
fullWidth <Typography variant="h6" fontWeight="bold" mb={2}>
label="Bezeichneter Wert im Satz" Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type}
placeholder="z.B. 7,8 %" </Typography>
value={ex.value} <FormControl fullWidth sx={{ mb: 2 }}>
onChange={(e) => updateExample(idx, "value", e.target.value)} <InputLabel>Typ</InputLabel>
required <Select
/> value={formData.type || 'string'}
{(formData.examples?.length || 0) > 1 && ( label="Typ"
<Button onChange={(e) => updateField('type', e.target.value)}
onClick={() => removeExample(idx)} >
sx={{ mt: 1 }} <MenuItem value="string">Text</MenuItem>
color="error" <MenuItem value="number">Zahl</MenuItem>
> <MenuItem value="date">Datum</MenuItem>
Entfernen <MenuItem value="boolean">Ja/Nein</MenuItem>
</Button> <MenuItem value="array">Liste (mehrfach)</MenuItem>
)} </Select>
</Box> </FormControl>
))} </Box>
<Button variant="outlined" onClick={addExample}>
+ Beispielsatz hinzufügen
</Button>
</Box>
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}> {mode === 'add' && (
<Button <>
variant="outlined" <Divider sx={{ my: 3 }} />
onClick={handleCancel} <Box mb={4}>
disabled={isSaving} <FormControlLabel
sx={{ control={
borderColor: "#383838", <Checkbox
color: "#383838", checked={formData.active !== false}
"&:hover": { borderColor: "#2e2e2e", backgroundColor: "#f5f5f5" }, onChange={(e) => updateField('active', e.target.checked)}
}} sx={{ color: '#383838' }}
> />
Abbrechen }
</Button> label="Aktiv"
<Button />
variant="contained" <Typography variant="body2" color="text.secondary" ml={4}>
onClick={handleSave} Die Kennzahl ist aktiv und wird angezeigt
disabled={isSaving || !formData.name?.trim()} </Typography>
sx={{ </Box>
backgroundColor: "#383838", <Box mt={3}>
"&:hover": { backgroundColor: "#2e2e2e" }, <FormControlLabel
}} control={
> <Checkbox
{isSaving ? ( checked={formData.mandatory || false}
<> onChange={(e) => updateField('mandatory', e.target.checked)}
<CircularProgress size={16} sx={{ mr: 1, color: "white" }} /> sx={{ color: '#383838' }}
{mode === "add" ? "Hinzufügen..." : "Speichern..."} />
</> }
) : mode === "add" ? ( label="Erforderlich"
"Hinzufügen" />
) : ( <Typography variant="body2" color="text.secondary" ml={4}>
"Speichern" Die Kennzahl erlaubt keine leeren Werte
)} </Typography>
</Button> </Box>
</Box> </>
</Paper> )}
<Snackbar
open={snackbarOpen} <Divider sx={{ my: 3 }} />
autoHideDuration={5000}
onClose={() => setSnackbarOpen(false)} {/* Hinweistext vor Beispielsätzen */}
anchorOrigin={{ vertical: "top", horizontal: "center" }} <Box mb={2} p={2} sx={{ backgroundColor: '#fff8e1', border: '1px solid #ffe082', borderRadius: 2 }}>
> <Typography variant="body1" sx={{ fontWeight: 'bold', mb: 1 }}>
<MuiAlert Hinweis zur Trainingsqualität
elevation={6} </Typography>
variant="filled" <Typography variant="body2">
onClose={() => setSnackbarOpen(false)} Damit das System neue Kennzahlen zuverlässig erkennen kann, empfehlen wir <strong>mindestens 5 Beispielsätze</strong> zu erstellen je mehr, desto besser.
severity={snackbarSeverity} </Typography>
sx={{ <Typography variant="body2" mt={1}>
width: "100%", <strong>Wichtig:</strong> Neue Kennzahlen werden erst in PDF-Dokumenten erkannt, wenn Sie den Button <em>"Neu trainieren"</em> auf der Konfigurationsseite ausführen.
display: "flex", </Typography>
justifyContent: "space-between", <Typography variant="body2" mt={1}>
alignItems: "center", <strong>Tipp:</strong> Sie können jederzeit weitere Beispielsätze hinzufügen oder vorhandene in der Kennzahlenverwaltung bearbeiten.
}} </Typography>
> </Box>
<span>{snackbarMessage}</span>
<Button <Box mb={4}>
color="inherit"
size="small" <Typography variant="h6" fontWeight="bold" mb={2}>
onClick={() => setSnackbarOpen(false)} Beispielsätze
> </Typography>
OK {(formData.examples || []).map((ex, idx) => (
</Button> <Box key={idx} sx={{ mb: 2, border: '1px solid #ccc', p: 2, borderRadius: 1 }}>
</MuiAlert> <TextField
</Snackbar> 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>
</>
);
} }
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`,
});