kaddidlehopper 0.1.2 → 0.3.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 (102) hide show
  1. package/add-ons/db/assets/DB-SETUP.md +65 -0
  2. package/add-ons/db/assets/_dot_env.local.append +2 -0
  3. package/add-ons/db/assets/drizzle.config.ts +10 -0
  4. package/add-ons/db/assets/src/db/index.ts +8 -0
  5. package/add-ons/db/assets/src/db/schema.ts +8 -0
  6. package/add-ons/db/assets/src/routes/db-example.tsx +118 -0
  7. package/add-ons/db/assets/src/server/guestbook.functions.ts +23 -0
  8. package/add-ons/db/info.json +21 -0
  9. package/add-ons/db/package.json +10 -0
  10. package/add-ons/forms/assets/public/form-example.html +14 -0
  11. package/add-ons/forms/assets/src/routes/form-example.tsx +76 -0
  12. package/add-ons/forms/info.json +21 -0
  13. package/add-ons/forms/package.json +3 -0
  14. package/dist/index.js +0 -0
  15. package/examples/ai-chat/README.md +36 -0
  16. package/examples/ai-chat/assets/src/lib/ai-hook.ts +21 -0
  17. package/examples/ai-chat/assets/src/lib/weather-tools.ts +30 -0
  18. package/examples/ai-chat/assets/src/routes/__root.tsx +57 -0
  19. package/examples/ai-chat/assets/src/routes/api.chat.ts +94 -0
  20. package/examples/ai-chat/assets/src/routes/index.tsx +141 -0
  21. package/examples/ai-chat/assets/src/styles.css +165 -0
  22. package/examples/ai-chat/info.json +24 -0
  23. package/examples/ai-chat/package.json +14 -0
  24. package/examples/calculator/README.md +16 -0
  25. package/examples/calculator/assets/src/components/Calculator.tsx +238 -0
  26. package/examples/calculator/assets/src/components/Header.tsx +17 -0
  27. package/examples/calculator/assets/src/routes/__root.tsx +57 -0
  28. package/examples/calculator/assets/src/routes/index.tsx +14 -0
  29. package/examples/calculator/info.json +19 -0
  30. package/examples/calculator/package.json +1 -0
  31. package/examples/dashboard/README.md +17 -0
  32. package/examples/dashboard/assets/src/components/Header.tsx +17 -0
  33. package/examples/dashboard/assets/src/routes/__root.tsx +57 -0
  34. package/examples/dashboard/assets/src/routes/index.tsx +158 -0
  35. package/examples/dashboard/info.json +19 -0
  36. package/examples/dashboard/package.json +6 -0
  37. package/examples/ecommerce/README.md +48 -0
  38. package/examples/ecommerce/assets/public/logo.png +0 -0
  39. package/examples/ecommerce/assets/public/motorcycle-scooter.jpg +0 -0
  40. package/examples/ecommerce/assets/src/components/BuyButton.tsx +35 -0
  41. package/examples/ecommerce/assets/src/components/Header.tsx +36 -0
  42. package/examples/ecommerce/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
  43. package/examples/ecommerce/assets/src/components/MotorcycleRecommendation.tsx +53 -0
  44. package/examples/ecommerce/assets/src/data/motorcycles.ts +27 -0
  45. package/examples/ecommerce/assets/src/lib/motorcycle-ai-hook.ts +24 -0
  46. package/examples/ecommerce/assets/src/lib/motorcycle-tools.ts +42 -0
  47. package/examples/ecommerce/assets/src/lib/stripe.server.ts +39 -0
  48. package/examples/ecommerce/assets/src/routes/__root.tsx +57 -0
  49. package/examples/ecommerce/assets/src/routes/api.motorcycle-chat.ts +78 -0
  50. package/examples/ecommerce/assets/src/routes/checkout/cancel.tsx +25 -0
  51. package/examples/ecommerce/assets/src/routes/checkout/success.tsx +25 -0
  52. package/examples/ecommerce/assets/src/routes/index.tsx +76 -0
  53. package/examples/ecommerce/assets/src/routes/motorcycles/$motorcycleId.tsx +55 -0
  54. package/examples/ecommerce/assets/src/store/motorcycle-assistant.ts +3 -0
  55. package/examples/ecommerce/assets/src/styles.css +212 -0
  56. package/examples/ecommerce/info.json +40 -0
  57. package/examples/ecommerce/package.json +13 -0
  58. package/examples/portfolio/README.md +49 -0
  59. package/examples/portfolio/assets/content/blog/getting-started-with-tanstack.md +53 -0
  60. package/examples/portfolio/assets/content/blog/react-19-features.md +78 -0
  61. package/examples/portfolio/assets/content/blog/tailwind-css-v4-guide.md +60 -0
  62. package/examples/portfolio/assets/content/education/code-school.md +17 -0
  63. package/examples/portfolio/assets/content/jobs/initech-junior.md +20 -0
  64. package/examples/portfolio/assets/content/projects/portfolio-site.md +15 -0
  65. package/examples/portfolio/assets/content/projects/task-manager.md +15 -0
  66. package/examples/portfolio/assets/content-collections.ts +65 -0
  67. package/examples/portfolio/assets/public/contact.html +6 -0
  68. package/examples/portfolio/assets/public/headshot-on-white.jpg +0 -0
  69. package/examples/portfolio/assets/src/components/Header.tsx +33 -0
  70. package/examples/portfolio/assets/src/components/ResumeAssistant.tsx +193 -0
  71. package/examples/portfolio/assets/src/components/ui/badge.tsx +46 -0
  72. package/examples/portfolio/assets/src/components/ui/card.tsx +92 -0
  73. package/examples/portfolio/assets/src/components/ui/checkbox.tsx +30 -0
  74. package/examples/portfolio/assets/src/components/ui/hover-card.tsx +44 -0
  75. package/examples/portfolio/assets/src/components/ui/separator.tsx +26 -0
  76. package/examples/portfolio/assets/src/lib/resume-ai-hook.ts +21 -0
  77. package/examples/portfolio/assets/src/lib/resume-tools.ts +165 -0
  78. package/examples/portfolio/assets/src/lib/utils.ts +6 -0
  79. package/examples/portfolio/assets/src/routes/__root.tsx +57 -0
  80. package/examples/portfolio/assets/src/routes/api.resume-chat.ts +116 -0
  81. package/examples/portfolio/assets/src/routes/blog/$slug.tsx +73 -0
  82. package/examples/portfolio/assets/src/routes/contact.tsx +121 -0
  83. package/examples/portfolio/assets/src/routes/index.tsx +55 -0
  84. package/examples/portfolio/assets/src/routes/projects.tsx +62 -0
  85. package/examples/portfolio/assets/src/routes/resume.tsx +220 -0
  86. package/examples/portfolio/assets/src/styles.css +138 -0
  87. package/examples/portfolio/info.json +56 -0
  88. package/examples/portfolio/package.json +26 -0
  89. package/examples/saas/README.md +16 -0
  90. package/examples/saas/assets/src/components/Header.tsx +17 -0
  91. package/examples/saas/assets/src/routes/__root.tsx +57 -0
  92. package/examples/saas/assets/src/routes/faq.tsx +94 -0
  93. package/examples/saas/assets/src/routes/index.tsx +197 -0
  94. package/examples/saas/info.json +26 -0
  95. package/examples/saas/package.json +1 -0
  96. package/examples/survey/README.md +20 -0
  97. package/examples/survey/assets/public/form-survey.html +15 -0
  98. package/examples/survey/assets/src/components/SurveyForm.tsx +128 -0
  99. package/examples/survey/assets/src/routes/index.tsx +14 -0
  100. package/examples/survey/info.json +19 -0
  101. package/examples/survey/package.json +1 -0
  102. package/package.json +9 -10
@@ -0,0 +1,65 @@
1
+ # Netlify DB Setup
2
+
3
+ After scaffolding your project with the DB add-on, follow these steps to get Netlify DB running.
4
+
5
+ ## Prerequisites
6
+
7
+ - A [Netlify](https://www.netlify.com/) account
8
+ - The [Netlify CLI](https://docs.netlify.com/cli/get-started/) installed (`npm install -g netlify-cli`)
9
+
10
+ ## 1. Link Your Project to Netlify
11
+
12
+ If you haven't already, link your local project to a Netlify site:
13
+
14
+ ```sh
15
+ netlify login
16
+ netlify init
17
+ ```
18
+
19
+ Follow the prompts to create a new site or link to an existing one.
20
+
21
+ ## 2. Initialize the Database
22
+
23
+ Run the Netlify DB initialization command to provision a Postgres database for your site:
24
+
25
+ ```sh
26
+ netlify db init
27
+ ```
28
+
29
+ This creates a Neon Postgres database attached to your Netlify site and sets the `DATABASE_URL` environment variable automatically.
30
+
31
+ ## 3. Push the Schema to the Database
32
+
33
+ Use Drizzle Kit to push your schema to the database:
34
+
35
+ ```sh
36
+ npx drizzle-kit push
37
+ ```
38
+
39
+ This creates the `guestbook` table defined in `src/db/schema.ts`.
40
+
41
+ ## 4. Run the Dev Server
42
+
43
+ Start local development via the Netlify CLI:
44
+
45
+ ```sh
46
+ netlify dev
47
+ ```
48
+
49
+ Then visit [http://localhost:8888/db-example](http://localhost:8888/db-example) to see the guestbook demo.
50
+
51
+ ## 5. Deploy
52
+
53
+ When you're ready, deploy to Netlify:
54
+
55
+ ```sh
56
+ netlify deploy --build --prod
57
+ ```
58
+
59
+ Or push to your git remote and Netlify will build and deploy automatically if continuous deployment is configured.
60
+
61
+ ## Troubleshooting
62
+
63
+ - **`DATABASE_URL` not set** — Make sure you've run `netlify db:init` and `netlify env:pull`.
64
+ - **Table not found errors** — Run `npx drizzle-kit push` to ensure the schema is applied.
65
+ - **Connection errors locally** — Confirm `.env.local` has the correct `DATABASE_URL` and that your IP is not blocked by the Neon project settings.
@@ -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,118 @@
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
+ const form = e.currentTarget;
22
+ setIsSubmitting(true);
23
+
24
+ const formData = new FormData(form);
25
+ const name = formData.get("name") as string;
26
+ const message = formData.get("message") as string;
27
+
28
+ try {
29
+ await addEntry({ data: { name, message } });
30
+ form.reset();
31
+ await router.invalidate();
32
+ } catch (error) {
33
+ console.error("Failed to add entry:", error);
34
+ } finally {
35
+ setIsSubmitting(false);
36
+ }
37
+ };
38
+
39
+ return (
40
+ <div className="min-h-screen bg-linear-to-br from-teal-900 via-emerald-800 to-cyan-900 flex items-center justify-center">
41
+ <div className="w-full max-w-lg px-4 py-12">
42
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-2 tracking-tight">
43
+ Guestbook
44
+ </h1>
45
+ <p className="text-teal-100/80 mb-8">
46
+ Sign the guestbook — powered by Netlify DB with Drizzle ORM.
47
+ </p>
48
+
49
+ <form onSubmit={handleSubmit} className="space-y-6 mb-12">
50
+ <div>
51
+ <label
52
+ htmlFor="name"
53
+ className="block text-sm font-medium text-teal-200 mb-2"
54
+ >
55
+ Name
56
+ </label>
57
+ <input
58
+ type="text"
59
+ id="name"
60
+ name="name"
61
+ required
62
+ 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"
63
+ placeholder="Your name"
64
+ />
65
+ </div>
66
+
67
+ <div>
68
+ <label
69
+ htmlFor="message"
70
+ className="block text-sm font-medium text-teal-200 mb-2"
71
+ >
72
+ Message
73
+ </label>
74
+ <textarea
75
+ id="message"
76
+ name="message"
77
+ required
78
+ rows={3}
79
+ 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"
80
+ placeholder="Leave a message..."
81
+ />
82
+ </div>
83
+
84
+ <button
85
+ type="submit"
86
+ disabled={isSubmitting}
87
+ 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"
88
+ >
89
+ {isSubmitting ? "Signing..." : "Sign Guestbook"}
90
+ </button>
91
+ </form>
92
+
93
+ <div className="space-y-4">
94
+ <h2 className="text-xl font-bold text-white">
95
+ {entries.length === 0
96
+ ? "No entries yet — be the first!"
97
+ : `${entries.length} ${entries.length === 1 ? "entry" : "entries"}`}
98
+ </h2>
99
+
100
+ {entries.map((entry) => (
101
+ <div
102
+ key={entry.id}
103
+ className="rounded-lg bg-white/10 border border-white/10 p-4"
104
+ >
105
+ <div className="flex items-baseline justify-between mb-1">
106
+ <span className="font-semibold text-white">{entry.name}</span>
107
+ <span className="text-xs text-teal-300/60">
108
+ {new Date(entry.createdAt).toLocaleDateString()}
109
+ </span>
110
+ </div>
111
+ <p className="text-teal-100/80 text-sm">{entry.message}</p>
112
+ </div>
113
+ ))}
114
+ </div>
115
+ </div>
116
+ </div>
117
+ );
118
+ }
@@ -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: "POST" }).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,10 @@
1
+ {
2
+ "dependencies": {
3
+ "drizzle-orm": "^0.44.0",
4
+ "@neondatabase/serverless": "^1.0.0",
5
+ "@netlify/neon": "^0.1.2"
6
+ },
7
+ "devDependencies": {
8
+ "drizzle-kit": "^0.31.0"
9
+ }
10
+ }
@@ -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
+ })