Add history limit setting (#150)

* Add history limit setting

Dearest Reviewer,

I have added a history limit setting. The limit change is applied instantly.
If the limit is 0 then no recordings are saved.

Becker

* remove extra save

save happens on exit

* move to debug menu

* use the text color

---------

Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
becker 2025-10-08 13:01:09 -07:00 committed by GitHub
parent 1432bac30f
commit d0e6a17c17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 136 additions and 40 deletions

View file

@ -44,8 +44,24 @@ pub async fn delete_history_entry(
id: i64, id: i64,
) -> Result<(), String> { ) -> Result<(), String> {
history_manager history_manager
.as_ref()
.delete_entry(id) .delete_entry(id)
.await .await
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
#[tauri::command]
pub async fn update_history_limit(
app: AppHandle,
history_manager: State<'_, Arc<HistoryManager>>,
limit: usize,
) -> Result<(), String> {
let mut settings = crate::settings::get_settings(&app);
settings.history_limit = limit;
crate::settings::write_settings(&app, settings);
history_manager
.update_history_limit(limit)
.map_err(|e| e.to_string())?;
Ok(())
}

View file

@ -162,8 +162,10 @@ pub fn run() {
TranscriptionManager::new(&app, model_manager.clone()) TranscriptionManager::new(&app, model_manager.clone())
.expect("Failed to initialize transcription manager"), .expect("Failed to initialize transcription manager"),
); );
let history_manager = let history_manager = Arc::new(
Arc::new(HistoryManager::new(&app).expect("Failed to initialize history manager")); HistoryManager::new(&app, settings.history_limit)
.expect("Failed to initialize history manager"),
);
// Add managers to Tauri's managed state // Add managers to Tauri's managed state
app.manage(recording_manager.clone()); app.manage(recording_manager.clone());
@ -243,7 +245,8 @@ pub fn run() {
commands::history::get_history_entries, commands::history::get_history_entries,
commands::history::toggle_history_entry_saved, commands::history::toggle_history_entry_saved,
commands::history::get_audio_file_path, commands::history::get_audio_file_path,
commands::history::delete_history_entry commands::history::delete_history_entry,
commands::history::update_history_limit
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View file

@ -5,13 +5,12 @@ use rusqlite::{params, Connection, OptionalExtension};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use tauri::{App, AppHandle, Emitter, Manager}; use tauri::{App, AppHandle, Emitter, Manager};
use tauri_plugin_sql::{Migration, MigrationKind}; use tauri_plugin_sql::{Migration, MigrationKind};
use crate::audio_toolkit::save_wav_file; use crate::audio_toolkit::save_wav_file;
const HISTORY_LIMIT: usize = 5;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HistoryEntry { pub struct HistoryEntry {
pub id: i64, pub id: i64,
@ -22,15 +21,15 @@ pub struct HistoryEntry {
pub transcription_text: String, pub transcription_text: String,
} }
#[derive(Clone)]
pub struct HistoryManager { pub struct HistoryManager {
app_handle: AppHandle, app_handle: AppHandle,
recordings_dir: PathBuf, recordings_dir: PathBuf,
db_path: PathBuf, db_path: PathBuf,
history_limit: AtomicUsize,
} }
impl HistoryManager { impl HistoryManager {
pub fn new(app: &App) -> Result<Self> { pub fn new(app: &App, history_limit: usize) -> Result<Self> {
let app_handle = app.app_handle().clone(); let app_handle = app.app_handle().clone();
// Create recordings directory in app data dir // Create recordings directory in app data dir
@ -48,6 +47,7 @@ impl HistoryManager {
app_handle, app_handle,
recordings_dir, recordings_dir,
db_path, db_path,
history_limit: AtomicUsize::new(history_limit),
}; };
// Initialize database // Initialize database
@ -99,6 +99,11 @@ impl HistoryManager {
audio_samples: Vec<f32>, audio_samples: Vec<f32>,
transcription_text: String, transcription_text: String,
) -> Result<()> { ) -> Result<()> {
// If history limit is 0, do not save at all.
if self.history_limit.load(Ordering::Relaxed) == 0 {
return Ok(());
}
let timestamp = Utc::now().timestamp(); let timestamp = Utc::now().timestamp();
let file_name = format!("handy-{}.wav", timestamp); let file_name = format!("handy-{}.wav", timestamp);
let title = self.format_timestamp_title(timestamp); let title = self.format_timestamp_title(timestamp);
@ -155,8 +160,9 @@ impl HistoryManager {
entries.push(row?); entries.push(row?);
} }
if entries.len() > HISTORY_LIMIT { let limit = self.history_limit.load(Ordering::Relaxed);
let entries_to_delete = &entries[HISTORY_LIMIT..]; if entries.len() > limit {
let entries_to_delete = &entries[limit..];
for (id, file_name) in entries_to_delete { for (id, file_name) in entries_to_delete {
// Delete database entry // Delete database entry
@ -242,19 +248,21 @@ impl HistoryManager {
let conn = self.get_connection()?; let conn = self.get_connection()?;
let mut stmt = conn.prepare( let mut stmt = conn.prepare(
"SELECT id, file_name, timestamp, saved, title, transcription_text "SELECT id, file_name, timestamp, saved, title, transcription_text
FROM transcription_history WHERE id = ?1" FROM transcription_history WHERE id = ?1",
)?; )?;
let entry = stmt.query_row([id], |row| { let entry = stmt
Ok(HistoryEntry { .query_row([id], |row| {
id: row.get("id")?, Ok(HistoryEntry {
file_name: row.get("file_name")?, id: row.get("id")?,
timestamp: row.get("timestamp")?, file_name: row.get("file_name")?,
saved: row.get("saved")?, timestamp: row.get("timestamp")?,
title: row.get("title")?, saved: row.get("saved")?,
transcription_text: row.get("transcription_text")?, title: row.get("title")?,
transcription_text: row.get("transcription_text")?,
})
}) })
}).optional()?; .optional()?;
Ok(entry) Ok(entry)
} }
@ -299,4 +307,10 @@ impl HistoryManager {
format!("Recording {}", timestamp) format!("Recording {}", timestamp)
} }
} }
pub fn update_history_limit(&self, new_limit: usize) -> Result<()> {
self.history_limit.swap(new_limit, Ordering::Relaxed);
self.cleanup_old_entries()?;
Ok(())
}
} }

View file

@ -110,6 +110,8 @@ pub struct AppSettings {
pub model_unload_timeout: ModelUnloadTimeout, pub model_unload_timeout: ModelUnloadTimeout,
#[serde(default = "default_word_correction_threshold")] #[serde(default = "default_word_correction_threshold")]
pub word_correction_threshold: f64, pub word_correction_threshold: f64,
#[serde(default = "default_history_limit")]
pub history_limit: usize,
#[serde(default)] #[serde(default)]
pub paste_method: PasteMethod, pub paste_method: PasteMethod,
} }
@ -146,6 +148,10 @@ fn default_word_correction_threshold() -> f64 {
0.18 0.18
} }
fn default_history_limit() -> usize {
5
}
pub const SETTINGS_STORE_PATH: &str = "settings_store.json"; pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
pub fn get_default_settings() -> AppSettings { pub fn get_default_settings() -> AppSettings {
@ -186,6 +192,7 @@ pub fn get_default_settings() -> AppSettings {
custom_words: Vec::new(), custom_words: Vec::new(),
model_unload_timeout: ModelUnloadTimeout::Never, model_unload_timeout: ModelUnloadTimeout::Never,
word_correction_threshold: default_word_correction_threshold(), word_correction_threshold: default_word_correction_threshold(),
history_limit: default_history_limit(),
paste_method: PasteMethod::default(), paste_method: PasteMethod::default(),
} }
} }

View file

@ -2,6 +2,7 @@ import React from "react";
import { WordCorrectionThreshold } from "./debug/WordCorrectionThreshold"; import { WordCorrectionThreshold } from "./debug/WordCorrectionThreshold";
import { AppDataDirectory } from "./AppDataDirectory"; import { AppDataDirectory } from "./AppDataDirectory";
import { SettingsGroup } from "../ui/SettingsGroup"; import { SettingsGroup } from "../ui/SettingsGroup";
import { HistoryLimit } from "./HistoryLimit";
export const DebugSettings: React.FC = () => { export const DebugSettings: React.FC = () => {
return ( return (
@ -9,6 +10,7 @@ export const DebugSettings: React.FC = () => {
<SettingsGroup title="Debug"> <SettingsGroup title="Debug">
<WordCorrectionThreshold descriptionMode="tooltip" grouped={true} /> <WordCorrectionThreshold descriptionMode="tooltip" grouped={true} />
<AppDataDirectory descriptionMode="tooltip" grouped={true} /> <AppDataDirectory descriptionMode="tooltip" grouped={true} />
<HistoryLimit descriptionMode="tooltip" grouped={true} />
</SettingsGroup> </SettingsGroup>
</div> </div>
); );

View file

@ -0,0 +1,48 @@
import React from "react";
import { useSettings } from "../../hooks/useSettings";
import { Input } from "../ui/Input";
import { SettingContainer } from "../ui/SettingContainer";
interface HistoryLimitProps {
descriptionMode?: "tooltip" | "inline";
grouped?: boolean;
}
export const HistoryLimit: React.FC<HistoryLimitProps> = ({
descriptionMode = "inline",
grouped = false,
}) => {
const { getSetting, updateSetting, isUpdating } = useSettings();
const historyLimit = getSetting("history_limit") ?? 5;
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value, 10);
if (!isNaN(value) && value >= 0) {
updateSetting("history_limit", value);
}
};
return (
<SettingContainer
title="History Limit"
description="Maximum number of transcription entries to keep in history"
descriptionMode={descriptionMode}
grouped={grouped}
layout="horizontal"
>
<div className="flex items-center space-x-2">
<Input
type="number"
min="0"
max="1000"
value={historyLimit}
onChange={handleChange}
disabled={isUpdating("history_limit")}
className="w-20"
/>
<span className="text-sm text-text">entries</span>
</div>
</SettingContainer>
);
};

View file

@ -18,3 +18,4 @@ export { CustomWords } from "./CustomWords";
export { AppDataDirectory } from "./AppDataDirectory"; export { AppDataDirectory } from "./AppDataDirectory";
export { ModelUnloadTimeoutSetting } from "./ModelUnloadTimeout"; export { ModelUnloadTimeoutSetting } from "./ModelUnloadTimeout";
export { StartHidden } from "./StartHidden"; export { StartHidden } from "./StartHidden";
export { HistoryLimit } from "./HistoryLimit";

View file

@ -53,6 +53,7 @@ export const SettingsSchema = z.object({
custom_words: z.array(z.string()).optional().default([]), custom_words: z.array(z.string()).optional().default([]),
model_unload_timeout: ModelUnloadTimeoutSchema.optional().default("never"), model_unload_timeout: ModelUnloadTimeoutSchema.optional().default("never"),
word_correction_threshold: z.number().optional().default(0.18), word_correction_threshold: z.number().optional().default(0.18),
history_limit: z.number().optional().default(5),
paste_method: PasteMethodSchema.optional().default("ctrl_v"), paste_method: PasteMethodSchema.optional().default("ctrl_v"),
}); });

View file

@ -45,6 +45,7 @@ const DEFAULT_SETTINGS: Partial<Settings> = {
overlay_position: "bottom", overlay_position: "bottom",
debug_mode: false, debug_mode: false,
custom_words: [], custom_words: [],
history_limit: 5,
}; };
const DEFAULT_AUDIO_DEVICE: AudioDevice = { const DEFAULT_AUDIO_DEVICE: AudioDevice = {
@ -224,6 +225,9 @@ export const useSettingsStore = create<SettingsStore>()(
case "paste_method": case "paste_method":
await invoke("change_paste_method_setting", { method: value }); await invoke("change_paste_method_setting", { method: value });
break; break;
case "history_limit":
await invoke("update_history_limit", { limit: value });
break;
case "bindings": case "bindings":
case "selected_model": case "selected_model":
break; break;
@ -265,15 +269,15 @@ export const useSettingsStore = create<SettingsStore>()(
set((state) => ({ set((state) => ({
settings: state.settings settings: state.settings
? { ? {
...state.settings, ...state.settings,
bindings: { bindings: {
...state.settings.bindings, ...state.settings.bindings,
[id]: { [id]: {
...state.settings.bindings[id], ...state.settings.bindings[id],
current_binding: binding, current_binding: binding,
},
}, },
} },
}
: null, : null,
})); }));
@ -286,15 +290,15 @@ export const useSettingsStore = create<SettingsStore>()(
set((state) => ({ set((state) => ({
settings: state.settings settings: state.settings
? { ? {
...state.settings, ...state.settings,
bindings: { bindings: {
...state.settings.bindings, ...state.settings.bindings,
[id]: { [id]: {
...state.settings.bindings[id], ...state.settings.bindings[id],
current_binding: originalBinding, current_binding: originalBinding,
},
}, },
} },
}
: null, : null,
})); }));
} }