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 68a7bd3..676011c 100755 Binary files a/project/frontend/bun.lockb and b/project/frontend/bun.lockb differ diff --git a/project/frontend/package.json b/project/frontend/package.json index 87b1e9c..c3b04dd 100644 --- a/project/frontend/package.json +++ b/project/frontend/package.json @@ -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": "^9.2.1", + "socket.io-client": "^4.8.1" }, "devDependencies": { "@biomejs/biome": "1.9.4", diff --git a/project/frontend/src/components/CircularProgressWithLabel.tsx b/project/frontend/src/components/CircularProgressWithLabel.tsx new file mode 100644 index 0000000..c14698a --- /dev/null +++ b/project/frontend/src/components/CircularProgressWithLabel.tsx @@ -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 ( + + + + {`${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");