Skip to content

Commit dc99e05

Browse files
committed
Revert "Deleting chat ui example as it's not ready"
This reverts commit b43914c.
1 parent dd68c21 commit dc99e05

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+7675
-0
lines changed

examples/chat-ui/.gitignore

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?
25+
26+
.wrangler
27+
28+
# Playwright test results
29+
test-results/
30+
playwright-report/

examples/chat-ui/.prettierrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"trailingComma": "es5",
3+
"tabWidth": 2,
4+
"semi": false,
5+
"singleQuote": true,
6+
"printWidth": 140
7+
}

examples/chat-ui/AGENT.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# AI Chat Template - Development Guide
2+
3+
## Commands
4+
- **Dev server**: `pnpm dev`
5+
- **Build**: `pnpm build` (runs TypeScript compilation then Vite build)
6+
- **Lint**: `pnpm lint` (ESLint)
7+
- **Deploy**: `pnpm deploy` (builds and deploys with Wrangler)
8+
- **Test**: `pnpm test` (Playwright E2E tests)
9+
- **Test UI**: `pnpm test:ui` (Playwright test runner with UI)
10+
- **Test headed**: `pnpm test:headed` (Run tests in visible browser)
11+
12+
## Code Style
13+
- **Formatting**: Prettier with 2-space tabs, single quotes, no semicolons, 140 char line width
14+
- **Imports**: Use `.tsx`/`.ts` extensions in imports, group by external/internal
15+
- **Components**: React FC with explicit typing, PascalCase names
16+
- **Hooks**: Custom hooks start with `use`, camelCase
17+
- **Types**: Define interfaces in `src/types/index.ts`, use `type` for unions
18+
- **Files**: Use PascalCase for components, camelCase for hooks/utilities
19+
- **State**: Use proper TypeScript typing for all state variables
20+
- **Error handling**: Use try/catch blocks with proper error propagation
21+
- **Database**: IndexedDB with typed interfaces, async/await pattern
22+
- **Styling**: Tailwind CSS classes, responsive design patterns
23+
24+
## Tech Stack
25+
React 19, TypeScript, Vite, Tailwind CSS, Hono API, Cloudflare Workers, IndexedDB

examples/chat-ui/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# AI chat template
2+
3+
An unofficial template for ⚛️ React ⨉ ⚡️ Vite ⨉ ⛅️ Cloudflare Workers AI.
4+
5+
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).
6+
7+
## Get started
8+
9+
Create the project using [create-cloudflare](https://www.npmjs.com/package/create-cloudflare):
10+
11+
```sh
12+
npm create cloudflare@latest -- --template thomasgauvin/ai-chat-template
13+
```
14+
15+
Run the project and deploy it:
16+
17+
```sh
18+
cd <project-name>
19+
npm install
20+
npm run dev
21+
```
22+
23+
```
24+
npm run deploy
25+
```
26+
27+
## What's next?
28+
29+
- Change the name of the package (in `package.json`)
30+
- Change the name of the worker (in `wrangler.jsonc`)

examples/chat-ui/api/index.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type { LanguageModelV1StreamPart } from 'ai'
2+
import { streamText, extractReasoningMiddleware, wrapLanguageModel } from 'ai'
3+
import { createWorkersAI } from 'workers-ai-provider'
4+
import { Hono } from 'hono'
5+
6+
interface Env {
7+
ASSETS: Fetcher
8+
AI: Ai
9+
}
10+
11+
type message = {
12+
role: 'system' | 'user' | 'assistant' | 'data'
13+
content: string
14+
}
15+
16+
const app = new Hono<{ Bindings: Env }>()
17+
18+
// Handle the /api/chat endpoint
19+
app.post('/api/chat', async (c) => {
20+
try {
21+
const { messages, reasoning }: { messages: message[]; reasoning: boolean } = await c.req.json()
22+
23+
const workersai = createWorkersAI({ binding: c.env.AI })
24+
25+
// Choose model based on reasoning preference
26+
const model = reasoning
27+
? wrapLanguageModel({
28+
model: workersai('@cf/deepseek-ai/deepseek-r1-distill-qwen-32b'),
29+
middleware: [
30+
extractReasoningMiddleware({ tagName: 'think' }),
31+
//custom middleware to inject <think> tag at the beginning of a reasoning if it is missing
32+
{
33+
wrapGenerate: async ({ doGenerate }) => {
34+
const result = await doGenerate()
35+
36+
if (!result.text?.includes('<think>')) {
37+
result.text = `<think>${result.text}`
38+
}
39+
40+
return result
41+
},
42+
wrapStream: async ({ doStream }) => {
43+
const { stream, ...rest } = await doStream()
44+
45+
let generatedText = ''
46+
const transformStream = new TransformStream<LanguageModelV1StreamPart, LanguageModelV1StreamPart>({
47+
transform(chunk, controller) {
48+
//we are manually adding the <think> tag because some times, distills of reasoning models omit it
49+
if (chunk.type === 'text-delta') {
50+
if (!generatedText.includes('<think>')) {
51+
generatedText += '<think>'
52+
controller.enqueue({
53+
type: 'text-delta',
54+
textDelta: '<think>',
55+
})
56+
}
57+
generatedText += chunk.textDelta
58+
}
59+
60+
controller.enqueue(chunk)
61+
},
62+
})
63+
64+
return {
65+
stream: stream.pipeThrough(transformStream),
66+
...rest,
67+
}
68+
},
69+
},
70+
],
71+
})
72+
: workersai('@cf/meta/llama-3.3-70b-instruct-fp8-fast')
73+
74+
const systemPrompt: message = {
75+
role: 'system',
76+
content: `
77+
- Do not wrap your responses in html tags.
78+
- Do not apply any formatting to your responses.
79+
- You are an expert conversational chatbot. Your objective is to be as helpful as possible.
80+
- You must keep your responses relevant to the user's prompt.
81+
- You must respond with a maximum of 512 tokens (300 words).
82+
- You must respond clearly and concisely, and explain your logic if required.
83+
- You must not provide any personal information.
84+
- Do not respond with your own personal opinions, and avoid topics unrelated to the user's prompt.
85+
${
86+
messages.length <= 1 &&
87+
`- Important REMINDER: You MUST provide a 5 word title at the END of your response using <chat-title> </chat-title> tags.
88+
If you do not do this, this session will error.
89+
For example, <chat-title>Hello and Welcome</chat-title> Hi, how can I help you today?
90+
`
91+
}
92+
`,
93+
}
94+
95+
const text = await streamText({
96+
model,
97+
messages: [systemPrompt, ...messages],
98+
maxTokens: 2048,
99+
maxRetries: 3,
100+
})
101+
102+
return text.toDataStreamResponse({
103+
sendReasoning: true,
104+
})
105+
} catch (error) {
106+
return c.json({ error: `Chat completion failed. ${(error as Error)?.message}` }, 500)
107+
}
108+
})
109+
110+
// Handle static assets and fallback routes
111+
app.all('*', async (c) => {
112+
if (c.env.ASSETS) {
113+
return c.env.ASSETS.fetch(c.req.raw)
114+
}
115+
return c.text('Not found', 404)
116+
})
117+
118+
export default app

examples/chat-ui/e2e/build.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { test, expect } from '@playwright/test'
2+
import { exec } from 'child_process'
3+
import { promisify } from 'util'
4+
5+
const execAsync = promisify(exec)
6+
7+
interface ExecError extends Error {
8+
stdout?: string
9+
stderr?: string
10+
}
11+
12+
test.describe('Build Tests', () => {
13+
test('should build without type errors', async () => {
14+
try {
15+
const { stderr } = await execAsync('pnpm build', {
16+
cwd: process.cwd(),
17+
timeout: 60000
18+
})
19+
20+
// Check for TypeScript compilation errors
21+
expect(stderr).not.toContain('error TS')
22+
expect(stderr).not.toContain('Type error')
23+
24+
} catch (error) {
25+
const execError = error as ExecError
26+
console.error('Build failed:', execError.stdout, execError.stderr)
27+
throw new Error(`Build failed: ${execError.message}`)
28+
}
29+
})
30+
31+
test('should lint without errors', async () => {
32+
try {
33+
const { stderr } = await execAsync('pnpm lint', {
34+
cwd: process.cwd(),
35+
timeout: 30000
36+
})
37+
38+
// ESLint should pass without errors
39+
expect(stderr).not.toContain('error')
40+
41+
} catch (error) {
42+
const execError = error as ExecError
43+
console.error('Lint failed:', execError.stdout, execError.stderr)
44+
throw new Error(`Lint failed: ${execError.message}`)
45+
}
46+
})
47+
})

0 commit comments

Comments
 (0)