cowork-dash 0.1.9__py3-none-any.whl → 0.2.1__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.
cowork_dash/components.py CHANGED
@@ -73,35 +73,119 @@ def format_message(role: str, content: str, colors: Dict, styles: Dict, is_new:
73
73
 
74
74
 
75
75
  def format_loading(colors: Dict):
76
- """Format loading indicator."""
76
+ """Format loading indicator with dots loader."""
77
77
  return html.Div([
78
- html.Div([
79
- html.Span("Agent", className="message-role-agent", style={
80
- "fontSize": "12px", "fontWeight": "500",
81
- "textTransform": "uppercase",
82
- }),
83
- ], style={"marginBottom": "5px"}),
84
- html.Span("Thinking", className="loading-dots thinking-pulse thinking-text", style={
85
- "fontSize": "15px", "fontWeight": "500",
86
- })
87
- ], className="chat-message chat-message-loading", style={"padding": "12px 15px"})
78
+ dmc.Loader(type="dots", size="sm")
79
+ ], className="chat-message chat-message-loading", style={
80
+ "padding": "12px 15px",
81
+ })
88
82
 
89
83
 
90
84
  def format_thinking(thinking_text: str, colors: Dict):
91
- """Format thinking as an inline subordinate message."""
85
+ """Format thinking as an inline subordinate message (non-collapsible)."""
92
86
  if not thinking_text:
93
87
  return None
94
88
 
95
- return html.Details([
96
- html.Summary("Thinking", className="details-summary details-summary-thinking"),
97
- html.Div(thinking_text, className="details-content details-content-thinking", style={
89
+ return html.Div([
90
+ html.Div("Thinking", style={
91
+ "fontSize": "12px",
92
+ "fontWeight": "500",
93
+ "color": colors.get("thinking", colors.get("text_muted", "#888")),
94
+ "marginBottom": "2px",
95
+ }),
96
+ html.Div(thinking_text, style={
98
97
  "whiteSpace": "pre-wrap",
98
+ "fontSize": "13px",
99
+ "color": colors.get("text_muted", "#666"),
100
+ "paddingLeft": "8px",
101
+ "borderLeft": f"2px solid {colors.get('thinking', '#7c4dff')}",
99
102
  })
100
- ], className="chat-details", style={
103
+ ], style={
101
104
  "marginBottom": "4px",
102
105
  })
103
106
 
104
107
 
108
+ def format_ai_text(text: str, colors: Dict, is_new: bool = False):
109
+ """Format AI text response (without the full message wrapper).
110
+
111
+ This is used when rendering ordered content items where thinking
112
+ and text are interleaved.
113
+ """
114
+ if not text:
115
+ return None
116
+
117
+ return dcc.Markdown(
118
+ text,
119
+ className="ai-text-block",
120
+ style={
121
+ "fontSize": "15px",
122
+ "lineHeight": "1.5",
123
+ "margin": "0",
124
+ "padding": "0",
125
+ }
126
+ )
127
+
128
+
129
+ def render_ordered_content_items(content_items: List[Dict], colors: Dict, styles: Dict = None, response_time: float = None) -> List:
130
+ """Render content items in their original emission order.
131
+
132
+ Args:
133
+ content_items: List of {"type": "text"|"thinking"|"display_inline", "content": ...}
134
+ colors: Color scheme dict
135
+ styles: Optional styles dict
136
+ response_time: Optional response time to show after the last text item
137
+
138
+ Returns:
139
+ List of rendered Dash components in order
140
+ """
141
+ if not content_items:
142
+ return []
143
+
144
+ rendered = []
145
+ last_text_index = None
146
+
147
+ # Find the last text item index for response time display
148
+ for i, item in enumerate(content_items):
149
+ if item.get("type") == "text":
150
+ last_text_index = i
151
+
152
+ for i, item in enumerate(content_items):
153
+ item_type = item.get("type")
154
+ content = item.get("content", "")
155
+
156
+ if not content:
157
+ continue
158
+
159
+ if item_type == "thinking":
160
+ block = format_thinking(content, colors)
161
+ if block:
162
+ rendered.append(block)
163
+ elif item_type == "text":
164
+ # For the last text item, we might want to show response time
165
+ is_last_text = (i == last_text_index)
166
+ block = format_ai_text(content, colors)
167
+ if block:
168
+ rendered.append(block)
169
+ # Add response time after the last text block
170
+ if is_last_text and response_time is not None:
171
+ time_display = f"{response_time:.1f}s" if response_time >= 1 else f"{response_time*1000:.0f}ms"
172
+ rendered.append(html.Span(
173
+ time_display,
174
+ style={
175
+ "fontSize": "11px",
176
+ "color": colors.get("text_muted", "#888"),
177
+ "marginLeft": "4px",
178
+ }
179
+ ))
180
+ elif item_type == "display_inline":
181
+ # content is the full display_inline item dict
182
+ block = render_display_inline_result(content, colors)
183
+ if block:
184
+ rendered.append(block)
185
+
186
+ return rendered
187
+
188
+
105
189
  def format_todos(todos, colors: Dict):
106
190
  """Format todo list. Handles both list format [{"content": ..., "status": ...}] and dict format {task_name: status}."""
107
191
  if not todos:
@@ -183,7 +267,7 @@ def format_todos_inline(todos, colors: Dict):
183
267
 
184
268
 
185
269
  def format_tool_call(tool_call: Dict, colors: Dict, is_completed: bool = False):
186
- """Format a single tool call as a submessage.
270
+ """Format a single tool call as a collapsible submessage.
187
271
 
188
272
  Args:
189
273
  tool_call: Dict with 'name', 'args', and optionally 'result', 'status'
@@ -195,23 +279,15 @@ def format_tool_call(tool_call: Dict, colors: Dict, is_completed: bool = False):
195
279
  tool_result = tool_call.get("result")
196
280
  tool_status = tool_call.get("status", "pending")
197
281
 
198
- # Status indicator - use CSS classes for theme awareness
282
+ # Status class for styling
199
283
  if tool_status == "success":
200
- status_icon = "✓"
201
284
  status_class = "tool-call-success"
202
- icon_class = "tool-call-icon-success"
203
285
  elif tool_status == "error":
204
- status_icon = "✗"
205
286
  status_class = "tool-call-error"
206
- icon_class = "tool-call-icon-error"
207
287
  elif tool_status == "running":
208
- status_icon = "◐"
209
288
  status_class = "tool-call-running"
210
- icon_class = "tool-call-icon-running"
211
289
  else: # pending
212
- status_icon = "○"
213
290
  status_class = "tool-call-pending"
214
- icon_class = "tool-call-icon-pending"
215
291
 
216
292
  # Format args for display (truncate if too long)
217
293
  args_display = ""
@@ -224,43 +300,444 @@ def format_tool_call(tool_call: Dict, colors: Dict, is_completed: bool = False):
224
300
  except:
225
301
  args_display = str(tool_args)[:500]
226
302
 
227
- # Build the tool call display using CSS classes
228
- tool_header = html.Div([
229
- html.Span(status_icon, className=icon_class, style={
230
- "marginRight": "10px",
231
- "fontWeight": "bold",
232
- }),
233
- html.Span("Tool: ", className="tool-call-label"),
234
- html.Span(tool_name, className="tool-call-name"),
235
- ], style={"display": "flex", "alignItems": "center"})
303
+ # Build inner content (shown when expanded)
304
+ inner_children = []
236
305
 
237
- # Arguments section (collapsible)
238
- args_section = None
306
+ # Arguments section
239
307
  if args_display:
240
- args_section = html.Details([
308
+ inner_children.append(html.Details([
241
309
  html.Summary("Arguments", className="tool-call-summary"),
242
310
  html.Pre(args_display, className="tool-call-args")
243
- ], style={"marginTop": "5px"})
311
+ ]))
244
312
 
245
- # Result section (collapsible, only if completed)
246
- result_section = None
313
+ # Result section (only if completed)
247
314
  if tool_result is not None and is_completed:
248
- result_display = str(tool_result)
249
- if len(result_display) > 500:
250
- result_display = result_display[:500] + "..."
315
+ # Special handling for display_inline results - render them richly
316
+ if tool_name == "display_inline" and isinstance(tool_result, dict) and tool_result.get("type") == "display_inline":
317
+ inner_children.append(render_display_inline_result(tool_result, colors))
318
+ else:
319
+ result_display = str(tool_result)
320
+ if len(result_display) > 500:
321
+ result_display = result_display[:500] + "..."
251
322
 
252
- result_section = html.Details([
253
- html.Summary("Result", className="tool-call-summary"),
254
- html.Pre(result_display, className="tool-call-result")
255
- ], style={"marginTop": "5px"})
323
+ inner_children.append(html.Details([
324
+ html.Summary("Result", className="tool-call-summary"),
325
+ html.Pre(result_display, className="tool-call-result")
326
+ ]))
327
+
328
+ # Wrap entire tool call in a details element
329
+ return html.Details([
330
+ html.Summary([
331
+ html.Span(className="tool-call-status-dot"),
332
+ html.Span(tool_name, className="tool-call-name"),
333
+ ], className="tool-call-header"),
334
+ html.Div(inner_children, className="tool-call-body") if inner_children else None
335
+ ], className=f"tool-call {status_class}")
256
336
 
257
- children = [tool_header]
258
- if args_section:
259
- children.append(args_section)
260
- if result_section:
261
- children.append(result_section)
262
337
 
263
- return html.Div(children, className=f"tool-call {status_class}")
338
+ def render_display_inline_result(result: Dict, colors: Dict) -> html.Div:
339
+ """Render display_inline tool result as rich interactive content.
340
+
341
+ Args:
342
+ result: The display_inline result dictionary
343
+ colors: Color scheme dict
344
+
345
+ Returns:
346
+ A Dash html.Div component with the rendered content
347
+ """
348
+ display_type = result.get("display_type", "text")
349
+ title = result.get("title")
350
+ data = result.get("data")
351
+ preview = result.get("preview")
352
+ error = result.get("error")
353
+ status = result.get("status", "success")
354
+ filename = result.get("filename")
355
+ downloadable = result.get("downloadable", False)
356
+ csv_data = result.get("csv")
357
+
358
+ # Get item ID for potential debugging
359
+ item_id = result.get("_item_id", "unknown")
360
+
361
+ # Build title header if present
362
+ header_children = []
363
+ if title:
364
+ header_children.append(
365
+ html.Div(title, className="display-inline-title", style={
366
+ "fontWeight": "600",
367
+ "fontSize": "15px",
368
+ "marginBottom": "8px",
369
+ })
370
+ )
371
+
372
+ # Error state
373
+ if status == "error":
374
+ return html.Div([
375
+ *header_children,
376
+ html.Div([
377
+ html.Span("Error: ", style={"fontWeight": "600"}),
378
+ html.Span(error or "Unknown error")
379
+ ], className="display-inline-error", style={
380
+ "color": "#e53e3e",
381
+ "padding": "10px",
382
+ "borderRadius": "5px",
383
+ "backgroundColor": "rgba(229, 62, 62, 0.1)",
384
+ })
385
+ ], className="display-inline-container")
386
+
387
+ content_element = None
388
+
389
+ # Render based on display type
390
+ if display_type == "image":
391
+ mime_type = result.get("mime_type", "image/png")
392
+ data_url = f"data:{mime_type};base64,{data}"
393
+ content_element = html.Img(
394
+ src=data_url,
395
+ className="display-inline-image",
396
+ style={
397
+ "maxWidth": "100%",
398
+ "maxHeight": "400px",
399
+ "borderRadius": "5px",
400
+ "objectFit": "contain",
401
+ }
402
+ )
403
+
404
+ elif display_type == "plotly":
405
+ content_element = dcc.Graph(
406
+ figure=data,
407
+ config={"displayModeBar": True, "responsive": True},
408
+ style={"height": "350px"},
409
+ className="display-inline-plotly"
410
+ )
411
+
412
+ elif display_type == "dataframe":
413
+ # Show preview with expand option for full data
414
+ preview_html = preview.get("html", "") if isinstance(preview, dict) else ""
415
+ rows_shown = preview.get("rows_shown", 0) if isinstance(preview, dict) else 0
416
+ total_rows = preview.get("total_rows", 0) if isinstance(preview, dict) else 0
417
+ columns = preview.get("columns", []) if isinstance(preview, dict) else []
418
+
419
+ # Summary line
420
+ summary = f"{total_rows} rows × {len(columns)} columns"
421
+ if rows_shown < total_rows:
422
+ summary += f" (showing first {rows_shown})"
423
+
424
+ content_element = html.Div([
425
+ html.Div(summary, className="display-inline-df-summary", style={
426
+ "fontSize": "12px",
427
+ "marginBottom": "5px",
428
+ "opacity": "0.8",
429
+ }),
430
+ html.Div(
431
+ dcc.Markdown(preview_html, dangerously_allow_html=True),
432
+ className="display-inline-dataframe",
433
+ style={
434
+ "overflowX": "auto",
435
+ "maxHeight": "300px",
436
+ "overflowY": "auto",
437
+ }
438
+ )
439
+ ])
440
+
441
+ elif display_type == "html":
442
+ # Show HTML preview directly in iframe (like PDF)
443
+ if not data:
444
+ content_element = html.Div(
445
+ "Error: HTML content is empty or missing",
446
+ style={"color": "red", "padding": "10px"}
447
+ )
448
+ else:
449
+ # Create fullscreen button
450
+ fullscreen_btn = html.Button(
451
+ DashIconify(icon="mdi:fullscreen", width=18),
452
+ id={"type": "fullscreen-btn", "index": item_id},
453
+ className="fullscreen-btn",
454
+ title="View fullscreen",
455
+ style={
456
+ "position": "absolute",
457
+ "top": "8px",
458
+ "right": "8px",
459
+ "background": "rgba(255,255,255,0.9)",
460
+ "border": "1px solid #ddd",
461
+ "borderRadius": "4px",
462
+ "cursor": "pointer",
463
+ "padding": "4px 6px",
464
+ "display": "flex",
465
+ "alignItems": "center",
466
+ "justifyContent": "center",
467
+ "zIndex": "10",
468
+ }
469
+ )
470
+ # Store data for fullscreen modal
471
+ fullscreen_store = dcc.Store(
472
+ id={"type": "fullscreen-data", "index": item_id},
473
+ data={"type": "html", "content": data, "title": title or "HTML Preview"}
474
+ )
475
+ content_element = html.Div([
476
+ fullscreen_btn,
477
+ html.Iframe(
478
+ srcDoc=data,
479
+ style={
480
+ "width": "100%",
481
+ "height": "400px",
482
+ "border": "none",
483
+ "borderRadius": "5px",
484
+ "backgroundColor": "white",
485
+ }
486
+ ),
487
+ fullscreen_store,
488
+ ], style={"position": "relative"})
489
+
490
+ elif display_type == "pdf":
491
+ mime_type = result.get("mime_type", "application/pdf")
492
+ # Debug: Check if data exists
493
+ if not data:
494
+ content_element = html.Div(
495
+ "Error: PDF data is empty or missing",
496
+ style={"color": "red", "padding": "10px"}
497
+ )
498
+ else:
499
+ data_url = f"data:{mime_type};base64,{data}"
500
+ # Create fullscreen button
501
+ fullscreen_btn = html.Button(
502
+ DashIconify(icon="mdi:fullscreen", width=18),
503
+ id={"type": "fullscreen-btn", "index": item_id},
504
+ className="fullscreen-btn",
505
+ title="View fullscreen",
506
+ style={
507
+ "position": "absolute",
508
+ "top": "8px",
509
+ "right": "8px",
510
+ "background": "rgba(255,255,255,0.9)",
511
+ "border": "1px solid #ddd",
512
+ "borderRadius": "4px",
513
+ "cursor": "pointer",
514
+ "padding": "4px 6px",
515
+ "display": "flex",
516
+ "alignItems": "center",
517
+ "justifyContent": "center",
518
+ "zIndex": "10",
519
+ }
520
+ )
521
+ # Store data for fullscreen modal
522
+ fullscreen_store = dcc.Store(
523
+ id={"type": "fullscreen-data", "index": item_id},
524
+ data={"type": "pdf", "content": data_url, "title": title or "PDF Preview"}
525
+ )
526
+ # Use iframe instead of embed for better browser compatibility
527
+ content_element = html.Div([
528
+ fullscreen_btn,
529
+ html.Iframe(
530
+ src=data_url,
531
+ style={
532
+ "width": "100%",
533
+ "height": "400px",
534
+ "border": "none",
535
+ "borderRadius": "5px",
536
+ }
537
+ ),
538
+ fullscreen_store,
539
+ ], style={"position": "relative"})
540
+
541
+ elif display_type == "json":
542
+ json_str = json.dumps(data, indent=2) if isinstance(data, (dict, list)) else str(data)
543
+ content_element = html.Details([
544
+ html.Summary("JSON Data", className="tool-call-summary"),
545
+ html.Pre(json_str, className="display-inline-json", style={
546
+ "maxHeight": "300px",
547
+ "overflowY": "auto",
548
+ "fontSize": "12px",
549
+ "padding": "10px",
550
+ "borderRadius": "5px",
551
+ })
552
+ ])
553
+
554
+ else: # text or unknown
555
+ text_content = str(data) if data else ""
556
+ if len(text_content) > 1000:
557
+ content_element = html.Details([
558
+ html.Summary(f"Text ({len(text_content)} chars)", className="tool-call-summary"),
559
+ html.Pre(text_content, className="display-inline-text", style={
560
+ "maxHeight": "300px",
561
+ "overflowY": "auto",
562
+ "whiteSpace": "pre-wrap",
563
+ "fontSize": "13px",
564
+ })
565
+ ])
566
+ else:
567
+ content_element = html.Pre(text_content, className="display-inline-text", style={
568
+ "whiteSpace": "pre-wrap",
569
+ "fontSize": "13px",
570
+ })
571
+
572
+ # Build footer with download option if applicable
573
+ footer_children = []
574
+ if filename:
575
+ footer_children.append(
576
+ html.Span(filename, className="display-inline-filename", style={
577
+ "fontSize": "11px",
578
+ "opacity": "0.7",
579
+ })
580
+ )
581
+
582
+ # Assemble final component with collapsible wrapper
583
+ # Note: Dash components evaluate to False in boolean context, so use 'is not None'
584
+
585
+ # Build the summary label for the collapsible header
586
+ display_type_labels = {
587
+ "image": "📷 Image",
588
+ "plotly": "📊 Chart",
589
+ "dataframe": "📋 Table",
590
+ "html": "🌐 HTML",
591
+ "pdf": "📄 PDF",
592
+ "json": "📝 JSON",
593
+ "text": "📝 Text",
594
+ "error": "⚠️ Error",
595
+ }
596
+ type_label = display_type_labels.get(display_type, "📎 Content")
597
+ summary_text = f"{type_label}: {title}" if title else type_label
598
+ if filename:
599
+ summary_text += f" ({filename})"
600
+
601
+ # Content to show inside the collapsible section
602
+ content_children = []
603
+ if content_element is not None:
604
+ content_children.append(content_element)
605
+ if footer_children:
606
+ content_children.append(
607
+ html.Div(footer_children, style={"marginTop": "5px"})
608
+ )
609
+
610
+ # Create "Add to Canvas" button with pattern-matching ID
611
+ # Using html.Button for reliable n_clicks behavior (DMC Tooltip can interfere with callbacks)
612
+ add_to_canvas_btn = html.Button(
613
+ DashIconify(icon="mdi:palette-outline", width=16),
614
+ id={"type": "add-display-to-canvas-btn", "index": item_id},
615
+ className="add-to-canvas-btn",
616
+ title="Add to Canvas",
617
+ style={
618
+ "background": "transparent",
619
+ "border": "none",
620
+ "cursor": "pointer",
621
+ "padding": "4px",
622
+ "borderRadius": "4px",
623
+ "display": "flex",
624
+ "alignItems": "center",
625
+ "justifyContent": "center",
626
+ }
627
+ )
628
+
629
+ # Create download button
630
+ download_btn = html.Button(
631
+ DashIconify(icon="mdi:download", width=16),
632
+ id={"type": "download-display-btn", "index": item_id},
633
+ className="download-display-btn",
634
+ title="Download",
635
+ style={
636
+ "background": "transparent",
637
+ "border": "none",
638
+ "cursor": "pointer",
639
+ "padding": "4px",
640
+ "borderRadius": "4px",
641
+ "display": "flex",
642
+ "alignItems": "center",
643
+ "justifyContent": "center",
644
+ "marginLeft": "4px",
645
+ }
646
+ )
647
+
648
+ # Store the result data in a hidden div for the callback to retrieve
649
+ result_store = dcc.Store(
650
+ id={"type": "display-inline-data", "index": item_id},
651
+ data=result
652
+ )
653
+
654
+ # Build summary with text and buttons
655
+ summary_content = html.Div([
656
+ html.Span(summary_text, className="display-inline-summary-text"),
657
+ html.Div([
658
+ add_to_canvas_btn,
659
+ download_btn,
660
+ ], style={"display": "flex", "alignItems": "center"}),
661
+ ], className="display-inline-summary-row", style={
662
+ "display": "flex",
663
+ "alignItems": "center",
664
+ "justifyContent": "space-between",
665
+ "width": "100%",
666
+ })
667
+
668
+ return html.Details([
669
+ html.Summary(summary_content, className="display-inline-summary"),
670
+ html.Div(content_children, className="display-inline-content", style={
671
+ "marginTop": "8px",
672
+ }),
673
+ result_store, # Hidden store for callback data
674
+ ], className="display-inline-container", open=True, style={
675
+ "padding": "10px",
676
+ "borderRadius": "8px",
677
+ "marginTop": "5px",
678
+ })
679
+
680
+
681
+ def extract_display_inline_results(tool_calls: List[Dict], colors: Dict) -> List:
682
+ """Extract and render display_inline results prominently from tool calls.
683
+
684
+ This function finds all display_inline tool results and renders them
685
+ as standalone visual elements that appear alongside messages, rather
686
+ than buried in the tool call details.
687
+
688
+ Args:
689
+ tool_calls: List of tool call dicts with 'name', 'args', 'result', 'status'
690
+ colors: Color scheme dict
691
+
692
+ Returns:
693
+ List of rendered display_inline components (may be empty)
694
+ """
695
+ if not tool_calls:
696
+ return []
697
+
698
+ results = []
699
+ for tc in tool_calls:
700
+ if tc.get("name") == "display_inline" and tc.get("status") in ("success", "error"):
701
+ result = tc.get("result")
702
+ # Check if result is a dict with display_inline structure
703
+ if isinstance(result, dict) and result.get("type") == "display_inline":
704
+ rendered = render_display_inline_result(result, colors)
705
+ results.append(rendered)
706
+
707
+ return results
708
+
709
+
710
+ def extract_thinking_from_tool_calls(tool_calls: List[Dict], colors: Dict) -> List:
711
+ """Extract think_tool calls and render them as thinking blocks.
712
+
713
+ Args:
714
+ tool_calls: List of tool call dicts with 'name', 'args', 'result', 'status'
715
+ colors: Color scheme dict
716
+
717
+ Returns:
718
+ List of rendered thinking components (may be empty)
719
+ """
720
+ if not tool_calls:
721
+ return []
722
+
723
+ results = []
724
+ for tc in tool_calls:
725
+ if tc.get("name") == "think_tool" and tc.get("status") in ("success", "error"):
726
+ result = tc.get("result")
727
+ if result:
728
+ # Extract thinking text from result
729
+ thinking_text = ""
730
+ if isinstance(result, str):
731
+ thinking_text = result
732
+ elif isinstance(result, dict):
733
+ thinking_text = result.get("reflection", str(result))
734
+
735
+ if thinking_text:
736
+ thinking_block = format_thinking(thinking_text, colors)
737
+ if thinking_block:
738
+ results.append(thinking_block)
739
+
740
+ return results
264
741
 
265
742
 
266
743
  def format_tool_calls_inline(tool_calls: List[Dict], colors: Dict):
@@ -269,14 +746,23 @@ def format_tool_calls_inline(tool_calls: List[Dict], colors: Dict):
269
746
  Args:
270
747
  tool_calls: List of tool call dicts with 'name', 'args', 'result', 'status'
271
748
  colors: Color scheme dict
749
+
750
+ Note: think_tool calls are excluded from this rendering - they are rendered
751
+ separately via extract_thinking_from_tool_calls() to appear in correct order.
272
752
  """
273
753
  if not tool_calls:
274
754
  return None
275
755
 
276
- # Count statuses
277
- completed = sum(1 for tc in tool_calls if tc.get("status") in ("success", "error"))
278
- total = len(tool_calls)
279
- running = sum(1 for tc in tool_calls if tc.get("status") == "running")
756
+ # Filter out think_tool calls - they are rendered separately as thinking blocks
757
+ filtered_tool_calls = [tc for tc in tool_calls if tc.get("name") != "think_tool"]
758
+
759
+ if not filtered_tool_calls:
760
+ return None
761
+
762
+ # Count statuses (on filtered list)
763
+ completed = sum(1 for tc in filtered_tool_calls if tc.get("status") in ("success", "error"))
764
+ total = len(filtered_tool_calls)
765
+ running = sum(1 for tc in filtered_tool_calls if tc.get("status") == "running")
280
766
 
281
767
  # Summary text and class
282
768
  if running > 0:
@@ -291,7 +777,7 @@ def format_tool_calls_inline(tool_calls: List[Dict], colors: Dict):
291
777
 
292
778
  tool_elements = [
293
779
  format_tool_call(tc, colors, is_completed=tc.get("status") in ("success", "error"))
294
- for tc in tool_calls
780
+ for tc in filtered_tool_calls
295
781
  ]
296
782
 
297
783
  return html.Details([
@@ -435,6 +921,8 @@ def _get_type_badge(item_type: str) -> dmc.Badge:
435
921
  "image": "green",
436
922
  "plotly": "violet",
437
923
  "mermaid": "cyan",
924
+ "pdf": "red",
925
+ "error": "red",
438
926
  }
439
927
  type_labels = {
440
928
  "markdown": "Text",
@@ -443,6 +931,8 @@ def _get_type_badge(item_type: str) -> dmc.Badge:
443
931
  "image": "Image",
444
932
  "plotly": "Plot",
445
933
  "mermaid": "Diagram",
934
+ "pdf": "PDF",
935
+ "error": "Error",
446
936
  }
447
937
  color = type_colors.get(item_type, "gray")
448
938
  label = type_labels.get(item_type, item_type.title())
@@ -611,6 +1101,30 @@ def render_canvas_items(canvas_items: List[Dict], colors: Dict, collapsed_ids: O
611
1101
  "overflow": "auto",
612
1102
  })
613
1103
 
1104
+ elif item_type == "pdf":
1105
+ pdf_data = item.get("data", "")
1106
+ mime_type = item.get("mime_type", "application/pdf")
1107
+ if not pdf_data:
1108
+ content = html.Div([
1109
+ html.Div("Error: PDF data is empty or missing", style={"color": "red"})
1110
+ ], className="canvas-item-content canvas-item-pdf", style={"padding": "10px"})
1111
+ else:
1112
+ data_url = f"data:{mime_type};base64,{pdf_data}"
1113
+ # Use iframe instead of embed for better browser compatibility
1114
+ content = html.Div([
1115
+ html.Iframe(
1116
+ src=data_url,
1117
+ style={
1118
+ "width": "100%",
1119
+ "height": "500px",
1120
+ "border": "none",
1121
+ "borderRadius": "5px",
1122
+ }
1123
+ )
1124
+ ], className="canvas-item-content canvas-item-pdf", style={
1125
+ "padding": "10px",
1126
+ })
1127
+
614
1128
  else:
615
1129
  # Unknown type
616
1130
  content = html.Div([