Files
Ely-GPUI-Components/src/chat/voice.rs
T
ZacharyZhang-NY d362f08d41 T23c: AI chat, input
- PromptInput: attachments and context above the field, the owner's tools and
  send below; Enter sends, Shift-Enter breaks the line; / offers commands and
  @ offers context over forms::Suggestions; files dropped on it attach behind a
  drop veil
- AttachmentButton, AttachmentChip with thumbnails and uploads, ContextChips
- VoiceInputButton and VoiceWaveform with a red dot and the time
- ParameterPanel, SystemPromptEditor, TokenCounter, CostEstimator
- forms: a Newline action on Shift-Enter keeps one-line fields to one line;
  replace_trigger moves beside Suggestions; choose and dropped shared
- gallery: composer, pickers and tuning
2026-09-26 06:29:20 -04:00

125 lines
3.6 KiB
Rust

use std::{rc::Rc, time::Instant};
use gpui::{App, ElementId, IntoElement, ParentElement, RenderOnce, Styled, Window, div};
use crate::{
buttons::{ButtonVariant, IconButton},
feedback::Timer,
forms::OnFlag,
motion::Pulse,
primitives::IconName,
theme::{ActiveTheme, ControlSize, TextSize},
};
/// Starts and stops dictation: a microphone at rest, a stop square while it records.
#[derive(IntoElement)]
pub struct VoiceInputButton {
id: ElementId,
recording: bool,
on_toggle: OnFlag,
}
impl VoiceInputButton {
/// `on_toggle` gets true to start recording, false to stop.
pub fn new(
id: impl Into<ElementId>,
recording: bool,
on_toggle: impl Fn(bool, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
recording,
on_toggle: Rc::new(on_toggle),
}
}
}
impl RenderOnce for VoiceInputButton {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let (recording, toggle) = (self.recording, self.on_toggle);
IconButton::new(
self.id,
if recording {
IconName::Square
} else {
IconName::Mic
},
)
.variant(if recording {
ButtonVariant::Secondary
} else {
ButtonVariant::Ghost
})
.size(ControlSize::Sm)
.tooltip(if recording {
"Stop dictation"
} else {
"Dictate"
})
.on_click(move |_, window, cx| {
log::info!("voice input: recording {}", !recording);
toggle(!recording, window, cx)
})
}
}
/// Sound as it records: a red dot, how long it has run, and bars that follow the latest levels, 0 to 1, oldest first. The dot's rings rest under reduced motion.
#[derive(IntoElement)]
pub struct VoiceWaveform {
id: ElementId,
since: Instant,
levels: Vec<f32>,
}
impl VoiceWaveform {
pub fn new(id: impl Into<ElementId>, since: Instant, levels: impl Into<Vec<f32>>) -> Self {
let levels: Vec<f32> = levels.into();
assert!(
levels.iter().all(|level| (0.0..=1.0).contains(level)),
"levels lie within 0 to 1"
);
Self {
id: id.into(),
since,
levels,
}
}
}
impl RenderOnce for VoiceWaveform {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let tall = theme.control_height(ControlSize::Sm);
div()
.flex()
.items_center()
.gap_2()
.child(
Pulse::new((self.id.clone(), "dot")).child(
div()
.size(theme.status_dot())
.rounded_full()
.bg(colors.danger),
),
)
.child(Timer::new((self.id.clone(), "time"), self.since).size(TextSize::Sm))
.child(
div()
.flex_1()
.h(tall)
.flex()
.items_center()
.justify_end()
.gap_px()
.children(self.levels.into_iter().map(|level| {
div()
.w(theme.progress_thickness())
.h(tall * level.max(0.1))
.rounded_full()
.bg(colors.fg_muted)
})),
)
}
}