diff --git a/flomo-ai-desktop/src/config/store.rs b/flomo-ai-desktop/src/config/store.rs index 7c82e97..1722813 100644 --- a/flomo-ai-desktop/src/config/store.rs +++ b/flomo-ai-desktop/src/config/store.rs @@ -17,9 +17,11 @@ pub struct PromptConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LLMConfig { + pub name: String, pub base_url: String, pub api_key: String, pub model: String, + pub enabled: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -36,7 +38,7 @@ pub enum ThemeMode { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppSettings { - pub llm_config: LLMConfig, + pub llm_configs: Vec, pub header_configs: Vec, pub prompt_configs: Vec, pub theme_config: ThemeConfig, @@ -45,11 +47,29 @@ pub struct AppSettings { impl Default for AppSettings { fn default() -> Self { Self { - llm_config: LLMConfig { - base_url: "https://api.openai.com/v1".to_string(), - api_key: String::new(), - model: "gpt-4o".to_string(), - }, + llm_configs: vec![ + LLMConfig { + name: "模型1".to_string(), + base_url: "https://api.openai.com/v1".to_string(), + api_key: String::new(), + model: "gpt-4o".to_string(), + enabled: true, + }, + LLMConfig { + name: "模型2".to_string(), + base_url: "https://api.openai.com/v1".to_string(), + api_key: String::new(), + model: "gpt-4o-mini".to_string(), + enabled: true, + }, + LLMConfig { + name: "模型3".to_string(), + base_url: "https://api.openai.com/v1".to_string(), + api_key: String::new(), + model: "gpt-3.5-turbo".to_string(), + enabled: true, + }, + ], header_configs: Vec::new(), prompt_configs: vec![ PromptConfig { diff --git a/flomo-ai-desktop/src/pages/main_page.rs b/flomo-ai-desktop/src/pages/main_page.rs index 50833ae..2e41565 100644 --- a/flomo-ai-desktop/src/pages/main_page.rs +++ b/flomo-ai-desktop/src/pages/main_page.rs @@ -1,31 +1,33 @@ use egui::{Ui, Color32, RichText, TextEdit, Button, ScrollArea}; +use std::sync::mpsc; use crate::config::AppSettings; -use crate::api::call_llm; +use crate::api::call_single_llm; pub enum OutputStatus { Waiting, Connecting, Completed, Error(String), - Stopped, } pub struct MainPage { pub input_text: String, - pub output_text: String, - pub status: OutputStatus, + pub output_texts: Vec, + pub statuses: Vec, pub selected_prompt_index: usize, - pub is_loading: bool, + pub is_loading: Vec, + pub result_receiver: Option)>>, } impl Default for MainPage { fn default() -> Self { Self { input_text: String::new(), - output_text: String::new(), - status: OutputStatus::Waiting, + output_texts: vec![String::new(), String::new(), String::new()], + statuses: vec![OutputStatus::Waiting, OutputStatus::Waiting, OutputStatus::Waiting], selected_prompt_index: 0, - is_loading: false, + is_loading: vec![false, false, false], + result_receiver: None, } } } @@ -36,11 +38,31 @@ impl MainPage { } pub fn ui(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) { + self.process_results(); + ScrollArea::vertical().show(ui, |ui| { self.render_content(ui, settings, ctx); }); } + fn process_results(&mut self) { + if let Some(ref receiver) = self.result_receiver { + while let Ok((index, result)) = receiver.try_recv() { + self.is_loading[index] = false; + match result { + Ok(text) => { + self.output_texts[index] = text; + self.statuses[index] = OutputStatus::Completed; + } + Err(e) => { + self.output_texts[index] = e.clone(); + self.statuses[index] = OutputStatus::Error(e); + } + } + } + } + } + fn render_content(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) { ui.add_space(16.0); @@ -107,24 +129,34 @@ impl MainPage { ui.label(RichText::new("优化结果").size(11.0).color(Color32::GRAY)); ui.add_space(6.0); - let status_text = match &self.status { + let enabled_models: Vec<_> = settings.llm_configs.iter().filter(|c| c.enabled).collect(); + + ScrollArea::horizontal().show(ui, |ui| { + ui.horizontal(|ui| { + for (idx, model) in enabled_models.iter().enumerate() { + self.render_result_card(ui, idx, &model.name); + } + }); + }); + + ui.add_space(16.0); + } + + fn render_result_card(&mut self, ui: &mut Ui, index: usize, model_name: &str) { + let status_text = match &self.statuses.get(index).unwrap_or(&OutputStatus::Waiting) { OutputStatus::Waiting => "等待发送".to_string(), OutputStatus::Connecting => "连接中…".to_string(), OutputStatus::Completed => "已完成".to_string(), - OutputStatus::Error(_) => "发生错误".to_string(), - OutputStatus::Stopped => "已停止".to_string(), + OutputStatus::Error(e) => format!("错误: {}", e), }; - let status_color = match &self.status { + let status_color = match &self.statuses.get(index).unwrap_or(&OutputStatus::Waiting) { OutputStatus::Waiting => Color32::from_rgb(100, 100, 255), OutputStatus::Connecting => Color32::from_rgb(255, 165, 0), OutputStatus::Completed => Color32::from_rgb(0, 180, 0), OutputStatus::Error(_) => Color32::RED, - OutputStatus::Stopped => Color32::GRAY, }; - ui.label(RichText::new(&status_text).size(11.0).color(status_color)); - let card_frame = egui::Frame::none() .fill(ui.style().visuals.widgets.inactive.bg_fill) .stroke(egui::Stroke::new(1.0, Color32::from_rgb(200, 200, 210))) @@ -132,45 +164,71 @@ impl MainPage { .rounding(6.0); card_frame.show(ui, |ui: &mut Ui| { - ui.add_sized([ui.available_width(), 2.0], egui::Separator::default()); - ui.add_space(8.0); + ui.add_sized([300.0, 0.0], |ui: &mut Ui| { + ui.set_width(300.0); + + ui.label(RichText::new(model_name).size(14.0).strong()); + ui.add_space(4.0); + + ui.label(RichText::new(&status_text).size(11.0).color(status_color)); + ui.add_space(8.0); + + ui.add_sized([300.0, 2.0], egui::Separator::default()); + ui.add_space(8.0); - if self.output_text.is_empty() { - ui.label(RichText::new("发送消息后结果将在此显示").size(13.0).color(Color32::GRAY)); - } else { - ui.add_sized( - [ui.available_width(), 150.0], - TextEdit::multiline(&mut self.output_text.clone()) - .desired_width(f32::INFINITY) - .desired_rows(6), - ); - } - - ui.add_space(8.0); - - ui.horizontal(|ui: &mut Ui| { - let copy_btn = Button::new("复制结果") - .fill(Color32::TRANSPARENT) - .stroke(egui::Stroke::new(1.0, Color32::from_rgb(100, 100, 255))) - .rounding(4.0); - - if ui.add(copy_btn).clicked() && !self.output_text.is_empty() { - self.copy_to_clipboard(); + let output_text = self.output_texts.get(index).map(|s| s.as_str()).unwrap_or(""); + + if output_text.is_empty() { + ui.label(RichText::new("发送消息后结果将在此显示").size(13.0).color(Color32::GRAY)); + } else { + ui.add_sized( + [300.0, 150.0], + TextEdit::multiline(&mut self.output_texts[index].clone()) + .desired_width(300.0) + .desired_rows(6), + ); } + + ui.add_space(8.0); + + ui.horizontal(|ui: &mut Ui| { + let copy_btn = Button::new("复制结果") + .fill(Color32::TRANSPARENT) + .stroke(egui::Stroke::new(1.0, Color32::from_rgb(100, 100, 255))) + .rounding(4.0); + + if ui.add(copy_btn).clicked() && !output_text.is_empty() { + if let Ok(mut clipboard) = arboard::Clipboard::new() { + let _ = clipboard.set_text(output_text); + } + } + }); + + ui }); }); - ui.add_space(16.0); + ui.add_space(8.0); } pub fn send_request(&mut self, settings: &AppSettings, ctx: egui::Context) { - if self.input_text.trim().is_empty() || self.is_loading { + if self.input_text.trim().is_empty() { return; } - self.status = OutputStatus::Connecting; - self.output_text = "正在生成...".to_string(); - self.is_loading = true; + let enabled_models: Vec<_> = settings.llm_configs.iter().filter(|c| c.enabled).collect(); + if enabled_models.is_empty() { + return; + } + + for i in 0..self.statuses.len() { + self.statuses[i] = OutputStatus::Connecting; + self.output_texts[i] = "正在生成...".to_string(); + self.is_loading[i] = true; + } + + let (tx, rx) = mpsc::channel(); + self.result_receiver = Some(rx); let prompt = if self.selected_prompt_index == 0 { None @@ -181,17 +239,22 @@ impl MainPage { }; let input = self.input_text.clone(); - let settings = settings.clone(); + let header_configs = settings.header_configs.clone(); - let handle = std::thread::spawn(move || { - let result = call_llm(&settings, input, prompt); - ctx.request_repaint(); - result - }); + for (idx, model) in enabled_models.iter().enumerate() { + let input = input.clone(); + let prompt = prompt.clone(); + let header_configs = header_configs.clone(); + let model = model.clone(); + let ctx = ctx.clone(); + let tx = tx.clone(); - std::thread::spawn(move || { - let _ = handle.join(); - }); + std::thread::spawn(move || { + let result = call_single_llm(&model, input, prompt, &header_configs); + let _ = tx.send((idx, result)); + ctx.request_repaint(); + }); + } } fn select_prompt_by_name(&mut self, name: &str, settings: &AppSettings, ctx: &egui::Context) { @@ -208,10 +271,4 @@ impl MainPage { self.send_request(settings, ctx.clone()); } } - - fn copy_to_clipboard(&self) { - if let Ok(mut clipboard) = arboard::Clipboard::new() { - let _ = clipboard.set_text(&self.output_text); - } - } }