const { useState, useRef, useEffect } = React; /* ===== Konstanten ===== */ const PLATFORMS = [ { value: "short", label: "Short Video" }, { value: "long", label: "Long Video" }, ]; const LS_LAST_PROJECT = "kveditor.lastProjectId"; // Untertitel-Schriftarten (Auswahl im Settings-Panel). Für den Burn-in muss die // Schrift auf dem Mac installiert sein — sonst greift eine Ersatzschrift. // Nur gebündelte Fonts: jede Auswahl ist als @font-face geladen UND liegt als // Datei im fontsdir des Burn-ins → Vorschau und Export nutzen exakt dieselbe Schrift. // Reihenfolge = fett→schlank. Familienname muss 1:1 zu @font-face + ASS passen. const SUB_FONT_CHOICES = [ "Montserrat Black", "Montserrat ExtraBold", "Montserrat SemiBold", "Montserrat", "Plus Jakarta Sans ExtraBold", "DM Sans", "DM Serif Display", "Fraunces Black", ]; const SUB_FONT_DEFAULT = "Montserrat Black"; const SUB_FONT_SIZE_DEFAULT = 24; // = Backend DEFAULT_FONT_SIZE; vollbild-tauglich const SUB_FONT_SIZES = [14, 16, 20, 24, 28, 32, 40, 48]; const LS_MULTICUT = "kveditor.multicutMode"; const CONTENT_TYPES = [ { value: "tofu", label: "TOFU — kontroverse Statements (max. 15s)", short: "TOFU" }, { value: "mofu", label: "MOFU — Insights & Takeaways (30–60s)", short: "MOFU" }, ]; const POST_PROCESSING_OPTIONS = [ { value: "segment", label: "Nur Segment-Schnitt (schnell)" }, { value: "v52", label: "Segment + V5.2 Feinschnitt (Ähs & Pausen raus)" }, ]; const SEGMENT_TYPE_LABELS = { content: "Content", silence: "Stille", breath: "Atmer", filler_word: "Füllwort", false_start: "Fehlstart", repetition: "Wiederholung", }; /* ===== API helper ===== */ async function api(method, path, body) { const opts = { method, headers: {} }; if (body && !(body instanceof FormData)) { opts.headers["Content-Type"] = "application/json"; opts.body = JSON.stringify(body); } else if (body instanceof FormData) { opts.body = body; } const res = await fetch(path, opts); if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || `HTTP ${res.status}`); } return res.json(); } /* ===== Admin-/Feedback-Ansicht ===== Zwei Ansichten aus EINER Codebasis: Kunden-Ansicht (Standard, ohne Feedback-Felder) und Admin-Ansicht mit Feedback pro Output-Feld. Das Passwort wird SERVER-seitig geprüft (/api/analyst/admin/verify) — im Frontend steht es nie, sonst wäre es bei öffentlich erreichbarem Server wirkungslos. Gesammelt wird nur; kein Auto-Fix, kein Auto-Commit. */ const AdminCtx = React.createContext({ admin: false, password: "", runId: "" }); function AdminToggle({ admin, password, onLogin, onLogout }) { const [offen, setOffen] = useState(false); const [pw, setPw] = useState(""); const [fehler, setFehler] = useState(""); const [pruefe, setPruefe] = useState(false); async function submit(e) { e?.preventDefault(); setPruefe(true); setFehler(""); try { await api("POST", "/api/analyst/admin/verify", { password: pw }); onLogin(pw); setOffen(false); setPw(""); } catch (err) { setFehler(err.message || "Falsches Passwort"); } finally { setPruefe(false); } } if (admin) { return (
ADMIN
); } return (
{offen && (
setPw(e.target.value)} placeholder="Passwort" style={{ width: "100%", padding: "7px 9px", fontSize: 13, border: "1px solid var(--line-strong)", borderRadius: 6 }} /> {fehler &&
{fehler}
}
)}
); } /* Ein Feedback-Block pro Output-Feld. In der Kunden-Ansicht rendert er nichts. */ function Feedback({ field }) { const { admin, password, runId } = React.useContext(AdminCtx); const [verdict, setVerdict] = useState(""); const [text, setText] = useState(""); const [status, setStatus] = useState(""); // "", "gespeichert", Fehlertext if (!admin || !runId) return null; async function speichern(v, t) { try { await api("POST", `/api/analyst/${runId}/feedback`, { password, field_id: field, verdict: v, text: t }); setStatus("gespeichert ✓"); setTimeout(() => setStatus(""), 1800); } catch (e) { setStatus(e.message || "Fehler"); } } const daumen = (wert, symbol) => ( ); return (
{daumen("up", "👍")}{daumen("down", "👎")} {field} {status}