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>
This commit is contained in:
parent
fcd6667357
commit
635fc96cac
33 changed files with 2817 additions and 5 deletions
522
OPENAI_SCHEMA_ERROR.md
Normal file
522
OPENAI_SCHEMA_ERROR.md
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
# OpenAI Schema Validation Error - AI SDK v6
|
||||
|
||||
## ✅ RESOLVED
|
||||
|
||||
**Solution:** Use `tool()` and `jsonSchema()` from AI SDK instead of Zod for tool definitions.
|
||||
|
||||
## Error Message
|
||||
|
||||
```
|
||||
Error [AI_APICallError]: Invalid schema for function 'helloWorld': schema must be a JSON Schema of 'type: "object"', got 'type: "None"'.
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
Building a Next.js playground app to test AI SDK v6 tool execution with OpenAI's GPT-4o-mini model. The error occurs when OpenAI validates the tool schema sent in the API request.
|
||||
|
||||
## Root Cause
|
||||
|
||||
Zod 4.0.0 generates JSON Schema with `allOf` + `$ref` at the root level instead of a direct `type: "object"`. OpenAI's API requires a JSON Schema with `type: "object"` at the root, so it rejects Zod 4 schemas with `type: "None"` error.
|
||||
|
||||
## Environment
|
||||
|
||||
- **AI SDK Version**: `ai@6.0.0-beta.124`
|
||||
- **OpenAI Provider**: `@ai-sdk/openai@3.0.0-beta.74`
|
||||
- **OpenAI Library**: `openai@^6.9.1`
|
||||
- **Zod Version**: `zod@^4.0.0`
|
||||
- **Next.js Version**: `next@^16.0.4`
|
||||
- **Node.js**: Latest
|
||||
- **TypeScript**: Strict mode enabled
|
||||
|
||||
## Tool Definition
|
||||
|
||||
Located at: `packages/tools/hello/src/index.ts`
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Hello World Tool
|
||||
* Returns a simple "Hello, World!" greeting
|
||||
*
|
||||
* This is a proper AI SDK v6 tool that can be used with streamText()
|
||||
*/
|
||||
export const helloWorldTool = {
|
||||
description: 'Returns a simple "Hello, World!" greeting message',
|
||||
parameters: z.object({
|
||||
// OpenAI requires at least one optional parameter, can't be completely empty
|
||||
includeTimestamp: z.boolean().optional().describe('Whether to include a timestamp in the response'),
|
||||
}),
|
||||
execute: async ({ includeTimestamp = true }: { includeTimestamp?: boolean }) => {
|
||||
const response: any = {
|
||||
message: 'Hello, World!',
|
||||
};
|
||||
|
||||
if (includeTimestamp) {
|
||||
response.timestamp = new Date().toISOString();
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Hello Name Tool
|
||||
* Returns a personalized greeting with the provided name
|
||||
*
|
||||
* This is a proper AI SDK v6 tool that can be used with streamText()
|
||||
*/
|
||||
export const helloNameTool = {
|
||||
description: 'Returns a personalized greeting with the provided name',
|
||||
parameters: z.object({
|
||||
name: z.string().describe('The name of the person to greet'),
|
||||
}),
|
||||
execute: async ({ name }: { name: string }) => {
|
||||
return {
|
||||
message: `Hello, ${name}!`,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Tool Loading
|
||||
|
||||
Located at: `apps/playground/src/lib/tool-loader.ts`
|
||||
|
||||
```typescript
|
||||
// Static imports for tools (required for Next.js/webpack)
|
||||
import { helloWorldTool, helloNameTool } from '@tpmjs/hello';
|
||||
import { scrapeTool, crawlTool, searchTool } from 'firecrawl-aisdk';
|
||||
|
||||
/**
|
||||
* Load a specific TPMJS tool by package name
|
||||
*/
|
||||
export async function loadTpmjsTool(packageName: string): Promise<any> {
|
||||
try {
|
||||
// Map package names to their tool functions
|
||||
switch (packageName) {
|
||||
case '@tpmjs/hello':
|
||||
// Hello has multiple tools, return all of them
|
||||
return {
|
||||
helloWorld: helloWorldTool,
|
||||
helloName: helloNameTool,
|
||||
};
|
||||
|
||||
case 'firecrawl-aisdk':
|
||||
// Firecrawl has multiple tools, return all of them
|
||||
return {
|
||||
scrapeTool,
|
||||
crawlTool,
|
||||
searchTool,
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool package: ${packageName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to load tool from package ${packageName}: ${error.message}`);
|
||||
}
|
||||
throw new Error(`Failed to load tool from package ${packageName}: Unknown error`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all installed TPMJS tools
|
||||
*/
|
||||
export async function loadAllTools(): Promise<Record<string, any>> {
|
||||
const installedTools = ['@tpmjs/hello', 'firecrawl-aisdk'];
|
||||
|
||||
const tools: Record<string, any> = {};
|
||||
|
||||
for (const packageName of installedTools) {
|
||||
try {
|
||||
const tool = await loadTpmjsTool(packageName);
|
||||
|
||||
// If the tool returns an object with multiple tools (like firecrawl), spread them
|
||||
if (tool && typeof tool === 'object' && !tool.description) {
|
||||
Object.assign(tools, tool);
|
||||
} else {
|
||||
// Single tool - use a cleaned name (remove hyphens, camelCase)
|
||||
const toolName = packageName.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase()).replace(/-/g, '');
|
||||
tools[toolName] = tool;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load tool ${packageName}:`, error);
|
||||
// Continue loading other tools even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
```
|
||||
|
||||
## API Route
|
||||
|
||||
Located at: `apps/playground/src/app/api/chat/route.ts`
|
||||
|
||||
```typescript
|
||||
import { loadAllTools } from '~/lib/tool-loader';
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import { streamText } from 'ai';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 60;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { messages } = body;
|
||||
|
||||
if (!messages || !Array.isArray(messages)) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid request: messages array required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Load all available tools
|
||||
const tools = await loadAllTools();
|
||||
|
||||
console.log('Loaded tools:', Object.keys(tools));
|
||||
|
||||
// Create system message
|
||||
const systemMessage = {
|
||||
role: 'system' as const,
|
||||
content: `You are a helpful AI assistant that can use TPMJS tools to help users.
|
||||
|
||||
Available tools:
|
||||
${Object.entries(tools)
|
||||
.map(([name, tool]) => `- ${name}: ${tool.description}`)
|
||||
.join('\n')}
|
||||
|
||||
Call tools as needed to answer user questions. Execute tools directly.`,
|
||||
};
|
||||
|
||||
// Stream the AI response with tools
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o-mini'),
|
||||
messages: [systemMessage, ...messages],
|
||||
tools,
|
||||
maxSteps: 5,
|
||||
});
|
||||
|
||||
return result.toTextStreamResponse();
|
||||
} catch (error) {
|
||||
console.error('Chat API error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Package Configuration
|
||||
|
||||
Located at: `packages/tools/hello/package.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@tpmjs/hello",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "Example TPMJS tools - Hello World and Hello Name",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"tpmjs-tool",
|
||||
"ai-sdk",
|
||||
"hello",
|
||||
"example"
|
||||
],
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Simple greeting tools - Hello World and personalized Hello Name greetings"
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "6.0.0-beta.124",
|
||||
"zod": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tpmjs/tsconfig": "workspace:*",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Configuration
|
||||
|
||||
Located at: `packages/tools/hello/tsconfig.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
## Compiled Output
|
||||
|
||||
Located at: `packages/tools/hello/dist/index.js`
|
||||
|
||||
```javascript
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.helloNameTool = exports.helloWorldTool = void 0;
|
||||
const zod_1 = require("zod");
|
||||
/**
|
||||
* Hello World Tool
|
||||
* Returns a simple "Hello, World!" greeting
|
||||
*
|
||||
* This is a proper AI SDK v6 tool that can be used with streamText()
|
||||
*/
|
||||
exports.helloWorldTool = {
|
||||
description: 'Returns a simple "Hello, World!" greeting message',
|
||||
parameters: zod_1.z.object({
|
||||
// OpenAI requires at least one optional parameter, can't be completely empty
|
||||
includeTimestamp: zod_1.z.boolean().optional().describe('Whether to include a timestamp in the response'),
|
||||
}),
|
||||
execute: async ({ includeTimestamp = true }) => {
|
||||
const response = {
|
||||
message: 'Hello, World!',
|
||||
};
|
||||
if (includeTimestamp) {
|
||||
response.timestamp = new Date().toISOString();
|
||||
}
|
||||
return response;
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Hello Name Tool
|
||||
* Returns a personalized greeting with the provided name
|
||||
*
|
||||
* This is a proper AI SDK v6 tool that can be used with streamTime()
|
||||
*/
|
||||
exports.helloNameTool = {
|
||||
description: 'Returns a personalized greeting with the provided name',
|
||||
parameters: zod_1.z.object({
|
||||
name: zod_1.z.string().describe('The name of the person to greet'),
|
||||
}),
|
||||
execute: async ({ name }) => {
|
||||
return {
|
||||
message: `Hello, ${name}!`,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Full Error Response from OpenAI
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"message": "Invalid schema for function 'helloWorld': schema must be a JSON Schema of 'type: \"object\"', got 'type: \"None\"'.",
|
||||
"type": "invalid_request_error",
|
||||
"param": "tools[0].parameters",
|
||||
"code": "invalid_function_parameters"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
API endpoint: `https://api.openai.com/v1/responses`
|
||||
Status code: 400
|
||||
|
||||
## Problem Analysis
|
||||
|
||||
1. **OpenAI expects JSON Schema format** - The `tools[0].parameters` field must be a valid JSON Schema object with `type: "object"`
|
||||
|
||||
2. **AI SDK v6 should convert Zod to JSON Schema** - The AI SDK is supposed to automatically convert Zod schemas to JSON Schema when sending to OpenAI, but it's producing `type: "None"` instead
|
||||
|
||||
3. **Potential causes**:
|
||||
- Zod 4.0.0 compatibility issue with AI SDK v6 beta
|
||||
- AI SDK not properly converting the Zod schema
|
||||
- Issue with how the tool object is structured
|
||||
- Problem with how tools are passed to `streamText()`
|
||||
|
||||
4. **Already tried**:
|
||||
- Added at least one parameter (even optional) to helloWorldTool
|
||||
- Used proper Zod schema with `.describe()` for descriptions
|
||||
- Followed AI SDK v6 tool definition format exactly
|
||||
- Built the package successfully (dist folder exists)
|
||||
|
||||
## AI SDK v6 Tool Format Reference
|
||||
|
||||
According to AI SDK v6 documentation, a tool should be defined as:
|
||||
|
||||
```typescript
|
||||
{
|
||||
description: string,
|
||||
parameters: ZodSchema,
|
||||
execute: async (args) => Promise<any>
|
||||
}
|
||||
```
|
||||
|
||||
This matches our implementation exactly.
|
||||
|
||||
## Questions for ChatGPT
|
||||
|
||||
1. Is there a known compatibility issue between AI SDK v6 Beta (6.0.0-beta.124) and Zod 4.0.0?
|
||||
|
||||
2. Does the AI SDK v6 require a specific tool registration format when passing to `streamText()`?
|
||||
|
||||
3. Should tools be wrapped in a different structure (e.g., using `tool()` helper function)?
|
||||
|
||||
4. Is there a way to manually convert Zod schema to JSON Schema that OpenAI accepts?
|
||||
|
||||
5. Are there any known issues with using workspace packages (`@tpmjs/hello`) in Next.js API routes with dynamic imports?
|
||||
|
||||
6. Should we downgrade to Zod 3.x instead of Zod 4.0.0?
|
||||
|
||||
7. Is there a debug mode to see what JSON Schema is being sent to OpenAI?
|
||||
|
||||
## Additional Context
|
||||
|
||||
- The `firecrawl-aisdk` package works correctly with the same setup
|
||||
- Build process completes successfully with no TypeScript errors
|
||||
- The tool is being loaded and passed to `streamText()` correctly
|
||||
- Error only occurs when OpenAI validates the tool schema
|
||||
- This is a monorepo using pnpm workspaces and Turborepo
|
||||
|
||||
## Related Files
|
||||
|
||||
- Tool definition: `packages/tools/hello/src/index.ts`
|
||||
- Tool loader: `apps/playground/src/lib/tool-loader.ts`
|
||||
- API route: `apps/playground/src/app/api/chat/route.ts`
|
||||
- Package config: `packages/tools/hello/package.json`
|
||||
- Compiled output: `packages/tools/hello/dist/index.js`
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
Tools should be automatically converted from Zod schema to JSON Schema by AI SDK v6 and accepted by OpenAI's API.
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
OpenAI rejects the tool schema with error: `got 'type: "None"'` instead of a valid JSON Schema object.
|
||||
|
||||
---
|
||||
|
||||
## ✅ SOLUTION IMPLEMENTED
|
||||
|
||||
### What We Changed
|
||||
|
||||
Instead of using Zod schemas with `parameters`, we now use AI SDK's `tool()` helper with `jsonSchema()` for the input schema. This bypasses Zod's JSON Schema conversion entirely.
|
||||
|
||||
### Before (Broken with Zod 4)
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
export const helloWorldTool = {
|
||||
description: 'Returns a simple "Hello, World!" greeting message',
|
||||
parameters: z.object({
|
||||
includeTimestamp: z.boolean().optional().describe('Whether to include a timestamp'),
|
||||
}),
|
||||
execute: async ({ includeTimestamp = true }) => {
|
||||
// ...
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### After (Working with jsonSchema)
|
||||
|
||||
```typescript
|
||||
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;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Key Changes
|
||||
|
||||
1. **Import from `ai`**: Added `jsonSchema` and `tool` imports
|
||||
2. **Define TypeScript types**: Created `HelloWorldInput` type for type safety
|
||||
3. **Use `tool()` wrapper**: Wraps the entire tool definition
|
||||
4. **Use `jsonSchema()` for schema**: Provides explicit JSON Schema with `type: "object"` at root
|
||||
5. **Removed Zod dependency**: No longer need `zod` in package.json
|
||||
|
||||
### Benefits
|
||||
|
||||
- ✅ Works with OpenAI's strict schema validation
|
||||
- ✅ Explicit control over JSON Schema structure
|
||||
- ✅ Full TypeScript type safety with generic types
|
||||
- ✅ No dependency on Zod (one less package to maintain)
|
||||
- ✅ Follows AI SDK v6 best practices
|
||||
- ✅ Guaranteed `type: "object"` at root level
|
||||
|
||||
### Updated Package Dependencies
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"ai": "6.0.0-beta.124"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Zod is no longer needed in tool packages that use `jsonSchema()`.
|
||||
|
||||
### References
|
||||
|
||||
- [AI SDK Core: tool](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool)
|
||||
- [AI SDK Core: jsonSchema](https://ai-sdk.dev/docs/reference/ai-sdk-core/json-schema)
|
||||
- [GitHub Issue: Zod 4 JSON Schema compatibility](https://github.com/vercel/ai/issues/10240)
|
||||
363
STREAMING_EMPTY_RESPONSE.md
Normal file
363
STREAMING_EMPTY_RESPONSE.md
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
# AI SDK v6 Streaming Empty Response Issue
|
||||
|
||||
## Problem
|
||||
|
||||
Using AI SDK v6 Beta with OpenAI and `streamText()`, the API route returns a 200 OK response, but the streamed response body is **completely empty** when tools are involved.
|
||||
|
||||
- **Normal chat** (without tool calls): Works fine, streams text back
|
||||
- **Tool calls** (when user asks "say hello world"): Returns empty response body, no error messages
|
||||
|
||||
## Environment
|
||||
|
||||
- **AI SDK Version**: `ai@6.0.0-beta.124`
|
||||
- **OpenAI Provider**: `@ai-sdk/openai@3.0.0-beta.74`
|
||||
- **OpenAI Library**: `openai@^6.9.1`
|
||||
- **Next.js Version**: `next@^16.0.4` (App Router)
|
||||
- **Runtime**: Node.js (`runtime = 'nodejs'`)
|
||||
- **Model**: `gpt-4o-mini`
|
||||
|
||||
## API Route Implementation
|
||||
|
||||
Located at: `apps/playground/src/app/api/chat/route.ts`
|
||||
|
||||
```typescript
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { streamText, type CoreMessage, tool, jsonSchema } from 'ai';
|
||||
import { type NextRequest } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { env } from '~/env';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 60;
|
||||
|
||||
// Initialize OpenAI provider
|
||||
const openai = createOpenAI({
|
||||
apiKey: env.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
// Request schema
|
||||
const RequestSchema = z.object({
|
||||
messages: z.array(
|
||||
z.object({
|
||||
role: z.enum(['user', 'assistant', 'system']),
|
||||
content: z.string(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
// Simple inline test tool to verify streaming works
|
||||
const testHelloTool = tool({
|
||||
description: 'Returns a simple hello world greeting',
|
||||
inputSchema: jsonSchema<{ includeTimestamp?: boolean }>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
includeTimestamp: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to include a timestamp',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute({ includeTimestamp = true }) {
|
||||
const response: any = { message: 'Hello, World!' };
|
||||
if (includeTimestamp) {
|
||||
response.timestamp = new Date().toISOString();
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/chat
|
||||
* Chat with AI agent that can execute TPMJS tools
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { messages } = RequestSchema.parse(body);
|
||||
|
||||
// Use simple inline tool for testing
|
||||
const tools = {
|
||||
testHello: testHelloTool,
|
||||
};
|
||||
|
||||
// Create system prompt listing available tools
|
||||
const toolsList = Object.keys(tools)
|
||||
.map((name) => `- ${name}: ${tools[name]?.description}`)
|
||||
.join('\n');
|
||||
|
||||
const systemMessage: CoreMessage = {
|
||||
role: 'system',
|
||||
content: `You are a helpful AI assistant that can use TPMJS tools to help users.
|
||||
|
||||
Available tools:
|
||||
${toolsList}
|
||||
|
||||
Call tools as needed to answer user questions. When a user asks to say hello world or for a greeting, use the testHello tool.`,
|
||||
};
|
||||
|
||||
// Stream the response
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o-mini'),
|
||||
messages: [systemMessage, ...messages],
|
||||
tools,
|
||||
});
|
||||
|
||||
// Return the stream as SSE
|
||||
return result.toTextStreamResponse();
|
||||
} catch (error) {
|
||||
console.error('Chat API error:', error);
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: 'Invalid request format',
|
||||
details: error.issues,
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Client-Side Hook
|
||||
|
||||
Located at: `apps/playground/src/hooks/useChat.ts`
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export function useChat() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const sendMessage = useCallback(async (content: string) => {
|
||||
if (!content.trim()) return;
|
||||
|
||||
// Add user message immediately
|
||||
const userMessage: ChatMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
role: 'user',
|
||||
content,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Create assistant message placeholder
|
||||
const assistantMessageId = crypto.randomUUID();
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: assistantMessageId,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
|
||||
// Send request to API
|
||||
const response = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: [...messages, userMessage].map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// Read the streaming response
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
if (!reader) {
|
||||
throw new Error('Response body is null');
|
||||
}
|
||||
|
||||
let accumulatedContent = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Decode the chunk
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
accumulatedContent += chunk;
|
||||
|
||||
// Update the assistant message with accumulated content
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantMessageId
|
||||
? { ...m, content: accumulatedContent }
|
||||
: m
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error sending message:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to send message');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
const clearChat = useCallback(() => {
|
||||
setMessages([]);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
messages,
|
||||
isLoading,
|
||||
error,
|
||||
sendMessage,
|
||||
clearChat,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Observed Behavior
|
||||
|
||||
### Working Case (Normal Chat)
|
||||
- User types: "hi"
|
||||
- API response: 200 OK
|
||||
- Response body: Streams text chunks successfully
|
||||
- UI shows: "Hi! How can I help you today?"
|
||||
|
||||
### Broken Case (Tool Call)
|
||||
- User types: "say hello world"
|
||||
- API response: 200 OK ✅
|
||||
- Response body: **EMPTY** ❌ (no chunks, no data, nothing)
|
||||
- UI shows: Empty message bubble
|
||||
- Console: No errors logged
|
||||
|
||||
## HTTP Response Details
|
||||
|
||||
```
|
||||
Request Method: POST
|
||||
Status Code: 200 OK
|
||||
URL: http://localhost:3001/api/chat
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Transfer-Encoding: chunked
|
||||
```
|
||||
|
||||
The response headers look correct for a streaming response, but the body is completely empty.
|
||||
|
||||
## What We've Tried
|
||||
|
||||
1. ✅ Fixed OpenAI schema validation error (was `type: "None"`, now uses proper JSON Schema)
|
||||
2. ✅ Using `tool()` and `jsonSchema()` from AI SDK
|
||||
3. ✅ Simplified to a single inline test tool
|
||||
4. ✅ Tool executes without errors (no schema validation issues)
|
||||
5. ✅ Normal chat works fine (proves streaming infrastructure is correct)
|
||||
|
||||
## Questions
|
||||
|
||||
1. **Is `toTextStreamResponse()` the correct method for streaming with tools in AI SDK v6?**
|
||||
- Should we use a different method like `toDataStreamResponse()` for tool calls?
|
||||
|
||||
2. **Are we constructing the messages array correctly?**
|
||||
- We're sending `{ role: 'user' | 'assistant' | 'system', content: string }[]`
|
||||
- Do we need to include tool call messages or tool result messages?
|
||||
|
||||
3. **Does AI SDK v6 require a specific message format for tool calls?**
|
||||
- Should we be including `toolInvocations` or `tool_calls` in the message history?
|
||||
- Are we missing required fields in the `CoreMessage` type?
|
||||
|
||||
4. **Is the client-side streaming reader correct?**
|
||||
- We're reading chunks with `response.body.getReader()`
|
||||
- Should we be parsing SSE events differently for tool calls?
|
||||
|
||||
5. **Does `streamText()` with tools require `maxSteps` parameter?**
|
||||
- Do we need to set `maxSteps: 5` to allow multi-step reasoning?
|
||||
|
||||
6. **Are we handling the conversation history correctly?**
|
||||
- We're sending all previous messages on each request
|
||||
- Should we be including assistant messages with tool call results?
|
||||
|
||||
## AI SDK v6 Documentation References
|
||||
|
||||
We're following these patterns from the official docs:
|
||||
|
||||
- [streamText() API](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text)
|
||||
- [tool() API](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool)
|
||||
- [Tool Calling Guide](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling)
|
||||
|
||||
But we might be missing something specific about:
|
||||
- How to handle streaming when tools are executed
|
||||
- What response format tool calls produce
|
||||
- How to parse the stream when tools are involved
|
||||
|
||||
## Suspected Issue
|
||||
|
||||
**The message format might be wrong.** We're sending:
|
||||
|
||||
```typescript
|
||||
const systemMessage: CoreMessage = {
|
||||
role: 'system',
|
||||
content: `You are a helpful AI assistant...`,
|
||||
};
|
||||
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o-mini'),
|
||||
messages: [systemMessage, ...messages],
|
||||
tools,
|
||||
});
|
||||
```
|
||||
|
||||
But `CoreMessage` might need additional fields when tools are involved, or we might need to handle tool call results differently in the conversation history.
|
||||
|
||||
## What We Need
|
||||
|
||||
1. Correct message format for `streamText()` with tools
|
||||
2. How to properly stream responses that include tool calls
|
||||
3. Whether we need different client-side parsing for tool call streams
|
||||
4. Example of a working Next.js API route using AI SDK v6 with `streamText()` and tools
|
||||
|
||||
## Repo Context
|
||||
|
||||
- Monorepo using Turborepo + pnpm workspaces
|
||||
- Next.js 16 App Router with Turbopack
|
||||
- TypeScript strict mode
|
||||
- All UI components from internal `@tpmjs/ui` package
|
||||
- Tools are imported from workspace package `@tpmjs/hello`
|
||||
400
USECHAT_INPUT_UNDEFINED.md
Normal file
400
USECHAT_INPUT_UNDEFINED.md
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
# 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:**
|
||||
```typescript
|
||||
<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`
|
||||
|
||||
```typescript
|
||||
'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`
|
||||
|
||||
```typescript
|
||||
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`
|
||||
|
||||
```typescript
|
||||
'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`
|
||||
|
||||
```typescript
|
||||
'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`
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [AI SDK Core: streamText](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text)
|
||||
- [AI SDK React: useChat](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat)
|
||||
- [AI SDK UI: Stream Protocol](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol)
|
||||
|
||||
## 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.
|
||||
2
apps/playground/.env.local.example
Normal file
2
apps/playground/.env.local.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Required: OpenAI API key
|
||||
OPENAI_API_KEY=sk-...
|
||||
6
apps/playground/next-env.d.ts
vendored
Normal file
6
apps/playground/next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
8
apps/playground/next.config.ts
Normal file
8
apps/playground/next.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: ['@tpmjs/ui', '@tpmjs/utils', '@tpmjs/types', '@tpmjs/env'],
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
46
apps/playground/package.json
Normal file
46
apps/playground/package.json
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "@tpmjs/playground",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3001",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint .",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf .next .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "3.0.0-beta.74",
|
||||
"@ai-sdk/react": "^2.0.106",
|
||||
"@tpmjs/env": "workspace:*",
|
||||
"@tpmjs/hello": "workspace:*",
|
||||
"@tpmjs/types": "workspace:*",
|
||||
"@tpmjs/ui": "workspace:*",
|
||||
"@tpmjs/utils": "workspace:*",
|
||||
"ai": "6.0.0-beta.124",
|
||||
"firecrawl-aisdk": "^0.7.2",
|
||||
"fish-joke-generator": "^1.1.0",
|
||||
"next": "^16.0.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"openai": "^6.9.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"zod": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tpmjs/eslint-config": "workspace:*",
|
||||
"@tpmjs/tailwind-config": "workspace:*",
|
||||
"@tpmjs/tsconfig": "workspace:*",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^16.0.4",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
6
apps/playground/postcss.config.mjs
Normal file
6
apps/playground/postcss.config.mjs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
70
apps/playground/src/app/api/chat/route.ts
Normal file
70
apps/playground/src/app/api/chat/route.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { type UIMessage, convertToModelMessages, stepCountIs, streamText } 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 body = await request.json();
|
||||
console.log('Request body:', JSON.stringify(body, null, 2));
|
||||
|
||||
const messages: UIMessage[] = body.messages || [];
|
||||
|
||||
// Load all available TPMJS tools
|
||||
const tools = await loadAllTools();
|
||||
|
||||
// Create system prompt listing available tools
|
||||
const toolsList = Object.keys(tools)
|
||||
.map((name) => {
|
||||
const tool = tools[name] as { description?: string } | undefined;
|
||||
return `- ${name}: ${tool?.description || 'No 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' },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
3
apps/playground/src/app/globals.css
Normal file
3
apps/playground/src/app/globals.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
42
apps/playground/src/app/layout.tsx
Normal file
42
apps/playground/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import { Space_Grotesk, Space_Mono } from 'next/font/google';
|
||||
import './globals.css';
|
||||
|
||||
const spaceGrotesk = Space_Grotesk({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-sans',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const spaceMono = Space_Mono({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '700'],
|
||||
variable: '--font-mono',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'TPMJS Playground - Test AI Tools',
|
||||
description: 'Interactive playground for testing TPMJS tools with AI agents',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
suppressHydrationWarning
|
||||
className={`${spaceGrotesk.variable} ${spaceMono.variable}`}
|
||||
>
|
||||
<body>
|
||||
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
22
apps/playground/src/app/page.tsx
Normal file
22
apps/playground/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
'use client';
|
||||
|
||||
import { ChatHeader } from '~/components/chat/ChatHeader';
|
||||
import { ChatInterface } from '~/components/chat/ChatInterface';
|
||||
import { ToolsSidebar } from '~/components/sidebar/ToolsSidebar';
|
||||
|
||||
export default function PlaygroundPage(): React.ReactElement {
|
||||
const handleClearChat = () => {
|
||||
// Refresh the page to clear chat
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col bg-background">
|
||||
<ChatHeader onClear={handleClearChat} />
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<ToolsSidebar />
|
||||
<ChatInterface />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
apps/playground/src/components/chat/ChatHeader.tsx
Normal file
52
apps/playground/src/components/chat/ChatHeader.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { useTheme } from 'next-themes';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface ChatHeaderProps {
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export function ChatHeader({ onClear }: ChatHeaderProps): React.ReactElement {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Avoid hydration mismatch
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="border-b border-border bg-background px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-xl font-bold">TPMJS Playground</h1>
|
||||
<Link
|
||||
href="https://tpmjs.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-foreground-secondary hover:text-foreground"
|
||||
>
|
||||
View Registry →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && (
|
||||
<Button variant="ghost" onClick={toggleTheme} size="md">
|
||||
{theme === 'dark' ? '☀️' : '🌙'}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" onClick={onClear} size="md">
|
||||
Clear Chat
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
52
apps/playground/src/components/chat/ChatInput.tsx
Normal file
52
apps/playground/src/components/chat/ChatInput.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
'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>
|
||||
);
|
||||
}
|
||||
38
apps/playground/src/components/chat/ChatInterface.tsx
Normal file
38
apps/playground/src/components/chat/ChatInterface.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useChat } from '~/hooks/useChat';
|
||||
import { ChatInput } from './ChatInput';
|
||||
import { ChatMessages } from './ChatMessages';
|
||||
|
||||
export function ChatInterface(): React.ReactElement {
|
||||
const { messages, sendMessage, status } = useChat();
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const isLoading = status !== 'ready';
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setInput(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (input.trim() && status === 'ready') {
|
||||
sendMessage({ text: input });
|
||||
setInput('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<ChatMessages messages={messages} />
|
||||
<ChatInput
|
||||
input={input}
|
||||
isLoading={isLoading}
|
||||
onInputChange={handleInputChange}
|
||||
onSubmit={handleSubmit}
|
||||
setInput={setInput}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
apps/playground/src/components/chat/ChatMessages.tsx
Normal file
50
apps/playground/src/components/chat/ChatMessages.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { MessageBubble } from './MessageBubble';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
createdAt?: Date;
|
||||
toolInvocations?: Array<{
|
||||
toolName: string;
|
||||
args: unknown;
|
||||
result?: unknown;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ChatMessagesProps {
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export function ChatMessages({ messages }: ChatMessagesProps): React.ReactElement {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
});
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-foreground-secondary">
|
||||
<div className="mb-2 text-4xl">🐟</div>
|
||||
<p className="text-lg">Start chatting to test TPMJS tools</p>
|
||||
<p className="mt-2 text-sm">Try asking: "Tell me a fish joke"</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||
{messages.map((message) => (
|
||||
<MessageBubble key={message.id} message={message} />
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
125
apps/playground/src/components/chat/MessageBubble.tsx
Normal file
125
apps/playground/src/components/chat/MessageBubble.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
'use client';
|
||||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Card, CardContent } from '@tpmjs/ui/Card/Card';
|
||||
|
||||
interface MessagePart {
|
||||
type: string; // Can be 'text', 'tool-{toolName}', 'step-start', etc.
|
||||
text?: string;
|
||||
toolCallId?: string;
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
state?: string;
|
||||
providerMetadata?: unknown;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
parts?: MessagePart[];
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
interface MessageBubbleProps {
|
||||
message: Message;
|
||||
}
|
||||
|
||||
export function MessageBubble({ message }: MessageBubbleProps): React.ReactElement {
|
||||
const isUser = message.role === 'user';
|
||||
|
||||
// Debug: Log message structure
|
||||
console.log('Message:', {
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
parts: message.parts,
|
||||
fullMessage: message,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
|
||||
<Card variant={isUser ? 'elevated' : 'outline'} className="w-full max-w-2xl">
|
||||
<CardContent className="p-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Badge variant={isUser ? 'default' : 'secondary'} size="sm">
|
||||
{isUser ? 'You' : 'AI'}
|
||||
</Badge>
|
||||
<span className="text-xs text-foreground-tertiary">
|
||||
{new Date(message.createdAt || Date.now()).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Render message parts */}
|
||||
{message.parts && message.parts.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{message.parts.map((part, idx) => {
|
||||
// Skip step-start markers
|
||||
if (part.type === 'step-start') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Render text parts
|
||||
if (part.type === 'text') {
|
||||
return (
|
||||
<div key={`text-${message.id}-${idx}`} className="whitespace-pre-wrap text-sm">
|
||||
{part.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render tool calls (type starts with 'tool-')
|
||||
if (part.type.startsWith('tool-')) {
|
||||
const toolName = part.type.replace('tool-', '');
|
||||
return (
|
||||
<div
|
||||
key={part.toolCallId || idx}
|
||||
className="rounded border border-amber-500/20 bg-amber-500/5 p-3"
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-2 border-b border-amber-500/20 pb-2">
|
||||
<span className="text-base">🔧</span>
|
||||
<strong className="font-mono text-sm text-foreground">{toolName}</strong>
|
||||
{part.state && (
|
||||
<Badge variant="secondary" size="sm">
|
||||
{part.state}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tool Input */}
|
||||
<div className="mb-3">
|
||||
<div className="mb-1 text-xs font-semibold text-foreground-secondary">
|
||||
Input:
|
||||
</div>
|
||||
<pre className="overflow-x-auto rounded bg-surface p-2 text-xs text-foreground-secondary">
|
||||
{JSON.stringify(part.input, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Tool Output */}
|
||||
{part.output && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold text-foreground-secondary">
|
||||
Output:
|
||||
</div>
|
||||
<pre className="overflow-x-auto rounded bg-surface p-2 text-xs text-foreground-secondary">
|
||||
{JSON.stringify(part.output, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
// Fallback to content if no parts
|
||||
<div className="whitespace-pre-wrap text-sm">{message.content || '...'}</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
apps/playground/src/components/sidebar/ToolsSidebar.tsx
Normal file
63
apps/playground/src/components/sidebar/ToolsSidebar.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
'use client';
|
||||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
|
||||
|
||||
// Hardcoded list of installed tools
|
||||
const INSTALLED_TOOLS = [
|
||||
{
|
||||
name: 'hello-world',
|
||||
description: 'Returns a simple "Hello, World!" greeting',
|
||||
category: 'text-analysis',
|
||||
},
|
||||
{
|
||||
name: 'hello-name',
|
||||
description: 'Returns a personalized greeting with a name',
|
||||
category: 'text-analysis',
|
||||
},
|
||||
{
|
||||
name: 'firecrawl (scrape)',
|
||||
description: 'Scrape content from any URL',
|
||||
category: 'web-scraping',
|
||||
},
|
||||
{
|
||||
name: 'firecrawl (crawl)',
|
||||
description: 'Crawl entire websites recursively',
|
||||
category: 'web-scraping',
|
||||
},
|
||||
{
|
||||
name: 'firecrawl (search)',
|
||||
description: 'Search the web for content',
|
||||
category: 'web-scraping',
|
||||
},
|
||||
];
|
||||
|
||||
export function ToolsSidebar(): React.ReactElement {
|
||||
return (
|
||||
<aside className="hidden w-64 border-r border-border bg-surface md:block">
|
||||
<div className="p-4">
|
||||
<h2 className="mb-4 text-lg font-bold">
|
||||
Available Tools <Badge variant="secondary">{INSTALLED_TOOLS.length}</Badge>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-2">
|
||||
{INSTALLED_TOOLS.map((tool) => (
|
||||
<Card key={tool.name} variant="outline" className="cursor-pointer hover:bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">{tool.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-foreground-secondary">{tool.description}</p>
|
||||
<div className="mt-2">
|
||||
<Badge variant="secondary" size="sm">
|
||||
{tool.category}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
7
apps/playground/src/env.ts
Normal file
7
apps/playground/src/env.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { createEnv } from '@tpmjs/env';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const env = createEnv({
|
||||
// Server-only
|
||||
OPENAI_API_KEY: z.string().min(1),
|
||||
});
|
||||
16
apps/playground/src/hooks/useChat.ts
Normal file
16
apps/playground/src/hooks/useChat.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
'use client';
|
||||
|
||||
import { useChat as useAISDKChat } from '@ai-sdk/react';
|
||||
import { DefaultChatTransport } from 'ai';
|
||||
|
||||
/**
|
||||
* Custom chat hook that wraps the official @ai-sdk/react useChat
|
||||
* Handles SSE streaming with tool calls and UI message protocol
|
||||
*/
|
||||
export function useChat() {
|
||||
return useAISDKChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: '/api/chat',
|
||||
}),
|
||||
});
|
||||
}
|
||||
46
apps/playground/src/hooks/useToolUsage.ts
Normal file
46
apps/playground/src/hooks/useToolUsage.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { ToolUsageStats } from '~/lib/types';
|
||||
|
||||
export function useToolUsage() {
|
||||
const [toolUsage, setToolUsage] = useState<Map<string, ToolUsageStats>>(new Map());
|
||||
|
||||
const trackTool = useCallback((packageName: string) => {
|
||||
setToolUsage((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existing = newMap.get(packageName);
|
||||
|
||||
if (existing) {
|
||||
newMap.set(packageName, {
|
||||
...existing,
|
||||
callCount: existing.callCount + 1,
|
||||
lastCalledAt: new Date(),
|
||||
});
|
||||
} else {
|
||||
newMap.set(packageName, {
|
||||
packageName,
|
||||
callCount: 1,
|
||||
lastCalledAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
return newMap;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const clearUsage = useCallback(() => {
|
||||
setToolUsage(new Map());
|
||||
}, []);
|
||||
|
||||
// Convert Map to array sorted by most recent first
|
||||
const toolUsageArray = Array.from(toolUsage.values()).sort(
|
||||
(a, b) => b.lastCalledAt.getTime() - a.lastCalledAt.getTime()
|
||||
);
|
||||
|
||||
return {
|
||||
toolUsage: toolUsageArray,
|
||||
trackTool,
|
||||
clearUsage,
|
||||
};
|
||||
}
|
||||
92
apps/playground/src/lib/tool-loader.ts
Normal file
92
apps/playground/src/lib/tool-loader.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// Static imports for tools (required for Next.js/webpack)
|
||||
import { helloNameTool, helloWorldTool } from '@tpmjs/hello';
|
||||
import { crawlTool, scrapeTool, searchTool } from 'firecrawl-aisdk';
|
||||
|
||||
/**
|
||||
* Load a specific TPMJS tool by package name
|
||||
*/
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here
|
||||
export async function loadTpmjsTool(packageName: string): Promise<Record<string, any>> {
|
||||
try {
|
||||
// Map package names to their tool functions
|
||||
switch (packageName) {
|
||||
case '@tpmjs/hello':
|
||||
// Hello has multiple tools, return all of them
|
||||
return {
|
||||
helloWorld: helloWorldTool,
|
||||
helloName: helloNameTool,
|
||||
};
|
||||
|
||||
case 'firecrawl-aisdk':
|
||||
// Firecrawl has multiple tools, return all of them
|
||||
return {
|
||||
scrapeTool,
|
||||
crawlTool,
|
||||
searchTool,
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool package: ${packageName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to load tool from package ${packageName}: ${error.message}`);
|
||||
}
|
||||
throw new Error(`Failed to load tool from package ${packageName}: Unknown error`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a valid AI SDK tool
|
||||
*/
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here
|
||||
function isCoreTool(obj: unknown): obj is Record<string, any> {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tool = obj as Record<string, unknown>;
|
||||
|
||||
// Check for required AI SDK tool properties
|
||||
return (
|
||||
typeof tool.description === 'string' &&
|
||||
typeof tool.parameters === 'object' &&
|
||||
typeof tool.execute === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all installed TPMJS tools
|
||||
*
|
||||
* For now, this is a manual list. In the future, we can scan node_modules
|
||||
* for packages with the "tpmjs-tool" keyword.
|
||||
*/
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here
|
||||
export async function loadAllTools(): Promise<Record<string, any>> {
|
||||
const installedTools = ['@tpmjs/hello', 'firecrawl-aisdk'];
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here
|
||||
const tools: Record<string, any> = {};
|
||||
|
||||
for (const packageName of installedTools) {
|
||||
try {
|
||||
const tool = await loadTpmjsTool(packageName);
|
||||
|
||||
// If the tool returns an object with multiple tools (like firecrawl), spread them
|
||||
if (tool && typeof tool === 'object' && !tool.description) {
|
||||
Object.assign(tools, tool);
|
||||
} else {
|
||||
// Single tool - use a cleaned name (remove hyphens, camelCase)
|
||||
const toolName = packageName
|
||||
.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())
|
||||
.replace(/-/g, '');
|
||||
tools[toolName] = tool;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load tool ${packageName}:`, error);
|
||||
// Continue loading other tools even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
37
apps/playground/src/lib/types.ts
Normal file
37
apps/playground/src/lib/types.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
export type MessageRole = 'user' | 'assistant' | 'system';
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: MessageRole;
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
toolCalls?: ToolCallInfo[];
|
||||
}
|
||||
|
||||
export interface ToolCallInfo {
|
||||
id: string;
|
||||
toolName: string;
|
||||
parameters: Record<string, unknown>;
|
||||
result?: unknown;
|
||||
status?: 'pending' | 'success' | 'error';
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface TokenUsage {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
totalTokens: number;
|
||||
estimatedCost: number;
|
||||
}
|
||||
|
||||
export interface ToolUsageStats {
|
||||
packageName: string;
|
||||
callCount: number;
|
||||
lastCalledAt: Date;
|
||||
}
|
||||
|
||||
export type SSEEvent =
|
||||
| { type: 'chunk'; data: { text: string } }
|
||||
| { type: 'tool-call'; data: ToolCallInfo }
|
||||
| { type: 'complete'; data: { tokenUsage?: TokenUsage } }
|
||||
| { type: 'error'; data: { message: string } };
|
||||
12
apps/playground/tailwind.config.ts
Normal file
12
apps/playground/tailwind.config.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import baseConfig from '@tpmjs/tailwind-config/base';
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
export default {
|
||||
...baseConfig,
|
||||
content: [
|
||||
'./src/app/**/*.{ts,tsx}',
|
||||
'./src/components/**/*.{ts,tsx}',
|
||||
'../../packages/ui/src/**/*.ts',
|
||||
],
|
||||
plugins: [...(baseConfig.plugins || []), require('@tailwindcss/typography')],
|
||||
} satisfies Config;
|
||||
12
apps/playground/tsconfig.json
Normal file
12
apps/playground/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "@tpmjs/tsconfig/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
80
packages/tools/hello/README.md
Normal file
80
packages/tools/hello/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# @tpmjs/hello
|
||||
|
||||
Simple example TPMJS tools for AI SDK v6 - demonstrates how to create tools that work with AI agents.
|
||||
|
||||
## Tools
|
||||
|
||||
### helloWorldTool
|
||||
Returns a simple "Hello, World!" greeting with a timestamp.
|
||||
|
||||
**Parameters:** None
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"message": "Hello, World!",
|
||||
"timestamp": "2024-12-04T..."
|
||||
}
|
||||
```
|
||||
|
||||
### helloNameTool
|
||||
Returns a personalized greeting with the provided name.
|
||||
|
||||
**Parameters:**
|
||||
- `name` (string, required): The name of the person to greet
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"message": "Hello, John!",
|
||||
"timestamp": "2024-12-04T..."
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### With AI SDK v6
|
||||
|
||||
```typescript
|
||||
import { streamText } from 'ai';
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import { helloWorldTool, helloNameTool } from '@tpmjs/hello';
|
||||
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o-mini'),
|
||||
messages: [{ role: 'user', content: 'Say hello to Alice' }],
|
||||
tools: {
|
||||
helloWorld: helloWorldTool,
|
||||
helloName: helloNameTool,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Template for Creating TPMJS Tools
|
||||
|
||||
This package serves as a template for creating your own TPMJS tools. Key requirements:
|
||||
|
||||
1. **Use AI SDK v6 Beta** (`ai@6.0.0-beta.124`)
|
||||
2. **Use Zod 4** for parameter validation
|
||||
3. **Export tool objects** with `description`, `parameters`, and `execute`
|
||||
4. **Add TPMJS metadata** to package.json:
|
||||
```json
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Your tool description"
|
||||
}
|
||||
```
|
||||
5. **Include the `tpmjs-tool` keyword** in package.json
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Build
|
||||
pnpm build
|
||||
|
||||
# Type check
|
||||
pnpm type-check
|
||||
|
||||
# Watch mode
|
||||
pnpm dev
|
||||
```
|
||||
27
packages/tools/hello/package.json
Normal file
27
packages/tools/hello/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "@tpmjs/hello",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "Example TPMJS tools - Hello World and Hello Name",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": ["tpmjs-tool", "ai-sdk", "hello", "example"],
|
||||
"tpmjs": {
|
||||
"category": "text-analysis",
|
||||
"description": "Simple greeting tools - Hello World and personalized Hello Name greetings"
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "6.0.0-beta.124"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tpmjs/tsconfig": "workspace:*",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"files": ["dist", "README.md"]
|
||||
}
|
||||
75
packages/tools/hello/src/index.ts
Normal file
75
packages/tools/hello/src/index.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { jsonSchema, tool } from 'ai';
|
||||
|
||||
/**
|
||||
* Input type for Hello World Tool
|
||||
*/
|
||||
type HelloWorldInput = {
|
||||
includeTimestamp?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hello World Tool
|
||||
* Returns a simple "Hello, World!" greeting
|
||||
*
|
||||
* This is a proper AI SDK v6 tool that can be used with streamText()
|
||||
* Uses jsonSchema() to avoid Zod 4 JSON Schema conversion issues with OpenAI
|
||||
*/
|
||||
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: Record<string, unknown> = {
|
||||
message: 'Hello, World!',
|
||||
};
|
||||
|
||||
if (includeTimestamp) {
|
||||
response.timestamp = new Date().toISOString();
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Input type for Hello Name Tool
|
||||
*/
|
||||
type HelloNameInput = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hello Name Tool
|
||||
* Returns a personalized greeting with the provided name
|
||||
*
|
||||
* This is a proper AI SDK v6 tool that can be used with streamText()
|
||||
* Uses jsonSchema() to avoid Zod 4 JSON Schema conversion issues with OpenAI
|
||||
*/
|
||||
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(),
|
||||
};
|
||||
},
|
||||
});
|
||||
20
packages/tools/hello/tsconfig.json
Normal file
20
packages/tools/hello/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
23
packages/tools/package.json
Normal file
23
packages/tools/package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "@tpmjs/tools",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Example TPMJS tools for AI SDK v6",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": ["tpmjs-tool", "ai-sdk", "example"],
|
||||
"dependencies": {
|
||||
"ai": "6.0.0-beta.124",
|
||||
"zod": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tpmjs/tsconfig": "workspace:*",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
37
packages/tools/src/index.ts
Normal file
37
packages/tools/src/index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Hello World Tool
|
||||
* Returns a simple "Hello, World!" greeting
|
||||
*
|
||||
* This is a proper AI SDK v6 tool that can be used with streamText()
|
||||
*/
|
||||
export const helloWorldTool = {
|
||||
description: 'Returns a simple "Hello, World!" greeting',
|
||||
parameters: z.object({}),
|
||||
execute: async () => {
|
||||
return {
|
||||
message: 'Hello, World!',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Hello Name Tool
|
||||
* Returns a personalized greeting with the provided name
|
||||
*
|
||||
* This is a proper AI SDK v6 tool that can be used with streamText()
|
||||
*/
|
||||
export const helloNameTool = {
|
||||
description: 'Returns a personalized greeting with the provided name',
|
||||
parameters: z.object({
|
||||
name: z.string().describe('The name of the person to greet'),
|
||||
}),
|
||||
execute: async ({ name }: { name: string }) => {
|
||||
return {
|
||||
message: `Hello, ${name}!`,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
};
|
||||
11
packages/tools/tsconfig.json
Normal file
11
packages/tools/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "@tpmjs/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
457
pnpm-lock.yaml
generated
457
pnpm-lock.yaml
generated
|
|
@ -39,6 +39,97 @@ importers:
|
|||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
|
||||
apps/playground:
|
||||
dependencies:
|
||||
'@ai-sdk/openai':
|
||||
specifier: 3.0.0-beta.74
|
||||
version: 3.0.0-beta.74(effect@3.18.4)(zod@4.1.13)
|
||||
'@ai-sdk/react':
|
||||
specifier: ^2.0.106
|
||||
version: 2.0.106(react@19.2.0)(zod@4.1.13)
|
||||
'@tpmjs/env':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/env
|
||||
'@tpmjs/hello':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/tools/hello
|
||||
'@tpmjs/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/types
|
||||
'@tpmjs/ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui
|
||||
'@tpmjs/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/utils
|
||||
ai:
|
||||
specifier: 6.0.0-beta.124
|
||||
version: 6.0.0-beta.124(effect@3.18.4)(zod@4.1.13)
|
||||
firecrawl-aisdk:
|
||||
specifier: ^0.7.2
|
||||
version: 0.7.2
|
||||
fish-joke-generator:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(openai@6.9.1(ws@8.18.3)(zod@4.1.13))(zod@4.1.13)
|
||||
next:
|
||||
specifier: ^16.0.4
|
||||
version: 16.0.4(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
openai:
|
||||
specifier: ^6.9.1
|
||||
version: 6.9.1(ws@8.18.3)(zod@4.1.13)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.0
|
||||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.0(react@19.2.0)
|
||||
zod:
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.13
|
||||
devDependencies:
|
||||
'@tailwindcss/typography':
|
||||
specifier: ^0.5.19
|
||||
version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6))
|
||||
'@tpmjs/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/config/eslint
|
||||
'@tpmjs/tailwind-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/config/tailwind
|
||||
'@tpmjs/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/config/tsconfig
|
||||
'@types/node':
|
||||
specifier: ^22.10.2
|
||||
version: 22.19.1
|
||||
'@types/react':
|
||||
specifier: ^19.0.2
|
||||
version: 19.2.7
|
||||
'@types/react-dom':
|
||||
specifier: ^19.0.2
|
||||
version: 19.2.3(@types/react@19.2.7)
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.22(postcss@8.5.6)
|
||||
eslint:
|
||||
specifier: ^9.39.1
|
||||
version: 9.39.1(jiti@1.21.7)
|
||||
eslint-config-next:
|
||||
specifier: ^16.0.4
|
||||
version: 16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
|
||||
postcss:
|
||||
specifier: ^8.5.1
|
||||
version: 8.5.6
|
||||
tailwindcss:
|
||||
specifier: ^3.4.17
|
||||
version: 3.4.18(tsx@4.20.6)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
|
||||
apps/web:
|
||||
dependencies:
|
||||
'@ai-sdk/openai':
|
||||
|
|
@ -131,10 +222,10 @@ importers:
|
|||
version: 10.4.22(postcss@8.5.6)
|
||||
eslint:
|
||||
specifier: ^9.39.1
|
||||
version: 9.39.1(jiti@1.21.7)
|
||||
version: 9.39.1(jiti@2.6.1)
|
||||
eslint-config-next:
|
||||
specifier: ^16.0.4
|
||||
version: 16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3)
|
||||
version: 16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
postcss:
|
||||
specifier: ^8.5.1
|
||||
version: 8.5.6
|
||||
|
|
@ -368,6 +459,22 @@ importers:
|
|||
specifier: ^4.1.13
|
||||
version: 4.1.13
|
||||
|
||||
packages/tools:
|
||||
dependencies:
|
||||
ai:
|
||||
specifier: 6.0.0-beta.124
|
||||
version: 6.0.0-beta.124(effect@3.18.4)(zod@4.1.13)
|
||||
zod:
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.13
|
||||
devDependencies:
|
||||
'@tpmjs/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:../config/tsconfig
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/tools/createBlogPost:
|
||||
dependencies:
|
||||
zod:
|
||||
|
|
@ -384,6 +491,19 @@ importers:
|
|||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/tools/hello:
|
||||
dependencies:
|
||||
ai:
|
||||
specifier: 6.0.0-beta.124
|
||||
version: 6.0.0-beta.124(effect@3.18.4)(zod@4.1.13)
|
||||
devDependencies:
|
||||
'@tpmjs/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:../../config/tsconfig
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/types:
|
||||
dependencies:
|
||||
zod:
|
||||
|
|
@ -497,12 +617,24 @@ packages:
|
|||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/gateway@2.0.18':
|
||||
resolution: {integrity: sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/openai@3.0.0-beta.74':
|
||||
resolution: {integrity: sha512-0AofFL0odf7dUjmpiKVBxemXWK7L5YTmqD+9sY3V/0yzxtWuP8effQVOTz3rXO7kSF8OabDxW4WUHGwAOBiIJA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.18':
|
||||
resolution: {integrity: sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider-utils@4.0.0-beta.40':
|
||||
resolution: {integrity: sha512-5345iQxWV1coKbs85vkrThsEmiFcIkgqMZUKFrVDM7E4FRqgsnWtn4394/RnShOKRun5K7TWIYCLz3GfRGy/Ig==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -519,10 +651,24 @@ packages:
|
|||
effect:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/provider@2.0.0':
|
||||
resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/provider@3.0.0-beta.22':
|
||||
resolution: {integrity: sha512-Ss0tgCZwzccS6MREhjAI28lkAV5PfJnoBFbv8mas74Cs5OseFKqxg3dEd1lRL8mf4Qapu1xpBLkV4/ldSShSdA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/react@2.0.106':
|
||||
resolution: {integrity: sha512-TU8ONNhm64GI7O60UDCcOz9CdyCp3emQwSYrSnq+QWBNgS8vDlRQ3ZwXyPNAJQdXyBTafVS2iyS0kvV+KXaPAQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -1449,6 +1595,10 @@ packages:
|
|||
'@types/react': '>=16'
|
||||
react: '>=16'
|
||||
|
||||
'@mendable/firecrawl-js@4.8.1':
|
||||
resolution: {integrity: sha512-40fW83AuGziIrhONyexdc/EvfSUFvlqgcwmpAoUxY7yAl84kfSbqTgxJ/5ycd4SoIDW2plQOXzC/rxwtwFUnYg==}
|
||||
engines: {node: '>=22.0.0'}
|
||||
|
||||
'@mswjs/interceptors@0.40.0':
|
||||
resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -2325,6 +2475,12 @@ packages:
|
|||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ai@5.0.106:
|
||||
resolution: {integrity: sha512-M5obwavxSJJ3tGlAFqI6eltYNJB0D20X6gIBCFx/KVorb/X1fxVVfiZZpZb+Gslu4340droSOjT0aKQFCarNVg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
ai@6.0.0-beta.124:
|
||||
resolution: {integrity: sha512-Apl4uNZLzc4sAUtu4W77DpM9o+bJJB46A/ayVLDck3MBv+rw4/qGBY9H6/gY9Xpc/CeVogyyEr1DxBMv47olCw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -2435,6 +2591,9 @@ packages:
|
|||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
autoprefixer@10.4.22:
|
||||
resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
|
@ -2450,6 +2609,9 @@ packages:
|
|||
resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
axios@1.13.2:
|
||||
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
|
||||
|
||||
axobject-query@4.1.0:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -2606,6 +2768,10 @@ packages:
|
|||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
|
|
@ -2713,6 +2879,10 @@ packages:
|
|||
defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
dependency-cruiser@17.3.1:
|
||||
resolution: {integrity: sha512-yWwszB4GKIBKK/xiHSQ6TVIV6k8byd+gMGT2RMQ+03wb1jGH48cSsLH29iUUZgGtKyLSH51NurbnjXV0+niUjA==}
|
||||
engines: {node: ^20.12||^22||>=24}
|
||||
|
|
@ -3081,6 +3251,17 @@ packages:
|
|||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
firecrawl-aisdk@0.7.2:
|
||||
resolution: {integrity: sha512-JyqKs12ScYcKmHnvkc/h/ItvcJxLjxn1WkFTe6w6RVb/LXVw7gqjOuRg2bvfsZeeqlTOyiLAzy5tbj4eMOgjRA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
fish-joke-generator@1.1.0:
|
||||
resolution: {integrity: sha512-w5NE7hIVEOBRcIYIc8AhOJyQSFhDfJF7/0www2p5g3t3XcQuYpRjQ8sclw601eXqrygBPbYchSM0+KfdlpESNg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
openai: ^6.0.0
|
||||
zod: ^4.0.0
|
||||
|
||||
fix-dts-default-cjs-exports@1.0.1:
|
||||
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
|
||||
|
||||
|
|
@ -3091,6 +3272,15 @@ packages:
|
|||
flatted@3.3.3:
|
||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||
|
||||
follow-redirects@1.15.11:
|
||||
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
for-each@0.3.5:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -3099,6 +3289,10 @@ packages:
|
|||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
format@0.2.2:
|
||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||
engines: {node: '>=0.4.x'}
|
||||
|
|
@ -3868,6 +4062,14 @@ packages:
|
|||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mimic-function@5.0.1:
|
||||
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4271,6 +4473,9 @@ packages:
|
|||
property-information@7.1.0:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -4658,6 +4863,11 @@ packages:
|
|||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
swr@2.3.7:
|
||||
resolution: {integrity: sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==}
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
tagged-tag@1.0.0:
|
||||
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||
engines: {node: '>=20'}
|
||||
|
|
@ -4685,6 +4895,10 @@ packages:
|
|||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
throttleit@2.1.0:
|
||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
|
|
@ -4873,6 +5087,9 @@ packages:
|
|||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
typescript-event-target@1.1.1:
|
||||
resolution: {integrity: sha512-dFSOFBKV6uwaloBCCUhxlD3Pr/P1a/tJdcmPrTXCHlEFD3faj0mztjcGn6VBAhQ0/Bdy8K3VWrrqwbt/ffsYsg==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
|
|
@ -4929,6 +5146,11 @@ packages:
|
|||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
|
|
@ -5149,12 +5371,20 @@ packages:
|
|||
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
zod-to-json-schema@3.25.0:
|
||||
resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==}
|
||||
peerDependencies:
|
||||
zod: ^3.25 || ^4
|
||||
|
||||
zod-validation-error@4.0.2:
|
||||
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
zod@4.1.13:
|
||||
resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==}
|
||||
|
||||
|
|
@ -5176,6 +5406,20 @@ snapshots:
|
|||
- arktype
|
||||
- effect
|
||||
|
||||
'@ai-sdk/gateway@2.0.18(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.18(zod@3.25.76)
|
||||
'@vercel/oidc': 3.0.5
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/gateway@2.0.18(zod@4.1.13)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.18(zod@4.1.13)
|
||||
'@vercel/oidc': 3.0.5
|
||||
zod: 4.1.13
|
||||
|
||||
'@ai-sdk/openai@3.0.0-beta.74(effect@3.18.4)(zod@4.1.13)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.0-beta.22
|
||||
|
|
@ -5186,6 +5430,20 @@ snapshots:
|
|||
- arktype
|
||||
- effect
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.18(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@standard-schema/spec': 1.0.0
|
||||
eventsource-parser: 3.0.6
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.18(zod@4.1.13)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@standard-schema/spec': 1.0.0
|
||||
eventsource-parser: 3.0.6
|
||||
zod: 4.1.13
|
||||
|
||||
'@ai-sdk/provider-utils@4.0.0-beta.40(effect@3.18.4)(zod@4.1.13)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.0-beta.22
|
||||
|
|
@ -5195,10 +5453,24 @@ snapshots:
|
|||
optionalDependencies:
|
||||
effect: 3.18.4
|
||||
|
||||
'@ai-sdk/provider@2.0.0':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/provider@3.0.0-beta.22':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@2.0.106(react@19.2.0)(zod@4.1.13)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 3.0.18(zod@4.1.13)
|
||||
ai: 5.0.106(zod@4.1.13)
|
||||
react: 19.2.0
|
||||
swr: 2.3.7(react@19.2.0)
|
||||
throttleit: 2.1.0
|
||||
optionalDependencies:
|
||||
zod: 4.1.13
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
|
|
@ -5982,6 +6254,15 @@ snapshots:
|
|||
'@types/react': 19.2.7
|
||||
react: 19.2.0
|
||||
|
||||
'@mendable/firecrawl-js@4.8.1':
|
||||
dependencies:
|
||||
axios: 1.13.2
|
||||
typescript-event-target: 1.1.1
|
||||
zod: 3.25.76
|
||||
zod-to-json-schema: 3.25.0(zod@3.25.76)
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
'@mswjs/interceptors@0.40.0':
|
||||
dependencies:
|
||||
'@open-draft/deferred-promise': 2.2.0
|
||||
|
|
@ -6889,6 +7170,22 @@ snapshots:
|
|||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
ai@5.0.106(zod@3.25.76):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 2.0.18(zod@3.25.76)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.18(zod@3.25.76)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
zod: 3.25.76
|
||||
|
||||
ai@5.0.106(zod@4.1.13):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 2.0.18(zod@4.1.13)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.18(zod@4.1.13)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
zod: 4.1.13
|
||||
|
||||
ai@6.0.0-beta.124(effect@3.18.4)(zod@4.1.13):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 2.0.0-beta.68(effect@3.18.4)(zod@4.1.13)
|
||||
|
|
@ -7029,6 +7326,8 @@ snapshots:
|
|||
|
||||
async-function@1.0.0: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
autoprefixer@10.4.22(postcss@8.5.6):
|
||||
dependencies:
|
||||
browserslist: 4.28.0
|
||||
|
|
@ -7045,6 +7344,14 @@ snapshots:
|
|||
|
||||
axe-core@4.11.0: {}
|
||||
|
||||
axios@1.13.2:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
form-data: 4.0.5
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
axobject-query@4.1.0: {}
|
||||
|
||||
bail@2.0.2: {}
|
||||
|
|
@ -7203,6 +7510,10 @@ snapshots:
|
|||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
commander@14.0.2: {}
|
||||
|
|
@ -7287,6 +7598,8 @@ snapshots:
|
|||
|
||||
defu@6.1.4: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
dependency-cruiser@17.3.1:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
|
@ -7586,7 +7899,7 @@ snapshots:
|
|||
eslint: 9.39.1(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@1.21.7))
|
||||
|
|
@ -7600,6 +7913,26 @@ snapshots:
|
|||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-config-next@16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 16.0.4
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@2.6.1))
|
||||
globals: 16.4.0
|
||||
typescript-eslint: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- '@typescript-eslint/parser'
|
||||
- eslint-import-resolver-webpack
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-node@0.3.9:
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
|
|
@ -7619,7 +7952,22 @@ snapshots:
|
|||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(eslint@9.39.1(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
get-tsconfig: 4.13.0
|
||||
is-bun-module: 2.0.0
|
||||
stable-hash: 0.0.5
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -7643,6 +7991,16 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
|
|
@ -7672,7 +8030,7 @@ snapshots:
|
|||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)):
|
||||
eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
|
|
@ -7699,6 +8057,33 @@ snapshots:
|
|||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
array.prototype.findlastindex: 1.2.6
|
||||
array.prototype.flat: 1.3.3
|
||||
array.prototype.flatmap: 1.3.3
|
||||
debug: 3.2.7
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
minimatch: 3.1.2
|
||||
object.fromentries: 2.0.8
|
||||
object.groupby: 1.0.3
|
||||
object.values: 1.2.1
|
||||
semver: 6.3.1
|
||||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.1(jiti@1.21.7)):
|
||||
dependencies:
|
||||
aria-query: 5.3.2
|
||||
|
|
@ -7752,6 +8137,17 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/parser': 7.28.5
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
hermes-parser: 0.25.1
|
||||
zod: 4.1.13
|
||||
zod-validation-error: 4.0.2(zod@4.1.13)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@1.21.7)):
|
||||
dependencies:
|
||||
array-includes: 3.1.9
|
||||
|
|
@ -7987,6 +8383,19 @@ snapshots:
|
|||
locate-path: 6.0.0
|
||||
path-exists: 4.0.0
|
||||
|
||||
firecrawl-aisdk@0.7.2:
|
||||
dependencies:
|
||||
'@mendable/firecrawl-js': 4.8.1
|
||||
ai: 5.0.106(zod@3.25.76)
|
||||
zod: 3.25.76
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
fish-joke-generator@1.1.0(openai@6.9.1(ws@8.18.3)(zod@4.1.13))(zod@4.1.13):
|
||||
dependencies:
|
||||
openai: 6.9.1(ws@8.18.3)(zod@4.1.13)
|
||||
zod: 4.1.13
|
||||
|
||||
fix-dts-default-cjs-exports@1.0.1:
|
||||
dependencies:
|
||||
magic-string: 0.30.21
|
||||
|
|
@ -8000,6 +8409,8 @@ snapshots:
|
|||
|
||||
flatted@3.3.3: {}
|
||||
|
||||
follow-redirects@1.15.11: {}
|
||||
|
||||
for-each@0.3.5:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
|
@ -8009,6 +8420,14 @@ snapshots:
|
|||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data@4.0.5:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
format@0.2.2: {}
|
||||
|
||||
formatly@0.3.0:
|
||||
|
|
@ -9014,6 +9433,12 @@ snapshots:
|
|||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
mimic-function@5.0.1: {}
|
||||
|
||||
min-indent@1.0.1: {}
|
||||
|
|
@ -9428,6 +9853,8 @@ snapshots:
|
|||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
pure-rand@6.1.0: {}
|
||||
|
|
@ -9952,6 +10379,12 @@ snapshots:
|
|||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
swr@2.3.7(react@19.2.0):
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
react: 19.2.0
|
||||
use-sync-external-store: 1.6.0(react@19.2.0)
|
||||
|
||||
tagged-tag@1.0.0: {}
|
||||
|
||||
tailwind-merge@2.6.0: {}
|
||||
|
|
@ -9996,6 +10429,8 @@ snapshots:
|
|||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
throttleit@2.1.0: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
|
@ -10214,6 +10649,8 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
typescript-event-target@1.1.1: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
ufo@1.6.1: {}
|
||||
|
|
@ -10303,6 +10740,10 @@ snapshots:
|
|||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
use-sync-external-store@1.6.0(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
util@0.12.5:
|
||||
|
|
@ -10511,10 +10952,16 @@ snapshots:
|
|||
|
||||
yoctocolors-cjs@2.1.3: {}
|
||||
|
||||
zod-to-json-schema@3.25.0(zod@3.25.76):
|
||||
dependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
zod-validation-error@4.0.2(zod@4.1.13):
|
||||
dependencies:
|
||||
zod: 4.1.13
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zod@4.1.13: {}
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue