fix: migrate playground to AI SDK v6 beta and resolve TypeScript errors
- Update @ai-sdk/react to v3.0.0-beta.131 for compatibility with AI SDK v6 - Fix tool execute method calls with type assertions in API routes - Update chat components to use UIMessage types from AI SDK - Move body option into DefaultChatTransport constructor - Remove deprecated onResponse option from useChat hook - Remove unused isCoreTool type guard function - Fix Button variant from 'primary' to 'default' Resolves all TypeScript compilation errors and build passes successfully. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
323c7496f0
commit
3aef5bb2bb
9 changed files with 596 additions and 154 deletions
|
|
@ -12,7 +12,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "3.0.0-beta.74",
|
||||
"@ai-sdk/react": "^2.0.106",
|
||||
"@ai-sdk/react": "3.0.0-beta.131",
|
||||
"@tpmjs/env": "workspace:*",
|
||||
"@tpmjs/hello": "workspace:*",
|
||||
"@tpmjs/search-registry": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
try {
|
||||
// biome-ignore lint/style/noNonNullAssertion: Tool created with tool() always has execute
|
||||
const searchResult = await searchTpmjsToolsTool.execute!(
|
||||
const result = await searchTpmjsToolsTool.execute!(
|
||||
{
|
||||
query: userQuery,
|
||||
limit: 5, // Get top 5 relevant tools
|
||||
|
|
@ -90,6 +90,13 @@ export async function POST(request: NextRequest) {
|
|||
{} as any
|
||||
);
|
||||
|
||||
// Type assertion: searchTpmjsToolsTool returns direct result, not AsyncIterable
|
||||
const searchResult = result as {
|
||||
query: string;
|
||||
matchCount: number;
|
||||
tools: any[];
|
||||
};
|
||||
|
||||
console.log(`📦 Found ${searchResult.matchCount} matching tools`);
|
||||
|
||||
if (searchResult.tools && searchResult.tools.length > 0) {
|
||||
|
|
|
|||
|
|
@ -7,15 +7,22 @@ export const dynamic = 'force-dynamic';
|
|||
export async function GET() {
|
||||
try {
|
||||
// Search for all tools (empty query returns all)
|
||||
const result = await searchTpmjsToolsTool.execute({
|
||||
query: '',
|
||||
limit: 100,
|
||||
});
|
||||
// biome-ignore lint/style/noNonNullAssertion: Tool created with tool() always has execute
|
||||
const result = await searchTpmjsToolsTool.execute!(
|
||||
{
|
||||
query: '',
|
||||
limit: 100,
|
||||
},
|
||||
{} as any
|
||||
);
|
||||
|
||||
// Type assertion: searchTpmjsToolsTool returns direct result, not AsyncIterable
|
||||
const searchResult = result as { query: string; matchCount: number; tools: any[] };
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
tools: result.tools,
|
||||
total: result.total,
|
||||
tools: searchResult.tools,
|
||||
total: searchResult.matchCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tools:', error);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import type { UIMessage } from 'ai';
|
||||
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[];
|
||||
messages: UIMessage[];
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,28 +2,11 @@
|
|||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Card, CardContent } from '@tpmjs/ui/Card/Card';
|
||||
import type { UIMessage } from 'ai';
|
||||
import { Streamdown } from 'streamdown';
|
||||
|
||||
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;
|
||||
message: UIMessage;
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -33,15 +16,6 @@ export function MessageBubble({
|
|||
}: 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">
|
||||
|
|
@ -51,7 +25,7 @@ export function MessageBubble({
|
|||
{isUser ? 'You' : 'AI'}
|
||||
</Badge>
|
||||
<span className="text-xs text-foreground-tertiary">
|
||||
{new Date(message.createdAt || Date.now()).toLocaleTimeString()}
|
||||
{new Date().toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -78,39 +52,43 @@ export function MessageBubble({
|
|||
// Render tool calls (type starts with 'tool-')
|
||||
if (part.type.startsWith('tool-')) {
|
||||
const toolName = part.type.replace('tool-', '');
|
||||
// Type assertion for tool parts
|
||||
const toolPart = part as any;
|
||||
return (
|
||||
<div
|
||||
key={part.toolCallId || idx}
|
||||
key={toolPart.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 && (
|
||||
{toolPart.state && (
|
||||
<Badge variant="secondary" size="sm">
|
||||
{part.state}
|
||||
{toolPart.state}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tool Input */}
|
||||
<div className="mb-3">
|
||||
<div className="mb-1 text-xs font-semibold text-foreground-secondary">
|
||||
Input:
|
||||
{toolPart.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(toolPart.input, null, 2)}
|
||||
</pre>
|
||||
</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 && (
|
||||
{toolPart.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)}
|
||||
{JSON.stringify(toolPart.output, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -122,11 +100,9 @@ export function MessageBubble({
|
|||
})}
|
||||
</div>
|
||||
) : (
|
||||
// Fallback to content if no parts
|
||||
// Fallback if no parts
|
||||
<div className="text-sm">
|
||||
<Streamdown isAnimating={isStreaming && !isUser}>
|
||||
{message.content || '...'}
|
||||
</Streamdown>
|
||||
<Streamdown isAnimating={isStreaming && !isUser}>...</Streamdown>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ export function SettingsSidebar(): React.ReactElement {
|
|||
const exists = envVars.some((env) => env.key === newKey);
|
||||
if (exists) {
|
||||
// Update existing
|
||||
setEnvVars(envVars.map((env) => (env.key === newKey ? { key: newKey, value: newValue } : env)));
|
||||
setEnvVars(
|
||||
envVars.map((env) => (env.key === newKey ? { key: newKey, value: newValue } : env))
|
||||
);
|
||||
} else {
|
||||
// Add new
|
||||
setEnvVars([...envVars, { key: newKey, value: newValue }]);
|
||||
|
|
@ -71,12 +73,16 @@ export function SettingsSidebar(): React.ReactElement {
|
|||
<Card variant="outline" className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">
|
||||
Environment Variables <Badge variant="secondary" size="sm">{envVars.length}</Badge>
|
||||
Environment Variables{' '}
|
||||
<Badge variant="secondary" size="sm">
|
||||
{envVars.length}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-xs text-foreground-secondary">
|
||||
Add API keys and other environment variables. They will be forwarded to tool executions.
|
||||
Add API keys and other environment variables. They will be forwarded to tool
|
||||
executions.
|
||||
</p>
|
||||
|
||||
{/* Add new env var form */}
|
||||
|
|
@ -95,7 +101,7 @@ export function SettingsSidebar(): React.ReactElement {
|
|||
onChange={(e) => setNewValue(e.target.value)}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<Button onClick={handleAddEnvVar} size="sm" variant="primary" className="w-full">
|
||||
<Button onClick={handleAddEnvVar} size="sm" variant="default" className="w-full">
|
||||
Add Variable
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -106,14 +112,22 @@ export function SettingsSidebar(): React.ReactElement {
|
|||
<p className="text-xs text-foreground-tertiary">No environment variables set</p>
|
||||
) : (
|
||||
envVars.map((env) => (
|
||||
<div key={env.key} className="flex items-center justify-between rounded border border-border bg-background p-2">
|
||||
<div
|
||||
key={env.key}
|
||||
className="flex items-center justify-between rounded border border-border bg-background p-2"
|
||||
>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<p className="truncate font-mono text-xs font-semibold">{env.key}</p>
|
||||
<p className="truncate font-mono text-xs text-foreground-tertiary">
|
||||
{env.value ? '•'.repeat(Math.min(env.value.length, 20)) : '(empty)'}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => handleRemoveEnvVar(env.key)} size="sm" variant="ghost" className="ml-2">
|
||||
<Button
|
||||
onClick={() => handleRemoveEnvVar(env.key)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="ml-2"
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -126,8 +140,8 @@ export function SettingsSidebar(): React.ReactElement {
|
|||
{/* Info Section */}
|
||||
<div className="mt-auto rounded border border-border bg-background p-3">
|
||||
<p className="text-xs text-foreground-secondary">
|
||||
<strong>Note:</strong> Environment variables are stored locally in your browser and sent with each tool
|
||||
execution request.
|
||||
<strong>Note:</strong> Environment variables are stored locally in your browser and sent
|
||||
with each tool execution request.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { useEnvVars } from '~/components/sidebar/SettingsSidebar';
|
|||
* Handles SSE streaming with tool calls and UI message protocol
|
||||
* Includes conversation ID tracking for dynamic tool loading
|
||||
*/
|
||||
export function useChat() {
|
||||
export function useChat(): ReturnType<typeof useAISDKChat> & { conversationId: string } {
|
||||
// Generate stable conversation ID for session
|
||||
const [conversationId] = useState(() => nanoid());
|
||||
|
||||
|
|
@ -19,35 +19,22 @@ export function useChat() {
|
|||
const envVars = useEnvVars();
|
||||
|
||||
// Convert env vars to object format
|
||||
const envObject = envVars.reduce((acc, { key, value }) => {
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
const envObject = envVars.reduce(
|
||||
(acc, { key, value }) => {
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
const chat = useAISDKChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: '/api/chat',
|
||||
body: {
|
||||
conversationId, // Pass conversation ID to API
|
||||
env: envObject, // Pass environment variables to API
|
||||
},
|
||||
}),
|
||||
body: {
|
||||
conversationId, // Pass conversation ID to API
|
||||
env: envObject, // Pass environment variables to API
|
||||
},
|
||||
onResponse: (response: Response) => {
|
||||
// Handle "tools loaded" response
|
||||
if (response.headers.get('content-type')?.includes('application/json')) {
|
||||
response
|
||||
.json()
|
||||
.then((data: any) => {
|
||||
if (data.type === 'tools_loaded') {
|
||||
console.log('🔧 Tools loaded:', data.loaded);
|
||||
// Optionally show toast/notification
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore JSON parsing errors for non-JSON responses
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -43,25 +43,6 @@ export async function loadTpmjsTool(packageName: string, exportName: string): Pr
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize tool name to match OpenAI's requirements
|
||||
* Pattern: ^[a-zA-Z0-9_-]+$ (only letters, numbers, underscores, hyphens)
|
||||
|
|
|
|||
553
pnpm-lock.yaml
generated
553
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue