# Install AI Autocomplete (Vanilla JS) — agent instructions

You are an AI coding agent installing **AI Autocomplete** into the user's web app using the framework-agnostic vanilla JS SDK. Follow these steps in order. Do not skip the "Hand off to the user" section — those steps are not yours to perform.

Reference docs: https://ai-autocomplete.com/docs/vanilla/getting-started

AI Autocomplete uses two key types:

- **Public keys** (`pk_v1_...`) — safe to ship in client bundles. Sent as a Bearer token directly from the browser. Use this for the basic install path below.
- **Secret keys** (`sk_v1_...`) — server-only. Your server exchanges a secret key for a short-lived access token (`at_v1_...`) that the SDK consumes. Recommended for production. See the "Production: secret key + access token" section at the end.

---

## Step 0 — Read the SDK README

Before any code changes, run:

```
npm view @magicx-eng/ai-autocomplete-vanilla readme
```

Read the output end-to-end. It documents the full options surface, both auth modes, the CSS variables, and the stable `data-aia-*` selector hooks — you'll need it in Steps 2 and 5. If you can't run shell commands, fetch `https://unpkg.com/@magicx-eng/ai-autocomplete-vanilla/README.md` instead — same content.

## Step 1 — Ask the user which integration tier

The SDK ships three tiers. **Stop, ask the user, and wait for their answer.** Do not pick on their behalf.

> "AI Autocomplete ships three integration tiers — which do you want?
>
> **Tier 1 — Full component (`renderMode: "full"`, the default)**
> Drop-in. The SDK creates and owns the textarea, pills, dropdown, and all state inside a container element you provide. This is the right choice for most apps.
>
> **Tier 2 — Dropdown only (`renderMode: "dropdown"`)**
> You own the input element. The SDK renders only the dropdown (with pills inside) into a container you provide. You forward keystrokes / focus events to the SDK. Pick this if you have a custom input — a rich-text editor, a styled search bar, an existing form field — that you want to keep.
>
> **Tier 3 — Headless (`renderMode: "headless"`)**
> No DOM. The SDK gives you state, actions, and a `subscribe()` callback. You render every pixel yourself. Pick this only if you're building a framework wrapper or need total control over rendering — most apps don't need this.
>
> Which one — full, dropdown-only, or headless?"

If the user says "you decide" or seems unsure, recommend Tier 1. Tier 3 is rarely the right answer for application code — if they're considering it because they want a custom input, Tier 2 is almost always what they actually want.

## Step 2 — Install and wire up the SDK

Sanity-check the project: there should be a `package.json` at the root and the project should already have a bundler set up (Vite, Webpack, esbuild, etc.) — this SDK is distributed as an npm package and is consumed via `import`. If there's no bundler, ask the user where their app lives or to set one up first.

Install the SDK with the project's package manager:

- pnpm: `pnpm add @magicx-eng/ai-autocomplete-vanilla`
- yarn: `yarn add @magicx-eng/ai-autocomplete-vanilla`
- npm: `npm install @magicx-eng/ai-autocomplete-vanilla`
- bun: `bun add @magicx-eng/ai-autocomplete-vanilla`

Then add the integration code for the tier the user picked. **Hardcode the public key as the literal string `"PASTE_PUBLIC_KEY_HERE"`** — leave that placeholder in the code. The user will replace it after Step 3. Do **not** invent a key, and do **not** create a `.env`/`.env.local` entry for it. Once the install is verified end-to-end it's the user's call how to manage the key long-term.

If you don't know where to mount the widget, ask the user before guessing.

**Tier 2 / Tier 3 rule: always call `ac.reset()` from your own submit handler.** Tier 1 (full component) handles this automatically. In Tier 2 and Tier 3 the consumer owns the input element and the submit flow — don't pass `onSubmit` to `new AIAutocomplete(...)`. Wire up submit however you like (a button click, a form submit, your own Enter handler) and call `ac.reset()` at the end of that handler. It clears the input and rotates the per-session `session_id` so the server's history stays accurate; skipping it degrades suggestion quality on subsequent submissions.

### Tier 1 — Full component

Add a container element to the user's HTML where the widget should appear:

```html
<div id="autocomplete-root"></div>
```

Then mount the SDK:

```ts
import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";

const root = document.getElementById("autocomplete-root");
if (!root) throw new Error("Missing #autocomplete-root container");

const ac = new AIAutocomplete(root, {
  apiConfig: {
    apiKey: "PASTE_PUBLIC_KEY_HERE",
  },
  mode: "dark",
  onSubmit: (result) => {
    console.log(result.query);
    console.log(result.completed_params);
  },
});
```

The `mode` option accepts `"light"` or `"dark"` and defaults to `"dark"`. Match the surrounding page — light theme → `mode: "light"`; dark or unsure → leave it as `"dark"` (or omit).

### Tier 2 — Dropdown only

The user provides their own input element; you wire its events into the SDK and provide a container for the dropdown:

```ts
import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";

const input = document.getElementById("my-input") as HTMLInputElement;
const dropdownContainer = document.getElementById("my-dropdown");
const submitButton = document.getElementById("my-submit");
if (!input || !dropdownContainer || !submitButton) {
  throw new Error("Missing input, dropdown container, or submit button");
}

const ac = new AIAutocomplete(dropdownContainer, {
  renderMode: "dropdown",
  apiConfig: { apiKey: "PASTE_PUBLIC_KEY_HERE" },
  onChange: (text) => {
    if (input.value !== text) input.value = text;
  },
});

const handleSubmit = () => {
  console.log(input.value);
  ac.reset();
};

input.addEventListener("input", () => ac.handleTextChange(input.value));
input.addEventListener("keydown", (e) => ac.handleKeyDown(e));
input.addEventListener("focus", () => ac.setFocused(true));
input.addEventListener("blur", () => ac.setFocused(false));
submitButton.addEventListener("click", handleSubmit);
```

The focus wiring (`setFocused`) is required — with `dropdownTrigger: "auto"` (the default) the dropdown only opens while the input is focused, and the SDK has no way to know that without you telling it.

### Tier 3 — Headless

The SDK exposes state, actions, and a `subscribe()` callback; you render everything yourself.

```ts
import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";

const ac = new AIAutocomplete(document.createElement("div"), {
  renderMode: "headless",
  apiConfig: { apiKey: "PASTE_PUBLIC_KEY_HERE" },
});

const handleSubmit = () => {
  const state = ac.getState();
  console.log(state.text, state.completedParams);
  ac.reset();
};

const unsubscribe = ac.subscribe((state) => {
  // render input, pills, dropdown, segments using state
});

// forward user events into the SDK
myInput.addEventListener("input", () => ac.handleTextChange(myInput.value));
myInput.addEventListener("keydown", (e) => ac.handleKeyDown(e));
myInput.addEventListener("focus", () => ac.setFocused(true));
myInput.addEventListener("blur", () => ac.setFocused(false));
mySubmitButton.addEventListener("click", handleSubmit);
```

The full `CoreState` shape (text, completedParams, suggestions, segments, filteredOptions, isDropdownOpen, activeDropdownIndex, isLoading, isReady, error, …) and the action methods (`selectOption`, `setActiveDropdownIndex`, `setActivePill`, `removeLastParam`, `reset`, `destroy`, …) are documented in the SDK README you read in Step 0. Confirm with the user how they want pills, segments, and the dropdown rendered before committing to a layout — Tier 3 is open-ended by design.

## Step 3 — Hand off to the user (you cannot do this)

After your code changes are complete, **stop** and print the following to the user verbatim — including the bullets and URLs. These are browser tasks; the agent cannot perform them.

> **Two browser steps you need to do before this works:**
>
> 1. Go to **https://ai-autocomplete.com/edit** and create your product. This configures what your autocomplete will suggest (the prompts, parameter types, and option lists).
> 2. Go to **https://ai-autocomplete.com/account/keys**, create a **public key**, copy it, and paste it in place of `PASTE_PUBLIC_KEY_HERE` in the code. The public key starts with `pk_v1_`.
>
> Once both are done, reload the page (or restart the dev server if hot reload didn't pick it up). Reply when you've confirmed it works (or share what's failing).

Do not pretend you can open URLs, create the product, or generate the key on the user's behalf. Wait for their reply before moving to Step 4.

## Step 4 — Verify it works

Once the user replies that they've pasted the key, walk them through verification:

1. Reload the page (or restart the dev server).
2. Confirm the autocomplete renders inside the container without console errors.
3. If the network tab shows **401 Unauthorized** on `/api/suggest`, the key is wrong or it isn't a public key — ask the user to re-copy it from `/account/keys` and confirm it starts with `pk_v1_`.
4. If you see **404** or "no suggestions returned", the product likely wasn't created yet — ask the user to confirm they finished Step 3.1.
5. **Tier 2/3 only:** if the dropdown never opens, confirm that the input's `focus`/`blur` listeners are calling `ac.setFocused(true)` / `ac.setFocused(false)` — without those, `dropdownTrigger: "auto"` will never trigger.

Wait for an explicit "yes, it works" before moving on.

## Step 5 — Offer to customize the styling

Once the user confirms the autocomplete works, ask:

> "Want to customize the look? The SDK exposes CSS variables and stable `data-aia-*` selector hooks for everything from pill colors to the submit button. I can apply changes against your existing palette/theme — tell me what you want different (e.g. 'match my brand primary', 'tighten the pill padding', 'switch to a solid dropdown'), or say 'looks fine' to wrap up."

If the user wants changes, refer to the **CSS Variables** and **Selector Hooks** tables in the SDK README you read in Step 0.

Apply overrides via a class on the container element you passed to `new AIAutocomplete(...)`. All defaults use `:where()` (zero specificity) so any class you write wins. For per-mode overrides, target `[data-mode="light"]` / `[data-mode="dark"]` on the same element.

For changes the variables don't cover, target the `data-aia-*` selector hooks (e.g. `[data-aia-dropdown]`, `[data-aia-pill]`, `[data-aia-textarea]`) — those are the stable public surface. Don't target hashed CSS-module class names.

In Tier 3 (headless) you own the DOM, so most styling is just your own CSS — the variables and selector hooks only apply to the dropdown DOM the SDK still renders.

If the user says "looks fine" or doesn't want changes, you're done. Mention briefly that for production they may want to switch to the access-token flow below, but don't build it unless asked.

---

## Production: secret key + access token (recommended)

Public keys work, but for production browser apps we recommend the access-token flow:

- The **secret key** (`sk_v1_...`) lives **only on the user's backend server** — never in the browser bundle, never in client-side env vars (no `VITE_` prefix), never accessible from frontend code.
- The user's **backend** exchanges the secret key for a short-lived **access token** (`at_v1_...`) by calling AI Autocomplete.
- The **frontend** SDK fetches that access token from a route on the user's *own* backend and uses it. The browser never touches the secret key.

Only build this once the basic install above is verified, and confirm with the user before adding server code. **If the project has no backend (e.g. a static site or pure SPA with no server runtime), STOP and tell the user — you cannot do the secret-key flow without a server. Stay on public keys.**

### A. Add server-side env vars

These live in the user's **backend** environment (host shell, Docker secrets, PaaS env config — wherever the server runs). They must NOT have a `VITE_` prefix (or any other client-side prefix) — that would expose them to the browser.

```
MAGICX_SECRET_KEY=sk_v1_...
MAGICX_PRODUCT_ID=<the product UUID from /edit>
```

The product UUID is visible in the URL of the product editor at `/edit`. Ask the user to copy it.

### B. Add the token-exchange route on the backend

> **This code runs on the user's backend server, not in the browser.** The secret key is read from server-side `process.env` and used in an outbound `fetch` from the server. The browser never sees it.

The backend hits `POST https://api.ai-autocomplete.com/api/auth/token` with the secret key as a Bearer token and the `product_id` in the body. The endpoint returns `{ access_token, expires_at, token_type }`. Forward that response to the browser unchanged.

Generic Node example — `server/ac-token.ts`:

```ts
// SERVER-ONLY
export async function getAccessToken() {
  const res = await fetch("https://api.ai-autocomplete.com/api/auth/token", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MAGICX_SECRET_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ product_id: process.env.MAGICX_PRODUCT_ID }),
  });

  if (!res.ok) {
    // 401 with error.code === "INVALID_SECRET_KEY" means a config problem
    // on the user's end — don't surface it to the browser as 401.
    throw new Error("Token exchange failed");
  }

  // { access_token, expires_at, token_type }
  return res.json();
}
```

Wire this into whatever HTTP framework the user runs (Express, Fastify, Hono, serverless handler) under a path like `/api/ac-token` — always on the **backend** side of the codebase.

### C. Switch the SDK to access-token mode (browser-side)

> **This code runs in the browser.** It contains no secret key — only the path of the user's own backend route from Step B.

```ts
// BROWSER
new AIAutocomplete(container, {
  apiConfig: {
    type: "accessToken",
    getAccessToken: async () => {
      const res = await fetch("/api/ac-token");
      const { access_token, expires_at } = await res.json();
      return { accessToken: access_token, expiresAt: expires_at };
    },
  },
  onSubmit: handleSubmit,
});
```

### D. Remove the public key

Once the access-token flow is verified, drop the hardcoded public key (or whatever the user moved it into) from your code — you don't need it anymore.

Full docs: https://ai-autocomplete.com/docs/vanilla/authentication
