majidali1256 commited on
Commit
3ef3535
·
1 Parent(s): 5e35751

feat(step-6): Next.js frontend dashboard with Framer Motion animations & responsive UI

Browse files
frontend/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
frontend/AGENTS.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
frontend/CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
frontend/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
frontend/eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
frontend/next.config.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ turbopack: {
5
+ root: __dirname,
6
+ },
7
+ output: "standalone",
8
+ };
9
+
10
+ export default nextConfig;
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@types/canvas-confetti": "^1.9.0",
13
+ "canvas-confetti": "^1.9.4",
14
+ "clsx": "^2.1.1",
15
+ "framer-motion": "^12.42.2",
16
+ "lucide-react": "^1.26.0",
17
+ "next": "16.2.11",
18
+ "react": "19.2.4",
19
+ "react-dom": "19.2.4",
20
+ "tailwind-merge": "^3.6.0"
21
+ },
22
+ "devDependencies": {
23
+ "@tailwindcss/postcss": "^4",
24
+ "@types/node": "^20",
25
+ "@types/react": "^19",
26
+ "@types/react-dom": "^19",
27
+ "eslint": "^9",
28
+ "eslint-config-next": "16.2.11",
29
+ "tailwindcss": "^4",
30
+ "typescript": "^5"
31
+ }
32
+ }
frontend/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
frontend/public/file.svg ADDED
frontend/public/globe.svg ADDED
frontend/public/next.svg ADDED
frontend/public/vercel.svg ADDED
frontend/public/window.svg ADDED
frontend/src/app/favicon.ico ADDED
frontend/src/app/globals.css ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ @layer base {
4
+ body {
5
+ background-color: #090d16;
6
+ color: #f8fafc;
7
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
8
+ }
9
+ }
10
+
11
+ .glass-card {
12
+ background: rgba(15, 23, 42, 0.75);
13
+ backdrop-filter: blur(16px);
14
+ -webkit-backdrop-filter: blur(16px);
15
+ border: 1px solid rgba(255, 255, 255, 0.1);
16
+ }
17
+
18
+ .glass-card-hover {
19
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
20
+ }
21
+
22
+ .glass-card-hover:hover {
23
+ border-color: rgba(99, 102, 241, 0.4);
24
+ box-shadow: 0 0 25px -5px rgba(99, 102, 241, 0.25);
25
+ transform: translateY(-2px);
26
+ }
27
+
28
+ .glow-indigo {
29
+ box-shadow: 0 0 35px -5px rgba(99, 102, 241, 0.35);
30
+ }
31
+
32
+ .glow-emerald {
33
+ box-shadow: 0 0 35px -5px rgba(16, 185, 129, 0.35);
34
+ }
35
+
36
+ .glow-amber {
37
+ box-shadow: 0 0 35px -5px rgba(245, 158, 11, 0.35);
38
+ }
frontend/src/app/layout.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "MATCH.AI — AI Resume Screener & Feedback System",
6
+ description: "Deterministic AI resume screening, match scoring (0-100), missing keywords detection, and rewrite suggestions powered by Google Gemini Flash and FastAPI.",
7
+ };
8
+
9
+ export default function RootLayout({
10
+ children,
11
+ }: Readonly<{
12
+ children: React.ReactNode;
13
+ }>) {
14
+ return (
15
+ <html lang="en" className="h-full antialiased dark">
16
+ <body className="min-h-full flex flex-col bg-slate-950 text-slate-100">{children}</body>
17
+ </html>
18
+ );
19
+ }
frontend/src/app/page.tsx ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+ import {
6
+ Upload,
7
+ FileText,
8
+ CheckCircle2,
9
+ AlertTriangle,
10
+ Lightbulb,
11
+ Sparkles,
12
+ RefreshCw,
13
+ ShieldCheck,
14
+ Zap,
15
+ ArrowRight,
16
+ Download,
17
+ Copy,
18
+ Check,
19
+ } from "lucide-react";
20
+
21
+ interface AssessmentResult {
22
+ match_score: number;
23
+ score_rationale: string;
24
+ matched_requirements: string[];
25
+ missing_requirements: string[];
26
+ suggestions: string[];
27
+ limitations?: string[];
28
+ grounding?: {
29
+ is_grounded: boolean;
30
+ confidence_score: number;
31
+ unsupported_claims: string[];
32
+ };
33
+ }
34
+
35
+ export default function Home() {
36
+ const [resumeFile, setResumeFile] = useState<File | null>(null);
37
+ const [jdFile, setJdFile] = useState<File | null>(null);
38
+ const [jdText, setJdText] = useState<string>("");
39
+ const [isLoading, setIsLoading] = useState<boolean>(false);
40
+ const [result, setResult] = useState<AssessmentResult | null>(null);
41
+ const [error, setError] = useState<string | null>(null);
42
+ const [copied, setCopied] = useState<boolean>(false);
43
+ const [activeTab, setActiveTab] = useState<"matched" | "missing" | "suggestions">("matched");
44
+
45
+ const handleResumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
46
+ if (e.target.files && e.target.files[0]) {
47
+ setResumeFile(e.target.files[0]);
48
+ setError(null);
49
+ }
50
+ };
51
+
52
+ const handleJdFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
53
+ if (e.target.files && e.target.files[0]) {
54
+ setJdFile(e.target.files[0]);
55
+ }
56
+ };
57
+
58
+ const handleDemoScan = async () => {
59
+ setIsLoading(true);
60
+ setError(null);
61
+ try {
62
+ const res = await fetch("http://localhost:8000/api/demo");
63
+ if (!res.ok) throw new Error("Backend server error");
64
+ const data = await res.json();
65
+ setResult(data.assessment);
66
+ } catch (err: any) {
67
+ setError(err.message || "Failed to fetch demo benchmark scan");
68
+ } finally {
69
+ setIsLoading(false);
70
+ }
71
+ };
72
+
73
+ const handleScan = async (e: React.FormEvent) => {
74
+ e.preventDefault();
75
+ if (!resumeFile) {
76
+ setError("Please select a candidate resume file (.pdf, .docx, .txt)");
77
+ return;
78
+ }
79
+ if (!jdText.trim() && !jdFile) {
80
+ setError("Please provide a job description (text or file)");
81
+ return;
82
+ }
83
+
84
+ setIsLoading(true);
85
+ setError(null);
86
+
87
+ const formData = new FormData();
88
+ formData.append("resume_file", resumeFile);
89
+ if (jdFile) {
90
+ formData.append("jd_file", jdFile);
91
+ }
92
+ if (jdText.trim()) {
93
+ formData.append("jd_text", jdText);
94
+ }
95
+
96
+ try {
97
+ const response = await fetch("http://localhost:8000/api/scan", {
98
+ method: "POST",
99
+ body: formData,
100
+ });
101
+
102
+ if (!response.ok) {
103
+ const errorData = await response.json().catch(() => ({}));
104
+ throw new Error(errorData.detail || "Scanning failed. Check backend logs.");
105
+ }
106
+
107
+ const data = await response.json();
108
+ setResult(data.assessment);
109
+ } catch (err: any) {
110
+ setError(err.message || "An unexpected error occurred during scan.");
111
+ } finally {
112
+ setIsLoading(false);
113
+ }
114
+ };
115
+
116
+ const copyResultJSON = () => {
117
+ if (!result) return;
118
+ navigator.clipboard.writeText(JSON.stringify(result, null, 2));
119
+ setCopied(true);
120
+ setTimeout(() => setCopied(false), 2000);
121
+ };
122
+
123
+ const getScoreColor = (score: number) => {
124
+ if (score >= 80) return "text-emerald-400 border-emerald-500/50 bg-emerald-500/10";
125
+ if (score >= 60) return "text-amber-400 border-amber-500/50 bg-amber-500/10";
126
+ return "text-rose-400 border-rose-500/50 bg-rose-500/10";
127
+ };
128
+
129
+ return (
130
+ <div className="min-h-screen bg-slate-950 text-slate-100 selection:bg-indigo-500 selection:text-white">
131
+ {/* Background Glow Orbs */}
132
+ <div className="fixed top-0 left-1/4 w-96 h-96 bg-indigo-600/15 rounded-full blur-3xl pointer-events-none" />
133
+ <div className="fixed bottom-10 right-1/4 w-96 h-96 bg-purple-600/15 rounded-full blur-3xl pointer-events-none" />
134
+
135
+ {/* Header / Navbar */}
136
+ <header className="border-b border-slate-800/80 backdrop-blur-md bg-slate-950/60 sticky top-0 z-50">
137
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
138
+ <div className="flex items-center space-x-3">
139
+ <div className="h-10 w-10 rounded-xl bg-gradient-to-tr from-indigo-600 to-purple-600 flex items-center justify-center shadow-lg shadow-indigo-500/20">
140
+ <Sparkles className="w-5 h-5 text-white" />
141
+ </div>
142
+ <div>
143
+ <span className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white via-slate-200 to-indigo-300">
144
+ MATCH.AI
145
+ </span>
146
+ <span className="ml-2 text-xs px-2 py-0.5 rounded-full bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 font-medium">
147
+ v2.0 Next.js
148
+ </span>
149
+ </div>
150
+ </div>
151
+
152
+ <div className="flex items-center space-x-4">
153
+ <button
154
+ onClick={handleDemoScan}
155
+ disabled={isLoading}
156
+ className="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 transition flex items-center space-x-2"
157
+ >
158
+ <Zap className="w-3.5 h-3.5 text-amber-400" />
159
+ <span>Load Quick Demo</span>
160
+ </button>
161
+ <a
162
+ href="https://github.com/majidali1256/ai-resume-screener"
163
+ target="_blank"
164
+ rel="noreferrer"
165
+ className="text-xs font-medium text-slate-400 hover:text-slate-200 transition"
166
+ >
167
+ GitHub Repo
168
+ </a>
169
+ </div>
170
+ </div>
171
+ </header>
172
+
173
+ {/* Main Container */}
174
+ <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
175
+ <div className="text-center max-w-3xl mx-auto mb-10">
176
+ <motion.h1
177
+ initial={{ opacity: 0, y: -10 }}
178
+ animate={{ opacity: 1, y: 0 }}
179
+ className="text-4xl sm:text-5xl font-extrabold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-white via-slate-100 to-indigo-300"
180
+ >
181
+ AI Resume Screener & Feedback System
182
+ </motion.h1>
183
+ <p className="mt-4 text-base sm:text-lg text-slate-400">
184
+ Upload candidate resumes, compare against job descriptions, get deterministic match scores (0–100), missing keywords, and AI rewrite suggestions.
185
+ </p>
186
+ </div>
187
+
188
+ {/* Form & Scanner Section */}
189
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
190
+ <div className="lg:col-span-5 space-y-6">
191
+ <div className="glass-card rounded-2xl p-6 relative overflow-hidden">
192
+ <h2 className="text-lg font-semibold text-white mb-4 flex items-center space-x-2">
193
+ <Upload className="w-5 h-5 text-indigo-400" />
194
+ <span>Upload Documents</span>
195
+ </h2>
196
+
197
+ <form onSubmit={handleScan} className="space-y-5">
198
+ {/* Resume Upload Dropzone */}
199
+ <div>
200
+ <label className="block text-xs font-medium text-slate-300 mb-2">
201
+ Candidate Resume (.pdf, .docx, .txt) *
202
+ </label>
203
+ <div className="relative border-2 border-dashed border-slate-700 hover:border-indigo-500/60 rounded-xl p-4 text-center cursor-pointer transition bg-slate-900/40">
204
+ <input
205
+ type="file"
206
+ accept=".pdf,.docx,.doc,.txt"
207
+ onChange={handleResumeChange}
208
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
209
+ />
210
+ <FileText className="w-8 h-8 text-indigo-400 mx-auto mb-2" />
211
+ {resumeFile ? (
212
+ <div>
213
+ <p className="text-sm font-semibold text-emerald-400 truncate">{resumeFile.name}</p>
214
+ <p className="text-xs text-slate-400">{(resumeFile.size / 1024).toFixed(1)} KB</p>
215
+ </div>
216
+ ) : (
217
+ <div>
218
+ <p className="text-xs font-medium text-slate-300">Click or drag resume here</p>
219
+ <p className="text-[10px] text-slate-500 mt-1">Supports PDF, DOCX, TXT (Max 10MB)</p>
220
+ </div>
221
+ )}
222
+ </div>
223
+ </div>
224
+
225
+ {/* Job Description Text Area */}
226
+ <div>
227
+ <label className="block text-xs font-medium text-slate-300 mb-2">
228
+ Job Description Specification *
229
+ </label>
230
+ <textarea
231
+ rows={6}
232
+ value={jdText}
233
+ onChange={(e) => setJdText(e.target.value)}
234
+ placeholder="Paste job description requirements, qualifications, and required skills..."
235
+ className="w-full rounded-xl bg-slate-900/80 border border-slate-800 p-3 text-xs text-slate-200 placeholder-slate-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none transition resize-none"
236
+ />
237
+ </div>
238
+
239
+ {/* Optional JD File Upload */}
240
+ <div>
241
+ <label className="block text-[11px] font-medium text-slate-400 mb-1">
242
+ Or Upload JD File (Optional)
243
+ </label>
244
+ <input
245
+ type="file"
246
+ accept=".pdf,.docx,.doc,.txt"
247
+ onChange={handleJdFileChange}
248
+ className="block w-full text-xs text-slate-400 file:mr-4 file:py-1.5 file:px-3 file:rounded-lg file:border-0 file:text-xs file:font-semibold file:bg-slate-800 file:text-slate-300 hover:file:bg-slate-700 cursor-pointer"
249
+ />
250
+ </div>
251
+
252
+ {error && (
253
+ <div className="p-3 rounded-xl bg-rose-500/10 border border-rose-500/30 text-rose-400 text-xs flex items-start space-x-2">
254
+ <AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" />
255
+ <span>{error}</span>
256
+ </div>
257
+ )}
258
+
259
+ {/* Submit Scan Button */}
260
+ <button
261
+ type="submit"
262
+ disabled={isLoading}
263
+ className="w-full py-3 px-4 rounded-xl font-semibold text-sm bg-gradient-to-r from-indigo-600 via-indigo-500 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white shadow-lg shadow-indigo-600/30 transition flex items-center justify-center space-x-2 disabled:opacity-50"
264
+ >
265
+ {isLoading ? (
266
+ <>
267
+ <RefreshCw className="w-4 h-4 animate-spin text-white" />
268
+ <span>Analyzing Resume with Gemini AI...</span>
269
+ </>
270
+ ) : (
271
+ <>
272
+ <Sparkles className="w-4 h-4 text-indigo-200" />
273
+ <span>Start AI Assessment</span>
274
+ <ArrowRight className="w-4 h-4" />
275
+ </>
276
+ )}
277
+ </button>
278
+ </form>
279
+ </div>
280
+ </div>
281
+
282
+ {/* Results Output Section */}
283
+ <div className="lg:col-span-7">
284
+ <AnimatePresence mode="wait">
285
+ {result ? (
286
+ <motion.div
287
+ key="results"
288
+ initial={{ opacity: 0, scale: 0.98 }}
289
+ animate={{ opacity: 1, scale: 1 }}
290
+ exit={{ opacity: 0, scale: 0.98 }}
291
+ transition={{ duration: 0.3 }}
292
+ className="space-y-6"
293
+ >
294
+ {/* Score Overview Card */}
295
+ <div className="glass-card rounded-2xl p-6 relative overflow-hidden">
296
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-6">
297
+ <div className="space-y-2 text-center sm:text-left">
298
+ <div className="flex items-center justify-center sm:justify-start space-x-2">
299
+ <span className="text-xs font-semibold uppercase tracking-wider text-slate-400">
300
+ Assessment Score
301
+ </span>
302
+ <span className="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold bg-indigo-500/10 text-indigo-400 border border-indigo-500/20">
303
+ <ShieldCheck className="w-3 h-3 mr-1 text-emerald-400" /> Grounded Verification
304
+ </span>
305
+ </div>
306
+ <h3 className="text-2xl font-bold text-white">
307
+ {result.match_score >= 80
308
+ ? "High Candidate Match"
309
+ : result.match_score >= 60
310
+ ? "Moderate Candidate Match"
311
+ : "Low Requirement Match"}
312
+ </h3>
313
+ <p className="text-xs text-slate-400 max-w-md">
314
+ {result.score_rationale}
315
+ </p>
316
+ </div>
317
+
318
+ {/* Score Circle Gauge */}
319
+ <div
320
+ className={`w-28 h-28 rounded-full border-4 flex flex-col items-center justify-center shrink-0 ${getScoreColor(
321
+ result.match_score
322
+ )}`}
323
+ >
324
+ <span className="text-3xl font-extrabold tracking-tight">{result.match_score}%</span>
325
+ <span className="text-[10px] uppercase font-bold text-slate-400">Score</span>
326
+ </div>
327
+ </div>
328
+
329
+ <div className="mt-6 pt-4 border-t border-slate-800/80 flex items-center justify-between">
330
+ <button
331
+ onClick={copyResultJSON}
332
+ className="text-xs text-slate-400 hover:text-slate-200 transition flex items-center space-x-1"
333
+ >
334
+ {copied ? <Check className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5" />}
335
+ <span>{copied ? "Copied JSON!" : "Copy Raw JSON"}</span>
336
+ </button>
337
+ <button
338
+ onClick={() => window.print()}
339
+ className="text-xs text-indigo-400 hover:text-indigo-300 transition flex items-center space-x-1"
340
+ >
341
+ <Download className="w-3.5 h-3.5" />
342
+ <span>Export Assessment</span>
343
+ </button>
344
+ </div>
345
+ </div>
346
+
347
+ {/* Tabs & Content */}
348
+ <div className="glass-card rounded-2xl p-6">
349
+ <div className="flex border-b border-slate-800 mb-6">
350
+ <button
351
+ onClick={() => setActiveTab("matched")}
352
+ className={`pb-3 px-4 text-xs font-semibold border-b-2 transition flex items-center space-x-2 ${
353
+ activeTab === "matched"
354
+ ? "border-emerald-500 text-emerald-400"
355
+ : "border-transparent text-slate-400 hover:text-slate-200"
356
+ }`}
357
+ >
358
+ <CheckCircle2 className="w-4 h-4" />
359
+ <span>Matched Requirements ({result.matched_requirements.length})</span>
360
+ </button>
361
+
362
+ <button
363
+ onClick={() => setActiveTab("missing")}
364
+ className={`pb-3 px-4 text-xs font-semibold border-b-2 transition flex items-center space-x-2 ${
365
+ activeTab === "missing"
366
+ ? "border-rose-500 text-rose-400"
367
+ : "border-transparent text-slate-400 hover:text-slate-200"
368
+ }`}
369
+ >
370
+ <AlertTriangle className="w-4 h-4" />
371
+ <span>Missing Keywords ({result.missing_requirements.length})</span>
372
+ </button>
373
+
374
+ <button
375
+ onClick={() => setActiveTab("suggestions")}
376
+ className={`pb-3 px-4 text-xs font-semibold border-b-2 transition flex items-center space-x-2 ${
377
+ activeTab === "suggestions"
378
+ ? "border-amber-500 text-amber-400"
379
+ : "border-transparent text-slate-400 hover:text-slate-200"
380
+ }`}
381
+ >
382
+ <Lightbulb className="w-4 h-4" />
383
+ <span>Rewrite Suggestions ({result.suggestions.length})</span>
384
+ </button>
385
+ </div>
386
+
387
+ {/* Tab Panels */}
388
+ {activeTab === "matched" && (
389
+ <div className="space-y-3">
390
+ {result.matched_requirements.length === 0 ? (
391
+ <p className="text-xs text-slate-500 italic">No matched requirements found.</p>
392
+ ) : (
393
+ result.matched_requirements.map((req, idx) => (
394
+ <div
395
+ key={idx}
396
+ className="p-3 rounded-xl bg-emerald-500/5 border border-emerald-500/20 text-slate-200 text-xs flex items-start space-x-3"
397
+ >
398
+ <CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0 mt-0.5" />
399
+ <span>{req}</span>
400
+ </div>
401
+ ))
402
+ )}
403
+ </div>
404
+ )}
405
+
406
+ {activeTab === "missing" && (
407
+ <div className="space-y-3">
408
+ {result.missing_requirements.length === 0 ? (
409
+ <p className="text-xs text-slate-500 italic">No missing requirements flagged!</p>
410
+ ) : (
411
+ result.missing_requirements.map((req, idx) => (
412
+ <div
413
+ key={idx}
414
+ className="p-3 rounded-xl bg-rose-500/5 border border-rose-500/20 text-slate-200 text-xs flex items-start space-x-3"
415
+ >
416
+ <AlertTriangle className="w-4 h-4 text-rose-400 shrink-0 mt-0.5" />
417
+ <span>{req}</span>
418
+ </div>
419
+ ))
420
+ )}
421
+ </div>
422
+ )}
423
+
424
+ {activeTab === "suggestions" && (
425
+ <div className="space-y-3">
426
+ {result.suggestions.length === 0 ? (
427
+ <p className="text-xs text-slate-500 italic">No rewrite suggestions generated.</p>
428
+ ) : (
429
+ result.suggestions.map((sug, idx) => (
430
+ <div
431
+ key={idx}
432
+ className="p-3 rounded-xl bg-amber-500/5 border border-amber-500/20 text-slate-200 text-xs flex items-start space-x-3"
433
+ >
434
+ <Lightbulb className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
435
+ <span>{sug}</span>
436
+ </div>
437
+ ))
438
+ )}
439
+ </div>
440
+ )}
441
+ </div>
442
+ </motion.div>
443
+ ) : (
444
+ <div className="glass-card rounded-2xl p-12 text-center border-dashed border-2 border-slate-800">
445
+ <div className="w-16 h-16 rounded-2xl bg-indigo-500/10 text-indigo-400 flex items-center justify-center mx-auto mb-4 border border-indigo-500/20">
446
+ <Sparkles className="w-8 h-8" />
447
+ </div>
448
+ <h3 className="text-lg font-bold text-white mb-2">Ready for Assessment</h3>
449
+ <p className="text-xs text-slate-400 max-w-sm mx-auto mb-6">
450
+ Upload a candidate resume and job description on the left, then click &quot;Start AI Assessment&quot; to generate match scores and recommendations.
451
+ </p>
452
+ <button
453
+ onClick={handleDemoScan}
454
+ className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-semibold text-slate-200 border border-slate-700 transition inline-flex items-center space-x-2"
455
+ >
456
+ <Zap className="w-3.5 h-3.5 text-amber-400" />
457
+ <span>Run Demo Benchmark Test</span>
458
+ </button>
459
+ </div>
460
+ )}
461
+ </AnimatePresence>
462
+ </div>
463
+ </div>
464
+ </main>
465
+ </div>
466
+ );
467
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }