GPUI Box GitHub

Components / agent

VoiceReactive builder

A standard voice-reactive visualization from a normalized host sample.

A builder is RenderOnce. Construct and mount it in one expression.

use gpui_kit::agent::VoiceReactive;

Reviewed in persona.

Construct
new(ident: impl Into<Ident>, sample: VoiceSample) -> Self
Options

Chain onto the value.

sample_at(elapsed: Duration) -> Self
Example · persona
fn persona(_window: &mut Window, cx: &mut App) -> AnyElement {
    let theme = cx.theme().clone();
    let speaking =
        AgentSnapshot::new(AgentDescriptor::new("guide", "Lyra").role("Expedition guide"))
            .presence(AgentPresence::Online)
            .execution(AgentExecutionState::Active(AgentActivity::Speaking));
    let listening =
        AgentSnapshot::new(AgentDescriptor::new("listener", "Moss").role("World companion"))
            .presence(AgentPresence::Online)
            .execution(AgentExecutionState::Waiting(WaitReason::UserInput));
    let speaking_voice =
        VoiceSample::new(VoiceState::Speaking, 0.76, 0.58).expect("scene voice is normalized");
    let listening_voice =
        VoiceSample::new(VoiceState::Listening, 0.32, 0.48).expect("scene voice is normalized");
    let idle = AgentSnapshot::new(AgentDescriptor::new("idle", "Ori").role("Archivist"))
        .presence(AgentPresence::Away)
        .execution(AgentExecutionState::Idle);
    let thinking = AgentSnapshot::new(AgentDescriptor::new("thinking", "Sable").role("Tactician"))
        .presence(AgentPresence::Online)
        .execution(AgentExecutionState::Active(AgentActivity::Thinking));
    let celebrating = AgentSnapshot::new(AgentDescriptor::new("victor", "Kite").role("Pathfinder"))
        .presence(AgentPresence::Online)
        .execution(AgentExecutionState::Completed(AgentOutcome::Succeeded));
    let unavailable_voice = VoiceSample::new(
        VoiceState::Unavailable("Voice input was not granted.".into()),
        0.0,
        0.0,
    )
    .expect("scene voice is normalized");

    let content = div()
        .column()
        .w(px(680.0))
        .gap_token(&theme, Space::Md)
        .child(
            div()
                .row_reading(cx.layout_direction())
                .items_center()
                .gap_token(&theme, Space::Xl)
                .child(
                    PersonaPortrait::new("scene.persona.portrait-speaking", speaking.clone())
                        .expression(PersonaExpression::Warm)
                        .voice(speaking_voice.clone())
                        .sample_at(Duration::from_millis(640)),
                )
                .child(
                    PersonaPortrait::new("scene.persona.portrait-listening", listening.clone())
                        .expression(PersonaExpression::Focused)
                        .voice(listening_voice.clone())
                        .sample_at(Duration::from_millis(640)),
                )
                .child(
                    div()
                        .column()
                        .gap_token(&theme, Space::Xs)
                        .child(
                            div()
                                .type_scale(&theme, TypeScale::Label)
                                .text_tone(&theme, TextTone::Primary)
                                .child("Voice-reactive"),
                        )
                        .child(
                            VoiceReactive::new("scene.persona.voice", speaking_voice)
                                .sample_at(Duration::from_millis(640)),
                        ),
                ),
        )
        .child(
            PersonaDialogue::new(
                "scene.persona.dialogue",
                DialogueTurn::markdown(
                    "crossroads",
                    speaking,
                    "The signal splits at the **crystal bridge.** I can scout either route without advancing the story for you.",
                )
                .streaming(true)
                .choice(DialogueChoice::new("bridge", "Cross the bridge").selected(true))
                .choice(DialogueChoice::new("cavern", "Enter the cavern"))
                .choice(
                    DialogueChoice::new("gate", "Open the sealed gate")
                        .unavailable("The gate key has not been verified."),
                ),
            )
            .expression(PersonaExpression::Warm)
            .voice(listening_voice)
            .on_event(|_, _, _| {}),
        )
        .child(
            div()
                .row_reading(cx.layout_direction())
                .items_center()
                .gap_token(&theme, Space::Xl)
                .child(
                    div()
                        .column()
                        .items_center()
                        .gap_token(&theme, Space::Xs)
                        .child(
                            PersonaPortrait::new("scene.persona.idle", idle)
                                .expression(PersonaExpression::Neutral)
                                .size(68.0),
                        )
                        .child(crate::foundation::text(
                            &theme,
                            TypeScale::Caption,
                            "Idle · neutral",
                        )),
                )
                .child(
                    div()
                        .column()
                        .items_center()
                        .gap_token(&theme, Space::Xs)
                        .child(
                            PersonaPortrait::new("scene.persona.thinking", thinking)
                                .expression(PersonaExpression::Focused)
                                .size(68.0),
                        )
                        .child(crate::foundation::text(
                            &theme,
                            TypeScale::Caption,
                            "Thinking · focused",
                        )),
                )
                .child(
                    div()
                        .column()
                        .items_center()
                        .gap_token(&theme, Space::Xs)
                        .child(
                            PersonaPortrait::new("scene.persona.celebrating", celebrating)
                                .expression(PersonaExpression::Celebrating)
                                .size(68.0),
                        )
                        .child(crate::foundation::text(
                            &theme,
                            TypeScale::Caption,
                            "Succeeded · celebrating",
                        )),
                )
                .child(
                    div()
                        .column()
                        .items_center()
                        .gap_token(&theme, Space::Xs)
                        .child(VoiceReactive::new(
                            "scene.persona.voice-unavailable",
                            unavailable_voice,
                        ))
                        .child(crate::foundation::text(
                            &theme,
                            TypeScale::Caption,
                            "Unavailable",
                        )),
                ),
        )
        ;

    stack(&theme)
        .size_full()
        .items_center()
        .justify_center()
        .child(content)
        .into_any_element()
}

Source: crates/gpui-kit/src/agent/persona.rs