/** * History store backed by SQLite via Tauri commands. * Replaces the localStorage-based history management in page.svelte.js. * * All mutations call the backend first, then update the reactive state * on success. FTS5 search is handled server-side. */ import { invoke } from '@tauri-apps/api/core'; // Reactive state let transcripts = $state([]); let loading = $state(false); let searchActive = $state(false); // BroadcastChannel for multi-window sync const historyChannel = typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel('kon_history_v2') : null; if (historyChannel) { historyChannel.onmessage = (event) => { if (event.data.type === 'history_updated') { transcripts.length = 0; transcripts.push(...event.data.transcripts); } }; } function broadcastHistory() { if (historyChannel) { historyChannel.postMessage({ type: 'history_updated', transcripts: $state.snapshot(transcripts), }); } } /** * Load transcription history from the SQLite backend. * @param {number} [limit=100] - Maximum number of results */ export async function loadHistory(limit = 100) { loading = true; searchActive = false; try { const result = await invoke('list_all_transcripts', { limit }); transcripts.length = 0; transcripts.push(...result); } catch (e) { console.error('Failed to load history:', e); } finally { loading = false; } } /** * Save a completed transcription to the SQLite backend. * Also saves segments if provided. * * @param {object} entry - Transcript data matching the save_transcript command params * @returns {Promise} Whether the save succeeded */ export async function saveTranscript(entry) { try { await invoke('save_transcript', { id: entry.id, text: entry.text || '', source: entry.source || 'microphone', title: entry.title || null, audioPath: entry.audioPath || null, duration: entry.duration || 0, engine: entry.engine || null, modelId: entry.modelId || null, inferenceMs: entry.inferenceMs || null, sampleRate: entry.sampleRate || null, audioChannels: entry.audioChannels || null, formatMode: entry.formatMode || null, removeFillers: entry.removeFillers ?? false, britishEnglish: entry.britishEnglish ?? false, antiHallucination: entry.antiHallucination ?? false, }); // Save segments if provided if (entry.segments && entry.segments.length > 0) { const segmentTuples = entry.segments.map(s => [ s.start ?? s.startTime ?? 0, s.end ?? s.endTime ?? 0, s.text || '', ]); await invoke('save_segments', { transcriptId: entry.id, segments: segmentTuples, }); } // Optimistic local update — add to front of list transcripts.unshift({ id: entry.id, text: entry.text, source: entry.source, title: entry.title || null, audioPath: entry.audioPath || null, duration: entry.duration || 0, engine: entry.engine || null, modelId: entry.modelId || null, inferenceMs: entry.inferenceMs || null, sampleRate: entry.sampleRate || null, audioChannels: entry.audioChannels || null, formatMode: entry.formatMode || null, removeFillers: entry.removeFillers ?? false, britishEnglish: entry.britishEnglish ?? false, antiHallucination: entry.antiHallucination ?? false, createdAt: new Date().toISOString(), // Keep original fields for UI compatibility during migration date: entry.date || new Date().toLocaleString('en-GB'), preview: entry.preview || (entry.text || '').slice(0, 120), segments: entry.segments || [], language: entry.language, template: entry.template, }); broadcastHistory(); return true; } catch (e) { console.error('Failed to save transcript:', e); return false; } } /** * Delete a transcript by ID. * @param {string} id - Transcript ID */ export async function deleteTranscript(id) { try { await invoke('delete_transcript_by_id', { id }); const idx = transcripts.findIndex(t => t.id === id); if (idx >= 0) transcripts.splice(idx, 1); broadcastHistory(); } catch (e) { console.error('Failed to delete transcript:', e); } } /** * Full-text search across transcripts via FTS5. * Replaces the local state with search results. * @param {string} query - Search query * @param {number} [limit=50] - Maximum results */ export async function searchTranscripts(query, limit = 50) { if (!query || !query.trim()) { // Empty query — restore full list await loadHistory(); return; } loading = true; searchActive = true; try { const result = await invoke('search_all_transcripts', { query: query.trim(), limit, }); transcripts.length = 0; transcripts.push(...result); } catch (e) { // FTS5 can throw on malformed queries — fall back to full list console.error('Search failed, falling back to full list:', e); await loadHistory(); } finally { loading = false; } } /** Get the reactive transcripts array. */ export function getHistory() { return transcripts; } /** Whether history is currently loading from the backend. */ export function getLoading() { return loading; } /** Whether a search filter is currently active. */ export function isSearchActive() { return searchActive; }