// Tauri commands wrapping the kon_storage transcript CRUD. // These are the bridge that lets the Svelte frontend treat SQLite as the // canonical store rather than localStorage. // // Day 4 of the upgrade plan. The frontend HistoryPage rename flow has had // a `// TODO: persist to SQLite when update_transcript exists` for some // time; that command now exists. // // Task 16 โ€” the legacy global-dictionary commands // (`list_dictionary_command`, `add_dictionary_entry_command`, // `delete_dictionary_entry_command`) were dropped; profile-scoped // `profile_terms` commands in `commands::profiles` are now canonical. use serde::{Deserialize, Serialize}; use kon_storage::{ count_transcripts, delete_transcript as db_delete_transcript, get_transcript as db_get_transcript, insert_transcript as db_insert_transcript, list_transcripts_paged, search_transcripts as db_search_transcripts, update_transcript as db_update_transcript, update_transcript_meta as db_update_transcript_meta, InsertTranscriptParams, TranscriptRow, }; use crate::AppState; /// Frontend-facing shape of a stored transcript. Mirrors `TranscriptRow` /// from the storage crate but drops fields the UI does not need yet /// (sample_rate, audio_channels, format flags) and renames to camelCase. /// /// Task 2.5 โ€” `starred`, `manualTags`, `template`, `language`, `segmentsJson` /// were added to back the viewer metadata that previously lived only in the /// removed `kon_history` localStorage cache. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct TranscriptDto { pub id: String, pub text: String, pub source: String, pub profile_id: String, pub title: Option, pub audio_path: Option, pub duration: f64, pub engine: Option, pub model_id: Option, pub created_at: String, pub starred: bool, pub manual_tags: String, pub template: String, pub language: String, pub segments_json: String, } impl From for TranscriptDto { fn from(r: TranscriptRow) -> Self { Self { id: r.id, text: r.text, source: r.source, profile_id: r.profile_id, title: r.title, audio_path: r.audio_path, duration: r.duration, engine: r.engine, model_id: r.model_id, created_at: r.created_at, starred: r.starred, manual_tags: r.manual_tags, template: r.template, language: r.language, segments_json: r.segments_json, } } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateTranscriptRequest { pub id: String, pub text: String, pub source: String, pub profile_id: Option, pub title: Option, pub audio_path: Option, pub duration: f64, pub engine: Option, pub model_id: Option, pub inference_ms: Option, pub sample_rate: Option, pub audio_channels: Option, pub format_mode: Option, pub remove_fillers: bool, pub british_english: bool, pub anti_hallucination: bool, } /// Insert a transcript into the canonical SQLite store. Called from the /// frontend after a recording or file transcription completes. The FTS5 /// index is updated automatically by trigger. #[tauri::command] pub async fn add_transcript( state: tauri::State<'_, AppState>, transcript: CreateTranscriptRequest, ) -> Result<(), String> { let params = InsertTranscriptParams { id: &transcript.id, text: &transcript.text, source: &transcript.source, profile_id: transcript .profile_id .as_deref() .unwrap_or(kon_storage::DEFAULT_PROFILE_ID), title: transcript.title.as_deref(), audio_path: transcript.audio_path.as_deref(), duration: transcript.duration, engine: transcript.engine.as_deref(), model_id: transcript.model_id.as_deref(), inference_ms: transcript.inference_ms, sample_rate: transcript.sample_rate, audio_channels: transcript.audio_channels, format_mode: transcript.format_mode.as_deref(), remove_fillers: transcript.remove_fillers, british_english: transcript.british_english, anti_hallucination: transcript.anti_hallucination, }; db_insert_transcript(&state.db, ¶ms) .await .map_err(|e| e.to_string()) } /// Paginated history list. Defaults to 50 rows from the most recent. #[tauri::command] pub async fn list_transcripts( state: tauri::State<'_, AppState>, limit: Option, offset: Option, ) -> Result, String> { let limit = limit.unwrap_or(50).clamp(1, 500); let offset = offset.unwrap_or(0).max(0); list_transcripts_paged(&state.db, limit, offset) .await .map(|rows| rows.into_iter().map(TranscriptDto::from).collect()) .map_err(|e| e.to_string()) } /// Total count of transcripts (for "showing X of N" UI). #[tauri::command] pub async fn count_transcripts_command(state: tauri::State<'_, AppState>) -> Result { count_transcripts(&state.db) .await .map_err(|e| e.to_string()) } #[tauri::command] pub async fn get_transcript( state: tauri::State<'_, AppState>, id: String, ) -> Result, String> { db_get_transcript(&state.db, &id) .await .map(|opt| opt.map(TranscriptDto::from)) .map_err(|e| e.to_string()) } /// Update mutable fields of an existing transcript. `text` and/or `title`. /// Returns the row count affected (0 if id not found). Fixes the long- /// standing "rename in History never persists" bug per /// architecture-review.md ยง13. #[tauri::command] pub async fn update_transcript( state: tauri::State<'_, AppState>, id: String, text: Option, title: Option, ) -> Result { db_update_transcript(&state.db, &id, text.as_deref(), title.as_deref()) .await .map_err(|e| e.to_string()) } #[tauri::command] pub async fn delete_transcript( state: tauri::State<'_, AppState>, id: String, ) -> Result<(), String> { db_delete_transcript(&state.db, &id) .await .map_err(|e| e.to_string()) } /// FTS5 search. Query syntax: bare words AND together; quote phrases; /// supports `OR`, `NOT`, prefix `*`. Returns up to 50 best-rank matches. #[tauri::command] pub async fn search_transcripts( state: tauri::State<'_, AppState>, query: String, ) -> Result, String> { let q = query.trim(); if q.is_empty() { return Ok(Vec::new()); } db_search_transcripts(&state.db, q, 50) .await .map(|rows| rows.into_iter().map(TranscriptDto::from).collect()) .map_err(|e| e.to_string()) } /// Task 2.5 โ€” patch-style update for the transcripts_meta columns. Each /// field is optional: `Some` overwrites, omitted / `None` preserves via /// COALESCE server-side. Separate from `update_transcript` (text / title) /// so the existing command surface stays untouched. #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateTranscriptMetaRequest { #[serde(default)] pub starred: Option, #[serde(default)] pub manual_tags: Option, #[serde(default)] pub template: Option, #[serde(default)] pub language: Option, #[serde(default)] pub segments_json: Option, } #[tauri::command] pub async fn update_transcript_meta_cmd( state: tauri::State<'_, AppState>, id: String, patch: UpdateTranscriptMetaRequest, ) -> Result { let row = db_update_transcript_meta( &state.db, &id, patch.starred, patch.manual_tags.as_deref(), patch.template.as_deref(), patch.language.as_deref(), patch.segments_json.as_deref(), ) .await .map_err(|e| e.to_string())?; Ok(TranscriptDto::from(row)) }