Minimal

This component offers an inline experience, crafted with React and Tailwind CSS. This component animates a line to the Realtime agent's volume level using canvas implementation.

Preview

Code

Copy the following code to your component file for example minimal-component.tsx.

"use client";
 
import React, { useState, useEffect, useRef } from 'react';
import { MicOff, Mic } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import useWebRTCAudioSession from '@/hooks/use-webrtc';
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
 
// Add interface for audio parameters
interface AudioParameters {
  waveIntensity: number;
  noiseLevel: number;
  frequency: number;
  complexity: number;
}
 
const AudioVisualizer: React.FC<{ audioData: Uint8Array; isSessionActive: boolean }> = ({ audioData, isSessionActive }) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
 
  useEffect(() => {
    if (isSessionActive) {
      const canvas = canvasRef.current;
      const context = canvas?.getContext('2d');
 
      if (canvas && context) {
        canvas.width = canvas.offsetWidth;
        canvas.height = canvas.offsetHeight;
      }
 
      draw();
    }
  }, [audioData, isSessionActive]);
 
  const draw = () => {
    const canvas = canvasRef.current;
    if (!canvas) return;
 
    const context = canvas.getContext('2d');
    if (!context) return;
 
    const { width, height } = canvas;
    context.clearRect(0, 0, width, height);
 
    const sliceWidth = (width / (audioData.length - 1)) * 2;
    const centerY = height / 2;
 
    context.lineWidth = 2;
    context.strokeStyle = '#9E9E9E';
    context.beginPath();
 
    let prevX = 0;
    let prevY = centerY;
 
    context.moveTo(prevX, prevY);
 
    for (let i = 0; i < audioData.length; i++) {
      const avgValue = (audioData[i] + audioData[Math.max(0, i - 1)]) / 2; // Averaging current and previous data points
      const v = avgValue / 255.0;
      const y = centerY + (v - 0.5) * height;
      const x = i * sliceWidth;
 
      context.bezierCurveTo((prevX + x) / 2, prevY, (prevX + x) / 2, y, x, y);
 
      prevX = x;
      prevY = y;
    }
 
    context.stroke();
  };
 
  return (
    <motion.canvas
      ref={canvasRef}
      className="w-full h-full"
      initial={{ opacity: 0 }}
      animate={{ opacity: isSessionActive ? 1 : 0 }}
      transition={{ duration: 0.5 }}
    />
  );
};
 
const AudioAnalyzer: React.FC<{ 
  volumeLevel: number; 
  isSessionActive: boolean;
  audioParams: AudioParameters;
}> = ({ volumeLevel, isSessionActive, audioParams }) => {
  const [audioData, setAudioData] = useState<Uint8Array>(new Uint8Array(128));
 
  useEffect(() => {
    if (!isSessionActive) {
      setAudioData(new Uint8Array(128));
      return;
    }
 
    const updateAudioData = () => {
      const dataArray = new Uint8Array(128);
      const time = Date.now() / 1000;
      
      for (let i = 0; i < dataArray.length; i++) {
        // Adjust wave parameters based on complexity
        const waves = [];
        for (let j = 1; j <= audioParams.complexity; j++) {
          waves.push(
            Math.sin(time * j * audioParams.frequency + i * 0.1) * 
            (audioParams.waveIntensity / j)
          );
        }
        
        const combinedWave = waves.reduce((a, b) => a + b, 0) * volumeLevel;
        const noise = (Math.random() - 0.5) * audioParams.noiseLevel * volumeLevel;
        
        dataArray[i] = Math.min(Math.max(128 + (combinedWave + noise) * 128, 0), 255);
      }
      setAudioData(dataArray);
    };
 
    const tick = () => {
      updateAudioData();
      animationFrameId = requestAnimationFrame(tick);
    };
 
    let animationFrameId = requestAnimationFrame(tick);
 
    return () => {
      cancelAnimationFrame(animationFrameId);
    };
  }, [volumeLevel, isSessionActive, audioParams]);
 
  return <AudioVisualizer audioData={audioData} isSessionActive={isSessionActive} />;
};
 
const MinimalComponent: React.FC<{
  audioParams: AudioParameters;
}> = ({ audioParams }) => {
  const { currentVolume: volumeLevel, isSessionActive, handleStartStopClick: toggleCall } = useWebRTCAudioSession('alloy');
  const [showVisualizer, setShowVisualizer] = useState(false);
 
  const handleToggleCall = () => {
    toggleCall();
    setShowVisualizer(!isSessionActive);
  };
 
  return (
    <div className="flex flex-col items-center justify-center min-h-full">
      <div className="flex items-center justify-center">
        <motion.button
          key="callButton"
          onClick={handleToggleCall}
          className="p-2 rounded-xl bg-secondary"
          whileTap={{ scale: 0.9 }}
          whileHover={{ scale: 1.1 }}
          initial={{ x: 0 }}
          animate={{ x: showVisualizer ? -10 : 0 }}
          transition={{ duration: 0.3 }}
          style={{ zIndex: 10, position: 'relative' }}
        >
          {isSessionActive ? <MicOff size={20} /> : <Mic size={20} />}
        </motion.button>
        <AnimatePresence>
          {showVisualizer && (
            <motion.div
              className="rounded-4xl"
              initial={{ width: 0, opacity: 0 }}
              animate={{ width: '100%', opacity: 1 }}
              exit={{ width: 0, opacity: 0 }}
              transition={{ duration: 0.3 }}
              style={{ marginLeft: '10px' }}
            >
              <AudioAnalyzer volumeLevel={volumeLevel} isSessionActive={isSessionActive} audioParams={audioParams} />
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    </div>
  );
};
 
const MinimalShowcase = () => {
  const [audioParams, setAudioParams] = useState<AudioParameters>({
    waveIntensity: 0.1,
    noiseLevel: 9,
    frequency: 5,
    complexity: 5
  });
 
  return (
    <div className="flex flex-col items-center justify-center min-h-full gap-4">
      <MinimalComponent audioParams={audioParams} />
      <div className="flex gap-2">
        <Popover>
          <PopoverTrigger asChild>
            <Button variant="outline" size="lg" className="mb-4">Animation Settings</Button>
          </PopoverTrigger>
          <PopoverContent className="w-80">
            <div className="grid gap-4">
              <div className="space-y-2">
                <h4 className="font-medium leading-none">Audio Visualization</h4>
                <p className="text-sm text-muted-foreground">Adjust the wave visualization parameters.</p>
              </div>
              <div className="grid gap-2">
                <div className="grid grid-cols-3 items-center gap-4">
                  <Label htmlFor="waveIntensity">Wave Intensity</Label>
                  <Input
                    id="waveIntensity"
                    type="number"
                    step={0.5}
                    value={audioParams.waveIntensity}
                    className="col-span-2"
                    onChange={(e) => setAudioParams({...audioParams, waveIntensity: parseFloat(e.target.value)})}
                  />
                </div>
                <div className="grid grid-cols-3 items-center gap-4">
                  <Label htmlFor="noiseLevel">Noise Level</Label>
                  <Input
                    id="noiseLevel"
                    type="number"
                    step={1}
                    value={audioParams.noiseLevel}
                    className="col-span-2"
                    onChange={(e) => setAudioParams({...audioParams, noiseLevel: parseFloat(e.target.value)})}
                  />
                </div>
                <div className="grid grid-cols-3 items-center gap-4">
                  <Label htmlFor="frequency">Frequency</Label>
                  <Input
                    id="frequency"
                    type="number"
                    step={1}
                    value={audioParams.frequency}
                    className="col-span-2"
                    onChange={(e) => setAudioParams({...audioParams, frequency: parseFloat(e.target.value)})}
                  />
                </div>
                <div className="grid grid-cols-3 items-center gap-4">
                  <Label htmlFor="complexity">Wave Complexity</Label>
                  <Input
                    id="complexity"
                    type="number"
                    min={1}
                    max={10}
                    value={audioParams.complexity}
                    className="col-span-2"
                    onChange={(e) => setAudioParams({...audioParams, complexity: parseInt(e.target.value)})}
                  />
                </div>
              </div>
            </div>
          </PopoverContent>
        </Popover>
      </div>
    </div>
  );
};
 
export default {MinimalComponent, MinimalShowcase};

Usage

Import the component in your file and then use it in your page. Ensure the use-webrtc hook is copied over to your project.

Note: This component uses Tailwind CSS, make sure to have it installed in your project.

import MinimalComponent from "@/components/openai-blocks/minimal-component";
 
export default function Home() {
  return (
    <main className="flex items-center justify-center h-screen">
      <MinimalComponent />
    </main>
  );
}