GUI: alle Module inkl. Transkript-Panel spaltenuebergreifend verschiebbar
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
40602779ce
commit
d55442a41f
+48
-37
@@ -391,7 +391,7 @@ async function openRecording(id) {
|
||||
// Draggable module blocks; the saved order wins, unknown/new modules append.
|
||||
// NOT modules: the transcript (own sticky right column) and the speaker-lane
|
||||
// visualizer (fixed full-width strip under the player, spanning both columns).
|
||||
const MOD_TITLES = { analyse: 'Zusammenfassung', todos: 'ToDos' };
|
||||
const MOD_TITLES = { analyse: 'Zusammenfassung', todos: 'ToDos', transcript: 'Transkript' };
|
||||
const vizHtml = spkOrder.length ? `
|
||||
<div class="viz-top">
|
||||
<div class="viz-wrap">
|
||||
@@ -420,11 +420,36 @@ async function openRecording(id) {
|
||||
${r.summary ? `<p class="summary">${escapeHtml(r.summary)}</p>` : '<div class="muted">Noch keine Zusammenfassung.</div>'}
|
||||
${(r.participants && r.participants.length) ? `<h3>Teilnehmer</h3><ul class="parts">${r.participants.map(p => `<li>👤 ${escapeHtml(p)}</li>`).join('')}</ul>` : ''}`,
|
||||
todos: (r.todos && r.todos.length) ? `<ul class="todos">${r.todos.map(t => `<li>${escapeHtml(t)}</li>`).join('')}</ul>` : '<div class="muted">Keine ToDos erkannt.</div>',
|
||||
transcript: `
|
||||
<div id="transcript-view">
|
||||
${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>
|
||||
<div id="table-view" class="hidden">${buildTableScene(spkOrder, scls, names, spkLabel)}</div>`,
|
||||
};
|
||||
const defOrder = ['analyse', 'todos'];
|
||||
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)));
|
||||
// Two drop columns; every module (incl. the transcript/table panel) can be
|
||||
// dragged within AND between them. Layout persists for all recordings.
|
||||
const defLayout = { main: ['analyse', 'todos'], side: ['transcript'] };
|
||||
let layout = null;
|
||||
try { layout = JSON.parse(localStorage.getItem('aime-detail-layout') || 'null'); } catch {}
|
||||
if (!layout || !Array.isArray(layout.main) || !Array.isArray(layout.side)) {
|
||||
// migrate the old single-column order if present
|
||||
let old = [];
|
||||
try { old = JSON.parse(localStorage.getItem('aime-detail-order') || '[]'); } catch {}
|
||||
layout = { main: old.filter(m => m !== 'transcript'), side: [] };
|
||||
}
|
||||
layout.main = layout.main.filter(m => mods[m] !== undefined);
|
||||
layout.side = layout.side.filter(m => mods[m] !== undefined && !layout.main.includes(m));
|
||||
for (const m of [...defLayout.main, ...defLayout.side])
|
||||
if (!layout.main.includes(m) && !layout.side.includes(m))
|
||||
(defLayout.side.includes(m) ? layout.side : layout.main).push(m);
|
||||
|
||||
const modHtml = (m) => `
|
||||
<section class="module${m === 'transcript' ? ' transcript-panel' : ''}" data-mod="${m}" ${m === 'transcript' ? 'id="transcript-panel"' : ''}>
|
||||
<div class="mod-head" draggable="true"><span class="mod-grip" title="Ziehen zum Umsortieren">⠿</span><h3 ${m === 'transcript' ? 'id="sidetitle"' : ''}>${MOD_TITLES[m]}</h3></div>
|
||||
<div class="mod-body">${mods[m]}</div>
|
||||
</section>`;
|
||||
|
||||
detail.innerHTML = `
|
||||
<div class="card">
|
||||
@@ -441,36 +466,19 @@ async function openRecording(id) {
|
||||
${spkOrder.length ? `<button class="btn small icon" id="viewswitch" title="Ansicht wechseln: Konferenztisch / Transkript"></button>` : ''}
|
||||
</div>
|
||||
<div class="detail-cols">
|
||||
<div class="detail-main">
|
||||
<div id="modules">
|
||||
${order.filter(m => mods[m]).map(m => `
|
||||
<section class="module" data-mod="${m}">
|
||||
<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 id="sidetitle">Transkript</h3></div>
|
||||
<div class="mod-body">
|
||||
<div id="transcript-view">
|
||||
${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>
|
||||
<div id="table-view" class="hidden">${buildTableScene(spkOrder, scls, names, spkLabel)}</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="mod-col" data-col="main">${layout.main.map(modHtml).join('')}</div>
|
||||
<div class="mod-col" data-col="side">${layout.side.map(modHtml).join('')}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Drag & drop: grab a module by its header, drop it above/below the others;
|
||||
// the order is saved and applies to every recording.
|
||||
const modWrap = detail.querySelector('#modules');
|
||||
// Drag & drop across BOTH columns: grab any module by its header, drop it in
|
||||
// either column at any position; the layout is saved for all recordings.
|
||||
const cols = [...detail.querySelectorAll('.mod-col')];
|
||||
// ":empty" never matches because of template whitespace -> class-driven.
|
||||
const markEmpty = () => cols.forEach(c => c.classList.toggle('empty', !c.querySelector('.module')));
|
||||
markEmpty();
|
||||
let dragged = null;
|
||||
modWrap.querySelectorAll('.mod-head').forEach(h => {
|
||||
detail.querySelectorAll('.mod-head').forEach(h => {
|
||||
h.addEventListener('dragstart', (e) => {
|
||||
dragged = h.closest('.module');
|
||||
dragged.classList.add('dragging');
|
||||
@@ -481,17 +489,20 @@ async function openRecording(id) {
|
||||
if (!dragged) return;
|
||||
dragged.classList.remove('dragging');
|
||||
dragged = null;
|
||||
const now = [...modWrap.querySelectorAll('.module')].map(m => m.dataset.mod);
|
||||
localStorage.setItem('aime-detail-order', JSON.stringify(now));
|
||||
const save = {};
|
||||
for (const c of cols) save[c.dataset.col] = [...c.querySelectorAll('.module')].map(m => m.dataset.mod);
|
||||
localStorage.setItem('aime-detail-layout', JSON.stringify(save));
|
||||
markEmpty();
|
||||
});
|
||||
});
|
||||
modWrap.addEventListener('dragover', (e) => {
|
||||
cols.forEach(col => col.addEventListener('dragover', (e) => {
|
||||
if (!dragged) return;
|
||||
e.preventDefault();
|
||||
const others = [...modWrap.querySelectorAll('.module:not(.dragging)')];
|
||||
const others = [...col.querySelectorAll('.module:not(.dragging)')];
|
||||
const next = others.find(m => e.clientY < m.getBoundingClientRect().top + m.offsetHeight / 2);
|
||||
if (next) modWrap.insertBefore(dragged, next); else modWrap.appendChild(dragged);
|
||||
});
|
||||
if (next) col.insertBefore(dragged, next); else col.appendChild(dragged);
|
||||
markEmpty();
|
||||
}));
|
||||
const reBtn = detail.querySelector('#reanalyze');
|
||||
if (reBtn) reBtn.onclick = async () => {
|
||||
// Re-runs speaker separation every time: empty = automatic detection (now
|
||||
|
||||
@@ -204,16 +204,18 @@ audio { width: 100%; margin-bottom: 10px; }
|
||||
/* ---- 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; }
|
||||
.mod-col { min-width: 0; display: flex; flex-direction: column; gap: 10px; min-height: 60px; }
|
||||
.mod-col[data-col="main"] { flex: 1 1 54%; }
|
||||
.mod-col[data-col="side"] { flex: 1 1 46%; }
|
||||
.mod-col.empty { flex: 0 0 26px; border: 1px dashed var(--line); border-radius: 12px; align-self: stretch; }
|
||||
.transcript-panel { max-height: calc(100vh - 90px); overflow-y: auto; position: sticky; top: 12px; }
|
||||
.transcript-panel .mod-head { position: sticky; top: 0; z-index: 1; }
|
||||
@media (max-width: 1100px) {
|
||||
.detail-cols { flex-direction: column; }
|
||||
.detail-side { position: static; width: 100%; }
|
||||
.transcript-panel { max-height: 60vh; }
|
||||
.mod-col { width: 100%; }
|
||||
.mod-col.empty { display: none; }
|
||||
.transcript-panel { max-height: 60vh; position: static; }
|
||||
}
|
||||
#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; }
|
||||
.mod-head {
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
|
||||
Was, Warum, betroffene Dateien/Backups).
|
||||
|
||||
## 2026-07-17 — GUI: Transkript/Am-Tisch-Panel ebenfalls verschiebbar (zwei Drop-Spalten)
|
||||
|
||||
Alle Bloecke — Zusammenfassung, ToDos UND das Transkript-/Tisch-Panel — sind jetzt per Drag & Drop
|
||||
frei anordenbar, auch SPALTENUEBERGREIFEND: zwei Drop-Zonen (links 54 % / rechts 46 %), Layout
|
||||
persistiert als localStorage['aime-detail-layout'] {main:[...], side:[...]} (altes
|
||||
aime-detail-order wird migriert). Eine leere Spalte kollabiert zu einem schmalen gestrichelten
|
||||
Streifen und bleibt Drop-Ziel (JS-Klasse .empty — :empty scheitert an Template-Whitespace).
|
||||
Bundle `index-B1y2-Z_b.js`.
|
||||
|
||||
## 2026-07-17 — GUI: Sprecher-Modul entfernt, Umbenennen direkt am Verlauf
|
||||
|
||||
Das separate "Sprecher"-Modul war redundant zu den Spur-Labels. Jetzt: Klick auf einen Namen links
|
||||
|
||||
Reference in New Issue
Block a user