use gpui::prelude::*;
use gpui::{
div, px, Context, Entity, EventEmitter, FocusHandle, IntoElement, SharedString, Window,
};
use crate::actionicon::ActionIcon;
use crate::devtools::Probed;
use crate::icon::IconName;
use crate::input::{TextArea, TextAreaEvent, TextAreaSubmit};
use crate::style::Variant;
use crate::theme::{theme, ColorName, Size};
#[derive(Debug, Clone)]
pub enum AIComposerEvent {
Submit(String),
Stop,
Attach,
Change(String),
}
pub struct AIComposer {
input: Entity<TextArea>,
busy: bool,
attachments: bool,
hint: Option<SharedString>,
size: Size,
disabled: bool,
}
impl EventEmitter<AIComposerEvent> for AIComposer {}
impl AIComposer {
pub fn new(cx: &mut Context<Self>) -> Self {
let input = cx.new(|cx| {
TextArea::new(cx)
.rows(1)
.max_rows(12)
.submit_on_enter(true)
.placeholder("Send a message\u{2026}")
});
cx.subscribe(&input, |this, input, _event: &TextAreaSubmit, cx| {
let text = input.read(cx).text();
this.submit(text, cx);
})
.detach();
cx.subscribe(&input, |_this, _input, event: &TextAreaEvent, cx| {
cx.emit(AIComposerEvent::Change(event.0.clone()));
})
.detach();
AIComposer {
input,
busy: false,
attachments: false,
hint: None,
size: Size::Sm,
disabled: false,
}
}
pub fn placeholder(self, placeholder: impl Into<SharedString>, cx: &mut Context<Self>) -> Self {
let placeholder = placeholder.into();
self.input.update(cx, |input, cx| {
input.set_placeholder(placeholder, cx);
});
self
}
pub fn attachments(mut self, attachments: bool) -> Self {
self.attachments = attachments;
self
}
pub fn hint(mut self, hint: impl Into<SharedString>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn size(mut self, size: Size) -> Self {
self.size = size;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn input(&self) -> &Entity<TextArea> {
&self.input
}
pub fn focus_handle(&self, cx: &gpui::App) -> FocusHandle {
self.input.read(cx).focus_handle()
}
pub fn text(&self, cx: &gpui::App) -> String {
self.input.read(cx).text()
}
pub fn set_text(&mut self, text: &str, cx: &mut Context<Self>) {
self.input.update(cx, |input, cx| input.set_text(text, cx));
cx.notify();
}
pub fn set_busy(&mut self, busy: bool, cx: &mut Context<Self>) {
self.busy = busy;
cx.notify();
}
pub fn is_busy(&self) -> bool {
self.busy
}
fn submit(&mut self, text: String, cx: &mut Context<Self>) {
if self.disabled || self.busy || text.trim().is_empty() {
return;
}
self.input.update(cx, |input, cx| input.set_text("", cx));
cx.emit(AIComposerEvent::Submit(text));
cx.notify();
}
}
impl Render for AIComposer {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let t = theme(cx);
let dimmed = t.dimmed().hsla();
let font_xs = t.font_size(Size::Xs);
let empty = self.input.read(cx).is_blank();
let action = if self.busy {
ActionIcon::new("guise-ai-stop", IconName::Square)
.variant(Variant::Filled)
.color(ColorName::Red)
.size(self.size)
.on_click(cx.listener(|_this, _event, _window, cx| {
cx.emit(AIComposerEvent::Stop);
}))
} else {
ActionIcon::new("guise-ai-send", IconName::ArrowUp)
.variant(Variant::Filled)
.size(self.size)
.disabled(self.disabled || empty)
.on_click(cx.listener(|this, _event, _window, cx| {
let text = this.input.read(cx).text();
this.submit(text, cx);
}))
};
div()
.flex()
.flex_col()
.gap(px(4.0))
.w_full()
.child(
div()
.flex()
.flex_row()
.items_end()
.gap(px(8.0))
.w_full()
.child(div().flex_1().min_w(px(0.0)).child(self.input.clone()))
.when(self.attachments, |row| {
row.child(
ActionIcon::new("guise-ai-attach", IconName::Paperclip)
.variant(Variant::Subtle)
.color(ColorName::Gray)
.size(self.size)
.disabled(self.disabled)
.on_click(cx.listener(|_this, _event, _window, cx| {
cx.emit(AIComposerEvent::Attach);
})),
)
})
.child(action),
)
.when_some(self.hint.clone(), |column, hint| {
column.child(div().text_size(px(font_xs)).text_color(dimmed).child(hint))
})
.probe("AIComposer")
}
}