const btn = document.getElementById('record-btn'); const statusText = document.getElementById('status-text'); const headerStatus = document.getElementById('header-status'); const instructionsEl = document.getElementById('instructions'); const transcriptList = document.getElementById('transcript-list'); const userChip = document.getElementById('user-chip'); const logoutBtn = document.getElementById('logout-btn'); const modal = document.getElementById('modal'); const modalTitle = document.getElementById('modal-title'); const modalBody = document.getElementById('modal-body'); const modalObsidianBtn = document.getElementById('modal-obsidian-btn'); const modalFolderBtn = document.getElementById('modal-folder-btn'); const modalOpenBtn = document.getElementById('modal-open-btn'); const modalCloseBtn = document.getElementById('modal-close-btn'); let _modalPath = null; let _modalPaths = null; let _modalFilename = null; const speakerCard = document.getElementById('speaker-card'); const speakerRows = document.getElementById('speaker-rows'); const speakerConfirmBtn = document.getElementById('speaker-confirm-btn'); const speakerAnonymBtn = document.getElementById('speaker-anonym-btn'); // state for excerpt navigation: { speakerId: { excerpts: [], idx: 0 } } let _speakerState = {}; const STATUS_LABELS = { idle: 'Bereit', recording: 'Aufnahme läuft\u2026', processing: 'Wird verarbeitet\u2026', awaiting_speakers: 'Sprecher benennen\u2026', error: 'Fehler', }; // Auth token is stored in sessionStorage so it's gone when the tab closes. // On first load, if no token is present the server will redirect to /login. const token = sessionStorage.getItem('token'); function authHeaders() { return token ? { 'Authorization': `Bearer ${token}` } : {}; } function apiFetch(url, options = {}) { return fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...authHeaders(), ...(options.headers || {}) }, }); } logoutBtn.addEventListener('click', () => { apiFetch('/logout', { method: 'POST' }).finally(() => { sessionStorage.removeItem('token'); location.href = '/login'; }); }); function openModal(filename, path, paths) { _modalPath = path; _modalPaths = paths || null; _modalFilename = filename; modalTitle.textContent = filename.replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-\d{4}-/, ''); modalBody.innerHTML = ''; modal.classList.remove('hidden'); apiFetch(`/transcripts/${encodeURIComponent(filename)}`) .then(r => r.text()) .then(md => { modalBody.innerHTML = DOMPurify.sanitize(marked.parse(md)); }); } function closeModal() { modal.classList.add('hidden'); _modalPath = null; _modalPaths = null; _modalFilename = null; } modalCloseBtn.addEventListener('click', closeModal); modal.querySelector('.modal-backdrop').addEventListener('click', closeModal); document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); }); modalObsidianBtn.addEventListener('click', () => { if (_modalPaths) { apiFetch('/open', { method: 'POST', body: JSON.stringify({ paths: Object.values(_modalPaths), mode: 'obsidian' }) }); } else if (_modalPath) { apiFetch('/open', { method: 'POST', body: JSON.stringify({ path: _modalPath, mode: 'obsidian' }) }); } }); modalFolderBtn.addEventListener('click', () => { if (_modalPath) apiFetch('/open', { method: 'POST', body: JSON.stringify({ path: _modalPath, mode: 'folder' }) }); }); modalOpenBtn.addEventListener('click', () => { if (_modalPath) apiFetch('/open', { method: 'POST', body: JSON.stringify({ path: _modalPath }) }); }); speakerConfirmBtn.addEventListener('click', () => submitSpeakers(true)); speakerAnonymBtn.addEventListener('click', () => submitSpeakers(false)); instructionsEl.addEventListener('input', async () => { await apiFetch('/instructions', { method: 'POST', body: JSON.stringify({ instructions: instructionsEl.value }), }); }); function setStatus(status) { btn.className = status; headerStatus.className = `status-badge ${status}`; const label = STATUS_LABELS[status] || status; statusText.textContent = status === 'error' ? label + ' — klicken zum Zurücksetzen' : label; headerStatus.textContent = label; btn.disabled = status === 'processing'; } btn.addEventListener('click', async () => { const r = await apiFetch('/toggle', { method: 'POST' }); const data = await r.json(); if (data.action === 'started') { setStatus('recording'); } else if (data.action === 'reset') { setStatus('idle'); } }); function connectWs() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket(`${proto}//${location.host}/ws?token=${encodeURIComponent(token || '')}`); ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.event === 'processing') setStatus('processing'); if (msg.event === 'saved') { setStatus('idle'); loadTranscripts(); } if (msg.event === 'error') { setStatus('error'); } if (msg.event === 'speakers_unknown') { setStatus('awaiting_speakers'); showSpeakerCard(msg.speakers); } }; ws.onclose = () => setTimeout(connectWs, 2000); } function showSpeakerCard(speakers) { _speakerState = {}; speakerRows.innerHTML = ''; speakers.forEach(({ id, excerpts }) => { _speakerState[id] = { excerpts, idx: 0 }; const row = document.createElement('div'); row.className = 'speaker-row'; const nav = document.createElement('div'); nav.className = 'excerpt-nav'; const prevBtn = document.createElement('button'); prevBtn.className = 'excerpt-nav-btn'; prevBtn.textContent = '‹'; prevBtn.title = 'Vorheriger Ausschnitt'; const nextBtn = document.createElement('button'); nextBtn.className = 'excerpt-nav-btn'; nextBtn.textContent = '›'; nextBtn.title = 'Nächster Ausschnitt'; const counter = document.createElement('span'); counter.className = 'excerpt-counter'; const excerptEl = document.createElement('div'); excerptEl.className = 'speaker-excerpt'; function updateExcerpt() { const st = _speakerState[id]; excerptEl.textContent = `"${st.excerpts[st.idx]}"`; counter.textContent = `${st.idx + 1} / ${st.excerpts.length}`; prevBtn.disabled = st.idx === 0; nextBtn.disabled = st.idx === st.excerpts.length - 1; } prevBtn.addEventListener('click', () => { _speakerState[id].idx--; updateExcerpt(); }); nextBtn.addEventListener('click', () => { _speakerState[id].idx++; updateExcerpt(); }); nav.append(prevBtn, counter, nextBtn); const input = document.createElement('input'); input.type = 'text'; input.className = 'speaker-name-input'; input.placeholder = `Name für ${id.replace('SPEAKER_', 'Sprecher ')}`; input.dataset.speakerId = id; row.append(nav, excerptEl, input); speakerRows.appendChild(row); updateExcerpt(); }); speakerCard.classList.remove('hidden'); } function hideSpeakerCard() { speakerCard.classList.add('hidden'); speakerRows.innerHTML = ''; _speakerState = {}; } async function submitSpeakers(useNames) { const mapping = {}; if (useNames) { speakerRows.querySelectorAll('.speaker-name-input').forEach(inp => { mapping[inp.dataset.speakerId] = inp.value.trim(); }); } else { Object.keys(_speakerState).forEach(id => { mapping[id] = ''; }); } hideSpeakerCard(); setStatus('processing'); await apiFetch('/speakers', { method: 'POST', body: JSON.stringify(mapping) }); } async function loadTranscripts() { const r = await apiFetch('/transcripts'); if (!r.ok) return; const items = await r.json(); transcriptList.replaceChildren( ...items.map((t) => { const div = document.createElement('div'); div.className = 'transcript-item'; const dateMatch = t.filename.match(/^(\d{4}-\d{2}-\d{2})-(\d{2})(\d{2})-/); const dateEl = document.createElement('span'); dateEl.className = 'meta item-date'; dateEl.textContent = dateMatch ? `${dateMatch[1]} ${dateMatch[2]}:${dateMatch[3]}` : ''; const name = document.createElement('span'); name.className = 'name'; name.textContent = t.filename.replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-\d{4}-/, ''); const meta = document.createElement('span'); meta.className = 'meta'; meta.textContent = `${Math.round(t.size / 1024 * 10) / 10} KB`; div.addEventListener('click', () => openModal(t.filename, t.path)); const reprocessBtn = document.createElement('button'); reprocessBtn.className = 'del-btn'; reprocessBtn.title = 'Neu verarbeiten'; reprocessBtn.innerHTML = ''; reprocessBtn.addEventListener('click', async (e) => { e.stopPropagation(); reprocessBtn.disabled = true; await apiFetch(`/transcripts/${encodeURIComponent(t.filename)}/reprocess`, { method: 'POST', body: JSON.stringify({ instructions: instructionsEl.value }), }); reprocessBtn.disabled = false; loadTranscripts(); }); const delBtn = document.createElement('button'); delBtn.className = 'del-btn'; delBtn.title = 'Löschen'; delBtn.innerHTML = ''; delBtn.addEventListener('click', async (e) => { e.stopPropagation(); await apiFetch(`/transcripts/${encodeURIComponent(t.filename)}`, { method: 'DELETE' }); loadTranscripts(); }); const actions = document.createElement('div'); actions.className = 'item-actions'; actions.append(reprocessBtn, delBtn); div.append(dateEl, name, meta, actions); return div; }) ); } (async () => { const r = await apiFetch('/status'); if (r.status === 401) { location.href = '/login'; return; } const data = await r.json(); setStatus(data.status); if (data.username) { userChip.textContent = data.username; } if (data.is_admin) { const gearLink = document.createElement('a'); gearLink.href = '/settings'; gearLink.className = 'back-btn'; gearLink.title = 'Einstellungen'; gearLink.textContent = '\u2699'; document.querySelector('.header-right').prepend(gearLink); } connectWs(); loadTranscripts(); })();