michael-agent 1.0.2__py3-none-any.whl → 1.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1032 @@
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>Resume Scoring - SmartRecruitAgent</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
9
+ <link rel="stylesheet" href="/static/styles.css">
10
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
11
+ </head>
12
+ <body>
13
+ <header class="bg-dark text-white p-3">
14
+ <div class="container">
15
+ <div class="d-flex justify-content-between align-items-center">
16
+ <h1 class="h3 mb-0">SmartRecruitAgent - Resume Scoring</h1>
17
+ <div>
18
+ <a href="/career-portal" class="btn btn-outline-light me-2">Career Portal</a>
19
+ <a href="/" class="btn btn-outline-light">Dashboard</a>
20
+ </div>
21
+ </div>
22
+ </div>
23
+ </header>
24
+
25
+ <div class="container mt-4">
26
+ <div class="row">
27
+ <!-- Filters and Job Selection -->
28
+ <div class="col-md-3">
29
+ <div class="card mb-4">
30
+ <div class="card-header bg-primary text-white">
31
+ Job Description
32
+ </div>
33
+ <div class="card-body">
34
+ <div class="mb-3">
35
+ <label for="job-select" class="form-label">Select Job</label>
36
+ <select class="form-select" id="job-select">
37
+ <option value="" selected>Loading jobs...</option>
38
+ </select>
39
+ </div>
40
+ <div id="job-details-container" class="d-none">
41
+ <h5 id="job-title" class="mt-3"></h5>
42
+ <p id="job-location" class="mb-1 text-muted"></p>
43
+ <div class="mb-2">
44
+ <span class="badge bg-info me-1" id="job-type"></span>
45
+ <span class="badge bg-secondary" id="job-level"></span>
46
+ </div>
47
+ <p class="mb-2 small">Required Skills:</p>
48
+ <div id="required-skills-container" class="mb-3 d-flex flex-wrap gap-1"></div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="card mb-4">
54
+ <div class="card-header bg-primary text-white">
55
+ Filters
56
+ </div>
57
+ <div class="card-body">
58
+ <div class="mb-3">
59
+ <label for="score-filter" class="form-label">Minimum Score</label>
60
+ <input type="range" class="form-range" id="score-filter" min="0" max="100" value="50">
61
+ <div class="d-flex justify-content-between">
62
+ <span>0</span>
63
+ <span id="score-value">50</span>
64
+ <span>100</span>
65
+ </div>
66
+ </div>
67
+ <div class="mb-3">
68
+ <label class="form-label d-block">Status</label>
69
+ <div class="form-check form-check-inline">
70
+ <input class="form-check-input" type="checkbox" id="status-new" value="new" checked>
71
+ <label class="form-check-label" for="status-new">New</label>
72
+ </div>
73
+ <div class="form-check form-check-inline">
74
+ <input class="form-check-input" type="checkbox" id="status-reviewed" value="reviewed" checked>
75
+ <label class="form-check-label" for="status-reviewed">Reviewed</label>
76
+ </div>
77
+ </div>
78
+ <div class="d-grid">
79
+ <button id="apply-filters" class="btn btn-primary">Apply Filters</button>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <div class="card">
85
+ <div class="card-header bg-primary text-white">
86
+ Statistics
87
+ </div>
88
+ <div class="card-body">
89
+ <canvas id="score-distribution-chart"></canvas>
90
+ <div class="mt-3 text-center">
91
+ <div class="d-flex justify-content-between">
92
+ <div>
93
+ <h6 class="mb-0" id="total-resumes">0</h6>
94
+ <small class="text-muted">Total</small>
95
+ </div>
96
+ <div>
97
+ <h6 class="mb-0 text-success" id="qualified-resumes">0</h6>
98
+ <small class="text-muted">Qualified</small>
99
+ </div>
100
+ <div>
101
+ <h6 class="mb-0 text-danger" id="unqualified-resumes">0</h6>
102
+ <small class="text-muted">Unqualified</small>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Resume List and Details -->
111
+ <div class="col-md-9">
112
+ <div class="card mb-4">
113
+ <div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
114
+ <span>Scored Resumes</span>
115
+ <div>
116
+ <button class="btn btn-sm btn-light" id="export-csv">Export CSV</button>
117
+ <button class="btn btn-sm btn-light ms-2" id="refresh-data">Refresh</button>
118
+ </div>
119
+ </div>
120
+ <div class="card-body">
121
+ <div class="table-responsive">
122
+ <table class="table table-hover">
123
+ <thead>
124
+ <tr>
125
+ <th>Candidate</th>
126
+ <th>Date</th>
127
+ <th>Score</th>
128
+ <th>Skills Match</th>
129
+ <th>Status</th>
130
+ <th>Actions</th>
131
+ </tr>
132
+ </thead>
133
+ <tbody id="resumes-table-body">
134
+ <tr>
135
+ <td colspan="6" class="text-center">Select a job to view candidates</td>
136
+ </tr>
137
+ </tbody>
138
+ </table>
139
+ </div>
140
+ </div>
141
+ </div>
142
+
143
+ <div class="card" id="resume-details-card" style="display: none;">
144
+ <div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
145
+ <span>Candidate Details</span>
146
+ <button type="button" class="btn-close btn-close-white" id="close-resume-details"></button>
147
+ </div>
148
+ <div class="card-body">
149
+ <div class="row">
150
+ <div class="col-md-8">
151
+ <h4 id="candidate-name"></h4>
152
+ <div class="d-flex align-items-center mb-3">
153
+ <div id="candidate-score-badge" class="me-3"></div>
154
+ <div id="candidate-contact" class="small text-muted"></div>
155
+ </div>
156
+
157
+ <div class="mb-3">
158
+ <h5>Skills Analysis</h5>
159
+ <div id="skills-match-container"></div>
160
+ </div>
161
+
162
+ <div class="mb-3">
163
+ <h5>Experience</h5>
164
+ <div id="experience-container"></div>
165
+ </div>
166
+
167
+ <div>
168
+ <h5>Education</h5>
169
+ <div id="education-container"></div>
170
+ </div>
171
+ </div>
172
+ <div class="col-md-4">
173
+ <div class="card border-info mb-3">
174
+ <div class="card-header bg-light">AI Analysis</div>
175
+ <div class="card-body">
176
+ <div id="ai-analysis-container"></div>
177
+ </div>
178
+ </div>
179
+
180
+ <div class="card border-success">
181
+ <div class="card-header bg-light">Actions</div>
182
+ <div class="card-body">
183
+ <div class="d-grid gap-2">
184
+ <button class="btn btn-outline-primary" id="view-resume-btn">
185
+ <i class="bi bi-file-earmark-text"></i> View Full Resume
186
+ </button>
187
+ <button class="btn btn-outline-success" id="schedule-interview-btn">
188
+ <i class="bi bi-calendar2-check"></i> Schedule Interview
189
+ </button>
190
+ <button class="btn btn-outline-warning" id="generate-questions-btn">
191
+ <i class="bi bi-question-circle"></i> Generate Questions
192
+ </button>
193
+ <div class="input-group">
194
+ <select class="form-select" id="status-update">
195
+ <option selected>Change Status...</option>
196
+ <option value="new">New</option>
197
+ <option value="shortlisted">Shortlist</option>
198
+ <option value="interviewed">Interviewed</option>
199
+ <option value="rejected">Reject</option>
200
+ <option value="hired">Hire</option>
201
+ </select>
202
+ <button class="btn btn-outline-secondary" id="update-status-btn">Update</button>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ <!-- Modal for Interview Questions -->
216
+ <div class="modal fade" id="questions-modal" tabindex="-1" aria-hidden="true">
217
+ <div class="modal-dialog modal-lg">
218
+ <div class="modal-content">
219
+ <div class="modal-header bg-primary text-white">
220
+ <h5 class="modal-title">Interview Questions</h5>
221
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
222
+ </div>
223
+ <div class="modal-body" id="questions-container">
224
+ <p class="text-center">Generating personalized questions...</p>
225
+ </div>
226
+ <div class="modal-footer">
227
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
228
+ <button type="button" class="btn btn-primary" id="copy-questions-btn">Copy All</button>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
235
+ <script>
236
+ document.addEventListener('DOMContentLoaded', function() {
237
+ // Chart initialization
238
+ const scoreChart = new Chart(document.getElementById('score-distribution-chart'), {
239
+ type: 'doughnut',
240
+ data: {
241
+ labels: ['Below 50', '50-70', '70-85', '85-100'],
242
+ datasets: [{
243
+ data: [0, 0, 0, 0],
244
+ backgroundColor: ['#dc3545', '#ffc107', '#0dcaf0', '#198754']
245
+ }]
246
+ },
247
+ options: {
248
+ responsive: true,
249
+ plugins: {
250
+ legend: {
251
+ position: 'bottom',
252
+ labels: {
253
+ font: {
254
+ size: 10
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+ });
261
+
262
+ // Score filter range functionality
263
+ const scoreFilter = document.getElementById('score-filter');
264
+ const scoreValue = document.getElementById('score-value');
265
+
266
+ scoreFilter.addEventListener('input', function() {
267
+ scoreValue.textContent = this.value;
268
+ });
269
+
270
+ // Load job listings
271
+ fetch('/api/jobs')
272
+ .then(response => response.json())
273
+ .then(data => {
274
+ const jobSelect = document.getElementById('job-select');
275
+ jobSelect.innerHTML = '';
276
+
277
+ if (data.jobs.length === 0) {
278
+ const option = document.createElement('option');
279
+ option.value = '';
280
+ option.textContent = 'No jobs available';
281
+ jobSelect.appendChild(option);
282
+ } else {
283
+ const defaultOption = document.createElement('option');
284
+ defaultOption.value = '';
285
+ defaultOption.textContent = 'Select a job';
286
+ jobSelect.appendChild(defaultOption);
287
+
288
+ data.jobs.forEach(job => {
289
+ const option = document.createElement('option');
290
+ option.value = job.id;
291
+ option.textContent = job.title;
292
+ jobSelect.appendChild(option);
293
+ });
294
+ }
295
+ })
296
+ .catch(error => console.error('Error loading jobs:', error));
297
+
298
+ // Job selection change handler
299
+ document.getElementById('job-select').addEventListener('change', function() {
300
+ const jobId = this.value;
301
+
302
+ if (!jobId) {
303
+ document.getElementById('job-details-container').classList.add('d-none');
304
+ document.getElementById('resumes-table-body').innerHTML =
305
+ '<tr><td colspan="6" class="text-center">Select a job to view candidates</td></tr>';
306
+ return;
307
+ }
308
+
309
+ // Load job details
310
+ fetch(`/api/jobs/${jobId}`)
311
+ .then(response => response.json())
312
+ .then(job => {
313
+ document.getElementById('job-title').textContent = job.title;
314
+ document.getElementById('job-location').textContent = job.location;
315
+ document.getElementById('job-type').textContent = job.employment_type;
316
+ document.getElementById('job-level').textContent = job.experience_level;
317
+
318
+ // Display required skills
319
+ const skillsContainer = document.getElementById('required-skills-container');
320
+ skillsContainer.innerHTML = '';
321
+ job.required_skills.forEach(skill => {
322
+ const badge = document.createElement('span');
323
+ badge.className = 'badge bg-light text-dark';
324
+ badge.textContent = skill;
325
+ skillsContainer.appendChild(badge);
326
+ });
327
+
328
+ document.getElementById('job-details-container').classList.remove('d-none');
329
+
330
+ // Load candidates for this job
331
+ loadCandidates(jobId);
332
+ })
333
+ .catch(error => console.error('Error loading job details:', error));
334
+ });
335
+
336
+ // Load candidates for a specific job
337
+ function loadCandidates(jobId, filters = {}) {
338
+ fetch(`/api/candidates?job_id=${jobId}${getFilterQueryString(filters)}`)
339
+ .then(response => response.json())
340
+ .then(data => {
341
+ renderCandidatesTable(data.candidates);
342
+ updateStatistics(data.statistics);
343
+ })
344
+ .catch(error => console.error('Error loading candidates:', error));
345
+ }
346
+
347
+ // Helper to build query string from filters
348
+ function getFilterQueryString(filters) {
349
+ const params = [];
350
+ if (filters.minScore) params.push(`min_score=${filters.minScore}`);
351
+ if (filters.status && filters.status.length) {
352
+ params.push(`status=${filters.status.join(',')}`);
353
+ }
354
+ return params.length ? '&' + params.join('&') : '';
355
+ }
356
+
357
+ // Format score for display
358
+ function formatScore(score) {
359
+ if (score === undefined || score === null) {
360
+ return "N/A";
361
+ }
362
+ return (parseFloat(score) * 100).toFixed(1) + "%";
363
+ }
364
+
365
+ // Render candidates table
366
+ function renderCandidatesTable(candidates) {
367
+ const tableBody = document.getElementById('resumes-table-body');
368
+ tableBody.innerHTML = '';
369
+
370
+ if (candidates.length === 0) {
371
+ tableBody.innerHTML = '<tr><td colspan="6" class="text-center">No candidates found</td></tr>';
372
+ return;
373
+ }
374
+
375
+ candidates.forEach(candidate => {
376
+ const row = document.createElement('tr');
377
+ row.className = 'candidate-row';
378
+ row.setAttribute('data-id', candidate.id);
379
+
380
+ // Score color class
381
+ const scoreClass = getScoreClass(candidate.score);
382
+
383
+ // Format date
384
+ const date = new Date(candidate.date_processed);
385
+ const formattedDate = date.toLocaleDateString();
386
+
387
+ row.innerHTML = `
388
+ <td>
389
+ <div class="fw-bold">${candidate.name}</div>
390
+ <div class="small text-muted">${candidate.email || 'No email provided'}</div>
391
+ </td>
392
+ <td>${formattedDate}</td>
393
+ <td>
394
+ <div class="d-flex align-items-center">
395
+ <div class="progress flex-grow-1 me-2" style="height: 8px;">
396
+ <div class="progress-bar ${scoreClass}" style="width: ${candidate.score}%;"></div>
397
+ </div>
398
+ <span class="small ${scoreClass}-text">${candidate.score}%</span>
399
+ </div>
400
+ </td>
401
+ <td>
402
+ <div class="d-flex align-items-center">
403
+ <span class="me-2">${candidate.skills_match.matched}/${candidate.skills_match.total}</span>
404
+ <div class="progress flex-grow-1" style="height: 8px;">
405
+ <div class="progress-bar bg-info"
406
+ style="width: ${candidate.skills_match.matched / candidate.skills_match.total * 100}%;"></div>
407
+ </div>
408
+ </div>
409
+ </td>
410
+ <td><span class="badge bg-secondary">${formatStatus(candidate.status)}</span></td>
411
+ <td>
412
+ <button class="btn btn-sm btn-outline-primary view-candidate-btn" data-id="${candidate.id}">
413
+ View
414
+ </button>
415
+ </td>
416
+ `;
417
+
418
+ tableBody.appendChild(row);
419
+ });
420
+
421
+ // Add event listeners to view buttons
422
+ document.querySelectorAll('.view-candidate-btn').forEach(btn => {
423
+ btn.addEventListener('click', function() {
424
+ const candidateId = this.getAttribute('data-id');
425
+ loadCandidateDetails(candidateId);
426
+ });
427
+ });
428
+ }
429
+
430
+ // Load candidate details
431
+ function loadCandidateDetails(candidateId) {
432
+ console.log('Loading candidate details for:', candidateId);
433
+ // Show loading indicator
434
+ document.getElementById('resume-details-card').style.display = 'block';
435
+ document.getElementById('candidate-name').textContent = 'Loading...';
436
+ document.getElementById('candidate-score-badge').innerHTML =
437
+ `<span class="badge bg-secondary">Loading...</span>`;
438
+ document.getElementById('candidate-contact').innerHTML =
439
+ `<div class="spinner-border spinner-border-sm text-secondary" role="status">
440
+ <span class="visually-hidden">Loading...</span>
441
+ </div>`;
442
+
443
+ // Clear content areas
444
+ document.getElementById('skills-match-container').innerHTML = '<p>Loading skills data...</p>';
445
+ document.getElementById('experience-container').innerHTML = '<p>Loading experience data...</p>';
446
+ document.getElementById('education-container').innerHTML = '<p>Loading education data...</p>';
447
+ document.getElementById('ai-analysis-container').innerHTML = '<p>Loading analysis data...</p>';
448
+
449
+ fetch(`/api/candidates/${candidateId}`)
450
+ .then(response => {
451
+ if (!response.ok) {
452
+ throw new Error(`Failed to load candidate details: ${response.status}`);
453
+ }
454
+ return response.json();
455
+ })
456
+ .then(candidate => {
457
+ console.log('Candidate data received:', candidate);
458
+
459
+ try {
460
+ // Display candidate details
461
+ document.getElementById('candidate-name').textContent = candidate.name || 'Unknown Candidate';
462
+
463
+ // Score badge
464
+ const score = typeof candidate.score === 'number' ? candidate.score : 0;
465
+ const scoreClass = getScoreClass(score);
466
+ document.getElementById('candidate-score-badge').innerHTML =
467
+ `<span class="badge ${scoreClass} fs-6">${score}% Match</span>`;
468
+
469
+ // Contact info
470
+ document.getElementById('candidate-contact').innerHTML =
471
+ `<i class="bi bi-envelope"></i> ${candidate.email || 'No email'} &nbsp;
472
+ <i class="bi bi-telephone"></i> ${candidate.phone || 'No phone'}`;
473
+
474
+ // Skills matching - make sure we don't pass undefined
475
+ renderSkillsMatch(candidate.skills_analysis || {});
476
+
477
+ // Experience - make sure we don't pass undefined
478
+ renderExperience(candidate.experience || []);
479
+
480
+ // Education - make sure we don't pass undefined
481
+ renderEducation(candidate.education || []);
482
+
483
+ // AI Analysis - make sure we don't pass undefined
484
+ renderAIAnalysis(candidate.ai_analysis || {});
485
+ } catch (error) {
486
+ console.error('Error processing candidate data:', error);
487
+ document.getElementById('resume-details-card').style.display = 'block';
488
+ document.getElementById('skills-match-container').innerHTML =
489
+ '<div class="alert alert-warning">Error processing candidate data. Some information may be incomplete.</div>';
490
+ }
491
+
492
+ // Scroll to the details
493
+ document.getElementById('resume-details-card').scrollIntoView({ behavior: 'smooth' });
494
+
495
+ // Set up the view resume button
496
+ document.getElementById('view-resume-btn').onclick = function() {
497
+ if (candidate.resume_path) {
498
+ window.open(`/resumes/${candidateId}`, '_blank');
499
+ } else {
500
+ alert('Resume file not available for viewing');
501
+ }
502
+ };
503
+
504
+ // Set up generate questions button with modal
505
+ const questionsModal = new bootstrap.Modal(document.getElementById('questions-modal'));
506
+ document.getElementById('generate-questions-btn').onclick = function() {
507
+ questionsModal.show();
508
+ document.getElementById('questions-container').innerHTML =
509
+ '<div class="d-flex justify-content-center"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div>';
510
+
511
+ // Make API call to generate questions
512
+ fetch(`/api/generate-questions/${candidateId}`)
513
+ .then(response => {
514
+ if (!response.ok) {
515
+ throw new Error(`Failed to generate questions: ${response.status}`);
516
+ }
517
+ return response.json();
518
+ })
519
+ .then(data => {
520
+ renderInterviewQuestions(data.questions);
521
+ })
522
+ .catch(error => {
523
+ console.error('Error generating questions:', error);
524
+ document.getElementById('questions-container').innerHTML =
525
+ '<div class="alert alert-danger">Failed to generate questions. Please try again later.</div>';
526
+ });
527
+ };
528
+ })
529
+ .catch(error => {
530
+ console.error('Error loading candidate details:', error);
531
+ document.getElementById('resume-details-card').style.display = 'block';
532
+ document.getElementById('candidate-name').textContent = 'Error Loading Data';
533
+ document.getElementById('candidate-score-badge').innerHTML = '';
534
+ document.getElementById('candidate-contact').innerHTML = '';
535
+ document.getElementById('skills-match-container').innerHTML =
536
+ `<div class="alert alert-danger">
537
+ <h5>Error loading candidate details</h5>
538
+ <p>We couldn't load the candidate details. Please try refreshing the page.</p>
539
+ <p><small>Error: ${error.message}</small></p>
540
+ </div>`;
541
+ document.getElementById('experience-container').innerHTML = '';
542
+ document.getElementById('education-container').innerHTML = '';
543
+ document.getElementById('ai-analysis-container').innerHTML = '';
544
+ });
545
+ }
546
+
547
+ // Render skills matching
548
+ function renderSkillsMatch(skillsAnalysis) {
549
+ const container = document.getElementById('skills-match-container');
550
+ container.innerHTML = '';
551
+
552
+ try {
553
+ // Handle missing skills analysis data
554
+ if (!skillsAnalysis || Object.keys(skillsAnalysis).length === 0) {
555
+ container.innerHTML = '<p class="text-muted">No skills analysis available</p>';
556
+ return;
557
+ }
558
+
559
+ // Create skills structure for required skills
560
+ const requiredDiv = document.createElement('div');
561
+ requiredDiv.className = 'mb-3';
562
+ requiredDiv.innerHTML = '<h6>Required Skills</h6>';
563
+
564
+ const requiredSkillsContainer = document.createElement('div');
565
+ requiredSkillsContainer.className = 'd-flex flex-wrap gap-2 mb-3';
566
+
567
+ // Handle both array format and object format for required skills
568
+ if (Array.isArray(skillsAnalysis.required)) {
569
+ const requiredSkills = skillsAnalysis.required;
570
+
571
+ if (requiredSkills.length === 0) {
572
+ requiredSkillsContainer.innerHTML = '<p class="text-muted">No required skills specified</p>';
573
+ } else {
574
+ requiredSkills.forEach(skill => {
575
+ if (skill) {
576
+ const badge = document.createElement('span');
577
+ badge.className = skill.found ? 'badge bg-success' : 'badge bg-danger';
578
+ badge.innerHTML = skill.name +
579
+ (skill.found ? ' <i class="bi bi-check"></i>' : ' <i class="bi bi-x"></i>');
580
+ requiredSkillsContainer.appendChild(badge);
581
+ }
582
+ });
583
+ }
584
+ } else if (skillsAnalysis.matched !== undefined && skillsAnalysis.total !== undefined) {
585
+ // Handle cases where we only have the counts but not the full skill details
586
+ requiredSkillsContainer.innerHTML = `<p>Matched ${skillsAnalysis.matched} of ${skillsAnalysis.total} required skills</p>`;
587
+ } else {
588
+ requiredSkillsContainer.innerHTML = '<p class="text-muted">No required skills data available</p>';
589
+ }
590
+
591
+ requiredDiv.appendChild(requiredSkillsContainer);
592
+ container.appendChild(requiredDiv);
593
+
594
+ // Additional skills
595
+ if (skillsAnalysis.additional && Array.isArray(skillsAnalysis.additional) && skillsAnalysis.additional.length > 0) {
596
+ const additionalDiv = document.createElement('div');
597
+ additionalDiv.innerHTML = '<h6>Additional Skills</h6>';
598
+
599
+ const additionalSkillsContainer = document.createElement('div');
600
+ additionalSkillsContainer.className = 'd-flex flex-wrap gap-2';
601
+
602
+ skillsAnalysis.additional.forEach(skill => {
603
+ if (typeof skill === 'string') {
604
+ const badge = document.createElement('span');
605
+ badge.className = 'badge bg-info';
606
+ badge.textContent = skill;
607
+ additionalSkillsContainer.appendChild(badge);
608
+ }
609
+ });
610
+
611
+ additionalDiv.appendChild(additionalSkillsContainer);
612
+ container.appendChild(additionalDiv);
613
+ }
614
+
615
+ } catch (error) {
616
+ console.error('Error rendering skills match:', error);
617
+ container.innerHTML = '<div class="alert alert-warning">Error displaying skills information</div>';
618
+ }
619
+ }
620
+
621
+ // Render experience
622
+ function renderExperience(experience) {
623
+ const container = document.getElementById('experience-container');
624
+ container.innerHTML = '';
625
+
626
+ try {
627
+ if (!experience || !Array.isArray(experience) || experience.length === 0) {
628
+ container.innerHTML = '<p class="text-muted">No experience information available</p>';
629
+ return;
630
+ }
631
+
632
+ experience.forEach(exp => {
633
+ try {
634
+ if (!exp) return; // Skip if experience entry is null or undefined
635
+
636
+ const expDiv = document.createElement('div');
637
+ expDiv.className = 'mb-3 border-bottom pb-3';
638
+
639
+ // Handle responsibilities - could be string, array of strings, or missing
640
+ let responsibilitiesHtml = 'No description available';
641
+ if (exp.responsibilities) {
642
+ if (Array.isArray(exp.responsibilities)) {
643
+ responsibilitiesHtml = exp.responsibilities.join('<br>');
644
+ } else if (typeof exp.responsibilities === 'string') {
645
+ responsibilitiesHtml = exp.responsibilities;
646
+ }
647
+ }
648
+
649
+ // Use proper property names from the snapshot data
650
+ expDiv.innerHTML = `
651
+ <div class="d-flex justify-content-between">
652
+ <div>
653
+ <h6 class="mb-0">${exp.position || 'Unknown Position'}</h6>
654
+ <div class="text-muted">${exp.company || 'Unknown Company'}</div>
655
+ </div>
656
+ <div class="text-muted small">${exp.duration || 'No dates'}</div>
657
+ </div>
658
+ <p class="mt-2 mb-0 small">${responsibilitiesHtml}</p>
659
+ `;
660
+
661
+ container.appendChild(expDiv);
662
+ } catch (expError) {
663
+ console.error('Error rendering experience entry:', expError);
664
+ }
665
+ });
666
+
667
+ // If we didn't add any entries due to errors
668
+ if (container.children.length === 0) {
669
+ container.innerHTML = '<p class="text-muted">No valid experience information available</p>';
670
+ }
671
+ } catch (error) {
672
+ console.error('Error rendering experience section:', error);
673
+ container.innerHTML = '<div class="alert alert-warning">Error displaying experience information</div>';
674
+ }
675
+ }
676
+
677
+ // Render education
678
+ function renderEducation(education) {
679
+ const container = document.getElementById('education-container');
680
+ container.innerHTML = '';
681
+
682
+ try {
683
+ if (!education || !Array.isArray(education) || education.length === 0) {
684
+ container.innerHTML = '<p class="text-muted">No education information available</p>';
685
+ return;
686
+ }
687
+
688
+ education.forEach(edu => {
689
+ try {
690
+ if (!edu) return; // Skip if education entry is null
691
+
692
+ const eduDiv = document.createElement('div');
693
+ eduDiv.className = 'mb-2';
694
+
695
+ const degree = edu.degree || 'Degree not specified';
696
+ const year = edu.year || 'N/A';
697
+ const institution = edu.institution || 'Institution not specified';
698
+
699
+ eduDiv.innerHTML = `
700
+ <div class="d-flex justify-content-between">
701
+ <h6 class="mb-0">${degree}</h6>
702
+ <div class="text-muted small">${year}</div>
703
+ </div>
704
+ <div class="text-muted">${institution}</div>
705
+ `;
706
+
707
+ container.appendChild(eduDiv);
708
+ } catch (eduError) {
709
+ console.error('Error rendering education entry:', eduError);
710
+ }
711
+ });
712
+
713
+ // If no entries were added due to errors
714
+ if (container.children.length === 0) {
715
+ container.innerHTML = '<p class="text-muted">No valid education information available</p>';
716
+ }
717
+ } catch (error) {
718
+ console.error('Error rendering education section:', error);
719
+ container.innerHTML = '<div class="alert alert-warning">Error displaying education information</div>';
720
+ }
721
+ }
722
+
723
+ // Render AI analysis
724
+ function renderAIAnalysis(analysis) {
725
+ const container = document.getElementById('ai-analysis-container');
726
+ container.innerHTML = '';
727
+
728
+ try {
729
+ if (!analysis || typeof analysis !== 'object') {
730
+ container.innerHTML = '<p class="text-muted">No AI analysis available</p>';
731
+ return;
732
+ }
733
+
734
+ // Check if we have any meaningful data
735
+ const hasData = analysis.strengths || analysis.weaknesses || analysis.overall;
736
+ if (!hasData) {
737
+ container.innerHTML = '<p class="text-muted">No AI analysis available</p>';
738
+ return;
739
+ }
740
+
741
+ // Strengths
742
+ const strengthsDiv = document.createElement('div');
743
+ strengthsDiv.className = 'mb-3';
744
+ strengthsDiv.innerHTML = '<h6 class="text-success">Strengths</h6>';
745
+
746
+ if (Array.isArray(analysis.strengths) && analysis.strengths.length > 0) {
747
+ const strengthsList = document.createElement('ul');
748
+ strengthsList.className = 'small';
749
+
750
+ analysis.strengths.forEach(strength => {
751
+ if (strength) {
752
+ const li = document.createElement('li');
753
+ li.textContent = strength;
754
+ strengthsList.appendChild(li);
755
+ }
756
+ });
757
+
758
+ // Only add if we have actual items
759
+ if (strengthsList.children.length > 0) {
760
+ strengthsDiv.appendChild(strengthsList);
761
+ } else {
762
+ strengthsDiv.innerHTML += '<p class="small text-muted">No strengths data available</p>';
763
+ }
764
+ } else {
765
+ strengthsDiv.innerHTML += '<p class="small text-muted">No strengths data available</p>';
766
+ }
767
+
768
+ container.appendChild(strengthsDiv);
769
+
770
+ // Weaknesses
771
+ const weaknessesDiv = document.createElement('div');
772
+ weaknessesDiv.className = 'mb-3';
773
+ weaknessesDiv.innerHTML = '<h6 class="text-danger">Areas for Improvement</h6>';
774
+
775
+ if (Array.isArray(analysis.weaknesses) && analysis.weaknesses.length > 0) {
776
+ const weaknessesList = document.createElement('ul');
777
+ weaknessesList.className = 'small';
778
+
779
+ analysis.weaknesses.forEach(weakness => {
780
+ if (weakness) {
781
+ const li = document.createElement('li');
782
+ li.textContent = weakness;
783
+ weaknessesList.appendChild(li);
784
+ }
785
+ });
786
+
787
+ // Only add if we have actual items
788
+ if (weaknessesList.children.length > 0) {
789
+ weaknessesDiv.appendChild(weaknessesList);
790
+ } else {
791
+ weaknessesDiv.innerHTML += '<p class="small text-muted">No areas for improvement identified</p>';
792
+ }
793
+ } else {
794
+ weaknessesDiv.innerHTML += '<p class="small text-muted">No areas for improvement identified</p>';
795
+ }
796
+
797
+ container.appendChild(weaknessesDiv);
798
+
799
+ // Overall assessment
800
+ container.innerHTML += `
801
+ <div>
802
+ <h6>Overall Assessment</h6>
803
+ <p class="small">${analysis.overall || 'No detailed analysis available.'}</p>
804
+ </div>
805
+ `;
806
+ } catch (error) {
807
+ console.error('Error rendering AI analysis:', error);
808
+ container.innerHTML = '<div class="alert alert-warning">Error displaying AI analysis</div>';
809
+ }
810
+ }
811
+
812
+ // Render interview questions
813
+ function renderInterviewQuestions(questions) {
814
+ const container = document.getElementById('questions-container');
815
+ container.innerHTML = '';
816
+
817
+ if (!questions) {
818
+ container.innerHTML = '<p class="text-muted">No questions could be generated</p>';
819
+ return;
820
+ }
821
+
822
+ // Technical questions - handle both array of strings and array of objects
823
+ if (questions.technical_questions || questions.technical) {
824
+ const technicalQuestions = questions.technical_questions || questions.technical;
825
+
826
+ if (technicalQuestions && technicalQuestions.length > 0) {
827
+ const techDiv = document.createElement('div');
828
+ techDiv.className = 'mb-4';
829
+ techDiv.innerHTML = '<h5 class="mb-3">Technical Questions</h5>';
830
+
831
+ const techList = document.createElement('ol');
832
+ techList.className = 'question-list';
833
+
834
+ technicalQuestions.forEach(question => {
835
+ const li = document.createElement('li');
836
+ li.className = 'mb-3';
837
+
838
+ if (typeof question === 'string') {
839
+ li.textContent = question;
840
+ } else if (typeof question === 'object') {
841
+ li.innerHTML = `
842
+ <div>${question.question}</div>
843
+ ${question.purpose ? `<div class="small text-muted mt-1">Purpose: ${question.purpose}</div>` : ''}
844
+ `;
845
+ }
846
+
847
+ techList.appendChild(li);
848
+ });
849
+
850
+ techDiv.appendChild(techList);
851
+ container.appendChild(techDiv);
852
+ }
853
+ }
854
+
855
+ // Behavioral questions - handle both array of strings and array of objects
856
+ if (questions.behavioral_questions || questions.behavioral) {
857
+ const behavioralQuestions = questions.behavioral_questions || questions.behavioral;
858
+
859
+ if (behavioralQuestions && behavioralQuestions.length > 0) {
860
+ const behavDiv = document.createElement('div');
861
+ behavDiv.className = 'mb-4';
862
+ behavDiv.innerHTML = '<h5 class="mb-3">Behavioral Questions</h5>';
863
+
864
+ const behavList = document.createElement('ol');
865
+ behavList.className = 'question-list';
866
+
867
+ behavioralQuestions.forEach(question => {
868
+ const li = document.createElement('li');
869
+ li.className = 'mb-3';
870
+
871
+ if (typeof question === 'string') {
872
+ li.textContent = question;
873
+ } else if (typeof question === 'object') {
874
+ li.innerHTML = `
875
+ <div>${question.question}</div>
876
+ ${question.purpose ? `<div class="small text-muted mt-1">Purpose: ${question.purpose}</div>` : ''}
877
+ `;
878
+ }
879
+
880
+ behavList.appendChild(li);
881
+ });
882
+
883
+ behavDiv.appendChild(behavList);
884
+ container.appendChild(behavDiv);
885
+ }
886
+ }
887
+
888
+ // Follow-up areas
889
+ if (questions.follow_up_areas && questions.follow_up_areas.length > 0) {
890
+ const followUpDiv = document.createElement('div');
891
+ followUpDiv.className = 'mb-4';
892
+ followUpDiv.innerHTML = '<h5 class="mb-3">Areas to Explore Further</h5>';
893
+
894
+ const followUpList = document.createElement('ul');
895
+ followUpList.className = 'question-list';
896
+
897
+ questions.follow_up_areas.forEach(area => {
898
+ const li = document.createElement('li');
899
+ li.className = 'mb-2';
900
+ li.textContent = area;
901
+ followUpList.appendChild(li);
902
+ });
903
+
904
+ followUpDiv.appendChild(followUpList);
905
+ container.appendChild(followUpDiv);
906
+ }
907
+ }
908
+
909
+ // Update statistics
910
+ function updateStatistics(stats) {
911
+ document.getElementById('total-resumes').textContent = stats.total;
912
+ document.getElementById('qualified-resumes').textContent = stats.qualified;
913
+ document.getElementById('unqualified-resumes').textContent = stats.unqualified;
914
+
915
+ // Update chart data
916
+ scoreChart.data.datasets[0].data = [
917
+ stats.score_ranges['0-50'],
918
+ stats.score_ranges['50-70'],
919
+ stats.score_ranges['70-85'],
920
+ stats.score_ranges['85-100']
921
+ ];
922
+ scoreChart.update();
923
+ }
924
+
925
+ // Apply filters button
926
+ document.getElementById('apply-filters').addEventListener('click', function() {
927
+ const jobId = document.getElementById('job-select').value;
928
+
929
+ if (!jobId) {
930
+ alert('Please select a job first');
931
+ return;
932
+ }
933
+
934
+ const minScore = document.getElementById('score-filter').value;
935
+
936
+ const statusFilters = [];
937
+ if (document.getElementById('status-new').checked) statusFilters.push('new');
938
+ if (document.getElementById('status-reviewed').checked) statusFilters.push('reviewed');
939
+
940
+ loadCandidates(jobId, {
941
+ minScore: minScore,
942
+ status: statusFilters
943
+ });
944
+ });
945
+
946
+ // Close resume details
947
+ document.getElementById('close-resume-details').addEventListener('click', function() {
948
+ document.getElementById('resume-details-card').style.display = 'none';
949
+ });
950
+
951
+ // Refresh data
952
+ document.getElementById('refresh-data').addEventListener('click', function() {
953
+ const jobId = document.getElementById('job-select').value;
954
+
955
+ if (!jobId) {
956
+ alert('Please select a job first');
957
+ return;
958
+ }
959
+
960
+ loadCandidates(jobId);
961
+ });
962
+
963
+ // Export to CSV
964
+ document.getElementById('export-csv').addEventListener('click', function() {
965
+ const jobId = document.getElementById('job-select').value;
966
+
967
+ if (!jobId) {
968
+ alert('Please select a job first');
969
+ return;
970
+ }
971
+
972
+ window.open(`/api/export-candidates?job_id=${jobId}`, '_blank');
973
+ });
974
+
975
+ // Status update
976
+ document.getElementById('update-status-btn').addEventListener('click', function() {
977
+ const status = document.getElementById('status-update').value;
978
+ if (status === 'Change Status...') {
979
+ alert('Please select a status');
980
+ return;
981
+ }
982
+
983
+ const candidateRow = document.querySelector('.candidate-row.active');
984
+ if (!candidateRow) return;
985
+
986
+ const candidateId = candidateRow.getAttribute('data-id');
987
+
988
+ fetch(`/api/candidates/${candidateId}/status`, {
989
+ method: 'PUT',
990
+ headers: {
991
+ 'Content-Type': 'application/json'
992
+ },
993
+ body: JSON.stringify({ status: status })
994
+ })
995
+ .then(response => response.json())
996
+ .then(data => {
997
+ if (data.success) {
998
+ alert('Status updated successfully');
999
+ // Refresh the candidate list
1000
+ const jobId = document.getElementById('job-select').value;
1001
+ loadCandidates(jobId);
1002
+ } else {
1003
+ alert('Error updating status: ' + data.error);
1004
+ }
1005
+ })
1006
+ .catch(error => {
1007
+ console.error('Error:', error);
1008
+ alert('Error updating status');
1009
+ });
1010
+ });
1011
+
1012
+ // Helper function to get score class based on score value
1013
+ function getScoreClass(score) {
1014
+ if (score >= 85) return 'bg-success';
1015
+ if (score >= 70) return 'bg-info';
1016
+ if (score >= 50) return 'bg-warning';
1017
+ return 'bg-danger';
1018
+ }
1019
+
1020
+ // Format status for display
1021
+ function formatStatus(status) {
1022
+ if (!status) return 'New';
1023
+
1024
+ // Capitalize first letter of each word
1025
+ return status.split('_')
1026
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
1027
+ .join(' ');
1028
+ }
1029
+ });
1030
+ </script>
1031
+ </body>
1032
+ </html>