1use super::app::{CONFIG_FIELDS, CONFIG_GLOBAL_FIELDS, ChatApp, ChatMode, MsgLinesCache};
2use super::handler::{config_field_label, config_field_value};
3use super::model::agent_config_path;
4use super::render::{build_message_lines_incremental, char_width, display_width, wrap_text};
5use ratatui::{
6 layout::{Constraint, Direction, Layout, Rect},
7 style::{Color, Modifier, Style},
8 text::{Line, Span},
9 widgets::{Block, Borders, List, ListItem, Paragraph},
10};
11
12pub fn draw_chat_ui(f: &mut ratatui::Frame, app: &mut ChatApp) {
13 let size = f.area();
14
15 let bg = Block::default().style(Style::default().bg(Color::Rgb(22, 22, 30)));
17 f.render_widget(bg, size);
18
19 let chunks = Layout::default()
20 .direction(Direction::Vertical)
21 .constraints([
22 Constraint::Length(3), Constraint::Min(5), Constraint::Length(5), Constraint::Length(1), ])
27 .split(size);
28
29 draw_title_bar(f, chunks[0], app);
31
32 if app.mode == ChatMode::Help {
34 draw_help(f, chunks[1]);
35 } else if app.mode == ChatMode::SelectModel {
36 draw_model_selector(f, chunks[1], app);
37 } else if app.mode == ChatMode::Config {
38 draw_config_screen(f, chunks[1], app);
39 } else {
40 draw_messages(f, chunks[1], app);
41 }
42
43 draw_input(f, chunks[2], app);
45
46 draw_hint_bar(f, chunks[3], app);
48
49 draw_toast(f, size, app);
51}
52
53pub fn draw_title_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
55 let model_name = app.active_model_name();
56 let msg_count = app.session.messages.len();
57 let loading = if app.is_loading {
58 " ⏳ 思考中..."
59 } else {
60 ""
61 };
62
63 let title_spans = vec![
64 Span::styled(" 💬 ", Style::default().fg(Color::Rgb(120, 180, 255))),
65 Span::styled(
66 "AI Chat",
67 Style::default()
68 .fg(Color::White)
69 .add_modifier(Modifier::BOLD),
70 ),
71 Span::styled(" │ ", Style::default().fg(Color::Rgb(60, 60, 80))),
72 Span::styled("🤖 ", Style::default()),
73 Span::styled(
74 model_name,
75 Style::default()
76 .fg(Color::Rgb(160, 220, 160))
77 .add_modifier(Modifier::BOLD),
78 ),
79 Span::styled(" │ ", Style::default().fg(Color::Rgb(60, 60, 80))),
80 Span::styled(
81 format!("📨 {} 条消息", msg_count),
82 Style::default().fg(Color::Rgb(180, 180, 200)),
83 ),
84 Span::styled(
85 loading,
86 Style::default()
87 .fg(Color::Rgb(255, 200, 80))
88 .add_modifier(Modifier::BOLD),
89 ),
90 ];
91
92 let title_block = Paragraph::new(Line::from(title_spans)).block(
93 Block::default()
94 .borders(Borders::ALL)
95 .border_type(ratatui::widgets::BorderType::Rounded)
96 .border_style(Style::default().fg(Color::Rgb(80, 100, 140)))
97 .style(Style::default().bg(Color::Rgb(28, 28, 40))),
98 );
99 f.render_widget(title_block, area);
100}
101
102pub fn draw_messages(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
104 let block = Block::default()
105 .borders(Borders::ALL)
106 .border_type(ratatui::widgets::BorderType::Rounded)
107 .border_style(Style::default().fg(Color::Rgb(50, 55, 70)))
108 .title(Span::styled(
109 " 对话记录 ",
110 Style::default()
111 .fg(Color::Rgb(140, 140, 170))
112 .add_modifier(Modifier::BOLD),
113 ))
114 .title_alignment(ratatui::layout::Alignment::Left)
115 .style(Style::default().bg(Color::Rgb(22, 22, 30)));
116
117 if app.session.messages.is_empty() && !app.is_loading {
119 let welcome_lines = vec![
120 Line::from(""),
121 Line::from(""),
122 Line::from(Span::styled(
123 " ╭──────────────────────────────────────╮",
124 Style::default().fg(Color::Rgb(60, 70, 90)),
125 )),
126 Line::from(Span::styled(
127 " │ │",
128 Style::default().fg(Color::Rgb(60, 70, 90)),
129 )),
130 Line::from(vec![
131 Span::styled(" │ ", Style::default().fg(Color::Rgb(60, 70, 90))),
132 Span::styled(
133 "Hi! What can I help you? ",
134 Style::default().fg(Color::Rgb(120, 140, 180)),
135 ),
136 Span::styled(" │", Style::default().fg(Color::Rgb(60, 70, 90))),
137 ]),
138 Line::from(Span::styled(
139 " │ │",
140 Style::default().fg(Color::Rgb(60, 70, 90)),
141 )),
142 Line::from(Span::styled(
143 " │ Type a message, press Enter │",
144 Style::default().fg(Color::Rgb(80, 90, 110)),
145 )),
146 Line::from(Span::styled(
147 " │ │",
148 Style::default().fg(Color::Rgb(60, 70, 90)),
149 )),
150 Line::from(Span::styled(
151 " ╰──────────────────────────────────────╯",
152 Style::default().fg(Color::Rgb(60, 70, 90)),
153 )),
154 ];
155 let empty = Paragraph::new(welcome_lines).block(block);
156 f.render_widget(empty, area);
157 return;
158 }
159
160 let inner_width = area.width.saturating_sub(4) as usize;
162 let bubble_max_width = (inner_width * 75 / 100).max(20);
164
165 let msg_count = app.session.messages.len();
167 let last_msg_len = app
168 .session
169 .messages
170 .last()
171 .map(|m| m.content.len())
172 .unwrap_or(0);
173 let streaming_len = app.streaming_content.lock().unwrap().len();
174 let current_browse_index = if app.mode == ChatMode::Browse {
175 Some(app.browse_msg_index)
176 } else {
177 None
178 };
179 let cache_hit = if let Some(ref cache) = app.msg_lines_cache {
180 cache.msg_count == msg_count
181 && cache.last_msg_len == last_msg_len
182 && cache.streaming_len == streaming_len
183 && cache.is_loading == app.is_loading
184 && cache.bubble_max_width == bubble_max_width
185 && cache.browse_index == current_browse_index
186 } else {
187 false
188 };
189
190 if !cache_hit {
191 let old_cache = app.msg_lines_cache.take();
193 let (new_lines, new_msg_start_lines, new_per_msg, new_stable_lines, new_stable_offset) =
194 build_message_lines_incremental(app, inner_width, bubble_max_width, old_cache.as_ref());
195 app.msg_lines_cache = Some(MsgLinesCache {
196 msg_count,
197 last_msg_len,
198 streaming_len,
199 is_loading: app.is_loading,
200 bubble_max_width,
201 browse_index: current_browse_index,
202 lines: new_lines,
203 msg_start_lines: new_msg_start_lines,
204 per_msg_lines: new_per_msg,
205 streaming_stable_lines: new_stable_lines,
206 streaming_stable_offset: new_stable_offset,
207 });
208 }
209
210 let cached = app.msg_lines_cache.as_ref().unwrap();
212 let all_lines = &cached.lines;
213 let total_lines = all_lines.len() as u16;
214
215 f.render_widget(block, area);
217
218 let inner = area.inner(ratatui::layout::Margin {
220 vertical: 1,
221 horizontal: 1,
222 });
223 let visible_height = inner.height;
224 let max_scroll = total_lines.saturating_sub(visible_height);
225
226 if app.mode != ChatMode::Browse {
228 if app.scroll_offset == u16::MAX || app.scroll_offset > max_scroll {
229 app.scroll_offset = max_scroll;
230 app.auto_scroll = true;
232 }
233 } else {
234 if let Some(target_line) = cached
236 .msg_start_lines
237 .iter()
238 .find(|(idx, _)| *idx == app.browse_msg_index)
239 .map(|(_, line)| *line as u16)
240 {
241 if target_line < app.scroll_offset {
243 app.scroll_offset = target_line;
244 } else if target_line >= app.scroll_offset + visible_height {
245 app.scroll_offset = target_line.saturating_sub(visible_height / 3);
246 }
247 if app.scroll_offset > max_scroll {
249 app.scroll_offset = max_scroll;
250 }
251 }
252 }
253
254 let bg_fill = Block::default().style(Style::default().bg(Color::Rgb(22, 22, 30)));
256 f.render_widget(bg_fill, inner);
257
258 let start = app.scroll_offset as usize;
260 let end = (start + visible_height as usize).min(all_lines.len());
261 for (i, line_idx) in (start..end).enumerate() {
262 let line = &all_lines[line_idx];
263 let y = inner.y + i as u16;
264 let line_area = Rect::new(inner.x, y, inner.width, 1);
265 let p = Paragraph::new(line.clone());
267 f.render_widget(p, line_area);
268 }
269}
270
271pub fn draw_input(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
275 let usable_width = area.width.saturating_sub(2 + 4) as usize;
277
278 let chars: Vec<char> = app.input.chars().collect();
279
280 let before_all: String = chars[..app.cursor_pos].iter().collect();
282 let before_width = display_width(&before_all);
283
284 let scroll_offset_chars = if before_width >= usable_width {
286 let target_width = before_width.saturating_sub(usable_width / 2);
288 let mut w = 0;
289 let mut skip = 0;
290 for (i, &ch) in chars.iter().enumerate() {
291 if w >= target_width {
292 skip = i;
293 break;
294 }
295 w += char_width(ch);
296 }
297 skip
298 } else {
299 0
300 };
301
302 let visible_chars = &chars[scroll_offset_chars..];
304 let cursor_in_visible = app.cursor_pos - scroll_offset_chars;
305
306 let before: String = visible_chars[..cursor_in_visible].iter().collect();
307 let cursor_ch = if cursor_in_visible < visible_chars.len() {
308 visible_chars[cursor_in_visible].to_string()
309 } else {
310 " ".to_string()
311 };
312 let after: String = if cursor_in_visible < visible_chars.len() {
313 visible_chars[cursor_in_visible + 1..].iter().collect()
314 } else {
315 String::new()
316 };
317
318 let prompt_style = if app.is_loading {
319 Style::default().fg(Color::Rgb(255, 200, 80))
320 } else {
321 Style::default().fg(Color::Rgb(100, 200, 130))
322 };
323 let prompt_text = if app.is_loading { " .. " } else { " > " };
324
325 let full_visible = format!("{}{}{}", before, cursor_ch, after);
327 let inner_height = area.height.saturating_sub(2) as usize; let wrapped_lines = wrap_text(&full_visible, usable_width);
329
330 let before_len = before.chars().count();
332 let cursor_len = cursor_ch.chars().count();
333 let cursor_global_pos = before_len; let mut cursor_line_idx: usize = 0;
335 {
336 let mut cumulative = 0usize;
337 for (li, wl) in wrapped_lines.iter().enumerate() {
338 let line_char_count = wl.chars().count();
339 if cumulative + line_char_count > cursor_global_pos {
340 cursor_line_idx = li;
341 break;
342 }
343 cumulative += line_char_count;
344 cursor_line_idx = li; }
346 }
347
348 let line_scroll = if wrapped_lines.len() <= inner_height {
350 0
351 } else if cursor_line_idx < inner_height {
352 0
353 } else {
354 cursor_line_idx.saturating_sub(inner_height - 1)
356 };
357
358 let mut display_lines: Vec<Line> = Vec::new();
360 let mut char_offset: usize = 0;
361 for wl in wrapped_lines.iter().take(line_scroll) {
363 char_offset += wl.chars().count();
364 }
365
366 for (_line_idx, wl) in wrapped_lines
367 .iter()
368 .skip(line_scroll)
369 .enumerate()
370 .take(inner_height.max(1))
371 {
372 let mut spans: Vec<Span> = Vec::new();
373 if _line_idx == 0 && line_scroll == 0 {
374 spans.push(Span::styled(prompt_text, prompt_style));
375 } else {
376 spans.push(Span::styled(" ", Style::default())); }
378
379 let line_chars: Vec<char> = wl.chars().collect();
381 let mut seg_start = 0;
382 for (ci, &ch) in line_chars.iter().enumerate() {
383 let global_idx = char_offset + ci;
384 let is_cursor = global_idx >= before_len && global_idx < before_len + cursor_len;
385
386 if is_cursor {
387 if ci > seg_start {
389 let seg: String = line_chars[seg_start..ci].iter().collect();
390 spans.push(Span::styled(seg, Style::default().fg(Color::White)));
391 }
392 spans.push(Span::styled(
393 ch.to_string(),
394 Style::default()
395 .fg(Color::Rgb(22, 22, 30))
396 .bg(Color::Rgb(200, 210, 240)),
397 ));
398 seg_start = ci + 1;
399 }
400 }
401 if seg_start < line_chars.len() {
403 let seg: String = line_chars[seg_start..].iter().collect();
404 spans.push(Span::styled(seg, Style::default().fg(Color::White)));
405 }
406
407 char_offset += line_chars.len();
408 display_lines.push(Line::from(spans));
409 }
410
411 if display_lines.is_empty() {
412 display_lines.push(Line::from(vec![
413 Span::styled(prompt_text, prompt_style),
414 Span::styled(
415 " ",
416 Style::default()
417 .fg(Color::Rgb(22, 22, 30))
418 .bg(Color::Rgb(200, 210, 240)),
419 ),
420 ]));
421 }
422
423 let input_widget = Paragraph::new(display_lines).block(
424 Block::default()
425 .borders(Borders::ALL)
426 .border_type(ratatui::widgets::BorderType::Rounded)
427 .border_style(if app.is_loading {
428 Style::default().fg(Color::Rgb(120, 100, 50))
429 } else {
430 Style::default().fg(Color::Rgb(60, 100, 80))
431 })
432 .title(Span::styled(
433 " 输入消息 ",
434 Style::default().fg(Color::Rgb(140, 140, 170)),
435 ))
436 .style(Style::default().bg(Color::Rgb(26, 26, 38))),
437 );
438
439 f.render_widget(input_widget, area);
440
441 if !app.is_loading {
444 let prompt_w: u16 = 4; let border_left: u16 = 1; let cursor_col_in_line = {
449 let mut col = 0usize;
450 let mut char_count = 0usize;
451 let mut skip_chars = 0usize;
453 for wl in wrapped_lines.iter().take(line_scroll) {
454 skip_chars += wl.chars().count();
455 }
456 for wl in wrapped_lines.iter().skip(line_scroll) {
458 let line_len = wl.chars().count();
459 if skip_chars + char_count + line_len > cursor_global_pos {
460 let pos_in_line = cursor_global_pos - (skip_chars + char_count);
462 col = wl.chars().take(pos_in_line).map(|c| char_width(c)).sum();
463 break;
464 }
465 char_count += line_len;
466 }
467 col as u16
468 };
469
470 let cursor_row_in_display = (cursor_line_idx - line_scroll) as u16;
472
473 let cursor_x = area.x + border_left + prompt_w + cursor_col_in_line;
474 let cursor_y = area.y + 1 + cursor_row_in_display; if cursor_x < area.x + area.width && cursor_y < area.y + area.height {
478 f.set_cursor_position((cursor_x, cursor_y));
479 }
480 }
481}
482
483pub fn draw_hint_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
485 let hints = match app.mode {
486 ChatMode::Chat => {
487 vec![
488 ("Enter", "发送"),
489 ("↑↓", "滚动"),
490 ("Ctrl+T", "切换模型"),
491 ("Ctrl+L", "清空"),
492 ("Ctrl+Y", "复制"),
493 ("Ctrl+B", "浏览"),
494 ("Ctrl+S", "流式切换"),
495 ("Ctrl+E", "配置"),
496 ("?/F1", "帮助"),
497 ("Esc", "退出"),
498 ]
499 }
500 ChatMode::SelectModel => {
501 vec![("↑↓/jk", "移动"), ("Enter", "确认"), ("Esc", "取消")]
502 }
503 ChatMode::Browse => {
504 vec![("↑↓", "选择消息"), ("y/Enter", "复制"), ("Esc", "返回")]
505 }
506 ChatMode::Help => {
507 vec![("任意键", "返回")]
508 }
509 ChatMode::Config => {
510 vec![
511 ("↑↓", "切换字段"),
512 ("Enter", "编辑"),
513 ("Tab", "切换 Provider"),
514 ("a", "新增"),
515 ("d", "删除"),
516 ("Esc", "保存返回"),
517 ]
518 }
519 };
520
521 let mut spans: Vec<Span> = Vec::new();
522 spans.push(Span::styled(" ", Style::default()));
523 for (i, (key, desc)) in hints.iter().enumerate() {
524 if i > 0 {
525 spans.push(Span::styled(
526 " │ ",
527 Style::default().fg(Color::Rgb(50, 50, 65)),
528 ));
529 }
530 spans.push(Span::styled(
531 format!(" {} ", key),
532 Style::default()
533 .fg(Color::Rgb(22, 22, 30))
534 .bg(Color::Rgb(100, 110, 140)),
535 ));
536 spans.push(Span::styled(
537 format!(" {}", desc),
538 Style::default().fg(Color::Rgb(120, 120, 150)),
539 ));
540 }
541
542 let hint_bar =
543 Paragraph::new(Line::from(spans)).style(Style::default().bg(Color::Rgb(22, 22, 30)));
544 f.render_widget(hint_bar, area);
545}
546
547pub fn draw_toast(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
549 if let Some((ref msg, is_error, _)) = app.toast {
550 let text_width = display_width(msg);
551 let toast_width = (text_width + 10).min(area.width as usize).max(16) as u16;
553 let toast_height: u16 = 3;
554
555 let x = area.width.saturating_sub(toast_width + 1);
557 let y: u16 = 1;
558
559 if x + toast_width <= area.width && y + toast_height <= area.height {
560 let toast_area = Rect::new(x, y, toast_width, toast_height);
561
562 let clear = Block::default().style(Style::default().bg(if is_error {
564 Color::Rgb(60, 20, 20)
565 } else {
566 Color::Rgb(20, 50, 30)
567 }));
568 f.render_widget(clear, toast_area);
569
570 let (icon, border_color, text_color) = if is_error {
571 ("❌", Color::Rgb(200, 70, 70), Color::Rgb(255, 130, 130))
572 } else {
573 ("✅", Color::Rgb(60, 160, 80), Color::Rgb(140, 230, 160))
574 };
575
576 let toast_widget = Paragraph::new(Line::from(vec![
577 Span::styled(format!(" {} ", icon), Style::default()),
578 Span::styled(msg.as_str(), Style::default().fg(text_color)),
579 ]))
580 .block(
581 Block::default()
582 .borders(Borders::ALL)
583 .border_type(ratatui::widgets::BorderType::Rounded)
584 .border_style(Style::default().fg(border_color))
585 .style(Style::default().bg(if is_error {
586 Color::Rgb(50, 18, 18)
587 } else {
588 Color::Rgb(18, 40, 25)
589 })),
590 );
591 f.render_widget(toast_widget, toast_area);
592 }
593 }
594}
595
596pub fn draw_model_selector(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
598 let items: Vec<ListItem> = app
599 .agent_config
600 .providers
601 .iter()
602 .enumerate()
603 .map(|(i, p)| {
604 let is_active = i == app.agent_config.active_index;
605 let marker = if is_active { " ● " } else { " ○ " };
606 let style = if is_active {
607 Style::default()
608 .fg(Color::Rgb(120, 220, 160))
609 .add_modifier(Modifier::BOLD)
610 } else {
611 Style::default().fg(Color::Rgb(180, 180, 200))
612 };
613 let detail = format!("{}{} ({})", marker, p.name, p.model);
614 ListItem::new(Line::from(Span::styled(detail, style)))
615 })
616 .collect();
617
618 let list = List::new(items)
619 .block(
620 Block::default()
621 .borders(Borders::ALL)
622 .border_type(ratatui::widgets::BorderType::Rounded)
623 .border_style(Style::default().fg(Color::Rgb(180, 160, 80)))
624 .title(Span::styled(
625 " 🔄 选择模型 ",
626 Style::default()
627 .fg(Color::Rgb(230, 210, 120))
628 .add_modifier(Modifier::BOLD),
629 ))
630 .style(Style::default().bg(Color::Rgb(28, 28, 40))),
631 )
632 .highlight_style(
633 Style::default()
634 .bg(Color::Rgb(50, 55, 80))
635 .fg(Color::White)
636 .add_modifier(Modifier::BOLD),
637 )
638 .highlight_symbol(" ▸ ");
639
640 f.render_stateful_widget(list, area, &mut app.model_list_state);
641}
642
643pub fn draw_help(f: &mut ratatui::Frame, area: Rect) {
645 let separator = Line::from(Span::styled(
646 " ─────────────────────────────────────────",
647 Style::default().fg(Color::Rgb(50, 55, 70)),
648 ));
649
650 let help_lines = vec![
651 Line::from(""),
652 Line::from(Span::styled(
653 " 📖 快捷键帮助",
654 Style::default()
655 .fg(Color::Rgb(120, 180, 255))
656 .add_modifier(Modifier::BOLD),
657 )),
658 Line::from(""),
659 separator.clone(),
660 Line::from(""),
661 Line::from(vec![
662 Span::styled(
663 " Enter ",
664 Style::default()
665 .fg(Color::Rgb(230, 210, 120))
666 .add_modifier(Modifier::BOLD),
667 ),
668 Span::styled("发送消息", Style::default().fg(Color::Rgb(200, 200, 220))),
669 ]),
670 Line::from(vec![
671 Span::styled(
672 " ↑ / ↓ ",
673 Style::default()
674 .fg(Color::Rgb(230, 210, 120))
675 .add_modifier(Modifier::BOLD),
676 ),
677 Span::styled(
678 "滚动对话记录",
679 Style::default().fg(Color::Rgb(200, 200, 220)),
680 ),
681 ]),
682 Line::from(vec![
683 Span::styled(
684 " ← / → ",
685 Style::default()
686 .fg(Color::Rgb(230, 210, 120))
687 .add_modifier(Modifier::BOLD),
688 ),
689 Span::styled(
690 "移动输入光标",
691 Style::default().fg(Color::Rgb(200, 200, 220)),
692 ),
693 ]),
694 Line::from(vec![
695 Span::styled(
696 " Ctrl+T ",
697 Style::default()
698 .fg(Color::Rgb(230, 210, 120))
699 .add_modifier(Modifier::BOLD),
700 ),
701 Span::styled("切换模型", Style::default().fg(Color::Rgb(200, 200, 220))),
702 ]),
703 Line::from(vec![
704 Span::styled(
705 " Ctrl+L ",
706 Style::default()
707 .fg(Color::Rgb(230, 210, 120))
708 .add_modifier(Modifier::BOLD),
709 ),
710 Span::styled(
711 "清空对话历史",
712 Style::default().fg(Color::Rgb(200, 200, 220)),
713 ),
714 ]),
715 Line::from(vec![
716 Span::styled(
717 " Ctrl+Y ",
718 Style::default()
719 .fg(Color::Rgb(230, 210, 120))
720 .add_modifier(Modifier::BOLD),
721 ),
722 Span::styled(
723 "复制最后一条 AI 回复",
724 Style::default().fg(Color::Rgb(200, 200, 220)),
725 ),
726 ]),
727 Line::from(vec![
728 Span::styled(
729 " Ctrl+B ",
730 Style::default()
731 .fg(Color::Rgb(230, 210, 120))
732 .add_modifier(Modifier::BOLD),
733 ),
734 Span::styled(
735 "浏览消息 (↑↓选择, y/Enter复制)",
736 Style::default().fg(Color::Rgb(200, 200, 220)),
737 ),
738 ]),
739 Line::from(vec![
740 Span::styled(
741 " Ctrl+S ",
742 Style::default()
743 .fg(Color::Rgb(230, 210, 120))
744 .add_modifier(Modifier::BOLD),
745 ),
746 Span::styled(
747 "切换流式/整体输出",
748 Style::default().fg(Color::Rgb(200, 200, 220)),
749 ),
750 ]),
751 Line::from(vec![
752 Span::styled(
753 " Ctrl+E ",
754 Style::default()
755 .fg(Color::Rgb(230, 210, 120))
756 .add_modifier(Modifier::BOLD),
757 ),
758 Span::styled(
759 "打开配置界面",
760 Style::default().fg(Color::Rgb(200, 200, 220)),
761 ),
762 ]),
763 Line::from(vec![
764 Span::styled(
765 " Esc / Ctrl+C ",
766 Style::default()
767 .fg(Color::Rgb(230, 210, 120))
768 .add_modifier(Modifier::BOLD),
769 ),
770 Span::styled("退出对话", Style::default().fg(Color::Rgb(200, 200, 220))),
771 ]),
772 Line::from(vec![
773 Span::styled(
774 " ? / F1 ",
775 Style::default()
776 .fg(Color::Rgb(230, 210, 120))
777 .add_modifier(Modifier::BOLD),
778 ),
779 Span::styled(
780 "显示 / 关闭此帮助",
781 Style::default().fg(Color::Rgb(200, 200, 220)),
782 ),
783 ]),
784 Line::from(""),
785 separator,
786 Line::from(""),
787 Line::from(Span::styled(
788 " 📁 配置文件:",
789 Style::default()
790 .fg(Color::Rgb(120, 180, 255))
791 .add_modifier(Modifier::BOLD),
792 )),
793 Line::from(Span::styled(
794 format!(" {}", agent_config_path().display()),
795 Style::default().fg(Color::Rgb(100, 100, 130)),
796 )),
797 ];
798
799 let help_block = Block::default()
800 .borders(Borders::ALL)
801 .border_type(ratatui::widgets::BorderType::Rounded)
802 .border_style(Style::default().fg(Color::Rgb(80, 100, 140)))
803 .title(Span::styled(
804 " 帮助 (按任意键返回) ",
805 Style::default().fg(Color::Rgb(140, 140, 170)),
806 ))
807 .style(Style::default().bg(Color::Rgb(24, 24, 34)));
808 let help_widget = Paragraph::new(help_lines).block(help_block);
809 f.render_widget(help_widget, area);
810}
811
812pub fn draw_config_screen(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
815 let bg = Color::Rgb(28, 28, 40);
816 let total_provider_fields = CONFIG_FIELDS.len();
817
818 let mut lines: Vec<Line> = Vec::new();
819 lines.push(Line::from(""));
820
821 lines.push(Line::from(vec![Span::styled(
823 " ⚙️ 模型配置",
824 Style::default()
825 .fg(Color::Rgb(120, 180, 255))
826 .add_modifier(Modifier::BOLD),
827 )]));
828 lines.push(Line::from(""));
829
830 let provider_count = app.agent_config.providers.len();
832 if provider_count > 0 {
833 let mut tab_spans: Vec<Span> = vec![Span::styled(" ", Style::default())];
834 for (i, p) in app.agent_config.providers.iter().enumerate() {
835 let is_current = i == app.config_provider_idx;
836 let is_active = i == app.agent_config.active_index;
837 let marker = if is_active { "● " } else { "○ " };
838 let label = format!(" {}{} ", marker, p.name);
839 if is_current {
840 tab_spans.push(Span::styled(
841 label,
842 Style::default()
843 .fg(Color::Rgb(22, 22, 30))
844 .bg(Color::Rgb(120, 180, 255))
845 .add_modifier(Modifier::BOLD),
846 ));
847 } else {
848 tab_spans.push(Span::styled(
849 label,
850 Style::default().fg(Color::Rgb(150, 150, 170)),
851 ));
852 }
853 if i < provider_count - 1 {
854 tab_spans.push(Span::styled(
855 " │ ",
856 Style::default().fg(Color::Rgb(50, 55, 70)),
857 ));
858 }
859 }
860 tab_spans.push(Span::styled(
861 " (● = 活跃模型, Tab 切换, s 设为活跃)",
862 Style::default().fg(Color::Rgb(80, 80, 100)),
863 ));
864 lines.push(Line::from(tab_spans));
865 } else {
866 lines.push(Line::from(Span::styled(
867 " (无 Provider,按 a 新增)",
868 Style::default().fg(Color::Rgb(180, 120, 80)),
869 )));
870 }
871 lines.push(Line::from(""));
872
873 lines.push(Line::from(Span::styled(
875 " ─────────────────────────────────────────",
876 Style::default().fg(Color::Rgb(50, 55, 70)),
877 )));
878 lines.push(Line::from(""));
879
880 if provider_count > 0 {
882 lines.push(Line::from(Span::styled(
883 " 📦 Provider 配置",
884 Style::default()
885 .fg(Color::Rgb(160, 220, 160))
886 .add_modifier(Modifier::BOLD),
887 )));
888 lines.push(Line::from(""));
889
890 for i in 0..total_provider_fields {
891 let is_selected = app.config_field_idx == i;
892 let label = config_field_label(i);
893 let value = if app.config_editing && is_selected {
894 app.config_edit_buf.clone()
896 } else {
897 config_field_value(app, i)
898 };
899
900 let pointer = if is_selected { " ▸ " } else { " " };
901 let pointer_style = if is_selected {
902 Style::default().fg(Color::Rgb(255, 200, 80))
903 } else {
904 Style::default()
905 };
906
907 let label_style = if is_selected {
908 Style::default()
909 .fg(Color::Rgb(230, 210, 120))
910 .add_modifier(Modifier::BOLD)
911 } else {
912 Style::default().fg(Color::Rgb(140, 140, 160))
913 };
914
915 let value_style = if app.config_editing && is_selected {
916 Style::default().fg(Color::White).bg(Color::Rgb(50, 55, 80))
917 } else if is_selected {
918 Style::default().fg(Color::White)
919 } else {
920 if CONFIG_FIELDS[i] == "api_key" {
922 Style::default().fg(Color::Rgb(100, 100, 120))
923 } else {
924 Style::default().fg(Color::Rgb(180, 180, 200))
925 }
926 };
927
928 let edit_indicator = if app.config_editing && is_selected {
929 " ✏️"
930 } else {
931 ""
932 };
933
934 lines.push(Line::from(vec![
935 Span::styled(pointer, pointer_style),
936 Span::styled(format!("{:<10}", label), label_style),
937 Span::styled(" ", Style::default()),
938 Span::styled(
939 if value.is_empty() {
940 "(空)".to_string()
941 } else {
942 value
943 },
944 value_style,
945 ),
946 Span::styled(edit_indicator, Style::default()),
947 ]));
948 }
949 }
950
951 lines.push(Line::from(""));
952 lines.push(Line::from(Span::styled(
954 " ─────────────────────────────────────────",
955 Style::default().fg(Color::Rgb(50, 55, 70)),
956 )));
957 lines.push(Line::from(""));
958
959 lines.push(Line::from(Span::styled(
961 " 🌐 全局配置",
962 Style::default()
963 .fg(Color::Rgb(160, 220, 160))
964 .add_modifier(Modifier::BOLD),
965 )));
966 lines.push(Line::from(""));
967
968 for i in 0..CONFIG_GLOBAL_FIELDS.len() {
969 let field_idx = total_provider_fields + i;
970 let is_selected = app.config_field_idx == field_idx;
971 let label = config_field_label(field_idx);
972 let value = if app.config_editing && is_selected {
973 app.config_edit_buf.clone()
974 } else {
975 config_field_value(app, field_idx)
976 };
977
978 let pointer = if is_selected { " ▸ " } else { " " };
979 let pointer_style = if is_selected {
980 Style::default().fg(Color::Rgb(255, 200, 80))
981 } else {
982 Style::default()
983 };
984
985 let label_style = if is_selected {
986 Style::default()
987 .fg(Color::Rgb(230, 210, 120))
988 .add_modifier(Modifier::BOLD)
989 } else {
990 Style::default().fg(Color::Rgb(140, 140, 160))
991 };
992
993 let value_style = if app.config_editing && is_selected {
994 Style::default().fg(Color::White).bg(Color::Rgb(50, 55, 80))
995 } else if is_selected {
996 Style::default().fg(Color::White)
997 } else {
998 Style::default().fg(Color::Rgb(180, 180, 200))
999 };
1000
1001 let edit_indicator = if app.config_editing && is_selected {
1002 " ✏️"
1003 } else {
1004 ""
1005 };
1006
1007 if CONFIG_GLOBAL_FIELDS[i] == "stream_mode" {
1009 let toggle_on = app.agent_config.stream_mode;
1010 let toggle_style = if toggle_on {
1011 Style::default()
1012 .fg(Color::Rgb(120, 220, 160))
1013 .add_modifier(Modifier::BOLD)
1014 } else {
1015 Style::default().fg(Color::Rgb(200, 100, 100))
1016 };
1017 let toggle_text = if toggle_on {
1018 "● 开启"
1019 } else {
1020 "○ 关闭"
1021 };
1022
1023 lines.push(Line::from(vec![
1024 Span::styled(pointer, pointer_style),
1025 Span::styled(format!("{:<10}", label), label_style),
1026 Span::styled(" ", Style::default()),
1027 Span::styled(toggle_text, toggle_style),
1028 Span::styled(
1029 if is_selected { " (Enter 切换)" } else { "" },
1030 Style::default().fg(Color::Rgb(80, 80, 100)),
1031 ),
1032 ]));
1033 } else {
1034 lines.push(Line::from(vec![
1035 Span::styled(pointer, pointer_style),
1036 Span::styled(format!("{:<10}", label), label_style),
1037 Span::styled(" ", Style::default()),
1038 Span::styled(
1039 if value.is_empty() {
1040 "(空)".to_string()
1041 } else {
1042 value
1043 },
1044 value_style,
1045 ),
1046 Span::styled(edit_indicator, Style::default()),
1047 ]));
1048 }
1049 }
1050
1051 lines.push(Line::from(""));
1052 lines.push(Line::from(""));
1053
1054 lines.push(Line::from(Span::styled(
1056 " ─────────────────────────────────────────",
1057 Style::default().fg(Color::Rgb(50, 55, 70)),
1058 )));
1059 lines.push(Line::from(""));
1060 lines.push(Line::from(vec![
1061 Span::styled(" ", Style::default()),
1062 Span::styled(
1063 "↑↓/jk",
1064 Style::default()
1065 .fg(Color::Rgb(230, 210, 120))
1066 .add_modifier(Modifier::BOLD),
1067 ),
1068 Span::styled(
1069 " 切换字段 ",
1070 Style::default().fg(Color::Rgb(120, 120, 150)),
1071 ),
1072 Span::styled(
1073 "Enter",
1074 Style::default()
1075 .fg(Color::Rgb(230, 210, 120))
1076 .add_modifier(Modifier::BOLD),
1077 ),
1078 Span::styled(" 编辑 ", Style::default().fg(Color::Rgb(120, 120, 150))),
1079 Span::styled(
1080 "Tab/←→",
1081 Style::default()
1082 .fg(Color::Rgb(230, 210, 120))
1083 .add_modifier(Modifier::BOLD),
1084 ),
1085 Span::styled(
1086 " 切换 Provider ",
1087 Style::default().fg(Color::Rgb(120, 120, 150)),
1088 ),
1089 Span::styled(
1090 "a",
1091 Style::default()
1092 .fg(Color::Rgb(230, 210, 120))
1093 .add_modifier(Modifier::BOLD),
1094 ),
1095 Span::styled(" 新增 ", Style::default().fg(Color::Rgb(120, 120, 150))),
1096 Span::styled(
1097 "d",
1098 Style::default()
1099 .fg(Color::Rgb(230, 210, 120))
1100 .add_modifier(Modifier::BOLD),
1101 ),
1102 Span::styled(" 删除 ", Style::default().fg(Color::Rgb(120, 120, 150))),
1103 Span::styled(
1104 "s",
1105 Style::default()
1106 .fg(Color::Rgb(230, 210, 120))
1107 .add_modifier(Modifier::BOLD),
1108 ),
1109 Span::styled(
1110 " 设为活跃 ",
1111 Style::default().fg(Color::Rgb(120, 120, 150)),
1112 ),
1113 Span::styled(
1114 "Esc",
1115 Style::default()
1116 .fg(Color::Rgb(230, 210, 120))
1117 .add_modifier(Modifier::BOLD),
1118 ),
1119 Span::styled(" 保存返回", Style::default().fg(Color::Rgb(120, 120, 150))),
1120 ]));
1121
1122 let content = Paragraph::new(lines)
1123 .block(
1124 Block::default()
1125 .borders(Borders::ALL)
1126 .border_type(ratatui::widgets::BorderType::Rounded)
1127 .border_style(Style::default().fg(Color::Rgb(80, 80, 110)))
1128 .title(Span::styled(
1129 " ⚙️ 模型配置编辑 ",
1130 Style::default()
1131 .fg(Color::Rgb(230, 210, 120))
1132 .add_modifier(Modifier::BOLD),
1133 ))
1134 .style(Style::default().bg(bg)),
1135 )
1136 .scroll((0, 0));
1137 f.render_widget(content, area);
1138}