Skip to main content

a2a_client/components/
task_viewer.rs

1//! Generic task viewing components
2
3use a2a_rs::domain::{Task, part};
4use serde::Serialize;
5
6/// View model for a task in a list
7#[derive(Debug, Serialize, Clone)]
8pub struct TaskView {
9    pub task_id: String,
10    pub state: String,
11    pub message_count: usize,
12    pub last_message_preview: Option<String>,
13}
14
15impl TaskView {
16    /// Create a TaskView from an A2A Task
17    pub fn from_task(task: Task) -> Self {
18        let message_count = task.history.len();
19        let last_message_preview = task.history.last().and_then(|msg| {
20            msg.parts.iter().find_map(|part| {
21                part.get_text()
22                    .map(|text| text.chars().take(100).collect::<String>())
23            })
24        });
25
26        let state = match task.status.as_option().map(|s| &s.state) {
27            Some(::buffa::EnumValue::Known(state)) => match state {
28                a2a_rs::domain::TaskState::TASK_STATE_SUBMITTED => "Submitted".to_string(),
29                a2a_rs::domain::TaskState::TASK_STATE_WORKING => "Working".to_string(),
30                a2a_rs::domain::TaskState::TASK_STATE_INPUT_REQUIRED => "InputRequired".to_string(),
31                a2a_rs::domain::TaskState::TASK_STATE_COMPLETED => "Completed".to_string(),
32                a2a_rs::domain::TaskState::TASK_STATE_CANCELED => "Canceled".to_string(),
33                a2a_rs::domain::TaskState::TASK_STATE_FAILED => "Failed".to_string(),
34                a2a_rs::domain::TaskState::TASK_STATE_REJECTED => "Rejected".to_string(),
35                a2a_rs::domain::TaskState::TASK_STATE_AUTH_REQUIRED => "AuthRequired".to_string(),
36                a2a_rs::domain::TaskState::TASK_STATE_UNSPECIFIED => "Unknown".to_string(),
37            },
38            Some(::buffa::EnumValue::Unknown(num)) => format!("Unknown({})", num),
39            None => "Unknown".to_string(),
40        };
41
42        Self {
43            task_id: task.id,
44            state,
45            message_count,
46            last_message_preview,
47        }
48    }
49}
50
51/// View model for a single message
52#[derive(Debug, Serialize, Clone)]
53pub struct MessageView {
54    pub id: String,
55    pub role: String,
56    pub content: String,
57}
58
59impl MessageView {
60    /// Create a MessageView from an A2A Message
61    pub fn from_message(msg: a2a_rs::domain::Message) -> Self {
62        // Extract text content from message parts
63        let content = msg
64            .parts
65            .iter()
66            .map(|part| match &part.content {
67                Some(part::Content::Text(text)) => text.clone(),
68                Some(part::Content::Raw(_)) => format!(
69                    "[File: {}]",
70                    if part.filename.is_empty() {
71                        "unnamed"
72                    } else {
73                        &part.filename
74                    }
75                ),
76                Some(part::Content::Url(url)) => format!(
77                    "[URL/File: {}]",
78                    if part.filename.is_empty() {
79                        url
80                    } else {
81                        &part.filename
82                    }
83                ),
84                Some(part::Content::Data(data)) => {
85                    let name = serde_json::to_value(&**data)
86                        .ok()
87                        .and_then(|v| {
88                            v.get("name")
89                                .and_then(|n| n.as_str())
90                                .map(|s| s.to_string())
91                        })
92                        .unwrap_or_else(|| "unnamed".to_string());
93                    format!("[Data: {}]", name)
94                }
95                None => String::new(),
96            })
97            .collect::<Vec<_>>()
98            .join("\n");
99
100        Self {
101            id: msg.message_id,
102            role: match &msg.role {
103                ::buffa::EnumValue::Known(a2a_rs::domain::Role::ROLE_USER) => "User".to_string(),
104                ::buffa::EnumValue::Known(a2a_rs::domain::Role::ROLE_AGENT) => "Agent".to_string(),
105                ::buffa::EnumValue::Known(r) => format!("{:?}", r),
106                ::buffa::EnumValue::Unknown(num) => format!("Unknown({})", num),
107            },
108            content,
109        }
110    }
111
112    /// Create a MessageView with JSON parsing for structured responses
113    pub fn from_message_with_json_parsing(msg: a2a_rs::domain::Message) -> Self {
114        let content = msg
115            .parts
116            .iter()
117            .filter_map(|part| part.get_text().map(|s| s.to_string()))
118            .collect::<Vec<_>>()
119            .join("\n");
120
121        // Try to parse as JSON for better display
122        let display_content =
123            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&content) {
124                if let Some(obj) = json_value.as_object() {
125                    match obj.get("type").and_then(|v| v.as_str()) {
126                        Some("form") => obj
127                            .get("instructions")
128                            .and_then(|v| v.as_str())
129                            .unwrap_or("Please fill out the form.")
130                            .to_string(),
131                        Some("result") => {
132                            let message = obj
133                                .get("message")
134                                .and_then(|v| v.as_str())
135                                .unwrap_or("Request processed.");
136                            let status = obj
137                                .get("status")
138                                .and_then(|v| v.as_str())
139                                .unwrap_or("unknown");
140                            format!("{}\n\nStatus: {}", message, status)
141                        }
142                        _ => serde_json::to_string_pretty(&json_value).unwrap_or(content.clone()),
143                    }
144                } else {
145                    content.clone()
146                }
147            } else {
148                content.clone()
149            };
150
151        Self {
152            id: msg.message_id,
153            role: match &msg.role {
154                ::buffa::EnumValue::Known(a2a_rs::domain::Role::ROLE_USER) => "User".to_string(),
155                ::buffa::EnumValue::Known(a2a_rs::domain::Role::ROLE_AGENT) => "Agent".to_string(),
156                ::buffa::EnumValue::Known(r) => format!("{:?}", r),
157                ::buffa::EnumValue::Unknown(num) => format!("Unknown({})", num),
158            },
159            content: display_content,
160        }
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use a2a_rs::domain::{Message, Task, TaskState, TaskStatus};
168
169    #[test]
170    fn test_task_view_from_task() {
171        let mut task = Task::new("task-123".to_string(), "ctx-1".to_string());
172        task.status = ::buffa::MessageField::some(TaskStatus::new(TaskState::Working, None));
173
174        // Add a message to history
175        let msg = Message::user_text("A preview text here".to_string(), "msg-1".to_string());
176        task.history = vec![msg];
177
178        let view = TaskView::from_task(task);
179        assert_eq!(view.task_id, "task-123");
180        assert_eq!(view.state, "Working");
181        assert_eq!(view.message_count, 1);
182        assert_eq!(
183            view.last_message_preview,
184            Some("A preview text here".to_string())
185        );
186    }
187
188    #[test]
189    fn test_task_view_empty_history() {
190        let task = Task::new("task-123".to_string(), "ctx-1".to_string());
191        let view = TaskView::from_task(task);
192        assert_eq!(view.message_count, 0);
193        assert_eq!(view.last_message_preview, None);
194    }
195
196    #[test]
197    fn test_message_view_from_message() {
198        let msg = Message::user_text("Hello world".to_string(), "msg-1".to_string());
199        let view = MessageView::from_message(msg);
200
201        assert_eq!(view.id, "msg-1");
202        assert_eq!(view.role, "User");
203        assert_eq!(view.content, "Hello world");
204    }
205
206    #[test]
207    fn test_message_view_with_json_parsing_result() {
208        let json_content = r#"{"type":"result","message":"Success!","status":"ok"}"#;
209        let msg = Message::user_text(json_content.to_string(), "msg-2".to_string());
210
211        let view = MessageView::from_message_with_json_parsing(msg);
212        assert_eq!(view.content, "Success!\n\nStatus: ok");
213    }
214
215    #[test]
216    fn test_message_view_with_json_parsing_form() {
217        let json_content = r#"{"type":"form","instructions":"Please provide name"}"#;
218        let msg = Message::user_text(json_content.to_string(), "msg-3".to_string());
219
220        let view = MessageView::from_message_with_json_parsing(msg);
221        assert_eq!(view.content, "Please provide name");
222    }
223}