Skip to content

Commit 4a91a12

Browse files
committed
feat: tanstack chat example for solid
1 parent 2c8dc4e commit 4a91a12

File tree

14 files changed

+1301
-0
lines changed

14 files changed

+1301
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# TanStack Chat Application
2+
3+
Am example chat application built with TanStack Start, TanStack Store, and Claude AI.
4+
5+
## Sidecar service
6+
7+
This applicaton requires a sidecar microservice to be running. The server is located in the `ai-streaming-service` directory.
8+
9+
In that directory you should edit the `.env.local` file to add your Anthropic API key:
10+
11+
```env
12+
ANTHROPIC_API_KEY=your_anthropic_api_key
13+
```
14+
15+
Then run the server:
16+
17+
```bash
18+
cd ai-streaming-service
19+
npm install
20+
npm run dev
21+
```
22+
23+
## ✨ Features
24+
25+
### AI Capabilities
26+
27+
- 🤖 Powered by Claude 3.5 Sonnet
28+
- 📝 Rich markdown formatting with syntax highlighting
29+
- 🎯 Customizable system prompts for tailored AI behavior
30+
- 🔄 Real-time message updates and streaming responses (coming soon)
31+
32+
### User Experience
33+
34+
- 🎨 Modern UI with Tailwind CSS and Lucide icons
35+
- 🔍 Conversation management and history
36+
- 🔐 Secure API key management
37+
- 📋 Markdown rendering with code highlighting
38+
39+
### Technical Features
40+
41+
- 📦 Centralized state management with TanStack Store
42+
- 🔌 Extensible architecture for multiple AI providers
43+
- 🛠️ TypeScript for type safety
44+
45+
## Architecture
46+
47+
### Tech Stack
48+
49+
- **Routing**: TanStack Router
50+
- **State Management**: TanStack Store
51+
- **Styling**: Tailwind CSS
52+
- **AI Integration**: Anthropic's Claude API
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# AI Streaming Server
2+
3+
An Express server with TypeScript that provides a streaming API endpoint for Anthropic's Claude AI model.
4+
5+
## Setup
6+
7+
1. Clone the repository
8+
2. Install dependencies:
9+
```bash
10+
npm install
11+
```
12+
3. Copy `.env.example` to `.env` and add your Anthropic API key:
13+
```bash
14+
cp .env.example .env
15+
```
16+
4. Edit `.env` and replace `your_api_key_here` with your actual Anthropic API key
17+
18+
## Development
19+
20+
Run in development mode with auto-reload:
21+
22+
```bash
23+
npm run dev
24+
```
25+
26+
Type checking:
27+
28+
```bash
29+
npm run typecheck
30+
```
31+
32+
## Production
33+
34+
Build the TypeScript code:
35+
36+
```bash
37+
npm run build
38+
```
39+
40+
Run in production mode:
41+
42+
```bash
43+
npm start
44+
```
45+
46+
## API Usage
47+
48+
### POST /api/chat
49+
50+
Endpoint that accepts chat messages and returns a streaming response from Claude.
51+
52+
#### Request Body
53+
54+
```typescript
55+
interface ChatMessage {
56+
role: "user" | "assistant";
57+
content: string;
58+
}
59+
60+
interface ChatRequest {
61+
messages: ChatMessage[];
62+
}
63+
```
64+
65+
Example request:
66+
67+
```json
68+
{
69+
"messages": [{ "role": "user", "content": "Hello, how are you?" }]
70+
}
71+
```
72+
73+
#### Response
74+
75+
The endpoint returns a Server-Sent Events (SSE) stream. Each event contains a chunk of the response from Claude.
76+
77+
Example usage with JavaScript/TypeScript:
78+
79+
```typescript
80+
const response = await fetch("http://localhost:3000/api/chat", {
81+
method: "POST",
82+
headers: {
83+
"Content-Type": "application/json",
84+
},
85+
body: JSON.stringify({
86+
messages: [{ role: "user", content: "Hello, how are you?" }],
87+
}),
88+
});
89+
90+
const reader = response.body!.getReader();
91+
const decoder = new TextDecoder();
92+
93+
while (true) {
94+
const { value, done } = await reader.read();
95+
if (done) break;
96+
97+
const chunk = decoder.decode(value);
98+
const lines = chunk.split("\n");
99+
100+
for (const line of lines) {
101+
if (line.startsWith("data: ")) {
102+
const data = JSON.parse(line.slice(6));
103+
if (data === "[DONE]") break;
104+
105+
// Handle the streaming response chunk
106+
console.log(data);
107+
}
108+
}
109+
}
110+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ANTHROPIC_API_KEY=your_api_key_here
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "ai-streaming-server",
3+
"version": "1.0.0",
4+
"description": "Express server for streaming Anthropic AI chat responses",
5+
"main": "dist/index.js",
6+
"type": "module",
7+
"scripts": {
8+
"build": "tsc",
9+
"start": "node dist/index.js",
10+
"dev": "tsx watch src/index.ts",
11+
"typecheck": "tsc --noEmit"
12+
},
13+
"dependencies": {
14+
"@anthropic-ai/sdk": "^0.18.0",
15+
"cors": "^2.8.5",
16+
"dotenv": "^16.4.5",
17+
"express": "^4.18.3"
18+
},
19+
"devDependencies": {
20+
"@types/cors": "^2.8.17",
21+
"@types/express": "^4.17.21",
22+
"@types/node": "^20.11.24",
23+
"tsx": "^4.7.1",
24+
"typescript": "^5.3.3"
25+
}
26+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import express, { Request, Response } from 'express'
2+
import cors from 'cors'
3+
import dotenv from 'dotenv'
4+
import Anthropic from '@anthropic-ai/sdk'
5+
6+
// Load environment variables
7+
dotenv.config()
8+
9+
const app = express()
10+
const port = process.env.PORT || 8080
11+
12+
// Middleware
13+
app.use(cors())
14+
app.use(express.json())
15+
16+
// Initialize Anthropic client
17+
const anthropic = new Anthropic({
18+
apiKey: process.env.ANTHROPIC_API_KEY,
19+
})
20+
21+
// Define types for the request body
22+
interface ChatMessage {
23+
role: 'user' | 'assistant'
24+
content: string
25+
}
26+
27+
interface ChatRequest {
28+
messages: ChatMessage[]
29+
}
30+
31+
// Streaming chat endpoint
32+
app.post(
33+
'/api/chat',
34+
async (req: Request<{}, {}, ChatRequest>, res: Response) => {
35+
try {
36+
const { messages } = req.body
37+
38+
if (!messages || !Array.isArray(messages)) {
39+
return res.status(400).json({ error: 'Messages array is required' })
40+
}
41+
42+
// Validate message format
43+
const isValidMessages = messages.every(
44+
(msg): msg is ChatMessage =>
45+
typeof msg === 'object' &&
46+
(msg.role === 'user' || msg.role === 'assistant') &&
47+
typeof msg.content === 'string',
48+
)
49+
50+
if (!isValidMessages) {
51+
return res.status(400).json({
52+
error:
53+
"Invalid message format. Each message must have 'role' and 'content'",
54+
})
55+
}
56+
57+
// Set up SSE headers
58+
res.setHeader('Content-Type', 'text/event-stream')
59+
res.setHeader('Cache-Control', 'no-cache')
60+
res.setHeader('Connection', 'keep-alive')
61+
62+
// Create the message stream
63+
const stream = await anthropic.messages.create({
64+
messages: messages.map((msg) => ({
65+
role: msg.role,
66+
content: msg.content,
67+
})),
68+
model: 'claude-3-opus-20240229',
69+
max_tokens: 4096,
70+
stream: true,
71+
})
72+
73+
// Stream the response
74+
for await (const chunk of stream) {
75+
if (chunk.type === 'content_block_delta') {
76+
res.write(`data: ${JSON.stringify(chunk)}\n\n`)
77+
}
78+
}
79+
80+
// End the stream
81+
res.write('data: [DONE]\n\n')
82+
res.end()
83+
} catch (error) {
84+
console.error('Error:', error)
85+
// If headers haven't been sent yet, send error response
86+
if (!res.headersSent) {
87+
res.status(500).json({ error: 'Internal server error' })
88+
} else {
89+
// If streaming has started, send error event
90+
const errorMessage =
91+
error instanceof Error ? error.message : 'Unknown error'
92+
res.write(`data: ${JSON.stringify({ error: errorMessage })}\n\n`)
93+
res.end()
94+
}
95+
}
96+
},
97+
)
98+
99+
// Start the server
100+
app.listen(port, () => {
101+
console.log(`Server is running on port ${port}`)
102+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "NodeNext",
5+
"moduleResolution": "NodeNext",
6+
"esModuleInterop": true,
7+
"strict": true,
8+
"skipLibCheck": true,
9+
"outDir": "dist",
10+
"rootDir": "src",
11+
"sourceMap": true
12+
},
13+
"include": ["src/**/*"],
14+
"exclude": ["node_modules", "dist"]
15+
}

0 commit comments

Comments
 (0)