Files
zesdex/src/view/chat.rs
T

129 lines
4.8 KiB
Rust
Raw Normal View History

use ratatui::layout::Rect;
use ratatui::style::{Style, Modifier};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use ratatui::Frame;
use super::theme::Theme;
fn _role_name(role: &crate::dto::chat::message::Role) -> &'static str {
match role {
crate::dto::chat::message::Role::User => "User",
crate::dto::chat::message::Role::Assistant => "Assistant",
crate::dto::chat::message::Role::System => "System",
crate::dto::chat::message::Role::Tool => "Tool",
}
}
fn role_badge(role: &crate::dto::chat::message::Role) -> &'static str {
match role {
crate::dto::chat::message::Role::User => "YOU",
crate::dto::chat::message::Role::Assistant => "AI ",
crate::dto::chat::message::Role::System => "SYS",
crate::dto::chat::message::Role::Tool => "TOOL",
}
}
fn format_timestamp(ts: i64) -> String {
if ts <= 0 { return "".to_string(); }
let secs = ts / 1000;
let mins = (secs / 60) % 60;
let hrs = (secs / 3600) % 24;
format!("{:02}:{:02}", hrs, mins)
}
pub fn draw_chat(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) {
let messages = &state.transcript_cache.messages;
let scroll_offset = state.scroll.offset;
let max_visible = (area.height as usize).saturating_sub(3);
let mut display_lines: Vec<Line> = Vec::new();
let _total_msgs = messages.len();
for msg in messages.iter() {
let role_color = match msg.role {
crate::dto::chat::message::Role::User => Theme::ROLE_USER,
crate::dto::chat::message::Role::Assistant => Theme::ROLE_ASSISTANT,
crate::dto::chat::message::Role::System => Theme::ROLE_SYSTEM,
crate::dto::chat::message::Role::Tool => Theme::ROLE_TOOL,
};
let prefix = match msg.role {
crate::dto::chat::message::Role::User => ">",
crate::dto::chat::message::Role::Assistant => "•",
crate::dto::chat::message::Role::System => "#",
crate::dto::chat::message::Role::Tool => "→",
};
let ts_str = format_timestamp(msg.timestamp);
let badge = role_badge(&msg.role);
let time_display = if ts_str.is_empty() { String::new() } else { format!(" [{}]", ts_str) };
let header = Line::from(vec![
Span::styled(
format!(" {} ", badge),
Style::default().fg(Theme::BG).bg(role_color).add_modifier(Modifier::BOLD),
),
Span::styled(
time_display,
Style::default().fg(Theme::DIM),
),
]);
let is_last = std::ptr::eq(msg, messages.last().unwrap());
let content_str = if msg.content.trim().is_empty() {
if is_last && state.turn_in_flight() {
"(streaming...)".to_string()
} else {
"(tool execution)".to_string()
}
} else {
msg.content.clone()
};
let mut content_spans = vec![Span::styled(format!("{} ", prefix), Style::default().fg(role_color))];
content_spans.extend(super::markdown::render_markdown(&content_str, area.width));
let content_line = Line::from(content_spans);
display_lines.push(header);
display_lines.push(content_line);
display_lines.push(Line::from(Span::raw("")));
}
if state.turn_in_flight() {
let spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let frame = spinner_frames[(state.misc.tick_count as usize / 2) % spinner_frames.len()];
display_lines.push(Line::from(vec![
Span::styled(" AI ", Style::default().fg(Theme::BG).bg(Theme::ROLE_ASSISTANT).add_modifier(Modifier::BOLD)),
Span::styled(format!(" {} Generating...", frame), Style::default().fg(Theme::DIM)),
]));
display_lines.push(Line::from(Span::raw("")));
}
let mut title = String::from(" Chat ");
if !messages.is_empty() {
title.push_str(&format!("[{} msgs]", messages.len()));
}
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Theme::BORDER))
.title(title);
let total = display_lines.len();
let max_offset = total.saturating_sub(max_visible);
let offset = scroll_offset.min(max_offset);
let end_idx = total.saturating_sub(offset);
let start_idx = end_idx.saturating_sub(max_visible);
let visible: Vec<Line> = if start_idx < end_idx && start_idx < total {
display_lines[start_idx..end_idx].to_vec()
} else {
display_lines[total.saturating_sub(max_visible)..total].to_vec()
};
let paragraph = Paragraph::new(visible)
.block(block)
.wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}