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,807 @@
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>SmartRecruitAgent Dashboard</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="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap">
10
+ <link rel="stylesheet" href="/static/styles.css">
11
+ <script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
14
+ </head>
15
+ <body>
16
+ <header class="bg-dark text-white p-3">
17
+ <div class="container">
18
+ <div class="d-flex justify-content-between align-items-center">
19
+ <h1 class="h3 mb-0 d-flex align-items-center">
20
+ <i class="bi bi-people-fill me-2"></i>
21
+ SmartRecruitAgent
22
+ </h1>
23
+ <div class="d-flex align-items-center">
24
+ <div id="connection-status" class="badge bg-success me-3">Connected</div>
25
+ <nav>
26
+ <ul class="nav">
27
+ <li class="nav-item">
28
+ <a class="nav-link text-white" href="/jd-creation">
29
+ <i class="bi bi-file-earmark-text me-1"></i> Create JD
30
+ </a>
31
+ </li>
32
+ <li class="nav-item">
33
+ <a class="nav-link text-white" href="/career-portal">
34
+ <i class="bi bi-briefcase me-1"></i> Career Portal
35
+ </a>
36
+ </li>
37
+ <li class="nav-item">
38
+ <a class="nav-link text-white" href="/resume-scoring">
39
+ <i class="bi bi-bar-chart me-1"></i> Scoring
40
+ </a>
41
+ </li>
42
+ <li class="nav-item">
43
+ <a class="nav-link text-white" href="/dashboard">
44
+ <i class="bi bi-speedometer2 me-1"></i> Dashboard
45
+ </a>
46
+ </li>
47
+ </ul>
48
+ </nav>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </header>
53
+
54
+ <div class="container mt-4">
55
+ <!-- Welcome Card with Stats -->
56
+ <div class="card mb-4 shadow-sm">
57
+ <div class="card-body p-4">
58
+ <div class="row align-items-center">
59
+ <div class="col-lg-8">
60
+ <h2 class="mb-1">Welcome to SmartRecruitAgent</h2>
61
+ <p class="lead text-muted mb-3">Your AI-powered recruitment assistant</p>
62
+ <div class="d-flex flex-wrap gap-2 mt-3">
63
+ <a href="/jd-creation" class="btn btn-primary">
64
+ <i class="bi bi-file-earmark-text me-1"></i> Create Job Description
65
+ </a>
66
+ <a href="/career-portal" class="btn btn-info text-white">
67
+ <i class="bi bi-briefcase me-1"></i> Career Portal
68
+ </a>
69
+ <a href="/resume-scoring" class="btn btn-success">
70
+ <i class="bi bi-bar-chart me-1"></i> View Resume Scoring
71
+ </a>
72
+ <a href="/upload-resume" class="btn btn-secondary">
73
+ <i class="bi bi-cloud-arrow-up me-1"></i> Upload Resumes
74
+ </a>
75
+ </div>
76
+ </div>
77
+ <div class="col-lg-4 text-center mt-4 mt-lg-0">
78
+ <div class="stat-card">
79
+ <h5>Total Resumes Processed</h5>
80
+ <div class="stat-number" id="dashboard-total-resumes">0</div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Workflow Navigation -->
88
+ <div class="card mb-3 shadow-sm">
89
+ <div class="card-header">
90
+ <i class="bi bi-signpost me-2"></i> Workflow Navigation
91
+ </div>
92
+ <div class="card-body">
93
+ <div class="d-flex flex-wrap gap-2">
94
+ <a href="#stage-jd" class="btn btn-sm workflow-nav-link">
95
+ <i class="bi bi-1-circle me-1"></i> Job Description
96
+ </a>
97
+ <a href="#stage-jd-poster" class="btn btn-sm workflow-nav-link">
98
+ <i class="bi bi-2-circle me-1"></i> Job Posting
99
+ </a>
100
+ <a href="#stage-ingest" class="btn btn-sm workflow-nav-link">
101
+ <i class="bi bi-3-circle me-1"></i> Resume Ingestion
102
+ </a>
103
+ <a href="#stage-analyze" class="btn btn-sm workflow-nav-link">
104
+ <i class="bi bi-4-circle me-1"></i> Resume Analysis
105
+ </a>
106
+ <a href="#stage-sentiment" class="btn btn-sm workflow-nav-link">
107
+ <i class="bi bi-5-circle me-1"></i> Sentiment Analysis
108
+ </a>
109
+ <a href="#stage-assessment" class="btn btn-sm workflow-nav-link">
110
+ <i class="bi bi-6-circle me-1"></i> Assessment
111
+ </a>
112
+ <a href="#stage-question" class="btn btn-sm workflow-nav-link">
113
+ <i class="bi bi-7-circle me-1"></i> Question Generation
114
+ </a>
115
+ <a href="#stage-notify" class="btn btn-sm workflow-nav-link">
116
+ <i class="bi bi-8-circle me-1"></i> Recruiter Notification
117
+ </a>
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ <!-- Node statuses container (was missing) -->
123
+ <div id="node-statuses" class="d-none"></div>
124
+
125
+ <!-- Main Workflow Pipeline visualization -->
126
+ <div class="card mb-4 shadow-sm">
127
+ <div class="card-header d-flex justify-content-between align-items-center">
128
+ <span><i class="bi bi-diagram-3 me-2"></i> Recruitment Workflow Pipeline</span>
129
+ <span class="badge bg-success" id="workflow-status">Active</span>
130
+ </div>
131
+ <div class="card-body p-0">
132
+ <div class="workflow-pipeline">
133
+ <!-- Stage 1: JD Generation -->
134
+ <div class="pipeline-stage" id="stage-jd">
135
+ <div class="stage-node">
136
+ <i class="bi bi-check-circle-fill"></i>
137
+ </div>
138
+ <div class="d-flex justify-content-between align-items-center mb-2">
139
+ <h5 class="mb-0">Job Description</h5>
140
+ <span class="badge" id="jd-generator-badge">Idle</span>
141
+ </div>
142
+ <p class="small mb-2">Create and manage job descriptions</p>
143
+
144
+ <div class="d-flex align-items-center">
145
+ <div class="stage-progress flex-grow-1 me-2" id="jd-generator-progress"></div>
146
+ <small class="text-muted" id="jd-generator-time">Last run: Never</small>
147
+ </div>
148
+
149
+ <div class="mt-3 d-flex justify-content-end">
150
+ <a href="/jd-creation" class="btn btn-sm btn-outline-primary">Go to Job Creation</a>
151
+ </div>
152
+ </div>
153
+
154
+ <div class="pipeline-arrow">
155
+ <i class="bi bi-arrow-down-circle-fill"></i>
156
+ </div>
157
+
158
+ <!-- Stage 2: Job Posting -->
159
+ <div class="pipeline-stage" id="stage-jd-poster">
160
+ <div class="stage-node">
161
+ <i class="bi bi-check-circle-fill"></i>
162
+ </div>
163
+ <div class="d-flex justify-content-between align-items-center mb-2">
164
+ <h5 class="mb-0">Job Posting</h5>
165
+ <span class="badge" id="jd-poster-badge">Idle</span>
166
+ </div>
167
+ <p class="small mb-2">Post job descriptions to external platforms</p>
168
+ <div class="d-flex align-items-center">
169
+ <div class="stage-progress flex-grow-1 me-2" id="jd-poster-progress"></div>
170
+ <small class="text-muted" id="jd-poster-time">Last run: Never</small>
171
+ </div>
172
+
173
+ <div class="mt-3 d-flex justify-content-end">
174
+ <a href="/career-portal" class="btn btn-sm btn-outline-primary">Go to Career Portal</a>
175
+ </div>
176
+ </div>
177
+
178
+ <div class="pipeline-arrow">
179
+ <i class="bi bi-arrow-down-circle-fill"></i>
180
+ </div>
181
+
182
+ <!-- Stage 3: Resume Ingestion -->
183
+ <div class="pipeline-stage" id="stage-ingest">
184
+ <div class="stage-node">
185
+ <i class="bi bi-check-circle-fill"></i>
186
+ </div>
187
+ <div class="d-flex justify-content-between align-items-center mb-2">
188
+ <h5 class="mb-0">Resume Ingestion</h5>
189
+ <span class="badge" id="resume-ingestor-badge">Idle</span>
190
+ </div>
191
+ <p class="small mb-2">Upload and process resume files</p>
192
+ <div class="d-flex align-items-center">
193
+ <div class="stage-progress flex-grow-1 me-2" id="resume-ingestor-progress"></div>
194
+ <small class="text-muted" id="resume-ingestor-time">Last run: Never</small>
195
+ </div>
196
+
197
+ <div class="mt-3 d-flex justify-content-end">
198
+ <a href="/upload-resume" class="btn btn-sm btn-outline-primary">Go to Resume Upload</a>
199
+ </div>
200
+ </div>
201
+
202
+ <div class="pipeline-arrow">
203
+ <i class="bi bi-arrow-down-circle-fill"></i>
204
+ </div>
205
+
206
+ <!-- Stage 4: Resume Analysis -->
207
+ <div class="pipeline-stage" id="stage-analyze">
208
+ <div class="stage-node">
209
+ <i class="bi bi-check-circle-fill"></i>
210
+ </div>
211
+ <div class="d-flex justify-content-between align-items-center mb-2">
212
+ <h5 class="mb-0">Resume Analysis</h5>
213
+ <span class="badge" id="resume-analyzer-badge">Idle</span>
214
+ </div>
215
+ <p class="small mb-2">Parse and score resumes against job requirements</p>
216
+ <div class="d-flex align-items-center">
217
+ <div class="stage-progress flex-grow-1 me-2" id="resume-analyzer-progress"></div>
218
+ <small class="text-muted" id="resume-analyzer-time">Last run: Never</small>
219
+ </div>
220
+
221
+ <div class="mt-3 d-flex justify-content-end">
222
+ <a href="/resume-analysis" class="btn btn-sm btn-outline-primary">Go to Resume Analysis</a>
223
+ </div>
224
+ </div>
225
+
226
+ <div class="pipeline-arrow">
227
+ <i class="bi bi-arrow-down-circle-fill"></i>
228
+ </div>
229
+
230
+ <!-- Stage 5: Sentiment Analysis -->
231
+ <div class="pipeline-stage" id="stage-sentiment">
232
+ <div class="stage-node">
233
+ <i class="bi bi-check-circle-fill"></i>
234
+ </div>
235
+ <div class="d-flex justify-content-between align-items-center mb-2">
236
+ <h5 class="mb-0">Sentiment Analysis</h5>
237
+ <span class="badge" id="sentiment-analysis-badge">Idle</span>
238
+ </div>
239
+ <p class="small mb-2">Analyze candidate sentiment and motivation</p>
240
+ <div class="d-flex align-items-center">
241
+ <div class="stage-progress flex-grow-1 me-2" id="sentiment-analysis-progress"></div>
242
+ <small class="text-muted" id="sentiment-analysis-time">Last run: Never</small>
243
+ </div>
244
+
245
+ <div class="mt-3 d-flex justify-content-end">
246
+ <a href="/sentiment-analysis" class="btn btn-sm btn-outline-primary">Go to Sentiment Analysis</a>
247
+ </div>
248
+ </div>
249
+
250
+ <div class="pipeline-arrow">
251
+ <i class="bi bi-arrow-down-circle-fill"></i>
252
+ </div>
253
+
254
+ <!-- Stage 6: Assessment -->
255
+ <div class="pipeline-stage" id="stage-assessment">
256
+ <div class="stage-node">
257
+ <i class="bi bi-check-circle-fill"></i>
258
+ </div>
259
+ <div class="d-flex justify-content-between align-items-center mb-2">
260
+ <h5 class="mb-0">Assessment</h5>
261
+ <span class="badge" id="assessment-handler-badge">Idle</span>
262
+ </div>
263
+ <p class="small mb-2">Send assessments to qualified candidates</p>
264
+ <div class="d-flex align-items-center">
265
+ <div class="stage-progress flex-grow-1 me-2" id="assessment-handler-progress"></div>
266
+ <small class="text-muted" id="assessment-handler-time">Last run: Never</small>
267
+ </div>
268
+
269
+ <div class="mt-3 d-flex justify-content-end">
270
+ <a href="/assessment" class="btn btn-sm btn-outline-primary">Go to Assessment</a>
271
+ </div>
272
+ </div>
273
+
274
+ <div class="pipeline-arrow">
275
+ <i class="bi bi-arrow-down-circle-fill"></i>
276
+ </div>
277
+
278
+ <!-- Stage 7: Question Generation -->
279
+ <div class="pipeline-stage" id="stage-question">
280
+ <div class="stage-node">
281
+ <i class="bi bi-check-circle-fill"></i>
282
+ </div>
283
+ <div class="d-flex justify-content-between align-items-center mb-2">
284
+ <h5 class="mb-0">Question Generation</h5>
285
+ <span class="badge" id="question-generator-badge">Idle</span>
286
+ </div>
287
+ <p class="small mb-2">Generate tailored interview questions</p>
288
+ <div class="d-flex align-items-center">
289
+ <div class="stage-progress flex-grow-1 me-2" id="question-generator-progress"></div>
290
+ <small class="text-muted" id="question-generator-time">Last run: Never</small>
291
+ </div>
292
+
293
+ <div class="mt-3 d-flex justify-content-end">
294
+ <a href="/question-generation" class="btn btn-sm btn-outline-primary">Go to Question Generation</a>
295
+ </div>
296
+ </div>
297
+
298
+ <div class="pipeline-arrow">
299
+ <i class="bi bi-arrow-down-circle-fill"></i>
300
+ </div>
301
+
302
+ <!-- Stage 8: Recruiter Notification -->
303
+ <div class="pipeline-stage" id="stage-notify">
304
+ <div class="stage-node">
305
+ <i class="bi bi-check-circle-fill"></i>
306
+ </div>
307
+ <div class="d-flex justify-content-between align-items-center mb-2">
308
+ <h5 class="mb-0">Recruiter Notification</h5>
309
+ <span class="badge" id="recruiter-notifier-badge">Idle</span>
310
+ </div>
311
+ <p class="small mb-2">Send results to recruiters</p>
312
+ <div class="d-flex align-items-center">
313
+ <div class="stage-progress flex-grow-1 me-2" id="recruiter-notifier-progress"></div>
314
+ <small class="text-muted" id="recruiter-notifier-time">Last run: Never</small>
315
+ </div>
316
+
317
+ <div class="mt-3 d-flex justify-content-end">
318
+ <a href="/recruiter-notifications" class="btn btn-sm btn-outline-primary">Go to Notifications</a>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ </div>
324
+
325
+ <div class="row">
326
+ <!-- Dashboard Stats -->
327
+ <div class="col-md-6">
328
+ <div class="card shadow-sm h-100">
329
+ <div class="card-header d-flex justify-content-between align-items-center">
330
+ <span><i class="bi bi-graph-up me-2"></i> System Stats</span>
331
+ <span class="badge bg-info text-white">Last 24h</span>
332
+ </div>
333
+ <div class="card-body">
334
+ <div class="row">
335
+ <div class="col-md-6 mb-3 mb-md-0">
336
+ <div class="stat-card">
337
+ <h5><i class="bi bi-file-earmark-text me-1"></i> Total Resumes</h5>
338
+ <div id="total-resumes" class="stat-number">0</div>
339
+ </div>
340
+ </div>
341
+ <div class="col-md-6">
342
+ <div class="stat-card">
343
+ <h5><i class="bi bi-calendar-check me-1"></i> Resumes Today</h5>
344
+ <div id="resumes-today" class="stat-number">0</div>
345
+ </div>
346
+ </div>
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </div>
351
+
352
+ <!-- Workflow Logs -->
353
+ <div class="col-md-6">
354
+ <div class="card shadow-sm h-100">
355
+ <div class="card-header d-flex justify-content-between align-items-center">
356
+ <span><i class="bi bi-journal-text me-2"></i> Workflow Logs</span>
357
+ <button id="refresh-logs" class="btn btn-sm btn-outline-light">
358
+ <i class="bi bi-arrow-clockwise me-1"></i> Refresh
359
+ </button>
360
+ </div>
361
+ <div class="card-body">
362
+ <div id="log-entries" class="log-container">
363
+ <!-- Log entries will be inserted here -->
364
+ <div class="text-center text-muted p-4">
365
+ <i class="bi bi-info-circle mb-2 display-6"></i>
366
+ <p>No log entries yet</p>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ </div>
374
+
375
+ <script>
376
+ // Connect to Socket.IO server
377
+ const socket = io();
378
+
379
+ // Track connection status
380
+ socket.on('connect', () => {
381
+ document.getElementById('connection-status').classList.remove('bg-danger');
382
+ document.getElementById('connection-status').classList.add('bg-success');
383
+ document.getElementById('connection-status').textContent = 'Connected';
384
+ });
385
+
386
+ socket.on('disconnect', () => {
387
+ document.getElementById('connection-status').classList.remove('bg-success');
388
+ document.getElementById('connection-status').classList.add('bg-danger');
389
+ document.getElementById('connection-status').textContent = 'Disconnected';
390
+ });
391
+
392
+ // Handle heartbeat
393
+ socket.on('heartbeat', (data) => {
394
+ console.log('Heartbeat received:', data.time);
395
+ });
396
+
397
+ // Handle status updates
398
+ socket.on('status_update', (data) => {
399
+ updateNodeStatuses(data.node_statuses);
400
+ updateWorkflowVisualization(data.node_statuses);
401
+ });
402
+
403
+ // Handle log updates
404
+ socket.on('log_update', (data) => {
405
+ updateLogEntries(data.logs);
406
+ });
407
+
408
+ // Handle workflow completion
409
+ socket.on('workflow_completed', function(data) {
410
+ console.log('Workflow completed:', data);
411
+ // Force refresh data
412
+ fetchRecentResumes();
413
+ updateWorkflowStats();
414
+ });
415
+
416
+ // Initial data fetch
417
+ fetchInitialData();
418
+
419
+ function fetchInitialData() {
420
+ // Fetch logs and status data
421
+ fetch('/api/logs')
422
+ .then(response => response.json())
423
+ .then(data => {
424
+ updateNodeStatuses(data.node_statuses);
425
+ updateLogEntries(data.logs);
426
+ })
427
+ .catch(error => console.error('Error fetching logs:', error));
428
+
429
+ // Fetch system stats
430
+ fetch('/api/status')
431
+ .then(response => response.json())
432
+ .then(data => {
433
+ document.getElementById('total-resumes').textContent = data.stats.total_resumes_processed;
434
+ document.getElementById('resumes-today').textContent = data.stats.resumes_today;
435
+ })
436
+ .catch(error => console.error('Error fetching status:', error));
437
+ }
438
+
439
+ // Fetch recent resumes (for workflow completion)
440
+ function fetchRecentResumes() {
441
+ fetch('/api/recent-resumes')
442
+ .then(response => response.json())
443
+ .then(data => {
444
+ updateResumeList(data.resumes);
445
+ })
446
+ .catch(error => {
447
+ console.error('Error fetching recent resumes:', error);
448
+ });
449
+ }
450
+
451
+ // Update node statuses in the DOM
452
+ function updateNodeStatuses(statuses) {
453
+ // We're still using the hidden container for compatibility
454
+ const container = document.getElementById('node-statuses');
455
+ container.innerHTML = '';
456
+
457
+ // Update the hierarchical workflow view
458
+ for (const [nodeName, nodeData] of Object.entries(statuses)) {
459
+ const statusClass = getStatusClass(nodeData.status);
460
+ const formattedName = formatNodeName(nodeName);
461
+ const lastRun = nodeData.last_run ? new Date(nodeData.last_run).toLocaleString() : 'Never';
462
+
463
+ // Create entry in the hidden container
464
+ const nodeElement = document.createElement('div');
465
+ nodeElement.className = 'node-status mb-2';
466
+ nodeElement.innerHTML = `
467
+ <div class="d-flex justify-content-between align-items-center">
468
+ <span class="node-name">${formattedName}</span>
469
+ <span class="badge ${statusClass}">${nodeData.status}</span>
470
+ </div>
471
+ <div class="text-muted small">Last run: ${lastRun}</div>
472
+ `;
473
+ container.appendChild(nodeElement);
474
+
475
+ // Update the badge in the workflow visualization
476
+ const badgeElement = document.getElementById(`${nodeName}-badge`);
477
+ if (badgeElement) {
478
+ badgeElement.className = `badge ${getStatusClass(nodeData.status)}`;
479
+ badgeElement.textContent = nodeData.status;
480
+
481
+ const progressElement = document.getElementById(`${nodeName}-progress`);
482
+ if (progressElement) {
483
+ progressElement.className = 'stage-progress';
484
+
485
+ if (nodeData.status.toLowerCase() === 'running' ||
486
+ nodeData.status.toLowerCase() === 'processing') {
487
+ progressElement.classList.add('in-progress');
488
+ progressElement.style.setProperty('--progress', '50%');
489
+ } else if (nodeData.status.toLowerCase() === 'completed' ||
490
+ nodeData.status.toLowerCase() === 'success') {
491
+ progressElement.classList.add('complete');
492
+ } else if (nodeData.status.toLowerCase() === 'error' ||
493
+ nodeData.status.toLowerCase() === 'failed') {
494
+ progressElement.classList.add('error');
495
+ }
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ // Update the hierarchical workflow view with more dynamic behavior
502
+ function updateWorkflowVisualization(statuses) {
503
+ for (const [nodeName, nodeData] of Object.entries(statuses)) {
504
+ const stageElement = document.getElementById(`stage-${nodeName.replace(/_/g, '-')}`);
505
+ const badgeElement = document.getElementById(`${nodeName}-badge`);
506
+ const progressElement = document.getElementById(`${nodeName}-progress`);
507
+ const timeElement = document.getElementById(`${nodeName}-time`);
508
+
509
+ if (!stageElement || !badgeElement || !progressElement) continue;
510
+
511
+ // Reset classes
512
+ stageElement.classList.remove('active', 'completed', 'error');
513
+
514
+ // Update badge with a nice transition
515
+ const oldClass = badgeElement.className;
516
+ badgeElement.className = `badge ${getStatusClass(nodeData.status)}`;
517
+ badgeElement.textContent = nodeData.status;
518
+
519
+ if (oldClass !== badgeElement.className) {
520
+ badgeElement.animate([
521
+ { transform: 'scale(1)' },
522
+ { transform: 'scale(1.2)' },
523
+ { transform: 'scale(1)' }
524
+ ], {
525
+ duration: 500,
526
+ easing: 'ease-in-out'
527
+ });
528
+ }
529
+
530
+ // Update time
531
+ if (timeElement && nodeData.last_run) {
532
+ const lastRun = new Date(nodeData.last_run).toLocaleString();
533
+ timeElement.textContent = `Last run: ${lastRun}`;
534
+ }
535
+
536
+ // Update progress bar with animation
537
+ progressElement.className = 'stage-progress';
538
+
539
+ // Set appropriate classes and animations based on status
540
+ const status = nodeData.status.toLowerCase();
541
+
542
+ if (['running', 'processing'].includes(status)) {
543
+ stageElement.classList.add('active');
544
+ progressElement.classList.add('in-progress');
545
+
546
+ // Use provided progress or default to animated progress
547
+ let progress = nodeData.progress || 50;
548
+ progressElement.style.setProperty('--progress', `${progress}%`);
549
+
550
+ // Add subtle pulse animation to the stage
551
+ stageElement.animate([
552
+ { boxShadow: '0 3px 10px rgba(0, 0, 0, 0.05)' },
553
+ { boxShadow: '0 6px 18px rgba(74, 108, 247, 0.2)' },
554
+ { boxShadow: '0 3px 10px rgba(0, 0, 0, 0.05)' }
555
+ ], {
556
+ duration: 2000,
557
+ iterations: Infinity
558
+ });
559
+ }
560
+ else if (['completed', 'success'].includes(status)) {
561
+ stageElement.classList.add('completed');
562
+ progressElement.classList.add('complete');
563
+ }
564
+ else if (['error', 'failed'].includes(status)) {
565
+ stageElement.classList.add('error');
566
+ progressElement.classList.add('error');
567
+ }
568
+
569
+ // Update node icon based on status
570
+ const nodeIcon = stageElement.querySelector('.stage-node i');
571
+ if (nodeIcon) {
572
+ if (['completed', 'success'].includes(status)) {
573
+ nodeIcon.className = 'bi bi-check-circle-fill';
574
+ } else if (['error', 'failed'].includes(status)) {
575
+ nodeIcon.className = 'bi bi-x-circle-fill';
576
+ } else if (['running', 'processing'].includes(status)) {
577
+ nodeIcon.className = 'bi bi-arrow-repeat';
578
+ } else {
579
+ nodeIcon.className = 'bi bi-circle-fill';
580
+ }
581
+ }
582
+
583
+ // Update counts if available
584
+ const countElement = document.getElementById(`${nodeName}-count`);
585
+ if (countElement && nodeData.count !== undefined) {
586
+ countElement.textContent = nodeData.count;
587
+ }
588
+ }
589
+ }
590
+
591
+ // Update log entries in the DOM with enhanced UI
592
+ function updateLogEntries(logs) {
593
+ const container = document.getElementById('log-entries');
594
+ container.innerHTML = '';
595
+
596
+ if (logs.length === 0) {
597
+ container.innerHTML = `
598
+ <div class="text-center text-muted p-4">
599
+ <i class="bi bi-info-circle mb-2 display-6"></i>
600
+ <p>No log entries yet</p>
601
+ </div>`;
602
+ return;
603
+ }
604
+
605
+ // Sort logs by timestamp (newest first)
606
+ const sortedLogs = [...logs].sort((a, b) => {
607
+ return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
608
+ });
609
+
610
+ // Display only the most recent 15 logs
611
+ const recentLogs = sortedLogs.slice(0, 15);
612
+
613
+ for (const log of recentLogs) {
614
+ const logElement = document.createElement('div');
615
+ logElement.className = 'log-entry mb-3';
616
+
617
+ const timestamp = new Date(log.timestamp).toLocaleString();
618
+ const candidateName = log.candidate_name || 'System';
619
+ const status = log.status || 'processing';
620
+ const statusClass = getStatusClass(status);
621
+
622
+ // Determine appropriate icon based on status
623
+ let statusIcon = 'info-circle';
624
+ if (status.toLowerCase() === 'success' || status.toLowerCase() === 'completed') {
625
+ statusIcon = 'check-circle';
626
+ } else if (status.toLowerCase() === 'error' || status.toLowerCase() === 'failed') {
627
+ statusIcon = 'exclamation-triangle';
628
+ } else if (status.toLowerCase() === 'running' || status.toLowerCase() === 'processing') {
629
+ statusIcon = 'arrow-repeat';
630
+ }
631
+
632
+ logElement.innerHTML = `
633
+ <div class="d-flex justify-content-between align-items-center mb-1">
634
+ <div>
635
+ <i class="bi bi-${statusIcon} me-2"></i>
636
+ <strong>${candidateName}</strong>
637
+ </div>
638
+ <span class="badge ${statusClass}">${status}</span>
639
+ </div>
640
+ <div class="log-message">${log.message || ''}</div>
641
+ <div class="d-flex justify-content-between align-items-center mt-2">
642
+ <span class="text-muted small">
643
+ <i class="bi bi-clock me-1"></i>${timestamp}
644
+ </span>
645
+ ${status.toLowerCase() === 'error' || status.toLowerCase() === 'failed' ?
646
+ `<button class="btn btn-sm btn-outline-danger retry-btn" data-job-id="${log.job_id || ''}">
647
+ <i class="bi bi-arrow-repeat me-1"></i>Retry
648
+ </button>` : ''}
649
+ </div>
650
+ `;
651
+
652
+ container.appendChild(logElement);
653
+
654
+ // Add fade-in animation to new log entries
655
+ logElement.animate([
656
+ { opacity: 0, transform: 'translateY(10px)' },
657
+ { opacity: 1, transform: 'translateY(0)' }
658
+ ], {
659
+ duration: 300,
660
+ easing: 'ease-out'
661
+ });
662
+ }
663
+
664
+ // Attach event listeners to retry buttons
665
+ document.querySelectorAll('.retry-btn').forEach(button => {
666
+ button.addEventListener('click', function() {
667
+ const jobId = this.getAttribute('data-job-id');
668
+ if (jobId) {
669
+ // Add visual feedback when clicking retry
670
+ this.innerHTML = '<i class="bi bi-hourglass-split me-1"></i>Retrying...';
671
+ this.disabled = true;
672
+ retryJob(jobId);
673
+ }
674
+ });
675
+ });
676
+ }
677
+
678
+ // Enhanced helper function to get status class for badges
679
+ function getStatusClass(status) {
680
+ const statusLower = status.toLowerCase();
681
+
682
+ switch (statusLower) {
683
+ case 'running':
684
+ case 'processing':
685
+ return 'bg-info text-white';
686
+ case 'completed':
687
+ case 'success':
688
+ return 'bg-success text-white';
689
+ case 'error':
690
+ case 'failed':
691
+ return 'bg-danger text-white';
692
+ case 'waiting':
693
+ return 'bg-secondary text-white';
694
+ case 'retrying':
695
+ return 'bg-warning text-dark';
696
+ case 'idle':
697
+ return 'bg-light text-dark';
698
+ case 'pending':
699
+ return 'bg-primary text-white';
700
+ default:
701
+ return 'bg-secondary text-white';
702
+ }
703
+ }
704
+
705
+ // Format node name for display
706
+ function formatNodeName(nodeName) {
707
+ return nodeName
708
+ .replace(/_/g, ' ')
709
+ .split(' ')
710
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
711
+ .join(' ');
712
+ }
713
+
714
+ // Improved retry function with visual feedback
715
+ function retryJob(jobId) {
716
+ // Show toast notification
717
+ showToast(`Retrying job ${jobId}...`, 'info');
718
+
719
+ fetch('/api/retry', {
720
+ method: 'POST',
721
+ headers: {
722
+ 'Content-Type': 'application/json'
723
+ },
724
+ body: JSON.stringify({ job_id: jobId })
725
+ })
726
+ .then(response => response.json())
727
+ .then(data => {
728
+ if (data.success) {
729
+ showToast(`Job ${jobId} retry successfully initiated`, 'success');
730
+ // Refresh data after successful retry
731
+ setTimeout(fetchInitialData, 1000);
732
+ } else {
733
+ showToast(`Failed to retry job ${jobId}`, 'danger');
734
+ }
735
+ })
736
+ .catch(error => {
737
+ console.error('Error retrying job:', error);
738
+ showToast(`Error retrying job: ${error.message}`, 'danger');
739
+ });
740
+ }
741
+
742
+ // Toast notification helper
743
+ function showToast(message, type = 'info') {
744
+ // Create toast container if it doesn't exist
745
+ let toastContainer = document.getElementById('toast-container');
746
+ if (!toastContainer) {
747
+ toastContainer = document.createElement('div');
748
+ toastContainer.id = 'toast-container';
749
+ toastContainer.className = 'position-fixed bottom-0 end-0 p-3';
750
+ toastContainer.style.zIndex = '1050';
751
+ document.body.appendChild(toastContainer);
752
+ }
753
+
754
+ // Create toast element
755
+ const toastId = `toast-${Date.now()}`;
756
+ const toast = document.createElement('div');
757
+ toast.className = `toast align-items-center text-white bg-${type} border-0`;
758
+ toast.id = toastId;
759
+ toast.setAttribute('role', 'alert');
760
+ toast.setAttribute('aria-live', 'assertive');
761
+ toast.setAttribute('aria-atomic', 'true');
762
+
763
+ toast.innerHTML = `
764
+ <div class="d-flex">
765
+ <div class="toast-body">
766
+ ${message}
767
+ </div>
768
+ <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
769
+ </div>
770
+ `;
771
+
772
+ toastContainer.appendChild(toast);
773
+
774
+ // Initialize and show toast
775
+ const bsToast = new bootstrap.Toast(toast, {
776
+ autohide: true,
777
+ delay: 3000
778
+ });
779
+ bsToast.show();
780
+
781
+ // Remove toast after it's hidden
782
+ toast.addEventListener('hidden.bs.toast', function() {
783
+ toast.remove();
784
+ });
785
+ }
786
+
787
+ // Manual refresh button with visual feedback
788
+ document.getElementById('refresh-logs').addEventListener('click', function() {
789
+ const button = this;
790
+ const originalContent = button.innerHTML;
791
+
792
+ // Change button text and disable it
793
+ button.innerHTML = '<i class="bi bi-hourglass-split me-1"></i> Refreshing...';
794
+ button.disabled = true;
795
+
796
+ // Fetch data
797
+ fetchInitialData();
798
+
799
+ // Reset button after delay
800
+ setTimeout(() => {
801
+ button.innerHTML = originalContent;
802
+ button.disabled = false;
803
+ }, 1000);
804
+ });
805
+ </script>
806
+ </body>
807
+ </html>