@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,110 @@
|
|
|
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
|
+
getJobsBySkill,
|
|
10
|
+
getAllJobs,
|
|
11
|
+
getAllEducation,
|
|
12
|
+
searchExperience,
|
|
13
|
+
} from '@/lib/resume-tools'
|
|
14
|
+
|
|
15
|
+
export const Route = createFileRoute('/api/resume-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 } = body
|
|
30
|
+
const data = body.data || {}
|
|
31
|
+
|
|
32
|
+
const SYSTEM_PROMPT = `You are a helpful resume assistant helping recruiters and hiring managers evaluate if this candidate is a good fit for their job requirements.
|
|
33
|
+
|
|
34
|
+
CAPABILITIES:
|
|
35
|
+
1. Use getJobsBySkill to find jobs where the candidate used specific technologies or skills
|
|
36
|
+
2. Use getAllJobs to get the candidate's complete work history with all details
|
|
37
|
+
3. Use getAllEducation to get the candidate's educational background
|
|
38
|
+
4. Use searchExperience to search for specific types of roles or experience by keywords
|
|
39
|
+
|
|
40
|
+
INSTRUCTIONS:
|
|
41
|
+
- When asked about specific technologies or skills, use getJobsBySkill to find relevant experience
|
|
42
|
+
- When asked about overall experience or career progression, use getAllJobs
|
|
43
|
+
- When asked about education or training, use getAllEducation
|
|
44
|
+
- When asked about specific types of roles (e.g., "senior", "lead"), use searchExperience
|
|
45
|
+
- Be professional, concise, and helpful in your responses
|
|
46
|
+
- Provide specific details from the resume when available
|
|
47
|
+
- When calculating years of experience, consider the date ranges provided
|
|
48
|
+
- If the candidate has experience with something, highlight specific roles and time periods
|
|
49
|
+
- If the candidate lacks certain experience, be honest but constructive
|
|
50
|
+
|
|
51
|
+
CONTEXT: You are helping evaluate this candidate's qualifications for potential job opportunities.`
|
|
52
|
+
|
|
53
|
+
// Determine the best available provider
|
|
54
|
+
let provider: 'anthropic' | 'openai' | 'gemini' | 'ollama' = data.provider || 'ollama'
|
|
55
|
+
let model: string = data.model || 'mistral:7b'
|
|
56
|
+
|
|
57
|
+
// Use the first available provider with an API key, fallback to ollama
|
|
58
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
59
|
+
provider = 'anthropic'
|
|
60
|
+
model = 'claude-haiku-4-5'
|
|
61
|
+
} else if (process.env.OPENAI_API_KEY) {
|
|
62
|
+
provider = 'openai'
|
|
63
|
+
model = 'gpt-4o'
|
|
64
|
+
} else if (process.env.GEMINI_API_KEY) {
|
|
65
|
+
provider = 'gemini'
|
|
66
|
+
model = 'gemini-2.0-flash-exp'
|
|
67
|
+
}
|
|
68
|
+
// else keep ollama as default
|
|
69
|
+
|
|
70
|
+
// Adapter factory pattern for multi-vendor support
|
|
71
|
+
const adapterConfig = {
|
|
72
|
+
anthropic: () =>
|
|
73
|
+
anthropicText((model || 'claude-haiku-4-5') as any),
|
|
74
|
+
openai: () => openaiText((model || 'gpt-4o') as any),
|
|
75
|
+
gemini: () => geminiText((model || 'gemini-2.0-flash-exp') as any),
|
|
76
|
+
ollama: () => ollamaText((model || 'mistral:7b') as any),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const adapter = adapterConfig[provider]()
|
|
80
|
+
|
|
81
|
+
const stream = chat({
|
|
82
|
+
adapter,
|
|
83
|
+
tools: [getJobsBySkill, getAllJobs, getAllEducation, searchExperience],
|
|
84
|
+
systemPrompts: [SYSTEM_PROMPT],
|
|
85
|
+
agentLoopStrategy: maxIterations(5),
|
|
86
|
+
messages,
|
|
87
|
+
abortController,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return toServerSentEventsResponse(stream, { abortController })
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
console.error('Resume chat error:', error)
|
|
93
|
+
if (error.name === 'AbortError' || abortController.signal.aborted) {
|
|
94
|
+
return new Response(null, { status: 499 })
|
|
95
|
+
}
|
|
96
|
+
return new Response(
|
|
97
|
+
JSON.stringify({
|
|
98
|
+
error: 'Failed to process chat request',
|
|
99
|
+
message: error.message,
|
|
100
|
+
}),
|
|
101
|
+
{
|
|
102
|
+
status: 500,
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
})
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
|
+
import { marked } from 'marked'
|
|
3
|
+
|
|
4
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
5
|
+
import { allJobs, allEducations } from 'content-collections'
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
7
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
|
8
|
+
import { Badge } from '@/components/ui/badge'
|
|
9
|
+
import { Separator } from '@/components/ui/separator'
|
|
10
|
+
import {
|
|
11
|
+
HoverCard,
|
|
12
|
+
HoverCardContent,
|
|
13
|
+
HoverCardTrigger,
|
|
14
|
+
} from '@/components/ui/hover-card'
|
|
15
|
+
|
|
16
|
+
import ResumeAssistant from '@/components/ResumeAssistant'
|
|
17
|
+
|
|
18
|
+
export const Route = createFileRoute('/')({
|
|
19
|
+
component: App,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function App() {
|
|
23
|
+
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
|
24
|
+
|
|
25
|
+
// Get unique tags from all jobs
|
|
26
|
+
const allTags = useMemo(() => {
|
|
27
|
+
const tags = new Set<string>()
|
|
28
|
+
allJobs.forEach((job) => {
|
|
29
|
+
job.tags.forEach((tag) => tags.add(tag))
|
|
30
|
+
})
|
|
31
|
+
return Array.from(tags).sort()
|
|
32
|
+
}, [])
|
|
33
|
+
|
|
34
|
+
// Filter jobs based on selected tags
|
|
35
|
+
const filteredJobs = useMemo(() => {
|
|
36
|
+
if (selectedTags.length === 0) return allJobs
|
|
37
|
+
return allJobs.filter((job) =>
|
|
38
|
+
selectedTags.some((tag) => job.tags.includes(tag)),
|
|
39
|
+
)
|
|
40
|
+
}, [selectedTags])
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<ResumeAssistant />
|
|
45
|
+
<div className="min-h-screen bg-linear-to-b from-gray-50 to-gray-100">
|
|
46
|
+
<div className="flex">
|
|
47
|
+
{/* Sidebar with filters */}
|
|
48
|
+
<div className="w-72 min-h-screen bg-white border-r shadow-sm p-8 sticky top-0">
|
|
49
|
+
<h3 className="text-lg font-semibold mb-6 text-gray-900">
|
|
50
|
+
Skills & Technologies
|
|
51
|
+
</h3>
|
|
52
|
+
<div className="space-y-4">
|
|
53
|
+
{allTags.map((tag) => (
|
|
54
|
+
<div key={tag} className="flex items-center space-x-3 group">
|
|
55
|
+
<Checkbox
|
|
56
|
+
id={tag}
|
|
57
|
+
checked={selectedTags.includes(tag)}
|
|
58
|
+
onCheckedChange={(checked) => {
|
|
59
|
+
if (checked) {
|
|
60
|
+
setSelectedTags([...selectedTags, tag])
|
|
61
|
+
} else {
|
|
62
|
+
setSelectedTags(selectedTags.filter((t) => t !== tag))
|
|
63
|
+
}
|
|
64
|
+
}}
|
|
65
|
+
className="data-[state=checked]:bg-blue-600"
|
|
66
|
+
/>
|
|
67
|
+
<label
|
|
68
|
+
htmlFor={tag}
|
|
69
|
+
className="text-sm font-medium leading-none text-gray-700 group-hover:text-gray-900 transition-colors cursor-pointer"
|
|
70
|
+
>
|
|
71
|
+
{tag}
|
|
72
|
+
</label>
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Main content */}
|
|
79
|
+
<div className="flex-1 p-8 lg:p-12">
|
|
80
|
+
<div className="max-w-4xl mx-auto space-y-12">
|
|
81
|
+
<div className="text-center space-y-4">
|
|
82
|
+
<h1 className="text-5xl font-bold bg-linear-to-r from-gray-900 via-gray-800 to-gray-900 bg-clip-text text-transparent">
|
|
83
|
+
My Resume
|
|
84
|
+
</h1>
|
|
85
|
+
<p className="text-gray-600 text-lg">
|
|
86
|
+
Professional Experience & Education
|
|
87
|
+
</p>
|
|
88
|
+
<Separator className="mt-8" />
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Career Summary */}
|
|
92
|
+
<Card className="border-0 shadow-lg bg-white/50 backdrop-blur-sm">
|
|
93
|
+
<CardHeader>
|
|
94
|
+
<CardTitle className="text-2xl text-gray-900">
|
|
95
|
+
Career Summary
|
|
96
|
+
</CardTitle>
|
|
97
|
+
</CardHeader>
|
|
98
|
+
<CardContent>
|
|
99
|
+
<div className="flex items-center gap-8">
|
|
100
|
+
<p className="text-gray-700 flex-1 leading-relaxed">
|
|
101
|
+
I am a passionate and driven professional seeking
|
|
102
|
+
opportunities that will leverage my extensive experience
|
|
103
|
+
in frontend development while providing continuous growth
|
|
104
|
+
and learning opportunities. My goal is to contribute to
|
|
105
|
+
innovative projects that challenge me to expand my skill
|
|
106
|
+
set and make meaningful impacts through technology.
|
|
107
|
+
</p>
|
|
108
|
+
<img
|
|
109
|
+
src="/headshot-on-white.jpg"
|
|
110
|
+
alt="Professional headshot"
|
|
111
|
+
className="w-44 h-52 rounded-2xl object-cover shadow-md transition-transform hover:scale-105"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</CardContent>
|
|
115
|
+
</Card>
|
|
116
|
+
|
|
117
|
+
{/* Work Experience */}
|
|
118
|
+
<section className="space-y-6">
|
|
119
|
+
<h2 className="text-3xl font-semibold text-gray-900">
|
|
120
|
+
Work Experience
|
|
121
|
+
</h2>
|
|
122
|
+
<div className="space-y-6">
|
|
123
|
+
{filteredJobs.map((job) => (
|
|
124
|
+
<Card
|
|
125
|
+
key={job.jobTitle}
|
|
126
|
+
className="border-0 shadow-md hover:shadow-lg transition-shadow"
|
|
127
|
+
>
|
|
128
|
+
<CardHeader>
|
|
129
|
+
<div className="flex justify-between items-start">
|
|
130
|
+
<div className="space-y-2">
|
|
131
|
+
<CardTitle className="text-xl text-gray-900">
|
|
132
|
+
{job.jobTitle}
|
|
133
|
+
</CardTitle>
|
|
134
|
+
<p className="text-blue-600 font-medium">
|
|
135
|
+
{job.company} - {job.location}
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
<Badge variant="secondary" className="text-sm">
|
|
139
|
+
{job.startDate} - {job.endDate || 'Present'}
|
|
140
|
+
</Badge>
|
|
141
|
+
</div>
|
|
142
|
+
</CardHeader>
|
|
143
|
+
<CardContent>
|
|
144
|
+
<p className="text-gray-700 mb-6 leading-relaxed">
|
|
145
|
+
{job.summary}
|
|
146
|
+
</p>
|
|
147
|
+
<div className="flex flex-wrap gap-2">
|
|
148
|
+
{job.tags.map((tag) => (
|
|
149
|
+
<HoverCard key={tag}>
|
|
150
|
+
<HoverCardTrigger>
|
|
151
|
+
<Badge
|
|
152
|
+
variant="outline"
|
|
153
|
+
className="hover:bg-gray-100 transition-colors cursor-pointer"
|
|
154
|
+
>
|
|
155
|
+
{tag}
|
|
156
|
+
</Badge>
|
|
157
|
+
</HoverCardTrigger>
|
|
158
|
+
<HoverCardContent className="w-64">
|
|
159
|
+
<p className="text-sm text-gray-600">
|
|
160
|
+
Experience with {tag} in professional
|
|
161
|
+
development
|
|
162
|
+
</p>
|
|
163
|
+
</HoverCardContent>
|
|
164
|
+
</HoverCard>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
{job.content && (
|
|
168
|
+
<div
|
|
169
|
+
className="mt-6 text-gray-700 prose prose-sm max-w-none"
|
|
170
|
+
dangerouslySetInnerHTML={{
|
|
171
|
+
__html: marked(job.content),
|
|
172
|
+
}}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
</CardContent>
|
|
176
|
+
</Card>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
</section>
|
|
180
|
+
|
|
181
|
+
{/* Education */}
|
|
182
|
+
<section className="space-y-6">
|
|
183
|
+
<h2 className="text-3xl font-semibold text-gray-900">
|
|
184
|
+
Education
|
|
185
|
+
</h2>
|
|
186
|
+
<div className="space-y-6">
|
|
187
|
+
{allEducations.map((education) => (
|
|
188
|
+
<Card
|
|
189
|
+
key={education.school}
|
|
190
|
+
className="border-0 shadow-md hover:shadow-lg transition-shadow"
|
|
191
|
+
>
|
|
192
|
+
<CardHeader>
|
|
193
|
+
<CardTitle className="text-xl text-gray-900">
|
|
194
|
+
{education.school}
|
|
195
|
+
</CardTitle>
|
|
196
|
+
</CardHeader>
|
|
197
|
+
<CardContent>
|
|
198
|
+
<p className="text-gray-700 leading-relaxed">
|
|
199
|
+
{education.summary}
|
|
200
|
+
</p>
|
|
201
|
+
{education.content && (
|
|
202
|
+
<div
|
|
203
|
+
className="mt-6 text-gray-700 prose prose-sm max-w-none"
|
|
204
|
+
dangerouslySetInnerHTML={{
|
|
205
|
+
__html: marked(education.content),
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
209
|
+
</CardContent>
|
|
210
|
+
</Card>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
</section>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
@@ -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,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Resume",
|
|
3
|
+
"description": "Professional resume template with content-collections and shadcn UI components 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": "ResumeHome"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"integrations": [
|
|
17
|
+
{
|
|
18
|
+
"type": "header-user",
|
|
19
|
+
"path": "src/components/ResumeAssistantButton",
|
|
20
|
+
"jsName": "ResumeAssistantButton"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"type": "vite-plugin",
|
|
24
|
+
"import": "import contentCollections from '@content-collections/vite'",
|
|
25
|
+
"code": "contentCollections()"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"dependsOn": [],
|
|
29
|
+
"variables": []
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"@radix-ui/react-checkbox": "^1.2.2",
|
|
4
|
+
"@radix-ui/react-hover-card": "^1.1.10",
|
|
5
|
+
"@radix-ui/react-separator": "^1.1.4",
|
|
6
|
+
"@radix-ui/react-slot": "^1.2.0",
|
|
7
|
+
"@tanstack/ai": "latest",
|
|
8
|
+
"@tanstack/ai-anthropic": "latest",
|
|
9
|
+
"@tanstack/ai-client": "latest",
|
|
10
|
+
"@tanstack/ai-gemini": "latest",
|
|
11
|
+
"@tanstack/ai-ollama": "latest",
|
|
12
|
+
"@tanstack/ai-openai": "latest",
|
|
13
|
+
"@tanstack/ai-react": "latest",
|
|
14
|
+
"@tanstack/store": "latest",
|
|
15
|
+
"class-variance-authority": "^0.7.1",
|
|
16
|
+
"clsx": "^2.1.1",
|
|
17
|
+
"marked": "^15.0.8",
|
|
18
|
+
"streamdown": "^1.6.5",
|
|
19
|
+
"tailwind-merge": "^3.0.2",
|
|
20
|
+
"tailwindcss-animate": "^1.0.7",
|
|
21
|
+
"zod": "^4.3.5"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@content-collections/core": "^0.13.1",
|
|
25
|
+
"@content-collections/vite": "^0.2.8"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/cta-framework-react-cra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.1",
|
|
4
4
|
"description": "CTA Framework for React (Create React App)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"author": "Jack Herrington <jherr@pobox.com>",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@tanstack/cta-engine": "0.
|
|
26
|
+
"@tanstack/cta-engine": "0.46.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^24.6.0",
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
export type Provider = 'openai' | 'anthropic' | 'gemini' | 'ollama'
|
|
2
|
-
|
|
3
|
-
export interface ModelOption {
|
|
4
|
-
provider: Provider
|
|
5
|
-
model: string
|
|
6
|
-
label: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const MODEL_OPTIONS: Array<ModelOption> = [
|
|
10
|
-
// OpenAI models
|
|
11
|
-
{ provider: 'openai', model: 'gpt-4o', label: 'OpenAI - GPT-4o' },
|
|
12
|
-
{ provider: 'openai', model: 'gpt-4o-mini', label: 'OpenAI - GPT-4o Mini' },
|
|
13
|
-
|
|
14
|
-
// Anthropic models
|
|
15
|
-
{
|
|
16
|
-
provider: 'anthropic',
|
|
17
|
-
model: 'claude-haiku-4-5',
|
|
18
|
-
label: 'Anthropic - Claude Haiku 4.5',
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
provider: 'anthropic',
|
|
22
|
-
model: 'claude-sonnet-4-5-20250929',
|
|
23
|
-
label: 'Anthropic - Claude Sonnet 4.5',
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
// Gemini models
|
|
27
|
-
{
|
|
28
|
-
provider: 'gemini',
|
|
29
|
-
model: 'gemini-2.0-flash-exp',
|
|
30
|
-
label: 'Gemini - 2.0 Flash',
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
// Ollama models
|
|
34
|
-
{ provider: 'ollama', model: 'mistral:7b', label: 'Ollama - Mistral 7B' },
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
const STORAGE_KEY = 'tanstack-ai-model-preference'
|
|
38
|
-
|
|
39
|
-
export function getStoredModelPreference(): ModelOption | null {
|
|
40
|
-
if (typeof window === 'undefined') return null
|
|
41
|
-
try {
|
|
42
|
-
const stored = localStorage.getItem(STORAGE_KEY)
|
|
43
|
-
if (stored) {
|
|
44
|
-
const parsed = JSON.parse(stored)
|
|
45
|
-
// Validate that the stored option still exists in MODEL_OPTIONS
|
|
46
|
-
const found = MODEL_OPTIONS.find(
|
|
47
|
-
(o) => o.provider === parsed.provider && o.model === parsed.model,
|
|
48
|
-
)
|
|
49
|
-
if (found) return found
|
|
50
|
-
}
|
|
51
|
-
} catch {
|
|
52
|
-
// Ignore storage errors
|
|
53
|
-
}
|
|
54
|
-
return null
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function setStoredModelPreference(option: ModelOption): void {
|
|
58
|
-
if (typeof window === 'undefined') return
|
|
59
|
-
try {
|
|
60
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(option))
|
|
61
|
-
} catch {
|
|
62
|
-
// Ignore storage errors
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function getDefaultModelOption(): ModelOption {
|
|
67
|
-
return getStoredModelPreference() || MODEL_OPTIONS[0]
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function getModelOptionsForProvider(provider: Provider): ModelOption[] {
|
|
71
|
-
return MODEL_OPTIONS.filter((o) => o.provider === provider)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function getAvailableModelOptions(
|
|
75
|
-
availableProviders: Provider[],
|
|
76
|
-
): ModelOption[] {
|
|
77
|
-
return MODEL_OPTIONS.filter((o) => availableProviders.includes(o.provider))
|
|
78
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { Provider } from './model-selection'
|
|
2
|
-
|
|
3
|
-
export interface VendorCapabilities {
|
|
4
|
-
chat: boolean
|
|
5
|
-
structured: boolean
|
|
6
|
-
image: boolean
|
|
7
|
-
transcription: boolean
|
|
8
|
-
tts: boolean
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const VENDOR_CAPABILITIES: Record<Provider, VendorCapabilities> = {
|
|
12
|
-
openai: {
|
|
13
|
-
chat: true,
|
|
14
|
-
structured: true,
|
|
15
|
-
image: true,
|
|
16
|
-
transcription: true,
|
|
17
|
-
tts: true,
|
|
18
|
-
},
|
|
19
|
-
anthropic: {
|
|
20
|
-
chat: true,
|
|
21
|
-
structured: true,
|
|
22
|
-
image: false,
|
|
23
|
-
transcription: false,
|
|
24
|
-
tts: false,
|
|
25
|
-
},
|
|
26
|
-
gemini: {
|
|
27
|
-
chat: true,
|
|
28
|
-
structured: true,
|
|
29
|
-
image: false,
|
|
30
|
-
transcription: false,
|
|
31
|
-
tts: false,
|
|
32
|
-
},
|
|
33
|
-
ollama: {
|
|
34
|
-
chat: true,
|
|
35
|
-
structured: true,
|
|
36
|
-
image: false,
|
|
37
|
-
transcription: false,
|
|
38
|
-
tts: false,
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function hasCapability(
|
|
43
|
-
provider: Provider,
|
|
44
|
-
capability: keyof VendorCapabilities,
|
|
45
|
-
): boolean {
|
|
46
|
-
return VENDOR_CAPABILITIES[provider]?.[capability] ?? false
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function getProvidersWithCapability(
|
|
50
|
-
capability: keyof VendorCapabilities,
|
|
51
|
-
): Provider[] {
|
|
52
|
-
return (Object.keys(VENDOR_CAPABILITIES) as Provider[]).filter(
|
|
53
|
-
(provider) => VENDOR_CAPABILITIES[provider][capability],
|
|
54
|
-
)
|
|
55
|
-
}
|