Formatierung durch black, extract-Funktion bereinigt
parent
77d169633e
commit
fd8bfa3952
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ spacy-transformers==1.3.3
|
|||
transformers==4.35.2
|
||||
torch
|
||||
flask
|
||||
requests
|
||||
requests
|
||||
flask-cors
|
||||
|
|
@ -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"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
|
@ -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À
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
{"running": false}
|
||||
|
|
@ -74,10 +74,6 @@ services:
|
|||
- COORDINATOR_URL=http://coordinator:5000
|
||||
ports:
|
||||
- 5053:5000
|
||||
depends_on:
|
||||
- coordinator
|
||||
|
||||
|
||||
|
||||
validate:
|
||||
build:
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
// }
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,6 +10,10 @@ export interface Kennzahl {
|
|||
active: boolean;
|
||||
exampleText?: string;
|
||||
markedValue?: string;
|
||||
examples?: {
|
||||
sentence: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const typeDisplayMapping: Record<string, string> = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue