This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend_app.py.save
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

1345 lines
42 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
pathlib import Path
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, FileResponse
from main import app as backend_app # Backend unter /api
BASE_DIR = Path.home() / "Arbeitsblaetter"
EINGANG_DIR = BASE_DIR / "Eingang"
BEREINIGT_DIR = BASE_DIR / "Bereinigt"
app = FastAPI(title="BreakPilot Frontend")
# Backend unter /api einhängen
app.mount("/api", backend_app)
@app.get("/", response_class=HTMLResponse)
def root():
return """
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>BreakPilot Start</title>
</head>
<body style="font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; background:#020617; color:#e5e7eb; margin:40px;">
<h1>BreakPilot Lokale App</h1>
<p>Die App läuft.</p>
<p>Moderne Oberfläche: <a href="/app" style="color:#22c55e;">/app</a></p>
<p>Backend-API-Doku: <a href="/api/docs" style="color:#60a5fa;">/api/docs</a></p>
</body>
</html>
"""
@app.get("/preview-file/{filename}")
def preview_file(filename: str):
path = EINGANG_DIR / filename
if not path.exists():
return {"error": "Datei nicht gefunden"}
if path.suffix.lower() not in {".jpg", ".jpeg", ".png"}:
return {"error": "Vorschau nur für JPG/PNG möglich"}
return FileResponse(str(path))
@app.get("/preview-clean-file/{filename}")
def preview_clean_file(filename: str):
path = BEREINIGT_DIR / filename
if not path.exists():
return {"error": "Datei nicht gefunden"}
if path.suffix.lower() not in {".jpg", ".jpeg", ".png"}:
return {"error": "Vorschau nur für JPG/PNG möglich"}
return FileResponse(str(path))
@app.get("/app", response_class=HTMLResponse)
def app_ui():
return """
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>BreakPilot Arbeitsblatt Studio</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root {
--bp-primary: #0f766e;
--bp-primary-soft: #ccfbf1;
--bp-bg: #020617;
--bp-surface: #020617;
--bp-border: #1f2937;
--bp-accent: #22c55e;
--bp-accent-soft: rgba(34,197,94,0.2);
--bp-text: #e5e7eb;
--bp-text-muted: #9ca3af;
--bp-danger: #ef4444;
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
background: radial-gradient(circle at top left, #1f2937 0, #020617 50%, #000 100%);
color: var(--bp-text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-root {
display: grid;
grid-template-rows: 56px 1fr 32px;
flex: 1;
min-height: 0;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
border-bottom: 1px solid rgba(148,163,184,0.3);
backdrop-filter: blur(18px);
background: linear-gradient(to right, rgba(15,23,42,0.9), rgba(15,23,42,0.6));
}
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.brand-logo {
width: 28px;
height: 28px;
border-radius: 999px;
border: 2px solid var(--bp-accent);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
color: var(--bp-accent);
}
.brand-text-main {
font-weight: 600;
font-size: 16px;
}
.brand-text-sub {
font-size: 12px;
color: var(--bp-text-muted);
}
.top-nav {
display: flex;
align-items: center;
gap: 18px;
font-size: 13px;
}
.top-nav-item {
cursor: pointer;
padding: 4px 10px;
border-radius: 999px;
color: var(--bp-text-muted);
border: 1px solid transparent;
}
.top-nav-item.active {
color: var(--bp-accent);
border-color: var(--bp-accent-soft);
background: rgba(15,23,42,0.7);
}
.top-actions {
display: flex;
align-items: center;
gap: 10px;
}
.pill {
font-size: 11px;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid rgba(148,163,184,0.5);
color: var(--bp-text-muted);
}
.main-layout {
display: grid;
grid-template-columns: 220px 1fr;
height: 100%;
min-height: 0;
}
.sidebar {
border-right: 1px solid rgba(148,163,184,0.25);
background: radial-gradient(circle at top, #020617 0, #020617 40%, #000 100%);
padding: 14px 10px;
display: flex;
flex-direction: column;
gap: 18px;
}
.sidebar-section-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--bp-text-muted);
padding: 0 6px;
}
.sidebar-menu {
display: flex;
flex-direction: column;
gap: 4px;
}
.sidebar-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
border-radius: 9px;
cursor: pointer;
font-size: 13px;
color: var(--bp-text-muted);
}
.sidebar-item.active {
background: rgba(15,23,42,0.8);
color: var(--bp-accent);
border: 1px solid rgba(45,212,191,0.35);
}
.sidebar-item-label {
display: flex;
align-items: center;
gap: 7px;
}
.sidebar-item-badge {
font-size: 10px;
border-radius: 999px;
padding: 2px 7px;
border: 1px solid rgba(148,163,184,0.4);
}
.sidebar-footer {
margin-top: auto;
font-size: 11px;
color: var(--bp-text-muted);
padding: 0 6px;
}
.content {
padding: 14px 16px 16px 16px;
display: grid;
grid-template-columns: 1fr; /* nur eine Spalte: aktiver Screen nutzt ganze Breite */
gap: 14px;
height: 100%;
min-height: 0;
}
.panel {
background: radial-gradient(circle at top left, #020617 0, #020617 50%, #000 100%);
border-radius: 16px;
border: 1px solid rgba(148,163,184,0.25);
padding: 12px 14px;
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.panel-title {
font-size: 14px;
font-weight: 500;
}
.panel-subtitle {
font-size: 12px;
color: var(--bp-text-muted);
}
.panel-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 8px;
overflow: hidden;
}
.small-pill {
font-size: 10px;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid rgba(148,163,184,0.5);
color: var(--bp-text-muted);
}
.upload-inline {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.upload-inline input[type=file] {
font-size: 11px;
}
.file-list {
list-style: none;
margin: 0;
padding: 0;
max-height: 110px;
overflow-y: auto;
border-radius: 10px;
border: 1px solid rgba(30,64,175,0.6);
background: rgba(15,23,42,0.7);
}
.file-item {
font-size: 12px;
padding: 6px 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.file-item:nth-child(odd) {
background: rgba(15,23,42,0.5);
}
.file-item:hover {
background: rgba(30,64,175,0.7);
}
.file-item.active {
background: rgba(59,130,246,0.7);
}
.file-item-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 190px;
}
.file-item-delete {
font-size: 12px;
color: #f97316;
cursor: pointer;
padding: 0 4px;
}
.file-item-delete:hover {
color: #fb923c;
}
.file-empty {
font-size: 12px;
color: var(--bp-text-muted);
}
.inline-process {
display: flex;
justify-content: flex-end;
margin-top: 4px;
}
.preview-container {
flex: 1;
border-radius: 12px;
border: 1px solid rgba(148,163,184,0.25);
background: radial-gradient(circle at top left, #020617 0, #020617 40%, #000 100%);
overflow: hidden;
display: flex;
align-items: stretch;
justify-content: center;
position: relative;
min-height: 0;
}
.preview-placeholder {
font-size: 12px;
color: var(--bp-text-muted);
text-align: center;
padding: 8px;
}
.compare-wrapper {
display: flex;
width: 100%;
height: 100%;
position: relative;
}
.compare-section {
flex: 1;
position: relative;
padding: 4px;
overflow: hidden;
}
.compare-section + .compare-section {
border-left: 1px solid rgba(148,163,184,0.3);
}
.compare-label {
position: absolute;
top: 4px;
left: 8px;
font-size: 10px;
padding: 2px 6px;
border-radius: 999px;
background: rgba(15,23,42,0.85);
color: var(--bp-text-muted);
}
.preview-img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
display: block;
}
.clean-iframe {
width: 100%;
height: 100%;
border: none;
background: #f9fafb;
border-radius: 8px;
}
.preview-nav {
position: absolute;
top: 50%;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
transform: translateY(-50%);
pointer-events: none;
}
.preview-nav button {
pointer-events: auto;
background: rgba(15,23,42,0.8);
border: 1px solid rgba(148,163,184,0.7);
border-radius: 999px;
color: var(--bp-text);
font-size: 14px;
padding: 4px 8px;
cursor: pointer;
}
.btn {
font-size: 12px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(148,163,184,0.5);
background: rgba(15,23,42,0.8);
color: var(--bp-text);
cursor: pointer;
}
.btn-primary {
border-color: var(--bp-accent);
background: linear-gradient(to right, var(--bp-primary), #15803d);
}
.btn-ghost {
background: transparent;
}
.btn-sm {
padding: 4px 8px;
font-size: 11px;
}
.btn:disabled {
opacity: 0.5;
cursor: default;
}
.panel-tools-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
}
.card-toggle-bar {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.toggle-pill {
font-size: 11px;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid rgba(148,163,184,0.5);
background: rgba(15,23,42,0.8);
color: var(--bp-text-muted);
cursor: pointer;
}
.toggle-pill.active {
border-color: var(--bp-accent);
color: var(--bp-accent);
background: rgba(22,163,74,0.1);
}
.cards-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-rows: minmax(140px, auto);
gap: 10px;
min-height: 0;
}
.card {
border-radius: 14px;
border: 1px solid rgba(148,163,184,0.25);
background: radial-gradient(circle at top left, rgba(15,23,42,0.9) 0, #020617 50%, #000 100%);
padding: 10px;
display: flex;
flex-direction: column;
cursor: pointer;
min-height: 0;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.card-title {
font-size: 13px;
font-weight: 500;
}
.card-badge {
font-size: 10px;
border-radius: 999px;
padding: 2px 7px;
border: 1px solid rgba(148,163,184,0.5);
color: var(--bp-text-muted);
}
.card-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 5px;
font-size: 11px;
color: var(--bp-text-muted);
}
.card-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.card-hidden {
display: none;
}
.card-full {
grid-column: 1 / -1;
min-height: 220px;
}
.card-half {
grid-column: span 1;
}
.card-large {
grid-column: 1 / -1;
}
.card-small {
grid-column: span 1;
}
.status-bar {
position: fixed;
right: 12px;
bottom: 44px;
min-width: 220px;
max-width: 320px;
padding: 8px 10px;
border-radius: 999px;
border: 1px solid rgba(148,163,184,0.4);
background: rgba(15,23,42,0.95);
font-size: 11px;
color: var(--bp-text-muted);
display: none;
z-index: 20;
}
.status-bar.visible {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--bp-accent);
}
.status-dot.error {
background: var(--bp-danger);
}
.status-text-main { color: var(--bp-text); }
.status-text-sub { font-size: 10px; }
.pager {
grid-row: 3;
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 12px;
color: var(--bp-text-muted);
border-top: 1px solid rgba(148,163,184,0.35);
background: rgba(15,23,42,0.9);
}
.pager button {
background: transparent;
border: 1px solid rgba(148,163,184,0.5);
border-radius: 999px;
color: var(--bp-text-muted);
font-size: 11px;
padding: 2px 8px;
cursor: pointer;
}
.pager button:disabled {
opacity: 0.4;
cursor: default;
}
.footer {
font-size: 11px;
color: #9ca3af;
text-align: center;
padding: 4px 8px;
background: #020617;
}
.footer a {
color: #9ca3af;
text-decoration: none;
margin: 0 6px;
}
.footer a:hover {
color: #e5e7eb;
}
@media (max-width: 900px) {
.main-layout {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
}
.content {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="app-root">
<header class="topbar">
<div class="brand">
<div class="brand-logo">BP</div>
<div>
<div class="brand-text-main">BreakPilot Studio</div>
<div class="brand-text-sub">Arbeitsblätter · Eltern · KI</div>
</div>
</div>
<nav class="top-nav">
<div class="top-nav-item active">Arbeitsblätter</div>
<div class="top-nav-item">Klassenarbeiten</div>
<div class="top-nav-item">Eltern-Chat</div>
<div class="top-nav-item">Notenspiegel</div>
<div class="top-nav-item">Einstellungen</div>
</nav>
<div class="top-actions">
<div class="pill">MVP · Lokal auf deinem Mac</div>
</div>
</header>
<div class="main-layout">
<aside class="sidebar">
<div>
<div class="sidebar-section-title">Bereiche</div>
<div class="sidebar-menu">
<div class="sidebar-item active">
<div class="sidebar-item-label">
<span>Arbeitsblatt Studio</span>
</div>
<div class="sidebar-item-badge">aktiv</div>
</div>
<div class="sidebar-item">
<div class="sidebar-item-label">
<span>Klassenarbeiten</span>
</div>
<div class="sidebar-item-badge">demnächst</div>
</div>
<div class="sidebar-item">
<div class="sidebar-item-label">
<span>Eltern & Chat</span>
</div>
<div class="sidebar-item-badge">demnächst</div>
</div>
<div class="sidebar-item">
<div class="sidebar-item-label">
<span>Analytics / Noten</span>
</div>
<div class="sidebar-item-badge">demnächst</div>
</div>
</div>
</div>
<div class="sidebar-footer">
CI / Brandbook: Farben kommen bereits aus Variablen und sind später leicht anpassbar.
</div>
</aside>
<main class="content">
<!-- Screen 1: Eingangsdateien & Alt/Neu-Vergleich -->
<section class="panel" id="panel-compare">
<div class="panel-header">
<div>
<div class="panel-title">Arbeitsblätter & Vergleich</div>
<div class="panel-subtitle">Links Scan · Rechts neu aufgebautes Arbeitsblatt</div>
</div>
<span class="small-pill" id="eingang-count">0 Dateien</span>
</div>
<div class="panel-body">
<div class="upload-inline">
<input type="file" id="file-input" multiple>
<button class="btn btn-sm btn-primary" id="btn-upload-inline">Hochladen</button>
</div>
<ul class="file-list" id="eingang-list"></ul>
<div class="inline-process">
<button class="btn btn-sm btn-primary" id="btn-full-process">
Arbeitsblätter neu aufbauen
</button>
</div>
<div class="preview-container" id="preview-container">
<div class="preview-placeholder">
Lade Arbeitsblätter hoch und klicke auf „Arbeitsblätter neu aufbauen“.<br>
Dann kannst du mit den Pfeilen durch die Scans und die bereinigten Versionen blättern.
</div>
</div>
</div>
</section>
<!-- Screen 2: Kacheln -->
<section class="panel" id="panel-tiles">
<div class="panel-tools-header">
<div>
<div class="panel-title">Aufbereitungs-Tools</div>
<div class="panel-subtitle">Kacheln für den Lernflow aktivieren/deaktivieren</div>
</div>
</div>
<div class="panel-body">
<div class="card-toggle-bar">
<button class="toggle-pill active" data-tile="original">Original</button>
<button class="toggle-pill active" data-tile="qa">FrageAntwort</button>
<button class="toggle-pill active" data-tile="mc">Multiple Choice</button>
<button class="toggle-pill active" data-tile="cloze">Lückentext</button>
</div>
<div class="cards-grid">
<!-- Original-Arbeitsblatt -->
<div class="card card-half" data-tile="original">
<div class="card-header">
<div class="card-title">Original-Arbeitsblatt</div>
<div class="card-badge">Neuaufbau</div>
</div>
<div class="card-body">
<div>Erzeugt bereinigte Versionen deiner Arbeitsblätter (ohne Handschrift) und baut saubere HTML-Arbeitsblätter, die im Vergleich rechts angezeigt werden.</div>
<div class="card-actions">
<button class="btn btn-sm btn-primary" id="btn-original-generate">
Neuaufbau starten
</button>
</div>
</div>
</div>
<!-- FrageAntwort -->
<div class="card card-half" data-tile="qa">
<div class="card-header">
<div class="card-title">FrageAntwort-Blatt</div>
<div class="card-badge">Kommen bald</div>
</div>
<div class="card-body">
<div>Aus dem Original-Arbeitsblatt entsteht ein FrageAntwort-Blatt. Elternmodus mit Übersetzung & Aussprache-Button wird hier andocken.</div>
<div class="card-actions">
<button class="btn btn-sm btn-ghost" disabled>Demnächst</button>
</div>
</div>
</div>
<!-- Multiple Choice -->
<div class="card card-half" data-tile="mc">
<div class="card-header">
<div class="card-title">Multiple Choice Test</div>
<div class="card-badge">Kommen bald</div>
</div>
<div class="card-body">
<div>Erzeugt passende MC-Aufgaben zur ursprünglichen Schwierigkeit (z. B. Klasse 7), ohne das Niveau zu verändern.</div>
<div class="card-actions">
<button class="btn btn-sm btn-ghost" disabled>Demnächst</button>
</div>
</div>
</div>
<!-- Lückentext -->
<div class="card card-half" data-tile="cloze">
<div class="card-header">
<div class="card-title">Lückentext</div>
<div class="card-badge">Kommen bald</div>
</div>
<div class="card-body">
<div>Erzeugt oder rekonstruiert Lückentexte mit sinnvoll aufgeteilten Lücken (z. B. „habe“ + „gemacht“ getrennt).</div>
<div class="card-actions">
<button class="btn btn-sm btn-ghost" disabled>Demnächst</button>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<div class="pager">
<button id="pager-prev">&lt;</button>
<span id="pager-label">1 von 2</span>
<button id="pager-next">&gt;</button>
</div>
<div class="status-bar" id="status-bar">
<div class="status-dot" id="status-dot"></div>
<div>
<div class="status-text-main" id="status-main"></div>
<div class="status-text-sub" id="status-sub"></div>
</div>
</div>
</div>
<div class="footer">
<span>BreakPilot · MVP lokal</span>
<span>·</span>
<a href="#">Impressum</a>
<a href="#">Datenschutz</a>
<a href="#">Kontakt</a>
</div>
<script>
const eingangListEl = document.getElementById('eingang-list');
const eingangCountEl = document.getElementById('eingang-count');
const previewContainer = document.getElementById('preview-container');
const fileInput = document.getElementById('file-input');
const btnUploadInline = document.getElementById('btn-upload-inline');
const btnFullProcess = document.getElementById('btn-full-process');
const btnOriginalGenerate = document.getElementById('btn-original-generate');
const statusBar = document.getElementById('status-bar');
const statusDot = document.getElementById('status-dot');
const statusMain = document.getElementById('status-main');
const statusSub = document.getElementById('status-sub');
const panelCompare = document.getElementById('panel-compare');
const panelTiles = document.getElementById('panel-tiles');
const pagerPrev = document.getElementById('pager-prev');
const pagerNext = document.getElementById('pager-next');
const pagerLabel = document.getElementById('pager-label');
let currentSelectedFile = null;
let eingangFiles = [];
let currentIndex = 0;
let worksheetPairs = {}; // original -> { clean_html, clean_image }
const tileState = {
original: true,
qa: true,
mc: true,
cloze: true,
primary: 'original',
};
let currentScreen = 1; // 1: Vergleich, 2: Kacheln
function showStatus(main, sub, isError) {
statusMain.textContent = main;
statusSub.textContent = sub || '';
if (isError) {
statusDot.classList.add('error');
} else {
statusDot.classList.remove('error');
}
statusBar.classList.add('visible');
setTimeout(() => {
statusBar.classList.remove('visible');
}, 4000);
}
function updateScreen() {
if (currentScreen === 1) {
panelCompare.style.display = 'flex';
panelTiles.style.display = 'none';
pagerLabel.textContent = '1 von 2';
pagerPrev.disabled = true;
pagerNext.disabled = false;
} else {
panelCompare.style.display = 'flex'; // Panel selbst bleibt flex, aber:
panelCompare.style.display = 'none';
panelTiles.style.display = 'flex';
pagerLabel.textContent = '2 von 2';
pagerPrev.disabled = false;
pagerNext.disabled = true;
}
}
pagerPrev.addEventListener('click', () => {
if (currentScreen > 1) {
currentScreen = 1;
updateScreen();
}
});
pagerNext.addEventListener('click', () => {
if (currentScreen < 2) {
currentScreen = 2;
updateScreen();
}
});
function setActiveFile(filename) {
currentSelectedFile = filename;
const idx = eingangFiles.indexOf(filename);
if (idx >= 0) {
currentIndex = idx;
}
document.querySelectorAll('.file-item').forEach(el => {
if (el.classList.contains('file-empty')) return;
if (el.dataset.filename === filename) {
el.classList.add('active');
} else {
el.classList.remove('active');
}
});
renderPreview(filename);
}
async function loadEingangFiles() {
try {
const res = await fetch('/api/eingang-dateien');
const data = await res.json();
const files = data.eingang || [];
eingangFiles = files.slice();
eingangListEl.innerHTML = '';
if (!files.length) {
const li = document.createElement('li');
li.className = 'file-item file-empty';
li.textContent = 'Noch keine Dateien im Eingang.';
eingangListEl.appendChild(li);
eingangCountEl.textContent = '0 Dateien';
currentSelectedFile = null;
previewContainer.innerHTML = '<div class="preview-placeholder">Noch keine Dateien. Lade oben links Arbeitsblätter hoch.</div>';
return;
}
files.forEach((filename) => {
const li = document.createElement('li');
li.className = 'file-item';
li.dataset.filename = filename;
const nameSpan = document.createElement('span');
nameSpan.className = 'file-item-name';
nameSpan.textContent = filename;
const deleteSpan = document.createElement('span');
deleteSpan.className = 'file-item-delete';
deleteSpan.textContent = '✕';
deleteSpan.addEventListener('click', async (e) => {
e.stopPropagation();
if (!confirm('Datei wirklich löschen?')) {
return;
}
try {
const resDel = await fetch('/api/eingang-dateien/' + encodeURIComponent(filename), {
method: 'DELETE',
});
const dataDel = await resDel.json();
if (resDel.ok && dataDel.status === 'OK') {
showStatus('Datei gelöscht', filename, false);
if (currentSelectedFile === filename) {
currentSelectedFile = null;
}
await loadEingangFiles();
await loadWorksheetPairs();
} else {
showStatus('Fehler beim Löschen', JSON.stringify(dataDel), true);
}
} catch (err) {
console.error(err);
showStatus('Fehler beim Löschen', err.message || String(err), true);
}
});
li.appendChild(nameSpan);
li.appendChild(deleteSpan);
li.addEventListener('click', () => {
setActiveFile(filename);
});
eingangListEl.appendChild(li);
});
eingangCountEl.textContent = files.length + ' Dateien';
if (!currentSelectedFile && files.length > 0) {
setActiveFile(files[0]);
} else if (currentSelectedFile) {
renderPreview(currentSelectedFile);
}
} catch (err) {
console.error(err);
showStatus('Fehler beim Laden der Eingang-Dateien', err.message || String(err), true);
}
}
async function loadWorksheetPairs() {
try {
const res = await fetch('/api/worksheet-pairs');
const data = await res.json();
worksheetPairs = {};
(data.pairs || []).forEach(p => {
if (!p.original) return;
const html = p.clean_html || null;
const img = p.clean_image || null;
if (html || img) {
worksheetPairs[p.original] = {
clean_html: html,
clean_image: img,
};
}
});
} catch (err) {
console.error(err);
showStatus('Fehler beim Laden der Alt/Neu-Paare', err.message || String(err), true);
}
}
function renderPreview(filename) {
if (!filename) {
previewContainer.innerHTML = '<div class="preview-placeholder">Wähle ein Arbeitsblatt aus.</div>';
return;
}
const lower = filename.toLowerCase();
const isImage = lower.endsWith('.jpg') || lower.endsWith('.jpeg') || lower.endsWith('.png');
const isPdf = lower.endsWith('.pdf');
const mapping = worksheetPairs[filename] || {};
const cleanHtml = mapping.clean_html;
const cleanImage = mapping.clean_image;
previewContainer.innerHTML = '';
const wrapper = document.createElement('div');
wrapper.className = 'compare-wrapper';
// Linke Seite: Scan (alt)
const originalSection = document.createElement('div');
originalSection.className = 'compare-section';
const originalLabel = document.createElement('div');
originalLabel.className = 'compare-label';
originalLabel.textContent = 'Scan (alt)';
originalSection.appendChild(originalLabel);
if (isImage) {
const src = '/preview-file/' + encodeURIComponent(filename);
const img = document.createElement('img');
img.className = 'preview-img';
img.src = src;
originalSection.appendChild(img);
} else if (isPdf) {
const placeholder = document.createElement('div');
placeholder.className = 'preview-placeholder';
placeholder.innerHTML = 'PDF-Vorschau ist hier reduziert. Öffne die Datei direkt aus dem Ordner:<br><br><code>~/Arbeitsblaetter/Eingang/' + filename + '</code>';
originalSection.appendChild(placeholder);
} else {
const placeholder = document.createElement('div');
placeholder.className = 'preview-placeholder';
placeholder.textContent = 'Dieses Format wird aktuell nicht direkt angezeigt.';
originalSection.appendChild(placeholder);
}
// Rechte Seite: Neu aufgebautes Arbeitsblatt
const newSection = document.createElement('div');
newSection.className = 'compare-section';
const newLabel = document.createElement('div');
newLabel.className = 'compare-label';
newLabel.textContent = 'Neu aufgebautes Arbeitsblatt';
newSection.appendChild(newLabel);
if (cleanHtml) {
const iframe = document.createElement('iframe');
iframe.className = 'clean-iframe';
iframe.src = '/api/clean-html/' + encodeURIComponent(cleanHtml);
newSection.appendChild(iframe);
} else if (cleanImage) {
const img = document.createElement('img');
img.className = 'preview-img';
img.src = '/preview-clean-file/' + encodeURIComponent(cleanImage);
newSection.appendChild(img);
} else {
const placeholder = document.createElement('div');
placeholder.className = 'preview-placeholder';
placeholder.textContent = 'Noch kein neu aufgebautes Arbeitsblatt vorhanden. Klicke auf „Arbeitsblätter neu aufbauen“.';
newSection.appendChild(placeholder);
}
wrapper.appendChild(originalSection);
wrapper.appendChild(newSection);
// Navigation (links/rechts) für mehrere Dateien
if (eingangFiles.length > 1) {
const nav = document.createElement('div');
nav.className = 'preview-nav';
const btnLeft = document.createElement('button');
btnLeft.textContent = '';
btnLeft.addEventListener('click', (e) => {
e.stopPropagation();
if (!eingangFiles.length) return;
currentIndex = (currentIndex - 1 + eingangFiles.length) % eingangFiles.length;
const nextFile = eingangFiles[currentIndex];
setActiveFile(nextFile);
});
const btnRight = document.createElement('button');
btnRight.textContent = '';
btnRight.addEventListener('click', (e) => {
e.stopPropagation();
if (!eingangFiles.length) return;
currentIndex = (currentIndex + 1) % eingangFiles.length;
const nextFile = eingangFiles[currentIndex];
setActiveFile(nextFile);
});
nav.appendChild(btnLeft);
nav.appendChild(btnRight);
wrapper.appendChild(nav);
}
previewContainer.appendChild(wrapper);
}
async function uploadInline() {
const files = fileInput.files;
if (!files || !files.length) {
showStatus('Keine Dateien ausgewählt', 'Bitte oben links Dateien auswählen.', true);
return;
}
const form = new FormData();
for (const f of files) {
form.append('files', f);
}
try {
showStatus('Lade Dateien hoch …', 'Bitte warten.', false);
const res = await fetch('/api/upload-multi', {
method: 'POST',
body: form,
});
const data = await res.json();
if (res.ok) {
const count = (data.saved_as || []).length;
showStatus('Upload abgeschlossen', count + ' Datei(en) gespeichert.', false);
fileInput.value = '';
await loadEingangFiles();
await loadWorksheetPairs();
} else {
showStatus('Fehler beim Upload', JSON.stringify(data), true);
}
} catch (err) {
console.error(err);
showStatus('Fehler beim Upload', err.message || String(err), true);
}
}
async function callFullPipeline() {
try {
if (!eingangFiles.length) {
showStatus('Keine Dateien im Eingang', 'Bitte zuerst Arbeitsblätter hochladen.', true);
return;
}
showStatus('Starte Neuaufbau …', 'Schritt 1/3: Handschrift-Bereinigung (MVP).', false);
await fetch('/api/remove-handwriting-all', { method: 'POST' });
showStatus('Analysiere Arbeitsblätter …', 'Schritt 2/3: KI-Analyse.', false);
const res1 = await fetch('/api/analyze-all', { method: 'POST' });
const data1 = await res1.json();
if (!res1.ok) {
showStatus('Analyse-Fehler', JSON.stringify(data1), true);
return;
}
const analyzedCount = (data1.analyzed || []).length;
showStatus('Analysen fertig', analyzedCount + ' Analyse-Datei(en). Starte HTML-Generierung …', false);
const res2 = await fetch('/api/generate-clean', { method: 'POST' });
const data2 = await res2.json();
if (!res2.ok) {
showStatus('Fehler bei HTML-Generierung', JSON.stringify(data2), true);
return;
}
const genCount = (data2.generated || []).length;
showStatus('HTML-Arbeitsblätter erzeugt', genCount + ' clean.html-Datei(en) in „Bereinigt“.', false);
await loadWorksheetPairs();
if (currentSelectedFile) {
renderPreview(currentSelectedFile);
} else if (eingangFiles.length) {
setActiveFile(eingangFiles[0]);
}
} catch (err) {
console.error(err);
showStatus('Fehler beim Neuaufbau', err.message || String(err), true);
}
}
function updateTilesLayout() {
const tiles = ['original', 'qa', 'mc', 'cloze'];
const active = tiles.filter(t => tileState[t]);
if (!tileState.primary || !tileState[tileState.primary] || active.indexOf(tileState.primary) === -1) {
tileState.primary = active[0] || null;
}
tiles.forEach(t => {
const card = document.querySelector('.card[data-tile="' + t + '"]');
if (!card) return;
card.classList.remove('card-hidden', 'card-full', 'card-half', 'card-large', 'card-small');
if (!tileState[t]) {
card.classList.add('card-hidden');
}
});
const activeCards = active.map(t => document.querySelector('.card[data-tile="' + t + '"]'));
if (active.length === 0) {
return;
} else if (active.length === 1) {
activeCards[0].classList.add('card-full');
} else if (active.length === 2) {
activeCards.forEach(c => c.classList.add('card-half'));
} else {
active.forEach(t => {
const card = document.querySelector('.card[data-tile="' + t + '"]');
if (!card) return;
if (t === tileState.primary) {
card.classList.add('card-large');
} else {
card.classList.add('card-small');
}
});
}
document.querySelectorAll('.toggle-pill').forEach(btn => {
const t = btn.dataset.tile;
if (tileState[t]) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
}
function initTileControls() {
document.querySelectorAll('.toggle-pill').forEach(btn => {
btn.addEventListener('click', (e) => {
const tile = btn.dataset.tile;
tileState[tile] = !tileState[tile];
updateTilesLayout();
e.stopPropagation();
});
});
document.querySelectorAll('.card').forEach(card => {
card.addEventListener('click', () => {
const tile = card.dataset.tile;
if (!tileState[tile]) return;
tileState.primary = tile;
updateTilesLayout();
});
});
updateTilesLayout();
}
btnUploadInline.addEventListener('click', uploadInline);
btnFullProcess.addEventListener('click', callFullPipeline);
btnOriginalGenerate.addEventListener('click', callFullPipeline);
loadEingangFiles();
loadWorksheetPairs();
initTileControls();
updateScreen();
</script>
</body>
</html>
"""