From ac1baa9b0fc3c4f542bdfa36abd92799959a73b9 Mon Sep 17 00:00:00 2001 From: xiaji Date: Sat, 9 May 2026 22:33:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96Rust=E6=A1=8C=E9=9D=A2?= =?UTF-8?q?=E5=BA=94=E7=94=A8UI=EF=BC=9A3=E5=8D=A1=E7=89=87=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E3=80=81=E4=B8=AD=E6=96=87=E5=AD=97=E4=BD=93=E3=80=81?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=8F=B0=E9=9A=90=E8=97=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flomo-ai-desktop/Cargo.toml | 3 + flomo-ai-desktop/src/app.rs | 231 ++++++++++++++++++++---------------- 2 files changed, 131 insertions(+), 103 deletions(-) diff --git a/flomo-ai-desktop/Cargo.toml b/flomo-ai-desktop/Cargo.toml index eb4c0e5..6edeaf2 100644 --- a/flomo-ai-desktop/Cargo.toml +++ b/flomo-ai-desktop/Cargo.toml @@ -22,3 +22,6 @@ lto = true codegen-units = 1 panic = "abort" strip = true + +[target.x86_64-pc-windows-gnu] +rustflags = ["-C", "link-args=-Wl,--subsystem,windows"] diff --git a/flomo-ai-desktop/src/app.rs b/flomo-ai-desktop/src/app.rs index 1fa211d..3ce23cd 100644 --- a/flomo-ai-desktop/src/app.rs +++ b/flomo-ai-desktop/src/app.rs @@ -31,14 +31,15 @@ pub struct FlomoAiApp { input_text: String, selected_prompt_index: usize, char_count: usize, - + model_displays: Vec, pending_results: Option>>, - + pending_tests: Vec<(usize, std::thread::JoinHandle>)>, + settings_selected_theme: ThemeMode, new_prompt_title: String, new_prompt_content: String, - + current_page: Page, theme_dirty: bool, } @@ -65,6 +66,7 @@ impl FlomoAiApp { char_count: 0, model_displays, pending_results: None, + pending_tests: Vec::new(), settings_selected_theme: ThemeMode::Light, new_prompt_title: String::new(), new_prompt_content: String::new(), @@ -73,6 +75,28 @@ impl FlomoAiApp { } } + fn poll_test_results(&mut self, ctx: &egui::Context) { + for (index, handle) in self.pending_tests.drain(..) { + match handle.join() { + Ok(Ok(response)) => { + let msg = if response.len() > 30 { + format!("成功: {}...", &response[..30]) + } else { + format!("成功: {}", response) + }; + self.model_displays[index].test_status = msg; + } + Ok(Err(e)) => { + self.model_displays[index].test_status = format!("失败: {}", e); + } + Err(_) => { + self.model_displays[index].test_status = "测试线程错误".to_string(); + } + } + ctx.request_repaint(); + } + } + fn poll_results(&mut self, ctx: &egui::Context) { if let Some(handle) = self.pending_results.take() { if handle.is_finished() { @@ -184,6 +208,7 @@ impl FlomoAiApp { impl eframe::App for FlomoAiApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.poll_results(ctx); + self.poll_test_results(ctx); if self.theme_dirty { let theme = AppTheme::from_mode(self.settings.theme_config.mode); @@ -287,79 +312,77 @@ impl FlomoAiApp { ui.label(egui::RichText::new("各模型返回结果").size(11.0).color(egui::Color32::GRAY)); ui.add_space(6.0); - let available_width = ui.available_width(); - let column_width = (available_width - 16.0) / 3.0; + let total_width = ui.available_width(); + let spacing = 8.0; + let column_width = (total_width - spacing * 2.0) / 3.0; - ui.horizontal(|ui| { - ui.set_height(200.0); - for (i, display) in self.model_displays.iter().enumerate() { - if i > 0 { - ui.add_space(8.0); - } - - ui.set_min_width(column_width); - + ui.horizontal_wrapped(|ui| { + ui.set_width(total_width); + + for d in self.model_displays.iter() { egui::Frame::none() - .fill(ui.style().visuals.widgets.inactive.bg_fill) + .fill(if d.enabled { + ui.style().visuals.widgets.inactive.bg_fill + } else { + egui::Color32::from_rgb(235, 235, 240) + }) .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(180, 180, 200))) .rounding(8.0) - .inner_margin(egui::Margin::same(10.0)) + .inner_margin(egui::Margin::same(8.0)) .show(ui, |ui| { - ui.set_min_width(column_width - 20.0); + ui.set_min_width(column_width - 16.0); - let enabled_color = if display.enabled { + let enabled_color = if d.enabled { egui::Color32::from_rgb(80, 80, 220) } else { egui::Color32::GRAY }; - - let enabled_dot = if display.enabled { "●" } else { "○" }; - ui.label(egui::RichText::new(format!("{} {}", enabled_dot, display.name)) + + let enabled_dot = if d.enabled { "●" } else { "○" }; + ui.label(egui::RichText::new(format!("{} {}", enabled_dot, d.name)) .size(13.0).strong().color(enabled_color)); - - ui.label(egui::RichText::new(&display.model).size(10.0).color(egui::Color32::GRAY)); - - ui.add_space(6.0); + + ui.label(egui::RichText::new(&d.model).size(10.0).color(egui::Color32::GRAY)); + + ui.add_space(4.0); ui.add_sized([ui.available_width(), 1.0], egui::Separator::default()); - - ui.add_space(6.0); - - let status_color = match &display.status { + + let status_color = match &d.status { ModelStatus::Waiting => egui::Color32::GRAY, ModelStatus::Loading => egui::Color32::from_rgb(255, 140, 0), ModelStatus::Completed => egui::Color32::from_rgb(0, 160, 0), ModelStatus::Error(_) => egui::Color32::RED, }; - - ui.label(egui::RichText::new(match &display.status { + + ui.label(egui::RichText::new(match &d.status { ModelStatus::Waiting => "● 就绪", ModelStatus::Loading => "◐ 生成中...", ModelStatus::Completed => "✓ 完成", ModelStatus::Error(_) => "✗ 错误", }).size(10.0).color(status_color)); - + ui.add_space(4.0); - - if display.result.is_empty() { + + if d.result.is_empty() { ui.label(egui::RichText::new("等待结果...").size(11.0).color(egui::Color32::GRAY)); } else { - let mut result_text = display.result.clone(); + let mut result_text = d.result.clone(); ui.add_sized( - [ui.available_width(), 120.0], + [ui.available_width(), 100.0], egui::TextEdit::multiline(&mut result_text) - .desired_rows(5) + .desired_rows(4) .frame(false), ); } - - ui.add_space(6.0); - - ui.horizontal(|ui| { - if ui.small_button("复制").clicked() && !display.result.is_empty() { - self.copy_to_clipboard(&display.result); - } - }); + + ui.add_space(4.0); + + if ui.small_button("复制").clicked() && !d.result.is_empty() { + self.copy_to_clipboard(&d.result); + } }); + + ui.add_space(spacing); } }); }); @@ -380,75 +403,77 @@ impl FlomoAiApp { ScrollArea::vertical().show(ui, |ui| { for i in 0..self.model_displays.len() { - let enabled = self.model_displays[i].enabled; - let name = self.model_displays[i].name.clone(); - let model = self.model_displays[i].model.clone(); - ui.add_space(8.0); - + ui.horizontal(|ui| { - self.model_displays[i].enabled ^= ui.checkbox(&mut self.model_displays[i].enabled, "").changed(); - ui.label(egui::RichText::new(&format!("模型 {} 配置", i + 1)).size(14.0).strong()); + ui.checkbox(&mut self.model_displays[i].enabled, ""); + let label = if self.model_displays[i].enabled { + format!("{} ({})", self.model_displays[i].name, self.model_displays[i].model) + } else { + format!("{} (已禁用)", self.model_displays[i].name) + }; + ui.label(egui::RichText::new(&label).size(14.0).strong()); }); - - if !enabled { - ui.label(egui::RichText::new("已禁用").size(11.0).color(egui::Color32::GRAY)); - } else { - ui.label("名称"); - ui.text_edit_singleline(&mut self.model_displays[i].name); - - ui.add_space(4.0); - - let mut base_url = self.settings.llm_configs.models.get(i) - .map(|m| m.base_url.clone()) - .unwrap_or_default(); - ui.label("API Base URL"); - ui.text_edit_singleline(&mut base_url); - if i < self.settings.llm_configs.models.len() { - self.settings.llm_configs.models[i].base_url = base_url.clone(); - } - - ui.add_space(4.0); - - let mut api_key = self.settings.llm_configs.models.get(i) - .map(|m| m.api_key.clone()) - .unwrap_or_default(); - ui.label("API Key"); - ui.text_edit_singleline(&mut api_key); - if i < self.settings.llm_configs.models.len() { - self.settings.llm_configs.models[i].api_key = api_key.clone(); - } - - ui.add_space(4.0); - - ui.label("Model"); - ui.text_edit_singleline(&mut self.model_displays[i].model); - - ui.add_space(6.0); - - let test_status = self.model_displays[i].test_status.clone(); - let test_btn = egui::Button::new("测试") - .fill(egui::Color32::from_rgb(100, 100, 255)) - .rounding(4.0) - .min_size(egui::vec2(60.0, 28.0)); - + + ui.add_space(4.0); + + ui.label("名称"); + ui.text_edit_singleline(&mut self.model_displays[i].name); + + ui.add_space(4.0); + + let mut base_url = self.settings.llm_configs.models.get(i) + .map(|m| m.base_url.clone()) + .unwrap_or_default(); + ui.label("API Base URL"); + ui.text_edit_singleline(&mut base_url); + if i < self.settings.llm_configs.models.len() { + self.settings.llm_configs.models[i].base_url = base_url.clone(); + } + + ui.add_space(4.0); + + let mut api_key = self.settings.llm_configs.models.get(i) + .map(|m| m.api_key.clone()) + .unwrap_or_default(); + ui.label("API Key"); + ui.text_edit_singleline(&mut api_key); + if i < self.settings.llm_configs.models.len() { + self.settings.llm_configs.models[i].api_key = api_key.clone(); + } + + ui.add_space(4.0); + + ui.label("Model"); + ui.text_edit_singleline(&mut self.model_displays[i].model); + if i < self.settings.llm_configs.models.len() { + self.settings.llm_configs.models[i].model = self.model_displays[i].model.clone(); + } + + ui.add_space(6.0); + + let test_status = self.model_displays[i].test_status.clone(); + let test_btn = egui::Button::new("测试") + .fill(egui::Color32::from_rgb(100, 100, 255)) + .rounding(4.0) + .min_size(egui::vec2(60.0, 28.0)); + + ui.horizontal(|ui| { if ui.add(test_btn).clicked() { if let Some(config) = self.settings.llm_configs.models.get(i).cloned() { self.model_displays[i].test_status = "测试中...".to_string(); let headers = self.settings.header_configs.clone(); - let ctx_clone = ctx.clone(); - let display_index = i; - - std::thread::spawn(move || { - let result = test_single_llm(&config, &headers); - ctx_clone.request_repaint(); + + let handle = std::thread::spawn(move || { + test_single_llm(&config, &headers) }); + self.pending_tests.push((i, handle)); } } - + ui.label(egui::RichText::new(&test_status).size(11.0).color(egui::Color32::GRAY)); - } - + }); + if i < self.model_displays.len() - 1 { ui.separator(); }