diff --git a/apps/www/.env.example b/apps/www/.env.example index d2f09fb8..0ff9c126 100644 --- a/apps/www/.env.example +++ b/apps/www/.env.example @@ -1,2 +1,3 @@ NEXT_PUBLIC_V0_URL=https://v0.dev -NEXT_PUBLIC_APP_URL=http://localhost:4000 \ No newline at end of file +NEXT_PUBLIC_APP_URL=http://localhost:4000 +NEXT_PUBLIC_ELEVENLABS_AGENT_ID=your-agent-id-here diff --git a/apps/www/app/(view)/view/[name]/page.tsx b/apps/www/app/(view)/view/[name]/page.tsx index 6941e947..367b8a00 100644 --- a/apps/www/app/(view)/view/[name]/page.tsx +++ b/apps/www/app/(view)/view/[name]/page.tsx @@ -7,6 +7,7 @@ import { z } from "zod" import { siteConfig } from "@/lib/config" import { getRegistryComponent, getRegistryItem } from "@/lib/registry" import { absoluteUrl, cn } from "@/lib/utils" +import { RegistryPreviewProvider } from "@/components/registry-preview-provider" export const revalidate = false export const dynamic = "force-static" @@ -95,9 +96,11 @@ export default async function BlockPage({ return ( <> -
- -
+ +
+ +
+
) } diff --git a/apps/www/components/cards/index.tsx b/apps/www/components/cards/index.tsx index 7a3e171b..e9484766 100644 --- a/apps/www/components/cards/index.tsx +++ b/apps/www/components/cards/index.tsx @@ -1,3 +1,7 @@ +"use client" + +import { ConversationProvider } from "@elevenlabs/react" + import { CardsActivityGoal } from "@/components/cards/activity-goal" import { CardsBarVisualizer } from "@/components/cards/bar-visualizer" import { CardsLiveRecording } from "@/components/cards/live-recording" @@ -15,60 +19,62 @@ import { ConversationBar } from "@/registry/elevenlabs-ui/ui/conversation-bar" export function CardsDemo() { return ( -
-
-
- -
- -
-
- {/* */} + +
+
+
+ +
+ +
+
+ {/* */} +
-
-
-
- - +
+
+ + - - - -
-
- -
- + + + +
+
+ +
+ +
-
-
-
- -
- -
- +
+
+ +
+ +
+ +
+
+ +
-
- +
+
-
- +
+ +
+
+
-
-
- -
-
-
-
+ ) } diff --git a/apps/www/components/component-preview.tsx b/apps/www/components/component-preview.tsx index 44ad7863..7857b256 100644 --- a/apps/www/components/component-preview.tsx +++ b/apps/www/components/component-preview.tsx @@ -2,6 +2,7 @@ import Image from "next/image" import { ComponentPreviewTabs } from "@/components/component-preview-tabs" import { ComponentSource } from "@/components/component-source" +import { RegistryPreviewProvider } from "@/components/registry-preview-provider" import { Index } from "@/registry/__index__" export function ComponentPreview({ @@ -63,7 +64,11 @@ export function ComponentPreview({ className={className} align={align} hideCode={hideCode} - component={} + component={ + + + + } source={} marginOff={marginOff} {...props} diff --git a/apps/www/components/registry-preview-provider.tsx b/apps/www/components/registry-preview-provider.tsx new file mode 100644 index 00000000..cb36f1d3 --- /dev/null +++ b/apps/www/components/registry-preview-provider.tsx @@ -0,0 +1,8 @@ +"use client" + +import type { ReactNode } from "react" +import { ConversationProvider } from "@elevenlabs/react" + +export function RegistryPreviewProvider({ children }: { children: ReactNode }) { + return {children} +} diff --git a/apps/www/package.json b/apps/www/package.json index 390e1893..13d717e9 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -22,9 +22,9 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@elevenlabs/client": "^0.12.0", + "@elevenlabs/client": "^1.7.0", "@elevenlabs/elevenlabs-js": "^2.20.1", - "@elevenlabs/react": "^0.12.0", + "@elevenlabs/react": "^1.6.0", "@faker-js/faker": "^8.2.0", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accessible-icon": "^1.1.1", diff --git a/apps/www/registry/elevenlabs-ui/blocks/voice-chat-01/page.tsx b/apps/www/registry/elevenlabs-ui/blocks/voice-chat-01/page.tsx index 87d52958..17ea9a58 100644 --- a/apps/www/registry/elevenlabs-ui/blocks/voice-chat-01/page.tsx +++ b/apps/www/registry/elevenlabs-ui/blocks/voice-chat-01/page.tsx @@ -2,7 +2,11 @@ import { useCallback, useEffect, useRef, useState } from "react" import type { ComponentProps } from "react" -import { useConversation } from "@elevenlabs/react" +import { + useConversation, + useConversationControls, + useConversationStatus, +} from "@elevenlabs/react" import { AudioLinesIcon, CheckIcon, @@ -107,25 +111,29 @@ const ChatAction = ({ } export default function Page() { + const { status } = useConversationStatus() + const { + startSession, + endSession, + sendUserMessage, + getInputVolume, + getOutputVolume, + } = useConversationControls() const [messages, setMessages] = useState([]) - const [agentState, setAgentState] = useState< - "disconnected" | "connecting" | "connected" | "disconnecting" | null - >("disconnected") const [textInput, setTextInput] = useState("") const [copiedIndex, setCopiedIndex] = useState(null) const [errorMessage, setErrorMessage] = useState(null) const mediaStreamRef = useRef(null) const isTextOnlyModeRef = useRef(true) - const conversation = useConversation({ + // Keep useConversation for callbacks that need access to local state + useConversation({ onConnect: () => { - // Only clear messages for voice mode if (!isTextOnlyModeRef.current) { setMessages([]) } }, onDisconnect: () => { - // Only clear messages for voice mode if (!isTextOnlyModeRef.current) { setMessages([]) } @@ -141,10 +149,6 @@ export default function Page() { }, onError: (error) => { console.error("Error:", error) - setAgentState("disconnected") - }, - onDebug: (debug) => { - console.log("Debug:", debug) }, }) @@ -180,7 +184,7 @@ export default function Page() { await getMicStream() } - await conversation.startSession({ + startSession({ agentId: DEFAULT_AGENT.agentId, connectionType: textOnly ? "websocket" : "webrtc", overrides: { @@ -191,35 +195,31 @@ export default function Page() { firstMessage: textOnly ? "" : undefined, }, }, - onStatusChange: (status) => setAgentState(status.status), }) } catch (error) { console.error(error) - setAgentState("disconnected") setMessages([]) } }, - [conversation, getMicStream] + [startSession, getMicStream] ) const handleCall = useCallback(async () => { - if (agentState === "disconnected" || agentState === null) { - setAgentState("connecting") + if (status === "disconnected" || status === "error") { try { await startConversation(false) } catch { - setAgentState("disconnected") + // getMicStream error already handled } - } else if (agentState === "connected") { - conversation.endSession() - setAgentState("disconnected") + } else if (status === "connected") { + endSession() if (mediaStreamRef.current) { mediaStreamRef.current.getTracks().forEach((t) => t.stop()) mediaStreamRef.current = null } } - }, [agentState, conversation, startConversation]) + }, [status, endSession, startConversation]) const handleTextInputChange = useCallback( (e: React.ChangeEvent) => { @@ -233,24 +233,21 @@ export default function Page() { const messageToSend = textInput - if (agentState === "disconnected" || agentState === null) { + if (status === "disconnected" || status === "error") { const userMessage: ChatMessage = { role: "user", content: messageToSend, } setTextInput("") - setAgentState("connecting") try { await startConversation(true, true) - // Add message once converstation started setMessages([userMessage]) - // Send message after connection is established - conversation.sendUserMessage(messageToSend) + sendUserMessage(messageToSend) } catch (error) { console.error("Failed to start conversation:", error) } - } else if (agentState === "connected") { + } else if (status === "connected") { const newMessage: ChatMessage = { role: "user", content: messageToSend, @@ -258,9 +255,9 @@ export default function Page() { setMessages((prev) => [...prev, newMessage]) setTextInput("") - conversation.sendUserMessage(messageToSend) + sendUserMessage(messageToSend) } - }, [textInput, agentState, conversation, startConversation]) + }, [textInput, status, startConversation, sendUserMessage]) const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -280,19 +277,26 @@ export default function Page() { } }, []) - const isCallActive = agentState === "connected" - const isTransitioning = - agentState === "connecting" || agentState === "disconnecting" + const isCallActive = status === "connected" + const isTransitioning = status === "connecting" - const getInputVolume = useCallback(() => { - const rawValue = conversation.getInputVolume?.() ?? 0 - return Math.min(1.0, Math.pow(rawValue, 0.5) * 2.5) - }, [conversation]) + const scaledInputVolume = useCallback(() => { + try { + const rawValue = getInputVolume() ?? 0 + return Math.min(1.0, Math.pow(rawValue, 0.5) * 2.5) + } catch { + return 0 + } + }, [getInputVolume]) - const getOutputVolume = useCallback(() => { - const rawValue = conversation.getOutputVolume?.() ?? 0 - return Math.min(1.0, Math.pow(rawValue, 0.5) * 2.5) - }, [conversation]) + const scaledOutputVolume = useCallback(() => { + try { + const rawValue = getOutputVolume() ?? 0 + return Math.min(1.0, Math.pow(rawValue, 0.5) * 2.5) + } catch { + return 0 + } + }, [getOutputVolume]) return (
@@ -317,17 +321,14 @@ export default function Page() {
{errorMessage ? (

{errorMessage}

- ) : agentState === "disconnected" || agentState === null ? ( + ) : status === "disconnected" || status === "error" ? (

Tap to start voice chat

- ) : agentState === "connected" ? ( + ) : status === "connected" ? (

Connected

) : isTransitioning ? ( - + ) : null}
@@ -335,7 +336,7 @@ export default function Page() {
} title={ - agentState === "connecting" ? ( + status === "connecting" ? ( - ) : agentState === "connected" ? ( + ) : status === "connected" ? ( ) : ( "Start a conversation" ) } description={ - agentState === "connecting" + status === "connecting" ? "Connecting..." - : agentState === "connected" + : status === "connected" ? "Ready to chat" : "Type a message or tap the voice button" } diff --git a/apps/www/registry/elevenlabs-ui/blocks/voice-chat-02/page.tsx b/apps/www/registry/elevenlabs-ui/blocks/voice-chat-02/page.tsx index 45febc21..63a72f88 100644 --- a/apps/www/registry/elevenlabs-ui/blocks/voice-chat-02/page.tsx +++ b/apps/www/registry/elevenlabs-ui/blocks/voice-chat-02/page.tsx @@ -1,7 +1,10 @@ "use client" import { useCallback, useState } from "react" -import { useConversation } from "@elevenlabs/react" +import { + useConversationControls, + useConversationStatus, +} from "@elevenlabs/react" import { AnimatePresence, motion } from "framer-motion" import { Loader2Icon, PhoneIcon, PhoneOffIcon } from "lucide-react" @@ -17,68 +20,56 @@ const DEFAULT_AGENT = { description: "Tap to start voice chat", } -type AgentState = - | "disconnected" - | "connecting" - | "connected" - | "disconnecting" - | null - export default function Page() { - const [agentState, setAgentState] = useState("disconnected") + const { status } = useConversationStatus() + const { startSession, endSession, getInputVolume, getOutputVolume } = + useConversationControls() const [errorMessage, setErrorMessage] = useState(null) - const conversation = useConversation({ - onConnect: () => console.log("Connected"), - onDisconnect: () => console.log("Disconnected"), - onMessage: (message) => console.log("Message:", message), - onError: (error) => { - console.error("Error:", error) - setAgentState("disconnected") - }, - }) - const startConversation = useCallback(async () => { try { setErrorMessage(null) await navigator.mediaDevices.getUserMedia({ audio: true }) - await conversation.startSession({ + startSession({ agentId: DEFAULT_AGENT.agentId, connectionType: "webrtc", - onStatusChange: (status) => setAgentState(status.status), }) } catch (error) { console.error("Error starting conversation:", error) - setAgentState("disconnected") if (error instanceof DOMException && error.name === "NotAllowedError") { setErrorMessage("Please enable microphone permissions in your browser.") } } - }, [conversation]) + }, [startSession]) const handleCall = useCallback(() => { - if (agentState === "disconnected" || agentState === null) { - setAgentState("connecting") + if (status === "disconnected" || status === "error") { startConversation() - } else if (agentState === "connected") { - conversation.endSession() - setAgentState("disconnected") + } else if (status === "connected") { + endSession() } - }, [agentState, conversation, startConversation]) + }, [status, endSession, startConversation]) - const isCallActive = agentState === "connected" - const isTransitioning = - agentState === "connecting" || agentState === "disconnecting" + const isCallActive = status === "connected" + const isTransitioning = status === "connecting" - const getInputVolume = useCallback(() => { - const rawValue = conversation.getInputVolume?.() ?? 0 - return Math.min(1.0, Math.pow(rawValue, 0.5) * 2.5) - }, [conversation]) + const scaledInputVolume = useCallback(() => { + try { + const rawValue = getInputVolume() ?? 0 + return Math.min(1.0, Math.pow(rawValue, 0.5) * 2.5) + } catch { + return 0 + } + }, [getInputVolume]) - const getOutputVolume = useCallback(() => { - const rawValue = conversation.getOutputVolume?.() ?? 0 - return Math.min(1.0, Math.pow(rawValue, 0.5) * 2.5) - }, [conversation]) + const scaledOutputVolume = useCallback(() => { + try { + const rawValue = getOutputVolume() ?? 0 + return Math.min(1.0, Math.pow(rawValue, 0.5) * 2.5) + } catch { + return 0 + } + }, [getOutputVolume]) return ( @@ -89,8 +80,8 @@ export default function Page() {
@@ -109,7 +100,7 @@ export default function Page() { > {errorMessage} - ) : agentState === "disconnected" || agentState === null ? ( + ) : status === "disconnected" || status === "error" ? ( {isTransitioning ? ( - + ) : ( Connected )} diff --git a/apps/www/registry/elevenlabs-ui/blocks/voice-chat-03/page.tsx b/apps/www/registry/elevenlabs-ui/blocks/voice-chat-03/page.tsx index 03654d1d..aa7b7133 100644 --- a/apps/www/registry/elevenlabs-ui/blocks/voice-chat-03/page.tsx +++ b/apps/www/registry/elevenlabs-ui/blocks/voice-chat-03/page.tsx @@ -1,6 +1,7 @@ "use client" import { useState } from "react" +import { useConversation } from "@elevenlabs/react" import { CheckIcon, CopyIcon } from "lucide-react" import { cn } from "@/registry/elevenlabs-ui/lib/utils" @@ -34,6 +35,20 @@ export default function Page() { const [messages, setMessages] = useState([]) const [copiedIndex, setCopiedIndex] = useState(null) + useConversation({ + onConnect: () => setMessages([]), + onDisconnect: () => setMessages([]), + onMessage: (message) => { + if (message.message) { + const newMessage: ChatMessage = { + role: message.source === "user" ? "user" : "assistant", + content: message.message, + } + setMessages((prev) => [...prev, newMessage]) + } + }, + }) + return (
@@ -112,8 +127,6 @@ export default function Page() { setMessages([])} - onDisconnect={() => setMessages([])} onSendMessage={(message) => { const userMessage: ChatMessage = { role: "user", @@ -121,14 +134,6 @@ export default function Page() { } setMessages((prev) => [...prev, userMessage]) }} - onMessage={(message) => { - const newMessage: ChatMessage = { - role: message.source === "user" ? "user" : "assistant", - content: message.message, - } - setMessages((prev) => [...prev, newMessage]) - }} - onError={(error) => console.error("Conversation error:", error)} />
diff --git a/apps/www/registry/elevenlabs-ui/examples/conversation-bar-demo.tsx b/apps/www/registry/elevenlabs-ui/examples/conversation-bar-demo.tsx index 36d1cfa1..8f408241 100644 --- a/apps/www/registry/elevenlabs-ui/examples/conversation-bar-demo.tsx +++ b/apps/www/registry/elevenlabs-ui/examples/conversation-bar-demo.tsx @@ -10,13 +10,7 @@ export default function ConversationBarDemo() { return (
- console.log("Connected")} - onDisconnect={() => console.log("Disconnected")} - onMessage={(message) => console.log("Message:", message)} - onError={(error) => console.error("Error:", error)} - /> +
) diff --git a/apps/www/registry/elevenlabs-ui/ui/conversation-bar.tsx b/apps/www/registry/elevenlabs-ui/ui/conversation-bar.tsx index 0d7937e8..bb144290 100644 --- a/apps/www/registry/elevenlabs-ui/ui/conversation-bar.tsx +++ b/apps/www/registry/elevenlabs-ui/ui/conversation-bar.tsx @@ -1,7 +1,11 @@ "use client" import * as React from "react" -import { useConversation } from "@elevenlabs/react" +import { + useConversationControls, + useConversationInput, + useConversationStatus, +} from "@elevenlabs/react" import { ArrowUpIcon, ChevronDown, @@ -19,12 +23,6 @@ import { LiveWaveform } from "@/registry/elevenlabs-ui/ui/live-waveform" import { Separator } from "@/registry/elevenlabs-ui/ui/separator" import { Textarea } from "@/registry/elevenlabs-ui/ui/textarea" -export type AgentConnectionState = - | "disconnected" - | "connecting" - | "connected" - | "disconnecting" - export interface ConversationBarProps { /** * ElevenLabs Agent ID to connect to @@ -41,26 +39,6 @@ export interface ConversationBarProps { */ waveformClassName?: string - /** - * Callback when conversation connects - */ - onConnect?: () => void - - /** - * Callback when conversation disconnects - */ - onDisconnect?: () => void - - /** - * Callback when an error occurs - */ - onError?: (error: Error) => void - - /** - * Callback when a message is received - */ - onMessage?: (message: { source: "user" | "ai"; message: string }) => void - /** * Callback when user sends a message */ @@ -70,282 +48,235 @@ export interface ConversationBarProps { export const ConversationBar = React.forwardRef< HTMLDivElement, ConversationBarProps ->( - ( - { - agentId, - className, - waveformClassName, - onConnect, - onDisconnect, - onError, - onMessage, - onSendMessage, +>(({ agentId, className, waveformClassName, onSendMessage }, ref) => { + const { status } = useConversationStatus() + const { startSession, endSession, sendUserMessage, sendContextualUpdate } = + useConversationControls() + const { isMuted, setMuted } = useConversationInput() + const [keyboardOpen, setKeyboardOpen] = React.useState(false) + const [textInput, setTextInput] = React.useState("") + const mediaStreamRef = React.useRef(null) + + const isConnected = status === "connected" + + const getMicStream = React.useCallback(async () => { + if (mediaStreamRef.current) return mediaStreamRef.current + + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + mediaStreamRef.current = stream + + return stream + }, []) + + const startConversation = React.useCallback(async () => { + try { + await getMicStream() + + startSession({ + agentId, + connectionType: "webrtc", + }) + } catch (error) { + console.error("Error starting conversation:", error) + } + }, [getMicStream, agentId, startSession]) + + const handleEndSession = React.useCallback(() => { + endSession() + + if (mediaStreamRef.current) { + mediaStreamRef.current.getTracks().forEach((t) => t.stop()) + mediaStreamRef.current = null + } + }, [endSession]) + + const toggleMute = React.useCallback(() => { + setMuted(!isMuted) + }, [isMuted, setMuted]) + + const handleStartOrEnd = React.useCallback(() => { + if (status === "connected" || status === "connecting") { + handleEndSession() + } else if (status === "disconnected" || status === "error") { + startConversation() + } + }, [status, handleEndSession, startConversation]) + + const handleSendText = React.useCallback(() => { + if (!textInput.trim()) return + + const messageToSend = textInput + sendUserMessage(messageToSend) + setTextInput("") + onSendMessage?.(messageToSend) + }, [sendUserMessage, textInput, onSendMessage]) + + const handleTextChange = React.useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value + setTextInput(value) + + if (value.trim() && isConnected) { + sendContextualUpdate(value) + } }, - ref - ) => { - const [isMuted, setIsMuted] = React.useState(false) - const [agentState, setAgentState] = - React.useState("disconnected") - const [keyboardOpen, setKeyboardOpen] = React.useState(false) - const [textInput, setTextInput] = React.useState("") - const mediaStreamRef = React.useRef(null) - - const conversation = useConversation({ - onConnect: () => { - onConnect?.() - }, - onDisconnect: () => { - setAgentState("disconnected") - onDisconnect?.() - setKeyboardOpen(false) - }, - onMessage: (message) => { - onMessage?.(message) - }, - micMuted: isMuted, - onError: (error: unknown) => { - console.error("Error:", error) - setAgentState("disconnected") - const errorObj = - error instanceof Error - ? error - : new Error( - typeof error === "string" ? error : JSON.stringify(error) - ) - onError?.(errorObj) - }, - }) - - const getMicStream = React.useCallback(async () => { - if (mediaStreamRef.current) return mediaStreamRef.current - - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) - mediaStreamRef.current = stream - - return stream - }, []) - - const startConversation = React.useCallback(async () => { - try { - setAgentState("connecting") - - await getMicStream() - - await conversation.startSession({ - agentId, - connectionType: "webrtc", - onStatusChange: (status) => setAgentState(status.status), - }) - } catch (error) { - console.error("Error starting conversation:", error) - setAgentState("disconnected") - onError?.(error as Error) + [sendContextualUpdate, isConnected] + ) + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleSendText() } - }, [conversation, getMicStream, agentId, onError]) - - const handleEndSession = React.useCallback(() => { - conversation.endSession() - setAgentState("disconnected") + }, + [handleSendText] + ) + React.useEffect(() => { + return () => { if (mediaStreamRef.current) { mediaStreamRef.current.getTracks().forEach((t) => t.stop()) - mediaStreamRef.current = null - } - }, [conversation]) - - const toggleMute = React.useCallback(() => { - setIsMuted((prev) => !prev) - }, []) - - const handleStartOrEnd = React.useCallback(() => { - if (agentState === "connected" || agentState === "connecting") { - handleEndSession() - } else if (agentState === "disconnected") { - startConversation() } - }, [agentState, handleEndSession, startConversation]) - - const handleSendText = React.useCallback(() => { - if (!textInput.trim()) return - - const messageToSend = textInput - conversation.sendUserMessage(messageToSend) - setTextInput("") - onSendMessage?.(messageToSend) - }, [conversation, textInput, onSendMessage]) - - const isConnected = agentState === "connected" - - const handleTextChange = React.useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value - setTextInput(value) - - if (value.trim() && isConnected) { - conversation.sendContextualUpdate(value) - } - }, - [conversation, isConnected] - ) - - const handleKeyDown = React.useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault() - handleSendText() - } - }, - [handleSendText] - ) - - React.useEffect(() => { - return () => { - if (mediaStreamRef.current) { - mediaStreamRef.current.getTracks().forEach((t) => t.stop()) - } - } - }, []) - - return ( -
- -
-
- {keyboardOpen && } -
-
-
-
-
+ +
+
+ {keyboardOpen && } +
+
+
+
+
+ - - {agentState === "disconnected" && ( -
- - Customer Support - -
- )} -
+ /> + {(status === "disconnected" || status === "error") && ( +
+ + Customer Support + +
+ )}
-
- - - - -
-
- -
-
-