Iterate on Assistant 2 composer UI (#11306)

- Change project index tool rendering in composer
- Update composer UI style

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
Nate Butler 2024-05-02 13:01:21 -04:00 committed by GitHub
parent 9bac64a9c1
commit 78a8a58ee2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 141 additions and 117 deletions

1
assets/icons/code.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-xml"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg>

After

Width:  |  Height:  |  Size: 293 B

13
assets/icons/spinner.svg Normal file
View file

@ -0,0 +1,13 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1803_28)">
<path d="M0.5 2C0.5 1.17157 1.17157 0.5 2 0.5V0.5C2.82843 0.5 3.5 1.17157 3.5 2V2C3.5 2.82843 2.82843 3.5 2 3.5V3.5C1.17157 3.5 0.5 2.82843 0.5 2V2Z" fill="black" fill-opacity="0.3"/>
<path d="M7.5 6C7.5 6.82843 6.82843 7.5 6 7.5V7.5C5.17157 7.5 4.5 6.82843 4.5 6V6C4.5 5.17157 5.17157 4.5 6 4.5V4.5C6.82843 4.5 7.5 5.17157 7.5 6V6Z" fill="black" fill-opacity="0.6"/>
<path d="M2 7.5C1.17157 7.5 0.5 6.82843 0.5 6V6C0.5 5.17157 1.17157 4.5 2 4.5V4.5C2.82843 4.5 3.5 5.17157 3.5 6V6C3.5 6.82843 2.82843 7.5 2 7.5V7.5Z" fill="black" fill-opacity="0.8"/>
<path d="M6 0.5C6.82843 0.5 7.5 1.17157 7.5 2V2C7.5 2.82843 6.82843 3.5 6 3.5V3.5C5.17157 3.5 4.5 2.82843 4.5 2V2C4.5 1.17157 5.17157 0.5 6 0.5V0.5Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_1803_28">
<rect width="8" height="8" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 956 B

View file

@ -269,7 +269,7 @@ impl AssistantChat {
composer_editor: cx.new_view(|cx| { composer_editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx); let mut editor = Editor::auto_height(80, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_placeholder_text("Type a message to the assistant", cx); editor.set_placeholder_text("Send a message…", cx);
editor editor
}), }),
list_state, list_state,
@ -372,10 +372,6 @@ impl AssistantChat {
})); }));
} }
fn can_submit(&self) -> bool {
self.pending_completion.is_none()
}
fn debug_project_index(&mut self, _: &DebugProjectIndex, cx: &mut ViewContext<Self>) { fn debug_project_index(&mut self, _: &DebugProjectIndex, cx: &mut ViewContext<Self>) {
if let Some(index) = &self.project_index { if let Some(index) = &self.project_index {
index.update(cx, |project_index, cx| { index.update(cx, |project_index, cx| {
@ -594,7 +590,6 @@ impl AssistantChat {
element.child(Composer::new( element.child(Composer::new(
body.clone(), body.clone(),
self.user_store.read(cx).current_user(), self.user_store.read(cx).current_user(),
true,
self.tool_registry.clone(), self.tool_registry.clone(),
crate::ui::ModelSelector::new( crate::ui::ModelSelector::new(
cx.view().downgrade(), cx.view().downgrade(),
@ -773,7 +768,6 @@ impl Render for AssistantChat {
.child(Composer::new( .child(Composer::new(
self.composer_editor.clone(), self.composer_editor.clone(),
self.user_store.read(cx).current_user(), self.user_store.read(cx).current_user(),
self.can_submit(),
self.tool_registry.clone(), self.tool_registry.clone(),
crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone()) crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
.into_any_element(), .into_any_element(),

View file

@ -1,14 +1,14 @@
use anyhow::Result; use anyhow::Result;
use assistant_tooling::LanguageModelTool; use assistant_tooling::LanguageModelTool;
use gpui::{prelude::*, AnyView, Model, Task}; use gpui::{percentage, prelude::*, Animation, AnimationExt, AnyView, Model, Task, Transformation};
use project::Fs; use project::Fs;
use schemars::JsonSchema; use schemars::JsonSchema;
use semantic_index::{ProjectIndex, Status}; use semantic_index::{ProjectIndex, Status};
use serde::Deserialize; use serde::Deserialize;
use std::sync::Arc; use std::{sync::Arc, time::Duration};
use ui::{ use ui::{
div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, SharedString, div, prelude::*, ButtonLike, CollapsibleContainer, Color, Icon, IconName, Indicator, Label,
WindowContext, SharedString, Tooltip, WindowContext,
}; };
use util::ResultExt as _; use util::ResultExt as _;
@ -255,12 +255,63 @@ impl Render for ProjectIndexStatusView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let status = self.project_index.read(cx).status(); let status = self.project_index.read(cx).status();
h_flex().gap_2().map(|element| match status { let is_enabled = match status {
Status::Idle => element.child(Label::new("Project index ready")), Status::Idle => true,
Status::Loading => element.child(Label::new("Project index loading...")), _ => false,
Status::Scanning { remaining_count } => element.child(Label::new(format!( };
"Project index scanning: {remaining_count} remaining..."
))), let icon = match status {
}) Status::Idle => Icon::new(IconName::Code)
.size(IconSize::XSmall)
.color(Color::Default),
Status::Loading => Icon::new(IconName::Code)
.size(IconSize::XSmall)
.color(Color::Muted),
Status::Scanning { .. } => Icon::new(IconName::Code)
.size(IconSize::XSmall)
.color(Color::Muted),
};
let indicator = match status {
Status::Idle => Some(Indicator::dot().color(Color::Success)),
Status::Scanning { .. } => Some(Indicator::dot().color(Color::Warning)),
Status::Loading => Some(Indicator::icon(
Icon::new(IconName::Spinner)
.color(Color::Accent)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
)),
};
ButtonLike::new("project-index")
.disabled(!is_enabled)
.child(
ui::IconWithIndicator::new(icon, indicator)
.indicator_border_color(Some(gpui::transparent_black())),
)
.tooltip({
move |cx| {
let (tooltip, meta) = match status {
Status::Idle => (
"Project index ready".to_string(),
Some("Click to disable".to_string()),
),
Status::Loading => ("Project index loading...".to_string(), None),
Status::Scanning { remaining_count } => (
"Project index scanning...".to_string(),
Some(format!("{} remaining...", remaining_count)),
),
};
if let Some(meta) = meta {
Tooltip::with_meta(tooltip, None, meta, cx)
} else {
Tooltip::text(tooltip, cx)
}
}
})
} }
} }

View file

@ -7,13 +7,12 @@ use std::sync::Arc;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{popover_menu, prelude::*, Avatar, ButtonLike, ContextMenu, Tooltip}; use ui::{popover_menu, prelude::*, Avatar, ButtonLike, ContextMenu, Tooltip};
use crate::{AssistantChat, CompletionProvider, Submit, SubmitMode}; use crate::{AssistantChat, CompletionProvider};
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Composer { pub struct Composer {
editor: View<Editor>, editor: View<Editor>,
player: Option<Arc<User>>, player: Option<Arc<User>>,
can_submit: bool,
tool_registry: Arc<ToolRegistry>, tool_registry: Arc<ToolRegistry>,
model_selector: AnyElement, model_selector: AnyElement,
} }
@ -22,14 +21,12 @@ impl Composer {
pub fn new( pub fn new(
editor: View<Editor>, editor: View<Editor>,
player: Option<Arc<User>>, player: Option<Arc<User>>,
can_submit: bool,
tool_registry: Arc<ToolRegistry>, tool_registry: Arc<ToolRegistry>,
model_selector: AnyElement, model_selector: AnyElement,
) -> Self { ) -> Self {
Self { Self {
editor, editor,
player, player,
can_submit,
tool_registry, tool_registry,
model_selector, model_selector,
} }
@ -55,100 +52,56 @@ impl RenderOnce for Composer {
.gap_3() .gap_3()
.child(player_avatar) .child(player_avatar)
.child( .child(
v_flex() v_flex().size_full().gap_1().child(
.size_full() v_flex()
.gap_1() .w_full()
.pr_4() .p_4()
.child( .bg(cx.theme().colors().editor_background)
v_flex() .rounded_lg()
.w_full() .child(
.p_4() v_flex()
.bg(cx.theme().colors().editor_background) .justify_between()
.rounded_lg() .w_full()
.child( .gap_2()
v_flex() .child({
.justify_between() let settings = ThemeSettings::get_global(cx);
.w_full() let text_style = TextStyle {
.gap_2() color: cx.theme().colors().editor_foreground,
.child({ font_family: settings.buffer_font.family.clone(),
let settings = ThemeSettings::get_global(cx); font_features: settings.buffer_font.features.clone(),
let text_style = TextStyle { font_size: font_size.into(),
color: cx.theme().colors().editor_foreground, font_weight: FontWeight::NORMAL,
font_family: settings.buffer_font.family.clone(), font_style: FontStyle::Normal,
font_features: settings.buffer_font.features.clone(), line_height: line_height.into(),
font_size: font_size.into(), background_color: None,
font_weight: FontWeight::NORMAL, underline: None,
font_style: FontStyle::Normal, strikethrough: None,
line_height: line_height.into(), white_space: WhiteSpace::Normal,
background_color: None, };
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new( EditorElement::new(
&self.editor, &self.editor,
EditorStyle { EditorStyle {
background: cx.theme().colors().editor_background, background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(), local_player: cx.theme().players().local(),
text: text_style, text: text_style,
..Default::default() ..Default::default()
}, },
) )
}) })
.child( .child(
h_flex() h_flex()
.flex_none() .flex_none()
.gap_2() .gap_2()
.justify_between() .justify_between()
.w_full() .w_full()
.child( .child(h_flex().gap_1().children(
h_flex().gap_1().child( self.tool_registry.status_views().iter().cloned(),
// IconButton/button ))
// Toggle - if enabled, .selected(true).selected_style(IconButtonStyle::Filled) .child(h_flex().gap_1().child(self.model_selector)),
// ),
// match status ),
// Tooltip::with_meta("some label explaining project index + status", "click to enable") ),
IconButton::new(
"add-context",
IconName::FileDoc,
)
.icon_color(Color::Muted),
), // .child(
// IconButton::new(
// "add-context",
// IconName::Plus,
// )
// .icon_color(Color::Muted),
// ),
)
.child(
Button::new("send-button", "Send")
.style(ButtonStyle::Filled)
.disabled(!self.can_submit)
.on_click(|_, cx| {
cx.dispatch_action(Box::new(Submit(
SubmitMode::Codebase,
)))
})
.tooltip(|cx| {
Tooltip::for_action(
"Submit message",
&Submit(SubmitMode::Codebase),
cx,
)
}),
),
),
),
)
.child(
h_flex()
.w_full()
.justify_between()
.child(self.model_selector)
.children(self.tool_registry.status_views().iter().cloned()),
),
) )
} }
} }
@ -205,10 +158,18 @@ impl RenderOnce for ModelSelector {
.overflow_x_hidden() .overflow_x_hidden()
.flex_grow() .flex_grow()
.whitespace_nowrap() .whitespace_nowrap()
.child(Label::new(self.model)), .child(
Label::new(self.model)
.size(LabelSize::Small)
.color(Color::Muted),
),
) )
.child( .child(
div().child(Icon::new(IconName::ChevronDown).color(Color::Muted)), div().child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
), ),
) )
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)

View file

@ -87,6 +87,7 @@ pub enum IconName {
ChevronUp, ChevronUp,
ExpandVertical, ExpandVertical,
Close, Close,
Code,
Collab, Collab,
Command, Command,
Control, Control,
@ -153,6 +154,7 @@ pub enum IconName {
Snip, Snip,
Space, Space,
Split, Split,
Spinner,
Tab, Tab,
Terminal, Terminal,
Trash, Trash,
@ -191,6 +193,7 @@ impl IconName {
IconName::ChevronUp => "icons/chevron_up.svg", IconName::ChevronUp => "icons/chevron_up.svg",
IconName::ExpandVertical => "icons/expand_vertical.svg", IconName::ExpandVertical => "icons/expand_vertical.svg",
IconName::Close => "icons/x.svg", IconName::Close => "icons/x.svg",
IconName::Code => "icons/code.svg",
IconName::Collab => "icons/user_group_16.svg", IconName::Collab => "icons/user_group_16.svg",
IconName::Command => "icons/command.svg", IconName::Command => "icons/command.svg",
IconName::Control => "icons/control.svg", IconName::Control => "icons/control.svg",
@ -257,6 +260,7 @@ impl IconName {
IconName::Snip => "icons/snip.svg", IconName::Snip => "icons/snip.svg",
IconName::Space => "icons/space.svg", IconName::Space => "icons/space.svg",
IconName::Split => "icons/split.svg", IconName::Split => "icons/split.svg",
IconName::Spinner => "icons/spinner.svg",
IconName::Tab => "icons/tab.svg", IconName::Tab => "icons/tab.svg",
IconName::Terminal => "icons/terminal.svg", IconName::Terminal => "icons/terminal.svg",
IconName::Trash => "icons/trash.svg", IconName::Trash => "icons/trash.svg",