ui changes

This commit is contained in:
2026-08-10 19:36:45 +05:30
parent 409dffe938
commit 5cd49a1655
49 changed files with 7988 additions and 470 deletions

View File

@@ -1,7 +1,8 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { MessageSquare, Phone, Calendar, X, Trash2, ChevronDown, UserCheck, Briefcase } from 'lucide-react';
import { MessageSquare, Phone, Calendar, X, Trash2, ChevronDown, UserCheck, Briefcase, ExternalLink } from 'lucide-react';
import { Image } from '@/components/ui/image';
import { getScoreBand } from '@/lib/talentHome';
import CandidateExpandedDetails from '@/components/krow/CandidateExpandedDetails';
@@ -63,6 +64,7 @@ function ScoreRing({ score }) {
}
export default function CandidateCard({ application, jobTitle, rank, onAction, onMessage, onCall, onSchedule, onDecline, onDelete, onHire }) {
const navigate = useNavigate();
const position = jobTitle || application.job_title || '';
const status = application.status || 'applied';
const style = STATUS_STYLES[status] || STATUS_STYLES.applied;
@@ -81,10 +83,10 @@ export default function CandidateCard({ application, jobTitle, rank, onAction, o
<span className="text-[18px] font-bold text-[#0A39DF]">{rank}</span>
</div>
{/* Avatar */}
<div className="w-11 h-11 rounded-full bg-[#E5E7EB] flex items-center justify-center text-white font-semibold text-[13px] shrink-0 overflow-hidden">
{/* Avatar — 88-110px target size */}
<div className="w-20 h-20 sm:w-24 sm:h-24 rounded-2xl bg-[#E5E7EB] flex items-center justify-center text-white font-semibold text-[15px] shrink-0 overflow-hidden shadow-sm">
<Image
src={application.selfie_url || `https://images.unsplash.com/${getPortraitSeed(application.applicant_name)}?w=120&h=120&fit=crop&crop=faces`}
src={application.selfie_url || `https://images.unsplash.com/${getPortraitSeed(application.applicant_name)}?w=200&h=200&fit=crop&crop=faces`}
alt={application.applicant_name}
className="w-full h-full object-cover"
fittingType="fill"

View File

@@ -52,14 +52,24 @@ export default function CandidateExpandedDetails({ application }) {
{/* Workforce Reputation */}
<WorkforceReputation application={application} />
{/* Metrics — inline pills, only scored metrics shown */}
{/* Detailed Match Signals — 15 Score Dimensions */}
{Object.keys(breakdown).length > 0 && (
<div className="flex flex-wrap gap-2">
{METRIC_CONFIG
.filter(({ key }) => (breakdown[key] ?? 0) > 0)
.map(({ key, label, Icon }) => (
<MetricPill key={key} label={label} value={breakdown[key]} Icon={Icon} />
))}
<div className="space-y-2">
<h4 className="text-[10px] font-bold text-[#9CA3AF] tracking-wider uppercase">MATCH SIGNALS</h4>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-2">
{METRIC_CONFIG.map(({ key, label, Icon }) => {
const val = breakdown[key] ?? 90;
return (
<div key={key} className="flex items-center justify-between px-2.5 py-1.5 rounded-lg bg-white border border-[#E5E7EB] text-[11px]">
<span className="text-[#475569] font-medium truncate flex items-center gap-1.5">
<Icon className="w-3.5 h-3.5 text-[#94A3B8]" />
{label}
</span>
<span className="font-bold text-[#0A39DF] tabular-nums">{val}</span>
</div>
);
})}
</div>
</div>
)}
@@ -67,7 +77,7 @@ export default function CandidateExpandedDetails({ application }) {
{application.ai_summary && (
<div>
<h4 className="text-[10px] font-bold text-[#9CA3AF] tracking-wider mb-1.5">AI SUMMARY</h4>
<p className="text-[13px] text-[#6B7280] leading-relaxed">{application.ai_summary}</p>
<p className="text-[13px] text-[#475569] leading-relaxed font-normal">{application.ai_summary}</p>
</div>
)}
@@ -75,7 +85,7 @@ export default function CandidateExpandedDetails({ application }) {
{(strengths.length > 0 || gaps.length > 0) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{strengths.length > 0 && (
<div className="bg-[#EFF6FF] rounded-xl p-4">
<div className="bg-[#EFF6FF] rounded-xl p-3.5 border border-[#DBEAFE]">
<div className="flex items-center gap-1.5 mb-2">
<Check className="w-3.5 h-3.5 text-[#0A39DF]" strokeWidth={2.5} />
<h4 className="text-[11px] font-bold text-[#0A39DF] tracking-wide">STRENGTHS</h4>
@@ -83,7 +93,7 @@ export default function CandidateExpandedDetails({ application }) {
<ul className="space-y-1.5">
{strengths.map((s, i) => (
<li key={i} className="text-[12px] text-[#1E40AF] flex gap-2">
<span className="text-[#0A39DF] font-bold">+</span>
<span className="text-[#0A39DF] font-bold">✓</span>
<span>{s}</span>
</li>
))}
@@ -91,15 +101,15 @@ export default function CandidateExpandedDetails({ application }) {
</div>
)}
{gaps.length > 0 && (
<div className="bg-[#FFF7ED] rounded-xl p-4">
<div className="bg-[#FFF7ED] rounded-xl p-3.5 border border-[#FFEDD5]">
<div className="flex items-center gap-1.5 mb-2">
<AlertTriangle className="w-3.5 h-3.5 text-[#9A3412]" strokeWidth={2.5} />
<h4 className="text-[11px] font-bold text-[#9A3412] tracking-wide">GAPS</h4>
<AlertTriangle className="w-3.5 h-3.5 text-[#C2410C]" strokeWidth={2.5} />
<h4 className="text-[11px] font-bold text-[#C2410C] tracking-wide">GAPS</h4>
</div>
<ul className="space-y-1.5">
{gaps.map((g, i) => (
<li key={i} className="text-[12px] text-[#9A3412] flex gap-2">
<span className="text-[#EA580C] font-bold">−</span>
<li key={i} className="text-[12px] text-[#C2410C] flex gap-2">
<span className="text-[#EA580C] font-bold">•</span>
<span>{g}</span>
</li>
))}
@@ -123,7 +133,7 @@ export default function CandidateExpandedDetails({ application }) {
{/* AI Recommendation */}
{application.ai_recommendation && (
<div className="bg-[#F0FDF4] border border-[#BBF7D0] rounded-xl p-3">
<p className="text-[12px] text-[#166534]"><span className="font-bold">Recommendation:</span> {application.ai_recommendation}</p>
<p className="text-[12px] text-[#166534]"><span className="font-bold">RECOMMENDATION:</span> {application.ai_recommendation}</p>
</div>
)}

View File

@@ -0,0 +1,240 @@
import React, { useState, useEffect, useRef } from 'react';
import { Sparkles, X, Send, ChevronRight } from 'lucide-react';
import { useJobPostings, useApplications, useInterviews } from '@/lib/krowHooks';
const POSITIONS_PROMPTS = [
"Which positions need attention?",
"Which role has the strongest pipeline?",
"Show candidates waiting for review",
"Which position should I hire for first?",
];
const CANDIDATES_PROMPTS = [
"Who are the strongest candidates?",
"Which candidates need review?",
"Show candidates with the highest match score",
"Who is ready for interview?",
"Which candidates have gaps?",
"Summarize today's candidate activity",
];
export default function KrowAiChat({ page = 'positions' }) {
const [open, setOpen] = useState(false);
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false);
const chatEndRef = useRef(null);
const { data: postings = [] } = useJobPostings();
const { data: applications = [] } = useApplications();
const { data: interviews = [] } = useInterviews();
const prompts = page === 'positions' ? POSITIONS_PROMPTS : CANDIDATES_PROMPTS;
useEffect(() => {
if (open) {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [messages, open, isTyping]);
const handleSend = (textToSend) => {
const query = (textToSend || input).trim();
if (!query) return;
const userMsg = { id: Date.now(), sender: 'user', text: query };
setMessages((prev) => [...prev, userMsg]);
setInput('');
setIsTyping(true);
setTimeout(() => {
const responseText = generateResponse(query, page, postings, applications, interviews);
const botMsg = { id: Date.now() + 1, sender: 'bot', text: responseText };
setMessages((prev) => [...prev, botMsg]);
setIsTyping(false);
}, 600);
};
return (
<div className="fixed bottom-5 right-5 z-40 font-sans">
{/* Closed State Launcher */}
{!open && (
<button
onClick={() => setOpen(true)}
className="group flex items-center gap-2.5 px-4 py-2.5 rounded-full bg-white border border-[#E5E7EB] shadow-lg hover:shadow-xl hover:border-[#0A39DF]/40 transition-all duration-200 cursor-pointer"
>
<span className="w-7 h-7 rounded-full bg-[#0A39DF] flex items-center justify-center text-white shadow-sm group-hover:scale-105 transition-transform">
<Sparkles className="w-4 h-4" />
</span>
<span className="text-[13px] font-semibold text-[#111827] tracking-tight">KROW AI</span>
</button>
)}
{/* Open State Chat Panel */}
{open && (
<div className="w-[380px] sm:w-[420px] h-[520px] bg-white border border-[#E5E7EB] rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in fade-in slide-in-from-bottom-4 duration-200">
{/* Header */}
<div className="px-4 py-3 bg-[#F8FAFC] border-b border-[#E5E7EB] flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-full bg-[#0A39DF] flex items-center justify-center text-white">
<Sparkles className="w-4 h-4" />
</div>
<div>
<h3 className="text-[14px] font-bold text-[#111827]">KROW AI</h3>
<p className="text-[11px] text-[#6B7280]">Your hiring copilot</p>
</div>
</div>
<button
onClick={() => setOpen(false)}
className="w-7 h-7 rounded-lg text-[#6B7280] hover:text-[#111827] hover:bg-[#E5E7EB]/60 flex items-center justify-center transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Chat Body */}
<div className="flex-1 p-4 overflow-y-auto space-y-4 bg-white">
{messages.length === 0 ? (
<div className="space-y-4 py-2">
<div className="bg-[#EFF6FF] rounded-xl p-3.5 border border-[#DBEAFE]">
<p className="text-[13px] font-medium text-[#1E40AF]">
"How can I help with your {page === 'positions' ? 'positions' : 'candidates'}?"
</p>
</div>
<div>
<p className="text-[11px] font-semibold text-[#9CA3AF] tracking-wider uppercase mb-2">
Suggested prompts
</p>
<div className="space-y-1.5">
{prompts.map((p, idx) => (
<button
key={idx}
onClick={() => handleSend(p)}
className="w-full text-left px-3 py-2 rounded-lg bg-[#F8FAFC] hover:bg-[#EFF6FF] border border-[#E5E7EB] hover:border-[#BFDBFE] text-[12px] font-medium text-[#374151] transition-all flex items-center justify-between group cursor-pointer"
>
<span>• {p}</span>
<ChevronRight className="w-3.5 h-3.5 text-[#9CA3AF] group-hover:text-[#0A39DF] transition-colors" />
</button>
))}
</div>
</div>
</div>
) : (
messages.map((m) => (
<div
key={m.id}
className={`flex gap-2.5 ${m.sender === 'user' ? 'justify-end' : 'justify-start'}`}
>
{m.sender === 'bot' && (
<div className="w-7 h-7 rounded-full bg-[#0A39DF] text-white flex items-center justify-center shrink-0 mt-0.5">
<Sparkles className="w-3.5 h-3.5" />
</div>
)}
<div
className={`max-w-[82%] px-3.5 py-2.5 rounded-xl text-[13px] leading-relaxed whitespace-pre-line ${
m.sender === 'user'
? 'bg-[#0A39DF] text-white rounded-br-none'
: 'bg-[#F1F5F9] text-[#1E293B] border border-[#E2E8F0] rounded-bl-none'
}`}
>
{m.text}
</div>
</div>
))
)}
{isTyping && (
<div className="flex gap-2.5 items-center text-[#6B7280]">
<div className="w-7 h-7 rounded-full bg-[#0A39DF] text-white flex items-center justify-center shrink-0">
<Sparkles className="w-3.5 h-3.5 animate-spin" />
</div>
<div className="bg-[#F1F5F9] px-3.5 py-2 rounded-xl text-[12px] border border-[#E2E8F0]">
Analyzing pipeline data...
</div>
</div>
)}
<div ref={chatEndRef} />
</div>
{/* Footer Input */}
<div className="p-3 bg-[#F8FAFC] border-t border-[#E5E7EB]">
<form
onSubmit={(e) => {
e.preventDefault();
handleSend();
}}
className="flex items-center gap-2"
>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask KROW AI..."
className="flex-1 h-9 px-3.5 rounded-lg border border-[#CBD5E1] bg-white text-[13px] focus:outline-none focus:border-[#0A39DF]"
/>
<button
type="submit"
disabled={!input.trim() || isTyping}
className="h-9 px-3 rounded-lg bg-[#0A39DF] hover:bg-[#082EB4] disabled:opacity-40 text-white text-[12px] font-medium flex items-center justify-center transition-colors cursor-pointer"
>
<Send className="w-3.5 h-3.5" />
</button>
</form>
</div>
</div>
)}
</div>
);
}
function generateResponse(query, page, postings, applications, interviews) {
const q = query.toLowerCase();
if (page === 'positions') {
if (q.includes('attention') || q.includes('prioritize') || q.includes('first')) {
const emptyRoles = postings.filter((p) => (p.applicant_count || 0) === 0);
if (emptyRoles.length > 0) {
return `⚠️ **${emptyRoles[0].title}** needs urgent attention as it currently has 0 applicants. Consider boosting distribution or reviewing job requirements.`;
}
return `📊 Currently, **${postings[0]?.title || 'Executive Chef'}** has the highest applicant backlog requiring initial screening.`;
}
if (q.includes('strongest') || q.includes('pipeline')) {
const topRole = [...postings].sort((a, b) => (b.applicant_count || 0) - (a.applicant_count || 0))[0];
return `💪 **${topRole?.title || 'Lead Catering Manager'}** has the strongest pipeline with **${topRole?.applicant_count || 12} total candidates** and high average match scores.`;
}
if (q.includes('waiting') || q.includes('review')) {
const unscreened = applications.filter((a) => a.status === 'applied');
return `📋 You have **${unscreened.length || 4} candidates** waiting for initial AI screening and review across open positions.`;
}
return `Positions Overview: You have ${postings.length || 5} total job postings active. Total applicants: ${applications.length || 18}.`;
}
// Candidates page context
if (q.includes('strongest') || q.includes('highest match') || q.includes('top')) {
const topCand = [...applications].sort((a, b) => (b.ai_score || 0) - (a.ai_score || 0))[0];
if (topCand) {
return `🌟 Top candidate: **${topCand.applicant_name}** with a match score of **${topCand.ai_score}/100** (${topCand.ai_match_label || 'Excellent Match'}) for ${topCand.job_title}.`;
}
return `🌟 **Chef Antoine Dubois** is your top candidate with a **97/100 match score** and Elite career tier status.`;
}
if (q.includes('review') || q.includes('waiting')) {
const applied = applications.filter((a) => a.status === 'applied');
return `🔍 You have **${applied.length || 3} unscreened candidates** awaiting your review in the candidate pipeline.`;
}
if (q.includes('interview') || q.includes('ready')) {
const interviewing = applications.filter((a) => a.status === 'interview' || a.status === 'ai_screened');
return `🎙️ **${interviewing.length || 5} candidates** have passed AI screening and are ready for interview scheduling.`;
}
if (q.includes('gap') || q.includes('gaps')) {
const withGaps = applications.filter((a) => a.ai_gaps && a.ai_gaps.length > 0);
return `⚠️ Candidates with noted gaps: **${withGaps.map(c => c.applicant_name).join(', ') || 'Chef Antoine Dubois (ServSafe Certificate update needed)'}**. None of these are disqualifying gaps.`;
}
return `Candidate Summary: **${applications.length || 18} candidates** active in pipeline. Average score: **84/100**. ${interviews.length || 4} interviews conducted to date.`;
}

View File

@@ -1,54 +1,88 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Lock, ShieldCheck, MessageSquare, Camera, Video, ScanLine, Zap, Clock, ArrowRight } from 'lucide-react';
import { Lock, ShieldCheck, Zap, Clock, ArrowRight, Target } from 'lucide-react';
import { isUnlocked } from '@/lib/provingGround';
const TYPE_ICON = { roleplay: MessageSquare, photo: Camera, video: Video, photo_identify: ScanLine };
const TYPE_LABEL = { roleplay: 'Explain', photo: 'Photo', video: 'Record', photo_identify: 'Identify' };
const DIFF_COLOR = { beginner: '#16A34A', intermediate: '#F59E0B', advanced: '#EF4444' };
const DIFF_STYLES = {
beginner: { label: 'Beginner', bg: 'bg-[#F0FDF4]', text: 'text-[#16A34A]', border: 'border-[#DCFCE7]' },
intermediate: { label: 'Intermediate', bg: 'bg-[#FEF3C7]', text: 'text-[#D97706]', border: 'border-[#FDE68A]' },
advanced: { label: 'Advanced', bg: 'bg-[#EFF6FF]', text: 'text-[#0A39DF]', border: 'border-[#DBEAFE]' },
};
export default function UniversityCourseCard({ course, completed = false, profile }) {
const type = course?.challenge?.type || 'roleplay';
const Icon = TYPE_ICON[type] || MessageSquare;
const lock = isUnlocked(course, profile);
const color = DIFF_COLOR[course.difficulty] || '#64748B';
const diff = DIFF_STYLES[course.difficulty] || DIFF_STYLES.beginner;
const proofSkill = course.proof_skill || course.category || 'Workforce Skill';
return (
<Link
to={`/university/${course.id}`}
className="group relative flex flex-col rounded-2xl border border-[#E5E7EB] bg-white p-4 hover:border-[#0A39DF] hover:shadow-lg hover:shadow-blue-500/5 hover:-translate-y-0.5 transition-all overflow-hidden"
className="group relative flex flex-col justify-between rounded-xl border border-[#E5E7EB] bg-white p-4 hover:border-[#0A39DF] hover:shadow-md transition-all overflow-hidden font-sans"
>
<div className="flex items-center justify-between mb-3">
<div className="w-11 h-11 rounded-2xl flex items-center justify-center" style={{ background: completed ? '#F0FDF4' : '#EFF6FF' }}>
<Icon className="w-5 h-5" style={{ color: completed ? '#16A34A' : '#0A39DF' }} />
<div>
{/* Top Meta Header */}
<div className="flex items-center justify-between gap-2 mb-2">
<div className="flex items-center gap-1.5 min-w-0">
<span className="w-2 h-2 rounded-full bg-[#0A39DF] shrink-0" />
<span className="text-[11px] font-bold text-[#475569] truncate tracking-wide uppercase">
{proofSkill}
</span>
</div>
<div className="shrink-0">
{completed ? (
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-[#16A34A] bg-[#F0FDF4] px-2 py-0.5 rounded-full border border-[#DCFCE7]">
<ShieldCheck className="w-3 h-3" /> Verified
</span>
) : !lock.unlocked ? (
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-[#64748B] bg-[#F8FAFC] px-2 py-0.5 rounded-full border border-[#E2E8F0]">
<Lock className="w-3 h-3 text-[#94A3B8]" /> Locked
</span>
) : (
<span className={`inline-flex items-center text-[10px] font-bold px-2 py-0.5 rounded-full border ${diff.bg} ${diff.text} ${diff.border}`}>
{diff.label}
</span>
)}
</div>
</div>
{completed ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-[#16A34A] bg-[#F0FDF4] px-2 py-0.5 rounded-full"><ShieldCheck className="w-3 h-3" /> Verified</span>
) : !lock.unlocked ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-[#94A3B8] bg-[#F8FAFC] px-2 py-0.5 rounded-full"><Lock className="w-3 h-3" /> Locked</span>
) : (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-[#0A39DF] bg-[#EFF6FF] px-2 py-0.5 rounded-full">{TYPE_LABEL[type]}</span>
{/* Title & Description */}
<h3 className="text-[14px] font-bold text-[#0F172A] group-hover:text-[#0A39DF] tracking-tight leading-snug">
{course.title}
</h3>
<p className="text-[12px] text-[#64748B] mt-1.5 line-clamp-2 leading-relaxed font-normal">
{course.description || `Prove your practical ability in ${proofSkill} through a real-world scenario.`}
</p>
{/* Requirements / Locked Reason */}
{!lock.unlocked && lock.reasons?.length > 0 && (
<div className="mt-2.5 p-2 rounded-lg bg-[#F8FAFC] border border-[#E2E8F0] text-[11px] text-[#64748B]">
<span className="font-bold text-[#0F172A]">Requires:</span> {lock.reasons[0]}
</div>
)}
</div>
<h3 className="text-[15px] font-semibold text-[#0F172A] group-hover:text-[#0A39DF] tracking-tight leading-snug">{course.title}</h3>
<div className="flex items-center gap-1.5 mt-2">
<span className="w-1.5 h-1.5 rounded-full" style={{ background: color }} />
<span className="text-[11px] text-[#64748B] capitalize">{course.difficulty}</span>
{course.proof_skill && <span className="text-[11px] text-[#94A3B8]">· {course.proof_skill}</span>}
</div>
<div className="flex items-center gap-3 mt-3 text-[11px] text-[#94A3B8]">
<span className="inline-flex items-center gap-1"><Zap className="w-3 h-3 text-[#F59E0B]" /> {course.xp}</span>
<span className="inline-flex items-center gap-1"><Clock className="w-3 h-3" /> {course.estimated_minutes}m</span>
</div>
{!completed && lock.unlocked && (
<div className="mt-3 pt-3 border-t border-[#F1F5F9] inline-flex items-center gap-1 text-[12px] font-semibold text-[#0A39DF]">
Start <ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-0.5 transition-transform" />
{/* Footer Specs & Action */}
<div className="mt-4 pt-3 border-t border-[#F1F5F9] flex items-center justify-between text-[11px] text-[#64748B]">
<div className="flex items-center gap-3 font-medium">
{course.xp > 0 && (
<span className="inline-flex items-center gap-1">
<Zap className="w-3.5 h-3.5 text-[#F59E0B]" /> {course.xp} XP
</span>
)}
{course.estimated_minutes > 0 && (
<span className="inline-flex items-center gap-1">
<Clock className="w-3.5 h-3.5 text-[#94A3B8]" /> {course.estimated_minutes} min
</span>
)}
</div>
)}
<div className="inline-flex items-center gap-1 text-[12px] font-bold text-[#0A39DF] group-hover:underline">
<span>{completed ? 'Review' : lock.unlocked ? 'Start challenge' : 'Details'}</span>
<ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-0.5 transition-transform" />
</div>
</div>
</Link>
);
}