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.
|
// Draggable module blocks; the saved order wins, unknown/new modules append.
|
||||||
// NOT modules: the transcript (own sticky right column) and the speaker-lane
|
// NOT modules: the transcript (own sticky right column) and the speaker-lane
|
||||||
// visualizer (fixed full-width strip under the player, spanning both columns).
|
// 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 ? `
|
const vizHtml = spkOrder.length ? `
|
||||||
<div class="viz-top">
|
<div class="viz-top">
|
||||||
<div class="viz-wrap">
|
<div class="viz-wrap">
|
||||||
<div class="lanes" id="lanes">
|
<div class="lanes" id="lanes">
|
||||||
${spkOrder.map((sp, i) => `
|
${spkOrder.map((sp, i) => `
|
||||||
<div class="lane-row ${scls[sp]}" data-sp="${sp}">
|
<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 =>
|
<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('')}
|
`<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>
|
</div>
|
||||||
@@ -406,24 +406,22 @@ async function openRecording(id) {
|
|||||||
<div class="playhead" id="playhead"></div>
|
<div class="playhead" id="playhead"></div>
|
||||||
</div>
|
</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>` : '';
|
</div>` : '';
|
||||||
const mods = {
|
const mods = {
|
||||||
analyse: `
|
analyse: `
|
||||||
${r.summary ? `<p class="summary">${escapeHtml(r.summary)}</p>` : '<div class="muted">Noch keine Zusammenfassung.</div>'}
|
${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>` : ''}`,
|
${(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>',
|
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 = [];
|
let order = [];
|
||||||
try { order = JSON.parse(localStorage.getItem('aime-detail-order') || '[]'); } catch {}
|
try { order = JSON.parse(localStorage.getItem('aime-detail-order') || '[]'); } catch {}
|
||||||
order = order.filter(m => mods[m] !== undefined).concat(defOrder.filter(m => !order.includes(m)));
|
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');
|
const liveboxEl = detail.querySelector('#livebox');
|
||||||
let liveTurnIdx = -1;
|
let liveTurnIdx = -1;
|
||||||
if (lanesEl) {
|
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) => {
|
const seek = (e) => {
|
||||||
|
if (!e.target.closest('.lane')) return;
|
||||||
const lane = lanesEl.querySelector('.lane');
|
const lane = lanesEl.querySelector('.lane');
|
||||||
if (!lane) return;
|
if (!lane) return;
|
||||||
const rc = lane.getBoundingClientRect();
|
const rc = lane.getBoundingClientRect();
|
||||||
@@ -652,6 +651,36 @@ async function openRecording(id) {
|
|||||||
};
|
};
|
||||||
lanesEl.addEventListener('click', seek);
|
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) => {
|
const updateViz = (t) => {
|
||||||
if (!lanesEl) return;
|
if (!lanesEl) return;
|
||||||
if (playheadEl) {
|
if (playheadEl) {
|
||||||
@@ -706,20 +735,7 @@ async function openRecording(id) {
|
|||||||
updateViz(t);
|
updateViz(t);
|
||||||
updateTable(t);
|
updateTable(t);
|
||||||
};
|
};
|
||||||
// Rename a speaker + (optionally) learn their voice into the DB.
|
// (Speaker renaming lives in the lane-label popover above.)
|
||||||
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'; }
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Spacebar toggles play/pause of the open recording (unless typing in a field).
|
// Spacebar toggles play/pause of the open recording (unless typing in a field).
|
||||||
if (state.spaceHandler) document.removeEventListener('keydown', state.spaceHandler);
|
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 ---- */
|
/* ---- Sprecher-Verlauf: fester Streifen unter dem Player, volle Breite ---- */
|
||||||
.viz-top {
|
.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;
|
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; }
|
.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; }
|
.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; }
|
.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,
|
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
|
||||||
Was, Warum, betroffene Dateien/Backups).
|
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
|
## 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.
|
Bei mehreren Sprechern je Tischkante (5-/6-Sitz-Layouts) zeigte der Schwanz nur auf die Kantenmitte.
|
||||||
|
|||||||
Reference in New Issue
Block a user