Add live dynamic AI parsing progress status banner and token counter

This commit is contained in:
2026-07-22 17:27:03 -04:00
parent 2b7500e025
commit a40d551820
+74 -7
View File
@@ -101,6 +101,14 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
const [isParsingResume, setIsParsingResume] = useState(false); const [isParsingResume, setIsParsingResume] = useState(false);
const [parseError, setParseError] = useState<string | null>(null); const [parseError, setParseError] = useState<string | null>(null);
const [parseStep, setParseStep] = useState<string>('');
const [parsedLiveStats, setParsedLiveStats] = useState<{
name?: string;
skillsCount?: number;
expCount?: number;
eduCount?: number;
provider?: string;
} | null>(null);
const handleResumeFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleResumeFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
@@ -108,6 +116,8 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
setIsParsingResume(true); setIsParsingResume(true);
setParseError(null); setParseError(null);
setParsedLiveStats(null);
setParseStep(`Reading document ${file.name}...`);
try { try {
let bodyData: any = { let bodyData: any = {
@@ -116,6 +126,7 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
}; };
if (file.name.endsWith('.docx') || file.name.endsWith('.doc')) { if (file.name.endsWith('.docx') || file.name.endsWith('.doc')) {
setParseStep('Extracting raw text from DOCX archive via Mammoth...');
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const base64 = btoa( const base64 = btoa(
new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), '') new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), '')
@@ -125,6 +136,7 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
bodyData.resumeText = await file.text(); bodyData.resumeText = await file.text();
} }
setParseStep('Analyzing profile structure with AI model...');
const res = await fetch('/api/gemini/parse-resume', { const res = await fetch('/api/gemini/parse-resume', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -134,6 +146,20 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
const data = await res.json(); const data = await res.json();
if (data.success && data.profile) { if (data.success && data.profile) {
const parsed = data.profile; const parsed = data.profile;
setParseStep('Structuring work history & technical skill tokens...');
const skills = Array.isArray(parsed.skills) && parsed.skills.length > 0 ? parsed.skills : prev.skills;
const experience = Array.isArray(parsed.experience) && parsed.experience.length > 0 ? parsed.experience : prev.experience;
const education = Array.isArray(parsed.education) && parsed.education.length > 0 ? parsed.education : prev.education;
setParsedLiveStats({
name: parsed.fullName || 'Candidate',
skillsCount: skills.length,
expCount: experience.length,
eduCount: education.length,
provider: data.providerUsed || 'Gemini 3.6 Flash'
});
setProfile(prev => ({ setProfile(prev => ({
...prev, ...prev,
fullName: parsed.fullName || prev.fullName, fullName: parsed.fullName || prev.fullName,
@@ -144,16 +170,19 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
githubUrl: parsed.githubUrl || prev.githubUrl, githubUrl: parsed.githubUrl || prev.githubUrl,
portfolioUrl: parsed.portfolioUrl || prev.portfolioUrl, portfolioUrl: parsed.portfolioUrl || prev.portfolioUrl,
summary: parsed.summary || prev.summary, summary: parsed.summary || prev.summary,
skills: Array.isArray(parsed.skills) && parsed.skills.length > 0 ? parsed.skills : prev.skills, skills,
experience: Array.isArray(parsed.experience) && parsed.experience.length > 0 ? parsed.experience : prev.experience, experience,
education: Array.isArray(parsed.education) && parsed.education.length > 0 ? parsed.education : prev.education education
})); }));
setParseStep('✓ Successfully parsed and updated profile fields!');
setTimeout(() => setIsParsingResume(false), 3000);
} else { } else {
setParseError(data.error || 'Failed to parse resume content.'); setParseError(data.error || 'Failed to parse resume content.');
setIsParsingResume(false);
} }
} catch (err: any) { } catch (err: any) {
setParseError(`Failed to read/parse file: ${err.message}`); setParseError(`Failed to read/parse file: ${err.message}`);
} finally {
setIsParsingResume(false); setIsParsingResume(false);
} }
}; };
@@ -187,8 +216,8 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<label className="px-4 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-200 font-semibold text-xs cursor-pointer flex items-center gap-2 transition-all"> <label className="px-4 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-200 font-semibold text-xs cursor-pointer flex items-center gap-2 transition-all">
<Sparkles className="w-4 h-4 text-amber-400" /> <Sparkles className="w-4 h-4 text-amber-400 animate-pulse" />
<span>{isParsingResume ? 'Parsing Resume with AI...' : 'Upload & Parse Resume'}</span> <span>{isParsingResume ? 'Parsing Resume...' : 'Upload & Parse Resume'}</span>
<input <input
type="file" type="file"
accept=".txt,.md,.json,.pdf,.docx" accept=".txt,.md,.json,.pdf,.docx"
@@ -204,11 +233,49 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
className="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs shadow-lg shadow-indigo-600/30 disabled:opacity-50 flex items-center gap-2 transition-all shrink-0" className="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs shadow-lg shadow-indigo-600/30 disabled:opacity-50 flex items-center gap-2 transition-all shrink-0"
> >
{savedSuccess ? <CheckCircle2 className="w-4 h-4 text-emerald-300" /> : <Save className="w-4 h-4" />} {savedSuccess ? <CheckCircle2 className="w-4 h-4 text-emerald-300" /> : <Save className="w-4 h-4" />}
<span>{isSaving ? 'Saving...' : savedSuccess ? 'Saved to Firestore!' : 'Save Master Profile'}</span> <span>{isSaving ? 'Saving...' : savedSuccess ? 'Saved to Local Disk!' : 'Save Master Profile'}</span>
</button> </button>
</div> </div>
</div> </div>
{/* Dynamic Live Parsing Status Modal / Banner */}
{isParsingResume && (
<div className="p-5 rounded-2xl bg-indigo-950/40 border border-indigo-500/30 shadow-xl space-y-3 animate-fadeIn">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<RefreshCw className="w-4 h-4 text-indigo-400 animate-spin" />
<span className="text-xs font-bold text-indigo-200">{parseStep}</span>
</div>
{parsedLiveStats && (
<span className="px-2.5 py-1 rounded-lg bg-indigo-900/60 border border-indigo-700/50 text-[10px] text-indigo-300 font-mono">
Engine: {parsedLiveStats.provider}
</span>
)}
</div>
{parsedLiveStats && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 pt-2 border-t border-indigo-900/50">
<div className="p-2.5 rounded-xl bg-slate-900/80 border border-indigo-900/50">
<span className="text-[10px] text-slate-400 uppercase tracking-wider block">Candidate</span>
<strong className="text-xs font-bold text-white">{parsedLiveStats.name}</strong>
</div>
<div className="p-2.5 rounded-xl bg-slate-900/80 border border-indigo-900/50">
<span className="text-[10px] text-slate-400 uppercase tracking-wider block">Skills Found</span>
<strong className="text-xs font-bold text-emerald-400">{parsedLiveStats.skillsCount} Technical Tokens</strong>
</div>
<div className="p-2.5 rounded-xl bg-slate-900/80 border border-indigo-900/50">
<span className="text-[10px] text-slate-400 uppercase tracking-wider block">Experience Items</span>
<strong className="text-xs font-bold text-indigo-400">{parsedLiveStats.expCount} Roles & Bullets</strong>
</div>
<div className="p-2.5 rounded-xl bg-slate-900/80 border border-indigo-900/50">
<span className="text-[10px] text-slate-400 uppercase tracking-wider block">Credentials</span>
<strong className="text-xs font-bold text-amber-400">{parsedLiveStats.eduCount} Certifications</strong>
</div>
</div>
)}
</div>
)}
{parseError && ( {parseError && (
<div className="p-4 rounded-xl bg-rose-500/10 border border-rose-500/30 text-rose-300 text-xs flex items-center gap-2"> <div className="p-4 rounded-xl bg-rose-500/10 border border-rose-500/30 text-rose-300 text-xs flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-rose-400 shrink-0" /> <AlertCircle className="w-4 h-4 text-rose-400 shrink-0" />