Massive upgrade to the Desktop Gui
This commit is contained in:
+113
-37
@@ -1,7 +1,9 @@
|
||||
use eframe::egui::{self, Color32, RichText};
|
||||
use serde_json::Value;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Instant;
|
||||
|
||||
fn pretty_json(text: &str) -> String {
|
||||
match serde_json::from_str::<Value>(text) {
|
||||
@@ -68,9 +70,8 @@ impl GuiTheme {
|
||||
),
|
||||
}
|
||||
}
|
||||
fn apply(&self, ctx: &egui::Context) {
|
||||
fn apply_to_visuals(&self, v: &mut egui::Visuals) {
|
||||
let (bg, surface, surface2, border, accent, text) = self.colors();
|
||||
let mut v = egui::Visuals::dark();
|
||||
v.override_text_color = Some(text);
|
||||
v.extreme_bg_color = bg;
|
||||
v.window_fill = surface;
|
||||
@@ -94,29 +95,36 @@ impl GuiTheme {
|
||||
v.widgets.active.bg_fill = accent.linear_multiply(0.25);
|
||||
v.widgets.active.bg_stroke = egui::Stroke::new(1.0, accent);
|
||||
v.widgets.active.fg_stroke = egui::Stroke::new(2.0, accent);
|
||||
ctx.set_visuals(v);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// ── API helper ──
|
||||
// ── Shared HTTP client (lazy, reused across all requests) ──
|
||||
|
||||
fn http_client() -> &'static reqwest::blocking::Client {
|
||||
static CLIENT: std::sync::OnceLock<reqwest::blocking::Client> = std::sync::OnceLock::new();
|
||||
CLIENT.get_or_init(|| {
|
||||
reqwest::blocking::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.expect("HTTP client build")
|
||||
})
|
||||
}
|
||||
|
||||
fn api_get(server_url: &str, path: &str) -> Result<String, String> {
|
||||
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())?;
|
||||
let resp = http_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<String, String> {
|
||||
let url = format!("{}{}", server_url, path);
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
let resp = http_client()
|
||||
.post(&url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.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())
|
||||
}
|
||||
|
||||
@@ -211,6 +219,8 @@ enum GuiMsg {
|
||||
BookResult(String),
|
||||
KnowledgeAddResult(String),
|
||||
TargetResult(String),
|
||||
ChatStreamChunk(String),
|
||||
ChatStreamDone,
|
||||
}
|
||||
|
||||
type Msg = (String, GuiMsg);
|
||||
@@ -219,6 +229,7 @@ struct AirustGui {
|
||||
server_url: String,
|
||||
mode: AppMode,
|
||||
theme: GuiTheme,
|
||||
theme_cache: egui::Visuals,
|
||||
active_tab: Tab,
|
||||
|
||||
// Chat
|
||||
@@ -226,6 +237,7 @@ struct AirustGui {
|
||||
sessions: Vec<ChatSession>,
|
||||
active_session_id: String,
|
||||
sessions_loaded: bool,
|
||||
chat_streaming: bool,
|
||||
|
||||
// Execute
|
||||
exec_cmd: String,
|
||||
@@ -236,6 +248,7 @@ struct AirustGui {
|
||||
script_timeout: String,
|
||||
script_lang: String,
|
||||
script_results: Vec<ExecResult>,
|
||||
last_detect: Instant,
|
||||
|
||||
// Self-Learn
|
||||
learn_topic: String,
|
||||
@@ -279,6 +292,7 @@ struct AirustGui {
|
||||
// Sidebar
|
||||
sidebar_total: String,
|
||||
sidebar_sources: Vec<(String, u64)>,
|
||||
stats_pending: bool,
|
||||
|
||||
tx: mpsc::Sender<Msg>,
|
||||
rx: mpsc::Receiver<Msg>,
|
||||
@@ -293,10 +307,15 @@ impl AirustGui {
|
||||
fn new(server_url: &str) -> Self {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let session_id = "sess_1".to_string();
|
||||
let ocean = GuiTheme::Ocean;
|
||||
let mut theme_cache = egui::Visuals::dark();
|
||||
ocean.apply_to_visuals(&mut theme_cache);
|
||||
|
||||
Self {
|
||||
server_url: server_url.to_string(),
|
||||
mode: AppMode::Startup,
|
||||
theme: GuiTheme::Ocean,
|
||||
theme_cache,
|
||||
active_tab: Tab::Chat,
|
||||
|
||||
chat_input: String::new(),
|
||||
@@ -307,6 +326,7 @@ impl AirustGui {
|
||||
}],
|
||||
active_session_id: session_id,
|
||||
sessions_loaded: false,
|
||||
chat_streaming: false,
|
||||
|
||||
exec_cmd: String::new(),
|
||||
exec_results: Vec::new(),
|
||||
@@ -315,6 +335,7 @@ impl AirustGui {
|
||||
script_timeout: "30".to_string(),
|
||||
script_lang: String::new(),
|
||||
script_results: Vec::new(),
|
||||
last_detect: Instant::now(),
|
||||
|
||||
learn_topic: String::new(),
|
||||
learn_results: String::new(),
|
||||
@@ -349,6 +370,7 @@ impl AirustGui {
|
||||
|
||||
sidebar_total: "—".to_string(),
|
||||
sidebar_sources: Vec::new(),
|
||||
stats_pending: false,
|
||||
|
||||
tx,
|
||||
rx,
|
||||
@@ -425,16 +447,20 @@ impl AirustGui {
|
||||
self.active_session_id = id;
|
||||
}
|
||||
|
||||
fn load_stats(&self) {
|
||||
fn load_stats(&mut self) {
|
||||
if self.stats_pending { return; }
|
||||
self.stats_pending = true;
|
||||
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(_) => {}
|
||||
}
|
||||
let result = match api_get(&url, "/api/knowledge/stats") {
|
||||
Ok(text) => ("_stats".into(), GuiMsg::StatsResult(text)),
|
||||
Err(_) => return,
|
||||
};
|
||||
let _ = tx.send(result);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn chrono_now_ms() -> u64 {
|
||||
@@ -444,7 +470,8 @@ fn chrono_now_ms() -> u64 {
|
||||
|
||||
impl eframe::App for AirustGui {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.theme.apply(ui.ctx());
|
||||
// Use cached theme visuals — rebuilt only on theme change (see theme combo)
|
||||
ui.ctx().set_visuals(self.theme_cache.clone());
|
||||
|
||||
if !self.sessions_loaded {
|
||||
self.sessions_loaded = true;
|
||||
@@ -459,8 +486,13 @@ impl eframe::App for AirustGui {
|
||||
"_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 });
|
||||
if s.messages.last().map(|m| m.role.as_str()) == Some("assistant") {
|
||||
s.messages.last_mut().unwrap().content = t;
|
||||
} else {
|
||||
s.messages.push(ChatMsg { role: "assistant".into(), content: t });
|
||||
}
|
||||
}
|
||||
self.chat_streaming = false;
|
||||
}
|
||||
"_detect_lang" => self.script_lang = t,
|
||||
"kn_results" => self.kn_results = t,
|
||||
@@ -498,6 +530,7 @@ impl eframe::App for AirustGui {
|
||||
self.studio_output = format!("{} {}\n{}\n{}\n---\n", r.command, status, r.stdout, r.stderr);
|
||||
}
|
||||
GuiMsg::StatsResult(text) => {
|
||||
self.stats_pending = false;
|
||||
if let Ok(v) = serde_json::from_str::<Value>(&text) {
|
||||
self.sidebar_total = v.get("total").map(|t| t.to_string()).unwrap_or_else(|| "—".into());
|
||||
self.sidebar_sources = v.get("sources")
|
||||
@@ -552,6 +585,16 @@ impl eframe::App for AirustGui {
|
||||
self.load_stats();
|
||||
}
|
||||
GuiMsg::TargetResult(text) => self.tgt_status = text,
|
||||
GuiMsg::ChatStreamChunk(text) => {
|
||||
if let Some(s) = self.active_session_mut() {
|
||||
if let Some(last) = s.messages.last_mut() {
|
||||
last.content = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
GuiMsg::ChatStreamDone => {
|
||||
self.chat_streaming = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,7 +603,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(16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,6 +708,7 @@ impl AirustGui {
|
||||
for t in GuiTheme::all() {
|
||||
if ui.selectable_label(*t == self.theme, t.name()).clicked() {
|
||||
self.theme = *t;
|
||||
t.apply_to_visuals(&mut self.theme_cache);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -770,6 +814,7 @@ impl AirustGui {
|
||||
fn ui_chat(&mut self, ui: &mut egui::Ui) {
|
||||
let avail = ui.available_height();
|
||||
let input_h = 40.0;
|
||||
let (_, _, _, _, _, text) = self.theme.colors();
|
||||
|
||||
// Messages
|
||||
egui::ScrollArea::vertical()
|
||||
@@ -779,16 +824,20 @@ impl AirustGui {
|
||||
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),
|
||||
"user" => text.linear_multiply(0.8),
|
||||
"assistant" => self.theme.colors().4,
|
||||
_ => self.theme.colors().5.linear_multiply(0.5),
|
||||
_ => text.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));
|
||||
if msg.content.is_empty() && msg.role == "assistant" && self.chat_streaming {
|
||||
ui.label(RichText::new("...").size(13.0).color(text.linear_multiply(0.5)));
|
||||
} else {
|
||||
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)));
|
||||
ui.label(RichText::new("Start a conversation...").size(12.0).color(text.linear_multiply(0.5)));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -802,22 +851,48 @@ impl AirustGui {
|
||||
egui::TextEdit::singleline(&mut self.chat_input).hint_text("Type your message..."),
|
||||
);
|
||||
let send = ui.button("Send");
|
||||
if send.clicked() || (resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) {
|
||||
let disabled = self.chat_streaming;
|
||||
if !disabled && (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() {
|
||||
if let Some(s) = self.active_session_mut() {
|
||||
s.messages.push(ChatMsg { role: "user".into(), content: msg.clone() });
|
||||
s.messages.push(ChatMsg { role: "assistant".into(), content: String::new() });
|
||||
}
|
||||
self.spawn("_chat_req", move |url| {
|
||||
match api_post(url, "/api/chat/sync", serde_json::json!({"message": msg})) {
|
||||
Ok(r) => {
|
||||
let resp = serde_json::from_str::<Value>(&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))
|
||||
self.chat_streaming = true;
|
||||
let tx = self.tx.clone();
|
||||
let url = self.server_url.clone();
|
||||
thread::spawn(move || {
|
||||
let url_f = format!("{}/api/chat", url);
|
||||
match http_client()
|
||||
.post(&url_f)
|
||||
.json(&serde_json::json!({"message": msg}))
|
||||
.send()
|
||||
{
|
||||
Ok(resp) => {
|
||||
let reader = BufReader::new(resp);
|
||||
let mut reply = String::new();
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
if line.starts_with("data: ") {
|
||||
let data = &line[6..];
|
||||
if data == "[DONE]" { break; }
|
||||
reply.push_str(data);
|
||||
let _ = tx.send(("_chat_stream".into(), GuiMsg::ChatStreamChunk(reply.clone())));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(("_chat_resp".into(), GuiMsg::Set(format!("Error reading stream: {}", e))));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = tx.send(("_chat_stream".into(), GuiMsg::ChatStreamDone));
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(("_chat_resp".into(), GuiMsg::Set(format!("Error: {}", e))));
|
||||
}
|
||||
Err(e) => ("_chat_resp".into(), GuiMsg::Set(format!("Error: {}", e))),
|
||||
}
|
||||
});
|
||||
self.chat_input.clear();
|
||||
@@ -949,8 +1024,9 @@ impl AirustGui {
|
||||
.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() {
|
||||
// Auto-detect language on text change (debounced 500ms)
|
||||
if resp.changed() && self.last_detect.elapsed().as_millis() > 500 {
|
||||
self.last_detect = Instant::now();
|
||||
let code = self.script_code.clone();
|
||||
self.spawn("_detect_lang", move |url| {
|
||||
match api_post(url, "/api/detect-lang", serde_json::json!({"code": code})) {
|
||||
|
||||
Reference in New Issue
Block a user