@tanstack/cta-framework-solid 0.10.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADD-ON-AUTHORING.md +129 -0
- package/LICENSE +21 -0
- package/add-ons/form/assets/src/routes/demo.form.tsx.ejs +352 -0
- package/add-ons/form/info.json +16 -0
- package/add-ons/form/package.json +5 -0
- package/add-ons/module-federation/assets/module-federation.config.js.ejs +27 -0
- package/add-ons/module-federation/assets/src/demo-mf-component.tsx +3 -0
- package/add-ons/module-federation/assets/src/demo-mf-self-contained.tsx +9 -0
- package/add-ons/module-federation/info.json +8 -0
- package/add-ons/module-federation/package.json +5 -0
- package/add-ons/sentry/assets/_dot_cursorrules.append +22 -0
- package/add-ons/sentry/assets/_dot_env.local.append +2 -0
- package/add-ons/sentry/assets/src/routes/demo.sentry.bad-event-handler.tsx +20 -0
- package/add-ons/sentry/info.json +16 -0
- package/add-ons/sentry/package.json +5 -0
- package/add-ons/solid-ui/README.md +9 -0
- package/add-ons/solid-ui/assets/src/lib/utils.ts +6 -0
- package/add-ons/solid-ui/assets/src/styles.css +138 -0
- package/add-ons/solid-ui/assets/ui.config.json +13 -0
- package/add-ons/solid-ui/info.json +12 -0
- package/add-ons/solid-ui/package.json +9 -0
- package/add-ons/start/assets/app.config.ts.ejs +19 -0
- package/add-ons/start/assets/src/api.ts +6 -0
- package/add-ons/start/assets/src/client.tsx +7 -0
- package/add-ons/start/assets/src/router.tsx.ejs +24 -0
- package/add-ons/start/assets/src/routes/demo.start.server-funcs.tsx +49 -0
- package/add-ons/start/assets/src/ssr.tsx +12 -0
- package/add-ons/start/info.json +17 -0
- package/add-ons/start/package.json +12 -0
- package/add-ons/store/assets/src/lib/demo-store.ts +13 -0
- package/add-ons/store/assets/src/routes/demo.store.tsx.ejs +77 -0
- package/add-ons/store/info.json +16 -0
- package/add-ons/store/package.json +6 -0
- package/add-ons/t3env/README.md +16 -0
- package/add-ons/t3env/assets/src/env.ts +39 -0
- package/add-ons/t3env/info.json +8 -0
- package/add-ons/t3env/package.json +6 -0
- package/add-ons/tanstack-query/assets/src/integrations/tanstack-query/header-user.tsx +5 -0
- package/add-ons/tanstack-query/assets/src/integrations/tanstack-query/provider.tsx +15 -0
- package/add-ons/tanstack-query/assets/src/routes/demo.tanstack-query.tsx +24 -0
- package/add-ons/tanstack-query/info.json +28 -0
- package/add-ons/tanstack-query/package.json +6 -0
- package/dist/index.js +18 -0
- package/dist/types/index.d.ts +1 -0
- package/examples/tanchat/README.md +52 -0
- package/examples/tanchat/assets/ai-streaming-server/README.md +110 -0
- package/examples/tanchat/assets/ai-streaming-server/_dot_env.example +1 -0
- package/examples/tanchat/assets/ai-streaming-server/package.json +26 -0
- package/examples/tanchat/assets/ai-streaming-server/src/index.ts +102 -0
- package/examples/tanchat/assets/ai-streaming-server/tsconfig.json +15 -0
- package/examples/tanchat/assets/src/components/demo.SettingsDialog.tsx +149 -0
- package/examples/tanchat/assets/src/demo.index.css +227 -0
- package/examples/tanchat/assets/src/lib/demo-store.ts +13 -0
- package/examples/tanchat/assets/src/routes/example.chat.tsx +435 -0
- package/examples/tanchat/assets/src/store/demo.hooks.ts +17 -0
- package/examples/tanchat/assets/src/store/demo.store.ts +133 -0
- package/examples/tanchat/info.json +15 -0
- package/examples/tanchat/package.json +7 -0
- package/package.json +33 -0
- package/project/base/README.md.ejs +215 -0
- package/project/base/_dot_cursorrules.append +35 -0
- package/project/base/_dot_gitignore +5 -0
- package/project/base/_dot_vscode/settings.json.ejs +35 -0
- package/project/base/index.html.ejs +20 -0
- package/project/base/package.json +23 -0
- package/project/base/public/favicon.ico +0 -0
- package/project/base/public/logo192.png +0 -0
- package/project/base/public/logo512.png +0 -0
- package/project/base/public/manifest.json +25 -0
- package/project/base/public/robots.txt +3 -0
- package/project/base/src/App.css.ejs +38 -0
- package/project/base/src/App.tsx.ejs +34 -0
- package/project/base/src/components/Header.tsx.ejs +26 -0
- package/project/base/src/logo.svg +120 -0
- package/project/base/src/main.tsx.ejs +126 -0
- package/project/base/src/routes/__root.tsx.ejs +38 -0
- package/project/base/src/routes/index.tsx.ejs +41 -0
- package/project/base/src/styles.css.ejs +15 -0
- package/project/base/tsconfig.json.ejs +31 -0
- package/project/base/vite.config.js.ejs +22 -0
- package/project/packages.json +18 -0
- package/src/index.ts +26 -0
- package/tests/snapshots/solid/solid-cr-js-npm.json +22 -0
- package/tests/snapshots/solid/solid-cr-ts-npm.json +23 -0
- package/tests/snapshots/solid/solid-cr-ts-start-npm.json +27 -0
- package/tests/snapshots/solid/solid-fr-ts-npm.json +24 -0
- package/tests/snapshots/solid/solid-fr-ts-tw-npm.json +23 -0
- package/tests/solid.test.ts +119 -0
- package/tests/test-utilities.ts +44 -0
- package/toolchains/biome/assets/biome.json.ejs +31 -0
- package/toolchains/biome/info.json +8 -0
- package/toolchains/biome/package.json +10 -0
- package/toolchains/eslint/assets/_dot_prettierignore +3 -0
- package/toolchains/eslint/assets/eslint.config.js +5 -0
- package/toolchains/eslint/assets/prettier.config.js +10 -0
- package/toolchains/eslint/info.json +8 -0
- package/toolchains/eslint/package.json +11 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { createEffect, createSignal, Show } from 'solid-js'
|
|
2
|
+
import { createFileRoute } from '@tanstack/solid-router'
|
|
3
|
+
import {
|
|
4
|
+
PlusCircle,
|
|
5
|
+
MessageCircle,
|
|
6
|
+
Trash2,
|
|
7
|
+
Send,
|
|
8
|
+
Settings,
|
|
9
|
+
Edit2,
|
|
10
|
+
} from 'lucide-solid'
|
|
11
|
+
import MarkdownIt from 'markdown-it'
|
|
12
|
+
import { SettingsDialog } from '../components/demo.SettingsDialog'
|
|
13
|
+
import {
|
|
14
|
+
useAppActions,
|
|
15
|
+
useAppSelectors,
|
|
16
|
+
useAppState,
|
|
17
|
+
} from '../store/demo.hooks'
|
|
18
|
+
import { store } from '../store/demo.store'
|
|
19
|
+
|
|
20
|
+
import '../demo.index.css'
|
|
21
|
+
|
|
22
|
+
type Message = {
|
|
23
|
+
id: string
|
|
24
|
+
role: 'user' | 'assistant'
|
|
25
|
+
content: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const md = new MarkdownIt()
|
|
29
|
+
|
|
30
|
+
function Home() {
|
|
31
|
+
const state = useAppState()
|
|
32
|
+
const actions = useAppActions()
|
|
33
|
+
const selectors = useAppSelectors()
|
|
34
|
+
|
|
35
|
+
const currentConversation = () =>
|
|
36
|
+
state().conversations.find((c) => c.id === state().currentConversationId)
|
|
37
|
+
const messages = () => currentConversation()?.messages || []
|
|
38
|
+
|
|
39
|
+
// Local state
|
|
40
|
+
const [input, setInput] = createSignal('')
|
|
41
|
+
const [editingChatId, setEditingChatId] = createSignal<string | null>(null)
|
|
42
|
+
const [isSettingsOpen, setIsSettingsOpen] = createSignal(false)
|
|
43
|
+
let messagesContainerRef: HTMLDivElement | null
|
|
44
|
+
const [pendingMessage, setPendingMessage] = createSignal<Message | null>(null)
|
|
45
|
+
|
|
46
|
+
const scrollToBottom = () => {
|
|
47
|
+
if (messagesContainerRef) {
|
|
48
|
+
messagesContainerRef.scrollTop = messagesContainerRef.scrollHeight
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Scroll to bottom when messages change or loading state changes
|
|
53
|
+
createEffect(() => {
|
|
54
|
+
state().isLoading
|
|
55
|
+
messages()
|
|
56
|
+
scrollToBottom()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const handleSubmit = async (e: any) => {
|
|
60
|
+
e.preventDefault()
|
|
61
|
+
if (!input().trim() || state().isLoading) return
|
|
62
|
+
|
|
63
|
+
const currentInput = input()
|
|
64
|
+
setInput('') // Clear input early for better UX
|
|
65
|
+
actions.setLoading(true)
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
let conversationId = state().currentConversationId
|
|
69
|
+
|
|
70
|
+
// If no current conversation, create one
|
|
71
|
+
if (!conversationId) {
|
|
72
|
+
conversationId = Date.now().toString()
|
|
73
|
+
const newConversation = {
|
|
74
|
+
id: conversationId,
|
|
75
|
+
title: currentInput.trim().slice(0, 30),
|
|
76
|
+
messages: [],
|
|
77
|
+
}
|
|
78
|
+
actions.addConversation(newConversation)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const userMessage: Message = {
|
|
82
|
+
id: Date.now().toString(),
|
|
83
|
+
role: 'user' as const,
|
|
84
|
+
content: currentInput.trim(),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add user message
|
|
88
|
+
actions.addMessage(conversationId, userMessage)
|
|
89
|
+
|
|
90
|
+
// Get active prompt
|
|
91
|
+
const activePrompt = selectors.getActivePrompt(store.state)
|
|
92
|
+
let systemPrompt
|
|
93
|
+
if (activePrompt) {
|
|
94
|
+
systemPrompt = {
|
|
95
|
+
value: activePrompt.content,
|
|
96
|
+
enabled: true,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const response = await fetch('http://localhost:8080/api/chat', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: {
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
messages: [...messages(), userMessage],
|
|
107
|
+
systemPrompt,
|
|
108
|
+
}),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const reader = response.body?.getReader()
|
|
112
|
+
if (!reader) {
|
|
113
|
+
throw new Error('No reader found in response')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const decoder = new TextDecoder()
|
|
117
|
+
|
|
118
|
+
let done = false
|
|
119
|
+
let newMessage = {
|
|
120
|
+
id: (Date.now() + 1).toString(),
|
|
121
|
+
role: 'assistant' as const,
|
|
122
|
+
content: '',
|
|
123
|
+
}
|
|
124
|
+
while (!done) {
|
|
125
|
+
const out = await reader.read()
|
|
126
|
+
done = out.done
|
|
127
|
+
if (!done) {
|
|
128
|
+
try {
|
|
129
|
+
const jsonTxt = decoder.decode(out.value).replace(/^data:\s+/, '')
|
|
130
|
+
console.log(jsonTxt)
|
|
131
|
+
const json = JSON.parse(jsonTxt)
|
|
132
|
+
if (json.type === 'content_block_delta') {
|
|
133
|
+
newMessage = {
|
|
134
|
+
...newMessage,
|
|
135
|
+
content: newMessage.content + json.delta.text,
|
|
136
|
+
}
|
|
137
|
+
setPendingMessage(newMessage)
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.error(e)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setPendingMessage(null)
|
|
146
|
+
if (newMessage.content.trim()) {
|
|
147
|
+
actions.addMessage(conversationId, newMessage)
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('Error:', error)
|
|
151
|
+
const errorMessage: Message = {
|
|
152
|
+
id: (Date.now() + 1).toString(),
|
|
153
|
+
role: 'assistant' as const,
|
|
154
|
+
content: 'Sorry, I encountered an error processing your request.',
|
|
155
|
+
}
|
|
156
|
+
if (state().currentConversationId) {
|
|
157
|
+
actions.addMessage(state().currentConversationId!, errorMessage)
|
|
158
|
+
}
|
|
159
|
+
} finally {
|
|
160
|
+
actions.setLoading(false)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const handleNewChat = () => {
|
|
165
|
+
const newConversation = {
|
|
166
|
+
id: Date.now().toString(),
|
|
167
|
+
title: 'New Chat',
|
|
168
|
+
messages: [],
|
|
169
|
+
}
|
|
170
|
+
actions.addConversation(newConversation)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const handleDeleteChat = (id: string) => {
|
|
174
|
+
actions.deleteConversation(id)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const handleUpdateChatTitle = (id: string, title: string) => {
|
|
178
|
+
actions.updateConversationTitle(id, title)
|
|
179
|
+
setEditingChatId(null)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Handle input change
|
|
183
|
+
const handleInputChange = (e: any) => {
|
|
184
|
+
setInput(e.target.value)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div class="relative flex h-[calc(100vh-32px)] bg-gray-900">
|
|
189
|
+
{/* Settings Button */}
|
|
190
|
+
<div class="absolute top-5 right-5 z-50">
|
|
191
|
+
<button
|
|
192
|
+
onClick={() => setIsSettingsOpen(true)}
|
|
193
|
+
class="w-10 h-10 rounded-full bg-gradient-to-r from-orange-500 to-red-600 flex items-center justify-center text-white hover:opacity-90 transition-opacity focus:outline-none focus:ring-2 focus:ring-orange-500"
|
|
194
|
+
>
|
|
195
|
+
<Settings class="w-5 h-5" />
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Sidebar */}
|
|
200
|
+
<div class="flex flex-col w-64 bg-gray-800 border-r border-gray-700">
|
|
201
|
+
<div class="p-4 border-b border-gray-700">
|
|
202
|
+
<button
|
|
203
|
+
onClick={handleNewChat}
|
|
204
|
+
class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-gradient-to-r from-orange-500 to-red-600 rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-orange-500 w-full justify-center"
|
|
205
|
+
>
|
|
206
|
+
<PlusCircle class="w-4 h-4" />
|
|
207
|
+
New Chat
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{/* Chat List */}
|
|
212
|
+
<div class="flex-1 overflow-y-auto">
|
|
213
|
+
{state().conversations.map((chat) => (
|
|
214
|
+
<div
|
|
215
|
+
class={`group flex items-center gap-3 px-3 py-2 cursor-pointer hover:bg-gray-700/50 ${
|
|
216
|
+
chat.id === state().currentConversationId
|
|
217
|
+
? 'bg-gray-700/50'
|
|
218
|
+
: ''
|
|
219
|
+
}`}
|
|
220
|
+
onClick={() => actions.setCurrentConversationId(chat.id)}
|
|
221
|
+
>
|
|
222
|
+
<MessageCircle class="w-4 h-4 text-gray-400" />
|
|
223
|
+
{editingChatId() === chat.id ? (
|
|
224
|
+
<input
|
|
225
|
+
type="text"
|
|
226
|
+
value={chat.title}
|
|
227
|
+
onChange={(e) =>
|
|
228
|
+
handleUpdateChatTitle(chat.id, e.target.value)
|
|
229
|
+
}
|
|
230
|
+
onBlur={() => setEditingChatId(null)}
|
|
231
|
+
onKeyDown={(e) => {
|
|
232
|
+
if (e.key === 'Enter') {
|
|
233
|
+
handleUpdateChatTitle(chat.id, chat.title)
|
|
234
|
+
}
|
|
235
|
+
}}
|
|
236
|
+
class="flex-1 bg-transparent text-sm text-white focus:outline-none"
|
|
237
|
+
autofocus
|
|
238
|
+
/>
|
|
239
|
+
) : (
|
|
240
|
+
<span class="flex-1 text-sm text-gray-300 truncate">
|
|
241
|
+
{chat.title}
|
|
242
|
+
</span>
|
|
243
|
+
)}
|
|
244
|
+
<div class="hidden group-hover:flex items-center gap-1">
|
|
245
|
+
<button
|
|
246
|
+
onClick={(e) => {
|
|
247
|
+
e.stopPropagation()
|
|
248
|
+
setEditingChatId(chat.id)
|
|
249
|
+
}}
|
|
250
|
+
class="p-1 text-gray-400 hover:text-white"
|
|
251
|
+
>
|
|
252
|
+
<Edit2 class="w-3 h-3" />
|
|
253
|
+
</button>
|
|
254
|
+
<button
|
|
255
|
+
onClick={(e) => {
|
|
256
|
+
e.stopPropagation()
|
|
257
|
+
handleDeleteChat(chat.id)
|
|
258
|
+
}}
|
|
259
|
+
class="p-1 text-gray-400 hover:text-red-500"
|
|
260
|
+
>
|
|
261
|
+
<Trash2 class="w-3 h-3" />
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
))}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Main Content */}
|
|
270
|
+
<div class="flex-1 flex flex-col">
|
|
271
|
+
{state().currentConversationId ? (
|
|
272
|
+
<>
|
|
273
|
+
{/* Messages */}
|
|
274
|
+
<div
|
|
275
|
+
ref={messagesContainerRef}
|
|
276
|
+
class="flex-1 overflow-y-auto pb-24"
|
|
277
|
+
>
|
|
278
|
+
<div class="max-w-3xl mx-auto w-full px-4">
|
|
279
|
+
{[...messages(), pendingMessage()]
|
|
280
|
+
.filter((v) => v)
|
|
281
|
+
.map((message) => (
|
|
282
|
+
<div
|
|
283
|
+
class={`py-6 ${
|
|
284
|
+
message!.role === 'assistant'
|
|
285
|
+
? 'bg-gradient-to-r from-orange-500/5 to-red-600/5'
|
|
286
|
+
: 'bg-transparent'
|
|
287
|
+
}`}
|
|
288
|
+
>
|
|
289
|
+
<div class="flex items-start gap-4 max-w-3xl mx-auto w-full">
|
|
290
|
+
{message!.role === 'assistant' ? (
|
|
291
|
+
<div class="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">
|
|
292
|
+
AI
|
|
293
|
+
</div>
|
|
294
|
+
) : (
|
|
295
|
+
<div class="w-8 h-8 rounded-lg bg-gray-700 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
|
|
296
|
+
Y
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
<div
|
|
300
|
+
innerHTML={md.render(message!.content)}
|
|
301
|
+
class="flex-1 min-w-0 text-white"
|
|
302
|
+
></div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
))}
|
|
306
|
+
{state().isLoading && (
|
|
307
|
+
<div class="py-6 bg-gradient-to-r from-orange-500/5 to-red-600/5">
|
|
308
|
+
<div class="flex items-start gap-4 max-w-3xl mx-auto w-full">
|
|
309
|
+
<div class="relative w-8 h-8 flex-shrink-0">
|
|
310
|
+
<div class="absolute inset-0 rounded-lg bg-gradient-to-r from-orange-500 via-red-500 to-orange-500 animate-[spin_2s_linear_infinite]"></div>
|
|
311
|
+
<div class="absolute inset-[2px] rounded-lg bg-gray-900 flex items-center justify-center">
|
|
312
|
+
<div class="relative w-full h-full rounded-lg bg-gradient-to-r from-orange-500 to-red-600 flex items-center justify-center">
|
|
313
|
+
<div class="absolute inset-0 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 animate-pulse"></div>
|
|
314
|
+
<span class="relative z-10 text-sm font-medium text-white">
|
|
315
|
+
AI
|
|
316
|
+
</span>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="flex items-center gap-3">
|
|
321
|
+
<div class="text-gray-400 font-medium text-lg">
|
|
322
|
+
Thinking
|
|
323
|
+
</div>
|
|
324
|
+
<div class="flex gap-2">
|
|
325
|
+
<div
|
|
326
|
+
class="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
|
|
327
|
+
style={{ 'animation-delay': '0ms' }}
|
|
328
|
+
></div>
|
|
329
|
+
<div
|
|
330
|
+
class="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
|
|
331
|
+
style={{ 'animation-delay': '200ms' }}
|
|
332
|
+
></div>
|
|
333
|
+
<div
|
|
334
|
+
class="w-2 h-2 rounded-full bg-orange-500 animate-[bounce_0.8s_infinite]"
|
|
335
|
+
style={{ 'animation-delay': '400ms' }}
|
|
336
|
+
></div>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
{/* Input */}
|
|
346
|
+
<div class="absolute bottom-0 right-0 left-64 bg-gray-900/80 backdrop-blur-sm border-t border-orange-500/10">
|
|
347
|
+
<div class="max-w-3xl mx-auto w-full px-4 py-3">
|
|
348
|
+
<form onSubmit={handleSubmit}>
|
|
349
|
+
<div class="relative">
|
|
350
|
+
<textarea
|
|
351
|
+
value={input()}
|
|
352
|
+
onKeyDown={(e) => {
|
|
353
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
354
|
+
e.preventDefault()
|
|
355
|
+
handleSubmit(e)
|
|
356
|
+
}
|
|
357
|
+
}}
|
|
358
|
+
placeholder="Type something clever (or don't, we won't judge)..."
|
|
359
|
+
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"
|
|
360
|
+
rows={1}
|
|
361
|
+
style={{ 'min-height': '44px', 'max-height': '200px' }}
|
|
362
|
+
onInput={(e) => {
|
|
363
|
+
const target = e.target as HTMLTextAreaElement
|
|
364
|
+
target.style.height = 'auto'
|
|
365
|
+
target.style.height =
|
|
366
|
+
Math.min(target.scrollHeight, 200) + 'px'
|
|
367
|
+
handleInputChange(e)
|
|
368
|
+
}}
|
|
369
|
+
/>
|
|
370
|
+
<button
|
|
371
|
+
type="submit"
|
|
372
|
+
disabled={!input().trim() || state().isLoading}
|
|
373
|
+
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"
|
|
374
|
+
>
|
|
375
|
+
<Send class="w-4 h-4" />
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
378
|
+
</form>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
</>
|
|
382
|
+
) : (
|
|
383
|
+
<div class="flex-1 flex items-center justify-center px-4">
|
|
384
|
+
<div class="text-center max-w-3xl mx-auto w-full">
|
|
385
|
+
<h1 class="text-6xl font-bold mb-4 bg-gradient-to-r from-orange-500 to-red-600 text-transparent bg-clip-text uppercase">
|
|
386
|
+
<span class="text-white">TanStack</span> Chat
|
|
387
|
+
</h1>
|
|
388
|
+
<p class="text-gray-400 mb-6 w-2/3 mx-auto text-lg">
|
|
389
|
+
You can ask me about anything, I might or might not have a good
|
|
390
|
+
answer, but you can still ask.
|
|
391
|
+
</p>
|
|
392
|
+
<form onSubmit={handleSubmit}>
|
|
393
|
+
<div class="relative max-w-xl mx-auto">
|
|
394
|
+
<textarea
|
|
395
|
+
value={input()}
|
|
396
|
+
onInput={handleInputChange}
|
|
397
|
+
onKeyDown={(e) => {
|
|
398
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
399
|
+
e.preventDefault()
|
|
400
|
+
handleSubmit(e)
|
|
401
|
+
}
|
|
402
|
+
}}
|
|
403
|
+
placeholder="Type something clever (or don't, we won't judge)..."
|
|
404
|
+
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"
|
|
405
|
+
rows={1}
|
|
406
|
+
style={{ 'min-height': '88px' }}
|
|
407
|
+
/>
|
|
408
|
+
<button
|
|
409
|
+
type="submit"
|
|
410
|
+
disabled={!input().trim() || state().isLoading}
|
|
411
|
+
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"
|
|
412
|
+
>
|
|
413
|
+
<Send class="w-4 h-4" />
|
|
414
|
+
</button>
|
|
415
|
+
</div>
|
|
416
|
+
</form>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
)}
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
{/* Settings Dialog */}
|
|
423
|
+
<Show when={isSettingsOpen()}>
|
|
424
|
+
<SettingsDialog
|
|
425
|
+
isOpen={isSettingsOpen()}
|
|
426
|
+
onClose={() => setIsSettingsOpen(false)}
|
|
427
|
+
/>
|
|
428
|
+
</Show>
|
|
429
|
+
</div>
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export const Route = createFileRoute('/example/chat')({
|
|
434
|
+
component: Home,
|
|
435
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useStore } from '@tanstack/solid-store'
|
|
2
|
+
import { store, actions, selectors } from './demo.store'
|
|
3
|
+
|
|
4
|
+
export type { State, Prompt, Conversation } from './demo.store'
|
|
5
|
+
|
|
6
|
+
export function useAppState() {
|
|
7
|
+
const state = useStore(store)
|
|
8
|
+
return state;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useAppActions() {
|
|
12
|
+
return actions
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useAppSelectors() {
|
|
16
|
+
return selectors
|
|
17
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Store } from '@tanstack/store'
|
|
2
|
+
import type { Message } from '../utils/demo.ai'
|
|
3
|
+
|
|
4
|
+
// Types
|
|
5
|
+
export interface Prompt {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
content: string
|
|
9
|
+
is_active: boolean
|
|
10
|
+
created_at: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Conversation {
|
|
14
|
+
id: string
|
|
15
|
+
title: string
|
|
16
|
+
messages: Message[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface State {
|
|
20
|
+
prompts: Prompt[]
|
|
21
|
+
conversations: Conversation[]
|
|
22
|
+
currentConversationId: string | null
|
|
23
|
+
isLoading: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const initialState: State = {
|
|
27
|
+
prompts: [],
|
|
28
|
+
conversations: [],
|
|
29
|
+
currentConversationId: null,
|
|
30
|
+
isLoading: false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const store = new Store<State>(initialState)
|
|
34
|
+
|
|
35
|
+
export const actions = {
|
|
36
|
+
// Prompt actions
|
|
37
|
+
createPrompt: (name: string, content: string) => {
|
|
38
|
+
const id = Date.now().toString()
|
|
39
|
+
store.setState(state => {
|
|
40
|
+
const updatedPrompts = state.prompts.map(p => ({ ...p, is_active: false }))
|
|
41
|
+
return {
|
|
42
|
+
...state,
|
|
43
|
+
prompts: [
|
|
44
|
+
...updatedPrompts,
|
|
45
|
+
{
|
|
46
|
+
id,
|
|
47
|
+
name,
|
|
48
|
+
content,
|
|
49
|
+
is_active: true,
|
|
50
|
+
created_at: Date.now()
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
deletePrompt: (id: string) => {
|
|
58
|
+
store.setState(state => ({
|
|
59
|
+
...state,
|
|
60
|
+
prompts: state.prompts.filter(p => p.id !== id)
|
|
61
|
+
}))
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
setPromptActive: (id: string, shouldActivate: boolean) => {
|
|
65
|
+
store.setState(state => ({
|
|
66
|
+
...state,
|
|
67
|
+
prompts: state.prompts.map(p => ({
|
|
68
|
+
...p,
|
|
69
|
+
is_active: p.id === id ? shouldActivate : false
|
|
70
|
+
}))
|
|
71
|
+
}))
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Chat actions
|
|
75
|
+
setConversations: (conversations: Conversation[]) => {
|
|
76
|
+
store.setState(state => ({ ...state, conversations }))
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
setCurrentConversationId: (id: string | null) => {
|
|
80
|
+
store.setState(state => ({ ...state, currentConversationId: id }))
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
addConversation: (conversation: Conversation) => {
|
|
84
|
+
store.setState(state => ({
|
|
85
|
+
...state,
|
|
86
|
+
conversations: [...state.conversations, conversation],
|
|
87
|
+
currentConversationId: conversation.id
|
|
88
|
+
}))
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
updateConversationTitle: (id: string, title: string) => {
|
|
92
|
+
store.setState(state => ({
|
|
93
|
+
...state,
|
|
94
|
+
conversations: state.conversations.map(conv =>
|
|
95
|
+
conv.id === id ? { ...conv, title } : conv
|
|
96
|
+
)
|
|
97
|
+
}))
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
deleteConversation: (id: string) => {
|
|
101
|
+
store.setState(state => ({
|
|
102
|
+
...state,
|
|
103
|
+
conversations: state.conversations.filter(conv => conv.id !== id),
|
|
104
|
+
currentConversationId: state.currentConversationId === id ? null : state.currentConversationId
|
|
105
|
+
}))
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
addMessage: (conversationId: string, message: Message) => {
|
|
109
|
+
store.setState(state => ({
|
|
110
|
+
...state,
|
|
111
|
+
conversations: state.conversations.map(conv =>
|
|
112
|
+
conv.id === conversationId
|
|
113
|
+
? { ...conv, messages: [...conv.messages, message] }
|
|
114
|
+
: conv
|
|
115
|
+
)
|
|
116
|
+
}))
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
setLoading: (isLoading: boolean) => {
|
|
120
|
+
store.setState(state => ({ ...state, isLoading }))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Selectors
|
|
125
|
+
export const selectors = {
|
|
126
|
+
getActivePrompt: (state: State) => state.prompts.find(p => p.is_active),
|
|
127
|
+
getCurrentConversation: (state: State) =>
|
|
128
|
+
state.conversations.find(c => c.id === state.currentConversationId),
|
|
129
|
+
getPrompts: (state: State) => state.prompts,
|
|
130
|
+
getConversations: (state: State) => state.conversations,
|
|
131
|
+
getCurrentConversationId: (state: State) => state.currentConversationId,
|
|
132
|
+
getIsLoading: (state: State) => state.isLoading
|
|
133
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "TanStack Chat",
|
|
3
|
+
"description": "A chat example that uses TanStack Start and TanStack Store. Features chat with Anthropic Sonnet, chat history and custom prompts.",
|
|
4
|
+
"phase": "example",
|
|
5
|
+
"type": "example",
|
|
6
|
+
"modes": ["file-router"],
|
|
7
|
+
"link": "",
|
|
8
|
+
"routes": [
|
|
9
|
+
{
|
|
10
|
+
"url": "/example/chat",
|
|
11
|
+
"name": "Chat"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"dependsOn": ["solid-ui", "store"]
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/cta-framework-solid",
|
|
3
|
+
"version": "0.10.0-alpha.20",
|
|
4
|
+
"description": "CTA Framework for Solid",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/types/index.d.ts",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/TanStack/create-tsrouter-app.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://tanstack.com/router",
|
|
13
|
+
"funding": {
|
|
14
|
+
"type": "github",
|
|
15
|
+
"url": "https://github.com/sponsors/tannerlinsley"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"solid",
|
|
19
|
+
"tanstack",
|
|
20
|
+
"router"
|
|
21
|
+
],
|
|
22
|
+
"author": "Jack Herrington <jherr@pobox.com>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@tanstack/cta-engine": "0.10.0-alpha.20"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.13.4",
|
|
29
|
+
"typescript": "^5.6.3",
|
|
30
|
+
"vitest": "^3.1.1"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {}
|
|
33
|
+
}
|