Skip to main content

j_cli/command/chat/
app.rs

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
14// ========== TUI 界面 ==========
15
16/// 后台线程发送给 TUI 的消息类型
17pub enum StreamMsg {
18    /// 收到一个流式文本块
19    Chunk,
20    /// 流式响应完成
21    Done,
22    /// 发生错误
23    Error(String),
24}
25
26/// TUI 应用状态
27pub struct ChatApp {
28    /// Agent 配置
29    pub agent_config: AgentConfig,
30    /// 当前对话会话
31    pub session: ChatSession,
32    /// 输入缓冲区
33    pub input: String,
34    /// 光标位置(字符索引)
35    pub cursor_pos: usize,
36    /// 当前模式
37    pub mode: ChatMode,
38    /// 消息列表滚动偏移
39    pub scroll_offset: u16,
40    /// 是否正在等待 AI 回复
41    pub is_loading: bool,
42    /// 模型选择列表状态
43    pub model_list_state: ListState,
44    /// Toast 通知消息 (内容, 是否错误, 创建时间)
45    pub toast: Option<(String, bool, std::time::Instant)>,
46    /// 用于接收后台流式回复的 channel
47    pub stream_rx: Option<mpsc::Receiver<StreamMsg>>,
48    /// 当前正在流式接收的 AI 回复内容(实时更新)
49    pub streaming_content: Arc<Mutex<String>>,
50    /// 消息渲染行缓存:(消息数, 最后一条消息内容hash, 气泡宽度) → 渲染好的行
51    /// 避免每帧都重新解析 Markdown
52    pub msg_lines_cache: Option<MsgLinesCache>,
53    /// 消息浏览模式中选中的消息索引
54    pub browse_msg_index: usize,
55    /// 流式节流:上次实际渲染流式内容时的长度
56    pub last_rendered_streaming_len: usize,
57    /// 流式节流:上次实际渲染流式内容的时间
58    pub last_stream_render_time: std::time::Instant,
59    /// 配置界面:当前选中的 provider 索引
60    pub config_provider_idx: usize,
61    /// 配置界面:当前选中的字段索引
62    pub config_field_idx: usize,
63    /// 配置界面:是否正在编辑某个字段
64    pub config_editing: bool,
65    /// 配置界面:编辑缓冲区
66    pub config_edit_buf: String,
67    /// 配置界面:编辑光标位置
68    pub config_edit_cursor: usize,
69    /// 流式输出时是否自动滚动到底部(用户手动上滚后关闭,发送新消息或滚到底部时恢复)
70    pub auto_scroll: bool,
71    /// 当前主题
72    pub theme: Theme,
73    /// 归档列表(缓存)
74    pub archives: Vec<super::archive::ChatArchive>,
75    /// 归档列表选中索引
76    pub archive_list_index: usize,
77    /// 归档确认模式的默认名称
78    pub archive_default_name: String,
79    /// 归档确认模式的用户自定义名称
80    pub archive_custom_name: String,
81    /// 归档确认模式是否正在编辑名称
82    pub archive_editing_name: bool,
83    /// 归档确认模式的光标位置
84    pub archive_edit_cursor: usize,
85    /// 还原确认模式:是否需要确认当前会话有消息
86    pub restore_confirm_needed: bool,
87}
88
89/// 消息渲染行缓存
90pub struct MsgLinesCache {
91    /// 会话消息数量
92    pub msg_count: usize,
93    /// 最后一条消息的内容长度(用于检测流式更新)
94    pub last_msg_len: usize,
95    /// 流式内容长度
96    pub streaming_len: usize,
97    /// 是否正在加载
98    pub is_loading: bool,
99    /// 气泡最大宽度(窗口变化时需要重算)
100    pub bubble_max_width: usize,
101    /// 浏览模式选中索引(None 表示非浏览模式)
102    pub browse_index: Option<usize>,
103    /// 缓存的渲染行
104    pub lines: Vec<Line<'static>>,
105    /// 每条消息(按 msg_index)的起始行号(用于浏览模式自动滚动)
106    pub msg_start_lines: Vec<(usize, usize)>, // (msg_index, start_line)
107    /// 按消息粒度缓存:每条历史消息的渲染行(key: 消息索引)
108    pub per_msg_lines: Vec<PerMsgCache>,
109    /// 流式增量渲染缓存:已完成段落的渲染行
110    pub streaming_stable_lines: Vec<Line<'static>>,
111    /// 流式增量渲染缓存:已缓存到 streaming_content 的字节偏移
112    pub streaming_stable_offset: usize,
113}
114
115/// 单条消息的渲染缓存
116pub struct PerMsgCache {
117    /// 消息内容长度(用于检测变化)
118    pub content_len: usize,
119    /// 渲染好的行
120    pub lines: Vec<Line<'static>>,
121    /// 对应的 msg_start_line(此消息在全局行列表中的起始行号,需在拼装时更新)
122    pub msg_index: usize,
123}
124
125/// Toast 通知显示时长(秒)
126pub const TOAST_DURATION_SECS: u64 = 4;
127
128#[derive(PartialEq)]
129pub enum ChatMode {
130    /// 正常对话模式(焦点在输入框)
131    Chat,
132    /// 模型选择模式
133    SelectModel,
134    /// 消息浏览模式(可选中消息并复制)
135    Browse,
136    /// 帮助
137    Help,
138    /// 配置编辑模式
139    Config,
140    /// 归档确认模式(确认归档名称)
141    ArchiveConfirm,
142    /// 归档列表模式(查看和还原归档)
143    ArchiveList,
144}
145
146/// 配置编辑界面的字段列表
147pub const CONFIG_FIELDS: &[&str] = &["name", "api_base", "api_key", "model"];
148/// 全局配置字段
149pub const CONFIG_GLOBAL_FIELDS: &[&str] = &[
150    "system_prompt",
151    "stream_mode",
152    "max_history_messages",
153    "theme",
154];
155/// 所有字段数 = provider 字段 + 全局字段
156pub 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, // 默认滚动到底部
176            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    /// 切换到下一个主题
203    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; // 清除缓存以触发重绘
207    }
208
209    /// 显示一条 toast 通知
210    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    /// 清理过期的 toast
215    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    /// 获取当前活跃的 provider
224    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    /// 获取当前模型名称
236    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    /// 构建发送给 API 的消息列表
243    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        // 只取最近的 N 条历史消息,避免 token 消耗过大
253        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    /// 发送消息(非阻塞,启动后台线程流式接收)
267    pub fn send_message(&mut self) {
268        let text = self.input.trim().to_string();
269        if text.is_empty() {
270            return;
271        }
272
273        // 添加用户消息
274        self.session.messages.push(ChatMessage {
275            role: "user".to_string(),
276            content: text,
277        });
278        self.input.clear();
279        self.cursor_pos = 0;
280        // 发送新消息时恢复自动滚动并滚到底部
281        self.auto_scroll = true;
282        self.scroll_offset = u16::MAX;
283
284        // 调用 API
285        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        // 重置流式节流状态和缓存
295        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        // 清空流式内容缓冲
302        {
303            let mut sc = self.streaming_content.lock().unwrap();
304            sc.clear();
305        }
306
307        // 创建 channel 用于后台线程 -> TUI 通信
308        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        // 启动后台线程执行 API 调用
316        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                    // 流式输出模式
343                    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                                        // 更新共享缓冲
359                                        {
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                    // 非流式输出模式:等待完整响应后一次性返回
377                    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    /// 处理后台流式消息(在主循环中每帧调用)
406    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        // 非阻塞地取出所有可用的消息
415        if let Some(ref rx) = self.stream_rx {
416            loop {
417                match rx.try_recv() {
418                    Ok(StreamMsg::Chunk) => {
419                        // 内容已经通过 Arc<Mutex<String>> 更新
420                        // 只有在用户没有手动滚动的情况下才自动滚到底部
421                        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            // 重置流式节流状态
448            self.last_rendered_streaming_len = 0;
449            // 清除缓存,流式结束后需要完整重建(新消息已加入 session)
450            self.msg_lines_cache = None;
451
452            if !had_error {
453                // 将流式内容作为完整回复添加到会话
454                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                    // 清空流式缓冲
464                    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                // 错误时也清空流式缓冲
472                self.streaming_content.lock().unwrap().clear();
473            }
474
475            // 自动保存对话历史
476            let _ = save_chat_session(&self.session);
477        }
478    }
479
480    /// 清空对话
481    pub fn clear_session(&mut self) {
482        self.session.messages.clear();
483        self.scroll_offset = 0;
484        self.msg_lines_cache = None; // 清除缓存
485        let _ = save_chat_session(&self.session);
486        self.show_toast("对话已清空", false);
487    }
488
489    /// 切换模型
490    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    /// 向上滚动消息
501    pub fn scroll_up(&mut self) {
502        self.scroll_offset = self.scroll_offset.saturating_sub(3);
503        // 用户手动上滚,关闭自动滚动
504        self.auto_scroll = false;
505    }
506
507    /// 向下滚动消息
508    pub fn scroll_down(&mut self) {
509        self.scroll_offset = self.scroll_offset.saturating_add(3);
510        // 注意:scroll_offset 可能超过 max_scroll,绘制时会校正。
511        // 如果用户滚到了底部(offset >= max_scroll),在绘制时会恢复 auto_scroll。
512    }
513
514    // ========== 归档相关方法 ==========
515
516    /// 开始归档确认流程
517    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    /// 开始还原流程(加载归档列表)
527    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    /// 执行归档
536    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                // 归档成功后清空当前会话
542                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    /// 执行还原归档
553    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                    // 清空当前会话
560                    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    /// 删除选中的归档
577    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                    // 刷新归档列表
585                    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}