166 lines
6.2 KiB
Rust
166 lines
6.2 KiB
Rust
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<Line<'static>>, 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)))
|
|
}));
|
|
}
|
|
}
|
|
}
|