Add bio field and enhance smart assignment with bio matching

- Add bio field to User model for judge/mentor profile descriptions
- Add bio step to onboarding wizard (optional step with 500 char limit)
- Enhance smart assignment to match judge bio against project description
  - Uses keyword extraction and Jaccard-like similarity scoring
  - Only applies if judge has a bio (no penalty for empty bio)
  - Max 15 points for bio match on top of existing scoring
- Fix geographic distribution query to use round relation for programId
- Update score breakdown: tags (40), bio (15), workload (25), country (15)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 15:27:28 +01:00
parent 3a7177c652
commit ff26769ce1
5 changed files with 205 additions and 25 deletions

View File

@@ -25,6 +25,7 @@ import {
import { toast } from 'sonner'
import { ExpertiseSelect } from '@/components/shared/expertise-select'
import { AvatarUpload } from '@/components/shared/avatar-upload'
import { Textarea } from '@/components/ui/textarea'
import {
User,
Phone,
@@ -36,9 +37,10 @@ import {
ArrowLeft,
Camera,
Globe,
FileText,
} from 'lucide-react'
type Step = 'name' | 'photo' | 'country' | 'phone' | 'tags' | 'preferences' | 'complete'
type Step = 'name' | 'photo' | 'country' | 'bio' | 'phone' | 'tags' | 'preferences' | 'complete'
export default function OnboardingPage() {
const router = useRouter()
@@ -48,6 +50,7 @@ export default function OnboardingPage() {
// Form state
const [name, setName] = useState('')
const [country, setCountry] = useState('')
const [bio, setBio] = useState('')
const [phoneNumber, setPhoneNumber] = useState('')
const [expertiseTags, setExpertiseTags] = useState<string[]>([])
const [lockedTags, setLockedTags] = useState<string[]>([])
@@ -70,6 +73,10 @@ export default function OnboardingPage() {
if (userData.country) {
setCountry(userData.country)
}
// Pre-fill bio if available
if (userData.bio) {
setBio(userData.bio)
}
// Pre-fill phone if available
if (userData.phoneNumber) {
setPhoneNumber(userData.phoneNumber)
@@ -96,10 +103,10 @@ export default function OnboardingPage() {
// Dynamic steps based on WhatsApp availability
const steps: Step[] = useMemo(() => {
if (whatsappEnabled) {
return ['name', 'photo', 'country', 'phone', 'tags', 'preferences', 'complete']
return ['name', 'photo', 'country', 'bio', 'phone', 'tags', 'preferences', 'complete']
}
// Skip phone step if WhatsApp is disabled
return ['name', 'photo', 'country', 'tags', 'preferences', 'complete']
return ['name', 'photo', 'country', 'bio', 'tags', 'preferences', 'complete']
}, [whatsappEnabled])
const currentIndex = steps.indexOf(step)
@@ -128,6 +135,7 @@ export default function OnboardingPage() {
await completeOnboarding.mutateAsync({
name,
country: country || undefined,
bio: bio || undefined,
phoneNumber: phoneNumber || undefined,
expertiseTags,
notificationPreference,
@@ -302,7 +310,49 @@ export default function OnboardingPage() {
</>
)}
{/* Step 4: Phone (only if WhatsApp enabled) */}
{/* Step 4: Bio */}
{step === 'bio' && (
<>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5 text-primary" />
About You
</CardTitle>
<CardDescription>
Tell us a bit about yourself and your expertise. This helps us match you with relevant projects. (Optional)
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="bio">Bio</Label>
<Textarea
id="bio"
value={bio}
onChange={(e) => setBio(e.target.value)}
placeholder="e.g., Marine biologist with 10 years experience in coral reef conservation and ocean acidification research..."
rows={4}
maxLength={500}
/>
<p className="text-xs text-muted-foreground text-right">
{bio.length}/500 characters
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={goBack} className="flex-1">
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
<Button onClick={goNext} className="flex-1">
{bio ? 'Continue' : 'Skip for now'}
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</>
)}
{/* Step 5: Phone (only if WhatsApp enabled) */}
{step === 'phone' && whatsappEnabled && (
<>
<CardHeader>
@@ -343,7 +393,7 @@ export default function OnboardingPage() {
</>
)}
{/* Step 5: Tags */}
{/* Step 6: Tags */}
{step === 'tags' && (
<>
<CardHeader>
@@ -377,7 +427,7 @@ export default function OnboardingPage() {
</>
)}
{/* Step 6: Preferences */}
{/* Step 7: Preferences */}
{step === 'preferences' && (
<>
<CardHeader>
@@ -434,6 +484,12 @@ export default function OnboardingPage() {
<span className="text-muted-foreground">Country:</span> {country}
</p>
)}
{bio && (
<p>
<span className="text-muted-foreground">Bio:</span>{' '}
{bio.length > 50 ? `${bio.substring(0, 50)}...` : bio}
</p>
)}
{whatsappEnabled && phoneNumber && (
<p>
<span className="text-muted-foreground">Phone:</span>{' '}