1use gpui::prelude::*;
21use gpui::{div, px, App, ClickEvent, ElementId, IntoElement, SharedString, Window};
22
23use crate::devtools::Probed;
24use crate::icon::{Icon, IconName};
25use crate::input::ClickHandler;
26use crate::markdown::Markdown;
27use crate::theme::{theme, ColorName, Size};
28
29#[derive(IntoElement)]
31pub struct AIReasoning {
32 id: ElementId,
33 text: SharedString,
34 label: Option<SharedString>,
35 open: bool,
36 streaming: bool,
37 size: Size,
38 on_toggle: Option<ClickHandler>,
39}
40
41impl AIReasoning {
42 pub fn new(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Self {
43 AIReasoning {
44 id: id.into(),
45 text: text.into(),
46 label: None,
47 open: false,
48 streaming: false,
49 size: Size::Sm,
50 on_toggle: None,
51 }
52 }
53
54 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
56 self.label = Some(label.into());
57 self
58 }
59
60 pub fn open(mut self, open: bool) -> Self {
61 self.open = open;
62 self
63 }
64
65 pub fn streaming(mut self, streaming: bool) -> Self {
67 self.streaming = streaming;
68 self
69 }
70
71 pub fn size(mut self, size: Size) -> Self {
72 self.size = size;
73 self
74 }
75
76 pub fn on_toggle(
77 mut self,
78 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
79 ) -> Self {
80 self.on_toggle = Some(Box::new(handler));
81 self
82 }
83}
84
85impl RenderOnce for AIReasoning {
86 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
87 let t = theme(cx);
88 let dimmed = t.dimmed().hsla();
89 let border = t.border().hsla();
90 let font_xs = t.font_size(Size::Xs);
91
92 let label = self.label.unwrap_or_else(|| {
93 if self.streaming {
94 SharedString::new_static("Thinking\u{2026}")
95 } else {
96 SharedString::new_static("Reasoning")
97 }
98 });
99
100 let header = div()
101 .id(self.id)
102 .flex()
103 .items_center()
104 .gap(px(6.0))
105 .cursor_pointer()
106 .text_size(px(font_xs))
107 .text_color(dimmed)
108 .child(
109 Icon::new(if self.open {
110 IconName::ChevronDown
111 } else {
112 IconName::ChevronRight
113 })
114 .size(Size::Xs)
115 .color(ColorName::Gray),
116 )
117 .child(
118 Icon::new(IconName::Brain)
119 .size(Size::Xs)
120 .color(ColorName::Gray),
121 )
122 .child(label)
123 .when_some(self.on_toggle, |header, handler| {
124 header.on_click(move |event, window, cx| handler(event, window, cx))
125 });
126
127 div()
128 .flex()
129 .flex_col()
130 .gap(px(6.0))
131 .w_full()
132 .child(header)
133 .when(self.open, |column| {
134 column.child(
135 div()
136 .w_full()
137 .pl(px(10.0))
138 .border_l_2()
139 .border_color(border)
140 .text_color(dimmed)
141 .child(Markdown::new(self.text).size(self.size)),
142 )
143 })
144 .probe("AIReasoning")
145 }
146}