Skip to main content

j_cli/command/chat/ui/
archive.rs

1use super::super::app::ChatApp;
2use ratatui::{
3    layout::Rect,
4    style::{Modifier, Style},
5    text::{Line, Span},
6    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
7};
8
9/// 绘制归档确认界面
10pub fn draw_archive_confirm(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
11    let t = &app.theme;
12    let mut lines: Vec<Line> = Vec::new();
13
14    lines.push(Line::from(""));
15    lines.push(Line::from(Span::styled(
16        "  📦 归档当前对话",
17        Style::default()
18            .fg(t.help_title)
19            .add_modifier(Modifier::BOLD),
20    )));
21    lines.push(Line::from(""));
22    lines.push(Line::from(Span::styled(
23        "  ─────────────────────────────────────────",
24        Style::default().fg(t.separator),
25    )));
26    lines.push(Line::from(""));
27    lines.push(Line::from(Span::styled(
28        "  即将归档当前对话,归档后当前会话将被清空。",
29        Style::default().fg(t.text_dim),
30    )));
31    lines.push(Line::from(""));
32
33    if app.archive_editing_name {
34        lines.push(Line::from(Span::styled(
35            "  请输入归档名称:",
36            Style::default().fg(t.text_white),
37        )));
38        lines.push(Line::from(""));
39
40        let name_with_cursor = if app.archive_custom_name.is_empty() {
41            vec![Span::styled(
42                " ",
43                Style::default().fg(t.cursor_fg).bg(t.cursor_bg),
44            )]
45        } else {
46            let chars: Vec<char> = app.archive_custom_name.chars().collect();
47            let mut spans: Vec<Span> = Vec::new();
48            for (i, &ch) in chars.iter().enumerate() {
49                if i == app.archive_edit_cursor {
50                    spans.push(Span::styled(
51                        ch.to_string(),
52                        Style::default().fg(t.cursor_fg).bg(t.cursor_bg),
53                    ));
54                } else {
55                    spans.push(Span::styled(
56                        ch.to_string(),
57                        Style::default().fg(t.text_white),
58                    ));
59                }
60            }
61            if app.archive_edit_cursor >= chars.len() {
62                spans.push(Span::styled(
63                    " ",
64                    Style::default().fg(t.cursor_fg).bg(t.cursor_bg),
65                ));
66            }
67            spans
68        };
69
70        lines.push(Line::from(vec![
71            Span::styled("    ", Style::default()),
72            Span::styled(
73                format!("archive-{}", chrono::Local::now().format("%Y-%m-%d")),
74                Style::default().fg(t.text_dim),
75            ),
76        ]));
77        lines.push(Line::from(
78            std::iter::once(Span::styled("    ", Style::default()))
79                .chain(name_with_cursor.into_iter())
80                .collect::<Vec<_>>(),
81        ));
82        lines.push(Line::from(""));
83        lines.push(Line::from(Span::styled(
84            "  提示:留空则使用默认名称(如 archive-2026-02-25)",
85            Style::default().fg(t.text_dim),
86        )));
87        lines.push(Line::from(""));
88        lines.push(Line::from(Span::styled(
89            "  ─────────────────────────────────────────",
90            Style::default().fg(t.separator),
91        )));
92        lines.push(Line::from(""));
93        lines.push(Line::from(vec![
94            Span::styled("  ", Style::default()),
95            Span::styled(
96                "Enter",
97                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
98            ),
99            Span::styled("  确认归档", Style::default().fg(t.help_desc)),
100        ]));
101        lines.push(Line::from(vec![
102            Span::styled("  ", Style::default()),
103            Span::styled(
104                "Esc",
105                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
106            ),
107            Span::styled("    取消", Style::default().fg(t.help_desc)),
108        ]));
109    } else {
110        lines.push(Line::from(vec![
111            Span::styled("  默认名称:", Style::default().fg(t.text_dim)),
112            Span::styled(
113                &app.archive_default_name,
114                Style::default()
115                    .fg(t.config_toggle_on)
116                    .add_modifier(Modifier::BOLD),
117            ),
118        ]));
119        lines.push(Line::from(""));
120        lines.push(Line::from(Span::styled(
121            "  ─────────────────────────────────────────",
122            Style::default().fg(t.separator),
123        )));
124        lines.push(Line::from(""));
125        lines.push(Line::from(vec![
126            Span::styled("  ", Style::default()),
127            Span::styled(
128                "Enter",
129                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
130            ),
131            Span::styled("  使用默认名称归档", Style::default().fg(t.help_desc)),
132        ]));
133        lines.push(Line::from(vec![
134            Span::styled("  ", Style::default()),
135            Span::styled(
136                "n",
137                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
138            ),
139            Span::styled("      自定义名称", Style::default().fg(t.help_desc)),
140        ]));
141        lines.push(Line::from(vec![
142            Span::styled("  ", Style::default()),
143            Span::styled(
144                "d",
145                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
146            ),
147            Span::styled("      仅清空不归档", Style::default().fg(t.help_desc)),
148        ]));
149        lines.push(Line::from(vec![
150            Span::styled("  ", Style::default()),
151            Span::styled(
152                "Esc",
153                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
154            ),
155            Span::styled("    取消", Style::default().fg(t.help_desc)),
156        ]));
157    }
158
159    let block = Block::default()
160        .borders(Borders::ALL)
161        .border_type(ratatui::widgets::BorderType::Rounded)
162        .border_style(Style::default().fg(t.border_title))
163        .title(Span::styled(" 归档确认 ", Style::default().fg(t.text_dim)))
164        .style(Style::default().bg(t.help_bg));
165    let widget = Paragraph::new(lines).block(block);
166    f.render_widget(widget, area);
167}
168
169/// 绘制归档列表界面
170pub fn draw_archive_list(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
171    let t = &app.theme;
172
173    if app.restore_confirm_needed {
174        let mut lines: Vec<Line> = Vec::new();
175        lines.push(Line::from(""));
176        lines.push(Line::from(Span::styled(
177            "  ⚠️  确认还原",
178            Style::default()
179                .fg(t.toast_error_text)
180                .add_modifier(Modifier::BOLD),
181        )));
182        lines.push(Line::from(""));
183        lines.push(Line::from(Span::styled(
184            "  当前对话未归档,还原将丢失当前对话内容!",
185            Style::default().fg(t.text_white),
186        )));
187        lines.push(Line::from(""));
188        lines.push(Line::from(Span::styled(
189            "  ─────────────────────────────────────────",
190            Style::default().fg(t.separator),
191        )));
192        lines.push(Line::from(""));
193        if let Some(archive) = app.archives.get(app.archive_list_index) {
194            lines.push(Line::from(vec![
195                Span::styled("  将还原归档:", Style::default().fg(t.text_dim)),
196                Span::styled(
197                    &archive.name,
198                    Style::default()
199                        .fg(t.config_toggle_on)
200                        .add_modifier(Modifier::BOLD),
201                ),
202            ]));
203        }
204        lines.push(Line::from(""));
205        lines.push(Line::from(vec![
206            Span::styled("  ", Style::default()),
207            Span::styled(
208                "y/Enter",
209                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
210            ),
211            Span::styled("  确认还原", Style::default().fg(t.help_desc)),
212        ]));
213        lines.push(Line::from(vec![
214            Span::styled("  ", Style::default()),
215            Span::styled(
216                "Esc",
217                Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
218            ),
219            Span::styled("     取消", Style::default().fg(t.help_desc)),
220        ]));
221
222        let block = Block::default()
223            .borders(Borders::ALL)
224            .border_type(ratatui::widgets::BorderType::Rounded)
225            .border_style(Style::default().fg(t.toast_error_border))
226            .title(Span::styled(" 还原确认 ", Style::default().fg(t.text_dim)))
227            .style(Style::default().bg(t.help_bg));
228        let widget = Paragraph::new(lines).block(block);
229        f.render_widget(widget, area);
230        return;
231    }
232
233    if app.archives.is_empty() {
234        let lines = vec![
235            Line::from(""),
236            Line::from(""),
237            Line::from(Span::styled(
238                "  📦 暂无归档对话",
239                Style::default().fg(t.text_dim).add_modifier(Modifier::BOLD),
240            )),
241            Line::from(""),
242            Line::from(Span::styled(
243                "  按 Ctrl+L 归档当前对话",
244                Style::default().fg(t.text_dim),
245            )),
246            Line::from(""),
247            Line::from(Span::styled(
248                "  按 Esc 返回聊天",
249                Style::default().fg(t.text_dim),
250            )),
251        ];
252
253        let block = Block::default()
254            .borders(Borders::ALL)
255            .border_type(ratatui::widgets::BorderType::Rounded)
256            .border_style(Style::default().fg(t.border_title))
257            .title(Span::styled(" 归档列表 ", Style::default().fg(t.text_dim)))
258            .style(Style::default().bg(t.help_bg));
259        let widget = Paragraph::new(lines).block(block);
260        f.render_widget(widget, area);
261        return;
262    }
263
264    let items: Vec<ListItem> = app
265        .archives
266        .iter()
267        .enumerate()
268        .map(|(i, archive)| {
269            let is_selected = i == app.archive_list_index;
270            let marker = if is_selected { "  ▸ " } else { "    " };
271            let msg_count = archive.messages.len();
272            let created_at = archive
273                .created_at
274                .split('T')
275                .next()
276                .unwrap_or(&archive.created_at);
277            let style = if is_selected {
278                Style::default()
279                    .fg(t.model_sel_active)
280                    .add_modifier(Modifier::BOLD)
281            } else {
282                Style::default().fg(t.model_sel_inactive)
283            };
284            let detail = format!(
285                "{}{}  📨 {} 条消息  📅 {}",
286                marker, archive.name, msg_count, created_at
287            );
288            ListItem::new(Line::from(Span::styled(detail, style)))
289        })
290        .collect();
291
292    let list = List::new(items)
293        .block(
294            Block::default()
295                .borders(Borders::ALL)
296                .border_type(ratatui::widgets::BorderType::Rounded)
297                .border_style(Style::default().fg(t.model_sel_border))
298                .title(Span::styled(
299                    " 📦 归档列表 (Enter 还原, d 删除, Esc 返回) ",
300                    Style::default()
301                        .fg(t.model_sel_title)
302                        .add_modifier(Modifier::BOLD),
303                ))
304                .style(Style::default().bg(t.bg_title)),
305        )
306        .highlight_style(
307            Style::default()
308                .bg(t.model_sel_highlight_bg)
309                .fg(t.text_white)
310                .add_modifier(Modifier::BOLD),
311        )
312        .highlight_symbol("");
313
314    let mut list_state = ListState::default();
315    list_state.select(Some(app.archive_list_index));
316    f.render_stateful_widget(list, area, &mut list_state);
317}