Refactor TUI message rendering: improve role label handling, add emoji labels, enhance line wrapping, and optimize loading indicator logic.

This commit is contained in:
2025-09-29 22:48:51 +02:00
parent c17af3fee5
commit b8d1866b7d

View File

@@ -361,10 +361,10 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
let mut lines: Vec<Line> = Vec::new(); let mut lines: Vec<Line> = Vec::new();
for (message_index, message) in conversation.messages.iter().enumerate() { for (message_index, message) in conversation.messages.iter().enumerate() {
let role = &message.role; let role = &message.role;
let prefix = match role { let (emoji, name) = match role {
Role::User => "👤 You:", Role::User => ("👤 ", "You: "),
Role::Assistant => "🤖 Assistant:", Role::Assistant => ("🤖 ", "Assistant: "),
Role::System => "⚙️ System:", Role::System => ("⚙️ ", "System: "),
}; };
let formatted = formatter.format_message(message); let formatted = formatter.format_message(message);
@@ -375,36 +375,87 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
.unwrap_or(false); .unwrap_or(false);
let show_role_labels = formatter.show_role_labels(); let show_role_labels = formatter.show_role_labels();
let indent = if show_role_labels { " " } else { "" };
if show_role_labels { if show_role_labels {
let mut role_spans = vec![Span::styled( // Calculate the prefix width for proper wrapping
prefix, let prefix = format!("{emoji}{name}");
role_color(role).add_modifier(Modifier::BOLD), let prefix_width = UnicodeWidthStr::width(prefix.as_str());
)];
// Add loading animation for Assistant if currently loading and this is the last message // Join all formatted lines into single content string
if matches!(role, Role::Assistant) && let content = formatted.join("\n");
// Add loading indicator if applicable
let loading_indicator = if matches!(role, Role::Assistant) &&
app.get_loading_indicator() != "" && app.get_loading_indicator() != "" &&
message_index == conversation.messages.len() - 1 && message_index == conversation.messages.len() - 1 &&
is_streaming { is_streaming {
role_spans.push(Span::styled( format!("{} ", app.get_loading_indicator())
format!(" {}", app.get_loading_indicator()), } else {
Style::default().fg(Color::Yellow), String::new()
)); };
}
lines.push(Line::from(role_spans)); // Wrap content considering available width minus prefix
let available_width = (content_width as usize).saturating_sub(prefix_width);
let chunks = if available_width > 0 {
wrap(&content, available_width)
} else {
vec![]
};
if chunks.is_empty() {
let mut first_line_spans = vec![
Span::raw(emoji),
Span::styled(name, role_color(role).add_modifier(Modifier::BOLD)),
];
if !loading_indicator.is_empty() {
first_line_spans.push(Span::styled(
loading_indicator,
Style::default().fg(Color::Yellow),
));
}
lines.push(Line::from(first_line_spans));
} else {
let chunks_len = chunks.len();
for (i, seg) in chunks.into_iter().enumerate() {
if i == 0 {
let mut first_line_spans = vec![
Span::raw(emoji),
Span::styled(name, role_color(role).add_modifier(Modifier::BOLD)),
];
if !loading_indicator.is_empty() {
first_line_spans.push(Span::styled(
loading_indicator.clone(),
Style::default().fg(Color::Yellow),
));
}
first_line_spans.push(Span::raw(seg.into_owned()));
if chunks_len == 1 && is_streaming {
first_line_spans.push(Span::styled("", Style::default().fg(Color::Magenta)));
}
lines.push(Line::from(first_line_spans));
} else {
let mut spans = vec![Span::raw(seg.into_owned())];
if i == chunks_len - 1 && is_streaming {
spans.push(Span::styled("", Style::default().fg(Color::Magenta)));
}
lines.push(Line::from(spans));
}
}
}
} else {
// No role labels - just show content
let content = formatted.join("\n");
let chunks = wrap(&content, content_width as usize);
let chunks_len = chunks.len();
for (i, seg) in chunks.into_iter().enumerate() {
let mut spans = vec![Span::raw(seg.into_owned())];
if i == chunks_len - 1 && is_streaming {
spans.push(Span::styled("", Style::default().fg(Color::Magenta)));
}
lines.push(Line::from(spans));
}
} }
for (i, line) in formatted.iter().enumerate() {
let mut spans = Vec::new();
spans.push(Span::raw(format!("{indent}{line}")));
if i == formatted.len() - 1 && is_streaming {
spans.push(Span::styled("", Style::default().fg(Color::Magenta)));
}
lines.push(Line::from(spans));
}
// Add an empty line after each message, except the last one // Add an empty line after each message, except the last one
if message_index < conversation.messages.len() - 1 { if message_index < conversation.messages.len() - 1 {
lines.push(Line::from("")); lines.push(Line::from(""));
@@ -419,8 +470,9 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
if app.get_loading_indicator() != "" && last_message_is_user { if app.get_loading_indicator() != "" && last_message_is_user {
let loading_spans = vec![ let loading_spans = vec![
Span::raw("🤖 "),
Span::styled( Span::styled(
"🤖 Assistant:", "Assistant:",
Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD), Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD),
), ),
Span::styled( Span::styled(
@@ -435,33 +487,19 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
lines.push(Line::from("No messages yet. Press 'i' to start typing.")); lines.push(Line::from("No messages yet. Press 'i' to start typing."));
} }
// Wrap lines to get accurate content height
let wrapped: Vec<Line> = {
use textwrap::wrap;
let mut out = Vec::new();
for l in &lines {
let s = l.to_string();
for w in wrap(&s, content_width as usize) {
out.push(Line::from(w.into_owned()));
}
}
out
};
// Update AutoScroll state with accurate content length // Update AutoScroll state with accurate content length
let auto_scroll = app.auto_scroll_mut(); let auto_scroll = app.auto_scroll_mut();
auto_scroll.content_len = wrapped.len(); auto_scroll.content_len = lines.len();
auto_scroll.on_viewport(viewport_height); auto_scroll.on_viewport(viewport_height);
let scroll_position = app.scroll().min(u16::MAX as usize) as u16; let scroll_position = app.scroll().min(u16::MAX as usize) as u16;
let paragraph = Paragraph::new(wrapped) let paragraph = Paragraph::new(lines)
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(95, 20, 135))), .border_style(Style::default().fg(Color::Rgb(95, 20, 135))),
) )
.wrap(Wrap { trim: false })
.scroll((scroll_position, 0)); .scroll((scroll_position, 0));
frame.render_widget(paragraph, area); frame.render_widget(paragraph, area);