# AI Chat Widget — multi-tenant SaaS (OpenAI + MySQL)

A hostable **SaaS** chat-widget platform. Any website owner signs up, gets a
one-line embed snippet, uploads a knowledge base (PDFs + a website crawl), and
their visitors get an AI chat bubble that answers from it — with live human
handoff. A drop-in **Tawk.to replacement** you can offer as a service.

```
 signup ─► tenant (own OpenAI vector store + widget key)
                 │
 PDFs / crawl ──►  vector store ──► /api/chat (Responses API + file_search) ──► widget.js (per-tenant)
                 │
 conversations + messages ──► MySQL ──► admin inbox (view history, take over live)
```

Each **tenant** (customer site) is fully isolated: its own vector store, widget
key, admin login, branding, conversations, and messages.

## Setup

```bash
npm install
cp .env.example .env        # set OPENAI_API_KEY + the DB_* MySQL settings
npm start                   # creates the DB/tables automatically on first run
# http://localhost:8000/admin        dashboard (sign up / log in)
# http://localhost:8000/widget.js    the embeddable script
```

Requires **MySQL** (any 5.7+/8.x). The server creates the database and tables on
startup — just point `DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME` at your server.

## Roles

- **Platform admin** (you) — logs in at `/superadmin` (env `SUPERADMIN_EMAIL` /
  `SUPERADMIN_PASSWORD`). Creates, edits, **pauses/resumes**, and deletes tenants.
  There is **no self-signup** — every tenant is created here.
- **Tenant** (a customer company) — logs in at `/admin` with credentials the
  platform admin gave them. Manages their knowledge base, conversations, install
  snippet, and settings.

## How a tenant is onboarded

1. Platform admin → `/superadmin` → **New tenant** (name, site, admin email,
   password, optional OpenAI key). A widget key is provisioned.
2. Tenant logs in at `/admin` and, under **Settings**, can set their **own
   OpenAI API key, SMTP server, notification email**, plus branding and the
   system prompt. (If they don't set a key/SMTP, the platform defaults are used.)
3. **Knowledge base** → upload PDFs and/or **crawl the website** (the vector
   store is created on first use, with the tenant's OpenAI key).
4. **Install** → copy the one-line snippet:
   ```html
   <script src="https://your-app.com/widget.js" data-tenant="wgt_xxxxx"></script>
   ```
5. **Conversations** → watch chats live and take over from the AI anytime.

A **paused** tenant's widget is disabled (verification and chat return
"unavailable") until the platform admin reactivates it.

## Platform admin (manage all tenants)

Set `SUPERADMIN_EMAIL` / `SUPERADMIN_PASSWORD` in `.env`, then log in at
**`/superadmin`** to see every tenant (name, site, chat count, widget key) and
delete any of them (which also removes their vector store and conversations).
Leave the vars blank to disable the console entirely.

## Password reset

On the dashboard login, **Forgot password?** emails a 1-hour reset link
(`/reset?token=…`). The link is single-use — it stops working once the password
changes. If SMTP is down and `EXPOSE_CODE_ON_FAILURE=true`, the link is logged to
the browser/server console for testing.

## Embed (per tenant)

The only line a customer adds to their site — `data-tenant` ties it to their
account (branding + knowledge base come from the server):

```html
<script src="http://localhost:8000/widget.js" data-tenant="YOUR_WIDGET_KEY"></script>
```

Optional attribute overrides:

```html
<script src="https://your-server.com/widget.js"
        data-title="Support"
        data-color="#0ea5e9"></script>
```

`data-api` lets the widget live on a different host than the API
(defaults to the script's own origin).

## Configuration (`.env`)

| Variable | Purpose |
|---|---|
| `OPENAI_API_KEY` | Your OpenAI key |
| `OPENAI_MODEL` | Answer model (default `gpt-4o-mini`) |
| `VECTOR_STORE_ID` | Set by `npm run ingest` |
| `SYSTEM_PROMPT` | Tone + guardrails for answers |
| `ASSISTANT_NAME` / `WIDGET_GREETING` | Widget branding |
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | Login for the `/admin` page. **Change these.** |
| `SECRET_KEY` | Signs the admin session cookie (random string in production) |
| `ALLOWED_ORIGINS` | CORS allow-list (comma-separated). Restrict in production |
| `PORT` | Server port (default 8000) |

## API

`POST /api/chat`
```json
{ "message": "How do I reset my password?", "previousResponseId": "resp_..." }
```
→ `{ "reply": "...", "responseId": "resp_..." }`

Send the returned `responseId` back on the next turn to keep conversation
context (OpenAI persists the thread — the client stays stateless).

`GET /api/health` → active model + whether the knowledge base is ready.

## Knowledge base: PDFs + website crawl

The assistant answers from whatever is in the knowledge base. Two sources:

- **PDFs** — upload at `/admin` or `npm run ingest`.
- **Your website** — in `/admin` → *Knowledge base* → *Crawl your website*, enter a
  URL and a page limit. The crawler fetches same-domain pages, extracts their
  text, and adds it so the widget can answer questions about the site it's on.
  Crawled content shows up as a `WEB` document you can delete anytime.

## Live human handoff

Every chat is stored, so a human can jump in:

- The widget has a **"talk to a person"** button (and detects phrases like
  "I want to speak to someone"). Requesting a human flags the conversation and
  emails `ADMIN_EMAIL`.
- In `/admin` → *Conversations*, you see every chat with full history. Open one
  and click **Take over** (or just start typing) to switch it from AI to live
  mode — the visitor now chats with you, not the AI.
- **Hand back to AI** returns control to the assistant; **Close** ends the chat.
- The visitor's widget updates within a couple of seconds (polling) — showing a
  "Connected to a team member" banner and your replies.

Conversations persist to `data/conversations.json` (single-instance; use a DB to
scale horizontally).

## Production notes
- **Change `ADMIN_USERNAME` / `ADMIN_PASSWORD`** and set a random `SECRET_KEY`.
- Email verification requires working SMTP — if it fails, visitors can't start a
  chat. Test with `npm run mail:test`.
- Set `COOKIE_SECURE=true` when serving the admin page over HTTPS.
- Set `ALLOWED_ORIGINS` to your real domain(s); never leave `*`.
- Serve over HTTPS behind a reverse proxy.
- The `/api/chat` endpoint is rate-limited to 20 msgs/min/IP — tune in `server.js`.
- Scanned/image-only PDFs need OCR before ingest (File Search won't read them).
