Krina2005 commited on
Commit
29017a7
·
verified ·
1 Parent(s): 44151da

Upload 9 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .env
6
+ .git
7
+ .gitignore
.gitignore ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python cache
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+
7
+ # Environment
8
+ .env
9
+
10
+ # OS files
11
+ .DS_Store
12
+
13
+ # IDE
14
+ .vscode/
15
+ .idea/
16
+
17
+ # Logs
18
+ *.log
19
+
20
+ *.ipynb
21
+ *.pkl
22
+ *.h5
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # System dependency (needed for sklearn / numpy)
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ libgomp1 \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy requirements first (for caching)
11
+ COPY requirements.txt .
12
+
13
+ # Install Python dependencies
14
+ RUN pip install --no-cache-dir --upgrade pip \
15
+ && pip install --no-cache-dir -r requirements.txt
16
+
17
+ # Copy project files
18
+ COPY . .
19
+
20
+ # Pre-download SBERT model (VERY IMPORTANT)
21
+ RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')"
22
+
23
+ # Hugging Face port
24
+ EXPOSE 7860
25
+
26
+ # Run Flask app
27
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory
2
+ from model_utils import predict_match
3
+
4
+ app = Flask(__name__, static_folder="static")
5
+
6
+ # Serve frontend
7
+ @app.route("/")
8
+ def home():
9
+ return send_from_directory("static", "index.html")
10
+
11
+ # Prediction API
12
+ @app.route("/predict", methods=["POST"])
13
+ def predict():
14
+ data = request.get_json()
15
+
16
+ resume = data.get("resume", "")
17
+ job_description = data.get("job_description", "")
18
+
19
+ prob, verdict = predict_match(resume, job_description)
20
+
21
+ return jsonify({
22
+ "match_probability": float(prob),
23
+ "verdict": verdict
24
+ })
25
+
26
+ # Run (for Hugging Face)
27
+ if __name__ == "__main__":
28
+ app.run(host="0.0.0.0", port=7860)
model_utils.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import tensorflow as tf
3
+ import joblib
4
+ from sentence_transformers import SentenceTransformer
5
+ from sklearn.metrics.pairwise import cosine_similarity
6
+
7
+ model = tf.keras.models.load_model("models/resume_match_model.h5")
8
+ scaler = joblib.load("models/scaler.pkl")
9
+
10
+ embedding_model = SentenceTransformer("all-MiniLM-L6-v2", device="cpu")
11
+
12
+
13
+ def predict_match(resume, jd):
14
+
15
+ resume_emb = embedding_model.encode([resume])
16
+ jd_emb = embedding_model.encode([jd])
17
+
18
+ cos_sim = cosine_similarity(resume_emb, jd_emb)
19
+
20
+ features = np.concatenate([
21
+ resume_emb,
22
+ jd_emb,
23
+ np.abs(resume_emb - jd_emb),
24
+ resume_emb * jd_emb,
25
+ cos_sim.reshape(-1,1)
26
+ ], axis=1)
27
+
28
+ features_scaled = scaler.transform(features)
29
+
30
+ prob = model.predict(features_scaled)[0][0]
31
+
32
+ verdict = "Accepted" if prob > 0.5 else "Rejected"
33
+
34
+ return float(prob), verdict
models/resume_match_model.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:443aca6225a7d5a30973e6775a29b797a4d6e9a95d69e1f6c2db8e2bb9c54115
3
+ size 11570480
models/scaler.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fb3ba264a2fda206b6e96952d69aaf9502a24f513b3b21353b765c860faa61e1
3
+ size 37487
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ flask
2
+ tensorflow==2.12.0
3
+ sentence-transformers
4
+ scikit-learn
5
+ numpy
6
+ joblib
7
+ pandas
static/index.html ADDED
@@ -0,0 +1,844 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Resume Matcher</title>
7
+
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Righteous&family=Urbanist:wght@400;500;600;700&display=swap" rel="stylesheet">
11
+
12
+ <script src="https://unpkg.com/lucide@latest"></script>
13
+
14
+ <style>
15
+ :root {
16
+ --bg-color: #05050A;
17
+ --surface-color: rgba(255, 255, 255, 0.03);
18
+ --surface-border: rgba(255, 255, 255, 0.08);
19
+ --primary: #8B5CF6;
20
+ --secondary: #3B82F6;
21
+ --accent: #06B6D4;
22
+ --success: #10B981;
23
+ --danger: #EF4444;
24
+ --text-main: #FFFFFF;
25
+ --text-muted: #9CA3AF;
26
+ --font-display: 'Righteous', cursive;
27
+ --font-body: 'Urbanist', sans-serif;
28
+ }
29
+
30
+ * {
31
+ margin: 0;
32
+ padding: 0;
33
+ box-sizing: border-box;
34
+ }
35
+
36
+ html {
37
+ scroll-behavior: smooth;
38
+ }
39
+
40
+ body {
41
+ background-color: var(--bg-color);
42
+ color: var(--text-main);
43
+ font-family: var(--font-body);
44
+ min-height: 100vh;
45
+ overflow-x: hidden;
46
+ display: flex;
47
+ flex-direction: column;
48
+ align-items: center;
49
+ }
50
+
51
+ /* Custom Scrollbar */
52
+ ::-webkit-scrollbar { width: 8px; }
53
+ ::-webkit-scrollbar-track { background: var(--bg-color); }
54
+ ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 4px; }
55
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); }
56
+
57
+ /* --- Fast Liquid Background Elements --- */
58
+ .bg-elements {
59
+ position: fixed;
60
+ top: 0;
61
+ left: 0;
62
+ width: 100vw;
63
+ height: 100vh;
64
+ z-index: -1;
65
+ overflow: hidden;
66
+ background: #05050A;
67
+ }
68
+
69
+ /* Wrappers handle the fast sweeping across the screen behind the card */
70
+ .blob-wrapper {
71
+ position: absolute;
72
+ width: 100%;
73
+ height: 100%;
74
+ top: 0;
75
+ left: 0;
76
+ }
77
+
78
+ .wrapper-1 { animation: drift1 12s infinite alternate ease-in-out; }
79
+ .wrapper-2 { animation: drift2 14s infinite alternate ease-in-out; }
80
+ .wrapper-3 { animation: drift3 16s infinite alternate ease-in-out; }
81
+
82
+ /* Blobs handle the fluid water morphing */
83
+ .blob {
84
+ position: absolute;
85
+ filter: blur(90px);
86
+ opacity: 0.5;
87
+ mix-blend-mode: screen;
88
+ animation: waterMorph 10s infinite linear;
89
+ }
90
+
91
+ .blob-1 {
92
+ top: -10%; left: -10%;
93
+ width: 600px; height: 600px;
94
+ background: var(--primary);
95
+ animation-duration: 12s;
96
+ }
97
+
98
+ .blob-2 {
99
+ bottom: -20%; right: -10%;
100
+ width: 700px; height: 700px;
101
+ background: var(--secondary);
102
+ animation-duration: 14s;
103
+ animation-direction: reverse;
104
+ }
105
+
106
+ .blob-3 {
107
+ top: 20%; left: -20%;
108
+ width: 500px; height: 500px;
109
+ background: var(--accent);
110
+ opacity: 0.35;
111
+ animation-duration: 10s;
112
+ }
113
+
114
+ /* Liquid Morphing Keyframes */
115
+ @keyframes waterMorph {
116
+ 0% {
117
+ border-radius: 40% 60% 70% 30% / 40% 40% 60% 50%;
118
+ transform: rotate(0deg) scale(1);
119
+ }
120
+ 34% {
121
+ border-radius: 70% 30% 50% 50% / 30% 30% 70% 70%;
122
+ transform: rotate(120deg) scale(1.05);
123
+ }
124
+ 67% {
125
+ border-radius: 100% 60% 60% 100% / 100% 100% 60% 60%;
126
+ transform: rotate(240deg) scale(0.95);
127
+ }
128
+ 100% {
129
+ border-radius: 40% 60% 70% 30% / 40% 40% 60% 50%;
130
+ transform: rotate(360deg) scale(1);
131
+ }
132
+ }
133
+
134
+ /* Faster Drifting Keyframes that cross the center of the screen */
135
+ @keyframes drift1 {
136
+ 0% { transform: translate(0, 0); }
137
+ 100% { transform: translate(60vw, 40vh); } /* Sweeps top-left to bottom-right */
138
+ }
139
+ @keyframes drift2 {
140
+ 0% { transform: translate(0, 0); }
141
+ 100% { transform: translate(-50vw, -50vh); } /* Sweeps bottom-right to top-left */
142
+ }
143
+ @keyframes drift3 {
144
+ 0% { transform: translate(0, 0); }
145
+ 100% { transform: translate(80vw, 10vh); } /* Sweeps straight across the middle */
146
+ }
147
+
148
+ /* --- Layout --- */
149
+ .container {
150
+ width: 100%;
151
+ max-width: 1200px;
152
+ padding: 4rem 2rem 2rem 2rem;
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: 3rem;
156
+ z-index: 1;
157
+ flex: 1;
158
+ }
159
+
160
+ /* Hero Section */
161
+ header {
162
+ text-align: center;
163
+ display: flex;
164
+ flex-direction: column;
165
+ align-items: center;
166
+ gap: 1.5rem;
167
+ animation: slideDown 0.6s ease-out forwards;
168
+ }
169
+
170
+ .ai-badge {
171
+ display: inline-flex;
172
+ align-items: center;
173
+ gap: 0.5rem;
174
+ padding: 0.5rem 1rem;
175
+ background: rgba(139, 92, 246, 0.1);
176
+ border: 1px solid rgba(139, 92, 246, 0.3);
177
+ border-radius: 100px;
178
+ font-size: 0.875rem;
179
+ font-weight: 700;
180
+ color: #C4B5FD;
181
+ text-transform: uppercase;
182
+ letter-spacing: 1px;
183
+ box-shadow: 0 0 20px rgba(139, 92, 246, 0.2);
184
+ animation: pulse-glow 2s infinite;
185
+ }
186
+
187
+ @keyframes pulse-glow {
188
+ 0%, 100% { box-shadow: 0 0 15px rgba(139, 92, 246, 0.2); }
189
+ 50% { box-shadow: 0 0 25px rgba(139, 92, 246, 0.5); }
190
+ }
191
+
192
+ h1 {
193
+ font-family: var(--font-display);
194
+ font-size: 4.5rem;
195
+ line-height: 1.1;
196
+ letter-spacing: 0.02em;
197
+ background: linear-gradient(to right, #FFFFFF, #A78BFA, #06B6D4);
198
+ background-clip: text;
199
+ -webkit-background-clip: text;
200
+ -webkit-text-fill-color: transparent;
201
+ text-shadow: 0px 4px 20px rgba(139, 92, 246, 0.3);
202
+ }
203
+
204
+ .subtitle {
205
+ font-size: 1.25rem;
206
+ color: var(--text-muted);
207
+ max-width: 600px;
208
+ font-weight: 500;
209
+ }
210
+
211
+ /* Glass Card */
212
+ .glass-card {
213
+ background: var(--surface-color);
214
+ backdrop-filter: blur(24px);
215
+ -webkit-backdrop-filter: blur(24px);
216
+ border: 1px solid var(--surface-border);
217
+ border-radius: 24px;
218
+ padding: 2.5rem;
219
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
220
+ animation: fadeUp 0.8s ease-out forwards;
221
+ }
222
+
223
+ .grid-layout {
224
+ display: grid;
225
+ grid-template-columns: 1fr 1fr;
226
+ gap: 2rem;
227
+ }
228
+
229
+ /* Form Elements */
230
+ .input-group {
231
+ display: flex;
232
+ flex-direction: column;
233
+ gap: 1rem;
234
+ position: relative;
235
+ }
236
+
237
+ .input-header {
238
+ display: flex;
239
+ justify-content: space-between;
240
+ align-items: center;
241
+ }
242
+
243
+ label {
244
+ font-family: var(--font-display);
245
+ font-size: 1.2rem;
246
+ letter-spacing: 1px;
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 0.5rem;
250
+ }
251
+
252
+ label i { color: var(--accent); }
253
+
254
+ .char-count {
255
+ font-size: 0.9rem;
256
+ color: var(--text-muted);
257
+ font-weight: 600;
258
+ }
259
+
260
+ textarea {
261
+ width: 100%;
262
+ height: 350px;
263
+ background: rgba(0, 0, 0, 0.3);
264
+ border: 1px solid var(--surface-border);
265
+ border-radius: 16px;
266
+ padding: 1.5rem;
267
+ color: var(--text-main);
268
+ font-family: var(--font-body);
269
+ font-size: 1rem;
270
+ line-height: 1.6;
271
+ resize: none;
272
+ transition: all 0.3s ease;
273
+ }
274
+
275
+ textarea:focus {
276
+ outline: none;
277
+ border-color: var(--primary);
278
+ box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.15);
279
+ background: rgba(0, 0, 0, 0.5);
280
+ }
281
+
282
+ textarea::placeholder { color: #4B5563; }
283
+
284
+ /* Controls */
285
+ .controls {
286
+ margin-top: 2.5rem;
287
+ display: flex;
288
+ align-items: center;
289
+ justify-content: space-between;
290
+ padding-top: 2rem;
291
+ border-top: 1px solid var(--surface-border);
292
+ }
293
+
294
+ .keyboard-hint {
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 0.5rem;
298
+ color: var(--text-muted);
299
+ font-size: 0.9rem;
300
+ font-weight: 600;
301
+ }
302
+
303
+ kbd {
304
+ background: rgba(255, 255, 255, 0.1);
305
+ padding: 0.3rem 0.6rem;
306
+ border-radius: 6px;
307
+ font-family: monospace;
308
+ font-size: 0.85rem;
309
+ color: var(--text-main);
310
+ }
311
+
312
+ .buttons { display: flex; gap: 1rem; }
313
+
314
+ button {
315
+ border: none;
316
+ cursor: pointer;
317
+ font-family: var(--font-display);
318
+ letter-spacing: 1px;
319
+ font-size: 1.1rem;
320
+ padding: 1rem 2rem;
321
+ border-radius: 12px;
322
+ display: flex;
323
+ align-items: center;
324
+ gap: 0.5rem;
325
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
326
+ }
327
+
328
+ .btn-clear {
329
+ background: transparent;
330
+ color: var(--text-muted);
331
+ border: 1px solid var(--surface-border);
332
+ }
333
+
334
+ .btn-clear:hover {
335
+ color: var(--text-main);
336
+ background: rgba(255, 255, 255, 0.05);
337
+ transform: scale(1.05);
338
+ }
339
+
340
+ .btn-analyze {
341
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
342
+ color: white;
343
+ position: relative;
344
+ overflow: hidden;
345
+ box-shadow: 0 10px 20px -10px rgba(139, 92, 246, 0.5);
346
+ }
347
+
348
+ .btn-analyze::before {
349
+ content: '';
350
+ position: absolute;
351
+ top: 0; left: -100%;
352
+ width: 100%; height: 100%;
353
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
354
+ transition: left 0.5s ease;
355
+ }
356
+
357
+ .btn-analyze:hover {
358
+ transform: translateY(-4px) scale(1.02);
359
+ box-shadow: 0 15px 25px -10px rgba(139, 92, 246, 0.7);
360
+ }
361
+
362
+ .btn-analyze:hover::before { left: 100%; }
363
+
364
+ /* Error States */
365
+ .error-msg {
366
+ color: var(--danger);
367
+ font-size: 0.9rem;
368
+ font-weight: 600;
369
+ position: absolute;
370
+ bottom: -25px; left: 0;
371
+ opacity: 0;
372
+ transform: translateY(-5px);
373
+ transition: all 0.3s ease;
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 0.4rem;
377
+ }
378
+
379
+ .has-error .error-msg { opacity: 1; transform: translateY(0); }
380
+ .has-error textarea {
381
+ border-color: rgba(239, 68, 68, 0.5);
382
+ background: rgba(239, 68, 68, 0.02);
383
+ }
384
+
385
+ /* Loader */
386
+ .loader-container {
387
+ display: none;
388
+ flex-direction: column;
389
+ align-items: center;
390
+ justify-content: center;
391
+ padding: 4rem 0;
392
+ gap: 1.5rem;
393
+ }
394
+
395
+ .spinner {
396
+ width: 54px; height: 54px;
397
+ border: 4px solid rgba(255, 255, 255, 0.1);
398
+ border-radius: 50%;
399
+ border-top-color: var(--accent);
400
+ animation: spin 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
401
+ }
402
+
403
+ .loader-text {
404
+ font-family: var(--font-display);
405
+ color: var(--accent);
406
+ letter-spacing: 3px;
407
+ font-size: 1.2rem;
408
+ animation: pulse 2s infinite;
409
+ }
410
+
411
+ /* Results Section */
412
+ .results-container {
413
+ display: none;
414
+ margin-top: 2rem;
415
+ animation: fadeUp 0.6s ease-out forwards;
416
+ }
417
+
418
+ .result-card {
419
+ background: linear-gradient(180deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.01) 100%);
420
+ border-radius: 24px;
421
+ padding: 3rem;
422
+ border: 1px solid var(--surface-border);
423
+ display: flex;
424
+ align-items: center;
425
+ justify-content: center;
426
+ gap: 4rem;
427
+ position: relative;
428
+ overflow: hidden;
429
+ }
430
+
431
+ .result-card::before {
432
+ content: '';
433
+ position: absolute;
434
+ top: 0; left: 0;
435
+ width: 100%; height: 5px;
436
+ background: var(--bg-glow);
437
+ transition: background 0.5s ease;
438
+ }
439
+
440
+ .score-circle {
441
+ position: relative;
442
+ width: 220px; height: 220px;
443
+ }
444
+
445
+ .score-circle svg {
446
+ width: 100%; height: 100%;
447
+ transform: rotate(-90deg);
448
+ }
449
+
450
+ .score-circle circle {
451
+ fill: none;
452
+ stroke-width: 10;
453
+ stroke-linecap: round;
454
+ }
455
+
456
+ .circle-bg { stroke: rgba(255, 255, 255, 0.05); }
457
+
458
+ .circle-progress {
459
+ stroke: var(--circle-color);
460
+ stroke-dasharray: 628;
461
+ stroke-dashoffset: 628;
462
+ transition: stroke-dashoffset 1.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
463
+ }
464
+
465
+ .score-value {
466
+ position: absolute;
467
+ top: 50%; left: 50%;
468
+ transform: translate(-50%, -50%);
469
+ font-family: var(--font-display);
470
+ font-size: 4rem;
471
+ color: var(--text-main);
472
+ }
473
+
474
+ .verdict-info {
475
+ display: flex;
476
+ flex-direction: column;
477
+ gap: 1rem;
478
+ }
479
+
480
+ .verdict-label {
481
+ color: var(--text-muted);
482
+ text-transform: uppercase;
483
+ letter-spacing: 3px;
484
+ font-size: 0.9rem;
485
+ font-weight: 700;
486
+ }
487
+
488
+ .verdict-text {
489
+ font-family: var(--font-display);
490
+ font-size: 3rem;
491
+ display: flex;
492
+ align-items: center;
493
+ gap: 1rem;
494
+ text-shadow: 0 0 25px var(--bg-glow);
495
+ animation: bounceIn 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
496
+ }
497
+
498
+ /* Footer */
499
+ footer {
500
+ width: 100%;
501
+ padding: 2rem 0;
502
+ display: flex;
503
+ flex-direction: column;
504
+ align-items: center;
505
+ gap: 1rem;
506
+ margin-top: auto;
507
+ opacity: 0;
508
+ animation: fadeUp 1s ease-out forwards;
509
+ animation-delay: 0.5s;
510
+ }
511
+
512
+ .footer-text {
513
+ color: var(--text-muted);
514
+ font-size: 1rem;
515
+ display: flex;
516
+ align-items: center;
517
+ gap: 0.5rem;
518
+ }
519
+
520
+ .heart-icon {
521
+ color: var(--danger);
522
+ animation: heartbeat 1.5s infinite;
523
+ }
524
+
525
+ .github-link {
526
+ color: var(--text-main);
527
+ background: rgba(255, 255, 255, 0.05);
528
+ padding: 14px;
529
+ border-radius: 50%;
530
+ display: flex;
531
+ align-items: center;
532
+ justify-content: center;
533
+ border: 1px solid var(--surface-border);
534
+ text-decoration: none;
535
+ transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
536
+ }
537
+
538
+ .github-link:hover {
539
+ transform: translateY(-8px) rotate(10deg) scale(1.1);
540
+ background: #ffffff;
541
+ color: #000000;
542
+ border-color: #ffffff;
543
+ box-shadow: 0 10px 25px rgba(255, 255, 255, 0.2);
544
+ }
545
+
546
+ /* Keyframes */
547
+ @keyframes slideDown {
548
+ from { opacity: 0; transform: translateY(-40px); }
549
+ to { opacity: 1; transform: translateY(0); }
550
+ }
551
+ @keyframes fadeUp {
552
+ from { opacity: 0; transform: translateY(40px); }
553
+ to { opacity: 1; transform: translateY(0); }
554
+ }
555
+ @keyframes spin { to { transform: rotate(360deg); } }
556
+ @keyframes pulse {
557
+ 0%, 100% { opacity: 1; transform: scale(1); }
558
+ 50% { opacity: 0.7; transform: scale(1.05); }
559
+ }
560
+ @keyframes bounceIn {
561
+ 0% { transform: scale(0.5); opacity: 0; }
562
+ 80% { transform: scale(1.05); opacity: 1; }
563
+ 100% { transform: scale(1); opacity: 1; }
564
+ }
565
+ @keyframes heartbeat {
566
+ 0%, 100% { transform: scale(1); }
567
+ 25% { transform: scale(1.2); }
568
+ 50% { transform: scale(1); }
569
+ 75% { transform: scale(1.2); }
570
+ }
571
+
572
+ /* --- Deep Responsiveness --- */
573
+ @media (max-width: 1024px) {
574
+ h1 { font-size: 3.5rem; }
575
+ textarea { height: 280px; }
576
+ .glass-card { padding: 2rem; }
577
+ }
578
+
579
+ @media (max-width: 768px) {
580
+ .container { padding: 2rem 1rem 1rem 1rem; gap: 2rem; }
581
+ h1 { font-size: 2.8rem; }
582
+ .subtitle { font-size: 1.1rem; }
583
+ .grid-layout { grid-template-columns: 1fr; gap: 1.5rem; }
584
+ textarea { height: 220px; }
585
+ .controls { flex-direction: column; gap: 1.5rem; padding-top: 1.5rem; }
586
+ .keyboard-hint { display: none; } /* Hide shortcut on mobile */
587
+ .buttons { width: 100%; display: grid; grid-template-columns: 1fr 2fr; }
588
+ button { justify-content: center; font-size: 1rem; padding: 0.8rem 1rem; }
589
+
590
+ /* Responsive Result Card */
591
+ .result-card { flex-direction: column; text-align: center; gap: 2rem; padding: 2rem 1rem; }
592
+ .verdict-text { justify-content: center; font-size: 2.5rem; }
593
+ .score-circle { width: 180px; height: 180px; }
594
+ .score-value { font-size: 3.2rem; }
595
+ }
596
+
597
+ @media (max-width: 480px) {
598
+ h1 { font-size: 2.2rem; }
599
+ .glass-card { padding: 1.25rem; border-radius: 16px; }
600
+ .buttons { grid-template-columns: 1fr; } /* Stack buttons on very small screens */
601
+ .verdict-text { font-size: 2rem; }
602
+ .verdict-info { gap: 0.5rem; }
603
+ }
604
+ </style>
605
+ </head>
606
+ <body>
607
+
608
+ <div class="bg-elements">
609
+ <div class="blob-wrapper wrapper-1">
610
+ <div class="blob blob-1"></div>
611
+ </div>
612
+ <div class="blob-wrapper wrapper-2">
613
+ <div class="blob blob-2"></div>
614
+ </div>
615
+ <div class="blob-wrapper wrapper-3">
616
+ <div class="blob blob-3"></div>
617
+ </div>
618
+ </div>
619
+
620
+ <main class="container">
621
+ <header>
622
+ <div class="ai-badge">
623
+ <i data-lucide="sparkles"></i> AI Powered
624
+ </div>
625
+ <h1>Resume Matcher</h1>
626
+ <p class="subtitle">See exactly how perfectly your resume aligns with any job description in seconds.</p>
627
+ </header>
628
+
629
+ <div class="glass-card">
630
+ <div class="grid-layout">
631
+ <div class="input-group" id="resumeGroup">
632
+ <div class="input-header">
633
+ <label><i data-lucide="file-text"></i> Your Resume</label>
634
+ <span class="char-count" id="resumeCount">0 chars</span>
635
+ </div>
636
+ <textarea id="resumeInput" placeholder="Paste your plain text resume content here..."></textarea>
637
+ <span class="error-msg"><i data-lucide="alert-triangle" width="16" height="16"></i> Resume is required</span>
638
+ </div>
639
+
640
+ <div class="input-group" id="jobGroup">
641
+ <div class="input-header">
642
+ <label><i data-lucide="briefcase"></i> Job Description</label>
643
+ <span class="char-count" id="jobCount">0 chars</span>
644
+ </div>
645
+ <textarea id="jobInput" placeholder="Paste the target job description here..."></textarea>
646
+ <span class="error-msg"><i data-lucide="alert-triangle" width="16" height="16"></i> Job description is required</span>
647
+ </div>
648
+ </div>
649
+
650
+ <div class="controls">
651
+ <div class="keyboard-hint">
652
+ <kbd>Ctrl</kbd> + <kbd>Enter</kbd> to analyze
653
+ </div>
654
+ <div class="buttons">
655
+ <button class="btn-clear" id="clearBtn">
656
+ <i data-lucide="rotate-ccw" width="20" height="20"></i> Clear
657
+ </button>
658
+ <button class="btn-analyze" id="analyzeBtn">
659
+ <i data-lucide="zap" width="20" height="20"></i> Analyze Match
660
+ </button>
661
+ </div>
662
+ </div>
663
+
664
+ <div class="loader-container" id="loader">
665
+ <div class="spinner"></div>
666
+ <div class="loader-text">CRUNCHING DATA...</div>
667
+ </div>
668
+
669
+ <div class="results-container" id="results">
670
+ <div class="result-card" id="resultCard">
671
+ <div class="score-circle">
672
+ <svg viewBox="0 0 220 220">
673
+ <circle class="circle-bg" cx="110" cy="110" r="100"></circle>
674
+ <circle class="circle-progress" id="progressCircle" cx="110" cy="110" r="100"></circle>
675
+ </svg>
676
+ <div class="score-value" id="scoreValue">0.00</div>
677
+ </div>
678
+ <div class="verdict-info">
679
+ <div class="verdict-label">AI Match Verdict</div>
680
+ <div class="verdict-text" id="verdictText">
681
+ </div>
682
+ </div>
683
+ </div>
684
+ </div>
685
+ </div>
686
+ </main>
687
+
688
+ <footer>
689
+ <div class="footer-text">
690
+ Built with <i data-lucide="heart" class="heart-icon" width="18" height="18"></i> with Flask • TensorFlow • NLP
691
+ </div>
692
+ <a href="https://github.com/Nishi1195/ResumeMatching-DLmodel" target="_blank" class="github-link" aria-label="GitHub Profile">
693
+ <i data-lucide="github" width="24" height="24"></i>
694
+ </a>
695
+ </footer>
696
+
697
+ <script>
698
+ lucide.createIcons();
699
+
700
+ // DOM Elements
701
+ const resumeInput = document.getElementById('resumeInput');
702
+ const jobInput = document.getElementById('jobInput');
703
+ const resumeCount = document.getElementById('resumeCount');
704
+ const jobCount = document.getElementById('jobCount');
705
+ const analyzeBtn = document.getElementById('analyzeBtn');
706
+ const clearBtn = document.getElementById('clearBtn');
707
+ const loader = document.getElementById('loader');
708
+ const results = document.getElementById('results');
709
+ const progressCircle = document.getElementById('progressCircle');
710
+ const scoreValue = document.getElementById('scoreValue');
711
+ const verdictText = document.getElementById('verdictText');
712
+ const resultCard = document.getElementById('resultCard');
713
+ const resumeGroup = document.getElementById('resumeGroup');
714
+ const jobGroup = document.getElementById('jobGroup');
715
+
716
+ // Character Counters
717
+ const updateCount = (input, counter) => {
718
+ counter.textContent = `${input.value.length.toLocaleString()} chars`;
719
+ };
720
+ resumeInput.addEventListener('input', () => updateCount(resumeInput, resumeCount));
721
+ jobInput.addEventListener('input', () => updateCount(jobInput, jobCount));
722
+
723
+ // Error handling
724
+ const clearErrors = () => {
725
+ resumeGroup.classList.remove('has-error');
726
+ jobGroup.classList.remove('has-error');
727
+ };
728
+
729
+ // 🚀 REAL API CALL (FIXED)
730
+ const analyzeMatch = async () => {
731
+ const resume = resumeInput.value.trim();
732
+ const jobDesc = jobInput.value.trim();
733
+ let hasError = false;
734
+
735
+ clearErrors();
736
+
737
+ if (!resume) { resumeGroup.classList.add('has-error'); hasError = true; }
738
+ if (!jobDesc) { jobGroup.classList.add('has-error'); hasError = true; }
739
+ if (hasError) return;
740
+
741
+ results.style.display = 'none';
742
+ loader.style.display = 'flex';
743
+ analyzeBtn.disabled = true;
744
+ analyzeBtn.style.opacity = '0.7';
745
+
746
+ loader.scrollIntoView({ behavior: 'smooth', block: 'center' });
747
+
748
+ try {
749
+ const response = await fetch("/predict", {
750
+ method: "POST",
751
+ headers: {
752
+ "Content-Type": "application/json"
753
+ },
754
+ body: JSON.stringify({
755
+ resume: resume,
756
+ job_description: jobDesc
757
+ })
758
+ });
759
+
760
+ if (!response.ok) {
761
+ throw new Error("Server error");
762
+ }
763
+
764
+ const data = await response.json();
765
+
766
+ updateResultsUI(data);
767
+
768
+ } catch (error) {
769
+ alert("⚠️ Failed to connect to backend. Please try again.");
770
+ } finally {
771
+ loader.style.display = 'none';
772
+ analyzeBtn.disabled = false;
773
+ analyzeBtn.style.opacity = '1';
774
+ }
775
+ };
776
+
777
+ // UI Update
778
+ const updateResultsUI = (data) => {
779
+ results.style.display = 'block';
780
+
781
+ const isAccepted = data.verdict === 'Accepted';
782
+ const color = isAccepted ? 'var(--success)' : 'var(--danger)';
783
+ const icon = isAccepted ? 'check-circle-2' : 'x-octagon';
784
+
785
+ resultCard.style.setProperty('--bg-glow', color);
786
+ resultCard.style.setProperty('--circle-color', color);
787
+
788
+ scoreValue.textContent = "0.000";
789
+ scoreValue.style.color = color;
790
+
791
+ let startTime = performance.now();
792
+ const duration = 1500;
793
+ const end = data.match_probability;
794
+
795
+ const animateScore = (currentTime) => {
796
+ const progress = Math.min((currentTime - startTime) / duration, 1);
797
+ const easeOut = 1 - Math.pow(1 - progress, 4);
798
+ scoreValue.textContent = (end * easeOut).toFixed(3);
799
+ if (progress < 1) requestAnimationFrame(animateScore);
800
+ };
801
+
802
+ requestAnimationFrame(animateScore);
803
+
804
+ setTimeout(() => {
805
+ const circumference = 628;
806
+ progressCircle.style.strokeDashoffset = circumference - (end * circumference);
807
+ }, 100);
808
+
809
+ verdictText.innerHTML = `
810
+ <i data-lucide="${icon}" width="42" height="42" style="color: ${color}"></i>
811
+ <span style="color: ${color}">${data.verdict}</span>
812
+ `;
813
+ lucide.createIcons();
814
+
815
+ setTimeout(() => {
816
+ const y = results.getBoundingClientRect().top + window.scrollY - 50;
817
+ window.scrollTo({ top: y, behavior: 'smooth' });
818
+ }, 150);
819
+ };
820
+
821
+ // Events
822
+ analyzeBtn.addEventListener('click', analyzeMatch);
823
+
824
+ clearBtn.addEventListener('click', () => {
825
+ resumeInput.value = '';
826
+ jobInput.value = '';
827
+ updateCount(resumeInput, resumeCount);
828
+ updateCount(jobInput, jobCount);
829
+ results.style.display = 'none';
830
+ clearErrors();
831
+ progressCircle.style.strokeDashoffset = 628;
832
+ window.scrollTo({ top: 0, behavior: 'smooth' });
833
+ });
834
+
835
+ document.addEventListener('keydown', (e) => {
836
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') analyzeMatch();
837
+ });
838
+
839
+ resumeInput.addEventListener('input', () => resumeGroup.classList.remove('has-error'));
840
+ jobInput.addEventListener('input', () => jobGroup.classList.remove('has-error'));
841
+
842
+ </script>
843
+ </body>
844
+ </html>