Refactor TUI message rendering: improve role label handling, add emoji labels, enhance line wrapping, and optimize loading indicator logic.
This commit is contained in:
@@ -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 {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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),
|
Style::default().fg(Color::Yellow),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
lines.push(Line::from(first_line_spans));
|
||||||
lines.push(Line::from(role_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()));
|
||||||
for (i, line) in formatted.iter().enumerate() {
|
if chunks_len == 1 && is_streaming {
|
||||||
let mut spans = Vec::new();
|
first_line_spans.push(Span::styled(" ▌", Style::default().fg(Color::Magenta)));
|
||||||
spans.push(Span::raw(format!("{indent}{line}")));
|
}
|
||||||
if i == formatted.len() - 1 && is_streaming {
|
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)));
|
spans.push(Span::styled(" ▌", Style::default().fg(Color::Magenta)));
|
||||||
}
|
}
|
||||||
lines.push(Line::from(spans));
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user