Minimal

This component offers an inline Vapi experience, crafted with React and Tailwind CSS. This component animates a line to the Vapi 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 useVapi from '@/hooks/use-vapi'; // Adjust the import path as needed
 
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 }> = ({ volumeLevel, isSessionActive }) => {
  const [audioData, setAudioData] = useState<Uint8Array>(new Uint8Array(128));
 
  useEffect(() => {
    if (!isSessionActive) {
      setAudioData(new Uint8Array(128));
      return;
    }
 
    const updateAudioData = () => {
      const dataArray = new Uint8Array(128);
 
      for (let i = 0; i < dataArray.length; i++) {
        const variability = (Math.random() - 0.5) * 0.5; // Reduced variability for less noise
        dataArray[i] = Math.min(Math.max(128 + volumeLevel * variability * 128, 0), 255);
      }
      setAudioData(dataArray);
    };
 
    const tick = () => {
      updateAudioData();
      animationFrameId = requestAnimationFrame(tick);
    };
 
    let animationFrameId = requestAnimationFrame(tick);
 
    return () => {
      cancelAnimationFrame(animationFrameId);
    };
  }, [volumeLevel, isSessionActive]);
 
  return <AudioVisualizer audioData={audioData} isSessionActive={isSessionActive} />;
};
 
const MinimalComponent: React.FC = () => {
  const { volumeLevel, isSessionActive, toggleCall } = useVapi();
  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} />
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    </div>
  );
};
 
export default MinimalComponent;

Usage

Import the component in your file and then use it in your page. Ensure Vapi hook is setup.

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

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