From 9aa6c8be875ebea1e09a94e05c9767d28c4779e3 Mon Sep 17 00:00:00 2001 From: Jaronim Pracht Date: Mon, 2 Jun 2025 15:03:39 +0200 Subject: [PATCH] add progress and file-upload to frontend --- project/backend/coordinator/app.py | 6 +- .../controller/pitch_book_controller.py | 3 + .../coordinator/controller/socketIO.py | 3 + project/backend/coordinator/requirements.txt | 8 + project/docker-compose.yml | 2 +- project/frontend/bun.lockb | Bin 143704 -> 146934 bytes project/frontend/package.json | 3 +- .../components/CircularProgressWithLabel.tsx | 33 +++ .../frontend/src/components/UploadPage.tsx | 268 +++++++++++------- project/frontend/src/components/pdfViewer.tsx | 168 +++++------ project/frontend/src/routeTree.gen.ts | 46 +-- .../src/routes/extractedResult.$pitchBook.tsx | 100 +++++++ .../frontend/src/routes/extractedResult.tsx | 101 ------- project/frontend/src/socket.ts | 6 + 14 files changed, 445 insertions(+), 302 deletions(-) create mode 100644 project/backend/coordinator/controller/socketIO.py create mode 100644 project/frontend/src/components/CircularProgressWithLabel.tsx create mode 100644 project/frontend/src/routes/extractedResult.$pitchBook.tsx delete mode 100644 project/frontend/src/routes/extractedResult.tsx create mode 100644 project/frontend/src/socket.ts diff --git a/project/backend/coordinator/app.py b/project/backend/coordinator/app.py index 4094652..d634965 100644 --- a/project/backend/coordinator/app.py +++ b/project/backend/coordinator/app.py @@ -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) diff --git a/project/backend/coordinator/controller/pitch_book_controller.py b/project/backend/coordinator/controller/pitch_book_controller.py index afa6042..450b546 100644 --- a/project/backend/coordinator/controller/pitch_book_controller.py +++ b/project/backend/coordinator/controller/pitch_book_controller.py @@ -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() diff --git a/project/backend/coordinator/controller/socketIO.py b/project/backend/coordinator/controller/socketIO.py new file mode 100644 index 0000000..2a82575 --- /dev/null +++ b/project/backend/coordinator/controller/socketIO.py @@ -0,0 +1,3 @@ +from flask_socketio import SocketIO + +socketio = SocketIO(cors_allowed_origins="*") diff --git a/project/backend/coordinator/requirements.txt b/project/backend/coordinator/requirements.txt index 7012057..963f260 100644 --- a/project/backend/coordinator/requirements.txt +++ b/project/backend/coordinator/requirements.txt @@ -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 diff --git a/project/docker-compose.yml b/project/docker-compose.yml index e4877ba..be29228 100644 --- a/project/docker-compose.yml +++ b/project/docker-compose.yml @@ -30,7 +30,7 @@ services: timeout: 5s retries: 5 ports: - - 5000:5000 + - 5050:5000 spacy: build: diff --git a/project/frontend/bun.lockb b/project/frontend/bun.lockb index 68a7bd3706c95b9dcddf3c3e4822bba061c00e44..676011c22448c88f859b20853066f9b21747ae99 100755 GIT binary patch delta 27822 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 SGk4){FWc;GR!^=~{(k{@mxL<- delta 26116 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%0AO*UX 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 + + + {`${Math.round(props.value)}%`} + + + ); +} diff --git a/project/frontend/src/components/UploadPage.tsx b/project/frontend/src/components/UploadPage.tsx index 6916b27..c4a57d0 100644 --- a/project/frontend/src/components/UploadPage.tsx +++ b/project/frontend/src/components/UploadPage.tsx @@ -1,100 +1,178 @@ -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([]) - const fileTypes = ["pdf"]; - const navigate = useNavigate() + const [files, setFiles] = useState([]); + const [pageId, setPageId] = useState(null); + const [loadingState, setLoadingState] = useState(null); + const fileTypes = ["pdf"]; + const navigate = useNavigate(); - return ( - { + 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 && ( + ({ color: "#fff", zIndex: theme.zIndex.drawer + 1 })} + open={pageId !== null && loadingState !== null} > - - navigate({ to: '/config' })}> - - - - - - - - - + + + )} + + + navigate({ to: "/config" })}> + + - ) -} \ No newline at end of file + + + + + + + + + ); +} diff --git a/project/frontend/src/components/pdfViewer.tsx b/project/frontend/src/components/pdfViewer.tsx index 2e8f7d1..5bb6b96 100644 --- a/project/frontend/src/components/pdfViewer.tsx +++ b/project/frontend/src/components/pdfViewer.tsx @@ -1,91 +1,99 @@ +import { useEffect, useRef, useState } from "react"; 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 "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, + "pdfjs-dist/build/pdf.worker.min.mjs", + import.meta.url, ).toString(); -export default function PDFViewer() { - const [numPages, setNumPages] = useState(null); - const [pageNumber, setPageNumber] = useState(1); - const [containerWidth, setContainerWidth] = useState(null); +interface PDFViewerProps { + pitchBookId: string; +} - const containerRef = useRef(null); +export default function PDFViewer({ pitchBookId }: PDFViewerProps) { + const [numPages, setNumPages] = useState(null); + const [pageNumber, setPageNumber] = useState(1); + const [containerWidth, setContainerWidth] = useState(null); + const containerRef = useRef(null); - const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => { - setNumPages(numPages); + const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => { + setNumPages(numPages); + }; + + useEffect(() => { + const updateWidth = () => { + if (containerRef.current) { + setContainerWidth(containerRef.current.offsetWidth); + } }; - useEffect(() => { - const updateWidth = () => { - if (containerRef.current) { - setContainerWidth(containerRef.current.offsetWidth); - } - }; + updateWidth(); + window.addEventListener("resize", updateWidth); + return () => window.removeEventListener("resize", updateWidth); + }, []); - updateWidth(); - window.addEventListener('resize', updateWidth); - return () => window.removeEventListener('resize', updateWidth); - }, []); - - return ( - + + + console.error("Es gab ein Fehler beim Laden des PDFs:", error) + } + onSourceError={(error) => console.error("Ungültige PDF:", error)} > - - console.error('Es gab ein Fehler beim Laden des PDFs:', error)} - onSourceError={(error) => console.error('Ungültige PDF:', error)}> - {containerWidth && ( - - )} - - - - setPageNumber(p => p - 1)}> - - - {pageNumber} / {numPages} - = (numPages || 1)} - onClick={() => setPageNumber(p => p + 1)} - > - - - - - ); -} \ No newline at end of file + {containerWidth && ( + + )} + + + + setPageNumber((p) => p - 1)} + > + + + + {pageNumber} / {numPages} + + = (numPages || 1)} + onClick={() => setPageNumber((p) => p + 1)} + > + + + + + ); +} diff --git a/project/frontend/src/routeTree.gen.ts b/project/frontend/src/routeTree.gen.ts index cade514..b3c2de7 100644 --- a/project/frontend/src/routeTree.gen.ts +++ b/project/frontend/src/routeTree.gen.ts @@ -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" } } } diff --git a/project/frontend/src/routes/extractedResult.$pitchBook.tsx b/project/frontend/src/routes/extractedResult.$pitchBook.tsx new file mode 100644 index 0000000..5f5fe37 --- /dev/null +++ b/project/frontend/src/routes/extractedResult.$pitchBook.tsx @@ -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 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 ( + + + + + Kennzahlen extrahiert aus:
+ FONDSNAME: TODO +
+
+ + + To-do: Table hierhin + + + + + + + + + + + +
+ ); +} diff --git a/project/frontend/src/routes/extractedResult.tsx b/project/frontend/src/routes/extractedResult.tsx deleted file mode 100644 index 15ce5b8..0000000 --- a/project/frontend/src/routes/extractedResult.tsx +++ /dev/null @@ -1,101 +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'; - -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 ( - - - - - Kennzahlen extrahiert aus:
FONDSNAME: TODO -
-
- - - To-do: Table hierhin - - - - - - - - - - - -
- ); -} \ No newline at end of file diff --git a/project/frontend/src/socket.ts b/project/frontend/src/socket.ts new file mode 100644 index 0000000..763c173 --- /dev/null +++ b/project/frontend/src/socket.ts @@ -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");