use egui::{Ui, Color32, RichText, TextEdit, Button, ScrollArea}; use std::sync::mpsc; use crate::config::AppSettings; use crate::api::call_single_llm; pub enum OutputStatus { Waiting, Connecting, Completed, Error(String), } pub struct MainPage { pub input_text: String, pub output_texts: Vec, pub statuses: Vec, pub selected_prompt_index: usize, pub is_loading: Vec, pub result_receiver: Option)>>, } impl Default for MainPage { fn default() -> Self { Self { input_text: String::new(), output_texts: vec![String::new(), String::new(), String::new()], statuses: vec![OutputStatus::Waiting, OutputStatus::Waiting, OutputStatus::Waiting], selected_prompt_index: 0, is_loading: vec![false, false, false], result_receiver: None, } } } impl MainPage { pub fn new() -> Self { Self::default() } pub fn ui(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) { self.process_results(); ScrollArea::vertical().show(ui, |ui| { self.render_content(ui, settings, ctx); }); } fn process_results(&mut self) { if let Some(ref receiver) = self.result_receiver { while let Ok((index, result)) = receiver.try_recv() { self.is_loading[index] = false; match result { Ok(text) => { self.output_texts[index] = text; self.statuses[index] = OutputStatus::Completed; } Err(e) => { self.output_texts[index] = e.clone(); self.statuses[index] = OutputStatus::Error(e); } } } } } fn render_content(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) { ui.add_space(16.0); 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 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::Connecting => "连接中…".to_string(), OutputStatus::Completed => "已完成".to_string(), OutputStatus::Error(e) => format!("错误: {}", e), }; let status_color = match &self.statuses.get(index).unwrap_or(&OutputStatus::Waiting) { 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, }; 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([300.0, 0.0], |ui: &mut Ui| { ui.set_width(300.0); ui.label(RichText::new(model_name).size(14.0).strong()); ui.add_space(4.0); ui.label(RichText::new(&status_text).size(11.0).color(status_color)); ui.add_space(8.0); ui.add_sized([300.0, 2.0], egui::Separator::default()); ui.add_space(8.0); let output_text = self.output_texts.get(index).map(|s| s.as_str()).unwrap_or(""); if output_text.is_empty() { ui.label(RichText::new("发送消息后结果将在此显示").size(13.0).color(Color32::GRAY)); } else { ui.add_sized( [300.0, 150.0], TextEdit::multiline(&mut self.output_texts[index].clone()) .desired_width(300.0) .desired_rows(6), ); } ui.add_space(8.0); ui.horizontal(|ui: &mut Ui| { let copy_btn = Button::new("复制结果") .fill(Color32::TRANSPARENT) .stroke(egui::Stroke::new(1.0, Color32::from_rgb(100, 100, 255))) .rounding(4.0); if ui.add(copy_btn).clicked() && !output_text.is_empty() { if let Ok(mut clipboard) = arboard::Clipboard::new() { let _ = clipboard.set_text(output_text); } } }); ui }); }); ui.add_space(8.0); } pub fn send_request(&mut self, settings: &AppSettings, ctx: egui::Context) { if self.input_text.trim().is_empty() { return; } let enabled_models: Vec<_> = settings.llm_configs.iter().filter(|c| c.enabled).collect(); if enabled_models.is_empty() { return; } for i in 0..self.statuses.len() { self.statuses[i] = OutputStatus::Connecting; self.output_texts[i] = "正在生成...".to_string(); self.is_loading[i] = true; } let (tx, rx) = mpsc::channel(); self.result_receiver = Some(rx); 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 header_configs = settings.header_configs.clone(); for (idx, model) in enabled_models.iter().enumerate() { let input = input.clone(); let prompt = prompt.clone(); let header_configs = header_configs.clone(); let model = model.clone(); let ctx = ctx.clone(); let tx = tx.clone(); std::thread::spawn(move || { 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) { 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()); } } }