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