diff --git a/app/web/src/main.js b/app/web/src/main.js index 73f7db6..ff1f8c1 100644 --- a/app/web/src/main.js +++ b/app/web/src/main.js @@ -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; // 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 = { analyse: ` ${r.summary ? `

${escapeHtml(r.summary)}

` : '
Noch keine Zusammenfassung.
'} @@ -420,12 +422,8 @@ async function openRecording(id) { `).join('')} ` : '', - transcript: ` - ${r.transcript_clean ? `

Dialog (KI · Sprecher aus Kontext)

${renderDialog(r.transcript_clean)}
` : ''} -
Klick auf ein Wort → Audio springt dorthin. Leertaste = Play/Pause.
-
${renderTurns(r.words, names, scls, spkOrder)}
`, }; - const defOrder = ['analyse', 'viz', 'todos', 'speakers', 'transcript']; + const defOrder = ['analyse', 'viz', 'todos', 'speakers']; let order = []; try { order = JSON.parse(localStorage.getItem('aime-detail-order') || '[]'); } catch {} order = order.filter(m => mods[m] !== undefined).concat(defOrder.filter(m => !order.includes(m))); @@ -442,12 +440,26 @@ async function openRecording(id) { ${(errored && !transcribeFailed) ? `
KI-Analyse fehlgeschlagen.
` : ''} ${(!pending && !errored && !transcribing && !transcribeFailed) ? `` : ''} -
- ${order.filter(m => mods[m]).map(m => ` -
-
⠿

${MOD_TITLES[m]}

-
${mods[m]}
-
`).join('')} +
+
+
+ ${order.filter(m => mods[m]).map(m => ` +
+
⠿

${MOD_TITLES[m]}

+
${mods[m]}
+
`).join('')} +
+
+
+
+

Transkript

+
+ ${r.transcript_clean ? `

Dialog (KI · Sprecher aus Kontext)

${renderDialog(r.transcript_clean)}
` : ''} +
Klick auf ein Wort → Audio springt dorthin. Leertaste = Play/Pause.
+
${renderTurns(r.words, names, scls, spkOrder)}
+
+
+
`; @@ -587,7 +599,16 @@ async function openRecording(id) { let active = null; for (const s of spans) { if (+s.dataset.start <= t) active = s; else break; } 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); }; // Rename a speaker + (optionally) learn their voice into the DB. diff --git a/app/web/src/style.css b/app/web/src/style.css index e75bd2a..184187e 100644 --- a/app/web/src/style.css +++ b/app/web/src/style.css @@ -199,8 +199,18 @@ audio { width: 100%; margin-bottom: 10px; } .item.rec:hover { border-color: var(--accent); } .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; } +.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; } .module { background: var(--inset); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; } .module.dragging { opacity: .45; border-style: dashed; } diff --git a/docs/08-changelog.md b/docs/08-changelog.md index 37e4c95..9d3b593 100644 --- a/docs/08-changelog.md +++ b/docs/08-changelog.md @@ -3,6 +3,13 @@ Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum, 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 Detail-Ansicht ist jetzt modular: fester Kopf (Titel, Player, Status), darunter Blöcke