{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "voice-nav-01", "description": "Voice-nav site navigation", "dependencies": [ "@elevenlabs/elevenlabs-js", "ai", "zod" ], "registryDependencies": [ "https://ui.elevenlabs.io/r/voice-button.json", "button", "card" ], "files": [ { "path": "blocks/voice-nav-01/page.tsx", "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { voiceToSiteAction } from \"@/app/voice-nav/actions/voice-to-site\"\nimport {\n Card,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\"\nimport { VoiceButton } from \"@/components/ui/voice-button\"\n\nconst AUDIO_CONSTRAINTS: MediaStreamConstraints = {\n audio: {\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n}\n\nconst SUPPORTED_MIME_TYPES = [\"audio/webm;codecs=opus\", \"audio/webm\"] as const\n\nfunction getMimeType(): string {\n for (const type of SUPPORTED_MIME_TYPES) {\n if (MediaRecorder.isTypeSupported(type)) {\n return type\n }\n }\n return \"audio/webm\"\n}\n\nexport default function Page() {\n const [url, setUrl] = useState(\"https://elevenlabs.io/docs\")\n const [isRecording, setIsRecording] = useState(false)\n const [isProcessing, setIsProcessing] = useState(false)\n const [error, setError] = useState(\"\")\n const [success, setSuccess] = useState(false)\n\n const mediaRecorderRef = useRef(null)\n const audioChunksRef = useRef([])\n const streamRef = useRef(null)\n\n const cleanupStream = useCallback(() => {\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((track) => track.stop())\n streamRef.current = null\n }\n }, [])\n\n const processAudio = useCallback(async (audioBlob: Blob) => {\n setIsProcessing(true)\n setError(\"\")\n setSuccess(false)\n\n try {\n const audioFile = new File([audioBlob], \"audio.webm\", {\n type: audioBlob.type,\n })\n\n const result = await voiceToSiteAction(audioFile)\n\n if (result.data?.url) {\n setUrl(result.data.url)\n setSuccess(true)\n }\n } catch (err) {\n console.error(\"Voice input error:\", err)\n setError(err instanceof Error ? err.message : \"Failed to process audio\")\n } finally {\n setIsProcessing(false)\n }\n }, [])\n\n const stopRecording = useCallback(() => {\n if (mediaRecorderRef.current?.state !== \"inactive\") {\n mediaRecorderRef.current?.stop()\n }\n cleanupStream()\n setIsRecording(false)\n }, [cleanupStream])\n\n const startRecording = useCallback(async () => {\n try {\n setError(\"\")\n audioChunksRef.current = []\n\n const stream =\n await navigator.mediaDevices.getUserMedia(AUDIO_CONSTRAINTS)\n streamRef.current = stream\n\n const mimeType = getMimeType()\n const mediaRecorder = new MediaRecorder(stream, { mimeType })\n mediaRecorderRef.current = mediaRecorder\n\n mediaRecorder.ondataavailable = (event: BlobEvent) => {\n if (event.data.size > 0) {\n audioChunksRef.current.push(event.data)\n }\n }\n\n mediaRecorder.onstop = () => {\n const audioBlob = new Blob(audioChunksRef.current, { type: mimeType })\n processAudio(audioBlob)\n }\n\n mediaRecorder.start()\n setIsRecording(true)\n } catch (err) {\n setError(\"Microphone permission denied\")\n console.error(\"Microphone error:\", err)\n }\n }, [processAudio])\n\n const handleVoiceToggle = useCallback(() => {\n if (isRecording) {\n stopRecording()\n } else {\n startRecording()\n }\n }, [isRecording, startRecording, stopRecording])\n\n useEffect(() => {\n return cleanupStream\n }, [cleanupStream])\n\n const voiceState = isProcessing\n ? \"processing\"\n : isRecording\n ? \"recording\"\n : success\n ? \"success\"\n : error\n ? \"error\"\n : \"idle\"\n\n return (\n
\n \n
\n \n \n
\n
\n Voice Navigation\n \n Navigate websites with your voice (e.g., “Take me to\n the quickstart guide”)\n \n
\n \n
\n {error &&

{error}

}\n
\n
\n
\n \n
\n
\n
\n
\n )\n}\n", "type": "registry:page", "target": "app/voice-nav/page.tsx" }, { "path": "blocks/voice-nav-01/actions/voice-to-site.ts", "content": "\"use server\"\n\nimport { ElevenLabsClient } from \"@elevenlabs/elevenlabs-js\"\nimport { SpeechToTextChunkResponseModel } from \"@elevenlabs/elevenlabs-js/api/types/SpeechToTextChunkResponseModel\"\nimport { generateObject } from \"ai\"\nimport { z } from \"zod\"\n\nconst BASE_URL = \"https://elevenlabs.io/docs\"\n\nasync function fetchSitemap(baseUrl: string): Promise {\n const sitemapUrl = `${baseUrl}/sitemap.xml`\n const response = await fetch(sitemapUrl)\n const xml = await response.text()\n const urlMatches = xml.match(/(.*?)<\\/loc>/g) || []\n\n const urls = urlMatches\n .map((match) => match.replace(/<\\/?loc>/g, \"\"))\n .filter((url): url is string => url !== null)\n\n return [...new Set(urls)]\n}\n\nexport async function voiceToSiteAction(audio: File) {\n try {\n if (!audio) {\n return { data: {} }\n }\n\n const apiKey = process.env.ELEVENLABS_API_KEY\n if (!apiKey) {\n console.warn(\"ElevenLabs API key not configured\")\n return { data: {} }\n }\n\n const client = new ElevenLabsClient({ apiKey })\n const audioBuffer = await audio.arrayBuffer()\n const file = new File([audioBuffer], audio.name || \"audio.webm\", {\n type: audio.type || \"audio/webm\",\n })\n\n const transcriptionResult = await client.speechToText.convert({\n file,\n modelId: \"scribe_v1\",\n languageCode: \"en\",\n })\n\n const transcribedText = (\n transcriptionResult as SpeechToTextChunkResponseModel\n ).text\n\n if (!transcribedText) {\n return { data: {} }\n }\n\n const sitemap = await fetchSitemap(BASE_URL)\n const sitemapContext = sitemap.join(\"\\n\")\n\n const { object: extractedIntent } = await generateObject({\n model: \"xai/grok-4-fast-non-reasoning\",\n schema: z.object({\n url: z.string().describe(\"The full URL from the sitemap\"),\n }),\n mode: \"json\",\n system: `You are a voice navigation assistant. Match user intent to a URL from the sitemap.\n\nAvailable URLs:\n${sitemapContext}\n\nReturn the full URL that best matches the user's intent.`,\n messages: [\n {\n role: \"user\",\n content: `User said: \"${transcribedText}\". Which URL best matches their intent?`,\n },\n ],\n })\n\n return {\n data: {\n url: extractedIntent.url,\n },\n }\n } catch (error) {\n console.error(\"Voice to site error:\", error)\n return { data: {} }\n }\n}\n", "type": "registry:file", "target": "app/voice-nav/actions/voice-to-site.ts" } ], "meta": { "iframeHeight": "900px", "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": [ "audio" ], "type": "registry:block" }