kaddidlehopper 0.1.2 → 0.2.0

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 (101) hide show
  1. package/add-ons/db/assets/_dot_env.local.append +2 -0
  2. package/add-ons/db/assets/drizzle.config.ts +10 -0
  3. package/add-ons/db/assets/src/db/index.ts +8 -0
  4. package/add-ons/db/assets/src/db/schema.ts +8 -0
  5. package/add-ons/db/assets/src/routes/db-example.tsx +117 -0
  6. package/add-ons/db/assets/src/server/guestbook.functions.ts +23 -0
  7. package/add-ons/db/info.json +21 -0
  8. package/add-ons/db/package.json +9 -0
  9. package/add-ons/forms/assets/public/form-example.html +14 -0
  10. package/add-ons/forms/assets/src/routes/form-example.tsx +76 -0
  11. package/add-ons/forms/info.json +21 -0
  12. package/add-ons/forms/package.json +3 -0
  13. package/dist/index.js +0 -0
  14. package/examples/ai-chat/README.md +36 -0
  15. package/examples/ai-chat/assets/src/lib/ai-hook.ts +21 -0
  16. package/examples/ai-chat/assets/src/lib/weather-tools.ts +30 -0
  17. package/examples/ai-chat/assets/src/routes/__root.tsx +57 -0
  18. package/examples/ai-chat/assets/src/routes/api.chat.ts +94 -0
  19. package/examples/ai-chat/assets/src/routes/index.tsx +141 -0
  20. package/examples/ai-chat/assets/src/styles.css +165 -0
  21. package/examples/ai-chat/info.json +24 -0
  22. package/examples/ai-chat/package.json +14 -0
  23. package/examples/calculator/README.md +16 -0
  24. package/examples/calculator/assets/src/components/Calculator.tsx +238 -0
  25. package/examples/calculator/assets/src/components/Header.tsx +17 -0
  26. package/examples/calculator/assets/src/routes/__root.tsx +57 -0
  27. package/examples/calculator/assets/src/routes/index.tsx +14 -0
  28. package/examples/calculator/info.json +19 -0
  29. package/examples/calculator/package.json +1 -0
  30. package/examples/dashboard/README.md +17 -0
  31. package/examples/dashboard/assets/src/components/Header.tsx +17 -0
  32. package/examples/dashboard/assets/src/routes/__root.tsx +57 -0
  33. package/examples/dashboard/assets/src/routes/index.tsx +158 -0
  34. package/examples/dashboard/info.json +19 -0
  35. package/examples/dashboard/package.json +6 -0
  36. package/examples/ecommerce/README.md +48 -0
  37. package/examples/ecommerce/assets/public/logo.png +0 -0
  38. package/examples/ecommerce/assets/public/motorcycle-scooter.jpg +0 -0
  39. package/examples/ecommerce/assets/src/components/BuyButton.tsx +35 -0
  40. package/examples/ecommerce/assets/src/components/Header.tsx +36 -0
  41. package/examples/ecommerce/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
  42. package/examples/ecommerce/assets/src/components/MotorcycleRecommendation.tsx +53 -0
  43. package/examples/ecommerce/assets/src/data/motorcycles.ts +27 -0
  44. package/examples/ecommerce/assets/src/lib/motorcycle-ai-hook.ts +24 -0
  45. package/examples/ecommerce/assets/src/lib/motorcycle-tools.ts +42 -0
  46. package/examples/ecommerce/assets/src/lib/stripe.server.ts +39 -0
  47. package/examples/ecommerce/assets/src/routes/__root.tsx +57 -0
  48. package/examples/ecommerce/assets/src/routes/api.motorcycle-chat.ts +78 -0
  49. package/examples/ecommerce/assets/src/routes/checkout/cancel.tsx +25 -0
  50. package/examples/ecommerce/assets/src/routes/checkout/success.tsx +25 -0
  51. package/examples/ecommerce/assets/src/routes/index.tsx +76 -0
  52. package/examples/ecommerce/assets/src/routes/motorcycles/$motorcycleId.tsx +55 -0
  53. package/examples/ecommerce/assets/src/store/motorcycle-assistant.ts +3 -0
  54. package/examples/ecommerce/assets/src/styles.css +212 -0
  55. package/examples/ecommerce/info.json +40 -0
  56. package/examples/ecommerce/package.json +13 -0
  57. package/examples/portfolio/README.md +49 -0
  58. package/examples/portfolio/assets/content/blog/getting-started-with-tanstack.md +53 -0
  59. package/examples/portfolio/assets/content/blog/react-19-features.md +78 -0
  60. package/examples/portfolio/assets/content/blog/tailwind-css-v4-guide.md +60 -0
  61. package/examples/portfolio/assets/content/education/code-school.md +17 -0
  62. package/examples/portfolio/assets/content/jobs/initech-junior.md +20 -0
  63. package/examples/portfolio/assets/content/projects/portfolio-site.md +15 -0
  64. package/examples/portfolio/assets/content/projects/task-manager.md +15 -0
  65. package/examples/portfolio/assets/content-collections.ts +65 -0
  66. package/examples/portfolio/assets/public/contact.html +6 -0
  67. package/examples/portfolio/assets/public/headshot-on-white.jpg +0 -0
  68. package/examples/portfolio/assets/src/components/Header.tsx +33 -0
  69. package/examples/portfolio/assets/src/components/ResumeAssistant.tsx +193 -0
  70. package/examples/portfolio/assets/src/components/ui/badge.tsx +46 -0
  71. package/examples/portfolio/assets/src/components/ui/card.tsx +92 -0
  72. package/examples/portfolio/assets/src/components/ui/checkbox.tsx +30 -0
  73. package/examples/portfolio/assets/src/components/ui/hover-card.tsx +44 -0
  74. package/examples/portfolio/assets/src/components/ui/separator.tsx +26 -0
  75. package/examples/portfolio/assets/src/lib/resume-ai-hook.ts +21 -0
  76. package/examples/portfolio/assets/src/lib/resume-tools.ts +165 -0
  77. package/examples/portfolio/assets/src/lib/utils.ts +6 -0
  78. package/examples/portfolio/assets/src/routes/__root.tsx +57 -0
  79. package/examples/portfolio/assets/src/routes/api.resume-chat.ts +116 -0
  80. package/examples/portfolio/assets/src/routes/blog/$slug.tsx +73 -0
  81. package/examples/portfolio/assets/src/routes/contact.tsx +121 -0
  82. package/examples/portfolio/assets/src/routes/index.tsx +55 -0
  83. package/examples/portfolio/assets/src/routes/projects.tsx +62 -0
  84. package/examples/portfolio/assets/src/routes/resume.tsx +220 -0
  85. package/examples/portfolio/assets/src/styles.css +138 -0
  86. package/examples/portfolio/info.json +56 -0
  87. package/examples/portfolio/package.json +26 -0
  88. package/examples/saas/README.md +16 -0
  89. package/examples/saas/assets/src/components/Header.tsx +17 -0
  90. package/examples/saas/assets/src/routes/__root.tsx +57 -0
  91. package/examples/saas/assets/src/routes/faq.tsx +94 -0
  92. package/examples/saas/assets/src/routes/index.tsx +197 -0
  93. package/examples/saas/info.json +26 -0
  94. package/examples/saas/package.json +1 -0
  95. package/examples/survey/README.md +20 -0
  96. package/examples/survey/assets/public/form-survey.html +15 -0
  97. package/examples/survey/assets/src/components/SurveyForm.tsx +128 -0
  98. package/examples/survey/assets/src/routes/index.tsx +14 -0
  99. package/examples/survey/info.json +19 -0
  100. package/examples/survey/package.json +1 -0
  101. package/package.json +9 -10
@@ -0,0 +1,2 @@
1
+ # Netlify DB - run `netlify db:init` to set up your database
2
+ # DATABASE_URL is automatically set by Netlify after initialization
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/db/schema.ts",
5
+ out: "./drizzle",
6
+ dialect: "postgresql",
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL!,
9
+ },
10
+ });
@@ -0,0 +1,8 @@
1
+ import { drizzle } from "drizzle-orm/neon-http";
2
+ import { neon } from "@neondatabase/serverless";
3
+ import * as schema from "./schema";
4
+
5
+ const sql = neon(process.env.DATABASE_URL!);
6
+ export const db = drizzle(sql, { schema });
7
+
8
+ export { schema };
@@ -0,0 +1,8 @@
1
+ import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
2
+
3
+ export const guestbook = pgTable("guestbook", {
4
+ id: serial("id").primaryKey(),
5
+ name: text("name").notNull(),
6
+ message: text("message").notNull(),
7
+ createdAt: timestamp("created_at").defaultNow().notNull(),
8
+ });
@@ -0,0 +1,117 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { useRouter } from "@tanstack/react-router";
3
+ import { useState } from "react";
4
+ import { getEntries, addEntry } from "@/server/guestbook.functions";
5
+
6
+ export const Route = createFileRoute("/db-example")({
7
+ loader: async () => {
8
+ const entries = await getEntries();
9
+ return { entries };
10
+ },
11
+ component: DBExample,
12
+ });
13
+
14
+ function DBExample() {
15
+ const { entries } = Route.useLoaderData();
16
+ const router = useRouter();
17
+ const [isSubmitting, setIsSubmitting] = useState(false);
18
+
19
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
20
+ e.preventDefault();
21
+ setIsSubmitting(true);
22
+
23
+ const formData = new FormData(e.currentTarget);
24
+ const name = formData.get("name") as string;
25
+ const message = formData.get("message") as string;
26
+
27
+ try {
28
+ await addEntry({ data: { name, message } });
29
+ e.currentTarget.reset();
30
+ await router.invalidate();
31
+ } catch (error) {
32
+ console.error("Failed to add entry:", error);
33
+ } finally {
34
+ setIsSubmitting(false);
35
+ }
36
+ };
37
+
38
+ return (
39
+ <div className="min-h-screen bg-linear-to-br from-teal-900 via-emerald-800 to-cyan-900 flex items-center justify-center">
40
+ <div className="w-full max-w-lg px-4 py-12">
41
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-2 tracking-tight">
42
+ Guestbook
43
+ </h1>
44
+ <p className="text-teal-100/80 mb-8">
45
+ Sign the guestbook — powered by Netlify DB with Drizzle ORM.
46
+ </p>
47
+
48
+ <form onSubmit={handleSubmit} className="space-y-6 mb-12">
49
+ <div>
50
+ <label
51
+ htmlFor="name"
52
+ className="block text-sm font-medium text-teal-200 mb-2"
53
+ >
54
+ Name
55
+ </label>
56
+ <input
57
+ type="text"
58
+ id="name"
59
+ name="name"
60
+ required
61
+ className="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 text-white placeholder-teal-300/50 focus:outline-none focus:ring-2 focus:ring-teal-400 focus:border-transparent"
62
+ placeholder="Your name"
63
+ />
64
+ </div>
65
+
66
+ <div>
67
+ <label
68
+ htmlFor="message"
69
+ className="block text-sm font-medium text-teal-200 mb-2"
70
+ >
71
+ Message
72
+ </label>
73
+ <textarea
74
+ id="message"
75
+ name="message"
76
+ required
77
+ rows={3}
78
+ className="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 text-white placeholder-teal-300/50 focus:outline-none focus:ring-2 focus:ring-teal-400 focus:border-transparent resize-none"
79
+ placeholder="Leave a message..."
80
+ />
81
+ </div>
82
+
83
+ <button
84
+ type="submit"
85
+ disabled={isSubmitting}
86
+ className="w-full px-8 py-3 bg-teal-500 hover:bg-teal-600 disabled:bg-teal-500/50 text-white font-semibold rounded-lg transition-colors shadow-lg shadow-teal-500/30"
87
+ >
88
+ {isSubmitting ? "Signing..." : "Sign Guestbook"}
89
+ </button>
90
+ </form>
91
+
92
+ <div className="space-y-4">
93
+ <h2 className="text-xl font-bold text-white">
94
+ {entries.length === 0
95
+ ? "No entries yet — be the first!"
96
+ : `${entries.length} ${entries.length === 1 ? "entry" : "entries"}`}
97
+ </h2>
98
+
99
+ {entries.map((entry) => (
100
+ <div
101
+ key={entry.id}
102
+ className="rounded-lg bg-white/10 border border-white/10 p-4"
103
+ >
104
+ <div className="flex items-baseline justify-between mb-1">
105
+ <span className="font-semibold text-white">{entry.name}</span>
106
+ <span className="text-xs text-teal-300/60">
107
+ {new Date(entry.createdAt).toLocaleDateString()}
108
+ </span>
109
+ </div>
110
+ <p className="text-teal-100/80 text-sm">{entry.message}</p>
111
+ </div>
112
+ ))}
113
+ </div>
114
+ </div>
115
+ </div>
116
+ );
117
+ }
@@ -0,0 +1,23 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ import { db, schema } from "@/db";
3
+ import { desc } from "drizzle-orm";
4
+
5
+ export const getEntries = createServerFn({ method: "GET" }).handler(
6
+ async () => {
7
+ const entries = await db
8
+ .select()
9
+ .from(schema.guestbook)
10
+ .orderBy(desc(schema.guestbook.createdAt));
11
+ return entries;
12
+ }
13
+ );
14
+
15
+ export const addEntry = createServerFn({ method: "POST" })
16
+ .inputValidator((data: { name: string; message: string }) => data)
17
+ .handler(async ({ data }) => {
18
+ const result = await db
19
+ .insert(schema.guestbook)
20
+ .values({ name: data.name, message: data.message })
21
+ .returning();
22
+ return result[0];
23
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "Netlify DB",
3
+ "phase": "add-on",
4
+ "description": "Add a guestbook demo powered by Netlify DB (Postgres) with Drizzle ORM. Demonstrates full CRUD with a serverless database.",
5
+ "link": "https://docs.netlify.com/database/overview/",
6
+ "modes": ["file-router"],
7
+ "type": "add-on",
8
+ "priority": 20,
9
+ "routes": [
10
+ {
11
+ "icon": "Database",
12
+ "url": "/db-example",
13
+ "name": "DB Example",
14
+ "path": "src/routes/db-example.tsx",
15
+ "jsName": "DBExample"
16
+ }
17
+ ],
18
+ "integrations": [],
19
+ "dependsOn": [],
20
+ "variables": []
21
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "dependencies": {
3
+ "drizzle-orm": "^0.44.0",
4
+ "@neondatabase/serverless": "^1.0.0"
5
+ },
6
+ "devDependencies": {
7
+ "drizzle-kit": "^0.31.0"
8
+ }
9
+ }
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Form</title>
6
+ </head>
7
+ <body>
8
+ <form name="signup" method="POST" data-netlify="true" netlify-honeypot="bot-field" hidden>
9
+ <input type="text" name="name" />
10
+ <input type="email" name="email" />
11
+ <input name="bot-field" />
12
+ </form>
13
+ </body>
14
+ </html>
@@ -0,0 +1,76 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ export const Route = createFileRoute('/form-example')({
4
+ component: FormExample,
5
+ })
6
+
7
+ function FormExample() {
8
+ return (
9
+ <div className="min-h-screen bg-linear-to-br from-teal-900 via-emerald-800 to-cyan-900 flex items-center justify-center">
10
+ <div className="w-full max-w-md px-4">
11
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-2 tracking-tight">
12
+ Sign up
13
+ </h1>
14
+ <p className="text-teal-100/80 mb-8">
15
+ Enter your name and email to get started.
16
+ </p>
17
+
18
+ <form
19
+ name="signup"
20
+ method="POST"
21
+ data-netlify="true"
22
+ netlify-honeypot="bot-field"
23
+ className="space-y-6"
24
+ >
25
+ <input type="hidden" name="form-name" value="signup" />
26
+ <p className="hidden" style={{ display: 'none' }}>
27
+ <label>
28
+ Don&apos;t fill this out: <input name="bot-field" />
29
+ </label>
30
+ </p>
31
+
32
+ <div>
33
+ <label
34
+ htmlFor="name"
35
+ className="block text-sm font-medium text-teal-200 mb-2"
36
+ >
37
+ Name
38
+ </label>
39
+ <input
40
+ type="text"
41
+ id="name"
42
+ name="name"
43
+ required
44
+ className="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 text-white placeholder-teal-300/50 focus:outline-none focus:ring-2 focus:ring-teal-400 focus:border-transparent"
45
+ placeholder="Your name"
46
+ />
47
+ </div>
48
+
49
+ <div>
50
+ <label
51
+ htmlFor="email"
52
+ className="block text-sm font-medium text-teal-200 mb-2"
53
+ >
54
+ Email
55
+ </label>
56
+ <input
57
+ type="email"
58
+ id="email"
59
+ name="email"
60
+ required
61
+ className="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 text-white placeholder-teal-300/50 focus:outline-none focus:ring-2 focus:ring-teal-400 focus:border-transparent"
62
+ placeholder="you@example.com"
63
+ />
64
+ </div>
65
+
66
+ <button
67
+ type="submit"
68
+ className="w-full px-8 py-3 bg-teal-500 hover:bg-teal-600 text-white font-semibold rounded-lg transition-colors shadow-lg shadow-teal-500/30"
69
+ >
70
+ Submit
71
+ </button>
72
+ </form>
73
+ </div>
74
+ </div>
75
+ )
76
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "Netlify Forms",
3
+ "phase": "add-on",
4
+ "description": "Add a signup form powered by Netlify Forms with spam protection via honeypot field. No server-side code required.",
5
+ "link": "https://docs.netlify.com/forms/setup/",
6
+ "modes": ["file-router"],
7
+ "type": "add-on",
8
+ "priority": 20,
9
+ "routes": [
10
+ {
11
+ "icon": "FormInput",
12
+ "url": "/form-example",
13
+ "name": "Form Example",
14
+ "path": "src/routes/form-example.tsx",
15
+ "jsName": "FormExample"
16
+ }
17
+ ],
18
+ "integrations": [],
19
+ "dependsOn": [],
20
+ "variables": []
21
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "dependencies": {}
3
+ }
package/dist/index.js CHANGED
File without changes
@@ -0,0 +1,36 @@
1
+ # AI Chat Example
2
+
3
+ A weather chat assistant built with TanStack Start and TanStack AI for Netlify deployment.
4
+
5
+ ## Features
6
+
7
+ - **AI Chat Interface**: Conversational weather assistant with streaming responses
8
+ - **Multi-Provider Support**: Works with Anthropic, OpenAI, Gemini, or Ollama
9
+ - **Tool Calling**: AI uses a getWeather tool to fetch weather data
10
+ - **Markdown Rendering**: Rich message formatting with syntax highlighting
11
+
12
+ ## Project Structure
13
+
14
+ ```
15
+ ├── src/
16
+ │ ├── lib/
17
+ │ │ ├── ai-hook.ts # Chat hook setup
18
+ │ │ └── weather-tools.ts # Weather tool definition
19
+ │ └── routes/
20
+ │ ├── __root.tsx # Root layout
21
+ │ ├── index.tsx # Chat UI
22
+ │ └── api.chat.ts # Chat API endpoint
23
+ ```
24
+
25
+ ## Development
26
+
27
+ ```bash
28
+ npm run dev
29
+ ```
30
+
31
+ Set one of these environment variables to use a cloud provider:
32
+ - `ANTHROPIC_API_KEY`
33
+ - `OPENAI_API_KEY`
34
+ - `GEMINI_API_KEY`
35
+
36
+ Falls back to Ollama (local) if no API key is set.
@@ -0,0 +1,21 @@
1
+ import {
2
+ fetchServerSentEvents,
3
+ useChat,
4
+ createChatClientOptions,
5
+ } from '@tanstack/ai-react'
6
+ import type { InferChatMessages } from '@tanstack/ai-react'
7
+
8
+ // Default chat options for simple usage
9
+ const defaultChatOptions = createChatClientOptions({
10
+ connection: fetchServerSentEvents('/api/chat'),
11
+ })
12
+
13
+ export type ChatMessages = InferChatMessages<typeof defaultChatOptions>
14
+
15
+ export const useAIChat = () => {
16
+ const chatOptions = createChatClientOptions({
17
+ connection: fetchServerSentEvents('/api/chat'),
18
+ })
19
+
20
+ return useChat(chatOptions)
21
+ }
@@ -0,0 +1,30 @@
1
+ import { toolDefinition } from '@tanstack/ai'
2
+ import { z } from 'zod'
3
+
4
+ // Tool definition for getting weather
5
+ export const getWeatherToolDef = toolDefinition({
6
+ name: 'getWeather',
7
+ description:
8
+ 'Get the current weather for a city. Returns temperature, condition, and humidity.',
9
+ inputSchema: z.object({
10
+ city: z.string().describe('The city to get weather for'),
11
+ }),
12
+ outputSchema: z.object({
13
+ city: z.string(),
14
+ temperature: z.number(),
15
+ condition: z.string(),
16
+ humidity: z.number(),
17
+ }),
18
+ })
19
+
20
+ // Server implementation - mock weather data
21
+ export const getWeather = getWeatherToolDef.server(({ city }) => {
22
+ // Mock weather data - in a real app, call a weather API
23
+ const conditions = ['sunny', 'cloudy', 'rainy', 'partly cloudy', 'windy']
24
+ return {
25
+ city,
26
+ temperature: Math.floor(Math.random() * 30) + 5,
27
+ condition: conditions[Math.floor(Math.random() * conditions.length)],
28
+ humidity: Math.floor(Math.random() * 50) + 30,
29
+ }
30
+ })
@@ -0,0 +1,57 @@
1
+ import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
2
+ import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
3
+ import { TanStackDevtools } from '@tanstack/react-devtools'
4
+
5
+ import appCss from '../styles.css?url'
6
+
7
+ export const Route = createRootRoute({
8
+ head: () => ({
9
+ meta: [
10
+ {
11
+ charSet: 'utf-8',
12
+ },
13
+ {
14
+ name: 'viewport',
15
+ content: 'width=device-width, initial-scale=1',
16
+ },
17
+ {
18
+ title: 'Weather Chat',
19
+ },
20
+ ],
21
+ links: [
22
+ {
23
+ rel: 'stylesheet',
24
+ href: appCss,
25
+ },
26
+ ],
27
+ }),
28
+ shellComponent: RootDocument,
29
+ })
30
+
31
+ function RootDocument({ children }: { children: React.ReactNode }) {
32
+ return (
33
+ <html lang="en">
34
+ <head>
35
+ <HeadContent />
36
+ </head>
37
+ <body>
38
+ <header className="p-4 flex items-center justify-between bg-gray-800 text-white shadow-lg">
39
+ <h1 className="text-xl font-semibold">Weather Chat</h1>
40
+ </header>
41
+ {children}
42
+ <TanStackDevtools
43
+ config={{
44
+ position: 'bottom-right',
45
+ }}
46
+ plugins={[
47
+ {
48
+ name: 'Tanstack Router',
49
+ render: <TanStackRouterDevtoolsPanel />,
50
+ },
51
+ ]}
52
+ />
53
+ <Scripts />
54
+ </body>
55
+ </html>
56
+ )
57
+ }
@@ -0,0 +1,94 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { chat, maxIterations, toServerSentEventsResponse } from '@tanstack/ai'
3
+ import { anthropicText } from '@tanstack/ai-anthropic'
4
+ import { openaiText } from '@tanstack/ai-openai'
5
+ import { geminiText } from '@tanstack/ai-gemini'
6
+ import { ollamaText } from '@tanstack/ai-ollama'
7
+
8
+ import { getWeather } from '@/lib/weather-tools'
9
+
10
+ const SYSTEM_PROMPT = `You are a helpful weather assistant. You can check the weather for any city using the getWeather tool.
11
+
12
+ CAPABILITIES:
13
+ 1. Use getWeather to get the current weather for any city
14
+
15
+ INSTRUCTIONS:
16
+ - When users ask about weather, use the getWeather tool to get the information
17
+ - Provide friendly, conversational responses about the weather
18
+ - You can give advice based on weather conditions (e.g., "bring an umbrella" if rainy)
19
+ - Keep responses concise but helpful`
20
+
21
+ export const Route = createFileRoute('/api/chat')({
22
+ server: {
23
+ handlers: {
24
+ POST: async ({ request }) => {
25
+ const requestSignal = request.signal
26
+
27
+ if (requestSignal.aborted) {
28
+ return new Response(null, { status: 499 })
29
+ }
30
+
31
+ const abortController = new AbortController()
32
+
33
+ try {
34
+ const body = await request.json()
35
+ const { messages } = body
36
+ const data = body.data || {}
37
+
38
+ // Determine the best available provider
39
+ let provider: 'anthropic' | 'openai' | 'gemini' | 'ollama' =
40
+ data.provider || 'ollama'
41
+ let model: string = data.model || 'mistral:7b'
42
+
43
+ // Use the first available provider with an API key, fallback to ollama
44
+ if (process.env.ANTHROPIC_API_KEY) {
45
+ provider = 'anthropic'
46
+ model = 'claude-haiku-4-5'
47
+ } else if (process.env.OPENAI_API_KEY) {
48
+ provider = 'openai'
49
+ model = 'gpt-4o'
50
+ } else if (process.env.GEMINI_API_KEY) {
51
+ provider = 'gemini'
52
+ model = 'gemini-2.0-flash-exp'
53
+ }
54
+
55
+ const adapterConfig = {
56
+ anthropic: () =>
57
+ anthropicText((model || 'claude-haiku-4-5') as any),
58
+ openai: () => openaiText((model || 'gpt-4o') as any),
59
+ gemini: () => geminiText((model || 'gemini-2.0-flash-exp') as any),
60
+ ollama: () => ollamaText((model || 'mistral:7b') as any),
61
+ }
62
+
63
+ const adapter = adapterConfig[provider]()
64
+
65
+ const stream = chat({
66
+ adapter,
67
+ tools: [getWeather],
68
+ systemPrompts: [SYSTEM_PROMPT],
69
+ agentLoopStrategy: maxIterations(5),
70
+ messages,
71
+ abortController,
72
+ })
73
+
74
+ return toServerSentEventsResponse(stream, { abortController })
75
+ } catch (error: any) {
76
+ console.error('Chat error:', error)
77
+ if (error.name === 'AbortError' || abortController.signal.aborted) {
78
+ return new Response(null, { status: 499 })
79
+ }
80
+ return new Response(
81
+ JSON.stringify({
82
+ error: 'Failed to process chat request',
83
+ message: error.message,
84
+ }),
85
+ {
86
+ status: 500,
87
+ headers: { 'Content-Type': 'application/json' },
88
+ },
89
+ )
90
+ }
91
+ },
92
+ },
93
+ },
94
+ })