cowork-dash 0.2.0__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 +18 -23
- cowork_dash/app.py +595 -223
- cowork_dash/assets/styles.css +20 -0
- cowork_dash/components.py +199 -28
- cowork_dash/layout.py +41 -0
- cowork_dash/tools.py +20 -35
- {cowork_dash-0.2.0.dist-info → cowork_dash-0.2.1.dist-info}/METADATA +1 -1
- {cowork_dash-0.2.0.dist-info → cowork_dash-0.2.1.dist-info}/RECORD +11 -11
- {cowork_dash-0.2.0.dist-info → cowork_dash-0.2.1.dist-info}/WHEEL +0 -0
- {cowork_dash-0.2.0.dist-info → cowork_dash-0.2.1.dist-info}/entry_points.txt +0 -0
- {cowork_dash-0.2.0.dist-info → cowork_dash-0.2.1.dist-info}/licenses/LICENSE +0 -0
cowork_dash/app.py
CHANGED
|
@@ -35,7 +35,8 @@ from .canvas import export_canvas_to_markdown, load_canvas_from_markdown
|
|
|
35
35
|
from .file_utils import build_file_tree, render_file_tree, read_file_content, get_file_download_data, load_folder_contents
|
|
36
36
|
from .components import (
|
|
37
37
|
format_message, format_loading, format_thinking, format_todos_inline, render_canvas_items, format_tool_calls_inline,
|
|
38
|
-
format_interrupt, extract_display_inline_results, render_display_inline_result, extract_thinking_from_tool_calls
|
|
38
|
+
format_interrupt, extract_display_inline_results, render_display_inline_result, extract_thinking_from_tool_calls,
|
|
39
|
+
render_ordered_content_items
|
|
39
40
|
)
|
|
40
41
|
from .layout import create_layout as create_layout_component
|
|
41
42
|
from .virtual_fs import get_session_manager
|
|
@@ -251,6 +252,7 @@ _agent_state = {
|
|
|
251
252
|
"thinking": "",
|
|
252
253
|
"todos": [],
|
|
253
254
|
"tool_calls": [], # Current turn's tool calls (reset each turn)
|
|
255
|
+
"content_items": [], # Ordered list of content: {"type": "text"|"thinking"|"tool_calls", "content": ...}
|
|
254
256
|
"display_inline_items": [], # Items pushed by display_inline tool (bypasses LangGraph)
|
|
255
257
|
"canvas": load_canvas_from_markdown(WORKSPACE_ROOT) if not USE_VIRTUAL_FS else [], # Load from canvas.md if exists (physical FS only)
|
|
256
258
|
"response": "",
|
|
@@ -277,6 +279,7 @@ def _get_default_agent_state() -> Dict[str, Any]:
|
|
|
277
279
|
"thinking": "",
|
|
278
280
|
"todos": [],
|
|
279
281
|
"tool_calls": [],
|
|
282
|
+
"content_items": [], # Ordered list of content: {"type": "text"|"thinking"|"tool_calls", "content": ...}
|
|
280
283
|
"display_inline_items": [], # Items pushed by display_inline tool (bypasses LangGraph)
|
|
281
284
|
"canvas": [],
|
|
282
285
|
"response": "",
|
|
@@ -484,29 +487,59 @@ def _run_agent_stream(message: str, resume_data: Dict = None, workspace_path: st
|
|
|
484
487
|
last_msg = msgs[-1] if isinstance(msgs, list) else msgs
|
|
485
488
|
msg_type = last_msg.__class__.__name__ if hasattr(last_msg, '__class__') else None
|
|
486
489
|
|
|
487
|
-
# Capture AIMessage tool_calls
|
|
488
|
-
if msg_type == 'AIMessage'
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
for existing_tc in current_state["tool_calls"]:
|
|
505
|
-
if existing_tc.get("id") == tc_id:
|
|
506
|
-
tool_call_map[tc_id] = existing_tc
|
|
507
|
-
break
|
|
490
|
+
# Capture AIMessage content and tool_calls
|
|
491
|
+
if msg_type == 'AIMessage':
|
|
492
|
+
# First, capture any text content from the AIMessage
|
|
493
|
+
if hasattr(last_msg, 'content') and last_msg.content:
|
|
494
|
+
content = last_msg.content
|
|
495
|
+
response_text = ""
|
|
496
|
+
if isinstance(content, str):
|
|
497
|
+
response_text = re.sub(
|
|
498
|
+
r"\{'id':\s*'[^']+',\s*'input':\s*\{.*?\},\s*'name':\s*'[^']+',\s*'type':\s*'tool_use'\}",
|
|
499
|
+
'', content, flags=re.DOTALL
|
|
500
|
+
).strip()
|
|
501
|
+
elif isinstance(content, list):
|
|
502
|
+
text_parts = [
|
|
503
|
+
block.get("text", "") if isinstance(block, dict) and block.get("type") == "text" else ""
|
|
504
|
+
for block in content
|
|
505
|
+
]
|
|
506
|
+
response_text = " ".join(filter(None, text_parts)).strip()
|
|
508
507
|
|
|
509
|
-
|
|
508
|
+
if response_text:
|
|
509
|
+
with state_lock:
|
|
510
|
+
current_state["response"] = response_text
|
|
511
|
+
# Add text to ordered content list
|
|
512
|
+
if "content_items" not in current_state:
|
|
513
|
+
current_state["content_items"] = []
|
|
514
|
+
current_state["content_items"].append({
|
|
515
|
+
"type": "text",
|
|
516
|
+
"content": response_text
|
|
517
|
+
})
|
|
518
|
+
current_state["last_update"] = time.time()
|
|
519
|
+
|
|
520
|
+
# Then, capture any tool_calls
|
|
521
|
+
if hasattr(last_msg, 'tool_calls') and last_msg.tool_calls:
|
|
522
|
+
with state_lock:
|
|
523
|
+
# Get existing tool call IDs to avoid duplicates
|
|
524
|
+
existing_ids = {tc.get("id") for tc in current_state["tool_calls"]}
|
|
525
|
+
|
|
526
|
+
for tc in last_msg.tool_calls:
|
|
527
|
+
serialized = _serialize_tool_call(tc)
|
|
528
|
+
tc_id = serialized["id"]
|
|
529
|
+
|
|
530
|
+
# Only add if not already in the list (avoid duplicates on resume)
|
|
531
|
+
if tc_id not in existing_ids:
|
|
532
|
+
tool_call_map[tc_id] = serialized
|
|
533
|
+
current_state["tool_calls"].append(serialized)
|
|
534
|
+
existing_ids.add(tc_id)
|
|
535
|
+
else:
|
|
536
|
+
# Update the map to reference the existing tool call
|
|
537
|
+
for existing_tc in current_state["tool_calls"]:
|
|
538
|
+
if existing_tc.get("id") == tc_id:
|
|
539
|
+
tool_call_map[tc_id] = existing_tc
|
|
540
|
+
break
|
|
541
|
+
|
|
542
|
+
current_state["last_update"] = time.time()
|
|
510
543
|
|
|
511
544
|
elif msg_type == 'ToolMessage' and hasattr(last_msg, 'name'):
|
|
512
545
|
# Update tool call status when we get the result
|
|
@@ -560,9 +593,16 @@ def _run_agent_stream(message: str, resume_data: Dict = None, workspace_path: st
|
|
|
560
593
|
elif isinstance(content, dict):
|
|
561
594
|
thinking_text = content.get('reflection', str(content))
|
|
562
595
|
|
|
563
|
-
# Update state immediately
|
|
596
|
+
# Update state immediately - add to ordered content_items
|
|
564
597
|
with state_lock:
|
|
565
598
|
current_state["thinking"] = thinking_text
|
|
599
|
+
# Add thinking to ordered content list
|
|
600
|
+
if "content_items" not in current_state:
|
|
601
|
+
current_state["content_items"] = []
|
|
602
|
+
current_state["content_items"].append({
|
|
603
|
+
"type": "thinking",
|
|
604
|
+
"content": thinking_text
|
|
605
|
+
})
|
|
566
606
|
current_state["last_update"] = time.time()
|
|
567
607
|
|
|
568
608
|
elif last_msg.name == 'write_todos':
|
|
@@ -716,26 +756,6 @@ def _run_agent_stream(message: str, resume_data: Dict = None, workspace_path: st
|
|
|
716
756
|
except Exception as e:
|
|
717
757
|
print(f"Failed to export canvas: {e}")
|
|
718
758
|
|
|
719
|
-
elif hasattr(last_msg, 'content'):
|
|
720
|
-
content = last_msg.content
|
|
721
|
-
response_text = ""
|
|
722
|
-
if isinstance(content, str):
|
|
723
|
-
response_text = re.sub(
|
|
724
|
-
r"\{'id':\s*'[^']+',\s*'input':\s*\{.*?\},\s*'name':\s*'[^']+',\s*'type':\s*'tool_use'\}",
|
|
725
|
-
'', content, flags=re.DOTALL
|
|
726
|
-
).strip()
|
|
727
|
-
elif isinstance(content, list):
|
|
728
|
-
text_parts = [
|
|
729
|
-
block.get("text", "") if isinstance(block, dict) else str(block)
|
|
730
|
-
for block in content
|
|
731
|
-
]
|
|
732
|
-
response_text = " ".join(text_parts).strip()
|
|
733
|
-
|
|
734
|
-
if response_text:
|
|
735
|
-
with state_lock:
|
|
736
|
-
current_state["response"] = response_text
|
|
737
|
-
current_state["last_update"] = time.time()
|
|
738
|
-
|
|
739
759
|
except Exception as e:
|
|
740
760
|
with state_lock:
|
|
741
761
|
current_state["error"] = str(e)
|
|
@@ -875,6 +895,7 @@ def call_agent(message: str, resume_data: Dict = None, workspace_path: str = Non
|
|
|
875
895
|
"thinking": "",
|
|
876
896
|
"todos": [],
|
|
877
897
|
"tool_calls": [], # Reset tool calls for this turn
|
|
898
|
+
"content_items": [], # Ordered list of content items
|
|
878
899
|
"canvas": existing_canvas, # Preserve existing canvas
|
|
879
900
|
"response": "",
|
|
880
901
|
"error": None,
|
|
@@ -1009,6 +1030,7 @@ def get_agent_state(session_id: Optional[str] = None) -> Dict[str, Any]:
|
|
|
1009
1030
|
state["tool_calls"] = copy.deepcopy(current_state["tool_calls"])
|
|
1010
1031
|
state["todos"] = copy.deepcopy(current_state["todos"])
|
|
1011
1032
|
state["canvas"] = copy.deepcopy(current_state["canvas"])
|
|
1033
|
+
state["content_items"] = copy.deepcopy(current_state.get("content_items", []))
|
|
1012
1034
|
state["display_inline_items"] = copy.deepcopy(current_state.get("display_inline_items", []))
|
|
1013
1035
|
return state
|
|
1014
1036
|
|
|
@@ -1034,6 +1056,13 @@ def push_display_inline_item(item: Dict[str, Any], session_id: Optional[str] = N
|
|
|
1034
1056
|
if "display_inline_items" not in current_state:
|
|
1035
1057
|
current_state["display_inline_items"] = []
|
|
1036
1058
|
current_state["display_inline_items"].append(item)
|
|
1059
|
+
# Also add to ordered content_items for proper interleaving
|
|
1060
|
+
if "content_items" not in current_state:
|
|
1061
|
+
current_state["content_items"] = []
|
|
1062
|
+
current_state["content_items"].append({
|
|
1063
|
+
"type": "display_inline",
|
|
1064
|
+
"content": item # Store the full item dict
|
|
1065
|
+
})
|
|
1037
1066
|
current_state["last_update"] = time.time()
|
|
1038
1067
|
|
|
1039
1068
|
|
|
@@ -1058,6 +1087,7 @@ def reset_agent_state(session_id: Optional[str] = None):
|
|
|
1058
1087
|
current_state["thinking"] = ""
|
|
1059
1088
|
current_state["todos"] = []
|
|
1060
1089
|
current_state["tool_calls"] = []
|
|
1090
|
+
current_state["content_items"] = []
|
|
1061
1091
|
current_state["display_inline_items"] = []
|
|
1062
1092
|
current_state["response"] = ""
|
|
1063
1093
|
current_state["error"] = None
|
|
@@ -1176,31 +1206,49 @@ def display_initial_messages(history, theme, skip_render, session_initialized, s
|
|
|
1176
1206
|
messages = []
|
|
1177
1207
|
for msg in history:
|
|
1178
1208
|
msg_response_time = msg.get("response_time") if msg["role"] == "assistant" else None
|
|
1179
|
-
|
|
1180
|
-
#
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
if
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
#
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1209
|
+
|
|
1210
|
+
# For user messages, render normally
|
|
1211
|
+
if msg["role"] == "user":
|
|
1212
|
+
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, is_new=False, response_time=msg_response_time))
|
|
1213
|
+
|
|
1214
|
+
# Show collapsed tool calls section
|
|
1215
|
+
if msg.get("tool_calls"):
|
|
1216
|
+
tool_calls_block = format_tool_calls_inline(msg["tool_calls"], colors)
|
|
1217
|
+
if tool_calls_block:
|
|
1218
|
+
messages.append(tool_calls_block)
|
|
1219
|
+
|
|
1220
|
+
# Render todos
|
|
1221
|
+
if msg.get("todos"):
|
|
1222
|
+
todos_block = format_todos_inline(msg["todos"], colors)
|
|
1223
|
+
if todos_block:
|
|
1224
|
+
messages.append(todos_block)
|
|
1225
|
+
|
|
1226
|
+
# Render ordered content items (thinking + text + display_inline in order)
|
|
1227
|
+
if msg.get("content_items"):
|
|
1228
|
+
content_blocks = render_ordered_content_items(msg["content_items"], colors, STYLES)
|
|
1229
|
+
messages.extend(content_blocks)
|
|
1230
|
+
else:
|
|
1231
|
+
# Fallback: extract thinking from tool calls (old format)
|
|
1232
|
+
if msg.get("tool_calls"):
|
|
1233
|
+
thinking_blocks = extract_thinking_from_tool_calls(msg["tool_calls"], colors)
|
|
1234
|
+
messages.extend(thinking_blocks)
|
|
1235
|
+
|
|
1236
|
+
# Extract and show display_inline results from tool calls (old format)
|
|
1237
|
+
if msg.get("tool_calls"):
|
|
1238
|
+
inline_results = extract_display_inline_results(msg["tool_calls"], colors)
|
|
1239
|
+
messages.extend(inline_results)
|
|
1240
|
+
|
|
1241
|
+
# Render display_inline items stored with this message (old format)
|
|
1242
|
+
if msg.get("display_inline_items"):
|
|
1243
|
+
for item in msg["display_inline_items"]:
|
|
1244
|
+
rendered = render_display_inline_result(item, colors)
|
|
1245
|
+
messages.append(rendered)
|
|
1246
|
+
else:
|
|
1247
|
+
# For assistant messages, check if content_items was used (text already rendered above)
|
|
1248
|
+
# In the new model, assistant content may be empty if content_items is in the user message
|
|
1249
|
+
if msg["content"]:
|
|
1250
|
+
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, is_new=False, response_time=msg_response_time))
|
|
1251
|
+
|
|
1204
1252
|
return messages, False, True, new_session_id
|
|
1205
1253
|
|
|
1206
1254
|
|
|
@@ -1268,25 +1316,41 @@ def handle_send_immediate(n_clicks, n_submit, message, history, theme, current_w
|
|
|
1268
1316
|
for i, m in enumerate(history):
|
|
1269
1317
|
is_new = (i == len(history) - 1)
|
|
1270
1318
|
msg_response_time = m.get("response_time") if m["role"] == "assistant" else None
|
|
1271
|
-
|
|
1272
|
-
#
|
|
1273
|
-
if m
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1319
|
+
|
|
1320
|
+
# For user messages, render normally
|
|
1321
|
+
if m["role"] == "user":
|
|
1322
|
+
messages.append(format_message(m["role"], m["content"], colors, STYLES, is_new=is_new, response_time=msg_response_time))
|
|
1323
|
+
|
|
1324
|
+
# Show collapsed tool calls section
|
|
1325
|
+
if m.get("tool_calls"):
|
|
1326
|
+
tool_calls_block = format_tool_calls_inline(m["tool_calls"], colors)
|
|
1327
|
+
if tool_calls_block:
|
|
1328
|
+
messages.append(tool_calls_block)
|
|
1329
|
+
|
|
1330
|
+
# Render todos
|
|
1331
|
+
if m.get("todos"):
|
|
1332
|
+
todos_block = format_todos_inline(m["todos"], colors)
|
|
1333
|
+
if todos_block:
|
|
1334
|
+
messages.append(todos_block)
|
|
1335
|
+
|
|
1336
|
+
# Render ordered content items (thinking + text + display_inline in order)
|
|
1337
|
+
if m.get("content_items"):
|
|
1338
|
+
content_blocks = render_ordered_content_items(m["content_items"], colors, STYLES)
|
|
1339
|
+
messages.extend(content_blocks)
|
|
1340
|
+
else:
|
|
1341
|
+
# Fallback: extract thinking from tool calls (old format)
|
|
1342
|
+
if m.get("tool_calls"):
|
|
1343
|
+
thinking_blocks = extract_thinking_from_tool_calls(m["tool_calls"], colors)
|
|
1344
|
+
messages.extend(thinking_blocks)
|
|
1345
|
+
|
|
1346
|
+
# Extract and show display_inline results from tool calls (old format)
|
|
1347
|
+
if m.get("tool_calls"):
|
|
1348
|
+
inline_results = extract_display_inline_results(m["tool_calls"], colors)
|
|
1349
|
+
messages.extend(inline_results)
|
|
1350
|
+
else:
|
|
1351
|
+
# For assistant messages, check if content_items was used
|
|
1352
|
+
if m["content"]:
|
|
1353
|
+
messages.append(format_message(m["role"], m["content"], colors, STYLES, is_new=is_new, response_time=msg_response_time))
|
|
1290
1354
|
|
|
1291
1355
|
messages.append(format_loading(colors))
|
|
1292
1356
|
|
|
@@ -1340,30 +1404,49 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1340
1404
|
messages = []
|
|
1341
1405
|
for msg in history_items:
|
|
1342
1406
|
msg_response_time = msg.get("response_time") if msg["role"] == "assistant" else None
|
|
1343
|
-
|
|
1344
|
-
#
|
|
1345
|
-
if msg
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1407
|
+
|
|
1408
|
+
# For user messages, render normally
|
|
1409
|
+
if msg["role"] == "user":
|
|
1410
|
+
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, response_time=msg_response_time))
|
|
1411
|
+
|
|
1412
|
+
# Show collapsed tool calls section
|
|
1413
|
+
if msg.get("tool_calls"):
|
|
1414
|
+
tool_calls_block = format_tool_calls_inline(msg["tool_calls"], colors)
|
|
1415
|
+
if tool_calls_block:
|
|
1416
|
+
messages.append(tool_calls_block)
|
|
1417
|
+
|
|
1418
|
+
# Render todos
|
|
1419
|
+
if msg.get("todos"):
|
|
1420
|
+
todos_block = format_todos_inline(msg["todos"], colors)
|
|
1421
|
+
if todos_block:
|
|
1422
|
+
messages.append(todos_block)
|
|
1423
|
+
|
|
1424
|
+
# Render ordered content items (thinking + text + display_inline in order)
|
|
1425
|
+
if msg.get("content_items"):
|
|
1426
|
+
content_blocks = render_ordered_content_items(msg["content_items"], colors, STYLES)
|
|
1427
|
+
messages.extend(content_blocks)
|
|
1428
|
+
else:
|
|
1429
|
+
# Fallback: extract thinking from tool calls (old format)
|
|
1430
|
+
if msg.get("tool_calls"):
|
|
1431
|
+
thinking_blocks = extract_thinking_from_tool_calls(msg["tool_calls"], colors)
|
|
1432
|
+
messages.extend(thinking_blocks)
|
|
1433
|
+
|
|
1434
|
+
# Extract and show display_inline results from tool calls (old format)
|
|
1435
|
+
if msg.get("tool_calls"):
|
|
1436
|
+
inline_results = extract_display_inline_results(msg["tool_calls"], colors)
|
|
1437
|
+
messages.extend(inline_results)
|
|
1438
|
+
|
|
1439
|
+
# Render display_inline items stored with this message (old format)
|
|
1440
|
+
if msg.get("display_inline_items"):
|
|
1441
|
+
for item in msg["display_inline_items"]:
|
|
1442
|
+
rendered = render_display_inline_result(item, colors)
|
|
1443
|
+
messages.append(rendered)
|
|
1444
|
+
else:
|
|
1445
|
+
# For assistant messages, check if content_items was used (text already rendered above)
|
|
1446
|
+
# In the new model, assistant content may be empty if content_items is in the user message
|
|
1447
|
+
if msg["content"]:
|
|
1448
|
+
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, response_time=msg_response_time))
|
|
1449
|
+
|
|
1367
1450
|
return messages
|
|
1368
1451
|
|
|
1369
1452
|
# Check for interrupt (human-in-the-loop)
|
|
@@ -1371,30 +1454,37 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1371
1454
|
# Agent is paused waiting for user input
|
|
1372
1455
|
messages = render_history_messages(history)
|
|
1373
1456
|
|
|
1374
|
-
#
|
|
1457
|
+
# Show collapsed tool calls section
|
|
1375
1458
|
if state.get("tool_calls"):
|
|
1376
|
-
# Show collapsed tool calls section first
|
|
1377
1459
|
tool_calls_block = format_tool_calls_inline(state["tool_calls"], colors)
|
|
1378
1460
|
if tool_calls_block:
|
|
1379
1461
|
messages.append(tool_calls_block)
|
|
1380
1462
|
|
|
1463
|
+
# Render todos
|
|
1381
1464
|
if state["todos"]:
|
|
1382
1465
|
todos_block = format_todos_inline(state["todos"], colors)
|
|
1383
1466
|
if todos_block:
|
|
1384
1467
|
messages.append(todos_block)
|
|
1385
1468
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
messages.extend(
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1469
|
+
# Render ordered content items (thinking + text + display_inline in order)
|
|
1470
|
+
if state.get("content_items"):
|
|
1471
|
+
content_blocks = render_ordered_content_items(state["content_items"], colors, STYLES)
|
|
1472
|
+
messages.extend(content_blocks)
|
|
1473
|
+
else:
|
|
1474
|
+
# Fallback: extract thinking from tool calls
|
|
1475
|
+
if state.get("tool_calls"):
|
|
1476
|
+
thinking_blocks = extract_thinking_from_tool_calls(state["tool_calls"], colors)
|
|
1477
|
+
messages.extend(thinking_blocks)
|
|
1478
|
+
|
|
1479
|
+
# Extract and show display_inline results from tool calls (old format)
|
|
1480
|
+
if state.get("tool_calls"):
|
|
1481
|
+
inline_results = extract_display_inline_results(state["tool_calls"], colors)
|
|
1482
|
+
messages.extend(inline_results)
|
|
1483
|
+
|
|
1484
|
+
# Render any queued display_inline items (old format)
|
|
1485
|
+
for item in display_inline_items:
|
|
1486
|
+
rendered = render_display_inline_result(item, colors)
|
|
1487
|
+
messages.append(rendered)
|
|
1398
1488
|
|
|
1399
1489
|
# Add interrupt UI
|
|
1400
1490
|
interrupt_block = format_interrupt(state["interrupt"], colors)
|
|
@@ -1411,60 +1501,36 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1411
1501
|
if state.get("start_time"):
|
|
1412
1502
|
response_time = time.time() - state["start_time"]
|
|
1413
1503
|
|
|
1414
|
-
# Agent finished - store tool calls, todos, and display_inline items with the USER message
|
|
1504
|
+
# Agent finished - store tool calls, todos, content_items, and display_inline items with the USER message
|
|
1415
1505
|
# (they appear after user msg in the UI)
|
|
1416
1506
|
saved_display_inline_items = False
|
|
1417
1507
|
if history:
|
|
1418
|
-
# Find the last user message and attach tool calls, todos, and display_inline items to it
|
|
1508
|
+
# Find the last user message and attach tool calls, todos, content_items, and display_inline items to it
|
|
1419
1509
|
for i in range(len(history) - 1, -1, -1):
|
|
1420
1510
|
if history[i]["role"] == "user":
|
|
1421
1511
|
if state.get("tool_calls"):
|
|
1422
1512
|
history[i]["tool_calls"] = state["tool_calls"]
|
|
1423
1513
|
if state.get("todos"):
|
|
1424
1514
|
history[i]["todos"] = state["todos"]
|
|
1515
|
+
if state.get("content_items"):
|
|
1516
|
+
history[i]["content_items"] = state["content_items"]
|
|
1425
1517
|
if display_inline_items:
|
|
1426
1518
|
history[i]["display_inline_items"] = display_inline_items
|
|
1427
1519
|
saved_display_inline_items = True
|
|
1428
1520
|
break
|
|
1429
1521
|
|
|
1430
1522
|
# Add assistant response to history (with response time)
|
|
1523
|
+
# Note: In the new model, content may be empty if content_items is used
|
|
1431
1524
|
assistant_msg = {
|
|
1432
1525
|
"role": "assistant",
|
|
1433
|
-
"content": state["response"] if state["response"] else f"Error: {state['error']}",
|
|
1526
|
+
"content": "" if state.get("content_items") else (state["response"] if state["response"] else f"Error: {state['error']}"),
|
|
1434
1527
|
"response_time": response_time,
|
|
1435
1528
|
}
|
|
1436
1529
|
|
|
1437
1530
|
history.append(assistant_msg)
|
|
1438
1531
|
|
|
1439
|
-
# Render all history
|
|
1440
|
-
|
|
1441
|
-
final_messages = []
|
|
1442
|
-
for i, msg in enumerate(history):
|
|
1443
|
-
is_new = (i >= len(history) - 1)
|
|
1444
|
-
msg_response_time = msg.get("response_time") if msg["role"] == "assistant" else None
|
|
1445
|
-
final_messages.append(format_message(msg["role"], msg["content"], colors, STYLES, is_new=is_new, response_time=msg_response_time))
|
|
1446
|
-
# Show collapsed tool calls section first
|
|
1447
|
-
if msg.get("tool_calls"):
|
|
1448
|
-
tool_calls_block = format_tool_calls_inline(msg["tool_calls"], colors)
|
|
1449
|
-
if tool_calls_block:
|
|
1450
|
-
final_messages.append(tool_calls_block)
|
|
1451
|
-
# Render todos stored with this message
|
|
1452
|
-
if msg.get("todos"):
|
|
1453
|
-
todos_block = format_todos_inline(msg["todos"], colors)
|
|
1454
|
-
if todos_block:
|
|
1455
|
-
final_messages.append(todos_block)
|
|
1456
|
-
# Extract and show thinking from tool calls
|
|
1457
|
-
if msg.get("tool_calls"):
|
|
1458
|
-
thinking_blocks = extract_thinking_from_tool_calls(msg["tool_calls"], colors)
|
|
1459
|
-
final_messages.extend(thinking_blocks)
|
|
1460
|
-
# Extract and show display_inline results prominently
|
|
1461
|
-
inline_results = extract_display_inline_results(msg["tool_calls"], colors)
|
|
1462
|
-
final_messages.extend(inline_results)
|
|
1463
|
-
# Render display_inline items stored with this message
|
|
1464
|
-
if msg.get("display_inline_items"):
|
|
1465
|
-
for item in msg["display_inline_items"]:
|
|
1466
|
-
rendered = render_display_inline_result(item, colors)
|
|
1467
|
-
final_messages.append(rendered)
|
|
1532
|
+
# Render all history using render_history_messages (which handles content_items)
|
|
1533
|
+
final_messages = render_history_messages(history)
|
|
1468
1534
|
|
|
1469
1535
|
# Render any NEW queued display_inline items only if not already saved to history
|
|
1470
1536
|
# (avoids duplicate rendering)
|
|
@@ -1476,34 +1542,40 @@ def poll_agent_updates(n_intervals, history, pending_message, theme, session_id)
|
|
|
1476
1542
|
# Disable polling, set skip flag to prevent display_initial_messages from re-rendering
|
|
1477
1543
|
return final_messages, history, True, True
|
|
1478
1544
|
else:
|
|
1479
|
-
# Agent still running - show loading with current tool_calls/todos/
|
|
1545
|
+
# Agent still running - show loading with current tool_calls/todos/content_items
|
|
1480
1546
|
messages = render_history_messages(history)
|
|
1481
1547
|
|
|
1482
|
-
#
|
|
1548
|
+
# Show collapsed tool calls section
|
|
1483
1549
|
if state.get("tool_calls"):
|
|
1484
|
-
# Show collapsed tool calls section first
|
|
1485
1550
|
tool_calls_block = format_tool_calls_inline(state["tool_calls"], colors)
|
|
1486
1551
|
if tool_calls_block:
|
|
1487
1552
|
messages.append(tool_calls_block)
|
|
1488
1553
|
|
|
1489
|
-
#
|
|
1554
|
+
# Render todos
|
|
1490
1555
|
if state["todos"]:
|
|
1491
1556
|
todos_block = format_todos_inline(state["todos"], colors)
|
|
1492
1557
|
if todos_block:
|
|
1493
1558
|
messages.append(todos_block)
|
|
1494
1559
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
messages.extend(
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1560
|
+
# Render ordered content items (thinking + text + display_inline in order)
|
|
1561
|
+
if state.get("content_items"):
|
|
1562
|
+
content_blocks = render_ordered_content_items(state["content_items"], colors, STYLES)
|
|
1563
|
+
messages.extend(content_blocks)
|
|
1564
|
+
else:
|
|
1565
|
+
# Fallback: extract thinking from tool calls
|
|
1566
|
+
if state.get("tool_calls"):
|
|
1567
|
+
thinking_blocks = extract_thinking_from_tool_calls(state["tool_calls"], colors)
|
|
1568
|
+
messages.extend(thinking_blocks)
|
|
1569
|
+
|
|
1570
|
+
# Extract and show display_inline results from tool calls (old format)
|
|
1571
|
+
if state.get("tool_calls"):
|
|
1572
|
+
inline_results = extract_display_inline_results(state["tool_calls"], colors)
|
|
1573
|
+
messages.extend(inline_results)
|
|
1574
|
+
|
|
1575
|
+
# Render any queued display_inline items (old format)
|
|
1576
|
+
for item in display_inline_items:
|
|
1577
|
+
rendered = render_display_inline_result(item, colors)
|
|
1578
|
+
messages.append(rendered)
|
|
1507
1579
|
|
|
1508
1580
|
# Add loading indicator
|
|
1509
1581
|
messages.append(format_loading(colors))
|
|
@@ -1550,36 +1622,55 @@ def handle_stop_button(n_clicks, history, theme, session_id):
|
|
|
1550
1622
|
request_agent_stop(session_id)
|
|
1551
1623
|
|
|
1552
1624
|
# Render current messages with a stopping indicator
|
|
1553
|
-
|
|
1554
|
-
def render_history_messages(history):
|
|
1625
|
+
def render_history_messages_local(history):
|
|
1555
1626
|
messages = []
|
|
1556
1627
|
for i, msg in enumerate(history):
|
|
1557
1628
|
msg_response_time = msg.get("response_time") if msg["role"] == "assistant" else None
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
#
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1629
|
+
|
|
1630
|
+
# For user messages, render normally
|
|
1631
|
+
if msg["role"] == "user":
|
|
1632
|
+
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, is_new=False, response_time=msg_response_time))
|
|
1633
|
+
|
|
1634
|
+
# Show collapsed tool calls section
|
|
1635
|
+
if msg.get("tool_calls"):
|
|
1636
|
+
tool_calls_block = format_tool_calls_inline(msg["tool_calls"], colors)
|
|
1637
|
+
if tool_calls_block:
|
|
1638
|
+
messages.append(tool_calls_block)
|
|
1639
|
+
|
|
1640
|
+
# Render todos
|
|
1641
|
+
if msg.get("todos"):
|
|
1642
|
+
todos_block = format_todos_inline(msg["todos"], colors)
|
|
1643
|
+
if todos_block:
|
|
1644
|
+
messages.append(todos_block)
|
|
1645
|
+
|
|
1646
|
+
# Render ordered content items (thinking + text + display_inline in order)
|
|
1647
|
+
if msg.get("content_items"):
|
|
1648
|
+
content_blocks = render_ordered_content_items(msg["content_items"], colors, STYLES)
|
|
1649
|
+
messages.extend(content_blocks)
|
|
1650
|
+
else:
|
|
1651
|
+
# Fallback: extract thinking from tool calls
|
|
1652
|
+
if msg.get("tool_calls"):
|
|
1653
|
+
thinking_blocks = extract_thinking_from_tool_calls(msg["tool_calls"], colors)
|
|
1654
|
+
messages.extend(thinking_blocks)
|
|
1655
|
+
|
|
1656
|
+
# Extract and show display_inline results from tool calls (old format)
|
|
1657
|
+
if msg.get("tool_calls"):
|
|
1658
|
+
inline_results = extract_display_inline_results(msg["tool_calls"], colors)
|
|
1659
|
+
messages.extend(inline_results)
|
|
1660
|
+
|
|
1661
|
+
# Render display_inline items stored with this message (old format)
|
|
1662
|
+
if msg.get("display_inline_items"):
|
|
1663
|
+
for item in msg["display_inline_items"]:
|
|
1664
|
+
rendered = render_display_inline_result(item, colors)
|
|
1665
|
+
messages.append(rendered)
|
|
1666
|
+
else:
|
|
1667
|
+
# For assistant messages, check if content_items was used
|
|
1668
|
+
if msg["content"]:
|
|
1669
|
+
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, is_new=False, response_time=msg_response_time))
|
|
1670
|
+
|
|
1580
1671
|
return messages
|
|
1581
1672
|
|
|
1582
|
-
messages =
|
|
1673
|
+
messages = render_history_messages_local(history)
|
|
1583
1674
|
|
|
1584
1675
|
# Add stopping message
|
|
1585
1676
|
messages.append(html.Div([
|
|
@@ -1652,33 +1743,50 @@ def handle_interrupt_response(approve_clicks, reject_clicks, edit_clicks, input_
|
|
|
1652
1743
|
resume_agent_from_interrupt(decision, action, session_id=session_id)
|
|
1653
1744
|
|
|
1654
1745
|
# Show loading state while agent resumes
|
|
1655
|
-
# Order: tool calls -> todos -> thinking -> display inline items
|
|
1656
1746
|
messages = []
|
|
1657
1747
|
for msg in history:
|
|
1658
1748
|
msg_response_time = msg.get("response_time") if msg["role"] == "assistant" else None
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1749
|
+
|
|
1750
|
+
# For user messages, render normally
|
|
1751
|
+
if msg["role"] == "user":
|
|
1752
|
+
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, response_time=msg_response_time))
|
|
1753
|
+
|
|
1754
|
+
# Show collapsed tool calls section
|
|
1755
|
+
if msg.get("tool_calls"):
|
|
1756
|
+
tool_calls_block = format_tool_calls_inline(msg["tool_calls"], colors)
|
|
1757
|
+
if tool_calls_block:
|
|
1758
|
+
messages.append(tool_calls_block)
|
|
1759
|
+
|
|
1760
|
+
# Render todos
|
|
1761
|
+
if msg.get("todos"):
|
|
1762
|
+
todos_block = format_todos_inline(msg["todos"], colors)
|
|
1763
|
+
if todos_block:
|
|
1764
|
+
messages.append(todos_block)
|
|
1765
|
+
|
|
1766
|
+
# Render ordered content items (thinking + text + display_inline in order)
|
|
1767
|
+
if msg.get("content_items"):
|
|
1768
|
+
content_blocks = render_ordered_content_items(msg["content_items"], colors, STYLES)
|
|
1769
|
+
messages.extend(content_blocks)
|
|
1770
|
+
else:
|
|
1771
|
+
# Fallback: extract thinking from tool calls
|
|
1772
|
+
if msg.get("tool_calls"):
|
|
1773
|
+
thinking_blocks = extract_thinking_from_tool_calls(msg["tool_calls"], colors)
|
|
1774
|
+
messages.extend(thinking_blocks)
|
|
1775
|
+
|
|
1776
|
+
# Extract and show display_inline results from tool calls (old format)
|
|
1777
|
+
if msg.get("tool_calls"):
|
|
1778
|
+
inline_results = extract_display_inline_results(msg["tool_calls"], colors)
|
|
1779
|
+
messages.extend(inline_results)
|
|
1780
|
+
|
|
1781
|
+
# Render display_inline items stored with this message (old format)
|
|
1782
|
+
if msg.get("display_inline_items"):
|
|
1783
|
+
for item in msg["display_inline_items"]:
|
|
1784
|
+
rendered = render_display_inline_result(item, colors)
|
|
1785
|
+
messages.append(rendered)
|
|
1786
|
+
else:
|
|
1787
|
+
# For assistant messages, check if content_items was used
|
|
1788
|
+
if msg["content"]:
|
|
1789
|
+
messages.append(format_message(msg["role"], msg["content"], colors, STYLES, response_time=msg_response_time))
|
|
1682
1790
|
|
|
1683
1791
|
messages.append(format_loading(colors))
|
|
1684
1792
|
|
|
@@ -2805,6 +2913,70 @@ def toggle_view(view_value):
|
|
|
2805
2913
|
)
|
|
2806
2914
|
|
|
2807
2915
|
|
|
2916
|
+
# Sidebar collapse/expand toggle
|
|
2917
|
+
@app.callback(
|
|
2918
|
+
[Output("sidebar-panel", "style"),
|
|
2919
|
+
Output("sidebar-expand-btn", "style"),
|
|
2920
|
+
Output("resize-handle", "style"),
|
|
2921
|
+
Output("sidebar-collapsed", "data")],
|
|
2922
|
+
[Input("collapse-sidebar-btn", "n_clicks"),
|
|
2923
|
+
Input("expand-sidebar-btn", "n_clicks")],
|
|
2924
|
+
[State("sidebar-collapsed", "data")],
|
|
2925
|
+
prevent_initial_call=True
|
|
2926
|
+
)
|
|
2927
|
+
def toggle_sidebar_collapse(collapse_clicks, expand_clicks, is_collapsed):
|
|
2928
|
+
"""Toggle sidebar between collapsed and expanded states."""
|
|
2929
|
+
ctx = callback_context
|
|
2930
|
+
if not ctx.triggered:
|
|
2931
|
+
raise PreventUpdate
|
|
2932
|
+
|
|
2933
|
+
triggered_id = ctx.triggered[0]["prop_id"].split(".")[0]
|
|
2934
|
+
|
|
2935
|
+
# Determine new state based on which button was clicked
|
|
2936
|
+
if triggered_id == "collapse-sidebar-btn":
|
|
2937
|
+
new_collapsed = True
|
|
2938
|
+
elif triggered_id == "expand-sidebar-btn":
|
|
2939
|
+
new_collapsed = False
|
|
2940
|
+
else:
|
|
2941
|
+
raise PreventUpdate
|
|
2942
|
+
|
|
2943
|
+
if new_collapsed:
|
|
2944
|
+
# Collapsed state - hide sidebar, show expand button
|
|
2945
|
+
return (
|
|
2946
|
+
{"display": "none"}, # Hide sidebar panel
|
|
2947
|
+
{ # Show expand button
|
|
2948
|
+
"display": "flex",
|
|
2949
|
+
"alignItems": "flex-start",
|
|
2950
|
+
"paddingTop": "10px",
|
|
2951
|
+
"borderLeft": "1px solid var(--mantine-color-default-border)",
|
|
2952
|
+
"background": "var(--mantine-color-body)",
|
|
2953
|
+
},
|
|
2954
|
+
{"display": "none"}, # Hide resize handle
|
|
2955
|
+
True, # Store collapsed state
|
|
2956
|
+
)
|
|
2957
|
+
else:
|
|
2958
|
+
# Expanded state - show sidebar, hide expand button
|
|
2959
|
+
return (
|
|
2960
|
+
{ # Show sidebar panel
|
|
2961
|
+
"flex": "1",
|
|
2962
|
+
"minWidth": "0",
|
|
2963
|
+
"minHeight": "0",
|
|
2964
|
+
"display": "flex",
|
|
2965
|
+
"flexDirection": "column",
|
|
2966
|
+
"background": "var(--mantine-color-body)",
|
|
2967
|
+
"borderLeft": "1px solid var(--mantine-color-default-border)",
|
|
2968
|
+
},
|
|
2969
|
+
{"display": "none"}, # Hide expand button
|
|
2970
|
+
{ # Show resize handle
|
|
2971
|
+
"width": "3px",
|
|
2972
|
+
"cursor": "col-resize",
|
|
2973
|
+
"background": "transparent",
|
|
2974
|
+
"flexShrink": "0",
|
|
2975
|
+
},
|
|
2976
|
+
False, # Store expanded state
|
|
2977
|
+
)
|
|
2978
|
+
|
|
2979
|
+
|
|
2808
2980
|
# Canvas content update
|
|
2809
2981
|
@app.callback(
|
|
2810
2982
|
Output("canvas-content", "children"),
|
|
@@ -3284,6 +3456,206 @@ def add_display_inline_to_canvas(n_clicks_list, data_list, theme, collapsed_ids,
|
|
|
3284
3456
|
return render_canvas_items(canvas_items, colors, collapsed_ids), "canvas"
|
|
3285
3457
|
|
|
3286
3458
|
|
|
3459
|
+
# =============================================================================
|
|
3460
|
+
# DOWNLOAD DISPLAY INLINE CALLBACK - Download display inline content
|
|
3461
|
+
# =============================================================================
|
|
3462
|
+
|
|
3463
|
+
@app.callback(
|
|
3464
|
+
Output("file-download", "data", allow_duplicate=True),
|
|
3465
|
+
Input({"type": "download-display-btn", "index": ALL}, "n_clicks"),
|
|
3466
|
+
State({"type": "display-inline-data", "index": ALL}, "data"),
|
|
3467
|
+
prevent_initial_call=True
|
|
3468
|
+
)
|
|
3469
|
+
def download_display_inline_content(n_clicks_list, data_list):
|
|
3470
|
+
"""Download a display_inline item when the download button is clicked."""
|
|
3471
|
+
import base64
|
|
3472
|
+
|
|
3473
|
+
ctx = callback_context
|
|
3474
|
+
# Check if any button was actually clicked
|
|
3475
|
+
if not n_clicks_list or not any(n_clicks_list):
|
|
3476
|
+
raise PreventUpdate
|
|
3477
|
+
|
|
3478
|
+
if not ctx.triggered:
|
|
3479
|
+
raise PreventUpdate
|
|
3480
|
+
|
|
3481
|
+
triggered = ctx.triggered[0]
|
|
3482
|
+
triggered_id = triggered["prop_id"]
|
|
3483
|
+
|
|
3484
|
+
# Parse the pattern-matching ID to get the index
|
|
3485
|
+
try:
|
|
3486
|
+
# Format: {"type":"download-display-btn","index":"abc123"}.n_clicks
|
|
3487
|
+
id_part = triggered_id.rsplit(".", 1)[0]
|
|
3488
|
+
id_dict = json.loads(id_part)
|
|
3489
|
+
clicked_index = id_dict.get("index")
|
|
3490
|
+
except (json.JSONDecodeError, KeyError, AttributeError):
|
|
3491
|
+
raise PreventUpdate
|
|
3492
|
+
|
|
3493
|
+
if not clicked_index:
|
|
3494
|
+
raise PreventUpdate
|
|
3495
|
+
|
|
3496
|
+
# Find the corresponding data
|
|
3497
|
+
display_data = None
|
|
3498
|
+
for data in data_list:
|
|
3499
|
+
if data and data.get("_item_id") == clicked_index:
|
|
3500
|
+
display_data = data
|
|
3501
|
+
break
|
|
3502
|
+
|
|
3503
|
+
if not display_data:
|
|
3504
|
+
raise PreventUpdate
|
|
3505
|
+
|
|
3506
|
+
display_type = display_data.get("display_type", "text")
|
|
3507
|
+
data = display_data.get("data")
|
|
3508
|
+
title = display_data.get("title", "download")
|
|
3509
|
+
filename = display_data.get("filename")
|
|
3510
|
+
|
|
3511
|
+
# Generate appropriate filename and content based on type
|
|
3512
|
+
if display_type == "image":
|
|
3513
|
+
mime_type = display_data.get("mime_type", "image/png")
|
|
3514
|
+
ext = mime_type.split("/")[-1]
|
|
3515
|
+
if ext == "jpeg":
|
|
3516
|
+
ext = "jpg"
|
|
3517
|
+
download_filename = filename or f"{title}.{ext}"
|
|
3518
|
+
return dict(
|
|
3519
|
+
content=data,
|
|
3520
|
+
filename=download_filename,
|
|
3521
|
+
base64=True
|
|
3522
|
+
)
|
|
3523
|
+
|
|
3524
|
+
elif display_type == "pdf":
|
|
3525
|
+
download_filename = filename or f"{title}.pdf"
|
|
3526
|
+
return dict(
|
|
3527
|
+
content=data,
|
|
3528
|
+
filename=download_filename,
|
|
3529
|
+
base64=True
|
|
3530
|
+
)
|
|
3531
|
+
|
|
3532
|
+
elif display_type == "html":
|
|
3533
|
+
download_filename = filename or f"{title}.html"
|
|
3534
|
+
return dict(
|
|
3535
|
+
content=data,
|
|
3536
|
+
filename=download_filename
|
|
3537
|
+
)
|
|
3538
|
+
|
|
3539
|
+
elif display_type == "dataframe":
|
|
3540
|
+
# Download as CSV
|
|
3541
|
+
csv_data = display_data.get("csv", {})
|
|
3542
|
+
columns = csv_data.get("columns", [])
|
|
3543
|
+
rows = csv_data.get("data", [])
|
|
3544
|
+
|
|
3545
|
+
# Build CSV content
|
|
3546
|
+
import csv
|
|
3547
|
+
import io
|
|
3548
|
+
output = io.StringIO()
|
|
3549
|
+
writer = csv.writer(output)
|
|
3550
|
+
writer.writerow(columns)
|
|
3551
|
+
for row in rows:
|
|
3552
|
+
writer.writerow([row.get(col, "") for col in columns])
|
|
3553
|
+
csv_content = output.getvalue()
|
|
3554
|
+
|
|
3555
|
+
download_filename = filename or f"{title}.csv"
|
|
3556
|
+
return dict(
|
|
3557
|
+
content=csv_content,
|
|
3558
|
+
filename=download_filename
|
|
3559
|
+
)
|
|
3560
|
+
|
|
3561
|
+
elif display_type == "json":
|
|
3562
|
+
json_content = json.dumps(data, indent=2) if isinstance(data, (dict, list)) else str(data)
|
|
3563
|
+
download_filename = filename or f"{title}.json"
|
|
3564
|
+
return dict(
|
|
3565
|
+
content=json_content,
|
|
3566
|
+
filename=download_filename
|
|
3567
|
+
)
|
|
3568
|
+
|
|
3569
|
+
elif display_type == "plotly":
|
|
3570
|
+
# Download Plotly chart as JSON
|
|
3571
|
+
json_content = json.dumps(data, indent=2) if isinstance(data, dict) else str(data)
|
|
3572
|
+
download_filename = filename or f"{title}.json"
|
|
3573
|
+
return dict(
|
|
3574
|
+
content=json_content,
|
|
3575
|
+
filename=download_filename
|
|
3576
|
+
)
|
|
3577
|
+
|
|
3578
|
+
else:
|
|
3579
|
+
# Text or unknown - download as .txt
|
|
3580
|
+
text_content = str(data) if data else ""
|
|
3581
|
+
download_filename = filename or f"{title}.txt"
|
|
3582
|
+
return dict(
|
|
3583
|
+
content=text_content,
|
|
3584
|
+
filename=download_filename
|
|
3585
|
+
)
|
|
3586
|
+
|
|
3587
|
+
|
|
3588
|
+
# =============================================================================
|
|
3589
|
+
# FULLSCREEN PREVIEW CALLBACK - Open HTML/PDF in fullscreen modal
|
|
3590
|
+
# =============================================================================
|
|
3591
|
+
|
|
3592
|
+
@app.callback(
|
|
3593
|
+
[Output("fullscreen-preview-modal", "opened"),
|
|
3594
|
+
Output("fullscreen-preview-modal", "title"),
|
|
3595
|
+
Output("fullscreen-preview-content", "children")],
|
|
3596
|
+
Input({"type": "fullscreen-btn", "index": ALL}, "n_clicks"),
|
|
3597
|
+
State({"type": "fullscreen-data", "index": ALL}, "data"),
|
|
3598
|
+
prevent_initial_call=True
|
|
3599
|
+
)
|
|
3600
|
+
def open_fullscreen_preview(n_clicks_list, data_list):
|
|
3601
|
+
"""Open fullscreen modal for HTML/PDF preview."""
|
|
3602
|
+
ctx = callback_context
|
|
3603
|
+
if not n_clicks_list or not any(n_clicks_list):
|
|
3604
|
+
raise PreventUpdate
|
|
3605
|
+
|
|
3606
|
+
# Find which button was clicked
|
|
3607
|
+
triggered = ctx.triggered[0]
|
|
3608
|
+
triggered_id = triggered["prop_id"]
|
|
3609
|
+
|
|
3610
|
+
try:
|
|
3611
|
+
id_part = triggered_id.rsplit(".", 1)[0]
|
|
3612
|
+
id_dict = json.loads(id_part)
|
|
3613
|
+
clicked_index = id_dict.get("index")
|
|
3614
|
+
except (json.JSONDecodeError, KeyError, AttributeError):
|
|
3615
|
+
raise PreventUpdate
|
|
3616
|
+
|
|
3617
|
+
# Find the corresponding data
|
|
3618
|
+
fullscreen_data = None
|
|
3619
|
+
for i, data in enumerate(data_list):
|
|
3620
|
+
if data and n_clicks_list[i]:
|
|
3621
|
+
# Match by checking trigger
|
|
3622
|
+
btn_ids = ctx.inputs_list[0]
|
|
3623
|
+
if i < len(btn_ids) and btn_ids[i].get("id", {}).get("index") == clicked_index:
|
|
3624
|
+
fullscreen_data = data
|
|
3625
|
+
break
|
|
3626
|
+
|
|
3627
|
+
if not fullscreen_data:
|
|
3628
|
+
raise PreventUpdate
|
|
3629
|
+
|
|
3630
|
+
content_type = fullscreen_data.get("type")
|
|
3631
|
+
content = fullscreen_data.get("content")
|
|
3632
|
+
title = fullscreen_data.get("title", "Preview")
|
|
3633
|
+
|
|
3634
|
+
if content_type == "html":
|
|
3635
|
+
preview_content = html.Iframe(
|
|
3636
|
+
srcDoc=content,
|
|
3637
|
+
style={
|
|
3638
|
+
"width": "100%",
|
|
3639
|
+
"height": "100%",
|
|
3640
|
+
"border": "none",
|
|
3641
|
+
"backgroundColor": "white",
|
|
3642
|
+
}
|
|
3643
|
+
)
|
|
3644
|
+
elif content_type == "pdf":
|
|
3645
|
+
preview_content = html.Iframe(
|
|
3646
|
+
src=content,
|
|
3647
|
+
style={
|
|
3648
|
+
"width": "100%",
|
|
3649
|
+
"height": "100%",
|
|
3650
|
+
"border": "none",
|
|
3651
|
+
}
|
|
3652
|
+
)
|
|
3653
|
+
else:
|
|
3654
|
+
preview_content = html.Div("Unsupported content type")
|
|
3655
|
+
|
|
3656
|
+
return True, title, preview_content
|
|
3657
|
+
|
|
3658
|
+
|
|
3287
3659
|
# =============================================================================
|
|
3288
3660
|
# THEME TOGGLE CALLBACK - Using DMC 2.4 forceColorScheme
|
|
3289
3661
|
# =============================================================================
|