mgbam's picture
Upload 14 files
d5db0b0 verified
/**
* @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<NodeJS.Timeout | null>(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<string>('');
const [resumeDocument, setResumeDocument] = useState<ResumeDocument>([]);
const [activeEditor, setActiveEditor] = useState<ActiveEditor | null>(null);
const [scoreData, setScoreData] = useState<ScoreResponse>({ 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<string | null>(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 (
<div className="app-container">
<header className="app-header">
<h1>AI Resume Studio</h1>
<p>Your intelligent co-pilot for crafting the perfect resume.</p>
</header>
{isGenerating && <div className="global-loading-overlay"><LoadingSpinner /></div>}
{error && <div className="error-message global-error">{error}</div>}
<div className="app-content-grid">
<aside className="control-column">
<ControlPanel
onGenerate={handleInitialGenerate}
onClear={clearAll}
activeEditor={activeEditor}
onRefine={handleRefineSection}
isGenerating={isGenerating}
isRefining={isRefining}
/>
</aside>
<main className="resume-column">
{resumeDocument.length > 0 ? (
<ResumePreview
document={resumeDocument}
onContentChange={handleContentUpdate}
activeSectionId={activeEditor?.sectionId}
onSectionFocus={setActiveEditor}
/>
) : (
<div className="placeholder-text">
<p>Your AI-crafted resume will appear here. Fill in the "Initial Setup" form and click "Generate" to begin!</p>
</div>
)}
</main>
<aside className="dashboard-column">
<JobMatchDashboard
score={scoreData.score}
suggestions={scoreData.suggestions}
isLoading={isScoring}
hasContent={resumeDocument.length > 0 && jobDescription.length > 0}
/>
</aside>
</div>
</div>
);
}