Implement chat and markdown views, enhance status bar, and add workflow panel
- Added `chat.rs` for rendering chat messages with timestamps and roles. - Introduced `markdown.rs` for rendering markdown content with styling. - Created `status.rs` to display the application status bar with session and message counts. - Developed `workflow.rs` to show the current workflow status, including tool calls and active jobs. - Established a `theme.rs` for centralized color management across the UI. - Updated `mod.rs` to include new modules and manage rendering logic.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
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();
|
||||
|
||||
if scroll_offset > 0 {
|
||||
let above = messages.len().saturating_sub(scroll_offset + max_visible).saturating_sub(1);
|
||||
if above > 0 {
|
||||
display_lines.push(Line::from(Span::styled(
|
||||
format!(" ↑ {} more messages above", above),
|
||||
Style::default().fg(Theme::DIM),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let iter_start = if scroll_offset + max_visible >= messages.len() {
|
||||
0usize
|
||||
} else {
|
||||
messages.len().saturating_sub(scroll_offset + max_visible)
|
||||
};
|
||||
|
||||
for msg in messages.iter().skip(iter_start).take(scroll_offset + max_visible) {
|
||||
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 content_str = if msg.content.is_empty() {
|
||||
"(streaming...)".to_string()
|
||||
} else {
|
||||
msg.content.clone()
|
||||
};
|
||||
let content_line = Line::from(vec![
|
||||
Span::styled(format!("{} ", prefix), Style::default().fg(role_color)),
|
||||
Span::styled(content_str, Style::default().fg(Theme::TEXT)),
|
||||
]);
|
||||
|
||||
display_lines.push(header);
|
||||
display_lines.push(content_line);
|
||||
display_lines.push(Line::from(Span::raw("")));
|
||||
}
|
||||
|
||||
if messages.len() > scroll_offset + max_visible {
|
||||
let below = messages.len().saturating_sub(scroll_offset + max_visible);
|
||||
display_lines.push(Line::from(Span::styled(
|
||||
format!(" ↓ {} more messages below", below),
|
||||
Style::default().fg(Theme::DIM),
|
||||
)));
|
||||
}
|
||||
|
||||
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 end_idx = total.saturating_sub(scroll_offset);
|
||||
let start_idx = end_idx.saturating_sub(max_visible);
|
||||
let end_idx = end_idx.min(total);
|
||||
let visible: Vec<Line> = if start_idx < end_idx {
|
||||
display_lines[start_idx..end_idx].to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let paragraph = Paragraph::new(visible)
|
||||
.block(block)
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
Reference in New Issue
Block a user