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::{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(app.theme.bg_primary));
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], app);
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 t = &app.theme;
56 let model_name = app.active_model_name();
57 let msg_count = app.session.messages.len();
58 let loading = if app.is_loading {
59 " ⏳ 思考中..."
60 } else {
61 ""
62 };
63
64 let title_spans = vec![
65 Span::styled(" 💬 ", Style::default().fg(t.title_icon)),
66 Span::styled(
67 "AI Chat",
68 Style::default()
69 .fg(t.text_white)
70 .add_modifier(Modifier::BOLD),
71 ),
72 Span::styled(" │ ", Style::default().fg(t.title_separator)),
73 Span::styled("🤖 ", Style::default()),
74 Span::styled(
75 model_name,
76 Style::default()
77 .fg(t.title_model)
78 .add_modifier(Modifier::BOLD),
79 ),
80 Span::styled(" │ ", Style::default().fg(t.title_separator)),
81 Span::styled(
82 format!("📨 {} 条消息", msg_count),
83 Style::default().fg(t.title_count),
84 ),
85 Span::styled(
86 loading,
87 Style::default()
88 .fg(t.title_loading)
89 .add_modifier(Modifier::BOLD),
90 ),
91 ];
92
93 let title_block = Paragraph::new(Line::from(title_spans)).block(
94 Block::default()
95 .borders(Borders::ALL)
96 .border_type(ratatui::widgets::BorderType::Rounded)
97 .border_style(Style::default().fg(t.border_title))
98 .style(Style::default().bg(t.bg_title)),
99 );
100 f.render_widget(title_block, area);
101}
102
103pub fn draw_messages(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
105 let t = &app.theme;
106 let block = Block::default()
107 .borders(Borders::ALL)
108 .border_type(ratatui::widgets::BorderType::Rounded)
109 .border_style(Style::default().fg(t.border_message))
110 .title(Span::styled(
111 " 对话记录 ",
112 Style::default().fg(t.text_dim).add_modifier(Modifier::BOLD),
113 ))
114 .title_alignment(ratatui::layout::Alignment::Left)
115 .style(Style::default().bg(t.bg_primary));
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(t.welcome_border),
125 )),
126 Line::from(Span::styled(
127 " │ │",
128 Style::default().fg(t.welcome_border),
129 )),
130 Line::from(vec![
131 Span::styled(" │ ", Style::default().fg(t.welcome_border)),
132 Span::styled(
133 "Hi! What can I help you? ",
134 Style::default().fg(t.welcome_text),
135 ),
136 Span::styled(" │", Style::default().fg(t.welcome_border)),
137 ]),
138 Line::from(Span::styled(
139 " │ │",
140 Style::default().fg(t.welcome_border),
141 )),
142 Line::from(Span::styled(
143 " │ Type a message, press Enter │",
144 Style::default().fg(t.welcome_hint),
145 )),
146 Line::from(Span::styled(
147 " │ │",
148 Style::default().fg(t.welcome_border),
149 )),
150 Line::from(Span::styled(
151 " ╰──────────────────────────────────────╯",
152 Style::default().fg(t.welcome_border),
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(app.theme.bg_primary));
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 let msg_area_bg = Style::default().bg(app.theme.bg_primary);
262 for (i, line_idx) in (start..end).enumerate() {
263 let line = &all_lines[line_idx];
264 let y = inner.y + i as u16;
265 let line_area = Rect::new(inner.x, y, inner.width, 1);
266 let p = Paragraph::new(line.clone()).style(msg_area_bg);
268 f.render_widget(p, line_area);
269 }
270}
271
272pub fn draw_input(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
276 let t = &app.theme;
277 let usable_width = area.width.saturating_sub(2 + 4) as usize;
279
280 let chars: Vec<char> = app.input.chars().collect();
281
282 let before_all: String = chars[..app.cursor_pos].iter().collect();
284 let before_width = display_width(&before_all);
285
286 let scroll_offset_chars = if before_width >= usable_width {
288 let target_width = before_width.saturating_sub(usable_width / 2);
290 let mut w = 0;
291 let mut skip = 0;
292 for (i, &ch) in chars.iter().enumerate() {
293 if w >= target_width {
294 skip = i;
295 break;
296 }
297 w += char_width(ch);
298 }
299 skip
300 } else {
301 0
302 };
303
304 let visible_chars = &chars[scroll_offset_chars..];
306 let cursor_in_visible = app.cursor_pos - scroll_offset_chars;
307
308 let before: String = visible_chars[..cursor_in_visible].iter().collect();
309 let cursor_ch = if cursor_in_visible < visible_chars.len() {
310 visible_chars[cursor_in_visible].to_string()
311 } else {
312 " ".to_string()
313 };
314 let after: String = if cursor_in_visible < visible_chars.len() {
315 visible_chars[cursor_in_visible + 1..].iter().collect()
316 } else {
317 String::new()
318 };
319
320 let prompt_style = if app.is_loading {
321 Style::default().fg(t.input_prompt_loading)
322 } else {
323 Style::default().fg(t.input_prompt)
324 };
325 let prompt_text = if app.is_loading { " .. " } else { " > " };
326
327 let full_visible = format!("{}{}{}", before, cursor_ch, after);
329 let inner_height = area.height.saturating_sub(2) as usize; let wrapped_lines = wrap_text(&full_visible, usable_width);
331
332 let before_len = before.chars().count();
334 let cursor_len = cursor_ch.chars().count();
335 let cursor_global_pos = before_len; let mut cursor_line_idx: usize = 0;
337 {
338 let mut cumulative = 0usize;
339 for (li, wl) in wrapped_lines.iter().enumerate() {
340 let line_char_count = wl.chars().count();
341 if cumulative + line_char_count > cursor_global_pos {
342 cursor_line_idx = li;
343 break;
344 }
345 cumulative += line_char_count;
346 cursor_line_idx = li; }
348 }
349
350 let line_scroll = if wrapped_lines.len() <= inner_height {
352 0
353 } else if cursor_line_idx < inner_height {
354 0
355 } else {
356 cursor_line_idx.saturating_sub(inner_height - 1)
358 };
359
360 let mut display_lines: Vec<Line> = Vec::new();
362 let mut char_offset: usize = 0;
363 for wl in wrapped_lines.iter().take(line_scroll) {
365 char_offset += wl.chars().count();
366 }
367
368 for (_line_idx, wl) in wrapped_lines
369 .iter()
370 .skip(line_scroll)
371 .enumerate()
372 .take(inner_height.max(1))
373 {
374 let mut spans: Vec<Span> = Vec::new();
375 if _line_idx == 0 && line_scroll == 0 {
376 spans.push(Span::styled(prompt_text, prompt_style));
377 } else {
378 spans.push(Span::styled(" ", Style::default())); }
380
381 let line_chars: Vec<char> = wl.chars().collect();
383 let mut seg_start = 0;
384 for (ci, &ch) in line_chars.iter().enumerate() {
385 let global_idx = char_offset + ci;
386 let is_cursor = global_idx >= before_len && global_idx < before_len + cursor_len;
387
388 if is_cursor {
389 if ci > seg_start {
391 let seg: String = line_chars[seg_start..ci].iter().collect();
392 spans.push(Span::styled(seg, Style::default().fg(t.text_white)));
393 }
394 spans.push(Span::styled(
395 ch.to_string(),
396 Style::default().fg(t.cursor_fg).bg(t.cursor_bg),
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(t.text_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(" ", Style::default().fg(t.cursor_fg).bg(t.cursor_bg)),
415 ]));
416 }
417
418 let input_widget = Paragraph::new(display_lines).block(
419 Block::default()
420 .borders(Borders::ALL)
421 .border_type(ratatui::widgets::BorderType::Rounded)
422 .border_style(if app.is_loading {
423 Style::default().fg(t.border_input_loading)
424 } else {
425 Style::default().fg(t.border_input)
426 })
427 .title(Span::styled(" 输入消息 ", Style::default().fg(t.text_dim)))
428 .style(Style::default().bg(t.bg_input)),
429 );
430
431 f.render_widget(input_widget, area);
432
433 if !app.is_loading {
436 let prompt_w: u16 = 4; let border_left: u16 = 1; let cursor_col_in_line = {
441 let mut col = 0usize;
442 let mut char_count = 0usize;
443 let mut skip_chars = 0usize;
445 for wl in wrapped_lines.iter().take(line_scroll) {
446 skip_chars += wl.chars().count();
447 }
448 for wl in wrapped_lines.iter().skip(line_scroll) {
450 let line_len = wl.chars().count();
451 if skip_chars + char_count + line_len > cursor_global_pos {
452 let pos_in_line = cursor_global_pos - (skip_chars + char_count);
454 col = wl.chars().take(pos_in_line).map(|c| char_width(c)).sum();
455 break;
456 }
457 char_count += line_len;
458 }
459 col as u16
460 };
461
462 let cursor_row_in_display = (cursor_line_idx - line_scroll) as u16;
464
465 let cursor_x = area.x + border_left + prompt_w + cursor_col_in_line;
466 let cursor_y = area.y + 1 + cursor_row_in_display; if cursor_x < area.x + area.width && cursor_y < area.y + area.height {
470 f.set_cursor_position((cursor_x, cursor_y));
471 }
472 }
473}
474
475pub fn draw_hint_bar(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
477 let t = &app.theme;
478 let hints = match app.mode {
479 ChatMode::Chat => {
480 vec![
481 ("Enter", "发送"),
482 ("↑↓", "滚动"),
483 ("Ctrl+T", "切换模型"),
484 ("Ctrl+L", "清空"),
485 ("Ctrl+Y", "复制"),
486 ("Ctrl+B", "浏览"),
487 ("Ctrl+S", "流式切换"),
488 ("Ctrl+E", "配置"),
489 ("?/F1", "帮助"),
490 ("Esc", "退出"),
491 ]
492 }
493 ChatMode::SelectModel => {
494 vec![("↑↓/jk", "移动"), ("Enter", "确认"), ("Esc", "取消")]
495 }
496 ChatMode::Browse => {
497 vec![("↑↓", "选择消息"), ("y/Enter", "复制"), ("Esc", "返回")]
498 }
499 ChatMode::Help => {
500 vec![("任意键", "返回")]
501 }
502 ChatMode::Config => {
503 vec![
504 ("↑↓", "切换字段"),
505 ("Enter", "编辑"),
506 ("Tab", "切换 Provider"),
507 ("a", "新增"),
508 ("d", "删除"),
509 ("Esc", "保存返回"),
510 ]
511 }
512 };
513
514 let mut spans: Vec<Span> = Vec::new();
515 spans.push(Span::styled(" ", Style::default()));
516 for (i, (key, desc)) in hints.iter().enumerate() {
517 if i > 0 {
518 spans.push(Span::styled(" │ ", Style::default().fg(t.hint_separator)));
519 }
520 spans.push(Span::styled(
521 format!(" {} ", key),
522 Style::default().fg(t.hint_key_fg).bg(t.hint_key_bg),
523 ));
524 spans.push(Span::styled(
525 format!(" {}", desc),
526 Style::default().fg(t.hint_desc),
527 ));
528 }
529
530 let hint_bar = Paragraph::new(Line::from(spans)).style(Style::default().bg(t.bg_primary));
531 f.render_widget(hint_bar, area);
532}
533
534pub fn draw_toast(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
536 let t = &app.theme;
537 if let Some((ref msg, is_error, _)) = app.toast {
538 let text_width = display_width(msg);
539 let toast_width = (text_width + 10).min(area.width as usize).max(16) as u16;
541 let toast_height: u16 = 3;
542
543 let x = area.width.saturating_sub(toast_width + 1);
545 let y: u16 = 1;
546
547 if x + toast_width <= area.width && y + toast_height <= area.height {
548 let toast_area = Rect::new(x, y, toast_width, toast_height);
549
550 let clear = Block::default().style(Style::default().bg(if is_error {
552 t.toast_error_bg
553 } else {
554 t.toast_success_bg
555 }));
556 f.render_widget(clear, toast_area);
557
558 let (icon, border_color, text_color) = if is_error {
559 ("❌", t.toast_error_border, t.toast_error_text)
560 } else {
561 ("✅", t.toast_success_border, t.toast_success_text)
562 };
563
564 let toast_widget = Paragraph::new(Line::from(vec![
565 Span::styled(format!(" {} ", icon), Style::default()),
566 Span::styled(msg.as_str(), Style::default().fg(text_color)),
567 ]))
568 .block(
569 Block::default()
570 .borders(Borders::ALL)
571 .border_type(ratatui::widgets::BorderType::Rounded)
572 .border_style(Style::default().fg(border_color))
573 .style(Style::default().bg(if is_error {
574 t.toast_error_bg
575 } else {
576 t.toast_success_bg
577 })),
578 );
579 f.render_widget(toast_widget, toast_area);
580 }
581 }
582}
583
584pub fn draw_model_selector(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
586 let t = &app.theme;
587 let items: Vec<ListItem> = app
588 .agent_config
589 .providers
590 .iter()
591 .enumerate()
592 .map(|(i, p)| {
593 let is_active = i == app.agent_config.active_index;
594 let marker = if is_active { " ● " } else { " ○ " };
595 let style = if is_active {
596 Style::default()
597 .fg(t.model_sel_active)
598 .add_modifier(Modifier::BOLD)
599 } else {
600 Style::default().fg(t.model_sel_inactive)
601 };
602 let detail = format!("{}{} ({})", marker, p.name, p.model);
603 ListItem::new(Line::from(Span::styled(detail, style)))
604 })
605 .collect();
606
607 let list = List::new(items)
608 .block(
609 Block::default()
610 .borders(Borders::ALL)
611 .border_type(ratatui::widgets::BorderType::Rounded)
612 .border_style(Style::default().fg(t.model_sel_border))
613 .title(Span::styled(
614 " 🔄 选择模型 ",
615 Style::default()
616 .fg(t.model_sel_title)
617 .add_modifier(Modifier::BOLD),
618 ))
619 .style(Style::default().bg(t.bg_title)),
620 )
621 .highlight_style(
622 Style::default()
623 .bg(t.model_sel_highlight_bg)
624 .fg(t.text_white)
625 .add_modifier(Modifier::BOLD),
626 )
627 .highlight_symbol(" ▸ ");
628
629 f.render_stateful_widget(list, area, &mut app.model_list_state);
630}
631
632pub fn draw_help(f: &mut ratatui::Frame, area: Rect, app: &ChatApp) {
634 let t = &app.theme;
635 let separator = Line::from(Span::styled(
636 " ─────────────────────────────────────────",
637 Style::default().fg(t.separator),
638 ));
639
640 let help_lines = vec![
641 Line::from(""),
642 Line::from(Span::styled(
643 " 📖 快捷键帮助",
644 Style::default()
645 .fg(t.help_title)
646 .add_modifier(Modifier::BOLD),
647 )),
648 Line::from(""),
649 separator.clone(),
650 Line::from(""),
651 Line::from(vec![
652 Span::styled(
653 " Enter ",
654 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
655 ),
656 Span::styled("发送消息", Style::default().fg(t.help_desc)),
657 ]),
658 Line::from(vec![
659 Span::styled(
660 " ↑ / ↓ ",
661 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
662 ),
663 Span::styled("滚动对话记录", Style::default().fg(t.help_desc)),
664 ]),
665 Line::from(vec![
666 Span::styled(
667 " ← / → ",
668 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
669 ),
670 Span::styled("移动输入光标", Style::default().fg(t.help_desc)),
671 ]),
672 Line::from(vec![
673 Span::styled(
674 " Ctrl+T ",
675 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
676 ),
677 Span::styled("切换模型", Style::default().fg(t.help_desc)),
678 ]),
679 Line::from(vec![
680 Span::styled(
681 " Ctrl+L ",
682 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
683 ),
684 Span::styled("清空对话历史", Style::default().fg(t.help_desc)),
685 ]),
686 Line::from(vec![
687 Span::styled(
688 " Ctrl+Y ",
689 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
690 ),
691 Span::styled("复制最后一条 AI 回复", Style::default().fg(t.help_desc)),
692 ]),
693 Line::from(vec![
694 Span::styled(
695 " Ctrl+B ",
696 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
697 ),
698 Span::styled(
699 "浏览消息 (↑↓选择, y/Enter复制)",
700 Style::default().fg(t.help_desc),
701 ),
702 ]),
703 Line::from(vec![
704 Span::styled(
705 " Ctrl+S ",
706 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
707 ),
708 Span::styled("切换流式/整体输出", Style::default().fg(t.help_desc)),
709 ]),
710 Line::from(vec![
711 Span::styled(
712 " Ctrl+E ",
713 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
714 ),
715 Span::styled("打开配置界面", Style::default().fg(t.help_desc)),
716 ]),
717 Line::from(vec![
718 Span::styled(
719 " Esc / Ctrl+C ",
720 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
721 ),
722 Span::styled("退出对话", Style::default().fg(t.help_desc)),
723 ]),
724 Line::from(vec![
725 Span::styled(
726 " ? / F1 ",
727 Style::default().fg(t.help_key).add_modifier(Modifier::BOLD),
728 ),
729 Span::styled("显示 / 关闭此帮助", Style::default().fg(t.help_desc)),
730 ]),
731 Line::from(""),
732 separator,
733 Line::from(""),
734 Line::from(Span::styled(
735 " 📁 配置文件:",
736 Style::default()
737 .fg(t.help_title)
738 .add_modifier(Modifier::BOLD),
739 )),
740 Line::from(Span::styled(
741 format!(" {}", agent_config_path().display()),
742 Style::default().fg(t.help_path),
743 )),
744 ];
745
746 let help_block = Block::default()
747 .borders(Borders::ALL)
748 .border_type(ratatui::widgets::BorderType::Rounded)
749 .border_style(Style::default().fg(t.border_title))
750 .title(Span::styled(
751 " 帮助 (按任意键返回) ",
752 Style::default().fg(t.text_dim),
753 ))
754 .style(Style::default().bg(t.help_bg));
755 let help_widget = Paragraph::new(help_lines).block(help_block);
756 f.render_widget(help_widget, area);
757}
758
759pub fn draw_config_screen(f: &mut ratatui::Frame, area: Rect, app: &mut ChatApp) {
762 let t = &app.theme;
763 let bg = t.bg_title;
764 let total_provider_fields = CONFIG_FIELDS.len();
765
766 let mut lines: Vec<Line> = Vec::new();
767 lines.push(Line::from(""));
768
769 lines.push(Line::from(vec![Span::styled(
771 " ⚙️ 模型配置",
772 Style::default()
773 .fg(t.config_title)
774 .add_modifier(Modifier::BOLD),
775 )]));
776 lines.push(Line::from(""));
777
778 let provider_count = app.agent_config.providers.len();
780 if provider_count > 0 {
781 let mut tab_spans: Vec<Span> = vec![Span::styled(" ", Style::default())];
782 for (i, p) in app.agent_config.providers.iter().enumerate() {
783 let is_current = i == app.config_provider_idx;
784 let is_active = i == app.agent_config.active_index;
785 let marker = if is_active { "● " } else { "○ " };
786 let label = format!(" {}{} ", marker, p.name);
787 if is_current {
788 tab_spans.push(Span::styled(
789 label,
790 Style::default()
791 .fg(t.config_tab_active_fg)
792 .bg(t.config_tab_active_bg)
793 .add_modifier(Modifier::BOLD),
794 ));
795 } else {
796 tab_spans.push(Span::styled(
797 label,
798 Style::default().fg(t.config_tab_inactive),
799 ));
800 }
801 if i < provider_count - 1 {
802 tab_spans.push(Span::styled(" │ ", Style::default().fg(t.separator)));
803 }
804 }
805 tab_spans.push(Span::styled(
806 " (● = 活跃模型, Tab 切换, s 设为活跃)",
807 Style::default().fg(t.config_dim),
808 ));
809 lines.push(Line::from(tab_spans));
810 } else {
811 lines.push(Line::from(Span::styled(
812 " (无 Provider,按 a 新增)",
813 Style::default().fg(t.config_toggle_off),
814 )));
815 }
816 lines.push(Line::from(""));
817
818 lines.push(Line::from(Span::styled(
820 " ─────────────────────────────────────────",
821 Style::default().fg(t.separator),
822 )));
823 lines.push(Line::from(""));
824
825 if provider_count > 0 {
827 lines.push(Line::from(Span::styled(
828 " 📦 Provider 配置",
829 Style::default()
830 .fg(t.config_section)
831 .add_modifier(Modifier::BOLD),
832 )));
833 lines.push(Line::from(""));
834
835 for i in 0..total_provider_fields {
836 let is_selected = app.config_field_idx == i;
837 let label = config_field_label(i);
838 let value = if app.config_editing && is_selected {
839 app.config_edit_buf.clone()
840 } else {
841 config_field_value(app, i)
842 };
843
844 let pointer = if is_selected { " ▸ " } else { " " };
845 let pointer_style = if is_selected {
846 Style::default().fg(t.config_pointer)
847 } else {
848 Style::default()
849 };
850
851 let label_style = if is_selected {
852 Style::default()
853 .fg(t.config_label_selected)
854 .add_modifier(Modifier::BOLD)
855 } else {
856 Style::default().fg(t.config_label)
857 };
858
859 let value_style = if app.config_editing && is_selected {
860 Style::default().fg(t.text_white).bg(t.config_edit_bg)
861 } else if is_selected {
862 Style::default().fg(t.text_white)
863 } else {
864 if CONFIG_FIELDS[i] == "api_key" {
865 Style::default().fg(t.config_api_key)
866 } else {
867 Style::default().fg(t.config_value)
868 }
869 };
870
871 let edit_indicator = if app.config_editing && is_selected {
872 " ✏️"
873 } else {
874 ""
875 };
876
877 lines.push(Line::from(vec![
878 Span::styled(pointer, pointer_style),
879 Span::styled(format!("{:<10}", label), label_style),
880 Span::styled(" ", Style::default()),
881 Span::styled(
882 if value.is_empty() {
883 "(空)".to_string()
884 } else {
885 value
886 },
887 value_style,
888 ),
889 Span::styled(edit_indicator, Style::default()),
890 ]));
891 }
892 }
893
894 lines.push(Line::from(""));
895 lines.push(Line::from(Span::styled(
897 " ─────────────────────────────────────────",
898 Style::default().fg(t.separator),
899 )));
900 lines.push(Line::from(""));
901
902 lines.push(Line::from(Span::styled(
904 " 🌐 全局配置",
905 Style::default()
906 .fg(t.config_section)
907 .add_modifier(Modifier::BOLD),
908 )));
909 lines.push(Line::from(""));
910
911 for i in 0..CONFIG_GLOBAL_FIELDS.len() {
912 let field_idx = total_provider_fields + i;
913 let is_selected = app.config_field_idx == field_idx;
914 let label = config_field_label(field_idx);
915 let value = if app.config_editing && is_selected {
916 app.config_edit_buf.clone()
917 } else {
918 config_field_value(app, field_idx)
919 };
920
921 let pointer = if is_selected { " ▸ " } else { " " };
922 let pointer_style = if is_selected {
923 Style::default().fg(t.config_pointer)
924 } else {
925 Style::default()
926 };
927
928 let label_style = if is_selected {
929 Style::default()
930 .fg(t.config_label_selected)
931 .add_modifier(Modifier::BOLD)
932 } else {
933 Style::default().fg(t.config_label)
934 };
935
936 let value_style = if app.config_editing && is_selected {
937 Style::default().fg(t.text_white).bg(t.config_edit_bg)
938 } else if is_selected {
939 Style::default().fg(t.text_white)
940 } else {
941 Style::default().fg(t.config_value)
942 };
943
944 let edit_indicator = if app.config_editing && is_selected {
945 " ✏️"
946 } else {
947 ""
948 };
949
950 if CONFIG_GLOBAL_FIELDS[i] == "stream_mode" {
952 let toggle_on = app.agent_config.stream_mode;
953 let toggle_style = if toggle_on {
954 Style::default()
955 .fg(t.config_toggle_on)
956 .add_modifier(Modifier::BOLD)
957 } else {
958 Style::default().fg(t.config_toggle_off)
959 };
960 let toggle_text = if toggle_on {
961 "● 开启"
962 } else {
963 "○ 关闭"
964 };
965
966 lines.push(Line::from(vec![
967 Span::styled(pointer, pointer_style),
968 Span::styled(format!("{:<10}", label), label_style),
969 Span::styled(" ", Style::default()),
970 Span::styled(toggle_text, toggle_style),
971 Span::styled(
972 if is_selected { " (Enter 切换)" } else { "" },
973 Style::default().fg(t.config_dim),
974 ),
975 ]));
976 } else if CONFIG_GLOBAL_FIELDS[i] == "theme" {
977 let theme_name = app.agent_config.theme.display_name();
979 lines.push(Line::from(vec![
980 Span::styled(pointer, pointer_style),
981 Span::styled(format!("{:<10}", label), label_style),
982 Span::styled(" ", Style::default()),
983 Span::styled(
984 format!("🎨 {}", theme_name),
985 Style::default()
986 .fg(t.config_toggle_on)
987 .add_modifier(Modifier::BOLD),
988 ),
989 Span::styled(
990 if is_selected { " (Enter 切换)" } else { "" },
991 Style::default().fg(t.config_dim),
992 ),
993 ]));
994 } else {
995 lines.push(Line::from(vec![
996 Span::styled(pointer, pointer_style),
997 Span::styled(format!("{:<10}", label), label_style),
998 Span::styled(" ", Style::default()),
999 Span::styled(
1000 if value.is_empty() {
1001 "(空)".to_string()
1002 } else {
1003 value
1004 },
1005 value_style,
1006 ),
1007 Span::styled(edit_indicator, Style::default()),
1008 ]));
1009 }
1010 }
1011
1012 lines.push(Line::from(""));
1013 lines.push(Line::from(""));
1014
1015 lines.push(Line::from(Span::styled(
1017 " ─────────────────────────────────────────",
1018 Style::default().fg(t.separator),
1019 )));
1020 lines.push(Line::from(""));
1021 lines.push(Line::from(vec![
1022 Span::styled(" ", Style::default()),
1023 Span::styled(
1024 "↑↓/jk",
1025 Style::default()
1026 .fg(t.config_hint_key)
1027 .add_modifier(Modifier::BOLD),
1028 ),
1029 Span::styled(" 切换字段 ", Style::default().fg(t.config_hint_desc)),
1030 Span::styled(
1031 "Enter",
1032 Style::default()
1033 .fg(t.config_hint_key)
1034 .add_modifier(Modifier::BOLD),
1035 ),
1036 Span::styled(" 编辑 ", Style::default().fg(t.config_hint_desc)),
1037 Span::styled(
1038 "Tab/←→",
1039 Style::default()
1040 .fg(t.config_hint_key)
1041 .add_modifier(Modifier::BOLD),
1042 ),
1043 Span::styled(" 切换 Provider ", Style::default().fg(t.config_hint_desc)),
1044 Span::styled(
1045 "a",
1046 Style::default()
1047 .fg(t.config_hint_key)
1048 .add_modifier(Modifier::BOLD),
1049 ),
1050 Span::styled(" 新增 ", Style::default().fg(t.config_hint_desc)),
1051 Span::styled(
1052 "d",
1053 Style::default()
1054 .fg(t.config_hint_key)
1055 .add_modifier(Modifier::BOLD),
1056 ),
1057 Span::styled(" 删除 ", Style::default().fg(t.config_hint_desc)),
1058 Span::styled(
1059 "s",
1060 Style::default()
1061 .fg(t.config_hint_key)
1062 .add_modifier(Modifier::BOLD),
1063 ),
1064 Span::styled(" 设为活跃 ", Style::default().fg(t.config_hint_desc)),
1065 Span::styled(
1066 "Esc",
1067 Style::default()
1068 .fg(t.config_hint_key)
1069 .add_modifier(Modifier::BOLD),
1070 ),
1071 Span::styled(" 保存返回", Style::default().fg(t.config_hint_desc)),
1072 ]));
1073
1074 let content = Paragraph::new(lines)
1075 .block(
1076 Block::default()
1077 .borders(Borders::ALL)
1078 .border_type(ratatui::widgets::BorderType::Rounded)
1079 .border_style(Style::default().fg(t.border_config))
1080 .title(Span::styled(
1081 " ⚙️ 模型配置编辑 ",
1082 Style::default()
1083 .fg(t.config_label_selected)
1084 .add_modifier(Modifier::BOLD),
1085 ))
1086 .style(Style::default().bg(bg)),
1087 )
1088 .scroll((0, 0));
1089 f.render_widget(content, area);
1090}