A minimal chatbot template built with Next.js, the AI SDK, shadcn/ui, shadcn/react, shadcn/typeset and the Vercel AI Gateway.
- Streaming chat with markdown rendering and shadcn/typeset
- Tool calling example
- Web search via each provider's built-in search tool
- Human-in-the-loop questionnaire. The model can ask clarifying questions, answered with the shadcn questionnaire component
That's it — no configuration needed. Vercel deployments authenticate to the AI Gateway automatically via OIDC, and usage runs on your team's AI Gateway credits.
pnpm install
Then give the app a gateway credential, either by pulling an OIDC token from your linked Vercel project:
vercel link
vercel env pull
or by creating an API key in the Vercel dashboard (AI Gateway → API Keys) and adding it to .env.local
:
cp .env.example .env.local
Start the dev server:
pnpm dev
| Env var | Required | Description |
|---|---|---|
AI_GATEWAY_API_KEY |
||
| Local dev only | AI Gateway API key. Not needed on Vercel deployments (OIDC). |
The model list lives in lib/models.ts — the first entry is the default model.
The /api/chat
route is public and unauthenticated — every request spends your AI Gateway credits. That's fine for a personal demo, but before putting it in front of real traffic you should:
Rate limit it. AddVercel Firewall / WAFrules orso a single client can't drain your credits (denial-of-wallet).@upstash/ratelimit
Cap spend. Set anAI Gateway spend limitas a backstop.Add auth if the chatbot isn't meant to be public.
The route already validates the request body, restricts models to lib/models.ts, caps output tokens and step count, and aborts generation on client disconnect — but those bound a single request, not overall volume.
app/api/chat/route.tsstreams responses withstreamText
components/chat.tsxrenders the conversation withuseChat
and shadcn chat primitives.tools/defines the tools — one file per tool (the filename is the model-facing tool name), composed intools/index.ts: a server-executed GitHub repo lookup, the interactiveask_user
questionnaire, and provider-native web search.
Assistant messages are a list of typed parts. components/chat-message.tsx switches on part.type
and delegates each one to a component in components/parts/:
| Part type | Component | Renders |
|---|---|---|
text |
||
tool-github_repo
github-repo-part.tsxtool-web_search
web-search-part.tsxtool-ask_user
ask-user-part.tsxquestion-card.tsx, pinned to the scroller bottom.source-url
sources-part.tsxTool parts move through states as the stream progresses — input-streaming
→ input-available
→ output-available
(or output-error
) — and each component switches on part.state
to show progress, results, and failures.
- Create
tools/<name>.ts
(the filename is the model-facing tool name) exporting atool()
with adescription
, aninputSchema
, and anexecute
function (omitexecute
for tools the user answers in the UI, likeask_user
), then register it intools/index.ts. - Add a part component in
components/parts/and acase "tool-<name>"
Message types are inferred from the tool definitions via InferUITools
, so part.input
and part.output
are fully typed in your part component — renaming a tool field is a build error, not a silent undefined
.
npx shadcn@latest add button
MIT — see LICENSE.