kaddidlehopper 0.1.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/CONTEXT.md +139 -0
- package/README.md +47 -0
- package/add-ons/ai/README.md +34 -0
- package/add-ons/ai/assets/_dot_env.local.append +13 -0
- package/add-ons/ai/assets/src/components/AIAssistant.tsx +149 -0
- package/add-ons/ai/assets/src/lib/ai-hook.ts +21 -0
- package/add-ons/ai/assets/src/lib/weather-tools.ts +30 -0
- package/add-ons/ai/assets/src/routes/api.chat.ts +94 -0
- package/add-ons/ai/assets/src/routes/chat.css +175 -0
- package/add-ons/ai/assets/src/routes/chat.tsx +141 -0
- package/add-ons/ai/info.json +27 -0
- package/add-ons/ai/package.json +17 -0
- package/add-ons/ai/small-logo.svg +8 -0
- package/dist/cli.js +251 -0
- package/dist/index.js +33 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/types.d.ts +14 -0
- package/dist/types.js +1 -0
- package/examples/blog/README.md +60 -0
- package/examples/blog/assets/content/posts/beach.md +12 -0
- package/examples/blog/assets/content/posts/jungle.md.ejs +12 -0
- package/examples/blog/assets/content/posts/mountains.md.ejs +12 -0
- package/examples/blog/assets/content/posts/snorkeling.md.ejs +12 -0
- package/examples/blog/assets/content/posts/waterfall.md.ejs +12 -0
- package/examples/blog/assets/content-collections.ts +30 -0
- package/examples/blog/assets/public/beach.jpg +0 -0
- package/examples/blog/assets/public/jungle.jpg +0 -0
- package/examples/blog/assets/public/mountains.jpg +0 -0
- package/examples/blog/assets/public/snorkeling.jpg +0 -0
- package/examples/blog/assets/public/waterfall.jpg +0 -0
- package/examples/blog/assets/src/components/Header.tsx +52 -0
- package/examples/blog/assets/src/components/VacayAssistant.tsx +205 -0
- package/examples/blog/assets/src/components/blog-posts.tsx +78 -0
- package/examples/blog/assets/src/components/ui/card.tsx +92 -0
- package/examples/blog/assets/src/lib/blog-ai-hook.ts +25 -0
- package/examples/blog/assets/src/lib/blog-tools.ts +111 -0
- package/examples/blog/assets/src/lib/utils.ts +6 -0
- package/examples/blog/assets/src/routes/__root.tsx +57 -0
- package/examples/blog/assets/src/routes/api.blog-chat.ts +117 -0
- package/examples/blog/assets/src/routes/category.$category.tsx +19 -0
- package/examples/blog/assets/src/routes/index.tsx +19 -0
- package/examples/blog/assets/src/routes/posts.$slug.tsx +63 -0
- package/examples/blog/assets/src/styles.css +138 -0
- package/examples/blog/info.json +43 -0
- package/examples/blog/package.json +23 -0
- package/examples/events/README.md +110 -0
- package/examples/events/assets/content/speakers/andre-costa.md +22 -0
- package/examples/events/assets/content/speakers/hans-mueller.md.ejs +22 -0
- package/examples/events/assets/content/speakers/isabella-martinez.md.ejs +22 -0
- package/examples/events/assets/content/speakers/kenji-nakamura.md.ejs +22 -0
- package/examples/events/assets/content/speakers/marie-dubois.md.ejs +20 -0
- package/examples/events/assets/content/speakers/priya-sharma.md.ejs +22 -0
- package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
- package/examples/events/assets/content/talks/french-macaron-mastery.md.ejs +39 -0
- package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md.ejs +39 -0
- package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md.ejs +39 -0
- package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md.ejs +36 -0
- package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md.ejs +32 -0
- package/examples/events/assets/content/talks/the-science-of-sugar.md.ejs +39 -0
- package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md.ejs +39 -0
- package/examples/events/assets/content-collections.ts +56 -0
- package/examples/events/assets/public/background-1.jpg +0 -0
- package/examples/events/assets/public/background-2.jpg +0 -0
- package/examples/events/assets/public/background-3.jpg +0 -0
- package/examples/events/assets/public/background-4.jpg +0 -0
- package/examples/events/assets/public/conference-logo.png +0 -0
- package/examples/events/assets/public/favicon.ico +0 -0
- package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
- package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
- package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
- package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
- package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
- package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
- package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
- package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
- package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
- package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
- package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
- package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
- package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
- package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
- package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
- package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
- package/examples/events/assets/src/components/Header.tsx +59 -0
- package/examples/events/assets/src/components/HeaderNav.tsx +67 -0
- package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
- package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
- package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
- package/examples/events/assets/src/components/TalkCard.tsx +77 -0
- package/examples/events/assets/src/components/ui/card.tsx +92 -0
- package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
- package/examples/events/assets/src/lib/conference-tools.ts +210 -0
- package/examples/events/assets/src/lib/model-selection.ts +1 -0
- package/examples/events/assets/src/lib/utils.ts +6 -0
- package/examples/events/assets/src/routes/__root.tsx +70 -0
- package/examples/events/assets/src/routes/api.remy-chat.ts +119 -0
- package/examples/events/assets/src/routes/index.tsx +192 -0
- package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
- package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
- package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
- package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
- package/examples/events/assets/src/routes/talks.index.tsx +40 -0
- package/examples/events/assets/src/styles.css +182 -0
- package/examples/events/info.json +74 -0
- package/examples/events/package.json +23 -0
- package/examples/marketing/README.md +60 -0
- package/examples/marketing/assets/public/logo.png +0 -0
- package/examples/marketing/assets/public/motorcycle-adventure.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-cruiser.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-scooter.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-sport.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-supersport.jpg +0 -0
- package/examples/marketing/assets/src/components/Header.tsx +36 -0
- package/examples/marketing/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
- package/examples/marketing/assets/src/components/MotorcycleRecommendation.tsx +53 -0
- package/examples/marketing/assets/src/data/motorcycles.ts.ejs +77 -0
- package/examples/marketing/assets/src/lib/motorcycle-ai-hook.ts +24 -0
- package/examples/marketing/assets/src/lib/motorcycle-tools.ts +42 -0
- package/examples/marketing/assets/src/routes/__root.tsx +57 -0
- package/examples/marketing/assets/src/routes/api.motorcycle-chat.ts +78 -0
- package/examples/marketing/assets/src/routes/index.tsx +72 -0
- package/examples/marketing/assets/src/routes/motorcycles/$motorcycleId.tsx +56 -0
- package/examples/marketing/assets/src/store/motorcycle-assistant.ts +3 -0
- package/examples/marketing/assets/src/styles.css +212 -0
- package/examples/marketing/info.json +38 -0
- package/examples/marketing/package.json +14 -0
- package/examples/resume/README.md +82 -0
- package/examples/resume/assets/content/education/code-school.md +17 -0
- package/examples/resume/assets/content/jobs/freelance.md.ejs +13 -0
- package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
- package/examples/resume/assets/content/jobs/initech-lead.md.ejs +29 -0
- package/examples/resume/assets/content/jobs/initrode-senior.md.ejs +28 -0
- package/examples/resume/assets/content-collections.ts +36 -0
- package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
- package/examples/resume/assets/src/components/Header.tsx +33 -0
- package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
- package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
- package/examples/resume/assets/src/components/ui/card.tsx +92 -0
- package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
- package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
- package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
- package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
- package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
- package/examples/resume/assets/src/lib/utils.ts +6 -0
- package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
- package/examples/resume/assets/src/routes/index.tsx +220 -0
- package/examples/resume/assets/src/styles.css +138 -0
- package/examples/resume/info.json +25 -0
- package/examples/resume/package.json +26 -0
- package/package.json +39 -0
- package/project/base/_dot_claude/skills/content-collections/SKILL.md +505 -0
- package/project/base/_dot_claude/skills/netlify-blobs/SKILL.md +410 -0
- package/project/base/_dot_claude/skills/netlify-db/SKILL.md +424 -0
- package/project/base/_dot_claude/skills/netlify-debugging/SKILL.md +419 -0
- package/project/base/_dot_claude/skills/netlify-forms/SKILL.md +243 -0
- package/project/base/_dot_claude/skills/netlify-functions/SKILL.md +372 -0
- package/project/base/_dot_claude/skills/tanstack-start-api-routes/SKILL.md +421 -0
- package/project/base/_dot_claude/skills/tanstack-start-loaders/SKILL.md +426 -0
- package/project/base/_dot_claude/skills/tanstack-start-project-setup/SKILL.md +493 -0
- package/project/base/_dot_claude/skills/tanstack-start-routes/SKILL.md +430 -0
- package/project/base/_dot_claude/skills/tanstack-start-server-functions/SKILL.md +445 -0
- package/project/base/_dot_claude/skills/tanstack-start-typesafe-routing/SKILL.md +494 -0
- package/project/base/_dot_gitignore +8 -0
- package/project/base/netlify.toml +7 -0
- package/project/base/package.json +33 -0
- package/project/base/public/favicon.ico +0 -0
- package/project/base/public/tanstack-circle-logo.png +0 -0
- package/project/base/public/tanstack-word-logo-white.svg +1 -0
- package/project/base/src/components/Header.tsx +17 -0
- package/project/base/src/components/HeaderNav.tsx.ejs +179 -0
- package/project/base/src/router.tsx +15 -0
- package/project/base/src/routes/__root.tsx +57 -0
- package/project/base/src/routes/index.tsx +48 -0
- package/project/base/src/styles.css +15 -0
- package/project/base/tsconfig.json +28 -0
- package/project/base/vite.config.ts.ejs +25 -0
- package/project/packages.json +22 -0
- package/scripts/check-outdated-packages.js +421 -0
- package/src/cli.ts +343 -0
- package/src/index.ts +49 -0
- package/src/types.ts +15 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { allPosts } from 'content-collections'
|
|
5
|
+
|
|
6
|
+
// Tool definition for getting the current blog post content
|
|
7
|
+
export const getPostBySlugToolDef = toolDefinition({
|
|
8
|
+
name: 'getCurrentBlogPost',
|
|
9
|
+
description:
|
|
10
|
+
'Get the full content and metadata of the current blog post the user is viewing. Use this to answer questions about the article.',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
slug: z.string().describe('The slug of the current blog post'),
|
|
13
|
+
}),
|
|
14
|
+
outputSchema: z.object({
|
|
15
|
+
title: z.string(),
|
|
16
|
+
summary: z.string(),
|
|
17
|
+
content: z.string(),
|
|
18
|
+
categories: z.array(z.string()),
|
|
19
|
+
date: z.string(),
|
|
20
|
+
image: z.string(),
|
|
21
|
+
}),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Server implementation
|
|
25
|
+
export const getPostBySlug = getPostBySlugToolDef.server(({ slug }) => {
|
|
26
|
+
const post = allPosts.find((post) => post.slug === slug)
|
|
27
|
+
if (!post) {
|
|
28
|
+
return {
|
|
29
|
+
title: 'Post not found',
|
|
30
|
+
summary: '',
|
|
31
|
+
content: 'The requested blog post was not found.',
|
|
32
|
+
categories: [],
|
|
33
|
+
date: '',
|
|
34
|
+
image: '',
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
title: post.title,
|
|
39
|
+
summary: post.summary,
|
|
40
|
+
content: post.content,
|
|
41
|
+
categories: post.categories,
|
|
42
|
+
date: post.date,
|
|
43
|
+
image: post.image,
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Tool definition for listing all available blog posts
|
|
48
|
+
export const getAllBlogPostsToolDef = toolDefinition({
|
|
49
|
+
name: 'getAllBlogPosts',
|
|
50
|
+
description:
|
|
51
|
+
'Get a list of all available blog posts with their titles, summaries, and categories. Useful for recommending related content or answering questions about other posts.',
|
|
52
|
+
inputSchema: z.object({}),
|
|
53
|
+
outputSchema: z.array(
|
|
54
|
+
z.object({
|
|
55
|
+
slug: z.string(),
|
|
56
|
+
title: z.string(),
|
|
57
|
+
summary: z.string(),
|
|
58
|
+
categories: z.array(z.string()),
|
|
59
|
+
date: z.string(),
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Server implementation
|
|
65
|
+
export const getAllBlogPosts = getAllBlogPostsToolDef.server(() => {
|
|
66
|
+
return allPosts.map((post) => ({
|
|
67
|
+
slug: post.slug,
|
|
68
|
+
title: post.title,
|
|
69
|
+
summary: post.summary,
|
|
70
|
+
categories: post.categories,
|
|
71
|
+
date: post.date,
|
|
72
|
+
}))
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Tool definition for suggesting related posts
|
|
76
|
+
export const searchBlogPostsToolDef = toolDefinition({
|
|
77
|
+
name: 'searchBlogPosts',
|
|
78
|
+
description:
|
|
79
|
+
"Search for blog posts by title, summary, or categories. Use this to find articles that match the user's query.",
|
|
80
|
+
inputSchema: z.object({
|
|
81
|
+
query: z.string().describe('The search query'),
|
|
82
|
+
}),
|
|
83
|
+
outputSchema: z.array(
|
|
84
|
+
z.object({
|
|
85
|
+
slug: z.string(),
|
|
86
|
+
title: z.string(),
|
|
87
|
+
summary: z.string(),
|
|
88
|
+
categories: z.array(z.string()),
|
|
89
|
+
sharedCategories: z.array(z.string()),
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Server implementation
|
|
95
|
+
export const searchBlogPosts = searchBlogPostsToolDef.server(({ query }) => {
|
|
96
|
+
return allPosts
|
|
97
|
+
.filter(
|
|
98
|
+
(post) =>
|
|
99
|
+
post.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
100
|
+
post.summary.toLowerCase().includes(query.toLowerCase()) ||
|
|
101
|
+
post.categories.some((cat) =>
|
|
102
|
+
cat.toLowerCase().includes(query.toLowerCase()),
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
.map((post) => ({
|
|
106
|
+
slug: post.slug,
|
|
107
|
+
title: post.title,
|
|
108
|
+
summary: post.summary,
|
|
109
|
+
categories: post.categories,
|
|
110
|
+
}))
|
|
111
|
+
})
|
|
@@ -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 Header from '../components/Header'
|
|
6
|
+
|
|
7
|
+
import appCss from '../styles.css?url'
|
|
8
|
+
|
|
9
|
+
export const Route = createRootRoute({
|
|
10
|
+
head: () => ({
|
|
11
|
+
meta: [
|
|
12
|
+
{
|
|
13
|
+
charSet: 'utf-8',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'viewport',
|
|
17
|
+
content: 'width=device-width, initial-scale=1',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
title: 'Hawaii Adventures Blog',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
links: [
|
|
24
|
+
{
|
|
25
|
+
rel: 'stylesheet',
|
|
26
|
+
href: appCss,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
}),
|
|
30
|
+
shellComponent: RootDocument,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
function RootDocument({ children }: { children: React.ReactNode }) {
|
|
34
|
+
return (
|
|
35
|
+
<html lang="en">
|
|
36
|
+
<head>
|
|
37
|
+
<HeadContent />
|
|
38
|
+
</head>
|
|
39
|
+
<body className="bg-[#f5f1e4] min-h-screen">
|
|
40
|
+
<Header />
|
|
41
|
+
<main className="mt-20">{children}</main>
|
|
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,117 @@
|
|
|
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 {
|
|
9
|
+
getPostBySlug,
|
|
10
|
+
getAllBlogPosts,
|
|
11
|
+
searchBlogPosts,
|
|
12
|
+
} from '@/lib/blog-tools'
|
|
13
|
+
import type { Provider } from '@/lib/model-selection'
|
|
14
|
+
|
|
15
|
+
export const Route = createFileRoute('/api/blog-chat')({
|
|
16
|
+
server: {
|
|
17
|
+
handlers: {
|
|
18
|
+
POST: async ({ request }) => {
|
|
19
|
+
const requestSignal = request.signal
|
|
20
|
+
|
|
21
|
+
if (requestSignal.aborted) {
|
|
22
|
+
return new Response(null, { status: 499 })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const abortController = new AbortController()
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const body = await request.json()
|
|
29
|
+
const { messages, slug } = body
|
|
30
|
+
const data = body.data || {}
|
|
31
|
+
|
|
32
|
+
const SYSTEM_PROMPT = slug
|
|
33
|
+
? `You are a helpful blog assistant. You help readers understand and engage with blog articles.
|
|
34
|
+
|
|
35
|
+
CAPABILITIES:
|
|
36
|
+
1. Use getPostBySlug to read the content of the article the user is currently viewing
|
|
37
|
+
2. Use getAllBlogPosts to see all available articles on the blog
|
|
38
|
+
3. Use searchBlogPosts to find articles that match the user's query
|
|
39
|
+
|
|
40
|
+
INSTRUCTIONS:
|
|
41
|
+
- When a user asks about "this article", "this post", or "what I'm reading", use getPostBySlug first
|
|
42
|
+
- Be conversational and helpful
|
|
43
|
+
- Summarize content when asked, highlight key points, and answer questions about the article
|
|
44
|
+
- If asked for recommendations, use getRelatedPosts to suggest similar content
|
|
45
|
+
- Keep responses concise but informative
|
|
46
|
+
- You can help explain concepts, provide additional context, or discuss themes from the articles
|
|
47
|
+
|
|
48
|
+
CONTEXT: The current article slug is "${slug}".`
|
|
49
|
+
: `You are a helpful blog assistant. You help readers understand and engage with blog articles.
|
|
50
|
+
|
|
51
|
+
CAPABILITIES:
|
|
52
|
+
1. Use getAllBlogPosts to see all available articles on the blog
|
|
53
|
+
2. UsUse searchBlogPosts to find articles that match the user's query
|
|
54
|
+
|
|
55
|
+
INSTRUCTIONS:
|
|
56
|
+
- When a user asks for a recommendation, use searchBlogPosts to suggest similar content
|
|
57
|
+
- Keep responses concise but informative
|
|
58
|
+
- You can help explain concepts, provide additional context, or discuss themes from the articles`
|
|
59
|
+
|
|
60
|
+
// Determine the best available provider
|
|
61
|
+
let provider: Provider = data.provider || 'ollama'
|
|
62
|
+
let model: string = data.model || 'mistral:7b'
|
|
63
|
+
|
|
64
|
+
// Use the first available provider with an API key, fallback to ollama
|
|
65
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
66
|
+
provider = 'anthropic'
|
|
67
|
+
model = 'claude-haiku-4-5'
|
|
68
|
+
} else if (process.env.OPENAI_API_KEY) {
|
|
69
|
+
provider = 'openai'
|
|
70
|
+
model = 'gpt-4o'
|
|
71
|
+
} else if (process.env.GEMINI_API_KEY) {
|
|
72
|
+
provider = 'gemini'
|
|
73
|
+
model = 'gemini-2.0-flash-exp'
|
|
74
|
+
}
|
|
75
|
+
// else keep ollama as default
|
|
76
|
+
|
|
77
|
+
// Adapter factory pattern for multi-vendor support
|
|
78
|
+
const adapterConfig = {
|
|
79
|
+
anthropic: () =>
|
|
80
|
+
anthropicText((model || 'claude-haiku-4-5') as any),
|
|
81
|
+
openai: () => openaiText((model || 'gpt-4o') as any),
|
|
82
|
+
gemini: () => geminiText((model || 'gemini-2.0-flash-exp') as any),
|
|
83
|
+
ollama: () => ollamaText((model || 'mistral:7b') as any),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const adapter = adapterConfig[provider]()
|
|
87
|
+
|
|
88
|
+
const stream = chat({
|
|
89
|
+
adapter,
|
|
90
|
+
tools: [getPostBySlug, getAllBlogPosts, searchBlogPosts],
|
|
91
|
+
systemPrompts: [SYSTEM_PROMPT],
|
|
92
|
+
agentLoopStrategy: maxIterations(5),
|
|
93
|
+
messages,
|
|
94
|
+
abortController,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return toServerSentEventsResponse(stream, { abortController })
|
|
98
|
+
} catch (error: any) {
|
|
99
|
+
console.error('Blog chat error:', error)
|
|
100
|
+
if (error.name === 'AbortError' || abortController.signal.aborted) {
|
|
101
|
+
return new Response(null, { status: 499 })
|
|
102
|
+
}
|
|
103
|
+
return new Response(
|
|
104
|
+
JSON.stringify({
|
|
105
|
+
error: 'Failed to process chat request',
|
|
106
|
+
message: error.message,
|
|
107
|
+
}),
|
|
108
|
+
{
|
|
109
|
+
status: 500,
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
|
|
3
|
+
import { allPosts } from "content-collections";
|
|
4
|
+
|
|
5
|
+
import BlogPosts from "@/components/blog-posts";
|
|
6
|
+
|
|
7
|
+
export const Route = createFileRoute("/category/$category")({
|
|
8
|
+
component: RouteComponent,
|
|
9
|
+
loader: async ({ params }) => {
|
|
10
|
+
const category = params.category;
|
|
11
|
+
const posts = allPosts.filter((post) => post.categories.includes(category));
|
|
12
|
+
return { category, posts };
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function RouteComponent() {
|
|
17
|
+
const { category, posts } = Route.useLoaderData();
|
|
18
|
+
return <BlogPosts title={category} posts={posts} />;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
import { allPosts } from 'content-collections'
|
|
4
|
+
|
|
5
|
+
import BlogPosts from '@/components/blog-posts'
|
|
6
|
+
import VacayAssistant from '@/components/VacayAssistant'
|
|
7
|
+
|
|
8
|
+
export const Route = createFileRoute('/')({
|
|
9
|
+
component: App,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
function App() {
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<VacayAssistant />
|
|
16
|
+
<BlogPosts title="Hawaii Adventures" posts={allPosts} />
|
|
17
|
+
</>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { marked } from 'marked'
|
|
3
|
+
|
|
4
|
+
import { allPosts } from 'content-collections'
|
|
5
|
+
|
|
6
|
+
import VacayAssistant from '@/components/VacayAssistant'
|
|
7
|
+
|
|
8
|
+
export const Route = createFileRoute('/posts/$slug')({
|
|
9
|
+
loader: async ({ params }) => {
|
|
10
|
+
const post = allPosts.find((post) => post.slug === params.slug)
|
|
11
|
+
if (!post) {
|
|
12
|
+
throw new Error('Post not found')
|
|
13
|
+
}
|
|
14
|
+
return post
|
|
15
|
+
},
|
|
16
|
+
component: RouteComponent,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
function RouteComponent() {
|
|
20
|
+
const post = Route.useLoaderData()
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="min-h-screen flex flex-col">
|
|
24
|
+
<VacayAssistant slug={post.slug} postTitle={post.title} />
|
|
25
|
+
|
|
26
|
+
<div
|
|
27
|
+
className="relative h-[66vh]"
|
|
28
|
+
style={{
|
|
29
|
+
backgroundImage: `url(/${post.image})`,
|
|
30
|
+
backgroundSize: 'cover',
|
|
31
|
+
backgroundPosition: 'center',
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<div className="absolute inset-0 bg-black/30" />
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="relative -mt-[40vh]">
|
|
38
|
+
<div className="container mx-auto max-w-4xl px-4 text-white">
|
|
39
|
+
<div className="rounded-2xl overflow-hidden backdrop-blur-xl bg-white/10 border border-white/20 shadow-2xl">
|
|
40
|
+
<div className="p-8 md:p-12">
|
|
41
|
+
{/* Title section */}
|
|
42
|
+
<h1 className="text-4xl font-serif italic md:text-5xl font-bold mb-4 drop-shadow-lg">
|
|
43
|
+
{post.title}
|
|
44
|
+
</h1>
|
|
45
|
+
|
|
46
|
+
{/* Summary section */}
|
|
47
|
+
<p className="text-lg text-white mb-8 drop-shadow">
|
|
48
|
+
{post.summary}
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
{/* Main content */}
|
|
52
|
+
<div className="prose prose-lg max-w-none prose-invert prose-p:text-white prose-headings:text-white prose-strong:text-white prose-a:text-teal-300">
|
|
53
|
+
<div
|
|
54
|
+
dangerouslySetInnerHTML={{ __html: marked(post.content) }}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
@plugin "tailwindcss-animate";
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
@apply m-0;
|
|
9
|
+
font-family:
|
|
10
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
|
11
|
+
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
12
|
+
-webkit-font-smoothing: antialiased;
|
|
13
|
+
-moz-osx-font-smoothing: grayscale;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
code {
|
|
17
|
+
font-family:
|
|
18
|
+
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
:root {
|
|
22
|
+
--background: oklch(1 0 0);
|
|
23
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
24
|
+
--card: oklch(1 0 0);
|
|
25
|
+
--card-foreground: oklch(0.141 0.005 285.823);
|
|
26
|
+
--popover: oklch(1 0 0);
|
|
27
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
28
|
+
--primary: oklch(0.21 0.006 285.885);
|
|
29
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
30
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
31
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
32
|
+
--muted: oklch(0.967 0.001 286.375);
|
|
33
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
34
|
+
--accent: oklch(0.967 0.001 286.375);
|
|
35
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
36
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
37
|
+
--destructive-foreground: oklch(0.577 0.245 27.325);
|
|
38
|
+
--border: oklch(0.92 0.004 286.32);
|
|
39
|
+
--input: oklch(0.92 0.004 286.32);
|
|
40
|
+
--ring: oklch(0.871 0.006 286.286);
|
|
41
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
42
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
43
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
44
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
45
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
46
|
+
--radius: 0.625rem;
|
|
47
|
+
--sidebar: oklch(0.985 0 0);
|
|
48
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
49
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
50
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
51
|
+
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
52
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
53
|
+
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
54
|
+
--sidebar-ring: oklch(0.871 0.006 286.286);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.dark {
|
|
58
|
+
--background: oklch(0.141 0.005 285.823);
|
|
59
|
+
--foreground: oklch(0.985 0 0);
|
|
60
|
+
--card: oklch(0.141 0.005 285.823);
|
|
61
|
+
--card-foreground: oklch(0.985 0 0);
|
|
62
|
+
--popover: oklch(0.141 0.005 285.823);
|
|
63
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
64
|
+
--primary: oklch(0.985 0 0);
|
|
65
|
+
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
66
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
67
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
68
|
+
--muted: oklch(0.274 0.006 286.033);
|
|
69
|
+
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
70
|
+
--accent: oklch(0.274 0.006 286.033);
|
|
71
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
72
|
+
--destructive: oklch(0.396 0.141 25.723);
|
|
73
|
+
--destructive-foreground: oklch(0.637 0.237 25.331);
|
|
74
|
+
--border: oklch(0.274 0.006 286.033);
|
|
75
|
+
--input: oklch(0.274 0.006 286.033);
|
|
76
|
+
--ring: oklch(0.442 0.017 285.786);
|
|
77
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
78
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
79
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
80
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
81
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
82
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
83
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
84
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
85
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
86
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
87
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
88
|
+
--sidebar-border: oklch(0.274 0.006 286.033);
|
|
89
|
+
--sidebar-ring: oklch(0.442 0.017 285.786);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@theme inline {
|
|
93
|
+
--color-background: var(--background);
|
|
94
|
+
--color-foreground: var(--foreground);
|
|
95
|
+
--color-card: var(--card);
|
|
96
|
+
--color-card-foreground: var(--card-foreground);
|
|
97
|
+
--color-popover: var(--popover);
|
|
98
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
99
|
+
--color-primary: var(--primary);
|
|
100
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
101
|
+
--color-secondary: var(--secondary);
|
|
102
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
103
|
+
--color-muted: var(--muted);
|
|
104
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
105
|
+
--color-accent: var(--accent);
|
|
106
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
107
|
+
--color-destructive: var(--destructive);
|
|
108
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
109
|
+
--color-border: var(--border);
|
|
110
|
+
--color-input: var(--input);
|
|
111
|
+
--color-ring: var(--ring);
|
|
112
|
+
--color-chart-1: var(--chart-1);
|
|
113
|
+
--color-chart-2: var(--chart-2);
|
|
114
|
+
--color-chart-3: var(--chart-3);
|
|
115
|
+
--color-chart-4: var(--chart-4);
|
|
116
|
+
--color-chart-5: var(--chart-5);
|
|
117
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
118
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
119
|
+
--radius-lg: var(--radius);
|
|
120
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
121
|
+
--color-sidebar: var(--sidebar);
|
|
122
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
123
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
124
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
125
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
126
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
127
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
128
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@layer base {
|
|
132
|
+
* {
|
|
133
|
+
@apply border-border outline-ring/50;
|
|
134
|
+
}
|
|
135
|
+
body {
|
|
136
|
+
@apply bg-background text-foreground;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Blog",
|
|
3
|
+
"description": "A Hawaii adventures travel blog built with content-collections and TanStack Start for Netlify.",
|
|
4
|
+
"phase": "example",
|
|
5
|
+
"modes": ["file-router"],
|
|
6
|
+
"type": "example",
|
|
7
|
+
"priority": 10,
|
|
8
|
+
"link": "",
|
|
9
|
+
"routes": [
|
|
10
|
+
{
|
|
11
|
+
"url": "/",
|
|
12
|
+
"path": "src/routes/index.tsx",
|
|
13
|
+
"jsName": "BlogHome"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"url": "/posts/$slug",
|
|
17
|
+
"path": "src/routes/posts.$slug.tsx",
|
|
18
|
+
"jsName": "BlogPost"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"url": "/category/$category",
|
|
22
|
+
"path": "src/routes/category.$category.tsx",
|
|
23
|
+
"jsName": "BlogCategory"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"integrations": [
|
|
27
|
+
{
|
|
28
|
+
"type": "vite-plugin",
|
|
29
|
+
"import": "import contentCollections from '@content-collections/vite'",
|
|
30
|
+
"code": "contentCollections()"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"dependsOn": [],
|
|
34
|
+
"variables": [],
|
|
35
|
+
"bareBones": {
|
|
36
|
+
"deleteFiles": [
|
|
37
|
+
"public/jungle.jpg",
|
|
38
|
+
"public/mountains.jpg",
|
|
39
|
+
"public/snorkeling.jpg",
|
|
40
|
+
"public/waterfall.jpg"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"@tanstack/ai": "0.2.2",
|
|
4
|
+
"@tanstack/ai-anthropic": "0.2.0",
|
|
5
|
+
"@tanstack/ai-client": "0.2.2",
|
|
6
|
+
"@tanstack/ai-gemini": "0.3.2",
|
|
7
|
+
"@tanstack/ai-ollama": "0.3.0",
|
|
8
|
+
"@tanstack/ai-openai": "0.3.0",
|
|
9
|
+
"@tanstack/ai-react": "0.2.2",
|
|
10
|
+
"@tanstack/store": "^0.8.0",
|
|
11
|
+
"class-variance-authority": "^0.7.1",
|
|
12
|
+
"clsx": "^2.1.1",
|
|
13
|
+
"marked": "^17.0.1",
|
|
14
|
+
"streamdown": "^2.1.0",
|
|
15
|
+
"tailwind-merge": "^3.0.2",
|
|
16
|
+
"tailwindcss-animate": "^1.0.7",
|
|
17
|
+
"zod": "^4.3.5"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@content-collections/core": "^0.13.1",
|
|
21
|
+
"@content-collections/vite": "^0.2.8"
|
|
22
|
+
}
|
|
23
|
+
}
|