skill_web/store/
settings.rs

1//! Settings state store
2//!
3//! Persisted to localStorage for cross-session settings.
4
5use serde::{Deserialize, Serialize};
6use yewdux::prelude::*;
7
8/// Theme options
9#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
10pub enum Theme {
11    Light,
12    Dark,
13    #[default]
14    System,
15}
16
17impl Theme {
18    pub fn as_str(&self) -> &'static str {
19        match self {
20            Self::Light => "light",
21            Self::Dark => "dark",
22            Self::System => "system",
23        }
24    }
25}
26
27/// Output format options
28#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
29pub enum OutputFormat {
30    #[default]
31    Json,
32    Raw,
33    Formatted,
34}
35
36impl OutputFormat {
37    pub fn as_str(&self) -> &'static str {
38        match self {
39            Self::Json => "json",
40            Self::Raw => "raw",
41            Self::Formatted => "formatted",
42        }
43    }
44}
45
46/// Embedding provider options
47#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
48pub enum EmbeddingProvider {
49    #[default]
50    FastEmbed,
51    OpenAI,
52    Ollama,
53}
54
55impl EmbeddingProvider {
56    pub fn as_str(&self) -> &'static str {
57        match self {
58            Self::FastEmbed => "fastembed",
59            Self::OpenAI => "openai",
60            Self::Ollama => "ollama",
61        }
62    }
63
64    pub fn display_name(&self) -> &'static str {
65        match self {
66            Self::FastEmbed => "FastEmbed (Local)",
67            Self::OpenAI => "OpenAI",
68            Self::Ollama => "Ollama",
69        }
70    }
71}
72
73/// Vector store backend options
74#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
75pub enum VectorBackend {
76    #[default]
77    InMemory,
78    Qdrant,
79}
80
81impl VectorBackend {
82    pub fn as_str(&self) -> &'static str {
83        match self {
84            Self::InMemory => "inmemory",
85            Self::Qdrant => "qdrant",
86        }
87    }
88
89    pub fn display_name(&self) -> &'static str {
90        match self {
91            Self::InMemory => "In-Memory",
92            Self::Qdrant => "Qdrant",
93        }
94    }
95}
96
97/// Search pipeline settings
98#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
99pub struct SearchSettings {
100    /// Embedding provider
101    pub embedding_provider: EmbeddingProvider,
102    /// Embedding model name
103    pub embedding_model: String,
104    /// Vector store backend
105    pub vector_backend: VectorBackend,
106    /// Enable hybrid search (BM25 + Vector)
107    pub enable_hybrid: bool,
108    /// Enable reranking (Cross-encoder)
109    pub enable_reranking: bool,
110    /// Qdrant URL (if using Qdrant)
111    pub qdrant_url: Option<String>,
112    /// Ollama URL (if using Ollama)
113    pub ollama_url: Option<String>,
114}
115
116impl Default for SearchSettings {
117    fn default() -> Self {
118        Self {
119            embedding_provider: EmbeddingProvider::FastEmbed,
120            embedding_model: "BAAI/bge-small-en-v1.5".to_string(),
121            vector_backend: VectorBackend::InMemory,
122            enable_hybrid: false,
123            enable_reranking: false,
124            qdrant_url: None,
125            ollama_url: Some("http://localhost:11434".to_string()),
126        }
127    }
128}
129
130/// API connection settings
131#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
132pub struct ApiSettings {
133    /// Base URL for the API
134    pub base_url: String,
135    /// Request timeout in seconds
136    pub timeout_secs: u32,
137}
138
139impl Default for ApiSettings {
140    fn default() -> Self {
141        Self {
142            base_url: "http://127.0.0.1:3000".to_string(),
143            timeout_secs: 30,
144        }
145    }
146}
147
148/// Settings store state
149#[derive(Clone, Debug, PartialEq, Store, Serialize, Deserialize)]
150#[store(storage = "local", storage_tab_sync)]
151pub struct SettingsStore {
152    /// UI theme
153    pub theme: Theme,
154    /// Default execution timeout in seconds
155    pub default_timeout: u32,
156    /// Default output format
157    pub output_format: OutputFormat,
158    /// Include metadata in execution output
159    pub include_metadata: bool,
160    /// History retention count
161    pub history_retention: u32,
162    /// Whether onboarding has been completed
163    pub onboarding_completed: bool,
164    /// Search pipeline settings
165    pub search: SearchSettings,
166    /// API connection settings
167    pub api: ApiSettings,
168    /// Keyboard shortcuts enabled
169    pub keyboard_shortcuts: bool,
170    /// Show tool parameters by default
171    pub expand_parameters: bool,
172    /// Auto-refresh interval in seconds (0 = disabled)
173    pub auto_refresh_interval: u32,
174}
175
176impl Default for SettingsStore {
177    fn default() -> Self {
178        Self {
179            theme: Theme::System,
180            default_timeout: 30,
181            output_format: OutputFormat::Json,
182            include_metadata: false,
183            history_retention: 1000,
184            onboarding_completed: false,
185            search: SearchSettings::default(),
186            api: ApiSettings::default(),
187            keyboard_shortcuts: true,
188            expand_parameters: false,
189            auto_refresh_interval: 0,
190        }
191    }
192}
193
194impl SettingsStore {
195    /// Check if the user needs to go through onboarding
196    pub fn needs_onboarding(&self) -> bool {
197        !self.onboarding_completed
198    }
199
200    /// Get the effective theme (resolving system preference)
201    pub fn effective_theme(&self) -> Theme {
202        // In a real implementation, we'd check the system preference here
203        // For now, just return the stored theme
204        self.theme.clone()
205    }
206}
207
208/// Settings store actions
209pub enum SettingsAction {
210    SetTheme(Theme),
211    SetDefaultTimeout(u32),
212    SetOutputFormat(OutputFormat),
213    SetIncludeMetadata(bool),
214    SetHistoryRetention(u32),
215    CompleteOnboarding,
216    // Search settings
217    SetEmbeddingProvider(EmbeddingProvider),
218    SetEmbeddingModel(String),
219    SetVectorBackend(VectorBackend),
220    SetEnableHybrid(bool),
221    SetEnableReranking(bool),
222    SetQdrantUrl(Option<String>),
223    SetOllamaUrl(Option<String>),
224    // API settings
225    SetApiBaseUrl(String),
226    SetApiTimeout(u32),
227    // UI preferences
228    SetKeyboardShortcuts(bool),
229    SetExpandParameters(bool),
230    SetAutoRefreshInterval(u32),
231    // Reset
232    ResetToDefaults,
233    ResetSearchSettings,
234}
235
236impl Reducer<SettingsStore> for SettingsAction {
237    fn apply(self, mut store: std::rc::Rc<SettingsStore>) -> std::rc::Rc<SettingsStore> {
238        let state = std::rc::Rc::make_mut(&mut store);
239
240        match self {
241            SettingsAction::SetTheme(theme) => {
242                state.theme = theme;
243            }
244            SettingsAction::SetDefaultTimeout(timeout) => {
245                state.default_timeout = timeout;
246            }
247            SettingsAction::SetOutputFormat(format) => {
248                state.output_format = format;
249            }
250            SettingsAction::SetIncludeMetadata(include) => {
251                state.include_metadata = include;
252            }
253            SettingsAction::SetHistoryRetention(count) => {
254                state.history_retention = count;
255            }
256            SettingsAction::CompleteOnboarding => {
257                state.onboarding_completed = true;
258            }
259            // Search settings
260            SettingsAction::SetEmbeddingProvider(provider) => {
261                state.search.embedding_provider = provider;
262            }
263            SettingsAction::SetEmbeddingModel(model) => {
264                state.search.embedding_model = model;
265            }
266            SettingsAction::SetVectorBackend(backend) => {
267                state.search.vector_backend = backend;
268            }
269            SettingsAction::SetEnableHybrid(enable) => {
270                state.search.enable_hybrid = enable;
271            }
272            SettingsAction::SetEnableReranking(enable) => {
273                state.search.enable_reranking = enable;
274            }
275            SettingsAction::SetQdrantUrl(url) => {
276                state.search.qdrant_url = url;
277            }
278            SettingsAction::SetOllamaUrl(url) => {
279                state.search.ollama_url = url;
280            }
281            // API settings
282            SettingsAction::SetApiBaseUrl(url) => {
283                state.api.base_url = url;
284            }
285            SettingsAction::SetApiTimeout(timeout) => {
286                state.api.timeout_secs = timeout;
287            }
288            // UI preferences
289            SettingsAction::SetKeyboardShortcuts(enable) => {
290                state.keyboard_shortcuts = enable;
291            }
292            SettingsAction::SetExpandParameters(expand) => {
293                state.expand_parameters = expand;
294            }
295            SettingsAction::SetAutoRefreshInterval(interval) => {
296                state.auto_refresh_interval = interval;
297            }
298            // Reset
299            SettingsAction::ResetToDefaults => {
300                *state = SettingsStore::default();
301            }
302            SettingsAction::ResetSearchSettings => {
303                state.search = SearchSettings::default();
304            }
305        }
306
307        store
308    }
309}