Formatierung durch black, extract-Funktion bereinigt

pull/94/head
Abdulrahman Dabbagh 2025-06-27 11:41:57 +02:00
parent 77d169633e
commit fd8bfa3952
21 changed files with 923 additions and 516 deletions

View File

@ -5,8 +5,6 @@ from dotenv import load_dotenv
from controller import register_routes 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
from controller.kennzahlen import kennzahlen_bp
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
@ -23,15 +21,11 @@ init_db(app)
register_routes(app) register_routes(app)
# Register blueprints
app.register_blueprint(kennzahlen_bp)
@app.route("/health") @app.route("/health")
def health_check(): def health_check():
return "OK" return "OK"
# 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)

View File

@ -6,10 +6,36 @@ from werkzeug.utils import secure_filename
from model.database import db from model.database import db
import os import os
import json import json
import requests
spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy") spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy")
SPACY_TRAINING_URL = os.getenv("SPACY_TRAINING_URL", "http://spacy:5052/train")
training_running_flag_path = os.path.join("spacy_training", "training_running.json")
@spacy_controller.route("/train", methods=["POST"])
def trigger_training():
try:
with open(training_running_flag_path, "w") as f:
json.dump({"running": True}, f)
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"]) @spacy_controller.route("/", methods=["GET"])
def get_all_files(): def get_all_files():
@ -33,7 +59,6 @@ def download_file(id):
@spacy_controller.route("/", methods=["POST"]) @spacy_controller.route("/", methods=["POST"])
def upload_file(): def upload_file():
print(request)
if "file" not in request.files: if "file" not in request.files:
return jsonify({"error": "No file part in the request"}), 400 return jsonify({"error": "No file part in the request"}), 400
@ -41,17 +66,13 @@ def upload_file():
if uploaded_file.filename == "": if uploaded_file.filename == "":
return jsonify({"error": "No selected file"}), 400 return jsonify({"error": "No selected file"}), 400
# Read file data once
file_data = uploaded_file.read() file_data = uploaded_file.read()
try: try:
if uploaded_file: fileName = uploaded_file.filename or ""
fileName = uploaded_file.filename or "" new_file = SpacyModel(filename=secure_filename(fileName), file=file_data)
new_file = SpacyModel(filename=secure_filename(fileName), file=file_data) db.session.add(new_file)
db.session.commit()
db.session.add(new_file) return jsonify(new_file.to_dict()), 201
db.session.commit()
return jsonify(new_file.to_dict()), 201
except Exception as e: except Exception as e:
print(e) print(e)
return jsonify({"error": "Invalid file format. Only PDF files are accepted"}), 400 return jsonify({"error": "Invalid file format. Only PDF files are accepted"}), 400
@ -65,14 +86,9 @@ def update_file(id):
uploaded_file = request.files["file"] uploaded_file = request.files["file"]
if uploaded_file.filename != "": if uploaded_file.filename != "":
file.filename = uploaded_file.filename file.filename = uploaded_file.filename
# Read file data once
file_data = uploaded_file.read() file_data = uploaded_file.read()
try: try:
if ( if puremagic.from_string(file_data, mime=True) == "application/pdf":
uploaded_file
and puremagic.from_string(file_data, mime=True) == "application/pdf"
):
file.file = file_data file.file = file_data
except Exception as e: except Exception as e:
print(e) print(e)
@ -81,7 +97,6 @@ def update_file(id):
file.kpi = request.form.get("kpi") file.kpi = request.form.get("kpi")
db.session.commit() db.session.commit()
return jsonify(file.to_dict()), 200 return jsonify(file.to_dict()), 200
@ -90,7 +105,6 @@ def delete_file(id):
file = SpacyModel.query.get_or_404(id) file = SpacyModel.query.get_or_404(id)
db.session.delete(file) db.session.delete(file)
db.session.commit() db.session.commit()
return jsonify({"message": f"File {id} deleted successfully"}), 200 return jsonify({"message": f"File {id} deleted successfully"}), 200
@ -110,7 +124,6 @@ def append_training_entry():
try: try:
os.makedirs(os.path.dirname(path), exist_ok=True) os.makedirs(os.path.dirname(path), exist_ok=True)
if os.path.exists(path): if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
@ -128,3 +141,16 @@ def append_training_entry():
except Exception as e: except Exception as e:
print(f"[ERROR] Fehler beim Schreiben: {e}") print(f"[ERROR] Fehler beim Schreiben: {e}")
return jsonify({"error": "Interner Fehler beim Schreiben."}), 500 return jsonify({"error": "Interner Fehler beim Schreiben."}), 500
@spacy_controller.route("/train-status", methods=["GET"])
def training_status():
try:
if os.path.exists(training_running_flag_path):
with open(training_running_flag_path, "r") as f:
status = json.load(f)
return jsonify(status), 200
else:
return jsonify({"running": False}), 200
except Exception as e:
return jsonify({"error": "Fehler beim Statuscheck", "details": str(e)}), 500

View File

@ -0,0 +1,8 @@
FROM python:3.11-slim
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
ENV PYTHONUNBUFFERED=1
CMD ["python", "extractExxeta.py"]

View File

@ -11,6 +11,8 @@ 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

View File

@ -1,9 +1,14 @@
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
from extractSpacy import extract from extractSpacy import extract, load_model
import requests import requests
import os import os
import json import json
from flask_cors import CORS from flask_cors import CORS
import shutil
import subprocess
training_status = {"running": False}
app = Flask(__name__) app = Flask(__name__)
@ -66,7 +71,7 @@ def append_training_entry():
else: else:
data = [] data = []
# Optional: Duplikate prüfen # Duplikate prüfen
if entry in data: if entry in data:
return jsonify({"message": "Eintrag existiert bereits."}), 200 return jsonify({"message": "Eintrag existiert bereits."}), 200
@ -80,5 +85,59 @@ def append_training_entry():
return jsonify({"error": "Interner Fehler beim Schreiben."}), 500 return jsonify({"error": "Interner Fehler beim Schreiben."}), 500
@app.route("/train", methods=["POST"])
def trigger_training():
from threading import Thread
import subprocess
import shutil
def run_training():
training_status["running"] = 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
Thread(target=run_training).start()
return jsonify({"message": "Training gestartet"}), 200
@app.route("/train-status", methods=["GET"])
def get_training_status():
return jsonify(training_status), 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
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() # ⬅ Modell nach dem Training direkt neu laden
except Exception as e:
print("Training failed:", e)
training_status["running"] = False
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)

View File

@ -2,9 +2,25 @@ 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 = []
@ -19,10 +35,6 @@ 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({ results.append({"label": ent.label_, "entity": ent.text, "page": page_num})
"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)

View File

@ -4,3 +4,4 @@ transformers==4.35.2
torch torch
flask flask
requests requests
flask-cors

View File

@ -1648,5 +1648,175 @@
"NEUEKENNZAHL" "NEUEKENNZAHL"
] ]
] ]
},
{
"text": "fhfhfh56",
"entities": [
[
6,
8,
"TEST545"
]
]
},
{
"text": "fhfhfh56",
"entities": [
[
6,
8,
"TEST345"
]
]
},
{
"text": "sdgds45",
"entities": [
[
6,
7,
"TEST243"
]
]
},
{
"text": "4t4r3",
"entities": [
[
4,
5,
"TEST243"
]
]
},
{
"text": "sdgds45",
"entities": [
[
6,
7,
"DGTDDTFHZ"
]
]
},
{
"text": "gjufzj45",
"entities": [
[
7,
8,
"DGTDDTFHZ"
]
]
},
{
"text": "irr beträgt 43",
"entities": [
[
12,
14,
"TEST3243"
]
]
},
{
"text": "irr beträgt 43",
"entities": [
[
12,
14,
"IRR"
]
]
},
{
"text": "Rendite besträgt 5 %",
"entities": [
[
17,
20,
"RENDITE"
]
]
},
{
"text": "RenditeX besträgt 5 %",
"entities": [
[
18,
21,
"RENDITE_X"
]
]
},
{
"text": "gtg3ahz8",
"entities": [
[
7,
8,
"ERTRETT"
]
]
},
{
"text": "wffwee 45",
"entities": [
[
7,
9,
"TEST45"
]
]
},
{
"text": "efwwef 45",
"entities": [
[
7,
9,
"TEST12"
]
]
},
{
"text": "wfwefwe34",
"entities": [
[
7,
9,
"TEST232"
]
]
},
{
"text": "fwefbmj34",
"entities": [
[
7,
9,
"TEST223"
]
]
},
{
"text": "asdas45",
"entities": [
[
5,
7,
"TEST122"
]
]
},
{
"text": "ewefw4",
"entities": [
[
5,
6,
"TEST3434"
]
]
} }
] ]

View File

@ -1,21 +1,33 @@
import spacy import spacy
from spacy.training.example import Example from spacy.training.example import Example
import json import json
import os
import shutil
import sys
def load_data(file_path): def load_data(file_path):
with open(file_path, "r", encoding="utf8") as f: with open(file_path, "r", encoding="utf8") as f:
raw = json.load(f) raw = json.load(f)
TRAIN_DATA = [] return [
for entry in raw: (
text = entry["text"] entry["text"],
entities = [(start, end, label) for start, end, label in entry["entities"]] {
TRAIN_DATA.append((text, {"entities": entities})) "entities": [
return TRAIN_DATA (start, end, label) for start, end, label in entry["entities"]
]
},
)
for entry in raw
]
def main(): def main():
TRAIN_DATA = load_data("annotation_data.json") # 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") nlp = spacy.blank("de")
ner = nlp.add_pipe("ner") ner = nlp.add_pipe("ner")
ner.add_label("KENNZAHL") ner.add_label("KENNZAHL")
@ -26,9 +38,43 @@ def main():
example = Example.from_dict(nlp.make_doc(text), annotations) example = Example.from_dict(nlp.make_doc(text), annotations)
nlp.update([example], drop=0.2, sgd=optimizer) nlp.update([example], drop=0.2, sgd=optimizer)
nlp.to_disk("output/model-last") temp_model_dir = "output/temp-model"
final_model_dir = "output/model-last"
backup_dir = "output/model-backup"
# nlp.to_disk("model/") # Speichert das Modell 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__": if __name__ == "__main__":

View File

@ -1,21 +1,21 @@
[paths] [paths]
train = "./data/train.spacy" train = null
dev = "./data/train.spacy" dev = null
vectors = null vectors = null
init_tok2vec = null init_tok2vec = null
[system] [system]
gpu_allocator = null
seed = 0 seed = 0
gpu_allocator = null
[nlp] [nlp]
lang = "de" lang = "de"
pipeline = ["tok2vec","ner"] pipeline = ["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,51 +38,34 @@ use_upper = true
nO = null nO = null
[components.ner.model.tok2vec] [components.ner.model.tok2vec]
@architectures = "spacy.Tok2VecListener.v1" @architectures = "spacy.HashEmbedCNN.v2"
width = ${components.tok2vec.model.encode.width} pretrained_vectors = null
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}
max_length = 0
gold_preproc = false gold_preproc = false
max_length = 0
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}
max_length = 0
gold_preproc = false gold_preproc = false
max_length = 0
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
@ -93,6 +76,8 @@ 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

View File

@ -17,84 +17,29 @@
"mode":"default" "mode":"default"
}, },
"labels":{ "labels":{
"tok2vec":[
],
"ner":[ "ner":[
"AUSSCH\u00dcTTUNGSRENDITE", "AUSSCH\u00dcTTUNGSRENDITE",
"IRR",
"KENNZAHL",
"LAUFZEIT", "LAUFZEIT",
"L\u00c4NDERALLOKATION", "L\u00c4NDERALLOKATION",
"MANAGMENTGEB\u00dcHREN", "MANAGMENTGEB\u00dcHREN",
"RENDITE", "RENDITE",
"RENDITE_X",
"RISIKOPROFIL", "RISIKOPROFIL",
"SEKTORENALLOKATION", "SEKTORENALLOKATION",
"TEST3243",
"ZIELAUSSCH\u00dcTTUNG", "ZIELAUSSCH\u00dcTTUNG",
"ZIELRENDITE" "ZIELRENDITE"
] ]
}, },
"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
}
} }

View File

@ -1 +1 @@
¥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À ¥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,"TEST3243":-11,"IRR":-12,"RENDITE_X":-13},"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,"TEST3243":-11,"IRR":-12,"RENDITE_X":-13},"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,"TEST3243":-11,"IRR":-12,"RENDITE_X":-13},"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,"TEST3243":-11,"IRR":-12,"RENDITE_X":-13},"5":{"":1}}£cfg<66>§neg_keyÀ

View File

@ -52,9 +52,7 @@
"*", "*",
"+", "+",
"+/D", "+/D",
"+/d",
"+AU", "+AU",
"+au",
",", ",",
",00", ",00",
",03", ",03",
@ -141,9 +139,6 @@
"/d,dd", "/d,dd",
"/ddd%/ddd%/ddd", "/ddd%/ddd%/ddd",
"/fk", "/fk",
"/xx",
"/xxx",
"/xxxx+",
"0", "0",
"0%+", "0%+",
"0,0", "0,0",
@ -278,8 +273,11 @@
"4,91", "4,91",
"40", "40",
"400", "400",
"43",
"45", "45",
"491", "491",
"4r3",
"4t4r3",
"5", "5",
"5%+", "5%+",
"5,0", "5,0",
@ -310,6 +308,7 @@
"67", "67",
"7", "7",
"7,1", "7,1",
"7,2",
"7,5", "7,5",
"7,5%+", "7,5%+",
"7,50", "7,50",
@ -648,7 +647,6 @@
"E.", "E.",
"EAN", "EAN",
"ECLF", "ECLF",
"EIT",
"EM", "EM",
"ERD", "ERD",
"ESG-", "ESG-",
@ -683,7 +681,6 @@
"F", "F",
"F.", "F.",
"FDR", "FDR",
"FIL",
"FR", "FR",
"FRANCE", "FRANCE",
"FUND", "FUND",
@ -794,11 +791,9 @@
"III.", "III.",
"INK", "INK",
"INREV", "INREV",
"ION",
"IRR", "IRR",
"IRR6.5", "IRR6.5",
"IT", "IT",
"ITE",
"IUM", "IUM",
"IV", "IV",
"IV.", "IV.",
@ -866,6 +861,7 @@
"K.", "K.",
"K.O.", "K.O.",
"KAGB", "KAGB",
"KENNZAHL",
"KINGDOM", "KINGDOM",
"KVG", "KVG",
"Kapitalstruktur", "Kapitalstruktur",
@ -1072,8 +1068,8 @@
"R.", "R.",
"R.I.P.", "R.I.P.",
"RE", "RE",
"REN",
"RENDITE", "RENDITE",
"RENDITE_X",
"REV", "REV",
"REWE", "REWE",
"RISIKOPROFIL", "RISIKOPROFIL",
@ -1088,8 +1084,10 @@
"Redaktion", "Redaktion",
"Region", "Region",
"Regionen", "Regionen",
"Rendite",
"Rendite-", "Rendite-",
"Rendite-Risiko-Profil", "Rendite-Risiko-Profil",
"RenditeX",
"Renovierungen", "Renovierungen",
"Rents", "Rents",
"Residential", "Residential",
@ -1169,6 +1167,7 @@
"T", "T",
"T.", "T.",
"TED", "TED",
"TEST3243",
"Tag", "Tag",
"Target", "Target",
"Target-IRR", "Target-IRR",
@ -1197,7 +1196,6 @@
"U.S.S.", "U.S.S.",
"UK", "UK",
"UND", "UND",
"UNG",
"UNITED", "UNITED",
"USt", "USt",
"Univ", "Univ",
@ -1310,6 +1308,7 @@
"Xxxxx-Xxxxx-Xxxxx", "Xxxxx-Xxxxx-Xxxxx",
"Xxxxx-xxx", "Xxxxx-xxx",
"Xxxxx-xxxx", "Xxxxx-xxxx",
"XxxxxX",
"Xxxxx\u0308xx", "Xxxxx\u0308xx",
"Xxxxx\u0308xxx-Xxxxx", "Xxxxx\u0308xxx-Xxxxx",
"Xxxxx\u0308xxxx", "Xxxxx\u0308xxxx",
@ -1410,14 +1409,12 @@
"advantage", "advantage",
"ae", "ae",
"aft", "aft",
"agb",
"age", "age",
"agreements", "agreements",
"aha", "aha",
"ahe", "ahe",
"ahl", "ahl",
"ahr", "ahr",
"aif",
"ail", "ail",
"aiming", "aiming",
"ain", "ain",
@ -1429,7 +1426,6 @@
"al.", "al.",
"ald", "ald",
"ale", "ale",
"alf",
"all", "all",
"allg", "allg",
"allg.", "allg.",
@ -1440,7 +1436,6 @@
"allokationsprofil", "allokationsprofil",
"als", "als",
"also", "also",
"alt",
"alternative", "alternative",
"aly", "aly",
"am.", "am.",
@ -1535,7 +1530,6 @@
"aussch\u00fcttungsrandite", "aussch\u00fcttungsrandite",
"aussch\u00fcttungsrendite", "aussch\u00fcttungsrendite",
"aussch\u00fcttungsrendites", "aussch\u00fcttungsrendites",
"aut",
"ave", "ave",
"ax.", "ax.",
"b", "b",
@ -1565,9 +1559,11 @@
"berlin", "berlin",
"bestandsentwicklung", "bestandsentwicklung",
"bestandsentwicklungen", "bestandsentwicklungen",
"bestr\u00e4gt",
"betr", "betr",
"betr.", "betr.",
"betreute", "betreute",
"betr\u00e4gt",
"bev\u00f6lkerungsprognose", "bev\u00f6lkerungsprognose",
"beziehungsweise", "beziehungsweise",
"bez\u00fcglich", "bez\u00fcglich",
@ -1643,7 +1639,6 @@
"cl.", "cl.",
"class", "class",
"cle", "cle",
"clf",
"closed", "closed",
"closing", "closing",
"closings", "closings",
@ -1663,7 +1658,6 @@
"construction", "construction",
"contract", "contract",
"contracts", "contracts",
"cor",
"core", "core",
"core+", "core+",
"core+/d", "core+/d",
@ -1672,7 +1666,6 @@
"could", "could",
"country", "country",
"creation", "creation",
"csp",
"csu", "csu",
"cts", "cts",
"currency", "currency",
@ -1756,7 +1749,6 @@
"dipl.", "dipl.",
"dipl.-ing", "dipl.-ing",
"dipl.-ing.", "dipl.-ing.",
"dis",
"discretionary", "discretionary",
"distributions", "distributions",
"diversification", "diversification",
@ -1767,7 +1759,6 @@
"dle", "dle",
"do", "do",
"do.", "do.",
"dom",
"domicile", "domicile",
"domiciled", "domiciled",
"don", "don",
@ -1783,7 +1774,7 @@
"durchschnittlich", "durchschnittlich",
"du\u2019s", "du\u2019s",
"dv.", "dv.",
"dxxx.\u20ac", "dxdxd",
"dy", "dy",
"d\u00e4nemark", "d\u00e4nemark",
"d\u2019", "d\u2019",
@ -1877,7 +1868,6 @@
"er.", "er.",
"erb", "erb",
"erbbaurechte", "erbbaurechte",
"erd",
"ere", "ere",
"erfolgten", "erfolgten",
"erg", "erg",
@ -1940,7 +1930,6 @@
"fam", "fam",
"fam.", "fam.",
"favour", "favour",
"fdr",
"feb", "feb",
"feb.", "feb.",
"fee", "fee",
@ -1950,6 +1939,7 @@
"festgelegt", "festgelegt",
"festgelegter", "festgelegter",
"ff", "ff",
"fhfhfh56",
"fierce", "fierce",
"fil", "fil",
"financially", "financially",
@ -2050,6 +2040,7 @@
"ght", "ght",
"gic", "gic",
"gie", "gie",
"gjufzj45",
"gl.", "gl.",
"global", "global",
"globale", "globale",
@ -2071,6 +2062,7 @@
"h.", "h.",
"h.c", "h.c",
"h.c.", "h.c.",
"h56",
"haltedauer", "haltedauer",
"halten", "halten",
"halten-strategie", "halten-strategie",
@ -2270,6 +2262,7 @@
"ize", "ize",
"j", "j",
"j.", "j.",
"j45",
"ja", "ja",
"jahr", "jahr",
"jahre", "jahre",
@ -2393,7 +2386,6 @@
"lto", "lto",
"ltv", "ltv",
"ltv-ziel", "ltv-ziel",
"lty",
"lu", "lu",
"lub", "lub",
"lue", "lue",
@ -2427,7 +2419,6 @@
"management", "management",
"manager", "manager",
"manager-defined", "manager-defined",
"managmentgeb\u00fchren",
"mandate", "mandate",
"mandates", "mandates",
"market", "market",
@ -2439,7 +2430,6 @@
"maximal", "maximal",
"maximaler", "maximaler",
"mbH", "mbH",
"mbh",
"means", "means",
"medizin", "medizin",
"medizinnahe", "medizinnahe",
@ -2662,8 +2652,6 @@
"partners", "partners",
"partnership", "partnership",
"pattern", "pattern",
"pci",
"pco",
"ped", "ped",
"pen", "pen",
"per", "per",
@ -2719,7 +2707,6 @@
"q.", "q.",
"q.e.d", "q.e.d",
"q.e.d.", "q.e.d.",
"qin",
"quality", "quality",
"quarterly", "quarterly",
"quota", "quota",
@ -2759,6 +2746,7 @@
"rendite", "rendite",
"rendite-", "rendite-",
"rendite-risiko-profil", "rendite-risiko-profil",
"renditex",
"renegotiation", "renegotiation",
"renovierungen", "renovierungen",
"rent", "rent",
@ -2773,7 +2761,6 @@
"retailinvestitionsvolumen", "retailinvestitionsvolumen",
"return", "return",
"returns", "returns",
"rev",
"reversion", "reversion",
"rewe", "rewe",
"rge", "rge",
@ -2800,12 +2787,10 @@
"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.",
@ -2818,6 +2803,7 @@
"s.o", "s.o",
"s.o.", "s.o.",
"s.w", "s.w",
"s45",
"sa", "sa",
"sa.", "sa.",
"sale", "sale",
@ -2828,6 +2814,7 @@
"scs", "scs",
"scsp", "scsp",
"sd.", "sd.",
"sdgds45",
"sector", "sector",
"sectors", "sectors",
"sed", "sed",
@ -2835,7 +2822,6 @@
"segment", "segment",
"sektor", "sektor",
"sektoraler", "sektoraler",
"sektorenallokation",
"selection", "selection",
"sen", "sen",
"sen.", "sen.",
@ -2849,7 +2835,6 @@
"set", "set",
"sf.", "sf.",
"sfdr", "sfdr",
"sg-",
"sg.", "sg.",
"short-term", "short-term",
"sicav-raif", "sicav-raif",
@ -2935,6 +2920,7 @@
"tc.", "tc.",
"td.", "td.",
"te-", "te-",
"teX",
"ted", "ted",
"tee", "tee",
"teflimmobilfe)-", "teflimmobilfe)-",
@ -3128,9 +3114,6 @@
"worldwide", "worldwide",
"x", "x",
"x'", "x'",
"x+xx",
"x+xxx",
"x-xxxx",
"x.", "x.",
"x.X", "x.X",
"x.X.", "x.X.",
@ -3157,38 +3140,23 @@
"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.",
"xxxx\u0308xx", "xxxxdd",
"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",
@ -3224,7 +3192,6 @@
"zielallokation", "zielallokation",
"zielanlagestrategie", "zielanlagestrategie",
"zielausschu\u0308ttung", "zielausschu\u0308ttung",
"zielaussch\u00fcttung",
"zielmarkts", "zielmarkts",
"zielm\u00e4rkte", "zielm\u00e4rkte",
"zielobjekte", "zielobjekte",
@ -3279,6 +3246,7 @@
"\u00e4", "\u00e4",
"\u00e4.", "\u00e4.",
"\u00e4gl", "\u00e4gl",
"\u00e4gt",
"\u00e4r.", "\u00e4r.",
"\u00e4rzteh\u00e4user", "\u00e4rzteh\u00e4user",
"\u00e4rzteh\u00e4usern", "\u00e4rzteh\u00e4usern",

View File

@ -0,0 +1 @@
{"running": false}

View File

@ -74,10 +74,6 @@ services:
- COORDINATOR_URL=http://coordinator:5000 - COORDINATOR_URL=http://coordinator:5000
ports: ports:
- 5053:5000 - 5053:5000
depends_on:
- coordinator
validate: validate:
build: build:

View File

@ -1,9 +1,13 @@
import { Box, Typography, Button, Paper, TextField, FormControlLabel, import {
Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress } from "@mui/material"; Box, Typography, Button, Paper, TextField, FormControlLabel,
Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress
} from "@mui/material";
import { useState, useEffect } from "react"; import { useState, useEffect } 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 { saveAs } from "file-saver"; import Snackbar from "@mui/material/Snackbar";
import MuiAlert from "@mui/material/Alert";
interface KPIFormProps { interface KPIFormProps {
mode: 'add' | 'edit'; mode: 'add' | 'edit';
@ -11,6 +15,7 @@ interface KPIFormProps {
onSave: (data: Partial<Kennzahl>) => Promise<void>; onSave: (data: Partial<Kennzahl>) => Promise<void>;
onCancel: () => void; onCancel: () => void;
loading?: boolean; loading?: boolean;
resetTrigger?: number;
} }
const emptyKPI: Partial<Kennzahl> = { const emptyKPI: Partial<Kennzahl> = {
@ -21,83 +26,132 @@ const emptyKPI: Partial<Kennzahl> = {
translation: '', translation: '',
example: '', example: '',
active: true, active: true,
exampleText: '', examples: [{ sentence: '', value: '' }],
markedValue: '',
}; };
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }: KPIFormProps) {
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false, resetTrigger }: KPIFormProps) {
const [formData, setFormData] = useState<Partial<Kennzahl>>(emptyKPI); const [formData, setFormData] = useState<Partial<Kennzahl>>(emptyKPI);
const [isSaving, setIsSaving] = useState(false); 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) {
setFormData(initialData); setFormData(initialData);
} else { } else if (mode === 'add') {
setFormData(emptyKPI); setFormData(emptyKPI);
} }
}, [mode, initialData]); }, [mode, initialData]);
useEffect(() => {
if (mode === 'add') {
setFormData(emptyKPI);
}
}, [resetTrigger]);
const handleSave = async () => { const handleSave = async () => {
if (!formData.name?.trim()) { if (!formData.name?.trim()) {
alert('Name ist erforderlich'); setSnackbarMessage("Name ist erforderlich");
setSnackbarSeverity("error");
setSnackbarOpen(true);
return; return;
} }
if (!formData.exampleText?.trim()) { if (!formData.examples || formData.examples.length === 0) {
alert('Beispielsatz ist erforderlich'); setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich");
setSnackbarSeverity("error");
setSnackbarOpen(true);
return; return;
} }
if (!formData.markedValue?.trim()) { for (const ex of formData.examples) {
alert('Bezeichneter Wert im Satz ist erforderlich'); if (!ex.sentence?.trim() || !ex.value?.trim()) {
return; setSnackbarMessage('Alle Beispielsätze müssen vollständig sein.');
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
} }
setIsSaving(true); setIsSaving(true);
try { try {
const spacyEntry = generateSpacyEntry(formData); const spacyEntries = generateSpacyEntries(formData);
//in localStorage merken // Für jeden einzelnen Beispielsatz:
const stored = localStorage.getItem("spacyData"); for (const entry of spacyEntries) {
const existingData = stored ? JSON.parse(stored) : []; // im localStorage speichern (zum Debuggen oder Vorschau)
const updated = [...existingData, spacyEntry]; const stored = localStorage.getItem("spacyData");
localStorage.setItem("spacyData", JSON.stringify(updated)); const existingData = stored ? JSON.parse(stored) : [];
const updated = [...existingData, entry];
localStorage.setItem("spacyData", JSON.stringify(updated));
// an Flask senden // POST Request an das Flask-Backend
const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", { const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
body: JSON.stringify(spacyEntry) body: JSON.stringify(entry)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Fehler beim Aufruf von append-training-entry");
}
console.log("SpaCy-Eintrag gespeichert:", data);
}
// Dann in die DB speichern
await onSave({
name: formData.name!,
description: formData.description || '',
mandatory: formData.mandatory ?? false,
type: formData.type || 'string',
translation: formData.translation || '',
example: formData.example || '',
position: formData.position ?? 0,
active: formData.active ?? true,
examples: [{ sentence: '', value: '' }]
}); });
// Formular zurücksetzen:
setFormData(emptyKPI);
const data = await response.json(); setSnackbarMessage("Beispielsätze gespeichert. Jetzt auf Neu trainieren klicken oder weitere Kennzahlen hinzufügen.");
console.log("Response von /append-training-entry:", data); setSnackbarSeverity("success");
setSnackbarOpen(true);
if (!response.ok) {
throw new Error(data.error || "Fehler beim Aufruf von append-training-entry");
}
if (!response.ok) {
throw new Error("Fehler vom Backend: " + response.status);
}
// anschließend in der Datenbank speichern
await onSave(formData);
alert("SpaCy-Eintrag erfolgreich gespeichert!");
} catch (e: any) { } catch (e: any) {
alert(e.message || "Fehler beim Erzeugen des Trainingsbeispiels."); // Prüfe auf 409-Fehler
if (e?.message?.includes("409") || e?.response?.status === 409) {
setSnackbarMessage("Diese Kennzahl existiert bereits. Sie können sie unter Konfiguration bearbeiten.");
setSnackbarSeverity("info");
setSnackbarOpen(true);
} else {
setSnackbarMessage(e.message || "Fehler beim Speichern.");
setSnackbarSeverity("error");
setSnackbarOpen(true);
}
console.error(e); console.error(e);
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}; };
const handleCancel = () => { const handleCancel = () => {
setFormData(emptyKPI);
onCancel(); onCancel();
}; };
@ -105,6 +159,24 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
setFormData(prev => ({ ...prev, [field]: value })); setFormData(prev => ({ ...prev, [field]: value }));
}; };
const updateExample = (index: number, 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) { if (loading) {
return ( return (
<Box <Box
@ -123,218 +195,221 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
} }
return ( return (
<Paper <>
elevation={2} <Paper
sx={{ elevation={2}
width: "90%", sx={{
maxWidth: 800, width: "90%",
p: 4, maxWidth: 800,
borderRadius: 2, p: 4,
backgroundColor: "white" borderRadius: 2,
}} backgroundColor: "white"
> }}
<Box mb={4}> >
<Typography variant="h6" fontWeight="bold" mb={2}> <Box mb={4}>
Kennzahl <Typography variant="h6" fontWeight="bold" mb={2}>
</Typography> Kennzahl
<TextField </Typography>
fullWidth <TextField
label="Name *" fullWidth
value={formData.name || ''} label="Name *"
onChange={(e) => updateField('name', e.target.value)} placeholder="z.B. IRR"
sx={{ mb: 2 }} value={formData.name || ''}
required onChange={(e) => updateField('name', e.target.value)}
error={!formData.name?.trim()} sx={{ mb: 2 }}
helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''} required
/> error={!formData.name?.trim()}
</Box> helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''}
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beispielsatz
</Typography>
<TextField
fullWidth
multiline
rows={3}
label="Beispielsatz"
required
value={formData.exampleText || ''}
onChange={(e) => updateField('exampleText', e.target.value)}
error={!formData.exampleText?.trim()}
helperText={
!formData.exampleText?.trim()
? "Beispielsatz ist erforderlich"
: "Ein vollständiger Satz, in dem der markierte Begriff vorkommt"
}
/>
<TextField
fullWidth
required
sx={{ mt: 2 }}
label="Bezeichneter Wert im Satz *"
value={formData.markedValue || ''}
onChange={(e) => updateField('markedValue', e.target.value)}
error={!formData.markedValue?.trim()}
helperText={
!formData.markedValue?.trim()
? "Markierter Begriff ist erforderlich"
: "Nur der Begriff, der im Satz markiert werden soll (z.B. Core/Core+)"
}
/>
<Box mt={3}>
<FormControlLabel
control={
<Checkbox
checked={formData.mandatory || false}
onChange={(e) => updateField('mandatory', e.target.checked)}
sx={{ color: '#383838' }}
/>
}
label="Erforderlich"
/> />
<Typography variant="body2" color="text.secondary" ml={4}> </Box>
Die Kennzahl erlaubt keine leeren Werte
<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>
{mode === 'add' && (
<>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<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>
<Box mt={3}>
<FormControlLabel
control={
<Checkbox
checked={formData.mandatory || false}
onChange={(e) => updateField('mandatory', e.target.checked)}
sx={{ color: '#383838' }}
/>
}
label="Erforderlich"
/>
<Typography variant="body2" color="text.secondary" ml={4}>
Die Kennzahl erlaubt keine leeren Werte
</Typography>
</Box>
</>
)}
<Divider sx={{ my: 3 }} />
{/* 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 oder vorhandene in der Kennzahlenverwaltung bearbeiten.
</Typography> </Typography>
</Box> </Box>
</Box>
<Divider sx={{ my: 3 }} /> <Box mb={4}>
<Box mb={4}> <Typography variant="h6" fontWeight="bold" mb={2}>
<Typography variant="h6" fontWeight="bold" mb={2}> Beispielsätze
Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type} </Typography>
</Typography> {(formData.examples || []).map((ex, idx) => (
<FormControl fullWidth sx={{ mb: 2 }}> <Box key={idx} sx={{ mb: 2, border: '1px solid #ccc', p: 2, borderRadius: 1 }}>
<InputLabel>Typ</InputLabel> <TextField
<Select fullWidth
value={formData.type || 'string'} multiline
label="Typ" label={`Beispielsatz ${idx + 1}`}
onChange={(e) => updateField('type', e.target.value)} 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" }
}}
> >
<MenuItem value="string">Text</MenuItem> Abbrechen
<MenuItem value="number">Zahl</MenuItem> </Button>
<MenuItem value="date">Datum</MenuItem> <Button
<MenuItem value="boolean">Ja/Nein</MenuItem> variant="contained"
<MenuItem value="array">Liste (mehrfach)</MenuItem> onClick={handleSave}
</Select> disabled={isSaving || !formData.name?.trim()}
</FormControl> sx={{
</Box> backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
<Divider sx={{ my: 3 }} /> }}
>
<Box mb={4}> {isSaving ? (
<Typography variant="h6" fontWeight="bold" mb={2}> <>
Synonyme & Übersetzungen <CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
</Typography> {mode === 'add' ? 'Hinzufügen...' : 'Speichern...'}
<TextField </>
fullWidth ) : (
label="Übersetzung" mode === 'add' ? 'Hinzufügen' : 'Speichern'
value={formData.translation || ''} )}
onChange={(e) => updateField('translation', e.target.value)} </Button>
helperText="z.B. Englische Übersetzung der Kennzahl" </Box>
/> </Paper>
</Box> <Snackbar
open={snackbarOpen}
<Divider sx={{ my: 3 }} /> autoHideDuration={5000}
onClose={() => setSnackbarOpen(false)}
<Box mb={4}> anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
<Typography variant="h6" fontWeight="bold" mb={2}> >
Beispiele von Kennzahl <MuiAlert
</Typography> elevation={6}
<TextField variant="filled"
fullWidth onClose={() => setSnackbarOpen(false)}
multiline severity={snackbarSeverity}
rows={2} sx={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
label="Beispiel"
value={formData.example || ''}
onChange={(e) => updateField('example', e.target.value)}
helperText="Beispielwerte für diese Kennzahl"
/>
</Box>
{mode === 'add' && (
<>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<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>
</>
)}
<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 <span>{snackbarMessage}</span>
</Button> <Button color="inherit" size="small" onClick={() => setSnackbarOpen(false)}>
<Button OK
variant="contained" </Button>
onClick={handleSave} </MuiAlert>
disabled={isSaving || !formData.name?.trim()}
sx={{ </Snackbar>
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>
); );
} }
function generateSpacyEntry(formData: Partial<Kennzahl>) {
const text = formData.exampleText?.trim() || "";
const value = formData.markedValue?.trim() || "";
const label = formData.name?.trim().toUpperCase() || "";
const start = text.indexOf(value); function generateSpacyEntries(formData: Partial<Kennzahl>) {
if (start === -1) { const label = formData.name?.trim().toUpperCase() || "";
throw new Error("Bezeichneter Begriff wurde im Satz nicht gefunden."); return (formData.examples || []).map(({ sentence, value }) => {
} const start = sentence.indexOf(value);
if (start === -1) {
return { throw new Error(`"${value}" nicht gefunden in Satz: "${sentence}"`);
text, }
entities: [[start, start + value.length, label]], return {
}; text: sentence,
entities: [[start, start + value.length, label]]
};
});
} }
// function appendAndDownload(newEntry: any, existing: any[] = []) {
// const updated = [...existing, newEntry];
// const blob = new Blob([JSON.stringify(updated, null, 2)], {
// type: "application/json",
// });
// saveAs(blob, "..\project\backend\spacy-service\spacy_training\annotation_data.json");
// }

View File

@ -5,6 +5,7 @@ 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 } => {
@ -47,19 +48,28 @@ 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({ to: "/config" }); navigate({
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({ to: "/config" }); navigate({
to: "/config",
search: from ? { from } : undefined,
});
}; };
return ( return (
@ -83,7 +93,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
@ -93,6 +103,7 @@ function ConfigAddPage() {
<KPIForm <KPIForm
mode="add" mode="add"
key={Date.now()}
onSave={handleSave} onSave={handleSave}
onCancel={handleCancel} onCancel={handleCancel}
/> />

View File

@ -1,5 +1,6 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { Box, Typography, IconButton, Button, CircularProgress, Paper, Divider import {
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";
@ -38,6 +39,7 @@ 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');
@ -72,7 +74,6 @@ 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);
@ -82,6 +83,7 @@ function KPIDetailPage() {
} }
}; };
const handleCancel = () => { const handleCancel = () => {
setIsEditing(false); setIsEditing(false);
}; };
@ -153,7 +155,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
@ -192,18 +194,13 @@ function KPIDetailPage() {
<Divider sx={{ my: 3 }} /> <Divider sx={{ my: 3 }} />
<Box mb={4}> <Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}> <Typography variant="h6" fontWeight="bold" mb={1}>
Beschreibung Erforderlich:
</Typography> </Typography>
<Typography variant="body1" color="text.secondary"> <Typography variant="body1" sx={{ mb: 2, fontSize: 16 }}>
{kennzahl.description || "Zurzeit ist die Beschreibung der Kennzahl leer. Klicken Sie auf den Bearbeiten-Button, um die Beschreibung zu ergänzen."} {kennzahl.mandatory ? 'Ja' : 'Nein'}
</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 }} />
@ -216,28 +213,6 @@ 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>
); );
@ -264,7 +239,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

View File

@ -3,18 +3,65 @@ 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";
export const Route = createFileRoute("/config")({ export const Route = createFileRoute("/config")({
component: ConfigPage, component: ConfigPage,
validateSearch: (search: Record<string, unknown>): { from?: string } => { validateSearch: (search: Record<string, unknown>): { from?: string; success?: string } => {
const from = typeof search.from === "string" ? search.from : undefined; return {
return { from }; from: typeof search.from === "string" ? search.from : undefined,
success: typeof search.success === "string" ? search.success : undefined
};
} }
}); });
function ConfigPage() { function ConfigPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { from } = Route.useSearch(); const { from, success } = 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);
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({
@ -31,46 +78,128 @@ function ConfigPage() {
} }
}; };
const handleTriggerTraining = () => {
setTrainingRunning(true);
setSnackbarMessage("Training wurde gestartet.");
setSnackbarOpen(true);
fetch(`${API_HOST}/api/spacy/train`, {
method: "POST",
}).catch(err => {
setSnackbarMessage("Fehler beim Starten des Trainings.");
setSnackbarOpen(true);
console.error(err);
});
pollTrainingStatus(); // Starte Überwachung
};
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);
}
} 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
width="100%" minHeight="100vh"
width="100vw"
bgcolor="white"
display="flex" display="flex"
justifyContent="space-between" flexDirection="column"
alignItems="center" alignItems="center"
px={4} pt={3}
pb={4}
> >
<Box display="flex" alignItems="center"> <Box
<IconButton onClick={handleBack}> width="100%"
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/> display="flex"
</IconButton> justifyContent="space-between"
<Typography variant="h5" fontWeight="bold" ml={3}> alignItems="center"
Konfiguration der Kennzahlen px={4}
</Typography>
</Box>
<Button
variant="contained"
onClick={handleAddNewKPI}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
> >
Neue Kennzahl hinzufügen {/* Linke Seite: Zurück & Titel */}
</Button> <Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Konfiguration der Kennzahlen
</Typography>
</Box>
{/* Rechte Seite: Buttons */}
<Box display="flex" gap={2}>
<Button
variant="contained"
onClick={handleTriggerTraining}
disabled={trainingRunning}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
{trainingRunning ? (
<CircularProgress size={24} sx={{ color: "white" }} />
) : (
"Neu trainieren"
)}
</Button>
<Button
variant="contained"
onClick={handleAddNewKPI}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
Neue Kennzahl hinzufügen
</Button>
</Box>
</Box>
{/* Tabelle */}
<Box sx={{ width: "100%", mt: 4, display: "flex", justifyContent: "center" }}>
<ConfigTable from={from} />
</Box>
</Box> </Box>
<Box sx={{ width: "100%", mt: 4, display: "flex", justifyContent: "center" }}>
<ConfigTable from={from} /> {/* Snackbar */}
</Box> <Snackbar
</Box> open={snackbarOpen}
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>
</>
); );
} }

View File

@ -10,6 +10,10 @@ export interface Kennzahl {
active: boolean; active: boolean;
exampleText?: string; exampleText?: string;
markedValue?: string; markedValue?: string;
examples?: {
sentence: string;
value: string;
}[];
} }
export const typeDisplayMapping: Record<string, string> = { export const typeDisplayMapping: Record<string, string> = {