rustyclaw_tui/components/
message_bubble.rs1use crate::markdown;
4use crate::theme;
5use iocraft::prelude::*;
6use rustyclaw_core::types::MessageRole;
7
8#[derive(Default, Props)]
9pub struct MessageBubbleProps {
10 pub role: Option<MessageRole>,
11 pub content: String,
12 pub assistant_name: Option<String>,
14}
15
16#[component]
17pub fn MessageBubble(props: &MessageBubbleProps) -> impl Into<AnyElement<'static>> {
18 let role = props.role.unwrap_or(MessageRole::System);
19 let fg = theme::role_color(&role);
20 let bg = theme::role_bg(&role);
21 let border = theme::role_border(&role);
22
23 let icon = role.icon();
24 let label = match role {
25 MessageRole::User => "You".to_string(),
26 MessageRole::Assistant => props
27 .assistant_name
28 .clone()
29 .unwrap_or_else(|| "Assistant".to_string()),
30 MessageRole::Info => "Info".to_string(),
31 MessageRole::Success => "Success".to_string(),
32 MessageRole::Warning => "Warning".to_string(),
33 MessageRole::Error => "Error".to_string(),
34 MessageRole::System => "System".to_string(),
35 MessageRole::ToolCall => "Tool Call".to_string(),
36 MessageRole::ToolResult => "Tool Result".to_string(),
37 MessageRole::Thinking => "Thinking".to_string(),
38 };
39
40 let display = if role == MessageRole::Thinking && props.content.len() > 120 {
42 format!("{}…", &props.content[..120])
43 } else if role == MessageRole::Assistant {
44 markdown::render_ansi(&props.content)
45 } else {
46 props.content.clone()
47 };
48
49 element! {
50 View(
51 width: 100pct,
52 margin_bottom: 1,
53 flex_direction: FlexDirection::Column,
54 background_color: bg,
55 border_style: BorderStyle::Round,
56 border_color: border,
57 border_edges: Edges::Left,
58 padding_left: 1,
59 padding_right: 1,
60 ) {
61 Text(content: format!("{} {}", icon, label), color: border, weight: Weight::Bold)
62 Text(content: display, color: fg, wrap: TextWrap::Wrap)
63 }
64 }
65}