# Talkform — Typeform backend, AI voice-interview frontend

Everything lives in `talkform/` (see `talkform/README.md` for the full docs).

## What was built

- **Backend identical in shape to Typeform.** Forms are stored and served with Typeform's Create API
  schema (fields with `id`/`ref`/`type`/`properties`/`validations`, welcome/thank-you screens,
  settings, hidden fields). Responses use Typeform's Responses API shape (`landing_id`, `token`,
  `response_id`, `landed_at`, `submitted_at`, `metadata`, `hidden`, `answers[]` with the exact
  answer objects: `text`, `number`, `boolean`, `email`, `url`, `date`, `phone_number`, `choice`,
  `choices`). Endpoints: `GET /me`, `GET|POST /forms`, `GET|PUT|DELETE /forms/{id}`,
  `GET|DELETE /forms/{id}/responses` (with `page_size`/`since`/`until`/`completed`/`fields`/
  `included_response_ids`/`sort`), `PUT|GET|DELETE /forms/{id}/webhooks/{tag}` (sends Typeform's
  `form_response` payload, signed with `Typeform-Signature` when a secret is set).
  Auth matches Typeform's: `Authorization: Bearer tfp_…` personal access tokens, plus cookie
  sessions for the dashboard. 16 field types supported with real validation (required, min/max,
  max_length, choice membership, scale ranges).
- **Frontend is purely a voice interview.** `/to/<form_id>` opens a hands-free conversation:
  the AI interviewer greets the respondent, asks for each field conversationally (never reading
  out types or options mechanically), listens (mic with silence-detection turn-taking), and
  records validated structured answers through `record_answer` tool calls. It knows which fields
  are required vs optional, what's still pending, handles multi-answer utterances ("a latte,
  three cups a day, never decaf" fills three fields), normalizes spoken emails/dates/numbers,
  re-asks on validation errors, lets people skip optional questions, and only submits once every
  required field is captured. Partial answers are persisted per turn, so abandoned interviews
  show up as in-progress responses. A "Type instead" fallback exists for denied mic permission.
- **Dashboard** (`/`): signup/login, form list, builder (questions, types, options, validations,
  welcome/thank-you text), interviewer persona/voice settings, raw Typeform JSON editor, responses
  table with CSV export and delete, API token management, and API docs.
- **Auth**: scrypt-hashed passwords, HttpOnly session cookies, SHA-256-hashed API tokens, per-user
  form isolation.

## Services & API keys

Only **OpenAI** is needed (STT `gpt-4o-mini-transcribe`, interviewer `gpt-4.1`, TTS
`gpt-4o-mini-tts`). An `OPENAI_API_KEY` was already present in this environment, so I used it
instead of asking; put yours in `talkform/.env` (`cp .env.example .env`). No other third-party
service is required — auth is self-hosted, storage is SQLite (Node's built-in `node:sqlite`).

## Running it

```bash
cd talkform
npm install
cp .env.example .env   # add OPENAI_API_KEY
npm run seed           # demo@talkform.dev / demo-password-123 + a demo form
npm start              # http://localhost:3000  (HOST=0.0.0.0 to expose)
```

## Verification

`npm test` → 8/8 passing, including a **live end-to-end interview** against OpenAI: a real
TTS-generated voice clip ("Hi, I'm Grace Hopper, and my email is grace dot hopper at example dot
com") is transcribed, the interviewer records `name` and a normalized `email`, then fills
`drink`/`cups`/`decaf` from one sentence, honours a skip on the optional field, calls
`finish_interview`, and the result is retrievable as a completed Typeform-style response with the
right answer objects (`choice.label = "Latte"`, `number = 3`, `boolean = false`, …).

## Assumptions / notes

- **The sandbox I ran in forbids `listen()` on any TCP port or unix socket** (EPERM), so I could
  not leave a server running or click through the browser UI here. Instead the Express app is
  exercised in-process by `test/harness.js` (real `http.IncomingMessage`/`ServerResponse`, no
  socket), which covers every route the UI calls. The browser-side audio code (MediaRecorder,
  VAD, playback) is written for Chrome/Safari but was not exercised in a browser in this run.
- The sandbox reaches the internet only through an authenticated HTTP proxy; Node's fetch
  ignores proxy env vars, so the OpenAI client honours `HTTPS_PROXY` via undici's `ProxyAgent`.
  This is harmless when no proxy is set.
- Interview sessions use request/response turns (record → upload → reply) rather than a
  streaming realtime connection. That keeps the engine simple and auditable (every tool call
  is validated server-side); swapping in OpenAI Realtime later would only touch
  `server/interview.js` and `public/interview.js`.
- Not implemented from Typeform's surface: themes/workspaces/images endpoints, logic jumps,
  payment/file-upload fields. Forms are stored as the Typeform JSON document rather than a
  normalized field table — the API contract is what's identical, not the storage layout.
