GUI: Transkript als eigene sticky Scroll-Spalte rechts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
This commit is contained in:
schakal00783
2026-07-17 00:51:04 +02:00
co-authored by Claude Fable 5
parent 4500e03ae4
commit 62e1dec960
3 changed files with 52 additions and 14 deletions
+34 -13
View File
@@ -389,7 +389,9 @@ async function openRecording(id) {
const durMs = r.duration_ms || (r.words && r.words.length ? r.words[r.words.length - 1].t_end_ms : 0) || 1; const durMs = r.duration_ms || (r.words && r.words.length ? r.words[r.words.length - 1].t_end_ms : 0) || 1;
// Draggable module blocks; the saved order wins, unknown/new modules append. // Draggable module blocks; the saved order wins, unknown/new modules append.
const MOD_TITLES = { analyse: 'Zusammenfassung', viz: 'Sprecher-Verlauf', todos: 'ToDos', speakers: 'Sprecher', transcript: 'Transkript' }; // The transcript is NOT a module: it lives in its own sticky, inner-scrolling
// right column so the (long) text never stretches the page.
const MOD_TITLES = { analyse: 'Zusammenfassung', viz: 'Sprecher-Verlauf', todos: 'ToDos', speakers: 'Sprecher' };
const mods = { const mods = {
analyse: ` analyse: `
${r.summary ? `<p class="summary">${escapeHtml(r.summary)}</p>` : '<div class="muted">Noch keine Zusammenfassung.</div>'} ${r.summary ? `<p class="summary">${escapeHtml(r.summary)}</p>` : '<div class="muted">Noch keine Zusammenfassung.</div>'}
@@ -420,12 +422,8 @@ async function openRecording(id) {
<button class="btn small spk-save" data-sp="${sp}">Speichern</button> <button class="btn small spk-save" data-sp="${sp}">Speichern</button>
</div>`).join('')} </div>`).join('')}
</div>` : '', </div>` : '',
transcript: `
${r.transcript_clean ? `<h3>Dialog <span class="muted" style="font-weight:400">(KI · Sprecher aus Kontext)</span></h3><div class="dialog">${renderDialog(r.transcript_clean)}</div>` : ''}
<div class="hint">Klick auf ein Wort → Audio springt dorthin. Leertaste = Play/Pause.</div>
<div id="transcript" class="transcript">${renderTurns(r.words, names, scls, spkOrder)}</div>`,
}; };
const defOrder = ['analyse', 'viz', 'todos', 'speakers', 'transcript']; const defOrder = ['analyse', 'viz', 'todos', 'speakers'];
let order = []; let order = [];
try { order = JSON.parse(localStorage.getItem('aime-detail-order') || '[]'); } catch {} try { order = JSON.parse(localStorage.getItem('aime-detail-order') || '[]'); } catch {}
order = order.filter(m => mods[m] !== undefined).concat(defOrder.filter(m => !order.includes(m))); order = order.filter(m => mods[m] !== undefined).concat(defOrder.filter(m => !order.includes(m)));
@@ -442,12 +440,26 @@ async function openRecording(id) {
${(errored && !transcribeFailed) ? `<div class="err-inline">KI-Analyse fehlgeschlagen. <button class="btn" id="reanalyze">Erneut versuchen</button></div>` : ''} ${(errored && !transcribeFailed) ? `<div class="err-inline">KI-Analyse fehlgeschlagen. <button class="btn" id="reanalyze">Erneut versuchen</button></div>` : ''}
${(!pending && !errored && !transcribing && !transcribeFailed) ? `<button class="btn small" id="reanalyze">↻ Analyse neu</button>` : ''} ${(!pending && !errored && !transcribing && !transcribeFailed) ? `<button class="btn small" id="reanalyze">↻ Analyse neu</button>` : ''}
</div> </div>
<div id="modules"> <div class="detail-cols">
${order.filter(m => mods[m]).map(m => ` <div class="detail-main">
<section class="module" data-mod="${m}"> <div id="modules">
<div class="mod-head" draggable="true"><span class="mod-grip" title="Ziehen zum Umsortieren">⠿</span><h3>${MOD_TITLES[m]}</h3></div> ${order.filter(m => mods[m]).map(m => `
<div class="mod-body">${mods[m]}</div> <section class="module" data-mod="${m}">
</section>`).join('')} <div class="mod-head" draggable="true"><span class="mod-grip" title="Ziehen zum Umsortieren">⠿</span><h3>${MOD_TITLES[m]}</h3></div>
<div class="mod-body">${mods[m]}</div>
</section>`).join('')}
</div>
</div>
<div class="detail-side">
<section class="module transcript-panel" id="transcript-panel">
<div class="mod-head"><h3>Transkript</h3></div>
<div class="mod-body">
${r.transcript_clean ? `<h3>Dialog <span class="muted" style="font-weight:400">(KI · Sprecher aus Kontext)</span></h3><div class="dialog">${renderDialog(r.transcript_clean)}</div>` : ''}
<div class="hint">Klick auf ein Wort → Audio springt dorthin. Leertaste = Play/Pause.</div>
<div id="transcript" class="transcript">${renderTurns(r.words, names, scls, spkOrder)}</div>
</div>
</section>
</div>
</div> </div>
</div>`; </div>`;
@@ -587,7 +599,16 @@ async function openRecording(id) {
let active = null; let active = null;
for (const s of spans) { if (+s.dataset.start <= t) active = s; else break; } for (const s of spans) { if (+s.dataset.start <= t) active = s; else break; }
spans.forEach(s => s.classList.toggle('active', s === active)); spans.forEach(s => s.classList.toggle('active', s === active));
if (active) { const r2 = active.getBoundingClientRect(); if (r2.top < 90 || r2.bottom > innerHeight - 40) active.scrollIntoView({ block: 'center', behavior: 'smooth' }); } // Follow the active word INSIDE the transcript panel (it scrolls itself now —
// scrollIntoView would also yank the page).
if (active) {
const panel = detail.querySelector('#transcript-panel');
if (panel) {
const pr = panel.getBoundingClientRect(), ar = active.getBoundingClientRect();
if (ar.top < pr.top + 40 || ar.bottom > pr.bottom - 40)
panel.scrollTo({ top: panel.scrollTop + (ar.top - pr.top) - panel.clientHeight / 2, behavior: 'smooth' });
}
}
updateViz(t); updateViz(t);
}; };
// Rename a speaker + (optionally) learn their voice into the DB. // Rename a speaker + (optionally) learn their voice into the DB.
+11 -1
View File
@@ -199,8 +199,18 @@ audio { width: 100%; margin-bottom: 10px; }
.item.rec:hover { border-color: var(--accent); } .item.rec:hover { border-color: var(--accent); }
.item.rec.sel { border-color: var(--accent); background: var(--accent-soft); } .item.rec.sel { border-color: var(--accent); background: var(--accent-soft); }
/* ---- Detail-Module (Drag & Drop) ---- */ /* ---- Detail: Module links, Transkript als eigene Scroll-Spalte rechts ---- */
.status-line { margin: 4px 0 10px; } .status-line { margin: 4px 0 10px; }
.detail-cols { display: flex; gap: 12px; align-items: flex-start; }
.detail-main { flex: 1 1 54%; min-width: 0; }
.detail-side { flex: 1 1 46%; min-width: 0; position: sticky; top: 12px; }
.transcript-panel { max-height: calc(100vh - 90px); overflow-y: auto; }
.transcript-panel .mod-head { position: sticky; top: 0; z-index: 1; cursor: default; }
@media (max-width: 1100px) {
.detail-cols { flex-direction: column; }
.detail-side { position: static; width: 100%; }
.transcript-panel { max-height: 60vh; }
}
#modules { display: flex; flex-direction: column; gap: 10px; } #modules { display: flex; flex-direction: column; gap: 10px; }
.module { background: var(--inset); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; } .module { background: var(--inset); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
.module.dragging { opacity: .45; border-style: dashed; } .module.dragging { opacity: .45; border-style: dashed; }
+7
View File
@@ -3,6 +3,13 @@
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum, Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
Was, Warum, betroffene Dateien/Backups). Was, Warum, betroffene Dateien/Backups).
## 2026-07-17 — GUI: Transkript als eigene Scroll-Spalte
Transkript (+ Dialog) aus den Drag&Drop-Modulen herausgelöst: eigene, sticky Spalte rechts neben
Zusammenfassung/Verlauf/ToDos/Sprecher, Höhe auf Viewport begrenzt, scrollt INTERN durch und folgt
dem aktiven Wort im Panel (statt die Seite zu ziehen). Unter 1100 px stapeln die Spalten
(Transkript max. 60 vh). Bundle .
## 2026-07-17 — GUI: Sprecher-Verlauf-Visualisierung + Drag&Drop-Module ## 2026-07-17 — GUI: Sprecher-Verlauf-Visualisierung + Drag&Drop-Module
Detail-Ansicht ist jetzt modular: fester Kopf (Titel, Player, Status), darunter Blöcke Detail-Ansicht ist jetzt modular: fester Kopf (Titel, Player, Status), darunter Blöcke