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 installThen give the app a gateway credential, either by pulling an OIDC token from your linked Vercel project:
vercel link
vercel env pullor by creating an API key in the Vercel dashboard (AI Gateway → API Keys) and adding it to .env.local:
cp .env.example .env.local
# then set AI_GATEWAY_API_KEY=...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. Add Vercel Firewall / WAF rules or
@upstash/ratelimitso a single client can't drain your credits (denial-of-wallet). - Cap spend. Set an AI Gateway spend limit as 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.ts streams responses with
streamText - components/chat.tsx renders the conversation with
useChatand shadcn chat primitives. - tools/ defines the tools — one file per tool (the filename is the model-facing tool name), composed in tools/index.ts: a server-executed GitHub repo lookup, the interactive
ask_userquestionnaire, 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 |
text-part.tsx | Markdown via react-markdown and shadcn/typeset. |
tool-github_repo |
github-repo-part.tsx | A spinner while the lookup runs, then a linked stat line (stars, forks, language). |
tool-web_search |
web-search-part.tsx | A "Searching the web…" status while the search runs, then a persistent "Searched the web" line per search. |
tool-ask_user |
ask-user-part.tsx | The answered questions inline. Pending questions render in question-card.tsx, pinned to the scroller bottom. |
source-url |
sources-part.tsx | Web search citations, deduped into a "Searched N websites" drawer once the message finishes streaming. |
Tool 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 anexecutefunction (omitexecutefor tools the user answers in the UI, likeask_user), then register it in tools/index.ts. - Add a part component in components/parts/ and a
case "tool-<name>"in chat-message.tsx.
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 buttonMIT — see LICENSE.