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 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)

View File

@ -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

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 --no-cache-dir -r requirements.txt
RUN pip install flask-cors
RUN python -m spacy download en_core_web_sm

View File

@ -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)

View File

@ -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)
return json.dumps(results, indent=2, ensure_ascii=False)

View File

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

View File

@ -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"
]
]
}
]

View File

@ -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__":

View File

@ -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

View File

@ -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
}
]
}

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",
"+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",

View File

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

View File

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

View File

@ -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<Kennzahl>) => Promise<void>;
onCancel: () => void;
loading?: boolean;
resetTrigger?: number;
}
const emptyKPI: Partial<Kennzahl> = {
@ -21,83 +26,132 @@ const emptyKPI: Partial<Kennzahl> = {
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<Partial<Kennzahl>>(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 (
<Box
@ -123,218 +195,221 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
}
return (
<Paper
elevation={2}
sx={{
width: "90%",
maxWidth: 800,
p: 4,
borderRadius: 2,
backgroundColor: "white"
}}
>
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Kennzahl
</Typography>
<TextField
fullWidth
label="Name *"
value={formData.name || ''}
onChange={(e) => updateField('name', e.target.value)}
sx={{ mb: 2 }}
required
error={!formData.name?.trim()}
helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''}
/>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
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"
<>
<Paper
elevation={2}
sx={{
width: "90%",
maxWidth: 800,
p: 4,
borderRadius: 2,
backgroundColor: "white"
}}
>
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Kennzahl
</Typography>
<TextField
fullWidth
label="Name *"
placeholder="z.B. IRR"
value={formData.name || ''}
onChange={(e) => updateField('name', e.target.value)}
sx={{ mb: 2 }}
required
error={!formData.name?.trim()}
helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''}
/>
<Typography variant="body2" color="text.secondary" ml={4}>
Die Kennzahl erlaubt keine leeren Werte
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type}
</Typography>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Typ</InputLabel>
<Select
value={formData.type || 'string'}
label="Typ"
onChange={(e) => updateField('type', e.target.value)}
>
<MenuItem value="string">Text</MenuItem>
<MenuItem value="number">Zahl</MenuItem>
<MenuItem value="date">Datum</MenuItem>
<MenuItem value="boolean">Ja/Nein</MenuItem>
<MenuItem value="array">Liste (mehrfach)</MenuItem>
</Select>
</FormControl>
</Box>
{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>
</Box>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<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)}
<Typography variant="h6" fontWeight="bold" mb={2}>
Beispielsätze
</Typography>
{(formData.examples || []).map((ex, idx) => (
<Box key={idx} sx={{ mb: 2, border: '1px solid #ccc', p: 2, borderRadius: 1 }}>
<TextField
fullWidth
multiline
label={`Beispielsatz ${idx + 1}`}
placeholder="z.B. Die IRR beträgt 7,8 %"
value={ex.sentence}
onChange={(e) => updateExample(idx, 'sentence', e.target.value)}
required
sx={{ mb: 1 }}
/>
<TextField
fullWidth
label="Bezeichneter Wert im Satz"
placeholder="z.B. 7,8 %"
value={ex.value}
onChange={(e) => updateExample(idx, 'value', e.target.value)}
required
/>
{(formData.examples?.length || 0) > 1 && (
<Button onClick={() => removeExample(idx)} sx={{ mt: 1 }} color="error">
Entfernen
</Button>
)}
</Box>
))}
<Button variant="outlined" onClick={addExample}>
+ Beispielsatz hinzufügen
</Button>
</Box>
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}>
<Button
variant="outlined"
onClick={handleCancel}
disabled={isSaving}
sx={{
borderColor: "#383838",
color: "#383838",
"&:hover": { borderColor: "#2e2e2e", backgroundColor: "#f5f5f5" }
}}
>
<MenuItem value="string">Text</MenuItem>
<MenuItem value="number">Zahl</MenuItem>
<MenuItem value="date">Datum</MenuItem>
<MenuItem value="boolean">Ja/Nein</MenuItem>
<MenuItem value="array">Liste (mehrfach)</MenuItem>
</Select>
</FormControl>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Synonyme & Übersetzungen
</Typography>
<TextField
fullWidth
label="Übersetzung"
value={formData.translation || ''}
onChange={(e) => updateField('translation', e.target.value)}
helperText="z.B. Englische Übersetzung der Kennzahl"
/>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beispiele von Kennzahl
</Typography>
<TextField
fullWidth
multiline
rows={2}
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
</Button>
<Button
variant="contained"
onClick={handleSave}
disabled={isSaving || !formData.name?.trim()}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
{isSaving ? (
<>
<CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
{mode === 'add' ? 'Hinzufügen...' : 'Speichern...'}
</>
) : (
mode === 'add' ? 'Hinzufügen' : 'Speichern'
)}
</Button>
</Box>
</Paper>
<Snackbar
open={snackbarOpen}
autoHideDuration={5000}
onClose={() => setSnackbarOpen(false)}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<MuiAlert
elevation={6}
variant="filled"
onClose={() => setSnackbarOpen(false)}
severity={snackbarSeverity}
sx={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
>
Abbrechen
</Button>
<Button
variant="contained"
onClick={handleSave}
disabled={isSaving || !formData.name?.trim()}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
{isSaving ? (
<>
<CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
{mode === 'add' ? 'Hinzufügen...' : 'Speichern...'}
</>
) : (
mode === 'add' ? 'Hinzufügen' : 'Speichern'
)}
</Button>
</Box>
</Paper>
<span>{snackbarMessage}</span>
<Button color="inherit" size="small" onClick={() => setSnackbarOpen(false)}>
OK
</Button>
</MuiAlert>
</Snackbar>
</>
);
}
function generateSpacyEntry(formData: Partial<Kennzahl>) {
const text = formData.exampleText?.trim() || "";
const value = formData.markedValue?.trim() || "";
const label = formData.name?.trim().toUpperCase() || "";
const start = text.indexOf(value);
if (start === -1) {
throw new Error("Bezeichneter Begriff wurde im Satz nicht gefunden.");
}
return {
text,
entities: [[start, start + value.length, label]],
};
function generateSpacyEntries(formData: Partial<Kennzahl>) {
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");
// }

View File

@ -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<string, unknown>): { 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() {
>
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Neue Kennzahl hinzufügen
@ -93,9 +103,10 @@ function ConfigAddPage() {
<KPIForm
mode="add"
key={Date.now()}
onSave={handleSave}
onCancel={handleCancel}
/>
</Box>
);
}
}

View File

@ -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() {
>
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Detailansicht
@ -192,18 +194,13 @@ function KPIDetailPage() {
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beschreibung
<Typography variant="h6" fontWeight="bold" mb={1}>
Erforderlich:
</Typography>
<Typography variant="body1" color="text.secondary">
{kennzahl.description || "Zurzeit ist die Beschreibung der Kennzahl leer. Klicken Sie auf den Bearbeiten-Button, um die Beschreibung zu ergänzen."}
<Typography variant="body1" sx={{ mb: 2, fontSize: 16 }}>
{kennzahl.mandatory ? 'Ja' : 'Nein'}
</Typography>
<Box mt={2}>
<Typography variant="body2" color="text.secondary">
<strong>Erforderlich:</strong> {kennzahl.mandatory ? 'Ja' : 'Nein'}
</Typography>
</Box>
</Box>
<Divider sx={{ my: 3 }} />
@ -216,28 +213,6 @@ function KPIDetailPage() {
{typeDisplayMapping[kennzahl.type] || kennzahl.type}
</Typography>
</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>
</Box>
);
@ -264,7 +239,7 @@ function KPIDetailPage() {
>
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Kennzahl bearbeiten
@ -280,4 +255,4 @@ function KPIDetailPage() {
/>
</Box>
);
}
}

View File

@ -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<string, unknown>): { from?: string } => {
const from = typeof search.from === "string" ? search.from : undefined;
return { from };
validateSearch: (search: Record<string, unknown>): { 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<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 = () => {
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 (
<Box
minHeight="100vh"
width="100vw"
bgcolor="white"
display="flex"
flexDirection="column"
alignItems="center"
pt={3}
pb={4}
>
<>
<Box
width="100%"
minHeight="100vh"
width="100vw"
bgcolor="white"
display="flex"
justifyContent="space-between"
flexDirection="column"
alignItems="center"
px={4}
pt={3}
pb={4}
>
<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>
<Button
variant="contained"
onClick={handleAddNewKPI}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
<Box
width="100%"
display="flex"
justifyContent="space-between"
alignItems="center"
px={4}
>
Neue Kennzahl hinzufügen
</Button>
{/* Linke Seite: Zurück & Titel */}
<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 sx={{ width: "100%", mt: 4, display: "flex", justifyContent: "center" }}>
<ConfigTable from={from} />
</Box>
</Box>
{/* Snackbar */}
<Snackbar
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;
exampleText?: string;
markedValue?: string;
examples?: {
sentence: string;
value: string;
}[];
}
export const typeDisplayMapping: Record<string, string> = {