diff --git a/project/backend/coordinator/app.py b/project/backend/coordinator/app.py index 47f4bb9..caeafca 100644 --- a/project/backend/coordinator/app.py +++ b/project/backend/coordinator/app.py @@ -5,8 +5,6 @@ from dotenv import load_dotenv from controller import register_routes from model.database import init_db from controller.socketIO import socketio -from controller.kennzahlen import kennzahlen_bp - app = Flask(__name__) CORS(app) @@ -23,15 +21,11 @@ init_db(app) register_routes(app) -# Register blueprints -app.register_blueprint(kennzahlen_bp) - - @app.route("/health") def health_check(): return "OK" -# für Docker wichtig: host='0.0.0.0' +# Für Docker wichtig: host='0.0.0.0' if __name__ == "__main__": socketio.run(app, debug=True, host="0.0.0.0", port=5050) diff --git a/project/backend/coordinator/controller/spacy_controller.py b/project/backend/coordinator/controller/spacy_controller.py index 3ce2f93..edcf653 100644 --- a/project/backend/coordinator/controller/spacy_controller.py +++ b/project/backend/coordinator/controller/spacy_controller.py @@ -6,10 +6,36 @@ from werkzeug.utils import secure_filename from model.database import db import os import json - +import requests 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"]) def get_all_files(): @@ -33,7 +59,6 @@ def download_file(id): @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 @@ -41,17 +66,13 @@ def upload_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 + 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 @@ -65,14 +86,9 @@ def update_file(id): 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" - ): + if puremagic.from_string(file_data, mime=True) == "application/pdf": file.file = file_data except Exception as e: print(e) @@ -81,7 +97,6 @@ def update_file(id): file.kpi = request.form.get("kpi") db.session.commit() - return jsonify(file.to_dict()), 200 @@ -90,7 +105,6 @@ 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 @@ -110,7 +124,6 @@ def append_training_entry(): try: os.makedirs(os.path.dirname(path), exist_ok=True) - if os.path.exists(path): with open(path, "r", encoding="utf-8") as f: data = json.load(f) @@ -128,3 +141,16 @@ def append_training_entry(): except Exception as e: print(f"[ERROR] Fehler beim Schreiben: {e}") 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 diff --git a/project/backend/exxetaGPT-service/Dockerfile b/project/backend/exxetaGPT-service/Dockerfile new file mode 100644 index 0000000..c2b6203 --- /dev/null +++ b/project/backend/exxetaGPT-service/Dockerfile @@ -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"] diff --git a/project/backend/spacy-service/Dockerfile b/project/backend/spacy-service/Dockerfile index d7ad008..20d5817 100644 --- a/project/backend/spacy-service/Dockerfile +++ b/project/backend/spacy-service/Dockerfile @@ -11,6 +11,8 @@ COPY requirements.txt /app RUN pip install --upgrade pip RUN pip install --no-cache-dir -r requirements.txt +RUN pip install flask-cors + RUN python -m spacy download en_core_web_sm diff --git a/project/backend/spacy-service/app.py b/project/backend/spacy-service/app.py index 2a0a82b..67da9ef 100644 --- a/project/backend/spacy-service/app.py +++ b/project/backend/spacy-service/app.py @@ -1,9 +1,14 @@ from flask import Flask, request, jsonify -from extractSpacy import extract +from extractSpacy import extract, load_model import requests import os import json from flask_cors import CORS +import shutil +import subprocess + + +training_status = {"running": False} app = Flask(__name__) @@ -66,7 +71,7 @@ def append_training_entry(): else: data = [] - # Optional: Duplikate prüfen + # Duplikate prüfen if entry in data: return jsonify({"message": "Eintrag existiert bereits."}), 200 @@ -80,5 +85,59 @@ def append_training_entry(): 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__": app.run(host="0.0.0.0", port=5052, debug=True) diff --git a/project/backend/spacy-service/extractSpacy.py b/project/backend/spacy-service/extractSpacy.py index 8ae4e31..0c607e4 100644 --- a/project/backend/spacy-service/extractSpacy.py +++ b/project/backend/spacy-service/extractSpacy.py @@ -2,9 +2,25 @@ import spacy import os import json + current_dir = os.path.dirname(os.path.abspath(__file__)) 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): results = [] @@ -19,10 +35,6 @@ def extract(pages_json): spacy_result = nlp(text) 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) \ No newline at end of file + return json.dumps(results, indent=2, ensure_ascii=False) diff --git a/project/backend/spacy-service/requirements.txt b/project/backend/spacy-service/requirements.txt index e78227c..e003cb3 100644 --- a/project/backend/spacy-service/requirements.txt +++ b/project/backend/spacy-service/requirements.txt @@ -3,4 +3,5 @@ spacy-transformers==1.3.3 transformers==4.35.2 torch flask -requests \ No newline at end of file +requests +flask-cors \ No newline at end of file diff --git a/project/backend/spacy-service/spacy_training/annotation_data.json b/project/backend/spacy-service/spacy_training/annotation_data.json index 71384aa..b76f4fe 100644 --- a/project/backend/spacy-service/spacy_training/annotation_data.json +++ b/project/backend/spacy-service/spacy_training/annotation_data.json @@ -1648,5 +1648,175 @@ "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" + ] + ] } ] \ No newline at end of file diff --git a/project/backend/spacy-service/spacy_training/ner_trainer.py b/project/backend/spacy-service/spacy_training/ner_trainer.py index 91795df..52cda7a 100644 --- a/project/backend/spacy-service/spacy_training/ner_trainer.py +++ b/project/backend/spacy-service/spacy_training/ner_trainer.py @@ -1,21 +1,33 @@ 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) - TRAIN_DATA = [] - for entry in raw: - text = entry["text"] - entities = [(start, end, label) for start, end, label in entry["entities"]] - TRAIN_DATA.append((text, {"entities": entities})) - return TRAIN_DATA + return [ + ( + entry["text"], + { + "entities": [ + (start, end, label) for start, end, label in entry["entities"] + ] + }, + ) + for entry in raw + ] 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") ner = nlp.add_pipe("ner") ner.add_label("KENNZAHL") @@ -26,9 +38,43 @@ def main(): example = Example.from_dict(nlp.make_doc(text), annotations) 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__": diff --git a/project/backend/spacy-service/spacy_training/output/model-last/config.cfg b/project/backend/spacy-service/spacy_training/output/model-last/config.cfg index 1cf80c4..be02f9c 100644 --- a/project/backend/spacy-service/spacy_training/output/model-last/config.cfg +++ b/project/backend/spacy-service/spacy_training/output/model-last/config.cfg @@ -1,21 +1,21 @@ [paths] -train = "./data/train.spacy" -dev = "./data/train.spacy" +train = null +dev = null vectors = null init_tok2vec = null [system] -gpu_allocator = null seed = 0 +gpu_allocator = null [nlp] lang = "de" -pipeline = ["tok2vec","ner"] -batch_size = 1000 +pipeline = ["ner"] disabled = [] before_creation = null after_creation = null after_pipeline_creation = null +batch_size = 1000 tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"} vectors = {"@vectors":"spacy.Vectors.v1"} @@ -38,51 +38,34 @@ use_upper = true nO = null [components.ner.model.tok2vec] -@architectures = "spacy.Tok2VecListener.v1" -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" +@architectures = "spacy.HashEmbedCNN.v2" +pretrained_vectors = null width = 96 depth = 4 +embed_size = 2000 window_size = 1 maxout_pieces = 3 +subword_features = true [corpora] [corpora.dev] @readers = "spacy.Corpus.v1" path = ${paths.dev} -max_length = 0 gold_preproc = false +max_length = 0 limit = 0 augmenter = null [corpora.train] @readers = "spacy.Corpus.v1" path = ${paths.train} -max_length = 0 gold_preproc = false +max_length = 0 limit = 0 augmenter = null [training] -dev_corpus = "corpora.dev" -train_corpus = "corpora.train" seed = ${system.seed} gpu_allocator = ${system.gpu_allocator} dropout = 0.1 @@ -93,6 +76,8 @@ max_steps = 20000 eval_frequency = 200 frozen_components = [] annotating_components = [] +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" before_to_disk = null before_update = null diff --git a/project/backend/spacy-service/spacy_training/output/model-last/meta.json b/project/backend/spacy-service/spacy_training/output/model-last/meta.json index 64f070d..a92a61a 100644 --- a/project/backend/spacy-service/spacy_training/output/model-last/meta.json +++ b/project/backend/spacy-service/spacy_training/output/model-last/meta.json @@ -17,84 +17,29 @@ "mode":"default" }, "labels":{ - "tok2vec":[ - - ], "ner":[ "AUSSCH\u00dcTTUNGSRENDITE", + "IRR", + "KENNZAHL", "LAUFZEIT", "L\u00c4NDERALLOKATION", "MANAGMENTGEB\u00dcHREN", "RENDITE", + "RENDITE_X", "RISIKOPROFIL", "SEKTORENALLOKATION", + "TEST3243", "ZIELAUSSCH\u00dcTTUNG", "ZIELRENDITE" ] }, "pipeline":[ - "tok2vec", "ner" ], "components":[ - "tok2vec", "ner" ], "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 - } + ] } \ No newline at end of file diff --git a/project/backend/spacy-service/spacy_training/output/model-last/ner/model b/project/backend/spacy-service/spacy_training/output/model-last/ner/model index 3e88595..baba9fa 100644 Binary files a/project/backend/spacy-service/spacy_training/output/model-last/ner/model and b/project/backend/spacy-service/spacy_training/output/model-last/ner/model differ diff --git a/project/backend/spacy-service/spacy_training/output/model-last/ner/moves b/project/backend/spacy-service/spacy_training/output/model-last/ner/moves index b76b203..9f82843 100644 --- a/project/backend/spacy-service/spacy_training/output/model-last/ner/moves +++ b/project/backend/spacy-service/spacy_training/output/model-last/ner/moves @@ -1 +1 @@ -movesL{"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}}cfgneg_key \ No newline at end of file +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}}cfgneg_key \ No newline at end of file diff --git a/project/backend/spacy-service/spacy_training/output/model-last/vocab/strings.json b/project/backend/spacy-service/spacy_training/output/model-last/vocab/strings.json index 7c963aa..0d8c921 100644 --- a/project/backend/spacy-service/spacy_training/output/model-last/vocab/strings.json +++ b/project/backend/spacy-service/spacy_training/output/model-last/vocab/strings.json @@ -52,9 +52,7 @@ "*", "+", "+/D", - "+/d", "+AU", - "+au", ",", ",00", ",03", @@ -141,9 +139,6 @@ "/d,dd", "/ddd%/ddd%/ddd", "/fk", - "/xx", - "/xxx", - "/xxxx+", "0", "0%+", "0,0", @@ -278,8 +273,11 @@ "4,91", "40", "400", + "43", "45", "491", + "4r3", + "4t4r3", "5", "5%+", "5,0", @@ -310,6 +308,7 @@ "67", "7", "7,1", + "7,2", "7,5", "7,5%+", "7,50", @@ -648,7 +647,6 @@ "E.", "EAN", "ECLF", - "EIT", "EM", "ERD", "ESG-", @@ -683,7 +681,6 @@ "F", "F.", "FDR", - "FIL", "FR", "FRANCE", "FUND", @@ -794,11 +791,9 @@ "III.", "INK", "INREV", - "ION", "IRR", "IRR6.5", "IT", - "ITE", "IUM", "IV", "IV.", @@ -866,6 +861,7 @@ "K.", "K.O.", "KAGB", + "KENNZAHL", "KINGDOM", "KVG", "Kapitalstruktur", @@ -1072,8 +1068,8 @@ "R.", "R.I.P.", "RE", - "REN", "RENDITE", + "RENDITE_X", "REV", "REWE", "RISIKOPROFIL", @@ -1088,8 +1084,10 @@ "Redaktion", "Region", "Regionen", + "Rendite", "Rendite-", "Rendite-Risiko-Profil", + "RenditeX", "Renovierungen", "Rents", "Residential", @@ -1169,6 +1167,7 @@ "T", "T.", "TED", + "TEST3243", "Tag", "Target", "Target-IRR", @@ -1197,7 +1196,6 @@ "U.S.S.", "UK", "UND", - "UNG", "UNITED", "USt", "Univ", @@ -1310,6 +1308,7 @@ "Xxxxx-Xxxxx-Xxxxx", "Xxxxx-xxx", "Xxxxx-xxxx", + "XxxxxX", "Xxxxx\u0308xx", "Xxxxx\u0308xxx-Xxxxx", "Xxxxx\u0308xxxx", @@ -1410,14 +1409,12 @@ "advantage", "ae", "aft", - "agb", "age", "agreements", "aha", "ahe", "ahl", "ahr", - "aif", "ail", "aiming", "ain", @@ -1429,7 +1426,6 @@ "al.", "ald", "ale", - "alf", "all", "allg", "allg.", @@ -1440,7 +1436,6 @@ "allokationsprofil", "als", "also", - "alt", "alternative", "aly", "am.", @@ -1535,7 +1530,6 @@ "aussch\u00fcttungsrandite", "aussch\u00fcttungsrendite", "aussch\u00fcttungsrendites", - "aut", "ave", "ax.", "b", @@ -1565,9 +1559,11 @@ "berlin", "bestandsentwicklung", "bestandsentwicklungen", + "bestr\u00e4gt", "betr", "betr.", "betreute", + "betr\u00e4gt", "bev\u00f6lkerungsprognose", "beziehungsweise", "bez\u00fcglich", @@ -1643,7 +1639,6 @@ "cl.", "class", "cle", - "clf", "closed", "closing", "closings", @@ -1663,7 +1658,6 @@ "construction", "contract", "contracts", - "cor", "core", "core+", "core+/d", @@ -1672,7 +1666,6 @@ "could", "country", "creation", - "csp", "csu", "cts", "currency", @@ -1756,7 +1749,6 @@ "dipl.", "dipl.-ing", "dipl.-ing.", - "dis", "discretionary", "distributions", "diversification", @@ -1767,7 +1759,6 @@ "dle", "do", "do.", - "dom", "domicile", "domiciled", "don", @@ -1783,7 +1774,7 @@ "durchschnittlich", "du\u2019s", "dv.", - "dxxx.\u20ac", + "dxdxd", "dy", "d\u00e4nemark", "d\u2019", @@ -1877,7 +1868,6 @@ "er.", "erb", "erbbaurechte", - "erd", "ere", "erfolgten", "erg", @@ -1940,7 +1930,6 @@ "fam", "fam.", "favour", - "fdr", "feb", "feb.", "fee", @@ -1950,6 +1939,7 @@ "festgelegt", "festgelegter", "ff", + "fhfhfh56", "fierce", "fil", "financially", @@ -2050,6 +2040,7 @@ "ght", "gic", "gie", + "gjufzj45", "gl.", "global", "globale", @@ -2071,6 +2062,7 @@ "h.", "h.c", "h.c.", + "h56", "haltedauer", "halten", "halten-strategie", @@ -2270,6 +2262,7 @@ "ize", "j", "j.", + "j45", "ja", "jahr", "jahre", @@ -2393,7 +2386,6 @@ "lto", "ltv", "ltv-ziel", - "lty", "lu", "lub", "lue", @@ -2427,7 +2419,6 @@ "management", "manager", "manager-defined", - "managmentgeb\u00fchren", "mandate", "mandates", "market", @@ -2439,7 +2430,6 @@ "maximal", "maximaler", "mbH", - "mbh", "means", "medizin", "medizinnahe", @@ -2662,8 +2652,6 @@ "partners", "partnership", "pattern", - "pci", - "pco", "ped", "pen", "per", @@ -2719,7 +2707,6 @@ "q.", "q.e.d", "q.e.d.", - "qin", "quality", "quarterly", "quota", @@ -2759,6 +2746,7 @@ "rendite", "rendite-", "rendite-risiko-profil", + "renditex", "renegotiation", "renovierungen", "rent", @@ -2773,7 +2761,6 @@ "retailinvestitionsvolumen", "return", "returns", - "rev", "reversion", "rewe", "rge", @@ -2800,12 +2787,10 @@ "rop", "rotterdam", "rr.", - "rre", "rs.", "rsg", "rst", "rte", - "rtt", "rz.", "r\u00f6m", "r\u00f6m.", @@ -2818,6 +2803,7 @@ "s.o", "s.o.", "s.w", + "s45", "sa", "sa.", "sale", @@ -2828,6 +2814,7 @@ "scs", "scsp", "sd.", + "sdgds45", "sector", "sectors", "sed", @@ -2835,7 +2822,6 @@ "segment", "sektor", "sektoraler", - "sektorenallokation", "selection", "sen", "sen.", @@ -2849,7 +2835,6 @@ "set", "sf.", "sfdr", - "sg-", "sg.", "short-term", "sicav-raif", @@ -2935,6 +2920,7 @@ "tc.", "td.", "te-", + "teX", "ted", "tee", "teflimmobilfe)-", @@ -3128,9 +3114,6 @@ "worldwide", "x", "x'", - "x+xx", - "x+xxx", - "x-xxxx", "x.", "x.X", "x.X.", @@ -3157,38 +3140,23 @@ "xemoours", "xit", "xx", - "xx-xxxx", "xx.", "xx.x", "xxXxx", "xxx", - "xxx-", "xxx-Xxxxx", "xxx-xxxx", "xxx.", - "xxxd.d", "xxxx", - "xxxx)-", - "xxxx)/xxxx", "xxxx+", - "xxxx+/x", - "xxxx+/xxxx", - "xxxx,dd", - "xxxx-", "xxxx-xx", - "xxxx-xx-xxxx", "xxxx-xxx", "xxxx-xxxx", - "xxxx-xxxx-xxx", - "xxxx-xxxx-xxxx", "xxxx.", - "xxxx\u0308xx", - "xxxx\u0308xxx-xxxx", - "xxxx\u0308xxxx", + "xxxxdd", "xxxx\u2019x", "xxx\u2019x", "xx\u0308x", - "xx\u0308xxxx", "xx\u2019x", "x\u0308xxx", "x\u0308xxxx", @@ -3224,7 +3192,6 @@ "zielallokation", "zielanlagestrategie", "zielausschu\u0308ttung", - "zielaussch\u00fcttung", "zielmarkts", "zielm\u00e4rkte", "zielobjekte", @@ -3279,6 +3246,7 @@ "\u00e4", "\u00e4.", "\u00e4gl", + "\u00e4gt", "\u00e4r.", "\u00e4rzteh\u00e4user", "\u00e4rzteh\u00e4usern", diff --git a/project/backend/spacy-service/spacy_training/training_running.json b/project/backend/spacy-service/spacy_training/training_running.json new file mode 100644 index 0000000..0aaa3c2 --- /dev/null +++ b/project/backend/spacy-service/spacy_training/training_running.json @@ -0,0 +1 @@ +{"running": false} diff --git a/project/docker-compose.yml b/project/docker-compose.yml index bfdd55a..954125f 100644 --- a/project/docker-compose.yml +++ b/project/docker-compose.yml @@ -74,10 +74,6 @@ services: - COORDINATOR_URL=http://coordinator:5000 ports: - 5053:5000 - depends_on: - - coordinator - - validate: build: diff --git a/project/frontend/src/components/KPIForm.tsx b/project/frontend/src/components/KPIForm.tsx index 520c241..bec82f6 100644 --- a/project/frontend/src/components/KPIForm.tsx +++ b/project/frontend/src/components/KPIForm.tsx @@ -1,9 +1,13 @@ -import { Box, Typography, Button, Paper, TextField, FormControlLabel, - Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress } from "@mui/material"; +import { + Box, Typography, Button, Paper, TextField, FormControlLabel, + Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress +} from "@mui/material"; import { useState, useEffect } from "react"; import type { Kennzahl } 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 { mode: 'add' | 'edit'; @@ -11,6 +15,7 @@ interface KPIFormProps { onSave: (data: Partial) => Promise; onCancel: () => void; loading?: boolean; + resetTrigger?: number; } const emptyKPI: Partial = { @@ -21,83 +26,132 @@ const emptyKPI: Partial = { translation: '', example: '', active: true, - exampleText: '', - markedValue: '', + examples: [{ sentence: '', value: '' }], }; -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>(emptyKPI); const [isSaving, setIsSaving] = useState(false); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(""); + const [snackbarSeverity, setSnackbarSeverity] = useState<'success' | 'error' | 'info'>("success"); + useEffect(() => { if (mode === 'edit' && initialData) { setFormData(initialData); - } else { + } else if (mode === 'add') { setFormData(emptyKPI); } }, [mode, initialData]); + useEffect(() => { + if (mode === 'add') { + setFormData(emptyKPI); + } + }, [resetTrigger]); + + + const handleSave = async () => { if (!formData.name?.trim()) { - alert('Name ist erforderlich'); + setSnackbarMessage("Name ist erforderlich"); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + return; } - if (!formData.exampleText?.trim()) { - alert('Beispielsatz ist erforderlich'); + if (!formData.examples || formData.examples.length === 0) { + setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich"); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + return; } - if (!formData.markedValue?.trim()) { - alert('Bezeichneter Wert im Satz ist erforderlich'); - return; + for (const ex of formData.examples) { + if (!ex.sentence?.trim() || !ex.value?.trim()) { + setSnackbarMessage('Alle Beispielsätze müssen vollständig sein.'); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + + return; + } } + setIsSaving(true); try { - const spacyEntry = generateSpacyEntry(formData); + const spacyEntries = generateSpacyEntries(formData); - //in localStorage merken - const stored = localStorage.getItem("spacyData"); - const existingData = stored ? JSON.parse(stored) : []; - const updated = [...existingData, spacyEntry]; - localStorage.setItem("spacyData", JSON.stringify(updated)); + // Für jeden einzelnen Beispielsatz: + for (const entry of spacyEntries) { + // im localStorage speichern (zum Debuggen oder Vorschau) + const stored = localStorage.getItem("spacyData"); + const existingData = stored ? JSON.parse(stored) : []; + const updated = [...existingData, entry]; + localStorage.setItem("spacyData", JSON.stringify(updated)); - // an Flask senden - const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(spacyEntry) + // POST Request an das Flask-Backend + const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + 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(); - console.log("Response von /append-training-entry:", data); - - if (!response.ok) { - throw new Error(data.error || "Fehler beim Aufruf von append-training-entry"); - } - - if (!response.ok) { - throw new Error("Fehler vom Backend: " + response.status); - } - - // anschließend in der Datenbank speichern - await onSave(formData); - - alert("SpaCy-Eintrag erfolgreich gespeichert!"); + setSnackbarMessage("Beispielsätze gespeichert. Jetzt auf ‚Neu trainieren‘ klicken oder weitere Kennzahlen hinzufügen."); + setSnackbarSeverity("success"); + setSnackbarOpen(true); } 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); } finally { setIsSaving(false); } + }; const handleCancel = () => { + setFormData(emptyKPI); onCancel(); }; @@ -105,6 +159,24 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false } 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) { return ( - - - Kennzahl - - updateField('name', e.target.value)} - sx={{ mb: 2 }} - required - error={!formData.name?.trim()} - helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''} - /> - - - - - - - Beispielsatz - - 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" - } - /> - - 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+)" - } - /> - - - - updateField('mandatory', e.target.checked)} - sx={{ color: '#383838' }} - /> - } - label="Erforderlich" + <> + + + + Kennzahl + + updateField('name', e.target.value)} + sx={{ mb: 2 }} + required + error={!formData.name?.trim()} + helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''} /> - - Die Kennzahl erlaubt keine leeren Werte + + + + + + + Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type} + + + Typ + + + + + + {mode === 'add' && ( + <> + + + updateField('active', e.target.checked)} + sx={{ color: '#383838' }} + /> + } + label="Aktiv" + /> + + Die Kennzahl ist aktiv und wird angezeigt + + + + updateField('mandatory', e.target.checked)} + sx={{ color: '#383838' }} + /> + } + label="Erforderlich" + /> + + Die Kennzahl erlaubt keine leeren Werte + + + + )} + + + + {/* Hinweistext vor Beispielsätzen */} + + + Hinweis zur Trainingsqualität + + + Damit das System neue Kennzahlen zuverlässig erkennen kann, empfehlen wir mindestens 5 Beispielsätze zu erstellen – je mehr, desto besser. + + + Wichtig: Neue Kennzahlen werden erst in PDF-Dokumenten erkannt, wenn Sie den Button "Neu trainieren" auf der Konfigurationsseite ausführen. + + + Tipp: Sie können jederzeit weitere Beispielsätze hinzufügen oder vorhandene in der Kennzahlenverwaltung bearbeiten. - - + - - - Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type} - - - Typ - - - - - - - - - Synonyme & Übersetzungen - - updateField('translation', e.target.value)} - helperText="z.B. Englische Übersetzung der Kennzahl" - /> - - - - - - - Beispiele von Kennzahl - - updateField('example', e.target.value)} - helperText="Beispielwerte für diese Kennzahl" - /> - - - {mode === 'add' && ( - <> - - - updateField('active', e.target.checked)} - sx={{ color: '#383838' }} - /> - } - label="Aktiv" - /> - - Die Kennzahl ist aktiv und wird angezeigt - - - - )} - - - + + + setSnackbarOpen(false)} + anchorOrigin={{ vertical: 'top', horizontal: 'center' }} + > + setSnackbarOpen(false)} + severity={snackbarSeverity} + sx={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }} > - Abbrechen - - - - + {snackbarMessage} + + + + + ); } -function generateSpacyEntry(formData: Partial) { - const text = formData.exampleText?.trim() || ""; - const value = formData.markedValue?.trim() || ""; - const label = formData.name?.trim().toUpperCase() || ""; - const start = text.indexOf(value); - if (start === -1) { - throw new Error("Bezeichneter Begriff wurde im Satz nicht gefunden."); - } - - return { - text, - entities: [[start, start + value.length, label]], - }; +function generateSpacyEntries(formData: Partial) { + const label = formData.name?.trim().toUpperCase() || ""; + return (formData.examples || []).map(({ sentence, value }) => { + const start = sentence.indexOf(value); + if (start === -1) { + throw new Error(`"${value}" nicht gefunden in Satz: "${sentence}"`); + } + 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"); -// } + + diff --git a/project/frontend/src/routes/config-add.tsx b/project/frontend/src/routes/config-add.tsx index 26bc90a..15db17a 100644 --- a/project/frontend/src/routes/config-add.tsx +++ b/project/frontend/src/routes/config-add.tsx @@ -5,6 +5,7 @@ import { KPIForm } from "../components/KPIForm"; import type { Kennzahl } from "../types/kpi"; import { API_HOST } from "../util/api"; + export const Route = createFileRoute("/config-add")({ component: ConfigAddPage, validateSearch: (search: Record): { from?: string } => { @@ -47,19 +48,28 @@ function ConfigAddPage() { body: JSON.stringify(kpiData), }); + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - navigate({ to: "/config" }); + navigate({ + to: "/config", + search: { success: "true", ...(from ? { from } : {}) }, + }); + } catch (error) { console.error('Error creating KPI:', error); throw error; } }; + const handleCancel = () => { - navigate({ to: "/config" }); + navigate({ + to: "/config", + search: from ? { from } : undefined, + }); }; return ( @@ -83,7 +93,7 @@ function ConfigAddPage() { > - + Neue Kennzahl hinzufügen @@ -93,9 +103,10 @@ function ConfigAddPage() { ); -} \ No newline at end of file +} diff --git a/project/frontend/src/routes/config-detail.$kpiId.tsx b/project/frontend/src/routes/config-detail.$kpiId.tsx index 04ef98c..425ff0d 100644 --- a/project/frontend/src/routes/config-detail.$kpiId.tsx +++ b/project/frontend/src/routes/config-detail.$kpiId.tsx @@ -1,5 +1,6 @@ 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"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import { useEffect, useState } from "react"; @@ -38,6 +39,7 @@ function KPIDetailPage() { try { setLoading(true); const response = await fetch(`${API_HOST}/api/kpi_setting/${kpiId}`); + if (!response.ok) { if (response.status === 404) { setError('KPI not found'); @@ -72,7 +74,6 @@ function KPIDetailPage() { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - const updatedKennzahl = await response.json(); setKennzahl(updatedKennzahl); setIsEditing(false); @@ -82,6 +83,7 @@ function KPIDetailPage() { } }; + const handleCancel = () => { setIsEditing(false); }; @@ -153,7 +155,7 @@ function KPIDetailPage() { > - + Detailansicht @@ -192,18 +194,13 @@ function KPIDetailPage() { - - Beschreibung + + Erforderlich: - - {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'} - - - Erforderlich: {kennzahl.mandatory ? 'Ja' : 'Nein'} - - @@ -216,28 +213,6 @@ function KPIDetailPage() { {typeDisplayMapping[kennzahl.type] || kennzahl.type} - - - - - - Synonyme & Übersetzungen - - - {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."} - - - - - - - - Beispiele von Kennzahl - - - {kennzahl.example || "Zurzeit gibt es keine Beispiele der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."} - - ); @@ -264,7 +239,7 @@ function KPIDetailPage() { > - + Kennzahl bearbeiten @@ -280,4 +255,4 @@ function KPIDetailPage() { /> ); -} \ No newline at end of file +} diff --git a/project/frontend/src/routes/config.tsx b/project/frontend/src/routes/config.tsx index ac64c3d..6f94090 100644 --- a/project/frontend/src/routes/config.tsx +++ b/project/frontend/src/routes/config.tsx @@ -3,18 +3,65 @@ import { Box, Button, IconButton, Typography } from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import { useNavigate } from "@tanstack/react-router"; 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")({ component: ConfigPage, - validateSearch: (search: Record): { from?: string } => { - const from = typeof search.from === "string" ? search.from : undefined; - return { from }; + validateSearch: (search: Record): { from?: string; success?: string } => { + return { + from: typeof search.from === "string" ? search.from : undefined, + success: typeof search.success === "string" ? search.success : undefined + }; } + }); function ConfigPage() { const navigate = useNavigate(); - const { from } = Route.useSearch(); + const { from, success } = Route.useSearch(); + const [snackbarOpen, setSnackbarOpen] = useState(success === "true"); + const [snackbarMessage, setSnackbarMessage] = useState("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 = () => { 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 ( - + <> - - - - - - Konfiguration der Kennzahlen - - - + {/* Linke Seite: Zurück & Titel */} + + + + + + Konfiguration der Kennzahlen + + + + {/* Rechte Seite: Buttons */} + + + + + + + + {/* Tabelle */} + + + - - - - + + {/* Snackbar */} + setSnackbarOpen(false)} + anchorOrigin={{ vertical: "top", horizontal: "center" }} + > + setSnackbarOpen(false)} + severity="success" + sx={{ width: "100%" }} + > + {snackbarMessage} + + + ); } \ No newline at end of file diff --git a/project/frontend/src/types/kpi.ts b/project/frontend/src/types/kpi.ts index 766fb0b..fed5c54 100644 --- a/project/frontend/src/types/kpi.ts +++ b/project/frontend/src/types/kpi.ts @@ -10,6 +10,10 @@ export interface Kennzahl { active: boolean; exampleText?: string; markedValue?: string; + examples?: { + sentence: string; + value: string; + }[]; } export const typeDisplayMapping: Record = {