#bugs #110

Merged
3023730 merged 5 commits from #bugs into main 2025-07-01 20:03:54 +02:00
4 changed files with 509 additions and 377 deletions

View File

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

View File

@ -188,6 +188,10 @@ 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"
@ -197,11 +201,6 @@ 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}
@ -213,11 +212,10 @@ export default function PDFViewer({
borderRadius: 0, borderRadius: 0,
boxShadow: "none", boxShadow: "none",
overflow: "auto", overflow: "auto",
display: "flex", display: willOverflow ? "block" : "flex",
justifyContent: "center", justifyContent: willOverflow ? "flex-start" : "center",
alignItems: "center", alignItems: willOverflow ? "flex-start" : "center",
marginTop: 2, padding: willOverflow ? `${Math.max(0, (500 - (contentWidth * (500 / containerWidth))) / 2)}px ${Math.max(0, (containerWidth - contentWidth) / 2)}px` : 0,
marginBottom: 2,
}} }}
> >
<Document <Document

View File

@ -1,6 +1,6 @@
import CssBaseline from "@mui/material/CssBaseline"; import CssBaseline from "@mui/material/CssBaseline";
import { ThemeProvider, createTheme } from "@mui/material/styles"; import { createTheme, ThemeProvider } from "@mui/material/styles";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { createRouter, RouterProvider } 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,8 @@ 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 * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx";
import { pdfjs } from "react-pdf"; import { pdfjs } from "react-pdf";
import * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx";
// Import the generated route tree // Import the generated route tree
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
@ -27,6 +26,7 @@ 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,4 +4,7 @@ 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';
export const socket = io(`${API_HOST}`); const url = new URL(API_HOST);
export const socket = io(`${url.host}`, {
path: `${url.pathname.replace(/^\/+/, "")}/socket.io`,
});