@tanstack/cta-framework-react-cra 0.44.3 → 0.46.1
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/{examples/tanchat/assets/src/components/example-AIAssistant.tsx → add-ons/ai/assets/src/components/demo-AIAssistant.tsx} +6 -8
- package/{examples/tanchat/assets/src/components/example-GuitarRecommendation.tsx → add-ons/ai/assets/src/components/demo-GuitarRecommendation.tsx} +2 -2
- package/{examples/tanchat/assets/src/lib/example.ai-hook.ts → add-ons/ai/assets/src/lib/demo-ai-hook.ts} +9 -9
- package/{examples/tanchat/assets/src/lib/example.guitar-tools.ts → add-ons/ai/assets/src/lib/demo-guitar-tools.ts} +1 -1
- package/{examples/tanchat/assets/src/routes/demo/tanchat.tsx → add-ons/ai/assets/src/routes/demo/ai-chat.tsx} +30 -150
- package/{examples/tanchat/assets/src/routes/demo/image.tsx → add-ons/ai/assets/src/routes/demo/ai-image.tsx} +2 -50
- package/add-ons/ai/assets/src/routes/demo/ai-structured.tsx +310 -0
- package/{examples/tanchat/assets/src/routes/demo/api.tanchat.ts → add-ons/ai/assets/src/routes/demo/api.ai.chat.ts} +16 -6
- package/{examples/tanchat/assets/src/routes/demo/api.image.ts → add-ons/ai/assets/src/routes/demo/api.ai.image.ts} +3 -5
- package/add-ons/ai/assets/src/routes/demo/api.ai.structured.ts +136 -0
- package/add-ons/ai/assets/src/routes/demo/api.ai.transcription.ts +89 -0
- package/{examples/tanchat/assets/src/routes/demo/api.tts.ts → add-ons/ai/assets/src/routes/demo/api.ai.tts.ts} +1 -1
- package/{examples/tanchat/assets/src/routes/example.guitars → add-ons/ai/assets/src/routes/demo/guitars}/$guitarId.tsx +3 -2
- package/{examples/tanchat/assets/src/routes/example.guitars → add-ons/ai/assets/src/routes/demo/guitars}/index.tsx +3 -2
- package/add-ons/ai/info.json +46 -0
- package/{examples/tanchat → add-ons/ai}/package.json +1 -1
- 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 +22 -0
- package/examples/events/assets/content/speakers/isabella-martinez.md +22 -0
- package/examples/events/assets/content/speakers/kenji-nakamura.md +22 -0
- package/examples/events/assets/content/speakers/marie-dubois.md +20 -0
- package/examples/events/assets/content/speakers/priya-sharma.md +22 -0
- package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
- package/examples/events/assets/content/talks/french-macaron-mastery.md +39 -0
- package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md +39 -0
- package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md +39 -0
- package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md +36 -0
- package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md +32 -0
- package/examples/events/assets/content/talks/the-science-of-sugar.md +39 -0
- package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md +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/HeroCarousel.tsx +61 -0
- package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
- package/examples/events/assets/src/components/RemyButton.tsx +18 -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/utils.ts +6 -0
- package/examples/events/assets/src/routes/api.remy-chat.ts +121 -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 +194 -0
- package/examples/events/info.json +63 -0
- package/examples/events/package.json +23 -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 +13 -0
- package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
- package/examples/resume/assets/content/jobs/initech-lead.md +29 -0
- package/examples/resume/assets/content/jobs/initrode-senior.md +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/ResumeAssistant.tsx +193 -0
- package/examples/resume/assets/src/components/ResumeAssistantButton.tsx +20 -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 +30 -0
- package/examples/resume/package.json +27 -0
- package/package.json +2 -2
- package/examples/tanchat/assets/src/lib/model-selection.ts +0 -78
- package/examples/tanchat/assets/src/lib/vendor-capabilities.ts +0 -55
- package/examples/tanchat/assets/src/routes/demo/api.available-providers.ts +0 -35
- package/examples/tanchat/assets/src/routes/demo/api.structured.ts +0 -168
- package/examples/tanchat/assets/src/routes/demo/api.transcription.ts +0 -89
- package/examples/tanchat/assets/src/routes/demo/structured.tsx +0 -460
- package/examples/tanchat/info.json +0 -46
- /package/{examples/tanchat → add-ons/ai}/README.md +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/_dot_env.local.append +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-flowers.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-motherboard.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-racing.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-steamer-trunk.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-superhero.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-traveling.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-guitar-video-games.jpg +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/public/example-ukelele-tanstack.jpg +0 -0
- /package/{examples/tanchat/assets/src/data/example-guitars.ts → add-ons/ai/assets/src/data/demo-guitars.ts} +0 -0
- /package/{examples/tanchat/assets/src/hooks/useAudioRecorder.ts → add-ons/ai/assets/src/hooks/demo-useAudioRecorder.ts} +0 -0
- /package/{examples/tanchat/assets/src/hooks/useTTS.ts → add-ons/ai/assets/src/hooks/demo-useTTS.ts} +0 -0
- /package/{examples/tanchat → add-ons/ai}/assets/src/lib/ai-devtools.tsx +0 -0
- /package/{examples/tanchat/assets/src/routes/demo/tanchat.css → add-ons/ai/assets/src/routes/demo/ai-chat.css} +0 -0
- /package/{examples/tanchat → add-ons/ai}/small-logo.svg +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Send, X, Briefcase, UserCheck } from "lucide-react";
|
|
3
|
+
import { Streamdown } from "streamdown";
|
|
4
|
+
import { Store } from "@tanstack/store";
|
|
5
|
+
|
|
6
|
+
import { useResumeChat } from "@/lib/resume-ai-hook";
|
|
7
|
+
import type { ResumeChatMessages } from "@/lib/resume-ai-hook";
|
|
8
|
+
|
|
9
|
+
function Messages({ messages }: { messages: ResumeChatMessages }) {
|
|
10
|
+
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (messagesContainerRef.current) {
|
|
14
|
+
messagesContainerRef.current.scrollTop =
|
|
15
|
+
messagesContainerRef.current.scrollHeight;
|
|
16
|
+
}
|
|
17
|
+
}, [messages]);
|
|
18
|
+
|
|
19
|
+
if (!messages.length) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex-1 flex flex-col items-center justify-center text-slate-300/60 text-sm px-6 py-8">
|
|
22
|
+
<div className="relative mb-4">
|
|
23
|
+
<Briefcase className="w-12 h-12 text-blue-400/40 animate-pulse" />
|
|
24
|
+
<UserCheck className="w-6 h-6 text-purple-400/60 absolute -bottom-1 -right-1" />
|
|
25
|
+
</div>
|
|
26
|
+
<p className="text-center text-slate-200/80 font-medium">
|
|
27
|
+
Welcome, Recruiter!
|
|
28
|
+
</p>
|
|
29
|
+
<p className="text-xs text-slate-300/40 mt-2 text-center max-w-[200px]">
|
|
30
|
+
Ask about skills, experience, or qualifications...
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto">
|
|
38
|
+
{messages.map(({ id, role, parts }) => (
|
|
39
|
+
<div
|
|
40
|
+
key={id}
|
|
41
|
+
className={`py-3 ${
|
|
42
|
+
role === "assistant"
|
|
43
|
+
? "bg-linear-to-r from-blue-500/5 via-purple-500/5 to-slate-500/5"
|
|
44
|
+
: "bg-transparent"
|
|
45
|
+
}`}
|
|
46
|
+
>
|
|
47
|
+
{parts.map((part, index) => {
|
|
48
|
+
if (part.type === "text" && part.content) {
|
|
49
|
+
return (
|
|
50
|
+
<div key={index} className="flex items-start gap-3 px-4">
|
|
51
|
+
{role === "assistant" ? (
|
|
52
|
+
<div className="w-7 h-7 rounded-full bg-linear-to-br from-blue-500 via-purple-500 to-slate-600 flex items-center justify-center text-xs font-bold text-white flex-shrink-0 shadow-lg shadow-blue-500/20">
|
|
53
|
+
<Briefcase className="w-4 h-4" />
|
|
54
|
+
</div>
|
|
55
|
+
) : (
|
|
56
|
+
<div className="w-7 h-7 rounded-full bg-slate-600 flex items-center justify-center text-xs font-medium text-white flex-shrink-0">
|
|
57
|
+
You
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
<div className="flex-1 min-w-0 text-slate-100 prose dark:prose-invert max-w-none prose-sm prose-p:text-slate-100 prose-headings:text-slate-200 prose-strong:text-slate-300">
|
|
61
|
+
<Streamdown>{part.content}</Streamdown>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
})}
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Export store for header control
|
|
75
|
+
export const showResumeAssistant = new Store(false);
|
|
76
|
+
|
|
77
|
+
export default function ResumeAssistant() {
|
|
78
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
79
|
+
const { messages, sendMessage, isLoading } = useResumeChat();
|
|
80
|
+
const [input, setInput] = useState("");
|
|
81
|
+
|
|
82
|
+
// Sync with store for header control
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
return showResumeAssistant.subscribe(() => {
|
|
85
|
+
setIsOpen(showResumeAssistant.state);
|
|
86
|
+
});
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const handleToggle = () => {
|
|
90
|
+
const newState = !isOpen;
|
|
91
|
+
setIsOpen(newState);
|
|
92
|
+
showResumeAssistant.setState(() => newState);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleSend = () => {
|
|
96
|
+
if (input.trim()) {
|
|
97
|
+
sendMessage(input);
|
|
98
|
+
setInput("");
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (!isOpen) return null;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="fixed top-20 right-4 z-[100] w-[400px] h-[520px] rounded-3xl shadow-2xl flex flex-col overflow-hidden border border-blue-500/20 backdrop-blur-xl bg-linear-to-b from-slate-900/98 via-slate-900/95 to-slate-800/98">
|
|
106
|
+
{/* Decorative top gradient */}
|
|
107
|
+
<div className="absolute top-0 left-0 right-0 h-32 bg-linear-to-b from-blue-500/10 via-purple-500/5 to-transparent pointer-events-none" />
|
|
108
|
+
|
|
109
|
+
{/* Header */}
|
|
110
|
+
<div className="relative flex items-center justify-between p-4 border-b border-blue-500/10">
|
|
111
|
+
<div className="flex items-center gap-3">
|
|
112
|
+
<div className="w-10 h-10 rounded-2xl bg-linear-to-br from-blue-500 via-purple-500 to-slate-600 flex items-center justify-center shadow-lg shadow-blue-500/30 rotate-3 hover:rotate-0 transition-transform">
|
|
113
|
+
<Briefcase className="w-5 h-5 text-white" />
|
|
114
|
+
</div>
|
|
115
|
+
<div>
|
|
116
|
+
<h3 className="font-bold text-slate-200 text-base tracking-tight">
|
|
117
|
+
Resume Assistant
|
|
118
|
+
</h3>
|
|
119
|
+
<p className="text-xs text-blue-300/50">Candidate Evaluation AI</p>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<button
|
|
123
|
+
onClick={handleToggle}
|
|
124
|
+
className="text-slate-300/50 hover:text-slate-100 transition-colors p-2 hover:bg-white/5 rounded-xl"
|
|
125
|
+
>
|
|
126
|
+
<X className="w-5 h-5" />
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Messages */}
|
|
131
|
+
<Messages messages={messages} />
|
|
132
|
+
|
|
133
|
+
{/* Loading indicator */}
|
|
134
|
+
{isLoading && (
|
|
135
|
+
<div className="px-4 py-3 border-t border-blue-500/10">
|
|
136
|
+
<div className="flex items-center gap-2 text-blue-400/80 text-xs">
|
|
137
|
+
<div className="flex gap-1">
|
|
138
|
+
<span className="w-2 h-2 bg-blue-400 rounded-full animate-bounce [animation-delay:-0.3s]"></span>
|
|
139
|
+
<span className="w-2 h-2 bg-purple-400 rounded-full animate-bounce [animation-delay:-0.15s]"></span>
|
|
140
|
+
<span className="w-2 h-2 bg-slate-400 rounded-full animate-bounce"></span>
|
|
141
|
+
</div>
|
|
142
|
+
<span className="font-medium">Analyzing experience...</span>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* Input */}
|
|
148
|
+
<div className="relative p-4 border-t border-blue-500/10 bg-slate-900/50">
|
|
149
|
+
<form
|
|
150
|
+
onSubmit={(e) => {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
handleSend();
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<div className="relative">
|
|
156
|
+
<textarea
|
|
157
|
+
value={input}
|
|
158
|
+
onChange={(e) => setInput(e.target.value)}
|
|
159
|
+
placeholder="Ask about skills, experience, or qualifications..."
|
|
160
|
+
disabled={isLoading}
|
|
161
|
+
className="w-full rounded-2xl border border-blue-500/20 bg-slate-800/50 pl-4 pr-12 py-3 text-sm text-slate-100 placeholder-slate-300/30 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-transparent resize-none overflow-hidden disabled:opacity-50 transition-all"
|
|
162
|
+
rows={1}
|
|
163
|
+
style={{ minHeight: "48px", maxHeight: "100px" }}
|
|
164
|
+
onInput={(e) => {
|
|
165
|
+
const target = e.target as HTMLTextAreaElement;
|
|
166
|
+
target.style.height = "auto";
|
|
167
|
+
target.style.height = Math.min(target.scrollHeight, 100) + "px";
|
|
168
|
+
}}
|
|
169
|
+
onKeyDown={(e) => {
|
|
170
|
+
if (
|
|
171
|
+
e.key === "Enter" &&
|
|
172
|
+
!e.shiftKey &&
|
|
173
|
+
input.trim() &&
|
|
174
|
+
!isLoading
|
|
175
|
+
) {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
handleSend();
|
|
178
|
+
}
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
<button
|
|
182
|
+
type="submit"
|
|
183
|
+
disabled={!input.trim() || isLoading}
|
|
184
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 p-2 rounded-xl bg-linear-to-r from-blue-500 to-purple-500 text-white disabled:opacity-30 disabled:bg-gray-600 disabled:from-gray-600 disabled:to-gray-600 transition-all hover:shadow-lg hover:shadow-blue-500/20"
|
|
185
|
+
>
|
|
186
|
+
<Send className="w-4 h-4" />
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</form>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Briefcase, ChevronRight } from "lucide-react";
|
|
2
|
+
import { showResumeAssistant } from "./ResumeAssistant";
|
|
3
|
+
|
|
4
|
+
export default function RemyButton() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="px-2 mb-2 w-full">
|
|
7
|
+
<button
|
|
8
|
+
onClick={() => showResumeAssistant.setState(true)}
|
|
9
|
+
className="w-full flex items-center justify-between px-4 py-2.5 rounded-lg bg-linear-to-r from-blue-500 to-purple-800 text-white hover:opacity-90 transition-opacity"
|
|
10
|
+
aria-label="Open Resume Assistant"
|
|
11
|
+
>
|
|
12
|
+
<div className="flex items-center gap-2">
|
|
13
|
+
<Briefcase size={24} />
|
|
14
|
+
<span className="text-sm">Resume Assistant</span>
|
|
15
|
+
</div>
|
|
16
|
+
<ChevronRight className="w-4 h-4" />
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
14
|
+
secondary:
|
|
15
|
+
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
16
|
+
destructive:
|
|
17
|
+
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
18
|
+
outline:
|
|
19
|
+
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "default",
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
function Badge({
|
|
29
|
+
className,
|
|
30
|
+
variant,
|
|
31
|
+
asChild = false,
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<"span"> &
|
|
34
|
+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
35
|
+
const Comp = asChild ? Slot : "span"
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Comp
|
|
39
|
+
data-slot="badge"
|
|
40
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="card"
|
|
9
|
+
className={cn(
|
|
10
|
+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
data-slot="card-header"
|
|
22
|
+
className={cn(
|
|
23
|
+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
data-slot="card-title"
|
|
35
|
+
className={cn("leading-none font-semibold", className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-slot="card-description"
|
|
45
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-slot="card-action"
|
|
55
|
+
className={cn(
|
|
56
|
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
57
|
+
className
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
data-slot="card-content"
|
|
68
|
+
className={cn("px-6", className)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-slot="card-footer"
|
|
78
|
+
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
Card,
|
|
86
|
+
CardHeader,
|
|
87
|
+
CardFooter,
|
|
88
|
+
CardTitle,
|
|
89
|
+
CardAction,
|
|
90
|
+
CardDescription,
|
|
91
|
+
CardContent,
|
|
92
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
3
|
+
import { CheckIcon } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Checkbox({
|
|
8
|
+
className,
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
11
|
+
return (
|
|
12
|
+
<CheckboxPrimitive.Root
|
|
13
|
+
data-slot="checkbox"
|
|
14
|
+
className={cn(
|
|
15
|
+
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<CheckboxPrimitive.Indicator
|
|
21
|
+
data-slot="checkbox-indicator"
|
|
22
|
+
className="flex items-center justify-center text-current transition-none"
|
|
23
|
+
>
|
|
24
|
+
<CheckIcon className="size-3.5" />
|
|
25
|
+
</CheckboxPrimitive.Indicator>
|
|
26
|
+
</CheckboxPrimitive.Root>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { Checkbox }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function HoverCard({
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
|
11
|
+
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function HoverCardTrigger({
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
|
17
|
+
return (
|
|
18
|
+
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function HoverCardContent({
|
|
23
|
+
className,
|
|
24
|
+
align = "center",
|
|
25
|
+
sideOffset = 4,
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
|
28
|
+
return (
|
|
29
|
+
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
|
30
|
+
<HoverCardPrimitive.Content
|
|
31
|
+
data-slot="hover-card-content"
|
|
32
|
+
align={align}
|
|
33
|
+
sideOffset={sideOffset}
|
|
34
|
+
className={cn(
|
|
35
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
</HoverCardPrimitive.Portal>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Separator({
|
|
7
|
+
className,
|
|
8
|
+
orientation = "horizontal",
|
|
9
|
+
decorative = true,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<SeparatorPrimitive.Root
|
|
14
|
+
data-slot="separator-root"
|
|
15
|
+
decorative={decorative}
|
|
16
|
+
orientation={orientation}
|
|
17
|
+
className={cn(
|
|
18
|
+
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { Separator }
|
|
@@ -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 type inference
|
|
9
|
+
const defaultChatOptions = createChatClientOptions({
|
|
10
|
+
connection: fetchServerSentEvents('/api/resume-chat'),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type ResumeChatMessages = InferChatMessages<typeof defaultChatOptions>
|
|
14
|
+
|
|
15
|
+
export const useResumeChat = () => {
|
|
16
|
+
const chatOptions = createChatClientOptions({
|
|
17
|
+
connection: fetchServerSentEvents('/api/resume-chat'),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
return useChat(chatOptions)
|
|
21
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { allJobs, allEducations } from 'content-collections'
|
|
5
|
+
|
|
6
|
+
// Tool definition for getting jobs by skill
|
|
7
|
+
export const getJobsBySkillToolDef = toolDefinition({
|
|
8
|
+
name: 'getJobsBySkill',
|
|
9
|
+
description:
|
|
10
|
+
'Find all jobs where the candidate used a specific technology or skill. Use this to check if the candidate has experience with particular technologies.',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
skill: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe(
|
|
15
|
+
'The skill or technology to search for (e.g., "React", "TypeScript", "Leadership")',
|
|
16
|
+
),
|
|
17
|
+
}),
|
|
18
|
+
outputSchema: z.array(
|
|
19
|
+
z.object({
|
|
20
|
+
jobTitle: z.string(),
|
|
21
|
+
company: z.string(),
|
|
22
|
+
location: z.string(),
|
|
23
|
+
startDate: z.string(),
|
|
24
|
+
endDate: z.string().optional(),
|
|
25
|
+
summary: z.string(),
|
|
26
|
+
tags: z.array(z.string()),
|
|
27
|
+
content: z.string(),
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Server implementation
|
|
33
|
+
export const getJobsBySkill = getJobsBySkillToolDef.server(({ skill }) => {
|
|
34
|
+
return allJobs.filter((job) =>
|
|
35
|
+
job.tags.some((tag) => tag.toLowerCase().includes(skill.toLowerCase())),
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Tool definition for getting all jobs
|
|
40
|
+
export const getAllJobsToolDef = toolDefinition({
|
|
41
|
+
name: 'getAllJobs',
|
|
42
|
+
description:
|
|
43
|
+
'Get a complete list of all work experience with full details including job titles, companies, dates, summaries, and skills. Use this to get an overview of the candidate\'s entire work history.',
|
|
44
|
+
inputSchema: z.object({}),
|
|
45
|
+
outputSchema: z.array(
|
|
46
|
+
z.object({
|
|
47
|
+
jobTitle: z.string(),
|
|
48
|
+
company: z.string(),
|
|
49
|
+
location: z.string(),
|
|
50
|
+
startDate: z.string(),
|
|
51
|
+
endDate: z.string().optional(),
|
|
52
|
+
summary: z.string(),
|
|
53
|
+
tags: z.array(z.string()),
|
|
54
|
+
content: z.string(),
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Server implementation
|
|
60
|
+
export const getAllJobs = getAllJobsToolDef.server(() => {
|
|
61
|
+
return allJobs.map((job) => ({
|
|
62
|
+
jobTitle: job.jobTitle,
|
|
63
|
+
company: job.company,
|
|
64
|
+
location: job.location,
|
|
65
|
+
startDate: job.startDate,
|
|
66
|
+
endDate: job.endDate,
|
|
67
|
+
summary: job.summary,
|
|
68
|
+
tags: job.tags,
|
|
69
|
+
content: job.content,
|
|
70
|
+
}))
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Tool definition for getting all education
|
|
74
|
+
export const getAllEducationToolDef = toolDefinition({
|
|
75
|
+
name: 'getAllEducation',
|
|
76
|
+
description:
|
|
77
|
+
'Get a complete list of all education history including schools, programs, dates, and skills learned. Use this to understand the candidate\'s educational background.',
|
|
78
|
+
inputSchema: z.object({}),
|
|
79
|
+
outputSchema: z.array(
|
|
80
|
+
z.object({
|
|
81
|
+
school: z.string(),
|
|
82
|
+
summary: z.string(),
|
|
83
|
+
startDate: z.string(),
|
|
84
|
+
endDate: z.string().optional(),
|
|
85
|
+
tags: z.array(z.string()),
|
|
86
|
+
content: z.string(),
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Server implementation
|
|
92
|
+
export const getAllEducation = getAllEducationToolDef.server(() => {
|
|
93
|
+
return allEducations.map((education) => ({
|
|
94
|
+
school: education.school,
|
|
95
|
+
summary: education.summary,
|
|
96
|
+
startDate: education.startDate,
|
|
97
|
+
endDate: education.endDate,
|
|
98
|
+
tags: education.tags,
|
|
99
|
+
content: education.content,
|
|
100
|
+
}))
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Tool definition for searching experience
|
|
104
|
+
export const searchExperienceToolDef = toolDefinition({
|
|
105
|
+
name: 'searchExperience',
|
|
106
|
+
description:
|
|
107
|
+
'Search for jobs by keywords in the job title, company name, summary, or content. Use this to find specific types of experience or roles.',
|
|
108
|
+
inputSchema: z.object({
|
|
109
|
+
query: z
|
|
110
|
+
.string()
|
|
111
|
+
.describe(
|
|
112
|
+
'The search query (e.g., "senior", "lead", "frontend", "startup")',
|
|
113
|
+
),
|
|
114
|
+
}),
|
|
115
|
+
outputSchema: z.array(
|
|
116
|
+
z.object({
|
|
117
|
+
jobTitle: z.string(),
|
|
118
|
+
company: z.string(),
|
|
119
|
+
location: z.string(),
|
|
120
|
+
startDate: z.string(),
|
|
121
|
+
endDate: z.string().optional(),
|
|
122
|
+
summary: z.string(),
|
|
123
|
+
tags: z.array(z.string()),
|
|
124
|
+
matchedIn: z
|
|
125
|
+
.array(z.string())
|
|
126
|
+
.describe('Which fields matched the search'),
|
|
127
|
+
}),
|
|
128
|
+
),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Server implementation
|
|
132
|
+
export const searchExperience = searchExperienceToolDef.server(({ query }) => {
|
|
133
|
+
const lowerQuery = query.toLowerCase()
|
|
134
|
+
|
|
135
|
+
return allJobs
|
|
136
|
+
.map((job) => {
|
|
137
|
+
const matchedIn: string[] = []
|
|
138
|
+
|
|
139
|
+
if (job.jobTitle.toLowerCase().includes(lowerQuery)) {
|
|
140
|
+
matchedIn.push('job title')
|
|
141
|
+
}
|
|
142
|
+
if (job.company.toLowerCase().includes(lowerQuery)) {
|
|
143
|
+
matchedIn.push('company')
|
|
144
|
+
}
|
|
145
|
+
if (job.summary.toLowerCase().includes(lowerQuery)) {
|
|
146
|
+
matchedIn.push('summary')
|
|
147
|
+
}
|
|
148
|
+
if (job.content.toLowerCase().includes(lowerQuery)) {
|
|
149
|
+
matchedIn.push('description')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { job, matchedIn }
|
|
153
|
+
})
|
|
154
|
+
.filter(({ matchedIn }) => matchedIn.length > 0)
|
|
155
|
+
.map(({ job, matchedIn }) => ({
|
|
156
|
+
jobTitle: job.jobTitle,
|
|
157
|
+
company: job.company,
|
|
158
|
+
location: job.location,
|
|
159
|
+
startDate: job.startDate,
|
|
160
|
+
endDate: job.endDate,
|
|
161
|
+
summary: job.summary,
|
|
162
|
+
tags: job.tags,
|
|
163
|
+
matchedIn,
|
|
164
|
+
}))
|
|
165
|
+
})
|