GUI: Weggezogen-Bug (stale detail poll) gefixt + Redesign mit Dark/Light-Switch
- Detail-Poll bricht bei Auswahlwechsel ab, pollt nur Status, EIN Rebuild bei Fertigstellung mit Player-Restore - Themes via CSS-Variablen (data-theme, localStorage, OS-Default), Kupfer-Akzent, Wellenform-Wortmarke, Pill-Tabs, focus-visible, prefers-reduced-motion 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
7391b0ded7
commit
63c7d30b5e
+44
-4
@@ -13,18 +13,34 @@ const state = { tab: 'record', recording: false, recStart: 0, timer: null, selec
|
||||
function fmt(ms) { const s = Math.floor(ms / 1000); return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; }
|
||||
function toast(msg, err) { const t = document.createElement('div'); t.className = 'toast' + (err ? ' err' : ''); t.textContent = msg; document.body.appendChild(t); setTimeout(() => t.remove(), 4000); }
|
||||
|
||||
// Theme: data-theme on <html>, chosen in index.html before first paint (no
|
||||
// flash); the toggle persists to localStorage.
|
||||
function applyTheme(t) {
|
||||
document.documentElement.dataset.theme = t;
|
||||
localStorage.setItem('aime-theme', t);
|
||||
const b = document.querySelector('#themetoggle');
|
||||
if (b) b.textContent = t === 'dark' ? '☀️' : '🌙';
|
||||
}
|
||||
|
||||
function render() {
|
||||
app.classList.toggle('wide', state.tab === 'recordings');
|
||||
const theme = document.documentElement.dataset.theme || 'dark';
|
||||
app.innerHTML = `
|
||||
<header>
|
||||
<h1>📞 Aime <span class="sub">Anruf-Assistent · RK3588S</span></h1>
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<svg class="brand-wave" viewBox="0 0 34 20" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><line x1="3" y1="7" x2="3" y2="13"/><line x1="9" y1="4" x2="9" y2="16"/><line x1="15" y1="1" x2="15" y2="19"/><line x1="21" y1="5" x2="21" y2="15"/><line x1="27" y1="8" x2="27" y2="12"/><line x1="33" y1="9.5" x2="33" y2="10.5"/></g></svg>
|
||||
<h1>Aime <span class="sub">Anruf-Assistent</span></h1>
|
||||
</div>
|
||||
<nav>
|
||||
${['record', 'bluetooth', 'recordings', 'voices', 'glossary'].map(t =>
|
||||
`<button class="tab ${state.tab === t ? 'active' : ''}" data-tab="${t}">${({ record: 'Aufnahme', bluetooth: 'Bluetooth', recordings: 'Aufnahmen', voices: 'Stimmen', glossary: 'Glossar' })[t]}</button>`).join('')}
|
||||
</nav>
|
||||
<button id="themetoggle" class="btn icon theme" title="Hell/Dunkel umschalten">${theme === 'dark' ? '☀️' : '🌙'}</button>
|
||||
</header>
|
||||
<main id="view"></main>`;
|
||||
app.querySelectorAll('.tab').forEach(b => b.onclick = () => { state.tab = b.dataset.tab; render(); });
|
||||
app.querySelector('#themetoggle').onclick = () =>
|
||||
applyTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark');
|
||||
({ record: viewRecord, bluetooth: viewBluetooth, recordings: viewRecordings, voices: viewVoices, glossary: viewGlossary })[state.tab]();
|
||||
}
|
||||
|
||||
@@ -417,8 +433,32 @@ async function openRecording(id) {
|
||||
catch (e) { toast(e.message, true); rtBtn.disabled = false; }
|
||||
};
|
||||
ensureAnaTicker();
|
||||
// Poll while transcription OR the NPU analysis is still running, then refresh.
|
||||
if (pending || transcribing) setTimeout(() => { const d = document.querySelector('#detail'); if (d && d.querySelector('.analysis')) openRecording(id); }, 5000);
|
||||
// Poll while transcription OR the NPU analysis is still running. The old
|
||||
// version re-ran openRecording(id) blindly every 5s — a stale closure: after
|
||||
// switching to ANOTHER recording the timer still fired and yanked the user
|
||||
// (and the playing audio) back to the analysing one. Now: stop when the
|
||||
// selection changed, poll ONLY the status while busy (no DOM rebuild), and
|
||||
// rebuild once when the results are in — restoring the player position.
|
||||
if (pending || transcribing) {
|
||||
const pollDetail = async () => {
|
||||
if (state.selectedId !== id || state.tab !== 'recordings') return; // user moved on
|
||||
if (!document.querySelector('#detail')) return; // view gone
|
||||
let cur;
|
||||
try { cur = await api.get('/api/recordings/' + id); } catch { return; }
|
||||
if (state.selectedId !== id || state.tab !== 'recordings') return; // re-check after await
|
||||
const busy = cur.analysis_status === 'pending' || cur.status === 'transcribing';
|
||||
if (busy) { setTimeout(pollDetail, 5000); return; }
|
||||
// Results arrived → one rebuild; keep the audio where the user had it.
|
||||
const a = document.querySelector('#detail #player');
|
||||
const keep = a && !a.paused ? { t: a.currentTime, play: true } : (a ? { t: a.currentTime, play: false } : null);
|
||||
await openRecording(id);
|
||||
if (keep && keep.t > 0) {
|
||||
const na = document.querySelector('#detail #player');
|
||||
if (na) { na.currentTime = keep.t; if (keep.play) na.play().catch(() => {}); }
|
||||
}
|
||||
};
|
||||
setTimeout(pollDetail, 5000);
|
||||
}
|
||||
const audio = detail.querySelector('#player');
|
||||
const spans = [...detail.querySelectorAll('.w')];
|
||||
spans.forEach(s => s.onclick = () => { audio.currentTime = (+s.dataset.start) / 1000 + 0.001; audio.play(); });
|
||||
|
||||
Reference in New Issue
Block a user