- Create new Next.js app at apps/playground for testing TPMJS tools - Implement AI SDK v6 patterns with DefaultChatTransport and UIMessage format - Create template tool package at packages/tools/hello with hello-world and hello-name tools - Use tool() and jsonSchema() helpers to avoid Zod 4 conversion issues with OpenAI - Add static tool loading system with switch statement (Next.js/webpack compatible) - Implement chat interface with tool call visualization showing inputs/outputs - Support multi-step tool execution with stepCountIs(5) - Stream responses with toUIMessageStreamResponse() for full tool support - Add sidebar showing available tools (static list) - Use parts-based message rendering for text and tool calls - Integrate firecrawl-aisdk tools (scrape, crawl, search) - Add theme toggle in header (defaults to light mode) - Fix responsive layout with max-width for message bubbles - Use biome-ignore comments for legitimate any types in tool loading 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
12 KiB
useChat Hook Returns Undefined Input Property
Problem
Using @ai-sdk/react's useChat hook, the input property is returning undefined, causing the application to crash when trying to call .trim() on it.
Error
TypeError: Cannot read properties of undefined (reading 'trim')
at ChatInput (src/components/chat/ChatInput.tsx:41:48)
Code that fails:
<Button type="submit" disabled={!input.trim() || isLoading} loading={isLoading} size="lg">
Environment
- AI SDK Version:
ai@6.0.0-beta.124 - AI SDK React:
@ai-sdk/react(latest version installed via pnpm) - OpenAI Provider:
@ai-sdk/openai@3.0.0-beta.74 - Next.js:
16.0.4(App Router with Turbopack) - React:
19.0.0 - Zod:
4.0.0(required, not downgrading) - TypeScript:
5.9.3
Current Implementation
Custom useChat Hook Wrapper
Located at: apps/playground/src/hooks/useChat.ts
'use client';
import { useChat as useAISDKChat } from '@ai-sdk/react';
/**
* Custom chat hook that wraps the official @ai-sdk/react useChat
* Handles SSE streaming with tool calls and UI message protocol
*/
export function useChat() {
const chat = useAISDKChat({
api: '/api/chat',
});
return {
messages: chat.messages,
input: chat.input,
isLoading: chat.isLoading,
error: chat.error,
handleInputChange: chat.handleInputChange,
handleSubmit: chat.handleSubmit,
setInput: chat.setInput,
reload: chat.reload,
stop: chat.stop,
};
}
API Route
Located at: apps/playground/src/app/api/chat/route.ts
import { createOpenAI } from '@ai-sdk/openai';
import { streamText, convertToModelMessages, stepCountIs, type UIMessage } from 'ai';
import { type NextRequest } from 'next/server';
import { env } from '~/env';
import { loadAllTools } from '~/lib/tool-loader';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
// Initialize OpenAI provider
const openai = createOpenAI({
apiKey: env.OPENAI_API_KEY,
});
/**
* POST /api/chat
* Chat with AI agent that can execute TPMJS tools
*/
export async function POST(request: NextRequest) {
try {
const { messages }: { messages: UIMessage[] } = await request.json();
// Load all available TPMJS tools
const tools = await loadAllTools();
// Create system prompt listing available tools
const toolsList = Object.keys(tools)
.map((name) => `- ${name}: ${tools[name]?.description}`)
.join('\n');
const system = `You are a helpful AI assistant that can use TPMJS tools to help users.
Available tools:
${toolsList}
When you use a tool, you MUST always follow up with a natural language answer to the user summarizing the result.`;
// Stream the response with multi-step tool usage enabled
const result = streamText({
model: openai('gpt-4o-mini'),
system,
messages: convertToModelMessages(messages),
tools,
stopWhen: stepCountIs(5), // Allow model to call tools AND generate text response
});
// Return UI message stream with tool calls and text
return result.toUIMessageStreamResponse();
} catch (error) {
console.error('Chat API error:', error);
return new Response(
JSON.stringify({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
}
Component Using the Hook
Located at: apps/playground/src/components/chat/ChatInterface.tsx
'use client';
import { useChat } from '~/hooks/useChat';
import { ChatInput } from './ChatInput';
import { ChatMessages } from './ChatMessages';
export function ChatInterface(): React.ReactElement {
const { messages, input, isLoading, handleInputChange, handleSubmit, setInput } = useChat();
return (
<div className="flex flex-1 flex-col">
<ChatMessages messages={messages} />
<ChatInput
input={input}
isLoading={isLoading}
onInputChange={handleInputChange}
onSubmit={handleSubmit}
setInput={setInput}
/>
</div>
);
}
ChatInput Component
Located at: apps/playground/src/components/chat/ChatInput.tsx
'use client';
import { Button } from '@tpmjs/ui/Button/Button';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import type { FormEvent } from 'react';
interface ChatInputProps {
input: string;
isLoading: boolean;
onInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onSubmit: (e: FormEvent<HTMLFormElement>) => void;
setInput: (value: string) => void;
}
export function ChatInput({ input, isLoading, onInputChange, onSubmit }: ChatInputProps): React.ReactElement {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (input.trim() && !isLoading) {
// Trigger form submission
const form = e.currentTarget.form;
if (form) {
form.requestSubmit();
}
}
}
};
return (
<form onSubmit={onSubmit} className="border-t border-border bg-background p-4">
<div className="mx-auto flex max-w-4xl gap-2">
<Textarea
value={input}
onChange={onInputChange}
onKeyDown={handleKeyDown}
placeholder="Ask me to tell you a fish joke..."
className="min-h-[60px] flex-1 resize-none"
disabled={isLoading}
rows={3}
/>
<Button type="submit" disabled={!input.trim() || isLoading} loading={isLoading} size="lg">
Send
</Button>
</div>
</form>
);
}
Tool Definition (Working)
The tools are defined using tool() and jsonSchema() from AI SDK to avoid Zod 4 compatibility issues:
Located at: packages/tools/hello/src/index.ts
import { jsonSchema, tool } from 'ai';
type HelloWorldInput = {
includeTimestamp?: boolean;
};
export const helloWorldTool = tool({
description: 'Returns a simple "Hello, World!" greeting message',
inputSchema: jsonSchema<HelloWorldInput>({
type: 'object',
properties: {
includeTimestamp: {
type: 'boolean',
description: 'Whether to include a timestamp in the response',
},
},
additionalProperties: false,
}),
async execute({ includeTimestamp = true }) {
const response: any = {
message: 'Hello, World!',
};
if (includeTimestamp) {
response.timestamp = new Date().toISOString();
}
return response;
},
});
type HelloNameInput = {
name: string;
};
export const helloNameTool = tool({
description: 'Returns a personalized greeting with the provided name',
inputSchema: jsonSchema<HelloNameInput>({
type: 'object',
properties: {
name: {
type: 'string',
description: 'The name of the person to greet',
},
},
required: ['name'],
additionalProperties: false,
}),
async execute({ name }) {
return {
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
};
},
});
What's Working
- ✅ API route receives requests correctly
- ✅ Tools are loaded and registered successfully
- ✅
streamText()withstopWhen: stepCountIs(5)configured - ✅
toUIMessageStreamResponse()returns proper SSE stream - ✅ curl test shows tools are called correctly with proper JSON Schema
- ✅ Stream format includes
tool-input-start,tool-output-available,text-deltaevents
What's NOT Working
- ❌
inputproperty fromuseChatisundefined - ❌ Application crashes when trying to access
input.trim() - ❌ Can't type in the chat input field
curl Test (Successful)
curl -N http://localhost:3001/api/chat \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"say hello thomas"}]}'
Response:
data: {"type":"start"}
data: {"type":"start-step"}
data: {"type":"tool-input-start","toolCallId":"call_...","toolName":"helloName"}
data: {"type":"tool-input-delta","toolCallId":"call_...","inputTextDelta":"{\""}
data: {"type":"tool-input-delta","toolCallId":"call_...","inputTextDelta":"name"}
data: {"type":"tool-input-delta","toolCallId":"call_...","inputTextDelta":"\":\""}
data: {"type":"tool-input-delta","toolCallId":"call_...","inputTextDelta":"Thomas"}
data: {"type":"tool-input-delta","toolCallId":"call_...","inputTextDelta":"\"}"}
data: {"type":"tool-input-available","toolCallId":"call_...","toolName":"helloName","input":{"name":"Thomas"},"providerMetadata":{...}}
data: {"type":"tool-output-available","toolCallId":"call_...","output":{"message":"Hello, Thomas!","timestamp":"2025-12-03T16:07:31.366Z"}}
data: {"type":"finish-step"}
data: {"type":"start-step"}
data: {"type":"text-start","id":"msg_...","providerMetadata":{...}}
data: {"type":"text-delta","id":"msg_...","delta":"Hello"}
data: {"type":"text-delta","id":"msg_...","delta":","}
data: {"type":"text-delta","id":"msg_...","delta":" Thomas"}
data: {"type":"text-delta","id":"msg_...","delta":"!"}
data: {"type":"text-end","id":"msg_...","providerMetadata":{...}}
data: {"type":"finish-step"}
data: {"type":"finish","finishReason":"stop"}
data: [DONE]
The API works perfectly - tools are called, results are returned, text is generated. The issue is purely on the React client side.
Questions
-
Is
@ai-sdk/react'suseChatcompatible with AI SDK v6 Beta (6.0.0-beta.124)?- Should we be using a different version of
@ai-sdk/react? - Are there known compatibility issues with AI SDK v6 Beta?
- Should we be using a different version of
-
Why is
inputundefined?- Does
useChatrequire specific initialization options? - Do we need to provide
initialMessagesorinitialInput? - Is there a required prop we're missing?
- Does
-
Is the API route format correct for
@ai-sdk/react'suseChat?- Should the API accept a different request format?
- Is
UIMessage[]the correct type for messages? - Should we use
toDataStreamResponse()instead oftoUIMessageStreamResponse()?
-
Do we need to handle client-side state differently?
- Should we initialize
inputwith a default value? - Is there a provider or context missing?
- Do we need to wrap the component tree with any providers?
- Should we initialize
-
Is there a version mismatch between packages?
ai@6.0.0-beta.124@ai-sdk/openai@3.0.0-beta.74@ai-sdk/react@?(unknown version)
-
Does Zod 4 affect the client-side hook?
- We fixed the server-side tool schemas using
jsonSchema() - Could there be client-side Zod 4 issues affecting
useChat?
- We fixed the server-side tool schemas using
Expected Behavior
The useChat hook should return:
input: string- Current input value (should be empty string initially)handleInputChange: (e) => void- Update input valuehandleSubmit: (e) => void- Submit form and send messagemessages: Message[]- Array of messagesisLoading: boolean- Loading state
Actual Behavior
input: undefined❌- Everything else appears to be defined
- Crash on first render when trying to access
input.trim()
Monorepo Context
- Turborepo monorepo with pnpm workspaces
- Next.js 16 App Router with Turbopack
- TypeScript strict mode
@tpmjs/uipackage for UI components@tpmjs/hellopackage for tools- Using workspace protocol (
workspace:*) for internal dependencies
Related Documentation
What We Need
- Correct version compatibility information for AI SDK v6 Beta + @ai-sdk/react
- Why
inputis undefined and how to fix it - Whether our API route format is correct for the React hook
- Any missing initialization or configuration for
useChat - Whether there are alternative approaches (custom SSE parsing, different hook, etc.)
We must keep Zod 4 and cannot downgrade. The server-side tools are working correctly with jsonSchema() workaround.