Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function Altair() {

useEffect(() => {
setConfig({
model: "models/gemini-2.0-flash-exp",
model: "models/gemini-2.5-flash-native-audio-preview-09-2025",
systemInstruction: {
parts: [
{
Expand Down
75 changes: 59 additions & 16 deletions src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
scrollbar-width: thin;

--font-family: "Space Mono", monospace;
--font-sans: "Google Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

/* */
--Neutral-00: #000;
Expand All @@ -51,11 +52,20 @@
--Red-500: #ff4600;
--Red-600: #e03c00;
--Red-700: #bd3000;

/* Modern UI Variables */
--glass-bg: rgba(28, 31, 33, 0.75);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
--backdrop-blur: 12px;
--radius-lg: 24px;
--radius-md: 16px;
--radius-sm: 8px;
}

body {
font-family: "Space Mono", monospace;
background: var(--Neutral-30);
font-family: var(--font-sans);
background: var(--Neutral-5);
}

.material-symbols-outlined {
Expand Down Expand Up @@ -116,20 +126,23 @@ body {
}

.streaming-console {
background: var(--Neutral-15);
color: var(--gray-300);
background: var(--Neutral-00);
color: var(--Neutral-90);
display: flex;
height: 100vh;
width: 100vw;
overflow: hidden;

a,
a:visited,
a:active {
color: var(--gray-300);
color: var(--Blue-400);
text-decoration: none;
}

.disabled {
pointer-events: none;
opacity: 0.5;

> * {
pointer-events: none;
Expand All @@ -146,28 +159,58 @@ body {
gap: 1rem;
max-width: 100%;
overflow: hidden;
background: radial-gradient(circle at 50% 50%, var(--Neutral-10) 0%, var(--Neutral-5) 100%);
}

.main-app-area {
display: flex;
flex: 1;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}

.function-call {
position: absolute;
top: 0;
width: 100%;
height: 50%;
overflow-y: auto;
position: relative;
padding-bottom: 100px; /* Space for control tray */

/* Ensure Altair graph overlays or sits nicely */
.vega-embed {
background: var(--glass-bg);
backdrop-filter: blur(var(--backdrop-blur));
padding: 24px;
border-radius: 24px;
border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
max-width: 90%;
max-height: 80vh;
overflow: auto;
z-index: 10; /* Above video if they overlap, or manage layout */

/* If both video and graph are present, we might want to position them.
For now, let's assume they stack or one is active. */
&:empty {
display: none;
}

summary {
color: var(--Neutral-90);
font-family: var(--font-sans);
}
}
}
}

/* video player */
.stream {
flex-grow: 1;
flex-grow: 0;
width: auto;
max-width: 90%;
border-radius: 32px;
max-height: fit-content;
height: auto;
max-height: 80vh;
border-radius: 24px;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
transition: all 0.3s ease;

&.hidden {
display: none;
}
}
2 changes: 1 addition & 1 deletion src/components/altair/Altair.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function AltairComponent() {
const { client, setConfig, setModel } = useLiveAPIContext();

useEffect(() => {
setModel("models/gemini-2.0-flash-exp");
setModel("models/gemini-2.5-flash-native-audio-preview-09-2025");
setConfig({
responseModalities: [Modality.AUDIO],
speechConfig: {
Expand Down
45 changes: 36 additions & 9 deletions src/components/audio-pulse/AudioPulse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import React from "react";
import { useEffect, useRef } from "react";
import c from "classnames";

const lineCount = 3;
const lineCount = 5;

export type AudioPulseProps = {
active: boolean;
Expand All @@ -33,14 +33,41 @@ export default function AudioPulse({ active, volume, hover }: AudioPulseProps) {
useEffect(() => {
let timeout: number | null = null;
const update = () => {
lines.current.forEach(
(line, i) =>
(line.style.height = `${Math.min(
24,
4 + volume * (i === 1 ? 400 : 60),
)}px`),
);
timeout = window.setTimeout(update, 100);
lines.current.forEach((line, i) => {
// Dynamic visualization
// Center bars are generally taller/more active
// Add some randomness for "aliveness" when active
const noise = Math.random() * 0.2;

// Scale volume to be more responsive (log-ish or just boosted)
const v = Math.min(1, volume * 2.5);

// Use 5 bars
// 0, 4: Outer
// 1, 3: Middle
// 2: Center

let targetHeight = 4; // Min height

if (volume > 0.01) {
const baseHeight = 6;
const maxHeight = 28;

if (i === 2) { // Center
targetHeight = baseHeight + (v * (maxHeight - baseHeight));
} else if (i === 1 || i === 3) { // Middle
targetHeight = baseHeight + (v * 0.8 * (maxHeight - baseHeight));
} else { // Outer
targetHeight = baseHeight + (v * 0.5 * (maxHeight - baseHeight));
}

// Add small jitter
targetHeight += (noise * 4);
}

line.style.height = `${Math.min(32, targetHeight)}px`;
});
timeout = window.setTimeout(update, 30); // Fast update
};

update();
Expand Down
49 changes: 38 additions & 11 deletions src/components/audio-pulse/audio-pulse.scss
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
.audioPulse {
display: flex;
width: 24px;
justify-content: space-evenly;
align-items: center;
transition: all 0.5s;
justify-content: space-between;
align-items: center; /* Center vertically */
gap: 4px;
padding: 0;
transition: opacity 0.3s ease;

& > div {
background-color: var(--Neutral-30);
border-radius: 1000px;
flex: 1;
background: linear-gradient(to top, var(--Blue-500), var(--Blue-400));
border-radius: 4px;
width: 4px;
min-height: 4px;
border-radius: 1000px;
transition: height 0.1s;
max-width: 6px;
transition: height 0.05s ease-out, background 0.2s ease; /* Faster height transition */
box-shadow: 0 0 4px rgba(31, 148, 255, 0.3);
position: relative;
}


&.hover > div {
animation: hover 1.4s infinite alternate ease-in-out;
}

height: 4px;
transition: opacity 0.333s;
&:not(.active) {
opacity: 0.3;

& > div {
background: var(--Neutral-30);
box-shadow: none;
}
}

&.active {
opacity: 1;

& > div {
background-color: var(--Neutral-80);
background: linear-gradient(to top, var(--Blue-500), var(--Blue-400));
box-shadow: 0 0 6px rgba(31, 148, 255, 0.5);

&:nth-child(1),
&:nth-child(5) {
background: linear-gradient(to top, var(--Blue-800), var(--Blue-500));
}

&:nth-child(2),
&:nth-child(4) {
background: linear-gradient(to top, var(--Blue-500), var(--Blue-400));
}

&:nth-child(3) {
background: linear-gradient(to top, var(--Blue-400), #a8d4ff);
box-shadow: 0 0 8px rgba(31, 148, 255, 0.6);
}
Comment on lines +39 to +55

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The background property is being redundantly set. The base style for & > div already defines the default gradient, so the declarations on line 39 and for :nth-child(2)/:nth-child(4) are unnecessary. Removing them will simplify the code.

      box-shadow: 0 0 6px rgba(31, 148, 255, 0.5);
      
      &:nth-child(1),
      &:nth-child(5) {
        background: linear-gradient(to top, var(--Blue-800), var(--Blue-500));
      }
      
      &:nth-child(3) {
        background: linear-gradient(to top, var(--Blue-400), #a8d4ff);
        box-shadow: 0 0 8px rgba(31, 148, 255, 0.6);
      }

}
}
}
Expand Down
14 changes: 4 additions & 10 deletions src/components/control-tray/ControlTray.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,14 @@ function ControlTray({
const renderCanvasRef = useRef<HTMLCanvasElement>(null);
const connectButtonRef = useRef<HTMLButtonElement>(null);

const { client, connected, connect, disconnect, volume } =
const { client, connected, connect, disconnect } =
useLiveAPIContext();

useEffect(() => {
if (!connected && connectButtonRef.current) {
connectButtonRef.current.focus();
}
}, [connected]);
useEffect(() => {
document.documentElement.style.setProperty(
"--volume",
`${Math.max(5, Math.min(inVolume * 200, 8))}px`
);
}, [inVolume]);

useEffect(() => {
const onData = (base64: string) => {
Expand Down Expand Up @@ -164,7 +158,7 @@ function ControlTray({
<canvas style={{ display: "none" }} ref={renderCanvasRef} />
<nav className={cn("actions-nav", { disabled: !connected })}>
<button
className={cn("action-button mic-button")}
className={cn("action-button mic-button", { muted })}
onClick={() => setMuted(!muted)}
>
{!muted ? (
Expand All @@ -174,8 +168,8 @@ function ControlTray({
)}
</button>

<div className="action-button no-action outlined">
<AudioPulse volume={volume} active={connected} hover={false} />
<div className="action-button no-action outlined audio-visualizer-button" title="Microphone input level">
<AudioPulse volume={inVolume} active={connected && !muted} hover={false} />
</div>

{supportsVideo && (
Expand Down
Loading