GUI: Sprecher-Modul entfernt; Umbenennen/Lernen via Popover am Spur-Label

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 01:36:03 +02:00
co-authored by Claude Fable 5
parent 6a51699e90
commit 40602779ce
3 changed files with 60 additions and 29 deletions
+44 -28
View File
@@ -391,14 +391,14 @@ 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', speakers: 'Sprecher' };
const MOD_TITLES = { analyse: 'Zusammenfassung', todos: 'ToDos' };
const vizHtml = spkOrder.length ? `
<div class="viz-top">
<div class="viz-wrap">
<div class="lanes" id="lanes">
${spkOrder.map((sp, i) => `
<div class="lane-row ${scls[sp]}" data-sp="${sp}">
<span class="lane-label"><span class="spk-dot"></span>${escapeHtml(spkLabel(sp, i))}</span>
<span class="lane-label" title="Klicken: umbenennen / Stimme lernen"><span class="spk-dot"></span>${escapeHtml(spkLabel(sp, i))}</span>
<div class="lane">${(segsBySpk[sp] || []).map(s =>
`<span class="seg" style="left:${(s.start / durMs * 100).toFixed(3)}%;width:${Math.max(.18, (s.end - s.start) / durMs * 100).toFixed(3)}%"></span>`).join('')}
</div>
@@ -406,24 +406,22 @@ async function openRecording(id) {
<div class="playhead" id="playhead"></div>
</div>
</div>
<div class="spkpop hidden" id="spkpop">
<input class="input" id="spkpop-name" placeholder="Name">
<label class="spk-learn-l"><input type="checkbox" id="spkpop-learn" checked> als Stimme lernen</label>
<div class="row" style="margin:8px 0 0">
<button class="btn small primary" id="spkpop-save">Speichern</button>
<button class="btn small" id="spkpop-cancel">Abbrechen</button>
</div>
</div>
</div>` : '';
const mods = {
analyse: `
${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>',
speakers: spkOrder.length ? `
<div class="hint">Namen vergeben → App lernt die Stimme.</div>
<div class="spk-edit">${spkOrder.map((sp, i) => `
<div class="spk-row ${scls[sp]}">
<span class="spk-dot"></span>
<input class="spk-name" data-sp="${sp}" value="${escapeHtml(names[sp] || '')}" placeholder="${escapeHtml(spkLabel(sp, i))}">
<label class="spk-learn-l"><input type="checkbox" class="spk-learn" checked> als Stimme lernen</label>
<button class="btn small spk-save" data-sp="${sp}">Speichern</button>
</div>`).join('')}
</div>` : '',
};
const defOrder = ['analyse', 'todos', 'speakers'];
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)));
@@ -641,8 +639,9 @@ async function openRecording(id) {
const liveboxEl = detail.querySelector('#livebox');
let liveTurnIdx = -1;
if (lanesEl) {
// Click (or drag) inside the lanes seeks; ignore the label column.
// Click inside a LANE seeks; clicks on the label column open the editor.
const seek = (e) => {
if (!e.target.closest('.lane')) return;
const lane = lanesEl.querySelector('.lane');
if (!lane) return;
const rc = lane.getBoundingClientRect();
@@ -652,6 +651,36 @@ async function openRecording(id) {
};
lanesEl.addEventListener('click', seek);
}
// Speaker editor popover: click a lane label -> rename + optionally learn the
// voice (replaces the old separate "Sprecher" module).
const pop = detail.querySelector('#spkpop');
if (pop && lanesEl) {
let popSp = null;
const popName = pop.querySelector('#spkpop-name');
lanesEl.querySelectorAll('.lane-label').forEach(l => l.onclick = (e) => {
e.stopPropagation();
const row = l.closest('.lane-row');
popSp = row.dataset.sp;
pop.style.top = (row.offsetTop + 30) + 'px';
pop.classList.remove('hidden');
const i = spkOrder.indexOf(popSp);
popName.value = names[popSp] || '';
popName.placeholder = spkLabel(popSp, i >= 0 ? i : 0);
popName.focus();
});
pop.querySelector('#spkpop-cancel').onclick = () => pop.classList.add('hidden');
pop.querySelector('#spkpop-save').onclick = async () => {
const name = popName.value.trim();
const enroll = pop.querySelector('#spkpop-learn').checked;
const btn = pop.querySelector('#spkpop-save');
btn.disabled = true; btn.textContent = '…';
try {
const res = await api.post('/api/recordings/' + id + '/speaker', { speaker: popSp, name, enroll });
toast(res.enrolled ? `„${name}" gespeichert + Stimme gelernt` : (res.learnMsg ? 'Gespeichert (' + res.learnMsg + ')' : 'Gespeichert'));
openRecording(id);
} catch (e) { toast(e.message, true); btn.disabled = false; btn.textContent = 'Speichern'; }
};
}
const updateViz = (t) => {
if (!lanesEl) return;
if (playheadEl) {
@@ -706,20 +735,7 @@ async function openRecording(id) {
updateViz(t);
updateTable(t);
};
// Rename a speaker + (optionally) learn their voice into the DB.
detail.querySelectorAll('.spk-save').forEach(btn => {
btn.onclick = async () => {
const row = btn.closest('.spk-row');
const name = row.querySelector('.spk-name').value.trim();
const enroll = row.querySelector('.spk-learn').checked;
btn.disabled = true; btn.textContent = '…';
try {
const res = await api.post('/api/recordings/' + id + '/speaker', { speaker: btn.dataset.sp, name, enroll });
toast(res.enrolled ? `„${name}" gespeichert + Stimme gelernt` : (res.learnMsg ? 'Gespeichert (' + res.learnMsg + ')' : 'Gespeichert'));
openRecording(id);
} catch (e) { toast(e.message, true); btn.disabled = false; btn.textContent = 'Speichern'; }
};
});
// (Speaker renaming lives in the lane-label popover above.)
// Spacebar toggles play/pause of the open recording (unless typing in a field).
if (state.spaceHandler) document.removeEventListener('keydown', state.spaceHandler);
+9 -1
View File
@@ -228,9 +228,17 @@ audio { width: 100%; margin-bottom: 10px; }
/* ---- Sprecher-Verlauf: fester Streifen unter dem Player, volle Breite ---- */
.viz-top {
margin: 4px 0 10px; padding: 10px 12px; background: var(--inset);
position: relative; margin: 4px 0 10px; padding: 10px 12px; background: var(--inset);
border: 1px solid var(--line); border-radius: 12px;
}
.lane-label { cursor: pointer; }
.lane-label:hover { color: var(--fg); text-decoration: underline dotted; }
.spkpop {
position: absolute; left: 12px; z-index: 5; width: 270px;
background: var(--surface); border: 1px solid var(--accent); border-radius: 10px;
padding: 10px 12px; box-shadow: 0 6px 22px rgba(0, 0, 0, .35);
}
.spkpop .input { margin-bottom: 6px; }
.viz-wrap { display: flex; gap: 14px; align-items: stretch; }
.lanes { flex: 1 1 58%; min-width: 0; position: relative; display: flex; flex-direction: column; gap: 6px; cursor: pointer; }
.lane-row { display: flex; align-items: center; gap: 8px; }
+7
View File
@@ -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: Sprecher-Modul entfernt, Umbenennen direkt am Verlauf
Das separate "Sprecher"-Modul war redundant zu den Spur-Labels. Jetzt: Klick auf einen Namen links
im Sprecher-Verlauf oeffnet ein Popover (Name aendern + "als Stimme lernen" + Speichern) — gleiche
API wie vorher (POST /speaker). Klick-Seek in den Spuren zuendet nur noch IN einer Spur, nicht mehr
auf der Label-Spalte. Bundle `index-Bj1pQQtk.js`.
## 2026-07-17 — GUI: Blasen-Oeffnung zeigt exakt auf den Sprecher
Bei mehreren Sprechern je Tischkante (5-/6-Sitz-Layouts) zeigte der Schwanz nur auf die Kantenmitte.