Skip to content

Adding a full chat example #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 94 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
f5f1943
Copying chat example across
geelen Jun 19, 2025
e949588
MCP tool calling woo!
geelen Jun 19, 2025
d25f6c1
Moving most of the debug logging behind localStorage.USE_MCP_DEBUG
geelen Jun 19, 2025
769475c
Working on tool call display
geelen Jun 19, 2025
6ebb156
working tool call ui!
geelen Jun 19, 2025
55c1756
deployng to workers.dev for sharing
geelen Jun 20, 2025
15bb636
Simplify chat UI to focus on main input with pastel gradient background
geelen Jun 20, 2025
668ecaf
Add animated rotating conical gradient background
geelen Jun 20, 2025
f94a80c
Switch to hue-rotate animation for background gradient
geelen Jun 20, 2025
d47124b
Slow down hue-rotate animation to 60 seconds
geelen Jun 20, 2025
433eac5
Add random starting point for hue-rotate animation
geelen Jun 20, 2025
b33bde6
Use CSS custom property for cleaner random animation delay
geelen Jun 21, 2025
6a79264
Add brain emoji model selector with fullscreen modal
geelen Jun 23, 2025
50e97bb
Isolate hue-rotate animation to background layer only
geelen Jun 23, 2025
3461d3c
Replace red X with grey 'none' text and add pointer cursor
geelen Jun 23, 2025
11360f3
Add MCP server indicator with plug emoji and modal
geelen Jun 23, 2025
b1f7709
Move number to left of plug emoji
geelen Jun 23, 2025
c3624e6
Keep MCP modal in React tree to maintain connection
geelen Jun 23, 2025
812fdf9
Fix MCP tools count update in plug icon indicator
geelen Jun 23, 2025
1514c64
Add cursor pointer to X button and click-outside-to-dismiss
geelen Jun 23, 2025
1f80d70
Add cursor pointer and click-outside-to-dismiss to model selector modal
geelen Jun 23, 2025
d15c884
Implement multiple MCP servers support
geelen Jun 23, 2025
644b3b6
Update plug indicator to show enabled vs total counts
geelen Jun 23, 2025
0ce394f
Update plug indicator format with comma separation
geelen Jun 23, 2025
1e68fca
Change plug indicator format to use forward slashes
geelen Jun 23, 2025
272fd56
Soften modal background with radial gradient
geelen Jun 23, 2025
32a6bf5
Replace gradient with uniform 80% black background
geelen Jun 23, 2025
78f0e36
Implement OAuth callback support for MCP servers
geelen Jun 23, 2025
089c1c9
Deploying to chat.use-mcp.dev
geelen Jun 23, 2025
ec7f1ff
Add Qwen 3 32B model under Groq provider
geelen Jun 23, 2025
4680b9d
Make gradient background fill entire content height
geelen Jun 23, 2025
71b07c4
Make gradient background repeat every 100vh
geelen Jun 23, 2025
a3200e0
Doing some manual CSS hax for a change
geelen Jun 23, 2025
5087eba
Update AGENT.md with chat UI development guidelines
geelen Jun 23, 2025
8049c6b
I am dum. CSS is smrt
geelen Jun 24, 2025
43dc39d
BG css tweaks
geelen Jun 24, 2025
ace85a6
Added a simple example server for the demo
geelen Jun 24, 2025
62000dc
feat: Add reasoning token support with expandable thinking blocks
geelen Jun 24, 2025
25857bc
fix: Use extractReasoningMiddleware to properly parse thinking blocks
geelen Jun 24, 2025
f53bc68
Added a simpe Hono server to test
geelen Jun 24, 2025
9a825d5
fix: Remove thinking XML tags from main message content
geelen Jun 24, 2025
03dd988
fix: Improve thinking tag removal with more aggressive cleaning
geelen Jun 24, 2025
6c7422f
fix: Add detailed debugging and use result.text for cleaned content
geelen Jun 24, 2025
ab9f3c0
fix: Capture reasoning events during streaming instead of from result
geelen Jun 24, 2025
33c914e
debug: Add more detailed logging for reasoning event handling
geelen Jun 24, 2025
68ecdeb
fix: Clean think tags from reasoning content before display
geelen Jun 24, 2025
bfe2bd5
feat: Implement live reasoning streaming with timing display
geelen Jun 24, 2025
b810f63
fix: Create assistant message immediately when reasoning starts
geelen Jun 24, 2025
78e0103
fix: Remove delay for instant reasoning block collapse
geelen Jun 24, 2025
ccad616
feat: Add sliding window effect for streaming reasoning text
geelen Jun 24, 2025
ba9e6c9
Add debug logging and Accept headers for MCP transport debugging
geelen Jun 24, 2025
03d95e7
Add httpOnly option to disable SSE fallback for debugging
geelen Jun 24, 2025
e7dd577
Replace httpOnly with transportType option and add UI controls
geelen Jun 24, 2025
6126405
Fix tool results being mixed into thinking blocks
geelen Jun 24, 2025
d2b6f80
Fix duplicate tool results and post-tool reasoning being hidden
geelen Jun 24, 2025
59026ad
Fix message ordering and prevent content duplication in thinking/fina…
geelen Jun 24, 2025
0ed5dba
Add intelligent content filtering to extract clean final answer
geelen Jun 24, 2025
55d54bc
Fix duplicate assistant messages by updating existing message
geelen Jun 24, 2025
8159950
Fix message ordering - create final response at end instead of updati…
geelen Jun 24, 2025
dffc1aa
Make thinking block collapse immediately when tool call starts
geelen Jun 24, 2025
4f87d82
Fix reasoning/content separation and eliminate empty message divs
geelen Jun 25, 2025
2c83910
Clean up debug logging in useStreamResponse.ts
geelen Jun 25, 2025
4e282ea
Add targeted debug logging for message operations
geelen Jun 25, 2025
12dc643
Enhance debug logging to show complete conversation state
geelen Jun 25, 2025
840fbb4
Simplify debug logging to show raw JSON messages
geelen Jun 25, 2025
ab73226
Remove all duplicate detection logic
geelen Jun 25, 2025
51fafb4
Branching off
geelen Jun 25, 2025
5e69116
fixing prettier
geelen Jun 25, 2025
6429f48
lfg manual style
geelen Jun 25, 2025
022747b
wip, changing the logic in useStreamResponse
geelen Jun 25, 2025
52c3e8d
wip, at least prettier likes it
geelen Jun 25, 2025
ba845e7
Big sigh, finally getting control of my markup
geelen Jun 25, 2025
caed6d3
Starting to realise how dumb the vibecoded UI was oops
geelen Jun 25, 2025
0c7b9e0
Ok turns out I can be dumb too
geelen Jun 25, 2025
5e41222
Handling reasoning then messages at least
geelen Jun 25, 2025
9b34461
tool responses are easy now too
geelen Jun 25, 2025
b07b0a7
visual tweaks
geelen Jun 25, 2025
cd8df15
Only updating one conversation at a time
geelen Jun 25, 2025
f460842
Ok _much_ better timing and just me understanding this stuff
geelen Jun 25, 2025
0d0f30f
Almost ready
geelen Jun 25, 2025
39043c1
fin
geelen Jun 25, 2025
4e4d9d9
Default transport type to HTTP instead of auto
geelen Jun 25, 2025
59d2dfc
Move transport toggle to left and make it only affect new servers
geelen Jun 25, 2025
c834b11
Round corners more on blue connected server transport type badge
geelen Jun 25, 2025
da4778e
Tighten markdown spacing in chat messages
geelen Jun 26, 2025
7cd0c7f
Merge branch 'main' into chat-example
geelen Jul 2, 2025
212c530
removing dupe server
geelen Jul 2, 2025
aab5630
Forcing parsed reasoning on groq
geelen Jul 2, 2025
5b99b66
Added inline in-conversation errors
geelen Jul 2, 2025
c810c38
Add `preventAutoAuth` (to chat example) (#18)
geelen Jul 2, 2025
5b55264
Chat example cooked and ready
geelen Jul 2, 2025
453a5af
I aint got time for this typescript nonsense rn
geelen Jul 2, 2025
ef3d818
fixing pnpm lock
geelen Jul 2, 2025
7f9c877
prettier
geelen Jul 2, 2025
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
30 changes: 30 additions & 0 deletions examples/chat-ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

.wrangler

# Playwright test results
test-results/
playwright-report/
7 changes: 7 additions & 0 deletions examples/chat-ui/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 140
}
73 changes: 73 additions & 0 deletions examples/chat-ui/AGENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# AI Chat Template - Development Guide

## Commands
- **Dev server**: `pnpm dev`
- **Build**: `pnpm build` (runs TypeScript compilation then Vite build)
- **Lint**: `pnpm lint` (ESLint)
- **Deploy**: `pnpm deploy` (builds and deploys with Wrangler)
- **Test**: `pnpm test` (Playwright E2E tests)
- **Test UI**: `pnpm test:ui` (Playwright test runner with UI)
- **Test headed**: `pnpm test:headed` (Run tests in visible browser)

## Code Style
- **Formatting**: Prettier with 2-space tabs, single quotes, no semicolons, 140 char line width
- **Imports**: Use `.tsx`/`.ts` extensions in imports, group by external/internal
- **Components**: React FC with explicit typing, PascalCase names
- **Hooks**: Custom hooks start with `use`, camelCase
- **Types**: Define interfaces in `src/types/index.ts`, use `type` for unions
- **Files**: Use PascalCase for components, camelCase for hooks/utilities
- **State**: Use proper TypeScript typing for all state variables
- **Error handling**: Use try/catch blocks with proper error propagation
- **Database**: IndexedDB with typed interfaces, async/await pattern
- **Styling**: Tailwind CSS classes, responsive design patterns

## Tech Stack
React 19, TypeScript, Vite, Tailwind CSS, Hono API, Cloudflare Workers, IndexedDB, React Router, use-mcp

## Chat UI Specific Guidelines

### Background Animation
- Use CSS `hue-rotate` filter for color cycling (more performant than gradient animation)
- Isolate animations to `::before` pseudo-elements with `z-index: -1` to prevent inheritance
- Use `background-size: 100% 100vh` and `background-repeat: repeat-y` for repeating patterns
- Initialize with random animation delay using CSS custom properties

### Modal Patterns
- Use `rgba(0,0,0,0.8)` for modal backdrops (avoid gradients that cause banding)
- Implement click-outside-to-dismiss with `onClick` on backdrop and `stopPropagation` on content
- Add `cursor-pointer` to all clickable elements including close buttons
- Use `document.body.style.overflow = 'hidden'` to prevent background scrolling

### MCP Server Management
- Store multiple servers in localStorage as JSON array (`mcpServers`)
- Track tool counts separately (`mcpServerToolCounts`) for disabled servers
- Use server-specific IDs for state management
- Each MCP server gets unique `useMcp` hook instance for proper isolation
- OAuth state parameter automatically handles multiple server differentiation

### State Persistence
- Use localStorage for user preferences (model selection, server configurations)
- Use sessionStorage for temporary state (single server URL in legacy components)
- Clear related data when removing servers or models

### UI Indicators
- Use emoji-based indicators (🧠 for models, 🔌 for MCP servers)
- Format counts as `enabled/total` (e.g., "1/2 servers, 3/5 tools")
- Place model selector on left, MCP servers on right for balance
- Show "none" instead of red symbols for cleaner unconfigured states

### Component Organization
- Keep disabled components in React tree but hidden with CSS (`{false && ...}`)
- This prevents MCP connections from being destroyed when modals close
- Use conditional rendering sparingly, prefer CSS visibility for stateful components

### Routing and OAuth
- Use React Router for OAuth callback routes (`/oauth/callback`)
- OAuth callback should match main app styling with loading indicators
- use-mcp library handles multiple server OAuth flows automatically via state parameters

### Performance Considerations
- Avoid animating gradients directly (causes repainting)
- Use transform and filter animations (hardware accelerated)
- Aggregate tools from multiple sources efficiently
- Minimize localStorage reads in render loops
30 changes: 30 additions & 0 deletions examples/chat-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# AI chat template

An unofficial template for ⚛️ React ⨉ ⚡️ Vite ⨉ ⛅️ Cloudflare Workers AI.

Full-stack AI chat application using Workers for the APIs (using the Cloudflare [vite-plugin](https://www.npmjs.com/package/@cloudflare/vite-plugin)) and Vite for the React application (hosted using [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/)). Provides chat functionality with [Workers AI](https://developers.cloudflare.com/workers-ai/), stores conversations in the browser's [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), and uses [ai-sdk](https://sdk.vercel.ai/docs/introduction), [tailwindcss](https://tailwindcss.com/) and [workers-ai-provider](https://github.com/cloudflare/workers-ai-provider).

## Get started

Create the project using [create-cloudflare](https://www.npmjs.com/package/create-cloudflare):

```sh
npm create cloudflare@latest -- --template thomasgauvin/ai-chat-template
```

Run the project and deploy it:

```sh
cd <project-name>
npm install
npm run dev
```

```
npm run deploy
```

## What's next?

- Change the name of the package (in `package.json`)
- Change the name of the worker (in `wrangler.jsonc`)
118 changes: 118 additions & 0 deletions examples/chat-ui/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { LanguageModelV1StreamPart } from 'ai'
import { streamText, extractReasoningMiddleware, wrapLanguageModel } from 'ai'
import { createWorkersAI } from 'workers-ai-provider'
import { Hono } from 'hono'

interface Env {
ASSETS: Fetcher
AI: Ai
}

type message = {
role: 'system' | 'user' | 'assistant' | 'data'
content: string
}

const app = new Hono<{ Bindings: Env }>()

// Handle the /api/chat endpoint
app.post('/api/chat', async (c) => {
try {
const { messages, reasoning }: { messages: message[]; reasoning: boolean } = await c.req.json()

const workersai = createWorkersAI({ binding: c.env.AI })

// Choose model based on reasoning preference
const model = reasoning
? wrapLanguageModel({
model: workersai('@cf/deepseek-ai/deepseek-r1-distill-qwen-32b'),
middleware: [
extractReasoningMiddleware({ tagName: 'think' }),
//custom middleware to inject <think> tag at the beginning of a reasoning if it is missing
{
wrapGenerate: async ({ doGenerate }) => {
const result = await doGenerate()

if (!result.text?.includes('<think>')) {
result.text = `<think>${result.text}`
}

return result
},
wrapStream: async ({ doStream }) => {
const { stream, ...rest } = await doStream()

let generatedText = ''
const transformStream = new TransformStream<LanguageModelV1StreamPart, LanguageModelV1StreamPart>({
transform(chunk, controller) {
//we are manually adding the <think> tag because some times, distills of reasoning models omit it
if (chunk.type === 'text-delta') {
if (!generatedText.includes('<think>')) {
generatedText += '<think>'
controller.enqueue({
type: 'text-delta',
textDelta: '<think>',
})
}
generatedText += chunk.textDelta
}

controller.enqueue(chunk)
},
})

return {
stream: stream.pipeThrough(transformStream),
...rest,
}
},
},
],
})
: workersai('@cf/meta/llama-3.3-70b-instruct-fp8-fast')

const systemPrompt: message = {
role: 'system',
content: `
- Do not wrap your responses in html tags.
- Do not apply any formatting to your responses.
- You are an expert conversational chatbot. Your objective is to be as helpful as possible.
- You must keep your responses relevant to the user's prompt.
- You must respond with a maximum of 512 tokens (300 words).
- You must respond clearly and concisely, and explain your logic if required.
- You must not provide any personal information.
- Do not respond with your own personal opinions, and avoid topics unrelated to the user's prompt.
${
messages.length <= 1 &&
`- Important REMINDER: You MUST provide a 5 word title at the END of your response using <chat-title> </chat-title> tags.
If you do not do this, this session will error.
For example, <chat-title>Hello and Welcome</chat-title> Hi, how can I help you today?
`
}
`,
}

const text = await streamText({
model,
messages: [systemPrompt, ...messages],
maxTokens: 2048,
maxRetries: 3,
})

return text.toDataStreamResponse({
sendReasoning: true,
})
} catch (error) {
return c.json({ error: `Chat completion failed. ${(error as Error)?.message}` }, 500)
}
})

// Handle static assets and fallback routes
app.all('*', async (c) => {
if (c.env.ASSETS) {
return c.env.ASSETS.fetch(c.req.raw)
}
return c.text('Not found', 404)
})

export default app
45 changes: 45 additions & 0 deletions examples/chat-ui/e2e/build.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { test, expect } from '@playwright/test'
import { exec } from 'child_process'
import { promisify } from 'util'

const execAsync = promisify(exec)

interface ExecError extends Error {
stdout?: string
stderr?: string
}

test.describe('Build Tests', () => {
test('should build without type errors', async () => {
try {
const { stderr } = await execAsync('pnpm build', {
cwd: process.cwd(),
timeout: 60000,
})

// Check for TypeScript compilation errors
expect(stderr).not.toContain('error TS')
expect(stderr).not.toContain('Type error')
} catch (error) {
const execError = error as ExecError
console.error('Build failed:', execError.stdout, execError.stderr)
throw new Error(`Build failed: ${execError.message}`)
}
})

test('should lint without errors', async () => {
try {
const { stderr } = await execAsync('pnpm lint', {
cwd: process.cwd(),
timeout: 30000,
})

// ESLint should pass without errors
expect(stderr).not.toContain('error')
} catch (error) {
const execError = error as ExecError
console.error('Lint failed:', execError.stdout, execError.stderr)
throw new Error(`Lint failed: ${execError.message}`)
}
})
})
Loading
Loading