edsl 0.1.51__py3-none-any.whl → 0.1.52__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.
- edsl/__init__.py +45 -34
- edsl/__version__.py +1 -1
- edsl/conversation/Conversation.py +2 -1
- edsl/coop/coop.py +2 -0
- edsl/interviews/answering_function.py +20 -21
- edsl/interviews/exception_tracking.py +4 -3
- edsl/interviews/interview_task_manager.py +5 -2
- edsl/invigilators/invigilators.py +32 -4
- edsl/jobs/html_table_job_logger.py +494 -257
- edsl/jobs/jobs_status_enums.py +1 -0
- edsl/jobs/remote_inference.py +46 -12
- edsl/language_models/language_model.py +148 -146
- edsl/results/results.py +31 -2
- edsl/tasks/task_history.py +45 -8
- edsl/templates/error_reporting/base.html +37 -4
- edsl/templates/error_reporting/exceptions_table.html +105 -33
- edsl/templates/error_reporting/interview_details.html +130 -126
- edsl/templates/error_reporting/overview.html +21 -25
- edsl/templates/error_reporting/report.css +215 -46
- edsl/templates/error_reporting/report.js +122 -20
- {edsl-0.1.51.dist-info → edsl-0.1.52.dist-info}/METADATA +1 -1
- {edsl-0.1.51.dist-info → edsl-0.1.52.dist-info}/RECORD +25 -25
- {edsl-0.1.51.dist-info → edsl-0.1.52.dist-info}/LICENSE +0 -0
- {edsl-0.1.51.dist-info → edsl-0.1.52.dist-info}/WHEEL +0 -0
- {edsl-0.1.51.dist-info → edsl-0.1.52.dist-info}/entry_points.txt +0 -0
@@ -7,307 +7,544 @@ from IPython.display import display, HTML
|
|
7
7
|
from .jobs_remote_inference_logger import JobLogger
|
8
8
|
from .jobs_status_enums import JobsStatus
|
9
9
|
|
10
|
-
|
10
|
+
|
11
11
|
class HTMLTableJobLogger(JobLogger):
|
12
|
-
def __init__(self, verbose=True,
|
12
|
+
def __init__(self, verbose=True, **kwargs):
|
13
13
|
super().__init__(verbose=verbose)
|
14
|
-
|
15
|
-
self.display_handle = display(HTML(""), display_id=True) if verbose else None
|
16
|
-
#self.display_handle = display(HTML(""), display_id=True)
|
14
|
+
self.display_handle = display(HTML(""), display_id=True)
|
17
15
|
self.current_message = None
|
18
16
|
self.log_id = str(uuid.uuid4())
|
19
17
|
self.is_expanded = True
|
20
18
|
self.spinner_chars = ["◐", "◓", "◑", "◒"]
|
21
19
|
self.spinner_idx = 0
|
22
|
-
self.
|
20
|
+
self.messages = [] # Store message history
|
21
|
+
self.external_link_icon = """
|
22
|
+
<svg
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
24
|
+
width="24"
|
25
|
+
height="24"
|
26
|
+
viewBox="0 0 24 24"
|
27
|
+
fill="none"
|
28
|
+
stroke="currentColor"
|
29
|
+
stroke-width="2"
|
30
|
+
stroke-linecap="round"
|
31
|
+
stroke-linejoin="round"
|
32
|
+
class="external-link-icon"
|
33
|
+
>
|
34
|
+
<path d="M15 3h6v6" />
|
35
|
+
<path d="M10 14 21 3" />
|
36
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
37
|
+
</svg>
|
38
|
+
"""
|
39
|
+
|
40
|
+
def _get_status_icon(self, status: JobsStatus) -> str:
|
41
|
+
"""Return appropriate icon for job status"""
|
42
|
+
if status == JobsStatus.RUNNING:
|
43
|
+
spinner = self.spinner_chars[self.spinner_idx]
|
44
|
+
self.spinner_idx = (self.spinner_idx + 1) % len(self.spinner_chars)
|
45
|
+
return f'<span class="status-icon status-running">{spinner}</span>'
|
46
|
+
elif status == JobsStatus.COMPLETED:
|
47
|
+
return '<span class="status-icon status-completed">✓</span>'
|
48
|
+
elif status == JobsStatus.PARTIALLY_FAILED:
|
49
|
+
return '<span class="status-icon status-partially-failed">✗</span>'
|
50
|
+
elif status == JobsStatus.FAILED:
|
51
|
+
return '<span class="status-icon status-failed">✗</span>'
|
52
|
+
else:
|
53
|
+
return '<span class="status-icon status-unknown">•</span>'
|
23
54
|
|
24
|
-
|
25
|
-
|
55
|
+
def _linkify(self, text: str) -> str:
|
56
|
+
"""Convert markdown-style links to HTML links"""
|
57
|
+
markdown_pattern = r"\[(.*?)\]\((.*?)\)"
|
58
|
+
return re.sub(
|
59
|
+
markdown_pattern,
|
60
|
+
r'<a href="\2" target="_blank" class="link">\1</a>',
|
61
|
+
text,
|
62
|
+
)
|
63
|
+
|
64
|
+
def _create_uuid_copy_button(self, uuid_value: str) -> str:
|
65
|
+
"""Create a UUID display with click-to-copy functionality"""
|
66
|
+
short_uuid = uuid_value
|
67
|
+
if len(uuid_value) > 12:
|
68
|
+
short_uuid = f"{uuid_value[:8]}...{uuid_value[-4:]}"
|
26
69
|
|
27
|
-
|
70
|
+
return f"""
|
71
|
+
<div class="uuid-container" title="{uuid_value}">
|
72
|
+
<span class="uuid-code">{short_uuid}</span>
|
73
|
+
<button class="copy-btn" onclick="navigator.clipboard.writeText('{uuid_value}').then(() => {{
|
74
|
+
const btn = this;
|
75
|
+
btn.querySelector('.copy-icon').style.display = 'none';
|
76
|
+
btn.querySelector('.check-icon').style.display = 'block';
|
77
|
+
setTimeout(() => {{
|
78
|
+
btn.querySelector('.check-icon').style.display = 'none';
|
79
|
+
btn.querySelector('.copy-icon').style.display = 'block';
|
80
|
+
}}, 1000);
|
81
|
+
}})" title="Copy to clipboard">
|
82
|
+
<svg class="copy-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
83
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
84
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
85
|
+
</svg>
|
86
|
+
<svg class="check-icon" style="display: none" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
87
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
88
|
+
</svg>
|
89
|
+
</button>
|
90
|
+
</div>
|
91
|
+
"""
|
92
|
+
|
93
|
+
def update(self, message: str, status: JobsStatus = JobsStatus.RUNNING):
|
94
|
+
"""Update the display with new message and current JobsInfo state"""
|
95
|
+
self.current_message = message
|
96
|
+
# Add to message history with timestamp
|
97
|
+
self.messages.append(
|
98
|
+
{
|
99
|
+
"text": message,
|
100
|
+
"status": status,
|
101
|
+
"timestamp": datetime.now().strftime("%H:%M:%S"),
|
102
|
+
}
|
103
|
+
)
|
28
104
|
|
29
|
-
|
30
|
-
|
105
|
+
if self.verbose:
|
106
|
+
self.display_handle.update(HTML(self._get_html(status)))
|
107
|
+
else:
|
31
108
|
return None
|
32
|
-
|
109
|
+
|
110
|
+
def _get_html(self, current_status: JobsStatus = JobsStatus.RUNNING) -> str:
|
111
|
+
"""Generate the complete HTML display with modern design"""
|
112
|
+
# CSS for modern styling
|
33
113
|
css = """
|
34
114
|
<style>
|
35
|
-
|
36
|
-
|
37
|
-
--jl-bg-primary: #ffffff;
|
38
|
-
--jl-bg-secondary: #f5f5f5;
|
39
|
-
--jl-border-color: #e0e0e0;
|
40
|
-
--jl-text-primary: #24292e;
|
41
|
-
--jl-text-secondary: #586069;
|
42
|
-
--jl-link-color: #0366d6;
|
43
|
-
--jl-success-color: #28a745;
|
44
|
-
--jl-error-color: #d73a49;
|
45
|
-
--jl-header-bg: #f1f1f1;
|
46
|
-
}
|
47
|
-
|
48
|
-
/* Dark theme variables */
|
49
|
-
.theme-dark {
|
50
|
-
--jl-bg-primary: #1e1e1e;
|
51
|
-
--jl-bg-secondary: #252526;
|
52
|
-
--jl-border-color: #2d2d2d;
|
53
|
-
--jl-text-primary: #cccccc;
|
54
|
-
--jl-text-secondary: #999999;
|
55
|
-
--jl-link-color: #4e94ce;
|
56
|
-
--jl-success-color: #89d185;
|
57
|
-
--jl-error-color: #f14c4c;
|
58
|
-
--jl-header-bg: #333333;
|
59
|
-
}
|
60
|
-
|
61
|
-
/* High contrast theme variables */
|
62
|
-
.theme-high-contrast {
|
63
|
-
--jl-bg-primary: #000000;
|
64
|
-
--jl-bg-secondary: #1a1a1a;
|
65
|
-
--jl-border-color: #404040;
|
66
|
-
--jl-text-primary: #ffffff;
|
67
|
-
--jl-text-secondary: #cccccc;
|
68
|
-
--jl-link-color: #66b3ff;
|
69
|
-
--jl-success-color: #00ff00;
|
70
|
-
--jl-error-color: #ff0000;
|
71
|
-
--jl-header-bg: #262626;
|
72
|
-
}
|
73
|
-
|
74
|
-
.job-logger {
|
75
|
-
font-family: system-ui, -apple-system, sans-serif;
|
115
|
+
.jobs-container {
|
116
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
76
117
|
max-width: 800px;
|
77
|
-
margin:
|
78
|
-
|
79
|
-
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
80
|
-
border-radius: 4px;
|
118
|
+
margin: 16px 0;
|
119
|
+
border-radius: 8px;
|
81
120
|
overflow: hidden;
|
121
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
|
122
|
+
color: #1a1a1a;
|
82
123
|
}
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
border: none;
|
88
|
-
border-radius: 4px 4px 0 0;
|
124
|
+
.jobs-header {
|
125
|
+
padding: 8px 12px;
|
126
|
+
background: linear-gradient(to right, #f7f9fc, #edf2f7);
|
127
|
+
border-bottom: 1px solid #e2e8f0;
|
89
128
|
cursor: pointer;
|
90
|
-
color: var(--jl-text-primary);
|
91
|
-
user-select: none;
|
92
|
-
font-weight: 500;
|
93
|
-
letter-spacing: 0.3px;
|
94
129
|
display: flex;
|
95
|
-
justify-content: space-between;
|
96
130
|
align-items: center;
|
97
|
-
|
98
|
-
|
99
|
-
.theme-select {
|
100
|
-
padding: 4px 8px;
|
101
|
-
border-radius: 4px;
|
102
|
-
border: 1px solid var(--jl-border-color);
|
103
|
-
background: var(--jl-bg-primary);
|
104
|
-
color: var(--jl-text-primary);
|
131
|
+
justify-content: space-between;
|
132
|
+
font-weight: 500;
|
105
133
|
font-size: 0.9em;
|
106
134
|
}
|
107
|
-
|
108
|
-
|
135
|
+
.jobs-content {
|
136
|
+
background: white;
|
137
|
+
}
|
138
|
+
.jobs-table {
|
109
139
|
width: 100%;
|
110
|
-
border-collapse:
|
111
|
-
|
112
|
-
|
113
|
-
border: 1px solid var(--jl-border-color);
|
114
|
-
margin-top: -1px;
|
115
|
-
}
|
116
|
-
|
117
|
-
.job-logger-cell {
|
140
|
+
border-collapse: collapse;
|
141
|
+
}
|
142
|
+
.jobs-table th, .jobs-table td {
|
118
143
|
padding: 12px 16px;
|
119
|
-
|
120
|
-
|
144
|
+
text-align: left;
|
145
|
+
border-bottom: 1px solid #edf2f7;
|
121
146
|
}
|
122
|
-
|
123
|
-
.job-logger-label {
|
124
|
-
font-weight: 500;
|
125
|
-
color: var(--jl-text-primary);
|
147
|
+
.jobs-table th {
|
126
148
|
width: 25%;
|
127
|
-
|
149
|
+
max-width: 140px;
|
150
|
+
background-color: #f7fafc;
|
151
|
+
font-weight: 500;
|
152
|
+
color: #4a5568;
|
128
153
|
}
|
129
|
-
|
130
|
-
|
131
|
-
color: var(--jl-text-secondary);
|
132
|
-
word-break: break-word;
|
154
|
+
.jobs-table td {
|
155
|
+
width: 75%;
|
133
156
|
}
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
background-color:
|
139
|
-
border: 1px solid
|
140
|
-
|
141
|
-
border-radius: 0 0 4px 4px;
|
142
|
-
color: var(--jl-text-primary);
|
143
|
-
font-size: 0.95em;
|
157
|
+
.section-header {
|
158
|
+
padding: 5px 12px;
|
159
|
+
font-weight: 600;
|
160
|
+
color: #1f2937;
|
161
|
+
background-color: #f7fafc;
|
162
|
+
border-bottom: 1px solid #cbd5e1;
|
163
|
+
font-size: 0.85em;
|
144
164
|
}
|
165
|
+
.two-column-grid {
|
166
|
+
display: flex;
|
167
|
+
flex-wrap: wrap;
|
168
|
+
gap: 1px;
|
169
|
+
background-color: #e2e8f0;
|
170
|
+
}
|
171
|
+
.column {
|
172
|
+
background-color: white;
|
173
|
+
}
|
174
|
+
.column:first-child {
|
175
|
+
flex: 1;
|
176
|
+
min-width: 150px;
|
177
|
+
}
|
178
|
+
.column:last-child {
|
179
|
+
flex: 2;
|
180
|
+
min-width: 300px;
|
181
|
+
}
|
182
|
+
.content-box {
|
183
|
+
padding: 5px 12px;
|
184
|
+
}
|
185
|
+
.link-item {
|
186
|
+
padding: 3px 0;
|
187
|
+
border-bottom: 1px solid #f1f5f9;
|
188
|
+
}
|
189
|
+
.link-item:last-child {
|
190
|
+
border-bottom: none;
|
191
|
+
}
|
192
|
+
.results-link .pill-link {
|
193
|
+
color: #059669;
|
194
|
+
font-weight: 500;
|
195
|
+
}
|
196
|
+
.results-link .pill-link:hover {
|
197
|
+
border-bottom-color: #059669;
|
198
|
+
}
|
199
|
+
.progress-link .pill-link {
|
200
|
+
color: #3b82f6;
|
201
|
+
font-weight: 500;
|
202
|
+
}
|
203
|
+
.progress-link .pill-link:hover {
|
204
|
+
border-bottom-color: #3b82f6;
|
205
|
+
}
|
206
|
+
.uuid-item {
|
207
|
+
padding: 3px 0;
|
208
|
+
border-bottom: 1px solid #f1f5f9;
|
209
|
+
display: flex;
|
210
|
+
align-items: center;
|
211
|
+
}
|
212
|
+
.uuid-item:last-child {
|
213
|
+
border-bottom: none;
|
214
|
+
}
|
215
|
+
.uuid-label {
|
216
|
+
font-weight: 500;
|
217
|
+
color: #4b5563;
|
218
|
+
font-size: 0.75em;
|
219
|
+
margin-right: 6px;
|
220
|
+
min-width: 80px;
|
221
|
+
}
|
222
|
+
.compact-links {
|
223
|
+
padding: 8px 16px;
|
224
|
+
line-height: 1.5;
|
225
|
+
}
|
226
|
+
.pill-link {
|
227
|
+
color: #3b82f6;
|
228
|
+
font-weight: 500;
|
229
|
+
text-decoration: none;
|
230
|
+
border-bottom: 1px dotted #bfdbfe;
|
231
|
+
transition: border-color 0.2s;
|
232
|
+
font-size: 0.75em;
|
233
|
+
display: inline-flex;
|
234
|
+
align-items: center;
|
235
|
+
gap: 4px;
|
236
|
+
}
|
237
|
+
.pill-link:hover {
|
238
|
+
border-bottom: 1px solid #3b82f6;
|
239
|
+
}
|
240
|
+
.external-link-icon {
|
241
|
+
width: 12px;
|
242
|
+
height: 12px;
|
243
|
+
opacity: 0.7;
|
244
|
+
}
|
245
|
+
.status-banner {
|
246
|
+
display: flex;
|
247
|
+
align-items: center;
|
248
|
+
padding: 5px 12px;
|
249
|
+
background-color: #f7fafc;
|
250
|
+
border-top: 1px solid #edf2f7;
|
251
|
+
font-size: 0.85em;
|
252
|
+
}
|
253
|
+
.status-running { color: #3b82f6; }
|
254
|
+
.status-completed { color: #10b981; }
|
255
|
+
.status-partially-failed { color: #d97706; }
|
256
|
+
.status-failed { color: #ef4444; }
|
257
|
+
.status-unknown { color: #6b7280; }
|
258
|
+
.status-icon {
|
259
|
+
display: inline-flex;
|
260
|
+
align-items: center;
|
261
|
+
justify-content: center;
|
262
|
+
width: 18px;
|
263
|
+
height: 18px;
|
264
|
+
margin-right: 6px;
|
265
|
+
font-weight: bold;
|
266
|
+
}
|
267
|
+
.link {
|
268
|
+
color: #3b82f6;
|
269
|
+
text-decoration: none;
|
270
|
+
border-bottom: 1px dotted #bfdbfe;
|
271
|
+
transition: border-color 0.2s;
|
272
|
+
}
|
273
|
+
.link:hover {
|
274
|
+
border-bottom: 1px solid #3b82f6;
|
275
|
+
}
|
276
|
+
.uuid-container {
|
277
|
+
display: flex;
|
278
|
+
align-items: center;
|
279
|
+
background-color: #f8fafc;
|
280
|
+
border-radius: 3px;
|
281
|
+
padding: 2px 6px;
|
282
|
+
font-family: monospace;
|
283
|
+
font-size: 0.75em;
|
284
|
+
flex: 1;
|
285
|
+
}
|
286
|
+
.uuid-code {
|
287
|
+
color: #4b5563;
|
288
|
+
overflow: hidden;
|
289
|
+
text-overflow: ellipsis;
|
290
|
+
flex: 1;
|
291
|
+
}
|
292
|
+
.copy-btn {
|
293
|
+
background-color: #e2e8f0;
|
294
|
+
border: none;
|
295
|
+
cursor: pointer;
|
296
|
+
margin-left: 4px;
|
297
|
+
padding: 4px;
|
298
|
+
border-radius: 3px;
|
299
|
+
transition: all 0.2s ease;
|
300
|
+
color: #4b5563;
|
301
|
+
display: flex;
|
302
|
+
align-items: center;
|
303
|
+
justify-content: center;
|
304
|
+
width: 22px;
|
305
|
+
height: 22px;
|
306
|
+
}
|
307
|
+
.copy-btn:hover {
|
308
|
+
background-color: #cbd5e1;
|
309
|
+
color: #1f2937;
|
310
|
+
}
|
311
|
+
.copy-icon, .check-icon {
|
312
|
+
display: block;
|
313
|
+
}
|
314
|
+
.message-log {
|
315
|
+
max-height: 120px;
|
316
|
+
overflow-y: auto;
|
317
|
+
border-top: 1px solid #edf2f7;
|
318
|
+
padding: 3px 0;
|
319
|
+
font-size: 0.85em;
|
320
|
+
}
|
321
|
+
.message-item {
|
322
|
+
display: flex;
|
323
|
+
padding: 2px 12px;
|
324
|
+
}
|
325
|
+
.message-timestamp {
|
326
|
+
color: #9ca3af;
|
327
|
+
font-size: 0.85em;
|
328
|
+
margin-right: 8px;
|
329
|
+
white-space: nowrap;
|
330
|
+
}
|
331
|
+
.status-indicator {
|
332
|
+
display: inline-block;
|
333
|
+
width: 8px;
|
334
|
+
height: 8px;
|
335
|
+
border-radius: 50%;
|
336
|
+
margin-right: 8px;
|
337
|
+
}
|
338
|
+
.expand-toggle {
|
339
|
+
display: inline-block;
|
340
|
+
width: 16px;
|
341
|
+
text-align: center;
|
342
|
+
margin-right: 6px;
|
343
|
+
font-size: 12px;
|
344
|
+
}
|
345
|
+
.badge {
|
346
|
+
display: inline-block;
|
347
|
+
padding: 2px 6px;
|
348
|
+
border-radius: 3px;
|
349
|
+
font-size: 0.8em;
|
350
|
+
font-weight: 500;
|
351
|
+
}
|
352
|
+
.status-running.badge { background-color: #dbeafe; }
|
353
|
+
.status-completed.badge { background-color: #d1fae5; }
|
354
|
+
.status-partially-failed.badge { background-color: #fef3c7; }
|
355
|
+
.status-failed.badge { background-color: #fee2e2; }
|
145
356
|
</style>
|
146
|
-
|
147
|
-
<script>
|
148
|
-
class ThemeManager {
|
149
|
-
constructor(logId, initialTheme = 'auto') {
|
150
|
-
this.logId = logId;
|
151
|
-
this.currentTheme = initialTheme;
|
152
|
-
this.darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
153
|
-
this.init();
|
154
|
-
}
|
155
|
-
|
156
|
-
init() {
|
157
|
-
this.setupThemeSwitcher();
|
158
|
-
this.updateTheme(this.currentTheme);
|
159
|
-
|
160
|
-
this.darkModeMediaQuery.addListener(() => {
|
161
|
-
if (this.currentTheme === 'auto') {
|
162
|
-
this.updateTheme('auto');
|
163
|
-
}
|
164
|
-
});
|
165
|
-
}
|
166
|
-
|
167
|
-
setupThemeSwitcher() {
|
168
|
-
const logger = document.querySelector(`#logger-${this.logId}`);
|
169
|
-
if (!logger) return;
|
170
|
-
|
171
|
-
const switcher = document.createElement('div');
|
172
|
-
switcher.className = 'theme-switcher';
|
173
|
-
switcher.innerHTML = `
|
174
|
-
<select id="theme-select-${this.logId}" class="theme-select">
|
175
|
-
<option value="auto">Auto</option>
|
176
|
-
<option value="light">Light</option>
|
177
|
-
<option value="dark">Dark</option>
|
178
|
-
<option value="high-contrast">High Contrast</option>
|
179
|
-
</select>
|
180
|
-
`;
|
181
|
-
|
182
|
-
const header = logger.querySelector('.job-logger-header');
|
183
|
-
header.appendChild(switcher);
|
184
|
-
|
185
|
-
const select = switcher.querySelector('select');
|
186
|
-
select.value = this.currentTheme;
|
187
|
-
select.addEventListener('change', (e) => {
|
188
|
-
this.updateTheme(e.target.value);
|
189
|
-
});
|
190
|
-
}
|
191
|
-
|
192
|
-
updateTheme(theme) {
|
193
|
-
const logger = document.querySelector(`#logger-${this.logId}`);
|
194
|
-
if (!logger) return;
|
195
|
-
|
196
|
-
this.currentTheme = theme;
|
197
|
-
|
198
|
-
logger.classList.remove('theme-light', 'theme-dark', 'theme-high-contrast');
|
199
|
-
|
200
|
-
if (theme === 'auto') {
|
201
|
-
const isDark = this.darkModeMediaQuery.matches;
|
202
|
-
logger.classList.add(isDark ? 'theme-dark' : 'theme-light');
|
203
|
-
} else {
|
204
|
-
logger.classList.add(`theme-${theme}`);
|
205
|
-
}
|
206
|
-
|
207
|
-
try {
|
208
|
-
localStorage.setItem('jobLoggerTheme', theme);
|
209
|
-
} catch (e) {
|
210
|
-
console.warn('Unable to save theme preference:', e);
|
211
|
-
}
|
212
|
-
}
|
213
|
-
}
|
214
|
-
|
215
|
-
window.initThemeManager = (logId, initialTheme) => {
|
216
|
-
new ThemeManager(logId, initialTheme);
|
217
|
-
};
|
218
|
-
</script>
|
219
|
-
"""
|
220
|
-
|
221
|
-
init_script = f"""
|
222
|
-
<script>
|
223
|
-
document.addEventListener('DOMContentLoaded', () => {{
|
224
|
-
window.initThemeManager('{self.log_id}', '{self.theme}');
|
225
|
-
}});
|
226
|
-
</script>
|
227
357
|
"""
|
228
|
-
|
229
358
|
|
230
|
-
|
359
|
+
# Group JobsInfo fields into categories
|
360
|
+
url_fields = []
|
361
|
+
uuid_fields = []
|
362
|
+
other_fields = []
|
231
363
|
|
232
|
-
def _get_table_row(self, key: str, value: str) -> str:
|
233
|
-
"""Generate a table row with key-value pair"""
|
234
|
-
return f"""
|
235
|
-
<tr>
|
236
|
-
<td class="job-logger-cell job-logger-label">{key}</td>
|
237
|
-
<td class="job-logger-cell job-logger-value">{value if value else 'None'}</td>
|
238
|
-
</tr>
|
239
|
-
"""
|
240
|
-
|
241
|
-
def _linkify(self, text: str) -> str:
|
242
|
-
"""Convert URLs in text to clickable links"""
|
243
|
-
url_pattern = r'(https?://[^\s<>"]+|www\.[^\s<>"]+)'
|
244
|
-
return re.sub(
|
245
|
-
url_pattern,
|
246
|
-
r'<a href="\1" target="_blank" class="job-logger-link">\1</a>',
|
247
|
-
text,
|
248
|
-
)
|
249
|
-
|
250
|
-
def _get_spinner(self, status: JobsStatus) -> str:
|
251
|
-
"""Get the current spinner frame if status is running"""
|
252
|
-
if status == JobsStatus.RUNNING:
|
253
|
-
spinner = self.spinner_chars[self.spinner_idx]
|
254
|
-
self.spinner_idx = (self.spinner_idx + 1) % len(self.spinner_chars)
|
255
|
-
return f'<span style="margin-right: 8px;">{spinner}</span>'
|
256
|
-
elif status == JobsStatus.COMPLETED:
|
257
|
-
return (
|
258
|
-
'<span style="margin-right: 8px;" class="job-logger-success">✓</span>'
|
259
|
-
)
|
260
|
-
elif status == JobsStatus.FAILED:
|
261
|
-
return '<span style="margin-right: 8px;" class="job-logger-error">✗</span>'
|
262
|
-
return ""
|
263
|
-
|
264
|
-
def _get_html(self, status: JobsStatus = JobsStatus.RUNNING) -> str:
|
265
|
-
"""Generate the complete HTML display with theme support"""
|
266
|
-
info_rows = ""
|
267
364
|
for field, _ in self.jobs_info.__annotations__.items():
|
268
365
|
if field != "pretty_names":
|
269
366
|
value = getattr(self.jobs_info, field)
|
270
|
-
|
367
|
+
if not value:
|
368
|
+
continue
|
369
|
+
|
271
370
|
pretty_name = self.jobs_info.pretty_names.get(
|
272
371
|
field, field.replace("_", " ").title()
|
273
372
|
)
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
373
|
+
|
374
|
+
if "url" in field.lower():
|
375
|
+
url_fields.append((field, pretty_name, value))
|
376
|
+
elif "uuid" in field.lower():
|
377
|
+
uuid_fields.append((field, pretty_name, value))
|
378
|
+
else:
|
379
|
+
other_fields.append((field, pretty_name, value))
|
380
|
+
|
381
|
+
# Build a two-column layout with links and UUIDs
|
382
|
+
content_html = """
|
383
|
+
<div class="two-column-grid">
|
384
|
+
<div class="column">
|
385
|
+
<div class="section-header">Links</div>
|
386
|
+
<div class="content-box">
|
387
|
+
"""
|
388
|
+
|
389
|
+
# Sort URLs to prioritize Results first, then Progress Bar
|
390
|
+
results_links = []
|
391
|
+
progress_links = []
|
392
|
+
other_links = []
|
393
|
+
|
394
|
+
for field, pretty_name, value in url_fields:
|
395
|
+
# Replace "Progress Bar" with "Progress Report"
|
396
|
+
if "progress_bar" in field.lower():
|
397
|
+
pretty_name = "Progress Report URL"
|
398
|
+
|
399
|
+
label = pretty_name.replace(" URL", "")
|
400
|
+
|
401
|
+
if "result" in field.lower():
|
402
|
+
results_links.append((field, pretty_name, value, label))
|
403
|
+
elif "progress" in field.lower():
|
404
|
+
progress_links.append((field, pretty_name, value, label))
|
405
|
+
else:
|
406
|
+
other_links.append((field, pretty_name, value, label))
|
407
|
+
|
408
|
+
# Add results links first with special styling
|
409
|
+
for field, pretty_name, value, label in results_links:
|
410
|
+
content_html += f"""
|
411
|
+
<div class="link-item results-link">
|
412
|
+
<a href="{value}" target="_blank" class="pill-link">{label}{self.external_link_icon}</a>
|
413
|
+
</div>
|
283
414
|
"""
|
284
415
|
|
285
|
-
|
286
|
-
|
416
|
+
# Then add progress links with different special styling
|
417
|
+
for field, pretty_name, value, label in progress_links:
|
418
|
+
content_html += f"""
|
419
|
+
<div class="link-item progress-link">
|
420
|
+
<a href="{value}" target="_blank" class="pill-link">{label}{self.external_link_icon}</a>
|
421
|
+
</div>
|
422
|
+
"""
|
287
423
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
424
|
+
# Then add other links
|
425
|
+
for field, pretty_name, value, label in other_links:
|
426
|
+
content_html += f"""
|
427
|
+
<div class="link-item">
|
428
|
+
<a href="{value}" target="_blank" class="pill-link">{label}{self.external_link_icon}</a>
|
429
|
+
</div>
|
430
|
+
"""
|
431
|
+
|
432
|
+
content_html += """
|
296
433
|
</div>
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
434
|
+
</div>
|
435
|
+
<div class="column">
|
436
|
+
<div class="section-header">Identifiers</div>
|
437
|
+
<div class="content-box">
|
438
|
+
"""
|
439
|
+
|
440
|
+
# Sort UUIDs to prioritize Result UUID first
|
441
|
+
uuid_fields.sort(key=lambda x: 0 if "result" in x[0].lower() else 1)
|
442
|
+
for field, pretty_name, value in uuid_fields:
|
443
|
+
# Create single-line UUID displays
|
444
|
+
content_html += f"""
|
445
|
+
<div class="uuid-item">
|
446
|
+
<span class="uuid-label">{pretty_name}:</span>{self._create_uuid_copy_button(value)}
|
447
|
+
</div>
|
448
|
+
"""
|
449
|
+
|
450
|
+
content_html += """
|
302
451
|
</div>
|
303
452
|
</div>
|
304
|
-
|
453
|
+
</div>
|
305
454
|
"""
|
306
455
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
456
|
+
# Add other fields in full width if any
|
457
|
+
if other_fields:
|
458
|
+
content_html += "<div class='section-header'>Additional Information</div>"
|
459
|
+
content_html += "<table class='jobs-table'>"
|
460
|
+
for field, pretty_name, value in other_fields:
|
461
|
+
content_html += f"""
|
462
|
+
<tr>
|
463
|
+
<th>{pretty_name}</th>
|
464
|
+
<td>{value}</td>
|
465
|
+
</tr>
|
466
|
+
"""
|
467
|
+
content_html += "</table>"
|
468
|
+
|
469
|
+
# Status banner
|
470
|
+
status_class = {
|
471
|
+
JobsStatus.RUNNING: "status-running",
|
472
|
+
JobsStatus.COMPLETED: "status-completed",
|
473
|
+
JobsStatus.PARTIALLY_FAILED: "status-partially-failed",
|
474
|
+
JobsStatus.FAILED: "status-failed",
|
475
|
+
}.get(current_status, "status-unknown")
|
476
|
+
|
477
|
+
status_icon = self._get_status_icon(current_status)
|
478
|
+
status_text = current_status.name.capitalize()
|
479
|
+
if current_status == JobsStatus.PARTIALLY_FAILED:
|
480
|
+
status_text = "Partially failed"
|
481
|
+
elif hasattr(current_status, "name"):
|
482
|
+
status_text = current_status.name.capitalize()
|
312
483
|
else:
|
313
|
-
|
484
|
+
status_text = str(current_status).capitalize()
|
485
|
+
|
486
|
+
status_banner = f"""
|
487
|
+
<div class="status-banner">
|
488
|
+
{status_icon}
|
489
|
+
<strong>Status:</strong> <span class="badge {status_class}">{status_text}</span>
|
490
|
+
<span style="flex-grow: 1;"></span>
|
491
|
+
<span>Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</span>
|
492
|
+
</div>
|
493
|
+
"""
|
494
|
+
|
495
|
+
# Message history
|
496
|
+
message_log = ""
|
497
|
+
if self.messages:
|
498
|
+
message_items = []
|
499
|
+
for msg in self.messages:
|
500
|
+
status_color = {
|
501
|
+
JobsStatus.RUNNING: "#3b82f6",
|
502
|
+
JobsStatus.COMPLETED: "#10b981",
|
503
|
+
JobsStatus.PARTIALLY_FAILED: "#f59e0b",
|
504
|
+
JobsStatus.FAILED: "#ef4444",
|
505
|
+
}.get(msg["status"], "#6b7280")
|
506
|
+
|
507
|
+
message_items.append(
|
508
|
+
f"""
|
509
|
+
<div class="message-item">
|
510
|
+
<span class="message-timestamp">{msg["timestamp"]}</span>
|
511
|
+
<span class="status-indicator" style="background-color: {status_color};"></span>
|
512
|
+
<div>{self._linkify(msg["text"])}</div>
|
513
|
+
</div>
|
514
|
+
"""
|
515
|
+
)
|
516
|
+
|
517
|
+
message_log = f"""
|
518
|
+
<div class="message-log">
|
519
|
+
{''.join(reversed(message_items))}
|
520
|
+
</div>
|
521
|
+
"""
|
522
|
+
|
523
|
+
display_style = "block" if self.is_expanded else "none"
|
524
|
+
|
525
|
+
return f"""
|
526
|
+
{css}
|
527
|
+
<div class="jobs-container">
|
528
|
+
<div class="jobs-header" onclick="
|
529
|
+
const content = document.getElementById('content-{self.log_id}');
|
530
|
+
const arrow = document.getElementById('arrow-{self.log_id}');
|
531
|
+
if (content.style.display === 'none') {{
|
532
|
+
content.style.display = 'block';
|
533
|
+
arrow.innerHTML = '⌃';
|
534
|
+
}} else {{
|
535
|
+
content.style.display = 'none';
|
536
|
+
arrow.innerHTML = '⌄';
|
537
|
+
}}">
|
538
|
+
<div>
|
539
|
+
<span id="arrow-{self.log_id}" class="expand-toggle">{'⌃' if self.is_expanded else '⌄'}</span>
|
540
|
+
Job Status 🦜
|
541
|
+
</div>
|
542
|
+
<div class="{status_class}">{status_text}</div>
|
543
|
+
</div>
|
544
|
+
<div id="content-{self.log_id}" class="jobs-content" style="display: {display_style};">
|
545
|
+
{content_html}
|
546
|
+
{status_banner}
|
547
|
+
{message_log}
|
548
|
+
</div>
|
549
|
+
</div>
|
550
|
+
"""
|