diff --git a/examples/README.md b/examples/README.md index e04570d09..b8aeea660 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,7 +6,7 @@ This directory contains comprehensive examples demonstrating TanStack AI across Choose an example based on your use case: -- **Want a full-stack TypeScript app?** → [TanStack Chat (ts-chat)](#tanstack-chat-ts-chat) +- **Want a full-stack TypeScript app?** → [TanStack Chat (ts-react-chat)](#tanstack-chat-ts-react-chat) - **Want a simple CLI tool?** → [CLI Example](#cli-example) - **Need a vanilla JS frontend?** → [Vanilla Chat](#vanilla-chat) - **Building a Python backend?** → [Python FastAPI Server](#python-fastapi-server) @@ -14,7 +14,7 @@ Choose an example based on your use case: ## TypeScript Examples -### TanStack Chat (ts-chat) +### TanStack Chat (ts-react-chat) A full-featured chat application built with the TanStack ecosystem. @@ -38,14 +38,14 @@ A full-featured chat application built with the TanStack ecosystem. **Getting Started:** ```bash -cd examples/ts-chat +cd examples/ts-react-chat pnpm install cp env.example .env # Edit .env and add your OPENAI_API_KEY pnpm start ``` -📖 [Full Documentation](ts-chat/README.md) +📖 [Full Documentation](ts-react-chat/README.md) --- @@ -245,7 +245,7 @@ Backend (TanStack Start API Route) AI Provider (OpenAI/Anthropic/etc.) ``` -**Example:** [TanStack Chat (ts-chat)](ts-chat/README.md) +**Example:** [TanStack Chat (ts-react-chat)](ts-react-chat/README.md) ### Multi-Language Backend @@ -393,8 +393,8 @@ python anthropic-server.py cd examples/vanilla-chat pnpm dev -# Terminal 3: Start ts-chat (full-stack) -cd examples/ts-chat +# Terminal 3: Start ts-react-chat (full-stack) +cd examples/ts-react-chat pnpm start ``` diff --git a/examples/ts-chat/.cta.json b/examples/ts-react-chat/.cta.json similarity index 87% rename from examples/ts-chat/.cta.json rename to examples/ts-react-chat/.cta.json index 8142313ea..db7fae646 100644 --- a/examples/ts-chat/.cta.json +++ b/examples/ts-react-chat/.cta.json @@ -1,5 +1,5 @@ { - "projectName": "ts-chat", + "projectName": "ts-react-chat", "mode": "file-router", "typescript": true, "tailwind": true, diff --git a/examples/ts-chat/.gitignore b/examples/ts-react-chat/.gitignore similarity index 100% rename from examples/ts-chat/.gitignore rename to examples/ts-react-chat/.gitignore diff --git a/examples/ts-chat/README.md b/examples/ts-react-chat/README.md similarity index 100% rename from examples/ts-chat/README.md rename to examples/ts-react-chat/README.md diff --git a/examples/ts-chat/api-verification.ts b/examples/ts-react-chat/api-verification.ts similarity index 100% rename from examples/ts-chat/api-verification.ts rename to examples/ts-react-chat/api-verification.ts diff --git a/examples/ts-chat/env.example b/examples/ts-react-chat/env.example similarity index 100% rename from examples/ts-chat/env.example rename to examples/ts-react-chat/env.example diff --git a/examples/ts-chat/package.json b/examples/ts-react-chat/package.json similarity index 93% rename from examples/ts-chat/package.json rename to examples/ts-react-chat/package.json index b76671bf3..3c4ebfb46 100644 --- a/examples/ts-chat/package.json +++ b/examples/ts-react-chat/package.json @@ -1,5 +1,5 @@ { - "name": "ts-chat", + "name": "ts-react-chat", "private": true, "type": "module", "scripts": { @@ -9,9 +9,6 @@ "test": "exit 0" }, "dependencies": { - "@ai-sdk/openai": "^2.0.73", - "@ai-sdk/provider": "^2.0.0", - "@ai-sdk/provider-utils": "^3.0.17", "@tailwindcss/vite": "^4.1.17", "@tanstack/ai": "workspace:*", "@tanstack/ai-anthropic": "workspace:*", diff --git a/examples/ts-chat/public/example-guitar-flowers.jpg b/examples/ts-react-chat/public/example-guitar-flowers.jpg similarity index 100% rename from examples/ts-chat/public/example-guitar-flowers.jpg rename to examples/ts-react-chat/public/example-guitar-flowers.jpg diff --git a/examples/ts-chat/public/example-guitar-motherboard.jpg b/examples/ts-react-chat/public/example-guitar-motherboard.jpg similarity index 100% rename from examples/ts-chat/public/example-guitar-motherboard.jpg rename to examples/ts-react-chat/public/example-guitar-motherboard.jpg diff --git a/examples/ts-chat/public/example-guitar-racing.jpg b/examples/ts-react-chat/public/example-guitar-racing.jpg similarity index 100% rename from examples/ts-chat/public/example-guitar-racing.jpg rename to examples/ts-react-chat/public/example-guitar-racing.jpg diff --git a/examples/ts-chat/public/example-guitar-steamer-trunk.jpg b/examples/ts-react-chat/public/example-guitar-steamer-trunk.jpg similarity index 100% rename from examples/ts-chat/public/example-guitar-steamer-trunk.jpg rename to examples/ts-react-chat/public/example-guitar-steamer-trunk.jpg diff --git a/examples/ts-chat/public/example-guitar-superhero.jpg b/examples/ts-react-chat/public/example-guitar-superhero.jpg similarity index 100% rename from examples/ts-chat/public/example-guitar-superhero.jpg rename to examples/ts-react-chat/public/example-guitar-superhero.jpg diff --git a/examples/ts-chat/public/example-guitar-traveling.jpg b/examples/ts-react-chat/public/example-guitar-traveling.jpg similarity index 100% rename from examples/ts-chat/public/example-guitar-traveling.jpg rename to examples/ts-react-chat/public/example-guitar-traveling.jpg diff --git a/examples/ts-chat/public/example-guitar-video-games.jpg b/examples/ts-react-chat/public/example-guitar-video-games.jpg similarity index 100% rename from examples/ts-chat/public/example-guitar-video-games.jpg rename to examples/ts-react-chat/public/example-guitar-video-games.jpg diff --git a/examples/ts-chat/public/favicon.ico b/examples/ts-react-chat/public/favicon.ico similarity index 100% rename from examples/ts-chat/public/favicon.ico rename to examples/ts-react-chat/public/favicon.ico diff --git a/examples/ts-chat/public/logo192.png b/examples/ts-react-chat/public/logo192.png similarity index 100% rename from examples/ts-chat/public/logo192.png rename to examples/ts-react-chat/public/logo192.png diff --git a/examples/ts-chat/public/logo512.png b/examples/ts-react-chat/public/logo512.png similarity index 100% rename from examples/ts-chat/public/logo512.png rename to examples/ts-react-chat/public/logo512.png diff --git a/examples/ts-chat/public/manifest.json b/examples/ts-react-chat/public/manifest.json similarity index 100% rename from examples/ts-chat/public/manifest.json rename to examples/ts-react-chat/public/manifest.json diff --git a/examples/ts-chat/public/robots.txt b/examples/ts-react-chat/public/robots.txt similarity index 100% rename from examples/ts-chat/public/robots.txt rename to examples/ts-react-chat/public/robots.txt diff --git a/examples/ts-chat/public/tanstack-circle-logo.png b/examples/ts-react-chat/public/tanstack-circle-logo.png similarity index 100% rename from examples/ts-chat/public/tanstack-circle-logo.png rename to examples/ts-react-chat/public/tanstack-circle-logo.png diff --git a/examples/ts-chat/public/tanstack-word-logo-white.svg b/examples/ts-react-chat/public/tanstack-word-logo-white.svg similarity index 100% rename from examples/ts-chat/public/tanstack-word-logo-white.svg rename to examples/ts-react-chat/public/tanstack-word-logo-white.svg diff --git a/examples/ts-chat/src/components/Approval.tsx b/examples/ts-react-chat/src/components/Approval.tsx similarity index 100% rename from examples/ts-chat/src/components/Approval.tsx rename to examples/ts-react-chat/src/components/Approval.tsx diff --git a/examples/ts-chat/src/components/Header.tsx b/examples/ts-react-chat/src/components/Header.tsx similarity index 100% rename from examples/ts-chat/src/components/Header.tsx rename to examples/ts-react-chat/src/components/Header.tsx diff --git a/examples/ts-chat/src/components/example-GuitarRecommendation.tsx b/examples/ts-react-chat/src/components/example-GuitarRecommendation.tsx similarity index 100% rename from examples/ts-chat/src/components/example-GuitarRecommendation.tsx rename to examples/ts-react-chat/src/components/example-GuitarRecommendation.tsx diff --git a/examples/ts-chat/src/data/example-guitars.ts b/examples/ts-react-chat/src/data/example-guitars.ts similarity index 100% rename from examples/ts-chat/src/data/example-guitars.ts rename to examples/ts-react-chat/src/data/example-guitars.ts diff --git a/examples/ts-chat/src/lib/guitar-tools.ts b/examples/ts-react-chat/src/lib/guitar-tools.ts similarity index 100% rename from examples/ts-chat/src/lib/guitar-tools.ts rename to examples/ts-react-chat/src/lib/guitar-tools.ts diff --git a/examples/ts-chat/src/lib/stub-adapter.ts b/examples/ts-react-chat/src/lib/stub-adapter.ts similarity index 100% rename from examples/ts-chat/src/lib/stub-adapter.ts rename to examples/ts-react-chat/src/lib/stub-adapter.ts diff --git a/examples/ts-chat/src/lib/stub-llm.ts b/examples/ts-react-chat/src/lib/stub-llm.ts similarity index 100% rename from examples/ts-chat/src/lib/stub-llm.ts rename to examples/ts-react-chat/src/lib/stub-llm.ts diff --git a/examples/ts-chat/src/logo.svg b/examples/ts-react-chat/src/logo.svg similarity index 100% rename from examples/ts-chat/src/logo.svg rename to examples/ts-react-chat/src/logo.svg diff --git a/examples/ts-chat/src/routeTree.gen.ts b/examples/ts-react-chat/src/routeTree.gen.ts similarity index 100% rename from examples/ts-chat/src/routeTree.gen.ts rename to examples/ts-react-chat/src/routeTree.gen.ts diff --git a/examples/ts-chat/src/router.tsx b/examples/ts-react-chat/src/router.tsx similarity index 100% rename from examples/ts-chat/src/router.tsx rename to examples/ts-react-chat/src/router.tsx diff --git a/examples/ts-chat/src/routes/__root.tsx b/examples/ts-react-chat/src/routes/__root.tsx similarity index 100% rename from examples/ts-chat/src/routes/__root.tsx rename to examples/ts-react-chat/src/routes/__root.tsx diff --git a/examples/ts-chat/src/routes/api.tanchat.ts b/examples/ts-react-chat/src/routes/api.tanchat.ts similarity index 100% rename from examples/ts-chat/src/routes/api.tanchat.ts rename to examples/ts-react-chat/src/routes/api.tanchat.ts diff --git a/examples/ts-chat/src/routes/api.test-chat.ts b/examples/ts-react-chat/src/routes/api.test-chat.ts similarity index 100% rename from examples/ts-chat/src/routes/api.test-chat.ts rename to examples/ts-react-chat/src/routes/api.test-chat.ts diff --git a/examples/ts-chat/src/routes/demo.tsx b/examples/ts-react-chat/src/routes/demo.tsx similarity index 100% rename from examples/ts-chat/src/routes/demo.tsx rename to examples/ts-react-chat/src/routes/demo.tsx diff --git a/examples/ts-chat/src/routes/example.guitars/$guitarId.tsx b/examples/ts-react-chat/src/routes/example.guitars/$guitarId.tsx similarity index 100% rename from examples/ts-chat/src/routes/example.guitars/$guitarId.tsx rename to examples/ts-react-chat/src/routes/example.guitars/$guitarId.tsx diff --git a/examples/ts-chat/src/routes/example.guitars/index.tsx b/examples/ts-react-chat/src/routes/example.guitars/index.tsx similarity index 100% rename from examples/ts-chat/src/routes/example.guitars/index.tsx rename to examples/ts-react-chat/src/routes/example.guitars/index.tsx diff --git a/examples/ts-chat/src/routes/index.tsx b/examples/ts-react-chat/src/routes/index.tsx similarity index 100% rename from examples/ts-chat/src/routes/index.tsx rename to examples/ts-react-chat/src/routes/index.tsx diff --git a/examples/ts-chat/src/routes/tanchat.css b/examples/ts-react-chat/src/routes/tanchat.css similarity index 100% rename from examples/ts-chat/src/routes/tanchat.css rename to examples/ts-react-chat/src/routes/tanchat.css diff --git a/examples/ts-chat/src/styles.css b/examples/ts-react-chat/src/styles.css similarity index 100% rename from examples/ts-chat/src/styles.css rename to examples/ts-react-chat/src/styles.css diff --git a/examples/ts-chat/src/utils/demo.tools.ts b/examples/ts-react-chat/src/utils/demo.tools.ts similarity index 100% rename from examples/ts-chat/src/utils/demo.tools.ts rename to examples/ts-react-chat/src/utils/demo.tools.ts diff --git a/examples/ts-chat/tsconfig.json b/examples/ts-react-chat/tsconfig.json similarity index 100% rename from examples/ts-chat/tsconfig.json rename to examples/ts-react-chat/tsconfig.json diff --git a/examples/ts-chat/vite.config.ts b/examples/ts-react-chat/vite.config.ts similarity index 100% rename from examples/ts-chat/vite.config.ts rename to examples/ts-react-chat/vite.config.ts diff --git a/examples/ts-solid-chat/.cta.json b/examples/ts-solid-chat/.cta.json new file mode 100644 index 000000000..506626021 --- /dev/null +++ b/examples/ts-solid-chat/.cta.json @@ -0,0 +1,12 @@ +{ + "projectName": "ts-solid-chat", + "mode": "file-router", + "typescript": true, + "tailwind": true, + "packageManager": "pnpm", + "addOnOptions": {}, + "git": true, + "version": 1, + "framework": "solid", + "chosenAddOns": ["nitro", "start", "tanchat", "store"] +} diff --git a/examples/ts-solid-chat/.gitignore b/examples/ts-solid-chat/.gitignore new file mode 100644 index 000000000..029f7fba9 --- /dev/null +++ b/examples/ts-solid-chat/.gitignore @@ -0,0 +1,12 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +count.txt +.env +.nitro +.tanstack +.output +.vinxi +todos.json diff --git a/examples/ts-solid-chat/README.md b/examples/ts-solid-chat/README.md new file mode 100644 index 000000000..a2f8e5146 --- /dev/null +++ b/examples/ts-solid-chat/README.md @@ -0,0 +1,325 @@ +Welcome to your new TanStack app! + +# Getting Started + +To run this application: + +```bash +pnpm install +pnpm start +``` + +# Building For Production + +To build this application for production: + +```bash +pnpm build +``` + +## Testing + +This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with: + +```bash +pnpm test +``` + +## Styling + +This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. + +# TanStack Chat Application + +An example chat application built with TanStack Start, TanStack Store, and **TanStack AI** (our open-source AI SDK). + +## .env Updates + +```env +OPENAI_API_KEY=your_openai_api_key +``` + +## ✨ Features + +### AI Capabilities + +- 🤖 Powered by **TanStack AI** with OpenAI GPT-4o +- 📝 Rich markdown formatting with syntax highlighting +- 🎯 Customizable system prompts for tailored AI behavior +- 🔄 Real-time streaming responses with Server-Sent Events +- 🔌 **Connection adapters** - flexible streaming architecture +- 🛠️ **Automatic tool execution loop** - tools are executed automatically by the SDK +- 🎸 Tool/function calling with guitar recommendations + +### User Experience + +- 🎨 Modern UI with Tailwind CSS and Lucide icons +- 🔍 Conversation management and history +- 🔐 Secure API key management +- 📋 Markdown rendering with code highlighting + +### Technical Features + +- 📦 Centralized state management with TanStack Store +- 🔌 Extensible architecture for multiple AI providers +- 🛠️ TypeScript for type safety + +## Architecture + +### Tech Stack + +- **Frontend Framework**: TanStack Start +- **Routing**: TanStack Router +- **State Management**: TanStack Store +- **Styling**: Tailwind CSS +- **AI Integration**: TanStack AI with OpenAI GPT-4o +- **Chat Client**: `@tanstack/ai-solid` with connection adapters +- **Streaming**: Server-Sent Events via `fetchServerSentEvents` +- **Tool Execution**: Automatic loop with `ToolCallManager` + +## Routing + +This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`. + +### Adding A Route + +To add a new route to your application just add another a new file in the `./src/routes` directory. + +TanStack will automatically generate the content of the route file for you. + +Now that you have two routes you can use a `Link` component to navigate between them. + +### Adding Links + +To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/solid-router`. + +```tsx +import { Link } from '@tanstack/solid-router' +``` + +Then anywhere in your JSX you can use it like so: + +```tsx +About +``` + +This will create a link that will navigate to the `/about` route. + +More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/solid/api/router/linkComponent). + +### Using A Layout + +In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `` component. + +Here is an example layout that includes a header: + +```tsx +import { Outlet, createRootRoute } from '@tanstack/solid-router' +import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' + +import { Link } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: () => ( + <> +
+ +
+ + + + ), +}) +``` + +The `` component is not required so you can remove it if you don't want it in your layout. + +More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/solid/guide/routing-concepts#layouts). + +## Data Fetching + +There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered. + +For example: + +```tsx +const peopleRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/people', + loader: async () => { + const response = await fetch('https://swapi.dev/api/people') + return response.json() as Promise<{ + results: { + name: string + }[] + }> + }, + component: () => { + const data = peopleRoute.useLoaderData() + return ( +
    + {data.results.map((person) => ( +
  • {person.name}
  • + ))} +
+ ) + }, +}) +``` + +Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#loader-parameters). + +### Solid-Query + +Solid-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze. + +First add your dependencies: + +```bash +pnpm add @tanstack/solid-query @tanstack/solid-query-devtools +``` + +Next we'll need to create a query client and provider. We recommend putting those in `main.tsx`. + +```tsx +import { QueryClient, QueryClientProvider } from '@tanstack/solid-query' + +// ... + +const queryClient = new QueryClient() + +// ... +``` + +You can also add TanStack Query Devtools to the root route (optional). + +```tsx +import { SolidQueryDevtools } from '@tanstack/solid-query-devtools' + +const rootRoute = createRootRoute({ + component: () => ( + <> + + + + + ), +}) +``` + +Now you can use `useQuery` to fetch your data. + +```tsx +import { useQuery } from '@tanstack/solid-query' + +import './App.css' + +function App() { + const { data } = useQuery({ + queryKey: ['people'], + queryFn: () => + fetch('https://swapi.dev/api/people') + .then((res) => res.json()) + .then((data) => data.results as { name: string }[]), + initialData: [], + }) + + return ( +
+
    + {data.map((person) => ( +
  • {person.name}
  • + ))} +
+
+ ) +} + +export default App +``` + +You can find out everything you need to know on how to use Solid-Query in the [Solid-Query documentation](https://tanstack.com/query/latest/docs/framework/solid/overview). + +## State Management + +Another common requirement for Solid applications is state management. There are many options for state management in Solid. TanStack Store provides a great starting point for your project. + +First you need to add TanStack Store as a dependency: + +```bash +pnpm add @tanstack/store +``` + +Now let's create a simple counter in the `src/App.tsx` file as a demonstration. + +```tsx +import { useStore } from '@tanstack/solid-store' +import { Store } from '@tanstack/store' +import './App.css' + +const countStore = new Store(0) + +function App() { + const count = useStore(countStore) + return ( +
+ +
+ ) +} + +export default App +``` + +One of the many nice features of TanStack Store is the ability to derive state from other state. That derived state will update when the base state updates. + +Let's check this out by doubling the count using derived state. + +```tsx +import { useStore } from '@tanstack/solid-store' +import { Store, Derived } from '@tanstack/store' +import './App.css' + +const countStore = new Store(0) + +const doubledStore = new Derived({ + fn: () => countStore.state * 2, + deps: [countStore], +}) +doubledStore.mount() + +function App() { + const count = useStore(countStore) + const doubledCount = useStore(doubledStore) + + return ( +
+ +
Doubled - {doubledCount}
+
+ ) +} + +export default App +``` + +We use the `Derived` class to create a new store that is derived from another store. The `Derived` class has a `mount` method that will start the derived store updating. + +Once we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook. + +You can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest). + +# Demo files + +Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed. + +# Learn More + +You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com). diff --git a/examples/ts-solid-chat/api-verification.ts b/examples/ts-solid-chat/api-verification.ts new file mode 100644 index 000000000..336ce12bb --- /dev/null +++ b/examples/ts-solid-chat/api-verification.ts @@ -0,0 +1 @@ +export {} diff --git a/examples/ts-solid-chat/env.example b/examples/ts-solid-chat/env.example new file mode 100644 index 000000000..613cb664b --- /dev/null +++ b/examples/ts-solid-chat/env.example @@ -0,0 +1,3 @@ +# OpenAI API Key +# Get yours at: https://platform.openai.com/api-keys +OPENAI_API_KEY=sk-... \ No newline at end of file diff --git a/examples/ts-solid-chat/package.json b/examples/ts-solid-chat/package.json new file mode 100644 index 000000000..a5fbb4076 --- /dev/null +++ b/examples/ts-solid-chat/package.json @@ -0,0 +1,56 @@ +{ + "name": "ts-solid-chat", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "build": "vite build", + "serve": "vite preview", + "test": "exit 0" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.17", + "@tanstack/ai": "workspace:*", + "@tanstack/ai-anthropic": "workspace:*", + "@tanstack/ai-client": "workspace:*", + "@tanstack/ai-devtools-core": "workspace:*", + "@tanstack/ai-gemini": "workspace:*", + "@tanstack/ai-ollama": "workspace:*", + "@tanstack/ai-openai": "workspace:*", + "@tanstack/ai-solid": "workspace:*", + "@tanstack/nitro-v2-vite-plugin": "^1.139.0", + "@tanstack/router-plugin": "^1.139.7", + "@tanstack/solid-ai-devtools": "workspace:*", + "@tanstack/solid-devtools": "^0.7.15", + "@tanstack/solid-router": "^1.132.0", + "@tanstack/solid-router-devtools": "^1.132.0", + "@tanstack/solid-router-ssr-query": "^1.131.7", + "@tanstack/solid-start": "^1.132.0", + "@tanstack/solid-store": "^0.7.0", + "@tanstack/store": "^0.8.0", + "highlight.js": "^11.11.1", + "lucide-solid": "^0.554.0", + "rehype-highlight": "^7.0.2", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", + "solid-js": "^1.9.10", + "solid-markdown": "^2.0.14", + "tailwindcss": "^4.1.17", + "vite-tsconfig-paths": "^5.1.4", + "zod": "^4.1.13" + }, + "devDependencies": { + "@solidjs/testing-library": "^0.8.10", + "@tanstack/devtools-event-client": "^0.3.5", + "@tanstack/devtools-vite": "^0.3.11", + "@testing-library/dom": "^10.4.1", + "@types/node": "^24.10.1", + "jsdom": "^27.2.0", + "typescript": "5.9.3", + "vite": "^7.2.4", + "vite-plugin-solid": "^2.11.10", + "vitest": "^4.0.14", + "web-vitals": "^5.1.0" + } +} diff --git a/examples/ts-solid-chat/public/example-guitar-flowers.jpg b/examples/ts-solid-chat/public/example-guitar-flowers.jpg new file mode 100644 index 000000000..debe785ef Binary files /dev/null and b/examples/ts-solid-chat/public/example-guitar-flowers.jpg differ diff --git a/examples/ts-solid-chat/public/example-guitar-motherboard.jpg b/examples/ts-solid-chat/public/example-guitar-motherboard.jpg new file mode 100644 index 000000000..8f1a8d72f Binary files /dev/null and b/examples/ts-solid-chat/public/example-guitar-motherboard.jpg differ diff --git a/examples/ts-solid-chat/public/example-guitar-racing.jpg b/examples/ts-solid-chat/public/example-guitar-racing.jpg new file mode 100644 index 000000000..44555574c Binary files /dev/null and b/examples/ts-solid-chat/public/example-guitar-racing.jpg differ diff --git a/examples/ts-solid-chat/public/example-guitar-steamer-trunk.jpg b/examples/ts-solid-chat/public/example-guitar-steamer-trunk.jpg new file mode 100644 index 000000000..7b9318939 Binary files /dev/null and b/examples/ts-solid-chat/public/example-guitar-steamer-trunk.jpg differ diff --git a/examples/ts-solid-chat/public/example-guitar-superhero.jpg b/examples/ts-solid-chat/public/example-guitar-superhero.jpg new file mode 100644 index 000000000..3bbea2743 Binary files /dev/null and b/examples/ts-solid-chat/public/example-guitar-superhero.jpg differ diff --git a/examples/ts-solid-chat/public/example-guitar-traveling.jpg b/examples/ts-solid-chat/public/example-guitar-traveling.jpg new file mode 100644 index 000000000..285647be5 Binary files /dev/null and b/examples/ts-solid-chat/public/example-guitar-traveling.jpg differ diff --git a/examples/ts-solid-chat/public/example-guitar-video-games.jpg b/examples/ts-solid-chat/public/example-guitar-video-games.jpg new file mode 100644 index 000000000..4987f77e7 Binary files /dev/null and b/examples/ts-solid-chat/public/example-guitar-video-games.jpg differ diff --git a/examples/ts-solid-chat/public/favicon.ico b/examples/ts-solid-chat/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/examples/ts-solid-chat/public/favicon.ico differ diff --git a/examples/ts-solid-chat/public/logo192.png b/examples/ts-solid-chat/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/examples/ts-solid-chat/public/logo192.png differ diff --git a/examples/ts-solid-chat/public/logo512.png b/examples/ts-solid-chat/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/examples/ts-solid-chat/public/logo512.png differ diff --git a/examples/ts-solid-chat/public/manifest.json b/examples/ts-solid-chat/public/manifest.json new file mode 100644 index 000000000..078ef5011 --- /dev/null +++ b/examples/ts-solid-chat/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "TanStack App", + "name": "Create TanStack App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/ts-solid-chat/public/robots.txt b/examples/ts-solid-chat/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/examples/ts-solid-chat/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/ts-solid-chat/public/tanstack-circle-logo.png b/examples/ts-solid-chat/public/tanstack-circle-logo.png new file mode 100644 index 000000000..9db3e67ba Binary files /dev/null and b/examples/ts-solid-chat/public/tanstack-circle-logo.png differ diff --git a/examples/ts-solid-chat/public/tanstack-word-logo-white.svg b/examples/ts-solid-chat/public/tanstack-word-logo-white.svg new file mode 100644 index 000000000..b6ec5086c --- /dev/null +++ b/examples/ts-solid-chat/public/tanstack-word-logo-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/ts-solid-chat/src/components/Approval.tsx b/examples/ts-solid-chat/src/components/Approval.tsx new file mode 100644 index 000000000..6d9708290 --- /dev/null +++ b/examples/ts-solid-chat/src/components/Approval.tsx @@ -0,0 +1,38 @@ +export interface ApprovalProps { + toolName: string + input: any + onApprove: () => void + onDeny: () => void +} + +export default function Approval({ + toolName, + input, + onApprove, + onDeny, +}: ApprovalProps) { + return ( +
+

Approve {toolName}?

+
+
+          {JSON.stringify(input, null, 2)}
+        
+
+
+ + +
+
+ ) +} diff --git a/examples/ts-solid-chat/src/components/Header.tsx b/examples/ts-solid-chat/src/components/Header.tsx new file mode 100644 index 000000000..67e226d83 --- /dev/null +++ b/examples/ts-solid-chat/src/components/Header.tsx @@ -0,0 +1,80 @@ +import { Link } from '@tanstack/solid-router' + +import { Guitar, Home, Menu, X } from 'lucide-solid' +import { createSignal } from 'solid-js' + +export default function Header() { + const [isOpen, setIsOpen] = createSignal(false) + + return ( + <> +
+ +

+ + TanStack Logo + +

+
+ + + + ) +} diff --git a/examples/ts-solid-chat/src/components/example-GuitarRecommendation.tsx b/examples/ts-solid-chat/src/components/example-GuitarRecommendation.tsx new file mode 100644 index 000000000..34e18c0c1 --- /dev/null +++ b/examples/ts-solid-chat/src/components/example-GuitarRecommendation.tsx @@ -0,0 +1,42 @@ +import { useNavigate } from '@tanstack/solid-router' + +import guitars from '../data/example-guitars' + +export default function GuitarRecommendation({ id }: { id: string }) { + const navigate = useNavigate() + const guitar = guitars.find((guitar) => guitar.id === +id) + if (!guitar) { + return null + } + return ( +
+
+ {guitar.name} +
+
+

{guitar.name}

+

+ {guitar.shortDescription} +

+
+
${guitar.price}
+ +
+
+
+ ) +} diff --git a/examples/ts-solid-chat/src/data/example-guitars.ts b/examples/ts-solid-chat/src/data/example-guitars.ts new file mode 100644 index 000000000..16b6764f7 --- /dev/null +++ b/examples/ts-solid-chat/src/data/example-guitars.ts @@ -0,0 +1,83 @@ +export interface Guitar { + id: number + name: string + image: string + description: string + shortDescription: string + price: number +} + +const guitars: Array = [ + { + id: 1, + name: 'Video Game Guitar', + image: '/example-guitar-video-games.jpg', + description: + "The Video Game Guitar is a unique acoustic guitar that features a design inspired by video games. It has a sleek, high-gloss finish and a comfortable playability. The guitar's ergonomic body and fast neck profile ensure comfortable playability for hours on end.", + shortDescription: + 'A unique electric guitar with a video game design, high-gloss finish, and comfortable playability.', + price: 699, + }, + { + id: 2, + name: 'Superhero Guitar', + image: '/example-guitar-superhero.jpg', + description: + "The Superhero Guitar is a bold black electric guitar that stands out with its unique superhero logo design. Its sleek, high-gloss finish and powerful pickups make it perfect for high-energy performances. The guitar's ergonomic body and fast neck profile ensure comfortable playability for hours on end.", + shortDescription: + 'A bold black electric guitar with a unique superhero logo, high-gloss finish, and powerful pickups.', + price: 699, + }, + { + id: 3, + name: 'Motherboard Guitar', + image: '/example-guitar-motherboard.jpg', + description: + "This guitar is a tribute to the motherboard of a computer. It's a unique and stylish instrument that will make you feel like a hacker. The intricate circuit-inspired design features actual LED lights that pulse with your playing intensity, while the neck is inlaid with binary code patterns that glow under stage lights. Each pickup has been custom-wound to produce tones ranging from clean digital precision to glitched-out distortion, perfect for electronic music fusion. The Motherboard Guitar seamlessly bridges the gap between traditional craftsmanship and cutting-edge technology, making it the ultimate instrument for the digital age musician.", + shortDescription: + 'A tech-inspired electric guitar featuring LED lights and binary code inlays that glow under stage lights.', + price: 649, + }, + { + id: 4, + name: 'Racing Guitar', + image: '/example-guitar-racing.jpg', + description: + "Engineered for speed and precision, the Racing Guitar embodies the spirit of motorsport in every curve and contour. Its aerodynamic body, painted in classic racing stripes and high-gloss finish, is crafted from lightweight materials that allow for effortless play during extended performances. The custom low-action setup and streamlined neck profile enable lightning-fast fretwork, while specially designed pickups deliver a high-octane tone that cuts through any mix. Built with performance-grade hardware including racing-inspired control knobs and checkered flag inlays, this guitar isn't just played—it's driven to the limits of musical possibility.", + shortDescription: + 'A lightweight, aerodynamic guitar with racing stripes and a low-action setup designed for speed and precision.', + price: 679, + }, + { + id: 5, + name: 'Steamer Trunk Guitar', + image: '/example-guitar-steamer-trunk.jpg', + description: + 'The Steamer Trunk Guitar is a semi-hollow body instrument that exudes vintage charm and character. Crafted from reclaimed antique luggage wood, it features brass hardware that adds a touch of elegance and durability. The fretboard is adorned with a world map inlay, making it a unique piece that tells a story of travel and adventure.', + shortDescription: + 'A semi-hollow body guitar with brass hardware and a world map inlay, crafted from reclaimed antique luggage wood.', + price: 629, + }, + { + id: 6, + name: "Travelin' Man Guitar", + image: '/example-guitar-traveling.jpg', + description: + "The Travelin' Man Guitar is an acoustic masterpiece adorned with vintage postcards from around the world. Each postcard tells a story of adventure and wanderlust, making this guitar a unique piece of art. Its rich, resonant tones and comfortable playability make it perfect for musicians who love to travel and perform.", + shortDescription: + 'An acoustic guitar with vintage postcards, rich tones, and comfortable playability.', + price: 499, + }, + { + id: 7, + name: 'Flowerly Love Guitar', + image: '/example-guitar-flowers.jpg', + description: + "The Flowerly Love Guitar is an acoustic masterpiece adorned with intricate floral designs on its body. Each flower is hand-painted, adding a touch of nature's beauty to the instrument. Its warm, resonant tones make it perfect for both intimate performances and larger gatherings.", + shortDescription: + 'An acoustic guitar with hand-painted floral designs and warm, resonant tones.', + price: 599, + }, +] + +export default guitars diff --git a/examples/ts-solid-chat/src/lib/guitar-tools.ts b/examples/ts-solid-chat/src/lib/guitar-tools.ts new file mode 100644 index 000000000..41a9bcf1e --- /dev/null +++ b/examples/ts-solid-chat/src/lib/guitar-tools.ts @@ -0,0 +1,104 @@ +import { tool } from '@tanstack/ai' +import guitars from '@/data/example-guitars' + +export const getGuitarsTool = tool({ + type: 'function', + function: { + name: 'getGuitars', + description: 'Get all products from the database', + parameters: { + type: 'object', + properties: {}, + required: [], + }, + }, + execute: async () => { + return JSON.stringify(guitars) + }, +}) + +export const recommendGuitarTool = tool({ + type: 'function', + function: { + name: 'recommendGuitar', + description: + 'REQUIRED tool to display a guitar recommendation to the user. This tool MUST be used whenever recommending a guitar - do NOT write recommendations yourself. This displays the guitar in a special appealing format with a buy button.', + parameters: { + type: 'object', + properties: { + id: { + type: 'string', + description: + 'The ID of the guitar to recommend (from the getGuitars results)', + }, + }, + required: ['id'], + }, + }, + // No execute = client-side tool +}) + +export const getPersonalGuitarPreferenceTool = tool({ + type: 'function', + function: { + name: 'getPersonalGuitarPreference', + description: + "Get the user's guitar preference from their local browser storage", + parameters: { + type: 'object', + properties: {}, + }, + }, + // No execute = client-side tool +}) + +export const addToWishListTool = tool({ + type: 'function', + function: { + name: 'addToWishList', + description: "Add a guitar to the user's wish list (requires approval)", + parameters: { + type: 'object', + properties: { + guitarId: { type: 'string' }, + }, + required: ['guitarId'], + }, + }, + needsApproval: true, + // No execute = client-side but needs approval +}) + +export const addToCartTool = tool({ + type: 'function', + function: { + name: 'addToCart', + description: 'Add a guitar to the shopping cart (requires approval)', + parameters: { + type: 'object', + properties: { + guitarId: { type: 'string' }, + quantity: { type: 'number' }, + }, + required: ['guitarId', 'quantity'], + }, + }, + needsApproval: true, + execute: async (args) => { + return JSON.stringify({ + success: true, + cartId: 'CART_' + Date.now(), + guitarId: args.guitarId, + quantity: args.quantity, + totalItems: args.quantity, + }) + }, +}) + +export const allTools = [ + getGuitarsTool, + recommendGuitarTool, + getPersonalGuitarPreferenceTool, + addToWishListTool, + addToCartTool, +] diff --git a/examples/ts-solid-chat/src/lib/stub-adapter.ts b/examples/ts-solid-chat/src/lib/stub-adapter.ts new file mode 100644 index 000000000..0d519594c --- /dev/null +++ b/examples/ts-solid-chat/src/lib/stub-adapter.ts @@ -0,0 +1,33 @@ +import type { + AIAdapter, + ChatCompletionOptions, + StreamChunk, +} from '@tanstack/ai' +import { stubLLM } from './stub-llm' + +/** + * Stub adapter for testing without using real LLM tokens + * Returns canned tool call responses based on user input + */ +export function stubAdapter(): AIAdapter< + string, + any, + any, + any, + any, + any, + any, + any, + any, + any +> { + return { + name: 'stub', + async *chatStream( + options: ChatCompletionOptions, + ): AsyncIterable { + // Use stub LLM instead of real API + yield* stubLLM(options.messages) + }, + } as any +} diff --git a/examples/ts-solid-chat/src/lib/stub-llm.ts b/examples/ts-solid-chat/src/lib/stub-llm.ts new file mode 100644 index 000000000..064851151 --- /dev/null +++ b/examples/ts-solid-chat/src/lib/stub-llm.ts @@ -0,0 +1,399 @@ +import type { ModelMessage, StreamChunk } from '@tanstack/ai' + +/** + * Stub LLM for testing tool calls without burning tokens + * Detects which tool to call from user message keywords + */ +export async function* stubLLM( + messages: ModelMessage[], +): AsyncIterable { + const lastMessage = messages[messages.length - 1] + const userMessage = lastMessage?.content?.toLowerCase() || '' + + const baseId = `stub_${Date.now()}` + const timestamp = Date.now() + + // Check if we have any assistant messages with tool calls already + // If so, this is a continuation after approval/execution, not a new request + // Handle both ModelMessage (toolCalls) and UIMessage (parts) formats + const hasExistingToolCalls = messages.some((m) => { + if (m.role !== 'assistant') return false + // Check ModelMessage format + if (m.toolCalls && m.toolCalls.length > 0) return true + // Check UIMessage format + if ((m as any).parts) { + const parts = (m as any).parts + return parts.some((p: any) => p.type === 'tool-call') + } + return false + }) + + if (hasExistingToolCalls && lastMessage?.role === 'assistant') { + // This means we're being called after an approval/tool execution + // Check if the user approved or denied by looking at tool results + let wasApproved = false + let wasDenied = false + + // Check for tool results + const toolResults = messages.filter((m) => m.role === 'tool') + if (toolResults.length > 0) { + // Check if any were successful or had errors + for (const result of toolResults) { + try { + const parsed = JSON.parse(result.content || '{}') + if (parsed.error && parsed.error.includes('declined')) { + wasDenied = true + } else if (parsed.success || !parsed.error) { + wasApproved = true + } + } catch { + // If we can't parse, assume success + wasApproved = true + } + } + } else { + // No tool results yet, must have just gotten approval response + // Check the assistant message for approval status in UIMessage format + if ((lastMessage as any).parts) { + const parts = (lastMessage as any).parts + for (const part of parts) { + if (part.type === 'tool-call' && part.approval) { + if (part.approval.approved === true) { + wasApproved = true + } else if (part.approval.approved === false) { + wasDenied = true + } + } + } + } else if (lastMessage.toolCalls) { + // This is a ModelMessage, check if it has approval info in content + // (won't have it here, but we can infer from lack of tool results) + // Default to approved for now + wasApproved = true + } + } + + let response = '' + if (wasDenied) { + response = + 'No worries! Maybe another time. Let me know if you need anything else.' + } else if (wasApproved) { + response = 'All set! Let me know if you need anything else.' + } else { + response = 'Let me know if you need anything else!' + } + + for (let i = 0; i < response.length; i++) { + yield { + type: 'content', + id: baseId, + model: 'stub-llm', + timestamp, + delta: response[i], + content: response.substring(0, i + 1), + role: 'assistant', + } + } + + yield { + type: 'done', + id: baseId, + model: 'stub-llm', + timestamp, + finishReason: 'stop', + } + return + } + + // Check if this is a follow-up after tool execution + const hasToolResults = messages.some((m) => m.role === 'tool') + + if (hasToolResults) { + // Check if user approved or denied + const lastAssistant = [...messages] + .reverse() + .find((m) => m.role === 'assistant' && m.toolCalls) + + if (lastAssistant) { + // Look for approval in tool results + const approvedTools = messages + .filter((m) => m.role === 'tool') + .filter((m) => { + try { + const result = JSON.parse(m.content || '{}') + return !result.error + } catch { + return true + } + }) + + const deniedTools = messages + .filter((m) => m.role === 'tool') + .filter((m) => { + try { + const result = JSON.parse(m.content || '{}') + return result.error?.includes('declined') + } catch { + return false + } + }) + + let responseText = '' + if (approvedTools.length > 0) { + responseText = "Good for you! I've processed that request." + } else if (deniedTools.length > 0) { + responseText = 'Bummer! Maybe another time.' + } else { + responseText = 'Complete! If you need anything else, feel free to ask.' + } + + // Send final response + for (const char of responseText) { + const accumulated = responseText.substring( + 0, + responseText.indexOf(char) + 1, + ) + yield { + type: 'content', + id: baseId, + model: 'stub-llm', + timestamp, + delta: char, + content: accumulated, + role: 'assistant', + } + } + + yield { + type: 'done', + id: baseId, + model: 'stub-llm', + timestamp, + finishReason: 'stop', + } + return + } + } + + // Detect which tool to call based on user message + if (userMessage.includes('preference')) { + // Send initial text + const initText = 'Let me check your preferences...' + for (let i = 0; i < initText.length; i++) { + yield { + type: 'content', + id: baseId, + model: 'stub-llm', + timestamp, + delta: initText[i], + content: initText.substring(0, i + 1), + role: 'assistant', + } + } + + // Call getPersonalGuitarPreference + yield { + type: 'tool_call', + id: baseId, + model: 'stub-llm', + timestamp, + toolCall: { + id: `call_${Date.now()}`, + type: 'function', + function: { + name: 'getPersonalGuitarPreference', + arguments: '{}', + }, + }, + index: 0, + } + + yield { + type: 'done', + id: baseId, + model: 'stub-llm', + timestamp, + finishReason: 'tool_calls', + } + return + } + + if (userMessage.includes('recommend') || userMessage.includes('acoustic')) { + // Send initial text + const initText = 'Let me find the perfect guitar for you!' + for (let i = 0; i < initText.length; i++) { + yield { + type: 'content', + id: baseId, + model: 'stub-llm', + timestamp, + delta: initText[i], + content: initText.substring(0, i + 1), + role: 'assistant', + } + } + + // Call getGuitars then recommendGuitar + const getGuitarsId = `call_${Date.now()}_1` + yield { + type: 'tool_call', + id: baseId, + model: 'stub-llm', + timestamp, + toolCall: { + id: getGuitarsId, + type: 'function', + function: { + name: 'getGuitars', + arguments: '{}', + }, + }, + index: 0, + } + + yield { + type: 'done', + id: baseId, + model: 'stub-llm', + timestamp, + finishReason: 'tool_calls', + } + + // After getGuitars result, call recommendGuitar + const recommendId = `call_${Date.now()}_2` + yield { + type: 'tool_call', + id: baseId + '_2', + model: 'stub-llm', + timestamp: timestamp + 100, + toolCall: { + id: recommendId, + type: 'function', + function: { + name: 'recommendGuitar', + arguments: JSON.stringify({ id: '6' }), + }, + }, + index: 0, + } + + yield { + type: 'done', + id: baseId + '_2', + model: 'stub-llm', + timestamp: timestamp + 100, + finishReason: 'tool_calls', + } + return + } + + if (userMessage.includes('wish list')) { + // Send initial text + const initText = + "I'll add that to your wish list. Just need your approval first!" + for (let i = 0; i < initText.length; i++) { + yield { + type: 'content', + id: baseId, + model: 'stub-llm', + timestamp, + delta: initText[i], + content: initText.substring(0, i + 1), + role: 'assistant', + } + } + + // Call addToWishList (needs approval) + yield { + type: 'tool_call', + id: baseId, + model: 'stub-llm', + timestamp, + toolCall: { + id: `call_${Date.now()}`, + type: 'function', + function: { + name: 'addToWishList', + arguments: JSON.stringify({ guitarId: '6' }), + }, + }, + index: 0, + } + + yield { + type: 'done', + id: baseId, + model: 'stub-llm', + timestamp, + finishReason: 'tool_calls', + } + return + } + + if (userMessage.includes('cart')) { + // Send initial text + const initText = + "Ready to add to your cart! I'll need your approval to proceed." + for (let i = 0; i < initText.length; i++) { + yield { + type: 'content', + id: baseId, + model: 'stub-llm', + timestamp, + delta: initText[i], + content: initText.substring(0, i + 1), + role: 'assistant', + } + } + + // Call addToCart (needs approval) + yield { + type: 'tool_call', + id: baseId, + model: 'stub-llm', + timestamp, + toolCall: { + id: `call_${Date.now()}`, + type: 'function', + function: { + name: 'addToCart', + arguments: JSON.stringify({ guitarId: '6', quantity: 1 }), + }, + }, + index: 0, + } + + yield { + type: 'done', + id: baseId, + model: 'stub-llm', + timestamp, + finishReason: 'tool_calls', + } + return + } + + // Default response + const response = + 'I can help with guitar preferences, recommendations, wish lists, and cart!' + for (const char of response) { + const accumulated = response.substring(0, response.indexOf(char) + 1) + yield { + type: 'content', + id: baseId, + model: 'stub-llm', + timestamp, + delta: char, + content: accumulated, + role: 'assistant', + } + } + + yield { + type: 'done', + id: baseId, + model: 'stub-llm', + timestamp, + finishReason: 'stop', + } +} diff --git a/examples/ts-solid-chat/src/logo.svg b/examples/ts-solid-chat/src/logo.svg new file mode 100644 index 000000000..fe53fe8d0 --- /dev/null +++ b/examples/ts-solid-chat/src/logo.svg @@ -0,0 +1,12 @@ + + + logo + + \ No newline at end of file diff --git a/examples/ts-solid-chat/src/routeTree.gen.ts b/examples/ts-solid-chat/src/routeTree.gen.ts new file mode 100644 index 000000000..667067058 --- /dev/null +++ b/examples/ts-solid-chat/src/routeTree.gen.ts @@ -0,0 +1,156 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ApiTestChatRouteImport } from './routes/api.test-chat' +import { Route as ApiTanchatRouteImport } from './routes/api.tanchat' +import { Route as ExampleGuitarsIndexRouteImport } from './routes/example.guitars/index' +import { Route as ExampleGuitarsGuitarIdRouteImport } from './routes/example.guitars/$guitarId' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiTestChatRoute = ApiTestChatRouteImport.update({ + id: '/api/test-chat', + path: '/api/test-chat', + getParentRoute: () => rootRouteImport, +} as any) +const ApiTanchatRoute = ApiTanchatRouteImport.update({ + id: '/api/tanchat', + path: '/api/tanchat', + getParentRoute: () => rootRouteImport, +} as any) +const ExampleGuitarsIndexRoute = ExampleGuitarsIndexRouteImport.update({ + id: '/example/guitars/', + path: '/example/guitars/', + getParentRoute: () => rootRouteImport, +} as any) +const ExampleGuitarsGuitarIdRoute = ExampleGuitarsGuitarIdRouteImport.update({ + id: '/example/guitars/$guitarId', + path: '/example/guitars/$guitarId', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/tanchat': typeof ApiTanchatRoute + '/api/test-chat': typeof ApiTestChatRoute + '/example/guitars/$guitarId': typeof ExampleGuitarsGuitarIdRoute + '/example/guitars': typeof ExampleGuitarsIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/tanchat': typeof ApiTanchatRoute + '/api/test-chat': typeof ApiTestChatRoute + '/example/guitars/$guitarId': typeof ExampleGuitarsGuitarIdRoute + '/example/guitars': typeof ExampleGuitarsIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/tanchat': typeof ApiTanchatRoute + '/api/test-chat': typeof ApiTestChatRoute + '/example/guitars/$guitarId': typeof ExampleGuitarsGuitarIdRoute + '/example/guitars/': typeof ExampleGuitarsIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/api/tanchat' + | '/api/test-chat' + | '/example/guitars/$guitarId' + | '/example/guitars' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/api/tanchat' + | '/api/test-chat' + | '/example/guitars/$guitarId' + | '/example/guitars' + id: + | '__root__' + | '/' + | '/api/tanchat' + | '/api/test-chat' + | '/example/guitars/$guitarId' + | '/example/guitars/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiTanchatRoute: typeof ApiTanchatRoute + ApiTestChatRoute: typeof ApiTestChatRoute + ExampleGuitarsGuitarIdRoute: typeof ExampleGuitarsGuitarIdRoute + ExampleGuitarsIndexRoute: typeof ExampleGuitarsIndexRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/test-chat': { + id: '/api/test-chat' + path: '/api/test-chat' + fullPath: '/api/test-chat' + preLoaderRoute: typeof ApiTestChatRouteImport + parentRoute: typeof rootRouteImport + } + '/api/tanchat': { + id: '/api/tanchat' + path: '/api/tanchat' + fullPath: '/api/tanchat' + preLoaderRoute: typeof ApiTanchatRouteImport + parentRoute: typeof rootRouteImport + } + '/example/guitars/': { + id: '/example/guitars/' + path: '/example/guitars' + fullPath: '/example/guitars' + preLoaderRoute: typeof ExampleGuitarsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/example/guitars/$guitarId': { + id: '/example/guitars/$guitarId' + path: '/example/guitars/$guitarId' + fullPath: '/example/guitars/$guitarId' + preLoaderRoute: typeof ExampleGuitarsGuitarIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiTanchatRoute: ApiTanchatRoute, + ApiTestChatRoute: ApiTestChatRoute, + ExampleGuitarsGuitarIdRoute: ExampleGuitarsGuitarIdRoute, + ExampleGuitarsIndexRoute: ExampleGuitarsIndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/examples/ts-solid-chat/src/router.tsx b/examples/ts-solid-chat/src/router.tsx new file mode 100644 index 000000000..d9d23a5d8 --- /dev/null +++ b/examples/ts-solid-chat/src/router.tsx @@ -0,0 +1,13 @@ +import { createRouter } from '@tanstack/solid-router' + +// Import the generated route tree +import { routeTree } from './routeTree.gen' + +// Create a new router instance +export const getRouter = () => { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultPreloadStaleTime: 0, + }) +} diff --git a/examples/ts-solid-chat/src/routes/__root.tsx b/examples/ts-solid-chat/src/routes/__root.tsx new file mode 100644 index 000000000..9aad43ea5 --- /dev/null +++ b/examples/ts-solid-chat/src/routes/__root.tsx @@ -0,0 +1,64 @@ +import { HeadContent, Scripts, createRootRoute } from '@tanstack/solid-router' +import { TanStackRouterDevtoolsPanel } from '@tanstack/solid-router-devtools' +import { TanStackDevtools } from '@tanstack/solid-devtools' +// import { aiDevtoolsPlugin } from "@tanstack/react-ai-devtools"; +import { HydrationScript } from 'solid-js/web' +import appCss from '../styles.css?url' +import Header from '../components/Header' +import type { JSXElement } from 'solid-js' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + { + title: 'TanStack Start Starter', + }, + ], + links: [ + { + rel: 'stylesheet', + href: appCss, + }, + ], + }), + + shellComponent: RootDocument, +}) + +function RootDocument({ children }: { children: JSXElement }) { + return ( + + + + + + +
+ {children} + , + }, + // aiDevtoolsPlugin(), + ]} + eventBusConfig={{ + connectToServerBus: true, + }} + /> + + + + ) +} diff --git a/examples/ts-solid-chat/src/routes/api.tanchat.ts b/examples/ts-solid-chat/src/routes/api.tanchat.ts new file mode 100644 index 000000000..cf863a105 --- /dev/null +++ b/examples/ts-solid-chat/src/routes/api.tanchat.ts @@ -0,0 +1,102 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { + chat, + toStreamResponse, + maxIterations, + chatOptions, +} from '@tanstack/ai' +import { openai } from '@tanstack/ai-openai' +// import { ollama } from "@tanstack/ai-ollama"; +// import { anthropic } from "@tanstack/ai-anthropic"; +// import { gemini } from "@tanstack/ai-gemini"; +import { allTools } from '@/lib/guitar-tools' + +const SYSTEM_PROMPT = `You are a helpful assistant for a guitar store. + +CRITICAL INSTRUCTIONS - YOU MUST FOLLOW THIS EXACT WORKFLOW: + +When a user asks for a guitar recommendation: +1. FIRST: Use the getGuitars tool (no parameters needed) +2. SECOND: Use the recommendGuitar tool with the ID of the guitar you want to recommend +3. NEVER write a recommendation directly - ALWAYS use the recommendGuitar tool + +IMPORTANT: +- The recommendGuitar tool will display the guitar in a special, appealing format +- You MUST use recommendGuitar for ANY guitar recommendation +- ONLY recommend guitars from our inventory (use getGuitars first) +- The recommendGuitar tool has a buy button - this is how customers purchase +- Do NOT describe the guitar yourself - let the recommendGuitar tool do it + +Example workflow: +User: "I want an acoustic guitar" +Step 1: Call getGuitars() +Step 2: Call recommendGuitar(id: "6") +Step 3: Done - do NOT add any text after calling recommendGuitar +` + +export const Route = createFileRoute('/api/tanchat')({ + server: { + handlers: { + POST: async ({ request }) => { + if (!process.env.OPENAI_API_KEY) { + return new Response( + JSON.stringify({ + error: + 'OPENAI_API_KEY not configured. Please add it to .env or .env.local', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }, + ) + } + + // Capture request signal before reading body (it may be aborted after body is consumed) + const requestSignal = request.signal + + // If request is already aborted, return early + if (requestSignal?.aborted) { + return new Response(null, { status: 499 }) // 499 = Client Closed Request + } + + const abortController = new AbortController() + + const { messages } = await request.json() + try { + // Use the stream abort signal for proper cancellation handling + const stream = chat({ + adapter: openai(), + model: 'gpt-4-turbo', + // model: "claude-sonnet-4-5-20250929", + // model: "smollm", + // model: "gemini-2.5-flash", + tools: allTools, + systemPrompts: [SYSTEM_PROMPT], + agentLoopStrategy: maxIterations(20), + messages, + providerOptions: { + tool_choice: 'auto', + }, + abortController, + }) + + return toStreamResponse(stream, { abortController }) + } catch (error: any) { + // If request was aborted, return early (don't send error response) + if (error.name === 'AbortError' || abortController.signal.aborted) { + return new Response(null, { status: 499 }) // 499 = Client Closed Request + } + return new Response( + JSON.stringify({ + error: error.message || 'An error occurred', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }, + ) + } + }, + }, + }, +}) diff --git a/examples/ts-solid-chat/src/routes/api.test-chat.ts b/examples/ts-solid-chat/src/routes/api.test-chat.ts new file mode 100644 index 000000000..f5d19607d --- /dev/null +++ b/examples/ts-solid-chat/src/routes/api.test-chat.ts @@ -0,0 +1,47 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { chat, maxIterations, toStreamResponse } from '@tanstack/ai' +import { stubAdapter } from '@/lib/stub-adapter' +import { allTools } from '@/lib/guitar-tools' + +export const Route = createFileRoute('/api/test-chat')({ + server: { + handlers: { + POST: async ({ request }) => { + const { messages } = await request.json() + + try { + const stream = chat({ + adapter: stubAdapter(), + messages, + model: 'gpt-4.1-nano', // Doesn't matter for stub + tools: allTools, + systemPrompts: [], + options: { + temperature: 0.7, + topP: 1, + frequencyPenalty: 0, + presencePenalty: 0, + maxTokens: 1000, + stream: true, + seed: 331423424, + }, + agentLoopStrategy: maxIterations(20), + providerOptions: {}, + }) + + return toStreamResponse(stream) + } catch (error: any) { + return new Response( + JSON.stringify({ + error: error.message || 'An error occurred', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }, + ) + } + }, + }, + }, +}) diff --git a/examples/ts-solid-chat/src/routes/example.guitars/$guitarId.tsx b/examples/ts-solid-chat/src/routes/example.guitars/$guitarId.tsx new file mode 100644 index 000000000..39da94fd8 --- /dev/null +++ b/examples/ts-solid-chat/src/routes/example.guitars/$guitarId.tsx @@ -0,0 +1,50 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' +import guitars from '../../data/example-guitars' + +export const Route = createFileRoute('/example/guitars/$guitarId')({ + component: RouteComponent, + loader: async ({ params }) => { + const guitar = guitars.find((guitar) => guitar.id === +params.guitarId) + if (!guitar) { + throw new Error('Guitar not found') + } + return guitar + }, +}) + +function RouteComponent() { + const guitar = Route.useLoaderData() + + return ( +
+
+ + ← Back to all guitars + +

{guitar().name}

+

{guitar().description}

+
+
+ ${guitar().price} +
+ +
+
+ +
+
+ {guitar().name} +
+
+
+ ) +} diff --git a/examples/ts-solid-chat/src/routes/example.guitars/index.tsx b/examples/ts-solid-chat/src/routes/example.guitars/index.tsx new file mode 100644 index 000000000..259dfe326 --- /dev/null +++ b/examples/ts-solid-chat/src/routes/example.guitars/index.tsx @@ -0,0 +1,51 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' +import guitars from '../../data/example-guitars' + +export const Route = createFileRoute('/example/guitars/')({ + component: GuitarsIndex, +}) + +function GuitarsIndex() { + return ( +
+

Featured Guitars

+
+ {guitars.map((guitar) => ( +
+ +
+
+ {guitar.name} +
+
+ +
+ View Details +
+
+ +
+

{guitar.name}

+

+ {guitar.shortDescription} +

+
+ ${guitar.price} +
+
+ +
+ ))} +
+
+ ) +} diff --git a/examples/ts-solid-chat/src/routes/index.tsx b/examples/ts-solid-chat/src/routes/index.tsx new file mode 100644 index 000000000..d33a1e133 --- /dev/null +++ b/examples/ts-solid-chat/src/routes/index.tsx @@ -0,0 +1,403 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { Send, Square } from 'lucide-solid' +import { fetchServerSentEvents, useChat } from '@tanstack/ai-solid' +import { createSignal, For, Show } from 'solid-js' +import type { UIMessage } from '@tanstack/ai-solid' + +import type { JSXElement } from 'solid-js' +import GuitarRecommendation from '@/components/example-GuitarRecommendation' + +function ChatInputArea({ children }: { children: JSXElement }) { + return ( +
+
{children}
+
+ ) +} + +function Messages(props: { + messages: Array + addToolApprovalResponse: (response: { + id: string + approved: boolean + }) => Promise +}) { + return ( +
+ + {({ role, parts }) => ( +
+
+ {role === 'assistant' ? ( +
+ AI +
+ ) : ( +
+ U +
+ )} +
+ {/* Render parts in order */} + + {(part, index) => { + if (part.type === 'text' && part.content) { + return ( +
+ {part.content} +
+ ) + } + + // Approval UI + if ( + part.type === 'tool-call' && + part.state === 'approval-requested' && + part.approval + ) { + return ( +
+

+ 🔒 Approval Required: {part.name} +

+
+
+                              {JSON.stringify(
+                                JSON.parse(part.arguments),
+                                null,
+                                2,
+                              )}
+                            
+
+
+ + +
+
+ ) + } + + // Guitar recommendation card + if ( + part.type === 'tool-call' && + part.name === 'recommendGuitar' && + part.output + ) { + try { + return ( +
+ +
+ ) + } catch { + return null + } + } + + return null + }} +
+
+
+
+ )} +
+
+ ) +} + +function DebugPanel(props: { + messages: Array + chunks: Array + onClearChunks: () => void +}) { + const [activeTab, setActiveTab] = createSignal<'messages' | 'chunks'>( + 'messages', + ) + + const exportToTypeScript = () => { + const tsCode = `const rawChunks = ${JSON.stringify(props.chunks, null, 2)};` + navigator.clipboard.writeText(tsCode) + alert('TypeScript code copied to clipboard!') + } + + return ( +
+
+

Debug Panel

+

+ View messages and raw stream chunks +

+ + {/* Tabs */} +
+ + +
+
+ +
+ {activeTab() === 'messages' && ( +
+
+              {JSON.stringify(props.messages, null, 2)}
+            
+
+ )} + + {activeTab() === 'chunks' && ( +
+
+ + +
+ + {/* Chunks Table */} +
+ + + + + + + + + + + + + {(chunk, idx) => { + const role = chunk.role || '-' + const toolType = chunk.toolCall?.type || '-' + const toolName = chunk.toolCall?.function?.name || '-' + + let detail = '-' + if (chunk.type === 'content' && chunk.content) { + detail = chunk.content + } else if ( + chunk.type === 'tool_call' && + chunk.toolCall?.function?.arguments + ) { + detail = chunk.toolCall.function.arguments + } else if ( + chunk.type === 'tool_result' && + chunk.content + ) { + detail = chunk.content + } else if (chunk.type === 'done') { + detail = `Finish: ${chunk.finishReason || 'unknown'}` + } + + // Truncate at 200 chars + if (detail.length > 200) { + detail = detail.substring(0, 200) + '...' + } + + return ( + + + + + + + + ) + }} + + +
TypeRoleTool TypeTool NameDetail
{chunk.type}{role}{toolType}{toolName} + {detail} +
+
+
+ )} +
+
+ ) +} + +function ChatPage() { + const [chunks, setChunks] = createSignal>([]) + + const { messages, sendMessage, isLoading, addToolApprovalResponse, stop } = + useChat({ + connection: fetchServerSentEvents('/api/tanchat'), + onChunk: (chunk: any) => { + setChunks((prev) => [...prev, chunk]) + }, + onToolCall: async ({ toolName, input }) => { + // Handle client-side tool execution + switch (toolName) { + case 'getPersonalGuitarPreference': + // Pure client tool - executes immediately + return { preference: 'acoustic' } + + case 'recommendGuitar': + // Client tool for UI display + return { id: input.id } + + case 'addToWishList': + // Hybrid: client execution AFTER approval + // Only runs after user approves + const wishList = JSON.parse( + localStorage.getItem('wishList') || '[]', + ) + wishList.push(input.guitarId) + localStorage.setItem('wishList', JSON.stringify(wishList)) + return { + success: true, + guitarId: input.guitarId, + totalItems: wishList.length, + } + + default: + throw new Error(`Unknown client tool: ${toolName}`) + } + }, + }) + const [input, setInput] = createSignal('') + + const clearChunks = () => setChunks([]) + + return ( +
+ {/* Left side - Chat (1/4 width) */} +
+
+

+ TanStack Chat +

+

+ Parts-based UIMessages with tool states +

+
+ + + + +
+ {isLoading() && ( +
+ +
+ )} +
+