tool_call_card_header.rs

  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}