{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "voice-chat-01", "description": "Voice chat 1", "dependencies": [ "@elevenlabs/react" ], "registryDependencies": [ "https://ui.elevenlabs.io/r/orb.json", "https://ui.elevenlabs.io/r/shimmering-text.json", "button", "card", "https://ui.elevenlabs.io/r/message.json", "https://ui.elevenlabs.io/r/response.json", "https://ui.elevenlabs.io/r/conversation.json", "scroll-area" ], "files": [ { "path": "blocks/voice-chat-01/page.tsx", "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport type { ComponentProps } from \"react\"\nimport {\n ConversationProvider,\n useConversation,\n useConversationControls,\n useConversationStatus,\n} from \"@elevenlabs/react\"\nimport {\n AudioLinesIcon,\n CheckIcon,\n CopyIcon,\n PhoneOffIcon,\n SendIcon,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Card,\n CardContent,\n CardFooter,\n CardHeader,\n} from \"@/components/ui/card\"\nimport {\n Conversation,\n ConversationContent,\n ConversationEmptyState,\n ConversationScrollButton,\n} from \"@/components/ui/conversation\"\nimport { Input } from \"@/components/ui/input\"\nimport { Message, MessageContent } from \"@/components/ui/message\"\nimport { Orb } from \"@/components/ui/orb\"\nimport { Response } from \"@/components/ui/response\"\nimport { ShimmeringText } from \"@/components/ui/shimmering-text\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\"\n\ntype SystemMessageType = \"initial\" | \"connecting\" | \"connected\" | \"error\"\n\ninterface ChatMessage {\n role: \"user\" | \"assistant\"\n content: string\n timestamp?: Date\n type?: SystemMessageType\n}\n\nconst DEFAULT_AGENT = {\n agentId: process.env.NEXT_PUBLIC_ELEVENLABS_AGENT_ID!,\n name: \"Customer Support\",\n description: \"AI Voice Assistant\",\n}\n\ntype ChatActionsProps = ComponentProps<\"div\">\n\nconst ChatActions = ({ className, children, ...props }: ChatActionsProps) => (\n
\n {children}\n
\n)\n\ntype ChatActionProps = ComponentProps & {\n tooltip?: string\n label?: string\n}\n\nconst ChatAction = ({\n tooltip,\n children,\n label,\n className,\n variant = \"ghost\",\n size = \"sm\",\n ...props\n}: ChatActionProps) => {\n const button = (\n \n {children}\n {label || tooltip}\n \n )\n\n if (tooltip) {\n return (\n \n \n {button}\n \n

{tooltip}

\n
\n
\n
\n )\n }\n\n return button\n}\n\nexport default function Page() {\n return (\n \n \n \n )\n}\n\nexport function VoiceChat01() {\n const { status } = useConversationStatus()\n const {\n startSession,\n endSession,\n sendUserMessage,\n getInputVolume,\n getOutputVolume,\n } = useConversationControls()\n const [messages, setMessages] = useState([])\n const [textInput, setTextInput] = useState(\"\")\n const [copiedIndex, setCopiedIndex] = useState(null)\n const [errorMessage, setErrorMessage] = useState(null)\n const mediaStreamRef = useRef(null)\n const isTextOnlyModeRef = useRef(true)\n\n // Keep useConversation for callbacks that need access to local state\n useConversation({\n onConnect: () => {\n if (!isTextOnlyModeRef.current) {\n setMessages([])\n }\n },\n onDisconnect: () => {\n if (!isTextOnlyModeRef.current) {\n setMessages([])\n }\n },\n onMessage: (message) => {\n if (message.message) {\n const newMessage: ChatMessage = {\n role: message.source === \"user\" ? \"user\" : \"assistant\",\n content: message.message,\n }\n setMessages((prev) => [...prev, newMessage])\n }\n },\n onError: (error) => {\n console.error(\"Error:\", error)\n },\n })\n\n const getMicStream = useCallback(async () => {\n if (mediaStreamRef.current) return mediaStreamRef.current\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true })\n mediaStreamRef.current = stream\n setErrorMessage(null)\n return stream\n } catch (error) {\n if (error instanceof DOMException && error.name === \"NotAllowedError\") {\n setErrorMessage(\"Please enable microphone permissions in your browser.\")\n }\n throw error\n }\n }, [])\n\n const startConversation = useCallback(\n async (\n textOnly: boolean = true,\n skipConnectingMessage: boolean = false\n ) => {\n try {\n isTextOnlyModeRef.current = textOnly\n\n if (!skipConnectingMessage) {\n setMessages([])\n }\n\n if (!textOnly) {\n await getMicStream()\n }\n\n startSession({\n agentId: DEFAULT_AGENT.agentId,\n connectionType: textOnly ? \"websocket\" : \"webrtc\",\n overrides: {\n conversation: {\n textOnly: textOnly,\n },\n agent: {\n firstMessage: textOnly ? \"\" : undefined,\n },\n },\n })\n } catch (error) {\n console.error(error)\n setMessages([])\n }\n },\n [startSession, getMicStream]\n )\n\n const handleCall = useCallback(async () => {\n if (status === \"disconnected\" || status === \"error\") {\n try {\n await startConversation(false)\n } catch {\n // getMicStream error already handled\n }\n } else if (status === \"connected\") {\n endSession()\n\n if (mediaStreamRef.current) {\n mediaStreamRef.current.getTracks().forEach((t) => t.stop())\n mediaStreamRef.current = null\n }\n }\n }, [status, endSession, startConversation])\n\n const handleTextInputChange = useCallback(\n (e: React.ChangeEvent) => {\n setTextInput(e.target.value)\n },\n []\n )\n\n const handleSendText = useCallback(async () => {\n if (!textInput.trim()) return\n\n const messageToSend = textInput\n\n if (status === \"disconnected\" || status === \"error\") {\n const userMessage: ChatMessage = {\n role: \"user\",\n content: messageToSend,\n }\n setTextInput(\"\")\n\n try {\n await startConversation(true, true)\n setMessages([userMessage])\n sendUserMessage(messageToSend)\n } catch (error) {\n console.error(\"Failed to start conversation:\", error)\n }\n } else if (status === \"connected\") {\n const newMessage: ChatMessage = {\n role: \"user\",\n content: messageToSend,\n }\n setMessages((prev) => [...prev, newMessage])\n setTextInput(\"\")\n\n sendUserMessage(messageToSend)\n }\n }, [textInput, status, startConversation, sendUserMessage])\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault()\n handleSendText()\n }\n },\n [handleSendText]\n )\n\n useEffect(() => {\n return () => {\n if (mediaStreamRef.current) {\n mediaStreamRef.current.getTracks().forEach((t) => t.stop())\n }\n }\n }, [])\n\n const isCallActive = status === \"connected\"\n const isTransitioning = status === \"connecting\"\n\n const scaledInputVolume = useCallback(() => {\n try {\n const rawValue = getInputVolume() ?? 0\n return Math.min(1.0, Math.pow(rawValue, 0.5) * 2.5)\n } catch {\n return 0\n }\n }, [getInputVolume])\n\n const scaledOutputVolume = useCallback(() => {\n try {\n const rawValue = getOutputVolume() ?? 0\n return Math.min(1.0, Math.pow(rawValue, 0.5) * 2.5)\n } catch {\n return 0\n }\n }, [getOutputVolume])\n\n return (\n \n \n
\n
\n \n
\n
\n

\n {DEFAULT_AGENT.name}\n

\n
\n {errorMessage ? (\n

{errorMessage}

\n ) : status === \"disconnected\" || status === \"error\" ? (\n

\n Tap to start voice chat\n

\n ) : status === \"connected\" ? (\n

Connected

\n ) : isTransitioning ? (\n \n ) : null}\n
\n
\n
\n \n
\n \n \n \n {messages.length === 0 ? (\n }\n title={\n status === \"connecting\" ? (\n \n ) : status === \"connected\" ? (\n \n ) : (\n \"Start a conversation\"\n )\n }\n description={\n status === \"connecting\"\n ? \"Connecting...\"\n : status === \"connected\"\n ? \"Ready to chat\"\n : \"Type a message or tap the voice button\"\n }\n />\n ) : (\n messages.map((message, index) => {\n return (\n
\n \n \n \n {message.content}\n \n \n {message.role === \"assistant\" && (\n
\n \n
\n )}\n
\n {message.role === \"assistant\" && (\n \n {\n navigator.clipboard.writeText(message.content)\n setCopiedIndex(index)\n setTimeout(() => setCopiedIndex(null), 2000)\n }}\n >\n {copiedIndex === index ? (\n \n ) : (\n \n )}\n \n \n )}\n
\n )\n })\n )}\n
\n \n
\n
\n \n
\n
\n \n \n \n Send message\n \n {!isCallActive && (\n \n \n Start voice call\n \n )}\n {isCallActive && (\n \n \n End call\n \n )}\n
\n
\n
\n \n )\n}\n", "type": "registry:page", "target": "app/voice-chat/page.tsx" } ], "meta": { "iframeHeight": "800px", "container": "w-full bg-surface min-h-svh flex px-4 py-12 items-center md:py-20 justify-center min-w-0", "mobile": "component" }, "categories": [ "agents" ], "type": "registry:block" }