edsl 0.1.57__py3-none-any.whl → 0.1.59__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,180 @@ 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
+ # Calculate credit totals
221
+ total_input_credits = sum(
222
+ cost.input_cost_credits_with_cache or 0
223
+ for cost in self.jobs_info.model_costs
224
+ )
225
+ total_output_credits = sum(
226
+ cost.output_cost_credits_with_cache or 0
227
+ for cost in self.jobs_info.model_costs
228
+ )
229
+ total_credits = total_input_credits + total_output_credits
230
+
231
+ # Generate cost rows HTML with class names for right alignment
232
+ cost_rows = "".join(
233
+ f"""
234
+ <tr>
235
+ <td>{cost.service or '-'}</td>
236
+ <td>{cost.model or '-'}</td>
237
+ <td class='token-count'>{cost.input_tokens:,}</td>
238
+ <td class='cost-value'>${cost.input_cost_usd:.4f}</td>
239
+ <td class='token-count'>{cost.output_tokens:,}</td>
240
+ <td class='cost-value'>${cost.output_cost_usd:.4f}</td>
241
+ <td class='cost-value'>${(cost.input_cost_usd or 0) + (cost.output_cost_usd or 0):.4f}</td>
242
+ <td class='cost-value'>{(cost.input_cost_credits_with_cache or 0) + (cost.output_cost_credits_with_cache or 0):,.2f}</td>
243
+ </tr>
244
+ """
245
+ for cost in self.jobs_info.model_costs
246
+ )
247
+
248
+ # Add total row with the same alignment classes
249
+ total_row = f"""
250
+ <tr class='totals-row'>
251
+ <td colspan='2'><strong>Totals</strong></td>
252
+ <td class='token-count'>{total_input_tokens:,}</td>
253
+ <td class='cost-value'>${total_input_cost:.4f}</td>
254
+ <td class='token-count'>{total_output_tokens:,}</td>
255
+ <td class='cost-value'>${total_output_cost:.4f}</td>
256
+ <td class='cost-value'>${total_cost:.4f}</td>
257
+ <td class='cost-value'>{total_credits:,.2f}</td>
258
+ </tr>
259
+ """
260
+
261
+ return f"""
262
+ <div class="model-costs-section">
263
+ <div class="model-costs-header" onclick="{self._collapse(f'model-costs-content-{self.log_id}', f'model-costs-arrow-{self.log_id}')}">
264
+ <span id="model-costs-arrow-{self.log_id}" class="expand-toggle">&#8963;</span>
265
+ <span>Model Costs (${total_cost:.4f} / {total_credits:,.2f} credits total)</span>
266
+ <span style="flex-grow: 1;"></span>
267
+ </div>
268
+ <div id="model-costs-content-{self.log_id}" class="model-costs-content">
269
+ <table class='model-costs-table'>
270
+ <thead>
271
+ <tr>
272
+ <th>Service</th>
273
+ <th>Model</th>
274
+ <th class="cost-header">Input Tokens</th>
275
+ <th class="cost-header">Input Cost</th>
276
+ <th class="cost-header">Output Tokens</th>
277
+ <th class="cost-header">Output Cost</th>
278
+ <th class="cost-header">Total Cost</th>
279
+ <th class="cost-header">Total Credits</th>
280
+ </tr>
281
+ </thead>
282
+ <tbody>
283
+ {cost_rows}
284
+ {total_row}
285
+ </tbody>
286
+ </table>
287
+ <p style="font-style: italic; margin-top: 8px; font-size: 0.85em; color: #4b5563;">
288
+ You can obtain the total credit cost by multiplying the total USD cost by 100. A lower credit cost indicates that you saved money by retrieving responses from the universal remote cache.
289
+ </p>
290
+ </div>
291
+ </div>
292
+ """
293
+
110
294
  def _get_html(self, current_status: JobsStatus = JobsStatus.RUNNING) -> str:
111
295
  """Generate the complete HTML display with modern design"""
112
- # CSS for modern styling
113
296
  css = """
114
297
  <style>
115
298
  .jobs-container {
@@ -131,6 +314,8 @@ class HTMLTableJobLogger(JobLogger):
131
314
  justify-content: space-between;
132
315
  font-weight: 500;
133
316
  font-size: 0.9em;
317
+ flex-wrap: wrap;
318
+ gap: 8px;
134
319
  }
135
320
  .jobs-content {
136
321
  background: white;
@@ -162,7 +347,7 @@ class HTMLTableJobLogger(JobLogger):
162
347
  border-bottom: 1px solid #cbd5e1;
163
348
  font-size: 0.85em;
164
349
  }
165
- .two-column-grid {
350
+ .three-column-grid {
166
351
  display: flex;
167
352
  flex-wrap: wrap;
168
353
  gap: 1px;
@@ -171,11 +356,15 @@ class HTMLTableJobLogger(JobLogger):
171
356
  .column {
172
357
  background-color: white;
173
358
  }
174
- .column:first-child {
359
+ .column:nth-child(1) { /* Job Links */
175
360
  flex: 1;
176
361
  min-width: 150px;
177
362
  }
178
- .column:last-child {
363
+ .column:nth-child(2) { /* Content */
364
+ flex: 1;
365
+ min-width: 150px;
366
+ }
367
+ .column:nth-child(3) { /* Identifiers */
179
368
  flex: 2;
180
369
  min-width: 300px;
181
370
  }
@@ -185,6 +374,7 @@ class HTMLTableJobLogger(JobLogger):
185
374
  .link-item {
186
375
  padding: 3px 0;
187
376
  border-bottom: 1px solid #f1f5f9;
377
+ font-size: 0.9em;
188
378
  }
189
379
  .link-item:last-child {
190
380
  border-bottom: none;
@@ -203,11 +393,18 @@ class HTMLTableJobLogger(JobLogger):
203
393
  .progress-link .pill-link:hover {
204
394
  border-bottom-color: #3b82f6;
205
395
  }
396
+ .remote-link .pill-link {
397
+ color: #4b5563;
398
+ font-weight: 500;
399
+ }
400
+ .remote-link .pill-link:hover {
401
+ border-bottom-color: #4b5563;
402
+ }
206
403
  .uuid-item {
207
404
  padding: 3px 0;
208
405
  border-bottom: 1px solid #f1f5f9;
209
406
  display: flex;
210
- align-items: center;
407
+ align-items: flex-start;
211
408
  }
212
409
  .uuid-item:last-child {
213
410
  border-bottom: none;
@@ -224,12 +421,10 @@ class HTMLTableJobLogger(JobLogger):
224
421
  line-height: 1.5;
225
422
  }
226
423
  .pill-link {
227
- color: #3b82f6;
228
424
  font-weight: 500;
229
425
  text-decoration: none;
230
426
  border-bottom: 1px dotted #bfdbfe;
231
427
  transition: border-color 0.2s;
232
- font-size: 0.75em;
233
428
  display: inline-flex;
234
429
  align-items: center;
235
430
  gap: 4px;
@@ -245,13 +440,16 @@ class HTMLTableJobLogger(JobLogger):
245
440
  .status-banner {
246
441
  display: flex;
247
442
  align-items: center;
443
+ flex-wrap: wrap;
444
+ gap: 8px;
248
445
  padding: 5px 12px;
249
446
  background-color: #f7fafc;
250
447
  border-top: 1px solid #edf2f7;
251
448
  font-size: 0.85em;
449
+ cursor: pointer;
252
450
  }
253
451
  .status-running { color: #3b82f6; }
254
- .status-completed { color: #10b981; }
452
+ .status-completed { color: #059669; }
255
453
  .status-partially-failed { color: #d97706; }
256
454
  .status-failed { color: #ef4444; }
257
455
  .status-unknown { color: #6b7280; }
@@ -273,15 +471,23 @@ class HTMLTableJobLogger(JobLogger):
273
471
  .link:hover {
274
472
  border-bottom: 1px solid #3b82f6;
275
473
  }
474
+ .uuid-container-wrapper {
475
+ display: flex;
476
+ flex-direction: column;
477
+ align-items: stretch;
478
+ gap: 4px;
479
+ flex: 1;
480
+ padding-bottom: 4px;
481
+ }
276
482
  .uuid-container {
277
483
  display: flex;
278
484
  align-items: center;
279
485
  background-color: #f8fafc;
280
486
  border-radius: 3px;
281
487
  padding: 2px 6px;
282
- font-family: monospace;
488
+ font-family: "SF Mono", "Cascadia Mono", monospace;
283
489
  font-size: 0.75em;
284
- flex: 1;
490
+ width: 100%; /* Make sure it fills the width */
285
491
  }
286
492
  .uuid-code {
287
493
  color: #4b5563;
@@ -353,6 +559,118 @@ class HTMLTableJobLogger(JobLogger):
353
559
  .status-completed.badge { background-color: #d1fae5; }
354
560
  .status-partially-failed.badge { background-color: #fef3c7; }
355
561
  .status-failed.badge { background-color: #fee2e2; }
562
+ .helper-text {
563
+ color: #4b5563;
564
+ font-size: 0.75em;
565
+ text-align: left;
566
+ }
567
+ /* Exception table styles */
568
+ .exception-section {
569
+ border-top: 1px solid #edf2f7;
570
+ }
571
+ .exception-header {
572
+ padding: 8px 12px;
573
+ background-color: #f7fafc;
574
+ display: flex;
575
+ align-items: center;
576
+ cursor: pointer;
577
+ font-size: 0.85em;
578
+ font-weight: 500;
579
+ user-select: none; /* Prevent text selection */
580
+ }
581
+ .exception-content {
582
+ padding: 12px;
583
+ }
584
+ .exception-table {
585
+ width: 100%;
586
+ border-collapse: collapse;
587
+ margin: 0;
588
+ font-size: 0.85em;
589
+ }
590
+ .exception-table th {
591
+ background-color: #f1f5f9;
592
+ color: #475569;
593
+ font-weight: 500;
594
+ text-align: left;
595
+ padding: 8px 12px;
596
+ border-bottom: 2px solid #e2e8f0;
597
+ }
598
+ .exception-table td {
599
+ padding: 6px 12px;
600
+ border-bottom: 1px solid #e2e8f0;
601
+ color: #1f2937;
602
+ text-align: left; /* Ensure left alignment */
603
+ }
604
+ .exception-table tr:last-child td {
605
+ border-bottom: none;
606
+ }
607
+ .exception-count {
608
+ font-weight: 500;
609
+ color: #ef4444;
610
+ }
611
+ /* Model costs table styles */
612
+ .model-costs-section {
613
+ border-top: 1px solid #edf2f7;
614
+ }
615
+ .model-costs-header {
616
+ padding: 8px 12px;
617
+ background-color: #f7fafc;
618
+ display: flex;
619
+ align-items: center;
620
+ cursor: pointer;
621
+ font-size: 0.85em;
622
+ font-weight: 500;
623
+ user-select: none;
624
+ }
625
+ .model-costs-content {
626
+ padding: 12px;
627
+ }
628
+ .model-costs-table {
629
+ width: 100%;
630
+ border-collapse: collapse;
631
+ margin: 0;
632
+ font-size: 0.85em;
633
+ }
634
+ .model-costs-table th {
635
+ background-color: #f1f5f9;
636
+ color: #475569;
637
+ font-weight: 500;
638
+ text-align: left; /* Default left alignment */
639
+ padding: 8px 12px;
640
+ border-bottom: 2px solid #e2e8f0;
641
+ }
642
+ .model-costs-table th.cost-header { /* New class for cost headers */
643
+ text-align: right;
644
+ }
645
+ .model-costs-table td {
646
+ padding: 6px 12px;
647
+ border-bottom: 1px solid #e2e8f0;
648
+ color: #1f2937;
649
+ text-align: left; /* Ensure left alignment for all cells by default */
650
+ }
651
+ .model-costs-table tr:last-child td {
652
+ border-bottom: none;
653
+ }
654
+ .token-count td, .cost-value td { /* Override for specific columns that need right alignment */
655
+ text-align: right;
656
+ }
657
+ .totals-row {
658
+ background-color: #f8fafc;
659
+ }
660
+ .totals-row td {
661
+ border-top: 2px solid #e2e8f0;
662
+ }
663
+ /* Model costs table styles */
664
+ .model-costs-table td.token-count,
665
+ .model-costs-table td.cost-value {
666
+ text-align: right; /* Right align the token counts and cost values */
667
+ }
668
+ .code-text {
669
+ font-family: "SF Mono", "Cascadia Mono", monospace;
670
+ background-color: #f8fafc;
671
+ padding: 1px 4px;
672
+ border-radius: 3px;
673
+ }
356
674
  </style>
357
675
  """
358
676
 
@@ -366,6 +684,8 @@ class HTMLTableJobLogger(JobLogger):
366
684
  "pretty_names",
367
685
  "completed_interviews",
368
686
  "failed_interviews",
687
+ "exception_summary",
688
+ "model_costs",
369
689
  ]:
370
690
  value = getattr(self.jobs_info, field)
371
691
  if not value:
@@ -382,17 +702,18 @@ class HTMLTableJobLogger(JobLogger):
382
702
  else:
383
703
  other_fields.append((field, pretty_name, value))
384
704
 
385
- # Build a two-column layout with links and UUIDs
705
+ # Build a three-column layout
386
706
  content_html = """
387
- <div class="two-column-grid">
707
+ <div class="three-column-grid">
388
708
  <div class="column">
389
- <div class="section-header">Links</div>
709
+ <div class="section-header">Job Links</div>
390
710
  <div class="content-box">
391
711
  """
392
712
 
393
- # Sort URLs to prioritize Results first, then Progress Bar
713
+ # Sort URLs to prioritize Results first, then Progress
394
714
  results_links = []
395
715
  progress_links = []
716
+ remote_links = []
396
717
  other_links = []
397
718
 
398
719
  for field, pretty_name, value in url_fields:
@@ -404,32 +725,46 @@ class HTMLTableJobLogger(JobLogger):
404
725
 
405
726
  if "result" in field.lower():
406
727
  results_links.append((field, pretty_name, value, label))
407
- elif "progress" in field.lower():
728
+ elif "progress" in field.lower() or "error_report" in field.lower():
408
729
  progress_links.append((field, pretty_name, value, label))
730
+ elif "remote_cache" in field.lower() or "remote_inference" in field.lower():
731
+ remote_links.append((field, pretty_name, value, label))
409
732
  else:
410
733
  other_links.append((field, pretty_name, value, label))
411
734
 
412
- # Add results links first with special styling
735
+ # Add results and progress links to first column
413
736
  for field, pretty_name, value, label in results_links:
414
737
  content_html += f"""
415
- <div class="link-item results-link">
738
+ <div class="link-item results-link" style="display: flex; align-items: center; justify-content: space-between;">
416
739
  <a href="{value}" target="_blank" class="pill-link">{label}{self.external_link_icon}</a>
740
+ {self._create_copy_button(value)}
417
741
  </div>
418
742
  """
419
743
 
420
744
  # Then add progress links with different special styling
421
745
  for field, pretty_name, value, label in progress_links:
422
746
  content_html += f"""
423
- <div class="link-item progress-link">
747
+ <div class="link-item progress-link" style="display: flex; align-items: center; justify-content: space-between;">
424
748
  <a href="{value}" target="_blank" class="pill-link">{label}{self.external_link_icon}</a>
749
+ {self._create_copy_button(value)}
425
750
  </div>
426
751
  """
427
752
 
428
- # Then add other links
429
- for field, pretty_name, value, label in other_links:
753
+ # Close first column and start second column
754
+ content_html += """
755
+ </div>
756
+ </div>
757
+ <div class="column">
758
+ <div class="section-header">Content</div>
759
+ <div class="content-box">
760
+ """
761
+
762
+ # Add remote links to middle column
763
+ for field, pretty_name, value, label in remote_links + other_links:
430
764
  content_html += f"""
431
- <div class="link-item">
765
+ <div class="link-item remote-link" style="display: flex; align-items: center; justify-content: space-between;">
432
766
  <a href="{value}" target="_blank" class="pill-link">{label}{self.external_link_icon}</a>
767
+ {self._create_copy_button(value)}
433
768
  </div>
434
769
  """
435
770
 
@@ -444,10 +779,18 @@ class HTMLTableJobLogger(JobLogger):
444
779
  # Sort UUIDs to prioritize Result UUID first
445
780
  uuid_fields.sort(key=lambda x: 0 if "result" in x[0].lower() else 1)
446
781
  for field, pretty_name, value in uuid_fields:
447
- # Create single-line UUID displays
782
+ if "result" in field.lower():
783
+ helper_text = "Use <span class='code-text'>Results.pull(uuid)</span> to fetch results."
784
+ elif "job" in field.lower():
785
+ helper_text = (
786
+ "Use <span class='code-text'>Jobs.pull(uuid)</span> to fetch job."
787
+ )
788
+ else:
789
+ helper_text = ""
790
+
448
791
  content_html += f"""
449
792
  <div class="uuid-item">
450
- <span class="uuid-label">{pretty_name}:</span>{self._create_uuid_copy_button(value)}
793
+ <span class="uuid-label">{pretty_name}:</span>{self._create_uuid_copy_button(value, helper_text)}
451
794
  </div>
452
795
  """
453
796
 
@@ -470,7 +813,7 @@ class HTMLTableJobLogger(JobLogger):
470
813
  """
471
814
  content_html += "</table>"
472
815
 
473
- # Status banner
816
+ # Status banner and message log
474
817
  status_class = {
475
818
  JobsStatus.RUNNING: "status-running",
476
819
  JobsStatus.COMPLETED: "status-completed",
@@ -488,9 +831,14 @@ class HTMLTableJobLogger(JobLogger):
488
831
  status_text = str(current_status).capitalize()
489
832
 
490
833
  status_banner = f"""
491
- <div class="status-banner">
492
- {status_icon}
493
- <strong>Status:</strong>&nbsp;<span class="badge {status_class}">{status_text}</span>
834
+ <div class="status-banner" onclick="{self._collapse(f'message-log-{self.log_id}', f'message-arrow-{self.log_id}')}">
835
+ <div style="display: flex; align-items: center; gap: 8px;">
836
+ <span id="message-arrow-{self.log_id}" class="expand-toggle">&#8963;</span>
837
+ <div style="display: flex; align-items: center;">
838
+ {status_icon}
839
+ <strong>Status:</strong>&nbsp;<span class="badge {status_class}">{status_text}</span>
840
+ </div>
841
+ </div>
494
842
  <span style="flex-grow: 1;"></span>
495
843
  <span>Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</span>
496
844
  </div>
@@ -519,7 +867,7 @@ class HTMLTableJobLogger(JobLogger):
519
867
  )
520
868
 
521
869
  message_log = f"""
522
- <div class="message-log">
870
+ <div id="message-log-{self.log_id}" class="message-log">
523
871
  {''.join(reversed(message_items))}
524
872
  </div>
525
873
  """
@@ -528,25 +876,25 @@ class HTMLTableJobLogger(JobLogger):
528
876
 
529
877
  header_status_text = status_text
530
878
  if (
531
- current_status == JobsStatus.PARTIALLY_FAILED
532
- and self.jobs_info.completed_interviews is not None
879
+ self.jobs_info.completed_interviews is not None
533
880
  and self.jobs_info.failed_interviews is not None
534
881
  ):
535
882
  header_status_text += f" ({self.jobs_info.completed_interviews:,} completed, {self.jobs_info.failed_interviews:,} failed)"
536
883
 
884
+ # Add model costs table before exceptions table
885
+ main_content = f"""
886
+ {content_html}
887
+ {status_banner}
888
+ {message_log}
889
+ {self._build_model_costs_table()}
890
+ {self._build_exceptions_table()}
891
+ """
892
+
893
+ # Return the complete HTML
537
894
  return f"""
538
895
  {css}
539
896
  <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
- }}">
897
+ <div class="jobs-header" onclick="{self._collapse(f'content-{self.log_id}', f'arrow-{self.log_id}')}">
550
898
  <div>
551
899
  <span id="arrow-{self.log_id}" class="expand-toggle">{'&#8963;' if self.is_expanded else '&#8964;'}</span>
552
900
  Job Status 🦜
@@ -554,9 +902,7 @@ class HTMLTableJobLogger(JobLogger):
554
902
  <div class="{status_class}">{header_status_text}</div>
555
903
  </div>
556
904
  <div id="content-{self.log_id}" class="jobs-content" style="display: {display_style};">
557
- {content_html}
558
- {status_banner}
559
- {message_log}
905
+ {main_content}
560
906
  </div>
561
907
  </div>
562
908
  """