Skip to content

enesakar/talking-forms

Repository files navigation

Talking Forms

Turn any form into a conversational experience. Three ways to fill it out — traditional form, AI-powered chat, or voice.

CleanShot.2026-02-13.at.19.39.57.mp4

Install

npm install talking-forms

Quick Start

import { TalkingForm } from "talking-forms"

function App() {
  return (
    <TalkingForm
      endpoint="/api/form"
      context="Coffee shop order"
      fields={[
        { name: "name", label: "Name", type: "text", required: true },
        { name: "drink", label: "Drink", type: "select", required: true, options: ["Espresso", "Latte", "Cappuccino"] },
        { name: "size", label: "Size", type: "select", required: true, options: ["Small", "Medium", "Large"] },
        { name: "extras", label: "Extras", type: "textarea" },
      ]}
      onSubmit={(data) => console.log(data)}
    />
  )
}

Users see a tabbed interface with three views — Form, Chat, and Voice. All three share state: fill a field in one view, it syncs to the others.

Server Setup

The endpoint prop points to your backend route that handles LLM question generation. API keys stay on your server.

Next.js (App Router)

// app/api/form/route.ts
import { createHandler } from "talking-forms/server"

const handler = createHandler({
  provider: "openai",
  apiKey: process.env.OPENAI_API_KEY,
})

export const POST = handler

Express

import express from "express"
import { createHandler } from "talking-forms/server"

const app = express()

app.post("/api/form", createHandler({
  provider: "openai",
  apiKey: process.env.OPENAI_API_KEY,
}))

Supported LLM Providers

Provider Value
OpenAI "openai"
Anthropic "anthropic"
Google Gemini "google"
createHandler({
  provider: "anthropic",
  apiKey: process.env.ANTHROPIC_API_KEY,
  model: "claude-sonnet-4-5-20250929", // optional
})

Voice Setup

Voice mode uses ElevenLabs for text-to-speech (reading questions) and speech-to-text (transcribing answers). Add a second server route:

Server

// app/api/voice/route.ts
import { createVoiceHandler } from "talking-forms/server"

export const POST = createVoiceHandler({
  apiKey: process.env.ELEVENLABS_API_KEY,
  voiceId: "optional-voice-id", // defaults to Rachel
})

Client

<TalkingForm
  endpoint="/api/form"
  voiceEndpoint="/api/voice"
  fields={[...]}
  onSubmit={...}
/>

The voice tab shows a hold-to-speak microphone button. Questions are spoken aloud via TTS, answers are transcribed via STT, and the same field validation + fuzzy matching applies.

Props

fields (required)

type Field = {
  name: string
  label: string
  type: "text" | "email" | "tel" | "number" | "textarea" | "select" | "date"
  required?: boolean
  placeholder?: string
  options?: string[]    // for type: "select"
  validation?: {
    min?: number
    max?: number
    pattern?: string
    message?: string
  }
}

endpoint (required)

Server route for LLM question generation.

onSubmit (required)

Called when the user completes the form from any view.

<TalkingForm
  onSubmit={(data) => {
    // data is Record<string, string>
    console.log(data)
  }}
/>

context

Describes what the form is for. Sent to the LLM so it generates contextually relevant questions.

<TalkingForm context="Coffee shop order" />
// LLM asks "What size drink would you like?" instead of "What size do you wear?"

voiceEndpoint

Server route for ElevenLabs TTS/STT. Required for the voice tab to work.

<TalkingForm voiceEndpoint="/api/voice" />

defaultView

Which tab to show first. Default: "form".

<TalkingForm defaultView="chat" />
// or
<TalkingForm defaultView="voice" />

chatGreeting

Custom first message in chat view.

<TalkingForm chatGreeting="Welcome! Let's get your order started." />

theme

Override colors and border radius.

<TalkingForm theme={{ primary: "#6366f1", radius: "0.5rem" }} />

onFieldChange

Called whenever a field value changes in any view.

<TalkingForm onFieldChange={(name, value) => console.log(name, value)} />

How It Works

Chat View

  1. Questions are generated by the LLM based on your field definitions and context
  2. Questions are prefetched one step ahead and cached — so responses feel instant
  3. User answers are validated client-side (email regex, number ranges, pattern matching)
  4. Select fields show clickable chips (small sets) or a searchable dropdown (large sets)
  5. Fuzzy matching handles typos and punctuation (Levenshtein distance)
  6. Progress bar tracks answered + skipped fields

Voice View

  1. Same question flow as chat, but spoken aloud via ElevenLabs TTS
  2. User holds the mic button to record, releases to send
  3. Audio is sent to ElevenLabs STT (Scribe v2, English) for transcription
  4. Transcribed text goes through the same validation + fuzzy matching
  5. If a select option doesn't match, the voice reads out the available options

Form View

  1. Standard form with dynamic field rendering based on your config
  2. Validation on blur with inline error messages
  3. All field types supported: text, email, tel, number, textarea, select, date

Shared State

All three views share the same state via useTalkingForm. Fill a field in chat, switch to form view — it's already there. Progress syncs across all tabs.

Select Fields

Select fields adapt their UI based on option count:

  • 8 or fewer options — clickable chip buttons in chat, spoken in voice
  • More than 8 options — searchable dropdown with type-to-filter

Fuzzy matching handles typos, punctuation, and partial matches automatically.

TypeScript

Full type safety out of the box.

import type { Field, TalkingFormProps, FormData } from "talking-forms"
import type { CreateHandlerOptions, CreateVoiceHandlerOptions } from "talking-forms/server"

License

MIT

About

Turn any form into a conversational experience — form, chat, or voice

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages