Files
bmp-project/index.html
syntaxbullet 99569d2cf3 initial commit
2026-05-13 17:26:13 +02:00

917 lines
30 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BMP Bewerbungscheck</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #071225;
--bg-deep: #000a2d;
--surface: rgba(10, 24, 50, 0.9);
--surface-soft: rgba(16, 34, 68, 0.9);
--surface-blue: rgba(18, 49, 96, 0.82);
--border: rgba(223, 232, 242, 0.16);
--border-strong: rgba(223, 232, 242, 0.34);
--text: #ffffff;
--muted: #b9c7d6;
--soft: #dfe8f2;
--accent: #ffd21f;
--accent-2: #f5b90b;
--accent-soft: rgba(255, 210, 31, 0.14);
--green: #22c55e;
--yellow: #f59e0b;
--red: #ef4444;
--shadow: 0 24px 70px rgba(0, 0, 0, 0.34);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 15px;
}
html { background: var(--bg); }
body {
min-height: 100vh;
background:
radial-gradient(circle at 18% 0%, rgba(255,255,255,0.12), transparent 32%),
linear-gradient(120deg, rgba(0, 10, 45, .96), rgba(7, 35, 77, .92)),
linear-gradient(30deg, transparent 0 44%, rgba(255,255,255,.08) 45% 47%, transparent 48% 100%),
var(--bg);
color: var(--text);
padding: 34px;
}
.app {
width: min(1120px, 100%);
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 26px;
animation: pageIn 0.38s ease-out both;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
gap: 28px;
align-items: end;
}
.brand-mark {
width: min(330px, 100%);
display: block;
margin: 8px auto 0;
border-radius: 0;
}
.eyebrow {
color: var(--accent);
font-size: 12px;
font-weight: 750;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 12px;
}
h1 {
max-width: 760px;
font-size: clamp(30px, 5vw, 54px);
font-weight: 760;
line-height: 1.02;
letter-spacing: 0;
color: var(--text);
}
.sub {
color: var(--soft);
font-size: clamp(16px, 2vw, 18px);
line-height: 1.6;
max-width: 720px;
margin-top: 16px;
}
.promise {
background: rgba(6, 18, 43, 0.74);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 18px;
display: grid;
gap: 12px;
}
.promise h2, .panel h2, .progress-card h2 {
color: var(--soft);
font-size: 13px;
font-weight: 750;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.promise-item {
display: grid;
grid-template-columns: 28px 1fr;
gap: 10px;
color: var(--soft);
font-size: 14px;
line-height: 1.45;
}
.icon {
width: 28px;
height: 28px;
border-radius: 999px;
background: var(--accent-soft);
color: var(--accent);
display: grid;
place-items: center;
font-weight: 760;
flex: none;
}
.layout {
display: grid;
grid-template-columns: minmax(320px, 430px) minmax(0, 1fr);
gap: 20px;
align-items: start;
}
.panel, .progress-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 24px;
min-width: 0;
backdrop-filter: blur(12px);
}
.panel {
display: flex;
flex-direction: column;
gap: 18px;
position: sticky;
top: 24px;
}
.panel-intro {
color: var(--muted);
line-height: 1.55;
font-size: 14px;
}
input[type="file"] { display: none; }
.drop-zone {
min-height: 220px;
border: 2px dashed var(--border-strong);
border-radius: 8px;
background: var(--surface-soft);
display: grid;
place-items: center;
text-align: center;
padding: 28px;
cursor: pointer;
transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
}
.drop-zone:hover, .drop-zone.over {
border-color: var(--accent);
background: var(--surface-blue);
transform: translateY(-1px);
box-shadow: 0 14px 36px rgba(255, 210, 31, 0.12);
}
.drop-zone.has-file {
border-color: rgba(34,197,94,0.72);
background: rgba(20, 83, 45, 0.34);
}
.drop-copy {
display: grid;
justify-items: center;
gap: 9px;
color: var(--soft);
line-height: 1.4;
}
.drop-title {
color: var(--text);
font-size: 18px;
font-weight: 750;
}
.drop-help {
color: var(--muted);
font-size: 13px;
}
.file-name {
color: var(--green);
font-weight: 700;
overflow-wrap: anywhere;
}
.actions {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
button {
min-height: 46px;
padding: 12px 16px;
border-radius: 7px;
border: 1px solid transparent;
background: var(--accent);
color: #08111f;
font-size: 15px;
font-weight: 720;
cursor: pointer;
transition: background 0.14s ease, transform 0.14s ease, box-shadow 0.14s ease, border-color 0.14s ease;
}
button:hover:not(:disabled) {
background: var(--accent-2);
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(255, 210, 31, 0.2);
}
button:disabled {
opacity: 0.48;
cursor: not-allowed;
}
.status {
min-height: 22px;
color: var(--muted);
font-size: 14px;
line-height: 1.45;
}
.progress-card {
display: flex;
flex-direction: column;
gap: 18px;
min-height: 650px;
}
.steps {
display: grid;
gap: 10px;
}
.step {
display: grid;
grid-template-columns: 34px 1fr auto;
gap: 12px;
align-items: center;
padding: 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-soft);
color: var(--muted);
transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
}
.step.active {
border-color: rgba(255,210,31,0.46);
background: var(--surface-blue);
transform: translateX(2px);
}
.step.done { background: rgba(20, 83, 45, 0.34); border-color: rgba(34,197,94,0.32); }
.step.error { background: rgba(127, 29, 29, 0.34); border-color: rgba(239,68,68,0.38); }
.dot {
width: 28px;
height: 28px;
border-radius: 999px;
background: #e2e8f0;
color: #172033;
display: grid;
place-items: center;
font-size: 13px;
font-weight: 760;
transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.step.active .dot {
background: var(--accent);
color: #08111f;
box-shadow: 0 0 0 6px rgba(255,210,31,0.14);
animation: pulse 1.25s ease-in-out infinite;
}
.step.done .dot { background: var(--green); color: #06121f; }
.step.error .dot { background: var(--red); color: white; }
.step-title {
display: grid;
gap: 3px;
}
.step strong {
color: var(--text);
font-size: 14px;
font-weight: 720;
}
.step small {
color: var(--muted);
line-height: 1.35;
}
.step-state {
color: var(--muted);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.step-detail {
grid-column: 2 / -1;
display: none;
gap: 8px;
border-top: 1px solid rgba(255,210,31,0.16);
padding-top: 10px;
color: var(--soft);
font-size: 12px;
line-height: 1.45;
}
.step-detail.visible {
display: grid;
}
.llm-progress-track {
height: 7px;
border-radius: 999px;
background: rgba(223,232,242,0.14);
overflow: hidden;
}
.llm-progress-fill {
width: 0%;
height: 100%;
border-radius: inherit;
background: var(--accent);
transition: width 0.22s ease;
}
.llm-progress-meta {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.llm-progress-current {
color: var(--muted);
}
.empty {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-soft);
padding: 22px;
color: var(--soft);
display: grid;
gap: 16px;
}
.empty h3 {
color: var(--text);
font-size: 20px;
line-height: 1.2;
}
.empty ul {
display: grid;
gap: 10px;
list-style: none;
}
.empty li {
display: grid;
grid-template-columns: 24px 1fr;
gap: 10px;
align-items: start;
line-height: 1.45;
}
.empty li::before {
content: "✓";
width: 24px;
height: 24px;
border-radius: 999px;
background: rgba(34,197,94,0.16);
color: var(--green);
display: grid;
place-items: center;
font-weight: 800;
font-size: 12px;
}
.result {
display: grid;
gap: 18px;
}
.result-head {
border: 1px solid var(--border);
border-radius: 8px;
background: linear-gradient(180deg, rgba(18, 49, 96, 0.92), rgba(10, 24, 50, 0.92));
padding: 20px;
display: grid;
gap: 12px;
}
.result-head-main {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 18px;
align-items: center;
}
.result-head h3 {
font-size: 24px;
line-height: 1.18;
}
.traffic-signal {
display: inline-grid;
grid-template-columns: auto 1fr;
gap: 10px;
align-items: center;
justify-self: end;
color: var(--soft);
font-size: 13px;
font-weight: 760;
}
.traffic-signal__body {
display: grid;
gap: 5px;
width: 28px;
padding: 5px;
border-radius: 999px;
background: #020617;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12), 0 12px 28px rgba(15, 23, 42, 0.24);
}
.traffic-signal__lamp {
width: 18px;
height: 18px;
border-radius: 999px;
background: #64748b;
opacity: 0.32;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.32);
}
.traffic-signal--gruen .traffic-signal__lamp--gruen {
background: #16a34a;
opacity: 1;
box-shadow: 0 0 0 3px rgba(22,163,74,0.18), 0 0 16px rgba(22,163,74,0.45);
}
.traffic-signal--gelb .traffic-signal__lamp--gelb {
background: #f59e0b;
opacity: 1;
box-shadow: 0 0 0 3px rgba(245,158,11,0.2), 0 0 16px rgba(245,158,11,0.5);
}
.traffic-signal--rot .traffic-signal__lamp--rot {
background: #dc2626;
opacity: 1;
box-shadow: 0 0 0 3px rgba(220,38,38,0.18), 0 0 16px rgba(220,38,38,0.45);
}
.summary {
border: 1px solid var(--border);
background: var(--surface);
border-radius: 8px;
padding: 18px;
color: var(--soft);
font-size: 15px;
line-height: 1.65;
white-space: pre-wrap;
}
.downloads {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-soft);
padding: 16px;
display: grid;
gap: 12px;
}
.download-title {
color: var(--text);
font-weight: 760;
}
.download-links {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.download-links a {
display: inline-flex;
align-items: center;
min-height: 38px;
color: #08111f;
text-decoration: none;
border-radius: 7px;
padding: 9px 12px;
background: var(--accent);
font-size: 14px;
font-weight: 700;
transition: background 0.14s ease, transform 0.14s ease, box-shadow 0.14s ease;
}
.download-links a:hover {
background: var(--accent-2);
transform: translateY(-1px);
box-shadow: 0 10px 22px rgba(37,99,235,0.18);
}
.reveal {
opacity: 0;
transform: translateY(10px);
max-height: 0;
overflow: hidden;
transition: opacity 0.24s ease, transform 0.24s ease, max-height 0.28s ease;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
max-height: 1800px;
}
.reveal.is-exiting {
opacity: 0;
transform: translateY(8px);
max-height: 0;
}
@keyframes pageIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.12); }
}
@media (max-width: 900px) {
body { padding: 20px; }
.hero, .layout { grid-template-columns: 1fr; }
.panel { position: static; }
.progress-card { min-height: auto; }
}
@media (max-width: 560px) {
body { padding: 14px; }
.panel, .progress-card, .promise { padding: 16px; }
.actions { grid-template-columns: 1fr; }
.step {
grid-template-columns: 30px 1fr;
align-items: start;
}
.step-state {
grid-column: 2;
white-space: normal;
}
.result-head-main {
grid-template-columns: 1fr;
}
.traffic-signal {
justify-self: start;
}
.drop-zone { min-height: 190px; padding: 22px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
</style>
</head>
<body>
<main class="app">
<header class="hero">
<div>
<p class="eyebrow">Bewerbungscheck</p>
<h1>PDF hochladen. Einschätzung und Bericht erhalten.</h1>
<p class="sub">Dieses Werkzeug liest einen ausgefüllten Fragebogen aus, fasst die wichtigsten Angaben zusammen und erstellt eine klare Einschätzung mit passenden Exporten.</p>
</div>
<aside class="promise" aria-label="Was Sie erhalten">
<h2>Was Sie erhalten</h2>
<div class="promise-item"><span class="icon">1</span><span>Eine verständliche Zusammenfassung des Unternehmens.</span></div>
<div class="promise-item"><span class="icon">2</span><span>Eine Ampel-Einschätzung als Entscheidungshilfe.</span></div>
<div class="promise-item"><span class="icon">3</span><span>Einen Bericht und Rohdaten zum Speichern oder Weitergeben.</span></div>
</aside>
</header>
<div class="layout">
<section class="panel" aria-labelledby="upload-title">
<div>
<h2 id="upload-title">Start</h2>
<p class="panel-intro">Wählen Sie den ausgefüllten PDF-Fragebogen aus. Danach starten Sie die Auswertung mit einem Klick.</p>
</div>
<div class="drop-zone" id="drop-zone">
<input type="file" id="file-input" accept=".pdf,application/pdf" />
<div class="drop-copy">
<span class="icon"></span>
<span class="drop-title" id="drop-label">PDF auswählen oder hier ablegen</span>
<span class="drop-help">Es wird nur eine PDF-Datei benötigt.</span>
</div>
</div>
<div class="actions">
<button id="run-btn" disabled>Auswertung starten</button>
</div>
<p class="status" id="status">Bereit, sobald eine PDF ausgewählt wurde.</p>
<img class="brand-mark" src="/assets/bmp-logo.png" alt="Der Bayerische Mittelstandspreis 2026" />
</section>
<section class="progress-card" aria-labelledby="progress-title">
<h2 id="progress-title">Aktueller Stand</h2>
<div class="steps">
<div class="step" id="step-upload">
<div class="dot">1</div>
<div class="step-title"><strong>Datei erhalten</strong><small>Die PDF wird übernommen.</small></div>
<span class="step-state">wartet</span>
</div>
<div class="step" id="step-extract">
<div class="dot">2</div>
<div class="step-title"><strong>Angaben lesen</strong><small>Die Antworten werden aus dem Fragebogen ausgelesen.</small></div>
<span class="step-state">wartet</span>
</div>
<div class="step" id="step-mask">
<div class="dot">3</div>
<div class="step-title"><strong>Datenschutz anwenden</strong><small>Personenbezogene Angaben werden reduziert.</small></div>
<span class="step-state">wartet</span>
</div>
<div class="step" id="step-summarize">
<div class="dot">4</div>
<div class="step-title"><strong>Einschätzung erstellen</strong><small>Die wichtigsten Punkte werden zusammengefasst und eingeordnet.</small></div>
<span class="step-state">wartet</span>
<div class="step-detail" id="llm-progress" aria-live="polite">
<div class="llm-progress-track"><div class="llm-progress-fill" id="llm-progress-fill"></div></div>
<div class="llm-progress-meta">
<span id="llm-progress-count">LLM-Aufrufe werden vorbereitet.</span>
<span id="llm-progress-time"></span>
</div>
<div class="llm-progress-current" id="llm-progress-current"></div>
</div>
</div>
<div class="step" id="step-export">
<div class="dot">5</div>
<div class="step-title"><strong>Downloads vorbereiten</strong><small>Bericht und Rohdaten werden bereitgestellt.</small></div>
<span class="step-state">wartet</span>
</div>
</div>
<div id="empty-state" class="empty reveal is-visible">
<h3>Nach der Auswertung sehen Sie hier das Ergebnis.</h3>
<ul>
<li>Eine kurze Zusammenfassung, damit Sie schnell verstehen, worum es geht.</li>
<li>Eine Ampel-Einschätzung für den schnellen Überblick.</li>
<li>Downloads für den Bericht, die Tabelle und die Rohdaten.</li>
</ul>
</div>
<div class="result reveal" id="result-section" hidden>
<div class="result-head">
<div class="result-head-main">
<div>
<h3>Die Auswertung ist fertig.</h3>
<p class="panel-intro">Prüfen Sie die Zusammenfassung und laden Sie den Bericht oder die Rohdaten herunter.</p>
</div>
<div class="traffic-signal" id="result-traffic" aria-label="Ampelbewertung">
<div class="traffic-signal__body">
<span class="traffic-signal__lamp traffic-signal__lamp--rot"></span>
<span class="traffic-signal__lamp traffic-signal__lamp--gelb"></span>
<span class="traffic-signal__lamp traffic-signal__lamp--gruen"></span>
</div>
<span id="result-traffic-label">-</span>
</div>
</div>
</div>
<div>
<h2>Zusammenfassung</h2>
<div class="summary" id="summary-text"></div>
</div>
<div class="downloads">
<div class="download-title">Downloads</div>
<div class="download-links" id="downloads"></div>
</div>
</div>
</section>
</div>
</main>
<script type="module">
const POLL_DELAY_MS = 900;
const params = new URLSearchParams(window.location.search);
const requestedModel = params.get("model")?.trim();
if (requestedModel) {
console.info(`BMP Bewerbungscheck verwendet Modell: ${requestedModel}`);
}
const dropZone = document.getElementById("drop-zone");
const fileInput = document.getElementById("file-input");
const dropLabel = document.getElementById("drop-label");
const runBtn = document.getElementById("run-btn");
const statusEl = document.getElementById("status");
const emptyState = document.getElementById("empty-state");
const resultSection = document.getElementById("result-section");
const downloadsEl = document.getElementById("downloads");
const summaryText = document.getElementById("summary-text");
const resultTraffic = document.getElementById("result-traffic");
const resultTrafficLabel = document.getElementById("result-traffic-label");
const llmProgress = document.getElementById("llm-progress");
const llmProgressFill = document.getElementById("llm-progress-fill");
const llmProgressCount = document.getElementById("llm-progress-count");
const llmProgressTime = document.getElementById("llm-progress-time");
const llmProgressCurrent = document.getElementById("llm-progress-current");
const steps = {
upload: document.getElementById("step-upload"),
extract: document.getElementById("step-extract"),
mask: document.getElementById("step-mask"),
summarize: document.getElementById("step-summarize"),
export: document.getElementById("step-export"),
};
let selectedFile = null;
function csrfHeaders(extra = {}) {
const csrf = document.cookie
.split("; ")
.find((part) => part.startsWith("bmp_demo_csrf="))
?.slice("bmp_demo_csrf=".length);
return csrf ? { ...extra, "X-BMP-CSRF": decodeURIComponent(csrf) } : extra;
}
dropZone.addEventListener("click", () => fileInput.click());
fileInput.addEventListener("change", () => setFile(fileInput.files[0]));
dropZone.addEventListener("dragover", (event) => {
event.preventDefault();
dropZone.classList.add("over");
});
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("over"));
dropZone.addEventListener("drop", (event) => {
event.preventDefault();
dropZone.classList.remove("over");
setFile(event.dataTransfer.files[0]);
});
runBtn.addEventListener("click", runPipeline);
function setFile(file) {
if (!file || file.type !== "application/pdf") {
statusEl.textContent = "Bitte wählen Sie eine PDF-Datei aus.";
return;
}
selectedFile = file;
dropLabel.textContent = file.name;
dropLabel.classList.add("file-name");
dropZone.classList.add("has-file");
runBtn.disabled = false;
statusEl.textContent = "Die Datei ist bereit. Sie können die Auswertung starten.";
updateStep("upload", "done", "bereit");
}
function updateStep(name, state, text) {
const el = steps[name];
el.className = `step ${state || ""}`.trim();
el.querySelector(".step-state").textContent = text;
}
function resetLlmProgress() {
llmProgress.classList.remove("visible");
llmProgressFill.style.width = "0%";
llmProgressCount.textContent = "LLM-Aufrufe werden vorbereitet.";
llmProgressTime.textContent = "";
llmProgressCurrent.textContent = "";
}
function formatElapsed(ms) {
const seconds = Math.max(0, Math.floor(ms / 1000));
const minutes = Math.floor(seconds / 60);
const rest = seconds % 60;
return minutes ? `${minutes}m ${String(rest).padStart(2, "0")}s` : `${rest}s`;
}
function operationLabel(operation) {
return {
segmentierung: "Antworten zuordnen",
zusammenfassung: "Antwort zusammenfassen",
scoring: "Ampel bewerten",
swot: "SWOT erstellen",
}[operation] || "LLM-Aufruf";
}
function renderLlmProgress(job) {
const progress = job.llmProgress || {};
const total = Number(progress.total || 0);
const done = Number(progress.done || 0);
const running = Number(progress.running || 0);
const failed = Number(progress.error || 0);
const known = done + failed;
const displayKnown = total ? Math.min(known, total) : known;
const percent = total ? Math.round((displayKnown / total) * 100) : 12;
llmProgress.classList.add("visible");
llmProgressFill.style.width = `${Math.max(8, Math.min(100, percent))}%`;
llmProgressCount.textContent = total
? `${displayKnown} von ${total} LLM-Aufrufen abgeschlossen${running ? `, ${running} läuft` : ""}`
: "LLM-Aufruf wird gestartet.";
const createdAt = Date.parse(job.createdAt || "");
llmProgressTime.textContent = Number.isFinite(createdAt)
? `läuft seit ${formatElapsed(Date.now() - createdAt)}`
: "";
const latest = progress.latest;
if (latest) {
const label = `${operationLabel(latest.operation)}: ${latest.label}`;
const status = latest.status === "running"
? "läuft gerade"
: latest.status === "done"
? `fertig${latest.durationMs ? ` nach ${formatElapsed(latest.durationMs)}` : ""}`
: "fehlgeschlagen";
llmProgressCurrent.textContent = `${label} · ${status}`;
} else {
llmProgressCurrent.textContent = job.statusMessage || "Die Anfrage wurde angenommen und wird verarbeitet.";
}
}
function showPanel(el) {
el.hidden = false;
el.classList.remove("is-exiting");
requestAnimationFrame(() => el.classList.add("is-visible"));
}
function hidePanel(el) {
if (el.hidden) return;
el.classList.remove("is-visible");
el.classList.add("is-exiting");
window.setTimeout(() => {
if (!el.classList.contains("is-visible")) {
el.hidden = true;
el.classList.remove("is-exiting");
}
}, 260);
}
async function runPipeline() {
if (!selectedFile) return;
runBtn.disabled = true;
hidePanel(emptyState);
hidePanel(resultSection);
downloadsEl.innerHTML = "";
updateStep("upload", "active", "läuft");
updateStep("extract", "", "wartet");
updateStep("mask", "", "wartet");
updateStep("summarize", "", "wartet");
updateStep("export", "", "wartet");
resetLlmProgress();
statusEl.textContent = "Die PDF wird hochgeladen.";
try {
const form = new FormData();
form.append("pdf", selectedFile);
const extractRes = await fetch("/extract", { method: "POST", headers: csrfHeaders(), body: form });
const extractJson = await extractRes.json();
if (!extractRes.ok || !extractJson.ok) throw new Error(extractJson.error || "Die Angaben konnten nicht gelesen werden.");
updateStep("upload", "done", "fertig");
updateStep("extract", "done", "fertig");
updateStep("mask", "active", "läuft");
statusEl.textContent = "Die Angaben wurden gelesen. Jetzt werden die Inhalte für die Einschätzung vorbereitet.";
const summaryRes = await fetch("/api/summarizer/summarize", {
method: "POST",
headers: csrfHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({
stem: extractJson.stem,
...(requestedModel ? { model: requestedModel } : {}),
}),
});
const summaryStart = await summaryRes.json();
if (!summaryRes.ok || !summaryStart.ok) throw new Error(summaryStart.error || "Die Einschätzung konnte nicht gestartet werden.");
updateStep("mask", "done", "fertig");
updateStep("summarize", "active", "läuft");
statusEl.textContent = "Die Einschätzung wird erstellt. Das kann einen Moment dauern.";
const job = await waitForJob(summaryStart.jobId);
updateStep("summarize", "done", "fertig");
updateStep("export", "active", "läuft");
statusEl.textContent = "Der Bericht wird vorbereitet.";
const resultRes = await fetch(`/api/pipeline/result/${encodeURIComponent(extractJson.stem)}`);
const result = await resultRes.json();
if (!resultRes.ok || !result.ok) throw new Error(result.error || "Das Ergebnis konnte nicht geladen werden.");
renderResult(result);
updateStep("export", "done", "fertig");
statusEl.textContent = "Fertig. Bericht und Rohdaten stehen bereit.";
} catch (error) {
const active = Object.entries(steps).find(([, el]) => el.classList.contains("active"));
if (active) updateStep(active[0], "error", "Fehler");
statusEl.textContent = String(error).replace(/^Error:\s*/, "");
} finally {
runBtn.disabled = !selectedFile;
}
}
async function waitForJob(jobId) {
for (;;) {
const jobResult = await fetch(`/api/summarizer/jobs/${jobId}`);
const job = await jobResult.json();
if (!jobResult.ok) throw new Error(job.error || "Die Auswertung konnte nicht geladen werden.");
const progress = job.total ? `${job.completed} von ${job.total}` : "läuft";
updateStep("summarize", "active", progress);
renderLlmProgress(job);
statusEl.textContent = job.statusMessage || "Die Einschätzung wird erstellt. Das kann einen Moment dauern.";
if (job.status === "done") return job;
if (job.status === "error") throw new Error(job.errors?.join("\\n") || "Die Auswertung ist fehlgeschlagen.");
await new Promise((resolve) => setTimeout(resolve, POLL_DELAY_MS));
}
}
function renderResult(result) {
const data = result.summary;
const ampel = data.ampelbewertung || {};
renderTrafficSignal(ampel.farbe);
summaryText.textContent = ampel.begruendung || "Für diese Auswertung liegt keine Zusammenfassung vor.";
downloadsEl.innerHTML = "";
for (const file of result.downloads || []) {
const link = document.createElement("a");
link.href = file.url;
link.textContent = friendlyDownloadLabel(file.label);
downloadsEl.appendChild(link);
}
showPanel(resultSection);
}
function renderTrafficSignal(color) {
const normalized = ["gruen", "gelb", "rot"].includes(color) ? color : "";
resultTraffic.className = `traffic-signal${normalized ? ` traffic-signal--${normalized}` : ""}`;
resultTrafficLabel.textContent = normalized
? { gruen: "Grün", gelb: "Gelb", rot: "Rot" }[normalized]
: "-";
}
function friendlyDownloadLabel(label) {
if (label.includes("Fragen-PDF")) return "Fragen als PDF";
if (label.includes("Fragen-HTML")) return "Fragen als Web-Bericht";
if (label.includes("PDF")) return "Bericht als PDF";
if (label.includes("XLSX")) return "Tabelle";
if (label.includes("Report-HTML")) return "Web-Bericht";
if (label.includes("JSON")) return "Rohdaten";
return label.replace(" herunterladen", "");
}
</script>
</body>
</html>