1use crate::i18n::Locale;
9use orbok_models::SearchCapability;
10use orbok_search::SearchMode;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ViewId {
15 Search,
16 Sources,
17 Indexing,
18 Storage,
19 Models,
20 Settings,
21}
22
23impl ViewId {
24 pub const ALL: &'static [ViewId] = &[
25 ViewId::Search,
26 ViewId::Sources,
27 ViewId::Indexing,
28 ViewId::Storage,
29 ViewId::Models,
30 ViewId::Settings,
31 ];
32}
33
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
36pub struct IndexHealth {
37 pub indexed: u64,
38 pub stale: u64,
39 pub failed: u64,
40 pub queued: u64,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct SourceCard {
46 pub display_name: String,
47 pub display_path: String,
48 pub indexed: u64,
49 pub stale: u64,
50 pub failed: u64,
51 pub active: bool,
52 pub source_id: String,
53}
54
55#[derive(Debug, Clone, PartialEq)]
58pub struct SearchResultDisplay {
59 pub display_path: String,
60 pub title: Option<String>,
61 pub heading_path: Option<String>,
62 pub snippet: Option<String>,
63 pub keyword_rank: u32,
64 pub badges: Vec<String>,
65}
66
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct WizardFileCheck {
71 pub relative_path: String,
72 pub found: bool,
73 pub size_mb: Option<f64>,
74}
75
76#[derive(Debug, Clone, PartialEq)]
78pub enum WizardState {
79 NotConfigured,
81 FileMissing { previous_dir: String, checks: Vec<WizardFileCheck> },
83 Checked { model_dir: String, checks: Vec<WizardFileCheck>, all_ok: bool },
85 Ready { model_dir: String },
87}
88
89#[derive(Debug, Clone)]
91pub struct AppState {
92 pub active_view: ViewId,
93 pub locale: Locale,
94 pub query: String,
95 pub last_query: Option<String>,
96 pub search_mode: SearchMode,
97 pub search_results: Vec<SearchResultDisplay>,
98 pub search_running: bool,
99 pub selected_result: Option<usize>,
100 pub storage_rows: Vec<(String, u64, u64)>,
101 pub health: IndexHealth,
102 pub sources: Vec<SourceCard>,
103 pub capability: SearchCapability,
104 pub storage_total_bytes: u64,
105 pub wizard: Option<WizardState>,
107 pub wizard_path_input: String,
109 pub source_path_input: String,
111}
112
113impl Default for AppState {
114 fn default() -> Self {
115 Self {
116 active_view: ViewId::Search,
117 locale: Locale::default(),
118 query: String::new(),
119 last_query: None,
120 search_mode: SearchMode::Auto,
121 search_results: Vec::new(),
122 search_running: false,
123 selected_result: None,
124 storage_rows: Vec::new(),
125 health: IndexHealth::default(),
126 sources: Vec::new(),
127 capability: SearchCapability::KeywordOnly,
128 storage_total_bytes: 0,
129 wizard: None,
130 wizard_path_input: String::new(),
131 source_path_input: String::new(),
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
138pub enum Message {
139 Switch(ViewId),
140 QueryChanged(String),
141 SubmitSearch,
142 SearchResultsReady(Vec<SearchResultDisplay>),
143 SearchError(String),
144 SelectResult(usize),
145 OpenSourceFile(String),
146 SetSearchMode(SearchMode),
147 PersistLocale(Locale),
148 SetLocale(Locale),
149 StorageDataReady(Vec<(String, u64, u64)>),
150 WizardPathChanged(String),
152 WizardValidate,
153 WizardChecked { model_dir: String, checks: Vec<WizardFileCheck>, all_ok: bool },
154 WizardAccept,
155 WizardSkip,
156 SourcePathChanged(String),
158 RequestAddSource,
159 SourceAdded(SourceCard),
160 SourceRemoved(String), ScanCompleted(IndexHealth),
162 HealthUpdated(IndexHealth),
164 SourcesLoaded(Vec<SourceCard>),
165}
166
167impl AppState {
168 pub fn update(&mut self, message: &Message) {
169 match message {
170 Message::Switch(view) => self.active_view = *view,
171 Message::QueryChanged(query) => self.query = query.clone(),
172 Message::SubmitSearch => {
173 let trimmed = self.query.trim();
174 if !trimmed.is_empty() {
175 self.last_query = Some(trimmed.to_string());
176 self.search_running = true;
177 self.search_results.clear();
178 self.selected_result = None;
179 }
180 }
181 Message::SearchResultsReady(results) => {
182 self.search_results = results.clone();
183 self.search_running = false;
184 self.selected_result = None;
185 }
186 Message::SearchError(_) => {
187 self.search_running = false;
188 }
189 Message::SelectResult(idx) => self.selected_result = Some(*idx),
190 Message::OpenSourceFile(_) => {} Message::SetSearchMode(mode) => self.search_mode = *mode,
192 Message::PersistLocale(locale) | Message::SetLocale(locale) => self.locale = *locale,
193 Message::StorageDataReady(rows) => self.storage_rows = rows.clone(),
194 Message::WizardPathChanged(p) => self.wizard_path_input = p.clone(),
195 Message::WizardValidate => {} Message::WizardChecked { model_dir, checks, all_ok } => {
197 self.wizard = Some(if *all_ok {
198 WizardState::Ready { model_dir: model_dir.clone() }
199 } else {
200 WizardState::Checked {
201 model_dir: model_dir.clone(),
202 checks: checks.clone(),
203 all_ok: false,
204 }
205 });
206 }
207 Message::WizardAccept => {
208 self.capability = SearchCapability::Hybrid;
211 self.wizard = None;
212 self.wizard_path_input = String::new();
213 }
214 Message::WizardSkip => {
215 self.capability = SearchCapability::KeywordOnly;
216 self.wizard = None;
217 self.wizard_path_input = String::new();
218 }
219 Message::SourcePathChanged(p) => self.source_path_input = p.clone(),
220 Message::RequestAddSource => {} Message::SourceAdded(card) => {
222 self.sources.push(card.clone());
223 self.source_path_input = String::new();
224 }
225 Message::SourceRemoved(id) => self.sources.retain(|s| s.source_id != *id),
226 Message::ScanCompleted(health) | Message::HealthUpdated(health) => {
227 self.health = *health;
228 }
230 Message::SourcesLoaded(cards) => self.sources = cards.clone(),
231 }
232 }
233}