189 lines
5.3 KiB
JavaScript
189 lines
5.3 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|