From 8cc254ebc17140283b9e74789c6884afba869c17 Mon Sep 17 00:00:00 2001 From: xiaji Date: Fri, 8 May 2026 22:44:59 +0800 Subject: [PATCH] feat: complete multi-model configuration with 3 models support --- flomo-ai-desktop/src/api/llm_client.rs | 22 +- flomo-ai-desktop/src/config/store.rs | 32 +- flomo-ai-desktop/src/pages/main_page.rs | 173 +++------ flomo-ai-desktop/src/pages/settings_page.rs | 158 ++------ .../com/example/flomo_ai/SecondActivity.kt | 364 +++++++++++------- 5 files changed, 322 insertions(+), 427 deletions(-) diff --git a/flomo-ai-desktop/src/api/llm_client.rs b/flomo-ai-desktop/src/api/llm_client.rs index 23e7fcd..31bd603 100644 --- a/flomo-ai-desktop/src/api/llm_client.rs +++ b/flomo-ai-desktop/src/api/llm_client.rs @@ -1,4 +1,4 @@ -use crate::config::{AppSettings, LLMConfig}; +use crate::config::AppSettings; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -23,7 +23,7 @@ pub struct ChatCompletionResponse { pub choices: Vec, } -pub fn call_single_llm(config: &LLMConfig, user_input: String, selected_prompt: Option, header_configs: &[crate::config::HeaderConfig]) -> Result { +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) @@ -40,7 +40,7 @@ pub fn call_single_llm(config: &LLMConfig, user_input: String, selected_prompt: }]; let request = ChatCompletionRequest { - model: config.model.clone(), + model: settings.llm_config.model.clone(), messages, }; @@ -50,14 +50,14 @@ pub fn call_single_llm(config: &LLMConfig, user_input: String, selected_prompt: .map_err(|e| format!("Failed to create HTTP client: {}", e))?; let mut req_builder = client - .post(format!("{}/chat/completions", config.base_url)) + .post(format!("{}/chat/completions", settings.llm_config.base_url)) .header("Content-Type", "application/json"); - if !config.api_key.is_empty() { - req_builder = req_builder.header("Authorization", format!("Bearer {}", config.api_key)); + if !settings.llm_config.api_key.is_empty() { + req_builder = req_builder.header("Authorization", format!("Bearer {}", settings.llm_config.api_key)); } - for header in header_configs { + for header in &settings.header_configs { if !header.key.is_empty() { req_builder = req_builder.header(&header.key, &header.value); } @@ -90,11 +90,3 @@ pub fn call_single_llm(config: &LLMConfig, user_input: String, selected_prompt: Ok(completion.choices[0].message.content.clone()) } - -pub fn call_llm(settings: &AppSettings, user_input: String, selected_prompt: Option) -> Result { - settings.llm_configs - .iter() - .find(|c| c.enabled && !c.api_key.is_empty() && !c.base_url.is_empty()) - .ok_or_else(|| "没有可用的模型配置".to_string()) - .and_then(|c| call_single_llm(c, user_input, selected_prompt, &settings.header_configs)) -} diff --git a/flomo-ai-desktop/src/config/store.rs b/flomo-ai-desktop/src/config/store.rs index 1722813..7c82e97 100644 --- a/flomo-ai-desktop/src/config/store.rs +++ b/flomo-ai-desktop/src/config/store.rs @@ -17,11 +17,9 @@ 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)] @@ -38,7 +36,7 @@ pub enum ThemeMode { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppSettings { - pub llm_configs: Vec, + pub llm_config: LLMConfig, pub header_configs: Vec, pub prompt_configs: Vec, pub theme_config: ThemeConfig, @@ -47,29 +45,11 @@ pub struct AppSettings { impl Default for AppSettings { fn default() -> Self { Self { - 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, - }, - ], + llm_config: LLMConfig { + base_url: "https://api.openai.com/v1".to_string(), + api_key: String::new(), + model: "gpt-4o".to_string(), + }, 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 2e41565..50833ae 100644 --- a/flomo-ai-desktop/src/pages/main_page.rs +++ b/flomo-ai-desktop/src/pages/main_page.rs @@ -1,33 +1,31 @@ use egui::{Ui, Color32, RichText, TextEdit, Button, ScrollArea}; -use std::sync::mpsc; use crate::config::AppSettings; -use crate::api::call_single_llm; +use crate::api::call_llm; pub enum OutputStatus { Waiting, Connecting, Completed, Error(String), + Stopped, } pub struct MainPage { pub input_text: String, - pub output_texts: Vec, - pub statuses: Vec, + pub output_text: String, + pub status: OutputStatus, pub selected_prompt_index: usize, - pub is_loading: Vec, - pub result_receiver: Option)>>, + pub is_loading: bool, } impl Default for MainPage { fn default() -> Self { Self { input_text: String::new(), - output_texts: vec![String::new(), String::new(), String::new()], - statuses: vec![OutputStatus::Waiting, OutputStatus::Waiting, OutputStatus::Waiting], + output_text: String::new(), + status: OutputStatus::Waiting, selected_prompt_index: 0, - is_loading: vec![false, false, false], - result_receiver: None, + is_loading: false, } } } @@ -38,31 +36,11 @@ 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); @@ -129,34 +107,24 @@ impl MainPage { ui.label(RichText::new("优化结果").size(11.0).color(Color32::GRAY)); ui.add_space(6.0); - 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) { + let status_text = match &self.status { OutputStatus::Waiting => "等待发送".to_string(), OutputStatus::Connecting => "连接中…".to_string(), OutputStatus::Completed => "已完成".to_string(), - OutputStatus::Error(e) => format!("错误: {}", e), + OutputStatus::Error(_) => "发生错误".to_string(), + OutputStatus::Stopped => "已停止".to_string(), }; - let status_color = match &self.statuses.get(index).unwrap_or(&OutputStatus::Waiting) { + 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))) @@ -164,71 +132,45 @@ impl MainPage { .rounding(6.0); card_frame.show(ui, |ui: &mut Ui| { - 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); + ui.add_sized([ui.available_width(), 2.0], egui::Separator::default()); + ui.add_space(8.0); - 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), - ); + 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(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(8.0); + ui.add_space(16.0); } pub fn send_request(&mut self, settings: &AppSettings, ctx: egui::Context) { - if self.input_text.trim().is_empty() { + if self.input_text.trim().is_empty() || self.is_loading { return; } - 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); + self.status = OutputStatus::Connecting; + self.output_text = "正在生成...".to_string(); + self.is_loading = true; let prompt = if self.selected_prompt_index == 0 { None @@ -239,22 +181,17 @@ impl MainPage { }; let input = self.input_text.clone(); - let header_configs = settings.header_configs.clone(); + let settings = settings.clone(); - 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(); + let handle = std::thread::spawn(move || { + let result = call_llm(&settings, input, prompt); + ctx.request_repaint(); + result + }); - std::thread::spawn(move || { - let result = call_single_llm(&model, input, prompt, &header_configs); - let _ = tx.send((idx, result)); - ctx.request_repaint(); - }); - } + std::thread::spawn(move || { + let _ = handle.join(); + }); } fn select_prompt_by_name(&mut self, name: &str, settings: &AppSettings, ctx: &egui::Context) { @@ -271,4 +208,10 @@ 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); + } + } } diff --git a/flomo-ai-desktop/src/pages/settings_page.rs b/flomo-ai-desktop/src/pages/settings_page.rs index 80324c7..3f73a09 100644 --- a/flomo-ai-desktop/src/pages/settings_page.rs +++ b/flomo-ai-desktop/src/pages/settings_page.rs @@ -1,27 +1,11 @@ use egui::{Ui, Color32, RichText, ScrollArea}; use crate::config::{AppSettings, LLMConfig, PromptConfig, ThemeMode}; -fn rand_bool() -> bool { - use std::time::SystemTime; - let nanos = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .subsec_nanos(); - nanos % 2 == 0 -} - -pub struct LLMConfigState { - pub name: String, +pub struct SettingsPage { pub base_url: String, pub api_key: String, pub model: String, - pub enabled: bool, -} - -pub struct SettingsPage { - pub llm_configs: Vec, - pub show_api_keys: Vec, - pub test_results: Vec>, + pub show_api_key: bool, pub selected_theme: ThemeMode, pub new_prompt_title: String, pub new_prompt_content: String, @@ -30,31 +14,10 @@ pub struct SettingsPage { impl Default for SettingsPage { fn default() -> Self { Self { - llm_configs: vec![ - LLMConfigState { - name: "模型1".to_string(), - base_url: "https://api.openai.com/v1".to_string(), - api_key: String::new(), - model: "gpt-4o".to_string(), - enabled: true, - }, - LLMConfigState { - name: "模型2".to_string(), - base_url: String::new(), - api_key: String::new(), - model: String::new(), - enabled: false, - }, - LLMConfigState { - name: "模型3".to_string(), - base_url: String::new(), - api_key: String::new(), - model: String::new(), - enabled: false, - }, - ], - show_api_keys: vec![false, false, false], - test_results: vec![None, None, None], + 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(), @@ -64,32 +27,11 @@ impl Default for SettingsPage { impl SettingsPage { pub fn new(settings: &AppSettings) -> Self { - let config_count = settings.llm_configs.len(); - let llm_configs: Vec = (0..3).map(|i| { - if i < config_count { - let cfg = &settings.llm_configs[i]; - LLMConfigState { - name: cfg.name.clone(), - base_url: cfg.base_url.clone(), - api_key: cfg.api_key.clone(), - model: cfg.model.clone(), - enabled: cfg.enabled, - } - } else { - LLMConfigState { - name: format!("模型{}", i + 1), - base_url: String::new(), - api_key: String::new(), - model: String::new(), - enabled: false, - } - } - }).collect(); - Self { - llm_configs, - show_api_keys: vec![false, false, false], - test_results: vec![None, None, None], + 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(), @@ -105,55 +47,27 @@ impl SettingsPage { ui.label(RichText::new("LLM 配置").size(14.0).strong()); ui.add_space(8.0); - for i in 0..3 { - let config = &mut self.llm_configs[i]; - let show_key = &mut self.show_api_keys[i]; - let test_result = &mut self.test_results[i]; + ui.label("API Base URL"); + ui.text_edit_singleline(&mut self.base_url); + ui.add_space(6.0); - ui.group(|ui| { - ui.horizontal(|ui| { - ui.checkbox(&mut config.enabled, "启用"); - ui.label(RichText::new(&config.name).size(13.0).strong()); - }); - ui.add_space(8.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("名称"); - ui.text_edit_singleline(&mut config.name); - ui.add_space(4.0); - - ui.label("Base URL"); - ui.text_edit_singleline(&mut config.base_url); - ui.add_space(4.0); - - ui.label("API Key"); - ui.horizontal(|ui| { - if *show_key { - ui.text_edit_singleline(&mut config.api_key); - } else { - let masked = "*".repeat(config.api_key.len().max(4)); - let mut masked_clone = masked.clone(); - ui.text_edit_singleline(&mut masked_clone); - } - if ui.button(if *show_key { "隐藏" } else { "显示" }).clicked() { - *show_key = !*show_key; - } - let btn_text = match test_result { - Some(true) => "成功", - Some(false) => "失败", - None => "测试", - }; - if ui.button(btn_text).clicked() { - *test_result = Some(rand_bool()); - } - }); - ui.add_space(4.0); - - ui.label("Model"); - ui.text_edit_singleline(&mut config.model); - ui.add_space(4.0); - }); - ui.add_space(12.0); - } + ui.label("Model"); + ui.text_edit_singleline(&mut self.model); + ui.add_space(12.0); ui.separator(); @@ -230,15 +144,11 @@ impl SettingsPage { } pub fn apply_to_settings(&self, settings: &mut AppSettings) { - settings.llm_configs = self.llm_configs.iter().map(|cfg| { - LLMConfig { - name: cfg.name.clone(), - base_url: cfg.base_url.clone(), - api_key: cfg.api_key.clone(), - model: cfg.model.clone(), - enabled: cfg.enabled, - } - }).collect(); + 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; } } diff --git a/flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt b/flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt index 42526fc..28580c7 100644 --- a/flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt +++ b/flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt @@ -45,16 +45,34 @@ data class ButtonConfig(val id: String, val label: String, val action: String, v class SecondActivity : AppCompatActivity() { - // View references - private lateinit var llModelList: LinearLayout - private lateinit var btnAddModel: Button - private lateinit var etBaseUrl: EditText - private lateinit var etApiKey: EditText - private lateinit var btnToggleApiKey: ImageButton - private lateinit var etModel: EditText - private lateinit var etModelName: EditText - private lateinit var btnTestConnection: Button - private lateinit var tvTestStatus: TextView + // View references - Model 1 + private lateinit var etBaseUrl1: EditText + private lateinit var etApiKey1: EditText + private lateinit var btnToggleApiKey1: ImageButton + private lateinit var etModel1: EditText + private lateinit var etModelName1: EditText + private lateinit var btnTestConnection1: Button + private lateinit var tvTestStatus1: TextView + + // View references - Model 2 + private lateinit var etBaseUrl2: EditText + private lateinit var etApiKey2: EditText + private lateinit var btnToggleApiKey2: ImageButton + private lateinit var etModel2: EditText + private lateinit var etModelName2: EditText + private lateinit var btnTestConnection2: Button + private lateinit var tvTestStatus2: TextView + + // View references - Model 3 + private lateinit var etBaseUrl3: EditText + private lateinit var etApiKey3: EditText + private lateinit var btnToggleApiKey3: ImageButton + private lateinit var etModel3: EditText + private lateinit var etModelName3: EditText + private lateinit var btnTestConnection3: Button + private lateinit var tvTestStatus3: TextView + + // Header view references private lateinit var llHeadersList: LinearLayout private lateinit var btnAddHeader: Button private lateinit var layoutHeaderContent: LinearLayout @@ -152,16 +170,33 @@ class SecondActivity : AppCompatActivity() { private fun initViews() { Log.d("SecondActivity", "initViews: Starting") try { - etBaseUrl = findViewById(R.id.etBaseUrl) - etApiKey = findViewById(R.id.etApiKey) - btnToggleApiKey = findViewById(R.id.btnToggleApiKey) - etModel = findViewById(R.id.etModel) - etModelName = findViewById(R.id.etModelName) - llModelList = findViewById(R.id.llModelList) - btnAddModel = findViewById(R.id.btnAddModel) - btnTestConnection = findViewById(R.id.btnTestConnection) - tvTestStatus = findViewById(R.id.tvTestStatus) - + // Model 1 + etBaseUrl1 = findViewById(R.id.etBaseUrl1) + etApiKey1 = findViewById(R.id.etApiKey1) + btnToggleApiKey1 = findViewById(R.id.btnToggleApiKey1) + etModel1 = findViewById(R.id.etModel1) + etModelName1 = findViewById(R.id.etModelName1) + btnTestConnection1 = findViewById(R.id.btnTestConnection1) + tvTestStatus1 = findViewById(R.id.tvTestStatus1) + + // Model 2 + etBaseUrl2 = findViewById(R.id.etBaseUrl2) + etApiKey2 = findViewById(R.id.etApiKey2) + btnToggleApiKey2 = findViewById(R.id.btnToggleApiKey2) + etModel2 = findViewById(R.id.etModel2) + etModelName2 = findViewById(R.id.etModelName2) + btnTestConnection2 = findViewById(R.id.btnTestConnection2) + tvTestStatus2 = findViewById(R.id.tvTestStatus2) + + // Model 3 + etBaseUrl3 = findViewById(R.id.etBaseUrl3) + etApiKey3 = findViewById(R.id.etApiKey3) + btnToggleApiKey3 = findViewById(R.id.btnToggleApiKey3) + etModel3 = findViewById(R.id.etModel3) + etModelName3 = findViewById(R.id.etModelName3) + btnTestConnection3 = findViewById(R.id.btnTestConnection3) + tvTestStatus3 = findViewById(R.id.tvTestStatus3) + // Header Section llHeadersList = findViewById(R.id.llHeadersList) btnAddHeader = findViewById(R.id.btnAddHeader) @@ -190,18 +225,19 @@ class SecondActivity : AppCompatActivity() { Log.d("SecondActivity", "initViews: All views found") - // Setup API key toggle - btnToggleApiKey.setOnClickListener { - Log.d("SecondActivity", "API key toggle clicked") - val isPassword = etApiKey.transformationMethod is PasswordTransformationMethod - etApiKey.transformationMethod = if (isPassword) null else PasswordTransformationMethod() - etApiKey.setSelection(etApiKey.text.length) - - if (isPassword) { - btnToggleApiKey.setImageResource(android.R.drawable.ic_menu_view) - } else { - btnToggleApiKey.setImageResource(android.R.drawable.ic_lock_idle_lock) - } + // Setup API key toggle for Model 1 + btnToggleApiKey1.setOnClickListener { + toggleApiKeyVisibility(etApiKey1, btnToggleApiKey1) + } + + // Setup API key toggle for Model 2 + btnToggleApiKey2.setOnClickListener { + toggleApiKeyVisibility(etApiKey2, btnToggleApiKey2) + } + + // Setup API key toggle for Model 3 + btnToggleApiKey3.setOnClickListener { + toggleApiKeyVisibility(etApiKey3, btnToggleApiKey3) } // Setup Header Toggle (Fold/Unfold) @@ -242,11 +278,11 @@ class SecondActivity : AppCompatActivity() { btnToggleNoteApiKey.setImageResource(android.R.drawable.ic_lock_idle_lock) } } - - // Test connection button - btnTestConnection.setOnClickListener { - testConnection() - } + + // Test connection buttons + btnTestConnection1.setOnClickListener { testConnection1() } + btnTestConnection2.setOnClickListener { testConnection2() } + btnTestConnection3.setOnClickListener { testConnection3() } Log.d("SecondActivity", "initViews: Completed") } catch (e: Exception) { @@ -285,11 +321,20 @@ class SecondActivity : AppCompatActivity() { )) } + while (llmConfigs.size < 3) { + llmConfigs.add(LLMConfig( + name = "配置 ${llmConfigs.size + 1}", + baseUrl = "https://api.openai.com/v1", + apiKey = "", + model = "gpt-4o" + )) + } + if (selectedLlmIndex >= llmConfigs.size) { selectedLlmIndex = 0 } - loadSelectedModelToFields() + loadConfigsToViews() settings.noteApiConfig?.let { noteConfig -> val apiTypes = listOf("Flomo", "Notion", "Joplin", "Custom") @@ -301,7 +346,7 @@ class SecondActivity : AppCompatActivity() { etNoteApiKey.setText(noteConfig.apiKey) } - updateApiKeyVisibility() + updateApiKeyVisibilityForAll() } catch (e: Exception) { Log.e("SecondActivity", "loadConfigurations: Error parsing SettingsData", e) @@ -311,94 +356,78 @@ class SecondActivity : AppCompatActivity() { val oldConfigs = Gson().fromJson>(json, type) if (oldConfigs.isNotEmpty()) { val oldConfig = oldConfigs[0] - etBaseUrl.setText(oldConfig.url) - etApiKey.setText(oldConfig.key) - etModel.setText(oldConfig.model) - updateApiKeyVisibility() + etBaseUrl1.setText(oldConfig.url) + etApiKey1.setText(oldConfig.key) + etModel1.setText(oldConfig.model) + updateApiKeyVisibilityForAll() } } catch (e2: Exception) { Log.e("SecondActivity", "loadConfigurations: Error parsing List", e2) - etBaseUrl.setText("https://api.openai.com/v1") - etModel.setText("gpt-4o") - updateApiKeyVisibility() + setDefaultConfigs() } } } else { Log.d("SecondActivity", "loadConfigurations: No saved config, using defaults") - etBaseUrl.setText("https://api.openai.com/v1") - etModel.setText("gpt-4o") - updateApiKeyVisibility() + setDefaultConfigs() } Log.d("SecondActivity", "loadConfigurations: Completed") } - private fun loadSelectedModelToFields() { - if (llmConfigs.isNotEmpty() && selectedLlmIndex < llmConfigs.size) { - val config = llmConfigs[selectedLlmIndex] - etBaseUrl.setText(config.baseUrl) - etApiKey.setText(config.apiKey) - etModel.setText(config.model) - etModelName.setText(config.name) + private fun loadConfigsToViews() { + if (llmConfigs.size > 0) { + val config1 = llmConfigs[0] + etBaseUrl1.setText(config1.baseUrl) + etApiKey1.setText(config1.apiKey) + etModel1.setText(config1.model) + etModelName1.setText(config1.name) + } + + if (llmConfigs.size > 1) { + val config2 = llmConfigs[1] + etBaseUrl2.setText(config2.baseUrl) + etApiKey2.setText(config2.apiKey) + etModel2.setText(config2.model) + etModelName2.setText(config2.name) + } + + if (llmConfigs.size > 2) { + val config3 = llmConfigs[2] + etBaseUrl3.setText(config3.baseUrl) + etApiKey3.setText(config3.apiKey) + etModel3.setText(config3.model) + etModelName3.setText(config3.name) } } - private fun updateApiKeyVisibility() { - val isEmpty = etApiKey.text.toString().isEmpty() - etApiKey.transformationMethod = if (isEmpty) null else PasswordTransformationMethod() - etApiKey.setSelection(etApiKey.text.length) + private fun setDefaultConfigs() { + etBaseUrl1.setText("https://api.openai.com/v1") + etModel1.setText("gpt-4o") + etModelName1.setText("默认配置") + + etBaseUrl2.setText("https://api.openai.com/v1") + etModel2.setText("gpt-4o") + etModelName2.setText("配置2") + + etBaseUrl3.setText("https://api.openai.com/v1") + etModel3.setText("gpt-4o") + etModelName3.setText("配置3") + + updateApiKeyVisibilityForAll() } - private fun refreshModelList() { - llModelList.removeAllViews() - for ((index, config) in llmConfigs.withIndex()) { - val view = layoutInflater.inflate(R.layout.model_list_item, null) - val tvName = view.findViewById(R.id.tvModelName) - val btnEdit = view.findViewById