feat: parallel LLM calls with 3 result cards
This commit is contained in:
@@ -17,9 +17,11 @@ 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)]
|
||||||
@@ -36,7 +38,7 @@ pub enum ThemeMode {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AppSettings {
|
pub struct AppSettings {
|
||||||
pub llm_config: LLMConfig,
|
pub llm_configs: Vec<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,
|
||||||
@@ -45,11 +47,29 @@ pub struct AppSettings {
|
|||||||
impl Default for AppSettings {
|
impl Default for AppSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
llm_config: LLMConfig {
|
llm_configs: vec![
|
||||||
|
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,31 +1,33 @@
|
|||||||
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_llm;
|
use crate::api::call_single_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_text: String,
|
pub output_texts: Vec<String>,
|
||||||
pub status: OutputStatus,
|
pub statuses: Vec<OutputStatus>,
|
||||||
pub selected_prompt_index: usize,
|
pub selected_prompt_index: usize,
|
||||||
pub is_loading: bool,
|
pub is_loading: Vec<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_text: String::new(),
|
output_texts: vec![String::new(), String::new(), String::new()],
|
||||||
status: OutputStatus::Waiting,
|
statuses: vec![OutputStatus::Waiting, OutputStatus::Waiting, OutputStatus::Waiting],
|
||||||
selected_prompt_index: 0,
|
selected_prompt_index: 0,
|
||||||
is_loading: false,
|
is_loading: vec![false, false, false],
|
||||||
|
result_receiver: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,11 +38,31 @@ impl MainPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) {
|
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);
|
||||||
|
|
||||||
@@ -107,24 +129,34 @@ 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 status_text = match &self.status {
|
let enabled_models: Vec<_> = settings.llm_configs.iter().filter(|c| c.enabled).collect();
|
||||||
|
|
||||||
|
ScrollArea::horizontal().show(ui, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
for (idx, model) in enabled_models.iter().enumerate() {
|
||||||
|
self.render_result_card(ui, idx, &model.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(16.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_result_card(&mut self, ui: &mut Ui, index: usize, model_name: &str) {
|
||||||
|
let status_text = match &self.statuses.get(index).unwrap_or(&OutputStatus::Waiting) {
|
||||||
OutputStatus::Waiting => "等待发送".to_string(),
|
OutputStatus::Waiting => "等待发送".to_string(),
|
||||||
OutputStatus::Connecting => "连接中…".to_string(),
|
OutputStatus::Connecting => "连接中…".to_string(),
|
||||||
OutputStatus::Completed => "已完成".to_string(),
|
OutputStatus::Completed => "已完成".to_string(),
|
||||||
OutputStatus::Error(_) => "发生错误".to_string(),
|
OutputStatus::Error(e) => format!("错误: {}", e),
|
||||||
OutputStatus::Stopped => "已停止".to_string(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let status_color = match &self.status {
|
let status_color = match &self.statuses.get(index).unwrap_or(&OutputStatus::Waiting) {
|
||||||
OutputStatus::Waiting => Color32::from_rgb(100, 100, 255),
|
OutputStatus::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)))
|
||||||
@@ -132,16 +164,27 @@ 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([ui.available_width(), 2.0], egui::Separator::default());
|
ui.add_sized([300.0, 0.0], |ui: &mut Ui| {
|
||||||
|
ui.set_width(300.0);
|
||||||
|
|
||||||
|
ui.label(RichText::new(model_name).size(14.0).strong());
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
ui.label(RichText::new(&status_text).size(11.0).color(status_color));
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
if self.output_text.is_empty() {
|
ui.add_sized([300.0, 2.0], egui::Separator::default());
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
let output_text = self.output_texts.get(index).map(|s| s.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
if output_text.is_empty() {
|
||||||
ui.label(RichText::new("发送消息后结果将在此显示").size(13.0).color(Color32::GRAY));
|
ui.label(RichText::new("发送消息后结果将在此显示").size(13.0).color(Color32::GRAY));
|
||||||
} else {
|
} else {
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
[ui.available_width(), 150.0],
|
[300.0, 150.0],
|
||||||
TextEdit::multiline(&mut self.output_text.clone())
|
TextEdit::multiline(&mut self.output_texts[index].clone())
|
||||||
.desired_width(f32::INFINITY)
|
.desired_width(300.0)
|
||||||
.desired_rows(6),
|
.desired_rows(6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -154,23 +197,38 @@ 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() && !self.output_text.is_empty() {
|
if ui.add(copy_btn).clicked() && !output_text.is_empty() {
|
||||||
self.copy_to_clipboard();
|
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||||
|
let _ = clipboard.set_text(output_text);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(16.0);
|
ui.add_space(8.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_request(&mut self, settings: &AppSettings, ctx: egui::Context) {
|
pub fn send_request(&mut self, settings: &AppSettings, ctx: egui::Context) {
|
||||||
if self.input_text.trim().is_empty() || self.is_loading {
|
if self.input_text.trim().is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.status = OutputStatus::Connecting;
|
let enabled_models: Vec<_> = settings.llm_configs.iter().filter(|c| c.enabled).collect();
|
||||||
self.output_text = "正在生成...".to_string();
|
if enabled_models.is_empty() {
|
||||||
self.is_loading = true;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..self.statuses.len() {
|
||||||
|
self.statuses[i] = OutputStatus::Connecting;
|
||||||
|
self.output_texts[i] = "正在生成...".to_string();
|
||||||
|
self.is_loading[i] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
self.result_receiver = Some(rx);
|
||||||
|
|
||||||
let prompt = if self.selected_prompt_index == 0 {
|
let prompt = if self.selected_prompt_index == 0 {
|
||||||
None
|
None
|
||||||
@@ -181,18 +239,23 @@ impl MainPage {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let input = self.input_text.clone();
|
let input = self.input_text.clone();
|
||||||
let settings = settings.clone();
|
let header_configs = settings.header_configs.clone();
|
||||||
|
|
||||||
let handle = std::thread::spawn(move || {
|
for (idx, model) in enabled_models.iter().enumerate() {
|
||||||
let result = call_llm(&settings, input, prompt);
|
let input = input.clone();
|
||||||
ctx.request_repaint();
|
let prompt = prompt.clone();
|
||||||
result
|
let header_configs = header_configs.clone();
|
||||||
});
|
let model = model.clone();
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let tx = tx.clone();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let _ = handle.join();
|
let result = call_single_llm(&model, input, prompt, &header_configs);
|
||||||
|
let _ = tx.send((idx, result));
|
||||||
|
ctx.request_repaint();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn select_prompt_by_name(&mut self, name: &str, settings: &AppSettings, ctx: &egui::Context) {
|
fn select_prompt_by_name(&mut self, name: &str, settings: &AppSettings, ctx: &egui::Context) {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
@@ -208,10 +271,4 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user