update ui changes

This commit is contained in:
2026-08-10 19:51:49 +05:30
parent 5cd49a1655
commit 7427359967
13 changed files with 388 additions and 271 deletions

View File

@@ -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.`;
}