Compare commits

..

No commits in common. "c9de2cb027134b29302856f5f0ca0e9235cb75c0" and "09c314eea3ce3b6435de7f890ed4d9329823a8a4" have entirely different histories.

46 changed files with 452 additions and 12908 deletions

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@ from model.database import init_db
from controller.socketIO import socketio
from controller.kennzahlen import kennzahlen_bp
app = Flask(__name__)
CORS(app)
socketio.init_app(app)
@ -26,7 +25,6 @@ register_routes(app)
# Register blueprints
app.register_blueprint(kennzahlen_bp)
@app.route("/health")
def health_check():
return "OK"

View File

@ -1,4 +1,4 @@
from controller.spacy_controller import spacy_controller
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

View File

@ -0,0 +1,61 @@
from flask import Blueprint, jsonify, request
from model.kennzahl import Kennzahl
from model.database import db
kennzahlen_bp = Blueprint('kennzahlen', __name__)
# Beispieldaten
EXAMPLE_DATA = [
{"pdf_id": "example", "label": "Fondsname", "value": "Fund Real Estate Prime Europe", "page": 1, "status": "ok"},
{"pdf_id": "example", "label": "Fondsmanager", "value": "", "page": 1, "status": "error"},
{"pdf_id": "example", "label": "Risikoprofil", "value": "Core/Core+", "page": 10, "status": "warning"},
{"pdf_id": "example", "label": "LTV", "value": "30-35 %", "page": 8, "status": "ok"},
{"pdf_id": "example", "label": "Ausschüttungsrendite", "value": "4%", "page": 34, "status": "ok"}
]
@kennzahlen_bp.route('/api/kennzahlen/init', methods=['POST'])
def init_kennzahlen():
try:
# Lösche existierende Beispieldaten
Kennzahl.query.filter_by(pdf_id='example').delete()
# Füge Beispieldaten ein
for data in EXAMPLE_DATA:
kennzahl = Kennzahl(
pdf_id=data['pdf_id'],
label=data['label'],
value=data['value'],
page=data['page'],
status=data['status']
)
db.session.add(kennzahl)
db.session.commit()
return jsonify({"message": "Kennzahlen erfolgreich initialisiert"})
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500
@kennzahlen_bp.route('/api/kennzahlen', methods=['GET'])
def get_kennzahlen():
pdf_id = request.args.get('pdf_id', 'example') # Default zu 'example' für Beispieldaten
kennzahlen = Kennzahl.query.filter_by(pdf_id=pdf_id).all()
return jsonify([k.to_dict() for k in kennzahlen])
@kennzahlen_bp.route('/api/kennzahlen/<label>', methods=['PUT'])
def update_kennzahl(label):
data = request.get_json()
pdf_id = request.args.get('pdf_id', 'example') # Default zu 'example' für Beispieldaten
kennzahl = Kennzahl.query.filter_by(pdf_id=pdf_id, label=label).first()
if not kennzahl:
return jsonify({'error': 'Kennzahl nicht gefunden'}), 404
kennzahl.value = data.get('value', kennzahl.value)
db.session.commit()
return jsonify(kennzahl.to_dict())

View File

@ -35,7 +35,7 @@ def create_kpi_setting():
"translation",
"example",
"position",
"active",
"active"
]
for field in required_fields:
if field not in data:
@ -61,7 +61,7 @@ def create_kpi_setting():
translation=data["translation"],
example=data["example"],
position=data["position"],
active=data["active"],
active=data["active"]
)
db.session.add(new_kpi_setting)
@ -136,12 +136,7 @@ def update_kpi_positions():
try:
for update_item in data:
if "id" not in update_item or "position" not in update_item:
return (
jsonify(
{"error": "Each item must have 'id' and 'position' fields"}
),
400,
)
return jsonify({"error": "Each item must have 'id' and 'position' fields"}), 400
kpi_setting = KPISettingModel.query.get_or_404(update_item["id"])
kpi_setting.position = update_item["position"]

View File

@ -19,6 +19,6 @@ def progress():
):
return jsonify({"error": "Invalid progress value"}), 400
socketio.emit("progress", {"id": int(data["id"]), "progress": data["progress"]})
socketio.emit("progress", {"id": data["id"], "progress": data["progress"]})
# Process the data and return a response
return jsonify({"message": "Progress updated"})

View File

@ -1,11 +1,10 @@
from flask import Blueprint, request, jsonify, send_file
from io import BytesIO
from model.spacy_model import SpacyModel
import puremagic
from werkzeug.utils import secure_filename
from model.database import db
import os
import json
spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy")
@ -92,39 +91,3 @@ def delete_file(id):
db.session.commit()
return jsonify({"message": f"File {id} deleted successfully"}), 200
@spacy_controller.route("/append-training-entry", methods=["POST"])
def append_training_entry():
entry = request.get_json()
if not entry or "text" not in entry or "entities" not in entry:
return (
jsonify(
{"error": "Ungültiges Format 'text' und 'entities' erforderlich."}
),
400,
)
path = os.path.join("spacy_training", "annotation_data.json")
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = []
if entry in data:
return jsonify({"message": "Eintrag existiert bereits."}), 200
data.append(entry)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return jsonify({"message": "Eintrag erfolgreich gespeichert."}), 200
except Exception as e:
print(f"[ERROR] Fehler beim Schreiben: {e}")
return jsonify({"error": "Interner Fehler beim Schreiben."}), 500

View File

@ -14,5 +14,4 @@ def init_db(app):
with app.app_context():
db.create_all()
from model.seed_data import seed_default_kpi_settings
seed_default_kpi_settings()

View File

@ -0,0 +1,26 @@
from .database import db
class Kennzahl(db.Model):
__tablename__ = 'kennzahlen'
id = db.Column(db.Integer, primary_key=True)
pdf_id = db.Column(db.String(100), nullable=False) # ID des PDFs
label = db.Column(db.String(100), nullable=False)
value = db.Column(db.String(100))
page = db.Column(db.Integer)
status = db.Column(db.String(20))
# Zusammengesetzter Unique-Constraint für pdf_id und label
__table_args__ = (
db.UniqueConstraint('pdf_id', 'label', name='unique_pdf_kennzahl'),
)
def to_dict(self):
return {
'pdf_id': self.pdf_id,
'label': self.label,
'value': self.value,
'page': self.page,
'status': self.status
}

View File

@ -38,12 +38,10 @@ class KPISettingModel(db.Model):
"translation": self.translation,
"example": self.example,
"position": self.position,
"active": self.active,
"active": self.active
}
def __init__(
self, name, description, mandatory, type, translation, example, position, active
):
def __init__(self, name, description, mandatory, type, translation, example, position, active):
self.name = name
self.description = description
self.mandatory = mandatory

View File

@ -1,7 +1,6 @@
from model.database import db
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import LargeBinary
from datetime import datetime
class PitchBookModel(db.Model):
@ -9,15 +8,9 @@ class PitchBookModel(db.Model):
filename: Mapped[str] = mapped_column()
file: Mapped[bytes] = mapped_column(LargeBinary)
kpi: Mapped[str | None]
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
def to_dict(self):
return {
"id": self.id,
"filename": self.filename,
"kpi": self.kpi,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
return {"id": self.id, "filename": self.filename, "kpi": self.kpi}
def __init__(self, filename, file):
self.filename = filename

View File

@ -1,7 +1,6 @@
from model.database import db
from model.kpi_setting_model import KPISettingModel, KPISettingType
def seed_default_kpi_settings():
if KPISettingModel.query.first() is not None:
print("KPI Settings bereits vorhanden, Seeding übersprungen")
@ -16,7 +15,7 @@ def seed_default_kpi_settings():
"translation": "Fund Name",
"example": "Alpha Real Estate Fund I",
"position": 1,
"active": True,
"active": True
},
{
"name": "Fondsmanager",
@ -26,7 +25,7 @@ def seed_default_kpi_settings():
"translation": "Fund Manager",
"example": "Max Mustermann",
"position": 2,
"active": True,
"active": True
},
{
"name": "AIFM",
@ -36,7 +35,7 @@ def seed_default_kpi_settings():
"translation": "AIFM",
"example": "Alpha Investment Management GmbH",
"position": 3,
"active": True,
"active": True
},
{
"name": "Datum",
@ -46,7 +45,7 @@ def seed_default_kpi_settings():
"translation": "Date",
"example": "05.05.2025",
"position": 4,
"active": True,
"active": True
},
{
"name": "Risikoprofil",
@ -56,7 +55,7 @@ def seed_default_kpi_settings():
"translation": "Risk Profile",
"example": "Core/Core++",
"position": 5,
"active": True,
"active": True
},
{
"name": "Artikel",
@ -66,7 +65,7 @@ def seed_default_kpi_settings():
"translation": "Article",
"example": "Artikel 8",
"position": 6,
"active": True,
"active": True
},
{
"name": "Zielrendite",
@ -76,7 +75,7 @@ def seed_default_kpi_settings():
"translation": "Target Return",
"example": "6.5",
"position": 7,
"active": True,
"active": True
},
{
"name": "Rendite",
@ -86,7 +85,7 @@ def seed_default_kpi_settings():
"translation": "Return",
"example": "5.8",
"position": 8,
"active": True,
"active": True
},
{
"name": "Zielausschüttung",
@ -96,7 +95,7 @@ def seed_default_kpi_settings():
"translation": "Target Distribution",
"example": "4.0",
"position": 9,
"active": True,
"active": True
},
{
"name": "Ausschüttung",
@ -106,7 +105,7 @@ def seed_default_kpi_settings():
"translation": "Distribution",
"example": "3.8",
"position": 10,
"active": True,
"active": True
},
{
"name": "Laufzeit",
@ -116,7 +115,7 @@ def seed_default_kpi_settings():
"translation": "Duration",
"example": "7 Jahre, 10, Evergreen",
"position": 11,
"active": True,
"active": True
},
{
"name": "LTV",
@ -126,7 +125,7 @@ def seed_default_kpi_settings():
"translation": "LTV",
"example": "65.0",
"position": 12,
"active": True,
"active": True
},
{
"name": "Managementgebühren",
@ -136,7 +135,7 @@ def seed_default_kpi_settings():
"translation": "Management Fees",
"example": "1.5",
"position": 13,
"active": True,
"active": True
},
{
"name": "Sektorenallokation",
@ -146,7 +145,7 @@ def seed_default_kpi_settings():
"translation": "Sector Allocation",
"example": "Büro, Wohnen, Logistik, Studentenwohnen",
"position": 14,
"active": True,
"active": True
},
{
"name": "Länderallokation",
@ -156,8 +155,8 @@ def seed_default_kpi_settings():
"translation": "Country Allocation",
"example": "Deutschland,Frankreich, Österreich, Schweiz",
"position": 15,
"active": True,
},
"active": True
}
]
print("Füge Standard KPI Settings hinzu...")
@ -171,16 +170,14 @@ def seed_default_kpi_settings():
translation=kpi_data["translation"],
example=kpi_data["example"],
position=kpi_data["position"],
active=kpi_data["active"],
active=kpi_data["active"]
)
db.session.add(kpi_setting)
try:
db.session.commit()
print(
f"Erfolgreich {len(default_kpi_settings)} Standard KPI Settings hinzugefügt"
)
print(f"Erfolgreich {len(default_kpi_settings)} Standard KPI Settings hinzugefügt")
except Exception as e:
db.session.rollback()
print(f"Fehler beim Hinzufügen der Standard KPI Settings: {e}")

View File

@ -3,19 +3,12 @@ from extractSpacy import extract
import requests
import os
import json
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
VALIDATE_SERVICE_URL = os.getenv("VALIDATE_SERVICE_URL", "http://localhost:5054/validate")
VALIDATE_SERVICE_URL = os.getenv(
"VALIDATE_SERVICE_URL", "http://localhost:5054/validate"
)
@app.route("/extract", methods=["POST"])
@app.route('/extract', methods=['POST'])
def extract_pdf():
json_data = request.get_json()
@ -23,19 +16,19 @@ def extract_pdf():
pages_data = json_data["extracted_text_per_page"]
entities_json = extract(pages_data)
entities = (
json.loads(entities_json) if isinstance(entities_json, str) else entities_json
)
entities = json.loads(entities_json) if isinstance(entities_json, str) else entities_json
validate_payload = {"id": pitchbook_id, "service": "spacy", "entities": entities}
validate_payload = {
"id": pitchbook_id,
"service": "spacy",
"entities": entities
}
print(f"[SPACY] Sending to validate service: {VALIDATE_SERVICE_URL}")
print(f"[SPACY] Payload: {validate_payload} entities for pitchbook {pitchbook_id}")
try:
response = requests.post(
VALIDATE_SERVICE_URL, json=validate_payload, timeout=600
)
response = requests.post(VALIDATE_SERVICE_URL, json=validate_payload, timeout=600)
print(f"[SPACY] Validate service response: {response.status_code}")
if response.status_code != 200:
print(f"[SPACY] Validate service error: {response.text}")
@ -45,40 +38,5 @@ def extract_pdf():
return jsonify("Sent to validate-service"), 200
@app.route("/append-training-entry", methods=["POST"])
def append_training_entry():
entry = request.get_json()
if not entry or "text" not in entry or "entities" not in entry:
return (
jsonify(
{"error": "Ungültiges Format 'text' und 'entities' erforderlich."}
),
400,
)
path = os.path.join("spacy_training", "annotation_data.json")
try:
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = []
# Optional: Duplikate prüfen
if entry in data:
return jsonify({"message": "Eintrag existiert bereits."}), 200
data.append(entry)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return jsonify({"message": "Eintrag erfolgreich gespeichert."}), 200
except Exception as e:
print(f"[ERROR] Fehler beim Schreiben der Datei: {e}")
return jsonify({"error": "Interner Fehler beim Schreiben."}), 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5052, debug=True)

View File

@ -1,35 +0,0 @@
from flask import Flask, request, jsonify
import os
import json
app = Flask(__name__)
ANNOTATION_FILE = (
"spacy_training/annotation_data.json" # relativer Pfad im Container/Projekt
)
@app.route("/api/spacy-training-entry", methods=["POST"])
def append_training_entry():
new_entry = request.get_json()
if not new_entry or "text" not in new_entry or "entities" not in new_entry:
return jsonify({"error": "Ungültiges Format"}), 400
# Bestehende Datei laden
if os.path.exists(ANNOTATION_FILE):
with open(ANNOTATION_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = []
# Optional: Duplikat vermeiden
if new_entry in data:
return jsonify({"message": "Eintrag bereits vorhanden."}), 200
# Anfügen
data.append(new_entry)
with open(ANNOTATION_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return jsonify({"message": "Eintrag erfolgreich gespeichert."}), 200

View File

@ -1,130 +0,0 @@
[paths]
train = null
dev = null
vectors = null
init_tok2vec = null
[system]
seed = 0
gpu_allocator = null
[nlp]
lang = "de"
pipeline = ["ner"]
disabled = []
before_creation = null
after_creation = null
after_pipeline_creation = null
batch_size = 1000
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
vectors = {"@vectors":"spacy.Vectors.v1"}
[components]
[components.ner]
factory = "ner"
incorrect_spans_key = null
moves = null
scorer = {"@scorers":"spacy.ner_scorer.v1"}
update_with_oracle_cut_size = 100
[components.ner.model]
@architectures = "spacy.TransitionBasedParser.v2"
state_type = "ner"
extra_state_tokens = false
hidden_width = 64
maxout_pieces = 2
use_upper = true
nO = null
[components.ner.model.tok2vec]
@architectures = "spacy.HashEmbedCNN.v2"
pretrained_vectors = null
width = 96
depth = 4
embed_size = 2000
window_size = 1
maxout_pieces = 3
subword_features = true
[corpora]
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[training]
seed = ${system.seed}
gpu_allocator = ${system.gpu_allocator}
dropout = 0.1
accumulate_gradient = 1
patience = 1600
max_epochs = 0
max_steps = 20000
eval_frequency = 200
frozen_components = []
annotating_components = []
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
before_to_disk = null
before_update = null
[training.batcher]
@batchers = "spacy.batch_by_words.v1"
discard_oversize = false
tolerance = 0.2
get_length = null
[training.batcher.size]
@schedules = "compounding.v1"
start = 100
stop = 1000
compound = 1.001
t = 0.0
[training.logger]
@loggers = "spacy.ConsoleLogger.v1"
progress_bar = false
[training.optimizer]
@optimizers = "Adam.v1"
beta1 = 0.9
beta2 = 0.999
L2_is_weight_decay = true
L2 = 0.01
grad_clip = 1.0
use_averages = false
eps = 0.00000001
learn_rate = 0.001
[training.score_weights]
ents_f = 1.0
ents_p = 0.0
ents_r = 0.0
ents_per_type = null
[pretraining]
[initialize]
vectors = ${paths.vectors}
init_tok2vec = ${paths.init_tok2vec}
vocab_data = null
lookups = null
before_init = null
after_init = null
[initialize.components]
[initialize.tokenizer]

View File

@ -1,63 +0,0 @@
{
"lang":"de",
"name":"pipeline",
"version":"0.0.0",
"spacy_version":">=3.8.7,<3.9.0",
"description":"",
"author":"",
"email":"",
"url":"",
"license":"",
"spacy_git_version":"4b65aa7",
"vectors":{
"width":0,
"vectors":0,
"keys":0,
"name":null,
"mode":"default"
},
"labels":{
"ner":[
"AUSSCH\u00dcTTUNGSRENDITE",
"ESFCG",
"HHGK",
"KENNZAHL",
"LAUFZEIT",
"LAUFZEIT2",
"L\u00c4NDERALLOKATION",
"MANAGMENTGEB\u00dcHREN",
"RENDITE",
"RISIKOPROFIL",
"SEKTORENALLOKATION",
"TEST",
"TEST3",
"TEST323",
"TEST33",
"TEST34",
"TEST345",
"TEST35",
"TEST36",
"TEST37",
"TEST4",
"TEST44",
"TEST45",
"TEST5",
"TEST65",
"TEST67",
"TEST88",
"TEST99",
"ZIELAUSSCH\u00dcTTUNG",
"ZIELRENDITE",
"ZZGER 33"
]
},
"pipeline":[
"ner"
],
"components":[
"ner"
],
"disabled":[
]
}

View File

@ -1,13 +0,0 @@
{
"moves":null,
"update_with_oracle_cut_size":100,
"multitasks":[
],
"min_action_freq":1,
"learn_tokens":false,
"beam_width":1,
"beam_density":0.0,
"beam_update_prob":0.0,
"incorrect_spans_key":null
}

View File

@ -1 +0,0 @@
¥movesÚÈ{"0":{},"1":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"2":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"3":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"4":{"":1,"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"5":{"":1}}£cfg<66>§neg_keyÀ

File diff suppressed because one or more lines are too long

View File

@ -1,122 +0,0 @@
[
{
"text": "Core",
"entities": [
[
0,
4,
"RISIKOPROFIL"
]
]
},
{
"text": "Core+",
"entities": [
[
0,
5,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+",
"entities": [
[
0,
10,
"RISIKOPROFIL"
]
]
},
{
"text": "Value Add",
"entities": [
[
0,
9,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Value Add",
"entities": [
[
0,
14,
"RISIKOPROFIL"
]
]
},
{
"text": "Core+/Value Add",
"entities": [
[
0,
15,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+/Value Add",
"entities": [
[
0,
20,
"RISIKOPROFIL"
]
]
},
{
"text": "The RE portfolio of the fund is a good illustration of Fond expertise in European core/core+ investments .",
"entities": [
[
82,
92,
"RISIKOPROFIL"
]
]
},
{
"text": "Risk level: Core/Core+",
"entities": [
[
12,
22,
"RISIKOPROFIL"
]
]
},
{
"text": "Different risk profile (core, core+, value-added)",
"entities": [
[
24,
48,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+ with OpCo premium",
"entities": [
[
0,
10,
"RISIKOPROFIL"
]
]
},
{
"text": "Core /Core+ Assets, well-established = Key Gateway Cities in Europe le.g. hotels in the market with minor asset London, Paris, Amsterdam, Berlin] management initiatives",
"entities": [
[
0,
11,
"RISIKOPROFIL"
]
]
}
]

View File

@ -37,8 +37,6 @@ services:
retries: 10
ports:
- 5050:5000
volumes:
- ./backend/spacy-service/spacy_training:/app/spacy_training
ocr:
build:

View File

@ -4,9 +4,7 @@ WORKDIR /usr/src/app
# install dependencies into temp directory
# this will cache them and speed up future builds
COPY package.json bun.lockb ./
#RUN bun install --frozen-lockfile
RUN bun install
RUN bun install --frozen-lockfile
COPY . .

View File

@ -3,14 +3,15 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico?v=1" />
<link rel="icon" href="/favicon.ico" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-tsrouter-app"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>Pitchbook Extractor</title>
<title>Create TanStack App - frontend</title>
</head>
<body>
<div id="app"></div>

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,6 @@
"@tanstack/react-router": "^1.114.3",
"@tanstack/react-router-devtools": "^1.114.3",
"@tanstack/router-plugin": "^1.114.3",
"file-saver": "^2.0.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-material-file-upload": "^0.0.4",
@ -34,7 +33,6 @@
"@biomejs/biome": "1.9.4",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/file-saver": "^2.0.7",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@ -3,7 +3,6 @@ import { Box, Typography, Button, Paper, TextField, FormControlLabel,
import { useState, useEffect } from "react";
import type { Kennzahl } from "../types/kpi";
import { typeDisplayMapping } from "../types/kpi";
// import { saveAs } from "file-saver";
interface KPIFormProps {
mode: 'add' | 'edit';
@ -20,9 +19,7 @@ const emptyKPI: Partial<Kennzahl> = {
type: 'string',
translation: '',
example: '',
active: true,
exampleText: '',
markedValue: '',
active: true
};
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }: KPIFormProps) {
@ -43,60 +40,16 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
return;
}
if (!formData.exampleText?.trim()) {
alert('Beispielsatz ist erforderlich');
return;
}
if (!formData.markedValue?.trim()) {
alert('Bezeichneter Wert im Satz ist erforderlich');
return;
}
setIsSaving(true);
try {
const spacyEntry = generateSpacyEntry(formData);
//in localStorage merken
const stored = localStorage.getItem("spacyData");
const existingData = stored ? JSON.parse(stored) : [];
const updated = [...existingData, spacyEntry];
localStorage.setItem("spacyData", JSON.stringify(updated));
// an Flask senden
const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(spacyEntry)
});
const data = await response.json();
console.log("Response von /append-training-entry:", data);
if (!response.ok) {
throw new Error(data.error || "Fehler beim Aufruf von append-training-entry");
}
if (!response.ok) {
throw new Error("Fehler vom Backend: " + response.status);
}
// anschließend in der Datenbank speichern
await onSave(formData);
alert("SpaCy-Eintrag erfolgreich gespeichert!");
} catch (e: any) {
alert(e.message || "Fehler beim Erzeugen des Trainingsbeispiels.");
console.error(e);
} catch (error) {
console.error('Error saving KPI:', error);
} finally {
setIsSaving(false);
}
};
const handleCancel = () => {
onCancel();
};
@ -153,40 +106,18 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beispielsatz
Beschreibung
</Typography>
<TextField
fullWidth
multiline
rows={3}
label="Beispielsatz"
required
value={formData.exampleText || ''}
onChange={(e) => updateField('exampleText', e.target.value)}
error={!formData.exampleText?.trim()}
helperText={
!formData.exampleText?.trim()
? "Beispielsatz ist erforderlich"
: "Ein vollständiger Satz, in dem der markierte Begriff vorkommt"
}
label="Beschreibung"
value={formData.description || ''}
onChange={(e) => updateField('description', e.target.value)}
helperText="Beschreibung der Kennzahl"
/>
<TextField
fullWidth
required
sx={{ mt: 2 }}
label="Bezeichneter Wert im Satz *"
value={formData.markedValue || ''}
onChange={(e) => updateField('markedValue', e.target.value)}
error={!formData.markedValue?.trim()}
helperText={
!formData.markedValue?.trim()
? "Markierter Begriff ist erforderlich"
: "Nur der Begriff, der im Satz markiert werden soll (z.B. Core/Core+)"
}
/>
<Box mt={3}>
<FormControlLabel
control={
@ -314,27 +245,3 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
</Paper>
);
}
function generateSpacyEntry(formData: Partial<Kennzahl>) {
const text = formData.exampleText?.trim() || "";
const value = formData.markedValue?.trim() || "";
const label = formData.name?.trim().toUpperCase() || "";
const start = text.indexOf(value);
if (start === -1) {
throw new Error("Bezeichneter Begriff wurde im Satz nicht gefunden.");
}
return {
text,
entities: [[start, start + value.length, label]],
};
}
// function appendAndDownload(newEntry: any, existing: any[] = []) {
// const updated = [...existing, newEntry];
// const blob = new Blob([JSON.stringify(updated, null, 2)], {
// type: "application/json",
// });
// saveAs(blob, "..\project\backend\spacy-service\spacy_training\annotation_data.json");
// }

View File

@ -42,43 +42,25 @@ export default function KennzahlenTable({
data,
pdfId,
settings,
from,
from
}: KennzahlenTableProps) {
const [editingIndex, setEditingIndex] = useState<string>("");
const [editValue, setEditValue] = useState("");
const [editingPageIndex, setEditingPageIndex] = useState<string>("");
const [editPageValue, setEditPageValue] = useState("");
const [hoveredPageIndex, setHoveredPageIndex] = useState<string>("");
const navigate = useNavigate();
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: (params: { id: string; newValue?: string; newPage?: number }) => {
const { id, newValue, newPage } = params;
mutationFn: (id: string) => {
const key = id.toUpperCase();
const updatedData = { ...data };
if (data[key] && data[key].length > 0) {
updatedData[key] = data[key].map((item) => ({
...item,
...(newValue !== undefined && { entity: newValue }),
...(newPage !== undefined && { page: newPage }),
}));
} else {
updatedData[key] = [{
label: key,
entity: newValue || "",
page: newPage || 0,
status: "single-source",
source: "manual"
}];
}
updatedData[key] = data[key]?.map((item) => ({
...item,
entity: editValue,
})) || [{ label: key, entity: editValue }];
return fetchPutKPI(Number(pdfId), updatedData);
},
onMutate: async (params: { id: string; newValue?: string; newPage?: number }) => {
const { id, newValue, newPage } = params;
onMutate: async (id: string) => {
await queryClient.cancelQueries({
queryKey: ["pitchBookKPI", pdfId],
});
@ -89,23 +71,10 @@ export default function KennzahlenTable({
queryClient.setQueryData(["pitchBookKPI", pdfId], () => {
const updatedData = { ...data };
if (data[key] && data[key].length > 0) {
updatedData[key] = data[key].map((item) => ({
...item,
...(newValue !== undefined && { entity: newValue }),
...(newPage !== undefined && { page: newPage }),
}));
} else {
updatedData[key] = [{
label: key,
entity: newValue || "",
page: newPage || 0,
status: "single-source",
source: "manual"
}];
}
updatedData[key] = data[key]?.map((item) => ({
...item,
entity: editValue,
})) || [{ label: key, entity: editValue }];
return updatedData;
});
@ -130,41 +99,19 @@ export default function KennzahlenTable({
setEditValue(value);
};
const startPageEditing = (value: number, index: string) => {
setEditingPageIndex(index);
setEditPageValue(value.toString());
};
// Bearbeitung beenden und Wert speichern
const handleSave = async (index: string) => {
mutate({ id: index, newValue: editValue });
// await updateKennzahl(rows[index].label, editValue);
mutate(index);
setEditingIndex("");
};
const handlePageSave = async (index: string) => {
const pageNumber = parseInt(editPageValue);
if (editPageValue === "" || pageNumber === 0) {
mutate({ id: index, newPage: 0 });
} else if (!isNaN(pageNumber) && pageNumber > 0) {
mutate({ id: index, newPage: pageNumber });
}
setEditingPageIndex("");
};
// Tastatureingaben verarbeiten
const handleKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => {
if (e.key === "Enter") {
handleSave(index);
} else if (e.key === "Escape") {
setEditingIndex("");
}
};
const handlePageKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => {
if (e.key === "Enter") {
handlePageSave(index);
} else if (e.key === "Escape") {
setEditingPageIndex("");
setEditingIndex("null");
}
};
@ -184,16 +131,14 @@ export default function KennzahlenTable({
<Table>
<TableHead>
<TableRow>
<TableCell width="30%">
<TableCell>
<strong>Kennzahl</strong>
</TableCell>
<TableCell width="55%">
<TableCell>
<strong>Wert</strong>
</TableCell>
<TableCell align="center" width="15%">
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 1 }}>
<strong>Seite</strong>
</Box>
<TableCell align="center">
<strong>Seite</strong>
</TableCell>
</TableRow>
</TableHead>
@ -220,17 +165,9 @@ export default function KennzahlenTable({
borderColor = "#f6ed48";
}
const currentPage = row.extractedValues.at(0)?.page ?? 0;
const isPageHovered = hoveredPageIndex === row.setting.name;
const canEditPage = !hasMultipleValues;
return (
<TableRow key={row.setting.name}>
<TableCell>{row.setting.name}
{row.setting.mandatory && (
<span> *</span>
)}
</TableCell>
<TableCell>{row.setting.name}</TableCell>
<TableCell
onClick={() => {
// Only allow inline editing for non-multiple value cells
@ -292,17 +229,12 @@ export default function KennzahlenTable({
</Tooltip>
) : (
<Tooltip
title={
hasNoValue ? (
<>
<b>Problem</b>
<br />
Es wurden keine Kennzahlen gefunden. Bitte
ergänzen!
</>
) : (
""
)
title={hasNoValue ?
<>
<b>Problem</b>
<br />
Es wurden keine Kennzahlen gefunden. Bitte ergänzen!
</> : ""
}
placement="bottom"
arrow
@ -329,10 +261,7 @@ export default function KennzahlenTable({
}}
>
{hasNoValue && (
<ErrorOutlineIcon
fontSize="small"
color="error"
/>
<ErrorOutlineIcon fontSize="small" color="error" />
)}
{editingIndex === row.setting.name ? (
<TextField
@ -371,99 +300,21 @@ export default function KennzahlenTable({
)}
</TableCell>
<TableCell align="center">
{editingPageIndex === row.setting.name ? (
<TextField
value={editPageValue}
onChange={(e) => {
const value = e.target.value;
if (value === '' || /^\d+$/.test(value)) {
setEditPageValue(value);
{(row.extractedValues.at(0)?.page ?? 0) > 0 ? (
<Link
component="button"
onClick={() => {
const extractedValue = row.extractedValues.at(0);
if (extractedValue?.page && extractedValue.page > 0) {
onPageClick?.(Number(extractedValue.page), extractedValue.entity || "");
}
}}
onKeyDown={(e) => handlePageKeyPress(e, row.setting.name)}
onBlur={() => handlePageSave(row.setting.name)}
autoFocus
size="small"
variant="standard"
sx={{
width: "60px",
"& .MuiInput-input": {
textAlign: "center"
}
}}
inputProps={{
min: 0,
style: { textAlign: 'center' }
}}
/>
sx={{ cursor: "pointer" }}
>
{row.extractedValues.at(0)?.page}
</Link>
) : (
<>
{currentPage > 0 ? (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
cursor: "pointer",
borderRadius: "4px",
minHeight: "32px",
minWidth: "100px",
}}
onClick={() => {
if (canEditPage) {
startPageEditing(currentPage, row.setting.name);
}
}}
>
<Link
component="button"
onClick={(e) => {
e.stopPropagation();
const extractedValue = row.extractedValues.at(0);
if (extractedValue?.page && extractedValue.page > 0) {
onPageClick?.(Number(extractedValue.page), extractedValue.entity || "");
}
}}
sx={{ cursor: "pointer" }}
>
{currentPage}
</Link>
</Box>
) : canEditPage ? (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
cursor: "pointer",
minHeight: "32px",
minWidth: "100px",
borderRadius: "4px",
backgroundColor: isPageHovered ? "#f8f9fa" : "transparent",
}}
onMouseEnter={() => setHoveredPageIndex(row.setting.name)}
onMouseLeave={() => setHoveredPageIndex("")}
onClick={() => startPageEditing(0, row.setting.name)}
>
<span style={{ color: "#999" }}>...</span>
<EditIcon
fontSize="small"
sx={{
position: "absolute",
left: "70px",
color: "black",
cursor: "pointer",
opacity: 0.7,
transition: "opacity 0.2s ease",
}}
/>
</Box>
) : (
""
)}
</>
""
)}
</TableCell>
</TableRow>

View File

@ -1,402 +1,186 @@
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import HourglassEmptyIcon from "@mui/icons-material/HourglassEmpty";
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
import {
Box,
Chip,
CircularProgress,
LinearProgress,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { Box, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, CircularProgress, Chip } from "@mui/material";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { useCallback, useEffect, useState } from "react";
import { socket } from "../socket";
import { fetchPitchBooksById } from "../util/api";
import { pitchBooksQueryOptions } from "../util/query";
import { formatDate } from "../util/date"
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
interface PitchBook {
id: number;
filename: string;
created_at: string;
kpi?:
| string
| {
[key: string]: {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
};
status?: "processing" | "completed";
id: number;
filename: string;
created_at: string;
kpi?: string | {
[key: string]: {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
};
status?: 'processing' | 'completed';
}
export function PitchBooksTable() {
const [loadingPitchBooks, setLoadingPitchBooks] = useState<
{
id: number;
progress: number;
filename?: string;
created_at?: string;
buffer: number;
intervalId?: number;
}[]
>([]);
const navigate = useNavigate();
const { data: pitchBooks, isLoading } = useSuspenseQuery(
pitchBooksQueryOptions(),
);
const navigate = useNavigate();
const { data: pitchBooks, isLoading } = useSuspenseQuery(pitchBooksQueryOptions());
const handleRowClick = (pitchBookId: number) => {
navigate({
to: "/extractedResult/$pitchBook",
params: { pitchBook: pitchBookId.toString() },
search: { from: "overview" },
});
};
const handleRowClick = (pitchBookId: number) => {
navigate({
to: "/extractedResult/$pitchBook",
params: { pitchBook: pitchBookId.toString() },
search: { from: "overview" }
});
};
const onConnection = useCallback(() => {
console.log("connected");
}, []);
const getKPIValue = (pitchBook: PitchBook, fieldName: string): string => {
if (!pitchBook.kpi || typeof pitchBook.kpi === 'string') {
try {
const parsedKPI = JSON.parse(pitchBook.kpi as string);
// Convert array to object format if needed
const kpiObj = Array.isArray(parsedKPI) ?
parsedKPI.reduce((acc: any, item: any) => {
if (!acc[item.label]) acc[item.label] = [];
acc[item.label].push(item);
return acc;
}, {}) : parsedKPI;
const queryClient = useQueryClient();
return kpiObj[fieldName]?.[0]?.entity || 'N/A';
} catch {
return 'N/A';
}
}
const onProgress = useCallback(
(progress: { id: number; progress: number }) => {
if (progress.progress === 100) {
setLoadingPitchBooks((prev) => {
const intervalId = prev.find(
(item) => item.id === progress.id,
)?.intervalId;
intervalId && clearInterval(intervalId);
return (pitchBook.kpi as any)[fieldName]?.[0]?.entity || 'N/A';
};
return [...prev.filter((item) => item.id !== progress.id)];
});
queryClient.invalidateQueries({
queryKey: pitchBooksQueryOptions().queryKey,
});
} else {
setLoadingPitchBooks((prev) => {
const oldItem = prev.find((item) => item.id === progress.id);
let intervalId = oldItem?.intervalId;
if (!oldItem) {
intervalId = setInterval(() => {
setLoadingPitchBooks((prev) => {
const oldItem = prev.find((item) => item.id === progress.id);
if (!oldItem) return prev;
const getStatus = (pitchBook: PitchBook) => {
if (pitchBook.kpi &&
((typeof pitchBook.kpi === 'string' && pitchBook.kpi !== '{}') ||
(typeof pitchBook.kpi === 'object' && Object.keys(pitchBook.kpi).length > 0))) {
return 'completed';
}
return 'processing';
};
return [
...prev.filter((e) => e.id !== progress.id),
{
id: progress.id,
progress: oldItem?.progress ?? progress.progress,
filename: oldItem?.filename,
buffer: oldItem ? oldItem.buffer + 0.5 : 0,
intervalId: oldItem.intervalId,
created_at: oldItem?.created_at,
},
];
});
}, 400);
if (isLoading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" height="400px">
<CircularProgress sx={{ color: "#383838" }} />
</Box>
);
}
fetchPitchBooksById(progress.id)
.then((res) => {
setLoadingPitchBooks((prev) => [
...prev.filter((item) => item.id !== progress.id),
{
id: progress.id,
progress: progress.progress,
filename: res.filename,
buffer: 0,
intervalId,
created_at: res.created_at,
},
]);
})
.catch((err) => {
console.error(err);
});
}
return [
...prev.filter((item) => item.id !== progress.id),
{
id: progress.id,
progress: progress.progress,
filename: oldItem?.filename,
created_at: oldItem?.created_at,
buffer: 0,
intervalId,
},
];
});
}
},
[queryClient],
);
return (
<TableContainer
component={Paper}
sx={{
width: "85%",
maxWidth: 1200,
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
}}
>
<Table>
<TableHead>
<TableRow sx={{ backgroundColor: "#f5f5f5" }}>
<TableCell sx={{ width: "60px" }}></TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Fondsname</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Fondsmanager</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Dateiname</TableCell>
<TableCell sx={{ fontWeight: "bold", width: "120px" }}>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{pitchBooks.map((pitchBook: PitchBook) => {
const status = getStatus(pitchBook);
const fundName = getKPIValue(pitchBook, 'FONDSNAME') ||
getKPIValue(pitchBook, 'FUND_NAME') ||
getKPIValue(pitchBook, 'NAME');
useEffect(() => {
socket.on("connect", onConnection);
socket.on("progress", onProgress);
return () => {
socket.off("connect", onConnection);
socket.off("progress", onProgress);
};
}, [onConnection, onProgress]);
const manager = getKPIValue(pitchBook, 'FONDSMANAGER') ||
getKPIValue(pitchBook, 'MANAGER') ||
getKPIValue(pitchBook, 'PORTFOLIO_MANAGER');
const getKPIValue = (pitchBook: PitchBook, fieldName: string): string => {
if (!pitchBook.kpi || typeof pitchBook.kpi === "string") {
try {
const parsedKPI = JSON.parse(pitchBook.kpi as string);
// Convert array to object format if needed
const kpiObj = Array.isArray(parsedKPI)
? parsedKPI.reduce((acc, item) => {
if (!acc[item.label]) acc[item.label] = [];
acc[item.label].push(item);
return acc;
}, {})
: parsedKPI;
return kpiObj[fieldName]?.[0]?.entity || "N/A";
} catch {
return "N/A";
}
}
return pitchBook.kpi[fieldName]?.[0]?.entity || "N/A";
};
const getStatus = (pitchBook: PitchBook) => {
if (
pitchBook.kpi &&
((typeof pitchBook.kpi === "string" && pitchBook.kpi !== "{}") ||
(typeof pitchBook.kpi === "object" &&
Object.keys(pitchBook.kpi).length > 0))
) {
return "completed";
}
return "processing";
};
if (isLoading) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="400px"
>
<CircularProgress sx={{ color: "#383838" }} />
</Box>
);
}
return (
<TableContainer
component={Paper}
sx={{
width: "85%",
maxWidth: 1200,
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
}}
>
<Table>
<TableHead>
<TableRow sx={{ backgroundColor: "#f5f5f5" }}>
<TableCell sx={{ width: "60px" }} />
<TableCell sx={{ fontWeight: "bold" }}>Fondsname</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Fondsmanager</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Hochgeladen am</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Dateiname</TableCell>
<TableCell sx={{ fontWeight: "bold", width: "120px" }}>
Status
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loadingPitchBooks
.sort((a, b) => a.id - b.id)
.map((pitchBook) => (
<TableRow key={pitchBook.id}>
<TableCell>
<Box
sx={{
width: 40,
height: 50,
backgroundColor: "#f0f0f0",
borderRadius: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
border: "1px solid #e0e0e0",
}}
>
<PictureAsPdfIcon fontSize="small" sx={{ color: "#666" }} />
</Box>
</TableCell>
<TableCell colSpan={2}>
<LinearProgress
variant="buffer"
value={pitchBook.progress}
valueBuffer={
pitchBook.buffer
? pitchBook.progress + pitchBook.buffer
: pitchBook.progress
}
/>
</TableCell>
<TableCell>
<Typography
variant="body2"
color="text.secondary"
fontSize="0.875rem"
>
{pitchBook.created_at && formatDate(pitchBook.created_at)}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body2"
color="text.secondary"
fontSize="0.875rem"
>
{pitchBook.filename}
</Typography>
</TableCell>
<TableCell>
<Chip
icon={<HourglassEmptyIcon />}
label="In Bearbeitung"
size="small"
sx={{
backgroundColor: "#fff3e0",
color: "#e65100",
"& .MuiChip-icon": {
color: "#e65100",
},
}}
/>
</TableCell>
</TableRow>
))}
{pitchBooks
.filter(
(pitchbook: PitchBook) =>
!loadingPitchBooks.some((e) => e.id === pitchbook.id),
)
.sort(
(a: PitchBook, b: PitchBook) =>
new Date(b.created_at).getTime() -
new Date(a.created_at).getTime(),
)
.map((pitchBook: PitchBook) => {
const status = getStatus(pitchBook);
const fundName =
getKPIValue(pitchBook, "FONDSNAME") ||
getKPIValue(pitchBook, "FUND_NAME") ||
getKPIValue(pitchBook, "NAME");
const manager =
getKPIValue(pitchBook, "FONDSMANAGER") ||
getKPIValue(pitchBook, "MANAGER") ||
getKPIValue(pitchBook, "PORTFOLIO_MANAGER");
return (
<TableRow
key={pitchBook.id}
onClick={() => handleRowClick(pitchBook.id)}
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: "#f9f9f9",
},
}}
>
<TableCell>
<Box
sx={{
width: 40,
height: 50,
backgroundColor: "#f0f0f0",
borderRadius: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
border: "1px solid #e0e0e0",
}}
>
<PictureAsPdfIcon
fontSize="small"
sx={{ color: "#666" }}
/>
</Box>
</TableCell>
<TableCell>
<Typography variant="body2" fontWeight="medium">
{fundName}
</Typography>
</TableCell>
<TableCell>{manager}</TableCell>
<TableCell>{formatDate(pitchBook.created_at)}</TableCell>
<TableCell>
<Typography
variant="body2"
color="text.secondary"
fontSize="0.875rem"
>
{pitchBook.filename}
</Typography>
</TableCell>
<TableCell>
{status === "completed" ? (
<Chip
icon={<CheckCircleIcon />}
label="Abgeschlossen"
size="small"
sx={{
backgroundColor: "#e8f5e9",
color: "#2e7d32",
"& .MuiChip-icon": {
color: "#2e7d32",
},
}}
/>
) : (
<Chip
icon={<HourglassEmptyIcon />}
label="In Bearbeitung"
size="small"
sx={{
backgroundColor: "#fff3e0",
color: "#e65100",
"& .MuiChip-icon": {
color: "#e65100",
},
}}
/>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
{pitchBooks.length === 0 && (
<Box p={4} textAlign="center">
<Typography color="text.secondary">
Keine Pitch Books vorhanden
</Typography>
</Box>
)}
</TableContainer>
);
return (
<TableRow
key={pitchBook.id}
onClick={() => handleRowClick(pitchBook.id)}
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: "#f9f9f9",
},
}}
>
<TableCell>
<Box
sx={{
width: 40,
height: 50,
backgroundColor: "#f0f0f0",
borderRadius: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
border: "1px solid #e0e0e0",
}}
>
<PictureAsPdfIcon fontSize="small" sx={{ color: "#666" }} />
</Box>
</TableCell>
<TableCell>
<Typography variant="body2" fontWeight="medium">
{fundName}
</Typography>
</TableCell>
<TableCell>{manager}</TableCell>
<TableCell>
<Typography variant="body2" color="text.secondary" fontSize="0.875rem">
{pitchBook.filename}
</Typography>
</TableCell>
<TableCell>
{status === 'completed' ? (
<Chip
icon={<CheckCircleIcon />}
label="Abgeschlossen"
size="small"
sx={{
backgroundColor: "#e8f5e9",
color: "#2e7d32",
"& .MuiChip-icon": {
color: "#2e7d32",
},
}}
/>
) : (
<Chip
icon={<HourglassEmptyIcon />}
label="In Bearbeitung"
size="small"
sx={{
backgroundColor: "#fff3e0",
color: "#e65100",
"& .MuiChip-icon": {
color: "#e65100",
},
}}
/>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
{pitchBooks.length === 0 && (
<Box p={4} textAlign="center">
<Typography color="text.secondary">
Keine Pitch Books vorhanden
</Typography>
</Box>
)}
</TableContainer>
);
}

View File

@ -1,12 +1,11 @@
import SettingsIcon from "@mui/icons-material/Settings";
import { Backdrop, Box, Button, IconButton, Paper, Typography } from "@mui/material";
import { useNavigate, useRouter } from "@tanstack/react-router";
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 { API_HOST } from "../util/api";
import { CircularProgressWithLabel } from "./CircularProgressWithLabel";
import DekaLogo from "../assets/Deka_logo.png";
import { API_HOST } from "../util/api";
export default function UploadPage() {
const [files, setFiles] = useState<File[]>([]);
@ -14,7 +13,6 @@ export default function UploadPage() {
const [loadingState, setLoadingState] = useState<number | null>(null);
const fileTypes = ["pdf"];
const navigate = useNavigate();
const router = useRouter();
const uploadFile = useCallback(async () => {
const formData = new FormData();
@ -88,50 +86,26 @@ export default function UploadPage() {
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="flex-start"
justifyContent="center"
height="100vh"
bgcolor="white"
pt={3}
>
<Box
width="100%"
maxWidth="1300px"
display="flex"
justifyContent="space-between"
alignItems="center"
px={8}
py={5}
justifyContent="flex-end"
px={2}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<img
src={DekaLogo}
alt="Company Logo"
style={{ height: "40px", width: "auto" }}
/>
</Box>
<IconButton onClick={() => navigate({ to: "/config" })}>
<SettingsIcon fontSize="large" />
</IconButton>
</Box>
<Typography
variant="h4"
component="h1"
sx={{
fontWeight: "bold",
color: "#383838",
marginBottom: 12,
marginTop: 6,
}}
>
Pitchbook Extractor
</Typography>
<Paper
elevation={3}
sx={{
width: 800,
height: 400,
width: 900,
height: 500,
backgroundColor: "#eeeeee",
borderRadius: 4,
display: "flex",
@ -204,7 +178,6 @@ export default function UploadPage() {
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
onMouseEnter={() => router.preloadRoute({ to: "/pitchbooks" })}
onClick={() => navigate({ to: "/pitchbooks" })}
>
Alle Pitch Books anzeigen

View File

@ -95,78 +95,53 @@ export default function PDFViewer({
useEffect(() => {
const tmpPos: string[] = [];
const tmpPosHighlight: string[] = [];
const textItems = textContent.filter(
(e) => e.text !== "" && e.text !== " ",
);
if (textContent.length === 0) {
setPosHighlight([]);
setPosHighlightFocus([]);
return;
}
const findTextPositions = (searchText: string): number[] => {
const positions: number[] = [];
const normalizedSearch = searchText.toLowerCase().trim();
textContent.forEach((item, index) => {
if (item.text.toLowerCase().trim() === normalizedSearch) {
positions.push(index);
}
});
if (positions.length === 0) {
let cumulativeText = '';
const textBoundaries: { start: number; end: number; index: number }[] = [];
textContent.forEach((item, index) => {
const start = cumulativeText.length;
cumulativeText += item.text;
const end = cumulativeText.length;
textBoundaries.push({ start, end, index });
});
const lowerCumulative = cumulativeText.toLowerCase();
let searchIndex = lowerCumulative.indexOf(normalizedSearch);
while (searchIndex !== -1) {
const endIndex = searchIndex + normalizedSearch.length;
textBoundaries.forEach(boundary => {
if (
(boundary.start <= searchIndex && searchIndex < boundary.end) || // Search starts in this item
(boundary.start < endIndex && endIndex <= boundary.end) || // Search ends in this item
(searchIndex <= boundary.start && boundary.end <= endIndex) // This item is completely within search
textItems.forEach((e, i) => {
for (const s of highlight
.filter((h) => h.page === pageNumber)
.map((h) => h.text)) {
if (s.split(" ")[0] === e.text) {
if (
s.split(" ").reduce((prev, curr, j) => {
return prev && curr === textItems[i + j].text;
}, true)
) {
for (
let k = textItems[i].i;
k < textItems[i + s.split(" ").length]?.i ||
k < textItems[i + s.split(" ").length - 1]?.i;
k++
) {
if (!positions.includes(boundary.index)) {
positions.push(boundary.index);
}
tmpPos.push(textContent[k].posKey);
}
});
searchIndex = lowerCumulative.indexOf(normalizedSearch, searchIndex + 1);
}
}
}
return positions.sort((a, b) => a - b);
};
highlight
.filter(h => h.page === pageNumber)
.forEach(highlightItem => {
const positions = findTextPositions(highlightItem.text);
positions.forEach(pos => {
if (pos >= 0 && pos < textContent.length) {
tmpPos.push(textContent[pos].posKey);
if (focusHighlight?.page === pageNumber) {
if (focusHighlight.text.split(" ")[0] === e.text) {
if (
focusHighlight.text.split(" ").reduce((prev, curr, j) => {
return prev && curr === textItems[i + j].text;
}, true)
) {
for (
let k = textItems[i].i;
k < textItems[i + focusHighlight.text.split(" ").length]?.i ||
k < textItems[i + focusHighlight.text.split(" ").length - 1]?.i;
k++
) {
tmpPosHighlight.push(textContent[k].posKey);
}
}
});
});
if (focusHighlight?.page === pageNumber && focusHighlight.text) {
const positions = findTextPositions(focusHighlight.text);
positions.forEach(pos => {
if (pos >= 0 && pos < textContent.length) {
tmpPosHighlight.push(textContent[pos].posKey);
}
});
}
setPosHighlight([...new Set(tmpPos)]);
setPosHighlightFocus([...new Set(tmpPosHighlight)]);
}
});
setPosHighlight(tmpPos);
setPosHighlightFocus(tmpPosHighlight);
}, [highlight, focusHighlight, pageNumber, textContent]);
const onGetTextSuccess: OnGetTextSuccess = useCallback((fullText) => {

View File

@ -1,5 +1,4 @@
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import EditIcon from "@mui/icons-material/Edit";
import {
Box,
Button,
@ -27,7 +26,7 @@ import {
useSuspenseQuery,
} from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState, type KeyboardEvent } from "react";
import { useEffect, useState } from "react";
import PDFViewer from "../components/pdfViewer";
import { fetchPutKPI } from "../util/api";
import { kpiQueryOptions } from "../util/query";
@ -66,73 +65,25 @@ function ExtractedResultsPage() {
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [customValue, setCustomValue] = useState("");
const [customPage, setCustomPage] = useState("");
const [editingCustomPage, setEditingCustomPage] = useState(false);
const [focusHighlightOverride, setFocusHighlightOverride] = useState<{ page: number; text: string } | null>(null);
const originalValue = kpiValues[0]?.entity || "";
const originalPage = kpiValues[0]?.page || 0;
// Funktion, um gleiche Werte zusammenzufassen und die Seiten zu sammeln
function groupKpiValues(values: Array<{ entity: string; page: number; [key: string]: any }>): Array<{ entity: string; pages: number[]; [key: string]: any }> {
const map = new Map<string, { entity: string; pages: number[]; [key: string]: any }>();
values.forEach((item: { entity: string; page: number; [key: string]: any }) => {
const key = item.entity.toLowerCase();
if (!map.has(key)) {
map.set(key, { ...item, pages: [item.page] });
} else {
const existingEntry = map.get(key)!;
if (!existingEntry.pages.includes(item.page)) {
existingEntry.pages.push(item.page);
}
}
});
return Array.from(map.values());
}
const groupedKpiValues: Array<{ entity: string; pages: number[]; [key: string]: any }> = groupKpiValues(kpiValues);
const selectedValue: string =
selectedIndex === -1 ? customValue : groupedKpiValues[selectedIndex]?.entity || "";
const selectedPage =
selectedIndex === -1
? (parseInt(customPage) > 0 ? parseInt(customPage) : 1)
: groupedKpiValues[selectedIndex]?.pages[0] || 1;
// Um zu prüfen, ob der Wert nur aus Leerzeichen besteht
const isSelectedValueEmpty = selectedIndex === -1 ? customValue.trim() === "" : !selectedValue;
const focusHighlight = focusHighlightOverride || {
page: groupedKpiValues.at(selectedIndex)?.pages[0] || -1,
text: groupedKpiValues.at(selectedIndex)?.entity || "",
};
const selectedValue =
selectedIndex === -1 ? customValue : kpiValues[selectedIndex]?.entity || "";
useEffect(() => {
const valueChanged = selectedValue !== originalValue;
const pageChanged = selectedPage !== originalPage;
setHasChanges(valueChanged || pageChanged);
}, [selectedValue, selectedPage, originalValue, originalPage]);
setHasChanges(selectedValue !== originalValue);
}, [selectedValue, originalValue]);
const { mutate: updateKPI } = useMutation({
mutationFn: () => {
const updatedData = { ...kpiData };
let baseObject;
if (selectedIndex >= 0) {
// Das Originalobjekt mit allen Feldern für diesen Wert suchen
const original = kpiValues.find(v => v.entity.toLowerCase() === groupedKpiValues[selectedIndex].entity.toLowerCase()) as { status?: string; source?: string } | undefined;
baseObject = {
label: kpi.toUpperCase(),
entity: groupedKpiValues[selectedIndex].entity,
page: groupedKpiValues[selectedIndex].pages[0],
status: original?.status || "single-source",
source: original?.source || "auto",
};
baseObject = kpiValues[selectedIndex];
} else {
baseObject = {
label: kpi.toUpperCase(),
entity: selectedValue,
page: selectedPage,
page: 0,
status: "single-source",
source: "manual",
};
@ -141,7 +92,6 @@ function ExtractedResultsPage() {
{
...baseObject,
entity: selectedValue,
page: selectedPage,
},
];
return fetchPutKPI(Number(pitchBook), updatedData);
@ -165,14 +115,11 @@ function ExtractedResultsPage() {
const value = event.target.value;
if (value === "custom") {
setSelectedIndex(-1);
setFocusHighlightOverride(null);
} else {
const index = Number.parseInt(value);
setSelectedIndex(index);
setCurrentPage(groupedKpiValues[index].pages[0]);
setCurrentPage(kpiValues[index].page);
setCustomValue("");
setCustomPage("");
setFocusHighlightOverride(null);
}
};
@ -182,33 +129,12 @@ function ExtractedResultsPage() {
const value = event.target.value;
setCustomValue(value);
setSelectedIndex(-1);
setFocusHighlightOverride(null);
}
const handleCustomPageChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const value = event.target.value;
// Allow empty string or positive numbers only (no 0)
if (value === '' || (/^\d+$/.test(value) && parseInt(value) > 0)) {
setCustomPage(value);
}
};
const handleRowClick = (index: number) => {
setCurrentPage(groupedKpiValues[index].pages[0]);
setCurrentPage(kpiValues[index].page);
setSelectedIndex(index);
setCustomValue("");
setCustomPage("");
setFocusHighlightOverride(null);
};
const handlePageClick = (page: number, entity: string) => {
setCurrentPage(page);
setFocusHighlightOverride({
page: page,
text: entity,
});
};
const handleBackClick = () => {
@ -240,16 +166,6 @@ function ExtractedResultsPage() {
updateKPI();
};
const startCustomPageEditing = () => {
setEditingCustomPage(true);
};
const handleCustomPageKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" || e.key === "Escape") {
setEditingCustomPage(false);
}
};
return (
<Box p={4}>
<Box sx={{ display: "flex", alignItems: "center", mb: 3 }}>
@ -287,18 +203,18 @@ function ExtractedResultsPage() {
<Table>
<TableHead>
<TableRow>
<TableCell width="85%">
<TableCell>
<strong>Gefundene Werte</strong>
</TableCell>
<TableCell align="center" width="15%">
<strong>Seiten</strong>
<TableCell align="center">
<strong>Seite</strong>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{groupedKpiValues.map((item, index) => (
{kpiValues.map((item, index) => (
<TableRow
key={`${item.entity}_${item.pages.join('_')}_${index}`}
key={`${item.entity}_${item.page}_${index}`}
sx={{
"&:hover": { backgroundColor: "#f9f9f9" },
cursor: "pointer",
@ -337,19 +253,16 @@ function ExtractedResultsPage() {
</Box>
</TableCell>
<TableCell align="center">
{item.pages.map((page: number, i: number) => (
<Link
key={page}
component="button"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
handlePageClick(page, item.entity);
}}
sx={{ cursor: "pointer", ml: i > 0 ? 1 : 0 }}
>
{page}
</Link>
))}
<Link
component="button"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
setCurrentPage(item.page);
}}
sx={{ cursor: "pointer" }}
>
{item.page}
</Link>
</TableCell>
</TableRow>
))}
@ -369,7 +282,6 @@ function ExtractedResultsPage() {
}}
onClick={() => {
setSelectedIndex(-1);
setFocusHighlightOverride(null);
}}
>
<Radio
@ -386,84 +298,25 @@ function ExtractedResultsPage() {
},
}}
/>
<Box sx={{ width: '100%' }}>
<TextField
placeholder="Einen abweichenden Wert eingeben..."
value={customValue}
onChange={handleCustomValueChange}
variant="standard"
fullWidth
InputProps={{
disableUnderline: true,
}}
sx={{
"& .MuiInput-input": {
padding: 0,
},
}}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
}}
error={selectedIndex === -1 && customValue !== "" && customValue.trim() === ""}
helperText={selectedIndex === -1 && customValue !== "" && customValue.trim() === "" ? "Der Wert, der angegeben wurde, ist leer." : ""}
/>
</Box>
</Box>
</TableCell>
<TableCell align="center">
{editingCustomPage ? (
<TextField
value={customPage}
onChange={handleCustomPageChange}
onKeyDown={handleCustomPageKeyPress}
onBlur={() => setEditingCustomPage(false)}
autoFocus
size="small"
placeholder="Einen abweichenden Wert eingeben..."
value={customValue}
onChange={handleCustomValueChange}
variant="standard"
fullWidth
InputProps={{
disableUnderline: true,
}}
sx={{
width: "60px",
"& .MuiInput-input": {
textAlign: "center"
}
}}
inputProps={{
min: 0,
style: { textAlign: 'center' }
}}
/>
) : (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 1,
cursor: "pointer",
minHeight: "24px",
minWidth: "100px",
margin: "0 auto",
padding: 0,
},
}}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
startCustomPageEditing();
}}
>
<span>{customPage || "..."}</span>
<EditIcon
fontSize="small"
sx={{
color: "black",
opacity: 0.7,
transition: "opacity 0.2s ease",
ml: 1
}}
onClick={(e) => {
e.stopPropagation();
startCustomPageEditing();
}}
/>
</Box>
)}
/>
</Box>
</TableCell>
</TableRow>
</TableBody>
@ -495,17 +348,20 @@ function ExtractedResultsPage() {
pitchBookId={pitchBook}
currentPage={currentPage}
onPageChange={setCurrentPage}
highlight={groupedKpiValues
.map((k) => k.pages.map((page: number) => ({ page, text: k.entity })))
.reduce((acc, val) => acc.concat(val), [])}
focusHighlight={focusHighlight}
highlight={Object.values(kpiValues)
.flat()
.map((k) => ({ page: k.page, text: k.entity }))}
focusHighlight={{
page: kpiValues.at(selectedIndex)?.page || -1,
text: kpiValues.at(selectedIndex)?.entity || "",
}}
/>
</Paper>
<Box mt={2} display="flex" justifyContent="flex-end" gap={2}>
<Button
variant="contained"
onClick={handleAcceptReview}
disabled={isSelectedValueEmpty}
disabled={!selectedValue}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },

View File

@ -8,8 +8,6 @@ export interface Kennzahl {
example: string;
position: number;
active: boolean;
exampleText?: string;
markedValue?: string;
}
export const typeDisplayMapping: Record<string, string> = {

View File

@ -1,6 +1,6 @@
import type { Kennzahl } from "@/types/kpi";
const API_HOST = import.meta.env.VITE_API_HOST || "http://localhost:5050";
const API_HOST = import.meta.env.VITE_API_HOST || 'http://localhost:5050';
export { API_HOST };
@ -15,7 +15,9 @@ export const fetchKPI = async (
source: string;
}[];
}> => {
const response = await fetch(`${API_HOST}/api/pitch_book/${pitchBookId}`);
const response = await fetch(
`${API_HOST}/api/pitch_book/${pitchBookId}`,
);
const data = await response.json();
return data.kpi ? getKPI(data.kpi) : {};
@ -44,10 +46,13 @@ export const fetchPutKPI = async (
const formData = new FormData();
formData.append("kpi", JSON.stringify(flattenKPIArray(kpi)));
const response = await fetch(`${API_HOST}/api/pitch_book/${pitchBookId}`, {
method: "PUT",
body: formData,
});
const response = await fetch(
`${API_HOST}/api/pitch_book/${pitchBookId}`,
{
method: "PUT",
body: formData,
},
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -114,11 +119,3 @@ export async function fetchPitchBooks() {
}
return response.json();
}
export async function fetchPitchBooksById(id: number) {
const response = await fetch(`${API_HOST}/api/pitch_book/${id}`);
if (!response.ok) {
throw new Error("Failed to fetch pitch books");
}
return response.json();
}

View File

@ -1,11 +0,0 @@
export const formatDate = (dateString: string): string => {
const date = new Date(dateString);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based
const day = String(date.getDate()).padStart(2, '0');
const year = date.getFullYear();
return `${hours}:${minutes} ${day}.${month}.${year}`;
};