From b9d73e2b30f3369e756848f7d1f54794d8af7ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 26 Sep 2026 07:50:09 -0400 Subject: [PATCH] T23e: AI chat, welcome - WelcomeScreen: a greeting for the hour over the owner's composer, starters and cards, centered while it fits and scrolled from its top when it does not - SuggestionChips, FollowUpSuggestions and CapabilityCards: each press asks the owner; rows and cards are Tab stops - PromptLibrary is documents::TemplatePicker over saved prompts - gallery: starters and cards fill the welcome composer, a saved prompt fills it too, send clears it with a toast, a follow-up asks its question --- examples/gallery/pages/chat/mod.rs | 8 + examples/gallery/pages/chat/welcome.rs | 158 ++++++++++++ src/chat/mod.rs | 2 + src/chat/tests/mod.rs | 1 + src/chat/tests/welcome.rs | 55 +++++ src/chat/welcome.rs | 327 +++++++++++++++++++++++++ tasks/ch21-30.md | 10 +- 7 files changed, 556 insertions(+), 5 deletions(-) create mode 100644 examples/gallery/pages/chat/welcome.rs create mode 100644 src/chat/tests/welcome.rs create mode 100644 src/chat/welcome.rs diff --git a/examples/gallery/pages/chat/mod.rs b/examples/gallery/pages/chat/mod.rs index 2cdd1ea..9eacfc3 100644 --- a/examples/gallery/pages/chat/mod.rs +++ b/examples/gallery/pages/chat/mod.rs @@ -4,6 +4,7 @@ mod compose; mod controls; mod history; mod talk; +mod welcome; use ely_gpui_component::feedback::{ToastViewport, Toaster}; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; @@ -64,6 +65,9 @@ const SCRIPT: &[Step] = &[ Step::Shot("export"), Step::Key("escape"), Step::Wait(200), + Step::Click("chat-follow-ups"), + Step::Wait(300), + Step::Shot("follow-up"), Step::DownAt("chat-sources", 20.0, 10.0), Step::UpAt("chat-sources", 20.0, 10.0), ]; @@ -72,6 +76,7 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement { let draft = compose::draft(window, cx); let history = history::history(window, cx); let toaster = keep("chat-toaster", Toaster::default, window, cx); + let opening = welcome::welcome_draft(window, cx); div() .child(talk::conversation(window, cx)) .child(talk::streaming(window, cx)) @@ -89,6 +94,9 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement { .child(history::conversations_section(&history, window, cx)) .child(history::projects_section(window, cx)) .child(history::shared_section(&history, &toaster, window, cx)) + .child(welcome::welcome_section(&opening, &toaster, window, cx)) + .child(welcome::follow_ups_section(window, cx)) + .child(welcome::library_section(&opening, window, cx)) .child(ToastViewport::new("chat-toasts", &toaster)) .into_any_element() } diff --git a/examples/gallery/pages/chat/welcome.rs b/examples/gallery/pages/chat/welcome.rs new file mode 100644 index 0000000..26a94e2 --- /dev/null +++ b/examples/gallery/pages/chat/welcome.rs @@ -0,0 +1,158 @@ +use ely_gpui_component::{ + chat::{ + CapabilityCards, FollowUpSuggestions, MessageAvatar, MessageBubble, PromptInput, Role, + StreamingMarkdown, SuggestionChips, WelcomeScreen, + }, + documents::{Template, TemplatePicker}, + feedback::{Toast, Toaster}, + forms::TextInput, + primitives::{IconName, Severity}, + theme::{ActiveTheme, Radius}, +}; +use gpui::{App, Entity, IntoElement, ParentElement, Styled, Window, div, px}; +use jiff::{Timestamp, tz::TimeZone}; + +use crate::{ + probe::probe, + ui::{keep, section, set}, +}; + +/// The welcome composer's text, which starters and saved prompts fill. +pub fn welcome_draft(window: &mut Window, cx: &mut App) -> Entity { + window.use_keyed_state("chat-welcome-draft", cx, |window, cx| { + TextInput::new(window, cx) + .multi_line(1, 6) + .placeholder("How can I help?") + }) +} + +pub fn welcome_section( + draft: &Entity, + toaster: &Entity, + _: &mut Window, + cx: &mut App, +) -> impl IntoElement + use<> { + let zone = TimeZone::try_system().expect("the gallery reads the system time zone"); + let hour = Timestamp::now().to_zoned(zone).hour() as u8; + let (filled, typed, sent, toaster) = + (draft.clone(), draft.clone(), draft.clone(), toaster.clone()); + let starters = [ + ("Explain a lift", IconName::Sparkles), + ("Review my palette", IconName::Palette), + ("Draft release notes", IconName::FileText), + ]; + let theme = cx.theme(); + section( + "WelcomeScreen / Greeting / SuggestionChips / StarterPrompts / CapabilityCards", + "The first thing a new conversation shows: a greeting for the time of day, the composer, prompts to start from and what the assistant can do; a press fills the composer.", + cx, + ) + .child( + div() + .w(px(760.)) + .h(px(520.)) + .rounded(theme.radius(Radius::Lg)) + .border_1() + .border_color(theme.colors.border) + .child( + WelcomeScreen::new("chat-welcome", hour) + .name("Ada") + .child(PromptInput::new("chat-welcome-compose", draft, move |_, cx| { + sent.update(cx, |input, cx| input.set_text("", cx)); + toaster.update(cx, |toaster, cx| { + toaster.push(Toast::new("Message sent").severity(Severity::Success), cx); + }) + })) + .child(SuggestionChips::new("chat-starters", starters, move |ix, _, cx| { + let text = starters[ix].0; + filled.update(cx, |input, cx| input.set_text(text, cx)) + })) + .child(CapabilityCards::new( + "chat-capabilities", + [ + (IconName::Globe, "Search the web", "Answers with sources you can open."), + (IconName::Code, "Write and run code", "Tries an idea before you use it."), + (IconName::FileText, "Read your files", "Pages and notes you attach."), + ], + move |ix, _, cx| { + let text = ["Search the web for ", "Write code that ", "Read my file and "][ix]; + typed.update(cx, |input, cx| input.set_text(text, cx)) + }, + )), + ), + ) +} + +pub fn follow_ups_section(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { + let asked = keep("chat-follow-up", || None::, window, cx); + let now_asked = *asked.read(cx); + section( + "FollowUpSuggestions", + "Questions to ask next, under an answer; a press asks one.", + cx, + ) + .child( + div() + .w(px(560.)) + .flex() + .flex_col() + .gap_3() + .child( + MessageBubble::new(Role::Assistant) + .avatar(MessageAvatar::new("chat-followup-mark", Role::Assistant, "Assistant")) + .child(StreamingMarkdown::new("chat-followup-text", "Dark themes lift their accents further, because saturated color on charcoal looks louder.", false)), + ) + .child(probe( + "chat-follow-ups", + div().w(px(560.)).child(FollowUpSuggestions::new( + "chat-follow-ups", + QUESTIONS, + move |ix, _, cx| set(&asked, Some(ix), cx), + )), + )) + .children(now_asked.map(|ix| MessageBubble::new(Role::User).child(QUESTIONS[ix]))), + ) +} + +const QUESTIONS: [&str; 3] = [ + "How much further, in numbers?", + "Does the same hold for text?", + "Show both themes side by side", +]; + +fn prompts() -> Vec