edsl 0.1.56__py3-none-any.whl → 0.1.58__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.
@@ -1,6 +1,7 @@
1
1
  import re
2
2
  import uuid
3
3
  from datetime import datetime
4
+ from typing import Union
4
5
 
5
6
  from IPython.display import display, HTML
6
7
 
@@ -61,16 +62,28 @@ class HTMLTableJobLogger(JobLogger):
61
62
  text,
62
63
  )
63
64
 
64
- def _create_uuid_copy_button(self, uuid_value: str) -> str:
65
+ def _create_uuid_copy_button(
66
+ self, uuid_value: str, helper_text: Union[str, None] = None
67
+ ) -> str:
65
68
  """Create a UUID display with click-to-copy functionality"""
66
69
  short_uuid = uuid_value
67
70
  if len(uuid_value) > 12:
68
71
  short_uuid = f"{uuid_value[:8]}...{uuid_value[-4:]}"
69
72
 
70
73
  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
+ <div class="uuid-container-wrapper">
75
+ <div class="uuid-container" title="{uuid_value}">
76
+ <span class="uuid-code">{short_uuid}</span>
77
+ {self._create_copy_button(uuid_value)}
78
+ </div>
79
+ {f'<div class="helper-text">{helper_text}</div>' if helper_text else ''}
80
+ </div>
81
+ """
82
+
83
+ def _create_copy_button(self, value: str) -> str:
84
+ """Create a button with click-to-copy functionality"""
85
+ return f"""
86
+ <button class="copy-btn" onclick="navigator.clipboard.writeText('{value}').then(() => {{
74
87
  const btn = this;
75
88
  btn.querySelector('.copy-icon').style.display = 'none';
76
89
  btn.querySelector('.check-icon').style.display = 'block';
@@ -87,7 +100,6 @@ class HTMLTableJobLogger(JobLogger):
87
100
  <polyline points="20 6 9 17 4 12"></polyline>
88
101
  </svg>
89
102
  </button>
90
- </div>
91
103
  """
92
104
 
93
105
  def update(self, message: str, status: JobsStatus = JobsStatus.RUNNING):
@@ -107,9 +119,163 @@ class HTMLTableJobLogger(JobLogger):
107
119
  else:
108
120
  return None
109
121
 
122
+ def _collapse(self, content_id: str, arrow_id: str) -> str:
123
+ """Generate the onclick JavaScript for collapsible sections"""
124
+ return f"""
125
+ const content = document.getElementById('{content_id}');
126
+ const arrow = document.getElementById('{arrow_id}');
127
+ if (content.style.display === 'none') {{
128
+ content.style.display = 'block';
129
+ arrow.innerHTML = '&#8963;';
130
+ }} else {{
131
+ content.style.display = 'none';
132
+ arrow.innerHTML = '&#8964;';
133
+ }}
134
+ """
135
+
136
+ def _build_exceptions_table(self) -> str:
137
+ """Generate HTML for the exceptions summary table section."""
138
+ if not self.jobs_info.exception_summary:
139
+ return ""
140
+
141
+ total_exceptions = sum(
142
+ exc.exception_count for exc in self.jobs_info.exception_summary
143
+ )
144
+
145
+ # Generate exception rows HTML before the return
146
+ exception_rows = "".join(
147
+ f"""
148
+ <tr>
149
+ <td>{exc.exception_type or '-'}</td>
150
+ <td>{exc.inference_service or '-'}</td>
151
+ <td>{exc.model or '-'}</td>
152
+ <td>{exc.question_name or '-'}</td>
153
+ <td class='exception-count'>{exc.exception_count:,}</td>
154
+ </tr>
155
+ """
156
+ for exc in self.jobs_info.exception_summary
157
+ )
158
+
159
+ # Get the error report URL if it exists
160
+ error_report_url = getattr(self.jobs_info, "error_report_url", None)
161
+ error_report_link = (
162
+ f"""
163
+ <div style="margin-bottom: 12px; font-size: 0.85em;">
164
+ <a href="{error_report_url}" target="_blank" class="pill-link">
165
+ View full exceptions report{self.external_link_icon}
166
+ </a>
167
+ </div>
168
+ """
169
+ if error_report_url
170
+ else ""
171
+ )
172
+
173
+ return f"""
174
+ <div class="exception-section">
175
+ <div class="exception-header" onclick="{self._collapse(f'exception-content-{self.log_id}', f'exception-arrow-{self.log_id}')}">
176
+ <span id="exception-arrow-{self.log_id}" class="expand-toggle">&#8963;</span>
177
+ <span>Exception Summary ({total_exceptions:,} total)</span>
178
+ <span style="flex-grow: 1;"></span>
179
+ </div>
180
+ <div id="exception-content-{self.log_id}" class="exception-content">
181
+ {error_report_link}
182
+ <table class='exception-table'>
183
+ <thead>
184
+ <tr>
185
+ <th>Exception Type</th>
186
+ <th>Service</th>
187
+ <th>Model</th>
188
+ <th>Question</th>
189
+ <th>Count</th>
190
+ </tr>
191
+ </thead>
192
+ <tbody>
193
+ {exception_rows}
194
+ </tbody>
195
+ </table>
196
+ </div>
197
+ </div>
198
+ """
199
+
200
+ def _build_model_costs_table(self) -> str:
201
+ """Generate HTML for the model costs summary table section."""
202
+ if not hasattr(self.jobs_info, "model_costs") or not self.jobs_info.model_costs:
203
+ return ""
204
+
205
+ # Calculate totals
206
+ total_input_tokens = sum(
207
+ cost.input_tokens or 0 for cost in self.jobs_info.model_costs
208
+ )
209
+ total_output_tokens = sum(
210
+ cost.output_tokens or 0 for cost in self.jobs_info.model_costs
211
+ )
212
+ total_input_cost = sum(
213
+ cost.input_cost_usd or 0 for cost in self.jobs_info.model_costs
214
+ )
215
+ total_output_cost = sum(
216
+ cost.output_cost_usd or 0 for cost in self.jobs_info.model_costs
217
+ )
218
+ total_cost = total_input_cost + total_output_cost
219
+
220
+ # Generate cost rows HTML with class names for right alignment
221
+ cost_rows = "".join(
222
+ f"""
223
+ <tr>
224
+ <td>{cost.service or '-'}</td>
225
+ <td>{cost.model or '-'}</td>
226
+ <td class='token-count'>{cost.input_tokens:,}</td>
227
+ <td class='cost-value'>${cost.input_cost_usd:.4f}</td>
228
+ <td class='token-count'>{cost.output_tokens:,}</td>
229
+ <td class='cost-value'>${cost.output_cost_usd:.4f}</td>
230
+ <td class='cost-value'>${(cost.input_cost_usd or 0) + (cost.output_cost_usd or 0):.4f}</td>
231
+ </tr>
232
+ """
233
+ for cost in self.jobs_info.model_costs
234
+ )
235
+
236
+ # Add total row with the same alignment classes
237
+ total_row = f"""
238
+ <tr class='totals-row'>
239
+ <td colspan='2'><strong>Totals</strong></td>
240
+ <td class='token-count'>{total_input_tokens:,}</td>
241
+ <td class='cost-value'>${total_input_cost:.4f}</td>
242
+ <td class='token-count'>{total_output_tokens:,}</td>
243
+ <td class='cost-value'>${total_output_cost:.4f}</td>
244
+ <td class='cost-value'>${total_cost:.4f}</td>
245
+ </tr>
246
+ """
247
+
248
+ return f"""
249
+ <div class="model-costs-section">
250
+ <div class="model-costs-header" onclick="{self._collapse(f'model-costs-content-{self.log_id}', f'model-costs-arrow-{self.log_id}')}">
251
+ <span id="model-costs-arrow-{self.log_id}" class="expand-toggle">&#8963;</span>
252
+ <span>Model Costs (${total_cost:.4f} total)</span>
253
+ <span style="flex-grow: 1;"></span>
254
+ </div>
255
+ <div id="model-costs-content-{self.log_id}" class="model-costs-content">
256
+ <table class='model-costs-table'>
257
+ <thead>
258
+ <tr>
259
+ <th>Service</th>
260
+ <th>Model</th>
261
+ <th class="cost-header">Input Tokens</th>
262
+ <th class="cost-header">Input Cost</th>
263
+ <th class="cost-header">Output Tokens</th>
264
+ <th class="cost-header">Output Cost</th>
265
+ <th class="cost-header">Total Cost</th>
266
+ </tr>
267
+ </thead>
268
+ <tbody>
269
+ {cost_rows}
270
+ {total_row}
271
+ </tbody>
272
+ </table>
273
+ </div>
274
+ </div>
275
+ """
276
+
110
277
  def _get_html(self, current_status: JobsStatus = JobsStatus.RUNNING) -> str:
111
278
  """Generate the complete HTML display with modern design"""
112
- # CSS for modern styling
113
279
  css = """
114
280
  <style>
115
281
  .jobs-container {
@@ -131,6 +297,8 @@ class HTMLTableJobLogger(JobLogger):
131
297
  justify-content: space-between;
132
298
  font-weight: 500;
133
299
  font-size: 0.9em;
300
+ flex-wrap: wrap;
301
+ gap: 8px;
134
302
  }
135
303
  .jobs-content {
136
304
  background: white;
@@ -162,7 +330,7 @@ class HTMLTableJobLogger(JobLogger):
162
330
  border-bottom: 1px solid #cbd5e1;
163
331
  font-size: 0.85em;
164
332
  }
165
- .two-column-grid {
333
+ .three-column-grid {
166
334
  display: flex;
167
335
  flex-wrap: wrap;
168
336
  gap: 1px;
@@ -171,11 +339,15 @@ class HTMLTableJobLogger(JobLogger):
171
339
  .column {
172
340
  background-color: white;
173
341
  }
174
- .column:first-child {
342
+ .column:nth-child(1) { /* Job Links */
175
343
  flex: 1;
176
344
  min-width: 150px;
177
345
  }
178
- .column:last-child {
346
+ .column:nth-child(2) { /* Content */
347
+ flex: 1;
348
+ min-width: 150px;
349
+ }
350
+ .column:nth-child(3) { /* Identifiers */
179
351
  flex: 2;
180
352
  min-width: 300px;
181
353
  }
@@ -185,6 +357,7 @@ class HTMLTableJobLogger(JobLogger):
185
357
  .link-item {
186
358
  padding: 3px 0;
187
359
  border-bottom: 1px solid #f1f5f9;
360
+ font-size: 0.9em;
188
361
  }
189
362
  .link-item:last-child {
190
363
  border-bottom: none;
@@ -203,11 +376,18 @@ class HTMLTableJobLogger(JobLogger):
203
376
  .progress-link .pill-link:hover {
204
377
  border-bottom-color: #3b82f6;
205
378
  }
379
+ .remote-link .pill-link {
380
+ color: #4b5563;
381
+ font-weight: 500;
382
+ }
383
+ .remote-link .pill-link:hover {
384
+ border-bottom-color: #4b5563;
385
+ }
206
386
  .uuid-item {
207
387
  padding: 3px 0;
208
388
  border-bottom: 1px solid #f1f5f9;
209
389
  display: flex;
210
- align-items: center;
390
+ align-items: flex-start;
211
391
  }
212
392
  .uuid-item:last-child {
213
393
  border-bottom: none;
@@ -224,12 +404,10 @@ class HTMLTableJobLogger(JobLogger):
224
404
  line-height: 1.5;
225
405
  }
226
406
  .pill-link {
227
- color: #3b82f6;
228
407
  font-weight: 500;
229
408
  text-decoration: none;
230
409
  border-bottom: 1px dotted #bfdbfe;
231
410
  transition: border-color 0.2s;
232
- font-size: 0.75em;
233
411
  display: inline-flex;
234
412
  align-items: center;
235
413
  gap: 4px;
@@ -245,13 +423,16 @@ class HTMLTableJobLogger(JobLogger):
245
423
  .status-banner {
246
424
  display: flex;
247
425
  align-items: center;
426
+ flex-wrap: wrap;
427
+ gap: 8px;
248
428
  padding: 5px 12px;
249
429
  background-color: #f7fafc;
250
430
  border-top: 1px solid #edf2f7;
251
431
  font-size: 0.85em;
432
+ cursor: pointer;
252
433
  }
253
434
  .status-running { color: #3b82f6; }
254
- .status-completed { color: #10b981; }
435
+ .status-completed { color: #059669; }
255
436
  .status-partially-failed { color: #d97706; }
256
437
  .status-failed { color: #ef4444; }
257
438
  .status-unknown { color: #6b7280; }
@@ -273,15 +454,23 @@ class HTMLTableJobLogger(JobLogger):
273
454
  .link:hover {
274
455
  border-bottom: 1px solid #3b82f6;
275
456
  }
457
+ .uuid-container-wrapper {
458
+ display: flex;
459
+ flex-direction: column;
460
+ align-items: stretch;
461
+ gap: 4px;
462
+ flex: 1;
463
+ padding-bottom: 4px;
464
+ }
276
465
  .uuid-container {
277
466
  display: flex;
278
467
  align-items: center;
279
468
  background-color: #f8fafc;
280
469
  border-radius: 3px;
281
470
  padding: 2px 6px;
282
- font-family: monospace;
471
+ font-family: "SF Mono", "Cascadia Mono", monospace;
283
472
  font-size: 0.75em;
284
- flex: 1;
473
+ width: 100%; /* Make sure it fills the width */
285
474
  }
286
475
  .uuid-code {
287
476
  color: #4b5563;
@@ -353,6 +542,118 @@ class HTMLTableJobLogger(JobLogger):
353
542
  .status-completed.badge { background-color: #d1fae5; }
354
543
  .status-partially-failed.badge { background-color: #fef3c7; }
355
544
  .status-failed.badge { background-color: #fee2e2; }
545
+ .helper-text {
546
+ color: #4b5563;
547
+ font-size: 0.75em;
548
+ text-align: left;
549
+ }
550
+ /* Exception table styles */
551
+ .exception-section {
552
+ border-top: 1px solid #edf2f7;
553
+ }
554
+ .exception-header {
555
+ padding: 8px 12px;
556
+ background-color: #f7fafc;
557
+ display: flex;
558
+ align-items: center;
559
+ cursor: pointer;
560
+ font-size: 0.85em;
561
+ font-weight: 500;
562
+ user-select: none; /* Prevent text selection */
563
+ }
564
+ .exception-content {
565
+ padding: 12px;
566
+ }
567
+ .exception-table {
568
+ width: 100%;
569
+ border-collapse: collapse;
570
+ margin: 0;
571
+ font-size: 0.85em;
572
+ }
573
+ .exception-table th {
574
+ background-color: #f1f5f9;
575
+ color: #475569;
576
+ font-weight: 500;
577
+ text-align: left;
578
+ padding: 8px 12px;
579
+ border-bottom: 2px solid #e2e8f0;
580
+ }
581
+ .exception-table td {
582
+ padding: 6px 12px;
583
+ border-bottom: 1px solid #e2e8f0;
584
+ color: #1f2937;
585
+ text-align: left; /* Ensure left alignment */
586
+ }
587
+ .exception-table tr:last-child td {
588
+ border-bottom: none;
589
+ }
590
+ .exception-count {
591
+ font-weight: 500;
592
+ color: #ef4444;
593
+ }
594
+ /* Model costs table styles */
595
+ .model-costs-section {
596
+ border-top: 1px solid #edf2f7;
597
+ }
598
+ .model-costs-header {
599
+ padding: 8px 12px;
600
+ background-color: #f7fafc;
601
+ display: flex;
602
+ align-items: center;
603
+ cursor: pointer;
604
+ font-size: 0.85em;
605
+ font-weight: 500;
606
+ user-select: none;
607
+ }
608
+ .model-costs-content {
609
+ padding: 12px;
610
+ }
611
+ .model-costs-table {
612
+ width: 100%;
613
+ border-collapse: collapse;
614
+ margin: 0;
615
+ font-size: 0.85em;
616
+ }
617
+ .model-costs-table th {
618
+ background-color: #f1f5f9;
619
+ color: #475569;
620
+ font-weight: 500;
621
+ text-align: left; /* Default left alignment */
622
+ padding: 8px 12px;
623
+ border-bottom: 2px solid #e2e8f0;
624
+ }
625
+ .model-costs-table th.cost-header { /* New class for cost headers */
626
+ text-align: right;
627
+ }
628
+ .model-costs-table td {
629
+ padding: 6px 12px;
630
+ border-bottom: 1px solid #e2e8f0;
631
+ color: #1f2937;
632
+ text-align: left; /* Ensure left alignment for all cells by default */
633
+ }
634
+ .model-costs-table tr:last-child td {
635
+ border-bottom: none;
636
+ }
637
+ .token-count td, .cost-value td { /* Override for specific columns that need right alignment */
638
+ text-align: right;
639
+ }
640
+ .totals-row {
641
+ background-color: #f8fafc;
642
+ }
643
+ .totals-row td {
644
+ border-top: 2px solid #e2e8f0;
645
+ }
646
+ /* Model costs table styles */
647
+ .model-costs-table td.token-count,
648
+ .model-costs-table td.cost-value {
649
+ text-align: right; /* Right align the token counts and cost values */
650
+ }
651
+ .code-text {
652
+ font-family: "SF Mono", "Cascadia Mono", monospace;
653
+ background-color: #f8fafc;
654
+ padding: 1px 4px;
655
+ border-radius: 3px;
656
+ }
356
657
  </style>
357
658
  """
358
659
 
@@ -366,6 +667,8 @@ class HTMLTableJobLogger(JobLogger):
366
667
  "pretty_names",
367
668
  "completed_interviews",
368
669
  "failed_interviews",
670
+ "exception_summary",
671
+ "model_costs",
369
672
  ]:
370
673
  value = getattr(self.jobs_info, field)
371
674
  if not value:
@@ -382,17 +685,18 @@ class HTMLTableJobLogger(JobLogger):
382
685
  else:
383
686
  other_fields.append((field, pretty_name, value))
384
687
 
385
- # Build a two-column layout with links and UUIDs
688
+ # Build a three-column layout
386
689
  content_html = """
387
- <div class="two-column-grid">
690
+ <div class="three-column-grid">
388
691
  <div class="column">
389
- <div class="section-header">Links</div>
692
+ <div class="section-header">Job Links</div>
390
693
  <div class="content-box">
391
694
  """
392
695
 
393
- # Sort URLs to prioritize Results first, then Progress Bar
696
+ # Sort URLs to prioritize Results first, then Progress
394
697
  results_links = []
395
698
  progress_links = []
699
+ remote_links = []
396
700
  other_links = []
397
701
 
398
702
  for field, pretty_name, value in url_fields:
@@ -404,32 +708,46 @@ class HTMLTableJobLogger(JobLogger):
404
708
 
405
709
  if "result" in field.lower():
406
710
  results_links.append((field, pretty_name, value, label))
407
- elif "progress" in field.lower():
711
+ elif "progress" in field.lower() or "error_report" in field.lower():
408
712
  progress_links.append((field, pretty_name, value, label))
713
+ elif "remote_cache" in field.lower() or "remote_inference" in field.lower():
714
+ remote_links.append((field, pretty_name, value, label))
409
715
  else:
410
716
  other_links.append((field, pretty_name, value, label))
411
717
 
412
- # Add results links first with special styling
718
+ # Add results and progress links to first column
413
719
  for field, pretty_name, value, label in results_links:
414
720
  content_html += f"""
415
- <div class="link-item results-link">
721
+ <div class="link-item results-link" style="display: flex; align-items: center; justify-content: space-between;">
416
722
  <a href="{value}" target="_blank" class="pill-link">{label}{self.external_link_icon}</a>
723
+ {self._create_copy_button(value)}
417
724
  </div>
418
725
  """
419
726
 
420
727
  # Then add progress links with different special styling
421
728
  for field, pretty_name, value, label in progress_links:
422
729
  content_html += f"""
423
- <div class="link-item progress-link">
730
+ <div class="link-item progress-link" style="display: flex; align-items: center; justify-content: space-between;">
424
731
  <a href="{value}" target="_blank" class="pill-link">{label}{self.external_link_icon}</a>
732
+ {self._create_copy_button(value)}
425
733
  </div>
426
734
  """
427
735
 
428
- # Then add other links
429
- for field, pretty_name, value, label in other_links:
736
+ # Close first column and start second column
737
+ content_html += """
738
+ </div>
739
+ </div>
740
+ <div class="column">
741
+ <div class="section-header">Content</div>
742
+ <div class="content-box">
743
+ """
744
+
745
+ # Add remote links to middle column
746
+ for field, pretty_name, value, label in remote_links + other_links:
430
747
  content_html += f"""
431
- <div class="link-item">
748
+ <div class="link-item remote-link" style="display: flex; align-items: center; justify-content: space-between;">
432
749
  <a href="{value}" target="_blank" class="pill-link">{label}{self.external_link_icon}</a>
750
+ {self._create_copy_button(value)}
433
751
  </div>
434
752
  """
435
753
 
@@ -444,10 +762,18 @@ class HTMLTableJobLogger(JobLogger):
444
762
  # Sort UUIDs to prioritize Result UUID first
445
763
  uuid_fields.sort(key=lambda x: 0 if "result" in x[0].lower() else 1)
446
764
  for field, pretty_name, value in uuid_fields:
447
- # Create single-line UUID displays
765
+ if "result" in field.lower():
766
+ helper_text = "Use <span class='code-text'>Results.pull(uuid)</span> to fetch results."
767
+ elif "job" in field.lower():
768
+ helper_text = (
769
+ "Use <span class='code-text'>Jobs.pull(uuid)</span> to fetch job."
770
+ )
771
+ else:
772
+ helper_text = ""
773
+
448
774
  content_html += f"""
449
775
  <div class="uuid-item">
450
- <span class="uuid-label">{pretty_name}:</span>{self._create_uuid_copy_button(value)}
776
+ <span class="uuid-label">{pretty_name}:</span>{self._create_uuid_copy_button(value, helper_text)}
451
777
  </div>
452
778
  """
453
779
 
@@ -470,7 +796,7 @@ class HTMLTableJobLogger(JobLogger):
470
796
  """
471
797
  content_html += "</table>"
472
798
 
473
- # Status banner
799
+ # Status banner and message log
474
800
  status_class = {
475
801
  JobsStatus.RUNNING: "status-running",
476
802
  JobsStatus.COMPLETED: "status-completed",
@@ -488,9 +814,14 @@ class HTMLTableJobLogger(JobLogger):
488
814
  status_text = str(current_status).capitalize()
489
815
 
490
816
  status_banner = f"""
491
- <div class="status-banner">
492
- {status_icon}
493
- <strong>Status:</strong>&nbsp;<span class="badge {status_class}">{status_text}</span>
817
+ <div class="status-banner" onclick="{self._collapse(f'message-log-{self.log_id}', f'message-arrow-{self.log_id}')}">
818
+ <div style="display: flex; align-items: center; gap: 8px;">
819
+ <span id="message-arrow-{self.log_id}" class="expand-toggle">&#8963;</span>
820
+ <div style="display: flex; align-items: center;">
821
+ {status_icon}
822
+ <strong>Status:</strong>&nbsp;<span class="badge {status_class}">{status_text}</span>
823
+ </div>
824
+ </div>
494
825
  <span style="flex-grow: 1;"></span>
495
826
  <span>Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</span>
496
827
  </div>
@@ -519,7 +850,7 @@ class HTMLTableJobLogger(JobLogger):
519
850
  )
520
851
 
521
852
  message_log = f"""
522
- <div class="message-log">
853
+ <div id="message-log-{self.log_id}" class="message-log">
523
854
  {''.join(reversed(message_items))}
524
855
  </div>
525
856
  """
@@ -528,25 +859,25 @@ class HTMLTableJobLogger(JobLogger):
528
859
 
529
860
  header_status_text = status_text
530
861
  if (
531
- current_status == JobsStatus.PARTIALLY_FAILED
532
- and self.jobs_info.completed_interviews is not None
862
+ self.jobs_info.completed_interviews is not None
533
863
  and self.jobs_info.failed_interviews is not None
534
864
  ):
535
865
  header_status_text += f" ({self.jobs_info.completed_interviews:,} completed, {self.jobs_info.failed_interviews:,} failed)"
536
866
 
867
+ # Add model costs table before exceptions table
868
+ main_content = f"""
869
+ {content_html}
870
+ {status_banner}
871
+ {message_log}
872
+ {self._build_model_costs_table()}
873
+ {self._build_exceptions_table()}
874
+ """
875
+
876
+ # Return the complete HTML
537
877
  return f"""
538
878
  {css}
539
879
  <div class="jobs-container">
540
- <div class="jobs-header" onclick="
541
- const content = document.getElementById('content-{self.log_id}');
542
- const arrow = document.getElementById('arrow-{self.log_id}');
543
- if (content.style.display === 'none') {{
544
- content.style.display = 'block';
545
- arrow.innerHTML = '&#8963;';
546
- }} else {{
547
- content.style.display = 'none';
548
- arrow.innerHTML = '&#8964;';
549
- }}">
880
+ <div class="jobs-header" onclick="{self._collapse(f'content-{self.log_id}', f'arrow-{self.log_id}')}">
550
881
  <div>
551
882
  <span id="arrow-{self.log_id}" class="expand-toggle">{'&#8963;' if self.is_expanded else '&#8964;'}</span>
552
883
  Job Status 🦜
@@ -554,9 +885,7 @@ class HTMLTableJobLogger(JobLogger):
554
885
  <div class="{status_class}">{header_status_text}</div>
555
886
  </div>
556
887
  <div id="content-{self.log_id}" class="jobs-content" style="display: {display_style};">
557
- {content_html}
558
- {status_banner}
559
- {message_log}
888
+ {main_content}
560
889
  </div>
561
890
  </div>
562
891
  """