diff --git a/.changeset/ai-threadid-and-empty-pnpm-key.md b/.changeset/ai-threadid-and-empty-pnpm-key.md new file mode 100644 index 000000000..693cbdb0b --- /dev/null +++ b/.changeset/ai-threadid-and-empty-pnpm-key.md @@ -0,0 +1,5 @@ +--- +'@tanstack/create': patch +--- + +Keep the AI chat and media demos compatible with the current SDK, including thread IDs, inferred message types and complete image options. Fix the Store devtools event map and guitar navigation used by the AI demo, and omit empty pnpm settings while preserving add-on build approvals. diff --git a/packages/cli/tests-e2e/ai-smoke.spec.ts b/packages/cli/tests-e2e/ai-smoke.spec.ts new file mode 100644 index 000000000..ed3ac6699 --- /dev/null +++ b/packages/cli/tests-e2e/ai-smoke.spec.ts @@ -0,0 +1,65 @@ +import { expect, test } from '@playwright/test' +import { createAppFixture, optimizePageForFastE2E } from './helpers' + +test('@blocking builds the AI scaffold and streams a mocked chat without provider calls', async ({ + page, +}) => { + const fixture = await createAppFixture({ + appName: 'react-ai-smoke-app', + framework: 'react', + packageManager: 'npm', + addOns: ['ai'], + runQualityGatesChecks: true, + }) + try { + const invalid = await page.request.post(`${fixture.url}/demo/api/ai/chat`, { + data: { messages: [] }, + }) + expect(invalid.status()).toBe(400) + await optimizePageForFastE2E(page) + let threadId: unknown + await page.route('**/demo/api/ai/chat', async (route) => { + threadId = route.request().postDataJSON().threadId + const events = [ + { type: 'RUN_STARTED', threadId, runId: 'mock-run' }, + { + type: 'TEXT_MESSAGE_START', + messageId: 'mock-message', + role: 'assistant', + }, + { + type: 'TEXT_MESSAGE_CONTENT', + messageId: 'mock-message', + delta: 'Local mock reply', + }, + { type: 'TEXT_MESSAGE_END', messageId: 'mock-message' }, + { type: 'RUN_FINISHED', threadId, runId: 'mock-run' }, + ] + await route.fulfill({ + contentType: 'text/event-stream', + body: events + .map((event) => `data: ${JSON.stringify(event)}\n\n`) + .join(''), + }) + }) + await page.addInitScript(() => localStorage.setItem('theme', 'light')) + await page.goto(`${fixture.url}/demo/ai-chat`) + await expect( + page.getByRole('button', { + name: 'Theme mode: light. Click to switch mode.', + exact: true, + }), + ).toBeVisible() + const input = page.getByPlaceholder('Type something clever...') + await input.fill('Hello') + await expect(page.locator('form button[type=submit]')).toBeEnabled() + await input.press('Enter') + await expect( + page.getByText('Local mock reply', { exact: true }), + ).toBeVisible() + expect(threadId).toBe('guitar-recommendations') + } finally { + await fixture.stop() + await fixture.cleanup() + } +}) diff --git a/packages/create/src/frameworks/react/add-ons/ai/assets/src/lib/demo-ai-hook.ts b/packages/create/src/frameworks/react/add-ons/ai/assets/src/lib/demo-ai-hook.ts index 1a113c54b..8d18198a3 100644 --- a/packages/create/src/frameworks/react/add-ons/ai/assets/src/lib/demo-ai-hook.ts +++ b/packages/create/src/frameworks/react/add-ons/ai/assets/src/lib/demo-ai-hook.ts @@ -3,7 +3,6 @@ import { useChat, createChatClientOptions, } from '@tanstack/ai-react' -import type { InferChatMessages } from '@tanstack/ai-react' import { clientTools } from '@tanstack/ai-client' import { recommendGuitarToolDef } from '#/lib/demo-guitar-tools' @@ -13,10 +12,13 @@ const recommendGuitarToolClient = recommendGuitarToolDef.client(({ id }) => ({ })) const chatOptions = createChatClientOptions({ + threadId: 'guitar-recommendations', connection: fetchServerSentEvents('/demo/api/ai/chat'), tools: clientTools(recommendGuitarToolClient), }) -export type ChatMessages = InferChatMessages - export const useGuitarRecommendationChat = () => useChat(chatOptions) + +export type ChatMessages = ReturnType< + typeof useGuitarRecommendationChat +>['messages'] diff --git a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/ai-image.tsx b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/ai-image.tsx index ad6cac556..f72e7de44 100644 --- a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/ai-image.tsx +++ b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/ai-image.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState } from 'react' import { createFileRoute } from '@tanstack/react-router' import { ImageIcon, Loader2, Download } from 'lucide-react' @@ -198,6 +198,6 @@ function ImagePage() { ) } -export const Route = createFileRoute('/demo/ai/image')({ +export const Route = createFileRoute('/demo/ai-image')({ component: ImagePage, }) diff --git a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.chat.ts b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.chat.ts index c715d0ea7..8a1f9b77e 100644 --- a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.chat.ts +++ b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.chat.ts @@ -40,10 +40,16 @@ export const Route = createFileRoute('/demo/api/ai/chat')({ try { const body = await request.json() - const { messages } = body + const { messages, threadId } = body + if (typeof threadId !== 'string' || !threadId.trim()) { + return Response.json( + { error: 'A threadId is required' }, + { status: 400 }, + ) + } // Determine the best available provider - let provider: string = 'ollama' + let provider: 'anthropic' | 'openai' | 'gemini' | 'ollama' = 'ollama' let model: string = 'mistral:7b' if (process.env.ANTHROPIC_API_KEY) { provider = 'anthropic' @@ -76,6 +82,7 @@ export const Route = createFileRoute('/demo/api/ai/chat')({ systemPrompts: [SYSTEM_PROMPT], agentLoopStrategy: maxIterations(5), messages, + threadId, abortController, }) diff --git a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.image.ts b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.image.ts index 4066f5828..d1da1da57 100644 --- a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.image.ts +++ b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.image.ts @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' -import { generateImage, createImageOptions } from '@tanstack/ai' +import { generateImage } from '@tanstack/ai' import { openaiImage } from '@tanstack/ai-openai' export const Route = createFileRoute('/demo/api/ai/image')({ @@ -34,12 +34,9 @@ export const Route = createFileRoute('/demo/api/ai/image')({ } try { - const options = createImageOptions({ - adapter: openaiImage('gpt-image-1'), - }) - const result = await generateImage({ - ...options, + adapter: openaiImage('gpt-image-1'), + threadId: crypto.randomUUID(), prompt, numberOfImages, size, diff --git a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.transcription.ts b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.transcription.ts index c7f1a4136..d7ff9777e 100644 --- a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.transcription.ts +++ b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.transcription.ts @@ -52,6 +52,7 @@ export const Route = createFileRoute('/demo/api/ai/transcription')({ const result = await generateTranscription({ adapter, + threadId: crypto.randomUUID(), audio: audioData, language: language || undefined, responseFormat: (responseFormat as any) || 'verbose_json', diff --git a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.tts.ts b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.tts.ts index dbfc14976..02849c798 100644 --- a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.tts.ts +++ b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/api.ai.tts.ts @@ -44,6 +44,7 @@ export const Route = createFileRoute('/demo/api/ai/tts')({ const result = await generateSpeech({ adapter, + threadId: crypto.randomUUID(), text, voice, format, diff --git a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/guitars/$guitarId.tsx b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/guitars/$guitarId.tsx index d858c0205..97ae6c43c 100644 --- a/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/guitars/$guitarId.tsx +++ b/packages/create/src/frameworks/react/add-ons/ai/assets/src/routes/demo/guitars/$guitarId.tsx @@ -20,7 +20,7 @@ function RouteComponent() {
- + ← Back to all guitars

{guitar.name}

diff --git a/packages/create/src/frameworks/react/add-ons/store/assets/src/lib/demo-store-devtools.tsx b/packages/create/src/frameworks/react/add-ons/store/assets/src/lib/demo-store-devtools.tsx index 0286dec8a..99f50409e 100644 --- a/packages/create/src/frameworks/react/add-ons/store/assets/src/lib/demo-store-devtools.tsx +++ b/packages/create/src/frameworks/react/add-ons/store/assets/src/lib/demo-store-devtools.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from 'react' import { store, fullName } from './demo-store' type EventMap = { - 'store-devtools:state': { + state: { firstName: string lastName: string fullName: string @@ -30,7 +30,7 @@ store.subscribe(() => { }) function DevtoolPanel() { - const [state, setState] = useState(() => ({ + const [state, setState] = useState(() => ({ firstName: store.state.firstName, lastName: store.state.lastName, fullName: fullName.state, diff --git a/packages/create/src/frameworks/react/project/base/package.json b/packages/create/src/frameworks/react/project/base/package.json index 79013bfec..a7a6322f5 100644 --- a/packages/create/src/frameworks/react/project/base/package.json +++ b/packages/create/src/frameworks/react/project/base/package.json @@ -12,6 +12,5 @@ "react": "^19.2.0", "react-dom": "^19.2.0" }, - "devDependencies": {}, - "pnpm": {} + "devDependencies": {} } diff --git a/packages/create/src/frameworks/solid/project/base/package.json b/packages/create/src/frameworks/solid/project/base/package.json index 8888fda3f..e97bbfa60 100644 --- a/packages/create/src/frameworks/solid/project/base/package.json +++ b/packages/create/src/frameworks/solid/project/base/package.json @@ -8,6 +8,5 @@ "@tanstack/solid-start": "latest", "solid-js": "^1.9.12" }, - "devDependencies": {}, - "pnpm": {} + "devDependencies": {} }