Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ai-threadid-and-empty-pnpm-key.md
Original file line number Diff line number Diff line change
@@ -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.
65 changes: 65 additions & 0 deletions packages/cli/tests-e2e/ai-smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<typeof chatOptions>

export const useGuitarRecommendationChat = () => useChat(chatOptions)

export type ChatMessages = ReturnType<
typeof useGuitarRecommendationChat
>['messages']
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -198,6 +198,6 @@ function ImagePage() {
)
}

export const Route = createFileRoute('/demo/ai/image')({
export const Route = createFileRoute('/demo/ai-image')({
component: ImagePage,
})
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -76,6 +82,7 @@ export const Route = createFileRoute('/demo/api/ai/chat')({
systemPrompts: [SYSTEM_PROMPT],
agentLoopStrategy: maxIterations(5),
messages,
threadId,
abortController,
})

Expand Down
Original file line number Diff line number Diff line change
@@ -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')({
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const Route = createFileRoute('/demo/api/ai/tts')({

const result = await generateSpeech({
adapter,
threadId: crypto.randomUUID(),
text,
voice,
format,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function RouteComponent() {
<main className="demo-page">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(20rem,28rem)] lg:items-center">
<section className="demo-panel">
<Link to="/demo/guitars/" className="mb-4 inline-block">
<Link to="/demo/guitars" className="mb-4 inline-block">
&larr; Back to all guitars
</Link>
<h1 className="demo-title mb-4">{guitar.name}</h1>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,7 +30,7 @@ store.subscribe(() => {
})

function DevtoolPanel() {
const [state, setState] = useState<EventMap['store-devtools:state']>(() => ({
const [state, setState] = useState<EventMap['state']>(() => ({
firstName: store.state.firstName,
lastName: store.state.lastName,
fullName: fullName.state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,5 @@
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {},
"pnpm": {}
"devDependencies": {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@
"@tanstack/solid-start": "latest",
"solid-js": "^1.9.12"
},
"devDependencies": {},
"pnpm": {}
"devDependencies": {}
}
Loading