Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
1345 lines
42 KiB
Plaintext
1345 lines
42 KiB
Plaintext
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">Frage–Antwort</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>
|
||
|
||
<!-- Frage–Antwort -->
|
||
<div class="card card-half" data-tile="qa">
|
||
<div class="card-header">
|
||
<div class="card-title">Frage–Antwort-Blatt</div>
|
||
<div class="card-badge">Kommen bald</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div>Aus dem Original-Arbeitsblatt entsteht ein Frage–Antwort-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"><</button>
|
||
<span id="pager-label">1 von 2</span>
|
||
<button id="pager-next">></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>
|
||
"""
|