npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

form-to-chat

v0.1.0

Published

Convert any HTML form into a conversational chat interface with optional LLM integration

Readme

form-to-chat

Turn any HTML <form> into a friendly conversation. Works for people, bots, and everything in between.

npm version license

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-chat and 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-chat

Quick 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 — immutable

Async 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 answer

Supported 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)

  1. Open chrome://extensions/
  2. Enable Developer mode
  3. 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