use crate::i18n::Locale;
use orbok_models::SearchCapability;
use orbok_search::SearchMode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavGroup {
Search,
Ai,
Settings,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViewId {
Search,
Sources,
Indexing,
Storage,
Models,
Settings,
}
impl ViewId {
pub const ALL: &'static [ViewId] = &[
ViewId::Search,
ViewId::Sources,
ViewId::Indexing,
ViewId::Storage,
ViewId::Models,
ViewId::Settings,
];
pub fn group(self) -> NavGroup {
match self {
ViewId::Search | ViewId::Sources => NavGroup::Search,
ViewId::Indexing | ViewId::Storage | ViewId::Models => NavGroup::Ai,
ViewId::Settings => NavGroup::Settings,
}
}
pub fn group_default(group: NavGroup) -> Self {
match group {
NavGroup::Search => ViewId::Search,
NavGroup::Ai => ViewId::Indexing,
NavGroup::Settings => ViewId::Settings,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct IndexHealth {
pub indexed: u64,
pub stale: u64,
pub failed: u64,
pub queued: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceCard {
pub display_name: String,
pub display_path: String,
pub indexed: u64,
pub stale: u64,
pub failed: u64,
pub active: bool,
pub source_id: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SearchResultDisplay {
pub display_path: String,
pub title: Option<String>,
pub heading_path: Option<String>,
pub snippet: Option<String>,
pub keyword_rank: u32,
pub badges: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct WizardFileCheck {
pub relative_path: String,
pub found: bool,
pub size_mb: Option<f64>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WizardState {
NotConfigured,
FileMissing { previous_dir: String, checks: Vec<WizardFileCheck> },
Checked { model_dir: String, checks: Vec<WizardFileCheck>, all_ok: bool },
Ready { model_dir: String },
Downloading {
dest_dir: String,
current_file: String,
bytes: u64,
total: Option<u64>,
files_done: u32,
files_total: u32,
},
}
#[derive(Debug, Clone)]
pub struct AppState {
pub active_view: ViewId,
pub locale: Locale,
pub query: String,
pub last_query: Option<String>,
pub search_mode: SearchMode,
pub search_results: Vec<SearchResultDisplay>,
pub search_running: bool,
pub selected_result: Option<usize>,
pub storage_rows: Vec<(String, u64, u64)>,
pub health: IndexHealth,
pub sources: Vec<SourceCard>,
pub capability: SearchCapability,
pub storage_total_bytes: u64,
pub wizard: Option<WizardState>,
pub wizard_path_input: String,
pub source_path_input: String,
pub show_advanced: bool,
}
impl Default for AppState {
fn default() -> Self {
Self {
active_view: ViewId::Search,
locale: Locale::default(),
query: String::new(),
last_query: None,
search_mode: SearchMode::Auto,
search_results: Vec::new(),
search_running: false,
selected_result: None,
storage_rows: Vec::new(),
health: IndexHealth::default(),
sources: Vec::new(),
capability: SearchCapability::KeywordOnly,
storage_total_bytes: 0,
wizard: None,
wizard_path_input: String::new(),
source_path_input: String::new(),
show_advanced: false,
}
}
}
#[derive(Debug, Clone)]
pub enum Message {
Switch(ViewId),
SwitchGroup(NavGroup),
ToggleAdvanced,
QueryChanged(String),
SubmitSearch,
SearchResultsReady(Vec<SearchResultDisplay>),
SearchError(String),
SelectResult(usize),
OpenSourceFile(String),
SetSearchMode(SearchMode),
PersistLocale(Locale),
SetLocale(Locale),
StorageDataReady(Vec<(String, u64, u64)>),
WizardPathChanged(String),
WizardValidate,
WizardChecked { model_dir: String, checks: Vec<WizardFileCheck>, all_ok: bool },
WizardAccept,
WizardSkip,
SourcePathChanged(String),
RequestAddSource,
SourceAdded(SourceCard),
SourceRemoved(String), ScanCompleted(IndexHealth),
DownloadModel,
DownloadStarted { dest_dir: String },
DownloadFileProgress {
file: String,
bytes: u64,
total: Option<u64>,
files_done: u32,
files_total: u32,
},
DownloadAllComplete { dest_dir: String },
DownloadFailed(String),
HealthUpdated(IndexHealth),
SourcesLoaded(Vec<SourceCard>),
}
impl AppState {
pub fn update(&mut self, message: &Message) {
match message {
Message::Switch(view) => self.active_view = *view,
Message::SwitchGroup(group) => self.active_view = ViewId::group_default(*group),
Message::ToggleAdvanced => self.show_advanced = !self.show_advanced,
Message::QueryChanged(query) => self.query = query.clone(),
Message::SubmitSearch => {
let trimmed = self.query.trim();
if !trimmed.is_empty() {
self.last_query = Some(trimmed.to_string());
self.search_running = true;
self.search_results.clear();
self.selected_result = None;
}
}
Message::SearchResultsReady(results) => {
self.search_results = results.clone();
self.search_running = false;
self.selected_result = None;
}
Message::SearchError(_) => {
self.search_running = false;
}
Message::SelectResult(idx) => self.selected_result = Some(*idx),
Message::OpenSourceFile(_) => {} Message::SetSearchMode(mode) => self.search_mode = *mode,
Message::PersistLocale(locale) | Message::SetLocale(locale) => self.locale = *locale,
Message::StorageDataReady(rows) => self.storage_rows = rows.clone(),
Message::WizardPathChanged(p) => self.wizard_path_input = p.clone(),
Message::WizardValidate => {} Message::WizardChecked { model_dir, checks, all_ok } => {
self.wizard = Some(if *all_ok {
WizardState::Ready { model_dir: model_dir.clone() }
} else {
WizardState::Checked {
model_dir: model_dir.clone(),
checks: checks.clone(),
all_ok: false,
}
});
}
Message::WizardAccept => {
self.capability = SearchCapability::Hybrid;
self.wizard = None;
self.wizard_path_input = String::new();
}
Message::WizardSkip => {
self.capability = SearchCapability::KeywordOnly;
self.wizard = None;
self.wizard_path_input = String::new();
}
Message::DownloadModel => {
}
Message::DownloadStarted { dest_dir } => {
self.wizard = Some(WizardState::Downloading {
dest_dir: dest_dir.clone(),
current_file: String::new(),
bytes: 0,
total: None,
files_done: 0,
files_total: 2,
});
}
Message::DownloadFileProgress { file, bytes, total, files_done, files_total } => {
if let Some(WizardState::Downloading { current_file, bytes: b, total: t, files_done: fd, files_total: ft, .. }) =
&mut self.wizard
{
*current_file = file.clone();
*b = *bytes;
*t = *total;
*fd = *files_done;
*ft = *files_total;
}
}
Message::DownloadAllComplete { dest_dir } => {
self.wizard = Some(WizardState::Ready { model_dir: dest_dir.clone() });
}
Message::DownloadFailed(_reason) => {
self.wizard = Some(WizardState::NotConfigured);
}
Message::SourcePathChanged(p) => self.source_path_input = p.clone(),
Message::RequestAddSource => {} Message::SourceAdded(card) => {
self.sources.push(card.clone());
self.source_path_input = String::new();
}
Message::SourceRemoved(id) => self.sources.retain(|s| s.source_id != *id),
Message::ScanCompleted(health) | Message::HealthUpdated(health) => {
self.health = *health;
}
Message::SourcesLoaded(cards) => self.sources = cards.clone(),
}
}
}