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,162 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useStore } from '@tanstack/react-store'
|
|
3
|
+
import { Send, X } from 'lucide-react'
|
|
4
|
+
import { Streamdown } from 'streamdown'
|
|
5
|
+
|
|
6
|
+
import { useMotorcycleChat } from '@/lib/motorcycle-ai-hook'
|
|
7
|
+
import type { ChatMessages } from '@/lib/ai-hook'
|
|
8
|
+
import { showMotorcycleAIAssistant } from '@/store/motorcycle-assistant'
|
|
9
|
+
|
|
10
|
+
import MotorcycleRecommendation from './MotorcycleRecommendation'
|
|
11
|
+
|
|
12
|
+
function Messages({ messages }: { messages: ChatMessages }) {
|
|
13
|
+
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (messagesContainerRef.current) {
|
|
17
|
+
messagesContainerRef.current.scrollTop =
|
|
18
|
+
messagesContainerRef.current.scrollHeight
|
|
19
|
+
}
|
|
20
|
+
}, [messages])
|
|
21
|
+
|
|
22
|
+
if (!messages.length) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex-1 flex items-center justify-center text-gray-400 text-sm">
|
|
25
|
+
Ask me about your next motorcycle.
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto">
|
|
32
|
+
{messages.map(({ id, role, parts }) => (
|
|
33
|
+
<div
|
|
34
|
+
key={id}
|
|
35
|
+
className={`py-3 ${
|
|
36
|
+
role === 'assistant'
|
|
37
|
+
? 'bg-linear-to-r from-orange-500/5 to-red-600/5'
|
|
38
|
+
: 'bg-transparent'
|
|
39
|
+
}`}
|
|
40
|
+
>
|
|
41
|
+
{parts.map((part, index) => {
|
|
42
|
+
if (part.type === 'text' && part.content) {
|
|
43
|
+
return (
|
|
44
|
+
<div key={index} className="flex items-start gap-2 px-4">
|
|
45
|
+
{role === 'assistant' ? (
|
|
46
|
+
<div className="w-6 h-6 rounded-lg bg-linear-to-r from-orange-500 to-red-600 flex items-center justify-center text-xs font-medium text-white flex-shrink-0">
|
|
47
|
+
AI
|
|
48
|
+
</div>
|
|
49
|
+
) : (
|
|
50
|
+
<div className="w-6 h-6 rounded-lg bg-gray-700 flex items-center justify-center text-xs font-medium text-white flex-shrink-0">
|
|
51
|
+
Y
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
<div className="flex-1 min-w-0 text-white prose dark:prose-invert max-w-none prose-sm">
|
|
55
|
+
<Streamdown>{part.content}</Streamdown>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
if (
|
|
61
|
+
part.type === 'tool-call' &&
|
|
62
|
+
part.name === 'recommendMotorcycle' &&
|
|
63
|
+
part.output
|
|
64
|
+
) {
|
|
65
|
+
return (
|
|
66
|
+
<div key={part.id} className="max-w-[80%] mx-auto">
|
|
67
|
+
<MotorcycleRecommendation id={String(part.output?.id)} />
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default function AIAssistant() {
|
|
79
|
+
const isOpen = useStore(showMotorcycleAIAssistant)
|
|
80
|
+
const { messages, sendMessage } = useMotorcycleChat()
|
|
81
|
+
const [input, setInput] = useState('')
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="relative z-50">
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => showMotorcycleAIAssistant.setState((state) => !state)}
|
|
87
|
+
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-black/90 text-white hover:bg-black/80 transition-all border border-orange-500/20 shadow-lg shadow-orange-500/10"
|
|
88
|
+
>
|
|
89
|
+
<div className="w-5 h-5 rounded-lg bg-linear-to-r from-orange-500 to-red-600 flex items-center justify-center text-xs font-medium">
|
|
90
|
+
AI
|
|
91
|
+
</div>
|
|
92
|
+
Motorcycle Expert
|
|
93
|
+
</button>
|
|
94
|
+
|
|
95
|
+
{isOpen && (
|
|
96
|
+
<div className="absolute top-full right-0 mt-2 w-[700px] h-[600px] bg-black/95 rounded-lg shadow-2xl border border-orange-500/20 flex flex-col backdrop-blur-sm">
|
|
97
|
+
<div className="flex items-center justify-between p-4 border-b border-orange-500/20 bg-linear-to-r from-orange-500/5 to-red-600/5">
|
|
98
|
+
<h3 className="font-semibold text-white flex items-center gap-2">
|
|
99
|
+
<span className="w-6 h-6 rounded-lg bg-linear-to-r from-orange-500 to-red-600 flex items-center justify-center text-xs">
|
|
100
|
+
AI
|
|
101
|
+
</span>
|
|
102
|
+
Motorcycle Expert
|
|
103
|
+
</h3>
|
|
104
|
+
<button
|
|
105
|
+
onClick={() =>
|
|
106
|
+
showMotorcycleAIAssistant.setState((state) => !state)
|
|
107
|
+
}
|
|
108
|
+
className="text-gray-400 hover:text-white transition-colors"
|
|
109
|
+
>
|
|
110
|
+
<X className="w-4 h-4" />
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<Messages messages={messages} />
|
|
115
|
+
|
|
116
|
+
<div className="p-4 border-t border-orange-500/20 bg-linear-to-r from-orange-500/5 to-red-600/5">
|
|
117
|
+
<form
|
|
118
|
+
onSubmit={(e) => {
|
|
119
|
+
e.preventDefault()
|
|
120
|
+
if (input.trim()) {
|
|
121
|
+
sendMessage(input)
|
|
122
|
+
setInput('')
|
|
123
|
+
}
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<div className="relative">
|
|
127
|
+
<textarea
|
|
128
|
+
value={input}
|
|
129
|
+
onChange={(e) => setInput(e.target.value)}
|
|
130
|
+
placeholder="Ask about our motorcycles..."
|
|
131
|
+
className="w-full rounded-lg border border-orange-500/20 bg-black/50 pl-3 pr-10 py-3 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500/30 focus:border-transparent resize-none overflow-hidden shadow-inner"
|
|
132
|
+
rows={1}
|
|
133
|
+
style={{ minHeight: '44px', maxHeight: '120px' }}
|
|
134
|
+
onInput={(e) => {
|
|
135
|
+
const target = e.target as HTMLTextAreaElement
|
|
136
|
+
target.style.height = 'auto'
|
|
137
|
+
target.style.height =
|
|
138
|
+
Math.min(target.scrollHeight, 120) + 'px'
|
|
139
|
+
}}
|
|
140
|
+
onKeyDown={(e) => {
|
|
141
|
+
if (e.key === 'Enter' && !e.shiftKey && input.trim()) {
|
|
142
|
+
e.preventDefault()
|
|
143
|
+
sendMessage(input)
|
|
144
|
+
setInput('')
|
|
145
|
+
}
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
<button
|
|
149
|
+
type="submit"
|
|
150
|
+
disabled={!input.trim()}
|
|
151
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-orange-500 hover:text-orange-400 disabled:text-gray-500 transition-colors focus:outline-none"
|
|
152
|
+
>
|
|
153
|
+
<Send className="w-4 h-4" />
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</form>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
import { showMotorcycleAIAssistant } from '@/store/motorcycle-assistant'
|
|
4
|
+
|
|
5
|
+
import motorcycles from '@/data/motorcycles'
|
|
6
|
+
|
|
7
|
+
export default function MotorcycleRecommendation({ id }: { id: string }) {
|
|
8
|
+
const navigate = useNavigate()
|
|
9
|
+
const motorcycle = motorcycles.find((motorcycle) => motorcycle.id === +id)
|
|
10
|
+
if (!motorcycle) {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
return (
|
|
14
|
+
<div className="my-4 rounded-lg overflow-hidden border border-orange-500/20 bg-gray-800/50">
|
|
15
|
+
<div className="aspect-[4/3] relative overflow-hidden">
|
|
16
|
+
<img
|
|
17
|
+
src={motorcycle.image}
|
|
18
|
+
alt={motorcycle.name}
|
|
19
|
+
className="w-full h-full object-cover"
|
|
20
|
+
/>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="p-4">
|
|
23
|
+
<h3 className="text-lg font-semibold text-white mb-2">
|
|
24
|
+
{motorcycle.name}
|
|
25
|
+
</h3>
|
|
26
|
+
<p className="text-sm text-gray-300 mb-1 line-clamp-2">
|
|
27
|
+
{motorcycle.shortDescription}
|
|
28
|
+
</p>
|
|
29
|
+
<p className="text-sm text-gray-400 mb-3">
|
|
30
|
+
{motorcycle.engineSize}cc •{' '}
|
|
31
|
+
{motorcycle.type.charAt(0).toUpperCase() + motorcycle.type.slice(1)}
|
|
32
|
+
</p>
|
|
33
|
+
<div className="flex items-center justify-between">
|
|
34
|
+
<div className="text-lg font-bold text-emerald-400">
|
|
35
|
+
${motorcycle.price.toLocaleString()}
|
|
36
|
+
</div>
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => {
|
|
39
|
+
navigate({
|
|
40
|
+
to: '/motorcycles/$motorcycleId',
|
|
41
|
+
params: { motorcycleId: motorcycle.id.toString() },
|
|
42
|
+
})
|
|
43
|
+
showMotorcycleAIAssistant.setState(() => false)
|
|
44
|
+
}}
|
|
45
|
+
className="bg-linear-to-r from-orange-500 to-red-600 text-white px-4 py-1.5 rounded-lg text-sm hover:opacity-90 transition-opacity"
|
|
46
|
+
>
|
|
47
|
+
View Details
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface Motorcycle {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
image: string;
|
|
5
|
+
description: string;
|
|
6
|
+
shortDescription: string;
|
|
7
|
+
price: number;
|
|
8
|
+
engineSize: number; // in CCs
|
|
9
|
+
type: "scooter" | "cruiser" | "adventure" | "sport" | "supersport";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const motorcycles: Array<Motorcycle> = [
|
|
13
|
+
{
|
|
14
|
+
id: 1,
|
|
15
|
+
name: "Luna-C Urbanite",
|
|
16
|
+
image: "/motorcycle-scooter.jpg",
|
|
17
|
+
description:
|
|
18
|
+
"The Luna-C Urbanite is the perfect city companion, combining style and practicality in a compact package. With its 125cc engine, it delivers excellent fuel efficiency while maintaining enough power for urban commuting. The comfortable seating position and lightweight frame make it easy to maneuver through city traffic, while the under-seat storage provides ample space for your daily essentials.",
|
|
19
|
+
shortDescription:
|
|
20
|
+
"A stylish and practical 125cc scooter perfect for urban commuting with excellent fuel efficiency.",
|
|
21
|
+
price: 3000,
|
|
22
|
+
engineSize: 125,
|
|
23
|
+
type: "scooter",
|
|
24
|
+
},
|
|
25
|
+
<% if (!addOnOption.project?.bareBones) { %>
|
|
26
|
+
{
|
|
27
|
+
id: 2,
|
|
28
|
+
name: "Luna-C Voyager",
|
|
29
|
+
image: "/motorcycle-cruiser.jpg",
|
|
30
|
+
description:
|
|
31
|
+
"The Luna-C Voyager is a classic cruiser that combines comfort with style. Its 750cc engine provides smooth, reliable power for long-distance rides, while the ergonomic design ensures comfort even on extended journeys. The classic cruiser styling with modern touches makes it stand out on any road.",
|
|
32
|
+
shortDescription:
|
|
33
|
+
"A comfortable 750cc cruiser perfect for long-distance rides with classic styling.",
|
|
34
|
+
price: 12000,
|
|
35
|
+
engineSize: 750,
|
|
36
|
+
type: "cruiser",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 3,
|
|
40
|
+
name: "Luna-C Explorer",
|
|
41
|
+
image: "/motorcycle-adventure.jpg",
|
|
42
|
+
description:
|
|
43
|
+
"The Luna-C Explorer is built for adventure, whether you're tackling city streets or exploring off-road trails. Its 850cc engine provides the perfect balance of power and control, while the dual-sport suspension system ensures a smooth ride in any condition. The upright riding position and comfortable seat make it ideal for long journeys.",
|
|
44
|
+
shortDescription:
|
|
45
|
+
"An 850cc adventure bike designed for both on-road and off-road exploration.",
|
|
46
|
+
price: 15000,
|
|
47
|
+
engineSize: 850,
|
|
48
|
+
type: "adventure",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 4,
|
|
52
|
+
name: "Luna-C Velocity",
|
|
53
|
+
image: "/motorcycle-sport.jpg",
|
|
54
|
+
description:
|
|
55
|
+
"The Luna-C Velocity is a high-performance sport bike that delivers thrilling acceleration and precise handling. Its 1000cc engine provides exceptional power, while the aerodynamic design and advanced suspension system ensure optimal performance on the track or the open road. The aggressive styling and premium components make it a true performance machine.",
|
|
56
|
+
shortDescription:
|
|
57
|
+
"A high-performance 1000cc sport bike with exceptional power and handling.",
|
|
58
|
+
price: 20000,
|
|
59
|
+
engineSize: 1000,
|
|
60
|
+
type: "sport",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 5,
|
|
64
|
+
name: "Luna-C Supersport",
|
|
65
|
+
image: "/motorcycle-supersport.jpg",
|
|
66
|
+
description:
|
|
67
|
+
"The Luna-C Supersport represents the pinnacle of motorcycle engineering. Its 1300cc engine delivers breathtaking power and acceleration, while the cutting-edge aerodynamics and race-inspired components ensure maximum performance. This machine is designed for experienced riders who demand the ultimate in speed and handling.",
|
|
68
|
+
shortDescription:
|
|
69
|
+
"The ultimate 1300cc supersport machine with race-inspired performance and aerodynamics.",
|
|
70
|
+
price: 25000,
|
|
71
|
+
engineSize: 1300,
|
|
72
|
+
type: "supersport",
|
|
73
|
+
},
|
|
74
|
+
<% } %>
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export default motorcycles;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchServerSentEvents,
|
|
3
|
+
useChat,
|
|
4
|
+
createChatClientOptions,
|
|
5
|
+
} from "@tanstack/ai-react";
|
|
6
|
+
import type { InferChatMessages } from "@tanstack/ai-react";
|
|
7
|
+
import { clientTools } from "@tanstack/ai-client";
|
|
8
|
+
|
|
9
|
+
import { recommendMotorcycleToolDef } from "@/lib/motorcycle-tools";
|
|
10
|
+
|
|
11
|
+
const recommendMotorcycleToolClient = recommendMotorcycleToolDef.client(
|
|
12
|
+
({ id }) => ({
|
|
13
|
+
id: +id,
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const chatOptions = createChatClientOptions({
|
|
18
|
+
connection: fetchServerSentEvents("/api/motorcycle-chat"),
|
|
19
|
+
tools: clientTools(recommendMotorcycleToolClient),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type ChatMessages = InferChatMessages<typeof chatOptions>;
|
|
23
|
+
|
|
24
|
+
export const useMotorcycleChat = () => useChat(chatOptions);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { toolDefinition } from "@tanstack/ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import motorcycles from "@/data/motorcycles";
|
|
4
|
+
|
|
5
|
+
// Tool definition for getting motorcycles
|
|
6
|
+
export const getMotorcyclesToolDef = toolDefinition({
|
|
7
|
+
name: "getMotorcycles",
|
|
8
|
+
description: "Get all motorcycles from the database",
|
|
9
|
+
inputSchema: z.object({}),
|
|
10
|
+
outputSchema: z.array(
|
|
11
|
+
z.object({
|
|
12
|
+
id: z.number(),
|
|
13
|
+
name: z.string(),
|
|
14
|
+
image: z.string(),
|
|
15
|
+
description: z.string(),
|
|
16
|
+
shortDescription: z.string(),
|
|
17
|
+
price: z.number(),
|
|
18
|
+
engineSize: z.number(),
|
|
19
|
+
type: z.enum(["scooter", "cruiser", "adventure", "sport", "supersport"]),
|
|
20
|
+
})
|
|
21
|
+
),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Server implementation
|
|
25
|
+
export const getMotorcycles = getMotorcyclesToolDef.server(() => motorcycles);
|
|
26
|
+
|
|
27
|
+
// Tool definition for motorcycle recommendation
|
|
28
|
+
export const recommendMotorcycleToolDef = toolDefinition({
|
|
29
|
+
name: "recommendMotorcycle",
|
|
30
|
+
description:
|
|
31
|
+
"REQUIRED tool to display a motorcycle recommendation to the user. This tool MUST be used whenever recommending a motorcycle - do NOT write recommendations yourself. This displays the motorcycle in a special appealing format with a view details button.",
|
|
32
|
+
inputSchema: z.object({
|
|
33
|
+
id: z
|
|
34
|
+
.union([z.string(), z.number()])
|
|
35
|
+
.describe(
|
|
36
|
+
"The ID of the motorcycle to recommend (from the getMotorcycles results)"
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
outputSchema: z.object({
|
|
40
|
+
id: z.number(),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
@@ -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: 'Luna-C Motorcycles',
|
|
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-black">
|
|
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,78 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { chat, maxIterations, toServerSentEventsResponse } from "@tanstack/ai";
|
|
3
|
+
import { anthropicText } from "@tanstack/ai-anthropic";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getMotorcycles,
|
|
7
|
+
recommendMotorcycleToolDef,
|
|
8
|
+
} from "@/lib/motorcycle-tools";
|
|
9
|
+
|
|
10
|
+
const SYSTEM_PROMPT = `You are a helpful assistant for a store that sells motorcycles.
|
|
11
|
+
|
|
12
|
+
You are also a motorcycle enthusiast who is passionate about our products and how they can bring joy to people's lives, the excitement of the open road and adventure. Emphasize these aspects in your responses.
|
|
13
|
+
|
|
14
|
+
CRITICAL INSTRUCTIONS - YOU MUST FOLLOW THIS EXACT WORKFLOW:
|
|
15
|
+
|
|
16
|
+
When a user asks for a motorcycle recommendation:
|
|
17
|
+
1. FIRST: Use the getMotorcycles tool (no parameters needed)
|
|
18
|
+
2. SECOND: Use the recommendMotorcycle tool with the ID of the motorcycle you want to recommend
|
|
19
|
+
3. NEVER write a recommendation directly - ALWAYS use the recommendMotorcycle tool
|
|
20
|
+
|
|
21
|
+
IMPORTANT:
|
|
22
|
+
- The recommendMotorcycle tool will display the motorcycle in a special, appealing format
|
|
23
|
+
- You MUST use recommendMotorcycle for ANY motorcycle recommendation
|
|
24
|
+
- ONLY recommend motorcycles from our inventory (use getMotorcycles first)
|
|
25
|
+
- The recommendMotorcycle tool has a view details button - this is how customers can learn more
|
|
26
|
+
- Do NOT describe the motorcycle yourself - let the recommendMotorcycle tool do it
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
export const Route = createFileRoute("/api/motorcycle-chat")({
|
|
30
|
+
server: {
|
|
31
|
+
handlers: {
|
|
32
|
+
POST: async ({ request }) => {
|
|
33
|
+
// Capture request signal before reading body (it may be aborted after body is consumed)
|
|
34
|
+
const requestSignal = request.signal;
|
|
35
|
+
|
|
36
|
+
// If request is already aborted, return early
|
|
37
|
+
if (requestSignal.aborted) {
|
|
38
|
+
return new Response(null, { status: 499 }); // 499 = Client Closed Request
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const abortController = new AbortController();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const body = await request.json();
|
|
45
|
+
const { messages } = body;
|
|
46
|
+
|
|
47
|
+
const adapter = anthropicText("claude-haiku-4-5");
|
|
48
|
+
|
|
49
|
+
const stream = chat({
|
|
50
|
+
adapter,
|
|
51
|
+
tools: [
|
|
52
|
+
getMotorcycles, // Server tool
|
|
53
|
+
recommendMotorcycleToolDef, // No server execute - client will handle
|
|
54
|
+
],
|
|
55
|
+
systemPrompts: [SYSTEM_PROMPT],
|
|
56
|
+
agentLoopStrategy: maxIterations(5),
|
|
57
|
+
messages,
|
|
58
|
+
abortController,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return toServerSentEventsResponse(stream, { abortController });
|
|
62
|
+
} catch (error: any) {
|
|
63
|
+
// If request was aborted, return early (don't send error response)
|
|
64
|
+
if (error.name === "AbortError" || abortController.signal.aborted) {
|
|
65
|
+
return new Response(null, { status: 499 }); // 499 = Client Closed Request
|
|
66
|
+
}
|
|
67
|
+
return new Response(
|
|
68
|
+
JSON.stringify({ error: "Failed to process chat request" }),
|
|
69
|
+
{
|
|
70
|
+
status: 500,
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Link, createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import motorcycles from '@/data/motorcycles'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/')({
|
|
5
|
+
component: MotorcyclesIndex,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function MotorcyclesIndex() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="bg-black text-white p-5">
|
|
11
|
+
<h1 className="text-3xl font-bold mb-16 text-center">
|
|
12
|
+
Luna-C Motorcycles
|
|
13
|
+
</h1>
|
|
14
|
+
<div className="max-w-7xl mx-auto">
|
|
15
|
+
{motorcycles.map((motorcycle, index) => (
|
|
16
|
+
<div
|
|
17
|
+
key={motorcycle.id}
|
|
18
|
+
className={`relative flex flex-col md:flex-row items-stretch gap-8 mb-32 ${
|
|
19
|
+
index % 2 === 1 ? 'md:flex-row-reverse' : ''
|
|
20
|
+
}`}
|
|
21
|
+
>
|
|
22
|
+
<div className="w-full md:w-[60%] relative">
|
|
23
|
+
<Link
|
|
24
|
+
to="/motorcycles/$motorcycleId"
|
|
25
|
+
params={{
|
|
26
|
+
motorcycleId: motorcycle.id.toString(),
|
|
27
|
+
}}
|
|
28
|
+
className="group block relative"
|
|
29
|
+
>
|
|
30
|
+
<div className="relative z-0 w-full aspect-[4/3]">
|
|
31
|
+
<div className="w-full h-full overflow-hidden rounded-2xl border border-gray-800/50 shadow-2xl">
|
|
32
|
+
<img
|
|
33
|
+
src={motorcycle.image}
|
|
34
|
+
alt={motorcycle.name}
|
|
35
|
+
className="w-full h-full object-cover motorcycle-image group-hover:scale-105 transition-transform duration-500"
|
|
36
|
+
/>
|
|
37
|
+
<div className="absolute inset-0 bg-linear-to-t from-black/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-emerald-500/80 text-white px-4 py-2 rounded-full text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-300 backdrop-blur-sm">
|
|
40
|
+
View Details
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</Link>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div
|
|
47
|
+
className={`w-full md:w-[50%] relative ${index % 2 === 0 ? 'md:-ml-24' : 'md:-mr-24'} md:my-12`}
|
|
48
|
+
>
|
|
49
|
+
<div className="bg-gray-900/40 backdrop-blur-xl rounded-2xl p-8 border border-gray-500/20 shadow-2xl relative z-10">
|
|
50
|
+
<div className="absolute inset-0 rounded-2xl bg-linear-to-br from-white/5 to-white/0"></div>
|
|
51
|
+
<div className="relative z-10">
|
|
52
|
+
<h2 className="text-2xl font-bold mb-3">{motorcycle.name}</h2>
|
|
53
|
+
<p className="text-gray-300 mb-2 font-medium">
|
|
54
|
+
{motorcycle.engineSize}cc •{' '}
|
|
55
|
+
{motorcycle.type.charAt(0).toUpperCase() +
|
|
56
|
+
motorcycle.type.slice(1)}
|
|
57
|
+
</p>
|
|
58
|
+
<p className="text-gray-100 mb-4 leading-relaxed">
|
|
59
|
+
{motorcycle.shortDescription}
|
|
60
|
+
</p>
|
|
61
|
+
<div className="text-2xl font-bold text-emerald-400">
|
|
62
|
+
${motorcycle.price.toLocaleString()}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Link, createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import motorcycles from "../../data/motorcycles";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/motorcycles/$motorcycleId")({
|
|
5
|
+
component: RouteComponent,
|
|
6
|
+
loader: async ({ params }) => {
|
|
7
|
+
const motorcycle = motorcycles.find(
|
|
8
|
+
(motorcycle) => motorcycle.id === +params.motorcycleId
|
|
9
|
+
);
|
|
10
|
+
if (!motorcycle) {
|
|
11
|
+
throw new Error("Motorcycle not found");
|
|
12
|
+
}
|
|
13
|
+
return motorcycle;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function RouteComponent() {
|
|
18
|
+
const motorcycle = Route.useLoaderData();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="relative min-h-[100vh] flex items-center bg-black text-white p-5">
|
|
22
|
+
<div className="relative z-10 w-[60%] bg-gray-900/60 backdrop-blur-md rounded-2xl p-8 border border-gray-800/50 shadow-xl">
|
|
23
|
+
<Link
|
|
24
|
+
to="/"
|
|
25
|
+
className="inline-block mb-4 text-emerald-400 hover:text-emerald-300"
|
|
26
|
+
>
|
|
27
|
+
← Back to all motorcycles
|
|
28
|
+
</Link>
|
|
29
|
+
<h1 className="text-3xl font-bold mb-2">{motorcycle.name}</h1>
|
|
30
|
+
<p className="text-gray-400 mb-2">
|
|
31
|
+
{motorcycle.engineSize}cc •{" "}
|
|
32
|
+
{motorcycle.type.charAt(0).toUpperCase() + motorcycle.type.slice(1)}
|
|
33
|
+
</p>
|
|
34
|
+
<p className="text-gray-300 mb-6">{motorcycle.description}</p>
|
|
35
|
+
<div className="flex items-center justify-between">
|
|
36
|
+
<div className="text-2xl font-bold text-emerald-400">
|
|
37
|
+
${motorcycle.price.toLocaleString()}
|
|
38
|
+
</div>
|
|
39
|
+
<button className="bg-emerald-600 hover:bg-emerald-500 text-white px-6 py-2 rounded-lg transition-colors">
|
|
40
|
+
Add to Cart
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="absolute top-0 right-0 w-[55%] h-full z-0">
|
|
46
|
+
<div className="w-full h-full overflow-hidden rounded-2xl border-4 border-gray-800 shadow-2xl">
|
|
47
|
+
<img
|
|
48
|
+
src={motorcycle.image}
|
|
49
|
+
alt={motorcycle.name}
|
|
50
|
+
className="w-full h-full object-cover motorcycle-image"
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|