update ui changes
This commit is contained in:
@@ -1,240 +0,0 @@
|
||||
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.`;
|
||||
}
|
||||
Reference in New Issue
Block a user