Files
Ben Sabic 79227ae991 docs: refresh adapter pages with hand-authored MDX (#474)
## Summary

Refreshes the adapter docs end-to-end so every adapter — official,
vendor-official, and community — now ships hand-authored MDX, lives
under a clean URL structure, and renders on a polished
sidebar/right-rail layout dedicated to `/adapters` (the shared `/docs`
chrome is untouched).

```mermaid
flowchart LR
  subgraph Before
    direction TB
    OB[official] --> CB[community<br/>incl. 5 vendor pages]
  end
  subgraph After
    direction TB
    OA[official] --> VA[vendor-official<br/>5 pages] --> CA[community]
  end
  Before -.-> After
```

### Content & routing

- **New `/adapters/vendor-official/<slug>` route** for vendor-maintained
adapters (Beeper Matrix, Photon iMessage, Liveblocks, Resend, Zernio).
Sidebar gets a third labelled group ("Vendor-Official Adapters") between
Official and Community, with a top divider matching the existing
Community treatment.
- **All 13 vendor-official + community adapters migrated** from runtime
README fetching to hand-authored MDX with rich `features:` matrices and
full body content (install, quick start, configuration, auth,
gateway/streaming, troubleshooting). README fetch stays as a fallback
for any future community adapter that hasn't been migrated yet, gated by
a new `mdxBody: true` frontmatter flag.
- **Messenger filter pages removed** (`/adapters/for/<messenger>` + the
"Browse by messenger" chip row on `/adapters`). Existing URLs
308-redirect to `/adapters`.
- **Permanent redirects** from
`/adapters/community/{matrix,imessage,resend,zernio,liveblocks}` to
their new `/adapters/vendor-official/...` paths.
- **Fixed** `/docs/adapters` and `/docs/state` so the bare pages are
accessible again — the previous catch-all redirect (`:slug*`) was
swallowing them. Switched to `:slug+` so subpath URLs still 308 while
the bare pages render.

### Visual polish

- **Adapter-only sidebar variant** (`AdaptersDocsLayout` +
`AdaptersSidebar`) with uppercase eyebrow separators, tighter rows, and
a thin themed scrollbar utility class. The shared `/docs` sidebar is
untouched.
- **Restyled `AdapterHero`**: drops the badges row + packageName, sits
the title inline with the logo, larger 17 px tagline, horizontal divider
beneath the block.
- **Restyled `PackageInstall`** as a tabbed dark single-line snippet
with a `$` prompt prefix and a copy button — replaces the previous
multi-line `CodeBlock` layout.
- **New "Deploy your chat app on Vercel" upsell card** (`<Upsell />`)
replaces the old `EditSource / ScrollTop / Feedback / CopyPage` footer
cluster on every adapter detail page.
- **Listing & messenger pages**: align the H1 to a tighter `text-4xl
sm:text-[44px]`, and the section headers to `text-base font-medium
tracking-tight` with a one-line muted lede.

### Tooling & tests

- Added `mdxBody: true` opt-in to the adapter frontmatter schema
(`source.config.ts`), and updated both detail-page handlers
(`community/[slug]` and the new `vendor-official/[slug]`) to render the
MDX body when present, falling back to README fetch otherwise.
- Refactored both detail-page handlers to flatten the body-render
branches into a `renderBody()` helper, removing the nested ternaries
that were tripping `lint/style/noNestedTernary`.
- New test file
[`packages/integration-tests/src/docs-adapters.test.ts`](https://github.com/vercel/chat/blob/docs/refresh-adapters/packages/integration-tests/src/docs-adapters.test.ts)
— **220 new assertions** covering:
- Adapter MDX frontmatter completeness, slug ↔ filename consistency, and
`type ∈ {platform, state}`.
- Vendor-official invariants: exactly the expected slugs,
`vendorOfficial: true`, `community: true`, `author`, `mdxBody: true`,
`<FeatureSupport />` rendered.
- Community invariants: `community: true` (never vendor-official),
`mdxBody: true`, `<FeatureSupport />`.
- Official invariants: never flagged, `packageName` always under
`@chat-adapter/*`.
- `adapters.json` ↔ MDX sync on `packageName` / `type` / `community` /
`vendorOfficial`.
- Extended `VALID_DOC_PACKAGES` so `docs-content.test.ts` accepts the
new vendor-official + community packages, plus `@chat-adapter/web`,
`@chat-adapter/web/react`, and `@chat-adapter/messenger`.

### Per-package AGENTS.md

- Added `AGENTS.md` to every official adapter and state adapter (14
packages), each tailored to that adapter's surface — overview, directory
layout, build/test commands, public exports, thread ID format, webhook
flow, authentication, format conversion, cards/streaming, platform
quirks, testing approach, coding conventions, and release rules.
- Added a one-line `CLAUDE.md` (`@AGENTS.md`) beside each so Claude Code
picks up the same instructions through its built-in resolver — same
convention as the root.

### Web adapter copy

- Cleaned up the Web adapter tagline (removed inline backticks) and
dropped the now-redundant "v1 scope" section from the body.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-05-12 16:01:19 +10:00

238 lines
6.9 KiB
Plaintext

---
title: Web
description: Web chat adapter that speaks the AI SDK useChat protocol.
packageName: "@chat-adapter/web"
slug: web
type: platform
logo: web
tagline: Lets a Chat SDK bot serve a browser chat UI alongside Slack, Teams, Discord — the same handler fires for every platform. Speaks the AI SDK UI message stream protocol so @ai-sdk/react's useChat and ai-elements work out of the box.
beta: true
features:
postMessage: yes
editMessage: no
deleteMessage: no
fileUploads: no
streaming:
status: yes
label: Native (SSE)
scheduledMessages: no
cardFormat:
status: partial
label: Markdown only (v1)
buttons: no
linkButtons: no
selectMenus: no
tables:
status: yes
label: GFM
fields: no
imagesInCards: no
modals: no
slashCommands: no
mentions: no
addReactions: no
removeReactions: no
typingIndicator: no
directMessages: yes
ephemeralMessages: no
customApiEndpoint: yes
fetchMessages:
status: partial
label: State cache
fetchThreadInfo:
status: yes
label: Synthesized
fetchChannelMessages:
status: partial
label: State cache
listThreads: no
postChannelMessage: no
---
## Install
<PackageInstall package="@chat-adapter/web ai @ai-sdk/react" />
## Quick start
```typescript title="lib/bot.ts" lineNumbers
import { Chat } from "chat";
import { createWebAdapter } from "@chat-adapter/web";
import { createMemoryState } from "@chat-adapter/state-memory";
export const bot = new Chat({
userName: "mybot",
adapters: {
web: createWebAdapter({
userName: "mybot",
getUser: (req) => ({ id: getUserIdFromCookie(req) }),
}),
},
state: createMemoryState(),
});
bot.onDirectMessage(async (thread, message) => {
await thread.post(`You said: ${message.text}`);
});
```
```typescript title="app/api/chat/route.ts" lineNumbers
import { after } from "next/server";
import { bot } from "@/lib/bot";
export async function POST(request: Request): Promise<Response> {
return bot.webhooks.web(request, {
waitUntil: (task) => after(() => task),
});
}
```
```tsx title="app/chat/page.tsx" lineNumbers
"use client";
import { useChat } from "@chat-adapter/web/react";
export default function ChatPage() {
const { messages, sendMessage, status, stop } = useChat();
// Render with `ai-elements` (<Conversation>, <Message>, <PromptInput>)
// or your own components — `messages`, `sendMessage`, `status` are the
// standard `@ai-sdk/react` API.
}
```
## Configuration
<TypeTable
type={{
userName: {
type: "string",
description:
"Bot username. Required by Chat SDK for mention detection and to seed the bot identity for assistant messages.",
},
getUser: {
type: "(request: Request) => WebUser | null | Promise<WebUser | null>",
description:
"Resolves the user from the inbound HTTP request. Returning `null` produces HTTP 401. This is the security boundary for the Web adapter.",
},
persistMessageHistory: {
type: "boolean",
default: "true",
description:
"Persist incoming message history in the configured state adapter. Set to `false` only if your handler re-derives history from the request body.",
},
threadIdFor: {
type: "({ user, conversationId }) => string",
description:
"Override how thread ids are derived. Default: `web:{user.id}:{conversationId}`.",
},
logger: {
type: "Logger",
description: 'Defaults to `ConsoleLogger("info")`.',
},
}}
/>
## Authentication
`getUser` is the **security boundary** for the Web adapter. Unlike Slack or Teams where the platform signs every webhook, web requests come straight from a browser — you must identify the caller yourself.
```typescript title="lib/bot.ts" lineNumbers
// NextAuth
createWebAdapter({
userName: "mybot",
getUser: async (req) => {
const session = await getServerSession(authOptions);
if (!session?.user) return null;
return { id: session.user.id, name: session.user.name };
},
});
// Clerk
createWebAdapter({
userName: "mybot",
getUser: async () => {
const { userId, sessionClaims } = await auth();
if (!userId) return null;
return { id: userId, name: sessionClaims?.name as string | undefined };
},
});
```
The resolved `user.id` is embedded in the Chat SDK thread id. Ids containing `:` are rejected with HTTP 400 — normalize them inside `getUser` (e.g. base64-encode them) if your auth provider emits ids like `provider:sub`.
## Advanced
### Threading
By default, each `useChat` conversation maps to one Chat SDK thread:
```
web:{user.id}:{conversationId}
```
`conversationId` is the `id` field useChat sends in its request body. If your client supplies one (`useChat({ id: "support-chat" })`), it's reused across reloads; otherwise a fresh id is generated per request.
Override with `threadIdFor` if you want a single thread per user:
```typescript
createWebAdapter({
userName: "mybot",
getUser,
threadIdFor: ({ user }) => `web:${user.id}:default`,
});
```
The encode/decode helpers are available on the adapter:
```typescript
adapter.encodeThreadId({ userId: "u1", conversationId: "abc" });
// → "web:u1:abc"
adapter.decodeThreadId("web:u1:abc");
// → { userId: "u1", conversationId: "abc" }
```
### Streaming
`thread.post` accepts an `AsyncIterable<string | StreamChunk>` and pumps deltas straight onto the SSE response — no edit loop, no rate limiting. Plays nicely with `streamText` from the AI SDK:
```typescript
import { streamText } from "ai";
bot.onDirectMessage(async (thread, message) => {
const result = streamText({ model, prompt: message.text });
await thread.post(result.textStream);
});
```
The adapter honors `request.signal`, so calling `stop()` from `useChat` short-circuits the iterator on the server.
### Message persistence
`persistMessageHistory` defaults to `true`. Web has no platform-side history API, so the only way for handlers to see prior turns via `thread.messages` is through the configured state adapter's cache. Set it to `false` only if your handler re-derives history from the request body's `messages[]`.
### React hook
`@chat-adapter/web/react` exports a thin wrapper around `@ai-sdk/react`'s `useChat` preconfigured with `DefaultChatTransport`:
```tsx
import { useChat } from "@chat-adapter/web/react";
const { messages, sendMessage, status, stop, regenerate } = useChat({
api: "/api/chat",
threadId: "support-1",
});
```
| Option | Description |
|--------|-------------|
| `api` | API endpoint for the Web adapter route. Defaults to `/api/chat`. |
| `threadId` | Chat SDK thread id — surfaces in the request body's `id`. Strongly recommended. |
| `experimental_throttle` | Throttle wait in ms for chat messages and data updates. |
| `resume` | Whether to resume an ongoing chat generation stream. |
| ...rest | All other options pass through to `@ai-sdk/react`'s `useChat`. |
For advanced configuration, use `@ai-sdk/react`'s `useChat` directly — there's nothing magical in the wrapper.
## Feature support
<FeatureSupport />