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.
- michael_agent/dashboard/static/styles.css +311 -0
- michael_agent/dashboard/templates/career_portal.html +482 -0
- michael_agent/dashboard/templates/dashboard.html +807 -0
- michael_agent/dashboard/templates/jd_creation.html +318 -0
- michael_agent/dashboard/templates/resume_scoring.html +1032 -0
- michael_agent/dashboard/templates/upload_resume.html +411 -0
- {michael_agent-1.0.2.dist-info → michael_agent-1.0.4.dist-info}/METADATA +1 -1
- {michael_agent-1.0.2.dist-info → michael_agent-1.0.4.dist-info}/RECORD +10 -4
- {michael_agent-1.0.2.dist-info → michael_agent-1.0.4.dist-info}/WHEEL +0 -0
- {michael_agent-1.0.2.dist-info → michael_agent-1.0.4.dist-info}/top_level.txt +0 -0
@@ -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>
|