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
11 changes: 11 additions & 0 deletions examples/ts-svelte-chat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.idea
65 changes: 65 additions & 0 deletions examples/ts-svelte-chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# ts-svelte-chat

A SvelteKit chat application powered by TanStack AI.

## Features

- 🎯 Multiple AI providers (OpenAI, Anthropic, Gemini, Ollama)
- πŸ› οΈ Tool execution (client-side and server-side)
- βœ… Tool approval workflow
- 🎸 Guitar recommendation example
- πŸ’­ Thinking process visualization
- 🎨 Modern UI with Tailwind CSS

## Setup

1. Install dependencies:

```bash
pnpm install
```

2. Set up environment variables:

If `.env` doesn't exist, copy from the example:

```bash
cp env.example .env
```

Then edit `.env` and add your API keys:

```bash
OPENAI_API_KEY=sk-your-actual-key-here
ANTHROPIC_API_KEY=sk-ant-your-actual-key-here
GEMINI_API_KEY=your-actual-key-here
```

**Important Notes:**

- The `.env` file must be in the project root (`examples/ts-svelte-chat/.env`)
- You must **restart the dev server** after creating or modifying `.env`
- Environment variables in SvelteKit are loaded at server startup
- These are server-side only variables (not exposed to the browser)

3. Start (or restart) the development server:

```bash
pnpm dev
```

4. Open [http://localhost:3000](http://localhost:3000)

## Architecture

This example demonstrates:

- **@tanstack/ai-svelte**: Svelte 5 hooks for chat functionality
- **SvelteKit**: Full-stack framework with API routes
- **Streaming**: Real-time response streaming
- **Tools**: Both client-side and server-side tool execution
- **Approvals**: Tool approval workflow

## License

MIT
12 changes: 12 additions & 0 deletions examples/ts-svelte-chat/env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# OpenAI
OPENAI_API_KEY=your-openai-key-here

# Anthropic (Claude)
ANTHROPIC_API_KEY=your-anthropic-key-here

# Google (Gemini)
GEMINI_API_KEY=your-gemini-key-here

# Ollama (local) - Optional, defaults to http://localhost:11434
# OLLAMA_BASE_URL=http://localhost:11434

41 changes: 41 additions & 0 deletions examples/ts-svelte-chat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "ts-svelte-chat",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "exit 0"
},
"dependencies": {
"@tanstack/ai": "workspace:*",
"@tanstack/ai-anthropic": "workspace:*",
"@tanstack/ai-client": "workspace:*",
"@tanstack/ai-gemini": "workspace:*",
"@tanstack/ai-ollama": "workspace:*",
"@tanstack/ai-openai": "workspace:*",
"@tanstack/ai-svelte": "workspace:*",
"highlight.js": "^11.11.1",
"lucide-svelte": "^0.468.0",
"marked": "^15.0.6",
"marked-highlight": "^2.2.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/kit": "^2.15.10",
"@sveltejs/vite-plugin-svelte": "^5.1.1",
"@tailwindcss/vite": "^4.1.17",
"@types/node": "^24.10.1",
"svelte": "^5.20.0",
"svelte-check": "^4.2.0",
"tailwindcss": "^4.1.17",
"tslib": "^2.8.1",
"typescript": "5.9.3",
"vite": "^7.2.4"
}
}
73 changes: 73 additions & 0 deletions examples/ts-svelte-chat/src/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
@import 'tailwindcss';

/* Code highlighting styles */
@import 'highlight.js/styles/github-dark.css';

body {
margin: 0;
padding: 0;
}

/* Markdown prose styles for dark mode */
.prose {
color: #e5e7eb;
}

.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
color: #f3f4f6;
}

.prose a {
color: #60a5fa;
}

.prose a:hover {
color: #93c5fd;
}

.prose code {
color: #f3f4f6;
background-color: #374151;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-size: 0.875em;
}

.prose pre {
background-color: #1f2937;
color: #f3f4f6;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
}

.prose pre code {
background-color: transparent;
padding: 0;
color: inherit;
}

.prose strong {
color: #f3f4f6;
}

.prose ul,
.prose ol {
color: #e5e7eb;
}

.prose blockquote {
color: #d1d5db;
border-left-color: #4b5563;
}

/* Ensure links in markdown are visible */
.prose p a {
color: #60a5fa;
text-decoration: underline;
}
12 changes: 12 additions & 0 deletions examples/ts-svelte-chat/src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
93 changes: 93 additions & 0 deletions examples/ts-svelte-chat/src/data/example-guitars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
export interface Guitar {
id: number
name: string
image: string
description: string
shortDescription: string
price: number
}

const guitars: Array<Guitar> = [
{
id: 1,
name: 'TanStack Ukelele',
image: '/example-ukelele-tanstack.jpg',
description:
"Introducing the TanStack Signature Ukuleleβ€”a beautifully handcrafted concert ukulele that combines exceptional sound quality with distinctive style. Featuring a warm, resonant koa-wood body with natural grain patterns, this instrument delivers the rich, mellow tones Hawaii is famous for. The exclusive TanStack palm tree inlay on the soundhole adds a unique touch of island flair, while the matching branded headstock makes this a true collector's piece for developers and musicians alike. Whether you're a beginner looking for a quality starter instrument or an experienced player wanting something special, the TanStack Ukulele brings together craftsmanship, character, and that unmistakable tropical spirit.",
shortDescription:
'Premium koa-wood ukulele featuring exclusive TanStack branding, perfect for beach vibes and island-inspired melodies.',
price: 299,
},
{
id: 2,
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: 3,
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: 4,
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: 5,
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: 6,
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: 7,
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: 8,
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
79 changes: 79 additions & 0 deletions examples/ts-svelte-chat/src/lib/components/ChatInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script lang="ts">
import { Send, Square } from 'lucide-svelte'

interface Props {
value: string
isLoading: boolean
onSend: (message: string) => void
onStop: () => void
}

let { value = $bindable(''), isLoading, onSend, onStop }: Props = $props()

let textarea: HTMLTextAreaElement | undefined = $state()

function handleInput(e: Event) {
const target = e.target as HTMLTextAreaElement
target.style.height = 'auto'
target.style.height = Math.min(target.scrollHeight, 200) + 'px'
}

function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey && value.trim()) {
e.preventDefault()
onSend(value)
value = ''
if (textarea) {
textarea.style.height = 'auto'
}
}
}

function handleSubmit() {
if (value.trim()) {
onSend(value)
value = ''
if (textarea) {
textarea.style.height = 'auto'
}
}
}
</script>

<div class="border-t border-orange-500/10 bg-gray-900/80 backdrop-blur-sm">
<div class="w-full px-4 py-3">
<div class="space-y-3">
{#if isLoading}
<div class="flex items-center justify-center">
<button
onclick={onStop}
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
>
<Square class="w-4 h-4 fill-current" />
Stop
</button>
</div>
{/if}
<div class="relative">
<textarea
bind:this={textarea}
bind:value
oninput={handleInput}
onkeydown={handleKeyDown}
placeholder="Type something clever (or don't, we won't judge)..."
class="w-full rounded-lg border border-orange-500/20 bg-gray-800/50 pl-4 pr-12 py-3 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:border-transparent resize-none overflow-hidden shadow-lg"
rows="1"
style="min-height: 44px; max-height: 200px"
disabled={isLoading}
></textarea>
<button
onclick={handleSubmit}
disabled={!value.trim() || isLoading}
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-orange-500 hover:text-orange-400 disabled:text-gray-500 transition-colors focus:outline-none"
>
<Send class="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
Loading
Loading