1use super::api::{create_openai_client, to_openai_messages};
2use super::model::{
3 AgentConfig, ChatMessage, ChatSession, ModelProvider, load_agent_config, load_chat_session,
4 save_agent_config, save_chat_session,
5};
6use super::theme::Theme;
7use crate::util::log::write_error_log;
8use async_openai::types::chat::CreateChatCompletionRequestArgs;
9use futures::StreamExt;
10use ratatui::text::Line;
11use ratatui::widgets::ListState;
12use std::sync::{Arc, Mutex, mpsc};
13
14pub enum StreamMsg {
18 Chunk,
20 Done,
22 Error(String),
24}
25
26pub struct ChatApp {
28 pub agent_config: AgentConfig,
30 pub session: ChatSession,
32 pub input: String,
34 pub cursor_pos: usize,
36 pub mode: ChatMode,
38 pub scroll_offset: u16,
40 pub is_loading: bool,
42 pub model_list_state: ListState,
44 pub toast: Option<(String, bool, std::time::Instant)>,
46 pub stream_rx: Option<mpsc::Receiver<StreamMsg>>,
48 pub streaming_content: Arc<Mutex<String>>,
50 pub msg_lines_cache: Option<MsgLinesCache>,
53 pub browse_msg_index: usize,
55 pub last_rendered_streaming_len: usize,
57 pub last_stream_render_time: std::time::Instant,
59 pub config_provider_idx: usize,
61 pub config_field_idx: usize,
63 pub config_editing: bool,
65 pub config_edit_buf: String,
67 pub config_edit_cursor: usize,
69 pub auto_scroll: bool,
71 pub theme: Theme,
73 pub archives: Vec<super::archive::ChatArchive>,
75 pub archive_list_index: usize,
77 pub archive_default_name: String,
79 pub archive_custom_name: String,
81 pub archive_editing_name: bool,
83 pub archive_edit_cursor: usize,
85 pub restore_confirm_needed: bool,
87}
88
89pub struct MsgLinesCache {
91 pub msg_count: usize,
93 pub last_msg_len: usize,
95 pub streaming_len: usize,
97 pub is_loading: bool,
99 pub bubble_max_width: usize,
101 pub browse_index: Option<usize>,
103 pub lines: Vec<Line<'static>>,
105 pub msg_start_lines: Vec<(usize, usize)>, pub per_msg_lines: Vec<PerMsgCache>,
109 pub streaming_stable_lines: Vec<Line<'static>>,
111 pub streaming_stable_offset: usize,
113}
114
115pub struct PerMsgCache {
117 pub content_len: usize,
119 pub lines: Vec<Line<'static>>,
121 pub msg_index: usize,
123}
124
125pub const TOAST_DURATION_SECS: u64 = 4;
127
128#[derive(PartialEq)]
129pub enum ChatMode {
130 Chat,
132 SelectModel,
134 Browse,
136 Help,
138 Config,
140 ArchiveConfirm,
142 ArchiveList,
144}
145
146pub const CONFIG_FIELDS: &[&str] = &["name", "api_base", "api_key", "model"];
148pub const CONFIG_GLOBAL_FIELDS: &[&str] = &[
150 "system_prompt",
151 "stream_mode",
152 "max_history_messages",
153 "theme",
154];
155pub fn config_total_fields() -> usize {
157 CONFIG_FIELDS.len() + CONFIG_GLOBAL_FIELDS.len()
158}
159
160impl ChatApp {
161 pub fn new() -> Self {
162 let agent_config = load_agent_config();
163 let session = load_chat_session();
164 let mut model_list_state = ListState::default();
165 if !agent_config.providers.is_empty() {
166 model_list_state.select(Some(agent_config.active_index));
167 }
168 let theme = Theme::from_name(&agent_config.theme);
169 Self {
170 agent_config,
171 session,
172 input: String::new(),
173 cursor_pos: 0,
174 mode: ChatMode::Chat,
175 scroll_offset: u16::MAX, is_loading: false,
177 model_list_state,
178 toast: None,
179 stream_rx: None,
180 streaming_content: Arc::new(Mutex::new(String::new())),
181 msg_lines_cache: None,
182 browse_msg_index: 0,
183 last_rendered_streaming_len: 0,
184 last_stream_render_time: std::time::Instant::now(),
185 config_provider_idx: 0,
186 config_field_idx: 0,
187 config_editing: false,
188 config_edit_buf: String::new(),
189 config_edit_cursor: 0,
190 auto_scroll: true,
191 theme,
192 archives: Vec::new(),
193 archive_list_index: 0,
194 archive_default_name: String::new(),
195 archive_custom_name: String::new(),
196 archive_editing_name: false,
197 archive_edit_cursor: 0,
198 restore_confirm_needed: false,
199 }
200 }
201
202 pub fn switch_theme(&mut self) {
204 self.agent_config.theme = self.agent_config.theme.next();
205 self.theme = Theme::from_name(&self.agent_config.theme);
206 self.msg_lines_cache = None; }
208
209 pub fn show_toast(&mut self, msg: impl Into<String>, is_error: bool) {
211 self.toast = Some((msg.into(), is_error, std::time::Instant::now()));
212 }
213
214 pub fn tick_toast(&mut self) {
216 if let Some((_, _, created)) = &self.toast {
217 if created.elapsed().as_secs() >= TOAST_DURATION_SECS {
218 self.toast = None;
219 }
220 }
221 }
222
223 pub fn active_provider(&self) -> Option<&ModelProvider> {
225 if self.agent_config.providers.is_empty() {
226 return None;
227 }
228 let idx = self
229 .agent_config
230 .active_index
231 .min(self.agent_config.providers.len() - 1);
232 Some(&self.agent_config.providers[idx])
233 }
234
235 pub fn active_model_name(&self) -> String {
237 self.active_provider()
238 .map(|p| p.name.clone())
239 .unwrap_or_else(|| "未配置".to_string())
240 }
241
242 pub fn build_api_messages(&self) -> Vec<ChatMessage> {
244 let mut messages = Vec::new();
245 if let Some(sys) = &self.agent_config.system_prompt {
246 messages.push(ChatMessage {
247 role: "system".to_string(),
248 content: sys.clone(),
249 });
250 }
251
252 let max_history = self.agent_config.max_history_messages;
254 let history_messages: Vec<_> = if self.session.messages.len() > max_history {
255 self.session.messages[self.session.messages.len() - max_history..].to_vec()
256 } else {
257 self.session.messages.clone()
258 };
259
260 for msg in history_messages {
261 messages.push(msg);
262 }
263 messages
264 }
265
266 pub fn send_message(&mut self) {
268 let text = self.input.trim().to_string();
269 if text.is_empty() {
270 return;
271 }
272
273 self.session.messages.push(ChatMessage {
275 role: "user".to_string(),
276 content: text,
277 });
278 self.input.clear();
279 self.cursor_pos = 0;
280 self.auto_scroll = true;
282 self.scroll_offset = u16::MAX;
283
284 let provider = match self.active_provider() {
286 Some(p) => p.clone(),
287 None => {
288 self.show_toast("未配置模型提供方,请先编辑配置文件", true);
289 return;
290 }
291 };
292
293 self.is_loading = true;
294 self.last_rendered_streaming_len = 0;
296 self.last_stream_render_time = std::time::Instant::now();
297 self.msg_lines_cache = None;
298
299 let api_messages = self.build_api_messages();
300
301 {
303 let mut sc = self.streaming_content.lock().unwrap();
304 sc.clear();
305 }
306
307 let (tx, rx) = mpsc::channel::<StreamMsg>();
309 self.stream_rx = Some(rx);
310
311 let streaming_content = Arc::clone(&self.streaming_content);
312
313 let use_stream = self.agent_config.stream_mode;
314
315 std::thread::spawn(move || {
317 let rt = match tokio::runtime::Runtime::new() {
318 Ok(rt) => rt,
319 Err(e) => {
320 let _ = tx.send(StreamMsg::Error(format!("创建异步运行时失败: {}", e)));
321 return;
322 }
323 };
324
325 rt.block_on(async {
326 let client = create_openai_client(&provider);
327 let openai_messages = to_openai_messages(&api_messages);
328
329 let request = match CreateChatCompletionRequestArgs::default()
330 .model(&provider.model)
331 .messages(openai_messages)
332 .build()
333 {
334 Ok(req) => req,
335 Err(e) => {
336 let _ = tx.send(StreamMsg::Error(format!("构建请求失败: {}", e)));
337 return;
338 }
339 };
340
341 if use_stream {
342 let mut stream = match client.chat().create_stream(request).await {
344 Ok(s) => s,
345 Err(e) => {
346 let error_msg = format!("API 请求失败: {}", e);
347 write_error_log("Chat API 流式请求创建", &error_msg);
348 let _ = tx.send(StreamMsg::Error(error_msg));
349 return;
350 }
351 };
352
353 while let Some(result) = stream.next().await {
354 match result {
355 Ok(response) => {
356 for choice in &response.choices {
357 if let Some(ref content) = choice.delta.content {
358 {
360 let mut sc = streaming_content.lock().unwrap();
361 sc.push_str(content);
362 }
363 let _ = tx.send(StreamMsg::Chunk);
364 }
365 }
366 }
367 Err(e) => {
368 let error_str = format!("{}", e);
369 write_error_log("Chat API 流式响应", &error_str);
370 let _ = tx.send(StreamMsg::Error(error_str));
371 return;
372 }
373 }
374 }
375 } else {
376 match client.chat().create(request).await {
378 Ok(response) => {
379 if let Some(choice) = response.choices.first() {
380 if let Some(ref content) = choice.message.content {
381 {
382 let mut sc = streaming_content.lock().unwrap();
383 sc.push_str(content);
384 }
385 let _ = tx.send(StreamMsg::Chunk);
386 }
387 }
388 }
389 Err(e) => {
390 let error_msg = format!("API 请求失败: {}", e);
391 write_error_log("Chat API 非流式请求", &error_msg);
392 let _ = tx.send(StreamMsg::Error(error_msg));
393 return;
394 }
395 }
396 }
397
398 let _ = tx.send(StreamMsg::Done);
399
400 let _ = tx.send(StreamMsg::Done);
401 });
402 });
403 }
404
405 pub fn poll_stream(&mut self) {
407 if self.stream_rx.is_none() {
408 return;
409 }
410
411 let mut finished = false;
412 let mut had_error = false;
413
414 if let Some(ref rx) = self.stream_rx {
416 loop {
417 match rx.try_recv() {
418 Ok(StreamMsg::Chunk) => {
419 if self.auto_scroll {
422 self.scroll_offset = u16::MAX;
423 }
424 }
425 Ok(StreamMsg::Done) => {
426 finished = true;
427 break;
428 }
429 Ok(StreamMsg::Error(e)) => {
430 self.show_toast(format!("请求失败: {}", e), true);
431 had_error = true;
432 finished = true;
433 break;
434 }
435 Err(mpsc::TryRecvError::Empty) => break,
436 Err(mpsc::TryRecvError::Disconnected) => {
437 finished = true;
438 break;
439 }
440 }
441 }
442 }
443
444 if finished {
445 self.stream_rx = None;
446 self.is_loading = false;
447 self.last_rendered_streaming_len = 0;
449 self.msg_lines_cache = None;
451
452 if !had_error {
453 let content = {
455 let sc = self.streaming_content.lock().unwrap();
456 sc.clone()
457 };
458 if !content.is_empty() {
459 self.session.messages.push(ChatMessage {
460 role: "assistant".to_string(),
461 content,
462 });
463 self.streaming_content.lock().unwrap().clear();
465 self.show_toast("回复完成 ✓", false);
466 }
467 if self.auto_scroll {
468 self.scroll_offset = u16::MAX;
469 }
470 } else {
471 self.streaming_content.lock().unwrap().clear();
473 }
474
475 let _ = save_chat_session(&self.session);
477 }
478 }
479
480 pub fn clear_session(&mut self) {
482 self.session.messages.clear();
483 self.scroll_offset = 0;
484 self.msg_lines_cache = None; let _ = save_chat_session(&self.session);
486 self.show_toast("对话已清空", false);
487 }
488
489 pub fn switch_model(&mut self) {
491 if let Some(sel) = self.model_list_state.selected() {
492 self.agent_config.active_index = sel;
493 let _ = save_agent_config(&self.agent_config);
494 let name = self.active_model_name();
495 self.show_toast(format!("已切换到: {}", name), false);
496 }
497 self.mode = ChatMode::Chat;
498 }
499
500 pub fn scroll_up(&mut self) {
502 self.scroll_offset = self.scroll_offset.saturating_sub(3);
503 self.auto_scroll = false;
505 }
506
507 pub fn scroll_down(&mut self) {
509 self.scroll_offset = self.scroll_offset.saturating_add(3);
510 }
513
514 pub fn start_archive_confirm(&mut self) {
518 use super::archive::generate_default_archive_name;
519 self.archive_default_name = generate_default_archive_name();
520 self.archive_custom_name = String::new();
521 self.archive_editing_name = false;
522 self.archive_edit_cursor = 0;
523 self.mode = ChatMode::ArchiveConfirm;
524 }
525
526 pub fn start_archive_list(&mut self) {
528 use super::archive::list_archives;
529 self.archives = list_archives();
530 self.archive_list_index = 0;
531 self.restore_confirm_needed = false;
532 self.mode = ChatMode::ArchiveList;
533 }
534
535 pub fn do_archive(&mut self, name: &str) {
537 use super::archive::create_archive;
538
539 match create_archive(name, self.session.messages.clone()) {
540 Ok(_) => {
541 self.clear_session();
543 self.show_toast(format!("对话已归档: {}", name), false);
544 }
545 Err(e) => {
546 self.show_toast(e, true);
547 }
548 }
549 self.mode = ChatMode::Chat;
550 }
551
552 pub fn do_restore(&mut self) {
554 use super::archive::restore_archive;
555
556 if let Some(archive) = self.archives.get(self.archive_list_index) {
557 match restore_archive(&archive.name) {
558 Ok(messages) => {
559 self.session.messages = messages;
561 self.scroll_offset = u16::MAX;
562 self.msg_lines_cache = None;
563 self.input.clear();
564 self.cursor_pos = 0;
565 let _ = save_chat_session(&self.session);
566 self.show_toast(format!("已还原归档: {}", archive.name), false);
567 }
568 Err(e) => {
569 self.show_toast(e, true);
570 }
571 }
572 }
573 self.mode = ChatMode::Chat;
574 }
575
576 pub fn do_delete_archive(&mut self) {
578 use super::archive::delete_archive;
579
580 if let Some(archive) = self.archives.get(self.archive_list_index) {
581 match delete_archive(&archive.name) {
582 Ok(_) => {
583 self.show_toast(format!("归档已删除: {}", archive.name), false);
584 self.archives = super::archive::list_archives();
586 if self.archive_list_index >= self.archives.len() && self.archive_list_index > 0
587 {
588 self.archive_list_index -= 1;
589 }
590 }
591 Err(e) => {
592 self.show_toast(e, true);
593 }
594 }
595 }
596 }
597}