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>
);
}