1use gpui::{Animation, AnimationExt, App, IntoElement, pulsating_between};
2use std::time::Duration;
3use ui::{Tooltip, prelude::*};
4
5/// A reusable header component for tool call cards.
6#[derive(IntoElement)]
7pub struct ToolCallCardHeader {
8 icon: IconName,
9 primary_text: SharedString,
10 secondary_text: Option<SharedString>,
11 is_loading: bool,
12 error: Option<String>,
13}
14
15impl ToolCallCardHeader {
16 pub fn new(icon: IconName, primary_text: impl Into<SharedString>) -> Self {
17 Self {
18 icon,
19 primary_text: primary_text.into(),
20 secondary_text: None,
21 is_loading: false,
22 error: None,
23 }
24 }
25
26 pub fn with_secondary_text(mut self, text: impl Into<SharedString>) -> Self {
27 self.secondary_text = Some(text.into());
28 self
29 }
30
31 pub fn loading(mut self) -> Self {
32 self.is_loading = true;
33 self
34 }
35
36 pub fn with_error(mut self, error: impl Into<String>) -> Self {
37 self.error = Some(error.into());
38 self
39 }
40}
41
42impl RenderOnce for ToolCallCardHeader {
43 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
44 let font_size = rems(0.8125);
45 let secondary_text = self.secondary_text;
46
47 h_flex()
48 .id("tool-label-container")
49 .gap_1p5()
50 .max_w_full()
51 .overflow_x_scroll()
52 .opacity(0.8)
53 .child(
54 h_flex().h(window.line_height()).justify_center().child(
55 Icon::new(self.icon)
56 .size(IconSize::XSmall)
57 .color(Color::Muted),
58 ),
59 )
60 .child(
61 h_flex()
62 .h(window.line_height())
63 .gap_1p5()
64 .text_size(font_size)
65 .map(|this| {
66 if let Some(error) = &self.error {
67 this.child(format!("{} failed", self.primary_text)).child(
68 IconButton::new("error_info", IconName::Warning)
69 .shape(ui::IconButtonShape::Square)
70 .icon_size(IconSize::XSmall)
71 .icon_color(Color::Warning)
72 .tooltip(Tooltip::text(error.clone())),
73 )
74 } else {
75 this.child(self.primary_text.clone())
76 }
77 })
78 .when_some(secondary_text, |this, secondary_text| {
79 this.child(
80 div()
81 .size(px(3.))
82 .rounded_full()
83 .bg(cx.theme().colors().text),
84 )
85 .child(div().text_size(font_size).child(secondary_text.clone()))
86 })
87 .with_animation(
88 "loading-label",
89 Animation::new(Duration::from_secs(2))
90 .repeat()
91 .with_easing(pulsating_between(0.6, 1.)),
92 move |this, delta| {
93 if self.is_loading {
94 this.opacity(delta)
95 } else {
96 this
97 }
98 },
99 ),
100 )
101 }
102}