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}
154
155impl Default for AppState {
156 fn default() -> Self {
157 Self {
158 active_view: ViewId::Search,
159 locale: Locale::default(),
160 query: String::new(),
161 last_query: None,
162 search_mode: SearchMode::Auto,
163 search_results: Vec::new(),
164 search_running: false,
165 selected_result: None,
166 storage_rows: Vec::new(),
167 health: IndexHealth::default(),
168 sources: Vec::new(),
169 capability: SearchCapability::KeywordOnly,
170 storage_total_bytes: 0,
171 wizard: None,
172 wizard_path_input: String::new(),
173 source_path_input: String::new(),
174 show_advanced: false,
175 notice: None,
176 }
177 }
178}
179
180#[derive(Debug, Clone)]
182pub enum Message {
183 Switch(ViewId),
184 SwitchGroup(NavGroup),
185 ToggleAdvanced,
186 ShowNotice(UserNotice),
187 ClearNotice,
188 QueryChanged(String),
189 SubmitSearch,
190 SearchResultsReady(Vec<SearchResultDisplay>),
191 SearchError(String),
192 SelectResult(usize),
193 OpenSourceFile(String),
194 SetSearchMode(SearchMode),
195 PersistLocale(Locale),
196 SetLocale(Locale),
197 StorageDataReady(Vec<(String, u64, u64)>),
198 WizardPathChanged(String),
200 WizardValidate,
201 WizardChecked { model_dir: String, checks: Vec<WizardFileCheck>, all_ok: bool },
202 WizardAccept,
203 WizardSkip,
204 SourcePathChanged(String),
206 RequestAddSource,
207 SourceAdded(SourceCard),
208 SourceRemoved(String), ScanCompleted(IndexHealth),
210 DownloadModel,
212 DownloadStarted { dest_dir: String },
213 DownloadFileProgress {
214 file: String,
215 bytes: u64,
216 total: Option<u64>,
217 files_done: u32,
218 files_total: u32,
219 },
220 DownloadAllComplete { dest_dir: String },
221 DownloadFailed(String),
222 HealthUpdated(IndexHealth),
224 SourcesLoaded(Vec<SourceCard>),
225}
226
227impl AppState {
228 pub fn update(&mut self, message: &Message) {
229 match message {
230 Message::Switch(view) => self.active_view = *view,
231 Message::SwitchGroup(group) => self.active_view = ViewId::group_default(*group),
232 Message::ToggleAdvanced => self.show_advanced = !self.show_advanced,
233 Message::ShowNotice(n) => self.notice = Some(n.clone()),
234 Message::ClearNotice => self.notice = None,
235 Message::QueryChanged(query) => self.query = query.clone(),
236 Message::SubmitSearch => {
237 let trimmed = self.query.trim();
238 if !trimmed.is_empty() {
239 self.last_query = Some(trimmed.to_string());
240 self.search_running = true;
241 self.search_results.clear();
242 self.selected_result = None;
243 }
244 }
245 Message::SearchResultsReady(results) => {
246 self.search_results = results.clone();
247 self.search_running = false;
248 self.selected_result = None;
249 self.notice = None;
250 }
251 Message::SearchError(_) => {
252 self.search_running = false;
253 self.notice = Some(UserNotice::SearchDidNotFinish);
254 }
255 Message::SelectResult(idx) => self.selected_result = Some(*idx),
256 Message::OpenSourceFile(_) => {} Message::SetSearchMode(mode) => self.search_mode = *mode,
258 Message::PersistLocale(locale) | Message::SetLocale(locale) => self.locale = *locale,
259 Message::StorageDataReady(rows) => self.storage_rows = rows.clone(),
260 Message::WizardPathChanged(p) => self.wizard_path_input = p.clone(),
261 Message::WizardValidate => {} Message::WizardChecked { model_dir, checks, all_ok } => {
263 self.wizard = Some(if *all_ok {
264 WizardState::Ready { model_dir: model_dir.clone() }
265 } else {
266 WizardState::Checked {
267 model_dir: model_dir.clone(),
268 checks: checks.clone(),
269 all_ok: false,
270 }
271 });
272 }
273 Message::WizardAccept => {
274 self.capability = SearchCapability::Hybrid;
277 self.wizard = None;
278 self.wizard_path_input = String::new();
279 }
280 Message::WizardSkip => {
281 self.capability = SearchCapability::KeywordOnly;
282 self.wizard = None;
283 self.wizard_path_input = String::new();
284 }
285 Message::DownloadModel => {
286 }
289 Message::DownloadStarted { dest_dir } => {
290 self.wizard = Some(WizardState::Downloading {
291 dest_dir: dest_dir.clone(),
292 current_file: String::new(),
293 bytes: 0,
294 total: None,
295 files_done: 0,
296 files_total: 2,
297 });
298 }
299 Message::DownloadFileProgress { file, bytes, total, files_done, files_total } => {
300 if let Some(WizardState::Downloading { current_file, bytes: b, total: t, files_done: fd, files_total: ft, .. }) =
301 &mut self.wizard
302 {
303 *current_file = file.clone();
304 *b = *bytes;
305 *t = *total;
306 *fd = *files_done;
307 *ft = *files_total;
308 }
309 }
310 Message::DownloadAllComplete { dest_dir } => {
311 self.wizard = Some(WizardState::Ready { model_dir: dest_dir.clone() });
313 }
314 Message::DownloadFailed(_reason) => {
315 self.wizard = Some(WizardState::NotConfigured);
317 }
318 Message::SourcePathChanged(p) => self.source_path_input = p.clone(),
319 Message::RequestAddSource => {} Message::SourceAdded(card) => {
321 self.sources.push(card.clone());
322 self.source_path_input = String::new();
323 self.notice = Some(UserNotice::FolderAdded);
324 }
325 Message::SourceRemoved(id) => self.sources.retain(|s| s.source_id != *id),
326 Message::ScanCompleted(health) | Message::HealthUpdated(health) => {
327 self.health = *health;
328 }
330 Message::SourcesLoaded(cards) => self.sources = cards.clone(),
331 }
332 }
333}