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:
co-authored by
Claude Fable 5
parent
6a51699e90
commit
40602779ce
+44
-28
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user