1use crate::i18n::Locale;
9use orbok_models::SearchCapability;
10use orbok_search::SearchMode;
11use crate::notice::UserNotice;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum NavGroup {
16 Search,
17 Ai,
18 Settings,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ViewId {
24 Search,
25 Sources,
26 Indexing,
27 Storage,
28 Models,
29 Settings,
30}
31
32impl ViewId {
33 pub const ALL: &'static [ViewId] = &[
34 ViewId::Search,
35 ViewId::Sources,
36 ViewId::Indexing,
37 ViewId::Storage,
38 ViewId::Models,
39 ViewId::Settings,
40 ];
41
42 pub fn group(self) -> NavGroup {
44 match self {
45 ViewId::Search | ViewId::Sources => NavGroup::Search,
46 ViewId::Indexing | ViewId::Storage | ViewId::Models => NavGroup::Ai,
47 ViewId::Settings => NavGroup::Settings,
48 }
49 }
50
51 pub fn group_default(group: NavGroup) -> Self {
53 match group {
54 NavGroup::Search => ViewId::Search,
55 NavGroup::Ai => ViewId::Indexing,
56 NavGroup::Settings => ViewId::Settings,
57 }
58 }
59}
60
61
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
64pub struct IndexHealth {
65 pub indexed: u64,
66 pub stale: u64,
67 pub failed: u64,
68 pub queued: u64,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct SourceCard {
74 pub display_name: String,
75 pub display_path: String,
76 pub indexed: u64,
77 pub stale: u64,
78 pub failed: u64,
79 pub active: bool,
80 pub source_id: String,
81}
82
83#[derive(Debug, Clone, PartialEq)]
86pub struct SearchResultDisplay {
87 pub display_path: String,
88 pub title: Option<String>,
89 pub heading_path: Option<String>,
90 pub snippet: Option<String>,
91 pub keyword_rank: u32,
92 pub badges: Vec<String>,
93}
94
95
96#[derive(Debug, Clone, PartialEq)]
98pub struct WizardFileCheck {
99 pub relative_path: String,
100 pub found: bool,
101 pub size_mb: Option<f64>,
102}
103
104#[derive(Debug, Clone, PartialEq)]
106pub enum WizardState {
107 NotConfigured,
109 FileMissing { previous_dir: String, checks: Vec<WizardFileCheck> },
111 Checked { model_dir: String, checks: Vec<WizardFileCheck>, all_ok: bool },
113 Ready { model_dir: String },
115 Downloading {
117 dest_dir: String,
118 current_file: String,
120 bytes: u64,
121 total: Option<u64>,
122 files_done: u32,
123 files_total: u32,
124 },
125}
126
127#[derive(Debug, Clone)]
129pub struct AppState {
130 pub active_view: ViewId,
131 pub locale: Locale,
132 pub query: String,
133 pub last_query: Option<String>,
134 pub search_mode: SearchMode,
135 pub search_results: Vec<SearchResultDisplay>,
136 pub search_running: bool,
137 pub selected_result: Option<usize>,
138 pub storage_rows: Vec<(String, u64, u64)>,
139 pub health: IndexHealth,
140 pub sources: Vec<SourceCard>,
141 pub capability: SearchCapability,
142 pub storage_total_bytes: u64,
143 pub wizard: Option<WizardState>,
145 pub wizard_path_input: String,
147 pub source_path_input: String,
149 pub show_advanced: bool,
151 pub notice: Option<UserNotice>,
153 pub confirm_reset: bool,
155}
156
157impl Default for AppState {
158 fn default() -> Self {
159 Self {
160 active_view: ViewId::Search,
161 locale: Locale::default(),
162 query: String::new(),
163 last_query: None,
164 search_mode: SearchMode::Auto,
165 search_results: Vec::new(),
166 search_running: false,
167 selected_result: None,
168 storage_rows: Vec::new(),
169 health: IndexHealth::default(),
170 sources: Vec::new(),
171 capability: SearchCapability::KeywordOnly,
172 storage_total_bytes: 0,
173 wizard: None,
174 wizard_path_input: String::new(),
175 source_path_input: String::new(),
176 show_advanced: false,
177 notice: None,
178 confirm_reset: false,
179 }
180 }
181}
182
183#[derive(Debug, Clone)]
185pub enum Message {
186 Switch(ViewId),
187 SwitchGroup(NavGroup),
188 ToggleAdvanced,
189 ShowNotice(UserNotice),
190 ClearNotice,
191 CleanSnippets,
193 CleanSearchCache,
194 AskResetCatalog,
195 ConfirmResetCatalog,
196 CancelResetCatalog,
197 CleanupDone, WizardBack,
200 QueryChanged(String),
201 SubmitSearch,
202 SearchResultsReady(Vec<SearchResultDisplay>),
203 SearchError(String),
204 SelectResult(usize),
205 OpenSourceFile(String),
206 SetSearchMode(SearchMode),
207 PersistLocale(Locale),
208 SetLocale(Locale),
209 StorageDataReady(Vec<(String, u64, u64)>),
210 WizardPathChanged(String),
212 WizardValidate,
213 WizardChecked { model_dir: String, checks: Vec<WizardFileCheck>, all_ok: bool },
214 WizardAccept,
215 WizardSkip,
216 SourcePathChanged(String),
218 RequestAddSource,
219 SourceAdded(SourceCard),
220 SourceRemoved(String), ScanCompleted(IndexHealth),
222 DownloadModel,
224 DownloadStarted { dest_dir: String },
225 DownloadFileProgress {
226 file: String,
227 bytes: u64,
228 total: Option<u64>,
229 files_done: u32,
230 files_total: u32,
231 },
232 DownloadAllComplete { dest_dir: String },
233 DownloadFailed(String),
234 HealthUpdated(IndexHealth),
236 SourcesLoaded(Vec<SourceCard>),
237}
238
239impl AppState {
240 pub fn update(&mut self, message: &Message) {
241 match message {
242 Message::Switch(view) => self.active_view = *view,
243 Message::SwitchGroup(group) => self.active_view = ViewId::group_default(*group),
244 Message::ToggleAdvanced => self.show_advanced = !self.show_advanced,
245 Message::AskResetCatalog => self.confirm_reset = true,
246 Message::CancelResetCatalog => self.confirm_reset = false,
247 Message::ConfirmResetCatalog => {
248 self.confirm_reset = false;
249 self.sources.clear();
251 self.health = crate::state::IndexHealth::default();
252 self.search_results.clear();
253 self.storage_rows.clear();
254 self.storage_total_bytes = 0;
255 }
256 Message::CleanSnippets | Message::CleanSearchCache => {
257 }
259 Message::CleanupDone => {
260 self.notice = Some(UserNotice::PreviewsCleared);
261 }
262 Message::WizardBack => {
263 self.wizard = Some(crate::state::WizardState::NotConfigured);
265 self.wizard_path_input = String::new();
266 }
267 Message::ShowNotice(n) => self.notice = Some(n.clone()),
268 Message::ClearNotice => self.notice = None,
269 Message::QueryChanged(query) => self.query = query.clone(),
270 Message::SubmitSearch => {
271 let trimmed = self.query.trim();
272 if !trimmed.is_empty() {
273 self.last_query = Some(trimmed.to_string());
274 self.search_running = true;
275 self.search_results.clear();
276 self.selected_result = None;
277 }
278 }
279 Message::SearchResultsReady(results) => {
280 self.search_results = results.clone();
281 self.search_running = false;
282 self.selected_result = None;
283 self.notice = None;
284 }
285 Message::SearchError(_) => {
286 self.search_running = false;
287 self.notice = Some(UserNotice::SearchDidNotFinish);
288 }
289 Message::SelectResult(idx) => self.selected_result = Some(*idx),
290 Message::OpenSourceFile(_) => {} Message::SetSearchMode(mode) => self.search_mode = *mode,
292 Message::PersistLocale(locale) | Message::SetLocale(locale) => self.locale = *locale,
293 Message::StorageDataReady(rows) => self.storage_rows = rows.clone(),
294 Message::WizardPathChanged(p) => self.wizard_path_input = p.clone(),
295 Message::WizardValidate => {} Message::WizardChecked { model_dir, checks, all_ok } => {
297 self.wizard = Some(if *all_ok {
298 WizardState::Ready { model_dir: model_dir.clone() }
299 } else {
300 WizardState::Checked {
301 model_dir: model_dir.clone(),
302 checks: checks.clone(),
303 all_ok: false,
304 }
305 });
306 }
307 Message::WizardAccept => {
308 self.capability = SearchCapability::Hybrid;
311 self.wizard = None;
312 self.wizard_path_input = String::new();
313 }
314 Message::WizardSkip => {
315 self.capability = SearchCapability::KeywordOnly;
316 self.wizard = None;
317 self.wizard_path_input = String::new();
318 }
319 Message::DownloadModel => {
320 }
323 Message::DownloadStarted { dest_dir } => {
324 self.wizard = Some(WizardState::Downloading {
325 dest_dir: dest_dir.clone(),
326 current_file: String::new(),
327 bytes: 0,
328 total: None,
329 files_done: 0,
330 files_total: 2,
331 });
332 }
333 Message::DownloadFileProgress { file, bytes, total, files_done, files_total } => {
334 if let Some(WizardState::Downloading { current_file, bytes: b, total: t, files_done: fd, files_total: ft, .. }) =
335 &mut self.wizard
336 {
337 *current_file = file.clone();
338 *b = *bytes;
339 *t = *total;
340 *fd = *files_done;
341 *ft = *files_total;
342 }
343 }
344 Message::DownloadAllComplete { dest_dir } => {
345 self.wizard = Some(WizardState::Ready { model_dir: dest_dir.clone() });
347 }
348 Message::DownloadFailed(_reason) => {
349 self.wizard = Some(WizardState::NotConfigured);
351 }
352 Message::SourcePathChanged(p) => self.source_path_input = p.clone(),
353 Message::RequestAddSource => {} Message::SourceAdded(card) => {
355 self.sources.push(card.clone());
356 self.source_path_input = String::new();
357 self.notice = Some(UserNotice::FolderAdded);
358 }
359 Message::SourceRemoved(id) => self.sources.retain(|s| s.source_id != *id),
360 Message::ScanCompleted(health) | Message::HealthUpdated(health) => {
361 self.health = *health;
362 }
364 Message::SourcesLoaded(cards) => self.sources = cards.clone(),
365 }
366 }
367}