import React, { useState, useEffect, useCallback, ChangeEvent, useRef } from 'react'; import { Building, UploadCloud, FileText, Download, Eye, LoaderCircle, CheckCircle, XCircle, Wand2, History, FileCode } from 'lucide-react'; import { fetchTemplates, fetchTemplateContent, convertResume, fetchConversionHistory, type ConvertedFile } from './services/resumeService'; // --- Type Definitions --- interface ConvertedFileWithContent extends ConvertedFile { template?: string; htmlContent?: string; pdfBlob?: Blob; } const formatDate = (date: Date): string => new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeStyle: 'short' }).format(date); // --- Main App Component --- const App: React.FC = () => { // State const [templates, setTemplates] = useState([]); const [selectedTemplate, setSelectedTemplate] = useState(''); const [resumeFile, setResumeFile] = useState(null); const [generatedHtml, setGeneratedHtml] = useState(null); const [processingState, setProcessingState] = useState<'idle' | 'parsing' | 'generating' | 'done' | 'error'>('idle'); const [errorMessage, setErrorMessage] = useState(''); const [isLoadingTemplates, setIsLoadingTemplates] = useState(true); const [isLoadingHistory, setIsLoadingHistory] = useState(true); const [convertedFiles, setConvertedFiles] = useState([]); const previewRef = useRef(null); // Effects useEffect(() => { // Fetch templates from R2 setIsLoadingTemplates(true); fetchTemplates().then(data => { setTemplates(data); if (data.length > 0) setSelectedTemplate(data[0]); }).catch(error => { console.error('Error loading templates:', error); setErrorMessage('Failed to load templates from R2'); }).finally(() => setIsLoadingTemplates(false)); // Fetch conversion history from R2 setIsLoadingHistory(true); fetchConversionHistory().then(data => { setConvertedFiles(data as ConvertedFileWithContent[]); }).catch(error => { console.error('Error loading history:', error); }).finally(() => setIsLoadingHistory(false)); }, []); // Handlers const handleFileChange = (e: ChangeEvent) => { if (e.target.files && e.target.files[0]) handleFileSelect(e.target.files[0]); }; const handleFileSelect = (file: File) => { const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']; if (allowedTypes.includes(file.type)) { setResumeFile(file); setGeneratedHtml(null); setProcessingState('idle'); } else { alert('Invalid file type. Please upload a PDF or DOCX file.'); } }; const handleGenerate = async () => { if (!resumeFile || !selectedTemplate) { setErrorMessage('Please select a template and upload a resume file.'); setProcessingState('error'); return; } setProcessingState('parsing'); setErrorMessage(''); setGeneratedHtml(null); try { // Call backend API to convert resume const result = await convertResume( resumeFile, selectedTemplate, (message) => { // Update UI with progress messages if (message.includes('Uploading')) setProcessingState('parsing'); else if (message.includes('Processing')) setProcessingState('generating'); } ); setGeneratedHtml(result.html_content); // Add to converted files list const newFile: ConvertedFileWithContent = { id: `${new Date().getTime()}-${resumeFile.name}`, name: resumeFile.name, url: result.html_url, size: new Blob([result.html_content]).size, lastModified: new Date().toISOString(), timestamp: new Date(), template: selectedTemplate, htmlContent: result.html_content, }; setConvertedFiles(prev => [newFile, ...prev]); setProcessingState('done'); } catch (e) { console.error(e); setErrorMessage(e instanceof Error ? e.message : 'An unknown error occurred during resume conversion.'); setProcessingState('error'); } }; const handleDownload = async (file: ConvertedFileWithContent, format: 'html' | 'pdf') => { const baseName = file.name.replace(/\.[^/.]+$/, ""); const templateName = file.template || 'resume'; const fileName = `${baseName}_${templateName}.${format}`; const link = document.createElement('a'); link.download = fileName; // For HTML files if (format === 'html') { if (file.htmlContent) { // Download from in-memory content const blob = new Blob([file.htmlContent], { type: 'text/html' }); link.href = URL.createObjectURL(blob); } else if (file.url) { // Download from R2 URL link.href = file.url; } else { console.error("No HTML content or URL to download."); return; } } else { // PDF format - not supported yet in backend alert('PDF download will be available soon. For now, you can print the HTML preview to PDF.'); return; } document.body.appendChild(link); link.click(); document.body.removeChild(link); if (file.htmlContent) { URL.revokeObjectURL(link.href); } }; const handlePreviewHistoryItem = async (file: ConvertedFileWithContent) => { if (file.htmlContent) { // Preview from in-memory content setGeneratedHtml(file.htmlContent); } else if (file.url) { // Fetch HTML content from R2 URL try { const response = await fetch(file.url); if (!response.ok) throw new Error('Failed to fetch file'); const htmlContent = await response.text(); setGeneratedHtml(htmlContent); } catch (e) { const message = e instanceof Error ? e.message : 'Unknown error'; setGeneratedHtml(`

Error loading preview: ${message}

`); } } else { setGeneratedHtml(`

Preview not available for this file.

`); } }; // UI Components const Card: React.FC<{ title: string; icon: React.ReactNode; children: React.ReactNode; className?: string }> = ({ title, icon, children, className }) => (
{icon}

{title}

{children}
); const dropzoneProps = { onDragOver: (e: React.DragEvent) => e.preventDefault(), onDrop: (e: React.DragEvent) => { e.preventDefault(); if (e.dataTransfer.files && e.dataTransfer.files[0]) { handleFileSelect(e.dataTransfer.files[0]); } }, }; const getStatusIndicator = () => { switch (processingState) { case 'parsing': return <> Extracting resume text with AI...; case 'generating': return <> Generating HTML & PDF with AI...; case 'done': return <> Generation complete!; case 'error': return <> {errorMessage}; default: return null; } }; const currentFile = convertedFiles.find(f => f.htmlContent === generatedHtml); return (

Smart Resume Formatter

Generate professional resumes with AI-powered template merging.

}> {isLoadingTemplates ? : ( )} }>
{getStatusIndicator()}
} className="lg:row-span-2">
{generatedHtml ? (