# A web page

> A chat box on your own site, talking to an agent: the pass your server hands out, what a visitor gets, the limits, and every option.

A chat box on your own site, talking to the same agent, with the same model and
run record. A visitor is a stranger, so they get only what you name.

1. Add the channel to the agent. It names the sites that may show the box and
   the tools a visitor's turn gets:

```ts
// This agent in a chat box on the shop's own site, for the people who visit it.
// A visitor is a stranger, so their turn gets the one tool named here, the tool
// itself, and nothing else the agent has: no notes, no jobs, no picking the model.
//
// The shop's own server holds a token made for this agent and gives each
// signed-in customer a pass from /api/chat-pass, with their customer id as the
// visitor, which is how orderStatus knows whose orders to look at. The page loads the box with one tag, from
// agent.myshop.com, which the shop's proxy sends on to this runtime:
//
//   <script src="https://agent.myshop.com/api/web/chat.js" data-agent="shop" data-pass="/api/chat-pass" async></script>
import { webChannel } from "@chloejs/core/channels";

import { orderStatus } from "../tools/orderStatus.ts";

export default webChannel({
  origins: ["https://myshop.com"],
  tools: [orderStatus],
  greeting: "Hi, I can tell you where an order is. What is its number?",
  limits: { perVisitor: { messages: 20, dollars: 0.25 }, perDay: { dollars: 3 } },
});
```

2. Make a token for that agent alone. It lives on your site's server, never in
   a page, and is printed once:

```sh
npx chloe tokens make "shop site" --agent shop
```

3. Give your site one route that hands its page a pass, and put the box on the
   page. This is a whole site that does both, in plain Node. Who the visitor is
   (here, a signed-in customer's id) comes from your site's own sign-in, never
   from anything the visitor can write, because it decides what the agent's
   tools show them:

```ts
// The shop's own web server: one page with the chat box on it, and the one route
// that hands that page a pass. The token lives here and never reaches a page.
//
//   npx chloe tokens make "shop site" --agent shop    prints the token, once
//   CHLOE_TOKEN=chloe_... node server.ts
//
// Two addresses, because two different things reach the runtime. The page loads
// the box from BOX, which is public (the shop's proxy passes the web routes to
// the runtime and nothing else), and this server asks for passes at CHLOE,
// which is the runtime itself. On one machine, both are http://127.0.0.1:3067,
// and the channel's `origins` has to include http://localhost:8080.
import { randomUUID } from "node:crypto";
import { createServer, type IncomingMessage } from "node:http";

const BOX = "https://agent.myshop.com";
const CHLOE = "http://127.0.0.1:3067";
const AGENT = "shop";

const PAGE = `<!doctype html>
<meta charset="utf-8">
<title>My shop</title>
<h1>My shop</h1>
<script src="${BOX}/api/web/chat.js" data-agent="${AGENT}" data-pass="/api/chat-pass" async></script>
`;

/**
 * The customer id of whoever sent this, from the shop's own sign-in, or nothing
 * for somebody not signed in. The shop's sessions go here. It is the only place
 * a customer id may come from: the agent's order tool shows that customer's
 * orders to whoever holds a pass made for them.
 */
function customerOf(request: IncomingMessage): string | undefined {
  return undefined;
}

/** An id for somebody not signed in, which can never be a customer's. */
const ANONYMOUS = /^anon-[0-9a-f-]{36}$/;

createServer(async (request, response) => {
  if (request.method === "POST" && request.url === "/api/chat-pass") {
    // A signed-in customer is their customer id. Anybody else gets a random id
    // in a cookie, so they find the same conversation tomorrow, and a cookie is
    // believed only when it holds one of those: whoever writes a customer's id
    // into it gets a new anonymous one instead, never that customer's orders.
    const kept = /(?:^|; )visitor=([^;]+)/.exec(request.headers.cookie ?? "")?.[1] ?? "";
    const visitor = customerOf(request) ?? (ANONYMOUS.test(kept) ? kept : `anon-${randomUUID()}`);
    const asked = await fetch(`${CHLOE}/api/agents/${AGENT}/web/pass`, {
      method: "POST",
      headers: { authorization: `Bearer ${process.env.CHLOE_TOKEN}`, "content-type": "application/json" },
      // Whatever the agent should know about them: their name, their plan.
      body: JSON.stringify({ visitor, facts: {} }),
    });
    // Handed on as it came: the pass, when it runs out, and the agent's greeting.
    response.writeHead(asked.ok ? 200 : 502, {
      "content-type": "application/json",
      ...(ANONYMOUS.test(visitor) && { "set-cookie": `visitor=${visitor}; HttpOnly; SameSite=Lax; Path=/; Max-Age=31536000` }),
    });
    return void response.end(await asked.text());
  }
  response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(PAGE);
}).listen(8080, () => console.log("http://localhost:8080"));
```

   In any framework it is the same route: `POST /api/agents/<id>/web/pass`
   with the token and `{ "visitor": "...", "facts": {...} }`. `facts` is
   whatever the agent should know about them: their name, their plan.

   To try it on one machine, set both addresses in it to
   `http://127.0.0.1:3067`, add `http://localhost:8080` to `origins`, run
   `npx chloe`, and open http://localhost:8080.

| Option | Default | What it controls |
|---|---|---|
| `origins` | required | The sites that may show the box, like `"https://myshop.com"`. |
| `tools` | none | The only tools a visitor's turn has. No memory, skills or self tools unless named, and naming the memory or self tools is refused. |
| `job` | none | A job every visitor's message goes to, instead of a turn. |
| `greeting` | none | What the box shows before anybody has written. |
| `limits` | 30 messages and $0.50 per visitor, $5 for everybody, per 24 hours | `{ perVisitor: { messages, dollars }, perDay: { dollars } }`. A turn past one is refused politely. |
| `model` | the agent's | The model visitors' turns use. |
| `pictures` | off | Whether a visitor may send pictures. |
| `chatHistory` | `{ messages: 10 }` | As on [every channel](https://chloejs.org/docs/channels#options-they-share). |

Besides `limits`, a visitor may send six messages a minute, of up to 4,000
characters each. A visitor never gets a `/command` but `/clear`, a model pick
or a sign-in.

**Reaching chloe.** It stays on loopback. Your own web server gives it an
address, here agent.myshop.com, and passes the box's routes and nothing else.
With Caddy:

```
agent.myshop.com {
    @web path_regexp ^/api/(agents/shop/web/(turn|history|clear)|web/(chat|client)\.js)$
    handle @web {
        reverse_proxy 127.0.0.1:3067 { flush_interval -1 }
    }
    respond 404
}
```

The pass route is not in it: your site's server reaches chloe directly.

**The box.** It sits in a corner, shows a tool's `title` while the agent works,
and writes the answer as it arrives. It keeps the conversation for the next
visit. Set `--chloe-accent`, `--chloe-font` and `--chloe-radius` on the page to
match your site, and `data-title`, `data-note`, `data-position="left"` and
`data-open` on the tag. For a look of your own, build on the client served
beside it:

```js
import { chloeChat } from "https://agent.myshop.com/api/web/client.js";

const chat = chloeChat({
  url: "https://agent.myshop.com",
  agent: "shop",
  pass: () => fetch("/api/chat-pass", { method: "POST" }).then((answer) => answer.json()),
});
const { messages } = await chat.history();
const { text } = await chat.send("Where is my order?", {
  onText: (soFar) => show(soFar),
  onStep: ({ text }) => status(text),
});
```

**What the agent knows about a visitor.** Each message reaches the model with
their id, the site, when they first came, how many messages before, their
country and browser, and your `facts`, never their IP address. With
`features: { memoryPerUser: true }` it keeps a note on each visitor.
`GET /api/agents/<id>/web/visitors` lists them.

**What you see.** Each visitor's conversation is in the agent's chat list,
marked web, and each turn is a run with its cost. A job asks a visitor with
`who: "web:<visitor>"`, and the question waits for the next time the box loads.

A pass is one agent, one site, one visitor, for an hour. Passes are signed with
`web-pass.key` in the state folder: delete it and restart, and every pass stops
working.
