Merge pull request 'Progress - Frontend' (#48) from #16-progress into main

Reviewed-on: #48
pull/50/head
Anastasia Hanna Ougolnikova 2025-06-03 13:44:57 +02:00
commit b9d7f425e5
21 changed files with 493 additions and 7907 deletions

View File

@ -0,0 +1,21 @@
# 1. Python-Image verwenden
FROM python:3.11-alpine
# 2. Arbeitsverzeichnis im Container setzen
WORKDIR /app
# 3. production-style server mit gunicorn
RUN pip install gunicorn eventlet
# 4. requirements.txt kopieren und Pakete installieren
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 5. Quellcode kopieren (z.B. app.py)
COPY . .
ENV PYTHONUNBUFFERED=1
EXPOSE 5000
CMD ["gunicorn", "--worker-class", "eventlet", "-w", "1", "--bind", "0.0.0.0:5000", "app:app"]

View File

@ -1,10 +1,14 @@
from flask import Flask
from flask_cors import CORS
import os
from dotenv import load_dotenv
from controller import register_routes
from model.database import init_db
from controller.socketIO import socketio
app = Flask(__name__)
CORS(app)
socketio.init_app(app)
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL")
@ -25,4 +29,4 @@ def health_check():
# für Docker wichtig: host='0.0.0.0'
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0")
socketio.run(app,debug=True, host="0.0.0.0", port=5050)

View File

@ -1,9 +1,11 @@
from controller.spacy_contoller import spacy_controller
from controller.kpi_setting_controller import kpi_setting_controller
from controller.pitch_book_controller import pitch_book_controller
from controller.progress_controller import progress_controller
def register_routes(app):
app.register_blueprint(kpi_setting_controller)
app.register_blueprint(pitch_book_controller)
app.register_blueprint(spacy_controller)
app.register_blueprint(progress_controller)

View File

@ -4,6 +4,7 @@ from model.pitch_book_model import PitchBookModel
from io import BytesIO
from werkzeug.utils import secure_filename
import puremagic
from controller.socketIO import socketio
pitch_book_controller = Blueprint("pitch_books", __name__, url_prefix="/api/pitch_book")
@ -54,6 +55,7 @@ def upload_file():
db.session.add(new_file)
db.session.commit()
socketio.emit("progress", {"id": new_file.id, "progress": 0})
return jsonify(new_file.to_dict()), 201
except Exception as e:
print(e)
@ -81,6 +83,7 @@ def update_file(id):
print(e)
if "kpi" in request.form:
socketio.emit("progress", {"id": id, "progress": 100})
file.kpi = request.form.get("kpi")
db.session.commit()

View File

@ -0,0 +1,19 @@
from flask import Blueprint, request, jsonify
from controller.socketIO import socketio
progress_controller = Blueprint("progress", __name__, url_prefix="/api/progress")
@progress_controller.route("/", methods=["POST"])
def progress():
data = request.get_json()
if 'id' not in data or 'progress' not in data:
return jsonify({"error": "Missing required fields. [id, progress]"}), 400
if not isinstance(data['progress'], (int, float)) or data['progress'] < 0 or data['progress'] >= 100:
return jsonify({"error": "Invalid progress value"}), 400
socketio.emit("progress", {"id": data["id"], "progress": data["progress"]})
# Process the data and return a response
return jsonify({"message": "Progress updated"})

View File

@ -0,0 +1,3 @@
from flask_socketio import SocketIO
socketio = SocketIO(cors_allowed_origins="*")

View File

@ -1,3 +1,4 @@
bidict==0.23.1
black==25.1.0
blinker==1.9.0
cfgv==3.4.0
@ -6,8 +7,11 @@ distlib==0.3.9
filelock==3.18.0
flake8==7.2.0
Flask==3.1.1
flask-cors==6.0.0
Flask-SocketIO==5.5.1
Flask-SQLAlchemy==3.1.1
greenlet==3.2.2
h11==0.16.0
identify==2.6.12
itsdangerous==2.2.0
Jinja2==3.1.6
@ -24,8 +28,12 @@ puremagic==1.29
pycodestyle==2.13.0
pyflakes==3.3.2
python-dotenv==1.1.0
python-engineio==4.12.1
python-socketio==5.13.0
PyYAML==6.0.2
simple-websocket==1.1.0
SQLAlchemy==2.0.41
typing_extensions==4.13.2
virtualenv==20.31.2
Werkzeug==3.1.3
wsproto==1.2.0

View File

@ -19,11 +19,11 @@ services:
coordinator:
build:
context: backend/coordinator
dockerfile: ../../Dockerfile
env_file:
- .env
depends_on:
- db
db:
condition: service_healthy
healthcheck:
test: wget --spider --no-verbose http://127.0.0.1:5000/health || exit 1
interval: 10s

View File

@ -12,7 +12,7 @@
},
"formatter": {
"enabled": true,
"indentStyle": "space"
"indentStyle": "tab"
},
"organizeImports": {
"enabled": true

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,8 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-material-file-upload": "^0.0.4",
"react-pdf": "^9.2.1"
"react-pdf": "^8.0.2",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",

View File

@ -0,0 +1,33 @@
import Box from "@mui/material/Box";
import CircularProgress, {
type CircularProgressProps,
} from "@mui/material/CircularProgress";
import Typography from "@mui/material/Typography";
export function CircularProgressWithLabel(
props: CircularProgressProps & { value: number },
) {
return (
<Box sx={{ position: "relative", display: "inline-flex" }}>
<CircularProgress {...props} />
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: "absolute",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
sx={{ color: "inherit" }}
>{`${Math.round(props.value)}%`}</Typography>
</Box>
</Box>
);
}

View File

@ -1,15 +1,90 @@
import { useState } from 'react'
import FileUpload from 'react-material-file-upload'
import {Box, Button, IconButton, Paper} from '@mui/material'
import { useNavigate } from '@tanstack/react-router'
import SettingsIcon from '@mui/icons-material/Settings';
import SettingsIcon from "@mui/icons-material/Settings";
import { Backdrop, Box, Button, IconButton, Paper } from "@mui/material";
import { useNavigate } from "@tanstack/react-router";
import { useCallback, useEffect, useState } from "react";
import FileUpload from "react-material-file-upload";
import { socket } from "../socket";
import { CircularProgressWithLabel } from "./CircularProgressWithLabel";
const PROGRESS = false;
export default function UploadPage() {
const [files, setFiles] = useState<File[]>([])
const [files, setFiles] = useState<File[]>([]);
const [pageId, setPageId] = useState<string | null>(null);
const [loadingState, setLoadingState] = useState<number | null>(null);
const fileTypes = ["pdf"];
const navigate = useNavigate()
const navigate = useNavigate();
const uploadFile = useCallback(async () => {
const formData = new FormData();
formData.append("file", files[0]);
const response = await fetch("http://localhost:5050/api/pitch_book", {
method: "POST",
body: formData,
});
if (response.ok) {
console.log("File uploaded successfully");
const data = await response.json();
console.log(data);
setPageId(data.id);
setLoadingState(0);
!PROGRESS &&
navigate({
to: "/extractedResult/$pitchBook",
params: { pitchBook: data.id },
});
} else {
console.error("Failed to upload file");
}
}, [files, navigate]);
const onConnection = useCallback(() => {
console.log("connected");
}, []);
const onProgress = useCallback(
(progress: { id: number; progress: number }) => {
console.log("Progress:", progress);
console.log(pageId);
if (Number(pageId) === progress.id) {
setLoadingState(progress.progress);
if (progress.progress === 100) {
navigate({
to: "/extractedResult/$pitchBook",
params: { pitchBook: progress.id.toString() },
});
}
}
},
[pageId, navigate],
);
useEffect(() => {
socket.on("connect", onConnection);
socket.on("progress", onProgress);
return () => {
socket.off("connect", onConnection);
socket.off("progress", onProgress);
};
}, [onConnection, onProgress]);
return (
<>
{PROGRESS && (
<Backdrop
sx={(theme) => ({ color: "#fff", zIndex: theme.zIndex.drawer + 1 })}
open={pageId !== null && loadingState !== null}
>
<CircularProgressWithLabel
color="inherit"
value={loadingState || 0}
size={60}
/>
</Backdrop>
)}
<Box
display="flex"
flexDirection="column"
@ -25,8 +100,8 @@ export default function UploadPage() {
justifyContent="flex-end"
px={2}
>
<IconButton onClick={() => navigate({ to: '/config' })}>
<SettingsIcon fontSize="large"/>
<IconButton onClick={() => navigate({ to: "/config" })}>
<SettingsIcon fontSize="large" />
</IconButton>
</Box>
<Paper
@ -34,49 +109,51 @@ export default function UploadPage() {
sx={{
width: 900,
height: 500,
backgroundColor: '#eeeeee',
backgroundColor: "#eeeeee",
borderRadius: 4,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Box sx={{
height: '100%',
width: '100%',
maxWidth: '100%',
margin: '0px',
padding: '0px',
'& .MuiBox-root': {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
textAlign: 'center',
<Box
sx={{
height: "100%",
width: "100%",
maxWidth: "100%",
margin: "0px",
padding: "0px",
"& .MuiBox-root": {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
border: "none",
textAlign: "center",
},
}}>
}}
>
<FileUpload
value={files}
onChange={setFiles}
accept={`.${fileTypes.join(', .')}`}
accept={`.${fileTypes.join(", .")}`}
title="Hier Dokument hinziehen"
buttonText="Datei auswählen"
sx={{
height: '100%',
width: '100%',
padding: '0px',
'& svg': {
color: '#9e9e9e',
height: "100%",
width: "100%",
padding: "0px",
"& svg": {
color: "#9e9e9e",
},
'& .MuiOutlinedInput-notchedOutline': {
border: 'none',
"& .MuiOutlinedInput-notchedOutline": {
border: "none",
},
'& .MuiButton-root': {
backgroundColor: '#9e9e9e',
"& .MuiButton-root": {
backgroundColor: "#9e9e9e",
},
'& .MuiTypography-root': {
fontSize: '1.25rem',
"& .MuiTypography-root": {
fontSize: "1.25rem",
fontWeight: 500,
marginBottom: 1,
},
@ -88,13 +165,14 @@ export default function UploadPage() {
variant="contained"
sx={{
mt: 4,
backgroundColor: '#383838',
backgroundColor: "#383838",
}}
disabled={files.length === 0}
onClick={() => navigate({ to: '/extractedResult' })}
onClick={uploadFile}
>
Kennzahlen extrahieren
</Button>
</Box>
)
</>
);
}

View File

@ -1,22 +1,18 @@
import { Document, Page, pdfjs } from "react-pdf";
import { useState, useRef, useEffect } from 'react';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import { Box, IconButton } from '@mui/material';
import ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft';
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
import testPDF from '/example.pdf';
import { useEffect, useRef, useState } from "react";
import { Document, Page } from "react-pdf";
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import "react-pdf/dist/esm/Page/TextLayer.css";
import ArrowCircleLeftIcon from "@mui/icons-material/ArrowCircleLeft";
import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight";
import { Box, IconButton } from "@mui/material";
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url,
).toString();
export default function PDFViewer() {
interface PDFViewerProps {
pitchBookId: string;
}
export default function PDFViewer({ pitchBookId }: PDFViewerProps) {
const [numPages, setNumPages] = useState<number | null>(null);
const [pageNumber, setPageNumber] = useState(1);
const [containerWidth, setContainerWidth] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
@ -31,8 +27,8 @@ export default function PDFViewer() {
};
updateWidth();
window.addEventListener('resize', updateWidth);
return () => window.removeEventListener('resize', updateWidth);
window.addEventListener("resize", updateWidth);
return () => window.removeEventListener("resize", updateWidth);
}, []);
return (
@ -48,23 +44,24 @@ export default function PDFViewer() {
<Box
ref={containerRef}
sx={{
width: '100%',
maxHeight: '90vh',
overflow: 'auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: "100%",
maxHeight: "90vh",
overflow: "auto",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Document file={testPDF}
<Document
file={`http://localhost:5050/api/pitch_book/${pitchBookId}/download`}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={(error) => console.error('Es gab ein Fehler beim Laden des PDFs:', error)}
onSourceError={(error) => console.error('Ungültige PDF:', error)}>
onLoadError={(error) =>
console.error("Es gab ein Fehler beim Laden des PDFs:", error)
}
onSourceError={(error) => console.error("Ungültige PDF:", error)}
>
{containerWidth && (
<Page
pageNumber={pageNumber}
width={containerWidth * 0.8}
/>
<Page pageNumber={pageNumber} width={containerWidth * 0.8} />
)}
</Document>
</Box>
@ -75,13 +72,18 @@ export default function PDFViewer() {
justifyContent="center"
gap={1}
>
<IconButton disabled={pageNumber <= 1} onClick={() => setPageNumber(p => p - 1)}>
<IconButton
disabled={pageNumber <= 1}
onClick={() => setPageNumber((p) => p - 1)}
>
<ArrowCircleLeftIcon fontSize="large" />
</IconButton>
<span>{pageNumber} / {numPages}</span>
<span>
{pageNumber} / {numPages}
</span>
<IconButton
disabled={pageNumber >= (numPages || 1)}
onClick={() => setPageNumber(p => p + 1)}
onClick={() => setPageNumber((p) => p + 1)}
>
<ArrowCircleRightIcon fontSize="large" />
</IconButton>

View File

@ -34,9 +34,8 @@ declare module "@tanstack/react-router" {
}
}
// Initialize PDF.js worker
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
"pdfjs-dist/build/pdf.worker.min.js",
import.meta.url,
).toString();

View File

@ -11,18 +11,12 @@
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as ExtractedResultImport } from './routes/extractedResult'
import { Route as ConfigImport } from './routes/config'
import { Route as IndexImport } from './routes/index'
import { Route as ExtractedResultPitchBookImport } from './routes/extractedResult.$pitchBook'
// Create/Update Routes
const ExtractedResultRoute = ExtractedResultImport.update({
id: '/extractedResult',
path: '/extractedResult',
getParentRoute: () => rootRoute,
} as any)
const ConfigRoute = ConfigImport.update({
id: '/config',
path: '/config',
@ -35,6 +29,12 @@ const IndexRoute = IndexImport.update({
getParentRoute: () => rootRoute,
} as any)
const ExtractedResultPitchBookRoute = ExtractedResultPitchBookImport.update({
id: '/extractedResult/$pitchBook',
path: '/extractedResult/$pitchBook',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
@ -53,11 +53,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ConfigImport
parentRoute: typeof rootRoute
}
'/extractedResult': {
id: '/extractedResult'
path: '/extractedResult'
fullPath: '/extractedResult'
preLoaderRoute: typeof ExtractedResultImport
'/extractedResult/$pitchBook': {
id: '/extractedResult/$pitchBook'
path: '/extractedResult/$pitchBook'
fullPath: '/extractedResult/$pitchBook'
preLoaderRoute: typeof ExtractedResultPitchBookImport
parentRoute: typeof rootRoute
}
}
@ -68,41 +68,41 @@ declare module '@tanstack/react-router' {
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/config': typeof ConfigRoute
'/extractedResult': typeof ExtractedResultRoute
'/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/config': typeof ConfigRoute
'/extractedResult': typeof ExtractedResultRoute
'/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/config': typeof ConfigRoute
'/extractedResult': typeof ExtractedResultRoute
'/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/config' | '/extractedResult'
fullPaths: '/' | '/config' | '/extractedResult/$pitchBook'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/config' | '/extractedResult'
id: '__root__' | '/' | '/config' | '/extractedResult'
to: '/' | '/config' | '/extractedResult/$pitchBook'
id: '__root__' | '/' | '/config' | '/extractedResult/$pitchBook'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ConfigRoute: typeof ConfigRoute
ExtractedResultRoute: typeof ExtractedResultRoute
ExtractedResultPitchBookRoute: typeof ExtractedResultPitchBookRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ConfigRoute: ConfigRoute,
ExtractedResultRoute: ExtractedResultRoute,
ExtractedResultPitchBookRoute: ExtractedResultPitchBookRoute,
}
export const routeTree = rootRoute
@ -117,7 +117,7 @@ export const routeTree = rootRoute
"children": [
"/",
"/config",
"/extractedResult"
"/extractedResult/$pitchBook"
]
},
"/": {
@ -126,8 +126,8 @@ export const routeTree = rootRoute
"/config": {
"filePath": "config.tsx"
},
"/extractedResult": {
"filePath": "extractedResult.tsx"
"/extractedResult/$pitchBook": {
"filePath": "extractedResult.$pitchBook.tsx"
}
}
}

View File

@ -0,0 +1,100 @@
import ContentPasteIcon from "@mui/icons-material/ContentPaste";
import { Box, Button, Paper, Typography } from "@mui/material";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import KennzahlenTable from "../components/KennzahlenTable";
import PDFViewer from "../components/pdfViewer";
export const Route = createFileRoute("/extractedResult/$pitchBook")({
component: ExtractedResultsPage,
});
function ExtractedResultsPage() {
const { pitchBook } = Route.useParams();
const navigate = useNavigate();
const status: "green" | "yellow" | "red" = "red";
const statusColor = {
red: "#f43131",
yellow: "#f6ed48",
green: "#3fd942",
}[status];
return (
<Box p={4}>
<Box display="flex" alignItems="center" gap={3}>
<Box
sx={{
width: 45,
height: 45,
borderRadius: "50%",
backgroundColor: statusColor,
top: 32,
left: 32,
}}
/>
<Typography variant="h5" gutterBottom>
Kennzahlen extrahiert aus: <br />
<strong>FONDSNAME: TODO</strong>
</Typography>
</Box>
<Box
display="flex"
gap={4}
sx={{
width: "100vw",
maxWidth: "100%",
height: "80vh",
mt: 4,
}}
>
<Paper
elevation={2}
sx={{
width: "45%",
height: "100%",
borderRadius: 2,
backgroundColor: "#eeeeee",
padding: 2,
overflow: "auto",
}}
>
<KennzahlenTable />
</Paper>
<Box
display="flex"
flexDirection="column"
justifyContent="space-between"
gap={5}
sx={{ width: "55%", height: "95%" }}
>
<Paper
elevation={2}
sx={{
height: "100%",
borderRadius: 2,
backgroundColor: "#eeeeee",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<PDFViewer pitchBookId={pitchBook} />
</Paper>
<Box mt={2} display="flex" justifyContent="flex-end" gap={2}>
<Button variant="contained" sx={{ backgroundColor: "#383838" }}>
<ContentPasteIcon sx={{ fontSize: 18, mr: 1 }} />
Kennzahlenzeile kopieren
</Button>
<Button
variant="contained"
sx={{ backgroundColor: "#383838" }}
onClick={() => navigate({ to: "/" })}
>
Neu hochladen
</Button>
</Box>
</Box>
</Box>
</Box>
);
}

View File

@ -1,103 +0,0 @@
import { Box, Paper, Typography, Button } from '@mui/material';
import {createFileRoute, useNavigate} from '@tanstack/react-router';
import PDFViewer from '../components/pdfViewer';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import KennzahlenTable from "../components/KennzahlenTable";
export const Route = createFileRoute('/extractedResult')({
component: ExtractedResultsPage,
});
function ExtractedResultsPage() {
const navigate = useNavigate();
const status: 'green' | 'yellow' | 'red' = 'red';
const statusColor = {
red: '#f43131',
yellow: '#f6ed48',
green: '#3fd942',
}[status];
return (
<Box p={4}>
<Box display="flex" alignItems="center" gap={3}>
<Box
sx={{
width: 45,
height: 45,
borderRadius: '50%',
backgroundColor: statusColor,
top: 32,
left: 32,
}}
/>
<Typography variant="h5" gutterBottom>
Kennzahlen extrahiert aus: <br/><strong>FONDSNAME: TODO</strong>
</Typography>
</Box>
<Box
display="flex"
gap={4}
sx={{
width: '100vw',
maxWidth: '100%',
height: '80vh',
mt: 4,
}}
>
<Paper
elevation={2}
sx={{
width: '45%',
height: '100%',
borderRadius: 2,
backgroundColor: '#eeeeee',
padding: 2, // Etwas Abstand innen
overflow: 'auto', // Scrollen falls Tabelle zu lang
}}
>
<KennzahlenTable />
</Paper>
<Box
display="flex"
flexDirection="column"
justifyContent="space-between"
gap={5}
sx={{ width: '55%', height: '95%' }}
>
<Paper
elevation={2}
sx={{
height: '100%',
borderRadius: 2,
backgroundColor: '#eeeeee',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<PDFViewer/>
</Paper>
<Box mt={2} display="flex" justifyContent="flex-end" gap={2}>
<Button
variant="contained"
sx={{ backgroundColor: '#383838' }}
>
<ContentPasteIcon sx={{ fontSize: 18, mr: 1 }} />
Kennzahlenzeile kopieren
</Button>
<Button
variant="contained"
sx={{ backgroundColor: '#383838' }}
onClick={() => navigate({ to: '/' })}
>
Neu hochladen
</Button>
</Box>
</Box>
</Box>
</Box>
);
}

View File

@ -0,0 +1,6 @@
import { io } from "socket.io-client";
// "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("http://localhost:5050");