/** * @license * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useEffect, useCallback } from 'react'; import ControlPanel from './components/ControlPanel'; import ResumePreview from './components/ResumePreview'; import JobMatchDashboard from './components/JobMatchDashboard'; import LoadingSpinner from './components/LoadingSpinner'; import type { InitialInput, ResumeDocument, ResumeSectionType, ScoreResponse, ActiveEditor } from './lib/types'; // Custom hook for debouncing const useDebounce = (callback: (...args: any[]) => void, delay: number) => { const [timeoutId, setTimeoutId] = useState(null); return useCallback((...args: any[]) => { if (timeoutId) { clearTimeout(timeoutId); } const newTimeoutId = setTimeout(() => { callback(...args); }, delay); setTimeoutId(newTimeoutId); }, [callback, delay, timeoutId]); }; export default function App() { const [jobDescription, setJobDescription] = useState(''); const [resumeDocument, setResumeDocument] = useState([]); const [activeEditor, setActiveEditor] = useState(null); const [scoreData, setScoreData] = useState({ score: 0, suggestions: [] }); const [isGenerating, setIsGenerating] = useState(false); // For initial generation const [isScoring, setIsScoring] = useState(false); // For real-time scoring const [isRefining, setIsRefining] = useState(false); // For co-pilot actions const [error, setError] = useState(null); const parseFullResumeText = (fullText: string): ResumeDocument => { const sections: ResumeDocument = []; const sectionTitles: ResumeSectionType[] = ["Professional Summary", "Skills Section", "Experience", "Education"]; const sectionRegex = new RegExp(`##\\s*(${sectionTitles.join('|')})\\s*([\\s\\S]*?)(?=\\n##\\s*(?:${sectionTitles.join('|')})|$)`, 'g'); let match; while ((match = sectionRegex.exec(fullText)) !== null) { const title = match[1].trim() as ResumeSectionType; const content = match[2].trim(); sections.push({ id: title, title, content }); } return sections; }; const handleInitialGenerate = async (input: InitialInput) => { setIsGenerating(true); setError(null); setJobDescription(input.jobDescription || ''); setResumeDocument([]); setScoreData({ score: 0, suggestions: [] }); try { const response = await fetch('http://127.0.0.1:5000/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), }); if (!response.ok) throw new Error((await response.json()).error || 'Server error'); const data = await response.json(); const parsedSections = parseFullResumeText(data.resume); setResumeDocument(parsedSections); } catch (err) { handleFetchError(err, "Could not generate resume."); } finally { setIsGenerating(false); } }; const debouncedScoreRequest = useDebounce(async (doc: ResumeDocument, jd: string) => { if (!jd || doc.length === 0) return; setIsScoring(true); try { const fullText = doc.map(s => `## ${s.title}\n${s.content}`).join('\n\n'); const response = await fetch('http://127.0.0.1:5000/api/score', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resume: fullText, job_description: jd }), }); if (!response.ok) return; // Fail silently on scoring const data: ScoreResponse = await response.json(); setScoreData(data); } catch (err) { // Don't show scoring errors to user to avoid being noisy console.error("Scoring error:", err); } finally { setIsScoring(false); } }, 1500); // 1.5-second debounce delay useEffect(() => { if (resumeDocument.length > 0 && jobDescription) { debouncedScoreRequest(resumeDocument, jobDescription); } }, [resumeDocument, jobDescription, debouncedScoreRequest]); const handleContentUpdate = (sectionId: ResumeSectionType, newContent: string) => { setResumeDocument(prevDoc => prevDoc.map(section => section.id === sectionId ? { ...section, content: newContent } : section ) ); }; const handleRefineSection = async (instruction: string) => { if (!activeEditor) return; setIsRefining(true); setError(null); try { const response = await fetch('http://127.0.0.1:5000/api/refine_section', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text_to_refine: activeEditor.content, instruction: instruction }), }); if (!response.ok) throw new Error((await response.json()).error || 'Server error during refinement.'); const data = await response.json(); handleContentUpdate(activeEditor.sectionId, data.refined_text); } catch (err) { handleFetchError(err, "Could not refine section."); } finally { setIsRefining(false); } }; const handleFetchError = (err: any, context: string) => { let message = err instanceof Error ? err.message : "An unknown error occurred."; if (message.includes('Failed to fetch')) { message = "Could not connect to the backend server. Is it running? Please start the Python server and try again."; } setError(`${context} ${message}`); }; const clearAll = () => { setJobDescription(''); setResumeDocument([]); setActiveEditor(null); setScoreData({ score: 0, suggestions: [] }); setError(null); }; return (

AI Resume Studio

Your intelligent co-pilot for crafting the perfect resume.

{isGenerating &&
} {error &&
{error}
}
{resumeDocument.length > 0 ? ( ) : (

Your AI-crafted resume will appear here. Fill in the "Initial Setup" form and click "Generate" to begin!

)}
); }