From b0daad7f362e83f3deba6d64ae1145fb5b7297f8 Mon Sep 17 00:00:00 2001 From: xiaji Date: Sat, 9 May 2026 10:20:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=9ALLM=E6=94=AF=E6=8C=81=20-=203?= =?UTF-8?q?=E4=B8=AA=E6=A8=A1=E5=9E=8B=E5=B9=B6=E5=8F=91=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flomo-ai-desktop/Cargo.toml | 7 +- flomo-ai-desktop/build.bat | 12 + flomo-ai-desktop/src/api/llm_client.rs | 98 ++- flomo-ai-desktop/src/app.rs | 728 +++++++++----------- flomo-ai-desktop/src/config/store.rs | 45 +- flomo-ai-desktop/src/main.rs | 2 +- flomo-ai-desktop/src/pages/main_page.rs | 217 ------ flomo-ai-desktop/src/pages/mod.rs | 4 - flomo-ai-desktop/src/pages/settings_page.rs | 154 ----- 9 files changed, 468 insertions(+), 799 deletions(-) create mode 100644 flomo-ai-desktop/build.bat delete mode 100644 flomo-ai-desktop/src/pages/main_page.rs delete mode 100644 flomo-ai-desktop/src/pages/settings_page.rs diff --git a/flomo-ai-desktop/Cargo.toml b/flomo-ai-desktop/Cargo.toml index cae4b4e..eb4c0e5 100644 --- a/flomo-ai-desktop/Cargo.toml +++ b/flomo-ai-desktop/Cargo.toml @@ -8,13 +8,12 @@ name = "flomo-ai" path = "src/main.rs" [dependencies] -egui = "0.29" -eframe = { version = "0.29", default-features = false, features = ["default_fonts", "glow"] } +egui = "0.30" +eframe = { version = "0.30", default-features = false, features = ["default_fonts", "glow"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } +reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "native-tls"] } dirs = "5" -clipboard-win = "5" arboard = "3" [profile.release] diff --git a/flomo-ai-desktop/build.bat b/flomo-ai-desktop/build.bat new file mode 100644 index 0000000..5bff704 --- /dev/null +++ b/flomo-ai-desktop/build.bat @@ -0,0 +1,12 @@ +@echo off +setlocal + +set PATH=C:\msys64\mingw64\bin;C:\msys64\usr\bin;%PATH% + +set CC=gcc +set CXX=g++ +set AR=ar +set LD=ld +set STRIP=strip + +cargo build --release --target x86_64-pc-windows-gnu \ No newline at end of file diff --git a/flomo-ai-desktop/src/api/llm_client.rs b/flomo-ai-desktop/src/api/llm_client.rs index 31bd603..c49ea58 100644 --- a/flomo-ai-desktop/src/api/llm_client.rs +++ b/flomo-ai-desktop/src/api/llm_client.rs @@ -1,5 +1,7 @@ -use crate::config::AppSettings; +use crate::config::{AppSettings, LLMConfig, HeaderConfig}; use serde::{Deserialize, Serialize}; +use std::sync::mpsc; +use std::thread; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChatMessage { @@ -23,24 +25,20 @@ pub struct ChatCompletionResponse { pub choices: Vec, } -pub fn call_llm(settings: &AppSettings, user_input: String, selected_prompt: Option) -> Result { - let full_content = if let Some(prompt) = selected_prompt { - if !prompt.is_empty() { - format!("{}{}", prompt, user_input) - } else { - user_input - } - } else { - user_input - }; +#[derive(Debug, Clone)] +pub struct ModelResult { + pub name: String, + pub response: Result, +} +pub fn call_single_llm(config: &LLMConfig, user_input: String, header_configs: &[HeaderConfig]) -> Result { let messages = vec![ChatMessage { role: "user".to_string(), - content: full_content, + content: user_input, }]; let request = ChatCompletionRequest { - model: settings.llm_config.model.clone(), + model: config.model.clone(), messages, }; @@ -50,14 +48,14 @@ pub fn call_llm(settings: &AppSettings, user_input: String, selected_prompt: Opt .map_err(|e| format!("Failed to create HTTP client: {}", e))?; let mut req_builder = client - .post(format!("{}/chat/completions", settings.llm_config.base_url)) + .post(format!("{}/chat/completions", config.base_url)) .header("Content-Type", "application/json"); - if !settings.llm_config.api_key.is_empty() { - req_builder = req_builder.header("Authorization", format!("Bearer {}", settings.llm_config.api_key)); + if !config.api_key.is_empty() { + req_builder = req_builder.header("Authorization", format!("Bearer {}", config.api_key)); } - for header in &settings.header_configs { + for header in header_configs { if !header.key.is_empty() { req_builder = req_builder.header(&header.key, &header.value); } @@ -90,3 +88,69 @@ pub fn call_llm(settings: &AppSettings, user_input: String, selected_prompt: Opt Ok(completion.choices[0].message.content.clone()) } + +pub fn call_all_llms( + settings: &AppSettings, + user_input: String, + selected_prompt: Option, +) -> Vec { + let enabled_models: Vec<_> = settings.llm_configs.models.iter() + .filter(|m| m.enabled && !m.model.is_empty() && !m.base_url.is_empty()) + .cloned() + .collect(); + + if enabled_models.is_empty() { + return vec![ModelResult { + name: "错误".to_string(), + response: Err("没有启用的模型".to_string()), + }]; + } + + let full_content = if let Some(prompt) = selected_prompt { + if !prompt.is_empty() { + format!("{}{}", prompt, user_input) + } else { + user_input + } + } else { + user_input + }; + + let (tx, rx) = mpsc::channel::(); + let headers = settings.header_configs.clone(); + + for model in enabled_models { + let input = full_content.clone(); + let tx = tx.clone(); + let model_name = model.name.clone(); + let headers = headers.clone(); + + thread::spawn(move || { + let result = call_single_llm(&model, input, &headers); + let _ = tx.send(ModelResult { + name: model_name, + response: result, + }); + }); + } + + drop(tx); + + let mut results = Vec::new(); + for result in rx { + results.push(result); + } + + results +} + +pub fn test_single_llm(config: &LLMConfig, header_configs: &[HeaderConfig]) -> Result { + if config.model.is_empty() { + return Err("模型未配置".to_string()); + } + if config.base_url.is_empty() { + return Err("Base URL未配置".to_string()); + } + + call_single_llm(config, "你好,请回复OK".to_string(), header_configs) +} \ No newline at end of file diff --git a/flomo-ai-desktop/src/app.rs b/flomo-ai-desktop/src/app.rs index 23416d4..adbe723 100644 --- a/flomo-ai-desktop/src/app.rs +++ b/flomo-ai-desktop/src/app.rs @@ -1,35 +1,44 @@ -use eframe::egui; -use crate::config::{AppSettings, load_settings, save_settings}; +use eframe::egui::{self, ScrollArea}; +use crate::config::{AppSettings, load_settings, save_settings, ThemeMode}; use crate::theme::AppTheme; -use crate::pages::OutputStatus; +use crate::api::{ModelResult, call_all_llms, test_single_llm}; enum Page { Main, Settings, } +#[derive(Clone)] +struct ModelDisplay { + enabled: bool, + name: String, + model: String, + result: String, + status: ModelStatus, + test_status: String, +} + +#[derive(Clone)] +enum ModelStatus { + Waiting, + Loading, + Completed, + Error(String), +} + pub struct FlomoAiApp { settings: AppSettings, input_text: String, - output_text: String, - status: OutputStatus, selected_prompt_index: usize, - is_loading: bool, char_count: usize, - pending_response: Option>>, - - settings_base_url: String, - settings_api_key: String, - settings_model: String, - settings_show_api_key: bool, - settings_selected_theme: crate::config::ThemeMode, + + model_displays: Vec, + pending_results: Option>>, + + settings_selected_theme: ThemeMode, new_prompt_title: String, new_prompt_content: String, - - test_status: String, - test_is_loading: bool, - pending_test: Option>>, - + current_page: Page, theme_dirty: bool, } @@ -40,91 +49,75 @@ impl FlomoAiApp { let theme = AppTheme::from_mode(settings.theme_config.mode); cc.egui_ctx.set_visuals(theme.visuals.clone()); - Self { - settings: settings.clone(), - input_text: String::new(), - output_text: String::new(), - status: OutputStatus::Waiting, - selected_prompt_index: 0, - is_loading: false, - char_count: 0, - pending_response: None, + let model_displays: Vec = settings.llm_configs.models.iter().map(|m| ModelDisplay { + enabled: m.enabled, + name: m.name.clone(), + model: m.model.clone(), + result: String::new(), + status: ModelStatus::Waiting, + test_status: "未测试".to_string(), + }).collect(); - settings_base_url: settings.llm_config.base_url, - settings_api_key: settings.llm_config.api_key, - settings_model: settings.llm_config.model, - settings_show_api_key: false, - settings_selected_theme: settings.theme_config.mode, + Self { + settings, + input_text: String::new(), + selected_prompt_index: 0, + char_count: 0, + model_displays, + pending_results: None, + settings_selected_theme: ThemeMode::Light, new_prompt_title: String::new(), new_prompt_content: String::new(), - - test_status: "点击测试连接".to_string(), - test_is_loading: false, - pending_test: None, - current_page: Page::Main, theme_dirty: false, } } - fn poll_response(&mut self, ctx: &egui::Context) { - if let Some(handle) = self.pending_response.take() { + fn poll_results(&mut self, ctx: &egui::Context) { + if let Some(handle) = self.pending_results.take() { if handle.is_finished() { match handle.join() { - Ok(Ok(text)) => { - self.output_text = text; - self.status = OutputStatus::Completed; - self.is_loading = false; - } - Ok(Err(e)) => { - self.output_text = format!("错误: {}", e); - self.status = OutputStatus::Error(e); - self.is_loading = false; + Ok(results) => { + for result in results { + for display in &mut self.model_displays { + if display.name == result.name { + match result.response { + Ok(text) => { + display.result = text; + display.status = ModelStatus::Completed; + } + Err(e) => { + display.result = format!("错误: {}", e); + display.status = ModelStatus::Error(e); + } + } + break; + } + } + } } Err(_) => { - self.output_text = "线程错误".to_string(); - self.status = OutputStatus::Error("Thread panic".to_string()); - self.is_loading = false; + for display in &mut self.model_displays { + if matches!(display.status, ModelStatus::Loading) { + display.status = ModelStatus::Error("线程错误".to_string()); + display.result = "线程错误".to_string(); + } + } } } - } else { - self.pending_response = Some(handle); ctx.request_repaint(); - } - } - - if let Some(handle) = self.pending_test.take() { - if handle.is_finished() { - match handle.join() { - Ok(Ok(text)) => { - self.test_status = format!("连接成功: {}", text.chars().take(30).collect::()); - self.test_is_loading = false; - } - Ok(Err(e)) => { - self.test_status = format!("连接失败: {}", e); - self.test_is_loading = false; - } - Err(_) => { - self.test_status = "测试线程错误".to_string(); - self.test_is_loading = false; - } - } } else { - self.pending_test = Some(handle); + self.pending_results = Some(handle); ctx.request_repaint(); } } } fn send_request(&mut self, 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 prompt = if self.selected_prompt_index == 0 { None } else if self.selected_prompt_index <= self.settings.prompt_configs.len() { @@ -133,84 +126,64 @@ impl FlomoAiApp { None }; + for display in &mut self.model_displays { + if display.enabled { + display.status = ModelStatus::Loading; + display.result = "生成中...".to_string(); + } else { + display.status = ModelStatus::Waiting; + display.result = String::new(); + } + } + let input = self.input_text.clone(); let settings = self.settings.clone(); let ctx_clone = ctx.clone(); let handle = std::thread::spawn(move || { - let result = crate::api::call_llm(&settings, input, prompt); + let results = call_all_llms(&settings, input, prompt); ctx_clone.request_repaint(); - result + results }); - self.pending_response = Some(handle); + self.pending_results = Some(handle); } fn select_prompt_by_name(&mut self, name: &str, ctx: &egui::Context) { - let mut found = false; for (i, prompt) in self.settings.prompt_configs.iter().enumerate() { if prompt.title == name { self.selected_prompt_index = i + 1; - found = true; break; } } - if !self.input_text.is_empty() && found { + if !self.input_text.is_empty() { self.send_request(ctx); } } - fn copy_to_clipboard(&self) { + fn copy_to_clipboard(&self, text: &str) { if let Ok(mut clipboard) = arboard::Clipboard::new() { - let _ = clipboard.set_text(&self.output_text); + let _ = clipboard.set_text(text); } } fn save_settings(&mut self) { - self.settings.llm_config.base_url = self.settings_base_url.clone(); - self.settings.llm_config.api_key = self.settings_api_key.clone(); - self.settings.llm_config.model = self.settings_model.clone(); + for (i, display) in self.model_displays.iter().enumerate() { + if i < self.settings.llm_configs.models.len() { + self.settings.llm_configs.models[i].enabled = display.enabled; + self.settings.llm_configs.models[i].name = display.name.clone(); + self.settings.llm_configs.models[i].model = display.model.clone(); + } + } self.settings.theme_config.mode = self.settings_selected_theme; let _ = save_settings(&self.settings); } - - fn test_connection(&mut self, ctx: &egui::Context) { - if self.test_is_loading { - return; - } - - if self.settings_base_url.is_empty() { - self.test_status = "错误: Base URL 不能为空".to_string(); - return; - } - - self.test_status = "测试中...".to_string(); - self.test_is_loading = true; - - let settings = self.settings.clone(); - let ctx_clone = ctx.clone(); - - let handle = std::thread::spawn(move || { - let mut test_settings = settings.clone(); - test_settings.llm_config.model = if settings.llm_config.model.is_empty() { - "gpt-4o".to_string() - } else { - settings.llm_config.model.clone() - }; - - let result = crate::api::call_llm(&test_settings, "你好,请回复OK".to_string(), None); - ctx_clone.request_repaint(); - result - }); - - self.pending_test = Some(handle); - } } impl eframe::App for FlomoAiApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - self.poll_response(ctx); + self.poll_results(ctx); if self.theme_dirty { let theme = AppTheme::from_mode(self.settings.theme_config.mode); @@ -229,22 +202,10 @@ impl FlomoAiApp { fn render_main(&mut self, ctx: &egui::Context) { egui::CentralPanel::default().show(ctx, |ui| { ui.horizontal(|ui| { - ui.vertical(|ui| { - ui.label(egui::RichText::new("AI优化").size(18.0).strong()); - let model_display = if self.settings.llm_config.model.is_empty() { - "未配置模型" - } else { - &self.settings.llm_config.model - }; - ui.label(egui::RichText::new(model_display).size(11.0).color(egui::Color32::from_rgb(100, 100, 255))); - }); + ui.label(egui::RichText::new("AI优化").size(18.0).strong()); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui.button("配置").clicked() { - self.settings_base_url = self.settings.llm_config.base_url.clone(); - self.settings_api_key = self.settings.llm_config.api_key.clone(); - self.settings_model = self.settings.llm_config.model.clone(); self.settings_selected_theme = self.settings.theme_config.mode; - self.settings_show_api_key = false; self.current_page = Page::Settings; } }); @@ -252,46 +213,35 @@ impl FlomoAiApp { ui.separator(); - // 提示词选择区:左侧标签+下拉框,右侧快捷按钮 + ui.label(egui::RichText::new("提示词").size(11.0).color(egui::Color32::GRAY)); + ui.add_space(4.0); + ui.horizontal(|ui| { - ui.vertical(|ui| { - ui.label(egui::RichText::new("提示词").size(11.0).color(egui::Color32::GRAY)); - ui.add_space(4.0); + let selected_text = if self.selected_prompt_index == 0 { + "无系统提示词".to_string() + } else { + self.settings.prompt_configs.get(self.selected_prompt_index - 1) + .map(|p| p.title.clone()) + .unwrap_or_else(|| "无系统提示词".to_string()) + }; - let selected_text = if self.selected_prompt_index == 0 { - "无系统提示词".to_string() - } else if self.selected_prompt_index <= self.settings.prompt_configs.len() { - self.settings.prompt_configs[self.selected_prompt_index - 1].title.clone() - } else { - "无系统提示词".to_string() - }; - - egui::ComboBox::from_id_salt("prompt_selector") - .selected_text(&selected_text) - .width(180.0) - .show_ui(ui, |ui| { - ui.selectable_value(&mut self.selected_prompt_index, 0, "无系统提示词"); - for (i, prompt) in self.settings.prompt_configs.iter().enumerate() { - ui.selectable_value(&mut self.selected_prompt_index, i + 1, &prompt.title); - } - }); - }); + egui::ComboBox::from_id_salt("prompt_selector") + .selected_text(&selected_text) + .width(180.0) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.selected_prompt_index, 0, "无系统提示词"); + for (i, prompt) in self.settings.prompt_configs.iter().enumerate() { + ui.selectable_value(&mut self.selected_prompt_index, i + 1, &prompt.title); + } + }); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let buttons = vec![ - ("🔍", "检查错别字"), - ("📋", "总结"), - ("🌐", "翻译"), - ("✨", "润色"), - ]; - - for (emoji, name) in buttons { + for (emoji, name) in [("🔍", "检查错别字"), ("📋", "总结"), ("🌐", "翻译"), ("✨", "润色")] { let btn = egui::Button::new(egui::RichText::new(emoji).size(16.0)) .fill(ui.style().visuals.widgets.inactive.bg_fill) .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(210, 210, 220))) .rounding(6.0) .min_size(egui::vec2(36.0, 36.0)); - if ui.add(btn).on_hover_text(name).clicked() { self.select_prompt_by_name(name, ctx); } @@ -301,40 +251,7 @@ impl FlomoAiApp { ui.add_space(14.0); - // 提示词详情:显示名称和内容 - let prompt_name = if self.selected_prompt_index == 0 { - "无系统提示词".to_string() - } else if self.selected_prompt_index <= self.settings.prompt_configs.len() { - self.settings.prompt_configs[self.selected_prompt_index - 1].title.clone() - } else { - "无系统提示词".to_string() - }; - - let prompt_content = if self.selected_prompt_index == 0 { - "无特殊指令".to_string() - } else if self.selected_prompt_index <= self.settings.prompt_configs.len() { - self.settings.prompt_configs[self.selected_prompt_index - 1].content.clone() - } else { - "无特殊指令".to_string() - }; - - egui::Frame::none() - .fill(ui.style().visuals.widgets.inactive.bg_fill) - .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(210, 210, 220))) - .rounding(6.0) - .inner_margin(egui::Margin::same(12.0)) - .show(ui, |ui: &mut egui::Ui| { - ui.label(egui::RichText::new(&prompt_name).size(13.0).strong()); - ui.add_space(6.0); - ui.add_sized([ui.available_width(), 1.0], egui::Separator::default()); - ui.add_space(6.0); - ui.label(egui::RichText::new(&prompt_content).size(11.0).color(egui::Color32::GRAY)); - }); - - ui.add_space(14.0); - - // 大模型返回结果 - ui.label(egui::RichText::new("大模型返回结果").size(11.0).color(egui::Color32::GRAY)); + ui.label(egui::RichText::new("输入内容").size(11.0).color(egui::Color32::GRAY)); ui.add_space(4.0); egui::Frame::none() @@ -346,94 +263,106 @@ impl FlomoAiApp { ui.add( egui::TextEdit::multiline(&mut self.input_text) .desired_width(f32::INFINITY) - .desired_rows(4) + .desired_rows(3) .hint_text("输入待发送内容…"), ); }); self.char_count = self.input_text.chars().count(); ui.label(format!("{}/4000", self.char_count)); - ui.add_space(8.0); ui.horizontal(|ui| { - if self.is_loading { - if ui.button("停止生成").clicked() { - self.status = OutputStatus::Stopped; - self.is_loading = false; - self.pending_response = None; - } + let send_btn = egui::Button::new("发送") + .fill(egui::Color32::from_rgb(100, 100, 255)) + .rounding(6.0) + .min_size(egui::vec2(80.0, 36.0)); + + if ui.add(send_btn).clicked() && !self.input_text.trim().is_empty() { + self.send_request(ctx); } - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let send_btn = egui::Button::new(egui::RichText::new("➤").size(16.0)) - .fill(egui::Color32::from_rgb(100, 100, 255)) - .rounding(6.0) - .min_size(egui::vec2(42.0, 42.0)); - - if ui.add(send_btn).clicked() && !self.is_loading && !self.input_text.trim().is_empty() { - self.send_request(ctx); - } - }); }); ui.add_space(14.0); - ui.label(egui::RichText::new("大模型返回结果").size(11.0).color(egui::Color32::GRAY)); + ui.label(egui::RichText::new("各模型返回结果").size(11.0).color(egui::Color32::GRAY)); ui.add_space(6.0); - let status_text = match &self.status { - OutputStatus::Waiting => "等待发送".to_string(), - OutputStatus::Connecting => "连接中…".to_string(), - OutputStatus::Completed => "已完成".to_string(), - OutputStatus::Error(_) => "发生错误".to_string(), - OutputStatus::Stopped => "已停止".to_string(), - }; + let available_width = ui.available_width(); + let column_width = (available_width - 10.0) / 3.0; - let status_color = match &self.status { - OutputStatus::Waiting => egui::Color32::from_rgb(100, 100, 255), - OutputStatus::Connecting => egui::Color32::from_rgb(255, 165, 0), - OutputStatus::Completed => egui::Color32::from_rgb(0, 180, 0), - OutputStatus::Error(_) => egui::Color32::RED, - OutputStatus::Stopped => egui::Color32::GRAY, - }; - - ui.label(egui::RichText::new(&status_text).size(11.0).color(status_color)); - - egui::Frame::none() - .fill(ui.style().visuals.widgets.inactive.bg_fill) - .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 210))) - .inner_margin(egui::Margin::same(12.0)) - .rounding(6.0) - .show(ui, |ui: &mut egui::Ui| { - ui.add_sized([ui.available_width(), 2.0], egui::Separator::default()); - ui.add_space(8.0); - - if self.output_text.is_empty() { - ui.label(egui::RichText::new("发送消息后结果将在此显示").size(13.0).color(egui::Color32::GRAY)); - } else { - ui.add_sized( - [ui.available_width(), 150.0], - egui::TextEdit::multiline(&mut self.output_text.clone()) - .desired_width(f32::INFINITY) - .desired_rows(6), - ); + ui.horizontal(|ui| { + for (i, display) in self.model_displays.iter().enumerate() { + if i > 0 { + ui.add_space(5.0); } - - ui.add_space(8.0); - - ui.horizontal(|ui: &mut egui::Ui| { - let copy_btn = egui::Button::new("复制结果") - .fill(egui::Color32::TRANSPARENT) - .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(100, 100, 255))) - .rounding(4.0); - - if ui.add(copy_btn).clicked() && !self.output_text.is_empty() { - self.copy_to_clipboard(); - } + + ui.set_min_width(column_width); + ui.set_max_width(column_width); + + ui.vertical(|ui| { + ui.set_width(column_width); + + let bg_color = if display.enabled { + ui.style().visuals.widgets.inactive.bg_fill + } else { + egui::Color32::from_rgb(240, 240, 240) + }; + + egui::Frame::none() + .fill(bg_color) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 210))) + .rounding(6.0) + .inner_margin(egui::Margin::same(8.0)) + .show(ui, |ui| { + let status_color = match &display.status { + ModelStatus::Waiting => egui::Color32::GRAY, + ModelStatus::Loading => egui::Color32::from_rgb(255, 165, 0), + ModelStatus::Completed => egui::Color32::from_rgb(0, 180, 0), + ModelStatus::Error(_) => egui::Color32::RED, + }; + + let enabled_dot = if display.enabled { "●" } else { "○" }; + ui.label(egui::RichText::new(format!("{} {}", enabled_dot, display.name)) + .size(12.0).strong() + .color(if display.enabled { egui::Color32::from_rgb(100, 100, 255) } else { egui::Color32::GRAY })); + + ui.label(egui::RichText::new(&display.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(4.0); + + if display.result.is_empty() { + ui.label(egui::RichText::new("等待结果...").size(11.0).color(egui::Color32::GRAY)); + } else { + ui.add_sized( + [ui.available_width(), 120.0], + egui::TextEdit::multiline(&mut display.result.clone()) + .desired_rows(5) + .frame(false), + ); + } + + ui.add_space(4.0); + ui.add_sized([ui.available_width(), 1.0], egui::Separator::default()); + ui.add_space(4.0); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new(match &display.status { + ModelStatus::Waiting => "就绪", + ModelStatus::Loading => "生成中...", + ModelStatus::Completed => "完成", + ModelStatus::Error(_) => "错误", + }).size(10.0).color(status_color)); + + if ui.small_button("复制").clicked() && !display.result.is_empty() { + self.copy_to_clipboard(&display.result); + } + }); + }); }); - }); - - ui.add_space(16.0); + } + }); }); } @@ -450,134 +379,141 @@ impl FlomoAiApp { ui.separator(); - let mut changed = false; - - ui.label(egui::RichText::new("LLM 配置").size(14.0).strong()); - ui.add_space(8.0); - - ui.label("API Base URL"); - ui.text_edit_singleline(&mut self.settings_base_url); - ui.add_space(6.0); - - ui.label("API Key"); - ui.horizontal(|ui| { - if self.settings_show_api_key { - ui.text_edit_singleline(&mut self.settings_api_key); - } else { - let mut masked = "*".repeat(self.settings_api_key.len()); - ui.text_edit_singleline(&mut masked); - } - if ui.button(if self.settings_show_api_key { "隐藏" } else { "显示" }).clicked() { - self.settings_show_api_key = !self.settings_show_api_key; - } - }); - ui.add_space(6.0); - - ui.label("Model"); - ui.text_edit_singleline(&mut self.settings_model); - ui.add_space(12.0); - - // Test connection button - ui.horizontal(|ui| { - let btn_text = if self.test_is_loading { "测试中..." } else { "测试连接" }; - let btn = egui::Button::new(btn_text) - .fill(egui::Color32::from_rgb(100, 100, 255)) - .rounding(4.0) - .min_size(egui::vec2(80.0, 32.0)); - - if ui.add(btn).clicked() && !self.test_is_loading { - self.test_connection(ctx); - } - - let status_color = if self.test_is_loading { - egui::Color32::from_rgb(255, 165, 0) - } else if self.test_status.starts_with("连接成功") { - egui::Color32::from_rgb(0, 180, 0) - } else if self.test_status.starts_with("连接失败") || self.test_status.starts_with("错误") { - egui::Color32::RED - } else { - egui::Color32::GRAY - }; - - ui.label(egui::RichText::new(&self.test_status).size(11.0).color(status_color)); - }); - - ui.separator(); - - ui.label(egui::RichText::new("主题").size(14.0).strong()); - ui.add_space(8.0); - - ui.vertical(|ui| { - if ui.radio_value(&mut self.settings_selected_theme, crate::config::ThemeMode::Light, "浅色模式").clicked() { - changed = true; - } - if ui.radio_value(&mut self.settings_selected_theme, crate::config::ThemeMode::Dark, "深色模式").clicked() { - changed = true; - } - if ui.radio_value(&mut self.settings_selected_theme, crate::config::ThemeMode::FollowSystem, "跟随系统").clicked() { - changed = true; - } - }); - ui.add_space(12.0); - - ui.separator(); - - ui.label(egui::RichText::new("提示词管理").size(14.0).strong()); - ui.add_space(8.0); - - let prompt_count = self.settings.prompt_configs.len(); - for i in 0..prompt_count { - let title = self.settings.prompt_configs[i].title.clone(); - let content = self.settings.prompt_configs[i].content.clone(); - - ui.horizontal(|ui| { - ui.label(egui::RichText::new(&title).size(13.0)); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.small_button("删除").clicked() { - self.settings.prompt_configs.remove(i); + 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()); + }); + + 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(2.0); - ui.label(egui::RichText::new(&content).size(11.0).color(egui::Color32::GRAY)); - ui.add_space(8.0); - } - - if self.settings.prompt_configs.len() != prompt_count { - changed = true; - } - - ui.separator(); - ui.add_space(8.0); - - ui.label(egui::RichText::new("添加新提示词").size(12.0).strong()); - ui.add_space(6.0); - - ui.label("标题"); - ui.text_edit_singleline(&mut self.new_prompt_title); - ui.add_space(4.0); - - ui.label("内容"); - ui.text_edit_multiline(&mut self.new_prompt_content); - ui.add_space(6.0); - - if ui.button("添加").clicked() { - if !self.new_prompt_title.trim().is_empty() && !self.new_prompt_content.trim().is_empty() { - self.settings.prompt_configs.push(crate::config::PromptConfig { - id: format!("custom-{}", self.settings.prompt_configs.len()), - title: self.new_prompt_title.clone(), - content: self.new_prompt_content.clone(), - }); - self.new_prompt_title.clear(); - self.new_prompt_content.clear(); - changed = true; + + 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)); + + 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(); + }); + } + } + + ui.label(egui::RichText::new(&test_status).size(11.0).color(egui::Color32::GRAY)); + } + + if i < self.model_displays.len() - 1 { + ui.separator(); + } } - } - if changed { - self.save_settings(); - } + ui.add_space(12.0); + ui.separator(); + + ui.label(egui::RichText::new("主题").size(14.0).strong()); + ui.add_space(8.0); + + for (mode, label) in [ + (ThemeMode::Light, "浅色模式"), + (ThemeMode::Dark, "深色模式"), + (ThemeMode::FollowSystem, "跟随系统"), + ] { + ui.radio_value(&mut self.settings_selected_theme, mode, label); + } + + ui.add_space(12.0); + ui.separator(); + ui.label(egui::RichText::new("提示词管理").size(14.0).strong()); + ui.add_space(8.0); + + for i in 0..self.settings.prompt_configs.len() { + let title = self.settings.prompt_configs[i].title.clone(); + ui.horizontal(|ui| { + ui.label(egui::RichText::new(&title).size(13.0)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.small_button("删除").clicked() { + self.settings.prompt_configs.remove(i); + } + }); + }); + } + + ui.add_space(8.0); + ui.separator(); + ui.add_space(8.0); + + ui.label(egui::RichText::new("添加新提示词").size(12.0).strong()); + ui.add_space(6.0); + + ui.label("标题"); + ui.text_edit_singleline(&mut self.new_prompt_title); + ui.add_space(4.0); + + ui.label("内容"); + ui.text_edit_multiline(&mut self.new_prompt_content); + ui.add_space(6.0); + + if ui.button("添加").clicked() { + if !self.new_prompt_title.trim().is_empty() && !self.new_prompt_content.trim().is_empty() { + self.settings.prompt_configs.push(crate::config::PromptConfig { + id: format!("custom-{}", self.settings.prompt_configs.len()), + title: self.new_prompt_title.clone(), + content: self.new_prompt_content.clone(), + }); + self.new_prompt_title.clear(); + self.new_prompt_content.clear(); + self.save_settings(); + } + } + }); }); } -} +} \ No newline at end of file diff --git a/flomo-ai-desktop/src/config/store.rs b/flomo-ai-desktop/src/config/store.rs index 7c82e97..dfdea0b 100644 --- a/flomo-ai-desktop/src/config/store.rs +++ b/flomo-ai-desktop/src/config/store.rs @@ -17,11 +17,48 @@ pub struct PromptConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LLMConfig { + pub enabled: bool, + pub name: String, pub base_url: String, pub api_key: String, pub model: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LLMConfigs { + pub models: Vec, +} + +impl Default for LLMConfigs { + fn default() -> Self { + Self { + models: vec![ + LLMConfig { + enabled: true, + name: "模型1".to_string(), + base_url: "https://api.openai.com/v1".to_string(), + api_key: String::new(), + model: "gpt-4o".to_string(), + }, + LLMConfig { + enabled: false, + name: "模型2".to_string(), + base_url: String::new(), + api_key: String::new(), + model: String::new(), + }, + LLMConfig { + enabled: false, + name: "模型3".to_string(), + base_url: String::new(), + api_key: String::new(), + model: String::new(), + }, + ], + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ThemeConfig { pub mode: ThemeMode, @@ -36,7 +73,7 @@ pub enum ThemeMode { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppSettings { - pub llm_config: LLMConfig, + pub llm_configs: LLMConfigs, pub header_configs: Vec, pub prompt_configs: Vec, pub theme_config: ThemeConfig, @@ -45,11 +82,7 @@ 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: LLMConfigs::default(), header_configs: Vec::new(), prompt_configs: vec![ PromptConfig { diff --git a/flomo-ai-desktop/src/main.rs b/flomo-ai-desktop/src/main.rs index d5f255e..d21b44b 100644 --- a/flomo-ai-desktop/src/main.rs +++ b/flomo-ai-desktop/src/main.rs @@ -40,7 +40,7 @@ fn setup_fonts(ctx: &egui::Context) { for (i, path) in font_paths.iter().enumerate() { if let Ok(data) = std::fs::read(path) { let name = format!("chinese_{}", i); - fonts.font_data.insert(name.clone(), egui::FontData::from_owned(data)); + fonts.font_data.insert(name.clone(), egui::FontData::from_owned(data).into()); fonts .families .entry(egui::FontFamily::Proportional) diff --git a/flomo-ai-desktop/src/pages/main_page.rs b/flomo-ai-desktop/src/pages/main_page.rs deleted file mode 100644 index 50833ae..0000000 --- a/flomo-ai-desktop/src/pages/main_page.rs +++ /dev/null @@ -1,217 +0,0 @@ -use egui::{Ui, Color32, RichText, TextEdit, Button, ScrollArea}; -use crate::config::AppSettings; -use crate::api::call_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 selected_prompt_index: usize, - pub is_loading: bool, -} - -impl Default for MainPage { - fn default() -> Self { - Self { - input_text: String::new(), - output_text: String::new(), - status: OutputStatus::Waiting, - selected_prompt_index: 0, - is_loading: false, - } - } -} - -impl MainPage { - pub fn new() -> Self { - Self::default() - } - - pub fn ui(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) { - ScrollArea::vertical().show(ui, |ui| { - self.render_content(ui, settings, ctx); - }); - } - - fn render_content(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) { - ui.add_space(16.0); - - ui.label(RichText::new("快速操作").size(11.0).color(Color32::GRAY)); - ui.add_space(6.0); - - ui.horizontal(|ui| { - let buttons = vec![ - ("🔍", "检查错别字"), - ("📋", "总结"), - ("🌐", "翻译"), - ("✨", "润色"), - ]; - - for (emoji, name) in buttons { - let btn = Button::new(RichText::new(emoji).size(16.0)) - .fill(Color32::from_rgb(245, 245, 247)) - .stroke(egui::Stroke::new(1.0, Color32::from_rgb(210, 210, 220))) - .rounding(6.0) - .min_size(egui::vec2(36.0, 36.0)); - - if ui.add(btn).on_hover_text(name).clicked() { - self.select_prompt_by_name(name, settings, ctx); - } - } - }); - - ui.add_space(14.0); - - ui.label(RichText::new("提示词").size(11.0).color(Color32::GRAY)); - ui.add_space(6.0); - - let selected_text = if self.selected_prompt_index == 0 { - "无系统提示词".to_string() - } else if self.selected_prompt_index <= settings.prompt_configs.len() { - settings.prompt_configs[self.selected_prompt_index - 1].title.clone() - } else { - "无系统提示词".to_string() - }; - - egui::ComboBox::from_id_salt("prompt_selector") - .selected_text(&selected_text) - .show_ui(ui, |ui| { - ui.selectable_value(&mut self.selected_prompt_index, 0, "无系统提示词"); - for (i, prompt) in settings.prompt_configs.iter().enumerate() { - ui.selectable_value(&mut self.selected_prompt_index, i + 1, &prompt.title); - } - }); - - ui.separator(); - - let prompt_content = if self.selected_prompt_index == 0 { - "无特殊指令".to_string() - } else if self.selected_prompt_index <= settings.prompt_configs.len() { - settings.prompt_configs[self.selected_prompt_index - 1].content.clone() - } else { - "无特殊指令".to_string() - }; - - ui.label(RichText::new(&prompt_content).size(11.0).color(Color32::GRAY)); - - ui.add_space(14.0); - - ui.label(RichText::new("优化结果").size(11.0).color(Color32::GRAY)); - ui.add_space(6.0); - - let status_text = match &self.status { - OutputStatus::Waiting => "等待发送".to_string(), - OutputStatus::Connecting => "连接中…".to_string(), - OutputStatus::Completed => "已完成".to_string(), - OutputStatus::Error(_) => "发生错误".to_string(), - OutputStatus::Stopped => "已停止".to_string(), - }; - - let status_color = match &self.status { - 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))) - .inner_margin(egui::Margin::same(12.0)) - .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); - - 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(); - } - }); - }); - - ui.add_space(16.0); - } - - pub fn send_request(&mut self, settings: &AppSettings, ctx: egui::Context) { - if self.input_text.trim().is_empty() || self.is_loading { - return; - } - - self.status = OutputStatus::Connecting; - self.output_text = "正在生成...".to_string(); - self.is_loading = true; - - let prompt = if self.selected_prompt_index == 0 { - None - } else if self.selected_prompt_index <= settings.prompt_configs.len() { - Some(settings.prompt_configs[self.selected_prompt_index - 1].content.clone()) - } else { - None - }; - - let input = self.input_text.clone(); - let settings = settings.clone(); - - let handle = std::thread::spawn(move || { - let result = call_llm(&settings, input, prompt); - ctx.request_repaint(); - result - }); - - std::thread::spawn(move || { - let _ = handle.join(); - }); - } - - fn select_prompt_by_name(&mut self, name: &str, settings: &AppSettings, ctx: &egui::Context) { - let mut found = false; - for (i, prompt) in settings.prompt_configs.iter().enumerate() { - if prompt.title == name { - self.selected_prompt_index = i + 1; - found = true; - break; - } - } - - if !self.input_text.is_empty() && found { - 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); - } - } -} diff --git a/flomo-ai-desktop/src/pages/mod.rs b/flomo-ai-desktop/src/pages/mod.rs index d5af578..e69de29 100644 --- a/flomo-ai-desktop/src/pages/mod.rs +++ b/flomo-ai-desktop/src/pages/mod.rs @@ -1,4 +0,0 @@ -pub mod main_page; -pub mod settings_page; - -pub use main_page::OutputStatus; diff --git a/flomo-ai-desktop/src/pages/settings_page.rs b/flomo-ai-desktop/src/pages/settings_page.rs deleted file mode 100644 index 3f73a09..0000000 --- a/flomo-ai-desktop/src/pages/settings_page.rs +++ /dev/null @@ -1,154 +0,0 @@ -use egui::{Ui, Color32, RichText, ScrollArea}; -use crate::config::{AppSettings, LLMConfig, PromptConfig, ThemeMode}; - -pub struct SettingsPage { - pub base_url: String, - pub api_key: String, - pub model: String, - pub show_api_key: bool, - pub selected_theme: ThemeMode, - pub new_prompt_title: String, - pub new_prompt_content: String, -} - -impl Default for SettingsPage { - fn default() -> Self { - Self { - base_url: "https://api.openai.com/v1".to_string(), - api_key: String::new(), - model: "gpt-4o".to_string(), - show_api_key: false, - selected_theme: ThemeMode::FollowSystem, - new_prompt_title: String::new(), - new_prompt_content: String::new(), - } - } -} - -impl SettingsPage { - pub fn new(settings: &AppSettings) -> Self { - Self { - base_url: settings.llm_config.base_url.clone(), - api_key: settings.llm_config.api_key.clone(), - model: settings.llm_config.model.clone(), - show_api_key: false, - selected_theme: settings.theme_config.mode, - new_prompt_title: String::new(), - new_prompt_content: String::new(), - } - } - - pub fn ui(&mut self, ui: &mut Ui, settings: &mut AppSettings) -> bool { - let mut changed = false; - - ScrollArea::vertical().show(ui, |ui| { - ui.add_space(16.0); - - ui.label(RichText::new("LLM 配置").size(14.0).strong()); - ui.add_space(8.0); - - ui.label("API Base URL"); - ui.text_edit_singleline(&mut self.base_url); - ui.add_space(6.0); - - ui.label("API Key"); - ui.horizontal(|ui| { - if self.show_api_key { - ui.text_edit_singleline(&mut self.api_key); - } else { - let mut masked = "*".repeat(self.api_key.len()); - ui.text_edit_singleline(&mut masked); - } - if ui.button(if self.show_api_key { "隐藏" } else { "显示" }).clicked() { - self.show_api_key = !self.show_api_key; - } - }); - ui.add_space(6.0); - - ui.label("Model"); - ui.text_edit_singleline(&mut self.model); - ui.add_space(12.0); - - ui.separator(); - - ui.label(RichText::new("主题").size(14.0).strong()); - ui.add_space(8.0); - - ui.vertical(|ui| { - let resp = ui.radio_value(&mut self.selected_theme, ThemeMode::Light, "浅色模式"); - if resp.clicked() && resp.changed() { changed = true; } - let resp = ui.radio_value(&mut self.selected_theme, ThemeMode::Dark, "深色模式"); - if resp.clicked() && resp.changed() { changed = true; } - let resp = ui.radio_value(&mut self.selected_theme, ThemeMode::FollowSystem, "跟随系统"); - if resp.clicked() && resp.changed() { changed = true; } - }); - ui.add_space(12.0); - - ui.separator(); - - ui.label(RichText::new("提示词管理").size(14.0).strong()); - ui.add_space(8.0); - - let prompt_count = settings.prompt_configs.len(); - for i in 0..prompt_count { - let title = settings.prompt_configs[i].title.clone(); - let content = settings.prompt_configs[i].content.clone(); - - ui.horizontal(|ui| { - ui.label(RichText::new(&title).size(13.0)); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.small_button("删除").clicked() { - settings.prompt_configs.remove(i); - } - }); - }); - - ui.add_space(2.0); - ui.label(RichText::new(&content).size(11.0).color(Color32::GRAY)); - ui.add_space(8.0); - } - - if settings.prompt_configs.len() != prompt_count { - changed = true; - } - - ui.separator(); - ui.add_space(8.0); - - ui.label(RichText::new("添加新提示词").size(12.0).strong()); - ui.add_space(6.0); - - ui.label("标题"); - ui.text_edit_singleline(&mut self.new_prompt_title); - ui.add_space(4.0); - - ui.label("内容"); - ui.text_edit_multiline(&mut self.new_prompt_content); - ui.add_space(6.0); - - if ui.button("添加").clicked() { - if !self.new_prompt_title.trim().is_empty() && !self.new_prompt_content.trim().is_empty() { - settings.prompt_configs.push(PromptConfig { - id: format!("custom-{}", settings.prompt_configs.len()), - title: self.new_prompt_title.clone(), - content: self.new_prompt_content.clone(), - }); - self.new_prompt_title.clear(); - self.new_prompt_content.clear(); - changed = true; - } - } - }); - - changed - } - - pub fn apply_to_settings(&self, settings: &mut AppSettings) { - settings.llm_config = LLMConfig { - base_url: self.base_url.clone(), - api_key: self.api_key.clone(), - model: self.model.clone(), - }; - settings.theme_config.mode = self.selected_theme; - } -}