221 lines
8.9 KiB
TypeScript
221 lines
8.9 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { FaBriefcase, FaUserGraduate, FaTools, FaFileUpload } from "react-icons/fa";
|
|
import { useState } from "react";
|
|
import CvSummaryPanel from "@/components/CvSummaryPanel";
|
|
|
|
interface SectionData {
|
|
score: number;
|
|
suggestions: string[];
|
|
summary: string;
|
|
keywords: { [key: string]: number };
|
|
}
|
|
|
|
interface OpenAiStats {
|
|
input_tokens: number;
|
|
output_tokens: number;
|
|
total_tokens: number;
|
|
cost: number;
|
|
}
|
|
|
|
interface SummaryData {
|
|
sections: {
|
|
Summary?: SectionData;
|
|
"Work Experience"?: SectionData;
|
|
Education?: SectionData;
|
|
Skills?: SectionData;
|
|
Certifications?: SectionData;
|
|
Projects?: SectionData;
|
|
};
|
|
openai_stats?: OpenAiStats;
|
|
error?: string;
|
|
}
|
|
|
|
export default function Home() {
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [summaryData, setSummaryData] = useState<SummaryData | null>(null);
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [isSummaryVisible, setIsSummaryVisible] = useState<boolean>(false);
|
|
const [showDebug, setShowDebug] = useState<boolean>(false);
|
|
|
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (event.target.files) {
|
|
setFile(event.target.files[0]);
|
|
setSummaryData(null);
|
|
setIsSummaryVisible(false);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (event: React.FormEvent) => {
|
|
event.preventDefault();
|
|
if (!file) return;
|
|
|
|
setLoading(true);
|
|
setSummaryData(null);
|
|
setIsSummaryVisible(false);
|
|
|
|
const formData = new FormData();
|
|
formData.append("cv", file);
|
|
|
|
try {
|
|
const response = await fetch("/api/upload-cv", {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
if (response.ok) {
|
|
const parsed: SummaryData = await response.json();
|
|
setSummaryData(parsed);
|
|
setIsSummaryVisible(true);
|
|
} else {
|
|
alert("CV summary failed.");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error summarizing CV:", error);
|
|
alert("An error occurred while summarizing the CV.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const toggleDebug = () => {
|
|
setShowDebug(!showDebug);
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)] bg-gray-50">
|
|
<main className="flex flex-col sm:flex-row gap-8 row-start-2 items-start">
|
|
<div className="flex flex-col gap-8 w-full sm:w-1/2 sm:items-start">
|
|
<h1 className="text-3xl font-bold text-gray-900">Welcome to Your CV Upgrade</h1>
|
|
<p className="text-lg text-center sm:text-left text-gray-700">
|
|
This platform is designed to help you enhance your CV and showcase your skills effectively.
|
|
</p>
|
|
<div className="flex flex-col gap-4 mt-6">
|
|
<div className="flex items-center">
|
|
<FaBriefcase className="text-2xl mr-2 text-gray-600" />
|
|
<p className="text-gray-700">Highlight your professional experience and achievements.</p>
|
|
</div>
|
|
<div className="flex items-center">
|
|
<FaUserGraduate className="text-2xl mr-2 text-gray-600" />
|
|
<p className="text-gray-700">Showcase your educational background and certifications.</p>
|
|
</div>
|
|
<div className="flex items-center">
|
|
<FaTools className="text-2xl mr-2 text-gray-600" />
|
|
<p className="text-gray-700">Utilize our tools to create a standout CV that gets noticed.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-start mt-8">
|
|
<h2 className="mb-2 text-2xl font-bold text-gray-900">Are you ready to pimp your CV?</h2>
|
|
<input
|
|
type="file"
|
|
accept=".pdf"
|
|
onChange={handleFileChange}
|
|
className="hidden"
|
|
id="cv-upload"
|
|
/>
|
|
<div className="flex space-x-2">
|
|
<label htmlFor="cv-upload" className="inline-flex items-center justify-center px-4 py-2 border border-gray-500 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 cursor-pointer disabled:opacity-50">
|
|
Upload CV
|
|
</label>
|
|
<button
|
|
onClick={handleSubmit}
|
|
className="inline-flex items-center justify-center px-4 py-2 border border-gray-500 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 cursor-pointer disabled:opacity-50"
|
|
disabled={loading || !file}
|
|
>
|
|
{loading ? "Summarizing..." : "Summarize CV"}
|
|
</button>
|
|
</div>
|
|
{file && <p className="mt-2 text-sm text-gray-700 font-normal flex items-center"><FaFileUpload className="mr-2 text-gray-600 text-2xl" /> Selected file: {file.name}</p>}
|
|
</div>
|
|
</div>
|
|
{/* Right Column - CV Summary Panel */}
|
|
<div className="w-full sm:w-1/2 sm:border-l sm:border-gray-200 sm:pl-8">
|
|
<button onClick={toggleDebug} className="mb-4 px-4 py-2 border border-gray-500 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 cursor-pointer">
|
|
{showDebug ? "Hide Debug Info" : "Show Debug Info"}
|
|
</button>
|
|
<div className={`${isSummaryVisible ? 'block' : 'hidden'} p-6 rounded-md`}>
|
|
{loading ? (
|
|
<div className="animate-pulse bg-gray-100 p-6 transition-opacity duration-500" style={{ animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite' }}>
|
|
<div className="h-4 bg-gray-300 rounded-md mb-2" />
|
|
<div className="h-4 bg-gray-300 rounded-md mb-2" />
|
|
<div className="h-4 bg-gray-300 rounded-md" />
|
|
</div>
|
|
) : summaryData ? (
|
|
<>
|
|
{summaryData.error ? (
|
|
<p className="text-red-500">{summaryData.error}</p>
|
|
) : (
|
|
<CvSummaryPanel analysisData={summaryData} summary={summaryData.sections.Summary?.summary || null} />
|
|
)}
|
|
{summaryData.openai_stats && showDebug && (
|
|
<div className="mt-4 border p-4 rounded-md bg-gray-100">
|
|
<h4 className="text-lg font-semibold text-gray-900 mb-2">OpenAI Stats</h4>
|
|
<p className="text-gray-700">Input Tokens: {summaryData.openai_stats.input_tokens}</p>
|
|
<p className="text-gray-700">Output Tokens: {summaryData.openai_stats.output_tokens}</p>
|
|
<p className="text-gray-700">Total Tokens: {summaryData.openai_stats.total_tokens}</p>
|
|
<p className="text-gray-700">Cost: ${summaryData.openai_stats.cost}</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<footer className="flex flex-col items-center justify-center mt-16 p-4 border-t border-gray-200 absolute bottom-0 left-0 right-0 w-full">
|
|
<p className="text-center text-gray-500 text-sm mb-4">
|
|
Powered by Vercel & OpenAI
|
|
</p>
|
|
<div className="flex gap-6 flex-wrap items-center justify-center">
|
|
<a
|
|
className="flex items-center gap-2 hover:underline hover:underline-offset-4 text-sm text-gray-600"
|
|
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<Image
|
|
aria-hidden
|
|
src="/file.svg"
|
|
alt="File icon"
|
|
width={16}
|
|
height={16}
|
|
/>
|
|
Learn
|
|
</a>
|
|
<a
|
|
className="flex items-center gap-2 hover:underline hover:underline-offset-4 text-sm text-gray-600"
|
|
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<Image
|
|
aria-hidden
|
|
src="/window.svg"
|
|
alt="Window icon"
|
|
width={16}
|
|
height={16}
|
|
/>
|
|
Examples
|
|
</a>
|
|
<a
|
|
className="flex items-center gap-2 hover:underline hover:underline-offset-4 text-sm text-gray-600"
|
|
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<Image
|
|
aria-hidden
|
|
src="/globe.svg"
|
|
alt="Globe icon"
|
|
width={16}
|
|
height={16}
|
|
/>
|
|
nextjs.org
|
|
</a>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|