@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.
Files changed (98) hide show
  1. package/ADD-ON-AUTHORING.md +129 -0
  2. package/LICENSE +21 -0
  3. package/add-ons/form/assets/src/routes/demo.form.tsx.ejs +352 -0
  4. package/add-ons/form/info.json +16 -0
  5. package/add-ons/form/package.json +5 -0
  6. package/add-ons/module-federation/assets/module-federation.config.js.ejs +27 -0
  7. package/add-ons/module-federation/assets/src/demo-mf-component.tsx +3 -0
  8. package/add-ons/module-federation/assets/src/demo-mf-self-contained.tsx +9 -0
  9. package/add-ons/module-federation/info.json +8 -0
  10. package/add-ons/module-federation/package.json +5 -0
  11. package/add-ons/sentry/assets/_dot_cursorrules.append +22 -0
  12. package/add-ons/sentry/assets/_dot_env.local.append +2 -0
  13. package/add-ons/sentry/assets/src/routes/demo.sentry.bad-event-handler.tsx +20 -0
  14. package/add-ons/sentry/info.json +16 -0
  15. package/add-ons/sentry/package.json +5 -0
  16. package/add-ons/solid-ui/README.md +9 -0
  17. package/add-ons/solid-ui/assets/src/lib/utils.ts +6 -0
  18. package/add-ons/solid-ui/assets/src/styles.css +138 -0
  19. package/add-ons/solid-ui/assets/ui.config.json +13 -0
  20. package/add-ons/solid-ui/info.json +12 -0
  21. package/add-ons/solid-ui/package.json +9 -0
  22. package/add-ons/start/assets/app.config.ts.ejs +19 -0
  23. package/add-ons/start/assets/src/api.ts +6 -0
  24. package/add-ons/start/assets/src/client.tsx +7 -0
  25. package/add-ons/start/assets/src/router.tsx.ejs +24 -0
  26. package/add-ons/start/assets/src/routes/demo.start.server-funcs.tsx +49 -0
  27. package/add-ons/start/assets/src/ssr.tsx +12 -0
  28. package/add-ons/start/info.json +17 -0
  29. package/add-ons/start/package.json +12 -0
  30. package/add-ons/store/assets/src/lib/demo-store.ts +13 -0
  31. package/add-ons/store/assets/src/routes/demo.store.tsx.ejs +77 -0
  32. package/add-ons/store/info.json +16 -0
  33. package/add-ons/store/package.json +6 -0
  34. package/add-ons/t3env/README.md +16 -0
  35. package/add-ons/t3env/assets/src/env.ts +39 -0
  36. package/add-ons/t3env/info.json +8 -0
  37. package/add-ons/t3env/package.json +6 -0
  38. package/add-ons/tanstack-query/assets/src/integrations/tanstack-query/header-user.tsx +5 -0
  39. package/add-ons/tanstack-query/assets/src/integrations/tanstack-query/provider.tsx +15 -0
  40. package/add-ons/tanstack-query/assets/src/routes/demo.tanstack-query.tsx +24 -0
  41. package/add-ons/tanstack-query/info.json +28 -0
  42. package/add-ons/tanstack-query/package.json +6 -0
  43. package/dist/index.js +18 -0
  44. package/dist/types/index.d.ts +1 -0
  45. package/examples/tanchat/README.md +52 -0
  46. package/examples/tanchat/assets/ai-streaming-server/README.md +110 -0
  47. package/examples/tanchat/assets/ai-streaming-server/_dot_env.example +1 -0
  48. package/examples/tanchat/assets/ai-streaming-server/package.json +26 -0
  49. package/examples/tanchat/assets/ai-streaming-server/src/index.ts +102 -0
  50. package/examples/tanchat/assets/ai-streaming-server/tsconfig.json +15 -0
  51. package/examples/tanchat/assets/src/components/demo.SettingsDialog.tsx +149 -0
  52. package/examples/tanchat/assets/src/demo.index.css +227 -0
  53. package/examples/tanchat/assets/src/lib/demo-store.ts +13 -0
  54. package/examples/tanchat/assets/src/routes/example.chat.tsx +435 -0
  55. package/examples/tanchat/assets/src/store/demo.hooks.ts +17 -0
  56. package/examples/tanchat/assets/src/store/demo.store.ts +133 -0
  57. package/examples/tanchat/info.json +15 -0
  58. package/examples/tanchat/package.json +7 -0
  59. package/package.json +33 -0
  60. package/project/base/README.md.ejs +215 -0
  61. package/project/base/_dot_cursorrules.append +35 -0
  62. package/project/base/_dot_gitignore +5 -0
  63. package/project/base/_dot_vscode/settings.json.ejs +35 -0
  64. package/project/base/index.html.ejs +20 -0
  65. package/project/base/package.json +23 -0
  66. package/project/base/public/favicon.ico +0 -0
  67. package/project/base/public/logo192.png +0 -0
  68. package/project/base/public/logo512.png +0 -0
  69. package/project/base/public/manifest.json +25 -0
  70. package/project/base/public/robots.txt +3 -0
  71. package/project/base/src/App.css.ejs +38 -0
  72. package/project/base/src/App.tsx.ejs +34 -0
  73. package/project/base/src/components/Header.tsx.ejs +26 -0
  74. package/project/base/src/logo.svg +120 -0
  75. package/project/base/src/main.tsx.ejs +126 -0
  76. package/project/base/src/routes/__root.tsx.ejs +38 -0
  77. package/project/base/src/routes/index.tsx.ejs +41 -0
  78. package/project/base/src/styles.css.ejs +15 -0
  79. package/project/base/tsconfig.json.ejs +31 -0
  80. package/project/base/vite.config.js.ejs +22 -0
  81. package/project/packages.json +18 -0
  82. package/src/index.ts +26 -0
  83. package/tests/snapshots/solid/solid-cr-js-npm.json +22 -0
  84. package/tests/snapshots/solid/solid-cr-ts-npm.json +23 -0
  85. package/tests/snapshots/solid/solid-cr-ts-start-npm.json +27 -0
  86. package/tests/snapshots/solid/solid-fr-ts-npm.json +24 -0
  87. package/tests/snapshots/solid/solid-fr-ts-tw-npm.json +23 -0
  88. package/tests/solid.test.ts +119 -0
  89. package/tests/test-utilities.ts +44 -0
  90. package/toolchains/biome/assets/biome.json.ejs +31 -0
  91. package/toolchains/biome/info.json +8 -0
  92. package/toolchains/biome/package.json +10 -0
  93. package/toolchains/eslint/assets/_dot_prettierignore +3 -0
  94. package/toolchains/eslint/assets/eslint.config.js +5 -0
  95. package/toolchains/eslint/assets/prettier.config.js +10 -0
  96. package/toolchains/eslint/info.json +8 -0
  97. package/toolchains/eslint/package.json +11 -0
  98. 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
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "dependencies": {
3
+ "highlight.js": "^11.11.1",
4
+ "markdown-it": "^14.1.0",
5
+ "lucide-solid": "^0.477.0"
6
+ }
7
+ }
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
+ }