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/agent.py +33 -17
- cowork_dash/app.py +1056 -160
- cowork_dash/assets/app.js +34 -0
- cowork_dash/assets/styles.css +804 -693
- cowork_dash/cli.py +9 -0
- cowork_dash/components.py +573 -59
- cowork_dash/config.py +12 -1
- cowork_dash/file_utils.py +43 -4
- cowork_dash/layout.py +43 -2
- cowork_dash/sandbox.py +361 -0
- cowork_dash/tools.py +656 -69
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.1.dist-info}/METADATA +1 -1
- cowork_dash-0.2.1.dist-info/RECORD +23 -0
- cowork_dash-0.1.9.dist-info/RECORD +0 -22
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.1.dist-info}/WHEEL +0 -0
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.1.dist-info}/entry_points.txt +0 -0
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.1.dist-info}/licenses/LICENSE +0 -0
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
96
|
-
html.
|
|
97
|
-
|
|
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
|
-
],
|
|
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
|
|
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
|
|
228
|
-
|
|
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
|
|
238
|
-
args_section = None
|
|
306
|
+
# Arguments section
|
|
239
307
|
if args_display:
|
|
240
|
-
|
|
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
|
-
]
|
|
311
|
+
]))
|
|
244
312
|
|
|
245
|
-
# Result section (
|
|
246
|
-
result_section = None
|
|
313
|
+
# Result section (only if completed)
|
|
247
314
|
if tool_result is not None and is_completed:
|
|
248
|
-
|
|
249
|
-
if
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
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([
|