Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
5429540f32 |
|
|
@ -1,4 +1,4 @@
|
||||||
from flask import Flask
|
from flask import Flask, send_from_directory, request
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
@ -6,7 +6,7 @@ from controller import register_routes
|
||||||
from model.database import init_db
|
from model.database import init_db
|
||||||
from controller.socketIO import socketio
|
from controller.socketIO import socketio
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__, static_folder='dist', static_url_path='/')
|
||||||
CORS(app)
|
CORS(app)
|
||||||
socketio.init_app(app)
|
socketio.init_app(app)
|
||||||
|
|
||||||
|
|
@ -25,7 +25,11 @@ register_routes(app)
|
||||||
def health_check():
|
def health_check():
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
@app.route('/', defaults={'path': 'index.html'})
|
||||||
|
@app.route('/<path:path>')
|
||||||
|
def catch_all(path):
|
||||||
|
return app.send_static_file('index.html')
|
||||||
|
|
||||||
# Für Docker wichtig: host='0.0.0.0'
|
# für Docker wichtig: host='0.0.0.0'
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
socketio.run(app, debug=True, host="0.0.0.0", port=5050)
|
socketio.run(app, debug=True, host="0.0.0.0", port=5050)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
from controller.spacy_controller import spacy_controller
|
from controller.spacy_contoller import spacy_controller
|
||||||
from controller.kpi_setting_controller import kpi_setting_controller, kpi_routes
|
from controller.kpi_setting_controller import kpi_setting_controller
|
||||||
from controller.pitch_book_controller import pitch_book_controller
|
from controller.pitch_book_controller import pitch_book_controller
|
||||||
from controller.progress_controller import progress_controller
|
from controller.progress_controller import progress_controller
|
||||||
|
|
||||||
|
|
||||||
def register_routes(app):
|
def register_routes(app):
|
||||||
app.register_blueprint(kpi_setting_controller)
|
app.register_blueprint(kpi_setting_controller)
|
||||||
app.register_blueprint(kpi_routes)
|
|
||||||
app.register_blueprint(pitch_book_controller)
|
app.register_blueprint(pitch_book_controller)
|
||||||
app.register_blueprint(spacy_controller)
|
app.register_blueprint(spacy_controller)
|
||||||
app.register_blueprint(progress_controller)
|
app.register_blueprint(progress_controller)
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@ from flask import Blueprint, request, jsonify
|
||||||
from model.database import db
|
from model.database import db
|
||||||
from model.kpi_setting_model import KPISettingModel, KPISettingType
|
from model.kpi_setting_model import KPISettingModel, KPISettingType
|
||||||
|
|
||||||
# Routen für /api/kpi/settings (Auslesen im Frontend)
|
|
||||||
kpi_routes = Blueprint("kpi_routes", __name__, url_prefix="/api/kpi")
|
|
||||||
|
|
||||||
# Routen für /api/kpi_setting/ (Hinzufügen, Ändern, Löschen)
|
|
||||||
kpi_setting_controller = Blueprint(
|
kpi_setting_controller = Blueprint(
|
||||||
"kpi_settings", __name__, url_prefix="/api/kpi_setting"
|
"kpi_settings", __name__, url_prefix="/api/kpi_setting"
|
||||||
)
|
)
|
||||||
|
|
@ -17,6 +14,12 @@ def get_all_kpi_settings():
|
||||||
return jsonify([kpi_setting.to_dict() for kpi_setting in kpi_settings]), 200
|
return jsonify([kpi_setting.to_dict() for kpi_setting in kpi_settings]), 200
|
||||||
|
|
||||||
|
|
||||||
|
@kpi_setting_controller.route("/<int:id>", methods=["GET"])
|
||||||
|
def get_kpi_setting(id):
|
||||||
|
kpi_setting = KPISettingModel.query.get_or_404(id)
|
||||||
|
return jsonify(kpi_setting.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
@kpi_setting_controller.route("/", methods=["POST"])
|
@kpi_setting_controller.route("/", methods=["POST"])
|
||||||
def create_kpi_setting():
|
def create_kpi_setting():
|
||||||
data = request.json
|
data = request.json
|
||||||
|
|
@ -26,12 +29,13 @@ def create_kpi_setting():
|
||||||
|
|
||||||
required_fields = [
|
required_fields = [
|
||||||
"name",
|
"name",
|
||||||
|
"description",
|
||||||
"mandatory",
|
"mandatory",
|
||||||
"type",
|
"type",
|
||||||
|
"translation",
|
||||||
|
"example",
|
||||||
"position",
|
"position",
|
||||||
"active",
|
"active",
|
||||||
"examples",
|
|
||||||
"is_trained",
|
|
||||||
]
|
]
|
||||||
for field in required_fields:
|
for field in required_fields:
|
||||||
if field not in data:
|
if field not in data:
|
||||||
|
|
@ -51,12 +55,13 @@ def create_kpi_setting():
|
||||||
|
|
||||||
new_kpi_setting = KPISettingModel(
|
new_kpi_setting = KPISettingModel(
|
||||||
name=data["name"],
|
name=data["name"],
|
||||||
|
description=data["description"],
|
||||||
mandatory=data["mandatory"],
|
mandatory=data["mandatory"],
|
||||||
type=kpi_type,
|
type=kpi_type,
|
||||||
|
translation=data["translation"],
|
||||||
|
example=data["example"],
|
||||||
position=data["position"],
|
position=data["position"],
|
||||||
active=data["active"],
|
active=data["active"],
|
||||||
examples=data.get("examples", []),
|
|
||||||
is_trained=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(new_kpi_setting)
|
db.session.add(new_kpi_setting)
|
||||||
|
|
@ -79,6 +84,9 @@ def update_kpi_setting(id):
|
||||||
return jsonify({"error": "KPI Setting with this name already exists"}), 409
|
return jsonify({"error": "KPI Setting with this name already exists"}), 409
|
||||||
kpi_setting.name = data["name"]
|
kpi_setting.name = data["name"]
|
||||||
|
|
||||||
|
if "description" in data:
|
||||||
|
kpi_setting.description = data["description"]
|
||||||
|
|
||||||
if "mandatory" in data:
|
if "mandatory" in data:
|
||||||
kpi_setting.mandatory = data["mandatory"]
|
kpi_setting.mandatory = data["mandatory"]
|
||||||
|
|
||||||
|
|
@ -92,18 +100,18 @@ def update_kpi_setting(id):
|
||||||
400,
|
400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "translation" in data:
|
||||||
|
kpi_setting.translation = data["translation"]
|
||||||
|
|
||||||
|
if "example" in data:
|
||||||
|
kpi_setting.example = data["example"]
|
||||||
|
|
||||||
if "position" in data:
|
if "position" in data:
|
||||||
kpi_setting.position = data["position"]
|
kpi_setting.position = data["position"]
|
||||||
|
|
||||||
if "active" in data:
|
if "active" in data:
|
||||||
kpi_setting.active = data["active"]
|
kpi_setting.active = data["active"]
|
||||||
|
|
||||||
if "examples" in data:
|
|
||||||
kpi_setting.examples = data["examples"]
|
|
||||||
|
|
||||||
if "is_trained" in data:
|
|
||||||
kpi_setting.is_trained = data["is_trained"]
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(kpi_setting.to_dict()), 200
|
return jsonify(kpi_setting.to_dict()), 200
|
||||||
|
|
@ -146,21 +154,3 @@ def update_kpi_positions():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({"error": f"Failed to update positions: {str(e)}"}), 500
|
return jsonify({"error": f"Failed to update positions: {str(e)}"}), 500
|
||||||
|
|
||||||
|
|
||||||
@kpi_routes.route("/settings", methods=["GET"])
|
|
||||||
def get_kpi_settings():
|
|
||||||
try:
|
|
||||||
kpis = KPISettingModel.query.all()
|
|
||||||
return jsonify([k.to_dict() for k in kpis]), 200
|
|
||||||
except Exception as e:
|
|
||||||
return (
|
|
||||||
jsonify({"error": "Fehler beim Abrufen der KPIs", "details": str(e)}),
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@kpi_setting_controller.route("/<int:id>", methods=["GET"])
|
|
||||||
def get_kpi_setting(id):
|
|
||||||
kpi_setting = KPISettingModel.query.get_or_404(id)
|
|
||||||
return jsonify(kpi_setting.to_dict()), 200
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from flask import Blueprint, request, jsonify, send_file, current_app
|
from flask import Blueprint, request, jsonify, send_file, current_app, make_response
|
||||||
from model.database import db
|
from model.database import db
|
||||||
from model.pitch_book_model import PitchBookModel
|
from model.pitch_book_model import PitchBookModel
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
@ -86,10 +86,25 @@ def get_file(id):
|
||||||
@pitch_book_controller.route("/<int:id>/download", methods=["GET"])
|
@pitch_book_controller.route("/<int:id>/download", methods=["GET"])
|
||||||
def download_file(id):
|
def download_file(id):
|
||||||
file = PitchBookModel.query.get_or_404(id)
|
file = PitchBookModel.query.get_or_404(id)
|
||||||
return send_file(
|
|
||||||
BytesIO(file.file), download_name=file.filename, as_attachment=True
|
response = make_response(
|
||||||
|
send_file(
|
||||||
|
BytesIO(file.file),
|
||||||
|
download_name=file.filename,
|
||||||
|
as_attachment=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Set cache headers
|
||||||
|
response.headers['Cache-Control'] = 'public, max-age=31536000' # 1 year
|
||||||
|
response.headers['Pragma'] = 'cache'
|
||||||
|
response.headers['Expires'] = 'Thu, 31 Dec 2037 23:55:55 GMT' # Far future date
|
||||||
|
|
||||||
|
return response
|
||||||
|
# return send_file(
|
||||||
|
# BytesIO(file.file), download_name=file.filename, as_attachment=True
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
@pitch_book_controller.route("/<int:id>", methods=["PUT"])
|
@pitch_book_controller.route("/<int:id>", methods=["PUT"])
|
||||||
def update_file(id):
|
def update_file(id):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy")
|
||||||
|
|
||||||
|
|
||||||
|
@spacy_controller.route("/", methods=["GET"])
|
||||||
|
def get_all_files():
|
||||||
|
files = SpacyModel.query.all()
|
||||||
|
return jsonify([file.to_dict() for file in files]), 200
|
||||||
|
|
||||||
|
|
||||||
|
@spacy_controller.route("/<int:id>", methods=["GET"])
|
||||||
|
def get_file(id):
|
||||||
|
file = SpacyModel.query.get_or_404(id)
|
||||||
|
return jsonify(file.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@spacy_controller.route("/<int:id>/download", methods=["GET"])
|
||||||
|
def download_file(id):
|
||||||
|
file = SpacyModel.query.get_or_404(id)
|
||||||
|
return send_file(
|
||||||
|
BytesIO(file.file), download_name=file.filename, as_attachment=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@spacy_controller.route("/", methods=["POST"])
|
||||||
|
def upload_file():
|
||||||
|
print(request)
|
||||||
|
if "file" not in request.files:
|
||||||
|
return jsonify({"error": "No file part in the request"}), 400
|
||||||
|
|
||||||
|
uploaded_file = request.files["file"]
|
||||||
|
if uploaded_file.filename == "":
|
||||||
|
return jsonify({"error": "No selected file"}), 400
|
||||||
|
|
||||||
|
# Read file data once
|
||||||
|
file_data = uploaded_file.read()
|
||||||
|
try:
|
||||||
|
if uploaded_file:
|
||||||
|
fileName = uploaded_file.filename or ""
|
||||||
|
new_file = SpacyModel(filename=secure_filename(fileName), file=file_data)
|
||||||
|
|
||||||
|
db.session.add(new_file)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify(new_file.to_dict()), 201
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return jsonify({"error": "Invalid file format. Only PDF files are accepted"}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@spacy_controller.route("/<int:id>", methods=["PUT"])
|
||||||
|
def update_file(id):
|
||||||
|
file = SpacyModel.query.get_or_404(id)
|
||||||
|
|
||||||
|
if "file" in request.files:
|
||||||
|
uploaded_file = request.files["file"]
|
||||||
|
if uploaded_file.filename != "":
|
||||||
|
file.filename = uploaded_file.filename
|
||||||
|
|
||||||
|
# Read file data once
|
||||||
|
file_data = uploaded_file.read()
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
uploaded_file
|
||||||
|
and puremagic.from_string(file_data, mime=True) == "application/pdf"
|
||||||
|
):
|
||||||
|
file.file = file_data
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
if "kpi" in request.form:
|
||||||
|
file.kpi = request.form.get("kpi")
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify(file.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@spacy_controller.route("/<int:id>", methods=["DELETE"])
|
||||||
|
def delete_file(id):
|
||||||
|
file = SpacyModel.query.get_or_404(id)
|
||||||
|
db.session.delete(file)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({"message": f"File {id} deleted successfully"}), 200
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
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 requests
|
|
||||||
from model.kpi_setting_model import KPISettingModel
|
|
||||||
|
|
||||||
|
|
||||||
spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy")
|
|
||||||
|
|
||||||
SPACY_TRAINING_URL = os.getenv("SPACY_TRAINING_URL", "http://spacy:5052/train")
|
|
||||||
SPACY_URL = os.getenv("SPACY_URL", "http://spacy:5052")
|
|
||||||
|
|
||||||
|
|
||||||
@spacy_controller.route("/train", methods=["POST"])
|
|
||||||
def trigger_training():
|
|
||||||
try:
|
|
||||||
response = requests.post(SPACY_TRAINING_URL, timeout=600)
|
|
||||||
if response.ok:
|
|
||||||
return jsonify({"message": "Training erfolgreich angestoßen."}), 200
|
|
||||||
else:
|
|
||||||
return (
|
|
||||||
jsonify({"error": "Training fehlgeschlagen", "details": response.text}),
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return (
|
|
||||||
jsonify(
|
|
||||||
{"error": "Fehler beim Senden an Trainingsservice", "details": str(e)}
|
|
||||||
),
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@spacy_controller.route("/", methods=["GET"])
|
|
||||||
def get_all_files():
|
|
||||||
files = SpacyModel.query.all()
|
|
||||||
return jsonify([file.to_dict() for file in files]), 200
|
|
||||||
|
|
||||||
|
|
||||||
@spacy_controller.route("/<int:id>", methods=["GET"])
|
|
||||||
def get_file(id):
|
|
||||||
file = SpacyModel.query.get_or_404(id)
|
|
||||||
return jsonify(file.to_dict()), 200
|
|
||||||
|
|
||||||
|
|
||||||
@spacy_controller.route("/<int:id>/download", methods=["GET"])
|
|
||||||
def download_file(id):
|
|
||||||
file = SpacyModel.query.get_or_404(id)
|
|
||||||
return send_file(
|
|
||||||
BytesIO(file.file), download_name=file.filename, as_attachment=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@spacy_controller.route("/", methods=["POST"])
|
|
||||||
def upload_file():
|
|
||||||
if "file" not in request.files:
|
|
||||||
return jsonify({"error": "No file part in the request"}), 400
|
|
||||||
|
|
||||||
uploaded_file = request.files["file"]
|
|
||||||
if uploaded_file.filename == "":
|
|
||||||
return jsonify({"error": "No selected file"}), 400
|
|
||||||
|
|
||||||
file_data = uploaded_file.read()
|
|
||||||
try:
|
|
||||||
fileName = uploaded_file.filename or ""
|
|
||||||
new_file = SpacyModel(filename=secure_filename(fileName), file=file_data)
|
|
||||||
db.session.add(new_file)
|
|
||||||
db.session.commit()
|
|
||||||
return jsonify(new_file.to_dict()), 201
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
return jsonify({"error": "Invalid file format. Only PDF files are accepted"}), 400
|
|
||||||
|
|
||||||
|
|
||||||
@spacy_controller.route("/<int:id>", methods=["PUT"])
|
|
||||||
def update_file(id):
|
|
||||||
file = SpacyModel.query.get_or_404(id)
|
|
||||||
|
|
||||||
if "file" in request.files:
|
|
||||||
uploaded_file = request.files["file"]
|
|
||||||
if uploaded_file.filename != "":
|
|
||||||
file.filename = uploaded_file.filename
|
|
||||||
file_data = uploaded_file.read()
|
|
||||||
try:
|
|
||||||
if puremagic.from_string(file_data, mime=True) == "application/pdf":
|
|
||||||
file.file = file_data
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
if "kpi" in request.form:
|
|
||||||
file.kpi = request.form.get("kpi")
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
return jsonify(file.to_dict()), 200
|
|
||||||
|
|
||||||
|
|
||||||
@spacy_controller.route("/<int:id>", methods=["DELETE"])
|
|
||||||
def delete_file(id):
|
|
||||||
file = SpacyModel.query.get_or_404(id)
|
|
||||||
db.session.delete(file)
|
|
||||||
db.session.commit()
|
|
||||||
return jsonify({"message": f"File {id} deleted successfully"}), 200
|
|
||||||
|
|
||||||
|
|
||||||
@spacy_controller.route("/append-training-entry", methods=["POST"])
|
|
||||||
def forward_training_entry():
|
|
||||||
entry = request.get_json()
|
|
||||||
try:
|
|
||||||
response = requests.post(f"{SPACY_URL}/append-training-entry", json=entry)
|
|
||||||
return jsonify(response.json()), response.status_code
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
# globale Variable oben einfügen
|
|
||||||
current_training_status = {"running": False}
|
|
||||||
|
|
||||||
|
|
||||||
@spacy_controller.route("/training/status", methods=["POST"])
|
|
||||||
def update_training_status():
|
|
||||||
data = request.get_json()
|
|
||||||
current_training_status["running"] = data.get("running", False)
|
|
||||||
|
|
||||||
if current_training_status["running"] is False:
|
|
||||||
try:
|
|
||||||
KPISettingModel.query.update({KPISettingModel.is_trained: True})
|
|
||||||
db.session.commit()
|
|
||||||
except Exception as e:
|
|
||||||
db.session.rollback()
|
|
||||||
return (
|
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"error": "is_trained konnte nicht aktualisiert werden",
|
|
||||||
"details": str(e),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify({"status": "success", "running": current_training_status["running"]})
|
|
||||||
|
|
||||||
|
|
||||||
@spacy_controller.route("/train-status", methods=["GET"])
|
|
||||||
def training_status():
|
|
||||||
return jsonify(current_training_status), 200
|
|
||||||
|
|
@ -2,8 +2,6 @@ from model.database import db
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
from sqlalchemy import Enum as SQLAlchemyEnum
|
from sqlalchemy import Enum as SQLAlchemyEnum
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
|
|
||||||
class KPISettingType(Enum):
|
class KPISettingType(Enum):
|
||||||
|
|
@ -20,36 +18,37 @@ class KPISettingModel(db.Model):
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
name: Mapped[str] = mapped_column(unique=True)
|
name: Mapped[str] = mapped_column(unique=True)
|
||||||
|
description: Mapped[str]
|
||||||
mandatory: Mapped[bool]
|
mandatory: Mapped[bool]
|
||||||
type: Mapped[KPISettingType] = mapped_column(
|
type: Mapped[KPISettingType] = mapped_column(
|
||||||
SQLAlchemyEnum(KPISettingType, native_enum=True)
|
SQLAlchemyEnum(KPISettingType, native_enum=True)
|
||||||
)
|
)
|
||||||
|
translation: Mapped[str]
|
||||||
|
example: Mapped[str]
|
||||||
position: Mapped[int]
|
position: Mapped[int]
|
||||||
active: Mapped[bool]
|
active: Mapped[bool]
|
||||||
examples: Mapped[list] = mapped_column(JSONB, default=[])
|
|
||||||
is_trained: Mapped[bool] = mapped_column(default=False)
|
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return OrderedDict(
|
return {
|
||||||
[
|
"id": self.id,
|
||||||
("id", self.id),
|
"name": self.name,
|
||||||
("name", self.name),
|
"description": self.description,
|
||||||
("mandatory", self.mandatory),
|
"mandatory": self.mandatory,
|
||||||
("type", self.type.value),
|
"type": self.type.value,
|
||||||
("position", self.position),
|
"translation": self.translation,
|
||||||
("examples", self.examples),
|
"example": self.example,
|
||||||
("active", self.active),
|
"position": self.position,
|
||||||
("is_trained", self.is_trained),
|
"active": self.active,
|
||||||
]
|
}
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name, mandatory, type, position, active, examples=None, is_trained=False
|
self, name, description, mandatory, type, translation, example, position, active
|
||||||
):
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.description = description
|
||||||
self.mandatory = mandatory
|
self.mandatory = mandatory
|
||||||
self.type = type
|
self.type = type
|
||||||
|
self.translation = translation
|
||||||
|
self.example = example
|
||||||
self.position = position
|
self.position = position
|
||||||
self.active = active
|
self.active = active
|
||||||
self.examples = examples or []
|
|
||||||
self.is_trained = is_trained
|
|
||||||
|
|
|
||||||
|
|
@ -10,258 +10,153 @@ def seed_default_kpi_settings():
|
||||||
default_kpi_settings = [
|
default_kpi_settings = [
|
||||||
{
|
{
|
||||||
"name": "Fondsname",
|
"name": "Fondsname",
|
||||||
|
"description": "Der vollständige Name des Investmentfonds",
|
||||||
"mandatory": True,
|
"mandatory": True,
|
||||||
"type": KPISettingType.STRING,
|
"type": KPISettingType.STRING,
|
||||||
|
"translation": "Fund Name",
|
||||||
|
"example": "Alpha Real Estate Fund I",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Der Fonds trägt den Namen Alpha Real Estate Fund I.",
|
|
||||||
"value": "Alpha Real Estate Fund I",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sentence": "Im Pitchbook wird der Fondsname als Alpha Real Estate Fund I angegeben.",
|
|
||||||
"value": "Alpha Real Estate Fund I",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Fondsmanager",
|
"name": "Fondsmanager",
|
||||||
|
"description": "Verantwortlicher Manager für die Fondsverwaltung",
|
||||||
"mandatory": True,
|
"mandatory": True,
|
||||||
"type": KPISettingType.STRING,
|
"type": KPISettingType.STRING,
|
||||||
|
"translation": "Fund Manager",
|
||||||
|
"example": "Max Mustermann",
|
||||||
"position": 2,
|
"position": 2,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Fondsmanager des Projekts ist Max Mustermann.",
|
|
||||||
"value": "Max Mustermann",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sentence": "Die Verwaltung liegt bei Max Mustermann.",
|
|
||||||
"value": "Max Mustermann",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "AIFM",
|
"name": "AIFM",
|
||||||
|
"description": "Alternative Investment Fund Manager",
|
||||||
"mandatory": True,
|
"mandatory": True,
|
||||||
"type": KPISettingType.STRING,
|
"type": KPISettingType.STRING,
|
||||||
|
"translation": "AIFM",
|
||||||
|
"example": "Alpha Investment Management GmbH",
|
||||||
"position": 3,
|
"position": 3,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "AIFM ist die Alpha Investment Management GmbH.",
|
|
||||||
"value": "Alpha Investment Management GmbH",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sentence": "Die Alpha Investment Management GmbH fungiert als AIFM.",
|
|
||||||
"value": "Alpha Investment Management GmbH",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Datum",
|
"name": "Datum",
|
||||||
|
"description": "Stichtag der Datenerfassung",
|
||||||
"mandatory": True,
|
"mandatory": True,
|
||||||
"type": KPISettingType.DATE,
|
"type": KPISettingType.DATE,
|
||||||
|
"translation": "Date",
|
||||||
|
"example": "05.05.2025",
|
||||||
"position": 4,
|
"position": 4,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Die Daten basieren auf dem Stand vom 05.05.2025.",
|
|
||||||
"value": "05.05.2025",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sentence": "Stichtag der Angaben ist der 05.05.2025.",
|
|
||||||
"value": "05.05.2025",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Risikoprofil",
|
"name": "Risikoprofil",
|
||||||
|
"description": "Klassifizierung des Risikos des Fonds",
|
||||||
"mandatory": True,
|
"mandatory": True,
|
||||||
"type": KPISettingType.STRING,
|
"type": KPISettingType.STRING,
|
||||||
|
"translation": "Risk Profile",
|
||||||
|
"example": "Core/Core++",
|
||||||
"position": 5,
|
"position": 5,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Der Fonds hat das Risikoprofil Core/Core++.",
|
|
||||||
"value": "Core/Core++",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sentence": "Einstufung des Fondsrisikos: Core/Core++.",
|
|
||||||
"value": "Core/Core++",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Artikel",
|
"name": "Artikel",
|
||||||
|
"description": "Artikel 8 SFDR-Klassifizierung",
|
||||||
"mandatory": False,
|
"mandatory": False,
|
||||||
"type": KPISettingType.BOOLEAN,
|
"type": KPISettingType.BOOLEAN,
|
||||||
|
"translation": "Article",
|
||||||
|
"example": "Artikel 8",
|
||||||
"position": 6,
|
"position": 6,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Der Fonds erfüllt die Anforderungen von Artikel 8.",
|
|
||||||
"value": "Artikel 8",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sentence": "Gemäß SFDR fällt dieser Fonds unter Artikel 8.",
|
|
||||||
"value": "Artikel 8",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Zielrendite",
|
"name": "Zielrendite",
|
||||||
|
"description": "Angestrebte jährliche Rendite in Prozent",
|
||||||
"mandatory": True,
|
"mandatory": True,
|
||||||
"type": KPISettingType.NUMBER,
|
"type": KPISettingType.NUMBER,
|
||||||
|
"translation": "Target Return",
|
||||||
|
"example": "6.5",
|
||||||
"position": 7,
|
"position": 7,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Die angestrebte Zielrendite liegt bei 6.5%.",
|
|
||||||
"value": "6.5%",
|
|
||||||
},
|
|
||||||
{"sentence": "Zielrendite des Fonds beträgt 6.5%.", "value": "6.5%"},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Rendite",
|
"name": "Rendite",
|
||||||
|
"description": "Tatsächlich erzielte Rendite in Prozent",
|
||||||
"mandatory": False,
|
"mandatory": False,
|
||||||
"type": KPISettingType.NUMBER,
|
"type": KPISettingType.NUMBER,
|
||||||
|
"translation": "Return",
|
||||||
|
"example": "5.8",
|
||||||
"position": 8,
|
"position": 8,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Die Rendite für das Jahr beträgt 5.8%.",
|
|
||||||
"value": "5.8%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sentence": "Im letzten Jahr wurde eine Rendite von 5.8% erzielt.",
|
|
||||||
"value": "5.8%",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Zielausschüttung",
|
"name": "Zielausschüttung",
|
||||||
|
"description": "Geplante Ausschüttung in Prozent",
|
||||||
"mandatory": False,
|
"mandatory": False,
|
||||||
"type": KPISettingType.NUMBER,
|
"type": KPISettingType.NUMBER,
|
||||||
|
"translation": "Target Distribution",
|
||||||
|
"example": "4.0",
|
||||||
"position": 9,
|
"position": 9,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{"sentence": "Die Zielausschüttung beträgt 4.0%.", "value": "4.0%"},
|
|
||||||
{
|
|
||||||
"sentence": "Geplante Ausschüttung: 4.0% pro Jahr.",
|
|
||||||
"value": "4.0%",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Ausschüttung",
|
"name": "Ausschüttung",
|
||||||
|
"description": "Tatsächliche Ausschüttung in Prozent",
|
||||||
"mandatory": False,
|
"mandatory": False,
|
||||||
"type": KPISettingType.NUMBER,
|
"type": KPISettingType.NUMBER,
|
||||||
|
"translation": "Distribution",
|
||||||
|
"example": "3.8",
|
||||||
"position": 10,
|
"position": 10,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Die Ausschüttung im Jahr 2024 lag bei 3.8%.",
|
|
||||||
"value": "3.8%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sentence": "Es wurde eine Ausschüttung von 3.8% vorgenommen.",
|
|
||||||
"value": "3.8%",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Laufzeit",
|
"name": "Laufzeit",
|
||||||
|
"description": "Geplante Laufzeit des Fonds",
|
||||||
"mandatory": True,
|
"mandatory": True,
|
||||||
"type": KPISettingType.STRING,
|
"type": KPISettingType.STRING,
|
||||||
|
"translation": "Duration",
|
||||||
|
"example": "7 Jahre, 10, Evergreen",
|
||||||
"position": 11,
|
"position": 11,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Die Laufzeit des Fonds beträgt 7 Jahre.",
|
|
||||||
"value": "7 Jahre",
|
|
||||||
},
|
|
||||||
{"sentence": "Geplante Dauer: Evergreen-Modell.", "value": "Evergreen"},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "LTV",
|
"name": "LTV",
|
||||||
|
"description": "Loan-to-Value Verhältnis in Prozent",
|
||||||
"mandatory": False,
|
"mandatory": False,
|
||||||
"type": KPISettingType.NUMBER,
|
"type": KPISettingType.NUMBER,
|
||||||
|
"translation": "LTV",
|
||||||
|
"example": "65.0",
|
||||||
"position": 12,
|
"position": 12,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{"sentence": "Der LTV beträgt 65.0%.", "value": "65.0%"},
|
|
||||||
{"sentence": "Loan-to-Value-Ratio: 65.0%.", "value": "65.0%"},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Managementgebühren",
|
"name": "Managementgebühren",
|
||||||
|
"description": "Jährliche Verwaltungsgebühren in Prozent",
|
||||||
"mandatory": True,
|
"mandatory": True,
|
||||||
"type": KPISettingType.NUMBER,
|
"type": KPISettingType.NUMBER,
|
||||||
|
"translation": "Management Fees",
|
||||||
|
"example": "1.5",
|
||||||
"position": 13,
|
"position": 13,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Die Managementgebühren betragen jährlich 1.5%.",
|
|
||||||
"value": "1.5%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sentence": "Für die Verwaltung wird eine Gebühr von 1.5% erhoben.",
|
|
||||||
"value": "1.5%",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Sektorenallokation",
|
"name": "Sektorenallokation",
|
||||||
|
"description": "Verteilung der Investments nach Sektoren",
|
||||||
"mandatory": False,
|
"mandatory": False,
|
||||||
"type": KPISettingType.ARRAY,
|
"type": KPISettingType.ARRAY,
|
||||||
|
"translation": "Sector Allocation",
|
||||||
|
"example": "Büro, Wohnen, Logistik, Studentenwohnen",
|
||||||
"position": 14,
|
"position": 14,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Die Sektorenallokation umfasst Büro, Wohnen und Logistik.",
|
|
||||||
"value": "Büro, Wohnen, Logistik",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sentence": "Investiert wird in Büro, Logistik und Studentenwohnen.",
|
|
||||||
"value": "Büro, Logistik, Studentenwohnen",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Länderallokation",
|
"name": "Länderallokation",
|
||||||
|
"description": "Geografische Verteilung der Investments",
|
||||||
"mandatory": False,
|
"mandatory": False,
|
||||||
"type": KPISettingType.ARRAY,
|
"type": KPISettingType.ARRAY,
|
||||||
|
"translation": "Country Allocation",
|
||||||
|
"example": "Deutschland,Frankreich, Österreich, Schweiz",
|
||||||
"position": 15,
|
"position": 15,
|
||||||
"active": True,
|
"active": True,
|
||||||
"is_trained": True,
|
|
||||||
"examples": [
|
|
||||||
{
|
|
||||||
"sentence": "Investitionen erfolgen in Deutschland, Frankreich und Österreich.",
|
|
||||||
"value": "Deutschland, Frankreich, Österreich",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sentence": "Die Länderallokation umfasst Deutschland, Schweiz und Frankreich.",
|
|
||||||
"value": "Deutschland, Schweiz, Frankreich",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -270,12 +165,13 @@ def seed_default_kpi_settings():
|
||||||
for kpi_data in default_kpi_settings:
|
for kpi_data in default_kpi_settings:
|
||||||
kpi_setting = KPISettingModel(
|
kpi_setting = KPISettingModel(
|
||||||
name=kpi_data["name"],
|
name=kpi_data["name"],
|
||||||
|
description=kpi_data["description"],
|
||||||
mandatory=kpi_data["mandatory"],
|
mandatory=kpi_data["mandatory"],
|
||||||
type=kpi_data["type"],
|
type=kpi_data["type"],
|
||||||
|
translation=kpi_data["translation"],
|
||||||
|
example=kpi_data["example"],
|
||||||
position=kpi_data["position"],
|
position=kpi_data["position"],
|
||||||
active=kpi_data["active"],
|
active=kpi_data["active"],
|
||||||
examples=kpi_data.get("examples", []),
|
|
||||||
is_trained=kpi_data["is_trained"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(kpi_setting)
|
db.session.add(kpi_setting)
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,9 @@ import json
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
VALIDATE_SERVICE_URL = os.getenv(
|
VALIDATE_SERVICE_URL = os.getenv("VALIDATE_SERVICE_URL", "http://localhost:5054/validate")
|
||||||
"VALIDATE_SERVICE_URL", "http://localhost:5054/validate"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@app.route('/extract', methods=['POST'])
|
||||||
@app.route("/extract", methods=["POST"])
|
|
||||||
def extract_text_from_ocr_json():
|
def extract_text_from_ocr_json():
|
||||||
json_data = request.get_json()
|
json_data = request.get_json()
|
||||||
|
|
||||||
|
|
@ -19,19 +16,19 @@ def extract_text_from_ocr_json():
|
||||||
pages_data = json_data["extracted_text_per_page"]
|
pages_data = json_data["extracted_text_per_page"]
|
||||||
|
|
||||||
entities_json = extract_with_exxeta(pages_data, pitchbook_id)
|
entities_json = extract_with_exxeta(pages_data, pitchbook_id)
|
||||||
entities = (
|
entities = json.loads(entities_json) if isinstance(entities_json, str) else entities_json
|
||||||
json.loads(entities_json) if isinstance(entities_json, str) else entities_json
|
|
||||||
)
|
|
||||||
|
|
||||||
validate_payload = {"id": pitchbook_id, "service": "exxeta", "entities": entities}
|
validate_payload = {
|
||||||
|
"id": pitchbook_id,
|
||||||
|
"service": "exxeta",
|
||||||
|
"entities": entities
|
||||||
|
}
|
||||||
|
|
||||||
print(f"[EXXETA] Sending to validate service: {VALIDATE_SERVICE_URL}")
|
print(f"[EXXETA] Sending to validate service: {VALIDATE_SERVICE_URL}")
|
||||||
print(f"[EXXETA] Payload: {validate_payload} entities for pitchbook {pitchbook_id}")
|
print(f"[EXXETA] Payload: {validate_payload} entities for pitchbook {pitchbook_id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(VALIDATE_SERVICE_URL, json=validate_payload, timeout=600)
|
||||||
VALIDATE_SERVICE_URL, json=validate_payload, timeout=600
|
|
||||||
)
|
|
||||||
print(f"[EXXETA] Validate service response: {response.status_code}")
|
print(f"[EXXETA] Validate service response: {response.status_code}")
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print(f"[EXXETA] Validate service error: {response.text}")
|
print(f"[EXXETA] Validate service error: {response.text}")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import logging
|
import logging
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
@ -16,20 +17,6 @@ TIMEOUT = 180
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_dynamic_labels():
|
|
||||||
url = f"{COORDINATOR_URL}/api/kpi_setting/"
|
|
||||||
try:
|
|
||||||
response = requests.get(url, timeout=10)
|
|
||||||
response.raise_for_status()
|
|
||||||
kpi_list = response.json()
|
|
||||||
labels = [kpi["name"].upper() for kpi in kpi_list if kpi.get("active", False)]
|
|
||||||
return labels
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Konnte dynamische Labels nicht laden: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def extract_with_exxeta(pages_json, pitchbook_id):
|
def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
|
@ -41,12 +28,11 @@ def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
for page_data in pages_json:
|
for page_data in pages_json:
|
||||||
i += 1
|
i += 1
|
||||||
if i % 8 == 0:
|
if i % 8 == 0:
|
||||||
requests.post(
|
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 35 + 60/len(pages_json)*i})
|
||||||
COORDINATOR_URL + "/api/progress",
|
|
||||||
json={"id": pitchbook_id, "progress": 35 + 60 / len(pages_json) * i},
|
|
||||||
)
|
|
||||||
|
|
||||||
page_num = page_data.get("page")
|
page_num = page_data.get("page")
|
||||||
|
page_data.get("page")
|
||||||
text = page_data.get("text", "")
|
text = page_data.get("text", "")
|
||||||
|
|
||||||
if not text:
|
if not text:
|
||||||
|
|
@ -65,9 +51,9 @@ def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
"- Gib die Antwort als **JSON-Array** im folgenden Format zurück:\n\n"
|
"- Gib die Antwort als **JSON-Array** im folgenden Format zurück:\n\n"
|
||||||
"[\n"
|
"[\n"
|
||||||
" {\n"
|
" {\n"
|
||||||
' "label": "FONDSNAME",\n'
|
" \"label\": \"FONDSNAME\",\n"
|
||||||
' "entity": "...",\n'
|
" \"entity\": \"...\",\n"
|
||||||
f' "page": {page_num},\n'
|
f" \"page\": {page_num},\n"
|
||||||
" },\n"
|
" },\n"
|
||||||
" ...\n"
|
" ...\n"
|
||||||
"]\n\n"
|
"]\n\n"
|
||||||
|
|
@ -75,29 +61,45 @@ def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
f"TEXT:\n{text}"
|
f"TEXT:\n{text}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
labels = get_dynamic_labels()
|
|
||||||
prompt_kennzahlen = "".join([f"- {label}\n" for label in labels])
|
|
||||||
prompt = (
|
prompt = (
|
||||||
"Bitte extrahiere relevante Fondskennzahlen aus dem folgenden Pitchbook-Text. "
|
"Bitte extrahiere relevante Fondskennzahlen aus dem folgenden Pitchbook-Text. "
|
||||||
"Analysiere den Text sorgfältig, um **nur exakt benannte und relevante Werte** zu extrahieren.\n\n"
|
"Analysiere den Text sorgfältig, um **nur exakt benannte und relevante Werte** zu extrahieren.\n\n"
|
||||||
|
|
||||||
"ZU EXTRAHIERENDE KENNZAHLEN (immer exakt wie unten angegeben):\n"
|
"ZU EXTRAHIERENDE KENNZAHLEN (immer exakt wie unten angegeben):\n"
|
||||||
f"{prompt_kennzahlen}\n"
|
"- FONDSNAME\n"
|
||||||
|
"- FONDSMANAGER\n"
|
||||||
|
"- AIFM (z. B. Name Kapitalverwaltungsgesellschaft)\n"
|
||||||
|
"- DATUM\n"
|
||||||
|
"- RISIKOPROFIL (z. B. CORE, CORE+, VALUE-ADDED, OPPORTUNISTISCH)\n"
|
||||||
|
"- ARTIKEL (z. B. ARTIKEL 6, 8, 9)\n"
|
||||||
|
"- ZIELRENDITE\n"
|
||||||
|
"- RENDITE\n"
|
||||||
|
"- ZIELAUSSCHÜTTUNG\n"
|
||||||
|
"- AUSSCHÜTTUNG\n"
|
||||||
|
"- LAUFZEIT\n"
|
||||||
|
"- LTV\n"
|
||||||
|
"- MANAGEMENTGEBÜHREN (ggf. mit Staffelung und Bezug auf NAV/GAV)\n"
|
||||||
|
"- SEKTORENALLOKATION (z. B. BÜRO, LOGISTIK, WOHNEN... inkl. %-Angaben)\n"
|
||||||
|
"- LÄNDERALLOKATION (z. B. DEUTSCHLAND, FRANKREICH, etc. inkl. %-Angaben)\n\n"
|
||||||
|
|
||||||
"WICHTIG:\n"
|
"WICHTIG:\n"
|
||||||
"- Gib **nur eine Entität pro Kennzahl** an - keine Listen oder Interpretationen.\n"
|
"- Gib **nur eine Entität pro Kennzahl** an - keine Listen oder Interpretationen.\n"
|
||||||
'- Wenn mehrere Varianten genannt werden (z. B. "Core und Core+"), gib sie im Originalformat als **eine entity** an.\n'
|
"- Wenn mehrere Varianten genannt werden (z. B. \"Core und Core+\"), gib sie im Originalformat als **eine entity** an.\n"
|
||||||
"- **Keine Vermutungen oder Ergänzungen**. Wenn keine Information enthalten ist, gib die Kennzahl **nicht aus**.\n"
|
"- **Keine Vermutungen oder Ergänzungen**. Wenn keine Information enthalten ist, gib die Kennzahl **nicht aus**.\n"
|
||||||
"- Extrahiere **nur wörtlich vorkommende Inhalte** (keine Berechnungen, keine Zusammenfassungen).\n"
|
"- Extrahiere **nur wörtlich vorkommende Inhalte** (keine Berechnungen, keine Zusammenfassungen).\n"
|
||||||
"- Jeder gefundene Wert muss einem der obigen Label **eindeutig zuordenbar** sein.\n\n"
|
"- Jeder gefundene Wert muss einem der obigen Label **eindeutig zuordenbar** sein.\n\n"
|
||||||
|
|
||||||
"FORMAT:\n"
|
"FORMAT:\n"
|
||||||
"Antworte als **reines JSON-Array** mit folgendem Format:\n"
|
"Antworte als **reines JSON-Array** mit folgendem Format:\n"
|
||||||
"[\n"
|
"[\n"
|
||||||
" {\n"
|
" {\n"
|
||||||
' "label": "Kennzahlname (exakt wie oben)",\n'
|
" \"label\": \"Kennzahlname (exakt wie oben)\",\n"
|
||||||
' "entity": "Wert aus dem Text (exakt im Original)",\n'
|
" \"entity\": \"Wert aus dem Text (exakt im Original)\",\n"
|
||||||
f' "page": {page_num},\n'
|
f" \"page\": {page_num},\n"
|
||||||
" },\n"
|
" },\n"
|
||||||
" ...\n"
|
" ...\n"
|
||||||
"]\n\n"
|
"]\n\n"
|
||||||
|
|
||||||
f"Falls keine Kennzahl enthalten ist, gib ein leeres Array [] zurück.\n\n"
|
f"Falls keine Kennzahl enthalten ist, gib ein leeres Array [] zurück.\n\n"
|
||||||
f"Nur JSON-Antwort - keine Kommentare, keine Erklärungen, kein Text außerhalb des JSON.\n\n"
|
f"Nur JSON-Antwort - keine Kommentare, keine Erklärungen, kein Text außerhalb des JSON.\n\n"
|
||||||
f"TEXT:\n{text}"
|
f"TEXT:\n{text}"
|
||||||
|
|
@ -105,30 +107,28 @@ def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": f"Bearer {EXXETA_API_KEY}",
|
"Authorization": f"Bearer {EXXETA_API_KEY}"
|
||||||
}
|
}
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": MODEL,
|
"model": MODEL,
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{"role": "system", "content": "Du bist ein Finanzanalyst. Antworte ausschließlich mit einem validen JSON-Array."},
|
||||||
"role": "system",
|
{"role": "user", "content": prompt}
|
||||||
"content": "Du bist ein Finanzanalyst. Antworte ausschließlich mit einem validen JSON-Array.",
|
|
||||||
},
|
|
||||||
{"role": "user", "content": prompt},
|
|
||||||
],
|
],
|
||||||
"temperature": 0.0,
|
"temperature": 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
url = f"{EXXETA_BASE_URL}/deployments/{MODEL}/chat/completions"
|
url = f"{EXXETA_BASE_URL}/deployments/{MODEL}/chat/completions"
|
||||||
|
|
||||||
for attempt in range(1, MAX_RETRIES + 1):
|
for attempt in range(1, MAX_RETRIES + 1):
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(url, headers=headers, json=payload, timeout=TIMEOUT)
|
||||||
url, headers=headers, json=payload, timeout=TIMEOUT
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
content = response.json()["choices"][0]["message"]["content"].strip()
|
|
||||||
|
content = response.json()["choices"][0]["message"]["content"]
|
||||||
|
content = content.strip()
|
||||||
|
|
||||||
if content.startswith("```json"):
|
if content.startswith("```json"):
|
||||||
content = content.split("```json")[1]
|
content = content.split("```json")[1]
|
||||||
if content.endswith("```"):
|
if content.endswith("```"):
|
||||||
|
|
@ -143,19 +143,14 @@ def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
if isinstance(page_results, list):
|
if isinstance(page_results, list):
|
||||||
results.extend(page_results)
|
results.extend(page_results)
|
||||||
break
|
break
|
||||||
except requests.exceptions.RequestException:
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
if attempt == MAX_RETRIES:
|
if attempt == MAX_RETRIES:
|
||||||
results.extend([])
|
results.extend([])
|
||||||
except Exception:
|
except Exception as e:
|
||||||
if attempt == MAX_RETRIES:
|
if attempt == MAX_RETRIES:
|
||||||
results.extend([])
|
results.extend([])
|
||||||
|
|
||||||
requests.post(
|
|
||||||
COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 95}
|
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 95})
|
||||||
)
|
|
||||||
return json.dumps(results, indent=2, ensure_ascii=False)
|
return json.dumps(results, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("📡 Test-Aufruf get_dynamic_labels:")
|
|
||||||
print(get_dynamic_labels())
|
|
||||||
|
|
|
||||||
|
|
@ -29,17 +29,19 @@ def convert_pdf_async(temp_path, pitchbook_id):
|
||||||
temp_path.unlink() # cleanup
|
temp_path.unlink() # cleanup
|
||||||
return {"error": "OCR processing failed - all PDFs must be OCR'd"}, 500
|
return {"error": "OCR processing failed - all PDFs must be OCR'd"}, 500
|
||||||
|
|
||||||
with open(ocr_path, "rb") as ocr_file:
|
with open(ocr_path, 'rb') as ocr_file:
|
||||||
ocr_file.seek(0)
|
ocr_file.seek(0)
|
||||||
result = pdf_to_json(ocr_file)
|
result = pdf_to_json(ocr_file)
|
||||||
|
|
||||||
payload = {"id": int(pitchbook_id), "extracted_text_per_page": result["pages"]}
|
|
||||||
|
payload = {
|
||||||
|
"id": int(pitchbook_id),
|
||||||
|
"extracted_text_per_page": result["pages"]
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Sending payload to EXXETA and SPACY services")
|
logger.info("Sending payload to EXXETA and SPACY services")
|
||||||
|
|
||||||
requests.post(
|
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 35})
|
||||||
COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 35}
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
exxeta_response = requests.post(EXXETA_URL, json=payload, timeout=600)
|
exxeta_response = requests.post(EXXETA_URL, json=payload, timeout=600)
|
||||||
logger.info(f"EXXETA response: {exxeta_response.status_code}")
|
logger.info(f"EXXETA response: {exxeta_response.status_code}")
|
||||||
|
|
@ -52,16 +54,14 @@ def convert_pdf_async(temp_path, pitchbook_id):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error calling SPACY: {e}")
|
logger.error(f"Error calling SPACY: {e}")
|
||||||
|
|
||||||
files = [("file", ("", open(ocr_path, "rb"), "application/pdf"))]
|
files=[
|
||||||
|
('file',('',open(ocr_path,'rb'),'application/pdf'))
|
||||||
|
]
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
requests.put(
|
|
||||||
f"{COORDINATOR_URL}/api/pitch_book/{pitchbook_id}",
|
requests.put(f"{COORDINATOR_URL}/api/pitch_book/{pitchbook_id}", files=files, timeout=600, headers=headers)
|
||||||
files=files,
|
|
||||||
timeout=600,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
logger.info("COORDINATOR response: Progress + File updated")
|
logger.info("COORDINATOR response: Progress + File updated")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error calling COORDINATOR: {e}")
|
logger.error(f"Error calling COORDINATOR: {e}")
|
||||||
|
|
@ -72,7 +72,7 @@ def convert_pdf_async(temp_path, pitchbook_id):
|
||||||
logger.error(f"Exception in OCR processing: {str(e)}", exc_info=True)
|
logger.error(f"Exception in OCR processing: {str(e)}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/ocr", methods=["POST"])
|
@app.route('/ocr', methods=['POST'])
|
||||||
def convert_extract_text_from_pdf():
|
def convert_extract_text_from_pdf():
|
||||||
if "file" not in request.files:
|
if "file" not in request.files:
|
||||||
return {"error": "No file"}, 400
|
return {"error": "No file"}, 400
|
||||||
|
|
@ -85,7 +85,7 @@ def convert_extract_text_from_pdf():
|
||||||
if not pitchbook_id:
|
if not pitchbook_id:
|
||||||
return {"error": "No ID"}, 400
|
return {"error": "No ID"}, 400
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
|
||||||
file.seek(0)
|
file.seek(0)
|
||||||
temp_file.write(file.read())
|
temp_file.write(file.read())
|
||||||
temp_path = Path(temp_file.name)
|
temp_path = Path(temp_file.name)
|
||||||
|
|
@ -93,7 +93,10 @@ def convert_extract_text_from_pdf():
|
||||||
thread = threading.Thread(target=convert_pdf_async, args=(temp_path, pitchbook_id))
|
thread = threading.Thread(target=convert_pdf_async, args=(temp_path, pitchbook_id))
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
return {"status": "sent", "message": "PDF successfully OCR'd and processed"}, 200
|
return {
|
||||||
|
"status": "sent",
|
||||||
|
"message": "PDF successfully OCR'd and processed"
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,9 @@ log_folder = TEMP_DIR / "logs"
|
||||||
output_folder.mkdir(exist_ok=True)
|
output_folder.mkdir(exist_ok=True)
|
||||||
log_folder.mkdir(exist_ok=True)
|
log_folder.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def pdf_to_json(pdf_input):
|
def pdf_to_json(pdf_input):
|
||||||
try:
|
try:
|
||||||
if hasattr(pdf_input, "read"):
|
if hasattr(pdf_input, 'read'):
|
||||||
pdf_input.seek(0)
|
pdf_input.seek(0)
|
||||||
|
|
||||||
with pdfplumber.open(pdf_input) as pdf:
|
with pdfplumber.open(pdf_input) as pdf:
|
||||||
|
|
@ -84,9 +83,7 @@ def ocr_pdf(input_file_path: Path):
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
if output_file.exists():
|
if output_file.exists():
|
||||||
logger.info(
|
logger.info(f"OCR successful, output file size: {output_file.stat().st_size} bytes")
|
||||||
f"OCR successful, output file size: {output_file.stat().st_size} bytes"
|
|
||||||
)
|
|
||||||
return output_file
|
return output_file
|
||||||
else:
|
else:
|
||||||
logger.error(f"OCR completed but output file not found: {output_file}")
|
logger.error(f"OCR completed but output file not found: {output_file}")
|
||||||
|
|
@ -122,4 +119,4 @@ def extract_text_to_json(pdf_path: Path):
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to extract text to JSON: {e}")
|
logger.error(f"Failed to extract text to JSON: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
@ -11,8 +11,6 @@ COPY requirements.txt /app
|
||||||
|
|
||||||
RUN pip install --upgrade pip
|
RUN pip install --upgrade pip
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
RUN pip install flask-cors
|
|
||||||
|
|
||||||
|
|
||||||
RUN python -m spacy download en_core_web_sm
|
RUN python -m spacy download en_core_web_sm
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,14 @@
|
||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify
|
||||||
from extractSpacy import extract, load_model
|
from extractSpacy import extract
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from flask_cors import CORS
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
training_status = {"running": False}
|
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
|
||||||
|
|
||||||
COORDINATOR_URL = os.getenv("COORDINATOR_URL", "http://coordinator:5000")
|
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():
|
def extract_pdf():
|
||||||
json_data = request.get_json()
|
json_data = request.get_json()
|
||||||
|
|
||||||
|
|
@ -28,19 +16,19 @@ def extract_pdf():
|
||||||
pages_data = json_data["extracted_text_per_page"]
|
pages_data = json_data["extracted_text_per_page"]
|
||||||
|
|
||||||
entities_json = extract(pages_data)
|
entities_json = extract(pages_data)
|
||||||
entities = (
|
entities = json.loads(entities_json) if isinstance(entities_json, str) else entities_json
|
||||||
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] Sending to validate service: {VALIDATE_SERVICE_URL}")
|
||||||
print(f"[SPACY] Payload: {validate_payload} entities for pitchbook {pitchbook_id}")
|
print(f"[SPACY] Payload: {validate_payload} entities for pitchbook {pitchbook_id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(VALIDATE_SERVICE_URL, json=validate_payload, timeout=600)
|
||||||
VALIDATE_SERVICE_URL, json=validate_payload, timeout=600
|
|
||||||
)
|
|
||||||
print(f"[SPACY] Validate service response: {response.status_code}")
|
print(f"[SPACY] Validate service response: {response.status_code}")
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print(f"[SPACY] Validate service error: {response.text}")
|
print(f"[SPACY] Validate service error: {response.text}")
|
||||||
|
|
@ -50,90 +38,5 @@ def extract_pdf():
|
||||||
return jsonify("Sent to validate-service"), 200
|
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 = []
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/train", methods=["POST"])
|
|
||||||
def trigger_training():
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
Thread(target=run_training).start()
|
|
||||||
return jsonify({"message": "Training gestartet"}), 200
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/reload-model", methods=["POST"])
|
|
||||||
def reload_model():
|
|
||||||
try:
|
|
||||||
load_model()
|
|
||||||
return jsonify({"message": "Modell wurde erfolgreich neu geladen."}), 200
|
|
||||||
except Exception as e:
|
|
||||||
return (
|
|
||||||
jsonify({"error": "Fehler beim Neuladen des Modells", "details": str(e)}),
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_training():
|
|
||||||
training_status["running"] = True
|
|
||||||
notify_coordinator(True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if os.path.exists("output/model-last"):
|
|
||||||
shutil.copytree(
|
|
||||||
"output/model-last", "output/model-backup", dirs_exist_ok=True
|
|
||||||
)
|
|
||||||
subprocess.run(["python", "spacy_training/ner_trainer.py"], check=True)
|
|
||||||
load_model()
|
|
||||||
except Exception as e:
|
|
||||||
print("Training failed:", e)
|
|
||||||
training_status["running"] = False
|
|
||||||
notify_coordinator(False)
|
|
||||||
|
|
||||||
|
|
||||||
def notify_coordinator(running: bool):
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"{COORDINATOR_URL}/api/spacy/training/status", json={"running": running}
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f"[SPACY] Coordinator: running = {running}, Status = {response.status_code}"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[SPACY] Fehler beim Senden des Trainingsstatus: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5052, debug=True)
|
app.run(host="0.0.0.0", port=5052, debug=True)
|
||||||
|
|
@ -2,25 +2,9 @@ import spacy
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
model_path = os.path.join(current_dir, "spacy_training/output/model-last")
|
model_path = os.path.join(current_dir, "spacy_training/output/model-last")
|
||||||
|
nlp = spacy.load(model_path)
|
||||||
|
|
||||||
# Globales NLP-Modell
|
|
||||||
nlp = None
|
|
||||||
|
|
||||||
|
|
||||||
def load_model():
|
|
||||||
global nlp
|
|
||||||
print("[INFO] Lade SpaCy-Modell aus spacy_training/output/model-last ...")
|
|
||||||
nlp = spacy.load("spacy_training/output/model-last")
|
|
||||||
print("[INFO] Modell erfolgreich geladen.")
|
|
||||||
|
|
||||||
|
|
||||||
# Initial einmal laden
|
|
||||||
load_model()
|
|
||||||
|
|
||||||
|
|
||||||
def extract(pages_json):
|
def extract(pages_json):
|
||||||
results = []
|
results = []
|
||||||
|
|
@ -35,6 +19,10 @@ def extract(pages_json):
|
||||||
|
|
||||||
spacy_result = nlp(text)
|
spacy_result = nlp(text)
|
||||||
for ent in spacy_result.ents:
|
for ent in spacy_result.ents:
|
||||||
results.append({"label": ent.label_, "entity": ent.text, "page": page_num})
|
results.append({
|
||||||
|
"label": ent.label_,
|
||||||
|
"entity": ent.text,
|
||||||
|
"page": page_num
|
||||||
|
})
|
||||||
|
|
||||||
return json.dumps(results, indent=2, ensure_ascii=False)
|
return json.dumps(results, indent=2, ensure_ascii=False)
|
||||||
|
|
@ -3,5 +3,4 @@ spacy-transformers==1.3.3
|
||||||
transformers==4.35.2
|
transformers==4.35.2
|
||||||
torch
|
torch
|
||||||
flask
|
flask
|
||||||
requests
|
requests
|
||||||
flask-cors
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
from flask import Flask, request, jsonify
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
ANNOTATION_FILE = "spacy_training/annotation_data.json"
|
|
||||||
|
|
||||||
|
|
||||||
@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 = []
|
|
||||||
|
|
||||||
# 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
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,85 @@
|
||||||
|
# This is an auto-generated partial config. To use it with 'spacy train'
|
||||||
|
# you can run spacy init fill-config to auto-fill all default settings:
|
||||||
|
# python -m spacy init fill-config ./base_config.cfg ./config.cfg
|
||||||
|
[paths]
|
||||||
|
train = ./data/train.spacy
|
||||||
|
dev = ./data/train.spacy
|
||||||
|
vectors = null
|
||||||
|
[system]
|
||||||
|
gpu_allocator = null
|
||||||
|
|
||||||
|
[nlp]
|
||||||
|
lang = "de"
|
||||||
|
pipeline = ["tok2vec","ner"]
|
||||||
|
batch_size = 1000
|
||||||
|
|
||||||
|
[components]
|
||||||
|
|
||||||
|
[components.tok2vec]
|
||||||
|
factory = "tok2vec"
|
||||||
|
|
||||||
|
[components.tok2vec.model]
|
||||||
|
@architectures = "spacy.Tok2Vec.v2"
|
||||||
|
|
||||||
|
[components.tok2vec.model.embed]
|
||||||
|
@architectures = "spacy.MultiHashEmbed.v2"
|
||||||
|
width = ${components.tok2vec.model.encode.width}
|
||||||
|
attrs = ["NORM", "PREFIX", "SUFFIX", "SHAPE"]
|
||||||
|
rows = [5000, 1000, 2500, 2500]
|
||||||
|
include_static_vectors = false
|
||||||
|
|
||||||
|
[components.tok2vec.model.encode]
|
||||||
|
@architectures = "spacy.MaxoutWindowEncoder.v2"
|
||||||
|
width = 96
|
||||||
|
depth = 4
|
||||||
|
window_size = 1
|
||||||
|
maxout_pieces = 3
|
||||||
|
|
||||||
|
[components.ner]
|
||||||
|
factory = "ner"
|
||||||
|
|
||||||
|
[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.Tok2VecListener.v1"
|
||||||
|
width = ${components.tok2vec.model.encode.width}
|
||||||
|
|
||||||
|
[corpora]
|
||||||
|
|
||||||
|
[corpora.train]
|
||||||
|
@readers = "spacy.Corpus.v1"
|
||||||
|
path = ${paths.train}
|
||||||
|
max_length = 0
|
||||||
|
|
||||||
|
[corpora.dev]
|
||||||
|
@readers = "spacy.Corpus.v1"
|
||||||
|
path = ${paths.dev}
|
||||||
|
max_length = 0
|
||||||
|
|
||||||
|
[training]
|
||||||
|
dev_corpus = "corpora.dev"
|
||||||
|
train_corpus = "corpora.train"
|
||||||
|
|
||||||
|
[training.optimizer]
|
||||||
|
@optimizers = "Adam.v1"
|
||||||
|
|
||||||
|
[training.batcher]
|
||||||
|
@batchers = "spacy.batch_by_words.v1"
|
||||||
|
discard_oversize = false
|
||||||
|
tolerance = 0.2
|
||||||
|
|
||||||
|
[training.batcher.size]
|
||||||
|
@schedules = "compounding.v1"
|
||||||
|
start = 100
|
||||||
|
stop = 1000
|
||||||
|
compound = 1.001
|
||||||
|
|
||||||
|
[initialize]
|
||||||
|
vectors = ${paths.vectors}
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
[paths]
|
[paths]
|
||||||
train = null
|
train = "./data/train.spacy"
|
||||||
dev = null
|
dev = "./data/train.spacy"
|
||||||
vectors = null
|
vectors = null
|
||||||
init_tok2vec = null
|
init_tok2vec = null
|
||||||
|
|
||||||
[system]
|
[system]
|
||||||
seed = 0
|
|
||||||
gpu_allocator = null
|
gpu_allocator = null
|
||||||
|
seed = 0
|
||||||
|
|
||||||
[nlp]
|
[nlp]
|
||||||
lang = "de"
|
lang = "de"
|
||||||
pipeline = ["ner"]
|
pipeline = ["tok2vec","ner"]
|
||||||
|
batch_size = 1000
|
||||||
disabled = []
|
disabled = []
|
||||||
before_creation = null
|
before_creation = null
|
||||||
after_creation = null
|
after_creation = null
|
||||||
after_pipeline_creation = null
|
after_pipeline_creation = null
|
||||||
batch_size = 1000
|
|
||||||
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
|
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
|
||||||
vectors = {"@vectors":"spacy.Vectors.v1"}
|
vectors = {"@vectors":"spacy.Vectors.v1"}
|
||||||
|
|
||||||
|
|
@ -38,34 +38,51 @@ use_upper = true
|
||||||
nO = null
|
nO = null
|
||||||
|
|
||||||
[components.ner.model.tok2vec]
|
[components.ner.model.tok2vec]
|
||||||
@architectures = "spacy.HashEmbedCNN.v2"
|
@architectures = "spacy.Tok2VecListener.v1"
|
||||||
pretrained_vectors = null
|
width = ${components.tok2vec.model.encode.width}
|
||||||
|
upstream = "*"
|
||||||
|
|
||||||
|
[components.tok2vec]
|
||||||
|
factory = "tok2vec"
|
||||||
|
|
||||||
|
[components.tok2vec.model]
|
||||||
|
@architectures = "spacy.Tok2Vec.v2"
|
||||||
|
|
||||||
|
[components.tok2vec.model.embed]
|
||||||
|
@architectures = "spacy.MultiHashEmbed.v2"
|
||||||
|
width = ${components.tok2vec.model.encode.width}
|
||||||
|
attrs = ["NORM","PREFIX","SUFFIX","SHAPE"]
|
||||||
|
rows = [5000,1000,2500,2500]
|
||||||
|
include_static_vectors = false
|
||||||
|
|
||||||
|
[components.tok2vec.model.encode]
|
||||||
|
@architectures = "spacy.MaxoutWindowEncoder.v2"
|
||||||
width = 96
|
width = 96
|
||||||
depth = 4
|
depth = 4
|
||||||
embed_size = 2000
|
|
||||||
window_size = 1
|
window_size = 1
|
||||||
maxout_pieces = 3
|
maxout_pieces = 3
|
||||||
subword_features = true
|
|
||||||
|
|
||||||
[corpora]
|
[corpora]
|
||||||
|
|
||||||
[corpora.dev]
|
[corpora.dev]
|
||||||
@readers = "spacy.Corpus.v1"
|
@readers = "spacy.Corpus.v1"
|
||||||
path = ${paths.dev}
|
path = ${paths.dev}
|
||||||
gold_preproc = false
|
|
||||||
max_length = 0
|
max_length = 0
|
||||||
|
gold_preproc = false
|
||||||
limit = 0
|
limit = 0
|
||||||
augmenter = null
|
augmenter = null
|
||||||
|
|
||||||
[corpora.train]
|
[corpora.train]
|
||||||
@readers = "spacy.Corpus.v1"
|
@readers = "spacy.Corpus.v1"
|
||||||
path = ${paths.train}
|
path = ${paths.train}
|
||||||
gold_preproc = false
|
|
||||||
max_length = 0
|
max_length = 0
|
||||||
|
gold_preproc = false
|
||||||
limit = 0
|
limit = 0
|
||||||
augmenter = null
|
augmenter = null
|
||||||
|
|
||||||
[training]
|
[training]
|
||||||
|
dev_corpus = "corpora.dev"
|
||||||
|
train_corpus = "corpora.train"
|
||||||
seed = ${system.seed}
|
seed = ${system.seed}
|
||||||
gpu_allocator = ${system.gpu_allocator}
|
gpu_allocator = ${system.gpu_allocator}
|
||||||
dropout = 0.1
|
dropout = 0.1
|
||||||
|
|
@ -76,8 +93,6 @@ max_steps = 20000
|
||||||
eval_frequency = 200
|
eval_frequency = 200
|
||||||
frozen_components = []
|
frozen_components = []
|
||||||
annotating_components = []
|
annotating_components = []
|
||||||
dev_corpus = "corpora.dev"
|
|
||||||
train_corpus = "corpora.train"
|
|
||||||
before_to_disk = null
|
before_to_disk = null
|
||||||
before_update = null
|
before_update = null
|
||||||
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import json
|
|
||||||
|
|
||||||
# Alte Daten laden
|
|
||||||
with open("annotation_data.json", "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
# Neue Kennzahl (als Dict/Objekt)
|
|
||||||
neuer_eintrag = {
|
|
||||||
"text": "Hier steht der Beispielsatz mit der neuen Kennzahl.",
|
|
||||||
"entities": [[1, 5, "NEUEKENNZAHL"]],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Anhängen
|
|
||||||
data.append(neuer_eintrag)
|
|
||||||
|
|
||||||
# Wieder speichern
|
|
||||||
with open("annotation_data.json", "w", encoding="utf-8") as f:
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import spacy
|
|
||||||
from spacy.training.example import Example
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def load_data(file_path):
|
|
||||||
with open(file_path, "r", encoding="utf8") as f:
|
|
||||||
raw = json.load(f)
|
|
||||||
return [
|
|
||||||
(
|
|
||||||
entry["text"],
|
|
||||||
{
|
|
||||||
"entities": [
|
|
||||||
(start, end, label) for start, end, label in entry["entities"]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
for entry in raw
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Stelle sicher, dass der "output"-Ordner existiert
|
|
||||||
os.makedirs("output", exist_ok=True)
|
|
||||||
|
|
||||||
TRAIN_DATA = load_data(os.path.join("spacy_training", "annotation_data.json"))
|
|
||||||
|
|
||||||
nlp = spacy.blank("de")
|
|
||||||
ner = nlp.add_pipe("ner")
|
|
||||||
ner.add_label("KENNZAHL")
|
|
||||||
|
|
||||||
optimizer = nlp.begin_training()
|
|
||||||
for i in range(20):
|
|
||||||
for text, annotations in TRAIN_DATA:
|
|
||||||
example = Example.from_dict(nlp.make_doc(text), annotations)
|
|
||||||
nlp.update([example], drop=0.2, sgd=optimizer)
|
|
||||||
|
|
||||||
temp_model_dir = "output/temp-model"
|
|
||||||
final_model_dir = "output/model-last"
|
|
||||||
backup_dir = "output/model-backup"
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Vorheriges temporäres Verzeichnis entfernen
|
|
||||||
if os.path.exists(temp_model_dir):
|
|
||||||
shutil.rmtree(temp_model_dir)
|
|
||||||
|
|
||||||
# Modell zunächst in temp speichern
|
|
||||||
nlp.to_disk(temp_model_dir)
|
|
||||||
|
|
||||||
# Backup der letzten Version (falls vorhanden)
|
|
||||||
if os.path.exists(final_model_dir):
|
|
||||||
if os.path.exists(backup_dir):
|
|
||||||
shutil.rmtree(backup_dir)
|
|
||||||
shutil.copytree(final_model_dir, backup_dir)
|
|
||||||
|
|
||||||
shutil.rmtree(final_model_dir)
|
|
||||||
|
|
||||||
# Modell verschieben
|
|
||||||
shutil.move(temp_model_dir, final_model_dir)
|
|
||||||
print("[INFO] Training abgeschlossen und Modell gespeichert.")
|
|
||||||
|
|
||||||
nlp.to_disk("spacy_training/output/model-last")
|
|
||||||
|
|
||||||
# Training beendet – Status auf False setzen
|
|
||||||
with open("spacy_training/training_running.json", "w") as f:
|
|
||||||
json.dump({"running": False}, f)
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[FEHLER] Während des Trainings ist ein Fehler aufgetreten: {e}")
|
|
||||||
if os.path.exists(temp_model_dir):
|
|
||||||
shutil.rmtree(temp_model_dir)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -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":[
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
|
@ -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À
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
<EFBFBD>
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<EFBFBD>
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"mode":"default"
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
[paths]
|
[paths]
|
||||||
train = null
|
train = "./data/train.spacy"
|
||||||
dev = null
|
dev = "./data/train.spacy"
|
||||||
vectors = null
|
vectors = null
|
||||||
init_tok2vec = null
|
init_tok2vec = null
|
||||||
|
|
||||||
[system]
|
[system]
|
||||||
seed = 0
|
|
||||||
gpu_allocator = null
|
gpu_allocator = null
|
||||||
|
seed = 0
|
||||||
|
|
||||||
[nlp]
|
[nlp]
|
||||||
lang = "de"
|
lang = "de"
|
||||||
pipeline = ["ner"]
|
pipeline = ["tok2vec","ner"]
|
||||||
|
batch_size = 1000
|
||||||
disabled = []
|
disabled = []
|
||||||
before_creation = null
|
before_creation = null
|
||||||
after_creation = null
|
after_creation = null
|
||||||
after_pipeline_creation = null
|
after_pipeline_creation = null
|
||||||
batch_size = 1000
|
|
||||||
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
|
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
|
||||||
vectors = {"@vectors":"spacy.Vectors.v1"}
|
vectors = {"@vectors":"spacy.Vectors.v1"}
|
||||||
|
|
||||||
|
|
@ -38,34 +38,51 @@ use_upper = true
|
||||||
nO = null
|
nO = null
|
||||||
|
|
||||||
[components.ner.model.tok2vec]
|
[components.ner.model.tok2vec]
|
||||||
@architectures = "spacy.HashEmbedCNN.v2"
|
@architectures = "spacy.Tok2VecListener.v1"
|
||||||
pretrained_vectors = null
|
width = ${components.tok2vec.model.encode.width}
|
||||||
|
upstream = "*"
|
||||||
|
|
||||||
|
[components.tok2vec]
|
||||||
|
factory = "tok2vec"
|
||||||
|
|
||||||
|
[components.tok2vec.model]
|
||||||
|
@architectures = "spacy.Tok2Vec.v2"
|
||||||
|
|
||||||
|
[components.tok2vec.model.embed]
|
||||||
|
@architectures = "spacy.MultiHashEmbed.v2"
|
||||||
|
width = ${components.tok2vec.model.encode.width}
|
||||||
|
attrs = ["NORM","PREFIX","SUFFIX","SHAPE"]
|
||||||
|
rows = [5000,1000,2500,2500]
|
||||||
|
include_static_vectors = false
|
||||||
|
|
||||||
|
[components.tok2vec.model.encode]
|
||||||
|
@architectures = "spacy.MaxoutWindowEncoder.v2"
|
||||||
width = 96
|
width = 96
|
||||||
depth = 4
|
depth = 4
|
||||||
embed_size = 2000
|
|
||||||
window_size = 1
|
window_size = 1
|
||||||
maxout_pieces = 3
|
maxout_pieces = 3
|
||||||
subword_features = true
|
|
||||||
|
|
||||||
[corpora]
|
[corpora]
|
||||||
|
|
||||||
[corpora.dev]
|
[corpora.dev]
|
||||||
@readers = "spacy.Corpus.v1"
|
@readers = "spacy.Corpus.v1"
|
||||||
path = ${paths.dev}
|
path = ${paths.dev}
|
||||||
gold_preproc = false
|
|
||||||
max_length = 0
|
max_length = 0
|
||||||
|
gold_preproc = false
|
||||||
limit = 0
|
limit = 0
|
||||||
augmenter = null
|
augmenter = null
|
||||||
|
|
||||||
[corpora.train]
|
[corpora.train]
|
||||||
@readers = "spacy.Corpus.v1"
|
@readers = "spacy.Corpus.v1"
|
||||||
path = ${paths.train}
|
path = ${paths.train}
|
||||||
gold_preproc = false
|
|
||||||
max_length = 0
|
max_length = 0
|
||||||
|
gold_preproc = false
|
||||||
limit = 0
|
limit = 0
|
||||||
augmenter = null
|
augmenter = null
|
||||||
|
|
||||||
[training]
|
[training]
|
||||||
|
dev_corpus = "corpora.dev"
|
||||||
|
train_corpus = "corpora.train"
|
||||||
seed = ${system.seed}
|
seed = ${system.seed}
|
||||||
gpu_allocator = ${system.gpu_allocator}
|
gpu_allocator = ${system.gpu_allocator}
|
||||||
dropout = 0.1
|
dropout = 0.1
|
||||||
|
|
@ -76,8 +93,6 @@ max_steps = 20000
|
||||||
eval_frequency = 200
|
eval_frequency = 200
|
||||||
frozen_components = []
|
frozen_components = []
|
||||||
annotating_components = []
|
annotating_components = []
|
||||||
dev_corpus = "corpora.dev"
|
|
||||||
train_corpus = "corpora.train"
|
|
||||||
before_to_disk = null
|
before_to_disk = null
|
||||||
before_update = null
|
before_update = null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,11 @@
|
||||||
"mode":"default"
|
"mode":"default"
|
||||||
},
|
},
|
||||||
"labels":{
|
"labels":{
|
||||||
|
"tok2vec":[
|
||||||
|
|
||||||
|
],
|
||||||
"ner":[
|
"ner":[
|
||||||
"AUSSCH\u00dcTTUNGSRENDITE",
|
"AUSSCH\u00dcTTUNGSRENDITE",
|
||||||
"KENNZAHL",
|
|
||||||
"LAUFZEIT",
|
"LAUFZEIT",
|
||||||
"L\u00c4NDERALLOKATION",
|
"L\u00c4NDERALLOKATION",
|
||||||
"MANAGMENTGEB\u00dcHREN",
|
"MANAGMENTGEB\u00dcHREN",
|
||||||
|
|
@ -31,12 +33,68 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"pipeline":[
|
"pipeline":[
|
||||||
|
"tok2vec",
|
||||||
"ner"
|
"ner"
|
||||||
],
|
],
|
||||||
"components":[
|
"components":[
|
||||||
|
"tok2vec",
|
||||||
"ner"
|
"ner"
|
||||||
],
|
],
|
||||||
"disabled":[
|
"disabled":[
|
||||||
|
|
||||||
]
|
],
|
||||||
|
"performance":{
|
||||||
|
"ents_f":0.9608938547,
|
||||||
|
"ents_p":1.0,
|
||||||
|
"ents_r":0.9247311828,
|
||||||
|
"ents_per_type":{
|
||||||
|
"RISIKOPROFIL":{
|
||||||
|
"p":1.0,
|
||||||
|
"r":1.0,
|
||||||
|
"f":1.0
|
||||||
|
},
|
||||||
|
"AUSSCH\u00dcTTUNGSRENDITE":{
|
||||||
|
"p":1.0,
|
||||||
|
"r":0.5925925926,
|
||||||
|
"f":0.7441860465
|
||||||
|
},
|
||||||
|
"LAUFZEIT":{
|
||||||
|
"p":1.0,
|
||||||
|
"r":1.0,
|
||||||
|
"f":1.0
|
||||||
|
},
|
||||||
|
"RENDITE":{
|
||||||
|
"p":1.0,
|
||||||
|
"r":1.0,
|
||||||
|
"f":1.0
|
||||||
|
},
|
||||||
|
"L\u00c4NDERALLOKATION":{
|
||||||
|
"p":1.0,
|
||||||
|
"r":0.8965517241,
|
||||||
|
"f":0.9454545455
|
||||||
|
},
|
||||||
|
"ZIELRENDITE":{
|
||||||
|
"p":1.0,
|
||||||
|
"r":1.0,
|
||||||
|
"f":1.0
|
||||||
|
},
|
||||||
|
"ZIELAUSSCH\u00dcTTUNG":{
|
||||||
|
"p":1.0,
|
||||||
|
"r":1.0,
|
||||||
|
"f":1.0
|
||||||
|
},
|
||||||
|
"MANAGMENTGEB\u00dcHREN":{
|
||||||
|
"p":1.0,
|
||||||
|
"r":1.0,
|
||||||
|
"f":1.0
|
||||||
|
},
|
||||||
|
"SEKTORENALLOKATION":{
|
||||||
|
"p":1.0,
|
||||||
|
"r":1.0,
|
||||||
|
"f":1.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tok2vec_loss":33.6051129291,
|
||||||
|
"ner_loss":740.5764770508
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
|
@ -1 +1 @@
|
||||||
‚¥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},"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},"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},"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},"5":{"":1}}£cfg<66>§neg_keyÀ
|
‚¥movesÚL{"0":{},"1":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2},"2":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2},"3":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2},"4":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2,"":1},"5":{"":1}}£cfg<66>§neg_keyÀ
|
||||||
|
|
@ -52,7 +52,9 @@
|
||||||
"*",
|
"*",
|
||||||
"+",
|
"+",
|
||||||
"+/D",
|
"+/D",
|
||||||
|
"+/d",
|
||||||
"+AU",
|
"+AU",
|
||||||
|
"+au",
|
||||||
",",
|
",",
|
||||||
",00",
|
",00",
|
||||||
",03",
|
",03",
|
||||||
|
|
@ -127,15 +129,21 @@
|
||||||
"/080%/212%/491",
|
"/080%/212%/491",
|
||||||
"/2,12",
|
"/2,12",
|
||||||
"/3",
|
"/3",
|
||||||
|
"/AuM",
|
||||||
"/Core+",
|
"/Core+",
|
||||||
"/FK",
|
"/FK",
|
||||||
"/XX",
|
"/XX",
|
||||||
|
"/XxX",
|
||||||
"/Xxxx+",
|
"/Xxxx+",
|
||||||
|
"/aum",
|
||||||
"/core+",
|
"/core+",
|
||||||
"/d",
|
"/d",
|
||||||
"/d,dd",
|
"/d,dd",
|
||||||
"/ddd%/ddd%/ddd",
|
"/ddd%/ddd%/ddd",
|
||||||
"/fk",
|
"/fk",
|
||||||
|
"/xx",
|
||||||
|
"/xxx",
|
||||||
|
"/xxxx+",
|
||||||
"0",
|
"0",
|
||||||
"0%+",
|
"0%+",
|
||||||
"0,0",
|
"0,0",
|
||||||
|
|
@ -273,6 +281,7 @@
|
||||||
"45",
|
"45",
|
||||||
"491",
|
"491",
|
||||||
"5",
|
"5",
|
||||||
|
"5%+",
|
||||||
"5,0",
|
"5,0",
|
||||||
"5,00",
|
"5,00",
|
||||||
"5,1",
|
"5,1",
|
||||||
|
|
@ -302,6 +311,7 @@
|
||||||
"7",
|
"7",
|
||||||
"7,1",
|
"7,1",
|
||||||
"7,5",
|
"7,5",
|
||||||
|
"7,5%+",
|
||||||
"7,50",
|
"7,50",
|
||||||
"7,50%+",
|
"7,50%+",
|
||||||
"7.5",
|
"7.5",
|
||||||
|
|
@ -318,6 +328,8 @@
|
||||||
"8-D",
|
"8-D",
|
||||||
"8-d",
|
"8-d",
|
||||||
"80",
|
"80",
|
||||||
|
"800",
|
||||||
|
"84,0",
|
||||||
"85",
|
"85",
|
||||||
"8D",
|
"8D",
|
||||||
"8d",
|
"8d",
|
||||||
|
|
@ -457,6 +469,7 @@
|
||||||
"Abteilung",
|
"Abteilung",
|
||||||
"Access",
|
"Access",
|
||||||
"Add",
|
"Add",
|
||||||
|
"Agreements",
|
||||||
"Aktive",
|
"Aktive",
|
||||||
"Aktueller",
|
"Aktueller",
|
||||||
"AlF",
|
"AlF",
|
||||||
|
|
@ -464,6 +477,7 @@
|
||||||
"Allocation",
|
"Allocation",
|
||||||
"Allokation",
|
"Allokation",
|
||||||
"Allokationsprofil",
|
"Allokationsprofil",
|
||||||
|
"Alternative",
|
||||||
"Amsterdam",
|
"Amsterdam",
|
||||||
"Andere",
|
"Andere",
|
||||||
"Anfrage",
|
"Anfrage",
|
||||||
|
|
@ -499,6 +513,7 @@
|
||||||
"Artikel",
|
"Artikel",
|
||||||
"Asset",
|
"Asset",
|
||||||
"Assets",
|
"Assets",
|
||||||
|
"AuM",
|
||||||
"Aufbau",
|
"Aufbau",
|
||||||
"Auflage",
|
"Auflage",
|
||||||
"Aufl\u00f6sung",
|
"Aufl\u00f6sung",
|
||||||
|
|
@ -568,7 +583,10 @@
|
||||||
"COR",
|
"COR",
|
||||||
"CORE",
|
"CORE",
|
||||||
"CSU",
|
"CSU",
|
||||||
|
"CSp",
|
||||||
|
"Cash",
|
||||||
"Cash-Flow-Stabilit\u00e4t",
|
"Cash-Flow-Stabilit\u00e4t",
|
||||||
|
"Cash-flow",
|
||||||
"Chr",
|
"Chr",
|
||||||
"Chr.",
|
"Chr.",
|
||||||
"Cie",
|
"Cie",
|
||||||
|
|
@ -630,6 +648,7 @@
|
||||||
"E.",
|
"E.",
|
||||||
"EAN",
|
"EAN",
|
||||||
"ECLF",
|
"ECLF",
|
||||||
|
"EIT",
|
||||||
"EM",
|
"EM",
|
||||||
"ERD",
|
"ERD",
|
||||||
"ESG-",
|
"ESG-",
|
||||||
|
|
@ -664,6 +683,7 @@
|
||||||
"F",
|
"F",
|
||||||
"F.",
|
"F.",
|
||||||
"FDR",
|
"FDR",
|
||||||
|
"FIL",
|
||||||
"FR",
|
"FR",
|
||||||
"FRANCE",
|
"FRANCE",
|
||||||
"FUND",
|
"FUND",
|
||||||
|
|
@ -774,9 +794,11 @@
|
||||||
"III.",
|
"III.",
|
||||||
"INK",
|
"INK",
|
||||||
"INREV",
|
"INREV",
|
||||||
|
"ION",
|
||||||
"IRR",
|
"IRR",
|
||||||
"IRR6.5",
|
"IRR6.5",
|
||||||
"IT",
|
"IT",
|
||||||
|
"ITE",
|
||||||
"IUM",
|
"IUM",
|
||||||
"IV",
|
"IV",
|
||||||
"IV.",
|
"IV.",
|
||||||
|
|
@ -802,6 +824,7 @@
|
||||||
"Interner",
|
"Interner",
|
||||||
"Invastitionsfokus",
|
"Invastitionsfokus",
|
||||||
"Investftionsvolumen",
|
"Investftionsvolumen",
|
||||||
|
"Investing",
|
||||||
"Investitionen",
|
"Investitionen",
|
||||||
"Investitions-annahmen",
|
"Investitions-annahmen",
|
||||||
"Investitionsphase",
|
"Investitionsphase",
|
||||||
|
|
@ -811,6 +834,7 @@
|
||||||
"Investmentzeitraum",
|
"Investmentzeitraum",
|
||||||
"Investoren",
|
"Investoren",
|
||||||
"Investtionszeltraum",
|
"Investtionszeltraum",
|
||||||
|
"Investtionszeltraum,10",
|
||||||
"Investtonszeltraum",
|
"Investtonszeltraum",
|
||||||
"Ireland",
|
"Ireland",
|
||||||
"Irland",
|
"Irland",
|
||||||
|
|
@ -842,7 +866,6 @@
|
||||||
"K.",
|
"K.",
|
||||||
"K.O.",
|
"K.O.",
|
||||||
"KAGB",
|
"KAGB",
|
||||||
"KENNZAHL",
|
|
||||||
"KINGDOM",
|
"KINGDOM",
|
||||||
"KVG",
|
"KVG",
|
||||||
"Kapitalstruktur",
|
"Kapitalstruktur",
|
||||||
|
|
@ -882,6 +905,7 @@
|
||||||
"Light",
|
"Light",
|
||||||
"Limited",
|
"Limited",
|
||||||
"Lisbon",
|
"Lisbon",
|
||||||
|
"Loan-to-Value",
|
||||||
"Local",
|
"Local",
|
||||||
"Logistics",
|
"Logistics",
|
||||||
"Logistik",
|
"Logistik",
|
||||||
|
|
@ -1032,10 +1056,12 @@
|
||||||
"Prof",
|
"Prof",
|
||||||
"Prof.",
|
"Prof.",
|
||||||
"Professor",
|
"Professor",
|
||||||
|
"Prognose",
|
||||||
"Prognostiderte",
|
"Prognostiderte",
|
||||||
"Prognostizierte",
|
"Prognostizierte",
|
||||||
"Projektentwicklungen",
|
"Projektentwicklungen",
|
||||||
"Projektentwicklungsrisiken",
|
"Projektentwicklungsrisiken",
|
||||||
|
"PropCo",
|
||||||
"Pt",
|
"Pt",
|
||||||
"Punkt",
|
"Punkt",
|
||||||
"Q",
|
"Q",
|
||||||
|
|
@ -1046,6 +1072,7 @@
|
||||||
"R.",
|
"R.",
|
||||||
"R.I.P.",
|
"R.I.P.",
|
||||||
"RE",
|
"RE",
|
||||||
|
"REN",
|
||||||
"RENDITE",
|
"RENDITE",
|
||||||
"REV",
|
"REV",
|
||||||
"REWE",
|
"REWE",
|
||||||
|
|
@ -1077,6 +1104,7 @@
|
||||||
"S",
|
"S",
|
||||||
"S'",
|
"S'",
|
||||||
"SCS",
|
"SCS",
|
||||||
|
"SCSp",
|
||||||
"SEKTORENALLOKATION",
|
"SEKTORENALLOKATION",
|
||||||
"SFDR",
|
"SFDR",
|
||||||
"SG-",
|
"SG-",
|
||||||
|
|
@ -1169,6 +1197,7 @@
|
||||||
"U.S.S.",
|
"U.S.S.",
|
||||||
"UK",
|
"UK",
|
||||||
"UND",
|
"UND",
|
||||||
|
"UNG",
|
||||||
"UNITED",
|
"UNITED",
|
||||||
"USt",
|
"USt",
|
||||||
"Univ",
|
"Univ",
|
||||||
|
|
@ -1235,6 +1264,7 @@
|
||||||
"XXXX",
|
"XXXX",
|
||||||
"XXXX-XXXX",
|
"XXXX-XXXX",
|
||||||
"XXXd.d",
|
"XXXd.d",
|
||||||
|
"XXXx",
|
||||||
"XXXxx",
|
"XXXxx",
|
||||||
"XXx",
|
"XXx",
|
||||||
"XXxxxx",
|
"XXxxxx",
|
||||||
|
|
@ -1259,15 +1289,18 @@
|
||||||
"Xxxx-XXX",
|
"Xxxx-XXX",
|
||||||
"Xxxx-Xxxx-Xxxxx",
|
"Xxxx-Xxxx-Xxxxx",
|
||||||
"Xxxx-Xxxxx-XXX",
|
"Xxxx-Xxxxx-XXX",
|
||||||
|
"Xxxx-xx-Xxxxx",
|
||||||
"Xxxx-xxx",
|
"Xxxx-xxx",
|
||||||
"Xxxx-xxxx",
|
"Xxxx-xxxx",
|
||||||
"Xxxx.",
|
"Xxxx.",
|
||||||
"Xxxx.-Xxx",
|
"Xxxx.-Xxx",
|
||||||
"Xxxx.-Xxx.",
|
"Xxxx.-Xxx.",
|
||||||
|
"XxxxXx",
|
||||||
"Xxxxx",
|
"Xxxxx",
|
||||||
"Xxxxx)-",
|
"Xxxxx)-",
|
||||||
"Xxxxx)/Xxxx",
|
"Xxxxx)/Xxxx",
|
||||||
"Xxxxx+",
|
"Xxxxx+",
|
||||||
|
"Xxxxx,dd",
|
||||||
"Xxxxx-",
|
"Xxxxx-",
|
||||||
"Xxxxx-XXX",
|
"Xxxxx-XXX",
|
||||||
"Xxxxx-XxX",
|
"Xxxxx-XxX",
|
||||||
|
|
@ -1348,7 +1381,6 @@
|
||||||
"a.g.",
|
"a.g.",
|
||||||
"a.m.",
|
"a.m.",
|
||||||
"a.z.",
|
"a.z.",
|
||||||
"a34",
|
|
||||||
"ab",
|
"ab",
|
||||||
"abb",
|
"abb",
|
||||||
"abb.",
|
"abb.",
|
||||||
|
|
@ -1378,11 +1410,14 @@
|
||||||
"advantage",
|
"advantage",
|
||||||
"ae",
|
"ae",
|
||||||
"aft",
|
"aft",
|
||||||
|
"agb",
|
||||||
"age",
|
"age",
|
||||||
|
"agreements",
|
||||||
"aha",
|
"aha",
|
||||||
"ahe",
|
"ahe",
|
||||||
"ahl",
|
"ahl",
|
||||||
"ahr",
|
"ahr",
|
||||||
|
"aif",
|
||||||
"ail",
|
"ail",
|
||||||
"aiming",
|
"aiming",
|
||||||
"ain",
|
"ain",
|
||||||
|
|
@ -1394,6 +1429,7 @@
|
||||||
"al.",
|
"al.",
|
||||||
"ald",
|
"ald",
|
||||||
"ale",
|
"ale",
|
||||||
|
"alf",
|
||||||
"all",
|
"all",
|
||||||
"allg",
|
"allg",
|
||||||
"allg.",
|
"allg.",
|
||||||
|
|
@ -1404,6 +1440,8 @@
|
||||||
"allokationsprofil",
|
"allokationsprofil",
|
||||||
"als",
|
"als",
|
||||||
"also",
|
"also",
|
||||||
|
"alt",
|
||||||
|
"alternative",
|
||||||
"aly",
|
"aly",
|
||||||
"am.",
|
"am.",
|
||||||
"ambulant",
|
"ambulant",
|
||||||
|
|
@ -1459,6 +1497,7 @@
|
||||||
"ary",
|
"ary",
|
||||||
"as",
|
"as",
|
||||||
"ase",
|
"ase",
|
||||||
|
"ash",
|
||||||
"ass",
|
"ass",
|
||||||
"asset",
|
"asset",
|
||||||
"assetor",
|
"assetor",
|
||||||
|
|
@ -1496,6 +1535,7 @@
|
||||||
"aussch\u00fcttungsrandite",
|
"aussch\u00fcttungsrandite",
|
||||||
"aussch\u00fcttungsrendite",
|
"aussch\u00fcttungsrendite",
|
||||||
"aussch\u00fcttungsrendites",
|
"aussch\u00fcttungsrendites",
|
||||||
|
"aut",
|
||||||
"ave",
|
"ave",
|
||||||
"ax.",
|
"ax.",
|
||||||
"b",
|
"b",
|
||||||
|
|
@ -1504,6 +1544,7 @@
|
||||||
"b.sc",
|
"b.sc",
|
||||||
"b.sc.",
|
"b.sc.",
|
||||||
"bahnhof",
|
"bahnhof",
|
||||||
|
"bal",
|
||||||
"balanced",
|
"balanced",
|
||||||
"basis",
|
"basis",
|
||||||
"bau",
|
"bau",
|
||||||
|
|
@ -1545,6 +1586,7 @@
|
||||||
"both",
|
"both",
|
||||||
"bps",
|
"bps",
|
||||||
"br.",
|
"br.",
|
||||||
|
"brands",
|
||||||
"broad",
|
"broad",
|
||||||
"brussels",
|
"brussels",
|
||||||
"bruttofondsverm\u00f6gens",
|
"bruttofondsverm\u00f6gens",
|
||||||
|
|
@ -1577,6 +1619,8 @@
|
||||||
"capital",
|
"capital",
|
||||||
"capped",
|
"capped",
|
||||||
"carbon",
|
"carbon",
|
||||||
|
"cash",
|
||||||
|
"cash-flow",
|
||||||
"cash-flow-stabilit\u00e4t",
|
"cash-flow-stabilit\u00e4t",
|
||||||
"cbd",
|
"cbd",
|
||||||
"cdu",
|
"cdu",
|
||||||
|
|
@ -1599,6 +1643,7 @@
|
||||||
"cl.",
|
"cl.",
|
||||||
"class",
|
"class",
|
||||||
"cle",
|
"cle",
|
||||||
|
"clf",
|
||||||
"closed",
|
"closed",
|
||||||
"closing",
|
"closing",
|
||||||
"closings",
|
"closings",
|
||||||
|
|
@ -1618,6 +1663,7 @@
|
||||||
"construction",
|
"construction",
|
||||||
"contract",
|
"contract",
|
||||||
"contracts",
|
"contracts",
|
||||||
|
"cor",
|
||||||
"core",
|
"core",
|
||||||
"core+",
|
"core+",
|
||||||
"core+/d",
|
"core+/d",
|
||||||
|
|
@ -1626,6 +1672,7 @@
|
||||||
"could",
|
"could",
|
||||||
"country",
|
"country",
|
||||||
"creation",
|
"creation",
|
||||||
|
"csp",
|
||||||
"csu",
|
"csu",
|
||||||
"cts",
|
"cts",
|
||||||
"currency",
|
"currency",
|
||||||
|
|
@ -1637,6 +1684,7 @@
|
||||||
"d+au",
|
"d+au",
|
||||||
"d+aut",
|
"d+aut",
|
||||||
"d,d",
|
"d,d",
|
||||||
|
"d,d%+",
|
||||||
"d,dd",
|
"d,dd",
|
||||||
"d,dd%+",
|
"d,dd%+",
|
||||||
"d,ddd",
|
"d,ddd",
|
||||||
|
|
@ -1662,7 +1710,6 @@
|
||||||
"darge",
|
"darge",
|
||||||
"darlehen",
|
"darlehen",
|
||||||
"das",
|
"das",
|
||||||
"dasda34",
|
|
||||||
"dat",
|
"dat",
|
||||||
"dd",
|
"dd",
|
||||||
"dd,d",
|
"dd,d",
|
||||||
|
|
@ -1688,6 +1735,7 @@
|
||||||
"der",
|
"der",
|
||||||
"dergleichen",
|
"dergleichen",
|
||||||
"des",
|
"des",
|
||||||
|
"destinations",
|
||||||
"deutsche",
|
"deutsche",
|
||||||
"deutsches",
|
"deutsches",
|
||||||
"deutschland",
|
"deutschland",
|
||||||
|
|
@ -1708,6 +1756,7 @@
|
||||||
"dipl.",
|
"dipl.",
|
||||||
"dipl.-ing",
|
"dipl.-ing",
|
||||||
"dipl.-ing.",
|
"dipl.-ing.",
|
||||||
|
"dis",
|
||||||
"discretionary",
|
"discretionary",
|
||||||
"distributions",
|
"distributions",
|
||||||
"diversification",
|
"diversification",
|
||||||
|
|
@ -1718,6 +1767,8 @@
|
||||||
"dle",
|
"dle",
|
||||||
"do",
|
"do",
|
||||||
"do.",
|
"do.",
|
||||||
|
"dom",
|
||||||
|
"domicile",
|
||||||
"domiciled",
|
"domiciled",
|
||||||
"don",
|
"don",
|
||||||
"down",
|
"down",
|
||||||
|
|
@ -1732,6 +1783,7 @@
|
||||||
"durchschnittlich",
|
"durchschnittlich",
|
||||||
"du\u2019s",
|
"du\u2019s",
|
||||||
"dv.",
|
"dv.",
|
||||||
|
"dxxx.\u20ac",
|
||||||
"dy",
|
"dy",
|
||||||
"d\u00e4nemark",
|
"d\u00e4nemark",
|
||||||
"d\u2019",
|
"d\u2019",
|
||||||
|
|
@ -1795,6 +1847,7 @@
|
||||||
"eln",
|
"eln",
|
||||||
"els",
|
"els",
|
||||||
"elt",
|
"elt",
|
||||||
|
"ely",
|
||||||
"em",
|
"em",
|
||||||
"em.",
|
"em.",
|
||||||
"emerging",
|
"emerging",
|
||||||
|
|
@ -1824,6 +1877,7 @@
|
||||||
"er.",
|
"er.",
|
||||||
"erb",
|
"erb",
|
||||||
"erbbaurechte",
|
"erbbaurechte",
|
||||||
|
"erd",
|
||||||
"ere",
|
"ere",
|
||||||
"erfolgten",
|
"erfolgten",
|
||||||
"erg",
|
"erg",
|
||||||
|
|
@ -1848,6 +1902,7 @@
|
||||||
"ete",
|
"ete",
|
||||||
"etr",
|
"etr",
|
||||||
"ets",
|
"ets",
|
||||||
|
"eturn",
|
||||||
"eu-offenlegungsverordnung",
|
"eu-offenlegungsverordnung",
|
||||||
"eur",
|
"eur",
|
||||||
"euro",
|
"euro",
|
||||||
|
|
@ -1885,6 +1940,7 @@
|
||||||
"fam",
|
"fam",
|
||||||
"fam.",
|
"fam.",
|
||||||
"favour",
|
"favour",
|
||||||
|
"fdr",
|
||||||
"feb",
|
"feb",
|
||||||
"feb.",
|
"feb.",
|
||||||
"fee",
|
"fee",
|
||||||
|
|
@ -1896,6 +1952,7 @@
|
||||||
"ff",
|
"ff",
|
||||||
"fierce",
|
"fierce",
|
||||||
"fil",
|
"fil",
|
||||||
|
"financially",
|
||||||
"finanzierung",
|
"finanzierung",
|
||||||
"finanzierungskonditionen",
|
"finanzierungskonditionen",
|
||||||
"finland",
|
"finland",
|
||||||
|
|
@ -1903,6 +1960,7 @@
|
||||||
"first",
|
"first",
|
||||||
"flagship",
|
"flagship",
|
||||||
"flow",
|
"flow",
|
||||||
|
"flow-oriented",
|
||||||
"fl\u00e4che",
|
"fl\u00e4che",
|
||||||
"focus",
|
"focus",
|
||||||
"focused",
|
"focused",
|
||||||
|
|
@ -1993,6 +2051,7 @@
|
||||||
"gic",
|
"gic",
|
||||||
"gie",
|
"gie",
|
||||||
"gl.",
|
"gl.",
|
||||||
|
"global",
|
||||||
"globale",
|
"globale",
|
||||||
"gmbh",
|
"gmbh",
|
||||||
"goal",
|
"goal",
|
||||||
|
|
@ -2172,6 +2231,7 @@
|
||||||
"investoren",
|
"investoren",
|
||||||
"investors",
|
"investors",
|
||||||
"investtionszeltraum",
|
"investtionszeltraum",
|
||||||
|
"investtionszeltraum,10",
|
||||||
"investtonszeltraum",
|
"investtonszeltraum",
|
||||||
"inw",
|
"inw",
|
||||||
"io.",
|
"io.",
|
||||||
|
|
@ -2314,6 +2374,8 @@
|
||||||
"lls",
|
"lls",
|
||||||
"llt",
|
"llt",
|
||||||
"llv",
|
"llv",
|
||||||
|
"lly",
|
||||||
|
"loan-to-value",
|
||||||
"local",
|
"local",
|
||||||
"locations",
|
"locations",
|
||||||
"lock-in",
|
"lock-in",
|
||||||
|
|
@ -2321,6 +2383,7 @@
|
||||||
"logistik",
|
"logistik",
|
||||||
"logistikimmobilien",
|
"logistikimmobilien",
|
||||||
"london",
|
"london",
|
||||||
|
"long-term",
|
||||||
"low",
|
"low",
|
||||||
"lps",
|
"lps",
|
||||||
"lso",
|
"lso",
|
||||||
|
|
@ -2330,6 +2393,7 @@
|
||||||
"lto",
|
"lto",
|
||||||
"ltv",
|
"ltv",
|
||||||
"ltv-ziel",
|
"ltv-ziel",
|
||||||
|
"lty",
|
||||||
"lu",
|
"lu",
|
||||||
"lub",
|
"lub",
|
||||||
"lue",
|
"lue",
|
||||||
|
|
@ -2363,6 +2427,7 @@
|
||||||
"management",
|
"management",
|
||||||
"manager",
|
"manager",
|
||||||
"manager-defined",
|
"manager-defined",
|
||||||
|
"managmentgeb\u00fchren",
|
||||||
"mandate",
|
"mandate",
|
||||||
"mandates",
|
"mandates",
|
||||||
"market",
|
"market",
|
||||||
|
|
@ -2374,6 +2439,7 @@
|
||||||
"maximal",
|
"maximal",
|
||||||
"maximaler",
|
"maximaler",
|
||||||
"mbH",
|
"mbH",
|
||||||
|
"mbh",
|
||||||
"means",
|
"means",
|
||||||
"medizin",
|
"medizin",
|
||||||
"medizinnahe",
|
"medizinnahe",
|
||||||
|
|
@ -2593,7 +2659,11 @@
|
||||||
"parformanceabh\u00e4ngige",
|
"parformanceabh\u00e4ngige",
|
||||||
"paris",
|
"paris",
|
||||||
"parks",
|
"parks",
|
||||||
|
"partners",
|
||||||
"partnership",
|
"partnership",
|
||||||
|
"pattern",
|
||||||
|
"pci",
|
||||||
|
"pco",
|
||||||
"ped",
|
"ped",
|
||||||
"pen",
|
"pen",
|
||||||
"per",
|
"per",
|
||||||
|
|
@ -2624,15 +2694,18 @@
|
||||||
"pricey",
|
"pricey",
|
||||||
"pricing",
|
"pricing",
|
||||||
"prime",
|
"prime",
|
||||||
|
"pro",
|
||||||
"prof",
|
"prof",
|
||||||
"prof.",
|
"prof.",
|
||||||
"profile",
|
"profile",
|
||||||
|
"prognose",
|
||||||
"prognostiderte",
|
"prognostiderte",
|
||||||
"prognostizierte",
|
"prognostizierte",
|
||||||
"program",
|
"program",
|
||||||
"projects",
|
"projects",
|
||||||
"projektentwicklungen",
|
"projektentwicklungen",
|
||||||
"projektentwicklungsrisiken",
|
"projektentwicklungsrisiken",
|
||||||
|
"propco",
|
||||||
"properties",
|
"properties",
|
||||||
"proprietary",
|
"proprietary",
|
||||||
"provide",
|
"provide",
|
||||||
|
|
@ -2646,6 +2719,7 @@
|
||||||
"q.",
|
"q.",
|
||||||
"q.e.d",
|
"q.e.d",
|
||||||
"q.e.d.",
|
"q.e.d.",
|
||||||
|
"qin",
|
||||||
"quality",
|
"quality",
|
||||||
"quarterly",
|
"quarterly",
|
||||||
"quota",
|
"quota",
|
||||||
|
|
@ -2682,6 +2756,7 @@
|
||||||
"relationships",
|
"relationships",
|
||||||
"remains",
|
"remains",
|
||||||
"ren",
|
"ren",
|
||||||
|
"rendite",
|
||||||
"rendite-",
|
"rendite-",
|
||||||
"rendite-risiko-profil",
|
"rendite-risiko-profil",
|
||||||
"renegotiation",
|
"renegotiation",
|
||||||
|
|
@ -2698,6 +2773,7 @@
|
||||||
"retailinvestitionsvolumen",
|
"retailinvestitionsvolumen",
|
||||||
"return",
|
"return",
|
||||||
"returns",
|
"returns",
|
||||||
|
"rev",
|
||||||
"reversion",
|
"reversion",
|
||||||
"rewe",
|
"rewe",
|
||||||
"rge",
|
"rge",
|
||||||
|
|
@ -2724,10 +2800,12 @@
|
||||||
"rop",
|
"rop",
|
||||||
"rotterdam",
|
"rotterdam",
|
||||||
"rr.",
|
"rr.",
|
||||||
|
"rre",
|
||||||
"rs.",
|
"rs.",
|
||||||
"rsg",
|
"rsg",
|
||||||
"rst",
|
"rst",
|
||||||
"rte",
|
"rte",
|
||||||
|
"rtt",
|
||||||
"rz.",
|
"rz.",
|
||||||
"r\u00f6m",
|
"r\u00f6m",
|
||||||
"r\u00f6m.",
|
"r\u00f6m.",
|
||||||
|
|
@ -2748,6 +2826,7 @@
|
||||||
"schweden",
|
"schweden",
|
||||||
"scope",
|
"scope",
|
||||||
"scs",
|
"scs",
|
||||||
|
"scsp",
|
||||||
"sd.",
|
"sd.",
|
||||||
"sector",
|
"sector",
|
||||||
"sectors",
|
"sectors",
|
||||||
|
|
@ -2756,6 +2835,7 @@
|
||||||
"segment",
|
"segment",
|
||||||
"sektor",
|
"sektor",
|
||||||
"sektoraler",
|
"sektoraler",
|
||||||
|
"sektorenallokation",
|
||||||
"selection",
|
"selection",
|
||||||
"sen",
|
"sen",
|
||||||
"sen.",
|
"sen.",
|
||||||
|
|
@ -2769,6 +2849,7 @@
|
||||||
"set",
|
"set",
|
||||||
"sf.",
|
"sf.",
|
||||||
"sfdr",
|
"sfdr",
|
||||||
|
"sg-",
|
||||||
"sg.",
|
"sg.",
|
||||||
"short-term",
|
"short-term",
|
||||||
"sicav-raif",
|
"sicav-raif",
|
||||||
|
|
@ -2789,6 +2870,7 @@
|
||||||
"sofern",
|
"sofern",
|
||||||
"sog",
|
"sog",
|
||||||
"sog.",
|
"sog.",
|
||||||
|
"solely",
|
||||||
"solvency",
|
"solvency",
|
||||||
"some",
|
"some",
|
||||||
"son",
|
"son",
|
||||||
|
|
@ -3046,6 +3128,9 @@
|
||||||
"worldwide",
|
"worldwide",
|
||||||
"x",
|
"x",
|
||||||
"x'",
|
"x'",
|
||||||
|
"x+xx",
|
||||||
|
"x+xxx",
|
||||||
|
"x-xxxx",
|
||||||
"x.",
|
"x.",
|
||||||
"x.X",
|
"x.X",
|
||||||
"x.X.",
|
"x.X.",
|
||||||
|
|
@ -3072,23 +3157,38 @@
|
||||||
"xemoours",
|
"xemoours",
|
||||||
"xit",
|
"xit",
|
||||||
"xx",
|
"xx",
|
||||||
|
"xx-xxxx",
|
||||||
"xx.",
|
"xx.",
|
||||||
"xx.x",
|
"xx.x",
|
||||||
"xxXxx",
|
"xxXxx",
|
||||||
"xxx",
|
"xxx",
|
||||||
|
"xxx-",
|
||||||
"xxx-Xxxxx",
|
"xxx-Xxxxx",
|
||||||
"xxx-xxxx",
|
"xxx-xxxx",
|
||||||
"xxx.",
|
"xxx.",
|
||||||
|
"xxxd.d",
|
||||||
"xxxx",
|
"xxxx",
|
||||||
|
"xxxx)-",
|
||||||
|
"xxxx)/xxxx",
|
||||||
"xxxx+",
|
"xxxx+",
|
||||||
|
"xxxx+/x",
|
||||||
|
"xxxx+/xxxx",
|
||||||
|
"xxxx,dd",
|
||||||
|
"xxxx-",
|
||||||
"xxxx-xx",
|
"xxxx-xx",
|
||||||
|
"xxxx-xx-xxxx",
|
||||||
"xxxx-xxx",
|
"xxxx-xxx",
|
||||||
"xxxx-xxxx",
|
"xxxx-xxxx",
|
||||||
|
"xxxx-xxxx-xxx",
|
||||||
|
"xxxx-xxxx-xxxx",
|
||||||
"xxxx.",
|
"xxxx.",
|
||||||
"xxxxdd",
|
"xxxx\u0308xx",
|
||||||
|
"xxxx\u0308xxx-xxxx",
|
||||||
|
"xxxx\u0308xxxx",
|
||||||
"xxxx\u2019x",
|
"xxxx\u2019x",
|
||||||
"xxx\u2019x",
|
"xxx\u2019x",
|
||||||
"xx\u0308x",
|
"xx\u0308x",
|
||||||
|
"xx\u0308xxxx",
|
||||||
"xx\u2019x",
|
"xx\u2019x",
|
||||||
"x\u0308xxx",
|
"x\u0308xxx",
|
||||||
"x\u0308xxxx",
|
"x\u0308xxxx",
|
||||||
|
|
@ -3124,6 +3224,7 @@
|
||||||
"zielallokation",
|
"zielallokation",
|
||||||
"zielanlagestrategie",
|
"zielanlagestrategie",
|
||||||
"zielausschu\u0308ttung",
|
"zielausschu\u0308ttung",
|
||||||
|
"zielaussch\u00fcttung",
|
||||||
"zielmarkts",
|
"zielmarkts",
|
||||||
"zielm\u00e4rkte",
|
"zielm\u00e4rkte",
|
||||||
"zielobjekte",
|
"zielobjekte",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -0,0 +1,563 @@
|
||||||
|
TRAINING_DATA = [
|
||||||
|
(
|
||||||
|
"Core",
|
||||||
|
{"entities": [[0, 4, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core+",
|
||||||
|
{"entities": [[0, 5, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core/Core+",
|
||||||
|
{"entities": [[0, 10, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Value Add",
|
||||||
|
{"entities": [[0, 9, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core/Value Add",
|
||||||
|
{"entities": [[0, 14, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core+/Value Add",
|
||||||
|
{"entities": [[0, 15, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core/Core+/Value Add",
|
||||||
|
{"entities": [[0, 20, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"The RE portfolio of the fund is a good illustration of Fond expertise in European core/core+ investments .",
|
||||||
|
{"entities": [[82, 92, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Risk level: Core/Core+",
|
||||||
|
{"entities": [[12, 22, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Different risk profile (core, core+, value-added)",
|
||||||
|
{"entities": [[24, 48, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core/Core+ with OpCo premium",
|
||||||
|
{"entities": [[0, 10, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"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"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Risikoprofil: Core, Core +",
|
||||||
|
{"entities": [[14, 26, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Name des Fonds Name des Investmentmanagers Allgemeine Informationen Name des Ansprechpartners Telefonnummer des Ansprechpartners E-Mail des Ansprechpartners Art des Anlagevehikels Struktur des Anlagevehikels Sitz des Anlagevehikels Struktur des Antagevehikels vom Manager festgelegter Stil Rechtsform Jahr des ersten Closings Laufzeit Geplantes Jahr der Auflösung Ziel-Netto-IRR / Gesamtrendite* Zielvolumen des Anlagevehikels Ziel-LTY ‚Aktueller LTV Ziirraiaein Maximaler LTV Zielregionfen)/Jand Zielsektoren Zielanlagestrategie INREV Fonds Offen Deutschland Core, Core + Offener Immobilien-Spezialfonds 2022 10 - 12 Jahre 2032 - 2034 7,50%+ 250 Mio. € 20% 0% 20% Führende Metropolregionen Deutschlands und ausgewählte Standorte >50T Einw. Wohnimmobilien Wertstabile Wohnimmobilien (mit Bestandsentwicklungen)",
|
||||||
|
{"entities": [[560, 572, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core/Core+ strategy, with tactical exposure to development projects aiming at enhancing the quality of the portfolio over time",
|
||||||
|
{"entities": [[0, 10, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Strategie - Übersicht Risikoprofil Core+ Halten-Strategie Kaufen — Halten (langfristig) — Exit 1. Nachvermietungsstrategie Anlagestrategien 2. Standortaufwertungsstrategie 3. Strategie der Aufwertung der Immobilien Niederlande (max. 35 %) Länderallokation Frankreich (max. 35 %) (in % vom Zielvolumen) Skandinavien (Schweden, Dänemark) (max. 35 %) Deutschland (<= 10 %)",
|
||||||
|
{"entities": [[35, 40, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core and Core+",
|
||||||
|
{"entities": [[0, 14, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"core, core+, value-added",
|
||||||
|
{"entities": [[0, 24, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Manage to Core: max 20%",
|
||||||
|
{"entities": [[10, 14, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Benefits of the core/ core+ segment",
|
||||||
|
{"entities": [[16, 27, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Drawbacks of the core/ core+ segment",
|
||||||
|
{"entities": [[17, 28, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Why a Core / Core + investment program?",
|
||||||
|
{"entities": [[6, 19, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Different risk profile (core, core+, value-added)",
|
||||||
|
{"entities": [[24, 48, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"INK MGallery Hotel Area: Amsterdam Core Tenant: Closed in 2018",
|
||||||
|
{"entities": [[35, 39, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"A strategy targeting high quality Core and Core+ buildings, with defined SRI objectives, in order to extract value through an active asset management.",
|
||||||
|
{"entities": [[34, 48, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Navigate the diversity of the Core/Core+ investment opportunities in European Prime Cities",
|
||||||
|
{"entities": [[30, 40, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"GEDis an open-ended Lux-based fund providing an attractive core/core+ real estate exposure, leveraging GRRE expertise in European RE markets. It offers diversification in terms of pan-European geographies and sectors: Offices, Retail and Hotels.",
|
||||||
|
{"entities": [[59, 69, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core assets leave less room for active asset management value creation",
|
||||||
|
{"entities": [[0, 4, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"capital preservation is defined here as a characteristic of core/core+ investments. There is no guarantee of capital.",
|
||||||
|
{"entities": [[60, 70, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Country / city BELGIUM Brussels BELGIUM Brussels SPAIN Madrid FRANCE Levallois FRANCE Paris 14 BELGIUM Brussels NETHERLANDS Rotterdam NETHERLANDS Rotterdam Sector Offices Offices Offices Offices Offices Offices Offices Logistics Risk Core",
|
||||||
|
{"entities": [[234, 238, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"GERD(a balanced pan-European open ended retail fund — under the form of a French collective undertaking for Real Estate investments “OPCI”) is the flagship ofQin France and combines RE and listed assets (respective targets of 60% and 40%) with max. 40% leverage. The RE portfolio of the fund is a good illustration Of expertise in European core/core+ investments.",
|
||||||
|
{"entities": [[340, 350, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Prime office assets in Prime markets are very pricey unless rent reversion is real. Risk premium remains attractive on a leveraged basis. Manage to core or build to core can make sense as a LT investor in main cities. Residential is also attractive",
|
||||||
|
{"entities": [[148, 152, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Paris region is a deep and liquid market. Rents have some potential to improve. Considering current low yield and fierce competition, office right outside CBD for Core + assets can be considered. Manage to core strategies could make sense.",
|
||||||
|
{"entities": [[163, 169, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Lisbon is a small market but it experienced a rapid economic recovery in recent years and is interesting for Core Offices, quality Retail assetor Hotel walls with top operators. Limited liquidity of this market means investment must be small",
|
||||||
|
{"entities": [[109, 113, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"4,0 %",
|
||||||
|
{"entities": [[0, 5, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Prognostizierte jährliche Ausschüttung von 4,0%",
|
||||||
|
{"entities": [[44, 48, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"20% über einer @ Ausschüttungsrendite von 4,0%",
|
||||||
|
{"entities": [[44, 48, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Prognostizierte Ausschüttungsrandite* Mindestanlage Mitgliedschaft Im Anlagesusschuss Ankaufs- / Verkaufs- / Verkaufs(Teflimmobilfe)- / Baumanagementgebahr (inkl. USt.) Parformanceabhängige Vergütung Einmalige Strukturierungsgebühr Laufzeit / Investtionszeltraum Ausschüttungsintervalle Deutsche Metropolregianen und umliegende Regionen mit Städten >50T Einwohner Artikel 8 Wohnimmobilien Deutschland ‚Aktive Bestandsentwicklung Offener Spezial-AlF mit festen Anlagebedingungen rd. 200 Mio. € / max. 20% rd. 250 Mio. € 7,5 % (nach Kosten & Gebühren, vor Steuern) 8 4,0 % {nach Kosten & Gebühren, var Steuern}",
|
||||||
|
{"entities": [[570, 575, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"5,00-5,25 % Ausschüttungsrendite",
|
||||||
|
{"entities": [[0, 11, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Zielrendite 5,00-5,25 % Ausschüttungsrendite",
|
||||||
|
{"entities": [[12, 23, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Auschüttungsrendite 4,9% 5,3%",
|
||||||
|
{"entities": [[21, 25, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Auschüttungsrendite 4,9% 5,3%",
|
||||||
|
{"entities": [[26, 30, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Auschittungsrendite 3,8% 5,7%",
|
||||||
|
{"entities": [[20, 24, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Auschittungsrendite 3,8% 5,7%",
|
||||||
|
{"entities": [[25, 29, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Auschüttungsrendite 4,5% 4,6%",
|
||||||
|
{"entities": [[21, 25, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Auschüttungsrendite 4,5% 4,6%",
|
||||||
|
{"entities": [[26, 30, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Auschüttungsrendite 5,0% 4,7%",
|
||||||
|
{"entities": [[26, 30, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Auschüttungsrendite 5,0% 4,7%",
|
||||||
|
{"entities": [[21, 25, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Auschüttungsrendite “eons a Nuremberg aha 5,0 % 4,8 %",
|
||||||
|
{"entities": [[43, 48, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Auschüttungsrendite “eons a Nuremberg aha 5,0 % 4,8 %",
|
||||||
|
{"entities": [[49, 54, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"3-4% dividend yield",
|
||||||
|
{"entities": [[0, 4, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Zielmärkte Klassifizierung SFDR Invastitionsfokus Rendite- / Risikoprofil Rechtsform Eigenkapital /FK Quote Investftionsvolumen Prognostizierte Gesamtrendite {IRR)* Prognostizierte Ausschüttungsrandite* Mindestanlage Mitgliedschaft Im Anlagesusschuss Ankaufs- / Verkaufs- / Verkaufs(Teflimmobilfe)- / Baumanagementgebahr (inkl. USt.) Parformanceabhängige Vergütung Einmalige Strukturierungsgebühr Deutsche Metropolregianen und umliegende Regionen mit Städten >50T Einwohner Artikel 8 Wohnimmobilien Deutschland ‚Aktive Bestandsentwicklung Offener Spezial-AlF mit festen Anlagebedingungen rd. 200 Mio. € / max. 20% rd. 250 Mio. € 7,5 % (nach Kosten & Gebühren, vor Steuern) 8 4,0 % {nach Kosten & Gebühren, var Steuern} 5Mio.€ Ab 10 Mio. € 1,40 % / 0,80 % /2,12% / 4,91 % Laufzeit / Investtionszeltraum Ausschüttungsintervalle 20 % über einer @ Ausschüttungsrendite von 4,0 % 0,1% der bis zum 31.12.2023 erfolgten Kapitalzusagen (max. 200.000 &) 10 bis 12 Jahre / bis zu 24 Monate angestrebt Mindestens jährlich",
|
||||||
|
{"entities": [[945, 960, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Laufzeit / Investtionszeltraum,10 bis 12 Jahre / bis zu 24 Monate angestrebt Ausschüttungsintervalle,Mindestens jährlich",
|
||||||
|
{"entities": [[31, 46, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"10-12 Jahre Laufzeit bei einem LTV von bis zu 20%",
|
||||||
|
{"entities": [[0, 11, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"vom Manager festgelegter Stil Rechtsform Jahr des ersten Closings Laufzeit Geplantes Jahr der Auflösung Ziel-Netto-IRR / Gesamtrendite* Zielvolumen des Anlagevehikels Ziel-LTY‚Aktueller LTV Zielsektoren Zielanlagestrategie Fonds Offen Deutschland Core, Core + Offener Immobilien-Spezialfonds 2022 10 - 12 Jahre",
|
||||||
|
{"entities": [[297, 310, "LAUFZEIT"], [247, 259, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Allgemeine Annahmen Ankaufsphase Haltedauer Zielobjektgröße Finanzierung Investitions-annahmen Zielrendite 24 Monate Investmentzeitraum 10 Jahre (+) EUR 20-75 Mio. Keine externe Finanzierung zum Auftakt (ausschließlich Darlehen der Anteilseigner). Die Finanzierung wird nach der Ankaufsphase und Stabilisierung der Zinssätze neu geprüft. Angestrebter LTV zwischen 25-40 % Investitionen für Renovierungen und ESG- Verbesserungen werden für jedes Objekt einzeln festgelegt. 5,00-5,25 % Ausschüttungsrendites",
|
||||||
|
{"entities": [[136, 148, "LAUFZEIT"], [472, 483, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Zielrendite 5,00-5,25 % Ausschüttungsrendite 1) Ankauf von Objekten an Tag eins mit 100% Eigenkapital. Die Strategie unterstellt die Aufnahme von Fremdkapital, sobald sich die Zins- und Finanzierungskonditionen nachhaltig stabilisieren. Strategie - Übersicht Risikoprofil Core+",
|
||||||
|
{"entities": [[12, 23, "AUSSCHÜTTUNGSRENDITE"], [272, 277, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Vehicle lifetime / investment period Open-ended fund",
|
||||||
|
{"entities": [[37, 52, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Vehicle / domicile Alternative Investment Fund / Luxembourg (e.g. SCSp SICAV-RAIF) Investment strategy eturn pro Real Estate (PropCo + OpCo) Investing in upscale hotels with long-term management contracts in major European destinations Core/Core+ with OpCo premium Management Agreements solely with financially strong and experienced partners/ global brands Cash flow-oriented Cash-flow pattern Target equity /AuM € 400m equity / € 800m AuM (50% Loan-to-Value) Vehicle lifetime / investment period Open-ended fund",
|
||||||
|
{"entities": [[498, 513, "LAUFZEIT"], [236, 245, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Vehicle type (Lux-RAIF) (net of fees) IRR6.5% ACCOR Vehicle structure Open-ended Targetvehiclesize € 400m (equity) Manager-defined Core/Core+ with | style OpCo Premium darge CLV. 50% Pt H | LTO N WORLDWIDE Year of first closing 2020 Target no. ofinvestors 1-5 Fund life (yrs} Open-ended Min-commitmentper —¢ 400m",
|
||||||
|
{"entities": [[131, 141, "RISIKOPROFIL"], [70, 80, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Fund term: Open-ended",
|
||||||
|
{"entities": [[11, 21, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Abdeckung der Risiko-Rendite-Bandbreite (Core, Core+, Value-Add)",
|
||||||
|
{"entities": [[41, 63, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"5,1% - 8,5% IRR!",
|
||||||
|
{"entities": [[0, 11, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Retailinvestitionsvolumen nach Ländern (2024) Vereinigtes Königreich, 26,4% Deutschland, 19,0% Andere, 19,7% Italien, 8,2% Irland, 3,3% N | Frankreich, Spanien, 8,1%",
|
||||||
|
{"entities": [[46, 75, "LÄNDERALLOKATION"], [76, 94, "LÄNDERALLOKATION"], [95, 108, "LÄNDERALLOKATION"], [109, 122, "LÄNDERALLOKATION"], [123, 135, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Erwartete IRR 5 (je nach Objekt- ‘A(E) 6.00% - 8,00%",
|
||||||
|
{"entities": [[39, 52, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Zielmarkts Deutsche Metropolregianen und umliegende Regionen mit Städten >50T Einwohner Klassifizierung SFDR Artikel 8 Invastitionsfokus Wohnimmobilien Deutschland Rendite- / Risikoprofil ‚Aktive Bestandsentwicklung Rechtsform Offener Spezial-AlF mit festen Anlagebedingungen Eigenkapital /FK Quote rd. 200 Mio. € / max. 20% Investftionsvolumen rd. 250 Mio. € Prognostiderte Gesamtrendite {IRR)* 7,5 % (nach Kosten & Gebühren, vor Steuern) Prognostizierte Ausschüttungsrandite* @ 4,0 % {nach Kosten & Gebühren, var Steuern} Mindestanlage 5Mio.€ Mitgliedschaft Im Anlagesusschuss Ab 10 Mio. € Ankaufs- / Verkaufs- / Verkaufs(Teflimmobilfe)- / Baumanagementgebahr (inkl. USt) 1,40 %/080%/212%/491% Parformanceabhängige Vergütung 20 % über einer ® Ausschüttungsrendite von 4,0% Einmalige Strukturierungsgebühr 0,1% der bis zum 31.12.2023 erfolgten Kapitalzusagen (max. 200.000 €) Laufzelt / Investtonszeltraum 10 bis 12 Jahre / bis zu 24 Monate angestrebt Ausschüttungsintervalle Mindestens jährlich",
|
||||||
|
{"entities": [[396, 401, "RENDITE"], [482, 487, "AUSSCHÜTTUNGSRENDITE"], [914, 929, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"= Prognostizierte jährliche Ausschüttung von @ 4,0%* = Prognostizierte Gesamtrendite (IRR) von 7,5%*",
|
||||||
|
{"entities": [[48, 52, "AUSSCHÜTTUNGSRENDITE"], [96, 100, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Prognose: 7,5%+ IRR auf Fondsebene",
|
||||||
|
{"entities": [[10, 14, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"= Prognostizierte jährliche Ausschüttung* von 84,0% = Prognostizierte Gesamtrendite (IRR}* von 7,5%",
|
||||||
|
{"entities": [[96, 100, "RENDITE"], [49, 53, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"= Lagefokussierung: Metropolregionen Deutschlands = Finanzierung: max. 20% LTV = Risikoprofil: Core, Core +",
|
||||||
|
{"entities": [[95, 107, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Performance-Fee: 20% über einer @ Ausschüttungsrendite von 4,0%",
|
||||||
|
{"entities": [[61, 65, "AUSSCHÜTTUNGSRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Fondstyp Offener Spezial-AIF nach KAGB mit festen Anlagebedingungen ESG-Klassifizierung Fonds gemäß Artikel 8 EU-Offenlegungsverordnung KVG IntReal GmbH, Hamburg Anlagestrategie Aufbau eines Objektportfolios aus Ärztehäusern, die langfristig vermietet sind Ärztehäuser, Laborimmobilien, im Verbund mit Ärztehäusern auch ambulant Zielobjekte betreute Wohngemeinschaften; Mietanteil Medizin und medizinnahe Dienstleistungen/Handel > 65 % (Objektebene) WALT >5 Jahre bei Ankauf Objektbaujahre Ab 2000 Anlagegrenzen Einzelinvestment 8-30 Mio. EUR Anzahl Objekte 10-20 Deutschland bundesweit; jeweiliges Einzugsgebiet > 25.000 Einwohner mit Regionen stabiler Bevölkerungsprognose Risikoprofil Core / Core +",
|
||||||
|
{"entities": [[689, 702, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Fondsvolumen 300 Mio. EUR Zielrendite (IRR) > 6,0 % p. a. Ausschuttung >5,0 % p. a. Ankaufszeitraum 2024-2026 Laufzeit 31.12.2036 Mindestanlage 10 Mio. EUR Anlageausschuss Ja, entscheidet u. a. über Objekterwerb (Mitglied kann ab 20 Mio. EUR gestellt werden) Gebührenstruktur Marktüblich (auf Anfrage) Projektentwicklungen keine Forward-Deals Möglich, maximal 18 Monate Vorlauf; keine Projektentwicklungsrisiken beim Fonds Erbbaurechte Möglich, sofern Laufzeit > 60 Jahre und angemessene Entschädigung bei Ablauf und Heimfall Status Objektpipeline vorhanden: siehe Folie 16 ff.",
|
||||||
|
{"entities": [[44, 57, "RENDITE"], [71, 83, "AUSSCHÜTTUNGSRENDITE"], [120, 130, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Niederlande (max. 35 %) Länderallokation Frankreich (max. 35 %) (in % vom Zielvolumen) Skandinavien (Schweden, Dänemark) (max. 35 %) Deutschland (<= 10 %)",
|
||||||
|
{"entities": [[0, 23, "LÄNDERALLOKATION"], [41, 63, "LÄNDERALLOKATION"], [87, 132, "LÄNDERALLOKATION"], [133, 154, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Führender Immobilien-Investmentmanager in den Nordics für globale ll institutionelle Investoren in Value Add und Core Strategien",
|
||||||
|
{"entities": [[101, 119, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core und Core+ Fonds",
|
||||||
|
{"entities": [[0, 14, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Risikoprofil Core / Core+",
|
||||||
|
{"entities": [[13, 25, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Durchschnittlich geplante jährliche Ausschüttung von 4,5-5,5% auf das investierte Eigenkapital an die Anleger Geplante Gesamtrendite von 5-6% (IRR) auf das eingezahlte Eigenkapital",
|
||||||
|
{"entities": [[54, 62, "AUSSCHÜTTUNGSRENDITE"], [138, 142, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Geografische Zielallokation nach Investitionsphase des Fonds: 1) Schweden 20-60% Allokation Länder 2) Finnland 20-60% 3) Norwegen 10-40% 4) Dänemark 10-40%",
|
||||||
|
{"entities": [[65, 80, "LÄNDERALLOKATION"], [102, 117, "LÄNDERALLOKATION"], [121, 136, "LÄNDERALLOKATION"], [140, 155, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Deutsches Spezial-Sondervermögen mit festen Anlagebedingungen ($284 KAGB) Immobilien- oder Infrastrukturquote (nach Solvency II) Core / Core+ Euro Hauptstadtregionen und andere Großstädte in den Nordics €500 Mio. 4,5-5,5% 15 Jahre; Fonds hat unbegrenzte Laufzeit; Investmentphase 4 Jahre Maximaler Fremdkapitalanteil 50% (LTV-Ziel bei Ankauf), Langfristiges LTV-Ziel auf Fondsebene ist 45% 0,625% p. a. des Bruttofondsvermögens Zeichnungen ab € 30 Mio. - 0,03 % Rabatt Zeichnungen ab € 50 Mio. - zusatzl. 0,03 % Rabatt 1,1% des Verkehrswertes 0,6% der Bruttoverkaufswert 10% wenn Hurdle Rate 5,0 % p. a. (IRR netto) überschritten wird (nach 15 Jahren berechnet) Ja",
|
||||||
|
{"entities": [[129, 141, "RISIKOPROFIL"], [213, 221, "ZIELRENDITE"], [242, 262, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Standort Helsinki, Finnland Sektor Bildungswesen, Schule& Kindertagesstätte Vermietbare Fläche 3.321 m? Leerstand bei Ankauf 0% / 0% Ankaufspreis+ Investitionen €21,4 Mio. + €0,2 Mio Eigenkapital €21,6 Mio. Ankaufs- / Stabilisierungs- / Exitrendite 5,0%/ 5,5%/ 5,0% NOI zum Ankaufszeitpunkt / Exit-NOI €1.1m/ €1.2m Zielrenditen (netto für LPs) 5,4% IRR/ 1.5x EM / DY 4,3% Ankauf / Exit Dezember 2023/ Dezember 2033",
|
||||||
|
{"entities": [[345, 349, "ZIELRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Evergreen/offene Fondsstrukturenv Core / Core+ Strategien",
|
||||||
|
{"entities": [[34, 46, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"BEE Henderson German 2012 Logistik Core/D/Art. 8 € 336 Mio. 12 (voll investiert) 13,0 % p.a.",
|
||||||
|
{"entities": [[35, 39, "RISIKOPROFIL"], [81, 87, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ICF German Logistics 2014 Logistik Core/D/Art. 8 € 400 Mio. 16 (voll investiert) 12,0 % p.a.",
|
||||||
|
{"entities": [[35, 39, "RISIKOPROFIL"], [81, 87, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Individualmandat 2015 Logistik Core / D+AU/ ArTt. 6 € 200 Mio. 8 (realisiert) 8,0 % p.a.",
|
||||||
|
{"entities": [[31, 35, "RISIKOPROFIL"], [78, 83, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"European Logistics Partnership” 2017 Logistik Value-Add / Europ/a - € 1.000 Mio. 28 (realisiert) 20,0 % p.a.",
|
||||||
|
{"entities": [[46, 55, "RISIKOPROFIL"], [97, 103, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"European Core Logistics Fund (ECLF 1) 2021 Logistik Core / Euro/p Arat. 8 € 314 Mio. 12 (voll investiert) 7,50 % p.a.",
|
||||||
|
{"entities": [[9, 13, "RISIKOPROFIL"], [106, 112, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"P-Logistik Europa Fonds (ECLF 2) 2022 Logistik Core / Euro/p Arat. 8 € 150 Mio.? A (voll investiert) 6,5 % p.a.?",
|
||||||
|
{"entities": [[47, 51, "RISIKOPROFIL"], [101, 106, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"First Business Parks 2015 Light Industrial Value Add / D+AUT € 100 Mio. 6 (realisiert) 16,0 % p.a.",
|
||||||
|
{"entities": [[43, 52, "RISIKOPROFIL"], [87, 93, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Unternehmensimmobilien Club 1 2016 Light Industrial Core+/D € 186 Mio. 9 (voll investiert) 13,0 % p.a.",
|
||||||
|
{"entities": [[91, 97, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Unternehmensimmobilien Club 1 2016 Light Industrial Core+/D € 186 Mio. 9 (voll investiert) 13,0 % p.a.",
|
||||||
|
{"entities": [[52, 57, "RISIKOPROFIL"], [91, 97, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Unternehmensimmobilien Club 2 2021 Light Industrial Core+/D € 262 Mio. 12 (voll investiert) 9,00 % p.a.",
|
||||||
|
{"entities": [[52, 57, "RISIKOPROFIL"], [92, 98, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Individualmandat 2022 Light Industrial Value-Add / Nordics € 100 Mio. 5 (voll investiert) 18,0 % p.a.",
|
||||||
|
{"entities": [[39, 48, "RISIKOPROFIL"], [90, 96, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"EUROPEAN CORE LOGISTICS FUND 3",
|
||||||
|
{"entities": [[9, 13, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core Investitionen",
|
||||||
|
{"entities": [[0, 4, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"8 % IRR",
|
||||||
|
{"entities": [[0, 3, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Rendite-Risiko-Profil Core ° Geographischer Fokus Kontinentaleuropaische Kernvolkswirtschaften nach Allokationsprofil * Sektoraler Fokus Logistikimmobilien nach Allokationsprofil Kapitalstruktur ° Eigenkapital € 250 Mio. ° Fremdkapital 50 % angestrebt, max. 60 % der Immobilienwerte (Objektebene) °e Mindestzeichnung € 10 Mio. Vehikelstruktur ° Rechtsform Immobilien-Spezial-AlF mit festen Anlagebedingungen nach 3 284 KAGB ° Klassifikation Artikel 8 Offenlegungsverordnung ¢ Anlagehorizont 10 Jahre mit Verlängerungsoption um 2 Jahre! ° Geplante Auflage 01 2025 Performanceziel? ° Ausschüttung 6,0 % p.a. (Durchschnitt 10 Jahre Haltedauer) ° Interner Zinsfuß (IRR) 8,0 % p.a. (10 Jahre Haltedauer, Target-IRR)",
|
||||||
|
{"entities": [[22, 26, "RISIKOPROFIL"], [596, 601, "AUSSCHÜTTUNGSRENDITE"], [667, 672, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core/Core+, mit Cash-Flow-Stabilität",
|
||||||
|
{"entities": [[0, 10, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Zielausschüttung: min. 5,10%",
|
||||||
|
{"entities": [[24, 29, "ZIELAUSSCHÜTTUNG"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Zielrendite (IRR): min. 5,50%",
|
||||||
|
{"entities": [[24, 29, "ZIELRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Rewe & Lidl Maxhütte-Haidhof é ae: 6 s Bahnhof Ankermieter REWE & Lidl er WALT 20 und 17 Jahre Miete p.a. 1.127.916 € Kaufpreis 21,43 Mio. € Faktor 19,00 x LTV / Zins 80% / 4,0% Ausschüttung 5,7 % IRR 7,1%",
|
||||||
|
{"entities": [[193, 198, "AUSSCHÜTTUNGSRENDITE"], [203, 207, "ZIELRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Real Estate Prime Europe Access the Core of European Prime Cities with a green SRI fund including a genuine low carbon commitment",
|
||||||
|
{"entities": [[36, 40, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"(FR, UK, DE, BE, NL, LU, Nordics, Allocation SP, IT, CH)",
|
||||||
|
{"entities": [[1, 32, "LÄNDERALLOKATION"], [45, 55, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"IRR: 6% - 7%",
|
||||||
|
{"entities": [[5, 12, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Europe | Germany 67 Value Add",
|
||||||
|
{"entities": [[9, 16, "LÄNDERALLOKATION"], [20, 29, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Germany, Norway 336 Core Plus",
|
||||||
|
{"entities": [[0, 7, "LÄNDERALLOKATION"], [20, 29, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"UK",
|
||||||
|
{"entities": [[0, 2, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"NORWAY",
|
||||||
|
{"entities": [[0, 6, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"9.8% IRR",
|
||||||
|
{"entities": [[0, 4, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Investment volume down 52% to €2.3 billion, with 4,000 100 14% value-add and core-plus increasing YoY",
|
||||||
|
{"entities": [[63, 86, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Geared Gross IRR‘ seeking a range of 16-18% per annum",
|
||||||
|
{"entities": [[37, 43, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Open-ended fund 24 months, incl. rolling reinvestment Sale of individual assets with respective management contracts or geared leases IRR: >6.5% | CoC: >5.0%",
|
||||||
|
{"entities": [[0, 10, "LAUFZEIT"], [139, 144, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Our investment strategy focuses on investing in upscale hotels in European prime locations, including DACH, Italy, Spain, Portugal, France, UK, Denmark, Benelux,and Poland.",
|
||||||
|
{"entities": [[102, 171, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core+ assets with value-add potential, Emerging Gateway Cities Helsinki] Core+ with Value well-mitigated risk and great upside Potential potential through asset improvement or = Max. 20% UK & Ireland {no contract renegotiation currency risk hedging], 80% tinental E > IRR target of 6-9%",
|
||||||
|
{"entities": [[0, 5, "RISIKOPROFIL"], [282, 286, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"10% net IRR since inception in 2018?",
|
||||||
|
{"entities": [[0, 3, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Eurozone: Benelux, France and Germany",
|
||||||
|
{"entities": [[10, 37, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Open-ended, with quarterly liquidity (redemption rights, dual pricing)",
|
||||||
|
{"entities": [[0, 10, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Class A & B (Institutional): 0.93% on NAV; Class D (Wholesale): 1.80% on NAV; Class P (Wholesale): 1.25% on NAV",
|
||||||
|
{"entities": [[29, 34, "MANAGMENTGEBÜHREN"], [64, 69, "MANAGMENTGEBÜHREN"], [99, 104, "MANAGMENTGEBÜHREN"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Risk profile: favour core > © at least and core+ assets with a targeted N 2 n allocation to value add assets to enhance returns",
|
||||||
|
{"entities": [[21, 25, "RISIKOPROFIL"], [43, 48, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"The Netherlands (38 assets) = Germany (9 assets) 10 largest Country assets split France (8 assets)",
|
||||||
|
{"entities": [[0, 15, "LÄNDERALLOKATION"], [30, 37, "LÄNDERALLOKATION"], [81, 87, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Expected IRR 10.9%",
|
||||||
|
{"entities": [[13, 18, "ZIELRENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Structure Open-end, perpetual life, Luxembourg domiciled Initial Target Size* €2 billion 6-8% total return,",
|
||||||
|
{"entities": [[10, 18, "LAUFZEIT"], [89, 93, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Geographic Focus: UK, Ireland, Iberia, Nordics, Netherlands, Germany, France, Italy",
|
||||||
|
{"entities": [[18, 83, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"IRR of 13-14%",
|
||||||
|
{"entities": [[7, 13, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Value-add",
|
||||||
|
{"entities": [[0, 9, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Geographic allocation NORDICS UNITED KINGDOM GERMANY FRANCE PORTUGAL BENELUX",
|
||||||
|
{"entities": [[22, 76, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Strong track record delivering a 17% net IRR, 1.7x net multiple across all divested assets (both discretionary and non-discretionary mandates)",
|
||||||
|
{"entities": [[33, 36, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Targeting a 7-8% net annual return and a 3-4% dividend yield, reflecting a target LTV of 35% (capped at 37.5%)",
|
||||||
|
{"entities": [[12, 16, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Sweden Norway Denmark Finland",
|
||||||
|
{"entities": [[0, 29, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Logistics Residential Office Other",
|
||||||
|
{"entities": [[0, 34, "SEKTORENALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Fund Term Open-ended with an initial 24-month lock-in for new investors",
|
||||||
|
{"entities": [[10, 20, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Management fee of 85 bps on NAV.",
|
||||||
|
{"entities": [[18, 24, "MANAGMENTGEBÜHREN"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Core/Core+ strategy, with tactical exposure to development projects aiming at enhancing the quality of the portfolio over time",
|
||||||
|
{"entities": [[0, 10, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Fund term: Open-ended",
|
||||||
|
{"entities": [[11, 21, "LAUFZEIT"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Return targets: The fund targets a net internal rate of return (IRR) of 8% and a net annual income yield of 5% with planned quarterly distributions’.",
|
||||||
|
{"entities": [[72, 74, "RENDITE"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Geographic scope: The fund has a broad mandate to invest in commercial and residential real estate across Sweden, Denmark, Finland, and Norway. 50% LTV Asset selection: Heirs to acquire high-quality, income-generating properties in major Nordic cities and enhance their value through active asset management. Portfolio construction: The goal is to build diversified portfolios that are appealing to core buyers upon exit.",
|
||||||
|
{"entities": [[106, 142, "LÄNDERALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Experience: Since 2012, | | has demonstrated its capability to build diversified and resilient portfolios for its core-plus funds. German Real Estate Quota advantage . Local expertise: extensive local relationships and proprietary deal flow in key Nordic markets provide a strategic advantage.",
|
||||||
|
{"entities": [[114, 123, "RISIKOPROFIL"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Target returns: 8% net IRR with 5% net annual income yield! * Geographic focus: Sweden, Denmark, Norway and Finland « Target leverage: 50% LTV (excluding short-term borrowing) « Sector exposure: office, logistics, public properties, retail (focused on grocery anchored and necessity driven retail) and residentials « Investment focus: high quality properties,",
|
||||||
|
{"entities": [[16, 18, "RENDITE"], [80, 115, "LÄNDERALLOKATION"], [195, 239, "SEKTORENALLOKATION"]]},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"The Fund 2 xemoours common limited partnership (SCS) (SICAV-RAIF) Investment Objective To pursue investments in commercial and residential properties throughout the Nordic Region Fund Target Size €300 million (equity) Return Targets Target net IRR of 8%, target net annual income yield of 5%",
|
||||||
|
{"entities": [[251, 253, "RENDITE"]]},
|
||||||
|
)
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import spacy
|
||||||
|
from spacy.cli.train import train
|
||||||
|
|
||||||
|
from spacy.tokens import DocBin
|
||||||
|
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from training_data import TRAINING_DATA
|
||||||
|
|
||||||
|
nlp = spacy.blank("de")
|
||||||
|
|
||||||
|
# create a DocBin object
|
||||||
|
db = DocBin()
|
||||||
|
|
||||||
|
for text, annot in tqdm(TRAINING_DATA):
|
||||||
|
doc = nlp.make_doc(text)
|
||||||
|
ents = []
|
||||||
|
# add character indexes
|
||||||
|
for start, end, label in annot["entities"]:
|
||||||
|
span = doc.char_span(start, end, label=label, alignment_mode="contract")
|
||||||
|
if span is None:
|
||||||
|
print(f"Skipping entity: |{text[start:end]}| Start: {start}, End: {end}, Label: {label}")
|
||||||
|
else:
|
||||||
|
ents.append(span)
|
||||||
|
# label the text with the ents
|
||||||
|
doc.ents = ents
|
||||||
|
db.add(doc)
|
||||||
|
|
||||||
|
# save the DocBin object
|
||||||
|
os.makedirs("./data", exist_ok=True)
|
||||||
|
db.to_disk("./data/train.spacy")
|
||||||
|
|
||||||
|
config_path = Path("config.cfg")
|
||||||
|
output_path = Path("output")
|
||||||
|
|
||||||
|
print("Starte Training...")
|
||||||
|
train(config_path, output_path)
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"running": false}
|
|
||||||
|
|
@ -40,9 +40,7 @@ def send_to_coordinator_service(processed_data, request_id):
|
||||||
|
|
||||||
def process_data_async(request_id, spacy_data, exxeta_data):
|
def process_data_async(request_id, spacy_data, exxeta_data):
|
||||||
try:
|
try:
|
||||||
requests.post(
|
requests.post(COORDINATOR_URL + "/api/progress", json={"id": request_id, "progress": 95})
|
||||||
COORDINATOR_URL + "/api/progress", json={"id": request_id, "progress": 95}
|
|
||||||
)
|
|
||||||
print(f"Start asynchronous processing for PitchBook: {request_id}")
|
print(f"Start asynchronous processing for PitchBook: {request_id}")
|
||||||
|
|
||||||
# Perform merge
|
# Perform merge
|
||||||
|
|
@ -98,6 +96,7 @@ def validate():
|
||||||
|
|
||||||
# If both datasets are present, start asynchronous processing
|
# If both datasets are present, start asynchronous processing
|
||||||
if spacy_data is not None and exxeta_data is not None:
|
if spacy_data is not None and exxeta_data is not None:
|
||||||
|
|
||||||
# Start asynchronous processing in a separate thread
|
# Start asynchronous processing in a separate thread
|
||||||
processing_thread = threading.Thread(
|
processing_thread = threading.Thread(
|
||||||
target=process_data_async,
|
target=process_data_async,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ def merge_entities(spacy_data, exxeta_data):
|
||||||
and s_entity_norm == e_entity_norm
|
and s_entity_norm == e_entity_norm
|
||||||
and s_page == e_page
|
and s_page == e_page
|
||||||
):
|
):
|
||||||
|
|
||||||
merged.append(
|
merged.append(
|
||||||
{
|
{
|
||||||
"label": s["label"],
|
"label": s["label"],
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,15 @@
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
import re
|
|
||||||
import requests
|
|
||||||
import os
|
|
||||||
|
|
||||||
# SETTINGS = [{"id": "Rendite", "type": "number"}]
|
|
||||||
COORDINATOR_URL = os.getenv("COORDINATOR_URL", "http://localhost:5000")
|
|
||||||
|
|
||||||
|
|
||||||
def validate_entities(entities):
|
def validate_entities(entities):
|
||||||
try:
|
|
||||||
response = requests.get(COORDINATOR_URL + "/api/kpi_setting/")
|
|
||||||
if response.status_code == 200:
|
|
||||||
settings = response.json()
|
|
||||||
else:
|
|
||||||
settings = []
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"Error fetching settings: {e}")
|
|
||||||
settings = []
|
|
||||||
# settings = SETTINGS
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
reduced_kpi: Dict[str, List[Dict[str, str]]] = {}
|
reduced_kpi: Dict[str, List[Dict[str, str | int]]] = {}
|
||||||
|
|
||||||
# reduce entities by label. Example: {"PERSON": [{"label": "PERSON", "entity": "John Doe", "status": "validated"}]}
|
|
||||||
for item in entities:
|
for item in entities:
|
||||||
label = item["label"]
|
label = item["label"]
|
||||||
if label not in reduced_kpi:
|
if label not in reduced_kpi:
|
||||||
reduced_kpi[label] = []
|
reduced_kpi[label] = []
|
||||||
reduced_kpi[label].append(item)
|
reduced_kpi[label].append(item)
|
||||||
|
|
||||||
reduced_kpi = delete_exxeta_unknown(reduced_kpi)
|
|
||||||
reduced_kpi = validate_number(reduced_kpi, settings)
|
|
||||||
reduced_kpi = delete_duplicate_entities(reduced_kpi)
|
|
||||||
|
|
||||||
for item in reduced_kpi.items():
|
for item in reduced_kpi.items():
|
||||||
if item[0] == "FONDSNAME":
|
if item[0] == "FONDSNAME":
|
||||||
result.extend(item[1])
|
result.extend(item[1])
|
||||||
|
|
@ -44,7 +21,6 @@ def validate_entities(entities):
|
||||||
result.extend(item[1])
|
result.extend(item[1])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Filter not validated, if there are valid values
|
|
||||||
validated = False
|
validated = False
|
||||||
for entity in item[1]:
|
for entity in item[1]:
|
||||||
if entity["status"] == "validated":
|
if entity["status"] == "validated":
|
||||||
|
|
@ -58,92 +34,13 @@ def validate_entities(entities):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def validate_number(entity_list, settings):
|
|
||||||
filtered_kpi = {}
|
|
||||||
for label, entity_list in entity_list.items():
|
|
||||||
setting = next((s for s in settings if s["name"].upper() == label), None)
|
|
||||||
if setting and setting["type"] == "number":
|
|
||||||
filtered_entities = [
|
|
||||||
entity
|
|
||||||
for entity in entity_list
|
|
||||||
if is_valid_number(str(entity["entity"]))
|
|
||||||
]
|
|
||||||
for entity in entity_list:
|
|
||||||
if not is_valid_number(str(entity["entity"])):
|
|
||||||
print(f"Invalid number: {entity}")
|
|
||||||
if filtered_entities: # Only add the label if there are entities left
|
|
||||||
filtered_kpi[label] = filtered_entities
|
|
||||||
else:
|
|
||||||
filtered_kpi[label] = entity_list
|
|
||||||
|
|
||||||
return filtered_kpi
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_number(number):
|
|
||||||
pattern = r"^[0-9\-\s%,.€]+$"
|
|
||||||
return (
|
|
||||||
any(char.isdigit() for char in number)
|
|
||||||
and not re.search(r"\d+\s\d+", number)
|
|
||||||
and re.fullmatch(pattern, number)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def delete_exxeta_unknown(entity_list):
|
|
||||||
filtered_kpi = {}
|
|
||||||
for label, entity_list in entity_list.items():
|
|
||||||
# Filter out entities with "nichtangegeben" or "n/a" (case-insensitive and stripped)
|
|
||||||
filtered_entities = [
|
|
||||||
entity
|
|
||||||
for entity in entity_list
|
|
||||||
if str(entity["entity"]).lower().replace(" ", "")
|
|
||||||
not in {"nichtangegeben", "n/a"}
|
|
||||||
]
|
|
||||||
for entity in entity_list:
|
|
||||||
if str(entity["entity"]).lower().replace(" ", "") in {
|
|
||||||
"nichtangegeben",
|
|
||||||
"n/a",
|
|
||||||
}:
|
|
||||||
print(f"filtered out: {entity}")
|
|
||||||
if filtered_entities: # Only add the label if there are entities left
|
|
||||||
filtered_kpi[label] = filtered_entities
|
|
||||||
return filtered_kpi
|
|
||||||
|
|
||||||
|
|
||||||
def delete_duplicate_entities(entity_list):
|
|
||||||
unique_entities = {}
|
|
||||||
for label, entity_list in entity_list.items():
|
|
||||||
values = set()
|
|
||||||
filtered_entities = []
|
|
||||||
for entity in entity_list:
|
|
||||||
if str(entity["entity"]).lower().replace(" ", "") not in values:
|
|
||||||
filtered_entities.append(entity)
|
|
||||||
else:
|
|
||||||
print(f"Duplicate entity: {entity}")
|
|
||||||
values.add(str(entity["entity"]).lower().replace(" ", ""))
|
|
||||||
if filtered_entities:
|
|
||||||
unique_entities[label] = filtered_entities
|
|
||||||
return unique_entities
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
entities = [
|
entities = [
|
||||||
# {"label": "PERSON", "entity": "John Doe", "status": "validated"},
|
{"label": "PERSON", "entity": "John Doe", "status": "validated"},
|
||||||
# {"label": "PERSON", "entity": "Exxeta", "status": "invalid"},
|
{"label": "PERSON", "entity": "Exxeta", "status": "invalid"},
|
||||||
# {"label": "ORG", "entity": "Google", "status": "invalid"},
|
{"label": "ORG", "entity": "Google", "status": "invalid"},
|
||||||
# {"label": "FONDSNAME", "entity": "Microsoft", "status": "validated"},
|
{"label": "FONDSNAME", "entity": "Microsoft", "status": "validated"},
|
||||||
# {"label": "FONDSNAME", "entity": "Amazon", "status": "invalid"},
|
{"label": "FONDSNAME", "entity": "Amazon", "status": "invalid"},
|
||||||
# {"label": "FONDSNAME", "entity": "Apple", "status": "invalid"},
|
{"label": "FONDSNAME", "entity": "Apple", "status": "invalid"}
|
||||||
{"label": "RENDITE", "entity": "8 8 8 8 8", "status": "validated"},
|
|
||||||
{"label": "RENDITE", "entity": "N/A", "status": "validated"},
|
|
||||||
{"label": "RENDITE", "entity": "nicht angegeben", "status": "validated"},
|
|
||||||
{"label": "RENDITE", "entity": "uaieluae--t>", "status": "validated"},
|
|
||||||
{"label": "RENDITE", "entity": "3,5", "status": "validated"},
|
|
||||||
{"label": "RENDITE", "entity": "3,5", "status": "validated"},
|
|
||||||
{"label": "RENDITE", "entity": "3 , 5", "status": "validated"},
|
|
||||||
{"label": "RENDITE", "entity": "3%", "status": "validated"},
|
|
||||||
{"label": "RENDITE", "entity": "", "status": "invalid"},
|
|
||||||
{"label": "RENDITE", "entity": "2 mehr als 6", "status": "invalid"},
|
|
||||||
{"label": "RENDITE", "entity": 2, "status": "invalid"},
|
|
||||||
]
|
]
|
||||||
print(validate_entities(entities))
|
print(validate_entities(entities))
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,6 @@ services:
|
||||||
- VALIDATE_SERVICE_URL=http://validate:5000/validate
|
- VALIDATE_SERVICE_URL=http://validate:5000/validate
|
||||||
ports:
|
ports:
|
||||||
- 5052:5052
|
- 5052:5052
|
||||||
volumes:
|
|
||||||
- ./backend/spacy-service/spacy_training:/app/spacy_training
|
|
||||||
|
|
||||||
exxeta:
|
exxeta:
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,4 @@ helm-charts
|
||||||
.editorconfig
|
.editorconfig
|
||||||
.idea
|
.idea
|
||||||
coverage*
|
coverage*
|
||||||
|
dist
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ WORKDIR /usr/src/app
|
||||||
# install dependencies into temp directory
|
# install dependencies into temp directory
|
||||||
# this will cache them and speed up future builds
|
# this will cache them and speed up future builds
|
||||||
COPY package.json bun.lockb ./
|
COPY package.json bun.lockb ./
|
||||||
#RUN bun install --frozen-lockfile
|
RUN bun install --frozen-lockfile
|
||||||
RUN bun install
|
|
||||||
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,15 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Web site created using create-tsrouter-app"
|
content="Web site created using create-tsrouter-app"
|
||||||
/>
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<title>Pitchbook Extractor</title>
|
<title>Create TanStack App - frontend</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -33,7 +33,6 @@
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/file-saver": "^2.0.7",
|
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@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 |
|
|
@ -6,56 +6,41 @@ import type { Kennzahl } from "../types/kpi";
|
||||||
import { getDisplayType } from "../types/kpi";
|
import { getDisplayType } from "../types/kpi";
|
||||||
import { fetchKennzahlen as fetchK } from "../util/api";
|
import { fetchKennzahlen as fetchK } from "../util/api";
|
||||||
import { API_HOST } from "../util/api";
|
import { API_HOST } from "../util/api";
|
||||||
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
|
|
||||||
|
|
||||||
|
type ConfigTableProps = {
|
||||||
|
|
||||||
|
|
||||||
export type ConfigTableProps = {
|
|
||||||
from?: string;
|
from?: string;
|
||||||
trainingRunning?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function ConfigTable({ from }: ConfigTableProps) {
|
||||||
|
|
||||||
export function ConfigTable({ from, trainingRunning }: ConfigTableProps) {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [kennzahlen, setKennzahlen] = useState<Kennzahl[]>([]);
|
const [kennzahlen, setKennzahlen] = useState<Kennzahl[]>([]);
|
||||||
const [draggedItem, setDraggedItem] = useState<Kennzahl | null>(null);
|
const [draggedItem, setDraggedItem] = useState<Kennzahl | null>(null);
|
||||||
const [isUpdatingPositions, setIsUpdatingPositions] = useState(false);
|
const [isUpdatingPositions, setIsUpdatingPositions] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const fetchKennzahlen = async () => {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
console.log("Fetching kennzahlen from API...");
|
|
||||||
const data = await fetchK();
|
|
||||||
console.log("Fetched kennzahlen:", data);
|
|
||||||
const sortedData = data.sort((a, b) => a.position - b.position);
|
|
||||||
setKennzahlen(sortedData);
|
|
||||||
setLoading(false);
|
|
||||||
break;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error fetching kennzahlen:", err);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const fetchKennzahlen = async () => {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
console.log("Fetching kennzahlen from API...");
|
||||||
|
const data = await fetchK();
|
||||||
|
console.log("Fetched kennzahlen:", data);
|
||||||
|
const sortedData = data.sort(
|
||||||
|
(a: Kennzahl, b: Kennzahl) => a.position - b.position,
|
||||||
|
);
|
||||||
|
setKennzahlen(sortedData);
|
||||||
|
setLoading(false);
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching kennzahlen:", err);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fetchKennzahlen();
|
fetchKennzahlen();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (trainingRunning === false) {
|
|
||||||
console.log("[ConfigTable] Training beendet → Kennzahlen neu laden...");
|
|
||||||
fetchKennzahlen();
|
|
||||||
}
|
|
||||||
}, [trainingRunning]);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleToggleActive = async (id: number) => {
|
const handleToggleActive = async (id: number) => {
|
||||||
const kennzahl = kennzahlen.find((k) => k.id === id);
|
const kennzahl = kennzahlen.find((k) => k.id === id);
|
||||||
if (!kennzahl) return;
|
if (!kennzahl) return;
|
||||||
|
|
@ -341,32 +326,12 @@ export function ConfigTable({ from, trainingRunning }: ConfigTableProps) {
|
||||||
padding: "12px",
|
padding: "12px",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
color: "#333",
|
color: "#333",
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "8px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span title={`Click to view details (ID: ${kennzahl.id})`}>
|
<span title={`Click to view details (ID: ${kennzahl.id})`}>
|
||||||
{kennzahl.name}
|
{kennzahl.name}
|
||||||
{kennzahl.mandatory && <span> *</span>}
|
|
||||||
</span>
|
</span>
|
||||||
{kennzahl.is_trained === false && (
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<b>Diese Kennzahl ist nicht trainiert.</b><br />
|
|
||||||
Klicken Sie oben auf <i>"Neu trainieren"</i>, um das Training zu starten.
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
arrow
|
|
||||||
placement="right"
|
|
||||||
>
|
|
||||||
<WarningAmberIcon sx={{ color: "#f57c00", fontSize: 20 }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style={{ padding: "12px" }}>
|
<td style={{ padding: "12px" }}>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -1,542 +1,247 @@
|
||||||
import {
|
import { Box, Typography, Button, Paper, TextField, FormControlLabel,
|
||||||
Box,
|
Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress } from "@mui/material";
|
||||||
Button,
|
import { useState, useEffect } from "react";
|
||||||
Checkbox,
|
|
||||||
CircularProgress,
|
|
||||||
Divider,
|
|
||||||
FormControl,
|
|
||||||
FormControlLabel,
|
|
||||||
InputLabel,
|
|
||||||
MenuItem,
|
|
||||||
Paper,
|
|
||||||
Select,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import MuiAlert from "@mui/material/Alert";
|
|
||||||
import Snackbar from "@mui/material/Snackbar";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import type { Kennzahl } from "../types/kpi";
|
import type { Kennzahl } from "../types/kpi";
|
||||||
import { typeDisplayMapping } from "../types/kpi";
|
import { typeDisplayMapping } from "../types/kpi";
|
||||||
import { API_HOST } from "../util/api";
|
|
||||||
|
|
||||||
interface KPIFormProps {
|
interface KPIFormProps {
|
||||||
mode: "add" | "edit";
|
mode: 'add' | 'edit';
|
||||||
initialData?: Kennzahl | null;
|
initialData?: Kennzahl | null;
|
||||||
onSave: (data: Partial<Kennzahl>) => Promise<void>;
|
onSave: (data: Partial<Kennzahl>) => Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
resetTrigger?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createEmptyKPI = (): Partial<Kennzahl> => ({
|
const emptyKPI: Partial<Kennzahl> = {
|
||||||
name: "",
|
name: '',
|
||||||
mandatory: false,
|
description: '',
|
||||||
type: "string",
|
mandatory: false,
|
||||||
active: true,
|
type: 'string',
|
||||||
examples: [{ sentence: "", value: "" }],
|
translation: '',
|
||||||
});
|
example: '',
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
|
||||||
export function KPIForm({
|
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }: KPIFormProps) {
|
||||||
mode,
|
const [formData, setFormData] = useState<Partial<Kennzahl>>(emptyKPI);
|
||||||
initialData,
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
onSave,
|
|
||||||
onCancel,
|
|
||||||
loading = false,
|
|
||||||
}: KPIFormProps) {
|
|
||||||
const [formData, setFormData] = useState<Partial<Kennzahl>>(createEmptyKPI());
|
|
||||||
const [originalExamples, setOriginalExamples] = useState<
|
|
||||||
Array<{ sentence: string; value: string }>
|
|
||||||
>([]);
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|
||||||
const [snackbarMessage, setSnackbarMessage] = useState("");
|
|
||||||
const [snackbarSeverity, setSnackbarSeverity] = useState<
|
|
||||||
"success" | "error" | "info"
|
|
||||||
>("success");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode === "edit" && initialData) {
|
if (mode === 'edit' && initialData) {
|
||||||
setOriginalExamples(initialData.examples || []);
|
setFormData(initialData);
|
||||||
setFormData({
|
} else {
|
||||||
...initialData,
|
setFormData(emptyKPI);
|
||||||
examples: [{ sentence: "", value: "" }],
|
}
|
||||||
});
|
}, [mode, initialData]);
|
||||||
} else if (mode === "add") {
|
|
||||||
setOriginalExamples([]);
|
|
||||||
setFormData(createEmptyKPI());
|
|
||||||
}
|
|
||||||
}, [mode, initialData]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!formData.name?.trim()) {
|
if (!formData.name?.trim()) {
|
||||||
setSnackbarMessage("Name ist erforderlich");
|
alert('Name ist erforderlich');
|
||||||
setSnackbarSeverity("error");
|
return;
|
||||||
setSnackbarOpen(true);
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === "add") {
|
setIsSaving(true);
|
||||||
if (!formData.examples || formData.examples.length === 0) {
|
try {
|
||||||
setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich");
|
await onSave(formData);
|
||||||
setSnackbarSeverity("error");
|
} catch (error) {
|
||||||
setSnackbarOpen(true);
|
console.error('Error saving KPI:', error);
|
||||||
return;
|
} finally {
|
||||||
}
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const newExamples = formData.examples.filter(
|
const handleCancel = () => {
|
||||||
(ex) => ex.sentence?.trim() && ex.value?.trim(),
|
onCancel();
|
||||||
);
|
};
|
||||||
|
|
||||||
if (newExamples.length === 0) {
|
const updateField = (field: keyof Kennzahl, value: any) => {
|
||||||
setSnackbarMessage(
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
"Mindestens ein vollständiger Beispielsatz ist erforderlich.",
|
};
|
||||||
);
|
|
||||||
setSnackbarSeverity("error");
|
|
||||||
setSnackbarOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newExamples = (formData.examples || []).filter(
|
if (loading) {
|
||||||
(ex) => ex.sentence?.trim() && ex.value?.trim(),
|
return (
|
||||||
);
|
<Box
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
minHeight="400px"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<CircularProgress sx={{ color: '#383838', mb: 2 }} />
|
||||||
|
<Typography>
|
||||||
|
{mode === 'edit' ? 'Lade KPI Details...' : 'Laden...'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (formData.examples && formData.examples.length > 0) {
|
return (
|
||||||
for (const ex of formData.examples) {
|
<Paper
|
||||||
if (!ex.sentence?.trim() && !ex.value?.trim()) continue;
|
elevation={2}
|
||||||
if (!ex.sentence?.trim() || !ex.value?.trim()) {
|
sx={{
|
||||||
setSnackbarMessage("Alle Beispielsätze müssen vollständig sein oder leer gelassen werden.");
|
width: "90%",
|
||||||
setSnackbarSeverity("error");
|
maxWidth: 800,
|
||||||
setSnackbarOpen(true);
|
p: 4,
|
||||||
return;
|
borderRadius: 2,
|
||||||
}
|
backgroundColor: "white"
|
||||||
}
|
}}
|
||||||
}
|
>
|
||||||
|
<Box mb={4}>
|
||||||
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||||
|
Kennzahl
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Name *"
|
||||||
|
value={formData.name || ''}
|
||||||
|
onChange={(e) => updateField('name', e.target.value)}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
required
|
||||||
|
error={!formData.name?.trim()}
|
||||||
|
helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
setIsSaving(true);
|
<Divider sx={{ my: 3 }} />
|
||||||
try {
|
|
||||||
if (newExamples.length > 0) {
|
|
||||||
const spacyEntries = generateSpacyEntries({
|
|
||||||
...formData,
|
|
||||||
examples: newExamples,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Für jeden einzelnen Beispielsatz:
|
<Box mb={4}>
|
||||||
for (const entry of spacyEntries) {
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||||
// im localStorage speichern (zum Debuggen oder Vorschau)
|
Beschreibung
|
||||||
const stored = localStorage.getItem("spacyData");
|
</Typography>
|
||||||
const existingData = stored ? JSON.parse(stored) : [];
|
<TextField
|
||||||
const updated = [...existingData, entry];
|
fullWidth
|
||||||
localStorage.setItem("spacyData", JSON.stringify(updated));
|
multiline
|
||||||
|
rows={3}
|
||||||
|
label="Beschreibung"
|
||||||
|
value={formData.description || ''}
|
||||||
|
onChange={(e) => updateField('description', e.target.value)}
|
||||||
|
helperText="Beschreibung der Kennzahl"
|
||||||
|
/>
|
||||||
|
|
||||||
// POST Request an das Flask-Backend
|
<Box mt={3}>
|
||||||
const response = await fetch(
|
<FormControlLabel
|
||||||
`${API_HOST}/api/spacy/append-training-entry`,
|
control={
|
||||||
{
|
<Checkbox
|
||||||
method: "POST",
|
checked={formData.mandatory || false}
|
||||||
headers: {
|
onChange={(e) => updateField('mandatory', e.target.checked)}
|
||||||
"Content-Type": "application/json",
|
sx={{ color: '#383838' }}
|
||||||
},
|
/>
|
||||||
body: JSON.stringify(entry),
|
}
|
||||||
},
|
label="Erforderlich"
|
||||||
);
|
/>
|
||||||
|
<Typography variant="body2" color="text.secondary" ml={4}>
|
||||||
|
Die Kennzahl erlaubt keine leeren Werte
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
const data = await response.json();
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
||||||
if (!response.ok) {
|
<Box mb={4}>
|
||||||
throw new Error(
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||||
data.error || "Fehler beim Aufruf von append-training-entry",
|
Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type}
|
||||||
);
|
</Typography>
|
||||||
}
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
|
<InputLabel>Typ</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={formData.type || 'string'}
|
||||||
|
label="Typ"
|
||||||
|
onChange={(e) => updateField('type', e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="string">Text</MenuItem>
|
||||||
|
<MenuItem value="number">Zahl</MenuItem>
|
||||||
|
<MenuItem value="date">Datum</MenuItem>
|
||||||
|
<MenuItem value="boolean">Ja/Nein</MenuItem>
|
||||||
|
<MenuItem value="array">Liste (mehrfach)</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
console.log("SpaCy-Eintrag gespeichert:", data);
|
<Divider sx={{ my: 3 }} />
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allExamples =
|
<Box mb={4}>
|
||||||
mode === "edit" ? [...originalExamples, ...newExamples] : newExamples;
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||||
|
Synonyme & Übersetzungen
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Übersetzung"
|
||||||
|
value={formData.translation || ''}
|
||||||
|
onChange={(e) => updateField('translation', e.target.value)}
|
||||||
|
helperText="z.B. Englische Übersetzung der Kennzahl"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
// Dann in die DB speichern
|
<Divider sx={{ my: 3 }} />
|
||||||
await onSave({
|
|
||||||
name: formData.name ?? "",
|
|
||||||
mandatory: formData.mandatory ?? false,
|
|
||||||
type: formData.type || "string",
|
|
||||||
position: formData.position ?? 0,
|
|
||||||
active: formData.active ?? true,
|
|
||||||
examples: allExamples,
|
|
||||||
is_trained: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Formular zurücksetzen:
|
<Box mb={4}>
|
||||||
if (mode === "add") {
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||||
setFormData(createEmptyKPI());
|
Beispiele von Kennzahl
|
||||||
} else {
|
</Typography>
|
||||||
setFormData((prev) => ({
|
<TextField
|
||||||
...prev,
|
fullWidth
|
||||||
examples: [{ sentence: "", value: "" }],
|
multiline
|
||||||
}));
|
rows={2}
|
||||||
}
|
label="Beispiel"
|
||||||
|
value={formData.example || ''}
|
||||||
|
onChange={(e) => updateField('example', e.target.value)}
|
||||||
|
helperText="Beispielwerte für diese Kennzahl"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
const successMessage = newExamples.length > 0
|
{mode === 'add' && (
|
||||||
? "Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder weitere Kennzahlen hinzufügen."
|
<>
|
||||||
: mode === "edit"
|
<Divider sx={{ my: 3 }} />
|
||||||
? "Kennzahl erfolgreich aktualisiert."
|
<Box mb={4}>
|
||||||
: "Kennzahl erfolgreich erstellt.";
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={formData.active !== false}
|
||||||
|
onChange={(e) => updateField('active', e.target.checked)}
|
||||||
|
sx={{ color: '#383838' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Aktiv"
|
||||||
|
/>
|
||||||
|
<Typography variant="body2" color="text.secondary" ml={4}>
|
||||||
|
Die Kennzahl ist aktiv und wird angezeigt
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
setSnackbarMessage(successMessage);
|
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}>
|
||||||
setSnackbarSeverity("success");
|
<Button
|
||||||
setSnackbarOpen(true);
|
variant="outlined"
|
||||||
} catch (e: any) {
|
onClick={handleCancel}
|
||||||
// Prüfe auf 409-Fehler
|
disabled={isSaving}
|
||||||
if (e?.message?.includes("409") || e?.response?.status === 409) {
|
sx={{
|
||||||
setSnackbarMessage(
|
borderColor: "#383838",
|
||||||
"Diese Kennzahl existiert bereits. Sie können sie unter -Konfiguration- bearbeiten.",
|
color: "#383838",
|
||||||
);
|
"&:hover": { borderColor: "#2e2e2e", backgroundColor: "#f5f5f5" }
|
||||||
setSnackbarSeverity("info");
|
}}
|
||||||
setSnackbarOpen(true);
|
>
|
||||||
} else {
|
Abbrechen
|
||||||
setSnackbarMessage(e.message || "Fehler beim Speichern.");
|
</Button>
|
||||||
setSnackbarSeverity("error");
|
<Button
|
||||||
setSnackbarOpen(true);
|
variant="contained"
|
||||||
}
|
onClick={handleSave}
|
||||||
console.error(e);
|
disabled={isSaving || !formData.name?.trim()}
|
||||||
} finally {
|
sx={{
|
||||||
setIsSaving(false);
|
backgroundColor: "#383838",
|
||||||
}
|
"&:hover": { backgroundColor: "#2e2e2e" },
|
||||||
};
|
}}
|
||||||
|
>
|
||||||
const handleCancel = () => {
|
{isSaving ? (
|
||||||
setFormData(createEmptyKPI());
|
<>
|
||||||
onCancel();
|
<CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
|
||||||
};
|
{mode === 'add' ? 'Hinzufügen...' : 'Speichern...'}
|
||||||
|
</>
|
||||||
const updateField = (field: keyof Kennzahl, value: any) => {
|
) : (
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
mode === 'add' ? 'Hinzufügen' : 'Speichern'
|
||||||
};
|
)}
|
||||||
|
</Button>
|
||||||
const updateExample = (
|
</Box>
|
||||||
index: number,
|
</Paper>
|
||||||
field: "sentence" | "value",
|
);
|
||||||
value: string,
|
}
|
||||||
) => {
|
|
||||||
const newExamples = [...(formData.examples || [])];
|
|
||||||
newExamples[index][field] = value;
|
|
||||||
updateField("examples", newExamples);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addExample = () => {
|
|
||||||
const newExamples = [
|
|
||||||
...(formData.examples || []),
|
|
||||||
{ sentence: "", value: "" },
|
|
||||||
];
|
|
||||||
updateField("examples", newExamples);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeExample = (index: number) => {
|
|
||||||
const newExamples = [...(formData.examples || [])];
|
|
||||||
newExamples.splice(index, 1);
|
|
||||||
updateField("examples", newExamples);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
display="flex"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
minHeight="400px"
|
|
||||||
flexDirection="column"
|
|
||||||
>
|
|
||||||
<CircularProgress sx={{ color: "#383838", mb: 2 }} />
|
|
||||||
<Typography>
|
|
||||||
{mode === "edit" ? "Lade KPI Details..." : "Laden..."}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Paper
|
|
||||||
elevation={2}
|
|
||||||
sx={{
|
|
||||||
width: "90%",
|
|
||||||
maxWidth: 800,
|
|
||||||
p: 4,
|
|
||||||
borderRadius: 2,
|
|
||||||
backgroundColor: "white",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box mb={4}>
|
|
||||||
<Typography variant="h6" fontWeight="bold" mb={2}>
|
|
||||||
Kennzahl
|
|
||||||
</Typography>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Name *"
|
|
||||||
placeholder="z.B. IRR"
|
|
||||||
value={formData.name || ""}
|
|
||||||
onChange={(e) => updateField("name", e.target.value)}
|
|
||||||
sx={{ mb: 2 }}
|
|
||||||
required
|
|
||||||
error={!formData.name?.trim()}
|
|
||||||
helperText={!formData.name?.trim() ? "Name ist erforderlich" : ""}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider sx={{ my: 3 }} />
|
|
||||||
|
|
||||||
<Box mb={4}>
|
|
||||||
<Typography variant="h6" fontWeight="bold" mb={2}>
|
|
||||||
Format:{" "}
|
|
||||||
{typeDisplayMapping[
|
|
||||||
formData.type as keyof typeof typeDisplayMapping
|
|
||||||
] || formData.type}
|
|
||||||
</Typography>
|
|
||||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
|
||||||
<InputLabel>Typ</InputLabel>
|
|
||||||
<Select
|
|
||||||
value={formData.type || "string"}
|
|
||||||
label="Typ"
|
|
||||||
onChange={(e) => updateField("type", e.target.value)}
|
|
||||||
>
|
|
||||||
<MenuItem value="string">Text</MenuItem>
|
|
||||||
<MenuItem value="number">Zahl</MenuItem>
|
|
||||||
<MenuItem value="date">Datum</MenuItem>
|
|
||||||
<MenuItem value="boolean">Ja/Nein</MenuItem>
|
|
||||||
<MenuItem value="array">Liste (mehrfach)</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider sx={{ my: 3 }} />
|
|
||||||
|
|
||||||
<Box mb={4}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={formData.active !== false}
|
|
||||||
onChange={(e) => updateField("active", e.target.checked)}
|
|
||||||
sx={{
|
|
||||||
color: "#666666",
|
|
||||||
"&.Mui-checked": {
|
|
||||||
color: "#333333",
|
|
||||||
},
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "rgba(102, 102, 102, 0.04)",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Aktiv"
|
|
||||||
/>
|
|
||||||
<Typography variant="body2" color="text.secondary" ml={4}>
|
|
||||||
Die Kennzahl ist aktiv und wird angezeigt
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box mt={3}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={formData.mandatory || false}
|
|
||||||
onChange={(e) => updateField("mandatory", e.target.checked)}
|
|
||||||
sx={{
|
|
||||||
color: "#666666",
|
|
||||||
"&.Mui-checked": {
|
|
||||||
color: "#333333",
|
|
||||||
},
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "rgba(102, 102, 102, 0.04)",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Erforderlich"
|
|
||||||
/>
|
|
||||||
<Typography variant="body2" color="text.secondary" ml={4}>
|
|
||||||
Die Kennzahl erlaubt keine leeren Werte
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider sx={{ my: 3 }} />
|
|
||||||
|
|
||||||
{/* Hinweistext wie viele Beispielsätzen vorhanden sind*/}
|
|
||||||
{mode === "edit" && originalExamples.length > 0 && (
|
|
||||||
<Box
|
|
||||||
mb={2}
|
|
||||||
p={2}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#e3f2fd",
|
|
||||||
border: "1px solid #90caf9",
|
|
||||||
borderRadius: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
|
|
||||||
Vorhandene Beispielsätze: {originalExamples.length}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2">
|
|
||||||
Diese Kennzahl hat bereits {originalExamples.length}{" "}
|
|
||||||
Beispielsätze. Neue Beispielsätze werden zu den vorhandenen
|
|
||||||
hinzugefügt.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Hinweistext vor Beispielsätzen */}
|
|
||||||
<Box
|
|
||||||
mb={2}
|
|
||||||
p={2}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#fff8e1",
|
|
||||||
border: "1px solid #ffe082",
|
|
||||||
borderRadius: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
|
|
||||||
Hinweis zur Trainingsqualität
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2">
|
|
||||||
Damit das System neue Kennzahlen zuverlässig erkennen kann,
|
|
||||||
empfehlen wir <strong>mindestens 5 Beispielsätze</strong> zu
|
|
||||||
erstellen – je mehr, desto besser.
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" mt={1}>
|
|
||||||
<strong>Wichtig:</strong> Neue Kennzahlen werden erst in
|
|
||||||
PDF-Dokumenten erkannt, wenn Sie den Button{" "}
|
|
||||||
<em>"Neu trainieren"</em> auf der Konfigurationsseite ausführen.
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" mt={1}>
|
|
||||||
<strong>Tipp:</strong> Sie können jederzeit weitere Beispielsätze
|
|
||||||
hinzufügen.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box mb={4}>
|
|
||||||
<Typography variant="h6" fontWeight="bold" mb={2}>
|
|
||||||
Beispielsätze
|
|
||||||
</Typography>
|
|
||||||
{(formData.examples || []).map((ex, idx) => (
|
|
||||||
<Box
|
|
||||||
key={idx}
|
|
||||||
sx={{ mb: 2, border: "1px solid #ccc", p: 2, borderRadius: 1 }}
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
multiline
|
|
||||||
label={`Beispielsatz ${idx + 1}`}
|
|
||||||
placeholder="z.B. Die IRR beträgt 7,8 %"
|
|
||||||
value={ex.sentence}
|
|
||||||
onChange={(e) => updateExample(idx, "sentence", e.target.value)}
|
|
||||||
required
|
|
||||||
sx={{ mb: 1 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Bezeichneter Wert im Satz"
|
|
||||||
placeholder="z.B. 7,8 %"
|
|
||||||
value={ex.value}
|
|
||||||
onChange={(e) => updateExample(idx, "value", e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
{(formData.examples?.length || 0) > 1 && (
|
|
||||||
<Button
|
|
||||||
onClick={() => removeExample(idx)}
|
|
||||||
sx={{ mt: 1 }}
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
Entfernen
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Button variant="outlined" onClick={addExample}>
|
|
||||||
+ Beispielsatz hinzufügen
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={handleCancel}
|
|
||||||
disabled={isSaving}
|
|
||||||
sx={{
|
|
||||||
borderColor: "#383838",
|
|
||||||
color: "#383838",
|
|
||||||
"&:hover": { borderColor: "#2e2e2e", backgroundColor: "#f5f5f5" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Abbrechen
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={isSaving || !formData.name?.trim()}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#383838",
|
|
||||||
"&:hover": { backgroundColor: "#2e2e2e" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isSaving ? (
|
|
||||||
<>
|
|
||||||
<CircularProgress size={16} sx={{ mr: 1, color: "white" }} />
|
|
||||||
{mode === "add" ? "Hinzufügen..." : "Speichern..."}
|
|
||||||
</>
|
|
||||||
) : mode === "add" ? (
|
|
||||||
"Hinzufügen"
|
|
||||||
) : (
|
|
||||||
"Speichern"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
<Snackbar
|
|
||||||
open={snackbarOpen}
|
|
||||||
autoHideDuration={5000}
|
|
||||||
onClose={() => setSnackbarOpen(false)}
|
|
||||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
|
||||||
>
|
|
||||||
<MuiAlert
|
|
||||||
elevation={6}
|
|
||||||
variant="filled"
|
|
||||||
onClose={() => setSnackbarOpen(false)}
|
|
||||||
severity={snackbarSeverity}
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{snackbarMessage}</span>
|
|
||||||
<Button
|
|
||||||
color="inherit"
|
|
||||||
size="small"
|
|
||||||
onClick={() => setSnackbarOpen(false)}
|
|
||||||
>
|
|
||||||
OK
|
|
||||||
</Button>
|
|
||||||
</MuiAlert>
|
|
||||||
</Snackbar>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateSpacyEntries(formData: Partial<Kennzahl>) {
|
|
||||||
const label = formData.name?.trim().toUpperCase() || "";
|
|
||||||
return (formData.examples || []).map(({ sentence, value }) => {
|
|
||||||
const trimmedValue = value.trim();
|
|
||||||
const start = sentence.indexOf(trimmedValue);
|
|
||||||
if (start === -1) {
|
|
||||||
throw new Error(
|
|
||||||
`"${trimmedValue}" nicht gefunden in Satz: "${sentence}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
text: sentence,
|
|
||||||
entities: [[start, start + trimmedValue.length, label]],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -143,9 +143,7 @@ export default function KennzahlenTable({
|
||||||
|
|
||||||
const handlePageSave = async (index: string) => {
|
const handlePageSave = async (index: string) => {
|
||||||
const pageNumber = parseInt(editPageValue);
|
const pageNumber = parseInt(editPageValue);
|
||||||
if (editPageValue === "" || pageNumber === 0) {
|
if (!isNaN(pageNumber) && pageNumber > 0) {
|
||||||
mutate({ id: index, newPage: 0 });
|
|
||||||
} else if (!isNaN(pageNumber) && pageNumber > 0) {
|
|
||||||
mutate({ id: index, newPage: pageNumber });
|
mutate({ id: index, newPage: pageNumber });
|
||||||
}
|
}
|
||||||
setEditingPageIndex("");
|
setEditingPageIndex("");
|
||||||
|
|
@ -184,10 +182,10 @@ export default function KennzahlenTable({
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell width="30%">
|
<TableCell width="25%">
|
||||||
<strong>Kennzahl</strong>
|
<strong>Kennzahl</strong>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell width="55%">
|
<TableCell width="60%">
|
||||||
<strong>Wert</strong>
|
<strong>Wert</strong>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center" width="15%">
|
<TableCell align="center" width="15%">
|
||||||
|
|
@ -226,11 +224,7 @@ export default function KennzahlenTable({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={row.setting.name}>
|
<TableRow key={row.setting.name}>
|
||||||
<TableCell>{row.setting.name}
|
<TableCell>{row.setting.name}</TableCell>
|
||||||
{row.setting.mandatory && (
|
|
||||||
<span> *</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
<TableCell
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Only allow inline editing for non-multiple value cells
|
// Only allow inline editing for non-multiple value cells
|
||||||
|
|
@ -376,7 +370,7 @@ export default function KennzahlenTable({
|
||||||
value={editPageValue}
|
value={editPageValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === '' || /^\d+$/.test(value)) {
|
if (value === '' || /^\d+$/.test(value) && parseInt(value) > 0) {
|
||||||
setEditPageValue(value);
|
setEditPageValue(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
@ -405,11 +399,14 @@ export default function KennzahlenTable({
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
cursor: "pointer",
|
cursor: canEditPage ? "pointer" : "default",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
minHeight: "32px",
|
minHeight: "32px",
|
||||||
minWidth: "100px",
|
minWidth: "100px",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={() => canEditPage && setHoveredPageIndex(row.setting.name)}
|
||||||
|
onMouseLeave={() => setHoveredPageIndex("")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (canEditPage) {
|
if (canEditPage) {
|
||||||
startPageEditing(currentPage, row.setting.name);
|
startPageEditing(currentPage, row.setting.name);
|
||||||
|
|
@ -429,6 +426,19 @@ export default function KennzahlenTable({
|
||||||
>
|
>
|
||||||
{currentPage}
|
{currentPage}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{isPageHovered && canEditPage && (
|
||||||
|
<EditIcon
|
||||||
|
fontSize="small"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "70px",
|
||||||
|
color: "#666",
|
||||||
|
opacity: 0.7,
|
||||||
|
transition: "opacity 0.2s ease",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
) : canEditPage ? (
|
) : canEditPage ? (
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -453,7 +463,7 @@ export default function KennzahlenTable({
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: "70px",
|
left: "70px",
|
||||||
color: "black",
|
color: "#555",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
transition: "opacity 0.2s ease",
|
transition: "opacity 0.2s ease",
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import { useCallback, useEffect, useState } from "react";
|
||||||
import { socket } from "../socket";
|
import { socket } from "../socket";
|
||||||
import { fetchPitchBooksById } from "../util/api";
|
import { fetchPitchBooksById } from "../util/api";
|
||||||
import { pitchBooksQueryOptions } from "../util/query";
|
import { pitchBooksQueryOptions } from "../util/query";
|
||||||
import { formatDate } from "../util/date"
|
|
||||||
|
|
||||||
interface PitchBook {
|
interface PitchBook {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -47,7 +46,6 @@ export function PitchBooksTable() {
|
||||||
id: number;
|
id: number;
|
||||||
progress: number;
|
progress: number;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
created_at?: string;
|
|
||||||
buffer: number;
|
buffer: number;
|
||||||
intervalId?: number;
|
intervalId?: number;
|
||||||
}[]
|
}[]
|
||||||
|
|
@ -78,6 +76,7 @@ export function PitchBooksTable() {
|
||||||
const intervalId = prev.find(
|
const intervalId = prev.find(
|
||||||
(item) => item.id === progress.id,
|
(item) => item.id === progress.id,
|
||||||
)?.intervalId;
|
)?.intervalId;
|
||||||
|
console.log(intervalId, prev);
|
||||||
intervalId && clearInterval(intervalId);
|
intervalId && clearInterval(intervalId);
|
||||||
|
|
||||||
return [...prev.filter((item) => item.id !== progress.id)];
|
return [...prev.filter((item) => item.id !== progress.id)];
|
||||||
|
|
@ -103,7 +102,6 @@ export function PitchBooksTable() {
|
||||||
filename: oldItem?.filename,
|
filename: oldItem?.filename,
|
||||||
buffer: oldItem ? oldItem.buffer + 0.5 : 0,
|
buffer: oldItem ? oldItem.buffer + 0.5 : 0,
|
||||||
intervalId: oldItem.intervalId,
|
intervalId: oldItem.intervalId,
|
||||||
created_at: oldItem?.created_at,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
@ -119,7 +117,6 @@ export function PitchBooksTable() {
|
||||||
filename: res.filename,
|
filename: res.filename,
|
||||||
buffer: 0,
|
buffer: 0,
|
||||||
intervalId,
|
intervalId,
|
||||||
created_at: res.created_at,
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
|
|
@ -133,7 +130,6 @@ export function PitchBooksTable() {
|
||||||
id: progress.id,
|
id: progress.id,
|
||||||
progress: progress.progress,
|
progress: progress.progress,
|
||||||
filename: oldItem?.filename,
|
filename: oldItem?.filename,
|
||||||
created_at: oldItem?.created_at,
|
|
||||||
buffer: 0,
|
buffer: 0,
|
||||||
intervalId,
|
intervalId,
|
||||||
},
|
},
|
||||||
|
|
@ -215,7 +211,6 @@ export function PitchBooksTable() {
|
||||||
<TableCell sx={{ width: "60px" }} />
|
<TableCell sx={{ width: "60px" }} />
|
||||||
<TableCell sx={{ fontWeight: "bold" }}>Fondsname</TableCell>
|
<TableCell sx={{ fontWeight: "bold" }}>Fondsname</TableCell>
|
||||||
<TableCell sx={{ fontWeight: "bold" }}>Fondsmanager</TableCell>
|
<TableCell sx={{ fontWeight: "bold" }}>Fondsmanager</TableCell>
|
||||||
<TableCell sx={{ fontWeight: "bold" }}>Hochgeladen am</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: "bold" }}>Dateiname</TableCell>
|
<TableCell sx={{ fontWeight: "bold" }}>Dateiname</TableCell>
|
||||||
<TableCell sx={{ fontWeight: "bold", width: "120px" }}>
|
<TableCell sx={{ fontWeight: "bold", width: "120px" }}>
|
||||||
Status
|
Status
|
||||||
|
|
@ -223,71 +218,6 @@ export function PitchBooksTable() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<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
|
{pitchBooks
|
||||||
.filter(
|
.filter(
|
||||||
(pitchbook: PitchBook) =>
|
(pitchbook: PitchBook) =>
|
||||||
|
|
@ -295,8 +225,8 @@ export function PitchBooksTable() {
|
||||||
)
|
)
|
||||||
.sort(
|
.sort(
|
||||||
(a: PitchBook, b: PitchBook) =>
|
(a: PitchBook, b: PitchBook) =>
|
||||||
new Date(b.created_at).getTime() -
|
new Date(a.created_at).getTime() -
|
||||||
new Date(a.created_at).getTime(),
|
new Date(b.created_at).getTime(),
|
||||||
)
|
)
|
||||||
.map((pitchBook: PitchBook) => {
|
.map((pitchBook: PitchBook) => {
|
||||||
const status = getStatus(pitchBook);
|
const status = getStatus(pitchBook);
|
||||||
|
|
@ -346,7 +276,6 @@ export function PitchBooksTable() {
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{manager}</TableCell>
|
<TableCell>{manager}</TableCell>
|
||||||
<TableCell>{formatDate(pitchBook.created_at)}</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
|
|
@ -360,7 +289,7 @@ export function PitchBooksTable() {
|
||||||
{status === "completed" ? (
|
{status === "completed" ? (
|
||||||
<Chip
|
<Chip
|
||||||
icon={<CheckCircleIcon />}
|
icon={<CheckCircleIcon />}
|
||||||
label="Extraktion Abgeschlossen"
|
label="Abgeschlossen"
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "#e8f5e9",
|
backgroundColor: "#e8f5e9",
|
||||||
|
|
@ -388,6 +317,63 @@ export function PitchBooksTable() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{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.filename}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
icon={<HourglassEmptyIcon />}
|
||||||
|
label="In Bearbeitung"
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#fff3e0",
|
||||||
|
color: "#e65100",
|
||||||
|
"& .MuiChip-icon": {
|
||||||
|
color: "#e65100",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
{pitchBooks.length === 0 && (
|
{pitchBooks.length === 0 && (
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
import { Backdrop, Box, Button, IconButton, Paper, Typography } from "@mui/material";
|
import { Backdrop, Box, Button, IconButton, Paper } from "@mui/material";
|
||||||
import { useNavigate, useRouter } from "@tanstack/react-router";
|
import { useNavigate, useRouter } from "@tanstack/react-router";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import FileUpload from "react-material-file-upload";
|
import FileUpload from "react-material-file-upload";
|
||||||
import { socket } from "../socket";
|
import { socket } from "../socket";
|
||||||
import { API_HOST } from "../util/api";
|
import { uploadPitchBook } from "../util/api";
|
||||||
|
// import { API_HOST } from "../util/api";
|
||||||
import { CircularProgressWithLabel } from "./CircularProgressWithLabel";
|
import { CircularProgressWithLabel } from "./CircularProgressWithLabel";
|
||||||
import DekaLogo from "../assets/Deka_logo.png";
|
|
||||||
|
|
||||||
export default function UploadPage() {
|
export default function UploadPage() {
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
|
@ -16,23 +16,23 @@ export default function UploadPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const uploadFile = useCallback(async () => {
|
// const uploadFile = useCallback(async () => {
|
||||||
const formData = new FormData();
|
// const formData = new FormData();
|
||||||
formData.append("file", files[0]);
|
// formData.append("file", files[0]);
|
||||||
const response = await fetch(`${API_HOST}/api/pitch_book`, {
|
// const response = await fetch(`${API_HOST}/api/pitch_book/`, {
|
||||||
method: "POST",
|
// method: "POST",
|
||||||
body: formData,
|
// body: formData,
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (response.ok) {
|
// if (response.ok) {
|
||||||
console.log("File uploaded successfully");
|
// console.log("File uploaded successfully");
|
||||||
const data = await response.json();
|
// const data = await response.json();
|
||||||
setPageId(data.id.toString());
|
// setPageId(data.id.toString());
|
||||||
setLoadingState(5);
|
// setLoadingState(5);
|
||||||
} else {
|
// } else {
|
||||||
console.error("Failed to upload file");
|
// console.error("Failed to upload file");
|
||||||
}
|
// }
|
||||||
}, [files]);
|
// }, [files]);
|
||||||
|
|
||||||
const onConnection = useCallback(() => {
|
const onConnection = useCallback(() => {
|
||||||
console.log("connected");
|
console.log("connected");
|
||||||
|
|
@ -88,50 +88,26 @@ export default function UploadPage() {
|
||||||
display="flex"
|
display="flex"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="flex-start"
|
justifyContent="center"
|
||||||
height="100vh"
|
height="100vh"
|
||||||
bgcolor="white"
|
bgcolor="white"
|
||||||
pt={3}
|
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
width="100%"
|
width="100%"
|
||||||
|
maxWidth="1300px"
|
||||||
display="flex"
|
display="flex"
|
||||||
justifyContent="space-between"
|
justifyContent="flex-end"
|
||||||
alignItems="center"
|
px={2}
|
||||||
px={8}
|
|
||||||
py={5}
|
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
|
||||||
<img
|
|
||||||
src={DekaLogo}
|
|
||||||
alt="Company Logo"
|
|
||||||
style={{ height: "40px", width: "auto" }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<IconButton onClick={() => navigate({ to: "/config" })}>
|
<IconButton onClick={() => navigate({ to: "/config" })}>
|
||||||
<SettingsIcon fontSize="large" />
|
<SettingsIcon fontSize="large" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="h4"
|
|
||||||
component="h1"
|
|
||||||
sx={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
color: "#383838",
|
|
||||||
marginBottom: 12,
|
|
||||||
marginTop: 3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Pitchbook Extractor
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
elevation={3}
|
elevation={3}
|
||||||
sx={{
|
sx={{
|
||||||
width: 800,
|
width: 900,
|
||||||
height: 400,
|
height: 500,
|
||||||
backgroundColor: "#eeeeee",
|
backgroundColor: "#eeeeee",
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -191,7 +167,13 @@ export default function UploadPage() {
|
||||||
backgroundColor: "#383838",
|
backgroundColor: "#383838",
|
||||||
}}
|
}}
|
||||||
disabled={files.length === 0}
|
disabled={files.length === 0}
|
||||||
onClick={uploadFile}
|
onClick={async () => {
|
||||||
|
setLoadingState(1);
|
||||||
|
setPageId("-1");
|
||||||
|
const id = await uploadPitchBook(files);
|
||||||
|
setPageId(id.toString());
|
||||||
|
setLoadingState(5);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Kennzahlen extrahieren
|
Kennzahlen extrahieren
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -207,9 +189,9 @@ export default function UploadPage() {
|
||||||
onMouseEnter={() => router.preloadRoute({ to: "/pitchbooks" })}
|
onMouseEnter={() => router.preloadRoute({ to: "/pitchbooks" })}
|
||||||
onClick={() => navigate({ to: "/pitchbooks" })}
|
onClick={() => navigate({ to: "/pitchbooks" })}
|
||||||
>
|
>
|
||||||
Alle Pitchbooks anzeigen
|
Alle Pitch Books anzeigen
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import "react-pdf/dist/esm/Page/AnnotationLayer.css";
|
||||||
import "react-pdf/dist/esm/Page/TextLayer.css";
|
import "react-pdf/dist/esm/Page/TextLayer.css";
|
||||||
import ArrowCircleLeftIcon from "@mui/icons-material/ArrowCircleLeft";
|
import ArrowCircleLeftIcon from "@mui/icons-material/ArrowCircleLeft";
|
||||||
import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight";
|
import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight";
|
||||||
import ZoomInIcon from "@mui/icons-material/ZoomIn";
|
|
||||||
import ZoomOutIcon from "@mui/icons-material/ZoomOut";
|
|
||||||
import { Box, IconButton } from "@mui/material";
|
import { Box, IconButton } from "@mui/material";
|
||||||
import type {
|
import type {
|
||||||
CustomTextRenderer,
|
CustomTextRenderer,
|
||||||
|
|
@ -32,7 +30,7 @@ export default function PDFViewer({
|
||||||
}: PDFViewerProps) {
|
}: PDFViewerProps) {
|
||||||
const [numPages, setNumPages] = useState<number | null>(null);
|
const [numPages, setNumPages] = useState<number | null>(null);
|
||||||
const [pageNumber, setPageNumber] = useState(currentPage || 1);
|
const [pageNumber, setPageNumber] = useState(currentPage || 1);
|
||||||
const [baseWidth, setBaseWidth] = useState<number | null>(null);
|
const [containerWidth, setContainerWidth] = useState<number | null>(null);
|
||||||
const [pdfKey, setPdfKey] = useState(Date.now());
|
const [pdfKey, setPdfKey] = useState(Date.now());
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [posHighlight, setPosHighlight] = useState<string[]>([]);
|
const [posHighlight, setPosHighlight] = useState<string[]>([]);
|
||||||
|
|
@ -40,7 +38,6 @@ export default function PDFViewer({
|
||||||
const [textContent, setTextContent] = useState<
|
const [textContent, setTextContent] = useState<
|
||||||
{ posKey: string; text: string; i: number }[]
|
{ posKey: string; text: string; i: number }[]
|
||||||
>([]);
|
>([]);
|
||||||
const [zoomLevel, setZoomLevel] = useState(1.0);
|
|
||||||
|
|
||||||
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
|
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
|
||||||
setNumPages(numPages);
|
setNumPages(numPages);
|
||||||
|
|
@ -49,9 +46,7 @@ export default function PDFViewer({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateWidth = () => {
|
const updateWidth = () => {
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
if (!baseWidth) {
|
setContainerWidth(containerRef.current.offsetWidth);
|
||||||
setBaseWidth(containerRef.current.offsetWidth);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -100,82 +95,53 @@ export default function PDFViewer({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tmpPos: string[] = [];
|
const tmpPos: string[] = [];
|
||||||
const tmpPosHighlight: string[] = [];
|
const tmpPosHighlight: string[] = [];
|
||||||
|
const textItems = textContent.filter(
|
||||||
|
(e) => e.text !== "" && e.text !== " ",
|
||||||
|
);
|
||||||
|
|
||||||
if (textContent.length === 0) {
|
textItems.forEach((e, i) => {
|
||||||
setPosHighlight([]);
|
for (const s of highlight
|
||||||
setPosHighlightFocus([]);
|
.filter((h) => h.page === pageNumber)
|
||||||
return;
|
.map((h) => h.text)) {
|
||||||
}
|
if (s.split(" ")[0] === e.text) {
|
||||||
const findTextPositions = (searchText: string): number[] => {
|
if (
|
||||||
const positions: number[] = [];
|
s.split(" ").reduce((prev, curr, j) => {
|
||||||
const normalizedSearch = searchText.toLowerCase().trim();
|
return prev && curr === textItems[i + j].text;
|
||||||
|
}, true)
|
||||||
textContent.forEach((item, index) => {
|
) {
|
||||||
if (item.text.toLowerCase().trim() === normalizedSearch) {
|
for (
|
||||||
positions.push(index);
|
let k = textItems[i].i;
|
||||||
}
|
k < textItems[i + s.split(" ").length]?.i ||
|
||||||
});
|
k < textItems[i + s.split(" ").length - 1]?.i;
|
||||||
|
k++
|
||||||
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
|
|
||||||
) {
|
) {
|
||||||
if (!positions.includes(boundary.index)) {
|
tmpPos.push(textContent[k].posKey);
|
||||||
positions.push(boundary.index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
searchIndex = lowerCumulative.indexOf(
|
|
||||||
normalizedSearch,
|
|
||||||
searchIndex + 1,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return positions.sort((a, b) => a - b);
|
|
||||||
};
|
if (focusHighlight?.page === pageNumber) {
|
||||||
highlight
|
if (focusHighlight.text.split(" ")[0] === e.text) {
|
||||||
.filter((h) => h.page === pageNumber)
|
if (
|
||||||
.forEach((highlightItem) => {
|
focusHighlight.text.split(" ").reduce((prev, curr, j) => {
|
||||||
const positions = findTextPositions(highlightItem.text);
|
return prev && curr === textItems[i + j].text;
|
||||||
positions.forEach((pos) => {
|
}, true)
|
||||||
if (pos >= 0 && pos < textContent.length) {
|
) {
|
||||||
tmpPos.push(textContent[pos].posKey);
|
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(tmpPos);
|
||||||
setPosHighlight([...new Set(tmpPos)]);
|
setPosHighlightFocus(tmpPosHighlight);
|
||||||
setPosHighlightFocus([...new Set(tmpPosHighlight)]);
|
|
||||||
}, [highlight, focusHighlight, pageNumber, textContent]);
|
}, [highlight, focusHighlight, pageNumber, textContent]);
|
||||||
|
|
||||||
const onGetTextSuccess: OnGetTextSuccess = useCallback((fullText) => {
|
const onGetTextSuccess: OnGetTextSuccess = useCallback((fullText) => {
|
||||||
|
|
@ -188,10 +154,6 @@ export default function PDFViewer({
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const contentWidth = baseWidth ? baseWidth * 0.98 * zoomLevel : 0;
|
|
||||||
const containerWidth = baseWidth ? baseWidth : 0;
|
|
||||||
const willOverflow = contentWidth > containerWidth;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
|
|
@ -199,23 +161,16 @@ export default function PDFViewer({
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
width="100%"
|
width="100%"
|
||||||
maxWidth="850px"
|
height="auto"
|
||||||
margin="0 auto"
|
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
width="100%"
|
|
||||||
height="500px"
|
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "transparent",
|
width: "100%",
|
||||||
border: "none",
|
height: "auto",
|
||||||
borderRadius: 0,
|
display: "flex",
|
||||||
boxShadow: "none",
|
justifyContent: "center",
|
||||||
overflow: "auto",
|
alignItems: "center",
|
||||||
display: willOverflow ? "block" : "flex",
|
|
||||||
justifyContent: willOverflow ? "flex-start" : "center",
|
|
||||||
alignItems: willOverflow ? "flex-start" : "center",
|
|
||||||
padding: willOverflow ? `${Math.max(0, (500 - (contentWidth * (500 / containerWidth))) / 2)}px ${Math.max(0, (containerWidth - contentWidth) / 2)}px` : 0,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Document
|
<Document
|
||||||
|
|
@ -227,10 +182,10 @@ export default function PDFViewer({
|
||||||
}
|
}
|
||||||
onSourceError={(error) => console.error("Ungültige PDF:", error)}
|
onSourceError={(error) => console.error("Ungültige PDF:", error)}
|
||||||
>
|
>
|
||||||
{baseWidth && (
|
{containerWidth && (
|
||||||
<Page
|
<Page
|
||||||
pageNumber={pageNumber}
|
pageNumber={pageNumber}
|
||||||
width={baseWidth * 0.98 * zoomLevel}
|
width={containerWidth * 0.98}
|
||||||
customTextRenderer={textRenderer}
|
customTextRenderer={textRenderer}
|
||||||
onGetTextSuccess={onGetTextSuccess}
|
onGetTextSuccess={onGetTextSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
@ -245,13 +200,6 @@ export default function PDFViewer({
|
||||||
gap={1}
|
gap={1}
|
||||||
p={1}
|
p={1}
|
||||||
>
|
>
|
||||||
<IconButton
|
|
||||||
disabled={zoomLevel <= 0.3}
|
|
||||||
onClick={() => setZoomLevel((z) => Math.max(0.3, z - 0.1))}
|
|
||||||
title="Verkleinern"
|
|
||||||
>
|
|
||||||
<ZoomOutIcon fontSize="large" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={pageNumber <= 1}
|
disabled={pageNumber <= 1}
|
||||||
onClick={() => handlePageChange(pageNumber - 1)}
|
onClick={() => handlePageChange(pageNumber - 1)}
|
||||||
|
|
@ -267,12 +215,6 @@ export default function PDFViewer({
|
||||||
>
|
>
|
||||||
<ArrowCircleRightIcon fontSize="large" />
|
<ArrowCircleRightIcon fontSize="large" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
|
||||||
onClick={() => setZoomLevel((z) => Math.min(3, z + 0.1))}
|
|
||||||
title="Vergrößern"
|
|
||||||
>
|
|
||||||
<ZoomInIcon fontSize="large" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import "react-pdf/dist/Page/TextLayer.css";
|
import "react-pdf/dist/Page/TextLayer.css";
|
||||||
|
|
@ -10,9 +10,10 @@ import "@fontsource/roboto/400.css";
|
||||||
import "@fontsource/roboto/500.css";
|
import "@fontsource/roboto/500.css";
|
||||||
import "@fontsource/roboto/700.css";
|
import "@fontsource/roboto/700.css";
|
||||||
|
|
||||||
import { pdfjs } from "react-pdf";
|
|
||||||
import * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx";
|
import * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx";
|
||||||
|
|
||||||
|
import { pdfjs } from "react-pdf";
|
||||||
|
|
||||||
// Import the generated route tree
|
// Import the generated route tree
|
||||||
import { routeTree } from "./routeTree.gen";
|
import { routeTree } from "./routeTree.gen";
|
||||||
|
|
||||||
|
|
@ -26,7 +27,6 @@ const router = createRouter({
|
||||||
scrollRestoration: true,
|
scrollRestoration: true,
|
||||||
defaultStructuralSharing: true,
|
defaultStructuralSharing: true,
|
||||||
defaultPreloadStaleTime: 0,
|
defaultPreloadStaleTime: 0,
|
||||||
basepath: "/ff",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register the router instance for type safety
|
// Register the router instance for type safety
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { KPIForm } from "../components/KPIForm";
|
||||||
import type { Kennzahl } from "../types/kpi";
|
import type { Kennzahl } from "../types/kpi";
|
||||||
import { API_HOST } from "../util/api";
|
import { API_HOST } from "../util/api";
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/config-add")({
|
export const Route = createFileRoute("/config-add")({
|
||||||
component: ConfigAddPage,
|
component: ConfigAddPage,
|
||||||
validateSearch: (search: Record<string, unknown>): { from?: string } => {
|
validateSearch: (search: Record<string, unknown>): { from?: string } => {
|
||||||
|
|
@ -48,28 +47,19 @@ function ConfigAddPage() {
|
||||||
body: JSON.stringify(kpiData),
|
body: JSON.stringify(kpiData),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate({
|
navigate({ to: "/config" });
|
||||||
to: "/config",
|
|
||||||
search: { success: "true", ...(from ? { from } : {}) },
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating KPI:', error);
|
console.error('Error creating KPI:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
navigate({
|
navigate({ to: "/config" });
|
||||||
to: "/config",
|
|
||||||
search: from ? { from } : undefined,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -93,7 +83,7 @@ function ConfigAddPage() {
|
||||||
>
|
>
|
||||||
<Box display="flex" alignItems="center">
|
<Box display="flex" alignItems="center">
|
||||||
<IconButton onClick={handleBack}>
|
<IconButton onClick={handleBack}>
|
||||||
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
|
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h5" fontWeight="bold" ml={3}>
|
<Typography variant="h5" fontWeight="bold" ml={3}>
|
||||||
Neue Kennzahl hinzufügen
|
Neue Kennzahl hinzufügen
|
||||||
|
|
@ -103,10 +93,9 @@ function ConfigAddPage() {
|
||||||
|
|
||||||
<KPIForm
|
<KPIForm
|
||||||
mode="add"
|
mode="add"
|
||||||
key={Date.now()}
|
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import {
|
import { Box, Typography, IconButton, Button, CircularProgress, Paper, Divider
|
||||||
Box, Typography, IconButton, Button, CircularProgress, Paper, Divider
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
@ -39,7 +38,6 @@ function KPIDetailPage() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(`${API_HOST}/api/kpi_setting/${kpiId}`);
|
const response = await fetch(`${API_HOST}/api/kpi_setting/${kpiId}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
setError('KPI not found');
|
setError('KPI not found');
|
||||||
|
|
@ -74,6 +72,7 @@ function KPIDetailPage() {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedKennzahl = await response.json();
|
const updatedKennzahl = await response.json();
|
||||||
setKennzahl(updatedKennzahl);
|
setKennzahl(updatedKennzahl);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
|
|
@ -83,7 +82,6 @@ function KPIDetailPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
};
|
};
|
||||||
|
|
@ -155,7 +153,7 @@ function KPIDetailPage() {
|
||||||
>
|
>
|
||||||
<Box display="flex" alignItems="center">
|
<Box display="flex" alignItems="center">
|
||||||
<IconButton onClick={handleBack}>
|
<IconButton onClick={handleBack}>
|
||||||
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
|
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h5" fontWeight="bold" ml={3}>
|
<Typography variant="h5" fontWeight="bold" ml={3}>
|
||||||
Detailansicht
|
Detailansicht
|
||||||
|
|
@ -194,13 +192,18 @@ function KPIDetailPage() {
|
||||||
<Divider sx={{ my: 3 }} />
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
||||||
<Box mb={4}>
|
<Box mb={4}>
|
||||||
<Typography variant="h6" fontWeight="bold" mb={1}>
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||||
Erforderlich:
|
Beschreibung
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" sx={{ mb: 2, fontSize: 16 }}>
|
<Typography variant="body1" color="text.secondary">
|
||||||
{kennzahl.mandatory ? 'Ja' : 'Nein'}
|
{kennzahl.description || "Zurzeit ist die Beschreibung der Kennzahl leer. Klicken Sie auf den Bearbeiten-Button, um die Beschreibung zu ergänzen."}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
<Box mt={2}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
<strong>Erforderlich:</strong> {kennzahl.mandatory ? 'Ja' : 'Nein'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider sx={{ my: 3 }} />
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
@ -213,6 +216,28 @@ function KPIDetailPage() {
|
||||||
{typeDisplayMapping[kennzahl.type] || kennzahl.type}
|
{typeDisplayMapping[kennzahl.type] || kennzahl.type}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
||||||
|
<Box mb={4}>
|
||||||
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||||
|
Synonyme & Übersetzungen
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
{kennzahl.translation || "Zurzeit gibt es keine Einträge für Synonyme und Übersetzungen der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
||||||
|
<Box mb={4}>
|
||||||
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
||||||
|
Beispiele von Kennzahl
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
{kennzahl.example || "Zurzeit gibt es keine Beispiele der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
@ -239,7 +264,7 @@ function KPIDetailPage() {
|
||||||
>
|
>
|
||||||
<Box display="flex" alignItems="center">
|
<Box display="flex" alignItems="center">
|
||||||
<IconButton onClick={handleBack}>
|
<IconButton onClick={handleBack}>
|
||||||
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
|
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h5" fontWeight="bold" ml={3}>
|
<Typography variant="h5" fontWeight="bold" ml={3}>
|
||||||
Kennzahl bearbeiten
|
Kennzahl bearbeiten
|
||||||
|
|
@ -255,4 +280,4 @@ function KPIDetailPage() {
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -3,87 +3,18 @@ import { Box, Button, IconButton, Typography } from "@mui/material";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { ConfigTable } from "../components/ConfigTable";
|
import { ConfigTable } from "../components/ConfigTable";
|
||||||
import { API_HOST } from "../util/api";
|
|
||||||
import Snackbar from "@mui/material/Snackbar";
|
|
||||||
import MuiAlert from "@mui/material/Alert";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/config")({
|
export const Route = createFileRoute("/config")({
|
||||||
component: ConfigPage,
|
component: ConfigPage,
|
||||||
validateSearch: (search: Record<string, unknown>): { from?: string; success?: string } => {
|
validateSearch: (search: Record<string, unknown>): { from?: string } => {
|
||||||
return {
|
const from = typeof search.from === "string" ? search.from : undefined;
|
||||||
from: typeof search.from === "string" ? search.from : undefined,
|
return { from };
|
||||||
success: typeof search.success === "string" ? search.success : undefined
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function ConfigPage() {
|
function ConfigPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { from, success } = Route.useSearch();
|
const { from } = Route.useSearch();
|
||||||
const [snackbarOpen, setSnackbarOpen] = useState(success === "true");
|
|
||||||
const [snackbarMessage, setSnackbarMessage] = useState<string>("Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder zuerst weitere Kennzahlen hinzufügen.");
|
|
||||||
const [trainingRunning, setTrainingRunning] = useState(false);
|
|
||||||
const [hasUntrainedKPIs, setHasUntrainedKPIs] = useState(false);
|
|
||||||
|
|
||||||
|
|
||||||
const fetchKPISettings = async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_HOST}/api/kpi/settings`);
|
|
||||||
const data = await res.json();
|
|
||||||
const untrainedExists = data.some((kpi: any) => {
|
|
||||||
return kpi.is_trained === false;
|
|
||||||
});
|
|
||||||
setHasUntrainedKPIs(untrainedExists);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Fehler beim Laden der KPIs:", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchKPISettings();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (success === "true") {
|
|
||||||
setTimeout(() => {
|
|
||||||
navigate({
|
|
||||||
to: "/config",
|
|
||||||
search: from ? { from } : undefined,
|
|
||||||
replace: true
|
|
||||||
});
|
|
||||||
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, [success]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkInitialTrainingStatus = async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_HOST}/api/spacy/train-status`);
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.running) {
|
|
||||||
setTrainingRunning(true);
|
|
||||||
pollTrainingStatus();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Initiale Trainingsstatus-Abfrage fehlgeschlagen", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkInitialTrainingStatus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
const handleAddNewKPI = () => {
|
const handleAddNewKPI = () => {
|
||||||
navigate({
|
navigate({
|
||||||
|
|
@ -100,160 +31,46 @@ function ConfigPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTriggerTraining = async () => {
|
|
||||||
setTrainingRunning(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_HOST}/api/spacy/train`, {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error("Training konnte nicht gestartet werden");
|
|
||||||
|
|
||||||
// Erfolgsmeldung erst hier anzeigen
|
|
||||||
setSnackbarMessage("Training wurde gestartet.");
|
|
||||||
setSnackbarOpen(true);
|
|
||||||
|
|
||||||
pollTrainingStatus(); // jetzt starten
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
setSnackbarMessage("Fehler beim Starten des Trainings.");
|
|
||||||
setSnackbarOpen(true);
|
|
||||||
setTrainingRunning(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const pollTrainingStatus = () => {
|
|
||||||
const interval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_HOST}/api/spacy/train-status`);
|
|
||||||
const data = await res.json();
|
|
||||||
console.log("Trainingsstatus:", data); //Debug-Ausgabe
|
|
||||||
if (!data.running) {
|
|
||||||
clearInterval(interval);
|
|
||||||
console.log("Training abgeschlossen – Snackbar wird ausgelöst");
|
|
||||||
setSnackbarMessage("Training abgeschlossen!");
|
|
||||||
setSnackbarOpen(true);
|
|
||||||
setTrainingRunning(false);
|
|
||||||
|
|
||||||
fetchKPISettings();
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Polling-Fehler:", err);
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box
|
||||||
|
minHeight="100vh"
|
||||||
|
width="100vw"
|
||||||
|
bgcolor="white"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
pt={3}
|
||||||
|
pb={4}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
minHeight="100vh"
|
width="100%"
|
||||||
width="100vw"
|
|
||||||
bgcolor="white"
|
|
||||||
display="flex"
|
display="flex"
|
||||||
flexDirection="column"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
pt={3}
|
px={4}
|
||||||
pb={4}
|
|
||||||
>
|
>
|
||||||
<Box
|
<Box display="flex" alignItems="center">
|
||||||
width="100%"
|
<IconButton onClick={handleBack}>
|
||||||
display="flex"
|
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
|
||||||
justifyContent="space-between"
|
</IconButton>
|
||||||
alignItems="center"
|
<Typography variant="h5" fontWeight="bold" ml={3}>
|
||||||
px={4}
|
Konfiguration der Kennzahlen
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleAddNewKPI}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#383838",
|
||||||
|
"&:hover": { backgroundColor: "#2e2e2e" },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Linke Seite: Zurück & Titel */}
|
Neue Kennzahl hinzufügen
|
||||||
<Box display="flex" alignItems="center">
|
</Button>
|
||||||
<IconButton onClick={handleBack}>
|
|
||||||
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
|
|
||||||
</IconButton>
|
|
||||||
<Typography variant="h5" fontWeight="bold" ml={3}>
|
|
||||||
Konfiguration der Kennzahlen
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box display="flex" flexDirection="column" alignItems="flex-end" gap={1}>
|
|
||||||
<Box display="flex" gap={2}>
|
|
||||||
{trainingRunning || !hasUntrainedKPIs ? (
|
|
||||||
<Tooltip title="Alle Kennzahlen sind bereits trainiert.">
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleTriggerTraining}
|
|
||||||
disabled
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#383838",
|
|
||||||
"&:hover": { backgroundColor: "#2e2e2e" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{trainingRunning ? (
|
|
||||||
<>
|
|
||||||
<CircularProgress size={20} sx={{ color: "white", mr: 1 }} />
|
|
||||||
Wird trainiert...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Neu trainieren"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleTriggerTraining}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#383838",
|
|
||||||
"&:hover": { backgroundColor: "#2e2e2e" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Neu trainieren
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleAddNewKPI}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#383838",
|
|
||||||
"&:hover": { backgroundColor: "#2e2e2e" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Neue Kennzahl hinzufügen
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Tabelle */}
|
|
||||||
<Box sx={{ width: "100%", mt: 4, display: "flex", justifyContent: "center" }}>
|
|
||||||
<ConfigTable from={from} trainingRunning={trainingRunning} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box sx={{ width: "100%", mt: 4, display: "flex", justifyContent: "center" }}>
|
||||||
{/* Snackbar */}
|
<ConfigTable from={from} />
|
||||||
<Snackbar
|
</Box>
|
||||||
open={snackbarOpen}
|
</Box>
|
||||||
autoHideDuration={4000}
|
|
||||||
onClose={() => setSnackbarOpen(false)}
|
|
||||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
|
||||||
>
|
|
||||||
<MuiAlert
|
|
||||||
elevation={6}
|
|
||||||
variant="filled"
|
|
||||||
onClose={() => setSnackbarOpen(false)}
|
|
||||||
severity="success"
|
|
||||||
sx={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{snackbarMessage}
|
|
||||||
</MuiAlert>
|
|
||||||
</Snackbar>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import ContentPasteIcon from "@mui/icons-material/ContentPaste";
|
import ContentPasteIcon from "@mui/icons-material/ContentPaste";
|
||||||
import { Box, Button, Paper, Typography, Snackbar, Alert, IconButton, Tooltip } from "@mui/material";
|
import { Box, Button, Paper, Typography, Snackbar, Alert, IconButton } from "@mui/material";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
|
|
@ -50,8 +50,6 @@ function ExtractedResultsPage() {
|
||||||
const { data: kpi } = useSuspenseQuery(kpiQueryOptions(pitchBook));
|
const { data: kpi } = useSuspenseQuery(kpiQueryOptions(pitchBook));
|
||||||
const { data: settings } = useSuspenseQuery(settingsQueryOptions());
|
const { data: settings } = useSuspenseQuery(settingsQueryOptions());
|
||||||
|
|
||||||
const fundName = kpi["FONDSNAME"]?.[0]?.entity;
|
|
||||||
|
|
||||||
const status = useMemo(() => {
|
const status = useMemo(() => {
|
||||||
let hasRedBorders = false;
|
let hasRedBorders = false;
|
||||||
let hasYellowBorders = false;
|
let hasYellowBorders = false;
|
||||||
|
|
@ -160,9 +158,7 @@ function ExtractedResultsPage() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography variant="h5" gutterBottom>
|
<Typography variant="h5" gutterBottom>
|
||||||
<strong>
|
<strong>Extrahierte Kennzahlen</strong>
|
||||||
{fundName ? `Kennzahlen extrahiert aus: ${fundName}` : "Extrahierte Kennzahlen"}
|
|
||||||
</strong>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -239,29 +235,17 @@ function ExtractedResultsPage() {
|
||||||
gap={2}
|
gap={2}
|
||||||
sx={{ flexShrink: 0 }}
|
sx={{ flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Button variant="contained" sx={{ backgroundColor: "#383838" }}
|
||||||
title={
|
onClick={handleCopyToClipboard}>
|
||||||
<>
|
<ContentPasteIcon sx={{ fontSize: 18, mr: 1 }} />
|
||||||
<b>Kennzahlen kopieren</b>
|
{copied ? "Kopiert!" : "Kennzahlenzeile kopieren"}
|
||||||
<br />
|
</Button>
|
||||||
Kopiert alle aktiven Kennzahlen als Excel-Zeile in die Zwischenablage. Kann direkt in Excel eingefügt werden.
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
placement="top"
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<Button variant="contained" sx={{ backgroundColor: "#383838" }}
|
|
||||||
onClick={handleCopyToClipboard}>
|
|
||||||
<ContentPasteIcon sx={{ fontSize: 18, mr: 1 }} />
|
|
||||||
{copied ? "Kopiert!" : "Kennzahlenzeile kopieren"}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ backgroundColor: "#383838" }}
|
sx={{ backgroundColor: "#383838" }}
|
||||||
onClick={() => navigate({ to: "/" })}
|
onClick={() => navigate({ to: "/" })}
|
||||||
>
|
>
|
||||||
Neues Pitchbook hochladen
|
Neu hochladen
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ import {
|
||||||
useSuspenseQuery,
|
useSuspenseQuery,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import { useEffect, useState, type KeyboardEvent } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import type { KeyboardEvent } from "react";
|
||||||
import PDFViewer from "../components/pdfViewer";
|
import PDFViewer from "../components/pdfViewer";
|
||||||
import { fetchPutKPI } from "../util/api";
|
import { fetchPutKPI } from "../util/api";
|
||||||
import { kpiQueryOptions } from "../util/query";
|
import { kpiQueryOptions } from "../util/query";
|
||||||
|
|
@ -68,45 +69,15 @@ function ExtractedResultsPage() {
|
||||||
const [customValue, setCustomValue] = useState("");
|
const [customValue, setCustomValue] = useState("");
|
||||||
const [customPage, setCustomPage] = useState("");
|
const [customPage, setCustomPage] = useState("");
|
||||||
const [editingCustomPage, setEditingCustomPage] = useState(false);
|
const [editingCustomPage, setEditingCustomPage] = useState(false);
|
||||||
const [focusHighlightOverride, setFocusHighlightOverride] = useState<{ page: number; text: string } | null>(null);
|
|
||||||
|
|
||||||
const originalValue = kpiValues[0]?.entity || "";
|
const originalValue = kpiValues[0]?.entity || "";
|
||||||
const originalPage = kpiValues[0]?.page || 0;
|
const originalPage = kpiValues[0]?.page || 0;
|
||||||
|
const selectedValue =
|
||||||
// Funktion, um gleiche Werte zusammenzufassen und die Seiten zu sammeln
|
selectedIndex === -1 ? customValue : kpiValues[selectedIndex]?.entity || "";
|
||||||
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 =
|
const selectedPage =
|
||||||
selectedIndex === -1
|
selectedIndex === -1
|
||||||
? (parseInt(customPage) > 0 ? parseInt(customPage) : 1)
|
? (parseInt(customPage) > 0 ? parseInt(customPage) : 1)
|
||||||
: groupedKpiValues[selectedIndex]?.pages[0] || 1;
|
: kpiValues[selectedIndex]?.page || 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 || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const valueChanged = selectedValue !== originalValue;
|
const valueChanged = selectedValue !== originalValue;
|
||||||
|
|
@ -119,15 +90,7 @@ function ExtractedResultsPage() {
|
||||||
const updatedData = { ...kpiData };
|
const updatedData = { ...kpiData };
|
||||||
let baseObject;
|
let baseObject;
|
||||||
if (selectedIndex >= 0) {
|
if (selectedIndex >= 0) {
|
||||||
// Das Originalobjekt mit allen Feldern für diesen Wert suchen
|
baseObject = kpiValues[selectedIndex];
|
||||||
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",
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
baseObject = {
|
baseObject = {
|
||||||
label: kpi.toUpperCase(),
|
label: kpi.toUpperCase(),
|
||||||
|
|
@ -165,14 +128,12 @@ function ExtractedResultsPage() {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
if (value === "custom") {
|
if (value === "custom") {
|
||||||
setSelectedIndex(-1);
|
setSelectedIndex(-1);
|
||||||
setFocusHighlightOverride(null);
|
|
||||||
} else {
|
} else {
|
||||||
const index = Number.parseInt(value);
|
const index = Number.parseInt(value);
|
||||||
setSelectedIndex(index);
|
setSelectedIndex(index);
|
||||||
setCurrentPage(groupedKpiValues[index].pages[0]);
|
setCurrentPage(kpiValues[index].page);
|
||||||
setCustomValue("");
|
setCustomValue("");
|
||||||
setCustomPage("");
|
setCustomPage("");
|
||||||
setFocusHighlightOverride(null);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -182,8 +143,7 @@ function ExtractedResultsPage() {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
setCustomValue(value);
|
setCustomValue(value);
|
||||||
setSelectedIndex(-1);
|
setSelectedIndex(-1);
|
||||||
setFocusHighlightOverride(null);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const handleCustomPageChange = (
|
const handleCustomPageChange = (
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
|
@ -196,19 +156,10 @@ function ExtractedResultsPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRowClick = (index: number) => {
|
const handleRowClick = (index: number) => {
|
||||||
setCurrentPage(groupedKpiValues[index].pages[0]);
|
setCurrentPage(kpiValues[index].page);
|
||||||
setSelectedIndex(index);
|
setSelectedIndex(index);
|
||||||
setCustomValue("");
|
setCustomValue("");
|
||||||
setCustomPage("");
|
setCustomPage("");
|
||||||
setFocusHighlightOverride(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageClick = (page: number, entity: string) => {
|
|
||||||
setCurrentPage(page);
|
|
||||||
setFocusHighlightOverride({
|
|
||||||
page: page,
|
|
||||||
text: entity,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const handleBackClick = () => {
|
||||||
|
|
@ -244,8 +195,10 @@ function ExtractedResultsPage() {
|
||||||
setEditingCustomPage(true);
|
setEditingCustomPage(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCustomPageKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
|
const handleCustomPageKeyPress = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (e.key === "Enter" || e.key === "Escape") {
|
if (e.key === "Enter") {
|
||||||
|
setEditingCustomPage(false);
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
setEditingCustomPage(false);
|
setEditingCustomPage(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -256,8 +209,8 @@ function ExtractedResultsPage() {
|
||||||
<IconButton onClick={handleBackClick} sx={{ mr: 2 }}>
|
<IconButton onClick={handleBackClick} sx={{ mr: 2 }}>
|
||||||
<ArrowBackIcon fontSize="large" sx={{ color: "#383838" }} />
|
<ArrowBackIcon fontSize="large" sx={{ color: "#383838" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h5">
|
<Typography variant="h5" fontWeight="bold">
|
||||||
Überprüfung der Kennzahl: <b><u>{kpi}</u></b>
|
Überprüfung der Kennzahl: {kpi}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
@ -291,14 +244,14 @@ function ExtractedResultsPage() {
|
||||||
<strong>Gefundene Werte</strong>
|
<strong>Gefundene Werte</strong>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center" width="15%">
|
<TableCell align="center" width="15%">
|
||||||
<strong>Seiten</strong>
|
<strong>Seite</strong>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{groupedKpiValues.map((item, index) => (
|
{kpiValues.map((item, index) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={`${item.entity}_${item.pages.join('_')}_${index}`}
|
key={`${item.entity}_${item.page}_${index}`}
|
||||||
sx={{
|
sx={{
|
||||||
"&:hover": { backgroundColor: "#f9f9f9" },
|
"&:hover": { backgroundColor: "#f9f9f9" },
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
|
|
@ -337,19 +290,16 @@ function ExtractedResultsPage() {
|
||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
{item.pages.map((page: number, i: number) => (
|
<Link
|
||||||
<Link
|
component="button"
|
||||||
key={page}
|
onClick={(e: React.MouseEvent) => {
|
||||||
component="button"
|
e.stopPropagation();
|
||||||
onClick={(e: React.MouseEvent) => {
|
setCurrentPage(item.page);
|
||||||
e.stopPropagation();
|
}}
|
||||||
handlePageClick(page, item.entity);
|
sx={{ cursor: "pointer" }}
|
||||||
}}
|
>
|
||||||
sx={{ cursor: "pointer", ml: i > 0 ? 1 : 0 }}
|
{item.page}
|
||||||
>
|
</Link>
|
||||||
{page}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
@ -369,7 +319,6 @@ function ExtractedResultsPage() {
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedIndex(-1);
|
setSelectedIndex(-1);
|
||||||
setFocusHighlightOverride(null);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Radio
|
<Radio
|
||||||
|
|
@ -386,28 +335,24 @@ function ExtractedResultsPage() {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ width: '100%' }}>
|
<TextField
|
||||||
<TextField
|
placeholder="Einen abweichenden Wert eingeben..."
|
||||||
placeholder="Einen abweichenden Wert eingeben..."
|
value={customValue}
|
||||||
value={customValue}
|
onChange={handleCustomValueChange}
|
||||||
onChange={handleCustomValueChange}
|
variant="standard"
|
||||||
variant="standard"
|
fullWidth
|
||||||
fullWidth
|
InputProps={{
|
||||||
InputProps={{
|
disableUnderline: true,
|
||||||
disableUnderline: true,
|
}}
|
||||||
}}
|
sx={{
|
||||||
sx={{
|
"& .MuiInput-input": {
|
||||||
"& .MuiInput-input": {
|
padding: 0,
|
||||||
padding: 0,
|
},
|
||||||
},
|
}}
|
||||||
}}
|
onClick={(e: React.MouseEvent) => {
|
||||||
onClick={(e: React.MouseEvent) => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
}}
|
||||||
}}
|
/>
|
||||||
error={selectedIndex === -1 && customValue !== "" && customValue.trim() === ""}
|
|
||||||
helperText={selectedIndex === -1 && customValue !== "" && customValue.trim() === "" ? "Der Wert, der angegeben wurde, ist leer." : ""}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
|
|
@ -452,7 +397,7 @@ function ExtractedResultsPage() {
|
||||||
<EditIcon
|
<EditIcon
|
||||||
fontSize="small"
|
fontSize="small"
|
||||||
sx={{
|
sx={{
|
||||||
color: "black",
|
color: "#666",
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
transition: "opacity 0.2s ease",
|
transition: "opacity 0.2s ease",
|
||||||
ml: 1
|
ml: 1
|
||||||
|
|
@ -495,17 +440,20 @@ function ExtractedResultsPage() {
|
||||||
pitchBookId={pitchBook}
|
pitchBookId={pitchBook}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
onPageChange={setCurrentPage}
|
onPageChange={setCurrentPage}
|
||||||
highlight={groupedKpiValues
|
highlight={Object.values(kpiValues)
|
||||||
.map((k) => k.pages.map((page: number) => ({ page, text: k.entity })))
|
.flat()
|
||||||
.reduce((acc, val) => acc.concat(val), [])}
|
.map((k) => ({ page: k.page, text: k.entity }))}
|
||||||
focusHighlight={focusHighlight}
|
focusHighlight={{
|
||||||
|
page: kpiValues.at(selectedIndex)?.page || -1,
|
||||||
|
text: kpiValues.at(selectedIndex)?.entity || "",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Box mt={2} display="flex" justifyContent="flex-end" gap={2}>
|
<Box mt={2} display="flex" justifyContent="flex-end" gap={2}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleAcceptReview}
|
onClick={handleAcceptReview}
|
||||||
disabled={isSelectedValueEmpty}
|
disabled={!selectedValue}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "#383838",
|
backgroundColor: "#383838",
|
||||||
"&:hover": { backgroundColor: "#2e2e2e" },
|
"&:hover": { backgroundColor: "#2e2e2e" },
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,4 @@ import { API_HOST } from "./util/api";
|
||||||
// "undefined" means the URL will be computed from the `window.location` object
|
// "undefined" means the URL will be computed from the `window.location` object
|
||||||
// const URL = process.env.NODE_ENV === 'production' ? undefined : 'http://localhost:4000';
|
// const URL = process.env.NODE_ENV === 'production' ? undefined : 'http://localhost:4000';
|
||||||
|
|
||||||
const url = new URL(API_HOST);
|
export const socket = io(`${API_HOST}`);
|
||||||
export const socket = io(`${url.host}`, {
|
|
||||||
path: `${url.pathname.replace(/^\/+/, "")}/socket.io`,
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
export interface Kennzahl {
|
export interface Kennzahl {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
description: string;
|
||||||
mandatory: boolean;
|
mandatory: boolean;
|
||||||
type: string;
|
type: string;
|
||||||
|
translation: string;
|
||||||
|
example: string;
|
||||||
position: number;
|
position: number;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
exampleText?: string;
|
|
||||||
markedValue?: string;
|
|
||||||
examples?: {
|
|
||||||
sentence: string;
|
|
||||||
value: string;
|
|
||||||
}[];
|
|
||||||
is_trained?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const typeDisplayMapping: Record<string, string> = {
|
export const typeDisplayMapping: Record<string, string> = {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Kennzahl } from "@/types/kpi";
|
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";
|
||||||
|
const API_HOST = "";
|
||||||
|
|
||||||
export { API_HOST };
|
export { API_HOST };
|
||||||
|
|
||||||
|
|
@ -122,3 +123,21 @@ export async function fetchPitchBooksById(id: number) {
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const uploadPitchBook = async (files: File[]): Promise<number> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", files[0]);
|
||||||
|
const response = await fetch(`${API_HOST}/api/pitch_book/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log("File uploaded successfully");
|
||||||
|
const data = await response.json();
|
||||||
|
return data.id.toString();
|
||||||
|
} else {
|
||||||
|
console.error("Failed to upload file");
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
export const formatDate = (dateString: string): string => {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
|
|
||||||
const hours = String(date.getHours() + 2).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}`;
|
|
||||||
};
|
|
||||||
|
|
@ -5,9 +5,6 @@ import { defineConfig } from "vite";
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [TanStackRouterVite({ autoCodeSplitting: true }), viteReact()],
|
plugins: [TanStackRouterVite({ autoCodeSplitting: true }), viteReact()],
|
||||||
build: {
|
|
||||||
chunkSizeWarningLimit: 1000, // default ist 500
|
|
||||||
},
|
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue