feat: complete multi-model configuration with 3 models support
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
use crate::config::{AppSettings, LLMConfig};
|
use crate::config::AppSettings;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -23,7 +23,7 @@ pub struct ChatCompletionResponse {
|
|||||||
pub choices: Vec<ChatChoice>,
|
pub choices: Vec<ChatChoice>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn call_single_llm(config: &LLMConfig, user_input: String, selected_prompt: Option<String>, header_configs: &[crate::config::HeaderConfig]) -> Result<String, String> {
|
pub fn call_llm(settings: &AppSettings, user_input: String, selected_prompt: Option<String>) -> Result<String, String> {
|
||||||
let full_content = if let Some(prompt) = selected_prompt {
|
let full_content = if let Some(prompt) = selected_prompt {
|
||||||
if !prompt.is_empty() {
|
if !prompt.is_empty() {
|
||||||
format!("{}{}", prompt, user_input)
|
format!("{}{}", prompt, user_input)
|
||||||
@@ -40,7 +40,7 @@ pub fn call_single_llm(config: &LLMConfig, user_input: String, selected_prompt:
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
let request = ChatCompletionRequest {
|
let request = ChatCompletionRequest {
|
||||||
model: config.model.clone(),
|
model: settings.llm_config.model.clone(),
|
||||||
messages,
|
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))?;
|
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
||||||
|
|
||||||
let mut req_builder = client
|
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");
|
.header("Content-Type", "application/json");
|
||||||
|
|
||||||
if !config.api_key.is_empty() {
|
if !settings.llm_config.api_key.is_empty() {
|
||||||
req_builder = req_builder.header("Authorization", format!("Bearer {}", config.api_key));
|
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() {
|
if !header.key.is_empty() {
|
||||||
req_builder = req_builder.header(&header.key, &header.value);
|
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())
|
Ok(completion.choices[0].message.content.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn call_llm(settings: &AppSettings, user_input: String, selected_prompt: Option<String>) -> Result<String, String> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,11 +17,9 @@ pub struct PromptConfig {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LLMConfig {
|
pub struct LLMConfig {
|
||||||
pub name: String,
|
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
pub enabled: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -38,7 +36,7 @@ pub enum ThemeMode {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AppSettings {
|
pub struct AppSettings {
|
||||||
pub llm_configs: Vec<LLMConfig>,
|
pub llm_config: LLMConfig,
|
||||||
pub header_configs: Vec<HeaderConfig>,
|
pub header_configs: Vec<HeaderConfig>,
|
||||||
pub prompt_configs: Vec<PromptConfig>,
|
pub prompt_configs: Vec<PromptConfig>,
|
||||||
pub theme_config: ThemeConfig,
|
pub theme_config: ThemeConfig,
|
||||||
@@ -47,29 +45,11 @@ pub struct AppSettings {
|
|||||||
impl Default for AppSettings {
|
impl Default for AppSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
llm_configs: vec![
|
llm_config: LLMConfig {
|
||||||
LLMConfig {
|
|
||||||
name: "模型1".to_string(),
|
|
||||||
base_url: "https://api.openai.com/v1".to_string(),
|
base_url: "https://api.openai.com/v1".to_string(),
|
||||||
api_key: String::new(),
|
api_key: String::new(),
|
||||||
model: "gpt-4o".to_string(),
|
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(),
|
header_configs: Vec::new(),
|
||||||
prompt_configs: vec![
|
prompt_configs: vec![
|
||||||
PromptConfig {
|
PromptConfig {
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
use egui::{Ui, Color32, RichText, TextEdit, Button, ScrollArea};
|
use egui::{Ui, Color32, RichText, TextEdit, Button, ScrollArea};
|
||||||
use std::sync::mpsc;
|
|
||||||
use crate::config::AppSettings;
|
use crate::config::AppSettings;
|
||||||
use crate::api::call_single_llm;
|
use crate::api::call_llm;
|
||||||
|
|
||||||
pub enum OutputStatus {
|
pub enum OutputStatus {
|
||||||
Waiting,
|
Waiting,
|
||||||
Connecting,
|
Connecting,
|
||||||
Completed,
|
Completed,
|
||||||
Error(String),
|
Error(String),
|
||||||
|
Stopped,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MainPage {
|
pub struct MainPage {
|
||||||
pub input_text: String,
|
pub input_text: String,
|
||||||
pub output_texts: Vec<String>,
|
pub output_text: String,
|
||||||
pub statuses: Vec<OutputStatus>,
|
pub status: OutputStatus,
|
||||||
pub selected_prompt_index: usize,
|
pub selected_prompt_index: usize,
|
||||||
pub is_loading: Vec<bool>,
|
pub is_loading: bool,
|
||||||
pub result_receiver: Option<mpsc::Receiver<(usize, Result<String, String>)>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MainPage {
|
impl Default for MainPage {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
input_text: String::new(),
|
input_text: String::new(),
|
||||||
output_texts: vec![String::new(), String::new(), String::new()],
|
output_text: String::new(),
|
||||||
statuses: vec![OutputStatus::Waiting, OutputStatus::Waiting, OutputStatus::Waiting],
|
status: OutputStatus::Waiting,
|
||||||
selected_prompt_index: 0,
|
selected_prompt_index: 0,
|
||||||
is_loading: vec![false, false, false],
|
is_loading: false,
|
||||||
result_receiver: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,31 +36,11 @@ impl MainPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) {
|
pub fn ui(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) {
|
||||||
self.process_results();
|
|
||||||
|
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
self.render_content(ui, settings, ctx);
|
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) {
|
fn render_content(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) {
|
||||||
ui.add_space(16.0);
|
ui.add_space(16.0);
|
||||||
|
|
||||||
@@ -129,34 +107,24 @@ impl MainPage {
|
|||||||
ui.label(RichText::new("优化结果").size(11.0).color(Color32::GRAY));
|
ui.label(RichText::new("优化结果").size(11.0).color(Color32::GRAY));
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
|
|
||||||
let enabled_models: Vec<_> = settings.llm_configs.iter().filter(|c| c.enabled).collect();
|
let status_text = match &self.status {
|
||||||
|
|
||||||
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::Waiting => "等待发送".to_string(),
|
||||||
OutputStatus::Connecting => "连接中…".to_string(),
|
OutputStatus::Connecting => "连接中…".to_string(),
|
||||||
OutputStatus::Completed => "已完成".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::Waiting => Color32::from_rgb(100, 100, 255),
|
||||||
OutputStatus::Connecting => Color32::from_rgb(255, 165, 0),
|
OutputStatus::Connecting => Color32::from_rgb(255, 165, 0),
|
||||||
OutputStatus::Completed => Color32::from_rgb(0, 180, 0),
|
OutputStatus::Completed => Color32::from_rgb(0, 180, 0),
|
||||||
OutputStatus::Error(_) => Color32::RED,
|
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()
|
let card_frame = egui::Frame::none()
|
||||||
.fill(ui.style().visuals.widgets.inactive.bg_fill)
|
.fill(ui.style().visuals.widgets.inactive.bg_fill)
|
||||||
.stroke(egui::Stroke::new(1.0, Color32::from_rgb(200, 200, 210)))
|
.stroke(egui::Stroke::new(1.0, Color32::from_rgb(200, 200, 210)))
|
||||||
@@ -164,27 +132,16 @@ impl MainPage {
|
|||||||
.rounding(6.0);
|
.rounding(6.0);
|
||||||
|
|
||||||
card_frame.show(ui, |ui: &mut Ui| {
|
card_frame.show(ui, |ui: &mut Ui| {
|
||||||
ui.add_sized([300.0, 0.0], |ui: &mut Ui| {
|
ui.add_sized([ui.available_width(), 2.0], egui::Separator::default());
|
||||||
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_space(8.0);
|
||||||
|
|
||||||
ui.add_sized([300.0, 2.0], egui::Separator::default());
|
if self.output_text.is_empty() {
|
||||||
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));
|
ui.label(RichText::new("发送消息后结果将在此显示").size(13.0).color(Color32::GRAY));
|
||||||
} else {
|
} else {
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
[300.0, 150.0],
|
[ui.available_width(), 150.0],
|
||||||
TextEdit::multiline(&mut self.output_texts[index].clone())
|
TextEdit::multiline(&mut self.output_text.clone())
|
||||||
.desired_width(300.0)
|
.desired_width(f32::INFINITY)
|
||||||
.desired_rows(6),
|
.desired_rows(6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -197,38 +154,23 @@ impl MainPage {
|
|||||||
.stroke(egui::Stroke::new(1.0, Color32::from_rgb(100, 100, 255)))
|
.stroke(egui::Stroke::new(1.0, Color32::from_rgb(100, 100, 255)))
|
||||||
.rounding(4.0);
|
.rounding(4.0);
|
||||||
|
|
||||||
if ui.add(copy_btn).clicked() && !output_text.is_empty() {
|
if ui.add(copy_btn).clicked() && !self.output_text.is_empty() {
|
||||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
self.copy_to_clipboard();
|
||||||
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let enabled_models: Vec<_> = settings.llm_configs.iter().filter(|c| c.enabled).collect();
|
self.status = OutputStatus::Connecting;
|
||||||
if enabled_models.is_empty() {
|
self.output_text = "正在生成...".to_string();
|
||||||
return;
|
self.is_loading = true;
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
let prompt = if self.selected_prompt_index == 0 {
|
||||||
None
|
None
|
||||||
@@ -239,23 +181,18 @@ impl MainPage {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let input = self.input_text.clone();
|
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 handle = std::thread::spawn(move || {
|
||||||
let input = input.clone();
|
let result = call_llm(&settings, input, prompt);
|
||||||
let prompt = prompt.clone();
|
ctx.request_repaint();
|
||||||
let header_configs = header_configs.clone();
|
result
|
||||||
let model = model.clone();
|
});
|
||||||
let ctx = ctx.clone();
|
|
||||||
let tx = tx.clone();
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let result = call_single_llm(&model, input, prompt, &header_configs);
|
let _ = handle.join();
|
||||||
let _ = tx.send((idx, result));
|
|
||||||
ctx.request_repaint();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn select_prompt_by_name(&mut self, name: &str, settings: &AppSettings, ctx: &egui::Context) {
|
fn select_prompt_by_name(&mut self, name: &str, settings: &AppSettings, ctx: &egui::Context) {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
@@ -271,4 +208,10 @@ impl MainPage {
|
|||||||
self.send_request(settings, ctx.clone());
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,11 @@
|
|||||||
use egui::{Ui, Color32, RichText, ScrollArea};
|
use egui::{Ui, Color32, RichText, ScrollArea};
|
||||||
use crate::config::{AppSettings, LLMConfig, PromptConfig, ThemeMode};
|
use crate::config::{AppSettings, LLMConfig, PromptConfig, ThemeMode};
|
||||||
|
|
||||||
fn rand_bool() -> bool {
|
pub struct SettingsPage {
|
||||||
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 base_url: String,
|
pub base_url: String,
|
||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
pub enabled: bool,
|
pub show_api_key: bool,
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SettingsPage {
|
|
||||||
pub llm_configs: Vec<LLMConfigState>,
|
|
||||||
pub show_api_keys: Vec<bool>,
|
|
||||||
pub test_results: Vec<Option<bool>>,
|
|
||||||
pub selected_theme: ThemeMode,
|
pub selected_theme: ThemeMode,
|
||||||
pub new_prompt_title: String,
|
pub new_prompt_title: String,
|
||||||
pub new_prompt_content: String,
|
pub new_prompt_content: String,
|
||||||
@@ -30,31 +14,10 @@ pub struct SettingsPage {
|
|||||||
impl Default for SettingsPage {
|
impl Default for SettingsPage {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
llm_configs: vec![
|
|
||||||
LLMConfigState {
|
|
||||||
name: "模型1".to_string(),
|
|
||||||
base_url: "https://api.openai.com/v1".to_string(),
|
base_url: "https://api.openai.com/v1".to_string(),
|
||||||
api_key: String::new(),
|
api_key: String::new(),
|
||||||
model: "gpt-4o".to_string(),
|
model: "gpt-4o".to_string(),
|
||||||
enabled: true,
|
show_api_key: false,
|
||||||
},
|
|
||||||
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],
|
|
||||||
selected_theme: ThemeMode::FollowSystem,
|
selected_theme: ThemeMode::FollowSystem,
|
||||||
new_prompt_title: String::new(),
|
new_prompt_title: String::new(),
|
||||||
new_prompt_content: String::new(),
|
new_prompt_content: String::new(),
|
||||||
@@ -64,32 +27,11 @@ impl Default for SettingsPage {
|
|||||||
|
|
||||||
impl SettingsPage {
|
impl SettingsPage {
|
||||||
pub fn new(settings: &AppSettings) -> Self {
|
pub fn new(settings: &AppSettings) -> Self {
|
||||||
let config_count = settings.llm_configs.len();
|
|
||||||
let llm_configs: Vec<LLMConfigState> = (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 {
|
Self {
|
||||||
llm_configs,
|
base_url: settings.llm_config.base_url.clone(),
|
||||||
show_api_keys: vec![false, false, false],
|
api_key: settings.llm_config.api_key.clone(),
|
||||||
test_results: vec![None, None, None],
|
model: settings.llm_config.model.clone(),
|
||||||
|
show_api_key: false,
|
||||||
selected_theme: settings.theme_config.mode,
|
selected_theme: settings.theme_config.mode,
|
||||||
new_prompt_title: String::new(),
|
new_prompt_title: String::new(),
|
||||||
new_prompt_content: String::new(),
|
new_prompt_content: String::new(),
|
||||||
@@ -105,55 +47,27 @@ impl SettingsPage {
|
|||||||
ui.label(RichText::new("LLM 配置").size(14.0).strong());
|
ui.label(RichText::new("LLM 配置").size(14.0).strong());
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
for i in 0..3 {
|
ui.label("API Base URL");
|
||||||
let config = &mut self.llm_configs[i];
|
ui.text_edit_singleline(&mut self.base_url);
|
||||||
let show_key = &mut self.show_api_keys[i];
|
ui.add_space(6.0);
|
||||||
let test_result = &mut self.test_results[i];
|
|
||||||
|
|
||||||
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("名称");
|
|
||||||
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.label("API Key");
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if *show_key {
|
if self.show_api_key {
|
||||||
ui.text_edit_singleline(&mut config.api_key);
|
ui.text_edit_singleline(&mut self.api_key);
|
||||||
} else {
|
} else {
|
||||||
let masked = "*".repeat(config.api_key.len().max(4));
|
let mut masked = "*".repeat(self.api_key.len());
|
||||||
let mut masked_clone = masked.clone();
|
ui.text_edit_singleline(&mut masked);
|
||||||
ui.text_edit_singleline(&mut masked_clone);
|
|
||||||
}
|
}
|
||||||
if ui.button(if *show_key { "隐藏" } else { "显示" }).clicked() {
|
if ui.button(if self.show_api_key { "隐藏" } else { "显示" }).clicked() {
|
||||||
*show_key = !*show_key;
|
self.show_api_key = !self.show_api_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.add_space(6.0);
|
||||||
|
|
||||||
ui.label("Model");
|
ui.label("Model");
|
||||||
ui.text_edit_singleline(&mut config.model);
|
ui.text_edit_singleline(&mut self.model);
|
||||||
ui.add_space(4.0);
|
|
||||||
});
|
|
||||||
ui.add_space(12.0);
|
ui.add_space(12.0);
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
@@ -230,15 +144,11 @@ impl SettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_to_settings(&self, settings: &mut AppSettings) {
|
pub fn apply_to_settings(&self, settings: &mut AppSettings) {
|
||||||
settings.llm_configs = self.llm_configs.iter().map(|cfg| {
|
settings.llm_config = LLMConfig {
|
||||||
LLMConfig {
|
base_url: self.base_url.clone(),
|
||||||
name: cfg.name.clone(),
|
api_key: self.api_key.clone(),
|
||||||
base_url: cfg.base_url.clone(),
|
model: self.model.clone(),
|
||||||
api_key: cfg.api_key.clone(),
|
};
|
||||||
model: cfg.model.clone(),
|
|
||||||
enabled: cfg.enabled,
|
|
||||||
}
|
|
||||||
}).collect();
|
|
||||||
settings.theme_config.mode = self.selected_theme;
|
settings.theme_config.mode = self.selected_theme;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,16 +45,34 @@ data class ButtonConfig(val id: String, val label: String, val action: String, v
|
|||||||
|
|
||||||
class SecondActivity : AppCompatActivity() {
|
class SecondActivity : AppCompatActivity() {
|
||||||
|
|
||||||
// View references
|
// View references - Model 1
|
||||||
private lateinit var llModelList: LinearLayout
|
private lateinit var etBaseUrl1: EditText
|
||||||
private lateinit var btnAddModel: Button
|
private lateinit var etApiKey1: EditText
|
||||||
private lateinit var etBaseUrl: EditText
|
private lateinit var btnToggleApiKey1: ImageButton
|
||||||
private lateinit var etApiKey: EditText
|
private lateinit var etModel1: EditText
|
||||||
private lateinit var btnToggleApiKey: ImageButton
|
private lateinit var etModelName1: EditText
|
||||||
private lateinit var etModel: EditText
|
private lateinit var btnTestConnection1: Button
|
||||||
private lateinit var etModelName: EditText
|
private lateinit var tvTestStatus1: TextView
|
||||||
private lateinit var btnTestConnection: Button
|
|
||||||
private lateinit var tvTestStatus: 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 llHeadersList: LinearLayout
|
||||||
private lateinit var btnAddHeader: Button
|
private lateinit var btnAddHeader: Button
|
||||||
private lateinit var layoutHeaderContent: LinearLayout
|
private lateinit var layoutHeaderContent: LinearLayout
|
||||||
@@ -152,15 +170,32 @@ class SecondActivity : AppCompatActivity() {
|
|||||||
private fun initViews() {
|
private fun initViews() {
|
||||||
Log.d("SecondActivity", "initViews: Starting")
|
Log.d("SecondActivity", "initViews: Starting")
|
||||||
try {
|
try {
|
||||||
etBaseUrl = findViewById(R.id.etBaseUrl)
|
// Model 1
|
||||||
etApiKey = findViewById(R.id.etApiKey)
|
etBaseUrl1 = findViewById(R.id.etBaseUrl1)
|
||||||
btnToggleApiKey = findViewById(R.id.btnToggleApiKey)
|
etApiKey1 = findViewById(R.id.etApiKey1)
|
||||||
etModel = findViewById(R.id.etModel)
|
btnToggleApiKey1 = findViewById(R.id.btnToggleApiKey1)
|
||||||
etModelName = findViewById(R.id.etModelName)
|
etModel1 = findViewById(R.id.etModel1)
|
||||||
llModelList = findViewById(R.id.llModelList)
|
etModelName1 = findViewById(R.id.etModelName1)
|
||||||
btnAddModel = findViewById(R.id.btnAddModel)
|
btnTestConnection1 = findViewById(R.id.btnTestConnection1)
|
||||||
btnTestConnection = findViewById(R.id.btnTestConnection)
|
tvTestStatus1 = findViewById(R.id.tvTestStatus1)
|
||||||
tvTestStatus = findViewById(R.id.tvTestStatus)
|
|
||||||
|
// 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
|
// Header Section
|
||||||
llHeadersList = findViewById(R.id.llHeadersList)
|
llHeadersList = findViewById(R.id.llHeadersList)
|
||||||
@@ -190,18 +225,19 @@ class SecondActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
Log.d("SecondActivity", "initViews: All views found")
|
Log.d("SecondActivity", "initViews: All views found")
|
||||||
|
|
||||||
// Setup API key toggle
|
// Setup API key toggle for Model 1
|
||||||
btnToggleApiKey.setOnClickListener {
|
btnToggleApiKey1.setOnClickListener {
|
||||||
Log.d("SecondActivity", "API key toggle clicked")
|
toggleApiKeyVisibility(etApiKey1, btnToggleApiKey1)
|
||||||
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 2
|
||||||
|
btnToggleApiKey2.setOnClickListener {
|
||||||
|
toggleApiKeyVisibility(etApiKey2, btnToggleApiKey2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup API key toggle for Model 3
|
||||||
|
btnToggleApiKey3.setOnClickListener {
|
||||||
|
toggleApiKeyVisibility(etApiKey3, btnToggleApiKey3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup Header Toggle (Fold/Unfold)
|
// Setup Header Toggle (Fold/Unfold)
|
||||||
@@ -243,10 +279,10 @@ class SecondActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test connection button
|
// Test connection buttons
|
||||||
btnTestConnection.setOnClickListener {
|
btnTestConnection1.setOnClickListener { testConnection1() }
|
||||||
testConnection()
|
btnTestConnection2.setOnClickListener { testConnection2() }
|
||||||
}
|
btnTestConnection3.setOnClickListener { testConnection3() }
|
||||||
|
|
||||||
Log.d("SecondActivity", "initViews: Completed")
|
Log.d("SecondActivity", "initViews: Completed")
|
||||||
} catch (e: Exception) {
|
} 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) {
|
if (selectedLlmIndex >= llmConfigs.size) {
|
||||||
selectedLlmIndex = 0
|
selectedLlmIndex = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSelectedModelToFields()
|
loadConfigsToViews()
|
||||||
|
|
||||||
settings.noteApiConfig?.let { noteConfig ->
|
settings.noteApiConfig?.let { noteConfig ->
|
||||||
val apiTypes = listOf("Flomo", "Notion", "Joplin", "Custom")
|
val apiTypes = listOf("Flomo", "Notion", "Joplin", "Custom")
|
||||||
@@ -301,7 +346,7 @@ class SecondActivity : AppCompatActivity() {
|
|||||||
etNoteApiKey.setText(noteConfig.apiKey)
|
etNoteApiKey.setText(noteConfig.apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateApiKeyVisibility()
|
updateApiKeyVisibilityForAll()
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("SecondActivity", "loadConfigurations: Error parsing SettingsData", e)
|
Log.e("SecondActivity", "loadConfigurations: Error parsing SettingsData", e)
|
||||||
@@ -311,94 +356,78 @@ class SecondActivity : AppCompatActivity() {
|
|||||||
val oldConfigs = Gson().fromJson<List<APIConfig>>(json, type)
|
val oldConfigs = Gson().fromJson<List<APIConfig>>(json, type)
|
||||||
if (oldConfigs.isNotEmpty()) {
|
if (oldConfigs.isNotEmpty()) {
|
||||||
val oldConfig = oldConfigs[0]
|
val oldConfig = oldConfigs[0]
|
||||||
etBaseUrl.setText(oldConfig.url)
|
etBaseUrl1.setText(oldConfig.url)
|
||||||
etApiKey.setText(oldConfig.key)
|
etApiKey1.setText(oldConfig.key)
|
||||||
etModel.setText(oldConfig.model)
|
etModel1.setText(oldConfig.model)
|
||||||
updateApiKeyVisibility()
|
updateApiKeyVisibilityForAll()
|
||||||
}
|
}
|
||||||
} catch (e2: Exception) {
|
} catch (e2: Exception) {
|
||||||
Log.e("SecondActivity", "loadConfigurations: Error parsing List<APIConfig>", e2)
|
Log.e("SecondActivity", "loadConfigurations: Error parsing List<APIConfig>", e2)
|
||||||
etBaseUrl.setText("https://api.openai.com/v1")
|
setDefaultConfigs()
|
||||||
etModel.setText("gpt-4o")
|
|
||||||
updateApiKeyVisibility()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d("SecondActivity", "loadConfigurations: No saved config, using defaults")
|
Log.d("SecondActivity", "loadConfigurations: No saved config, using defaults")
|
||||||
etBaseUrl.setText("https://api.openai.com/v1")
|
setDefaultConfigs()
|
||||||
etModel.setText("gpt-4o")
|
|
||||||
updateApiKeyVisibility()
|
|
||||||
}
|
}
|
||||||
Log.d("SecondActivity", "loadConfigurations: Completed")
|
Log.d("SecondActivity", "loadConfigurations: Completed")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSelectedModelToFields() {
|
private fun loadConfigsToViews() {
|
||||||
if (llmConfigs.isNotEmpty() && selectedLlmIndex < llmConfigs.size) {
|
if (llmConfigs.size > 0) {
|
||||||
val config = llmConfigs[selectedLlmIndex]
|
val config1 = llmConfigs[0]
|
||||||
etBaseUrl.setText(config.baseUrl)
|
etBaseUrl1.setText(config1.baseUrl)
|
||||||
etApiKey.setText(config.apiKey)
|
etApiKey1.setText(config1.apiKey)
|
||||||
etModel.setText(config.model)
|
etModel1.setText(config1.model)
|
||||||
etModelName.setText(config.name)
|
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() {
|
private fun setDefaultConfigs() {
|
||||||
val isEmpty = etApiKey.text.toString().isEmpty()
|
etBaseUrl1.setText("https://api.openai.com/v1")
|
||||||
etApiKey.transformationMethod = if (isEmpty) null else PasswordTransformationMethod()
|
etModel1.setText("gpt-4o")
|
||||||
etApiKey.setSelection(etApiKey.text.length)
|
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() {
|
private fun updateApiKeyVisibilityForAll() {
|
||||||
llModelList.removeAllViews()
|
updateApiKeyVisibility(etApiKey1, btnToggleApiKey1)
|
||||||
for ((index, config) in llmConfigs.withIndex()) {
|
updateApiKeyVisibility(etApiKey2, btnToggleApiKey2)
|
||||||
val view = layoutInflater.inflate(R.layout.model_list_item, null)
|
updateApiKeyVisibility(etApiKey3, btnToggleApiKey3)
|
||||||
val tvName = view.findViewById<TextView>(R.id.tvModelName)
|
|
||||||
val btnEdit = view.findViewById<Button>(R.id.btnEditModel)
|
|
||||||
val btnDelete = view.findViewById<Button>(R.id.btnDeleteModel)
|
|
||||||
|
|
||||||
tvName.text = config.name.ifEmpty { "未命名" }
|
|
||||||
|
|
||||||
btnEdit.setOnClickListener {
|
|
||||||
selectedLlmIndex = index
|
|
||||||
loadSelectedModelToFields()
|
|
||||||
Toast.makeText(this, "正在编辑: ${config.name}", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
btnDelete.setOnClickListener {
|
private fun updateApiKeyVisibility(editText: EditText, button: ImageButton) {
|
||||||
if (llmConfigs.size <= 1) {
|
val isEmpty = editText.text.toString().isEmpty()
|
||||||
Toast.makeText(this, "至少保留一个配置", Toast.LENGTH_SHORT).show()
|
editText.transformationMethod = if (isEmpty) null else PasswordTransformationMethod()
|
||||||
return@setOnClickListener
|
editText.setSelection(editText.text.length)
|
||||||
}
|
|
||||||
llmConfigs.removeAt(index)
|
|
||||||
if (selectedLlmIndex >= llmConfigs.size) {
|
|
||||||
selectedLlmIndex = llmConfigs.size - 1
|
|
||||||
}
|
|
||||||
loadSelectedModelToFields()
|
|
||||||
refreshModelList()
|
|
||||||
Toast.makeText(this, "配置已删除", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
llModelList.addView(view)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUI() {
|
private fun setupUI() {
|
||||||
refreshModelList()
|
|
||||||
|
|
||||||
btnAddModel.setOnClickListener {
|
|
||||||
val newName = "新配置 ${llmConfigs.size + 1}"
|
|
||||||
val newConfig = LLMConfig(
|
|
||||||
name = newName,
|
|
||||||
baseUrl = etBaseUrl.text.toString().ifEmpty { "https://api.openai.com/v1" },
|
|
||||||
apiKey = "",
|
|
||||||
model = "gpt-4o"
|
|
||||||
)
|
|
||||||
llmConfigs.add(newConfig)
|
|
||||||
selectedLlmIndex = llmConfigs.size - 1
|
|
||||||
loadSelectedModelToFields()
|
|
||||||
refreshModelList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup headers
|
// Setup headers
|
||||||
llHeadersList.removeAllViews()
|
llHeadersList.removeAllViews()
|
||||||
if (headerConfigs.isEmpty()) {
|
if (headerConfigs.isEmpty()) {
|
||||||
@@ -453,20 +482,28 @@ class SecondActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testConnection() {
|
private fun toggleApiKeyVisibility(editText: EditText, button: ImageButton) {
|
||||||
val baseUrl = etBaseUrl.text.toString().trim()
|
val isPassword = editText.transformationMethod is PasswordTransformationMethod
|
||||||
val apiKey = etApiKey.text.toString().trim()
|
editText.transformationMethod = if (isPassword) null else PasswordTransformationMethod()
|
||||||
val model = etModel.text.toString().trim()
|
editText.setSelection(editText.text.length)
|
||||||
|
|
||||||
|
if (isPassword) {
|
||||||
|
button.setImageResource(android.R.drawable.ic_menu_view)
|
||||||
|
} else {
|
||||||
|
button.setImageResource(android.R.drawable.ic_lock_idle_lock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testConnection(baseUrl: String, apiKey: String, model: String, statusView: TextView, button: Button) {
|
||||||
if (baseUrl.isEmpty()) {
|
if (baseUrl.isEmpty()) {
|
||||||
tvTestStatus.text = "错误: Base URL 不能为空"
|
statusView.text = "错误: Base URL 不能为空"
|
||||||
tvTestStatus.setTextColor(ContextCompat.getColor(this, R.color.error))
|
statusView.setTextColor(ContextCompat.getColor(this, R.color.error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tvTestStatus.text = "测试中..."
|
statusView.text = "测试中..."
|
||||||
tvTestStatus.setTextColor(ContextCompat.getColor(this, R.color.warning))
|
statusView.setTextColor(ContextCompat.getColor(this, R.color.warning))
|
||||||
btnTestConnection.isEnabled = false
|
button.isEnabled = false
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
@@ -499,7 +536,7 @@ class SecondActivity : AppCompatActivity() {
|
|||||||
val response = client.newCall(request).execute()
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
btnTestConnection.isEnabled = true
|
button.isEnabled = true
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val responseBody = response.body?.string()
|
val responseBody = response.body?.string()
|
||||||
val responseJson = org.json.JSONObject(responseBody ?: "")
|
val responseJson = org.json.JSONObject(responseBody ?: "")
|
||||||
@@ -507,27 +544,57 @@ class SecondActivity : AppCompatActivity() {
|
|||||||
if (choices != null && choices.length() > 0) {
|
if (choices != null && choices.length() > 0) {
|
||||||
val message = choices.getJSONObject(0).optJSONObject("message")
|
val message = choices.getJSONObject(0).optJSONObject("message")
|
||||||
val content = message?.optString("content", "") ?: ""
|
val content = message?.optString("content", "") ?: ""
|
||||||
tvTestStatus.text = "连接成功: ${content.take(20)}"
|
statusView.text = "连接成功: ${content.take(20)}"
|
||||||
tvTestStatus.setTextColor(ContextCompat.getColor(this@SecondActivity, R.color.success))
|
statusView.setTextColor(ContextCompat.getColor(this@SecondActivity, R.color.success))
|
||||||
} else {
|
} else {
|
||||||
tvTestStatus.text = "连接成功,但返回格式异常"
|
statusView.text = "连接成功,但返回格式异常"
|
||||||
tvTestStatus.setTextColor(ContextCompat.getColor(this@SecondActivity, R.color.warning))
|
statusView.setTextColor(ContextCompat.getColor(this@SecondActivity, R.color.warning))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tvTestStatus.text = "连接失败: ${response.code} ${response.message}"
|
statusView.text = "连接失败: ${response.code} ${response.message}"
|
||||||
tvTestStatus.setTextColor(ContextCompat.getColor(this@SecondActivity, R.color.error))
|
statusView.setTextColor(ContextCompat.getColor(this@SecondActivity, R.color.error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
btnTestConnection.isEnabled = true
|
button.isEnabled = true
|
||||||
tvTestStatus.text = "连接失败: ${e.message}"
|
statusView.text = "连接失败: ${e.message}"
|
||||||
tvTestStatus.setTextColor(ContextCompat.getColor(this@SecondActivity, R.color.error))
|
statusView.setTextColor(ContextCompat.getColor(this@SecondActivity, R.color.error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun testConnection1() {
|
||||||
|
testConnection(
|
||||||
|
etBaseUrl1.text.toString().trim(),
|
||||||
|
etApiKey1.text.toString().trim(),
|
||||||
|
etModel1.text.toString().trim(),
|
||||||
|
tvTestStatus1,
|
||||||
|
btnTestConnection1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testConnection2() {
|
||||||
|
testConnection(
|
||||||
|
etBaseUrl2.text.toString().trim(),
|
||||||
|
etApiKey2.text.toString().trim(),
|
||||||
|
etModel2.text.toString().trim(),
|
||||||
|
tvTestStatus2,
|
||||||
|
btnTestConnection2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testConnection3() {
|
||||||
|
testConnection(
|
||||||
|
etBaseUrl3.text.toString().trim(),
|
||||||
|
etApiKey3.text.toString().trim(),
|
||||||
|
etModel3.text.toString().trim(),
|
||||||
|
tvTestStatus3,
|
||||||
|
btnTestConnection3
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun addHeaderEntry(key: String = "", value: String = "") {
|
private fun addHeaderEntry(key: String = "", value: String = "") {
|
||||||
val view = layoutInflater.inflate(R.layout.header_entry, null)
|
val view = layoutInflater.inflate(R.layout.header_entry, null)
|
||||||
val etKey = view.findViewById<EditText>(R.id.etHeaderKey)
|
val etKey = view.findViewById<EditText>(R.id.etHeaderKey)
|
||||||
@@ -567,23 +634,26 @@ class SecondActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveConfigurations() {
|
private fun saveConfigurations() {
|
||||||
if (llmConfigs.isNotEmpty() && selectedLlmIndex < llmConfigs.size) {
|
// Update LLM configs from views
|
||||||
val currentConfig = llmConfigs[selectedLlmIndex]
|
llmConfigs.clear()
|
||||||
llmConfigs[selectedLlmIndex] = currentConfig.copy(
|
|
||||||
name = etModelName.text.toString().ifEmpty { "未命名" },
|
|
||||||
baseUrl = etBaseUrl.text.toString(),
|
|
||||||
apiKey = etApiKey.text.toString(),
|
|
||||||
model = etModel.text.toString()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
llmConfigs.add(LLMConfig(
|
llmConfigs.add(LLMConfig(
|
||||||
name = etModelName.text.toString().ifEmpty { "默认配置" },
|
name = etModelName1.text.toString().ifEmpty { "默认配置" },
|
||||||
baseUrl = etBaseUrl.text.toString(),
|
baseUrl = etBaseUrl1.text.toString(),
|
||||||
apiKey = etApiKey.text.toString(),
|
apiKey = etApiKey1.text.toString(),
|
||||||
model = etModel.text.toString()
|
model = etModel1.text.toString()
|
||||||
|
))
|
||||||
|
llmConfigs.add(LLMConfig(
|
||||||
|
name = etModelName2.text.toString().ifEmpty { "配置2" },
|
||||||
|
baseUrl = etBaseUrl2.text.toString(),
|
||||||
|
apiKey = etApiKey2.text.toString(),
|
||||||
|
model = etModel2.text.toString()
|
||||||
|
))
|
||||||
|
llmConfigs.add(LLMConfig(
|
||||||
|
name = etModelName3.text.toString().ifEmpty { "配置3" },
|
||||||
|
baseUrl = etBaseUrl3.text.toString(),
|
||||||
|
apiKey = etApiKey3.text.toString(),
|
||||||
|
model = etModel3.text.toString()
|
||||||
))
|
))
|
||||||
selectedLlmIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
headerConfigs.clear()
|
headerConfigs.clear()
|
||||||
for (i in 0 until llHeadersList.childCount) {
|
for (i in 0 until llHeadersList.childCount) {
|
||||||
@@ -629,10 +699,10 @@ class SecondActivity : AppCompatActivity() {
|
|||||||
apiConfig = APIConfig(
|
apiConfig = APIConfig(
|
||||||
System.currentTimeMillis(),
|
System.currentTimeMillis(),
|
||||||
"llm-config",
|
"llm-config",
|
||||||
etBaseUrl.text.toString(),
|
etBaseUrl1.text.toString(),
|
||||||
etApiKey.text.toString(),
|
etApiKey1.text.toString(),
|
||||||
"",
|
"",
|
||||||
etModel.text.toString()
|
etModel1.text.toString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user