917 lines
30 KiB
HTML
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>
|