feat: 多LLM支持 - 3个模型并发请求
This commit is contained in:
@@ -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]
|
||||
|
||||
12
flomo-ai-desktop/build.bat
Normal file
12
flomo-ai-desktop/build.bat
Normal file
@@ -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
|
||||
@@ -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<ChatChoice>,
|
||||
}
|
||||
|
||||
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 {
|
||||
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<String, String>,
|
||||
}
|
||||
|
||||
pub fn call_single_llm(config: &LLMConfig, user_input: String, header_configs: &[HeaderConfig]) -> Result<String, String> {
|
||||
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<String>,
|
||||
) -> Vec<ModelResult> {
|
||||
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::<ModelResult>();
|
||||
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<String, String> {
|
||||
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)
|
||||
}
|
||||
@@ -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<std::thread::JoinHandle<Result<String, String>>>,
|
||||
|
||||
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<ModelDisplay>,
|
||||
pending_results: Option<std::thread::JoinHandle<Vec<ModelResult>>>,
|
||||
|
||||
settings_selected_theme: ThemeMode,
|
||||
new_prompt_title: String,
|
||||
new_prompt_content: String,
|
||||
|
||||
test_status: String,
|
||||
test_is_loading: bool,
|
||||
pending_test: Option<std::thread::JoinHandle<Result<String, String>>>,
|
||||
|
||||
|
||||
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<ModelDisplay> = 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::<String>());
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<LLMConfig>,
|
||||
}
|
||||
|
||||
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<HeaderConfig>,
|
||||
pub prompt_configs: Vec<PromptConfig>,
|
||||
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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod main_page;
|
||||
pub mod settings_page;
|
||||
|
||||
pub use main_page::OutputStatus;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user