use gpui::prelude::*;
use gpui::{div, px, App, ClickEvent, ElementId, IntoElement, SharedString, Window};
use crate::devtools::Probed;
use crate::feedback::{Loader, LoaderVariant};
use crate::icon::{Icon, IconName};
use crate::input::ClickHandler;
use crate::style::MONO_FAMILY;
use crate::theme::{theme, Color, ColorName, Size};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AIToolStatus {
#[default]
Pending,
Running,
Ok,
Error,
}
impl AIToolStatus {
fn label(self) -> &'static str {
match self {
AIToolStatus::Pending => "queued",
AIToolStatus::Running => "running",
AIToolStatus::Ok => "done",
AIToolStatus::Error => "failed",
}
}
}
#[derive(IntoElement)]
pub struct AIToolCall {
id: ElementId,
name: SharedString,
status: AIToolStatus,
arguments: Option<SharedString>,
result: Option<SharedString>,
meta: Option<SharedString>,
open: bool,
expandable: Option<bool>,
size: Size,
on_toggle: Option<ClickHandler>,
}
impl AIToolCall {
pub fn new(id: impl Into<ElementId>, name: impl Into<SharedString>) -> Self {
AIToolCall {
id: id.into(),
name: name.into(),
status: AIToolStatus::default(),
arguments: None,
result: None,
meta: None,
open: false,
expandable: None,
size: Size::Sm,
on_toggle: None,
}
}
pub fn status(mut self, status: AIToolStatus) -> Self {
self.status = status;
self
}
pub fn arguments(mut self, arguments: impl Into<SharedString>) -> Self {
self.arguments = Some(arguments.into());
self
}
pub fn result(mut self, result: impl Into<SharedString>) -> Self {
self.result = Some(result.into());
self
}
pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
self.meta = Some(meta.into());
self
}
pub fn open(mut self, open: bool) -> Self {
self.open = open;
self
}
pub fn expandable(mut self, expandable: bool) -> Self {
self.expandable = Some(expandable);
self
}
pub fn size(mut self, size: Size) -> Self {
self.size = size;
self
}
pub fn on_toggle(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_toggle = Some(Box::new(handler));
self
}
}
impl RenderOnce for AIToolCall {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let t = theme(cx);
let dimmed = t.dimmed().hsla();
let text_color = t.text().hsla();
let border = t.border().hsla();
let surface = t.surface_hover().alpha(0.6);
let radius = t.radius(Size::Sm);
let font_xs = t.font_size(Size::Xs);
let mono_font = t.font_size(self.size) * 0.92;
let accent: Color = match self.status {
AIToolStatus::Pending => t.dimmed(),
AIToolStatus::Running => t.info(),
AIToolStatus::Ok => t.success(),
AIToolStatus::Error => t.danger(),
};
let accent_hsla = accent.hsla();
let badge = match self.status {
AIToolStatus::Running => Loader::new()
.variant(LoaderVariant::Dots)
.size(Size::Xs)
.color(ColorName::Blue)
.into_any_element(),
AIToolStatus::Ok => Icon::new(IconName::Check)
.size(Size::Xs)
.color(ColorName::Green)
.into_any_element(),
AIToolStatus::Error => Icon::new(IconName::CircleX)
.size(Size::Xs)
.color(ColorName::Red)
.into_any_element(),
AIToolStatus::Pending => Icon::new(IconName::Clock)
.size(Size::Xs)
.color(ColorName::Gray)
.into_any_element(),
};
let expandable = self
.expandable
.unwrap_or(self.arguments.is_some() || self.result.is_some());
let has_toggle = self.on_toggle.is_some() && expandable;
let header = div()
.id(self.id)
.flex()
.items_center()
.gap(px(8.0))
.w_full()
.text_size(px(font_xs))
.when(has_toggle, |header| header.cursor_pointer())
.child(badge)
.child(
div()
.flex()
.items_center()
.gap(px(6.0))
.flex_1()
.min_w(px(0.0))
.text_color(text_color)
.child(
Icon::new(IconName::Wrench)
.size(Size::Xs)
.color(ColorName::Gray),
)
.child(self.name.clone()),
)
.when_some(self.meta.clone(), |header, meta| {
header.child(div().text_color(dimmed).child(meta))
})
.child(
div()
.text_color(accent_hsla)
.child(SharedString::new_static(self.status.label())),
)
.when(expandable, |header| {
header.child(
Icon::new(if self.open {
IconName::ChevronDown
} else {
IconName::ChevronRight
})
.size(Size::Xs)
.color(ColorName::Gray),
)
})
.when_some(self.on_toggle, |header, handler| {
header.on_click(move |event, window, cx| handler(event, window, cx))
});
let block = |title: &'static str, body: SharedString, color| {
div()
.flex()
.flex_col()
.gap(px(2.0))
.child(
div()
.text_size(px(font_xs))
.text_color(dimmed)
.child(SharedString::new_static(title)),
)
.child(
div()
.w_full()
.px(px(8.0))
.py(px(6.0))
.rounded(px(radius))
.bg(surface)
.font_family(MONO_FAMILY)
.text_size(px(mono_font))
.text_color(color)
.child(body),
)
};
div()
.flex()
.flex_col()
.gap(px(6.0))
.w_full()
.px(px(10.0))
.py(px(8.0))
.rounded(px(radius))
.border_1()
.border_color(if self.status == AIToolStatus::Error {
accent_hsla
} else {
border
})
.child(header)
.when(self.open, |card| {
card.when_some(self.arguments, |card, arguments| {
card.child(block("Arguments", arguments, text_color))
})
.when_some(self.result, |card, result| {
card.child(block(
if self.status == AIToolStatus::Error {
"Error"
} else {
"Result"
},
result,
if self.status == AIToolStatus::Error {
accent_hsla
} else {
text_color
},
))
})
})
.probe("AIToolCall")
}
}