- messages: yours in a quiet bubble, the assistant's as prose beside its mark, system notes, errors; avatars, headers with model and time, footers - MessageList over gpui's bottom-aligned list: drawn near the view, held at the newest, counting arrivals while away; a way back down; day separators - streaming text and markdown reveal toward the text as it grows, with a caret; a blinking cursor before the first words - CodeBlock with language, copy, run, apply, download and folding; MarkdownRenderer::code lets messages draw fences with it - pictures, grids, files, voice notes, video over host frames, link previews - quotes, actions, an editor that takes focus, versions, regenerate, stop and continue - errors with retry, a rate limit counting down, feedback, thinking blocks - gallery: the AI Chat page with scripted states
185 lines
7.3 KiB
Rust
185 lines
7.3 KiB
Rust
use std::time::{Duration, Instant};
|
|
|
|
use ely_gpui_component::{
|
|
buttons::{Button, ButtonVariant},
|
|
chat::{
|
|
BranchNavigator, ErrorMessage, FeedbackForm, MessageActions, MessageAvatar, MessageBubble,
|
|
MessageEditor, QuoteReply, RateLimitNotice, Role, ThinkingBlock, ThinkingDuration,
|
|
ThinkingIndicator, continue_button, regenerate_button, stop_button,
|
|
},
|
|
forms::TextInput,
|
|
primitives::IconName,
|
|
};
|
|
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
|
|
|
use crate::{
|
|
probe::probe,
|
|
ui::{keep, section, set},
|
|
};
|
|
|
|
const SENT: &str = "What does a lift do to a color?";
|
|
|
|
fn field(
|
|
key: &'static str,
|
|
text: &str,
|
|
hint: &str,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Entity<TextInput> {
|
|
let (text, hint) = (text.to_string(), hint.to_string());
|
|
window.use_keyed_state(key, cx, move |window, cx| {
|
|
let mut input = TextInput::new(window, cx)
|
|
.multi_line(2, 6)
|
|
.placeholder(hint);
|
|
input.set_text(text, cx);
|
|
input
|
|
})
|
|
}
|
|
|
|
pub fn actions(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let rating = keep("chat-rating", || None::<bool>, window, cx);
|
|
let now_rating = *rating.read(cx);
|
|
let branch = keep("chat-branch", || 1_usize, window, cx);
|
|
let now_branch = *branch.read(cx);
|
|
let editing = keep("chat-editing", || false, window, cx);
|
|
let sent = keep("chat-sent", || SharedString::from(SENT), window, cx);
|
|
let now_sent = sent.read(cx).clone();
|
|
let draft = field("chat-draft", SENT, "Your message", window, cx);
|
|
let open = editing.clone();
|
|
let body = if *editing.read(cx) {
|
|
let (save, cancel, text) = (editing.clone(), editing.clone(), draft.clone());
|
|
MessageEditor::new(
|
|
"chat-editor",
|
|
&draft,
|
|
now_sent,
|
|
move |_, cx| {
|
|
let next = SharedString::from(text.read(cx).text().trim().to_string());
|
|
set(&sent, next, cx);
|
|
set(&save, false, cx)
|
|
},
|
|
move |_, cx| set(&cancel, false, cx),
|
|
)
|
|
.into_any_element()
|
|
} else {
|
|
div()
|
|
.flex()
|
|
.items_center()
|
|
.gap_3()
|
|
.child(div().child(now_sent))
|
|
.child(probe(
|
|
"chat-edit-open",
|
|
Button::new("chat-edit", "Edit")
|
|
.variant(ButtonVariant::Ghost)
|
|
.icon(IconName::Pencil)
|
|
.on_click(move |_, _, cx| set(&open, true, cx)),
|
|
))
|
|
.into_any_element()
|
|
};
|
|
section(
|
|
"QuoteReply / MessageActions / MessageEditor / BranchNavigator / RegenerateButton / StopGeneratingButton / ContinueButton",
|
|
"A quote to answer, the actions on a message, a sent message opened to change (the submit key saves, Escape cancels), the versions an answer has, and the buttons that regenerate, stop and continue.",
|
|
cx,
|
|
)
|
|
.child(
|
|
div()
|
|
.w(px(560.))
|
|
.flex()
|
|
.flex_col()
|
|
.gap_4()
|
|
.child(QuoteReply::new("chat-quote", "Assistant", "A lift blends a color toward white. Half a lift sits midway between the color and white.").on_remove(|_, _| log::info!("gallery: quote removed")))
|
|
.child(body)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.items_center()
|
|
.justify_between()
|
|
.child(
|
|
MessageActions::new("chat-actions", SENT)
|
|
.on_edit(|_, _| log::info!("gallery: edit"))
|
|
.on_regenerate(|_, _| log::info!("gallery: regenerate"))
|
|
.rating(now_rating, move |next, _, cx| set(&rating, next, cx)),
|
|
)
|
|
.child(BranchNavigator::new("chat-branch", now_branch, 3, move |next, _, cx| set(&branch, next, cx))),
|
|
)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.gap_2()
|
|
.child(regenerate_button("chat-regenerate").on_click(|_, _, _| log::info!("gallery: regenerate")))
|
|
.child(stop_button("chat-stop").on_click(|_, _, _| log::info!("gallery: stop")))
|
|
.child(continue_button("chat-continue").on_click(|_, _, _| log::info!("gallery: continue"))),
|
|
),
|
|
)
|
|
}
|
|
|
|
pub fn status(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let until = *keep(
|
|
"chat-limit",
|
|
|| Instant::now() + Duration::from_secs(222),
|
|
window,
|
|
cx,
|
|
)
|
|
.read(cx);
|
|
let picked = keep(
|
|
"chat-reasons",
|
|
|| vec![SharedString::from("Too long")],
|
|
window,
|
|
cx,
|
|
);
|
|
let now_picked = picked.read(cx).clone();
|
|
let note = field("chat-note", "", "Anything else? (optional)", window, cx);
|
|
section(
|
|
"ErrorMessage / RetryMessage / RateLimitNotice / FeedbackForm",
|
|
"An answer that failed and a way to try again; a limit reached, counting down to when it lifts; and after a bad mark, what went wrong.",
|
|
cx,
|
|
)
|
|
.child(
|
|
div()
|
|
.w(px(560.))
|
|
.flex()
|
|
.flex_col()
|
|
.gap_4()
|
|
.child(
|
|
MessageBubble::new(Role::Error)
|
|
.avatar(MessageAvatar::new("chat-error-mark", Role::Error, "Assistant"))
|
|
.child(ErrorMessage::new("chat-error", "The answer stopped halfway").detail("The connection closed before the end. Nothing was lost.").on_retry(|_, _| log::info!("gallery: retry"))),
|
|
)
|
|
.child(RateLimitNotice::new("chat-limit", until).on_upgrade(|_, _| log::info!("gallery: raise limit")))
|
|
.child(
|
|
FeedbackForm::new(
|
|
"chat-feedback",
|
|
["Wrong", "Unclear", "Too long", "Missed the point"],
|
|
now_picked,
|
|
¬e,
|
|
move |next, _, cx| set(&picked, next.to_vec(), cx),
|
|
|_, _| log::info!("gallery: feedback sent"),
|
|
)
|
|
.on_cancel(|_, _| log::info!("gallery: feedback cancelled")),
|
|
),
|
|
)
|
|
}
|
|
|
|
const REASONING: &str = "The reader asks what a lift does. Start from the definition: a blend toward white. Then give the two themes, since they lift by different amounts, and say why: a dark ground makes saturated color look louder, so it needs less of it.";
|
|
|
|
pub fn thinking(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
section(
|
|
"ThinkingBlock / ReasoningPanel / ThinkingIndicator / ThinkingDuration",
|
|
"The reasoning behind an answer, folded under how long it took; open, its steps read in a quiet column. While the assistant works, a word says what it is doing.",
|
|
cx,
|
|
)
|
|
.child(
|
|
div()
|
|
.w(px(560.))
|
|
.flex()
|
|
.flex_col()
|
|
.gap_4()
|
|
.child(probe(
|
|
"chat-thinking",
|
|
div().w(px(560.)).child(ThinkingBlock::new("chat-thought", REASONING, false, Duration::from_secs(12))),
|
|
))
|
|
.child(ThinkingBlock::new("chat-thinking-now", "Weighing how dark themes change", true, Duration::from_secs(3)))
|
|
.child(ThinkingIndicator::new("chat-searching").label("Searching the web"))
|
|
.child(ThinkingDuration::new(Duration::from_secs(75))),
|
|
)
|
|
}
|