form-to-chat
v0.1.0
Published
Convert any HTML form into a conversational chat interface with optional LLM integration
Maintainers
Readme
form-to-chat
Turn any HTML
<form>into a friendly conversation. Works for people, bots, and everything in between.
Why
Nobody enjoys filling out forms. But everyone knows how to have a conversation.
form-to-chat takes your existing HTML forms and presents them as a step-by-step chat. Users answer one question at a time, with validation, friendly error messages, and a summary at the end. No redesign needed -- point it at a <form> and it works.
It also speaks the language of AI agents. Any form can export itself as a tool definition for function-calling LLMs, sync values in real time with the hidden original form, and fire standard DOM events that scripts and automation tools can listen to.
Three ways to use it:
- As a library --
npm install form-to-chatand use the JS API or<form-chat>web component - As a Chrome extension -- detects forms on any page and overlays a chat widget (see
extensions/) - As an agent protocol -- export tool definitions, listen to events, automate with Puppeteer/Playwright
What it does
Given a standard HTML form:
<form id="signup">
<label for="name">Full Name</label>
<input type="text" id="name" name="name" required />
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
<label for="plan">Plan</label>
<select id="plan" name="plan">
<option value="free">Free</option>
<option value="pro">Pro ($9/mo)</option>
</select>
</form>form-to-chat transforms it into a step-by-step chat:
Bot: Hey! Let's get you signed up. What's your full name?
You: Jane Doe
Bot: Nice! What's your email address?
You: [email protected]
Bot: Which plan would you prefer? Free or Pro ($9/mo)?
You: Pro
Bot: All done.
Install
npm install form-to-chatQuick Start
One-shot fill (agent)
import { FormToChat } from 'form-to-chat';
const form = await FormToChat.from('#signup');
const answers = await form.fill({
name: 'Jane Doe',
email: '[email protected]',
plan: 'pro',
});
// answers = { name: 'Jane Doe', email: '[email protected]', plan: 'pro' }Step-by-step (with snapshots)
const form = await FormToChat.from('#signup');
const snap1 = await form.answer('Jane Doe');
// snap1 = { answers: { name: 'Jane' }, current: { name: 'email' }, done: false }
const snap2 = await form.answer('[email protected]');
// snap2.answers = { name: 'Jane', email: '[email protected]' }
// snap1 is still frozen — immutableAsync iterator
const form = await FormToChat.from('#signup');
for await (const step of form) {
console.log(step.question); // "What's your full name?"
console.log(step.field); // { name: 'name', type: 'text', required: true }
const snap = await step.answer('Jane Doe');
// snap = frozen snapshot of state after this answer
}Web Component (for humans)
<script type="module">
import 'form-to-chat/ui';
</script>
<form id="my-form">
<!-- your fields -->
</form>
<form-chat form="#my-form" theme="dark" greeting="Let's do this!"></form-chat>With AI (optional, zero deps)
const form = await FormToChat.from('#signup', { ai: 'openai' });
// Reads OPENAI_API_KEY from env. Rephrases questions naturally,
// validates answers, generates friendly summaries.Works with any OpenAI-compatible endpoint via raw fetch() — no SDK needed:
// Ollama / local LLM
const form = await FormToChat.from('#signup', {
ai: { baseURL: 'http://localhost:11434/v1', model: 'llama3' }
});
// Groq
const form = await FormToChat.from('#signup', {
ai: { baseURL: 'https://api.groq.com/openai/v1', model: 'llama-3.1-8b-instant' }
});
// Any OpenAI-compatible endpoint
const form = await FormToChat.from('#signup', {
ai: { baseURL: 'https://my-proxy.com/v1', model: 'my-model', apiKey: '...' }
});Or bring your own function — ultimate flexibility:
const form = await FormToChat.from('#signup', {
ai: async (messages) => {
const resp = await myAnthropicClient.messages.create({
model: 'claude-sonnet-4-20250514',
messages,
});
return resp.content[0].text;
}
});Built-in providers: openai, gemini, groq, together, ollama.
API
FormToChat.from(source, options?)
Creates a form instance from any source.
// CSS selector
const form = await FormToChat.from('#signup');
// HTMLFormElement
const form = await FormToChat.from(document.querySelector('form'));
// Raw HTML string (works in Node with linkedom)
const form = await FormToChat.from('<form><input name="q" required /></form>');Properties (read-only, return frozen copies)
| Property | Type | Description |
|---|---|---|
| .fields | readonly FormField[] | All parsed form fields (frozen) |
| .answers | Readonly<Record<string, string>> | Collected answers (frozen copy) |
| .current | FormField \| null | Field currently being asked |
| .pending | readonly FormField[] | Fields not yet answered (frozen) |
| .progress | Readonly<{ current, total, percent }> | Numeric progress |
| .done | boolean | Whether all fields are answered |
| .schema | FormSchema | Full parsed schema |
Methods
| Method | Returns | Description |
|---|---|---|
| .fill(data) | Record<string, string> | Fill all fields in one shot. |
| .answer(text) | Snapshot | Answer current question. Returns frozen state. |
| .snapshot() | Snapshot | Take a frozen snapshot of current state. |
| .reset() | this | Start over from the beginning. |
| for await (step of form) | Step | Iterate through steps as an async generator. |
Snapshot (frozen, immutable)
Every .answer() and step.answer() returns a Snapshot — a frozen object you can hold, compare, or log without worrying about mutation:
interface Snapshot {
readonly answers: Readonly<Record<string, string>>;
readonly current: FormField | null;
readonly done: boolean;
readonly pending: readonly FormField[];
readonly progress: { readonly current: number; readonly total: number; readonly percent: number };
}Step object (from iterator)
| Property | Type | Description |
|---|---|---|
| step.field | FormField | The field being asked |
| step.question | string | The generated question |
| step.index | number | Step index (0-based) |
| step.total | number | Total steps |
| step.answer(value) | Promise<Snapshot> | Submit answer, get snapshot |
| step.skip() | Promise<Snapshot> | Skip optional field, get snapshot |
Agent Protocol (Web Component)
When using the <form-chat> web component, agents can access the same API:
const chat = document.querySelector('form-chat');
// One-shot fill
await chat.agent.fill({ name: 'Jane', email: '[email protected]', plan: 'pro' });
// Or step-by-step
chat.agent.fields; // all fields
chat.agent.current; // field being asked
chat.agent.answers; // collected so far
chat.agent.pending; // remaining fields
chat.agent.done; // complete?
await chat.agent.answer('Jane Doe');
await chat.agent.reset();Global discovery
// Find all <form-chat> elements on the page
const forms = FormToChat.discover();
// → [{ id, element, agent }]Data attributes (DOM-only fallback)
For agents that can't execute JS:
| Attribute | Description |
|---|---|
| data-f2c-state | Engine state: idle, asking, done |
| data-f2c-progress | Step progress like 1/4 |
| data-f2c-schema | Full parsed schema (JSON) |
| data-f2c-current-field | Current field metadata (JSON) |
| data-f2c-collected | Answers collected so far (JSON) |
Tool Definition Export
Generate an OpenAI/Gemini-compatible tool (function-calling) definition from any form. This lets an LLM fill the form in a single invocation via tool-use:
import { FormToChat } from 'form-to-chat';
const form = await FormToChat.from('#signup');
const tool = form.toToolDefinition();
// Send the tool to your LLM
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Sign me up as Jane Doe, [email protected], pro plan' }],
tools: [tool],
});
// If the model calls the tool, fill the form with the returned args
if (response.choices[0].message.tool_calls) {
const args = JSON.parse(response.choices[0].message.tool_calls[0].function.arguments);
await form.fill(args);
}Also available on the web component and as a standalone function:
// Web component
const chat = document.querySelector('form-chat');
const tool = chat.agent.toToolDefinition('register_user', 'Register a new user account');
// Standalone (works with raw schemas)
import { schemaToToolDefinition, parseForm } from 'form-to-chat';
const schema = parseForm('<form>...</form>');
const tool = schemaToToolDefinition(schema);The generated definition maps field types to JSON Schema types:
| Field type | JSON Schema type | Extras |
|---|---|---|
| text, email, url, tel, date, ... | string | Format hints in description |
| number, range | number | minimum, maximum |
| checkbox | boolean | -- |
| select, radio | string | enum from options |
Validation constraints (minLength, maxLength, pattern) are included as JSON Schema properties.
DOM Events
The <form-chat> element dispatches standard CustomEvents that bubble and cross Shadow DOM boundaries. Use these for event-driven automation:
const chat = document.querySelector('form-chat');
chat.addEventListener('f2c-question', (e) => {
const { step, stepIndex, totalSteps } = e.detail;
console.log(`Step ${stepIndex + 1}/${totalSteps}: ${step.question}`);
console.log('Field:', step.field.name, step.field.type);
});
chat.addEventListener('f2c-step-complete', (e) => {
const { step, stepIndex } = e.detail;
console.log(`Answered ${step.field.name}: ${step.answer}`);
});
chat.addEventListener('f2c-complete', (e) => {
console.log('All answers:', e.detail.data);
});
chat.addEventListener('f2c-validation-error', (e) => {
console.warn(`Validation failed: ${e.detail.error}`);
});| Event | detail shape |
|---|---|
| f2c-greeting | { message } |
| f2c-question | { step: { field, question }, stepIndex, totalSteps } |
| f2c-step-complete | { step: { field, question, answer }, stepIndex } |
| f2c-validation-error | { step: { field, question }, stepIndex, error } |
| f2c-complete | { data } |
Accessible Form Sync
When the <form-chat> widget is mounted, the original HTML form is not removed from the page. Instead, it is visually hidden using an accessible technique (clip: rect(0,0,0,0)) that keeps it in the accessibility tree and reachable by browser automation tools (Puppeteer, Playwright, Selenium).
Real-time sync: As the user answers each question in the chat, the corresponding input in the original form is updated immediately, with standard input and change events fired. Framework bindings (React, Vue, Angular) that listen to these events will react automatically.
Two-way binding: If an external script (e.g. a browser automation agent) fills a field on the original hidden form, the chat engine detects the change and auto-advances to the next question.
// An automation script can fill the original form directly:
const emailInput = document.querySelector('#signup input[name="email"]');
emailInput.value = '[email protected]';
emailInput.dispatchEvent(new Event('change', { bubbles: true }));
// The chat widget detects this and auto-submits the answerSupported Input Types
All HTML5 input types are handled with type-specific validation and natural questions:
| Type | Validation | Question example |
|---|---|---|
| text | length, pattern | "What's your name?" |
| email | RFC 5322 | "What's your email?" |
| number | min/max | "Age? (between 18 and 99)" |
| tel | phone format | "What's your phone number?" |
| url | http/https | "What's your website URL?" |
| password | length | "Please enter your password." |
| date | ISO 8601, min/max | "What date? (YYYY-MM-DD)" |
| datetime-local | ISO, bounds | "What date and time? (YYYY-MM-DDTHH:MM)" |
| time | 24h format, bounds | "What time? (HH:MM)" |
| month | YYYY-MM | "Which month? (YYYY-MM)" |
| week | YYYY-W## | "Which week? (YYYY-W24)" |
| color | hex code | "What color? (#ff6600)" |
| range | min/max/step | "What value? (0–100)" |
| search | free text | "What would you like to search for?" |
| select | option matching | "Which plan? Options: Free, Pro" |
| radio | option matching | Clickable chips |
| checkbox | yes/no variants | "Would you like to subscribe? (yes/no)" |
| textarea | length | "Please share your message." |
| datalist | suggestion parsing | Options parsed as suggestions |
Theming
Override CSS custom properties on the <form-chat> element:
form-chat {
--f2c-primary: #6366f1;
--f2c-bg: #0c0c1d;
--f2c-surface: #13132b;
--f2c-text: #e2e8f0;
--f2c-bot-bubble: #1a1a3e;
--f2c-user-bubble: #6366f1;
--f2c-radius: 18px;
--f2c-font: 'Inter', system-ui, sans-serif;
}Chrome Extension
The extensions/ directory contains a standalone Chrome extension that works on any webpage. No setup required -- it detects forms automatically.
Install (developer mode)
- Open
chrome://extensions/ - Enable Developer mode
- Click Load unpacked and select the
extensions/folder
How it works
- Scans every page for
<form>elements with 2 or more input fields - Adds a small chat icon to the top-right of each form
- Click the icon to open a floating, draggable chat panel
- The panel adapts its color scheme to match the page (light or dark)
- Answers are validated in real time (email format, required fields, option matching)
- Previously-entered values are saved and suggested on future visits
- Browser autofill works for standard fields (name, email, phone)
- Press Escape or click the X to close and restore the original form
Features
| Feature | Details | |---|---| | Form detection | 2+ visible named inputs, plus MutationObserver for SPAs | | Adaptive theme | Reads page background color, derives a matching palette | | Autofill | Browser native autocomplete + extension storage suggestions | | Validation | Email, URL, phone, number, select options, required fields | | Draggable | Grab the header bar to move the panel anywhere | | Keyboard | Escape to close, Enter to submit |
Legacy API
The building blocks are still exported for advanced use:
import { parseForm, ConversationEngine, validateField } from 'form-to-chat';
const schema = parseForm('#signup');
const engine = new ConversationEngine(schema);
engine.on('question', (e) => console.log(e.step.question));
await engine.start();
await engine.answer('Jane Doe');License
MIT
