feat(history): migrate history to SQLite with FTS5 search
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
188
src/lib/stores/history.svelte.js
Normal file
188
src/lib/stores/history.svelte.js
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 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<boolean>} 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;
|
||||
}
|
||||
Reference in New Issue
Block a user