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