edsl 0.1.50__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.
Files changed (119) hide show
  1. edsl/__init__.py +45 -34
  2. edsl/__version__.py +1 -1
  3. edsl/base/base_exception.py +2 -2
  4. edsl/buckets/bucket_collection.py +1 -1
  5. edsl/buckets/exceptions.py +32 -0
  6. edsl/buckets/token_bucket_api.py +26 -10
  7. edsl/caching/cache.py +5 -2
  8. edsl/caching/remote_cache_sync.py +5 -5
  9. edsl/caching/sql_dict.py +12 -11
  10. edsl/config/__init__.py +1 -1
  11. edsl/config/config_class.py +4 -2
  12. edsl/conversation/Conversation.py +9 -5
  13. edsl/conversation/car_buying.py +1 -3
  14. edsl/conversation/mug_negotiation.py +2 -6
  15. edsl/coop/__init__.py +11 -8
  16. edsl/coop/coop.py +15 -13
  17. edsl/coop/coop_functions.py +1 -1
  18. edsl/coop/ep_key_handling.py +1 -1
  19. edsl/coop/price_fetcher.py +2 -2
  20. edsl/coop/utils.py +2 -2
  21. edsl/dataset/dataset.py +144 -63
  22. edsl/dataset/dataset_operations_mixin.py +14 -6
  23. edsl/dataset/dataset_tree.py +3 -3
  24. edsl/dataset/display/table_renderers.py +6 -3
  25. edsl/dataset/file_exports.py +4 -4
  26. edsl/dataset/r/ggplot.py +3 -3
  27. edsl/inference_services/available_model_fetcher.py +2 -2
  28. edsl/inference_services/data_structures.py +5 -5
  29. edsl/inference_services/inference_service_abc.py +1 -1
  30. edsl/inference_services/inference_services_collection.py +1 -1
  31. edsl/inference_services/service_availability.py +3 -3
  32. edsl/inference_services/services/azure_ai.py +3 -3
  33. edsl/inference_services/services/google_service.py +1 -1
  34. edsl/inference_services/services/test_service.py +1 -1
  35. edsl/instructions/change_instruction.py +5 -4
  36. edsl/instructions/instruction.py +1 -0
  37. edsl/instructions/instruction_collection.py +5 -4
  38. edsl/instructions/instruction_handler.py +10 -8
  39. edsl/interviews/answering_function.py +20 -21
  40. edsl/interviews/exception_tracking.py +3 -2
  41. edsl/interviews/interview.py +1 -1
  42. edsl/interviews/interview_status_dictionary.py +1 -1
  43. edsl/interviews/interview_task_manager.py +7 -4
  44. edsl/interviews/request_token_estimator.py +3 -2
  45. edsl/interviews/statistics.py +2 -2
  46. edsl/invigilators/invigilators.py +34 -6
  47. edsl/jobs/__init__.py +39 -2
  48. edsl/jobs/async_interview_runner.py +1 -1
  49. edsl/jobs/check_survey_scenario_compatibility.py +5 -5
  50. edsl/jobs/data_structures.py +2 -2
  51. edsl/jobs/html_table_job_logger.py +494 -257
  52. edsl/jobs/jobs.py +2 -2
  53. edsl/jobs/jobs_checks.py +5 -5
  54. edsl/jobs/jobs_component_constructor.py +2 -2
  55. edsl/jobs/jobs_pricing_estimation.py +1 -1
  56. edsl/jobs/jobs_runner_asyncio.py +2 -2
  57. edsl/jobs/jobs_status_enums.py +1 -0
  58. edsl/jobs/remote_inference.py +47 -13
  59. edsl/jobs/results_exceptions_handler.py +2 -2
  60. edsl/language_models/language_model.py +151 -145
  61. edsl/notebooks/__init__.py +24 -1
  62. edsl/notebooks/exceptions.py +82 -0
  63. edsl/notebooks/notebook.py +7 -3
  64. edsl/notebooks/notebook_to_latex.py +1 -1
  65. edsl/prompts/__init__.py +23 -2
  66. edsl/prompts/prompt.py +1 -1
  67. edsl/questions/__init__.py +4 -4
  68. edsl/questions/answer_validator_mixin.py +0 -5
  69. edsl/questions/compose_questions.py +2 -2
  70. edsl/questions/descriptors.py +1 -1
  71. edsl/questions/question_base.py +32 -3
  72. edsl/questions/question_base_prompts_mixin.py +4 -4
  73. edsl/questions/question_budget.py +503 -102
  74. edsl/questions/question_check_box.py +658 -156
  75. edsl/questions/question_dict.py +176 -2
  76. edsl/questions/question_extract.py +401 -61
  77. edsl/questions/question_free_text.py +77 -9
  78. edsl/questions/question_functional.py +118 -9
  79. edsl/questions/{derived/question_likert_five.py → question_likert_five.py} +2 -2
  80. edsl/questions/{derived/question_linear_scale.py → question_linear_scale.py} +3 -4
  81. edsl/questions/question_list.py +246 -26
  82. edsl/questions/question_matrix.py +586 -73
  83. edsl/questions/question_multiple_choice.py +213 -47
  84. edsl/questions/question_numerical.py +360 -29
  85. edsl/questions/question_rank.py +401 -124
  86. edsl/questions/question_registry.py +3 -3
  87. edsl/questions/{derived/question_top_k.py → question_top_k.py} +3 -3
  88. edsl/questions/{derived/question_yes_no.py → question_yes_no.py} +3 -4
  89. edsl/questions/register_questions_meta.py +2 -1
  90. edsl/questions/response_validator_abc.py +6 -2
  91. edsl/questions/response_validator_factory.py +10 -12
  92. edsl/results/report.py +1 -1
  93. edsl/results/result.py +7 -4
  94. edsl/results/results.py +500 -271
  95. edsl/results/results_selector.py +2 -2
  96. edsl/scenarios/construct_download_link.py +3 -3
  97. edsl/scenarios/scenario.py +1 -2
  98. edsl/scenarios/scenario_list.py +41 -23
  99. edsl/surveys/survey_css.py +3 -3
  100. edsl/surveys/survey_simulator.py +2 -1
  101. edsl/tasks/__init__.py +22 -2
  102. edsl/tasks/exceptions.py +72 -0
  103. edsl/tasks/task_history.py +48 -11
  104. edsl/templates/error_reporting/base.html +37 -4
  105. edsl/templates/error_reporting/exceptions_table.html +105 -33
  106. edsl/templates/error_reporting/interview_details.html +130 -126
  107. edsl/templates/error_reporting/overview.html +21 -25
  108. edsl/templates/error_reporting/report.css +215 -46
  109. edsl/templates/error_reporting/report.js +122 -20
  110. edsl/tokens/__init__.py +27 -1
  111. edsl/tokens/exceptions.py +37 -0
  112. edsl/tokens/interview_token_usage.py +3 -2
  113. edsl/tokens/token_usage.py +4 -3
  114. {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/METADATA +1 -1
  115. {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/RECORD +118 -116
  116. edsl/questions/derived/__init__.py +0 -0
  117. {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/LICENSE +0 -0
  118. {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/WHEEL +0 -0
  119. {edsl-0.1.50.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, theme="auto", **kwargs):
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.theme = theme # Can be "auto", "light", or "dark"
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
- # Initialize CSS once when the logger is created
25
- self._init_css()
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
- def _init_css(self):
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
- """Initialize the CSS styles with enhanced theme support"""
30
- if not self.verbose:
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
- /* Base theme variables */
36
- :root {
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: 10px 0;
78
- color: var(--jl-text-primary);
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
- .job-logger-header {
85
- padding: 12px 16px;
86
- background: var(--jl-header-bg);
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
- .job-logger-table {
135
+ .jobs-content {
136
+ background: white;
137
+ }
138
+ .jobs-table {
109
139
  width: 100%;
110
- border-collapse: separate;
111
- border-spacing: 0;
112
- background: var(--jl-bg-primary);
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
- border-bottom: 1px solid var(--jl-border-color);
120
- line-height: 1.4;
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
- background: var(--jl-bg-secondary);
149
+ max-width: 140px;
150
+ background-color: #f7fafc;
151
+ font-weight: 500;
152
+ color: #4a5568;
128
153
  }
129
-
130
- .job-logger-value {
131
- color: var(--jl-text-secondary);
132
- word-break: break-word;
154
+ .jobs-table td {
155
+ width: 75%;
133
156
  }
134
-
135
- .job-logger-status {
136
- margin: 0;
137
- padding: 12px 16px;
138
- background-color: var(--jl-bg-secondary);
139
- border: 1px solid var(--jl-border-color);
140
- border-top: none;
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
- display(HTML(css + init_script))
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
- value = self._linkify(str(value)) if value else None
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
- info_rows += self._get_table_row(pretty_name, value)
275
-
276
- message_html = ""
277
- if self.current_message:
278
- spinner = self._get_spinner(status)
279
- message_html = f"""
280
- <div class="job-logger-status">
281
- {spinner}<strong>Current Status:</strong> {self._linkify(self.current_message)}
282
- </div>
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
- display_style = "block" if self.is_expanded else "none"
286
- arrow = "▼" if self.is_expanded else "▶"
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
- return f"""
289
- <!-- #region Remove Inference Info -->
290
- <div id="logger-{self.log_id}" class="job-logger">
291
- <div class="job-logger-header">
292
- <span>
293
- <span id="arrow-{self.log_id}">{arrow}</span>
294
- Job Status ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
295
- </span>
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
- <div id="content-{self.log_id}" style="display: {display_style};">
298
- <table class="job-logger-table">
299
- {info_rows}
300
- </table>
301
- {message_html}
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
- <!-- # endregion -->
453
+ </div>
305
454
  """
306
455
 
307
- def update(self, message: str, status: JobsStatus = JobsStatus.RUNNING):
308
- """Update the display with new message and current JobsInfo state"""
309
- self.current_message = message
310
- if self.verbose:
311
- self.display_handle.update(HTML(self._get_html(status)))
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
- return None
484
+ status_text = str(current_status).capitalize()
485
+
486
+ status_banner = f"""
487
+ <div class="status-banner">
488
+ {status_icon}
489
+ <strong>Status:</strong>&nbsp;<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 = '&#8963;';
534
+ }} else {{
535
+ content.style.display = 'none';
536
+ arrow.innerHTML = '&#8964;';
537
+ }}">
538
+ <div>
539
+ <span id="arrow-{self.log_id}" class="expand-toggle">{'&#8963;' if self.is_expanded else '&#8964;'}</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
+ """