diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..4cbb505 --- /dev/null +++ b/frontend/app.js @@ -0,0 +1,122 @@ +const btn = document.getElementById('record-btn'); +const statusText = document.getElementById('status-text'); +const headerStatus = document.getElementById('header-status'); +const preview = document.getElementById('preview'); +const instructionsEl = document.getElementById('instructions'); +const transcriptList = document.getElementById('transcript-list'); +const userChip = document.getElementById('user-chip'); +const logoutBtn = document.getElementById('logout-btn'); + +const STATUS_LABELS = { + idle: 'Bereit', + recording: 'Aufnahme läuft\u2026', + processing: 'Wird verarbeitet\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'; + }); +}); + +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 = label; + headerStatus.textContent = label; + btn.disabled = status === 'processing'; +} + +btn.addEventListener('click', () => apiFetch('/toggle', { method: 'POST' })); + +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 === 'transcribed' || msg.event === 'refined') { + const text = msg.raw || msg.markdown || ''; + preview.textContent = text; + preview.classList.add('has-content'); + } + if (msg.event === 'saved') { + setStatus('idle'); + loadTranscripts(); + } + if (msg.event === 'error') { + setStatus('idle'); + preview.textContent = `Fehler: ${msg.message}`; + } + }; + ws.onclose = () => setTimeout(connectWs, 2000); +} + +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 name = document.createElement('span'); + name.textContent = t.filename.replace('.md', ''); + + const meta = document.createElement('span'); + meta.className = 'meta'; + meta.textContent = `${Math.round(t.size / 1024 * 10) / 10} KB`; + + div.append(name, meta); + div.addEventListener('click', () => { + apiFetch('/open', { + method: 'POST', + body: JSON.stringify({ path: t.path }), + }); + }); + 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; + } + connectWs(); + loadTranscripts(); +})(); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..58bb770 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,170 @@ + + + + + + tüit Transkriptor + + + + + +
+
+

tüit Transkriptor

+
+ Bereit + + +
+
+
+
+ + Klicken zum Starten +
+ +
+ + +
+ +
+ +
Noch keine Aufnahme verarbeitet.
+
+ +
+ +
+
+
+ + +