diff --git a/app/web/src/main.js b/app/web/src/main.js index 466f93e..9fbec63 100644 --- a/app/web/src/main.js +++ b/app/web/src/main.js @@ -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 ? `
${spkOrder.map((sp, i) => `
- ${escapeHtml(spkLabel(sp, i))} + ${escapeHtml(spkLabel(sp, i))}
${(segsBySpk[sp] || []).map(s => ``).join('')}
@@ -406,24 +406,22 @@ async function openRecording(id) {
+
` : ''; const mods = { analyse: ` ${r.summary ? `

${escapeHtml(r.summary)}

` : '
Noch keine Zusammenfassung.
'} ${(r.participants && r.participants.length) ? `

Teilnehmer

` : ''}`, todos: (r.todos && r.todos.length) ? `` : '
Keine ToDos erkannt.
', - speakers: spkOrder.length ? ` -
Namen vergeben → App lernt die Stimme.
-
${spkOrder.map((sp, i) => ` -
- - - - -
`).join('')} -
` : '', }; - 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); diff --git a/app/web/src/style.css b/app/web/src/style.css index 15e2043..a9b01e8 100644 --- a/app/web/src/style.css +++ b/app/web/src/style.css @@ -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; } diff --git a/docs/08-changelog.md b/docs/08-changelog.md index 08febf3..ab8d42b 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: 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.