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.
- package/add-ons/db/assets/_dot_env.local.append +2 -0
- package/add-ons/db/assets/drizzle.config.ts +10 -0
- package/add-ons/db/assets/src/db/index.ts +8 -0
- package/add-ons/db/assets/src/db/schema.ts +8 -0
- package/add-ons/db/assets/src/routes/db-example.tsx +117 -0
- package/add-ons/db/assets/src/server/guestbook.functions.ts +23 -0
- package/add-ons/db/info.json +21 -0
- package/add-ons/db/package.json +9 -0
- package/add-ons/forms/assets/public/form-example.html +14 -0
- package/add-ons/forms/assets/src/routes/form-example.tsx +76 -0
- package/add-ons/forms/info.json +21 -0
- package/add-ons/forms/package.json +3 -0
- package/dist/index.js +0 -0
- package/examples/ai-chat/README.md +36 -0
- package/examples/ai-chat/assets/src/lib/ai-hook.ts +21 -0
- package/examples/ai-chat/assets/src/lib/weather-tools.ts +30 -0
- package/examples/ai-chat/assets/src/routes/__root.tsx +57 -0
- package/examples/ai-chat/assets/src/routes/api.chat.ts +94 -0
- package/examples/ai-chat/assets/src/routes/index.tsx +141 -0
- package/examples/ai-chat/assets/src/styles.css +165 -0
- package/examples/ai-chat/info.json +24 -0
- package/examples/ai-chat/package.json +14 -0
- package/examples/calculator/README.md +16 -0
- package/examples/calculator/assets/src/components/Calculator.tsx +238 -0
- package/examples/calculator/assets/src/components/Header.tsx +17 -0
- package/examples/calculator/assets/src/routes/__root.tsx +57 -0
- package/examples/calculator/assets/src/routes/index.tsx +14 -0
- package/examples/calculator/info.json +19 -0
- package/examples/calculator/package.json +1 -0
- package/examples/dashboard/README.md +17 -0
- package/examples/dashboard/assets/src/components/Header.tsx +17 -0
- package/examples/dashboard/assets/src/routes/__root.tsx +57 -0
- package/examples/dashboard/assets/src/routes/index.tsx +158 -0
- package/examples/dashboard/info.json +19 -0
- package/examples/dashboard/package.json +6 -0
- package/examples/ecommerce/README.md +48 -0
- package/examples/ecommerce/assets/public/logo.png +0 -0
- package/examples/ecommerce/assets/public/motorcycle-scooter.jpg +0 -0
- package/examples/ecommerce/assets/src/components/BuyButton.tsx +35 -0
- package/examples/ecommerce/assets/src/components/Header.tsx +36 -0
- package/examples/ecommerce/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
- package/examples/ecommerce/assets/src/components/MotorcycleRecommendation.tsx +53 -0
- package/examples/ecommerce/assets/src/data/motorcycles.ts +27 -0
- package/examples/ecommerce/assets/src/lib/motorcycle-ai-hook.ts +24 -0
- package/examples/ecommerce/assets/src/lib/motorcycle-tools.ts +42 -0
- package/examples/ecommerce/assets/src/lib/stripe.server.ts +39 -0
- package/examples/ecommerce/assets/src/routes/__root.tsx +57 -0
- package/examples/ecommerce/assets/src/routes/api.motorcycle-chat.ts +78 -0
- package/examples/ecommerce/assets/src/routes/checkout/cancel.tsx +25 -0
- package/examples/ecommerce/assets/src/routes/checkout/success.tsx +25 -0
- package/examples/ecommerce/assets/src/routes/index.tsx +76 -0
- package/examples/ecommerce/assets/src/routes/motorcycles/$motorcycleId.tsx +55 -0
- package/examples/ecommerce/assets/src/store/motorcycle-assistant.ts +3 -0
- package/examples/ecommerce/assets/src/styles.css +212 -0
- package/examples/ecommerce/info.json +40 -0
- package/examples/ecommerce/package.json +13 -0
- package/examples/portfolio/README.md +49 -0
- package/examples/portfolio/assets/content/blog/getting-started-with-tanstack.md +53 -0
- package/examples/portfolio/assets/content/blog/react-19-features.md +78 -0
- package/examples/portfolio/assets/content/blog/tailwind-css-v4-guide.md +60 -0
- package/examples/portfolio/assets/content/education/code-school.md +17 -0
- package/examples/portfolio/assets/content/jobs/initech-junior.md +20 -0
- package/examples/portfolio/assets/content/projects/portfolio-site.md +15 -0
- package/examples/portfolio/assets/content/projects/task-manager.md +15 -0
- package/examples/portfolio/assets/content-collections.ts +65 -0
- package/examples/portfolio/assets/public/contact.html +6 -0
- package/examples/portfolio/assets/public/headshot-on-white.jpg +0 -0
- package/examples/portfolio/assets/src/components/Header.tsx +33 -0
- package/examples/portfolio/assets/src/components/ResumeAssistant.tsx +193 -0
- package/examples/portfolio/assets/src/components/ui/badge.tsx +46 -0
- package/examples/portfolio/assets/src/components/ui/card.tsx +92 -0
- package/examples/portfolio/assets/src/components/ui/checkbox.tsx +30 -0
- package/examples/portfolio/assets/src/components/ui/hover-card.tsx +44 -0
- package/examples/portfolio/assets/src/components/ui/separator.tsx +26 -0
- package/examples/portfolio/assets/src/lib/resume-ai-hook.ts +21 -0
- package/examples/portfolio/assets/src/lib/resume-tools.ts +165 -0
- package/examples/portfolio/assets/src/lib/utils.ts +6 -0
- package/examples/portfolio/assets/src/routes/__root.tsx +57 -0
- package/examples/portfolio/assets/src/routes/api.resume-chat.ts +116 -0
- package/examples/portfolio/assets/src/routes/blog/$slug.tsx +73 -0
- package/examples/portfolio/assets/src/routes/contact.tsx +121 -0
- package/examples/portfolio/assets/src/routes/index.tsx +55 -0
- package/examples/portfolio/assets/src/routes/projects.tsx +62 -0
- package/examples/portfolio/assets/src/routes/resume.tsx +220 -0
- package/examples/portfolio/assets/src/styles.css +138 -0
- package/examples/portfolio/info.json +56 -0
- package/examples/portfolio/package.json +26 -0
- package/examples/saas/README.md +16 -0
- package/examples/saas/assets/src/components/Header.tsx +17 -0
- package/examples/saas/assets/src/routes/__root.tsx +57 -0
- package/examples/saas/assets/src/routes/faq.tsx +94 -0
- package/examples/saas/assets/src/routes/index.tsx +197 -0
- package/examples/saas/info.json +26 -0
- package/examples/saas/package.json +1 -0
- package/examples/survey/README.md +20 -0
- package/examples/survey/assets/public/form-survey.html +15 -0
- package/examples/survey/assets/src/components/SurveyForm.tsx +128 -0
- package/examples/survey/assets/src/routes/index.tsx +14 -0
- package/examples/survey/info.json +19 -0
- package/examples/survey/package.json +1 -0
- package/package.json +9 -10
|
@@ -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,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'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
|
+
}
|
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
|
+
})
|