diff --git a/src/gui.rs b/src/gui.rs index 943a947..15e8cb2 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -10,6 +10,8 @@ fn pretty_json(text: &str) -> String { } } +// ── Themes ── + #[derive(Clone, Copy, PartialEq, Eq)] enum GuiTheme { Ocean, @@ -96,10 +98,32 @@ impl GuiTheme { } } +// ── API helper ── + +fn api_get(server_url: &str, path: &str) -> Result { + let url = format!("{}{}", server_url, path); + let client = reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(true) + .build() + .map_err(|e| e.to_string())?; + let resp = client.get(&url).send().map_err(|e| e.to_string())?; + resp.text().map_err(|e| e.to_string()) +} + +fn api_post(server_url: &str, path: &str, body: Value) -> Result { + let url = format!("{}{}", server_url, path); + let client = reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(true) + .build() + .map_err(|e| e.to_string())?; + let resp = client.post(&url).json(&body).send().map_err(|e| e.to_string())?; + resp.text().map_err(|e| e.to_string()) +} + pub fn run_gui(server_url: &str) { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() - .with_inner_size([900.0, 600.0]) + .with_inner_size([1200.0, 750.0]) .with_title("AiRust — Cyber Operator"), ..Default::default() }; @@ -115,41 +139,78 @@ pub fn run_gui(server_url: &str) { #[derive(Clone, Copy, PartialEq, Eq)] enum Tab { Chat, - Exec, - ScriptGen, + Execute, + Scripts, + SelfLearn, + ScriptStudio, + PdfImport, + Ocr, + Books, + Datasets, Knowledge, - Import, - Dataset, - Target, } impl Tab { fn all() -> &'static [Tab] { &[ - Tab::Chat, - Tab::Exec, - Tab::ScriptGen, - Tab::Knowledge, - Tab::Import, - Tab::Dataset, - Tab::Target, + Tab::Chat, Tab::Execute, Tab::Scripts, Tab::SelfLearn, + Tab::ScriptStudio, Tab::PdfImport, Tab::Ocr, Tab::Books, + Tab::Datasets, Tab::Knowledge, ] } fn label(&self) -> &'static str { match self { Tab::Chat => "Chat", - Tab::Exec => "Execute", - Tab::ScriptGen => "Script Gen", + Tab::Execute => "Execute", + Tab::Scripts => "Scripts", + Tab::SelfLearn => "Self-Learn", + Tab::ScriptStudio => "Script Studio", + Tab::PdfImport => "PDF Import", + Tab::Ocr => "OCR", + Tab::Books => "Books", + Tab::Datasets => "Datasets", Tab::Knowledge => "Knowledge", - Tab::Import => "Import", - Tab::Dataset => "Dataset", - Tab::Target => "Target", } } } +#[derive(Clone)] +struct ChatSession { + id: String, + name: String, + messages: Vec, +} + +#[derive(Clone)] +struct ChatMsg { + role: String, + content: String, +} + +struct ExecResult { + command: String, + success: bool, + stdout: String, + stderr: String, + exit_code: Option, +} + enum GuiMsg { Set(String), + ExecResult(ExecResult), + ScriptResult(ExecResult), + StudioResult(String, String), + StudioExecResult(ExecResult), + StatsResult(String), + SearchResult(String), + LearnResult(String), + AutoLearnResult(String), + DatasetResult(String), + PdfResult(String), + OcrResult(String), + BookResult(String), + KnowledgeAddResult(String), + TargetResult(String), } type Msg = (String, GuiMsg); @@ -159,25 +220,66 @@ struct AirustGui { mode: AppMode, theme: GuiTheme, active_tab: Tab, + + // Chat chat_input: String, - chat_log: String, + sessions: Vec, + active_session_id: String, + sessions_loaded: bool, + + // Execute + exec_cmd: String, + exec_results: Vec, + + // Scripts + script_code: String, + script_timeout: String, + script_lang: String, + script_results: Vec, + + // Self-Learn + learn_topic: String, + learn_results: String, + learn_progress: String, + + // Script Studio + studio_desc: String, + studio_output: String, + studio_code: String, + studio_lang: String, + + // PDF Import + pdf_status: String, + + // OCR + ocr_status: String, + ocr_text: String, + + // Books + book_status: String, + + // Datasets + ds_desc: String, + ds_count: String, + ds_add_knowledge: bool, + ds_out: String, + + // Knowledge kn_search_q: String, kn_results: String, kn_add_src: String, kn_add_ins: String, kn_add_out: String, kn_status: String, - exec_cmd: String, - exec_args: String, - exec_out: String, - sgen_desc: String, - sgen_out: String, - imp_status: String, - ds_desc: String, - ds_count: String, - ds_out: String, + + // Target tgt_val: String, tgt_status: String, + + // Sidebar + sidebar_total: String, + sidebar_sources: Vec<(String, u64)>, + tx: mpsc::Sender, rx: mpsc::Receiver, } @@ -190,109 +292,266 @@ enum AppMode { impl AirustGui { fn new(server_url: &str) -> Self { let (tx, rx) = mpsc::channel(); + let session_id = "sess_1".to_string(); Self { server_url: server_url.to_string(), mode: AppMode::Startup, theme: GuiTheme::Ocean, active_tab: Tab::Chat, + chat_input: String::new(), - chat_log: String::from("AiRust — Cyber Operator\n\n"), + sessions: vec![ChatSession { + id: session_id.clone(), + name: "Chat 1".to_string(), + messages: Vec::new(), + }], + active_session_id: session_id, + sessions_loaded: false, + + exec_cmd: String::new(), + exec_results: Vec::new(), + + script_code: String::new(), + script_timeout: "30".to_string(), + script_lang: String::new(), + script_results: Vec::new(), + + learn_topic: String::new(), + learn_results: String::new(), + learn_progress: String::new(), + + studio_desc: String::new(), + studio_output: String::new(), + studio_code: String::new(), + studio_lang: String::new(), + + pdf_status: String::new(), + + ocr_status: String::new(), + ocr_text: String::new(), + + book_status: String::new(), + + ds_desc: String::new(), + ds_count: "10".to_string(), + ds_add_knowledge: true, + ds_out: String::new(), + kn_search_q: String::new(), kn_results: String::new(), kn_add_src: String::new(), kn_add_ins: String::new(), kn_add_out: String::new(), kn_status: String::new(), - exec_cmd: String::new(), - exec_args: String::new(), - exec_out: String::new(), - sgen_desc: String::new(), - sgen_out: String::new(), - imp_status: String::new(), - ds_desc: String::new(), - ds_count: String::from("5"), - ds_out: String::new(), + tgt_val: String::new(), tgt_status: String::new(), + + sidebar_total: "—".to_string(), + sidebar_sources: Vec::new(), + tx, rx, } } - fn spawn(&self, label: &str, f: F) + fn spawn(&self, _label: &str, f: F) where - F: FnOnce(&str) -> String + Send + 'static, + F: FnOnce(&str) -> Msg + Send + 'static, { let url = self.server_url.clone(); let tx = self.tx.clone(); - let lbl = label.to_string(); thread::spawn(move || { - let result = f(&url); - let _ = tx.send((lbl, GuiMsg::Set(result))); + let msg = f(&url); + let _ = tx.send(msg); }); } - fn api_get(&self, label: &str, path: &str) { - let url = format!("{}{}", self.server_url, path); - let tx = self.tx.clone(); - let lbl = label.to_string(); - thread::spawn(move || { - let client = reqwest::blocking::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - let result = match client.get(&url).send() { - Ok(r) => r.text().unwrap_or_default(), - Err(e) => format!("Error: {}", e), - }; - let _ = tx.send((lbl, GuiMsg::Set(result))); - }); + fn active_session(&self) -> Option<&ChatSession> { + self.sessions.iter().find(|s| s.id == self.active_session_id) } - fn api_post(&self, label: &str, path: &str, body: Value) { - let url = format!("{}{}", self.server_url, path); - let tx = self.tx.clone(); - let lbl = label.to_string(); - thread::spawn(move || { - let client = reqwest::blocking::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - let result = match client.post(&url).json(&body).send() { - Ok(r) => { - let raw = r.text().unwrap_or_default(); - match serde_json::from_str::(&raw) { - Ok(v) => serde_json::to_string_pretty(&v).unwrap_or(raw), - Err(_) => raw, - } + fn active_session_mut(&mut self) -> Option<&mut ChatSession> { + self.sessions.iter_mut().find(|s| s.id == self.active_session_id) + } + + fn render_session_list(&self, ui: &mut egui::Ui) { + let active_id = &self.active_session_id; + for s in &self.sessions { + let is_active = s.id == *active_id; + let preview = s.messages.last().map(|m| { + if m.content.len() > 30 { format!("{}...", &m.content[..30]) } else { m.content.clone() } + }).unwrap_or_default(); + + ui.horizontal(|ui| { + let label = if is_active { + RichText::new(&s.name).color(self.theme.colors().4) + } else { + RichText::new(&s.name) + }; + if ui.selectable_label(is_active, label).clicked() && !is_active { + let id = s.id.clone(); + self.spawn("_switch_session", move |_| ("_switch_session".into(), GuiMsg::Set(id))); } - Err(e) => format!("Error: {}", e), - }; - let _ = tx.send((lbl, GuiMsg::Set(result))); + if ui.button("✕").clicked() { + let id = s.id.clone(); + self.spawn("_del_session", move |_| ("_del_session".into(), GuiMsg::Set(id))); + } + }); + if !preview.is_empty() { + ui.label(RichText::new(preview).size(10.0).color(self.theme.colors().5.linear_multiply(0.6))); + } + } + } + + fn do_delete_session(&mut self, id: &str) { + if self.sessions.len() <= 1 { + return; + } + self.sessions.retain(|s| s.id != id); + if !self.sessions.iter().any(|s| s.id == self.active_session_id) { + self.active_session_id = self.sessions[0].id.clone(); + } + } + + fn do_new_session(&mut self) { + let n = self.sessions.len() + 1; + let id = format!("sess_{}", chrono_now_ms()); + self.sessions.push(ChatSession { + id: id.clone(), + name: format!("Chat {}", n), + messages: Vec::new(), + }); + self.active_session_id = id; + } + + fn load_stats(&self) { + let url = self.server_url.clone(); + let tx = self.tx.clone(); + thread::spawn(move || { + match api_get(&url, "/api/knowledge/stats") { + Ok(text) => { let _ = tx.send(("_stats".into(), GuiMsg::StatsResult(text))); } + Err(_) => {} + } }); } } +fn chrono_now_ms() -> u64 { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64 +} + impl eframe::App for AirustGui { fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { self.theme.apply(ui.ctx()); + if !self.sessions_loaded { + self.sessions_loaded = true; + self.load_stats(); + } + while let Ok((label, msg)) = self.rx.try_recv() { - let target = match msg { - GuiMsg::Set(t) => t, - }; - match label.as_str() { - "chat" => self.chat_log = target, - "kn_search" => self.kn_results = target, - "kn_stats" => self.kn_status = target, - "kn_add" => self.kn_status = target, - "exec" => self.exec_out = target, - "sgen" => self.sgen_out = target, - "import" => self.imp_status = target, - "dataset" => self.ds_out = target, - "target_set" => self.tgt_status = target, - "target_clear" => self.tgt_status = target, - _ => {} + match msg { + GuiMsg::Set(t) => { + match label.as_str() { + "_switch_session" => self.active_session_id = t, + "_del_session" => self.do_delete_session(&t), + "_chat_resp" => { + if let Some(s) = self.active_session_mut() { + s.messages.push(ChatMsg { role: "assistant".into(), content: t }); + } + } + "_detect_lang" => self.script_lang = t, + "kn_results" => self.kn_results = t, + "kn_stats" => self.kn_status = t, + "exec" => self.exec_results.push(ExecResult { + command: self.exec_cmd.clone(), success: false, stdout: String::new(), + stderr: t, exit_code: None, + }), + "tgt_status" => self.tgt_status = t, + "tgt_clear" => self.tgt_status = t, + "pdf_status" => self.pdf_status = t, + "ocr_status" => { self.ocr_status = t; } + "ocr_text" => { self.ocr_text = t; self.ocr_status = "OCR complete.".to_string(); } + "book_status" => self.book_status = t, + "ds_out" => self.ds_out = t, + "learn_progress" => self.learn_progress = t, + "learn_results_add" => { + self.learn_results = format!("{}\n{}\n---\n", self.learn_results, t); + self.load_stats(); + } + "studio_desc" => self.studio_output = t, + "studio_code" => { self.studio_code = t; } + "studio_lang" => { self.studio_lang = t; } + _ => {} + } + } + GuiMsg::ExecResult(r) => self.exec_results.push(r), + GuiMsg::ScriptResult(r) => self.script_results.push(r), + GuiMsg::StudioResult(code, lang) => { + self.studio_code = code; + self.studio_lang = lang; + } + GuiMsg::StudioExecResult(r) => { + let status = if r.success { "SUCCESS" } else if r.exit_code.is_none() { "TIMEOUT" } else { "ERROR" }; + self.studio_output = format!("{} {}\n{}\n{}\n---\n", r.command, status, r.stdout, r.stderr); + } + GuiMsg::StatsResult(text) => { + if let Ok(v) = serde_json::from_str::(&text) { + self.sidebar_total = v.get("total").map(|t| t.to_string()).unwrap_or_else(|| "—".into()); + self.sidebar_sources = v.get("sources") + .and_then(|s| s.as_array()) + .map(|arr| { + arr.iter().filter_map(|item| { + let src = item.get(0).and_then(|s| s.as_str()).unwrap_or("?").to_string(); + let cnt = item.get(1).and_then(|c| c.as_u64()).unwrap_or(0); + Some((src, cnt)) + }).collect() + }) + .unwrap_or_default(); + } + } + GuiMsg::SearchResult(text) => self.kn_results = text, + GuiMsg::LearnResult(text) => { + self.learn_progress = String::new(); + self.learn_results = format!("{}{}\n---\n", self.learn_results, text); + self.load_stats(); + } + GuiMsg::AutoLearnResult(text) => { + self.learn_progress = String::new(); + self.learn_results = format!("{}{}\n---\n", self.learn_results, text); + self.load_stats(); + } + GuiMsg::DatasetResult(text) => { + if text.contains("__REFRESH_STATS__") { + self.ds_out = text.replace("__REFRESH_STATS__", ""); + self.load_stats(); + } else { + self.ds_out = text; + } + } + GuiMsg::PdfResult(text) => self.pdf_status = text, + GuiMsg::OcrResult(text) => { + self.ocr_status = "OCR complete.".to_string(); + if let Ok(v) = serde_json::from_str::(&text) { + if v.get("success").and_then(|s| s.as_bool()).unwrap_or(false) { + self.ocr_text = v.get("text").and_then(|t| t.as_str()).unwrap_or("").to_string(); + } else { + self.ocr_status = v.get("error").and_then(|e| e.as_str()).unwrap_or("OCR failed").to_string(); + } + } else { + self.ocr_status = text; + } + } + GuiMsg::BookResult(text) => self.book_status = text, + GuiMsg::KnowledgeAddResult(text) => { + self.kn_status = text; + self.kn_add_ins.clear(); + self.kn_add_out.clear(); + self.load_stats(); + } + GuiMsg::TargetResult(text) => self.tgt_status = text, } } @@ -301,8 +560,7 @@ impl eframe::App for AirustGui { AppMode::Desktop => self.ui_desktop(ui), } - ui.ctx() - .request_repaint_after(std::time::Duration::from_millis(200)); + ui.ctx().request_repaint_after(std::time::Duration::from_millis(200)); } } @@ -313,41 +571,29 @@ impl AirustGui { egui::CentralPanel::default().show_inside(ui, |ui| { let avail = ui.available_size(); let (w, h) = (avail.x, avail.y); - let btn_w = 260.0; - let btn_h = 100.0; - let spacing = 30.0; - ui.allocate_space(egui::vec2(0.0, h * 0.25)); - ui.vertical_centered(|ui| { ui.heading(RichText::new("AiRust").size(32.0)); ui.label(RichText::new("Cyber Operator").size(16.0)); ui.add_space(30.0); - ui.horizontal(|ui| { + let btn_w = 260.0; + let btn_h = 100.0; + let spacing = 30.0; let total_w = 2.0 * btn_w + spacing; ui.allocate_space(egui::vec2((w - total_w).max(0.0) * 0.5, 0.0)); - - if ui - .add_sized( - egui::vec2(btn_w, btn_h), - egui::Button::new(RichText::new("🌐 Web Browser").size(18.0)), - ) - .clicked() - { + if ui.add_sized( + egui::vec2(btn_w, btn_h), + egui::Button::new(RichText::new("🌐 Web Browser").size(18.0)), + ).clicked() { let _ = open::that(format!("https://{}", "127.0.0.1:3000")); self.mode = AppMode::Desktop; } - ui.add_space(spacing); - - if ui - .add_sized( - egui::vec2(btn_w, btn_h), - egui::Button::new(RichText::new("🖥️ Desktop GUI").size(18.0)), - ) - .clicked() - { + if ui.add_sized( + egui::vec2(btn_w, btn_h), + egui::Button::new(RichText::new("🖥️ Desktop GUI").size(18.0)), + ).clicked() { self.mode = AppMode::Desktop; } }); @@ -360,93 +606,218 @@ impl AirustGui { impl AirustGui { fn ui_desktop(&mut self, ui: &mut egui::Ui) { - egui::Panel::top("tab_bar").show_inside(ui, |ui| { - ui.horizontal(|ui| { - ui.heading("AiRust"); + let (_, _, _, _, accent, text) = self.theme.colors(); + + // ── Sidebar ── + egui::SidePanel::left("sidebar") + .resizable(false) + .default_width(250.0) + .show_inside(ui, |ui| { + // Sidebar header + ui.heading(RichText::new("AiRust").color(accent).size(18.0)); + ui.label(RichText::new("Cyber Operator v0.2.0").size(11.0).color(text.linear_multiply(0.6))); ui.separator(); + + // System Status + ui.label(RichText::new("SYSTEM STATUS").size(11.0).color(text.linear_multiply(0.6)).strong()); + ui.horizontal(|ui| { + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical_centered(|ui| { + ui.label(RichText::new(&self.sidebar_total).size(20.0).color(accent).strong()); + ui.label(RichText::new("Entries").size(10.0).color(text.linear_multiply(0.6))); + }); + }); + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical_centered(|ui| { + ui.label(RichText::new(&self.sidebar_sources.len().to_string()).size(20.0).color(accent).strong()); + ui.label(RichText::new("Sources").size(10.0).color(text.linear_multiply(0.6))); + }); + }); + }); + + // Knowledge Sources + ui.add_space(8.0); + ui.label(RichText::new("SOURCES").size(11.0).color(text.linear_multiply(0.6)).strong()); + egui::ScrollArea::vertical().max_height(150.0).show(ui, |ui| { + for (src, cnt) in &self.sidebar_sources { + ui.horizontal(|ui| { + ui.label(RichText::new(src).size(12.0)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label(RichText::new(cnt.to_string()).size(12.0).color(accent).strong()); + }); + }); + } + if self.sidebar_sources.is_empty() { + ui.label(RichText::new("No sources").size(11.0).color(text.linear_multiply(0.5))); + } + }); + + ui.separator(); + + // Theme + ui.label(RichText::new("THEME").size(11.0).color(text.linear_multiply(0.6)).strong()); + egui::ComboBox::from_id_salt("sidebar_theme") + .selected_text(self.theme.name()) + .width(ui.available_width()) + .show_ui(ui, |ui| { + for t in GuiTheme::all() { + if ui.selectable_label(*t == self.theme, t.name()).clicked() { + self.theme = *t; + } + } + }); + + ui.separator(); + + // Target + ui.label(RichText::new("TARGET").size(11.0).color(text.linear_multiply(0.6)).strong()); + ui.horizontal(|ui| { + ui.add_sized( + [ui.available_width() - 50.0, 24.0], + egui::TextEdit::singleline(&mut self.tgt_val).hint_text("IP / hostname..."), + ); + if ui.button("Set").clicked() { + let val = self.tgt_val.clone(); + self.spawn("tgt_status", move |url| { + match api_post(url, "/api/target/set", serde_json::json!({"target": val})) { + Ok(r) => ("tgt_status".into(), GuiMsg::TargetResult(r)), + Err(e) => ("tgt_status".into(), GuiMsg::TargetResult(format!("Error: {}", e))), + } + }); + } + }); + if !self.tgt_status.is_empty() { + ui.label(RichText::new(&self.tgt_status).size(10.0).color(text.linear_multiply(0.6))); + } + + ui.separator(); + + // Chat Sessions + ui.label(RichText::new("CHAT SESSIONS").size(11.0).color(text.linear_multiply(0.6)).strong()); + if ui.button("+ New Chat").clicked() { + self.do_new_session(); + } + egui::ScrollArea::vertical().max_height(180.0).show(ui, |ui| { + self.render_session_list(ui); + }); + if ui.button("Delete All Chats").clicked() { + let id = format!("sess_{}", chrono_now_ms()); + self.sessions = vec![ChatSession { + id: id.clone(), name: "Chat 1".to_string(), messages: Vec::new(), + }]; + self.active_session_id = id; + } + + ui.separator(); + + // Quick Actions + ui.label(RichText::new("QUICK ACTIONS").size(11.0).color(text.linear_multiply(0.6)).strong()); + if ui.button("Clear Chat").clicked() { + if let Some(s) = self.active_session_mut() { + s.messages.clear(); + } + } + if ui.button("Purge Knowledge").clicked() { + self.spawn("kn_stats", move |url| { + match api_post(url, "/api/knowledge/purge", serde_json::json!({})) { + Ok(r) => ("kn_stats".into(), GuiMsg::Set(r)), + Err(e) => ("kn_stats".into(), GuiMsg::Set(format!("Error: {}", e))), + } + }); + } + if ui.button("Kill Server").clicked() { + let url = self.server_url.clone(); + thread::spawn(move || { let _ = api_post(&url, "/api/shutdown", serde_json::json!({})); }); + } + }); + + // ── Main area ── + egui::TopBottomPanel::top("tab_bar").show_inside(ui, |ui| { + ui.horizontal(|ui| { let active = &mut self.active_tab; for tab in Tab::all() { let selected = *tab == *active; - if ui - .selectable_label(selected, RichText::new(tab.label()).size(14.0)) - .clicked() - { + if ui.selectable_label(selected, RichText::new(tab.label()).size(13.0)).clicked() { *active = *tab; } } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label(RichText::new("Theme:").size(12.0)); - egui::ComboBox::from_id_salt("theme_combo") - .selected_text(self.theme.name()) - .width(130.0) - .show_ui(ui, |ui| { - for t in GuiTheme::all() { - if ui.selectable_label(*t == self.theme, t.name()).clicked() { - self.theme = *t; - } - } - }); - }); }); }); - egui::CentralPanel::default().show_inside(ui, |ui| match self.active_tab { - Tab::Chat => self.ui_chat(ui), - Tab::Exec => self.ui_exec(ui), - Tab::ScriptGen => self.ui_scriptgen(ui), - Tab::Knowledge => self.ui_knowledge(ui), - Tab::Import => self.ui_import(ui), - Tab::Dataset => self.ui_dataset(ui), - Tab::Target => self.ui_target(ui), + egui::CentralPanel::default().show_inside(ui, |ui| { + match self.active_tab { + Tab::Chat => self.ui_chat(ui), + Tab::Execute => self.ui_exec(ui), + Tab::Scripts => self.ui_scripts(ui), + Tab::SelfLearn => self.ui_self_learn(ui), + Tab::ScriptStudio => self.ui_script_studio(ui), + Tab::PdfImport => self.ui_pdf_import(ui), + Tab::Ocr => self.ui_ocr(ui), + Tab::Books => self.ui_books(ui), + Tab::Datasets => self.ui_datasets(ui), + Tab::Knowledge => self.ui_knowledge(ui), + } }); } } -// ── Tab UIs ── +// ── Tab Implementations ── impl AirustGui { + // ── Chat Tab ── fn ui_chat(&mut self, ui: &mut egui::Ui) { let avail = ui.available_height(); let input_h = 40.0; - let btn_w = 70.0; + // Messages egui::ScrollArea::vertical() .max_height((avail - input_h - 10.0).max(100.0)) .stick_to_bottom(true) - .show(ui, |ui| ui.label(&self.chat_log)); + .show(ui, |ui| { + if let Some(session) = self.active_session() { + for msg in &session.messages { + let role_color = match msg.role.as_str() { + "user" => self.theme.colors().5.linear_multiply(0.8), + "assistant" => self.theme.colors().4, + _ => self.theme.colors().5.linear_multiply(0.5), + }; + ui.label(RichText::new(format!("[{}]", msg.role)).size(11.0).color(role_color).strong()); + ui.label(RichText::new(&msg.content).size(13.0)); + ui.add_space(6.0); + } + if session.messages.is_empty() { + ui.label(RichText::new("Start a conversation...").size(12.0).color(self.theme.colors().5.linear_multiply(0.5))); + } + } + }); + // Input ui.separator(); + let btn_w = 70.0; ui.horizontal(|ui| { - ui.add_sized( + let resp = ui.add_sized( [ui.available_width() - btn_w - 10.0, 30.0], - egui::TextEdit::singleline(&mut self.chat_input).hint_text("Ask anything..."), + egui::TextEdit::singleline(&mut self.chat_input).hint_text("Type your message..."), ); - if ui.button("Send").clicked() { + let send = ui.button("Send"); + if send.clicked() || (resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) { let msg = self.chat_input.trim().to_string(); if !msg.is_empty() { - let body = serde_json::json!({"message": msg}); - let saved = self.chat_log.clone(); - self.chat_log = format!("{}> {}\n", saved, msg); - self.spawn("chat", move |url| { - let url = format!("{}/api/chat/sync", url); - let c = reqwest::blocking::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - match c.post(&url).json(&body).send() { + if let Some(s) = self.active_session_mut() { + s.messages.push(ChatMsg { role: "user".into(), content: msg.clone() }); + } + self.spawn("_chat_req", move |url| { + match api_post(url, "/api/chat/sync", serde_json::json!({"message": msg})) { Ok(r) => { - let raw = r.text().unwrap_or_default(); - let resp = match serde_json::from_str::(&raw) { - Ok(v) => v - .get("response") - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or(raw), - Err(_) => raw, - }; - format!("{}< {}\n", saved, resp) + let resp = serde_json::from_str::(&r) + .ok() + .and_then(|v| v.get("response").and_then(|v| v.as_str()).map(String::from)) + .unwrap_or(r); + ("_chat_resp".into(), GuiMsg::Set(resp)) } - Err(e) => format!("{}< Error: {}\n", saved, e), + Err(e) => ("_chat_resp".into(), GuiMsg::Set(format!("Error: {}", e))), } }); self.chat_input.clear(); @@ -455,270 +826,601 @@ impl AirustGui { }); } + // ── Execute Tab ── fn ui_exec(&mut self, ui: &mut egui::Ui) { - ui.heading("Execute Command"); - ui.label("Command:"); - ui.text_edit_singleline(&mut self.exec_cmd); - ui.label("Arguments (space-separated):"); - ui.text_edit_singleline(&mut self.exec_args); - if ui.button("Run").clicked() { - let body = serde_json::json!({ - "command": self.exec_cmd, - "args": self.exec_args.split_whitespace().collect::>(), - "timeout_secs": 30 - }); - self.exec_out = String::new(); - self.spawn("exec", move |url| { - let url = format!("{}/api/exec", url); - let c = reqwest::blocking::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - match c.post(&url).json(&body).send() { - Ok(r) => pretty_json(&r.text().unwrap_or_default()), - Err(e) => format!("Error: {}", e), - } - }); - } - ui.separator(); - egui::ScrollArea::vertical() - .max_height(ui.available_height() - 10.0) - .show(ui, |ui| ui.label(&self.exec_out)); - } - - fn ui_scriptgen(&mut self, ui: &mut egui::Ui) { - ui.heading("Script Generator"); - ui.label("Describe what the script should do:"); - ui.add_sized( - [ui.available_width(), 80.0], - egui::TextEdit::multiline(&mut self.sgen_desc), - ); - if ui.button("Generate").clicked() { - let body = serde_json::json!({"description": self.sgen_desc}); - self.sgen_out = String::new(); - self.spawn("sgen", move |url| { - let url = format!("{}/api/scriptgen", url); - let c = reqwest::blocking::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - match c.post(&url).json(&body).send() { - Ok(r) => { - let raw = r.text().unwrap_or_default(); - match serde_json::from_str::(&raw) { - Ok(v) => v - .get("result") - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or(raw), - Err(_) => raw, - } - } - Err(e) => format!("Error: {}", e), - } - }); - } - ui.separator(); - egui::ScrollArea::vertical() - .max_height(ui.available_height() - 10.0) - .show(ui, |ui| ui.monospace(&self.sgen_out)); - } - - fn ui_knowledge(&mut self, ui: &mut egui::Ui) { - ui.heading("Knowledge Base"); - if ui.button("Stats").clicked() { - self.api_get("kn_stats", "/api/knowledge/stats"); - } - ui.separator(); - ui.label("Search:"); ui.horizontal(|ui| { - ui.add_sized( + ui.label("Command:"); + let resp = ui.add_sized( [ui.available_width() - 60.0, 24.0], - egui::TextEdit::singleline(&mut self.kn_search_q).hint_text("query"), + egui::TextEdit::singleline(&mut self.exec_cmd).hint_text("e.g., ping -n 4 127.0.0.1"), ); - if ui.button("Go").clicked() { - let body = serde_json::json!({"query": self.kn_search_q}); - self.api_post("kn_search", "/api/knowledge/search", body); + if ui.button("Run").clicked() || (resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) { + let cmd = self.exec_cmd.clone(); + let parts: Vec<&str> = cmd.split_whitespace().collect(); + let command = parts.first().cloned().unwrap_or("").to_string(); + let args: Vec = parts[1..].iter().map(|s| s.to_string()).collect(); + self.spawn("exec_res", move |url| { + match api_post(url, "/api/exec", serde_json::json!({"command": command, "args": args, "timeout_secs": 30})) { + Ok(r) => { + if let Ok(v) = serde_json::from_str::(&r) { + let result = v.get("result").unwrap_or(&v); + ("_exec".into(), GuiMsg::ExecResult(ExecResult { + command: result.get("command").and_then(|c| c.as_str()).unwrap_or(&cmd).to_string(), + success: result.get("success").and_then(|s| s.as_bool()).unwrap_or(false), + stdout: result.get("stdout").and_then(|s| s.as_str()).unwrap_or("").to_string(), + stderr: result.get("stderr").and_then(|s| s.as_str()).unwrap_or("").to_string(), + exit_code: result.get("exit_code").and_then(|c| c.as_i64()).map(|c| c as i32), + })) + } else { + ("_exec".into(), GuiMsg::ExecResult(ExecResult { + command: cmd, success: false, stdout: String::new(), + stderr: r, exit_code: None, + })) + } + } + Err(e) => ("_exec".into(), GuiMsg::ExecResult(ExecResult { + command: cmd, success: false, stdout: String::new(), + stderr: e, exit_code: None, + })), + } + }); } }); - egui::ScrollArea::vertical() - .max_height(120.0) - .show(ui, |ui| ui.label(&self.kn_results)); + ui.separator(); - ui.label("Add Entry:"); - ui.horizontal(|ui| { - ui.label("Source:"); - ui.add_sized( - [120.0, 24.0], - egui::TextEdit::singleline(&mut self.kn_add_src), - ); - }); - ui.add_sized( - [ui.available_width(), 40.0], - egui::TextEdit::multiline(&mut self.kn_add_ins).hint_text("Instruction"), - ); - ui.add_sized( - [ui.available_width(), 60.0], - egui::TextEdit::multiline(&mut self.kn_add_out).hint_text("Output"), - ); - if ui.button("Add").clicked() { - let body = serde_json::json!({ - "source": self.kn_add_src, - "instruction": self.kn_add_ins, - "output": self.kn_add_out, + egui::ScrollArea::vertical() + .max_height(ui.available_height() - 10.0) + .show(ui, |ui| { + let (_, _, _, _, accent, success_color, error_color, warning_color) = self.cmd_colors(); + for r in &self.exec_results { + let status = if r.success { "SUCCESS" } else if r.exit_code.is_none() { "TIMEOUT" } else { "ERROR" }; + let s_color = if r.success { success_color } else if r.exit_code.is_none() { warning_color } else { error_color }; + ui.group(|ui| { + ui.horizontal(|ui| { + ui.label(RichText::new(&r.command).color(accent).strong()); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label(RichText::new(status).color(s_color).strong()); + }); + }); + if !r.stdout.is_empty() { + ui.label(RichText::new(&r.stdout).size(12.0)); + } + if !r.stderr.is_empty() { + ui.label(RichText::new(&r.stderr).size(12.0).color(error_color)); + } + }); + } + if self.exec_results.is_empty() { + ui.label(RichText::new("Run a command to see output.").size(12.0).color(self.theme.colors().5.linear_multiply(0.5))); + } }); - self.api_post("kn_add", "/api/knowledge", body); - } - ui.separator(); - egui::ScrollArea::vertical() - .max_height(ui.available_height() - 200.0) - .show(ui, |ui| ui.label(&self.kn_status)); } - fn ui_import(&mut self, ui: &mut egui::Ui) { - ui.heading("Import"); - if ui.button("Import PDF...").clicked() { - if let Some(path) = rfd::FileDialog::new() - .add_filter("PDF", &["pdf"]) - .pick_file() - { + fn cmd_colors(&self) -> (Color32, Color32, Color32, Color32, Color32, Color32, Color32, Color32) { + let (bg, surface, surface2, border, accent, _text) = self.theme.colors(); + let success = Color32::from_rgb(0x00, 0xff, 0x88); + let error = Color32::from_rgb(0xff, 0x33, 0x66); + let warning = Color32::from_rgb(0xff, 0xaa, 0x00); + (bg, surface, surface2, border, accent, success, error, warning) + } + + // ── Scripts Tab ── + fn ui_scripts(&mut self, ui: &mut egui::Ui) { + let (_, _, _, _, accent, success_color, error_color, _) = self.cmd_colors(); + + // Toolbar + ui.horizontal(|ui| { + let lang_badge = if self.script_lang.is_empty() { "No code" } else { &self.script_lang }; + ui.label(RichText::new(lang_badge).size(12.0).color(accent)); + if ui.button("Test").clicked() { + let code = self.script_code.clone(); + let timeout: u64 = self.script_timeout.parse().unwrap_or(30); + self.spawn("script_res", move |url| { + match api_post(url, "/api/script", serde_json::json!({"code": code, "timeout_secs": timeout})) { + Ok(r) => parse_exec_result(&r, "Test"), + Err(e) => ("_script".into(), GuiMsg::ScriptResult(ExecResult { + command: "Test".into(), success: false, stdout: String::new(), stderr: e, exit_code: None, + })), + } + }); + } + if ui.button("Execute").clicked() { + let code = self.script_code.clone(); + let timeout: u64 = self.script_timeout.parse().unwrap_or(30); + let lang = self.script_lang.clone(); + self.spawn("script_res", move |url| { + match api_post(url, "/api/script", serde_json::json!({"code": code, "timeout_secs": timeout})) { + Ok(r) => parse_exec_result(&r, &lang), + Err(e) => ("_script".into(), GuiMsg::ScriptResult(ExecResult { + command: lang, success: false, stdout: String::new(), stderr: e, exit_code: None, + })), + } + }); + } + ui.add_space(8.0); + ui.label("Timeout:"); + ui.add_sized([60.0, 24.0], egui::TextEdit::singleline(&mut self.script_timeout)); + }); + + // Code editor + let editor = egui::TextEdit::multiline(&mut self.script_code) + .font(egui::TextStyle::Monospace) + .desired_rows(10) + .hint_text("# Paste your code here — language is auto-detected..."); + let resp = ui.add_sized([ui.available_width(), ui.available_height() * 0.4], editor); + + // Auto-detect language on text change + if resp.changed() { + let code = self.script_code.clone(); + self.spawn("_detect_lang", move |url| { + match api_post(url, "/api/detect-lang", serde_json::json!({"code": code})) { + Ok(r) => { + let lang = serde_json::from_str::(&r) + .ok() + .and_then(|v| v.get("language").and_then(|l| l.as_str()).map(String::from)) + .unwrap_or_else(|| "?".into()); + ("_detect_lang".into(), GuiMsg::Set(lang)) + } + Err(_) => ("_detect_lang".into(), GuiMsg::Set("?".into())), + } + }); + } + + // Output + ui.separator(); + egui::ScrollArea::vertical() + .max_height(ui.available_height() - 10.0) + .show(ui, |ui| { + for r in &self.script_results { + let status = if r.success { "SUCCESS" } else if r.exit_code.is_none() { "TIMEOUT" } else { "ERROR" }; + let s_color = if r.success { success_color } else if r.exit_code.is_none() { Color32::from_rgb(0xff, 0xaa, 0x00) } else { error_color }; + ui.group(|ui| { + ui.horizontal(|ui| { + ui.label(RichText::new(&r.command).color(accent).strong()); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label(RichText::new(status).color(s_color).strong()); + }); + }); + if !r.stdout.is_empty() { + ui.label(RichText::new(&r.stdout).size(12.0)); + } + if !r.stderr.is_empty() { + ui.label(RichText::new(&r.stderr).size(12.0).color(error_color)); + } + }); + } + }); + } + + // ── Self-Learn Tab ── + fn ui_self_learn(&mut self, ui: &mut egui::Ui) { + let (_, _, _, _, accent, _, _, _) = self.cmd_colors(); + + ui.heading(RichText::new("Self-Learning Engine").color(accent)); + ui.label(RichText::new("The AI searches the web, derives structured knowledge, and adds it to the knowledge base.").size(12.0).color(self.theme.colors().5.linear_multiply(0.7))); + + ui.add_space(10.0); + ui.horizontal(|ui| { + let resp = ui.add_sized( + [ui.available_width() - 200.0, 24.0], + egui::TextEdit::singleline(&mut self.learn_topic).hint_text("Topic to learn about..."), + ); + if ui.button("Learn from Web").clicked() || (resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) { + let topic = self.learn_topic.clone(); + self.learn_progress = format!("Learning about: {}...", topic); + self.spawn("learn_res", move |url| { + match api_post(url, "/api/learn/search", serde_json::json!({"query": topic})) { + Ok(r) => ("_learn".into(), GuiMsg::LearnResult(r)), + Err(e) => ("_learn".into(), GuiMsg::LearnResult(format!("Error: {}", e))), + } + }); + self.learn_topic.clear(); + } + if ui.button("Auto-Learn (5)").clicked() { + self.learn_progress = "Auto-learning in progress...".to_string(); + self.spawn("auto_learn_res", move |url| { + match api_post(url, "/api/learn/auto", serde_json::json!({})) { + Ok(r) => ("_auto_learn".into(), GuiMsg::AutoLearnResult(r)), + Err(e) => ("_auto_learn".into(), GuiMsg::AutoLearnResult(format!("Error: {}", e))), + } + }); + } + }); + + // Progress + if !self.learn_progress.is_empty() { + ui.label(RichText::new(&self.learn_progress).size(12.0).color(accent)); + } + + // Results + ui.separator(); + egui::ScrollArea::vertical() + .max_height(ui.available_height() - 120.0) + .show(ui, |ui| { + if !self.learn_results.is_empty() { + ui.label(RichText::new(&self.learn_results).size(12.0)); + } else { + ui.label(RichText::new("No learning results yet.").size(12.0).color(self.theme.colors().5.linear_multiply(0.5))); + } + }); + } + + // ── Script Studio Tab ── + fn ui_script_studio(&mut self, ui: &mut egui::Ui) { + let (_, _, _, _, accent, _, _, _) = self.cmd_colors(); + let (_, _, _, _, _, text) = self.theme.colors(); + + // Toolbar + ui.horizontal(|ui| { + if ui.button("Generate").clicked() { + let desc = self.studio_desc.clone(); + self.studio_output = "Generating script...".to_string(); + self.spawn("studio_gen", move |url| { + match api_post(url, "/api/scriptgen", serde_json::json!({"description": desc})) { + Ok(r) => { + if let Ok(v) = serde_json::from_str::(&r) { + let code = v.get("code").and_then(|c| c.as_str()).unwrap_or("").to_string(); + let lang = v.get("language").and_then(|l| l.as_str()).unwrap_or("?").to_string(); + ("_studio_gen".into(), GuiMsg::StudioResult(code, lang)) + } else { + ("_studio_gen".into(), GuiMsg::StudioResult(r, "?".into())) + } + } + Err(e) => ("_studio_gen".into(), GuiMsg::StudioResult(format!("Error: {}", e), "?".into())), + } + }); + } + if ui.button("Copy Code").clicked() { + ui.ctx().copy_text(self.studio_code.clone()); + } + if ui.button("Run Now").clicked() { + let code = self.studio_code.clone(); + let lang = self.studio_lang.clone(); + self.spawn("studio_run", move |url| { + match api_post(url, "/api/script", serde_json::json!({"language": lang, "code": code, "timeout_secs": 30})) { + Ok(r) => parse_exec_result(&r, &lang), + Err(e) => ("_studio_run".into(), GuiMsg::StudioExecResult(ExecResult { + command: lang, success: false, stdout: String::new(), stderr: e, exit_code: None, + })), + } + }); + } + if ui.button("Test").clicked() { + let code = self.studio_code.clone(); + let lang = self.studio_lang.clone(); + self.spawn("studio_run", move |url| { + match api_post(url, "/api/script", serde_json::json!({"language": lang, "code": code, "timeout_secs": 30})) { + Ok(r) => parse_exec_result(&r, &format!("Test - {}", lang)), + Err(e) => ("_studio_run".into(), GuiMsg::StudioExecResult(ExecResult { + command: format!("Test - {}", lang), success: false, stdout: String::new(), stderr: e, exit_code: None, + })), + } + }); + } + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if !self.studio_lang.is_empty() { + ui.label(RichText::new(&format!("Language: {}", self.studio_lang)).size(12.0).color(text.linear_multiply(0.6))); + } + }); + }); + + // Description input + ui.add_sized( + [ui.available_width(), 50.0], + egui::TextEdit::multiline(&mut self.studio_desc) + .hint_text("Describe what you need — the AI chooses the best language automatically..."), + ); + + ui.separator(); + + // Output area + egui::ScrollArea::vertical() + .max_height(ui.available_height() - 100.0) + .show(ui, |ui| { + if !self.studio_code.is_empty() { + ui.group(|ui| { + let lang_label = if self.studio_lang.is_empty() { "Generated" } else { &self.studio_lang }; + ui.label(RichText::new(format!("Generated {} script", lang_label)).color(accent).strong()); + ui.label(RichText::new(&self.studio_code).size(12.0)); + }); + } + if !self.studio_output.is_empty() && self.studio_output != "Generating script..." { + ui.label(RichText::new(&self.studio_output).size(12.0)); + } else if self.studio_output.is_empty() { + ui.label(RichText::new("Describe a script above and click Generate.").size(12.0).color(self.theme.colors().5.linear_multiply(0.5))); + } + }); + } + + // ── PDF Import Tab ── + fn ui_pdf_import(&mut self, ui: &mut egui::Ui) { + let (_, _, _, _, accent, _, _, _) = self.cmd_colors(); + ui.heading(RichText::new("PDF Distillation").color(accent)); + ui.label(RichText::new("Upload a PDF and the AI distills it into structured JSONL knowledge entries.").size(12.0).color(self.theme.colors().5.linear_multiply(0.7))); + + ui.add_space(10.0); + if ui.button("Select PDF...").clicked() { + if let Some(path) = rfd::FileDialog::new().add_filter("PDF", &["pdf"]).pick_file() { let path_s = path.to_string_lossy().to_string(); - self.imp_status = format!("Importing {}...", &path_s); - self.spawn("import", move |url| { - let url = format!("{}/api/pdf/import", url); - let c = reqwest::blocking::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - match c - .post(&url) - .multipart( - reqwest::blocking::multipart::Form::new() - .file("file", &path_s) - .expect("valid path"), - ) - .send() - { - Ok(r) => pretty_json(&r.text().unwrap_or_default()), - Err(e) => format!("Error: {}", e), - } - }); - } - } - if ui.button("Import EPUB...").clicked() { - if let Some(path) = rfd::FileDialog::new() - .add_filter("EPUB", &["epub"]) - .pick_file() - { - let path_s = path.to_string_lossy().to_string(); - self.imp_status = format!("Importing {}...", &path_s); - self.spawn("import", move |url| { - let url = format!("{}/api/import/epub", url); - let c = reqwest::blocking::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - match c - .post(&url) - .multipart( - reqwest::blocking::multipart::Form::new() - .file("file", &path_s) - .expect("valid path"), - ) - .send() - { - Ok(r) => pretty_json(&r.text().unwrap_or_default()), - Err(e) => format!("Error: {}", e), - } - }); - } - } - if ui.button("OCR Image...").clicked() { - if let Some(path) = rfd::FileDialog::new() - .add_filter("Images", &["png", "jpg", "jpeg", "bmp", "tiff"]) - .pick_file() - { - let path_s = path.to_string_lossy().to_string(); - self.imp_status = format!("Running OCR on {}...", &path_s); - self.spawn("import", move |url| { - let url = format!("{}/api/ocr", url); - let c = reqwest::blocking::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - match c - .post(&url) - .multipart( - reqwest::blocking::multipart::Form::new() - .file("image", &path_s) - .expect("valid path"), - ) - .send() - { - Ok(r) => pretty_json(&r.text().unwrap_or_default()), - Err(e) => format!("Error: {}", e), + self.pdf_status = "Distilling...".to_string(); + self.spawn("pdf_res", move |url| { + let url_f = format!("{}/api/pdf/import", url); + let client = reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(true).build().unwrap(); + match client.post(&url_f).multipart( + reqwest::blocking::multipart::Form::new().file("file", &path_s).expect("valid path") + ).send() { + Ok(r) => ("_pdf".into(), GuiMsg::PdfResult(pretty_json(&r.text().unwrap_or_default()))), + Err(e) => ("_pdf".into(), GuiMsg::PdfResult(format!("Error: {}", e))), } }); } } + ui.separator(); egui::ScrollArea::vertical() .max_height(ui.available_height() - 100.0) - .show(ui, |ui| ui.label(&self.imp_status)); + .show(ui, |ui| { + if !self.pdf_status.is_empty() { + ui.label(RichText::new(&self.pdf_status).size(12.0)); + } + }); } - fn ui_dataset(&mut self, ui: &mut egui::Ui) { - ui.heading("Dataset Generator"); - ui.label("Description:"); + // ── OCR Tab ── + fn ui_ocr(&mut self, ui: &mut egui::Ui) { + let (_, _, _, _, accent, _, _, _) = self.cmd_colors(); + ui.heading(RichText::new("OCR — Image to Text").color(accent)); + ui.label(RichText::new("Upload a screenshot or scanned page. Requires Tesseract installed.").size(12.0).color(self.theme.colors().5.linear_multiply(0.7))); + + ui.add_space(10.0); + if ui.button("Select Image...").clicked() { + if let Some(path) = rfd::FileDialog::new().add_filter("Images", &["png", "jpg", "jpeg", "bmp", "tiff"]).pick_file() { + let path_s = path.to_string_lossy().to_string(); + self.ocr_status = "Running OCR...".to_string(); + self.spawn("ocr_res", move |url| { + let url_f = format!("{}/api/ocr", url); + let client = reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(true).build().unwrap(); + let result = match client.post(&url_f).multipart( + reqwest::blocking::multipart::Form::new().file("file", &path_s).expect("valid path") + ).send() { + Ok(r) => r.text().unwrap_or_default(), + Err(e) => format!("ERROR:{}", e), + }; + ("_ocr".into(), GuiMsg::OcrResult(result)) + }); + } + } + + if !self.ocr_text.is_empty() { + ui.add_space(8.0); + ui.label(RichText::new("Extracted Text").color(accent).strong()); + ui.add_sized( + [ui.available_width(), 150.0], + egui::TextEdit::multiline(&mut self.ocr_text).font(egui::TextStyle::Monospace), + ); + ui.horizontal(|ui| { + if ui.button("Copy Text").clicked() { + ui.ctx().copy_text(self.ocr_text.clone()); + } + if ui.button("Add to Knowledge").clicked() { + let text = self.ocr_text.clone(); + self.spawn("kn_add", move |url| { + match api_post(url, "/api/knowledge", serde_json::json!({ + "source": "ocr", "instruction": "OCR extracted text content", "output": text + })) { + Ok(r) => ("kn_add".into(), GuiMsg::KnowledgeAddResult(r)), + Err(e) => ("kn_add".into(), GuiMsg::KnowledgeAddResult(format!("Error: {}", e))), + } + }); + } + }); + } + + ui.separator(); + egui::ScrollArea::vertical() + .max_height(ui.available_height() - 300.0) + .show(ui, |ui| { + if !self.ocr_status.is_empty() { + ui.label(RichText::new(&self.ocr_status).size(12.0)); + } + }); + } + + // ── Books Tab ── + fn ui_books(&mut self, ui: &mut egui::Ui) { + let (_, _, _, _, accent, _, _, _) = self.cmd_colors(); + ui.heading(RichText::new("Book Import (EPUB)").color(accent)); + ui.label(RichText::new("Upload an EPUB ebook to distill into knowledge entries.").size(12.0).color(self.theme.colors().5.linear_multiply(0.7))); + + ui.add_space(10.0); + if ui.button("Select EPUB...").clicked() { + if let Some(path) = rfd::FileDialog::new().add_filter("EPUB", &["epub"]).pick_file() { + let path_s = path.to_string_lossy().to_string(); + self.book_status = "Processing book...".to_string(); + self.spawn("book_res", move |url| { + let url_f = format!("{}/api/import/epub", url); + let client = reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(true).build().unwrap(); + match client.post(&url_f).multipart( + reqwest::blocking::multipart::Form::new().file("file", &path_s).expect("valid path") + ).send() { + Ok(r) => ("_book".into(), GuiMsg::BookResult(pretty_json(&r.text().unwrap_or_default()))), + Err(e) => ("_book".into(), GuiMsg::BookResult(format!("Error: {}", e))), + } + }); + } + } + + ui.separator(); + egui::ScrollArea::vertical() + .max_height(ui.available_height() - 100.0) + .show(ui, |ui| { + if !self.book_status.is_empty() { + ui.label(RichText::new(&self.book_status).size(12.0)); + } + }); + } + + // ── Datasets Tab ── + fn ui_datasets(&mut self, ui: &mut egui::Ui) { + let (_, _, _, _, accent, _, _, _) = self.cmd_colors(); + ui.heading(RichText::new("Synthetic Dataset Generator").color(accent)); + ui.label(RichText::new("Describe the data you need and generate structured JSONL datasets.").size(12.0).color(self.theme.colors().5.linear_multiply(0.7))); + + ui.add_space(10.0); + ui.label("Dataset description:"); ui.add_sized( [ui.available_width(), 80.0], - egui::TextEdit::multiline(&mut self.ds_desc), + egui::TextEdit::multiline(&mut self.ds_desc).hint_text("e.g., nmap scanning techniques with example commands and outputs"), ); + ui.horizontal(|ui| { - ui.label("Count:"); + ui.label("Samples:"); ui.add_sized([60.0, 24.0], egui::TextEdit::singleline(&mut self.ds_count)); + ui.checkbox(&mut self.ds_add_knowledge, "Add to knowledge base"); if ui.button("Generate").clicked() { - let count: u32 = self.ds_count.parse().unwrap_or(5).clamp(1, 50); - let body = serde_json::json!({"description": self.ds_desc, "count": count}); - self.ds_out = String::new(); - self.spawn("dataset", move |url| { - let url = format!("{}/api/dataset/generate", url); - let c = reqwest::blocking::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - match c.post(&url).json(&body).send() { - Ok(r) => pretty_json(&r.text().unwrap_or_default()), - Err(e) => format!("Error: {}", e), + let desc = self.ds_desc.clone(); + let count: u32 = self.ds_count.parse().unwrap_or(10).clamp(1, 50); + let add_kn = self.ds_add_knowledge; + self.ds_out = "Generating...".to_string(); + let add_kn2 = add_kn; + self.spawn("ds_res", move |url| { + match api_post(url, "/api/dataset/generate", serde_json::json!({ + "description": desc, "count": count, "add_to_knowledge": add_kn, + })) { + Ok(r) => ("_ds".into(), GuiMsg::DatasetResult(if add_kn2 { format!("{}__REFRESH_STATS__", pretty_json(&r)) } else { pretty_json(&r) })), + Err(e) => ("_ds".into(), GuiMsg::DatasetResult(format!("Error: {}", e))), } }); } }); + ui.separator(); egui::ScrollArea::vertical() - .max_height(ui.available_height() - 10.0) - .show(ui, |ui| ui.monospace(&self.ds_out)); + .max_height(ui.available_height() - 180.0) + .show(ui, |ui| { + if !self.ds_out.is_empty() { + ui.label(RichText::new(&self.ds_out).size(12.0)); + } else { + ui.label(RichText::new("No dataset generated yet.").size(12.0).color(self.theme.colors().5.linear_multiply(0.5))); + } + }); } - fn ui_target(&mut self, ui: &mut egui::Ui) { - ui.heading("Target"); - ui.label("Set a target IP/hostname injected into every chat context."); + // ── Knowledge Tab ── + fn ui_knowledge(&mut self, ui: &mut egui::Ui) { + let (_, _, _, _, accent, _, _, _) = self.cmd_colors(); + + // Add Knowledge + ui.heading(RichText::new("Add Knowledge").color(accent)); ui.horizontal(|ui| { - ui.add_sized( - [200.0, 24.0], - egui::TextEdit::singleline(&mut self.tgt_val).hint_text("192.168.1.1"), - ); - if ui.button("Set").clicked() { - let body = serde_json::json!({"target": self.tgt_val}); - self.api_post("target_set", "/api/target/set", body); + ui.label("Source:"); + ui.add_sized([150.0, 24.0], egui::TextEdit::singleline(&mut self.kn_add_src)); + }); + ui.add_sized( + [ui.available_width(), 24.0], + egui::TextEdit::singleline(&mut self.kn_add_ins).hint_text("Question / Topic"), + ); + ui.add_sized( + [ui.available_width(), 60.0], + egui::TextEdit::multiline(&mut self.kn_add_out).hint_text("Answer / Content..."), + ); + if ui.button("Save").clicked() { + let src = self.kn_add_src.clone(); + let ins = self.kn_add_ins.clone(); + let out = self.kn_add_out.clone(); + if !ins.is_empty() && !out.is_empty() { + self.spawn("kn_add", move |url| { + match api_post(url, "/api/knowledge", serde_json::json!({ + "source": if src.is_empty() { "user" } else { &src }, + "instruction": ins, "output": out, + })) { + Ok(r) => ("kn_add".into(), GuiMsg::KnowledgeAddResult(r)), + Err(e) => ("kn_add".into(), GuiMsg::KnowledgeAddResult(format!("Error: {}", e))), + } + }); } - if ui.button("Clear").clicked() { - self.api_post("target_clear", "/api/target/clear", serde_json::json!({})); + } + + ui.add_space(10.0); + + // Search Knowledge + ui.heading(RichText::new("Search Knowledge").color(accent)); + ui.horizontal(|ui| { + let resp = ui.add_sized( + [ui.available_width() - 70.0, 24.0], + egui::TextEdit::singleline(&mut self.kn_search_q).hint_text("Search query..."), + ); + if ui.button("Search").clicked() || (resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) { + let q = self.kn_search_q.clone(); + self.kn_results = "Searching...".to_string(); + self.spawn("kn_search", move |url| { + match api_post(url, "/api/knowledge/search", serde_json::json!({"query": q})) { + Ok(r) => ("kn_search".into(), GuiMsg::SearchResult(r)), + Err(e) => ("kn_search".into(), GuiMsg::SearchResult(format!("Error: {}", e))), + } + }); } }); + ui.separator(); egui::ScrollArea::vertical() - .max_height(ui.available_height() - 80.0) - .show(ui, |ui| ui.label(&self.tgt_status)); + .max_height(ui.available_height() - 250.0) + .show(ui, |ui| { + if !self.kn_results.is_empty() && self.kn_results != "Searching..." { + if let Ok(v) = serde_json::from_str::(&self.kn_results) { + if let Some(results) = v.get("results").and_then(|r| r.as_array()) { + for r in results { + let source = r.get("source").and_then(|s| s.as_str()).unwrap_or("?"); + let instruction = r.get("instruction").and_then(|s| s.as_str()).unwrap_or(""); + let output = r.get("output").and_then(|s| s.as_str()).unwrap_or(""); + ui.group(|ui| { + ui.label(RichText::new(format!("[{}]", source)).size(11.0).color(self.theme.colors().5.linear_multiply(0.6))); + ui.label(RichText::new(format!("Q: {}", instruction)).size(12.0).color(accent)); + ui.label(RichText::new(output).size(12.0)); + }); + } + if results.is_empty() { + ui.label(RichText::new("No results found.").size(12.0).color(self.theme.colors().5.linear_multiply(0.5))); + } + } else { + ui.label(RichText::new(&self.kn_results).size(12.0)); + } + } else { + ui.label(RichText::new(&self.kn_results).size(12.0)); + } + } else { + ui.label(RichText::new("Search the knowledge base to see results.").size(12.0).color(self.theme.colors().5.linear_multiply(0.5))); + } + }); + + // Status + if !self.kn_status.is_empty() { + ui.label(RichText::new(&self.kn_status).size(11.0).color(self.theme.colors().5.linear_multiply(0.6))); + } } } + +// ── Helpers ── + +fn parse_exec_result(text: &str, label: &str) -> (String, GuiMsg) { + if let Ok(v) = serde_json::from_str::(text) { + let result = v.get("result").unwrap_or(&v); + ("_script".into(), GuiMsg::ScriptResult(ExecResult { + command: result.get("command").and_then(|c| c.as_str()).unwrap_or(label).to_string(), + success: result.get("success").and_then(|s| s.as_bool()).unwrap_or(false), + stdout: result.get("stdout").and_then(|s| s.as_str()).unwrap_or("").to_string(), + stderr: result.get("stderr").and_then(|s| s.as_str()).unwrap_or("").to_string(), + exit_code: result.get("exit_code").and_then(|c| c.as_i64()).map(|c| c as i32), + })) + } else { + ("_script".into(), GuiMsg::ScriptResult(ExecResult { + command: label.to_string(), success: false, stdout: String::new(), + stderr: text.to_string(), exit_code: None, + })) + } +} + + diff --git a/target/debug/.fingerprint/AiRust-301136bf2d3c6683/dep-bin-AiRust-cli b/target/debug/.fingerprint/AiRust-301136bf2d3c6683/dep-bin-AiRust-cli index 7c075dc..960b4bd 100644 Binary files a/target/debug/.fingerprint/AiRust-301136bf2d3c6683/dep-bin-AiRust-cli and b/target/debug/.fingerprint/AiRust-301136bf2d3c6683/dep-bin-AiRust-cli differ diff --git a/target/debug/AiRust-cli.exe b/target/debug/AiRust-cli.exe index 3294ab0..56089db 100644 Binary files a/target/debug/AiRust-cli.exe and b/target/debug/AiRust-cli.exe differ diff --git a/target/debug/AiRust_cli.pdb b/target/debug/AiRust_cli.pdb index b7ce770..b98e320 100644 Binary files a/target/debug/AiRust_cli.pdb and b/target/debug/AiRust_cli.pdb differ diff --git a/target/debug/deps/AiRust_cli.exe b/target/debug/deps/AiRust_cli.exe index 3294ab0..56089db 100644 Binary files a/target/debug/deps/AiRust_cli.exe and b/target/debug/deps/AiRust_cli.exe differ diff --git a/target/debug/deps/AiRust_cli.pdb b/target/debug/deps/AiRust_cli.pdb index b7ce770..b98e320 100644 Binary files a/target/debug/deps/AiRust_cli.pdb and b/target/debug/deps/AiRust_cli.pdb differ