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:
co-authored by
Claude Fable 5
parent
4500e03ae4
commit
62e1dec960
+28
-7
@@ -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,6 +440,8 @@ 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 class="detail-cols">
|
||||||
|
<div class="detail-main">
|
||||||
<div id="modules">
|
<div id="modules">
|
||||||
${order.filter(m => mods[m]).map(m => `
|
${order.filter(m => mods[m]).map(m => `
|
||||||
<section class="module" data-mod="${m}">
|
<section class="module" data-mod="${m}">
|
||||||
@@ -449,6 +449,18 @@ async function openRecording(id) {
|
|||||||
<div class="mod-body">${mods[m]}</div>
|
<div class="mod-body">${mods[m]}</div>
|
||||||
</section>`).join('')}
|
</section>`).join('')}
|
||||||
</div>
|
</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>`;
|
||||||
|
|
||||||
// Drag & drop: grab a module by its header, drop it above/below the others;
|
// Drag & drop: grab a module by its header, drop it above/below the others;
|
||||||
@@ -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
@@ -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; }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user