nextjs-chatbot
Provides an opinionated, production-ready framework for building web chatbots with tool calling and database session storage.
Install
mkdir -p .claude/skills/nextjs-chatbot && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/16946" && unzip -o skill.zip -d .claude/skills/nextjs-chatbot && rm skill.zipInstalls to .claude/skills/nextjs-chatbot
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Advanced patterns for production Next.js web chatbots built with AI SDK 6 + ai-elements. Covers tool calling with human-in-the-loop (HITL) approval, PostgreSQL session persistence, GDPR consent gating, SQL-first search, per-tool UI rendering, popup widget embedding, message feedback, follow-up suggestions, scope enforcement, and evals. Use when building a customer support bot, conversational interface, or any web chatbot needing tool approval, database sessions, or custom tool output components. Not a scaffolding tool — use `/ai-app` to scaffold from scratch, `/ai-sdk-6` for general SDK questions, `/ai-elements` for chat UI components, `/vercel:chat-sdk` for multi-platform (Slack/Teams/Discord) bots.Key capabilities
- →Implement human-in-the-loop (HITL) approval for tool calls
- →Manage PostgreSQL session persistence for chatbot conversations
- →Enforce GDPR consent gating for chatbot interactions
- →Perform SQL-first search within the chatbot context
- →Render custom UI components per tool state
How it works
The skill provides advanced patterns for production Next.js web chatbots, focusing on features like HITL approval, PostgreSQL session persistence, GDPR consent, and custom tool UI rendering.
Inputs & outputs
When to use nextjs-chatbot
- →Build customer support bots
- →Implement tool approval flows
- →Set up PostgreSQL session persistence
- →Create conversational interfaces
About this skill
Next.js Chatbot
Opinionated blueprint for production web chatbots. Focuses on patterns not covered by /ai-sdk-7, /ai-elements, or /nextjs-shadcn — use those skills for general SDK, component, and framework questions. For multi-platform bots (Slack, Teams, Discord), use /vercel:chat-sdk instead.
Stack defaults
- Runtime: bun
- Model: a non-reasoning flagship with reasoning effort off — chat latency is the product. Read the version from the provider's live catalog, not from here.
- AI SDK:
ai@7—ToolLoopAgent,createAgentUIStreamResponse. ⚠️ Checkpackage.json— on a v6 codebase the names below are wrong; see If the project is still on ai@6. - UI: shadcn/ui (Base UI base) + ai-elements (see
/ai-elementsfor component docs) - Scroll:
@shadcn/reactMessageScroller — don't hand-roll stick-to-bottom - Markdown: shadcn typeset (
typeset typeset-chat), streaming-stable - ORM: Drizzle + PostgreSQL
- State: Zustand for client-side chat state (consent, session, suggestions)
- Attachments: See
/ai-elementsAttachments component for file upload
Recommended MCP servers
- next-devtools (
next-devtools-mcp@latestvia npx) — route inspection, build diagnostics. See nextjs.org/docs/app/guides/mcp - ai-elements (via
mcp-remote→https://registry.ai-sdk.dev/api/mcp) — component registry search
Add both to the project's .mcp.json (claude mcp add writes it for you);
.claude/settings.json only enables and permits servers, it does not define them.
Agent setup
export function createAgent(opts?: { model?: LanguageModel }) {
return new ToolLoopAgent({
model: opts?.model ?? openai(CHAT_MODEL), // one constant, read from env
instructions, // NOT `system` — that is v6
reasoning: "low", // portable top-level, see below
tools,
stopWhen: isStepCount(10),
});
}
export const agent = createAgent();
export type AgentUIMessage = InferAgentUIMessage<typeof agent>;
Export both factory and singleton — factory needed for benchmarks. Wrap with devToolsMiddleware() in dev.
⚠️ Reasoning effort is the portable top-level reasoning, not
providerOptions.openai.reasoningEffort — set both and the top-level one is
silently ignored, so your setting does nothing. Raising it above "low" means
keeping sendReasoning: false on the stream: the Responses API rejects a
reasoning part that comes back on the next turn without its required follower,
and a context trimmer that strips step-start/tool-* usually doesn't strip
this one.
Route handler
export const maxDuration = 60;
export async function POST(request: Request) {
const { messages, chatId, ...consent } = await request.json();
// 1. Validate consent — return 403 if missing
// 2. Await session upsert BEFORE streaming (FK dependency)
return createAgentUIStreamResponse({
agent,
uiMessages: messages,
generateMessageId: createIdGenerator({ prefix: "msg", size: 16 }),
consumeSseStream: ({ stream }) => consumeStream({ stream }),
experimental_transform: smoothStream({ delayInMs: 15, chunking: "word" }),
sendReasoning: false,
onEnd: async ({ responseMessage }) => { /* save — see persistence.md */ },
});
}
Azure OpenAI model routing
Azure's Responses API does support non-reasoning models (gpt-4o), but multi-turn tool calls hit an intermittent 400 Item with id 'fc_...' not found error (Microsoft Q&A). Workaround: route non-reasoning models to Chat Completions (azure.chat()); reasoning models (gpt-5.x, o-series) use Responses API (default):
const isReasoning = /^(o[1-9]|gpt-5)/.test(deployment);
export const chatModel = isReasoning ? azure(deployment) : azure.chat(deployment);
Set reasoning only for reasoning models to avoid warnings.
If the project is still on ai@6
Everything on this page is patterns, not API surface, so it carries over — but
the snippets above are v7 names and v6 doesn't know them. Some fail loudly
(an undefined import); the dangerous ones fail quietly — a callback that never
fires or a step limit that never applies, which reads as a model bug rather than
a typo. Check package.json and translate back:
| v7 | v6 |
|---|---|
instructions | system |
isStepCount | stepCountIs |
onEnd (on the stream) | onFinish — the client useChat onFinish is a different, live callback and keeps its name in both |
telemetry | experimental_telemetry |
reasoning: "low" | providerOptions: { openai: { reasoningEffort: "low" } } |
agent-level toolApproval: { myTool: "user-approval" } | needsApproval: true on the tool() — one of the quiet ones: left on a v7 tool it is simply ignored and the tool executes without asking |
Use /ai-sdk-6 for the rest; /ai-sdk if you don't know the version yet.
Never let raw error text reach the browser
onError streams its return value verbatim to the client. String(error)
there puts a provider 401, an endpoint URL or a Postgres constraint on a
customer's screen — and logs nothing, so you find out from a screenshot.
onError: (error) => { // log the real thing, return a sentence
console.error("[chat]", error);
return "Something went wrong generating the answer. Try again.";
}
The client needs the same guard separately: a non-2xx response never goes
through the stream, so useChat's onError receives an Error whose message is
the raw response body. Map it to a fixed set of sentences before rendering.
Custom streams need X-Accel-Buffering: no
Only for responses you build yourself — NDJSON upload progress, an SSE grid fill.
createAgentUIStreamResponse already sets it. Without the header a buffering
proxy (nginx, Cloud Run) holds the whole stream and delivers the progress bar in
one jump at the end, which is worse than having no progress bar. Pair it with
Cache-Control: no-store, no-transform.
⚠️ An error emitted inside an already-open stream arrives with HTTP 200 — the status is long gone. Rejections that happen before the stream opens do use real codes. Client code has to handle both shapes.
Client transport patterns
Dynamic context via transport body
Inject per-request context (e.g., a saved document for edit mode) from the client:
// Simple: body function on DefaultChatTransport
const transport = new DefaultChatTransport({
api: "/api/chat",
body: () => ({ documentContext: activeDocRef.current }),
});
// Fine-grained: prepareSendMessagesRequest (official API)
const transport = new DefaultChatTransport({
prepareSendMessagesRequest: ({ id, messages }) => ({
body: { id, message: messages.at(-1), context: extraRef.current },
}),
});
Server reads extra fields from the request body and passes to agent factory.
Chat remount (new conversation)
Always call stop() before clearing — otherwise the active stream writes into the new conversation:
const { messages, sendMessage, stop, setMessages } = useChat({ transport });
const startNew = useCallback(() => {
stop(); // Cancel active stream FIRST
setMessages([]);
clearStoredMessages(); // If using localStorage
setChatId(crypto.randomUUID());
setConversationKey(k => k + 1);
}, [stop, setMessages]);
localStorage persistence (no DB)
For lightweight chatbots that don't need server-side persistence:
// Load on init via messages prop (NOT useEffect + setMessages)
const initialMessages = useMemo(() => {
const stored = loadStoredMessages();
return stored?.length ? (stored as UIMessage[]) : undefined;
}, []);
const { messages, sendMessage } = useChat({
transport,
messages: initialMessages, // useChat accepts initial messages
onFinish: ({ messages: all }) => saveStoredMessages(all),
});
Hydration: Zustand + localStorage
Zustand stores that read localStorage in create() cause React hydration mismatch (server: false, client: true). Fix with a mounted gate:
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
// In render:
{!mounted || !hasConsented ? <ConsentGate /> : <Chat />}
Same class of bug, different cause: relative timestamps ("6 seconds ago") on
a server-rendered history list. The server and the client compute a different
string, React calls it a mismatch and throws the whole subtree away to re-render
it. Either render an absolute, locale-independent date, or gate the relative one
behind the same mounted flag.
Adding a new tool
- Create
lib/ai/tools/my-tool.tswithtool()fromai - Export from
lib/ai/tools/index.ts - Add to
toolsobject in the agent file - Document in the agent's
instructionsstring - Add UI renderer in
chat-message.tsx(handletool-myToolpart type)
Structured output tools (schema-as-output)
When the tool generates structured data (not query/compute), use the pass-through pattern — the Zod schema defines the output, execute just validates and returns:
const generateDocTool = tool({
description: "Generate structured documentation",
inputSchema: MyDocSchema, // Zod schema IS the output shape
execute: async (data) => data, // Validate and return
});
LLM-resilient enums — LLMs sometimes append extra text to enum values. Use lenient transforms:
const LenientCategory = z.string().transform((val) => {
const valid = ["Business", "Technical", "Legal"] as const;
return valid.find((c) => val.startsWith(c)) ?? "Business";
});
Building a new chatbot
When scaffolding from scratch, read checklist.md for the full setup sequence.
Theming
Theme setup belongs to /nextjs-shadcn: oklch variables in globals.css,
Content truncated.
When not to use it
- →When scaffolding a chatbot from scratch (use `/ai-app` instead)
- →When general AI SDK questions arise (use `/ai-sdk-6` instead)
- →When only chat UI components are needed (use `/ai-elements` instead)
Limitations
- →Not a scaffolding tool
- →Focuses on web chatbots, not multi-platform bots
- →Requires specific AI SDK 6 and ai-elements patterns
How it compares
This skill offers opinionated blueprints for production web chatbots with specific patterns for HITL approval, database sessions, and custom tool output components, unlike general SDKs or UI libraries.
Compared to similar skills
nextjs-chatbot side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| nextjs-chatbot (this skill) | 0 | 2mo | No flags | Advanced |
| add-feature | 0 | 5mo | No flags | Intermediate |
| nextjs-supabase-auth | 12 | 6mo | No flags | Intermediate |
| app-specific-patterns | 4 | 2mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by laguagu
View all by laguagu →You might also like
add-feature
jujunjun110
新しい機能を追加する。ユーザーが機能の説明をしたとき、DDD4層に従って全レイヤーのファイルを生成する
nextjs-supabase-auth
davila7
Expert integration of Supabase Auth with Next.js App Router Use when: supabase auth next, authentication next.js, login supabase, auth middleware, protected route.
app-specific-patterns
growilabs
GROWI main application (apps/app) specific patterns for Next.js, Jotai, SWR, and testing. Auto-invoked when working in apps/app.
javascript-typescript-typescript-scaffold
sickn33
You are a TypeScript project architecture expert specializing in scaffolding production-ready Node.js and frontend applications. Generate complete project structures with modern tooling (pnpm, Vite, N
server-actions
davepoon
This skill should be used when the user asks about "Server Actions", "form handling in Next.js", "mutations", "useFormState", "useFormStatus", "revalidatePath", "revalidateTag", or needs guidance on data mutations and form submissions in Next.js App Router.
web-frameworks
mrgoonie
Build modern full-stack web applications with Next.js (App Router, Server Components, RSC, PPR, SSR, SSG, ISR), Turborepo (monorepo management, task pipelines, remote caching, parallel execution), and RemixIcon (3100+ SVG icons in outlined/filled styles). Use when creating React applications, implementing server-side rendering, setting up monorepos with multiple packages, optimizing build performance and caching strategies, adding icon libraries, managing shared dependencies, or working with TypeScript full-stack projects.