diff --git a/examples/conversational-ai-nextjs/convai-demo/.eslintrc.json b/examples/conversational-ai-nextjs/convai-demo/.eslintrc.json new file mode 100644 index 0000000..3722418 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/examples/conversational-ai-nextjs/convai-demo/.gitignore b/examples/conversational-ai-nextjs/convai-demo/.gitignore new file mode 100644 index 0000000..9eada78 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/.gitignore @@ -0,0 +1,43 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files (can opt-in for commiting if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# jetbrains +.idea \ No newline at end of file diff --git a/examples/conversational-ai-nextjs/convai-demo/README.md b/examples/conversational-ai-nextjs/convai-demo/README.md new file mode 100644 index 0000000..d05a930 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/README.md @@ -0,0 +1,18 @@ +## Conversational AI Demo + +Run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +## Learn More + +- [Conversational AI Tutorial](https://elevenlabs.io/docs/product/introduction) +- [Conversational AI SDK](https://elevenlabs.io/docs/libraries/conversational-ai-sdk-js) diff --git a/examples/conversational-ai-nextjs/convai-demo/app/api/signed-url/route.ts b/examples/conversational-ai-nextjs/convai-demo/app/api/signed-url/route.ts new file mode 100644 index 0000000..5ad464f --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/app/api/signed-url/route.ts @@ -0,0 +1,33 @@ +import {NextResponse} from "next/server"; + +export async function GET() { + const agentId = process.env.AGENT_ID + const apiKey = process.env.XI_API_KEY + if (!agentId) { + throw Error('AGENT_ID is not set') + } + if (!apiKey) { + throw Error('XI_API_KEY is not set') + } + try { + const response = await fetch( + `https://api.elevenlabs.io/v1/convai/conversation/get_signed_url?agent_id=${agentId}`, + { + method: 'GET', + headers: { + 'xi-api-key': apiKey, + } + } + ); + + if (!response.ok) { + throw new Error('Failed to get signed URL'); + } + + const data = await response.json(); + return NextResponse.json({signedUrl: data.signed_url}) + } catch (error) { + console.error('Error:', error); + return NextResponse.json({ error: 'Failed to get signed URL' }, { status: 500 }); + } +} diff --git a/examples/conversational-ai-nextjs/convai-demo/app/favicon.ico b/examples/conversational-ai-nextjs/convai-demo/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/examples/conversational-ai-nextjs/convai-demo/app/favicon.ico differ diff --git a/examples/conversational-ai-nextjs/convai-demo/app/globals.css b/examples/conversational-ai-nextjs/convai-demo/app/globals.css new file mode 100644 index 0000000..00f44e5 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/app/globals.css @@ -0,0 +1,138 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: -apple-system, "system-ui", sans-serif; +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + --primary: 24 9.8% 10%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 20 14.3% 4.1%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + + .dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + --primary: 60 9.1% 97.8%; + --primary-foreground: 24 9.8% 10%; + --secondary: 12 6.5% 15.1%; + --secondary-foreground: 60 9.1% 97.8%; + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 12 6.5% 15.1%; + --input: 12 6.5% 15.1%; + --ring: 24 5.7% 82.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } +} + + +/* Orb Styling */ +.orb { + width: 180px; + height: 180px; + border-radius: 50%; + position: relative; + overflow: hidden; +} + +.animate-orb { + animation: wave 0.4s infinite ease-in-out; +} + +.animate-orb-slow { + animation: wave 2s infinite ease-in-out; +} + +.orb-active { + background: radial-gradient(circle at center, #c7c7c7, #908e8e, #595959); +} + +.orb-inactive { + background: radial-gradient(circle at center, + rgba(200, 200, 200, 0.8), + rgba(150, 150, 150, 0.6), + rgba(100, 100, 100, 0.4)); +} + +/* Inner Gradient Layer for Waving Effect */ +.orb::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + + border-radius: 50%; + animation: wave-motion 6s infinite linear; +} + +/* Keyframes for Waving Animation */ +@keyframes wave { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} + +@keyframes wave-motion { + 0% { + transform: translate(0, 0) rotate(0deg); + } + 50% { + transform: translate(10px, 10px) rotate(180deg); + } + 100% { + transform: translate(0, 0) rotate(360deg); + } +} \ No newline at end of file diff --git a/examples/conversational-ai-nextjs/convai-demo/app/layout.tsx b/examples/conversational-ai-nextjs/convai-demo/app/layout.tsx new file mode 100644 index 0000000..8288373 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/app/layout.tsx @@ -0,0 +1,49 @@ +import type {Metadata} from "next"; +import "./globals.css"; +import {BackgroundWave} from "@/components/background-wave"; +import Link from "next/link"; +import {ElevenLabsLogo, GithubLogo} from "@/components/logos"; + +export const metadata: Metadata = { + title: "ConvAI", +}; + +export default function RootLayout({children}: Readonly<{ children: React.ReactNode }>) { + return ( + + +
+ + {children} + +
+ + + ); +} diff --git a/examples/conversational-ai-nextjs/convai-demo/app/page.tsx b/examples/conversational-ai-nextjs/convai-demo/app/page.tsx new file mode 100644 index 0000000..b0ed41b --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/app/page.tsx @@ -0,0 +1,12 @@ +import {ConvAI} from "@/components/ConvAI"; + +export default function Home() { + return ( +
+
+ +
+
+ ); +} diff --git a/examples/conversational-ai-nextjs/convai-demo/components.json b/examples/conversational-ai-nextjs/convai-demo/components.json new file mode 100644 index 0000000..bdb38ea --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "stone", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/examples/conversational-ai-nextjs/convai-demo/components/Code.tsx b/examples/conversational-ai-nextjs/convai-demo/components/Code.tsx new file mode 100644 index 0000000..54d6bdb --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/components/Code.tsx @@ -0,0 +1,5 @@ +import {PropsWithChildren} from "react"; + +export function Code({children}: PropsWithChildren) { + return {children} +} \ No newline at end of file diff --git a/examples/conversational-ai-nextjs/convai-demo/components/ConvAI.tsx b/examples/conversational-ai-nextjs/convai-demo/components/ConvAI.tsx new file mode 100644 index 0000000..8b54333 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/components/ConvAI.tsx @@ -0,0 +1,113 @@ +"use client" + +import {Button} from "@/components/ui/button"; +import * as React from "react"; +import {useState} from "react"; +import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; +import {Conversation} from "@11labs/client"; +import {cn} from "@/lib/utils"; + +async function requestMicrophonePermission() { + try { + await navigator.mediaDevices.getUserMedia({audio: true}) + return true + } catch { + console.error('Microphone permission denied') + return false + } +} + +async function getSignedUrl(): Promise { + const response = await fetch('/api/signed-url') + if (!response.ok) { + throw Error('Failed to get signed url') + } + const data = await response.json() + return data.signedUrl +} + +export function ConvAI() { + const [conversation, setConversation] = useState(null) + const [isConnected, setIsConnected] = useState(false) + const [isSpeaking, setIsSpeaking] = useState(false) + + async function startConversation() { + const hasPermission = await requestMicrophonePermission() + if (!hasPermission) { + alert("No permission") + return; + } + const signedUrl = await getSignedUrl() + const conversation = await Conversation.startSession({ + signedUrl: signedUrl, + onConnect: () => { + setIsConnected(true) + setIsSpeaking(true) + }, + onDisconnect: () => { + setIsConnected(false) + setIsSpeaking(false) + }, + onError: (error) => { + console.log(error) + alert('An error occurred during the conversation') + }, + onModeChange: ({mode}) => { + setIsSpeaking(mode === 'speaking') + }, + }) + setConversation(conversation) + } + + async function endConversation() { + if (!conversation) { + return + } + await conversation.endSession() + setConversation(null) + } + + return ( +
+ + + + + {isConnected ? ( + isSpeaking ? `Agent is speaking` : 'Agent is listening' + ) : ( + 'Disconnected' + )} + + +
+
+ + + + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/examples/conversational-ai-nextjs/convai-demo/components/background-wave.tsx b/examples/conversational-ai-nextjs/convai-demo/components/background-wave.tsx new file mode 100644 index 0000000..1713efa --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/components/background-wave.tsx @@ -0,0 +1,15 @@ +"use client"; +import { motion } from "framer-motion"; + +export const BackgroundWave = () => { + return ( + + ); +}; diff --git a/examples/conversational-ai-nextjs/convai-demo/components/logos.tsx b/examples/conversational-ai-nextjs/convai-demo/components/logos.tsx new file mode 100644 index 0000000..649e06b --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/components/logos.tsx @@ -0,0 +1,44 @@ +export const ElevenLabsLogo = ({ className }: { className?: string }) => { + return ( + + + + + + + + + + + + + + + ); +}; + +export const GithubLogo = ({ className }: { className?: string }) => { + return ( + + + + ); +}; \ No newline at end of file diff --git a/examples/conversational-ai-nextjs/convai-demo/components/ui/button.tsx b/examples/conversational-ai-nextjs/convai-demo/components/ui/button.tsx new file mode 100644 index 0000000..c29e141 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import {Slot} from "@radix-ui/react-slot" +import {cva, type VariantProps} from "class-variance-authority" + +import {cn} from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + xs: "h-6 rounded-md px-3 text-xs", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + xl: "h-12 rounded-lg px-10", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({className, variant, size, asChild = false, ...props}, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export {Button, buttonVariants} diff --git a/examples/conversational-ai-nextjs/convai-demo/components/ui/card.tsx b/examples/conversational-ai-nextjs/convai-demo/components/ui/card.tsx new file mode 100644 index 0000000..cabfbfc --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/examples/conversational-ai-nextjs/convai-demo/lib/utils.ts b/examples/conversational-ai-nextjs/convai-demo/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/examples/conversational-ai-nextjs/convai-demo/next.config.ts b/examples/conversational-ai-nextjs/convai-demo/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/examples/conversational-ai-nextjs/convai-demo/package.json b/examples/conversational-ai-nextjs/convai-demo/package.json new file mode 100644 index 0000000..4779eb0 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/package.json @@ -0,0 +1,37 @@ +{ + "name": "convai-demo", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@11labs/client": "0.0.1", + "@radix-ui/react-slot": "^1.1.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "framer-motion": "^11.11.11", + "lucide-react": "^0.454.0", + "next": "15.0.2", + "react": "19.0.0-rc-02c0e824-20241028", + "react-dom": "19.0.0-rc-02c0e824-20241028", + "tailwind-merge": "^2.5.4", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "15.0.2", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/examples/conversational-ai-nextjs/convai-demo/public/wave-loop.mp4 b/examples/conversational-ai-nextjs/convai-demo/public/wave-loop.mp4 new file mode 100644 index 0000000..95c9900 Binary files /dev/null and b/examples/conversational-ai-nextjs/convai-demo/public/wave-loop.mp4 differ diff --git a/examples/conversational-ai-nextjs/convai-demo/tailwind.config.ts b/examples/conversational-ai-nextjs/convai-demo/tailwind.config.ts new file mode 100644 index 0000000..ebc9f38 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/tailwind.config.ts @@ -0,0 +1,63 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + darkMode: ["class"], + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + } + } + }, + plugins: [require("tailwindcss-animate")], +}; +export default config; diff --git a/examples/conversational-ai-nextjs/convai-demo/tsconfig.json b/examples/conversational-ai-nextjs/convai-demo/tsconfig.json new file mode 100644 index 0000000..d8b9323 --- /dev/null +++ b/examples/conversational-ai-nextjs/convai-demo/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}