From 810827e0bb3bd7e431196a17e8bab02f8b676c1e Mon Sep 17 00:00:00 2001 From: Jaronim Pracht Date: Tue, 1 Jul 2025 18:58:24 +0200 Subject: [PATCH] fix host url and prepare for deployment --- project/frontend/src/components/KPIForm.tsx | 887 +++++++++++--------- project/frontend/src/main.tsx | 8 +- project/frontend/src/socket.ts | 5 +- 3 files changed, 485 insertions(+), 415 deletions(-) diff --git a/project/frontend/src/components/KPIForm.tsx b/project/frontend/src/components/KPIForm.tsx index ff9ef86..4827a91 100644 --- a/project/frontend/src/components/KPIForm.tsx +++ b/project/frontend/src/components/KPIForm.tsx @@ -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) => Promise; - onCancel: () => void; - loading?: boolean; - resetTrigger?: number; + mode: "add" | "edit"; + initialData?: Kennzahl | null; + onSave: (data: Partial) => Promise; + onCancel: () => void; + loading?: boolean; + resetTrigger?: number; } const emptyKPI: Partial = { - 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>(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>(emptyKPI); - const [originalExamples, setOriginalExamples] = useState>([]); - 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 ( + + + + {mode === "edit" ? "Lade KPI Details..." : "Laden..."} + + + ); + } + return ( + <> + + + + Kennzahl + + updateField("name", e.target.value)} + sx={{ mb: 2 }} + required + error={!formData.name?.trim()} + helperText={!formData.name?.trim() ? "Name ist erforderlich" : ""} + /> + - const handleCancel = () => { - setFormData(emptyKPI); - onCancel(); - }; + - const updateField = (field: keyof Kennzahl, value: any) => { - setFormData(prev => ({ ...prev, [field]: value })); - }; + + + Format:{" "} + {typeDisplayMapping[ + formData.type as keyof typeof typeDisplayMapping + ] || formData.type} + + + Typ + + + - 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); - }; + + updateField("active", e.target.checked)} + sx={{ + color: "#666666", + "&.Mui-checked": { + color: "#333333", + }, + "&:hover": { + backgroundColor: "rgba(102, 102, 102, 0.04)", + }, + }} + /> + } + label="Aktiv" + /> + + Die Kennzahl ist aktiv und wird angezeigt + + + + updateField("mandatory", e.target.checked)} + sx={{ + color: "#666666", + "&.Mui-checked": { + color: "#333333", + }, + "&:hover": { + backgroundColor: "rgba(102, 102, 102, 0.04)", + }, + }} + /> + } + label="Erforderlich" + /> + + Die Kennzahl erlaubt keine leeren Werte + + - const removeExample = (index: number) => { - const newExamples = [...(formData.examples || [])]; - newExamples.splice(index, 1); - updateField('examples', newExamples); - }; + + {/* Hinweistext wie viele Beispielsätzen vorhanden sind*/} + {mode === "edit" && originalExamples.length > 0 && ( + + + Vorhandene Beispielsätze: {originalExamples.length} + + + Diese Kennzahl hat bereits {originalExamples.length}{" "} + Beispielsätze. Neue Beispielsätze werden zu den vorhandenen + hinzugefügt. + + + )} - if (loading) { - return ( - - - - {mode === 'edit' ? 'Lade KPI Details...' : 'Laden...'} - - - ); - } + {/* Hinweistext vor Beispielsätzen */} + + + Hinweis zur Trainingsqualität + + + Damit das System neue Kennzahlen zuverlässig erkennen kann, + empfehlen wir mindestens 5 Beispielsätze zu + erstellen – je mehr, desto besser. + + + Wichtig: Neue Kennzahlen werden erst in + PDF-Dokumenten erkannt, wenn Sie den Button{" "} + "Neu trainieren" auf der Konfigurationsseite ausführen. + + + Tipp: Sie können jederzeit weitere Beispielsätze + hinzufügen oder vorhandene in der Kennzahlenverwaltung bearbeiten. + + - return ( - <> - - - - Kennzahl - - updateField('name', e.target.value)} - sx={{ mb: 2 }} - required - error={!formData.name?.trim()} - helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''} - /> - + + + Beispielsätze + + {(formData.examples || []).map((ex, idx) => ( + + updateExample(idx, "sentence", e.target.value)} + required + sx={{ mb: 1 }} + /> - + updateExample(idx, "value", e.target.value)} + required + /> + {(formData.examples?.length || 0) > 1 && ( + + )} + + ))} - - - Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type} - - - Typ - - - + + - - - - updateField('active', e.target.checked)} - sx={{ - color: '#666666', - '&.Mui-checked': { - color: '#333333', - }, - '&:hover': { - backgroundColor: 'rgba(102, 102, 102, 0.04)', - } - }} - /> - } - label="Aktiv" - /> - - Die Kennzahl ist aktiv und wird angezeigt - - - - updateField('mandatory', e.target.checked)} - sx={{ - color: '#666666', - '&.Mui-checked': { - color: '#333333', - }, - '&:hover': { - backgroundColor: 'rgba(102, 102, 102, 0.04)', - } - }} - /> - } - label="Erforderlich" - /> - - Die Kennzahl erlaubt keine leeren Werte - - - - - - {/* Hinweistext wie viele Beispielsätzen vorhanden sind*/} - {mode === 'edit' && originalExamples.length > 0 && ( - - - Vorhandene Beispielsätze: {originalExamples.length} - - - Diese Kennzahl hat bereits {originalExamples.length} Beispielsätze. Neue Beispielsätze werden zu den vorhandenen hinzugefügt. - - - )} - - {/* Hinweistext vor Beispielsätzen */} - - - Hinweis zur Trainingsqualität - - - Damit das System neue Kennzahlen zuverlässig erkennen kann, empfehlen wir mindestens 5 Beispielsätze zu erstellen – je mehr, desto besser. - - - Wichtig: Neue Kennzahlen werden erst in PDF-Dokumenten erkannt, wenn Sie den Button "Neu trainieren" auf der Konfigurationsseite ausführen. - - - Tipp: Sie können jederzeit weitere Beispielsätze hinzufügen oder vorhandene in der Kennzahlenverwaltung bearbeiten. - - - - - - - Beispielsätze - - {(formData.examples || []).map((ex, idx) => ( - - updateExample(idx, 'sentence', e.target.value)} - required - sx={{ mb: 1 }} - /> - - updateExample(idx, 'value', e.target.value)} - required - /> - {(formData.examples?.length || 0) > 1 && ( - - )} - - ))} - - - - - - - - - - setSnackbarOpen(false)} - anchorOrigin={{ vertical: 'top', horizontal: 'center' }} - > - setSnackbarOpen(false)} - severity={snackbarSeverity} - sx={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }} - > - {snackbarMessage} - - - - - - ); + + + + + + setSnackbarOpen(false)} + anchorOrigin={{ vertical: "top", horizontal: "center" }} + > + setSnackbarOpen(false)} + severity={snackbarSeverity} + sx={{ + width: "100%", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + }} + > + {snackbarMessage} + + + + + ); } - function generateSpacyEntries(formData: Partial) { - 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]], + }; + }); } - - - diff --git a/project/frontend/src/main.tsx b/project/frontend/src/main.tsx index bffa8aa..626676b 100644 --- a/project/frontend/src/main.tsx +++ b/project/frontend/src/main.tsx @@ -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 diff --git a/project/frontend/src/socket.ts b/project/frontend/src/socket.ts index 563ef27..cbba6b5 100644 --- a/project/frontend/src/socket.ts +++ b/project/frontend/src/socket.ts @@ -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`, +});