1use crate::i18n::Locale;
9use orbok_models::SearchCapability;
10use orbok_search::SearchMode;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum NavGroup {
15 Search,
16 Ai,
17 Settings,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ViewId {
23 Search,
24 Sources,
25 Indexing,
26 Storage,
27 Models,
28 Settings,
29}
30
31impl ViewId {
32 pub const ALL: &'static [ViewId] = &[
33 ViewId::Search,
34 ViewId::Sources,
35 ViewId::Indexing,
36 ViewId::Storage,
37 ViewId::Models,
38 ViewId::Settings,
39 ];
40
41 pub fn group(self) -> NavGroup {
43 match self {
44 ViewId::Search | ViewId::Sources => NavGroup::Search,
45 ViewId::Indexing | ViewId::Storage | ViewId::Models => NavGroup::Ai,
46 ViewId::Settings => NavGroup::Settings,
47 }
48 }
49
50 pub fn group_default(group: NavGroup) -> Self {
52 match group {
53 NavGroup::Search => ViewId::Search,
54 NavGroup::Ai => ViewId::Indexing,
55 NavGroup::Settings => ViewId::Settings,
56 }
57 }
58}
59
60
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
63pub struct IndexHealth {
64 pub indexed: u64,
65 pub stale: u64,
66 pub failed: u64,
67 pub queued: u64,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct SourceCard {
73 pub display_name: String,
74 pub display_path: String,
75 pub indexed: u64,
76 pub stale: u64,
77 pub failed: u64,
78 pub active: bool,
79 pub source_id: String,
80}
81
82#[derive(Debug, Clone, PartialEq)]
85pub struct SearchResultDisplay {
86 pub display_path: String,
87 pub title: Option<String>,
88 pub heading_path: Option<String>,
89 pub snippet: Option<String>,
90 pub keyword_rank: u32,
91 pub badges: Vec<String>,
92}
93
94
95#[derive(Debug, Clone, PartialEq)]
97pub struct WizardFileCheck {
98 pub relative_path: String,
99 pub found: bool,
100 pub size_mb: Option<f64>,
101}
102
103#[derive(Debug, Clone, PartialEq)]
105pub enum WizardState {
106 NotConfigured,
108 FileMissing { previous_dir: String, checks: Vec<WizardFileCheck> },
110 Checked { model_dir: String, checks: Vec<WizardFileCheck>, all_ok: bool },
112 Ready { model_dir: String },
114 Downloading {
116 dest_dir: String,
117 current_file: String,
119 bytes: u64,
120 total: Option<u64>,
121 files_done: u32,
122 files_total: u32,
123 },
124}
125
126#[derive(Debug, Clone)]
128pub struct AppState {
129 pub active_view: ViewId,
130 pub locale: Locale,
131 pub query: String,
132 pub last_query: Option<String>,
133 pub search_mode: SearchMode,
134 pub search_results: Vec<SearchResultDisplay>,
135 pub search_running: bool,
136 pub selected_result: Option<usize>,
137 pub storage_rows: Vec<(String, u64, u64)>,
138 pub health: IndexHealth,
139 pub sources: Vec<SourceCard>,
140 pub capability: SearchCapability,
141 pub storage_total_bytes: u64,
142 pub wizard: Option<WizardState>,
144 pub wizard_path_input: String,
146 pub source_path_input: String,
148 pub show_advanced: bool,
150}
151
152impl Default for AppState {
153 fn default() -> Self {
154 Self {
155 active_view: ViewId::Search,
156 locale: Locale::default(),
157 query: String::new(),
158 last_query: None,
159 search_mode: SearchMode::Auto,
160 search_results: Vec::new(),
161 search_running: false,
162 selected_result: None,
163 storage_rows: Vec::new(),
164 health: IndexHealth::default(),
165 sources: Vec::new(),
166 capability: SearchCapability::KeywordOnly,
167 storage_total_bytes: 0,
168 wizard: None,
169 wizard_path_input: String::new(),
170 source_path_input: String::new(),
171 show_advanced: false,
172 }
173 }
174}
175
176#[derive(Debug, Clone)]
178pub enum Message {
179 Switch(ViewId),
180 SwitchGroup(NavGroup),
181 ToggleAdvanced,
182 QueryChanged(String),
183 SubmitSearch,
184 SearchResultsReady(Vec<SearchResultDisplay>),
185 SearchError(String),
186 SelectResult(usize),
187 OpenSourceFile(String),
188 SetSearchMode(SearchMode),
189 PersistLocale(Locale),
190 SetLocale(Locale),
191 StorageDataReady(Vec<(String, u64, u64)>),
192 WizardPathChanged(String),
194 WizardValidate,
195 WizardChecked { model_dir: String, checks: Vec<WizardFileCheck>, all_ok: bool },
196 WizardAccept,
197 WizardSkip,
198 SourcePathChanged(String),
200 RequestAddSource,
201 SourceAdded(SourceCard),
202 SourceRemoved(String), ScanCompleted(IndexHealth),
204 DownloadModel,
206 DownloadStarted { dest_dir: String },
207 DownloadFileProgress {
208 file: String,
209 bytes: u64,
210 total: Option<u64>,
211 files_done: u32,
212 files_total: u32,
213 },
214 DownloadAllComplete { dest_dir: String },
215 DownloadFailed(String),
216 HealthUpdated(IndexHealth),
218 SourcesLoaded(Vec<SourceCard>),
219}
220
221impl AppState {
222 pub fn update(&mut self, message: &Message) {
223 match message {
224 Message::Switch(view) => self.active_view = *view,
225 Message::SwitchGroup(group) => self.active_view = ViewId::group_default(*group),
226 Message::ToggleAdvanced => self.show_advanced = !self.show_advanced,
227 Message::QueryChanged(query) => self.query = query.clone(),
228 Message::SubmitSearch => {
229 let trimmed = self.query.trim();
230 if !trimmed.is_empty() {
231 self.last_query = Some(trimmed.to_string());
232 self.search_running = true;
233 self.search_results.clear();
234 self.selected_result = None;
235 }
236 }
237 Message::SearchResultsReady(results) => {
238 self.search_results = results.clone();
239 self.search_running = false;
240 self.selected_result = None;
241 }
242 Message::SearchError(_) => {
243 self.search_running = false;
244 }
245 Message::SelectResult(idx) => self.selected_result = Some(*idx),
246 Message::OpenSourceFile(_) => {} Message::SetSearchMode(mode) => self.search_mode = *mode,
248 Message::PersistLocale(locale) | Message::SetLocale(locale) => self.locale = *locale,
249 Message::StorageDataReady(rows) => self.storage_rows = rows.clone(),
250 Message::WizardPathChanged(p) => self.wizard_path_input = p.clone(),
251 Message::WizardValidate => {} Message::WizardChecked { model_dir, checks, all_ok } => {
253 self.wizard = Some(if *all_ok {
254 WizardState::Ready { model_dir: model_dir.clone() }
255 } else {
256 WizardState::Checked {
257 model_dir: model_dir.clone(),
258 checks: checks.clone(),
259 all_ok: false,
260 }
261 });
262 }
263 Message::WizardAccept => {
264 self.capability = SearchCapability::Hybrid;
267 self.wizard = None;
268 self.wizard_path_input = String::new();
269 }
270 Message::WizardSkip => {
271 self.capability = SearchCapability::KeywordOnly;
272 self.wizard = None;
273 self.wizard_path_input = String::new();
274 }
275 Message::DownloadModel => {
276 }
279 Message::DownloadStarted { dest_dir } => {
280 self.wizard = Some(WizardState::Downloading {
281 dest_dir: dest_dir.clone(),
282 current_file: String::new(),
283 bytes: 0,
284 total: None,
285 files_done: 0,
286 files_total: 2,
287 });
288 }
289 Message::DownloadFileProgress { file, bytes, total, files_done, files_total } => {
290 if let Some(WizardState::Downloading { current_file, bytes: b, total: t, files_done: fd, files_total: ft, .. }) =
291 &mut self.wizard
292 {
293 *current_file = file.clone();
294 *b = *bytes;
295 *t = *total;
296 *fd = *files_done;
297 *ft = *files_total;
298 }
299 }
300 Message::DownloadAllComplete { dest_dir } => {
301 self.wizard = Some(WizardState::Ready { model_dir: dest_dir.clone() });
303 }
304 Message::DownloadFailed(_reason) => {
305 self.wizard = Some(WizardState::NotConfigured);
307 }
308 Message::SourcePathChanged(p) => self.source_path_input = p.clone(),
309 Message::RequestAddSource => {} Message::SourceAdded(card) => {
311 self.sources.push(card.clone());
312 self.source_path_input = String::new();
313 }
314 Message::SourceRemoved(id) => self.sources.retain(|s| s.source_id != *id),
315 Message::ScanCompleted(health) | Message::HealthUpdated(health) => {
316 self.health = *health;
317 }
319 Message::SourcesLoaded(cards) => self.sources = cards.clone(),
320 }
321 }
322}