tpmjs/USECHAT_INPUT_UNDEFINED.md
Ajax Davis 635fc96cac feat: implement playground app with AI SDK v6 tool execution
- 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>
2025-12-04 02:51:02 +10:00

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

  1. API route receives requests correctly
  2. Tools are loaded and registered successfully
  3. streamText() with stopWhen: stepCountIs(5) configured
  4. toUIMessageStreamResponse() returns proper SSE stream
  5. curl test shows tools are called correctly with proper JSON Schema
  6. Stream format includes tool-input-start, tool-output-available, text-delta events

What's NOT Working

  1. input property from useChat is undefined
  2. Application crashes when trying to access input.trim()
  3. 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

  1. Is @ai-sdk/react's useChat compatible 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?
  2. Why is input undefined?

    • Does useChat require specific initialization options?
    • Do we need to provide initialMessages or initialInput?
    • Is there a required prop we're missing?
  3. Is the API route format correct for @ai-sdk/react's useChat?

    • Should the API accept a different request format?
    • Is UIMessage[] the correct type for messages?
    • Should we use toDataStreamResponse() instead of toUIMessageStreamResponse()?
  4. Do we need to handle client-side state differently?

    • Should we initialize input with a default value?
    • Is there a provider or context missing?
    • Do we need to wrap the component tree with any providers?
  5. 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)
  6. 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?

Expected Behavior

The useChat hook should return:

  • input: string - Current input value (should be empty string initially)
  • handleInputChange: (e) => void - Update input value
  • handleSubmit: (e) => void - Submit form and send message
  • messages: Message[] - Array of messages
  • isLoading: 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/ui package for UI components
  • @tpmjs/hello package for tools
  • Using workspace protocol (workspace:*) for internal dependencies

What We Need

  1. Correct version compatibility information for AI SDK v6 Beta + @ai-sdk/react
  2. Why input is undefined and how to fix it
  3. Whether our API route format is correct for the React hook
  4. Any missing initialization or configuration for useChat
  5. 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.