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
|
||||
|
||||
Reference in New Issue
Block a user