use crate::client::tui::app::{App, MessageRole}; use crate::session::{ToolStatus, TurnBlock, TurnPhase, TurnStatus}; use ratatui::{ Frame, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph}, }; pub fn render(frame: &mut Frame, area: Rect, app: &App) { let content_width = area.width.saturating_sub(4).max(1) as usize; let mut lines = Vec::new(); if app.messages.is_empty() { lines.push(Line::from("开始一段对话,或从左侧选择已有会话。")); } for message in &app.messages { let (label, color) = match message.role { MessageRole::User => ("你", Color::Blue), MessageRole::Assistant => ("PicoBot", Color::Green), MessageRole::System => ("系统", Color::Yellow), }; lines.push(Line::from(Span::styled( label, Style::default().fg(color).add_modifier(Modifier::BOLD), ))); if let Some(reasoning) = &message.reasoning_content { lines.push(Line::from(Span::styled( "思考过程", Style::default() .fg(Color::DarkGray) .add_modifier(Modifier::ITALIC), ))); push_wrapped(&mut lines, reasoning, content_width, Color::DarkGray); } for source_line in message.content.lines() { let wrapped = textwrap::wrap(source_line, content_width); if wrapped.is_empty() { lines.push(Line::from("")); } else { lines.extend( wrapped .into_iter() .map(|line| Line::from(line.into_owned())), ); } } if message.completion_status != crate::bus::CompletionStatus::Completed { lines.push(Line::from(Span::styled( format!("[{}]", message.completion_status.as_str()), Style::default().fg(Color::Yellow), ))); } for attachment in &message.attachments { lines.push(Line::from(Span::styled( format!(" [附件 {}] {}", attachment.index + 1, attachment.name), Style::default().fg(Color::Cyan), ))); } lines.push(Line::from("")); } if let Some(turn) = &app.active_turn { lines.push(Line::from(Span::styled( "PicoBot", Style::default() .fg(Color::Green) .add_modifier(Modifier::BOLD), ))); for block in &turn.blocks { match block { TurnBlock::Reasoning { text, .. } => { lines.push(Line::from(Span::styled( "思考过程", Style::default() .fg(Color::DarkGray) .add_modifier(Modifier::ITALIC), ))); push_wrapped(&mut lines, text, content_width, Color::DarkGray); } TurnBlock::Assistant { text, .. } => { push_wrapped(&mut lines, text, content_width, Color::Reset); } TurnBlock::Tool { name, status, preview, .. } => { let status = match status { ToolStatus::Running => "执行中", ToolStatus::Completed => "已完成", ToolStatus::Cancelled => "已停止", ToolStatus::Failed => "失败", }; lines.push(Line::from(Span::styled( format!("工具 · {name} · {status}"), Style::default().fg(Color::Magenta), ))); if let Some(preview) = preview { push_wrapped(&mut lines, preview, content_width, Color::DarkGray); } } } } let phase = match turn.phase { TurnPhase::Queued => "排队中", TurnPhase::Reasoning => "思考中", TurnPhase::Responding => "生成中", TurnPhase::Acting => "调用工具中", TurnPhase::Finalizing => "收尾中", }; let status = match turn.status { TurnStatus::Running => phase, TurnStatus::Completed => "已完成", TurnStatus::Cancelled => "已停止", TurnStatus::Failed => "失败", }; lines.push(Line::from(Span::styled( format!("● {status}"), Style::default().fg(if turn.status == TurnStatus::Failed { Color::Red } else { Color::Cyan }), ))); if let Some(error) = &turn.error { push_wrapped(&mut lines, error, content_width, Color::Red); } lines.push(Line::from("")); } else if app.pending_responses > 0 { lines.push(Line::from(Span::styled( "● 正在思考…", Style::default().fg(Color::Cyan), ))); } let visible_height = area.height.saturating_sub(2); let line_count = u16::try_from(lines.len()).unwrap_or(u16::MAX); let max_scroll = line_count.saturating_sub(visible_height); let scroll = max_scroll.saturating_sub(app.chat_scroll_from_bottom.min(max_scroll)); let title = if app.chat_scroll_from_bottom > 0 { " 对话 · 已暂停自动滚动 " } else { " 对话 " }; frame.render_widget( Paragraph::new(lines) .scroll((scroll, 0)) .block(Block::default().title(title).borders(Borders::ALL)), area, ); } fn push_wrapped(lines: &mut Vec>, text: &str, width: usize, color: Color) { for source_line in text.lines() { let wrapped = textwrap::wrap(source_line, width); if wrapped.is_empty() { lines.push(Line::from("")); } else { lines.extend(wrapped.into_iter().map(|line| { Line::from(Span::styled(line.into_owned(), Style::default().fg(color))) })); } } }