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}
115
116#[derive(Debug, Clone)]
118pub struct AppState {
119 pub active_view: ViewId,
120 pub locale: Locale,
121 pub query: String,
122 pub last_query: Option<String>,
123 pub search_mode: SearchMode,
124 pub search_results: Vec<SearchResultDisplay>,
125 pub search_running: bool,
126 pub selected_result: Option<usize>,
127 pub storage_rows: Vec<(String, u64, u64)>,
128 pub health: IndexHealth,
129 pub sources: Vec<SourceCard>,
130 pub capability: SearchCapability,
131 pub storage_total_bytes: u64,
132 pub wizard: Option<WizardState>,
134 pub wizard_path_input: String,
136 pub source_path_input: String,
138}
139
140impl Default for AppState {
141 fn default() -> Self {
142 Self {
143 active_view: ViewId::Search,
144 locale: Locale::default(),
145 query: String::new(),
146 last_query: None,
147 search_mode: SearchMode::Auto,
148 search_results: Vec::new(),
149 search_running: false,
150 selected_result: None,
151 storage_rows: Vec::new(),
152 health: IndexHealth::default(),
153 sources: Vec::new(),
154 capability: SearchCapability::KeywordOnly,
155 storage_total_bytes: 0,
156 wizard: None,
157 wizard_path_input: String::new(),
158 source_path_input: String::new(),
159 }
160 }
161}
162
163#[derive(Debug, Clone)]
165pub enum Message {
166 Switch(ViewId),
167 SwitchGroup(NavGroup),
168 QueryChanged(String),
169 SubmitSearch,
170 SearchResultsReady(Vec<SearchResultDisplay>),
171 SearchError(String),
172 SelectResult(usize),
173 OpenSourceFile(String),
174 SetSearchMode(SearchMode),
175 PersistLocale(Locale),
176 SetLocale(Locale),
177 StorageDataReady(Vec<(String, u64, u64)>),
178 WizardPathChanged(String),
180 WizardValidate,
181 WizardChecked { model_dir: String, checks: Vec<WizardFileCheck>, all_ok: bool },
182 WizardAccept,
183 WizardSkip,
184 SourcePathChanged(String),
186 RequestAddSource,
187 SourceAdded(SourceCard),
188 SourceRemoved(String), ScanCompleted(IndexHealth),
190 HealthUpdated(IndexHealth),
192 SourcesLoaded(Vec<SourceCard>),
193}
194
195impl AppState {
196 pub fn update(&mut self, message: &Message) {
197 match message {
198 Message::Switch(view) => self.active_view = *view,
199 Message::SwitchGroup(group) => self.active_view = ViewId::group_default(*group),
200 Message::QueryChanged(query) => self.query = query.clone(),
201 Message::SubmitSearch => {
202 let trimmed = self.query.trim();
203 if !trimmed.is_empty() {
204 self.last_query = Some(trimmed.to_string());
205 self.search_running = true;
206 self.search_results.clear();
207 self.selected_result = None;
208 }
209 }
210 Message::SearchResultsReady(results) => {
211 self.search_results = results.clone();
212 self.search_running = false;
213 self.selected_result = None;
214 }
215 Message::SearchError(_) => {
216 self.search_running = false;
217 }
218 Message::SelectResult(idx) => self.selected_result = Some(*idx),
219 Message::OpenSourceFile(_) => {} Message::SetSearchMode(mode) => self.search_mode = *mode,
221 Message::PersistLocale(locale) | Message::SetLocale(locale) => self.locale = *locale,
222 Message::StorageDataReady(rows) => self.storage_rows = rows.clone(),
223 Message::WizardPathChanged(p) => self.wizard_path_input = p.clone(),
224 Message::WizardValidate => {} Message::WizardChecked { model_dir, checks, all_ok } => {
226 self.wizard = Some(if *all_ok {
227 WizardState::Ready { model_dir: model_dir.clone() }
228 } else {
229 WizardState::Checked {
230 model_dir: model_dir.clone(),
231 checks: checks.clone(),
232 all_ok: false,
233 }
234 });
235 }
236 Message::WizardAccept => {
237 self.capability = SearchCapability::Hybrid;
240 self.wizard = None;
241 self.wizard_path_input = String::new();
242 }
243 Message::WizardSkip => {
244 self.capability = SearchCapability::KeywordOnly;
245 self.wizard = None;
246 self.wizard_path_input = String::new();
247 }
248 Message::SourcePathChanged(p) => self.source_path_input = p.clone(),
249 Message::RequestAddSource => {} Message::SourceAdded(card) => {
251 self.sources.push(card.clone());
252 self.source_path_input = String::new();
253 }
254 Message::SourceRemoved(id) => self.sources.retain(|s| s.source_id != *id),
255 Message::ScanCompleted(health) | Message::HealthUpdated(health) => {
256 self.health = *health;
257 }
259 Message::SourcesLoaded(cards) => self.sources = cards.clone(),
260 }
261 }
262}