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
${r.participants.map(p => `- 👤 ${escapeHtml(p)}
`).join('')}
` : ''}`,
todos: (r.todos && r.todos.length) ? `
${r.todos.map(t => `- ${escapeHtml(t)}
`).join('')}
` : '
Keine ToDos erkannt.
',
- speakers: spkOrder.length ? `
-
Namen vergeben → App lernt die Stimme.
-
` : '',
};
- 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.