diff --git a/app/main.py b/app/main.py index 6300610..60fe05e 100644 --- a/app/main.py +++ b/app/main.py @@ -55,6 +55,7 @@ def initialize_db() -> None: filename TEXT NOT NULL, audio_path TEXT NOT NULL, transcript TEXT NOT NULL DEFAULT '', + corrected_transcript TEXT NOT NULL DEFAULT '', status TEXT NOT NULL )""" ) @@ -74,6 +75,8 @@ def initialize_db() -> None: columns = {row[1] for row in connection.execute("PRAGMA table_info(notes)")} if "original_audio_path" not in columns: connection.execute("ALTER TABLE notes ADD COLUMN original_audio_path TEXT") + if "corrected_transcript" not in columns: + connection.execute("ALTER TABLE notes ADD COLUMN corrected_transcript TEXT NOT NULL DEFAULT ''") initialize_db() @@ -636,7 +639,7 @@ def list_notes(q: str = "", status: str = "") -> list[dict[str, str]]: query = " AND ".join(f'"{term.replace(chr(34), chr(34) * 2)}"' for term in search_terms) if status: rows = connection.execute( - """SELECT n.id, n.created_at, n.filename, n.transcript, n.status + """SELECT n.id, n.created_at, n.filename, n.transcript, n.corrected_transcript, n.status FROM note_search s JOIN notes n ON n.id = s.note_id WHERE note_search MATCH ? AND n.status = ? ORDER BY n.created_at DESC LIMIT 100""", @@ -644,20 +647,20 @@ def list_notes(q: str = "", status: str = "") -> list[dict[str, str]]: ).fetchall() else: rows = connection.execute( - """SELECT n.id, n.created_at, n.filename, n.transcript, n.status + """SELECT n.id, n.created_at, n.filename, n.transcript, n.corrected_transcript, n.status FROM note_search s JOIN notes n ON n.id = s.note_id WHERE note_search MATCH ? ORDER BY n.created_at DESC LIMIT 100""", (query,), ).fetchall() elif status: rows = connection.execute( - """SELECT id, created_at, filename, transcript, status + """SELECT id, created_at, filename, transcript, corrected_transcript, status FROM notes WHERE status = ? ORDER BY created_at DESC LIMIT 100""", (status,), ).fetchall() else: rows = connection.execute( - """SELECT id, created_at, filename, transcript, status + """SELECT id, created_at, filename, transcript, corrected_transcript, status FROM notes ORDER BY created_at DESC LIMIT 100""" ).fetchall() return [dict(row) for row in rows] @@ -684,14 +687,24 @@ def delete_note(note_id: str) -> dict[str, str]: async def save_transcript(note_id: str, payload: dict[str, str]) -> dict[str, str]: transcript = payload.get("transcript", "").strip() with connect_db() as connection: - note = connection.execute("SELECT status FROM notes WHERE id = ?", (note_id,)).fetchone() + note = connection.execute("SELECT transcript FROM notes WHERE id = ?", (note_id,)).fetchone() if not note: raise HTTPException(status_code=404, detail="Note not found") - status = "written" if note["status"] == "written" else "transcribed" if transcript else "awaiting_transcription" - update_transcript(note_id, transcript, status) - if transcript and note["status"] != "written": - remove_transcribed_audio(note_id) - return {"id": note_id, "status": status} + with connect_db() as connection: + connection.execute( + "UPDATE notes SET corrected_transcript = ? WHERE id = ?", + (transcript, note_id), + ) + connection.execute("DELETE FROM note_search WHERE note_id = ?", (note_id,)) + searchable_text = transcript or note["transcript"] + if searchable_text.strip(): + connection.execute( + "INSERT INTO note_search (note_id, transcript) VALUES (?, ?)", + (note_id, searchable_text.strip()), + ) + connection.execute("UPDATE backup_state SET revision = revision + 1 WHERE id = 1") + schedule_backup() + return {"id": note_id, "status": "saved"} app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="frontend") \ No newline at end of file diff --git a/frontend/app.js b/frontend/app.js index 613b700..e86dbc2 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -6,7 +6,8 @@ const MAX_RECORDING_MS = 30 * 60 * 1000; const elements = Object.fromEntries([ 'record-button', 'record-title', 'record-help', 'record-time', 'record-meter-fill', 'audio-level', 'audio-level-fill', 'capture-message', 'audio-file-input', 'queue-list', 'queue-empty', 'queue-count', 'sync-button', 'archive-list', 'archive-empty', - 'network-state', 'storage-status', 'voice-mode', 'write-mode', 'voice-panel', 'write-panel', 'written-note', 'draft-status', 'save-written-note', + 'network-state', 'storage-status', 'main', 'voice-mode', 'write-mode', 'read-mode', 'voice-panel', 'write-panel', 'written-note', 'draft-status', 'save-written-note', + 'queue-section', 'archive-section', 'archive-title', 'settings-button', 'settings-dialog', 'backup-form', 'backup-fields', 'backup-url', 'backup-username', 'backup-password', 'clear-backup-password', 'close-settings', 'test-backup', 'backup-status', 'retry-backup', ].map((id) => [id, document.getElementById(id)])); @@ -71,12 +72,18 @@ const deleteTextRecord = (id) => textTransaction('readwrite', (store) => store.d function setCaptureMode(mode) { const writing = mode === 'write'; - elements['voice-mode'].classList.toggle('active', !writing); + const reading = mode === 'read'; + elements['voice-mode'].classList.toggle('active', !writing && !reading); elements['write-mode'].classList.toggle('active', writing); - elements['voice-mode'].setAttribute('aria-selected', String(!writing)); + elements['read-mode'].classList.toggle('active', reading); + elements['voice-mode'].setAttribute('aria-selected', String(!writing && !reading)); elements['write-mode'].setAttribute('aria-selected', String(writing)); - elements['voice-panel'].hidden = writing; - elements['write-panel'].hidden = !writing; + elements['read-mode'].setAttribute('aria-selected', String(reading)); + elements['voice-panel'].hidden = writing || reading; + elements['write-panel'].hidden = !writing || reading; + elements['main'].classList.toggle('reading', reading); + elements['archive-title'].textContent = reading ? 'Stored transcriptions' : 'Saved notes'; + loadAwaitingNotes(); } async function saveDraft() { @@ -537,12 +544,19 @@ function renderArchive(notes) { const transcript = document.createElement('textarea'); transcript.className = 'archive-transcript'; transcript.placeholder = isWrittenNote ? 'Write or edit your note…' : note.status === 'transcribing' ? 'Transcription in progress…' : 'Transcript not available yet. Add or correct it here.'; - transcript.value = note.transcript; - transcript.setAttribute('aria-label', `${isWrittenNote ? 'Note' : 'Transcript'} for ${note.filename}`); + transcript.value = note.corrected_transcript || note.transcript; + transcript.setAttribute('aria-label', `${isWrittenNote ? 'Note' : 'Edited transcript'} for ${note.filename}`); + const original = document.createElement('details'); + original.className = 'original-transcript'; + const originalLabel = document.createElement('summary'); + originalLabel.textContent = 'Original transcript'; + const originalText = document.createElement('p'); + originalText.textContent = note.transcript || 'No original transcript is available.'; + original.append(originalLabel, originalText); const save = document.createElement('button'); save.className = 'save-transcript'; save.type = 'button'; - save.textContent = isWrittenNote ? 'SAVE NOTE' : 'SAVE TRANSCRIPT'; + save.textContent = isWrittenNote ? 'SAVE NOTE' : 'SAVE EDIT'; save.addEventListener('click', async () => { save.disabled = true; try { @@ -550,7 +564,10 @@ function renderArchive(notes) { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ transcript: transcript.value }), }); if (!response.ok) throw new Error('Save failed'); - await loadAwaitingNotes(); + save.textContent = 'SAVED'; + window.setTimeout(() => { + if (save.isConnected) save.textContent = isWrittenNote ? 'SAVE NOTE' : 'SAVE EDIT'; + }, 1500); } catch { save.textContent = 'RETRY SAVE'; } finally { @@ -569,9 +586,9 @@ function renderArchive(notes) { progress.setAttribute('aria-valuemax', '100'); progress.setAttribute('aria-valuetext', 'In progress'); progress.append(document.createElement('span')); - body.append(name, transcript, save, status, progress); + body.append(name, original, transcript, save, status, progress); } else { - body.append(name, transcript, save, status); + body.append(name, original, transcript, save, status); } const remove = createDeleteButton(`Delete note ${note.filename}`); remove.addEventListener('click', async () => { @@ -596,13 +613,15 @@ async function loadAwaitingNotes() { return; } try { - const responses = await Promise.all([ - fetch('/api/notes?status=awaiting_transcription'), - fetch('/api/notes?status=written'), - fetch('/api/notes?status=transcribing'), - fetch('/api/notes?status=transcription_failed'), - fetch('/api/notes?status=empty_transcript'), - ]); + const responses = elements['main'].classList.contains('reading') + ? [await fetch('/api/notes')] + : await Promise.all([ + fetch('/api/notes?status=awaiting_transcription'), + fetch('/api/notes?status=written'), + fetch('/api/notes?status=transcribing'), + fetch('/api/notes?status=transcription_failed'), + fetch('/api/notes?status=empty_transcript'), + ]); if (responses.some((response) => !response.ok)) throw new Error('Archive unavailable'); const notes = (await Promise.all(responses.map((response) => response.json()))).flat(); notes.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); @@ -617,6 +636,7 @@ async function loadAwaitingNotes() { elements['record-button'].addEventListener('click', () => mediaRecorder?.state === 'recording' ? stopRecording() : startRecording()); elements['voice-mode'].addEventListener('click', () => setCaptureMode('voice')); elements['write-mode'].addEventListener('click', () => setCaptureMode('write')); +elements['read-mode'].addEventListener('click', () => setCaptureMode('read')); elements['written-note'].addEventListener('input', scheduleDraftSave); elements['save-written-note'].addEventListener('click', queueWrittenNote); window.addEventListener('pagehide', () => { diff --git a/frontend/index.html b/frontend/index.html index 045d7dd..68dfc7c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -50,6 +50,7 @@
New recordings wait here until the server confirms they’re stored.
-