// Generated by scripts/generate-manifest.mjs. Do not edit by hand. import type { AddOnCompiled } from '../../../../../types.js' type TemplateRecord = Record type TemplateAddOn = TemplateRecord & { integrations?: Array routes?: Array } type TemplateRenderContext = { [key: string]: any packageManager: any projectName: any typescript: any tailwind: any blank: any js: any jsx: any fileRouter: any codeRouter: any routerOnly: any includeExamples: any addOnEnabled: Record addOnOption: Record addOns: Array integrations: Array routes: Array getPackageManagerAddScript: (...args: Array) => string getPackageManagerRunScript: (...args: Array) => string getPackageManagerExecuteScript: (...args: Array) => string relativePath: (...args: Array) => string integrationImportContent: (...args: Array) => string integrationImportCode: (...args: Array) => string | undefined renderTemplate: (content: string) => string ignoreFile: () => never } type TemplateRenderer = (context: TemplateRenderContext) => string | undefined function __escapeXML(value: unknown) { if (value === undefined || value === null) { return '' } return String(value).replace(/[&<>'"]/g, (character) => { switch (character) { case '&': return '&' case '<': return '<' case '>': return '>' case '"': return '"' case "'": return ''' default: return character } }) } export function getManifestTemplateKey(template: string) { let hash = 0x811c9dc5 for (let i = 0; i < template.length; i++) { hash ^= template.charCodeAt(i) hash = Math.imul(hash, 0x01000193) >>> 0 } return `${hash.toString(16).padStart(8, '0')}:${template.length}` } const templateRenderers: Record = { } export function hasManifestTemplate(template: string) { return getManifestTemplateKey(template) in templateRenderers } export function renderManifestTemplate( template: string, context: TemplateRenderContext, ) { const key = getManifestTemplateKey(template) const renderer = templateRenderers[key] if (!renderer) { throw new Error(`Template ${key} was not precompiled into the manifest`) } return renderer(context) ?? '' } export const addOn = { "name": "TanStack Chat", "description": "A chat example that uses TanStack Start and TanStack Store. Features chat with Anthropic Sonnet, chat history and custom prompts.", "phase": "example", "type": "example", "category": "other", "color": "#00D1B2", "modes": [ "file-router" ], "link": "", "routes": [ { "url": "/example/chat", "name": "Chat" } ], "dependsOn": [ "solid-ui", "store" ], "id": "tanchat", "version": "0.0.0", "packageAdditions": { "dependencies": { "highlight.js": "^11.11.1", "lucide-solid": "^0.577.0", "markdown-it": "^14.1.0" } }, "readme": "# TanStack Chat Application\n\nAm example chat application built with TanStack Start, TanStack Store, and Claude AI.\n\n## Sidecar service\n\nThis applicaton requires a sidecar microservice to be running. The server is located in the `ai-streaming-service` directory.\n\nIn that directory you should edit the `.env.local` file to add your Anthropic API key:\n\n```env\nANTHROPIC_API_KEY=your_anthropic_api_key\n```\n\nThen run the server:\n\n```bash\ncd ai-streaming-service\nnpm install\nnpm run dev\n```\n\n## ✨ Features\n\n### AI Capabilities\n\n- 🤖 Powered by Claude 3.5 Sonnet\n- 📝 Rich markdown formatting with syntax highlighting\n- 🎯 Customizable system prompts for tailored AI behavior\n- 🔄 Real-time message updates and streaming responses (coming soon)\n\n### User Experience\n\n- 🎨 Modern UI with Tailwind CSS and Lucide icons\n- 🔍 Conversation management and history\n- 🔐 Secure API key management\n- 📋 Markdown rendering with code highlighting\n\n### Technical Features\n\n- 📦 Centralized state management with TanStack Store\n- 🔌 Extensible architecture for multiple AI providers\n- 🛠️ TypeScript for type safety\n\n## Architecture\n\n### Tech Stack\n\n- **Routing**: TanStack Router\n- **State Management**: TanStack Store\n- **Styling**: Tailwind CSS\n- **AI Integration**: Anthropic's Claude API\n", "readmeIsEjs": false, "files": { "ai-streaming-server/README.md": "# AI Streaming Server\n\nAn Express server with TypeScript that provides a streaming API endpoint for Anthropic's Claude AI model.\n\n## Setup\n\n1. Clone the repository\n2. Install dependencies:\n ```bash\n npm install\n ```\n3. Copy `.env.example` to `.env` and add your Anthropic API key:\n ```bash\n cp .env.example .env\n ```\n4. Edit `.env` and replace `your_api_key_here` with your actual Anthropic API key\n\n## Development\n\nRun in development mode with auto-reload:\n\n```bash\nnpm run dev\n```\n\nType checking:\n\n```bash\nnpm run typecheck\n```\n\n## Production\n\nBuild the TypeScript code:\n\n```bash\nnpm run build\n```\n\nRun in production mode:\n\n```bash\nnpm start\n```\n\n## API Usage\n\n### POST /api/chat\n\nEndpoint that accepts chat messages and returns a streaming response from Claude.\n\n#### Request Body\n\n```typescript\ninterface ChatMessage {\n role: \"user\" | \"assistant\";\n content: string;\n}\n\ninterface ChatRequest {\n messages: ChatMessage[];\n}\n```\n\nExample request:\n\n```json\n{\n \"messages\": [{ \"role\": \"user\", \"content\": \"Hello, how are you?\" }]\n}\n```\n\n#### Response\n\nThe endpoint returns a Server-Sent Events (SSE) stream. Each event contains a chunk of the response from Claude.\n\nExample usage with JavaScript/TypeScript:\n\n```typescript\nconst response = await fetch(\"http://localhost:3000/api/chat\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n messages: [{ role: \"user\", content: \"Hello, how are you?\" }],\n }),\n});\n\nconst reader = response.body!.getReader();\nconst decoder = new TextDecoder();\n\nwhile (true) {\n const { value, done } = await reader.read();\n if (done) break;\n\n const chunk = decoder.decode(value);\n const lines = chunk.split(\"\\n\");\n\n for (const line of lines) {\n if (line.startsWith(\"data: \")) {\n const data = JSON.parse(line.slice(6));\n if (data === \"[DONE]\") break;\n\n // Handle the streaming response chunk\n console.log(data);\n }\n }\n}\n```\n", "ai-streaming-server/_dot_env.example": "ANTHROPIC_API_KEY=your_api_key_here ", "ai-streaming-server/package.json": "{\n \"name\": \"ai-streaming-server\",\n \"version\": \"1.0.0\",\n \"description\": \"Express server for streaming Anthropic AI chat responses\",\n \"main\": \"dist/index.js\",\n \"type\": \"module\",\n \"scripts\": {\n \"build\": \"tsc\",\n \"start\": \"node dist/index.js\",\n \"dev\": \"tsx watch src/index.ts\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"dependencies\": {\n \"@anthropic-ai/sdk\": \"^0.65.0\",\n \"cors\": \"^2.8.5\",\n \"dotenv\": \"^17.3.1\",\n \"express\": \"^5.1.0\"\n },\n \"devDependencies\": {\n \"@types/cors\": \"^2.8.17\",\n \"@types/express\": \"^5.0.3\",\n \"@types/node\": \"^24.6.0\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^6.0.2\"\n }\n}\n", "ai-streaming-server/src/index.ts": "import express, { Request, Response } from 'express'\nimport cors from 'cors'\nimport dotenv from 'dotenv'\nimport Anthropic from '@anthropic-ai/sdk'\n\n// Load environment variables\ndotenv.config()\n\nconst app = express()\nconst port = process.env.PORT || 8080\n\n// Middleware\napp.use(cors())\napp.use(express.json())\n\n// Initialize Anthropic client\nconst anthropic = new Anthropic({\n apiKey: process.env.ANTHROPIC_API_KEY,\n})\n\n// Define types for the request body\ninterface ChatMessage {\n role: 'user' | 'assistant'\n content: string\n}\n\ninterface ChatRequest {\n messages: ChatMessage[]\n}\n\n// Streaming chat endpoint\napp.post(\n '/api/chat',\n async (req: Request<{}, {}, ChatRequest>, res: Response) => {\n try {\n const { messages } = req.body\n\n if (!messages || !Array.isArray(messages)) {\n return res.status(400).json({ error: 'Messages array is required' })\n }\n\n // Validate message format\n const isValidMessages = messages.every(\n (msg): msg is ChatMessage =>\n typeof msg === 'object' &&\n (msg.role === 'user' || msg.role === 'assistant') &&\n typeof msg.content === 'string',\n )\n\n if (!isValidMessages) {\n return res.status(400).json({\n error:\n \"Invalid message format. Each message must have 'role' and 'content'\",\n })\n }\n\n // Set up SSE headers\n res.setHeader('Content-Type', 'text/event-stream')\n res.setHeader('Cache-Control', 'no-cache')\n res.setHeader('Connection', 'keep-alive')\n\n // Create the message stream\n const stream = await anthropic.messages.create({\n messages: messages.map((msg) => ({\n role: msg.role,\n content: msg.content,\n })),\n model: 'claude-3-opus-20240229',\n max_tokens: 4096,\n stream: true,\n })\n\n // Stream the response\n for await (const chunk of stream) {\n if (chunk.type === 'content_block_delta') {\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`)\n }\n }\n\n // End the stream\n res.write('data: [DONE]\\n\\n')\n res.end()\n } catch (error) {\n console.error('Error:', error)\n // If headers haven't been sent yet, send error response\n if (!res.headersSent) {\n res.status(500).json({ error: 'Internal server error' })\n } else {\n // If streaming has started, send error event\n const errorMessage =\n error instanceof Error ? error.message : 'Unknown error'\n res.write(`data: ${JSON.stringify({ error: errorMessage })}\\n\\n`)\n res.end()\n }\n }\n },\n)\n\n// Start the server\napp.listen(port, () => {\n console.log(`Server is running on port ${port}`)\n})\n", "ai-streaming-server/tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"esModuleInterop\": true,\n \"strict\": true,\n \"skipLibCheck\": true,\n \"outDir\": \"dist\",\n \"rootDir\": \"src\",\n \"sourceMap\": true\n },\n \"include\": [\"src/**/*\"],\n \"exclude\": [\"node_modules\", \"dist\"]\n}\n", "src/components/demo.SettingsDialog.tsx": "import { PlusCircle, Trash2 } from 'lucide-solid'\nimport { useAppActions, useAppState } from '../store/demo.hooks'\nimport { createSignal } from 'solid-js'\n\ninterface SettingsDialogProps {\n isOpen: boolean\n onClose: () => void\n}\n\nexport function SettingsDialog(props: SettingsDialogProps) {\n const [promptForm, setPromptForm] = createSignal({ name: '', content: '' })\n const [isAddingPrompt, setIsAddingPrompt] = createSignal(false)\n const state = useAppState()\n const actions = useAppActions();\n\n const handleAddPrompt = () => {\n if (!promptForm.name.trim() || !promptForm().content.trim()) return\n actions.createPrompt(promptForm().name, promptForm().content)\n setPromptForm({ name: '', content: '' })\n setIsAddingPrompt(false)\n }\n\n const handleClose = () => {\n props.onClose()\n setIsAddingPrompt(false)\n setPromptForm({ name: '', content: '' })\n }\n\n if (!props.isOpen) return null\n\n return (\n
{\n if (e.target === e.currentTarget) handleClose()\n }}>\n
e.stopPropagation()}>\n
\n
\n

Settings

\n \n \n \n \n \n
\n\n
\n {/* Prompts Management */}\n
\n
\n \n setIsAddingPrompt(true)}\n class=\"flex items-center gap-2 px-3 py-1.5 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\"\n >\n \n Add Prompt\n \n
\n\n {isAddingPrompt() && (\n
\n setPromptForm(prev => ({ ...prev, name: e.target.value }))}\n placeholder=\"Prompt name...\"\n class=\"w-full px-3 py-2 text-sm text-white bg-gray-700 rounded-lg border border-gray-600 focus:border-orange-500 focus:ring-1 focus:ring-orange-500\"\n />\n setPromptForm(prev => ({ ...prev, content: e.target.value }))}\n placeholder=\"Enter prompt content...\"\n class=\"w-full h-32 px-3 py-2 text-sm text-white bg-gray-700 rounded-lg border border-gray-600 focus:border-orange-500 focus:ring-1 focus:ring-orange-500\"\n />\n
\n setIsAddingPrompt(false)}\n class=\"px-3 py-1.5 text-sm font-medium text-gray-300 hover:text-white focus:outline-none\"\n >\n Cancel\n \n \n Save Prompt\n \n
\n
\n )}\n\n
\n {state().prompts.map((prompt) => (\n
\n
\n

{prompt.name}

\n

{prompt.content}

\n
\n
\n \n actions.deletePrompt(prompt.id)}\n class=\"p-1 text-gray-400 hover:text-red-500\"\n >\n \n \n
\n
\n ))}\n
\n

\n Create and manage custom system prompts. Only one prompt can be active at a time.\n

\n
\n\n
\n\n
\n \n Cancel\n \n \n Close\n \n
\n
\n
\n
\n )\n}\n", "src/demo.index.css": "@import \"tailwindcss\";\n@import \"highlight.js/styles/github-dark.css\";\n\n/* Custom scrollbar styles */\n::-webkit-scrollbar {\n width: 8px;\n}\n\n::-webkit-scrollbar-track {\n background: transparent;\n}\n\n::-webkit-scrollbar-thumb {\n background-color: rgba(156, 163, 175, 0.5);\n border-radius: 4px;\n}\n\n::-webkit-scrollbar-thumb:hover {\n background-color: rgba(156, 163, 175, 0.7);\n}\n\n/* Smooth transitions for dark mode */\nhtml {\n transition: background-color 0.3s ease;\n}\n\n/* Markdown content styles */\n.prose {\n max-width: none;\n color: #e5e7eb; /* text-gray-200 */\n}\n\n/* .prose p {\n margin-top: 1.25em;\n margin-bottom: 1.25em;\n} */\n\n.prose code {\n color: #e5e7eb;\n background-color: rgba(31, 41, 55, 0.5);\n padding: 0.2em 0.4em;\n border-radius: 0.375rem;\n font-size: 0.875em;\n}\n\n.prose pre {\n background-color: rgba(31, 41, 55, 0.5);\n border-radius: 0.5rem;\n padding: 1rem;\n margin: 1.25em 0;\n overflow-x: auto;\n}\n\n.prose pre code {\n background-color: transparent;\n padding: 0;\n border-radius: 0;\n color: inherit;\n}\n\n.prose h1,\n.prose h2,\n.prose h3,\n.prose h4 {\n color: #f9fafb; /* text-gray-50 */\n /* margin-top: 2em; */\n /* margin-bottom: 1em; */\n}\n\n.prose ul,\n.prose ol {\n margin-top: 1.25em;\n margin-bottom: 1.25em;\n padding-left: 1.625em;\n}\n\n.prose li {\n margin-top: 0.5em;\n margin-bottom: 0.5em;\n}\n\n.prose blockquote {\n border-left-color: #f97316; /* orange-500 */\n background-color: rgba(249, 115, 22, 0.1);\n padding: 1em;\n margin: 1.25em 0;\n border-radius: 0.5rem;\n}\n\n.prose hr {\n border-color: rgba(249, 115, 22, 0.2);\n margin: 2em 0;\n}\n\n.prose a {\n color: #f97316; /* orange-500 */\n text-decoration: underline;\n text-decoration-thickness: 0.1em;\n text-underline-offset: 0.2em;\n}\n\n.prose a:hover {\n color: #fb923c; /* orange-400 */\n}\n\n.prose table {\n width: 100%;\n border-collapse: collapse;\n margin: 1.25em 0;\n}\n\n.prose th,\n.prose td {\n padding: 0.75em;\n border: 1px solid rgba(249, 115, 22, 0.2);\n}\n\n.prose th {\n background-color: rgba(249, 115, 22, 0.1);\n font-weight: 600;\n}\n\n/* Message transition animations */\n.message-enter {\n opacity: 0;\n transform: translateY(10px);\n}\n\n.message-enter-active {\n opacity: 1;\n transform: translateY(0);\n transition:\n opacity 300ms,\n transform 300ms;\n}\n\n.message-exit {\n opacity: 1;\n}\n\n.message-exit-active {\n opacity: 0;\n transition: opacity 300ms;\n}\n\n/* Add/update these styles to match AI formatting capabilities */\n.prose h1 {\n font-size: 2em;\n /* margin-top: 1em; */\n margin-bottom: 0.5em;\n}\n\n.prose h2 {\n font-size: 1.5em;\n margin-top: 1em;\n margin-bottom: 0.5em;\n}\n\n.prose h3 {\n font-size: 1.25em;\n margin-top: 1em;\n margin-bottom: 0.5em;\n}\n\n.prose ul {\n list-style-type: disc;\n padding-left: 1.5em;\n}\n\n.prose ol {\n list-style-type: decimal;\n padding-left: 1.5em;\n}\n\n.prose table {\n width: 100%;\n border-collapse: collapse;\n margin: 1em 0;\n}\n\n.prose th,\n.prose td {\n border: 1px solid rgba(249, 115, 22, 0.2);\n padding: 0.5em;\n}\n\n.prose th {\n background-color: rgba(249, 115, 22, 0.1);\n}\n\n.prose strong {\n color: #f9fafb; /* text-gray-50 */\n font-weight: 600;\n}\n\n.prose em {\n font-style: italic;\n}\n\n.prose blockquote {\n border-left: 4px solid #f97316; /* orange-500 */\n padding-left: 1em;\n margin: 1em 0;\n color: #d1d5db; /* text-gray-300 */\n}\n\n/* Ensure code blocks match the AI's formatting */\n.prose code {\n color: #e5e7eb;\n background-color: rgba(31, 41, 55, 0.5);\n padding: 0.2em 0.4em;\n border-radius: 0.375rem;\n font-size: 0.875em;\n}\n\n.prose pre {\n background-color: rgba(31, 41, 55, 0.5);\n border-radius: 0.5rem;\n padding: 1rem;\n margin: 1em 0;\n}\n\n.prose pre code {\n background-color: transparent;\n padding: 0;\n border-radius: 0;\n}\n", "src/lib/demo-store.ts": "import { Store } from '@tanstack/store'\n\nexport const store = new Store({\n firstName: 'Jane',\n lastName: 'Smith',\n})\n\nexport const fullName = new Store(`${store.state.firstName} ${store.state.lastName}`)\n\nstore.subscribe(() => {\n fullName.setState(() => `${store.state.firstName} ${store.state.lastName}`)\n})\n", "src/routes/example.chat.tsx": "import { createEffect, createSignal, Show } from 'solid-js'\nimport { createFileRoute } from '@tanstack/solid-router'\nimport {\n PlusCircle,\n MessageCircle,\n Trash2,\n Send,\n Settings,\n Edit2,\n} from 'lucide-solid'\nimport MarkdownIt from 'markdown-it'\nimport { SettingsDialog } from '../components/demo.SettingsDialog'\nimport {\n useAppActions,\n useAppSelectors,\n useAppState,\n} from '../store/demo.hooks'\nimport { store } from '../store/demo.store'\n\nimport '../demo.index.css'\n\ntype Message = {\n id: string\n role: 'user' | 'assistant'\n content: string\n}\n\nconst md = new MarkdownIt()\n\nfunction Home() {\n const state = useAppState()\n const actions = useAppActions()\n const selectors = useAppSelectors()\n\n const currentConversation = () =>\n state().conversations.find((c) => c.id === state().currentConversationId)\n const messages = () => currentConversation()?.messages || []\n\n // Local state\n const [input, setInput] = createSignal('')\n const [editingChatId, setEditingChatId] = createSignal(null)\n const [isSettingsOpen, setIsSettingsOpen] = createSignal(false)\n let messagesContainerRef: HTMLDivElement | null\n const [pendingMessage, setPendingMessage] = createSignal(null)\n\n const scrollToBottom = () => {\n if (messagesContainerRef) {\n messagesContainerRef.scrollTop = messagesContainerRef.scrollHeight\n }\n }\n\n // Scroll to bottom when messages change or loading state changes\n createEffect(() => {\n state().isLoading\n messages()\n scrollToBottom()\n })\n\n const handleSubmit = async (e: any) => {\n e.preventDefault()\n if (!input().trim() || state().isLoading) return\n\n const currentInput = input()\n setInput('') // Clear input early for better UX\n actions.setLoading(true)\n\n try {\n let conversationId = state().currentConversationId\n\n // If no current conversation, create one\n if (!conversationId) {\n conversationId = Date.now().toString()\n const newConversation = {\n id: conversationId,\n title: currentInput.trim().slice(0, 30),\n messages: [],\n }\n actions.addConversation(newConversation)\n }\n\n const userMessage: Message = {\n id: Date.now().toString(),\n role: 'user' as const,\n content: currentInput.trim(),\n }\n\n // Add user message\n actions.addMessage(conversationId, userMessage)\n\n // Get active prompt\n const activePrompt = selectors.getActivePrompt(store.state)\n let systemPrompt\n if (activePrompt) {\n systemPrompt = {\n value: activePrompt.content,\n enabled: true,\n }\n }\n\n const response = await fetch('http://localhost:8080/api/chat', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n messages: [...messages(), userMessage],\n systemPrompt,\n }),\n })\n\n const reader = response.body?.getReader()\n if (!reader) {\n throw new Error('No reader found in response')\n }\n\n const decoder = new TextDecoder()\n\n let done = false\n let newMessage = {\n id: (Date.now() + 1).toString(),\n role: 'assistant' as const,\n content: '',\n }\n while (!done) {\n const out = await reader.read()\n done = out.done\n if (!done) {\n try {\n const jsonTxt = decoder.decode(out.value).replace(/^data:\\s+/, '')\n console.log(jsonTxt)\n const json = JSON.parse(jsonTxt)\n if (json.type === 'content_block_delta') {\n newMessage = {\n ...newMessage,\n content: newMessage.content + json.delta.text,\n }\n setPendingMessage(newMessage)\n }\n } catch (e) {\n console.error(e)\n }\n }\n }\n\n setPendingMessage(null)\n if (newMessage.content.trim()) {\n actions.addMessage(conversationId, newMessage)\n }\n } catch (error) {\n console.error('Error:', error)\n const errorMessage: Message = {\n id: (Date.now() + 1).toString(),\n role: 'assistant' as const,\n content: 'Sorry, I encountered an error processing your request.',\n }\n if (state().currentConversationId) {\n actions.addMessage(state().currentConversationId!, errorMessage)\n }\n } finally {\n actions.setLoading(false)\n }\n }\n\n const handleNewChat = () => {\n const newConversation = {\n id: Date.now().toString(),\n title: 'New Chat',\n messages: [],\n }\n actions.addConversation(newConversation)\n }\n\n const handleDeleteChat = (id: string) => {\n actions.deleteConversation(id)\n }\n\n const handleUpdateChatTitle = (id: string, title: string) => {\n actions.updateConversationTitle(id, title)\n setEditingChatId(null)\n }\n\n // Handle input change\n const handleInputChange = (e: any) => {\n setInput(e.target.value)\n }\n\n return (\n
\n {/* Settings Button */}\n
\n setIsSettingsOpen(true)}\n 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\"\n >\n \n \n
\n\n {/* Sidebar */}\n
\n
\n \n \n New Chat\n \n
\n\n {/* Chat List */}\n
\n {state().conversations.map((chat) => (\n actions.setCurrentConversationId(chat.id)}\n >\n \n {editingChatId() === chat.id ? (\n \n handleUpdateChatTitle(chat.id, e.target.value)\n }\n onBlur={() => setEditingChatId(null)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n handleUpdateChatTitle(chat.id, chat.title)\n }\n }}\n class=\"flex-1 bg-transparent text-sm text-white focus:outline-none\"\n autofocus\n />\n ) : (\n \n {chat.title}\n \n )}\n
\n {\n e.stopPropagation()\n setEditingChatId(chat.id)\n }}\n class=\"p-1 text-gray-400 hover:text-white\"\n >\n \n \n {\n e.stopPropagation()\n handleDeleteChat(chat.id)\n }}\n class=\"p-1 text-gray-400 hover:text-red-500\"\n >\n \n \n
\n
\n ))}\n
\n
\n\n {/* Main Content */}\n
\n {state().currentConversationId ? (\n <>\n {/* Messages */}\n \n
\n {[...messages(), pendingMessage()]\n .filter((v) => v)\n .map((message) => (\n \n
\n {message!.role === 'assistant' ? (\n
\n AI\n
\n ) : (\n
\n Y\n
\n )}\n
\n
\n
\n ))}\n {state().isLoading && (\n
\n
\n
\n
\n
\n
\n
\n \n AI\n \n
\n
\n
\n
\n
\n Thinking\n
\n
\n
\n
\n
\n
\n \n \n \n )}\n \n \n\n {/* Input */}\n
\n
\n
\n
\n {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n handleSubmit(e)\n }\n }}\n placeholder=\"Type something clever...\"\n 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\"\n rows={1}\n style={{ 'min-height': '44px', 'max-height': '200px' }}\n onInput={(e) => {\n const target = e.target as HTMLTextAreaElement\n target.style.height = 'auto'\n target.style.height =\n Math.min(target.scrollHeight, 200) + 'px'\n handleInputChange(e)\n }}\n />\n \n \n \n
\n
\n
\n
\n \n ) : (\n
\n
\n

\n TanStack Chat\n

\n

\n You can ask me about anything, I might or might not have a good\n answer, but you can still ask.\n

\n
\n
\n {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n handleSubmit(e)\n }\n }}\n placeholder=\"Type something clever...\"\n 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\"\n rows={1}\n style={{ 'min-height': '88px' }}\n />\n \n \n \n
\n
\n
\n
\n )}\n \n\n {/* Settings Dialog */}\n \n setIsSettingsOpen(false)}\n />\n \n \n )\n}\n\nexport const Route = createFileRoute('/example/chat')({\n component: Home,\n})\n", "src/store/demo.hooks.ts": "import { useStore } from '@tanstack/solid-store'\nimport { store, actions, selectors } from './demo.store'\n\nexport type { State, Prompt, Conversation } from './demo.store'\n\nexport function useAppState() {\n const state = useStore(store)\n return state;\n}\n\nexport function useAppActions() {\n return actions\n}\n\nexport function useAppSelectors() {\n return selectors\n} ", "src/store/demo.store.ts": "import { Store } from '@tanstack/store'\nimport type { Message } from '../utils/demo.ai'\n\n// Types\nexport interface Prompt {\n id: string\n name: string\n content: string\n is_active: boolean\n created_at: number\n}\n\nexport interface Conversation {\n id: string\n title: string\n messages: Message[]\n}\n\nexport interface State {\n prompts: Prompt[]\n conversations: Conversation[]\n currentConversationId: string | null\n isLoading: boolean\n}\n\nconst initialState: State = {\n prompts: [],\n conversations: [],\n currentConversationId: null,\n isLoading: false\n}\n\nexport const store = new Store(initialState)\n\nexport const actions = {\n // Prompt actions\n createPrompt: (name: string, content: string) => {\n const id = Date.now().toString()\n store.setState(state => {\n const updatedPrompts = state.prompts.map(p => ({ ...p, is_active: false }))\n return {\n ...state,\n prompts: [\n ...updatedPrompts,\n {\n id,\n name,\n content,\n is_active: true,\n created_at: Date.now()\n }\n ]\n }\n })\n },\n\n deletePrompt: (id: string) => {\n store.setState(state => ({\n ...state,\n prompts: state.prompts.filter(p => p.id !== id)\n }))\n },\n\n setPromptActive: (id: string, shouldActivate: boolean) => {\n store.setState(state => ({\n ...state,\n prompts: state.prompts.map(p => ({\n ...p,\n is_active: p.id === id ? shouldActivate : false\n }))\n }))\n },\n\n // Chat actions\n setConversations: (conversations: Conversation[]) => {\n store.setState(state => ({ ...state, conversations }))\n },\n\n setCurrentConversationId: (id: string | null) => {\n store.setState(state => ({ ...state, currentConversationId: id }))\n },\n\n addConversation: (conversation: Conversation) => {\n store.setState(state => ({\n ...state,\n conversations: [...state.conversations, conversation],\n currentConversationId: conversation.id\n }))\n },\n\n updateConversationTitle: (id: string, title: string) => {\n store.setState(state => ({\n ...state,\n conversations: state.conversations.map(conv =>\n conv.id === id ? { ...conv, title } : conv\n )\n }))\n },\n\n deleteConversation: (id: string) => {\n store.setState(state => ({\n ...state,\n conversations: state.conversations.filter(conv => conv.id !== id),\n currentConversationId: state.currentConversationId === id ? null : state.currentConversationId\n }))\n },\n\n addMessage: (conversationId: string, message: Message) => {\n store.setState(state => ({\n ...state,\n conversations: state.conversations.map(conv =>\n conv.id === conversationId\n ? { ...conv, messages: [...conv.messages, message] }\n : conv\n )\n }))\n },\n\n setLoading: (isLoading: boolean) => {\n store.setState(state => ({ ...state, isLoading }))\n }\n}\n\n// Selectors\nexport const selectors = {\n getActivePrompt: (state: State) => state.prompts.find(p => p.is_active),\n getCurrentConversation: (state: State) => \n state.conversations.find(c => c.id === state.currentConversationId),\n getPrompts: (state: State) => state.prompts,\n getConversations: (state: State) => state.conversations,\n getCurrentConversationId: (state: State) => state.currentConversationId,\n getIsLoading: (state: State) => state.isLoading\n} " }, "deletedFiles": [] } satisfies AddOnCompiled