Add typed generation preferences

This commit is contained in:
Georg Bauer
2026-07-24 15:26:15 +02:00
parent ad7167e84a
commit bccce697af
9 changed files with 529 additions and 28 deletions

View File

@@ -4,6 +4,7 @@ pub(crate) use view::app_theme;
use crate::database::{AppPreferences, Database, ProjectWithSessions};
use crate::model::{self, DownloadOutcome, DownloadProgress, ManagedArtifactId, ModelChoice};
use crate::settings::{GenerationPreferences, ReasoningMode};
use iced::{Size, Subscription, Task, keyboard, window};
use rfd::AsyncFileDialog;
use std::fs;
@@ -21,18 +22,109 @@ struct PreferenceDraft {
model: ModelChoice,
dspark_enabled: bool,
idle_timeout_minutes: String,
context_tokens: String,
max_generated_tokens: String,
system_prompt: String,
temperature: String,
top_p: String,
min_p: String,
seed: String,
reasoning_mode: ReasoningMode,
}
impl PreferenceDraft {
fn from_saved(preferences: &AppPreferences) -> Result<Self, String> {
let model = ModelChoice::from_id(&preferences.selected_model)
.ok_or_else(|| format!("Unsupported model: {}", preferences.selected_model))?;
let generation = preferences.generation()?;
Ok(Self {
model,
dspark_enabled: model.supports_dspark() && preferences.dspark_enabled,
idle_timeout_minutes: preferences.idle_timeout_minutes.to_string(),
context_tokens: generation.context_tokens.to_string(),
max_generated_tokens: generation.max_generated_tokens.to_string(),
system_prompt: generation.system_prompt,
temperature: generation
.temperature
.map_or_else(String::new, |value| value.to_string()),
top_p: generation
.top_p
.map_or_else(String::new, |value| value.to_string()),
min_p: generation
.min_p
.map_or_else(String::new, |value| value.to_string()),
seed: generation
.seed
.map_or_else(String::new, |value| value.to_string()),
reasoning_mode: generation.reasoning_mode,
})
}
fn generation(&self) -> Result<GenerationPreferences, String> {
let preferences = GenerationPreferences {
context_tokens: parse_positive_i32("Context tokens", &self.context_tokens)?,
max_generated_tokens: parse_positive_i32(
"Maximum generated tokens",
&self.max_generated_tokens,
)?,
system_prompt: self.system_prompt.clone(),
temperature: parse_optional_f32("Temperature", &self.temperature)?,
top_p: parse_optional_f32("Top-p", &self.top_p)?,
min_p: parse_optional_f32("Min-p", &self.min_p)?,
seed: parse_optional_u64("Seed", &self.seed)?,
reasoning_mode: self.reasoning_mode,
};
preferences.validate()?;
Ok(preferences)
}
fn reset(&mut self) {
let defaults = GenerationPreferences::default();
self.model = ModelChoice::default();
self.dspark_enabled = false;
self.idle_timeout_minutes = "10".into();
self.context_tokens = defaults.context_tokens.to_string();
self.max_generated_tokens = defaults.max_generated_tokens.to_string();
self.system_prompt = defaults.system_prompt;
self.temperature.clear();
self.top_p.clear();
self.min_p.clear();
self.seed.clear();
self.reasoning_mode = defaults.reasoning_mode;
}
}
fn parse_positive_i32(name: &str, value: &str) -> Result<i32, String> {
value
.trim()
.parse::<i32>()
.ok()
.filter(|value| *value > 0)
.ok_or_else(|| format!("{name} must be a positive whole number."))
}
fn parse_optional_f32(name: &str, value: &str) -> Result<Option<f32>, String> {
let value = value.trim();
if value.is_empty() {
Ok(None)
} else {
value
.parse()
.map(Some)
.map_err(|_| format!("{name} must be a number or left blank for the DS4 default."))
}
}
fn parse_optional_u64(name: &str, value: &str) -> Result<Option<u64>, String> {
let value = value.trim();
if value.is_empty() {
Ok(None)
} else {
value
.parse()
.map(Some)
.map_err(|_| format!("{name} must be a positive whole number or left blank."))
}
}
pub(crate) struct App {
@@ -96,6 +188,15 @@ pub(crate) enum Message {
PreferenceModelChanged(ModelChoice),
PreferenceDsparkChanged(bool),
PreferenceTimeoutChanged(String),
PreferenceContextChanged(String),
PreferenceMaxTokensChanged(String),
PreferenceSystemPromptChanged(String),
PreferenceTemperatureChanged(String),
PreferenceTopPChanged(String),
PreferenceMinPChanged(String),
PreferenceSeedChanged(String),
PreferenceReasoningChanged(ReasoningMode),
ResetPreferences,
SavePreferences,
DownloadArtifact(ManagedArtifactId),
ValidateArtifact(ManagedArtifactId),
@@ -237,6 +338,42 @@ impl App {
self.preference_draft.idle_timeout_minutes = value;
self.preference_error = None;
}
Message::PreferenceContextChanged(value) => {
self.preference_draft.context_tokens = value;
self.preference_error = None;
}
Message::PreferenceMaxTokensChanged(value) => {
self.preference_draft.max_generated_tokens = value;
self.preference_error = None;
}
Message::PreferenceSystemPromptChanged(value) => {
self.preference_draft.system_prompt = value;
self.preference_error = None;
}
Message::PreferenceTemperatureChanged(value) => {
self.preference_draft.temperature = value;
self.preference_error = None;
}
Message::PreferenceTopPChanged(value) => {
self.preference_draft.top_p = value;
self.preference_error = None;
}
Message::PreferenceMinPChanged(value) => {
self.preference_draft.min_p = value;
self.preference_error = None;
}
Message::PreferenceSeedChanged(value) => {
self.preference_draft.seed = value;
self.preference_error = None;
}
Message::PreferenceReasoningChanged(value) => {
self.preference_draft.reasoning_mode = value;
self.preference_error = None;
}
Message::ResetPreferences => {
self.preference_draft.reset();
self.preference_error = None;
}
Message::SavePreferences => self.save_preferences(),
Message::DownloadArtifact(artifact) => {
self.start_model_operation(artifact, ModelOperation::Download)
@@ -464,13 +601,25 @@ impl App {
self.preference_error = Some("Idle timeout must be between 1 and 1440 minutes.".into());
return;
}
let generation = match self.preference_draft.generation() {
Ok(generation) => generation,
Err(error) => {
self.preference_error = Some(error);
return;
}
};
let model = self.preference_draft.model;
let dspark_enabled = model.supports_dspark() && self.preference_draft.dspark_enabled;
let Some(database) = &mut self.database else {
return;
};
match database.update_preferences(model.id(), dspark_enabled, idle_timeout_minutes) {
match database.update_preferences(
model.id(),
dspark_enabled,
idle_timeout_minutes,
&generation,
) {
Ok(preferences) => {
self.preferences = preferences;
self.preference_draft = PreferenceDraft::from_saved(&self.preferences)