refactor: Umsetzungsbewertung hängt an Kette statt Vorlage

- Neues Feld kette_id in ki_bewertungen für umsetzung_match
- 3.757 bestehende Bewertungen migriert
- API: Neueste Version + ältere Versionen getrennt im Response
- Explorer: Umsetzungsgrad in Panel 2 (Kette), nicht mehr in Panel 3 (Vorlage)
- 'Vorherige Bewertungen' Button aufklappbar mit Score + Begründung + Zeitstempel
This commit is contained in:
Dotty Dotter 2026-04-01 23:42:02 +02:00
parent c5939a6873
commit e35dab8f7d
4 changed files with 60 additions and 50 deletions

View File

@ -124,6 +124,7 @@ class KetteDetail(BaseModel):
strang: str | None = None strang: str | None = None
ampel: dict | None = None ampel: dict | None = None
umsetzung: dict | None = None umsetzung: dict | None = None
umsetzung_versionen: list[dict] | None = None
class PaginatedVorlagen(BaseModel): class PaginatedVorlagen(BaseModel):

View File

@ -273,12 +273,13 @@ def _run_ketten_bewertung(kette_id: int, anmerkung: str, job_id: str):
_jobs[job_id] = {"status": "error", "error": str(result)} _jobs[job_id] = {"status": "error", "error": str(result)}
return return
# Keep old versions, insert new # Keep old versions, insert new (linked to kette, not vorlage)
conn.execute( conn.execute(
"""INSERT INTO ki_bewertungen (vorlage_id, typ, score, begruendung, anmerkungen, modell, prompt_version, erstellt_at) """INSERT INTO ki_bewertungen (vorlage_id, kette_id, typ, score, begruendung, anmerkungen, modell, prompt_version, erstellt_at)
VALUES (?, 'umsetzung_match', ?, ?, ?, 'qwen-plus-latest', 'v2-reeval', ?)""", VALUES (?, ?, 'umsetzung_match', ?, ?, ?, 'qwen-plus-latest', 'v2-reeval', ?)""",
( (
kette["ursprung_id"], kette["ursprung_id"],
kette_id,
result.get("score"), result.get("score"),
result.get("begruendung"), result.get("begruendung"),
json.dumps(result, ensure_ascii=False), json.dumps(result, ensure_ascii=False),

View File

@ -191,32 +191,37 @@ def get_kette(kette_id: int, conn=Depends(_db)):
# Graph/Perlenschnur data # Graph/Perlenschnur data
graph = get_kette_graph(conn, kette_id) graph = get_kette_graph(conn, kette_id)
# Umsetzungsbewertung (neueste KI-Bewertung für Ursprungsvorlage) # Umsetzungsbewertung (alle Versionen für diese Kette, neueste zuerst)
umsetzung = None umsetzung = None
if row["ursprung_id"]: umsetzung_versionen = []
ub_row = conn.execute( import json as _json
ub_rows = conn.execute(
"""SELECT score, begruendung, anmerkungen, erstellt_at, prompt_version """SELECT score, begruendung, anmerkungen, erstellt_at, prompt_version
FROM ki_bewertungen FROM ki_bewertungen
WHERE vorlage_id = ? AND typ = 'umsetzung_match' WHERE kette_id = ? AND typ = 'umsetzung_match'
ORDER BY id DESC LIMIT 1""", ORDER BY id DESC""",
(row["ursprung_id"],), (kette_id,),
).fetchone() ).fetchall()
if ub_row: for i, ub_row in enumerate(ub_rows):
import json as _json
details = {} details = {}
if ub_row["anmerkungen"]: if ub_row["anmerkungen"]:
try: try:
details = _json.loads(ub_row["anmerkungen"]) details = _json.loads(ub_row["anmerkungen"])
except Exception: except Exception:
pass pass
umsetzung = { entry = {
"score": ub_row["score"], "score": ub_row["score"],
"bewertung": details.get("bewertung", ""), "bewertung": details.get("bewertung", ""),
"begruendung": ub_row["begruendung"], "begruendung": ub_row["begruendung"],
"kernpunkt_erfuellt": details.get("kernpunkt_erfuellt"), "kernpunkt_erfuellt": details.get("kernpunkt_erfuellt"),
"details": details.get("details", ""), "details": details.get("details", ""),
"erstellt_at": ub_row["erstellt_at"], "erstellt_at": ub_row["erstellt_at"],
"prompt_version": ub_row["prompt_version"],
} }
if i == 0:
umsetzung = entry
else:
umsetzung_versionen.append(entry)
strang = row["strang"] strang = row["strang"]
ampel_data = get_ampel(strang or "", row["status"] or "") ampel_data = get_ampel(strang or "", row["status"] or "")
@ -244,4 +249,5 @@ def get_kette(kette_id: int, conn=Depends(_db)):
strang=strang, strang=strang,
ampel=ampel_data, ampel=ampel_data,
umsetzung=umsetzung, umsetzung=umsetzung,
umsetzung_versionen=umsetzung_versionen if umsetzung_versionen else None,
) )

View File

@ -31,6 +31,7 @@
let mobileTab = $state<'liste' | 'kette' | 'detail'>('liste'); let mobileTab = $state<'liste' | 'kette' | 'detail'>('liste');
let showVolltext = $state(false); let showVolltext = $state(false);
let showVersionen = $state(false); let showVersionen = $state(false);
let showUmsetzungVersionen = $state(false);
let showReeval = $state(false); let showReeval = $state(false);
let reevalAnmerkung = $state(''); let reevalAnmerkung = $state('');
let reevalStatus = $state<'idle' | 'running' | 'done' | 'error'>('idle'); let reevalStatus = $state<'idle' | 'running' | 'done' | 'error'>('idle');
@ -364,6 +365,28 @@
{#if u.details} {#if u.details}
<p class="text-[10px] text-gray-500 mt-1 leading-snug">{u.details}</p> <p class="text-[10px] text-gray-500 mt-1 leading-snug">{u.details}</p>
{/if} {/if}
{#if selectedKette.umsetzung_versionen?.length}
<button onclick={() => showUmsetzungVersionen = !showUmsetzungVersionen}
class="text-[10px] text-gray-400 hover:text-gray-600 mt-2 flex items-center gap-1">
<span>{showUmsetzungVersionen ? '▼' : '▶'}</span>
{selectedKette.umsetzung_versionen.length} vorherige Bewertung{selectedKette.umsetzung_versionen.length > 1 ? 'en' : ''}
</button>
{#if showUmsetzungVersionen}
<div class="mt-2 space-y-2">
{#each selectedKette.umsetzung_versionen as v}
<div class="rounded border border-gray-200 bg-gray-50 p-2">
<div class="flex items-center gap-2 mb-1">
<span class="text-[10px] font-bold {v.score >= 0.7 ? 'text-green-700' : v.score >= 0.4 ? 'text-amber-700' : 'text-red-700'}">
{Math.round((v.score || 0) * 100)}%
</span>
<span class="text-[10px] text-gray-400">{v.erstellt_at || ''}</span>
</div>
<p class="text-[10px] text-gray-500">{v.begruendung}</p>
</div>
{/each}
</div>
{/if}
{/if}
</div> </div>
{/if} {/if}
@ -529,28 +552,7 @@
</div> </div>
{/if} {/if}
<!-- Umsetzungsbewertung --> <!-- Umsetzungsbewertung → jetzt in der Ketten-Ansicht (Panel 2) -->
{#if selectedVorlage.umsetzungsbewertungen?.length}
<div class="rounded-xl border border-gray-200 p-5">
<h3 class="text-sm font-semibold text-gray-900 mb-3">📊 Umsetzungsbewertung</h3>
{#each selectedVorlage.umsetzungsbewertungen as ub}
<div class="p-3 rounded-lg border {ub.score >= 0.7 ? 'border-green-200 bg-green-50' : ub.score >= 0.4 ? 'border-amber-200 bg-amber-50' : 'border-red-200 bg-red-50'}">
<div class="flex items-center gap-3 mb-1.5">
<div class="w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold
{ub.score >= 0.7 ? 'bg-green-200 text-green-800' : ub.score >= 0.4 ? 'bg-amber-200 text-amber-800' : 'bg-red-200 text-red-800'}">
{Math.round((ub.score || 0) * 100)}%
</div>
<span class="text-sm font-medium {ub.score >= 0.7 ? 'text-green-800' : ub.score >= 0.4 ? 'text-amber-800' : 'text-red-800'}">
{ub.score >= 0.7 ? 'Weitgehend umgesetzt' : ub.score >= 0.4 ? 'Teilweise umgesetzt' : 'Kaum umgesetzt'}
</span>
</div>
{#if ub.begruendung}
<p class="text-xs text-gray-700">{ub.begruendung}</p>
{/if}
</div>
{/each}
</div>
{/if}
<!-- Neu bewerten --> <!-- Neu bewerten -->
<div class="rounded-xl border border-gray-200 p-4"> <div class="rounded-xl border border-gray-200 p-4">