Skip to content

Commit 2c8dc4e

Browse files
committed
feat: examples
1 parent 51ca8eb commit 2c8dc4e

File tree

7 files changed

+206
-139
lines changed

7 files changed

+206
-139
lines changed

src/options.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -251,25 +251,25 @@ export async function promptForOptions(
251251
}
252252

253253
// Select any examples
254-
const selectedExamples: Array<string> = []
255-
// const examples = allAddOns.filter((addOn) => addOn.type === 'example')
256-
// if (options.typescript && examples.length > 0) {
257-
// const value = await multiselect({
258-
// message: 'Would you like any examples?',
259-
// options: examples.map((addOn) => ({
260-
// value: addOn.id,
261-
// label: addOn.name,
262-
// hint: addOn.description,
263-
// })),
264-
// required: false,
265-
// })
254+
let selectedExamples: Array<string> = []
255+
const examples = allAddOns.filter((addOn) => addOn.type === 'example')
256+
if (options.typescript && examples.length > 0) {
257+
const value = await multiselect({
258+
message: 'Would you like any examples?',
259+
options: examples.map((addOn) => ({
260+
value: addOn.id,
261+
label: addOn.name,
262+
hint: addOn.description,
263+
})),
264+
required: false,
265+
})
266266

267-
// if (isCancel(value)) {
268-
// cancel('Operation cancelled.')
269-
// process.exit(0)
270-
// }
271-
// selectedExamples = value
272-
// }
267+
if (isCancel(value)) {
268+
cancel('Operation cancelled.')
269+
process.exit(0)
270+
}
271+
selectedExamples = value
272+
}
273273

274274
if (selectedAddOns.length > 0 || selectedExamples.length > 0) {
275275
options.chosenAddOns = await finalizeAddOns(

templates/react/add-on/start/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
"dependencies": {
88
"@tailwindcss/postcss": "^4.0.7",
9-
"@tanstack/react-start": "^1.111.12",
9+
"@tanstack/react-start": "^1.112.1",
1010
"postcss": "^8.5.2",
1111
"vinxi": "^0.5.3",
1212
"vite-tsconfig-paths": "^5.1.4"

templates/react/base/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
"test": "vitest run"
1010
},
1111
"dependencies": {
12-
"@tanstack/react-router": "^1.104.1",
13-
"@tanstack/router-devtools": "^1.104.3",
12+
"@tanstack/react-router": "^1.112.0",
13+
"@tanstack/router-devtools": "^1.112.0",
1414
"react": "^19.0.0",
1515
"react-dom": "^19.0.0"
1616
},

templates/react/example/tanchat/assets/src/routes/example.chat.tsx.ejs renamed to templates/react/example/tanchat/assets/src/routes/example.chat.tsx

Lines changed: 119 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import { createFileRoute } from '@tanstack/react-router'
22
import { useEffect, useState, useRef } from 'react'
3-
import { PlusCircle, MessageCircle, ChevronLeft, ChevronRight, Trash2, X, Menu, Send, Settings, User, LogOut, Edit2 } from 'lucide-react'
3+
import {
4+
PlusCircle,
5+
MessageCircle,
6+
Trash2,
7+
Send,
8+
Settings,
9+
Edit2,
10+
} from 'lucide-react'
411
import ReactMarkdown from 'react-markdown'
512
import rehypeRaw from 'rehype-raw'
613
import rehypeSanitize from 'rehype-sanitize'
714
import rehypeHighlight from 'rehype-highlight'
15+
816
import { SettingsDialog } from '../components/demo.SettingsDialog'
917
import { useAppState } from '../store/demo.hooks'
1018
import { store } from '../store/demo.store'
11-
import { genAIResponse, type Message } from '../utils/demo.ai'
12-
import "../demo.index.css"
19+
import { genAIResponse } from '../utils/demo.ai'
20+
21+
import type { Message } from '../utils/demo.ai'
22+
23+
import '../demo.index.css'
1324

1425
function Home() {
1526
const {
@@ -23,22 +34,23 @@ function Home() {
2334
addMessage,
2435
setLoading,
2536
getCurrentConversation,
26-
getActivePrompt
37+
getActivePrompt,
2738
} = useAppState()
2839

2940
const currentConversation = getCurrentConversation(store.state)
3041
const messages = currentConversation?.messages || []
31-
42+
3243
// Local state
3344
const [input, setInput] = useState('')
3445
const [editingChatId, setEditingChatId] = useState<string | null>(null)
3546
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
36-
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
3747
const messagesContainerRef = useRef<HTMLDivElement>(null)
48+
const [pendingMessage, setPendingMessage] = useState<Message | null>(null)
3849

3950
const scrollToBottom = () => {
4051
if (messagesContainerRef.current) {
41-
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight
52+
messagesContainerRef.current.scrollTop =
53+
messagesContainerRef.current.scrollHeight
4254
}
4355
}
4456

@@ -50,7 +62,7 @@ function Home() {
5062
const handleSubmit = async (e: React.FormEvent) => {
5163
e.preventDefault()
5264
if (!input.trim() || isLoading) return
53-
65+
5466
const currentInput = input
5567
setInput('') // Clear input early for better UX
5668
setLoading(true)
@@ -64,7 +76,7 @@ function Home() {
6476
const newConversation = {
6577
id: conversationId,
6678
title: currentInput.trim().slice(0, 30),
67-
messages: []
79+
messages: [],
6880
}
6981
addConversation(newConversation)
7082
}
@@ -84,29 +96,52 @@ function Home() {
8496
if (activePrompt) {
8597
systemPrompt = {
8698
value: activePrompt.content,
87-
enabled: true
99+
enabled: true,
88100
}
89101
}
90102

91103
// Get AI response
92104
const response = await genAIResponse({
93105
data: {
94106
messages: [...messages, userMessage],
95-
systemPrompt
96-
}
107+
systemPrompt,
108+
},
97109
})
98110

99-
if (!response.text?.trim()) {
100-
throw new Error('Received empty response from AI')
111+
const reader = response.body?.getReader()
112+
if (!reader) {
113+
throw new Error('No reader found in response')
101114
}
102115

103-
const assistantMessage: Message = {
116+
const decoder = new TextDecoder()
117+
118+
let done = false
119+
let newMessage = {
104120
id: (Date.now() + 1).toString(),
105121
role: 'assistant' as const,
106-
content: response.text
122+
content: '',
123+
}
124+
while (!done) {
125+
const out = await reader.read()
126+
done = out.done
127+
if (!done) {
128+
try {
129+
const json = JSON.parse(decoder.decode(out.value))
130+
if (json.type === 'content_block_delta') {
131+
newMessage = {
132+
...newMessage,
133+
content: newMessage.content + json.delta.text,
134+
}
135+
setPendingMessage(newMessage)
136+
}
137+
} catch (e) {}
138+
}
107139
}
108140

109-
addMessage(conversationId, assistantMessage)
141+
setPendingMessage(null)
142+
if (newMessage.content.trim()) {
143+
addMessage(conversationId, newMessage)
144+
}
110145
} catch (error) {
111146
console.error('Error:', error)
112147
const errorMessage: Message = {
@@ -126,7 +161,7 @@ function Home() {
126161
const newConversation = {
127162
id: Date.now().toString(),
128163
title: 'New Chat',
129-
messages: []
164+
messages: [],
130165
}
131166
addConversation(newConversation)
132167
}
@@ -184,7 +219,9 @@ function Home() {
184219
<input
185220
type="text"
186221
value={chat.title}
187-
onChange={(e) => handleUpdateChatTitle(chat.id, e.target.value)}
222+
onChange={(e) =>
223+
handleUpdateChatTitle(chat.id, e.target.value)
224+
}
188225
onBlur={() => setEditingChatId(null)}
189226
onKeyDown={(e) => {
190227
if (e.key === 'Enter') {
@@ -229,37 +266,47 @@ function Home() {
229266
{currentConversationId ? (
230267
<>
231268
{/* Messages */}
232-
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto pb-24">
269+
<div
270+
ref={messagesContainerRef}
271+
className="flex-1 overflow-y-auto pb-24"
272+
>
233273
<div className="max-w-3xl mx-auto w-full px-4">
234-
{messages.map((message) => (
235-
<div
236-
key={message.id}
237-
className={`py-6 ${message.role === 'assistant'
238-
? 'bg-gradient-to-r from-orange-500/5 to-red-600/5'
239-
: 'bg-transparent'
240-
}`}
241-
>
242-
<div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
243-
{message.role === 'assistant' ? (
244-
<div className="w-8 h-8 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 mt-2 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
245-
AI
246-
</div>
247-
) : (
248-
<div className="w-8 h-8 rounded-lg bg-gray-700 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
249-
Y
274+
{[...messages, pendingMessage]
275+
.filter((v) => v)
276+
.map((message) => (
277+
<div
278+
key={message!.id}
279+
className={`py-6 ${
280+
message!.role === 'assistant'
281+
? 'bg-gradient-to-r from-orange-500/5 to-red-600/5'
282+
: 'bg-transparent'
283+
}`}
284+
>
285+
<div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
286+
{message!.role === 'assistant' ? (
287+
<div className="w-8 h-8 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 mt-2 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
288+
AI
289+
</div>
290+
) : (
291+
<div className="w-8 h-8 rounded-lg bg-gray-700 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
292+
Y
293+
</div>
294+
)}
295+
<div className="flex-1 min-w-0">
296+
<ReactMarkdown
297+
className="prose dark:prose-invert max-w-none"
298+
rehypePlugins={[
299+
rehypeRaw,
300+
rehypeSanitize,
301+
rehypeHighlight,
302+
]}
303+
>
304+
{message!.content}
305+
</ReactMarkdown>
250306
</div>
251-
)}
252-
<div className="flex-1 min-w-0">
253-
<ReactMarkdown
254-
className="prose dark:prose-invert max-w-none"
255-
rehypePlugins={[rehypeRaw, rehypeSanitize, rehypeHighlight]}
256-
>
257-
{message.content}
258-
</ReactMarkdown>
259307
</div>
260308
</div>
261-
</div>
262-
))}
309+
))}
263310
{isLoading && (
264311
<div className="py-6 bg-gradient-to-r from-orange-500/5 to-red-600/5">
265312
<div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
@@ -268,16 +315,29 @@ function Home() {
268315
<div className="absolute inset-[2px] rounded-lg bg-gray-900 flex items-center justify-center">
269316
<div className="relative w-full h-full rounded-lg bg-gradient-to-r from-orange-500 to-red-600 flex items-center justify-center">
270317
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 animate-pulse"></div>
271-
<span className="relative z-10 text-sm font-medium text-white">AI</span>
318+
<span className="relative z-10 text-sm font-medium text-white">
319+
AI
320+
</span>
272321
</div>
273322
</div>
274323
</div>
275324
<div className="flex items-center gap-3">
276-
<div className="text-gray-400 font-medium text-lg">Thinking</div>
325+
<div className="text-gray-400 font-medium text-lg">
326+
Thinking
327+
</div>
277328
<div className="flex gap-2">
278-
<div className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]" style={{ animationDelay: '0ms' }}></div>
279-
<div className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]" style={{ animationDelay: '200ms' }}></div>
280-
<div className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]" style={{ animationDelay: '400ms' }}></div>
329+
<div
330+
className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
331+
style={{ animationDelay: '0ms' }}
332+
></div>
333+
<div
334+
className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
335+
style={{ animationDelay: '200ms' }}
336+
></div>
337+
<div
338+
className="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
339+
style={{ animationDelay: '400ms' }}
340+
></div>
281341
</div>
282342
</div>
283343
</div>
@@ -305,9 +365,10 @@ function Home() {
305365
rows={1}
306366
style={{ minHeight: '44px', maxHeight: '200px' }}
307367
onInput={(e) => {
308-
const target = e.target as HTMLTextAreaElement;
309-
target.style.height = 'auto';
310-
target.style.height = Math.min(target.scrollHeight, 200) + 'px';
368+
const target = e.target as HTMLTextAreaElement
369+
target.style.height = 'auto'
370+
target.style.height =
371+
Math.min(target.scrollHeight, 200) + 'px'
311372
}}
312373
/>
313374
<button
@@ -329,7 +390,8 @@ function Home() {
329390
<span className="text-white">TanStack</span> Chat
330391
</h1>
331392
<p className="text-gray-400 mb-6 w-2/3 mx-auto text-lg">
332-
You can ask me about anything, I might or might not have a good answer, but you can still ask.
393+
You can ask me about anything, I might or might not have a good
394+
answer, but you can still ask.
333395
</p>
334396
<form onSubmit={handleSubmit}>
335397
<div className="relative max-w-xl mx-auto">
@@ -370,6 +432,6 @@ function Home() {
370432
)
371433
}
372434

373-
export const Route = createFileRoute('/')({
374-
component: Home
435+
export const Route = createFileRoute('/example/chat')({
436+
component: Home,
375437
})

0 commit comments

Comments
 (0)