{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "voice-form-01", "description": "Voice-fill form", "dependencies": [ "@elevenlabs/elevenlabs-js", "ai", "zod" ], "registryDependencies": [ "https://ui.elevenlabs.io/r/voice-button.json", "https://ui.elevenlabs.io/r/live-waveform.json", "button", "card", "form", "input" ], "files": [ { "path": "blocks/voice-form-01/page.tsx", "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { useForm } from \"react-hook-form\"\n\nimport { cn } from \"@/lib/utils\"\nimport { voiceToFormAction } from \"@/app/voice-form/actions/voice-to-form\"\nimport {\n exampleFormSchema,\n ExampleFormValues,\n} from \"@/app/voice-form/schema\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\"\nimport {\n Form,\n FormControl,\n FormField,\n FormItem,\n FormLabel,\n FormMessage,\n} from \"@/components/ui/form\"\nimport { Input } from \"@/components/ui/input\"\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 [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 form = useForm({\n resolver: zodResolver(exampleFormSchema),\n defaultValues: {\n firstName: \"\",\n lastName: \"\",\n },\n mode: \"onChange\",\n })\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(\n 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 voiceToFormAction(audioFile)\n\n if (result.data && Object.keys(result.data).length > 0) {\n Object.entries(result.data).forEach(([key, value]) => {\n if (value) {\n form.setValue(key as keyof ExampleFormValues, value as string, {\n shouldValidate: true,\n })\n }\n })\n setSuccess(true)\n setTimeout(() => setSuccess(false), 2000)\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 [form]\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 onSubmit = (data: ExampleFormValues) => {\n console.log(\"Form submitted:\", data)\n }\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 Voice Fill\n Powered by ElevenLabs Scribe\n
\n \n
\n
\n \n
\n \n
\n (\n \n First Name *\n \n \n \n \n \n )}\n />\n (\n \n Last Name *\n \n \n \n \n \n )}\n />\n
\n \n \n
\n
\n
\n
\n )\n}\n", "type": "registry:page", "target": "app/voice-form/page.tsx" }, { "path": "blocks/voice-form-01/schema.ts", "content": "import { z } from \"zod\"\n\nexport const exampleFormSchema = z.object({\n firstName: z.string().min(2, {\n message: \"First name must be at least 2 characters.\",\n }),\n lastName: z.string().min(2, {\n message: \"Last name must be at least 2 characters.\",\n }),\n})\n\nexport type ExampleFormValues = z.infer\n", "type": "registry:file", "target": "app/voice-form/schema.ts" }, { "path": "blocks/voice-form-01/actions/voice-to-form.ts", "content": "\"use server\"\n\nimport { ElevenLabsClient } from \"@elevenlabs/elevenlabs-js\"\nimport { SpeechToTextChunkResponseModel } from \"@elevenlabs/elevenlabs-js/api/types/SpeechToTextChunkResponseModel\"\nimport { generateObject } from \"ai\"\n\nimport { exampleFormSchema } from \"@/app/voice-form/schema\"\n\nconst extractionSchema = exampleFormSchema.partial()\n\nexport async function voiceToFormAction(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 schemaShape = exampleFormSchema.shape\n const fieldNames = Object.keys(schemaShape)\n\n const { object: extractedData } = await generateObject({\n model: \"xai/grok-4-fast-non-reasoning\",\n schema: extractionSchema,\n mode: \"json\",\n system: `You are a form field extraction assistant. Extract ${fieldNames.join(\" and \")} from the speech.\n\nCRITICAL RULES:\n1. If just a name is said (like \"John Smith\"), extract it as firstName: \"John\", lastName: \"Smith\"\n2. Handle patterns like \"My name is...\", \"I'm...\", etc.\n3. Do NOT return empty strings - use undefined for fields not mentioned\n4. ONLY extract information that is EXPLICITLY mentioned`,\n prompt: `Extract form field values from this speech: \"${transcribedText}\"\n\nIMPORTANT: If this appears to be just a person's name, extract it as firstName and lastName fields.`,\n })\n\n return {\n data: extractedData,\n }\n } catch (error) {\n console.error(\"Voice to form error:\", error)\n return { data: {} }\n }\n}\n", "type": "registry:file", "target": "app/voice-form/actions/voice-to-form.ts" } ], "meta": { "iframeHeight": "700px", "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" }