use super::{colors, Animation};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
widgets::Widget,
};
use unicode_width::UnicodeWidthChar;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AgentRole {
Architect,
Coder,
Tester,
Reviewer,
Documenter,
DevOps,
Security,
Performance,
}
impl AgentRole {
pub fn icon(&self) -> &'static str {
match self {
AgentRole::Architect => "๐๏ธ",
AgentRole::Coder => "๐ป",
AgentRole::Tester => "๐งช",
AgentRole::Reviewer => "๐๏ธ",
AgentRole::Documenter => "๐",
AgentRole::DevOps => "๐",
AgentRole::Security => "๐",
AgentRole::Performance => "โก",
}
}
pub fn ascii_icon(&self) -> &'static str {
match self {
AgentRole::Architect => "[A]",
AgentRole::Coder => "[C]",
AgentRole::Tester => "[T]",
AgentRole::Reviewer => "[R]",
AgentRole::Documenter => "[D]",
AgentRole::DevOps => "[O]",
AgentRole::Security => "[S]",
AgentRole::Performance => "[P]",
}
}
pub fn color(&self) -> Color {
match self {
AgentRole::Architect => colors::PRIMARY, AgentRole::Coder => colors::SECONDARY, AgentRole::Tester => colors::ACCENT, AgentRole::Reviewer => colors::PURPLE, AgentRole::Documenter => colors::WARNING, AgentRole::DevOps => colors::ERROR, AgentRole::Security => colors::SUCCESS, AgentRole::Performance => colors::ORANGE, }
}
pub fn name(&self) -> &'static str {
match self {
AgentRole::Architect => "Architect",
AgentRole::Coder => "Coder",
AgentRole::Tester => "Tester",
AgentRole::Reviewer => "Reviewer",
AgentRole::Documenter => "Documenter",
AgentRole::DevOps => "DevOps",
AgentRole::Security => "Security",
AgentRole::Performance => "Performance",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ActivityLevel {
#[default]
Idle,
Low,
Medium,
High,
Max,
Complete,
Error,
}
impl ActivityLevel {
pub fn symbol(&self) -> &'static str {
match self {
ActivityLevel::Idle => "โ",
ActivityLevel::Low => "โ",
ActivityLevel::Medium => "โ",
ActivityLevel::High => "โ",
ActivityLevel::Max => "โ",
ActivityLevel::Complete => "โ",
ActivityLevel::Error => "โ",
}
}
pub fn dots(&self) -> u8 {
match self {
ActivityLevel::Idle => 0,
ActivityLevel::Low => 1,
ActivityLevel::Medium => 2,
ActivityLevel::High => 3,
ActivityLevel::Max => 4,
ActivityLevel::Complete => 5,
ActivityLevel::Error => 0,
}
}
}
pub struct AgentAvatar {
role: AgentRole,
activity: ActivityLevel,
token_count: u64,
pulse_phase: f32,
name: String,
ascii_mode: bool,
}
impl AgentAvatar {
pub fn new(role: AgentRole) -> Self {
Self {
role,
activity: ActivityLevel::Idle,
token_count: 0,
pulse_phase: 0.0,
name: format!("{}-1", role.name()),
ascii_mode: false,
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn with_activity(mut self, activity: ActivityLevel) -> Self {
self.activity = activity;
self
}
pub fn with_tokens(mut self, tokens: u64) -> Self {
self.token_count = tokens;
self
}
pub fn ascii_mode(mut self, enabled: bool) -> Self {
self.ascii_mode = enabled;
self
}
pub fn set_activity(&mut self, activity: ActivityLevel) {
self.activity = activity;
}
pub fn set_tokens(&mut self, tokens: u64) {
self.token_count = tokens;
}
pub fn add_tokens(&mut self, tokens: u64) {
self.token_count += tokens;
}
pub fn role(&self) -> AgentRole {
self.role
}
pub fn activity(&self) -> ActivityLevel {
self.activity
}
fn pulse_intensity(&self) -> u8 {
if self.activity == ActivityLevel::Idle {
return 255;
}
let pulse = (self.pulse_phase.sin() + 1.0) / 2.0;
(128.0 + pulse * 127.0) as u8
}
fn pulsed_color(&self) -> Color {
let base = self.role.color();
let intensity = self.pulse_intensity() as f32 / 255.0;
if let Color::Rgb(r, g, b) = base {
Color::Rgb(
(r as f32 * intensity) as u8,
(g as f32 * intensity) as u8,
(b as f32 * intensity) as u8,
)
} else {
base
}
}
fn format_tokens(&self) -> String {
if self.token_count >= 1_000_000 {
format!("{:.1}M", self.token_count as f64 / 1_000_000.0)
} else if self.token_count >= 1_000 {
format!("{}K", self.token_count / 1_000)
} else {
format!("{}", self.token_count)
}
}
}
impl Animation for AgentAvatar {
fn update(&mut self, delta_time: f32) {
let speed = match self.activity {
ActivityLevel::Idle => 0.5,
ActivityLevel::Low => 1.0,
ActivityLevel::Medium => 2.0,
ActivityLevel::High => 3.0,
ActivityLevel::Max => 5.0,
ActivityLevel::Complete => 0.0,
ActivityLevel::Error => 4.0,
};
self.pulse_phase += delta_time * speed;
if self.pulse_phase > std::f32::consts::PI * 2.0 {
self.pulse_phase -= std::f32::consts::PI * 2.0;
}
}
fn is_complete(&self) -> bool {
false }
}
impl Widget for &AgentAvatar {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width < 12 || area.height < 5 {
return;
}
let pulsed_color = self.pulsed_color();
let border_style = Style::default().fg(pulsed_color);
let content_style = Style::default().fg(Color::White);
buf[(area.x, area.y)]
.set_symbol("โ")
.set_style(border_style);
for x in area.x + 1..area.x + area.width - 1 {
buf[(x, area.y)].set_symbol("โ").set_style(border_style);
}
buf[(area.x + area.width - 1, area.y)]
.set_symbol("โ")
.set_style(border_style);
for y in area.y + 1..area.y + area.height - 1 {
buf[(area.x, y)].set_symbol("โ").set_style(border_style);
buf[(area.x + area.width - 1, y)]
.set_symbol("โ")
.set_style(border_style);
}
buf[(area.x, area.y + area.height - 1)]
.set_symbol("โ")
.set_style(border_style);
for x in area.x + 1..area.x + area.width - 1 {
buf[(x, area.y + area.height - 1)]
.set_symbol("โ")
.set_style(border_style);
}
buf[(area.x + area.width - 1, area.y + area.height - 1)]
.set_symbol("โ")
.set_style(border_style);
let inner_x = area.x + 2;
let inner_y = area.y + 1;
let icon = if self.ascii_mode {
self.role.ascii_icon()
} else {
self.role.icon()
};
let mut x_offset = 0u16;
for ch in icon.chars() {
let char_width = ch.width().unwrap_or(1) as u16;
if inner_x + x_offset + char_width < area.x + area.width {
buf[(inner_x + x_offset, inner_y)]
.set_symbol(&ch.to_string())
.set_style(Style::default().fg(self.role.color()));
x_offset += char_width;
}
}
let max_name_len = (area.width - 6).min(10) as usize;
let name_display: String = self.name.chars().take(max_name_len).collect();
for (i, ch) in name_display.chars().enumerate() {
let x = inner_x + x_offset + 1 + i as u16;
if x < area.x + area.width - 1 {
buf[(x, inner_y)]
.set_symbol(&ch.to_string())
.set_style(content_style);
}
}
if area.height > 3 {
let activity_y = inner_y + 1;
let dots_filled = self.activity.dots();
let activity_color = match self.activity {
ActivityLevel::Complete => colors::SUCCESS,
ActivityLevel::Error => colors::ERROR,
_ => pulsed_color,
};
for i in 0..5 {
let symbol = if i < dots_filled { "โ" } else { "โ" };
let x = inner_x + i as u16 * 2;
if x < area.x + area.width - 1 {
buf[(x, activity_y)]
.set_symbol(symbol)
.set_style(Style::default().fg(activity_color));
}
}
let status = self.activity.symbol();
let status_x = inner_x + 11;
if status_x < area.x + area.width - 1 {
buf[(status_x, activity_y)].set_symbol(status).set_style(
Style::default()
.fg(activity_color)
.add_modifier(Modifier::BOLD),
);
}
}
if area.height > 4 {
let token_y = inner_y + 2;
let token_text = format!("๐ซ {}", self.format_tokens());
let display_text = if self.ascii_mode {
format!("* {}", self.format_tokens())
} else {
token_text
};
let mut token_x_offset = 0u16;
for ch in display_text.chars() {
let char_width = ch.width().unwrap_or(1) as u16;
if inner_x + token_x_offset + char_width < area.x + area.width {
buf[(inner_x + token_x_offset, token_y)]
.set_symbol(&ch.to_string())
.set_style(Style::default().fg(Color::Gray));
token_x_offset += char_width;
}
}
}
}
}
#[cfg(test)]
#[path = "../../../../tests/unit/ui/tui/animation/agent_avatar/agent_avatar_test.rs"]
mod tests;