android-midscene-automation 0.2.4 → 1.1.1
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.
- package/CHANGELOG.md +10 -0
- package/README.md +159 -29
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +4 -2
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +12 -0
- package/src/App.vue +10 -5
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +340 -121
- package/src/types.ts +1 -1
|
@@ -0,0 +1,3787 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<!--
|
|
3
|
+
Copyright 2026 Google LLC
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
16
|
+
-->
|
|
17
|
+
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="UTF-8">
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
22
|
+
<title>Artemis Debug UI</title>
|
|
23
|
+
<link rel="icon" type="image/png" sizes="192x192" href="/logo.png?v=2">
|
|
24
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/logo.png?v=2">
|
|
25
|
+
<link rel="icon" type="image/svg+xml" href="/logo.svg?v=2">
|
|
26
|
+
<link rel="shortcut icon" href="/favicon.ico?v=2">
|
|
27
|
+
<link rel="apple-touch-icon" href="/logo.png?v=2">
|
|
28
|
+
<style>
|
|
29
|
+
pre.light-code {
|
|
30
|
+
background: transparent !important;
|
|
31
|
+
padding: 0 !important;
|
|
32
|
+
color: inherit !important;
|
|
33
|
+
box-shadow: none !important;
|
|
34
|
+
border-radius: 0 !important;
|
|
35
|
+
}
|
|
36
|
+
body {
|
|
37
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
38
|
+
margin: 0;
|
|
39
|
+
display: flex;
|
|
40
|
+
height: 100vh;
|
|
41
|
+
background-color: #f5f7fb;
|
|
42
|
+
color: #333;
|
|
43
|
+
}
|
|
44
|
+
#sidebar {
|
|
45
|
+
width: 350px;
|
|
46
|
+
background-color: #fff;
|
|
47
|
+
border-right: 1px solid #e0e6ed;
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
padding: 20px;
|
|
51
|
+
box-shadow: 2px 0 5px rgba(0,0,0,0.05);
|
|
52
|
+
}
|
|
53
|
+
#content {
|
|
54
|
+
flex: 1;
|
|
55
|
+
padding: 30px;
|
|
56
|
+
overflow-y: auto;
|
|
57
|
+
background-color: #f9fafc;
|
|
58
|
+
}
|
|
59
|
+
h1, h2, h3 {
|
|
60
|
+
margin-top: 0;
|
|
61
|
+
color: #1a202c;
|
|
62
|
+
}
|
|
63
|
+
.session-item {
|
|
64
|
+
padding: 12px;
|
|
65
|
+
border: 1px solid #e2e8f0;
|
|
66
|
+
margin-bottom: 10px;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
border-radius: 6px;
|
|
69
|
+
transition: background-color 0.2s, border-color 0.2s;
|
|
70
|
+
background-color: #fff;
|
|
71
|
+
}
|
|
72
|
+
.session-item:hover {
|
|
73
|
+
background-color: #edf2f7;
|
|
74
|
+
border-color: #cbd5e0;
|
|
75
|
+
}
|
|
76
|
+
.session-item.active {
|
|
77
|
+
background-color: #3182ce;
|
|
78
|
+
color: white;
|
|
79
|
+
border-color: #3182ce;
|
|
80
|
+
}
|
|
81
|
+
.session-item.active h3 {
|
|
82
|
+
color: white;
|
|
83
|
+
}
|
|
84
|
+
.tree-node {
|
|
85
|
+
margin-left: 20px;
|
|
86
|
+
border-left: 2px solid #cbd5e0;
|
|
87
|
+
padding-left: 15px;
|
|
88
|
+
position: relative;
|
|
89
|
+
}
|
|
90
|
+
.tree-node::before {
|
|
91
|
+
content: '';
|
|
92
|
+
position: absolute;
|
|
93
|
+
left: -2px;
|
|
94
|
+
top: 12px;
|
|
95
|
+
width: 10px;
|
|
96
|
+
border-top: 2px solid #cbd5e0;
|
|
97
|
+
}
|
|
98
|
+
.node-header {
|
|
99
|
+
padding: 8px 12px;
|
|
100
|
+
background-color: #fff;
|
|
101
|
+
border: 1px solid #e2e8f0;
|
|
102
|
+
margin-bottom: 6px;
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
border-radius: 4px;
|
|
105
|
+
display: flex;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
align-items: center;
|
|
108
|
+
transition: background-color 0.2s;
|
|
109
|
+
}
|
|
110
|
+
.node-header:hover {
|
|
111
|
+
background-color: #edf2f7;
|
|
112
|
+
}
|
|
113
|
+
.node-header.active {
|
|
114
|
+
border-color: #3182ce;
|
|
115
|
+
background-color: #ebf8ff;
|
|
116
|
+
}
|
|
117
|
+
.status-success { color: #38a169; font-weight: bold; }
|
|
118
|
+
.status-failed { color: #e53e3e; font-weight: bold; }
|
|
119
|
+
.status-running { color: #d69e2e; font-weight: bold; }
|
|
120
|
+
|
|
121
|
+
.type-agent { background-color: #e6fffa; border-color: #b2f5ea; color: #234e52; }
|
|
122
|
+
.type-tool { background-color: #ebf8ff; border-color: #bee3f8; color: #2c5282; }
|
|
123
|
+
.type-span { background-color: #fffaf0; border-color: #feebc8; color: #744210; }
|
|
124
|
+
.type-llm_call { background-color: #f7fafc; border-color: #edf2f7; color: #4a5568; }
|
|
125
|
+
.type-node { background-color: #faf5ff; border-color: #e9d8fd; color: #44337a; }
|
|
126
|
+
.type-action { background-color: #fff5f5; border-color: #fed7d7; color: #7b341e; }
|
|
127
|
+
|
|
128
|
+
.pill {
|
|
129
|
+
font-size: 0.75rem;
|
|
130
|
+
padding: 2px 6px;
|
|
131
|
+
border-radius: 9999px;
|
|
132
|
+
text-transform: uppercase;
|
|
133
|
+
font-weight: bold;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pre {
|
|
137
|
+
background-color: #2d3748;
|
|
138
|
+
color: #fff;
|
|
139
|
+
padding: 15px;
|
|
140
|
+
border-radius: 6px;
|
|
141
|
+
overflow-x: auto;
|
|
142
|
+
font-family: 'Courier New', Courier, monospace;
|
|
143
|
+
font-size: 0.9rem;
|
|
144
|
+
}
|
|
145
|
+
.detail-section {
|
|
146
|
+
background-color: #fff;
|
|
147
|
+
padding: 20px;
|
|
148
|
+
border-radius: 8px;
|
|
149
|
+
border: 1px solid #e0e6ed;
|
|
150
|
+
margin-bottom: 20px;
|
|
151
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
152
|
+
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
|
153
|
+
}
|
|
154
|
+
.streaming-active {
|
|
155
|
+
border-color: #4299e1;
|
|
156
|
+
box-shadow: 0 0 12px rgba(66, 153, 225, 0.4);
|
|
157
|
+
}
|
|
158
|
+
img, video {
|
|
159
|
+
max-width: 100%;
|
|
160
|
+
height: auto;
|
|
161
|
+
border-radius: 6px;
|
|
162
|
+
border: 1px solid #cbd5e0;
|
|
163
|
+
margin-top: 10px;
|
|
164
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
|
165
|
+
}
|
|
166
|
+
.media-container {
|
|
167
|
+
margin-top: 15px;
|
|
168
|
+
background-color: #f7fafc;
|
|
169
|
+
padding: 15px;
|
|
170
|
+
border-radius: 6px;
|
|
171
|
+
text-align: center;
|
|
172
|
+
}
|
|
173
|
+
.media-label {
|
|
174
|
+
font-size: 0.85rem;
|
|
175
|
+
color: #718096;
|
|
176
|
+
margin-bottom: 5px;
|
|
177
|
+
display: block;
|
|
178
|
+
}
|
|
179
|
+
#session-list {
|
|
180
|
+
overflow-y: auto;
|
|
181
|
+
flex: 1;
|
|
182
|
+
}
|
|
183
|
+
.refresh-btn {
|
|
184
|
+
padding: 8px 12px;
|
|
185
|
+
background-color: #3182ce;
|
|
186
|
+
color: white;
|
|
187
|
+
border: none;
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
font-weight: bold;
|
|
191
|
+
margin-bottom: 15px;
|
|
192
|
+
transition: background-color 0.2s;
|
|
193
|
+
}
|
|
194
|
+
.refresh-btn:hover {
|
|
195
|
+
background-color: #2b6cb0;
|
|
196
|
+
}
|
|
197
|
+
.node-title {
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
gap: 8px;
|
|
201
|
+
}
|
|
202
|
+
.horizontal-dashboard {
|
|
203
|
+
display: flex;
|
|
204
|
+
flex-wrap: nowrap;
|
|
205
|
+
overflow-x: auto;
|
|
206
|
+
gap: 16px;
|
|
207
|
+
padding-bottom: 12px;
|
|
208
|
+
margin-top: 10px;
|
|
209
|
+
}
|
|
210
|
+
.horizontal-dashboard > .tree-node {
|
|
211
|
+
flex: 0 0 auto;
|
|
212
|
+
width: 420px;
|
|
213
|
+
max-width: 85vw;
|
|
214
|
+
border-left: none;
|
|
215
|
+
margin-left: 0;
|
|
216
|
+
padding-left: 0;
|
|
217
|
+
background-color: #fff;
|
|
218
|
+
border: 1px solid #e2e8f0;
|
|
219
|
+
border-radius: 8px;
|
|
220
|
+
padding: 12px;
|
|
221
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.04);
|
|
222
|
+
}
|
|
223
|
+
.horizontal-dashboard > .tree-node::before {
|
|
224
|
+
display: none;
|
|
225
|
+
}
|
|
226
|
+
@keyframes pulse-animation {
|
|
227
|
+
0% {
|
|
228
|
+
transform: scale(0.5);
|
|
229
|
+
opacity: 1;
|
|
230
|
+
}
|
|
231
|
+
100% {
|
|
232
|
+
transform: scale(1.5);
|
|
233
|
+
opacity: 0;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
</style>
|
|
237
|
+
</head>
|
|
238
|
+
<body>
|
|
239
|
+
<div id="sidebar">
|
|
240
|
+
<h2>Run Task</h2>
|
|
241
|
+
<div style="margin-bottom: 20px;">
|
|
242
|
+
<textarea id="task-goal" style="width: 100%; height: 60px; border-radius: 4px; border: 1px solid #e2e8f0; padding: 8px; box-sizing: border-box; font-family: inherit;" placeholder="Enter task goal..."></textarea>
|
|
243
|
+
<div style="margin-top: 8px; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; font-size: 0.85rem; color: #475569;">
|
|
244
|
+
<label for="task-profile-select" style="font-weight: 600;">Agent Architecture:</label>
|
|
245
|
+
<select id="task-profile-select" style="padding: 4px 8px; border-radius: 4px; border: 1px solid #cbd5e0; font-size: 0.85rem; background-color: #f8fafc; color: #334155; outline: none; cursor: pointer; flex: 1; margin-left: 10px;">
|
|
246
|
+
<option value="flash">⚡ Flash (Reactive Fast Loop)</option>
|
|
247
|
+
<option value="pro">🚀 Pro (Multi-Agent Cognitive Graph)</option>
|
|
248
|
+
</select>
|
|
249
|
+
</div>
|
|
250
|
+
<div style="display: flex; gap: 10px; margin-top: 5px;">
|
|
251
|
+
<button class="refresh-btn" style="flex: 1; margin-bottom: 0;" onclick="runTask()">Run Task</button>
|
|
252
|
+
<button class="refresh-btn" style="flex: 1; margin-bottom: 0; background-color: #e53e3e;" onclick="stopTask()">Stop Task</button>
|
|
253
|
+
</div>
|
|
254
|
+
<div style="display: flex; gap: 10px; margin-top: 5px;">
|
|
255
|
+
<button class="refresh-btn" id="resume-btn" style="flex: 1; margin-bottom: 0; background-color: #38a169; display: none;" onclick="resumeTask()">Resume Task</button>
|
|
256
|
+
</div>
|
|
257
|
+
<div id="run-status" style="font-size: 0.85rem; margin-top: 5px;"></div>
|
|
258
|
+
</div>
|
|
259
|
+
<h2>Sessions</h2>
|
|
260
|
+
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
|
261
|
+
<button class="refresh-btn" style="flex: 1; margin-bottom: 0;" onclick="loadSessions()">Refresh List</button>
|
|
262
|
+
<button class="refresh-btn" style="flex: 1; margin-bottom: 0; background-color: #e53e3e;" onclick="cleanupHistory()">Clean History</button>
|
|
263
|
+
</div>
|
|
264
|
+
<div style="margin-bottom: 15px;">
|
|
265
|
+
<input type="text" id="session-search-input" oninput="filterSessionsList()" placeholder="🔍 Filter by Session ID or Goal..." style="width: 100%; padding: 8px 12px; border-radius: 6px; border: 1px solid #cbd5e0; font-size: 0.85rem; box-sizing: border-box; outline: none; background-color: #fff; color: #1e293b; transition: border-color 0.2s, box-shadow 0.2s;" onfocus="this.style.borderColor='#3182ce'; this.style.boxShadow='0 0 0 2px rgba(49,130,206,0.2)';" onblur="this.style.borderColor='#cbd5e0'; this.style.boxShadow='none';"/>
|
|
266
|
+
</div>
|
|
267
|
+
<div id="session-list">Loading sessions...</div>
|
|
268
|
+
</div>
|
|
269
|
+
<div id="content">
|
|
270
|
+
<div style="display: flex; align-items: flex-start; justify-content: space-between; gap: 15px; margin-bottom: 10px;">
|
|
271
|
+
<div style="display: flex; align-items: flex-start; gap: 15px; flex: 1;">
|
|
272
|
+
<button onclick="toggleSidebar()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; padding: 0; color: #4a5568; margin-top: 5px;" title="Toggle Sidebar">☰</button>
|
|
273
|
+
<div id="session-header" style="flex: 1;">
|
|
274
|
+
<h1 style="margin-top: 0;">Select a session from the sidebar</h1>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<!-- Tab Navigation Bar -->
|
|
281
|
+
<div id="session-tabs-bar" style="display: none; gap: 8px; border-bottom: 2px solid #edf2f7; padding-bottom: 8px; margin-bottom: 15px; align-items: center;">
|
|
282
|
+
<button class="tab-btn active" onclick="switchTab('steps')" style="padding: 8px 16px; border: none; background: none; font-weight: bold; font-size: 0.95rem; cursor: pointer; border-bottom: 2px solid #3182ce; color: #3182ce; transition: color 0.2s, border-color 0.2s; outline: none;">👣 Steps Timeline</button>
|
|
283
|
+
<button class="tab-btn" onclick="switchTab('traces')" style="padding: 8px 16px; border: none; background: none; font-weight: bold; font-size: 0.95rem; cursor: pointer; color: #718096; transition: color 0.2s, border-color 0.2s; outline: none;">🌳 Traces Tree</button>
|
|
284
|
+
<button class="tab-btn" onclick="switchTab('replay')" style="padding: 8px 16px; border: none; background: none; font-weight: bold; font-size: 0.95rem; cursor: pointer; color: #718096; transition: color 0.2s, border-color 0.2s; outline: none;">🔄 Step Replay</button>
|
|
285
|
+
|
|
286
|
+
<div style="margin-left: auto; display: flex; gap: 8px;">
|
|
287
|
+
<button class="refresh-btn" style="margin-bottom: 0; background-color: #38bdf8; color: #0f172a; font-weight: bold;" onclick="openNotesModal()">📚 View All Notes</button>
|
|
288
|
+
<button class="refresh-btn" style="margin-bottom: 0; background-color: #38bdf8; color: #0f172a; font-weight: bold;" onclick="openPlanModal()">📋 View Task Plan</button>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<!-- Steps Timeline Tab Panel -->
|
|
293
|
+
<div id="steps-container" style="display: none;">
|
|
294
|
+
<div style="padding: 20px; color: #718096; text-align: center;">Select a session from the sidebar to view steps.</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<!-- Traces Tree Tab Panel -->
|
|
298
|
+
<div id="traces-container-wrapper" style="display: none;">
|
|
299
|
+
<div style="margin-bottom: 10px; display: flex; align-items: center; gap: 10px;">
|
|
300
|
+
<label style="font-size: 0.9rem; color: #4a5568; cursor: pointer; user-select: none;">
|
|
301
|
+
<input type="checkbox" id="show-logs-check" onchange="loadTree()" style="margin-right: 4px;"/> Show Log Traces
|
|
302
|
+
</label>
|
|
303
|
+
</div>
|
|
304
|
+
<div id="tree-container"></div>
|
|
305
|
+
<div id="details-container"></div>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<!-- Step Replay Tab Panel -->
|
|
309
|
+
<div id="replay-container-wrapper" style="display: none; height: calc(100vh - 180px); overflow-y: auto; padding-right: 10px;">
|
|
310
|
+
<!-- Global Replay Header (Device Selector, etc.) -->
|
|
311
|
+
<!-- Global Replay Header -->
|
|
312
|
+
<div style="background-color: white; border: 1px solid #e2e8f0; border-radius: 8px; padding: 15px 20px; display: flex; align-items: center; justify-content: space-between; gap: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); margin-bottom: 20px; flex-shrink: 0;">
|
|
313
|
+
<div style="display: flex; align-items: center; gap: 10px;">
|
|
314
|
+
<span style="font-size: 1.25rem;">🔄</span>
|
|
315
|
+
<h2 style="margin: 0; font-size: 1.1rem; color: #0f172a; font-weight: 700;">Interactive Step Replay Timeline</h2>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<!-- Horizontal Steps Navigation Bar (Sticky/Pinned) -->
|
|
320
|
+
<div id="replay-steps-nav" style="display: none; gap: 12px; overflow-x: auto; padding: 12px 10px; margin-bottom: 20px; scroll-behavior: smooth; -webkit-overflow-scrolling: touch; border-bottom: 1px solid #e2e8f0; position: sticky; top: 0; background: rgba(255, 255, 255, 0.97); z-index: 50; box-shadow: 0 4px 6px -4px rgba(0,0,0,0.05);">
|
|
321
|
+
<!-- Step nav items will be dynamically populated here -->
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<!-- Steps Replay Vertical Timeline Container -->
|
|
325
|
+
<div id="replay-timeline-container">
|
|
326
|
+
<div style="padding: 40px; color: #718096; text-align: center; font-size: 1.1rem;">Select a session from the sidebar to load the replay timeline.</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<!-- Task Plan Modal -->
|
|
332
|
+
<div id="plan-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(15, 23, 42, 0.65); backdrop-filter: blur(8px); z-index: 9999; justify-content: center; align-items: center;">
|
|
333
|
+
<div style="background-color: #1e293b; color: #f8fafc; width: 700px; max-width: 90%; max-height: 80vh; border-radius: 12px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3); display: flex; flex-direction: column; overflow: hidden; border: 1px solid #334155;">
|
|
334
|
+
<div style="padding: 16px 24px; background-color: #0f172a; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center;">
|
|
335
|
+
<h2 style="margin: 0; color: #38bdf8; font-size: 1.25rem; font-weight: 600;">📋 Active Task Plan</h2>
|
|
336
|
+
<button onclick="closePlanModal()" style="background: none; border: none; color: #94a3b8; font-size: 1.5rem; cursor: pointer; line-height: 1;">×</button>
|
|
337
|
+
</div>
|
|
338
|
+
<div style="padding: 24px; overflow-y: auto; flex: 1;">
|
|
339
|
+
<pre id="plan-modal-content" style="margin: 0; background-color: #0f172a; padding: 16px; border-radius: 8px; color: #e2e8f0; font-family: 'Courier New', Courier, monospace; font-size: 0.9rem; white-space: pre-wrap;">Loading task plan...</pre>
|
|
340
|
+
</div>
|
|
341
|
+
<div style="padding: 12px 24px; background-color: #0f172a; border-top: 1px solid #334155; text-align: right;">
|
|
342
|
+
<button class="refresh-btn" style="margin: 0; background-color: #38bdf8; color: #0f172a; font-weight: bold;" onclick="fetchTaskPlan()">Refresh Plan</button>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<!-- All Notes Modal -->
|
|
348
|
+
<div id="notes-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(15, 23, 42, 0.65); backdrop-filter: blur(8px); z-index: 9999; justify-content: center; align-items: center;">
|
|
349
|
+
<div style="background-color: #1e293b; color: #f8fafc; width: 800px; max-width: 90%; max-height: 80vh; border-radius: 12px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3); display: flex; flex-direction: column; overflow: hidden; border: 1px solid #334155;">
|
|
350
|
+
<div style="padding: 16px 24px; background-color: #0f172a; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center;">
|
|
351
|
+
<h2 style="margin: 0; color: #38bdf8; font-size: 1.25rem; font-weight: 600;">📚 All Notes</h2>
|
|
352
|
+
<button onclick="closeNotesModal()" style="background: none; border: none; color: #94a3b8; font-size: 1.5rem; cursor: pointer; line-height: 1;">×</button>
|
|
353
|
+
</div>
|
|
354
|
+
<div style="padding: 24px; overflow-y: auto; flex: 1;" id="notes-modal-content">
|
|
355
|
+
Loading notes...
|
|
356
|
+
</div>
|
|
357
|
+
<div style="padding: 12px 24px; background-color: #0f172a; border-top: 1px solid #334155; text-align: right;">
|
|
358
|
+
<button class="refresh-btn" style="margin: 0; background-color: #38bdf8; color: #0f172a; font-weight: bold;" onclick="fetchAllNotes()">Refresh Notes</button>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<script>
|
|
364
|
+
function toggleSidebar() {
|
|
365
|
+
const sidebar = document.getElementById('sidebar');
|
|
366
|
+
if (sidebar.style.display === 'none') {
|
|
367
|
+
sidebar.style.display = 'flex';
|
|
368
|
+
} else {
|
|
369
|
+
sidebar.style.display = 'none';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Coordinate visualization helpers
|
|
374
|
+
function createPointMarker(x, y, actionType, overlay, isNormalized, sequenceIndex, totalSequence) {
|
|
375
|
+
let pctX = x / 10;
|
|
376
|
+
let pctY = y / 10;
|
|
377
|
+
|
|
378
|
+
const container = overlay.parentElement;
|
|
379
|
+
const img = container ? container.querySelector('img') : null;
|
|
380
|
+
if (!isNormalized && img && img.naturalWidth && img.naturalHeight) {
|
|
381
|
+
if (x > 1000 || y > 1000 || (x > 100 && y > 100 && img.naturalWidth > 500)) {
|
|
382
|
+
pctX = (x / img.naturalWidth) * 100;
|
|
383
|
+
pctY = (y / img.naturalHeight) * 100;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const marker = document.createElement('div');
|
|
388
|
+
marker.style.position = 'absolute';
|
|
389
|
+
marker.style.left = `${pctX}%`;
|
|
390
|
+
marker.style.top = `${pctY}%`;
|
|
391
|
+
marker.style.transform = 'translate(-50%, -50%)';
|
|
392
|
+
marker.style.zIndex = '10';
|
|
393
|
+
marker.style.pointerEvents = 'none';
|
|
394
|
+
|
|
395
|
+
const dotSize = sequenceIndex !== undefined ? 20 : 14;
|
|
396
|
+
const dot = document.createElement('div');
|
|
397
|
+
dot.style.width = `${dotSize}px`;
|
|
398
|
+
dot.style.height = `${dotSize}px`;
|
|
399
|
+
dot.style.borderRadius = '50%';
|
|
400
|
+
dot.style.backgroundColor = '#ef4444';
|
|
401
|
+
dot.style.border = '2px solid white';
|
|
402
|
+
dot.style.boxShadow = '0 0 6px rgba(0,0,0,0.4)';
|
|
403
|
+
if (sequenceIndex !== undefined) {
|
|
404
|
+
dot.style.display = 'flex';
|
|
405
|
+
dot.style.alignItems = 'center';
|
|
406
|
+
dot.style.justifyContent = 'center';
|
|
407
|
+
dot.style.fontSize = '11px';
|
|
408
|
+
dot.style.fontWeight = 'bold';
|
|
409
|
+
dot.style.color = 'white';
|
|
410
|
+
dot.innerText = String(sequenceIndex);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const pulseSize = dotSize * 2;
|
|
414
|
+
const pulse = document.createElement('div');
|
|
415
|
+
pulse.style.position = 'absolute';
|
|
416
|
+
pulse.style.top = `-${dotSize / 2 + 2}px`;
|
|
417
|
+
pulse.style.left = `-${dotSize / 2 + 2}px`;
|
|
418
|
+
pulse.style.width = `${pulseSize}px`;
|
|
419
|
+
pulse.style.height = `${pulseSize}px`;
|
|
420
|
+
pulse.style.borderRadius = '50%';
|
|
421
|
+
pulse.style.border = '2px solid #ef4444';
|
|
422
|
+
pulse.style.animation = 'pulse-animation 1.5s ease-out 3 forwards';
|
|
423
|
+
pulse.style.pointerEvents = 'none';
|
|
424
|
+
|
|
425
|
+
marker.appendChild(pulse);
|
|
426
|
+
marker.appendChild(dot);
|
|
427
|
+
|
|
428
|
+
const label = document.createElement('div');
|
|
429
|
+
if (sequenceIndex !== undefined) {
|
|
430
|
+
label.innerText = totalSequence && totalSequence > 1 ? `#${sequenceIndex} TAP` : actionType.toUpperCase();
|
|
431
|
+
} else {
|
|
432
|
+
label.innerText = actionType.toUpperCase();
|
|
433
|
+
}
|
|
434
|
+
label.style.position = 'absolute';
|
|
435
|
+
label.style.top = `${dotSize + 2}px`;
|
|
436
|
+
label.style.left = '50%';
|
|
437
|
+
label.style.transform = 'translateX(-50%)';
|
|
438
|
+
label.style.backgroundColor = 'rgba(15, 23, 42, 0.9)';
|
|
439
|
+
label.style.color = 'white';
|
|
440
|
+
label.style.padding = '1.5px 5px';
|
|
441
|
+
label.style.borderRadius = '3px';
|
|
442
|
+
label.style.fontSize = '8px';
|
|
443
|
+
label.style.fontWeight = 'bold';
|
|
444
|
+
label.style.whiteSpace = 'nowrap';
|
|
445
|
+
label.style.boxShadow = '0 1px 3px rgba(0,0,0,0.3)';
|
|
446
|
+
|
|
447
|
+
marker.appendChild(label);
|
|
448
|
+
overlay.appendChild(marker);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function createSequenceConnector(x1, y1, x2, y2, overlay, isNormalized) {
|
|
452
|
+
let pctX1 = x1 / 10;
|
|
453
|
+
let pctY1 = y1 / 10;
|
|
454
|
+
let pctX2 = x2 / 10;
|
|
455
|
+
let pctY2 = y2 / 10;
|
|
456
|
+
|
|
457
|
+
const container = overlay.parentElement;
|
|
458
|
+
const img = container ? container.querySelector('img') : null;
|
|
459
|
+
if (!isNormalized && img && img.naturalWidth && img.naturalHeight) {
|
|
460
|
+
if (x1 > 1000 || y1 > 1000 || x2 > 1000 || y2 > 1000 || (x1 > 100 && y1 > 100 && img.naturalWidth > 500)) {
|
|
461
|
+
pctX1 = (x1 / img.naturalWidth) * 100;
|
|
462
|
+
pctY1 = (y1 / img.naturalHeight) * 100;
|
|
463
|
+
pctX2 = (x2 / img.naturalWidth) * 100;
|
|
464
|
+
pctY2 = (y2 / img.naturalHeight) * 100;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let svg = overlay.querySelector('svg');
|
|
469
|
+
if (!svg) {
|
|
470
|
+
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
471
|
+
svg.setAttribute('width', '100%');
|
|
472
|
+
svg.setAttribute('height', '100%');
|
|
473
|
+
svg.style.position = 'absolute';
|
|
474
|
+
svg.style.top = '0';
|
|
475
|
+
svg.style.left = '0';
|
|
476
|
+
svg.style.pointerEvents = 'none';
|
|
477
|
+
|
|
478
|
+
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
479
|
+
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
|
480
|
+
marker.setAttribute('id', 'seq-arrow');
|
|
481
|
+
marker.setAttribute('viewBox', '0 0 10 10');
|
|
482
|
+
marker.setAttribute('refX', '5');
|
|
483
|
+
marker.setAttribute('refY', '5');
|
|
484
|
+
marker.setAttribute('markerWidth', '6');
|
|
485
|
+
marker.setAttribute('markerHeight', '6');
|
|
486
|
+
marker.setAttribute('orient', 'auto');
|
|
487
|
+
|
|
488
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
489
|
+
path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
|
|
490
|
+
path.setAttribute('fill', '#ef4444');
|
|
491
|
+
|
|
492
|
+
marker.appendChild(path);
|
|
493
|
+
defs.appendChild(marker);
|
|
494
|
+
svg.appendChild(defs);
|
|
495
|
+
overlay.appendChild(svg);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
499
|
+
line.setAttribute('x1', `${pctX1}%`);
|
|
500
|
+
line.setAttribute('y1', `${pctY1}%`);
|
|
501
|
+
line.setAttribute('x2', `${pctX2}%`);
|
|
502
|
+
line.setAttribute('y2', `${pctY2}%`);
|
|
503
|
+
line.setAttribute('stroke', '#ef4444');
|
|
504
|
+
line.setAttribute('stroke-width', '2.5');
|
|
505
|
+
line.setAttribute('marker-end', 'url(#seq-arrow)');
|
|
506
|
+
line.setAttribute('stroke-dasharray', '4,4');
|
|
507
|
+
|
|
508
|
+
svg.appendChild(line);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function createLineMarker(x1, y1, x2, y2, overlay, isNormalized) {
|
|
512
|
+
let pctX1 = x1 / 10;
|
|
513
|
+
let pctY1 = y1 / 10;
|
|
514
|
+
let pctX2 = x2 / 10;
|
|
515
|
+
let pctY2 = y2 / 10;
|
|
516
|
+
|
|
517
|
+
const container = overlay.parentElement;
|
|
518
|
+
const img = container ? container.querySelector('img') : null;
|
|
519
|
+
if (!isNormalized && img && img.naturalWidth && img.naturalHeight) {
|
|
520
|
+
if (x1 > 1000 || y1 > 1000 || x2 > 1000 || y2 > 1000 || (x1 > 100 && y1 > 100 && img.naturalWidth > 500)) {
|
|
521
|
+
pctX1 = (x1 / img.naturalWidth) * 100;
|
|
522
|
+
pctY1 = (y1 / img.naturalHeight) * 100;
|
|
523
|
+
pctX2 = (x2 / img.naturalWidth) * 100;
|
|
524
|
+
pctY2 = (y2 / img.naturalHeight) * 100;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
let svg = overlay.querySelector('svg');
|
|
529
|
+
if (!svg) {
|
|
530
|
+
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
531
|
+
svg.setAttribute('width', '100%');
|
|
532
|
+
svg.setAttribute('height', '100%');
|
|
533
|
+
svg.style.position = 'absolute';
|
|
534
|
+
svg.style.top = '0';
|
|
535
|
+
svg.style.left = '0';
|
|
536
|
+
svg.style.pointerEvents = 'none';
|
|
537
|
+
|
|
538
|
+
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
539
|
+
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
|
540
|
+
marker.setAttribute('id', 'arrow');
|
|
541
|
+
marker.setAttribute('viewBox', '0 0 10 10');
|
|
542
|
+
marker.setAttribute('refX', '5');
|
|
543
|
+
marker.setAttribute('refY', '5');
|
|
544
|
+
marker.setAttribute('markerWidth', '6');
|
|
545
|
+
marker.setAttribute('markerHeight', '6');
|
|
546
|
+
marker.setAttribute('orient', 'auto-start-reverse');
|
|
547
|
+
|
|
548
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
549
|
+
path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
|
|
550
|
+
path.setAttribute('fill', '#ef4444');
|
|
551
|
+
|
|
552
|
+
marker.appendChild(path);
|
|
553
|
+
defs.appendChild(marker);
|
|
554
|
+
svg.appendChild(defs);
|
|
555
|
+
overlay.appendChild(svg);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
559
|
+
line.setAttribute('x1', `${pctX1}%`);
|
|
560
|
+
line.setAttribute('y1', `${pctY1}%`);
|
|
561
|
+
line.setAttribute('x2', `${pctX2}%`);
|
|
562
|
+
line.setAttribute('y2', `${pctY2}%`);
|
|
563
|
+
line.setAttribute('stroke', '#ef4444');
|
|
564
|
+
line.setAttribute('stroke-width', '3');
|
|
565
|
+
line.setAttribute('marker-end', 'url(#arrow)');
|
|
566
|
+
line.setAttribute('stroke-dasharray', '5,5');
|
|
567
|
+
|
|
568
|
+
const startCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
569
|
+
startCircle.setAttribute('cx', `${pctX1}%`);
|
|
570
|
+
startCircle.setAttribute('cy', `${pctY1}%`);
|
|
571
|
+
startCircle.setAttribute('r', '4');
|
|
572
|
+
startCircle.setAttribute('fill', '#ef4444');
|
|
573
|
+
startCircle.setAttribute('stroke', 'white');
|
|
574
|
+
startCircle.setAttribute('stroke-width', '1.5');
|
|
575
|
+
|
|
576
|
+
svg.appendChild(line);
|
|
577
|
+
svg.appendChild(startCircle);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function drawActionCoordinates(actionObj, overlayElement) {
|
|
581
|
+
if (!actionObj || !overlayElement) return;
|
|
582
|
+
overlayElement.innerHTML = '';
|
|
583
|
+
|
|
584
|
+
const actions = Array.isArray(actionObj) ? actionObj : [actionObj];
|
|
585
|
+
|
|
586
|
+
actions.forEach(act => {
|
|
587
|
+
if (!act || typeof act !== 'object') return;
|
|
588
|
+
|
|
589
|
+
const actionName = (act.action || act.name || 'click').toLowerCase();
|
|
590
|
+
const args = (act.payload && act.payload.args) || act.args || {};
|
|
591
|
+
|
|
592
|
+
// Filter out non-touch / system actions that do not operate via touch coordinates
|
|
593
|
+
const nonDrawingActions = [
|
|
594
|
+
'manage_app', 'launch_app', 'open_app', 'stop_app', 'close_app',
|
|
595
|
+
'press_key', 'press_home', 'press_back', 'wait_for_delay', 'wait', 'delay',
|
|
596
|
+
'report_task_status', 'report_failure_analysis'
|
|
597
|
+
];
|
|
598
|
+
if (nonDrawingActions.includes(actionName)) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// 1. Multi-point click sequence
|
|
603
|
+
const isSequenceAction = actionName.includes('sequence') || Boolean(act.sequence || args.sequence || act.normalized_sequence || args.normalized_sequence);
|
|
604
|
+
let rawSeq = act.normalized_sequence || args.normalized_sequence || act.sequence || args.sequence || act.targets || args.targets;
|
|
605
|
+
if (typeof rawSeq === 'string') {
|
|
606
|
+
try { rawSeq = JSON.parse(rawSeq); } catch (e) {}
|
|
607
|
+
}
|
|
608
|
+
if (Array.isArray(rawSeq) && rawSeq.length > 0) {
|
|
609
|
+
const validPoints = [];
|
|
610
|
+
rawSeq.forEach(pt => {
|
|
611
|
+
if (typeof pt === 'string') {
|
|
612
|
+
try { pt = JSON.parse(pt); } catch (e) {}
|
|
613
|
+
}
|
|
614
|
+
if (Array.isArray(pt) && pt.length >= 2 && typeof pt[0] === 'number' && typeof pt[1] === 'number') {
|
|
615
|
+
validPoints.push([pt[0], pt[1]]);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
if (validPoints.length > 0 && (isSequenceAction || validPoints.length > 1)) {
|
|
619
|
+
const isNormSeq = Boolean(act.normalized_sequence || args.normalized_sequence) || validPoints.every(p => p[0] <= 1000 && p[1] <= 1000);
|
|
620
|
+
for (let i = 0; i < validPoints.length - 1; i++) {
|
|
621
|
+
createSequenceConnector(validPoints[i][0], validPoints[i][1], validPoints[i+1][0], validPoints[i+1][1], overlayElement, isNormSeq);
|
|
622
|
+
}
|
|
623
|
+
validPoints.forEach((pt, idx) => {
|
|
624
|
+
createPointMarker(pt[0], pt[1], actionName, overlayElement, isNormSeq, idx + 1, validPoints.length);
|
|
625
|
+
});
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const isSwipeLike = actionName.includes('swipe') || actionName.includes('scroll') || actionName.includes('drag') || actionName.includes('slide');
|
|
631
|
+
|
|
632
|
+
// 2. Separate start and end coordinates
|
|
633
|
+
const normStart = act.normalized_start_coordinates || args.normalized_start_coordinates;
|
|
634
|
+
const normEnd = act.normalized_end_coordinates || args.normalized_end_coordinates;
|
|
635
|
+
const startCoords = normStart || act.start_coordinates || act.start_point || act.start || act.from ||
|
|
636
|
+
args.start_coordinates || args.start_point || args.start || args.from;
|
|
637
|
+
const endCoords = normEnd || act.end_coordinates || act.end_point || act.end || act.to ||
|
|
638
|
+
args.end_coordinates || args.end_point || args.end || args.to;
|
|
639
|
+
if (startCoords && Array.isArray(startCoords) && endCoords && Array.isArray(endCoords) &&
|
|
640
|
+
startCoords.length >= 2 && endCoords.length >= 2) {
|
|
641
|
+
createLineMarker(
|
|
642
|
+
startCoords[0], startCoords[1],
|
|
643
|
+
endCoords[0], endCoords[1],
|
|
644
|
+
overlayElement,
|
|
645
|
+
Boolean(normStart && normEnd)
|
|
646
|
+
);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// 3. 4-coordinate array
|
|
651
|
+
const normCoords = act.normalized_coordinates || args.normalized_coordinates;
|
|
652
|
+
const isNorm = Boolean(normCoords);
|
|
653
|
+
const rawCoords = normCoords || act.coordinates || act.coords || act.target ||
|
|
654
|
+
args.target || args.coordinates || args.coords ||
|
|
655
|
+
(isSwipeLike && Array.isArray(args.action) ? args.action : null) ||
|
|
656
|
+
(isSwipeLike && Array.isArray(act.action) ? act.action : null) ||
|
|
657
|
+
(isSwipeLike && Array.isArray(args.gesture) ? args.gesture : null) ||
|
|
658
|
+
(isSwipeLike && Array.isArray(act.gesture) ? act.gesture : null);
|
|
659
|
+
|
|
660
|
+
if (rawCoords && Array.isArray(rawCoords)) {
|
|
661
|
+
if (rawCoords.length === 4 && typeof rawCoords[0] === 'number') {
|
|
662
|
+
createLineMarker(rawCoords[0], rawCoords[1], rawCoords[2], rawCoords[3], overlayElement, isNorm);
|
|
663
|
+
return;
|
|
664
|
+
} else if (rawCoords.length === 2 && typeof rawCoords[0] === 'number') {
|
|
665
|
+
if (!isSwipeLike) {
|
|
666
|
+
createPointMarker(rawCoords[0], rawCoords[1], actionName, overlayElement, isNorm);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// 4. Directional swipe / scroll ONLY for swipe-like actions
|
|
673
|
+
if (isSwipeLike) {
|
|
674
|
+
const rawDir = (typeof args.action === 'string' ? args.action : null) ||
|
|
675
|
+
(typeof act.action === 'string' && act.action !== actionName ? act.action : null) ||
|
|
676
|
+
args.direction || act.direction || args.gesture || act.gesture || '';
|
|
677
|
+
const dirStr = String(rawDir).toLowerCase().trim();
|
|
678
|
+
|
|
679
|
+
let sx = 500, sy = 750, ex = 500, ey = 250;
|
|
680
|
+
if (dirStr.includes('up') || dirStr.includes('top') || actionName.includes('up')) {
|
|
681
|
+
sx = 500; sy = 750; ex = 500; ey = 250;
|
|
682
|
+
} else if (dirStr.includes('down') || dirStr.includes('bottom') || actionName.includes('down')) {
|
|
683
|
+
sx = 500; sy = 250; ex = 500; ey = 750;
|
|
684
|
+
} else if (dirStr.includes('left') || actionName.includes('left')) {
|
|
685
|
+
sx = 800; sy = 500; ex = 200; ey = 500;
|
|
686
|
+
} else if (dirStr.includes('right') || actionName.includes('right')) {
|
|
687
|
+
sx = 200; sy = 500; ex = 800; ey = 500;
|
|
688
|
+
}
|
|
689
|
+
createLineMarker(sx, sy, ex, ey, overlayElement, true);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (rawCoords && Array.isArray(rawCoords) && rawCoords.length === 2 && typeof rawCoords[0] === 'number') {
|
|
694
|
+
createPointMarker(rawCoords[0], rawCoords[1], actionName, overlayElement, isNorm);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function setupImageCoordinates(imgId, overlayId, actionObj) {
|
|
700
|
+
const img = document.getElementById(imgId);
|
|
701
|
+
const overlay = document.getElementById(overlayId);
|
|
702
|
+
if (!img || !overlay) return;
|
|
703
|
+
|
|
704
|
+
const draw = () => drawActionCoordinates(actionObj, overlay);
|
|
705
|
+
|
|
706
|
+
if (img.complete && img.naturalWidth > 0) {
|
|
707
|
+
draw();
|
|
708
|
+
} else {
|
|
709
|
+
img.onload = draw;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
let currentSessionId = null;
|
|
714
|
+
let sessionsMap = {};
|
|
715
|
+
let isBackgroundTaskPanelOpen = false;
|
|
716
|
+
let currentBackgroundTasksData = [];
|
|
717
|
+
|
|
718
|
+
function toggleVideoContent(forceState) {
|
|
719
|
+
const vc = document.getElementById('video-content');
|
|
720
|
+
const icon = document.getElementById('video-toggle-btn-icon');
|
|
721
|
+
if (!vc) return;
|
|
722
|
+
const shouldExpand = forceState !== undefined ? forceState : (vc.style.display === 'none');
|
|
723
|
+
vc.style.display = shouldExpand ? 'block' : 'none';
|
|
724
|
+
if (icon) icon.textContent = shouldExpand ? '➖' : '➕';
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function updateMiniVideoPlayer(videoUrl, expand = false) {
|
|
728
|
+
const container = document.getElementById('floating-video-container');
|
|
729
|
+
const globalVideo = document.getElementById('global-video');
|
|
730
|
+
const globalSrc = document.getElementById('global-video-src');
|
|
731
|
+
const errorBox = document.getElementById('video-error-box');
|
|
732
|
+
const errorMsg = document.getElementById('video-error-text');
|
|
733
|
+
const overlay = document.getElementById('global-video-overlay');
|
|
734
|
+
if (!container || !globalVideo) return;
|
|
735
|
+
|
|
736
|
+
globalVideo.onerror = function() {
|
|
737
|
+
globalVideo.style.display = 'none';
|
|
738
|
+
if (overlay) overlay.style.display = 'none';
|
|
739
|
+
if (errorBox) {
|
|
740
|
+
errorBox.style.display = 'block';
|
|
741
|
+
if (errorMsg) errorMsg.textContent = "Video file failed to play or was not found on server.";
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
if (videoUrl) {
|
|
746
|
+
globalVideo.style.display = 'block';
|
|
747
|
+
if (overlay) overlay.style.display = 'block';
|
|
748
|
+
if (errorBox) errorBox.style.display = 'none';
|
|
749
|
+
|
|
750
|
+
globalVideo.src = videoUrl;
|
|
751
|
+
if (globalSrc) globalSrc.src = videoUrl;
|
|
752
|
+
globalVideo.load();
|
|
753
|
+
container.style.display = 'block';
|
|
754
|
+
toggleVideoContent(expand);
|
|
755
|
+
} else if (currentSessionId) {
|
|
756
|
+
globalVideo.style.display = 'none';
|
|
757
|
+
if (overlay) overlay.style.display = 'none';
|
|
758
|
+
if (errorBox) {
|
|
759
|
+
errorBox.style.display = 'block';
|
|
760
|
+
if (errorMsg) errorMsg.textContent = "No video recording file was found for this session on disk or database records.";
|
|
761
|
+
}
|
|
762
|
+
globalVideo.pause();
|
|
763
|
+
container.style.display = 'block';
|
|
764
|
+
toggleVideoContent(expand);
|
|
765
|
+
} else {
|
|
766
|
+
container.style.display = 'none';
|
|
767
|
+
globalVideo.pause();
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async function reloadSessionVideo() {
|
|
772
|
+
if (!currentSessionId) {
|
|
773
|
+
alert("Please select a session first.");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
const res = await fetch('/api/sessions');
|
|
778
|
+
const sessions = await res.json();
|
|
779
|
+
sessionsMap = {};
|
|
780
|
+
sessions.forEach(s => { sessionsMap[s.session_id] = s; });
|
|
781
|
+
const cur = sessionsMap[currentSessionId];
|
|
782
|
+
const vUrl = cur ? cur.video_url : null;
|
|
783
|
+
|
|
784
|
+
updateMiniVideoPlayer(vUrl, true);
|
|
785
|
+
} catch (err) {
|
|
786
|
+
console.error("Failed to reload video:", err);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function adjustContentPadding() {
|
|
791
|
+
const container = document.getElementById('global-task-container');
|
|
792
|
+
const content = document.getElementById('content');
|
|
793
|
+
if (container && container.style.display !== 'none') {
|
|
794
|
+
requestAnimationFrame(() => {
|
|
795
|
+
content.style.paddingBottom = (container.offsetHeight + 40) + 'px';
|
|
796
|
+
});
|
|
797
|
+
} else {
|
|
798
|
+
content.style.paddingBottom = '30px';
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function toggleBackgroundTaskPanel() {
|
|
803
|
+
isBackgroundTaskPanelOpen = !isBackgroundTaskPanelOpen;
|
|
804
|
+
const panel = document.getElementById('global-task-panel');
|
|
805
|
+
const icon = document.getElementById('task-panel-toggle-icon');
|
|
806
|
+
if (isBackgroundTaskPanelOpen) {
|
|
807
|
+
panel.style.display = 'block';
|
|
808
|
+
icon.innerText = '▼ Collapse';
|
|
809
|
+
} else {
|
|
810
|
+
panel.style.display = 'none';
|
|
811
|
+
icon.innerText = '▲ Expand';
|
|
812
|
+
}
|
|
813
|
+
updateBackgroundTasksUI(currentBackgroundTasksData);
|
|
814
|
+
adjustContentPadding();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function formatBgStreamDisplayText(streamData) {
|
|
818
|
+
if (!streamData) return '';
|
|
819
|
+
if (typeof streamData === 'string') return streamData;
|
|
820
|
+
const text = streamData.text || '';
|
|
821
|
+
const thinking = streamData.thinking || '';
|
|
822
|
+
return thinking ? `[Thinking]\n${thinking}\n\n[Output]\n${text}` : text;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function updateBackgroundTaskStreamDisplay(targetId) {
|
|
826
|
+
// Only the streaming trace and the session aggregate change per chunk,
|
|
827
|
+
// so address their <pre> elements directly instead of scanning the document.
|
|
828
|
+
const candidateIds = new Set();
|
|
829
|
+
if (targetId) candidateIds.add(targetId);
|
|
830
|
+
if (currentSessionId) candidateIds.add(currentSessionId);
|
|
831
|
+
candidateIds.forEach(id => {
|
|
832
|
+
const pre = document.getElementById(`bg-stream-${id}`);
|
|
833
|
+
if (!pre) return;
|
|
834
|
+
const streamData = nodeStreams[id] || (id === currentSessionId ? nodeStreams[targetId] : null);
|
|
835
|
+
if (streamData) {
|
|
836
|
+
updateStreamPreContent(pre, formatBgStreamDisplayText(streamData));
|
|
837
|
+
scheduleStreamAutoScroll(pre);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function updateBackgroundTasksUI(tasks) {
|
|
843
|
+
currentBackgroundTasksData = tasks || [];
|
|
844
|
+
const container = document.getElementById('global-task-container');
|
|
845
|
+
if (!tasks || tasks.length === 0) {
|
|
846
|
+
if (!container.style.display || container.style.display === 'none') {
|
|
847
|
+
container.style.display = 'none';
|
|
848
|
+
adjustContentPadding();
|
|
849
|
+
}
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
container.style.display = 'flex';
|
|
854
|
+
|
|
855
|
+
const runningTasks = tasks.filter(t => t.status === 'running');
|
|
856
|
+
const completedTasks = tasks.filter(t => t.status === 'completed');
|
|
857
|
+
const barText = document.getElementById('task-bar-text');
|
|
858
|
+
const spinner = document.getElementById('task-bar-spinner');
|
|
859
|
+
|
|
860
|
+
if (runningTasks.length > 0) {
|
|
861
|
+
spinner.style.display = 'block';
|
|
862
|
+
barText.innerText = `Background Tasks (Running: ${runningTasks.length}, Completed: ${completedTasks.length}) - Latest: ${runningTasks[runningTasks.length - 1].summary}`;
|
|
863
|
+
} else {
|
|
864
|
+
spinner.style.display = 'none';
|
|
865
|
+
barText.innerText = `All background tasks completed (${tasks.length})`;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (isBackgroundTaskPanelOpen) {
|
|
869
|
+
const list = document.getElementById('background-tasks-list');
|
|
870
|
+
const existingItems = {};
|
|
871
|
+
Array.from(list.children).forEach(child => {
|
|
872
|
+
if (child.dataset && child.dataset.taskKey) {
|
|
873
|
+
existingItems[child.dataset.taskKey] = child;
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
const seenKeys = new Set();
|
|
877
|
+
|
|
878
|
+
tasks.forEach((task, index) => {
|
|
879
|
+
const taskKey = String(task.trace_id || task.task_id || task.id || `idx-${index}`);
|
|
880
|
+
seenKeys.add(taskKey);
|
|
881
|
+
|
|
882
|
+
let statusColor = '#94a3b8';
|
|
883
|
+
let statusLabel = task.status;
|
|
884
|
+
if (task.status === 'running') { statusColor = '#d69e2e'; statusLabel = 'Running'; }
|
|
885
|
+
else if (task.status === 'completed') { statusColor = '#10b981'; statusLabel = 'Completed'; }
|
|
886
|
+
|
|
887
|
+
let durationStr = '';
|
|
888
|
+
let timeStr = '';
|
|
889
|
+
if (task.start_time) {
|
|
890
|
+
const end = task.end_time || (Date.now() / 1000);
|
|
891
|
+
durationStr = `${(end - task.start_time).toFixed(1)}s`;
|
|
892
|
+
timeStr = formatRelativeTime(task.start_time);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const existing = existingItems[taskKey];
|
|
896
|
+
if (existing) {
|
|
897
|
+
// Diff update: only touch changed text/status to preserve DOM and scroll state
|
|
898
|
+
const summaryEl = existing.querySelector('.bg-task-summary');
|
|
899
|
+
if (summaryEl && summaryEl.innerText !== task.summary) summaryEl.innerText = task.summary;
|
|
900
|
+
const timeEl = existing.querySelector('.bg-task-time');
|
|
901
|
+
if (timeEl && timeEl.innerText !== timeStr) timeEl.innerText = timeStr;
|
|
902
|
+
const statusEl = existing.querySelector('.bg-task-status');
|
|
903
|
+
if (statusEl) {
|
|
904
|
+
const statusText = `● ${statusLabel}`;
|
|
905
|
+
if (statusEl.innerText !== statusText) statusEl.innerText = statusText;
|
|
906
|
+
statusEl.style.color = statusColor;
|
|
907
|
+
}
|
|
908
|
+
const durationEl = existing.querySelector('.bg-task-duration');
|
|
909
|
+
if (durationEl) {
|
|
910
|
+
const durationText = durationStr ? `(${durationStr})` : '';
|
|
911
|
+
if (durationEl.innerText !== durationText) durationEl.innerText = durationText;
|
|
912
|
+
}
|
|
913
|
+
if (task.trace_id) {
|
|
914
|
+
const pre = document.getElementById(`bg-stream-${task.trace_id}`);
|
|
915
|
+
if (!pre) {
|
|
916
|
+
existing.appendChild(createBgTaskStreamContainer(task));
|
|
917
|
+
} else {
|
|
918
|
+
const displayText = formatBgStreamDisplayText(nodeStreams[task.trace_id]) || task.logs || 'Waiting for stream output...';
|
|
919
|
+
updateStreamPreContent(pre, displayText);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const item = document.createElement('div');
|
|
926
|
+
item.dataset.taskKey = taskKey;
|
|
927
|
+
item.style.backgroundColor = '#1f2937';
|
|
928
|
+
item.style.border = '1px solid #374151';
|
|
929
|
+
item.style.borderRadius = '6px';
|
|
930
|
+
item.style.padding = '12px';
|
|
931
|
+
|
|
932
|
+
item.innerHTML = `
|
|
933
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
934
|
+
<span class="bg-task-summary" style="color: #f3f4f6; font-weight: 600; font-size: 0.9rem;">${escapeHtml(task.summary)}</span>
|
|
935
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
936
|
+
<span class="bg-task-time" style="color: #6b7280; font-size: 0.75rem;">${timeStr}</span>
|
|
937
|
+
<span class="bg-task-status" style="color: ${statusColor}; font-size: 0.8rem; font-weight: bold;">● ${escapeHtml(statusLabel)}</span>
|
|
938
|
+
<span class="bg-task-duration" style="color: #6b7280; font-size: 0.75rem;">${durationStr ? `(${durationStr})` : ''}</span>
|
|
939
|
+
</div>
|
|
940
|
+
</div>
|
|
941
|
+
`;
|
|
942
|
+
|
|
943
|
+
if (task.trace_id) {
|
|
944
|
+
item.appendChild(createBgTaskStreamContainer(task));
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
list.appendChild(item);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// Remove items for tasks that no longer exist
|
|
951
|
+
Array.from(list.children).forEach(child => {
|
|
952
|
+
const key = child.dataset ? child.dataset.taskKey : null;
|
|
953
|
+
if (key && !seenKeys.has(key)) child.remove();
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
adjustContentPadding();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function createBgTaskStreamContainer(task) {
|
|
960
|
+
const streamContainer = document.createElement('div');
|
|
961
|
+
streamContainer.style.marginTop = '8px';
|
|
962
|
+
streamContainer.style.backgroundColor = '#111827';
|
|
963
|
+
streamContainer.style.borderRadius = '4px';
|
|
964
|
+
streamContainer.style.padding = '8px';
|
|
965
|
+
streamContainer.style.borderLeft = '2px solid #3b82f6';
|
|
966
|
+
|
|
967
|
+
const label = document.createElement('div');
|
|
968
|
+
label.style.fontSize = '0.7rem';
|
|
969
|
+
label.style.color = '#6b7280';
|
|
970
|
+
label.style.marginBottom = '4px';
|
|
971
|
+
label.innerText = 'Live LLM Stream:';
|
|
972
|
+
|
|
973
|
+
const pre = document.createElement('pre');
|
|
974
|
+
pre.id = `bg-stream-${task.trace_id}`;
|
|
975
|
+
pre.style.margin = '0';
|
|
976
|
+
pre.style.padding = '0';
|
|
977
|
+
pre.style.backgroundColor = 'transparent';
|
|
978
|
+
pre.style.color = '#d1d5db';
|
|
979
|
+
pre.style.fontSize = '0.8rem';
|
|
980
|
+
pre.style.maxHeight = '120px';
|
|
981
|
+
pre.style.overflowY = 'auto';
|
|
982
|
+
pre.style.whiteSpace = 'pre-wrap';
|
|
983
|
+
const displayText = formatBgStreamDisplayText(nodeStreams[task.trace_id]);
|
|
984
|
+
updateStreamPreContent(pre, displayText || task.logs || 'Waiting for stream output...');
|
|
985
|
+
|
|
986
|
+
streamContainer.appendChild(label);
|
|
987
|
+
streamContainer.appendChild(pre);
|
|
988
|
+
|
|
989
|
+
scheduleStreamAutoScroll(pre);
|
|
990
|
+
return streamContainer;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
let activeNodeId = null;
|
|
994
|
+
let pollInterval = null;
|
|
995
|
+
let eventSource = null;
|
|
996
|
+
let activeNodeDetails = null;
|
|
997
|
+
let traceMap = {};
|
|
998
|
+
let nodeStreams = {};
|
|
999
|
+
|
|
1000
|
+
// Cap for session-level aggregated stream text; keep only the tail beyond this size
|
|
1001
|
+
const SESSION_STREAM_TEXT_CAP = 200000;
|
|
1002
|
+
let nodeStreamsPersistTimer = null;
|
|
1003
|
+
|
|
1004
|
+
function trimStreamEntry(entry) {
|
|
1005
|
+
if (!entry || typeof entry !== 'object') return;
|
|
1006
|
+
if (entry.text && entry.text.length > SESSION_STREAM_TEXT_CAP) {
|
|
1007
|
+
entry.text = entry.text.slice(-SESSION_STREAM_TEXT_CAP);
|
|
1008
|
+
}
|
|
1009
|
+
if (entry.thinking && entry.thinking.length > SESSION_STREAM_TEXT_CAP) {
|
|
1010
|
+
entry.thinking = entry.thinking.slice(-SESSION_STREAM_TEXT_CAP);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function flushNodeStreamsPersist() {
|
|
1015
|
+
if (nodeStreamsPersistTimer) {
|
|
1016
|
+
clearTimeout(nodeStreamsPersistTimer);
|
|
1017
|
+
nodeStreamsPersistTimer = null;
|
|
1018
|
+
}
|
|
1019
|
+
if (!currentSessionId) return;
|
|
1020
|
+
try {
|
|
1021
|
+
sessionStorage.setItem('nodeStreams_' + currentSessionId, JSON.stringify(nodeStreams));
|
|
1022
|
+
} catch (e) {
|
|
1023
|
+
// Ignore quota/serialization errors; persistence is best-effort
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function scheduleNodeStreamsPersist() {
|
|
1028
|
+
if (nodeStreamsPersistTimer) return;
|
|
1029
|
+
nodeStreamsPersistTimer = setTimeout(() => {
|
|
1030
|
+
nodeStreamsPersistTimer = null;
|
|
1031
|
+
flushNodeStreamsPersist();
|
|
1032
|
+
}, 2000);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
async function fetchTaskPlan() {
|
|
1036
|
+
if (!currentSessionId) return;
|
|
1037
|
+
const contentPre = document.getElementById('plan-modal-content');
|
|
1038
|
+
contentPre.innerText = 'Fetching latest task plan...';
|
|
1039
|
+
try {
|
|
1040
|
+
const res = await fetch(`/api/sessions/${currentSessionId}/plan`);
|
|
1041
|
+
const data = await res.json();
|
|
1042
|
+
contentPre.innerText = data.plan || 'No plan content returned.';
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
contentPre.innerText = `Error fetching plan: ${err.message}`;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function openPlanModal() {
|
|
1049
|
+
document.getElementById('plan-modal').style.display = 'flex';
|
|
1050
|
+
fetchTaskPlan();
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function closePlanModal() {
|
|
1054
|
+
document.getElementById('plan-modal').style.display = 'none';
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async function fetchAllNotes() {
|
|
1058
|
+
if (!currentSessionId) return;
|
|
1059
|
+
const contentDiv = document.getElementById('notes-modal-content');
|
|
1060
|
+
contentDiv.innerHTML = '<div style="color: #e2e8f0; font-family: monospace;">Fetching notes...</div>';
|
|
1061
|
+
try {
|
|
1062
|
+
const res = await fetch(`/api/sessions/${currentSessionId}/notes`);
|
|
1063
|
+
const data = await res.json();
|
|
1064
|
+
if (data.notes && Object.keys(data.notes).length > 0) {
|
|
1065
|
+
let html = '';
|
|
1066
|
+
for (const [filename, content] of Object.entries(data.notes)) {
|
|
1067
|
+
html += `<h3 style="color: #38bdf8; margin-top: 0;">${escapeHtml(filename)}</h3>`;
|
|
1068
|
+
html += `<pre style="margin: 0 0 20px 0; background-color: #0f172a; padding: 16px; border-radius: 8px; color: #e2e8f0; font-family: 'Courier New', Courier, monospace; font-size: 0.9rem; white-space: pre-wrap;">${escapeHtml(content)}</pre>`;
|
|
1069
|
+
}
|
|
1070
|
+
contentDiv.innerHTML = html;
|
|
1071
|
+
} else {
|
|
1072
|
+
contentDiv.innerHTML = '<div style="color: #e2e8f0; font-family: monospace;">No notes found for this session.</div>';
|
|
1073
|
+
}
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
contentDiv.innerHTML = `<div style="color: #ef4444; font-family: monospace;">Error fetching notes: ${escapeHtml(err.message)}</div>`;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function openNotesModal() {
|
|
1080
|
+
document.getElementById('notes-modal').style.display = 'flex';
|
|
1081
|
+
fetchAllNotes();
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function closeNotesModal() {
|
|
1085
|
+
document.getElementById('notes-modal').style.display = 'none';
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function resumeTask() {
|
|
1089
|
+
const statusDiv = document.getElementById('run-status');
|
|
1090
|
+
statusDiv.innerHTML = '<span style="color: #d69e2e;">Resuming task...</span>';
|
|
1091
|
+
try {
|
|
1092
|
+
const res = await fetch('/api/resume', { method: 'POST' });
|
|
1093
|
+
if (res.ok) {
|
|
1094
|
+
statusDiv.innerHTML = `<span style="color: #38a169;">Task resumed!</span>`;
|
|
1095
|
+
document.getElementById('resume-btn').style.display = 'none';
|
|
1096
|
+
} else {
|
|
1097
|
+
const data = await res.json();
|
|
1098
|
+
statusDiv.innerHTML = `<span style="color: #e53e3e;">Failed to resume: ${escapeHtml(data.detail || 'Unknown error')}</span>`;
|
|
1099
|
+
}
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
statusDiv.innerHTML = `<span style="color: #e53e3e;">Error: ${escapeHtml(err.message)}</span>`;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async function stopTask() {
|
|
1106
|
+
const statusDiv = document.getElementById('run-status');
|
|
1107
|
+
statusDiv.innerHTML = '<span style="color: #d69e2e;">Stopping task...</span>';
|
|
1108
|
+
try {
|
|
1109
|
+
const res = await fetch('/api/stop', { method: 'POST' });
|
|
1110
|
+
const data = await res.json();
|
|
1111
|
+
if (res.ok) {
|
|
1112
|
+
statusDiv.innerHTML = `<span style="color: #38a169;">Task stopped!</span>`;
|
|
1113
|
+
setTimeout(loadSessions, 2000);
|
|
1114
|
+
} else {
|
|
1115
|
+
statusDiv.innerHTML = `<span style="color: #e53e3e;">Failed to stop: ${escapeHtml(data.detail || 'Unknown error')}</span>`;
|
|
1116
|
+
}
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
statusDiv.innerHTML = `<span style="color: #e53e3e;">Error: ${escapeHtml(err.message)}</span>`;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function runTask() {
|
|
1123
|
+
const goal = document.getElementById('task-goal').value;
|
|
1124
|
+
const profileElement = document.getElementById('task-profile-select');
|
|
1125
|
+
const profile = profileElement ? profileElement.value : 'flash';
|
|
1126
|
+
const statusDiv = document.getElementById('run-status');
|
|
1127
|
+
if (!goal) {
|
|
1128
|
+
statusDiv.innerHTML = '<span style="color: #e53e3e;">Please enter a goal</span>';
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
statusDiv.innerHTML = '<span style="color: #d69e2e;">Starting task...</span>';
|
|
1132
|
+
try {
|
|
1133
|
+
const res = await fetch('/api/run', {
|
|
1134
|
+
method: 'POST',
|
|
1135
|
+
headers: {
|
|
1136
|
+
'Content-Type': 'application/json'
|
|
1137
|
+
},
|
|
1138
|
+
body: JSON.stringify({ goal: goal, profile: profile })
|
|
1139
|
+
});
|
|
1140
|
+
const data = await res.json();
|
|
1141
|
+
if (res.ok) {
|
|
1142
|
+
statusDiv.innerHTML = `<span style="color: #38a169;">Task started!</span>`;
|
|
1143
|
+
document.getElementById('task-goal').value = '';
|
|
1144
|
+
setTimeout(loadSessions, 2000);
|
|
1145
|
+
} else {
|
|
1146
|
+
statusDiv.innerHTML = `<span style="color: #e53e3e;">Failed: ${escapeHtml(data.detail || 'Unknown error')}</span>`;
|
|
1147
|
+
}
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
statusDiv.innerHTML = `<span style="color: #e53e3e;">Error: ${escapeHtml(err.message)}</span>`;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async function deleteSession(event, sessionId) {
|
|
1154
|
+
event.stopPropagation();
|
|
1155
|
+
if (!confirm('Are you sure you want to delete this session and all its files?')) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
try {
|
|
1159
|
+
const res = await fetch(`/api/sessions/${sessionId}/delete`, { method: 'POST' });
|
|
1160
|
+
if (res.ok) {
|
|
1161
|
+
if (currentSessionId === sessionId) {
|
|
1162
|
+
currentSessionId = null;
|
|
1163
|
+
updateMiniVideoPlayer(null);
|
|
1164
|
+
|
|
1165
|
+
document.getElementById('session-header').innerHTML = '<h1>Select a session from the sidebar</h1>';
|
|
1166
|
+
document.getElementById('session-tabs-bar').style.display = 'none';
|
|
1167
|
+
document.getElementById('steps-container').style.display = 'none';
|
|
1168
|
+
document.getElementById('traces-container-wrapper').style.display = 'none';
|
|
1169
|
+
document.getElementById('replay-container-wrapper').style.display = 'none';
|
|
1170
|
+
if (eventSource) {
|
|
1171
|
+
eventSource.close();
|
|
1172
|
+
eventSource = null;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
loadSessions();
|
|
1176
|
+
} else {
|
|
1177
|
+
const data = await res.json();
|
|
1178
|
+
alert(`Failed to delete session: ${data.detail || 'Unknown error'}`);
|
|
1179
|
+
}
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
alert(`Error: ${err.message}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
let allSessionsData = [];
|
|
1186
|
+
|
|
1187
|
+
async function loadSessions() {
|
|
1188
|
+
try {
|
|
1189
|
+
const res = await fetch('/api/sessions');
|
|
1190
|
+
allSessionsData = await res.json();
|
|
1191
|
+
filterSessionsList();
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
document.getElementById('session-list').innerText = 'Failed to load sessions';
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function filterSessionsList() {
|
|
1198
|
+
const input = document.getElementById('session-search-input');
|
|
1199
|
+
const query = input ? input.value.trim().toLowerCase() : '';
|
|
1200
|
+
|
|
1201
|
+
const filtered = allSessionsData.filter(session => {
|
|
1202
|
+
if (!query) return true;
|
|
1203
|
+
const sid = (session.session_id || '').toLowerCase();
|
|
1204
|
+
const goal = (session.initial_goal || '').toLowerCase();
|
|
1205
|
+
return sid.includes(query) || goal.includes(query);
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
renderSessionsList(filtered);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function renderSessionsList(sessions) {
|
|
1212
|
+
const list = document.getElementById('session-list');
|
|
1213
|
+
if (!list) return;
|
|
1214
|
+
list.innerHTML = '';
|
|
1215
|
+
sessionsMap = {};
|
|
1216
|
+
|
|
1217
|
+
if (sessions.length === 0) {
|
|
1218
|
+
list.innerHTML = '<div style="padding: 15px; color: #718096; text-align: center; font-size: 0.85rem;">No matching sessions found.</div>';
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
sessions.forEach(session => {
|
|
1223
|
+
sessionsMap[session.session_id] = session;
|
|
1224
|
+
const div = document.createElement('div');
|
|
1225
|
+
div.className = `session-item ${session.session_id === currentSessionId ? 'active' : ''}`;
|
|
1226
|
+
div.onclick = () => selectSession(session.session_id, session.initial_goal, session.start_time);
|
|
1227
|
+
|
|
1228
|
+
const time = new Date(session.start_time * 1000).toLocaleString();
|
|
1229
|
+
const titleText = session.initial_goal || 'Unnamed Session';
|
|
1230
|
+
const sidDisplay = session.session_id || '';
|
|
1231
|
+
const deviceSerial = session.device_serial || session.device_id || '';
|
|
1232
|
+
const devicePrefix = deviceSerial ? `${escapeHtml(deviceSerial)} · ` : '';
|
|
1233
|
+
|
|
1234
|
+
div.innerHTML = `
|
|
1235
|
+
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
|
1236
|
+
<div style="flex: 1; min-width: 0;">
|
|
1237
|
+
<h3 style="font-size: 0.95rem; margin-bottom: 3px; margin-top: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${escapeHtml(titleText)}">${escapeHtml(titleText)}</h3>
|
|
1238
|
+
<div style="font-size: 0.75rem; font-family: monospace; opacity: 0.85; margin-bottom: 3px; word-break: break-all; color: ${session.session_id === currentSessionId ? '#edf2f7' : '#4a5568'};">ID: ${escapeHtml(sidDisplay)}</div>
|
|
1239
|
+
<small style="color: ${session.session_id === currentSessionId ? '#e2e8f0' : '#718096'}; font-size: 0.75rem;">${devicePrefix}${time}</small>
|
|
1240
|
+
</div>
|
|
1241
|
+
<button onclick="deleteSession(event, '${escapeHtml(session.session_id)}')" style="background: none; border: none; color: ${session.session_id === currentSessionId ? '#fff' : '#e53e3e'}; cursor: pointer; font-size: 1.1rem; padding: 0 0 0 8px; line-height: 1; flex-shrink: 0;" title="Delete Session">🗑️</button>
|
|
1242
|
+
</div>
|
|
1243
|
+
`;
|
|
1244
|
+
list.appendChild(div);
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
let sessionStartTime = null;
|
|
1249
|
+
let activeTab = 'steps';
|
|
1250
|
+
|
|
1251
|
+
function switchTab(tabName) {
|
|
1252
|
+
activeTab = tabName;
|
|
1253
|
+
|
|
1254
|
+
// Update tab button active states
|
|
1255
|
+
const buttons = document.querySelectorAll('.tab-btn');
|
|
1256
|
+
buttons.forEach(btn => {
|
|
1257
|
+
if (btn.getAttribute('onclick').includes(tabName)) {
|
|
1258
|
+
btn.classList.add('active');
|
|
1259
|
+
btn.style.borderBottom = '2px solid #3182ce';
|
|
1260
|
+
btn.style.color = '#3182ce';
|
|
1261
|
+
} else {
|
|
1262
|
+
btn.classList.remove('active');
|
|
1263
|
+
btn.style.borderBottom = 'none';
|
|
1264
|
+
btn.style.color = '#718096';
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
const stepsContainer = document.getElementById('steps-container');
|
|
1269
|
+
const tracesWrapper = document.getElementById('traces-container-wrapper');
|
|
1270
|
+
const replayWrapper = document.getElementById('replay-container-wrapper');
|
|
1271
|
+
|
|
1272
|
+
if (tabName === 'steps') {
|
|
1273
|
+
stepsContainer.style.display = 'block';
|
|
1274
|
+
tracesWrapper.style.display = 'none';
|
|
1275
|
+
replayWrapper.style.display = 'none';
|
|
1276
|
+
loadSteps();
|
|
1277
|
+
} else if (tabName === 'traces') {
|
|
1278
|
+
stepsContainer.style.display = 'none';
|
|
1279
|
+
tracesWrapper.style.display = 'block';
|
|
1280
|
+
replayWrapper.style.display = 'none';
|
|
1281
|
+
loadTree();
|
|
1282
|
+
} else if (tabName === 'replay') {
|
|
1283
|
+
stepsContainer.style.display = 'none';
|
|
1284
|
+
tracesWrapper.style.display = 'none';
|
|
1285
|
+
replayWrapper.style.display = 'block';
|
|
1286
|
+
loadReplaySteps();
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const userExpandedStepIds = new Set();
|
|
1291
|
+
|
|
1292
|
+
function onStepDetailsToggle(detailsEl, stepId) {
|
|
1293
|
+
if (!detailsEl) return;
|
|
1294
|
+
if (detailsEl.open) {
|
|
1295
|
+
userExpandedStepIds.add(stepId);
|
|
1296
|
+
loadStepTraces(stepId, detailsEl);
|
|
1297
|
+
} else {
|
|
1298
|
+
userExpandedStepIds.delete(stepId);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function selectSession(sessionId, goal, startTime) {
|
|
1303
|
+
// Flush any pending stream persistence for the previous session before switching
|
|
1304
|
+
flushNodeStreamsPersist();
|
|
1305
|
+
currentSessionId = sessionId;
|
|
1306
|
+
sessionStartTime = startTime;
|
|
1307
|
+
userExpandedStepIds.clear();
|
|
1308
|
+
stepTracesCache = {};
|
|
1309
|
+
stepFilterMap = {};
|
|
1310
|
+
document.getElementById('session-header').innerHTML = `<h1>${escapeHtml(goal || 'Session Details')}</h1><p><small>ID: ${escapeHtml(sessionId)}</small></p>`;
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
const sessionObj = sessionsMap[sessionId];
|
|
1315
|
+
updateMiniVideoPlayer(sessionObj ? sessionObj.video_url : null);
|
|
1316
|
+
|
|
1317
|
+
// Make session tabs bar visible
|
|
1318
|
+
document.getElementById('session-tabs-bar').style.display = 'flex';
|
|
1319
|
+
|
|
1320
|
+
// Set correct display states based on active tab
|
|
1321
|
+
const stepsContainer = document.getElementById('steps-container');
|
|
1322
|
+
const tracesWrapper = document.getElementById('traces-container-wrapper');
|
|
1323
|
+
const replayWrapper = document.getElementById('replay-container-wrapper');
|
|
1324
|
+
|
|
1325
|
+
stepsContainer.style.display = activeTab === 'steps' ? 'block' : 'none';
|
|
1326
|
+
tracesWrapper.style.display = activeTab === 'traces' ? 'block' : 'none';
|
|
1327
|
+
replayWrapper.style.display = activeTab === 'replay' ? 'block' : 'none';
|
|
1328
|
+
|
|
1329
|
+
loadSessions(); // Refresh list to update active class
|
|
1330
|
+
|
|
1331
|
+
// Attempt to recover persisted stream data from sessionStorage to survive browser reloads
|
|
1332
|
+
const savedStreams = sessionStorage.getItem('nodeStreams_' + sessionId);
|
|
1333
|
+
if (savedStreams) {
|
|
1334
|
+
try {
|
|
1335
|
+
nodeStreams = JSON.parse(savedStreams);
|
|
1336
|
+
} catch(e) {
|
|
1337
|
+
nodeStreams = {};
|
|
1338
|
+
}
|
|
1339
|
+
} else {
|
|
1340
|
+
nodeStreams = {};
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (activeTab === 'steps') {
|
|
1344
|
+
loadSteps();
|
|
1345
|
+
} else if (activeTab === 'traces') {
|
|
1346
|
+
loadTree();
|
|
1347
|
+
} else if (activeTab === 'replay') {
|
|
1348
|
+
loadReplaySteps();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Connect to SSE
|
|
1352
|
+
if (eventSource) eventSource.close();
|
|
1353
|
+
eventSource = new EventSource(`/api/stream/${sessionId}`);
|
|
1354
|
+
|
|
1355
|
+
eventSource.addEventListener('llm_stream', function(e) {
|
|
1356
|
+
const data = JSON.parse(e.data);
|
|
1357
|
+
const traceId = data.execution_id;
|
|
1358
|
+
|
|
1359
|
+
if (!nodeStreams[traceId]) {
|
|
1360
|
+
nodeStreams[traceId] = { text: '', thinking: '', parentTraceId: data.parent_trace_id };
|
|
1361
|
+
} else if (typeof nodeStreams[traceId] === 'string') {
|
|
1362
|
+
nodeStreams[traceId] = { text: nodeStreams[traceId], thinking: '', parentTraceId: data.parent_trace_id };
|
|
1363
|
+
} else {
|
|
1364
|
+
nodeStreams[traceId].parentTraceId = data.parent_trace_id || nodeStreams[traceId].parentTraceId;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (data.is_thinking) {
|
|
1368
|
+
nodeStreams[traceId].thinking += data.chunk;
|
|
1369
|
+
} else {
|
|
1370
|
+
nodeStreams[traceId].text += data.chunk;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const sessionId = data.session_id || currentSessionId;
|
|
1374
|
+
if (sessionId) {
|
|
1375
|
+
if (!nodeStreams[sessionId]) {
|
|
1376
|
+
nodeStreams[sessionId] = { text: '', thinking: '' };
|
|
1377
|
+
} else if (typeof nodeStreams[sessionId] === 'string') {
|
|
1378
|
+
nodeStreams[sessionId] = { text: nodeStreams[sessionId], thinking: '' };
|
|
1379
|
+
}
|
|
1380
|
+
if (data.is_thinking) {
|
|
1381
|
+
nodeStreams[sessionId].thinking += data.chunk;
|
|
1382
|
+
} else {
|
|
1383
|
+
nodeStreams[sessionId].text += data.chunk;
|
|
1384
|
+
}
|
|
1385
|
+
trimStreamEntry(nodeStreams[sessionId]);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
scheduleNodeStreamsPersist();
|
|
1389
|
+
|
|
1390
|
+
updateStreamDisplay(traceId);
|
|
1391
|
+
updateBackgroundTaskStreamDisplay(traceId);
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
eventSource.addEventListener('trace_recorded', function(e) {
|
|
1395
|
+
const data = JSON.parse(e.data);
|
|
1396
|
+
if (data.trace_id && data.name) {
|
|
1397
|
+
traceMap[data.trace_id] = data.name;
|
|
1398
|
+
}
|
|
1399
|
+
if (data.status && data.status !== 'running') {
|
|
1400
|
+
if (nodeStreams[data.trace_id]) delete nodeStreams[data.trace_id];
|
|
1401
|
+
const finishedNode = document.getElementById(`node-div-${data.trace_id}`);
|
|
1402
|
+
if (finishedNode) {
|
|
1403
|
+
finishedNode.querySelectorAll('.node-stream-container').forEach(el => el.remove());
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
scheduleDebouncedReload();
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
eventSource.addEventListener('step_recorded', function(e) {
|
|
1410
|
+
scheduleDebouncedReload();
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
eventSource.addEventListener('step_updated', function(e) {
|
|
1414
|
+
scheduleDebouncedReload();
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
eventSource.addEventListener('background_tasks_updated', function(e) {
|
|
1418
|
+
const tasks = JSON.parse(e.data);
|
|
1419
|
+
updateBackgroundTasksUI(tasks);
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
eventSource.addEventListener('task_paused', function(e) {
|
|
1423
|
+
const data = JSON.parse(e.data);
|
|
1424
|
+
document.getElementById('run-status').innerHTML = `<span style="color: #e53e3e;">Task Paused (LLM Error): ${escapeHtml(data.error)}</span>`;
|
|
1425
|
+
document.getElementById('resume-btn').style.display = 'block';
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
eventSource.addEventListener('task_resumed', function(e) {
|
|
1429
|
+
document.getElementById('run-status').innerHTML = `<span style="color: #38a169;">Task resumed!</span>`;
|
|
1430
|
+
document.getElementById('resume-btn').style.display = 'none';
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
if (pollInterval) clearInterval(pollInterval);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
// --- Step Replay Tab Logic ---
|
|
1438
|
+
|
|
1439
|
+
let replaySteps = [];
|
|
1440
|
+
let replayConfig = [];
|
|
1441
|
+
let activeReplayNodeId = null;
|
|
1442
|
+
let activeReplayNodeDetails = null;
|
|
1443
|
+
const replayInProgressSteps = new Set();
|
|
1444
|
+
|
|
1445
|
+
async function fetchDevices() {
|
|
1446
|
+
try {
|
|
1447
|
+
const res = await fetch(`/api/devices?_t=${Date.now()}`);
|
|
1448
|
+
const devices = await res.json();
|
|
1449
|
+
const select = document.getElementById('replay-device-select');
|
|
1450
|
+
select.innerHTML = '';
|
|
1451
|
+
|
|
1452
|
+
if (devices.length === 0) {
|
|
1453
|
+
select.innerHTML = '<option value="">No devices detected</option>';
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
devices.forEach(d => {
|
|
1458
|
+
const opt = document.createElement('option');
|
|
1459
|
+
opt.value = d.serial;
|
|
1460
|
+
opt.innerText = `${d.serial} (${d.status})`;
|
|
1461
|
+
select.appendChild(opt);
|
|
1462
|
+
});
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
console.error("Failed to fetch devices:", err);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
let replayTools = [];
|
|
1469
|
+
|
|
1470
|
+
async function fetchReplayTools() {
|
|
1471
|
+
try {
|
|
1472
|
+
const res = await fetch(`/api/replay/tools?_t=${Date.now()}`);
|
|
1473
|
+
replayTools = await res.json();
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
console.error("Failed to fetch replay tools:", err);
|
|
1476
|
+
replayTools = [];
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
async function changeReplayTool(stepNum) {
|
|
1481
|
+
const toolSelect = document.getElementById(`replay-tool-select-${stepNum}`);
|
|
1482
|
+
if (!toolSelect) return;
|
|
1483
|
+
const toolName = toolSelect.value;
|
|
1484
|
+
const form = document.getElementById(`replay-params-form-${stepNum}`);
|
|
1485
|
+
if (!form) return;
|
|
1486
|
+
|
|
1487
|
+
form.innerHTML = '<div style="grid-column: span 2; color: #64748b; font-style: italic; font-size: 0.8rem; padding: 10px 0;">Loading parameters...</div>';
|
|
1488
|
+
|
|
1489
|
+
try {
|
|
1490
|
+
const res = await fetch(`/api/replay/config?tool_name=${encodeURIComponent(toolName)}&_t=${Date.now()}`);
|
|
1491
|
+
if (!res.ok) throw new Error(await res.text());
|
|
1492
|
+
const params = await res.json();
|
|
1493
|
+
|
|
1494
|
+
form.innerHTML = '';
|
|
1495
|
+
|
|
1496
|
+
// Get the step data to populate defaults
|
|
1497
|
+
const step = replaySteps.find(s => s.step_number === stepNum);
|
|
1498
|
+
|
|
1499
|
+
if (params.length === 0) {
|
|
1500
|
+
form.innerHTML = '<div style="grid-column: span 2; color: #64748b; font-style: italic; font-size: 0.8rem; padding: 10px 0;">No parameters required for this tool.</div>';
|
|
1501
|
+
loadCachedReplayForStep(stepNum);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
params.forEach(param => {
|
|
1506
|
+
const val = (step && step.param_defaults && step.param_defaults[toolName] && step.param_defaults[toolName][param.name]) || param.default || '';
|
|
1507
|
+
const labelText = param.name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
1508
|
+
|
|
1509
|
+
const group = document.createElement('div');
|
|
1510
|
+
group.style.display = 'flex';
|
|
1511
|
+
group.style.flexDirection = 'column';
|
|
1512
|
+
group.style.gap = '5px';
|
|
1513
|
+
group.innerHTML = `<label style="font-size: 0.8rem; font-weight: 600; color: #475569;">${escapeHtml(labelText)}</label>`;
|
|
1514
|
+
|
|
1515
|
+
if (param.input_type === 'textarea') {
|
|
1516
|
+
const textarea = document.createElement('textarea');
|
|
1517
|
+
textarea.id = `replay-input-${param.name}-${stepNum}`;
|
|
1518
|
+
textarea.value = val;
|
|
1519
|
+
textarea.placeholder = `Optional ${labelText}...`;
|
|
1520
|
+
textarea.style.cssText = 'padding: 8px 10px; border-radius: 6px; border: 1px solid #cbd5e0; font-family: inherit; font-size: 0.85rem; height: 60px; resize: vertical; outline: none;';
|
|
1521
|
+
group.appendChild(textarea);
|
|
1522
|
+
} else if (param.input_type === 'checkbox') {
|
|
1523
|
+
const checkboxContainer = document.createElement('div');
|
|
1524
|
+
checkboxContainer.style.display = 'flex';
|
|
1525
|
+
checkboxContainer.style.alignItems = 'center';
|
|
1526
|
+
checkboxContainer.style.gap = '8px';
|
|
1527
|
+
checkboxContainer.style.marginTop = '5px';
|
|
1528
|
+
|
|
1529
|
+
const checkbox = document.createElement('input');
|
|
1530
|
+
checkbox.id = `replay-input-${param.name}-${stepNum}`;
|
|
1531
|
+
checkbox.type = 'checkbox';
|
|
1532
|
+
checkbox.checked = !!val;
|
|
1533
|
+
checkbox.style.cursor = 'pointer';
|
|
1534
|
+
|
|
1535
|
+
const label = document.createElement('label');
|
|
1536
|
+
label.htmlFor = checkbox.id;
|
|
1537
|
+
label.style.fontSize = '0.8rem';
|
|
1538
|
+
label.style.color = '#475569';
|
|
1539
|
+
label.innerText = 'Enable';
|
|
1540
|
+
|
|
1541
|
+
checkboxContainer.appendChild(checkbox);
|
|
1542
|
+
checkboxContainer.appendChild(label);
|
|
1543
|
+
group.appendChild(checkboxContainer);
|
|
1544
|
+
} else {
|
|
1545
|
+
const input = document.createElement('input');
|
|
1546
|
+
input.id = `replay-input-${param.name}-${stepNum}`;
|
|
1547
|
+
input.type = param.input_type || 'text';
|
|
1548
|
+
input.value = val;
|
|
1549
|
+
input.placeholder = `Enter ${labelText}...`;
|
|
1550
|
+
input.style.cssText = 'padding: 8px 10px; border-radius: 6px; border: 1px solid #cbd5e0; font-family: inherit; font-size: 0.85rem; outline: none;';
|
|
1551
|
+
group.appendChild(input);
|
|
1552
|
+
}
|
|
1553
|
+
form.appendChild(group);
|
|
1554
|
+
});
|
|
1555
|
+
} catch (err) {
|
|
1556
|
+
form.innerHTML = `<div style="grid-column: span 2; color: #dc2626; font-size: 0.8rem;">Failed to load parameters: ${escapeHtml(err.message)}</div>`;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// Reload cached replay traces for the selected tool
|
|
1560
|
+
loadCachedReplayForStep(stepNum);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
async function loadReplaySteps(preserveScroll = false) {
|
|
1564
|
+
if (!currentSessionId) return;
|
|
1565
|
+
|
|
1566
|
+
const wrapper = document.getElementById('replay-container-wrapper');
|
|
1567
|
+
const savedScrollTop = preserveScroll && wrapper ? wrapper.scrollTop : 0;
|
|
1568
|
+
|
|
1569
|
+
// Capture currently active step nav card if any
|
|
1570
|
+
let activeStepNum = null;
|
|
1571
|
+
const activeNavCard = document.querySelector('.step-nav-card.active');
|
|
1572
|
+
if (activeNavCard) {
|
|
1573
|
+
activeStepNum = parseInt(activeNavCard.id.split('-').pop());
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const container = document.getElementById('replay-timeline-container');
|
|
1577
|
+
container.innerHTML = '<div style="padding: 30px; color: #718096; text-align: center; font-size: 1rem;"><div class="spinner" style="width: 20px; height: 20px; border: 2px solid #3182ce; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; display: inline-block; margin-right: 8px; vertical-align: middle;"></div>Loading replay timeline...</div>';
|
|
1578
|
+
|
|
1579
|
+
const navContainer = document.getElementById('replay-steps-nav');
|
|
1580
|
+
if (navContainer) {
|
|
1581
|
+
navContainer.innerHTML = '';
|
|
1582
|
+
navContainer.style.display = 'none';
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// Fetch registered tools
|
|
1586
|
+
if (replayTools.length === 0) {
|
|
1587
|
+
await fetchReplayTools();
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
try {
|
|
1591
|
+
const res = await fetch(`/api/sessions/${currentSessionId}/replay_steps?_t=${Date.now()}`);
|
|
1592
|
+
if (!res.ok) throw new Error(await res.text());
|
|
1593
|
+
replaySteps = await res.json();
|
|
1594
|
+
|
|
1595
|
+
if (replaySteps.length === 0) {
|
|
1596
|
+
container.innerHTML = '<div style="padding: 40px; color: #718096; text-align: center; font-size: 1.1rem;">🔄 No steps recorded yet for this session.</div>';
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
container.innerHTML = '';
|
|
1601
|
+
if (navContainer) {
|
|
1602
|
+
navContainer.style.display = 'flex';
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
replaySteps.forEach(step => {
|
|
1606
|
+
const hasPre = step.pre_screenshot && step.pre_screenshot !== 'None' && step.pre_screenshot !== 'null' && String(step.pre_screenshot).trim() !== '';
|
|
1607
|
+
let hasPost = step.post_screenshot && step.post_screenshot !== 'None' && step.post_screenshot !== 'null' && String(step.post_screenshot).trim() !== '';
|
|
1608
|
+
if (hasPre && hasPost && step.pre_screenshot === step.post_screenshot) {
|
|
1609
|
+
hasPost = false;
|
|
1610
|
+
}
|
|
1611
|
+
const hasImages = hasPre || hasPost;
|
|
1612
|
+
const stepNum = step.step_number;
|
|
1613
|
+
|
|
1614
|
+
// Create horizontal nav card
|
|
1615
|
+
if (navContainer) {
|
|
1616
|
+
const navCard = document.createElement('div');
|
|
1617
|
+
navCard.className = 'step-nav-card';
|
|
1618
|
+
navCard.id = `step-nav-card-${stepNum}`;
|
|
1619
|
+
|
|
1620
|
+
const navBorderColor = step.is_replayed ? '#a7f3d0' : '#e2e8f0';
|
|
1621
|
+
const navBgColor = step.is_replayed ? '#f0fdf4' : 'white';
|
|
1622
|
+
navCard.style.cssText = `flex: 0 0 auto; min-width: 140px; max-width: 200px; background: ${navBgColor}; border: 1.5px solid ${navBorderColor}; border-radius: 8px; padding: 10px 12px; display: flex; align-items: center; gap: 10px; cursor: pointer; transition: border-color 0.2s, background-color 0.2s, box-shadow 0.2s, transform 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.02); user-select: none;`;
|
|
1623
|
+
|
|
1624
|
+
if (activeStepNum === stepNum) {
|
|
1625
|
+
navCard.classList.add('active');
|
|
1626
|
+
navCard.style.borderColor = '#3182ce';
|
|
1627
|
+
navCard.style.backgroundColor = step.is_replayed ? '#ecfdf5' : '#ebf8ff';
|
|
1628
|
+
navCard.style.boxShadow = '0 2px 4px rgba(49, 130, 206, 0.1)';
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
navCard.onmouseover = () => {
|
|
1632
|
+
if (!navCard.classList.contains('active')) {
|
|
1633
|
+
navCard.style.borderColor = step.is_replayed ? '#86efac' : '#cbd5e0';
|
|
1634
|
+
navCard.style.transform = 'translateY(-1px)';
|
|
1635
|
+
navCard.style.boxShadow = '0 3px 6px rgba(0,0,0,0.05)';
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
navCard.onmouseout = () => {
|
|
1639
|
+
if (!navCard.classList.contains('active')) {
|
|
1640
|
+
navCard.style.borderColor = navBorderColor;
|
|
1641
|
+
navCard.style.transform = 'translateY(0)';
|
|
1642
|
+
navCard.style.boxShadow = '0 1px 2px rgba(0,0,0,0.02)';
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
|
|
1646
|
+
navCard.onclick = () => {
|
|
1647
|
+
document.querySelectorAll('.step-nav-card').forEach(c => {
|
|
1648
|
+
c.classList.remove('active');
|
|
1649
|
+
const isCardReplayed = replaySteps.find(s => `step-nav-card-${s.step_number}` === c.id)?.is_replayed;
|
|
1650
|
+
c.style.borderColor = isCardReplayed ? '#a7f3d0' : '#e2e8f0';
|
|
1651
|
+
c.style.backgroundColor = isCardReplayed ? '#f0fdf4' : 'white';
|
|
1652
|
+
c.style.boxShadow = '0 1px 2px rgba(0,0,0,0.02)';
|
|
1653
|
+
});
|
|
1654
|
+
navCard.classList.add('active');
|
|
1655
|
+
navCard.style.borderColor = '#3182ce';
|
|
1656
|
+
navCard.style.backgroundColor = step.is_replayed ? '#ecfdf5' : '#ebf8ff';
|
|
1657
|
+
navCard.style.boxShadow = '0 2px 4px rgba(49, 130, 206, 0.1)';
|
|
1658
|
+
|
|
1659
|
+
const targetCard = document.getElementById(`replay-step-card-${stepNum}`);
|
|
1660
|
+
if (targetCard) {
|
|
1661
|
+
targetCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
|
|
1665
|
+
navCard.innerHTML = `
|
|
1666
|
+
<span style="background-color: ${step.is_replayed ? '#10b981' : '#3182ce'}; color: white; border-radius: 50%; width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: bold; flex-shrink: 0;">
|
|
1667
|
+
${step.is_replayed ? '✓' : stepNum}
|
|
1668
|
+
</span>
|
|
1669
|
+
<div style="display: flex; flex-direction: column; overflow: hidden; flex: 1;">
|
|
1670
|
+
<span style="font-size: 0.8rem; font-weight: 700; color: #1e293b; display: flex; align-items: center; justify-content: space-between; gap: 4px;">
|
|
1671
|
+
<span>Step ${stepNum}</span>
|
|
1672
|
+
${step.is_replayed ? '<span style="font-size: 0.65rem; color: #10b981; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em;">Replayed</span>' : ''}
|
|
1673
|
+
</span>
|
|
1674
|
+
<span style="font-size: 0.7rem; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${escapeHtml(step.summary)}">${escapeHtml(step.summary || 'Executing...')}</span>
|
|
1675
|
+
</div>
|
|
1676
|
+
`;
|
|
1677
|
+
navContainer.appendChild(navCard);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const stepCard = document.createElement('div');
|
|
1681
|
+
stepCard.className = 'step-card';
|
|
1682
|
+
stepCard.id = `replay-step-card-${stepNum}`;
|
|
1683
|
+
const borderStyle = step.is_replayed
|
|
1684
|
+
? 'border: 1px solid #10b981; border-left: 5px solid #10b981;'
|
|
1685
|
+
: 'border: 1px solid #e2e8f0;';
|
|
1686
|
+
stepCard.style.cssText = `background: white; ${borderStyle} border-radius: 8px; margin-bottom: 30px; box-shadow: 0 4px 6px rgba(0,0,0,0.03); overflow: hidden;`;
|
|
1687
|
+
|
|
1688
|
+
// Header HTML
|
|
1689
|
+
const headerHtml = `
|
|
1690
|
+
<div style="background-color: #f8fafc; border-bottom: 1px solid #e2e8f0; padding: 12px 20px; display: flex; justify-content: space-between; align-items: center;">
|
|
1691
|
+
<h3 style="margin: 0; color: #1e293b; font-size: 1.05rem; display: flex; align-items: center; gap: 10px;">
|
|
1692
|
+
<span style="background-color: #3182ce; color: white; border-radius: 50%; width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; font-size: 0.8rem; font-weight: bold;">${stepNum}</span>
|
|
1693
|
+
<span>${escapeHtml(step.summary || 'Step Executing...')}</span>
|
|
1694
|
+
${step.is_replayed ? '<span style="font-size: 0.65rem; background-color: #10b981; color: white; padding: 2px 6px; border-radius: 4px; font-weight: bold; text-transform: uppercase; margin-left: 10px;">Replayed</span>' : ''}
|
|
1695
|
+
</h3>
|
|
1696
|
+
<span style="color: #64748b; font-size: 0.85rem; font-weight: 500;">${formatRelativeTime(step.timestamp)}</span>
|
|
1697
|
+
</div>
|
|
1698
|
+
`;
|
|
1699
|
+
|
|
1700
|
+
// Content panel grid
|
|
1701
|
+
let contentHtml = `<div style="padding: 20px; display: flex; flex-direction: column; gap: 20px;">`;
|
|
1702
|
+
|
|
1703
|
+
// Screenshots comparison (if available)
|
|
1704
|
+
if (hasImages) {
|
|
1705
|
+
contentHtml += `
|
|
1706
|
+
<div style="display: grid; grid-template-columns: ${(hasPre && hasPost) ? '1fr 1fr' : '1fr'}; gap: 20px; max-width: ${(hasPre && hasPost) ? '100%' : '360px'}; margin: 0 auto; width: 100%;">
|
|
1707
|
+
`;
|
|
1708
|
+
if (hasPre) {
|
|
1709
|
+
const preSrc = getSafeLocalUrl(step.pre_screenshot);
|
|
1710
|
+
contentHtml += `
|
|
1711
|
+
<div style="text-align: center;">
|
|
1712
|
+
<span style="font-size: 0.75rem; font-weight: 600; color: #64748b; display: block; margin-bottom: 6px;">${hasPost ? 'Pre-Action (Before)' : 'Screenshot'}</span>
|
|
1713
|
+
<div class="image-wrapper" style="border: 1px solid #e2e8f0; border-radius: 6px; background-color: #f8fafc; cursor: zoom-in; max-height: 250px; overflow: hidden; display: flex; align-items: center; justify-content: center; box-shadow: 0 1px 3px rgba(0,0,0,0.03);" onclick="zoomImage('${escapeHtml(step.pre_screenshot)}')">
|
|
1714
|
+
<div style="position: relative; display: inline-block; max-width: 100%;">
|
|
1715
|
+
<img id="replay-img-${stepNum}" src="${escapeHtml(preSrc)}" style="max-height: 240px; max-width: 100%; object-fit: contain; display: block; margin: 0 auto;"/>
|
|
1716
|
+
<div id="replay-overlay-${stepNum}" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none;"></div>
|
|
1717
|
+
</div>
|
|
1718
|
+
</div>
|
|
1719
|
+
</div>
|
|
1720
|
+
`;
|
|
1721
|
+
}
|
|
1722
|
+
if (hasPost) {
|
|
1723
|
+
const postSrc = getSafeLocalUrl(step.post_screenshot);
|
|
1724
|
+
contentHtml += `
|
|
1725
|
+
<div style="text-align: center;">
|
|
1726
|
+
<span style="font-size: 0.75rem; font-weight: 600; color: #64748b; display: block; margin-bottom: 6px;">Post-Action (After)</span>
|
|
1727
|
+
<div class="image-wrapper" style="border: 1px solid #e2e8f0; border-radius: 6px; background-color: #f8fafc; cursor: zoom-in; max-height: 250px; overflow: hidden; display: flex; align-items: center; justify-content: center; box-shadow: 0 1px 3px rgba(0,0,0,0.03);">
|
|
1728
|
+
<img src="${escapeHtml(postSrc)}" style="max-height: 240px; max-width: 100%; object-fit: contain;" onclick="zoomImage('${escapeHtml(step.post_screenshot)}')"/>
|
|
1729
|
+
</div>
|
|
1730
|
+
</div>
|
|
1731
|
+
`;
|
|
1732
|
+
}
|
|
1733
|
+
contentHtml += `</div>`;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// Main workspace grid (Strictly Top-Down Layout)
|
|
1737
|
+
contentHtml += `
|
|
1738
|
+
<div style="display: flex; flex-direction: column; gap: 25px; margin-top: 15px;">
|
|
1739
|
+
<!-- 1. Original Traces Tree -->
|
|
1740
|
+
<div class="detail-section" style="background-color: white; border: 1px solid #e2e8f0; border-radius: 8px; padding: 18px; display: flex; flex-direction: column; max-height: 500px; margin-bottom: 0; box-shadow: 0 1px 3px rgba(0,0,0,0.02);">
|
|
1741
|
+
<h3 style="margin-top: 0; border-bottom: 2px solid #edf2f7; padding-bottom: 10px; color: #475569; font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.05em; display: flex; align-items: center; gap: 6px;">
|
|
1742
|
+
<span>🌳 Original Traces Tree</span>
|
|
1743
|
+
</h3>
|
|
1744
|
+
<div id="replay-original-tree-${stepNum}" style="flex: 1; overflow-y: auto;">
|
|
1745
|
+
<div style="color: #94a3b8; font-style: italic; text-align: center; padding: 30px 0;">Loading original traces...</div>
|
|
1746
|
+
</div>
|
|
1747
|
+
</div>
|
|
1748
|
+
|
|
1749
|
+
<!-- 2. Replay Configuration & Parameters -->
|
|
1750
|
+
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 18px; display: flex; flex-direction: column; gap: 15px; box-shadow: 0 1px 2px rgba(0,0,0,0.01);">
|
|
1751
|
+
<div style="display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #edf2f7; padding-bottom: 8px;">
|
|
1752
|
+
<h4 style="margin: 0; font-size: 0.85rem; color: #475569; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: none; padding-bottom: 0;">⚙️ Replay Configuration</h4>
|
|
1753
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
1754
|
+
<label for="replay-tool-select-${stepNum}" style="font-size: 0.82rem; font-weight: 600; color: #475569;">Tool for Replay:</label>
|
|
1755
|
+
<select id="replay-tool-select-${stepNum}" onchange="changeReplayTool(${stepNum})" style="padding: 4px 10px; border-radius: 6px; border: 1px solid #cbd5e0; font-size: 0.8rem; background-color: white; color: #334155; outline: none; cursor: pointer; font-family: inherit;">
|
|
1756
|
+
<!-- Tool options dynamically populated -->
|
|
1757
|
+
</select>
|
|
1758
|
+
</div>
|
|
1759
|
+
</div>
|
|
1760
|
+
<div id="replay-params-form-${stepNum}" style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
|
1761
|
+
<!-- Parameter inputs dynamically created below -->
|
|
1762
|
+
</div>
|
|
1763
|
+
|
|
1764
|
+
<!-- Execution Controls (Integrated at the bottom of parameters) -->
|
|
1765
|
+
<div style="display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 12px; border-top: 1px solid #e2e8f0; margin-top: 5px;">
|
|
1766
|
+
<button class="refresh-btn" id="btn-execute-replay-${stepNum}" style="margin-bottom: 0; background-color: #38a169; display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px;" onclick="runReplay(${stepNum})">
|
|
1767
|
+
<span>▶ Run Live Replay</span>
|
|
1768
|
+
</button>
|
|
1769
|
+
<span id="replay-status-${stepNum}" style="font-size: 0.82rem; color: #64748b; font-style: italic; text-align: right;">Ready</span>
|
|
1770
|
+
</div>
|
|
1771
|
+
</div>
|
|
1772
|
+
|
|
1773
|
+
<!-- 3. Live Replay Traces Tree -->
|
|
1774
|
+
<div class="detail-section" style="background-color: white; border: 1px solid #e2e8f0; border-radius: 8px; padding: 18px; display: flex; flex-direction: column; max-height: 500px; margin-bottom: 0; box-shadow: 0 1px 3px rgba(0,0,0,0.02); transition: border-color 0.3s;" id="replay-live-tree-panel-${stepNum}">
|
|
1775
|
+
<h3 style="margin-top: 0; border-bottom: 2px solid #edf2f7; padding-bottom: 10px; color: #38a169; font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.05em; display: flex; align-items: center; justify-content: space-between; gap: 6px; width: 100%;">
|
|
1776
|
+
<span>⚡ Live Replay Traces Tree</span>
|
|
1777
|
+
<span id="replay-id-display-${stepNum}" style="font-size: 0.75rem; color: #64748b; font-weight: normal; background: #f1f5f9; padding: 2px 8px; border-radius: 4px; text-transform: none; display: none;"></span>
|
|
1778
|
+
</h3>
|
|
1779
|
+
<div id="replay-live-tree-${stepNum}" style="flex: 1; overflow-y: auto;">
|
|
1780
|
+
<div style="color: #94a3b8; font-style: italic; text-align: center; padding: 30px 0;" id="replay-live-tree-placeholder-${stepNum}">Run a live replay to populate replayed traces tree.</div>
|
|
1781
|
+
</div>
|
|
1782
|
+
</div>
|
|
1783
|
+
</div>
|
|
1784
|
+
`;
|
|
1785
|
+
|
|
1786
|
+
contentHtml += `</div>`; // End Card Content
|
|
1787
|
+
|
|
1788
|
+
stepCard.innerHTML = headerHtml + contentHtml;
|
|
1789
|
+
container.appendChild(stepCard);
|
|
1790
|
+
|
|
1791
|
+
// Populate tool dropdown and trigger dynamic parameters load
|
|
1792
|
+
const toolSelect = document.getElementById(`replay-tool-select-${stepNum}`);
|
|
1793
|
+
if (toolSelect && replayTools.length > 0) {
|
|
1794
|
+
toolSelect.innerHTML = '';
|
|
1795
|
+
replayTools.forEach(t => {
|
|
1796
|
+
const opt = document.createElement('option');
|
|
1797
|
+
opt.value = t.name;
|
|
1798
|
+
opt.innerText = t.display_name;
|
|
1799
|
+
opt.title = t.description;
|
|
1800
|
+
toolSelect.appendChild(opt);
|
|
1801
|
+
});
|
|
1802
|
+
toolSelect.value = replayTools[0].name;
|
|
1803
|
+
}
|
|
1804
|
+
changeReplayTool(stepNum);
|
|
1805
|
+
|
|
1806
|
+
// Load original traces tree immediately
|
|
1807
|
+
loadOriginalTreeForStep(stepNum, step.step_id);
|
|
1808
|
+
|
|
1809
|
+
if (hasPre && step.action_taken) {
|
|
1810
|
+
setupImageCoordinates(`replay-img-${stepNum}`, `replay-overlay-${stepNum}`, step.action_taken);
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
// Restore scroll position if requested
|
|
1815
|
+
if (preserveScroll && wrapper) {
|
|
1816
|
+
setTimeout(() => {
|
|
1817
|
+
wrapper.scrollTop = savedScrollTop;
|
|
1818
|
+
}, 50);
|
|
1819
|
+
}
|
|
1820
|
+
} catch (err) {
|
|
1821
|
+
container.innerHTML = `<div style="color: #e53e3e; padding: 20px; text-align: center; font-weight: bold;">Error: ${escapeHtml(err.message)}</div>`;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function getSafeLocalUrl(url) {
|
|
1826
|
+
if (!url || url === 'None' || url === 'null' || url === 'undefined') return '';
|
|
1827
|
+
let targetUrl = String(url).trim();
|
|
1828
|
+
if (!targetUrl || targetUrl === 'None' || targetUrl === 'null' || targetUrl === 'undefined') return '';
|
|
1829
|
+
if (targetUrl.startsWith('/') && !targetUrl.startsWith('/local_file') && !targetUrl.startsWith('/images')) {
|
|
1830
|
+
targetUrl = 'file://' + targetUrl;
|
|
1831
|
+
}
|
|
1832
|
+
if (targetUrl.startsWith('file://')) {
|
|
1833
|
+
return '/local_file?path=' + encodeURIComponent(targetUrl);
|
|
1834
|
+
}
|
|
1835
|
+
return targetUrl;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
async function loadOriginalTreeForStep(stepNum, stepId) {
|
|
1839
|
+
const container = document.getElementById(`replay-original-tree-${stepNum}`);
|
|
1840
|
+
try {
|
|
1841
|
+
const res = await fetch(`/api/steps/${stepId}/traces`);
|
|
1842
|
+
if (!res.ok) throw new Error(await res.text());
|
|
1843
|
+
const treeData = await res.json();
|
|
1844
|
+
renderOriginalReplayTreeForStep(stepNum, treeData);
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
container.innerHTML = `<div style="color: #e53e3e; padding: 10px; font-size: 0.85rem;">Failed to load original traces: ${escapeHtml(err.message)}</div>`;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
async function loadCachedReplayForStep(stepNum) {
|
|
1851
|
+
const container = document.getElementById(`replay-live-tree-${stepNum}`);
|
|
1852
|
+
const panel = document.getElementById(`replay-live-tree-panel-${stepNum}`);
|
|
1853
|
+
const toolSelect = document.getElementById(`replay-tool-select-${stepNum}`);
|
|
1854
|
+
const toolName = toolSelect ? toolSelect.value : '';
|
|
1855
|
+
try {
|
|
1856
|
+
let url = `/api/sessions/${currentSessionId}/steps/${stepNum}/replay_traces`;
|
|
1857
|
+
const queryParams = new URLSearchParams();
|
|
1858
|
+
if (toolName) {
|
|
1859
|
+
queryParams.append("tool_name", toolName);
|
|
1860
|
+
}
|
|
1861
|
+
queryParams.append("_t", Date.now());
|
|
1862
|
+
url += "?" + queryParams.toString();
|
|
1863
|
+
const res = await fetch(url);
|
|
1864
|
+
if (res.ok) {
|
|
1865
|
+
const data = await res.json();
|
|
1866
|
+
if (data && data.live && data.live.length > 0) {
|
|
1867
|
+
renderLiveReplayTreeForStep(stepNum, data.live);
|
|
1868
|
+
if (panel) panel.style.borderColor = '#10b981';
|
|
1869
|
+
const statusText = document.getElementById(`replay-status-${stepNum}`);
|
|
1870
|
+
if (statusText) {
|
|
1871
|
+
statusText.innerText = "Loaded cached replay traces.";
|
|
1872
|
+
statusText.style.color = "#16a34a";
|
|
1873
|
+
}
|
|
1874
|
+
const replayIdDisplay = document.getElementById(`replay-id-display-${stepNum}`);
|
|
1875
|
+
if (replayIdDisplay && data.replay_id) {
|
|
1876
|
+
replayIdDisplay.innerText = `Replay ID: ${data.replay_id}`;
|
|
1877
|
+
replayIdDisplay.style.display = 'inline-block';
|
|
1878
|
+
} else if (replayIdDisplay) {
|
|
1879
|
+
replayIdDisplay.style.display = 'none';
|
|
1880
|
+
}
|
|
1881
|
+
} else {
|
|
1882
|
+
// Clear previous live tree if no cached trace exists for this tool
|
|
1883
|
+
if (container) {
|
|
1884
|
+
container.innerHTML = '<div style="color: #94a3b8; font-style: italic; text-align: center; padding: 30px 0;" id="replay-live-tree-placeholder-' + stepNum + '">Run a live replay to populate replayed traces tree.</div>';
|
|
1885
|
+
}
|
|
1886
|
+
if (panel) panel.style.borderColor = '#e2e8f0';
|
|
1887
|
+
const statusText = document.getElementById(`replay-status-${stepNum}`);
|
|
1888
|
+
if (statusText) {
|
|
1889
|
+
statusText.innerText = "Ready";
|
|
1890
|
+
statusText.style.color = "#64748b";
|
|
1891
|
+
}
|
|
1892
|
+
const replayIdDisplay = document.getElementById(`replay-id-display-${stepNum}`);
|
|
1893
|
+
if (replayIdDisplay) {
|
|
1894
|
+
replayIdDisplay.style.display = 'none';
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
console.error(`Failed to load cached replay traces for step ${stepNum}:`, err);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function renderOriginalReplayTreeForStep(stepNum, treeData) {
|
|
1904
|
+
const container = document.getElementById(`replay-original-tree-${stepNum}`);
|
|
1905
|
+
if (!container) return;
|
|
1906
|
+
container.innerHTML = '';
|
|
1907
|
+
const showLogs = true;
|
|
1908
|
+
|
|
1909
|
+
if (!treeData || treeData.length === 0) {
|
|
1910
|
+
container.innerHTML = '<div style="color: #718096; font-style: italic; text-align: center; padding: 20px 0;">No original traces recorded for this step.</div>';
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
renderReplayTreeNodes(treeData, container, showLogs, stepNum);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function renderLiveReplayTreeForStep(stepNum, treeData) {
|
|
1918
|
+
const container = document.getElementById(`replay-live-tree-${stepNum}`);
|
|
1919
|
+
if (!container) return;
|
|
1920
|
+
container.innerHTML = '';
|
|
1921
|
+
const showLogs = true;
|
|
1922
|
+
|
|
1923
|
+
if (!treeData || treeData.length === 0) {
|
|
1924
|
+
container.innerHTML = '<div style="color: #718096; font-style: italic; text-align: center; padding: 20px 0;">No replayed traces recorded yet.</div>';
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
renderReplayTreeNodes(treeData, container, showLogs, stepNum);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
function renderReplayTreeNodes(nodes, container, showLogs, stepNum, depth = 0) {
|
|
1932
|
+
const allowedTypes = ['agent', 'tool', 'llm_call', 'node', 'action', 'thinking', 'raw_thinking'];
|
|
1933
|
+
const visibleNodes = nodes.filter(n => showLogs || allowedTypes.includes(n.type));
|
|
1934
|
+
|
|
1935
|
+
visibleNodes.forEach(node => {
|
|
1936
|
+
const nodeRow = document.createElement('div');
|
|
1937
|
+
nodeRow.style.marginLeft = `${depth * 15}px`;
|
|
1938
|
+
nodeRow.style.marginBottom = '6px';
|
|
1939
|
+
|
|
1940
|
+
if (node.type === 'raw_thinking' || node.type === 'thinking') {
|
|
1941
|
+
const thoughtText = extractThinkingText(node.payload);
|
|
1942
|
+
const isNative = node.type === 'thinking';
|
|
1943
|
+
const boxColor = isNative ? '#16a34a' : '#475569';
|
|
1944
|
+
const boxBg = isNative ? '#f0fdf4' : '#f8fafc';
|
|
1945
|
+
const boxBorder = isNative ? '#dcfce7' : '#e2e8f0';
|
|
1946
|
+
const label = isNative ? '🧠 Model Native Thinking' : '📝 Model Raw Thinking / Plan';
|
|
1947
|
+
|
|
1948
|
+
nodeRow.innerHTML = `
|
|
1949
|
+
<div style="background-color: ${boxBg}; border-left: 4px solid ${boxColor}; border-radius: 6px; padding: 12px 15px; border: 1px solid ${boxBorder}; border-left-width: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.02);">
|
|
1950
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
1951
|
+
<h4 style="margin: 0; color: ${boxColor}; font-size: 0.85rem; font-weight: 700; text-transform: uppercase; display: flex; align-items: center; gap: 6px;">
|
|
1952
|
+
<span>${label}</span>
|
|
1953
|
+
</h4>
|
|
1954
|
+
<div style="display: flex; align-items: center; gap: 8px; font-size: 0.75rem; color: #718096;">
|
|
1955
|
+
<span>${formatRelativeTime(node.timestamp)}</span>
|
|
1956
|
+
<span class="status-${escapeHtml(node.status)}">${escapeHtml(node.status)}</span>
|
|
1957
|
+
</div>
|
|
1958
|
+
</div>
|
|
1959
|
+
<div style="white-space: pre-wrap; font-size: 0.88rem; color: #1e293b; line-height: 1.6; font-family: inherit;">${escapeHtml(thoughtText || '(No thinking content)')}</div>
|
|
1960
|
+
</div>
|
|
1961
|
+
`;
|
|
1962
|
+
container.appendChild(nodeRow);
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
nodeRow.style.padding = '8px 12px';
|
|
1967
|
+
nodeRow.style.backgroundColor = 'white';
|
|
1968
|
+
nodeRow.style.border = '1px solid #e2e8f0';
|
|
1969
|
+
nodeRow.style.borderRadius = '6px';
|
|
1970
|
+
nodeRow.style.boxShadow = '0 1px 2px rgba(0,0,0,0.01)';
|
|
1971
|
+
|
|
1972
|
+
const typePill = `<span class="pill type-${escapeHtml(node.type)}" style="font-size: 0.7rem; padding: 2px 6px; margin-right: 8px; border-radius: 4px; font-weight: bold; display: inline-block;">${escapeHtml(node.type)}</span>`;
|
|
1973
|
+
const statusColor = node.status === 'success' || node.status === 'SUCCESS' ? '#16a34a' : (node.status === 'failed' || node.status === 'FAILED' ? '#dc2626' : '#d97706');
|
|
1974
|
+
const statusDot = `<span style="color: ${statusColor}; font-weight: bold; margin-right: 8px; font-size: 0.78rem;">● ${escapeHtml(node.status)}</span>`;
|
|
1975
|
+
|
|
1976
|
+
const header = document.createElement('div');
|
|
1977
|
+
header.style.display = 'flex';
|
|
1978
|
+
header.style.justifyContent = 'space-between';
|
|
1979
|
+
header.style.alignItems = 'center';
|
|
1980
|
+
header.style.cursor = 'pointer';
|
|
1981
|
+
header.style.userSelect = 'none';
|
|
1982
|
+
|
|
1983
|
+
header.innerHTML = `
|
|
1984
|
+
<div style="display: flex; align-items: center;">
|
|
1985
|
+
${typePill}
|
|
1986
|
+
<span style="font-weight: bold; font-size: 0.82rem; color: #1e293b;">${escapeHtml(node.name)}</span>
|
|
1987
|
+
</div>
|
|
1988
|
+
<div style="font-size: 0.78rem; display: flex; align-items: center; gap: 8px;">
|
|
1989
|
+
<span style="color: #64748b; font-size: 0.75rem;">${formatRelativeTime(node.timestamp)}</span>
|
|
1990
|
+
${statusDot}
|
|
1991
|
+
${node.duration ? `<span style="color: #64748b;">${formatNodeDurationBreakdown(node)}</span>` : ''}
|
|
1992
|
+
</div>
|
|
1993
|
+
`;
|
|
1994
|
+
|
|
1995
|
+
const detailsContainer = document.createElement('div');
|
|
1996
|
+
detailsContainer.style.display = node.trace_id === activeReplayNodeId ? 'block' : 'none';
|
|
1997
|
+
detailsContainer.style.marginTop = '10px';
|
|
1998
|
+
detailsContainer.style.borderTop = '1px solid #f1f5f9';
|
|
1999
|
+
detailsContainer.style.paddingTop = '10px';
|
|
2000
|
+
|
|
2001
|
+
header.onclick = async () => {
|
|
2002
|
+
if (detailsContainer.style.display === 'none') {
|
|
2003
|
+
activeReplayNodeId = node.trace_id;
|
|
2004
|
+
|
|
2005
|
+
detailsContainer.innerHTML = '<div style="font-size: 0.8rem; color: #64748b; padding: 10px 0;"><div class="spinner" style="width: 12px; height: 12px; border: 2px solid #64748b; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; display: inline-block; margin-right: 6px; vertical-align: middle;"></div>Loading details...</div>';
|
|
2006
|
+
detailsContainer.style.display = 'block';
|
|
2007
|
+
|
|
2008
|
+
try {
|
|
2009
|
+
let traceDetails = node;
|
|
2010
|
+
const needsFetch = !node.llm_call && (!node.payload || Object.keys(node.payload).length === 0 ||
|
|
2011
|
+
(node.type === 'llm_call' && !node.payload.prompt) ||
|
|
2012
|
+
(node.type === 'tool' && node.payload.result === undefined));
|
|
2013
|
+
if (needsFetch) {
|
|
2014
|
+
let url = `/api/traces/${node.trace_id}`;
|
|
2015
|
+
if (currentSessionId && stepNum !== undefined) {
|
|
2016
|
+
url += `?session_id=${currentSessionId}&step_number=${stepNum}`;
|
|
2017
|
+
}
|
|
2018
|
+
const res = await fetch(url);
|
|
2019
|
+
if (res.ok) {
|
|
2020
|
+
traceDetails = await res.json();
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
if (traceDetails.type === 'thinking' && traceDetails.payload) {
|
|
2025
|
+
detailsContainer.innerHTML = `
|
|
2026
|
+
<div class="thinking-box native" style="background-color: #f0fdf4; border-left: 4px solid #16a34a; border-radius: 6px; padding: 12px 15px; border: 1px solid #dcfce7; border-left-width: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.02); margin-bottom: 0;">
|
|
2027
|
+
<h4 style="margin: 0 0 6px 0; color: #16a34a; font-size: 0.85rem; font-weight: 700; text-transform: uppercase;">🧠 Operator Native Thinking</h4>
|
|
2028
|
+
<div style="white-space: pre-wrap; font-size: 0.88rem; color: #1f2937; line-height: 1.6; font-family: inherit;">${escapeHtml(traceDetails.payload.thought)}</div>
|
|
2029
|
+
</div>
|
|
2030
|
+
`;
|
|
2031
|
+
} else if (traceDetails.type === 'raw_thinking' && traceDetails.payload) {
|
|
2032
|
+
detailsContainer.innerHTML = `
|
|
2033
|
+
<div class="thinking-box raw" style="background-color: #f8fafc; border-left: 4px solid #475569; border-radius: 6px; padding: 12px 15px; border: 1px solid #e2e8f0; border-left-width: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.02); margin-bottom: 0;">
|
|
2034
|
+
<h4 style="margin: 0 0 6px 0; color: #475569; font-size: 0.85rem; font-weight: 700; text-transform: uppercase;">📝 Operator Raw Thinking / Plan</h4>
|
|
2035
|
+
<div style="white-space: pre-wrap; font-size: 0.88rem; color: #374151; line-height: 1.6; font-family: inherit;">${escapeHtml(traceDetails.payload.thought)}</div>
|
|
2036
|
+
</div>
|
|
2037
|
+
`;
|
|
2038
|
+
} else if (traceDetails.type === 'llm_call') {
|
|
2039
|
+
const payloadToRender = traceDetails.llm_call || traceDetails.payload;
|
|
2040
|
+
detailsContainer.innerHTML = renderLLMCallPayload(payloadToRender);
|
|
2041
|
+
} else if (traceDetails.llm_call) {
|
|
2042
|
+
detailsContainer.innerHTML = renderLLMCallPayload(traceDetails.llm_call);
|
|
2043
|
+
} else if (traceDetails.type === 'tool') {
|
|
2044
|
+
detailsContainer.innerHTML = renderToolPayload(traceDetails.payload);
|
|
2045
|
+
} else {
|
|
2046
|
+
detailsContainer.innerHTML = `
|
|
2047
|
+
<pre style="margin: 0; font-size: 0.8rem; color: #334155; font-family: monospace; white-space: pre-wrap; background-color: #f8fafc; padding: 10px; border-radius: 4px; border: 1px solid #e2e8f0; max-height: 250px; overflow-y: auto;">${escapeHtml(JSON.stringify(traceDetails.payload, null, 2))}</pre>
|
|
2048
|
+
`;
|
|
2049
|
+
const mediaHtml = detectAndRenderMedia(traceDetails.payload);
|
|
2050
|
+
if (mediaHtml) {
|
|
2051
|
+
const mediaDiv = document.createElement('div');
|
|
2052
|
+
mediaDiv.innerHTML = mediaHtml;
|
|
2053
|
+
detailsContainer.appendChild(mediaDiv);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
detailsContainer.innerHTML = `<div style="font-size: 0.8rem; color: #dc2626;">Error loading details: ${escapeHtml(err.message)}</div>`;
|
|
2058
|
+
}
|
|
2059
|
+
} else {
|
|
2060
|
+
detailsContainer.style.display = 'none';
|
|
2061
|
+
if (activeReplayNodeId === node.trace_id) {
|
|
2062
|
+
activeReplayNodeId = null;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
|
|
2067
|
+
nodeRow.appendChild(header);
|
|
2068
|
+
nodeRow.appendChild(detailsContainer);
|
|
2069
|
+
container.appendChild(nodeRow);
|
|
2070
|
+
|
|
2071
|
+
if (node.children && node.children.length > 0) {
|
|
2072
|
+
const childrenWrapper = document.createElement('div');
|
|
2073
|
+
renderReplayTreeNodes(node.children, childrenWrapper, showLogs, stepNum, depth + 1);
|
|
2074
|
+
container.appendChild(childrenWrapper);
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
function generateReplayId() {
|
|
2080
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
2081
|
+
return crypto.randomUUID();
|
|
2082
|
+
}
|
|
2083
|
+
return 'r-' + Math.random().toString(36).substring(2, 15) + '-' + Date.now().toString(36);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
async function runReplay(stepNum) {
|
|
2087
|
+
const deviceId = "replay-device";
|
|
2088
|
+
|
|
2089
|
+
const userSubmits = {};
|
|
2090
|
+
const inputsToDisable = [];
|
|
2091
|
+
|
|
2092
|
+
const suffix = `-${stepNum}`;
|
|
2093
|
+
const inputs = document.querySelectorAll(`[id^="replay-input-"]`);
|
|
2094
|
+
inputs.forEach(inputEl => {
|
|
2095
|
+
if (inputEl.id.endsWith(suffix)) {
|
|
2096
|
+
const paramName = inputEl.id.substring("replay-input-".length, inputEl.id.length - suffix.length);
|
|
2097
|
+
let val = inputEl.value;
|
|
2098
|
+
if (inputEl.type === 'checkbox') {
|
|
2099
|
+
val = inputEl.checked;
|
|
2100
|
+
}
|
|
2101
|
+
userSubmits[paramName] = val;
|
|
2102
|
+
inputsToDisable.push(inputEl);
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
const statusText = document.getElementById(`replay-status-${stepNum}`);
|
|
2107
|
+
const runBtn = document.getElementById(`btn-execute-replay-${stepNum}`);
|
|
2108
|
+
const liveTreePanel = document.getElementById(`replay-live-tree-panel-${stepNum}`);
|
|
2109
|
+
const liveContainer = document.getElementById(`replay-live-tree-${stepNum}`);
|
|
2110
|
+
|
|
2111
|
+
// Set loading state
|
|
2112
|
+
replayInProgressSteps.add(stepNum);
|
|
2113
|
+
runBtn.disabled = true;
|
|
2114
|
+
inputsToDisable.forEach(input => { input.disabled = true; });
|
|
2115
|
+
// deviceSelect removed
|
|
2116
|
+
statusText.innerText = "Replay running in sandbox on device...";
|
|
2117
|
+
statusText.style.color = "#d97706";
|
|
2118
|
+
liveTreePanel.style.borderColor = "#f59e0b";
|
|
2119
|
+
|
|
2120
|
+
liveContainer.innerHTML = '<div style="color: #d97706; text-align: center; font-style: italic; padding: 50px 0;"><div class="spinner" style="width: 20px; height: 20px; border: 2px solid #d97706; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; display: inline-block; margin-right: 8px; vertical-align: middle;"></div>Replay executing. Traces stream will populate upon completion...</div>';
|
|
2121
|
+
|
|
2122
|
+
const replayId = generateReplayId();
|
|
2123
|
+
const replayIdDisplay = document.getElementById(`replay-id-display-${stepNum}`);
|
|
2124
|
+
if (replayIdDisplay) {
|
|
2125
|
+
replayIdDisplay.innerText = `Replay ID: ${replayId}`;
|
|
2126
|
+
replayIdDisplay.style.display = 'inline-block';
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
try {
|
|
2130
|
+
const toolSelect = document.getElementById(`replay-tool-select-${stepNum}`);
|
|
2131
|
+
const toolName = toolSelect ? toolSelect.value : '';
|
|
2132
|
+
|
|
2133
|
+
const payload = {
|
|
2134
|
+
device_id: deviceId,
|
|
2135
|
+
user_submits: userSubmits,
|
|
2136
|
+
replay_id: replayId
|
|
2137
|
+
};
|
|
2138
|
+
if (toolName) {
|
|
2139
|
+
payload.tool_name = toolName;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
const res = await fetch(`/api/sessions/${currentSessionId}/steps/${stepNum}/replay`, {
|
|
2143
|
+
method: 'POST',
|
|
2144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2145
|
+
body: JSON.stringify(payload)
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
if (!res.ok) {
|
|
2149
|
+
const errText = await res.text();
|
|
2150
|
+
let errMsg = errText;
|
|
2151
|
+
try {
|
|
2152
|
+
const parsed = JSON.parse(errText);
|
|
2153
|
+
if (parsed && parsed.detail) errMsg = parsed.detail;
|
|
2154
|
+
} catch (_) {}
|
|
2155
|
+
throw new Error(errMsg);
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
const data = await res.json();
|
|
2159
|
+
|
|
2160
|
+
// Success State
|
|
2161
|
+
statusText.innerText = "Replay complete! Traces tree updated.";
|
|
2162
|
+
statusText.style.color = "#16a34a";
|
|
2163
|
+
liveTreePanel.style.borderColor = "#10b981";
|
|
2164
|
+
|
|
2165
|
+
renderLiveReplayTreeForStep(stepNum, data.live || []);
|
|
2166
|
+
loadReplaySteps(true); // Refresh to update Replayed badges, preserving scroll position
|
|
2167
|
+
} catch (err) {
|
|
2168
|
+
// Failure State
|
|
2169
|
+
statusText.innerText = `Replay failed: ${err.message}`;
|
|
2170
|
+
statusText.style.color = "#dc2626";
|
|
2171
|
+
liveTreePanel.style.borderColor = "#ef4444";
|
|
2172
|
+
liveContainer.innerHTML = `<div style="color: #dc2626; text-align: center; padding: 30px 10px; font-weight: bold;">Replay Execution Error:<br/><pre style="background-color: #fef2f2; color: #991b1b; border: 1px solid #fee2e2; margin-top: 15px; text-align: left; max-height: 200px; overflow-y: auto;">${escapeHtml(err.message)}</pre></div>`;
|
|
2173
|
+
} finally {
|
|
2174
|
+
// Restore controls
|
|
2175
|
+
replayInProgressSteps.delete(stepNum);
|
|
2176
|
+
runBtn.disabled = false;
|
|
2177
|
+
inputsToDisable.forEach(input => { input.disabled = false; });
|
|
2178
|
+
// deviceSelect removed
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
let reloadTimeout = null;
|
|
2183
|
+
function scheduleDebouncedReload() {
|
|
2184
|
+
if (reloadTimeout) return;
|
|
2185
|
+
reloadTimeout = setTimeout(() => {
|
|
2186
|
+
reloadTimeout = null;
|
|
2187
|
+
if (activeTab === 'steps') {
|
|
2188
|
+
loadSteps(true);
|
|
2189
|
+
} else if (activeTab === 'traces') {
|
|
2190
|
+
loadTree();
|
|
2191
|
+
} else if (activeTab === 'replay') {
|
|
2192
|
+
// Periodic refresh only targets steps with a replay currently in progress
|
|
2193
|
+
if (replaySteps && replaySteps.length > 0 && replayInProgressSteps.size > 0) {
|
|
2194
|
+
replaySteps.forEach(s => {
|
|
2195
|
+
if (replayInProgressSteps.has(s.step_number)) {
|
|
2196
|
+
loadCachedReplayForStep(s.step_number);
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}, 400); // Smooth 400ms presentation buffer interlock
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// --- Steps Timeline & Enhanced Payload Rendering Helpers ---
|
|
2205
|
+
|
|
2206
|
+
function escapeHtml(text) {
|
|
2207
|
+
if (text === null || text === undefined) return '';
|
|
2208
|
+
return String(text)
|
|
2209
|
+
.replace(/&/g, "&")
|
|
2210
|
+
.replace(/</g, "<")
|
|
2211
|
+
.replace(/>/g, ">")
|
|
2212
|
+
.replace(/"/g, """)
|
|
2213
|
+
.replace(/'/g, "'");
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
function isHumanThinking(text) {
|
|
2217
|
+
if (!text) return false;
|
|
2218
|
+
let trimmed = String(text).trim();
|
|
2219
|
+
if (!trimmed) return false;
|
|
2220
|
+
if (trimmed.startsWith('```')) {
|
|
2221
|
+
const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
2222
|
+
if (match) trimmed = match[1].trim();
|
|
2223
|
+
}
|
|
2224
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) return false;
|
|
2225
|
+
try {
|
|
2226
|
+
const parsed = JSON.parse(trimmed);
|
|
2227
|
+
if (typeof parsed === 'object' && parsed !== null) return false;
|
|
2228
|
+
} catch (e) {}
|
|
2229
|
+
if (/^\s*\{.*"label"\s*:.*"results"\s*:/s.test(trimmed) ||
|
|
2230
|
+
/^\s*\{.*"point"\s*:.*"label"\s*:/s.test(trimmed) ||
|
|
2231
|
+
/^\s*\{.*"action"\s*:/s.test(trimmed) ||
|
|
2232
|
+
/^\s*\{.*"name"\s*:/s.test(trimmed) ||
|
|
2233
|
+
/^\s*\{\s*"[^"]+"\s*:\s*/.test(trimmed) ||
|
|
2234
|
+
/^\s*\[\s*\{\s*"[^"]+"\s*:\s*/.test(trimmed)) {
|
|
2235
|
+
return false;
|
|
2236
|
+
}
|
|
2237
|
+
return true;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
function formatRelativeTime(timestamp) {
|
|
2241
|
+
if (!sessionStartTime) return new Date(timestamp * 1000).toLocaleTimeString();
|
|
2242
|
+
const diff = timestamp - sessionStartTime;
|
|
2243
|
+
return `+${diff.toFixed(1)}s`;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// Memoized per node object: fresh tree fetches produce fresh objects, so stale
|
|
2247
|
+
// entries are dropped automatically by the WeakMap without explicit clearing.
|
|
2248
|
+
const descendantDurationCache = new WeakMap();
|
|
2249
|
+
|
|
2250
|
+
function getDescendantDurations(node) {
|
|
2251
|
+
const cached = descendantDurationCache.get(node);
|
|
2252
|
+
if (cached) return cached;
|
|
2253
|
+
let stats = {
|
|
2254
|
+
llm: 0,
|
|
2255
|
+
tool: 0,
|
|
2256
|
+
action: 0,
|
|
2257
|
+
agent: 0
|
|
2258
|
+
};
|
|
2259
|
+
if (node.children) {
|
|
2260
|
+
node.children.forEach(child => {
|
|
2261
|
+
if (child.duration) {
|
|
2262
|
+
if (child.type === 'llm_call') {
|
|
2263
|
+
stats.llm += child.duration;
|
|
2264
|
+
} else if (child.type === 'tool') {
|
|
2265
|
+
stats.tool += child.duration;
|
|
2266
|
+
} else if (child.type === 'action') {
|
|
2267
|
+
stats.action += child.duration;
|
|
2268
|
+
} else if (child.type === 'agent') {
|
|
2269
|
+
stats.agent += child.duration;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const childStats = getDescendantDurations(child);
|
|
2273
|
+
stats.llm += childStats.llm;
|
|
2274
|
+
stats.tool += childStats.tool;
|
|
2275
|
+
stats.action += childStats.action;
|
|
2276
|
+
stats.agent += childStats.agent;
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
descendantDurationCache.set(node, stats);
|
|
2280
|
+
return stats;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
function formatNodeDurationBreakdown(node) {
|
|
2284
|
+
if (!node.duration) return '';
|
|
2285
|
+
const stats = getDescendantDurations(node);
|
|
2286
|
+
let breakdown = [];
|
|
2287
|
+
|
|
2288
|
+
function formatVal(val) {
|
|
2289
|
+
if (val > 0 && val < 0.01) return '<0.01s';
|
|
2290
|
+
return `${val.toFixed(2)}s`;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
if (stats.llm > 0) breakdown.push(`LLM: ${formatVal(stats.llm)}`);
|
|
2294
|
+
if (stats.tool > 0) breakdown.push(`Tool: ${formatVal(stats.tool)}`);
|
|
2295
|
+
if (stats.action > 0) breakdown.push(`Action: ${formatVal(stats.action)}`);
|
|
2296
|
+
if (stats.agent > 0) breakdown.push(`Agent: ${formatVal(stats.agent)}`);
|
|
2297
|
+
|
|
2298
|
+
if (breakdown.length > 0) {
|
|
2299
|
+
return `(${formatVal(node.duration)} | ${breakdown.join(', ')})`;
|
|
2300
|
+
}
|
|
2301
|
+
return `(${formatVal(node.duration)})`;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
function zoomImage(src) {
|
|
2305
|
+
const modal = document.getElementById('zoom-modal');
|
|
2306
|
+
const img = document.getElementById('zoom-modal-img');
|
|
2307
|
+
img.src = getSafeLocalUrl(src);
|
|
2308
|
+
modal.style.display = 'flex';
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
function closeZoomModal() {
|
|
2312
|
+
document.getElementById('zoom-modal').style.display = 'none';
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// Delegated zoom click handler: avoids inlining large data URIs into onclick attributes.
|
|
2316
|
+
// Elements marked with data-zoom-img zoom the contained (or own) img's current src.
|
|
2317
|
+
document.addEventListener('click', function(e) {
|
|
2318
|
+
const zoomTarget = e.target && e.target.closest ? e.target.closest('[data-zoom-img]') : null;
|
|
2319
|
+
if (!zoomTarget) return;
|
|
2320
|
+
const img = zoomTarget.tagName === 'IMG' ? zoomTarget : zoomTarget.querySelector('img');
|
|
2321
|
+
if (img && img.src) {
|
|
2322
|
+
zoomImage(img.src);
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
|
|
2326
|
+
// Lazily-rendered Raw JSON payload blocks: stringify + fill only on first expand.
|
|
2327
|
+
const rawJsonPayloadStore = new Map();
|
|
2328
|
+
let rawJsonPayloadSeq = 0;
|
|
2329
|
+
|
|
2330
|
+
function registerRawJsonPayload(payload) {
|
|
2331
|
+
rawJsonPayloadSeq += 1;
|
|
2332
|
+
rawJsonPayloadStore.set(String(rawJsonPayloadSeq), payload);
|
|
2333
|
+
// Bound memory: drop entries whose <details> block left the DOM
|
|
2334
|
+
// (skip the most recent few, which may not be attached yet)
|
|
2335
|
+
if (rawJsonPayloadStore.size > 50) {
|
|
2336
|
+
rawJsonPayloadStore.forEach((_, key) => {
|
|
2337
|
+
if (Number(key) <= rawJsonPayloadSeq - 10 &&
|
|
2338
|
+
!document.querySelector(`details[data-raw-json-id="${key}"]`)) {
|
|
2339
|
+
rawJsonPayloadStore.delete(key);
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
return rawJsonPayloadSeq;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
function onRawJsonToggle(detailsEl) {
|
|
2347
|
+
if (!detailsEl || !detailsEl.open) return;
|
|
2348
|
+
if (detailsEl.getAttribute('data-raw-loaded') === 'true') return;
|
|
2349
|
+
detailsEl.setAttribute('data-raw-loaded', 'true');
|
|
2350
|
+
const id = detailsEl.getAttribute('data-raw-json-id');
|
|
2351
|
+
const pre = detailsEl.querySelector('pre');
|
|
2352
|
+
if (pre && rawJsonPayloadStore.has(id)) {
|
|
2353
|
+
pre.textContent = JSON.stringify(rawJsonPayloadStore.get(id), null, 2);
|
|
2354
|
+
rawJsonPayloadStore.delete(id);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
function extractThinkingText(payload) {
|
|
2359
|
+
if (!payload) return '';
|
|
2360
|
+
if (typeof payload === 'string') {
|
|
2361
|
+
try {
|
|
2362
|
+
const parsed = JSON.parse(payload);
|
|
2363
|
+
return parsed.thought || parsed.text || parsed.content || payload;
|
|
2364
|
+
} catch (e) {
|
|
2365
|
+
return payload;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
if (typeof payload === 'object') {
|
|
2369
|
+
return payload.thought || payload.text || payload.content || JSON.stringify(payload, null, 2);
|
|
2370
|
+
}
|
|
2371
|
+
return String(payload);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
function detectAndRenderNativeThoughts(payload) {
|
|
2375
|
+
let thoughts = [];
|
|
2376
|
+
if (payload && payload.response && Array.isArray(payload.response)) {
|
|
2377
|
+
payload.response.forEach(gen => {
|
|
2378
|
+
if (gen && typeof gen === 'object') {
|
|
2379
|
+
const content = gen.content;
|
|
2380
|
+
if (Array.isArray(content)) {
|
|
2381
|
+
content.forEach(block => {
|
|
2382
|
+
if (block && block.type === 'thinking' && block.thinking) {
|
|
2383
|
+
thoughts.push(block.thinking.trim ? block.thinking.trim() : block.thinking);
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
if (thoughts.length === 0) return '';
|
|
2391
|
+
const thinkingText = thoughts.join('\n\n');
|
|
2392
|
+
return `
|
|
2393
|
+
<div class="thought-container" style="background-color: #f0fdf4; border-left: 4px solid #16a34a; padding: 16px; margin-bottom: 15px; border-radius: 8px; border: 1px solid #dcfce7; border-left-width: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);">
|
|
2394
|
+
<div style="font-weight: 700; color: #16a34a; margin-bottom: 8px; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; display: flex; align-items: center; gap: 6px;">
|
|
2395
|
+
<span>🧠 Model Native Thinking</span>
|
|
2396
|
+
</div>
|
|
2397
|
+
<div style="white-space: pre-wrap; font-family: inherit; color: #1e293b; font-size: 0.9rem; line-height: 1.6;">${escapeHtml(thinkingText)}</div>
|
|
2398
|
+
</div>
|
|
2399
|
+
`;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
function renderLLMCallPayload(payload) {
|
|
2403
|
+
let html = '';
|
|
2404
|
+
|
|
2405
|
+
// 1. Native thinking block at the very top
|
|
2406
|
+
const thinkingHtml = detectAndRenderNativeThoughts(payload);
|
|
2407
|
+
if (thinkingHtml) {
|
|
2408
|
+
html += thinkingHtml;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
// 2. Error message if present
|
|
2412
|
+
if (payload.error) {
|
|
2413
|
+
html += `
|
|
2414
|
+
<div style="background-color: #fef2f2; border: 1px solid #fee2e2; border-left: 4px solid #ef4444; border-radius: 6px; padding: 15px; margin-bottom: 15px;">
|
|
2415
|
+
<h4 style="margin: 0 0 8px 0; color: #ef4444; font-weight: bold; font-size: 0.85rem; text-transform: uppercase;">❌ LLM Error</h4>
|
|
2416
|
+
<pre class="light-code" style="margin: 0; font-size: 0.85rem; color: #991b1b; font-family: monospace; white-space: pre-wrap;">${escapeHtml(payload.error)}</pre>
|
|
2417
|
+
</div>
|
|
2418
|
+
`;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// 3. Conversation thread messages
|
|
2422
|
+
if (payload.messages && Array.isArray(payload.messages)) {
|
|
2423
|
+
html += '<div class="llm-conversation" style="display: flex; flex-direction: column; gap: 16px; margin-bottom: 20px;">';
|
|
2424
|
+
|
|
2425
|
+
payload.messages.forEach(msg => {
|
|
2426
|
+
const isSystem = msg.type === 'system';
|
|
2427
|
+
const roleColor = isSystem ? '#475569' : (msg.type === 'human' ? '#2563eb' : '#059669');
|
|
2428
|
+
const roleBg = isSystem ? '#f8fafc' : (msg.type === 'human' ? '#eff6ff' : '#ecfdf5');
|
|
2429
|
+
const roleLabel = isSystem ? 'System Prompt' : (msg.type === 'human' ? 'User / Prompt' : 'AI / Assistant');
|
|
2430
|
+
|
|
2431
|
+
if (isSystem) {
|
|
2432
|
+
html += `
|
|
2433
|
+
<details style="border: 1px solid #e2e8f0; border-radius: 6px; background-color: #f8fafc; overflow: hidden;">
|
|
2434
|
+
<summary style="padding: 10px 15px; font-weight: bold; font-size: 0.85rem; color: #475569; cursor: pointer; background-color: #f1f5f9; outline: none; user-select: none;">
|
|
2435
|
+
⚙️ ${roleLabel} (Collapsible, click to expand)
|
|
2436
|
+
</summary>
|
|
2437
|
+
<div style="padding: 15px; border-top: 1px solid #e2e8f0; white-space: pre-wrap; font-family: monospace; font-size: 0.82rem; color: #334155; max-height: 250px; overflow-y: auto; line-height: 1.5;">${escapeHtml(msg.content)}</div>
|
|
2438
|
+
</details>
|
|
2439
|
+
`;
|
|
2440
|
+
} else {
|
|
2441
|
+
let contentHtml = '';
|
|
2442
|
+
if (typeof msg.content === 'string') {
|
|
2443
|
+
contentHtml = `<div style="white-space: pre-wrap; font-family: inherit; font-size: 0.88rem; line-height: 1.5; color: #1e293b;">${escapeHtml(msg.content)}</div>`;
|
|
2444
|
+
} else if (Array.isArray(msg.content)) {
|
|
2445
|
+
contentHtml = '<div style="display: flex; flex-direction: column; gap: 10px;">';
|
|
2446
|
+
msg.content.forEach(block => {
|
|
2447
|
+
if (block.type === 'text' && block.text) {
|
|
2448
|
+
contentHtml += `<div style="white-space: pre-wrap; font-family: inherit; font-size: 0.88rem; line-height: 1.5; color: #1e293b;">${escapeHtml(block.text)}</div>`;
|
|
2449
|
+
} else if (block.type === 'image_url' && block.image_url && block.image_url.url) {
|
|
2450
|
+
contentHtml += `
|
|
2451
|
+
<div class="media-container" style="max-width: 280px; border: 1px solid #e2e8f0; border-radius: 6px; overflow: hidden; background-color: #f8fafc; cursor: zoom-in;" data-zoom-img>
|
|
2452
|
+
<img src="${escapeHtml(block.image_url.url)}" style="width: 100%; display: block;"/>
|
|
2453
|
+
</div>
|
|
2454
|
+
`;
|
|
2455
|
+
} else {
|
|
2456
|
+
contentHtml += `<pre style="margin: 0; font-size: 0.8rem; background-color: #f1f5f9; padding: 5px; border-radius: 4px; color: #334155;">${escapeHtml(JSON.stringify(block, null, 2))}</pre>`;
|
|
2457
|
+
}
|
|
2458
|
+
});
|
|
2459
|
+
contentHtml += '</div>';
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
html += `
|
|
2463
|
+
<div class="msg-bubble" style="background-color: ${roleBg}; border: 1px solid ${roleColor}20; border-radius: 8px; overflow: hidden; border-left: 4px solid ${roleColor}; box-shadow: 0 1px 2px rgba(0,0,0,0.02);">
|
|
2464
|
+
<div style="padding: 6px 12px; background-color: ${roleColor}08; border-bottom: 1px solid ${roleColor}0f; color: ${roleColor}; font-weight: bold; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em;">
|
|
2465
|
+
${roleLabel}
|
|
2466
|
+
</div>
|
|
2467
|
+
<div style="padding: 12px 15px;">
|
|
2468
|
+
${contentHtml}
|
|
2469
|
+
</div>
|
|
2470
|
+
</div>
|
|
2471
|
+
`;
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
|
|
2475
|
+
html += '</div>';
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
// 4. Response output if not already fully merged
|
|
2479
|
+
if (payload.response && Array.isArray(payload.response)) {
|
|
2480
|
+
html += '<div style="margin-top: 15px; display: flex; flex-direction: column; gap: 12px;">';
|
|
2481
|
+
payload.response.forEach((gen, idx) => {
|
|
2482
|
+
let textContent = '';
|
|
2483
|
+
let structuredBlocks = [];
|
|
2484
|
+
|
|
2485
|
+
if (typeof gen === 'string') {
|
|
2486
|
+
textContent = gen;
|
|
2487
|
+
} else if (gen && typeof gen === 'object') {
|
|
2488
|
+
if (typeof gen.content === 'string') {
|
|
2489
|
+
textContent = gen.content;
|
|
2490
|
+
} else if (Array.isArray(gen.content)) {
|
|
2491
|
+
structuredBlocks = gen.content;
|
|
2492
|
+
} else if (gen.text) {
|
|
2493
|
+
textContent = gen.text;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
let responseHtml = '';
|
|
2498
|
+
if (structuredBlocks.length > 0) {
|
|
2499
|
+
responseHtml = '<div style="display: flex; flex-direction: column; gap: 10px;">';
|
|
2500
|
+
structuredBlocks.forEach(block => {
|
|
2501
|
+
if (block.type === 'text' && block.text) {
|
|
2502
|
+
responseHtml += `<div style="white-space: pre-wrap; font-family: inherit; font-size: 0.88rem; line-height: 1.5; color: #1e293b;">${escapeHtml(block.text)}</div>`;
|
|
2503
|
+
} else if (block.type === 'thinking') {
|
|
2504
|
+
responseHtml += `
|
|
2505
|
+
<div style="font-size: 0.8rem; color: #16a34a; font-style: italic; font-weight: bold; border-left: 2px solid #16a34a; padding-left: 6px; margin: 4px 0;">
|
|
2506
|
+
↳ [Native thinking block processed and displayed above]
|
|
2507
|
+
</div>
|
|
2508
|
+
`;
|
|
2509
|
+
} else {
|
|
2510
|
+
responseHtml += `<pre style="margin: 0; font-size: 0.8rem; background-color: #f1f5f9; padding: 5px; border-radius: 4px; color: #334155;">${escapeHtml(JSON.stringify(block, null, 2))}</pre>`;
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
responseHtml += '</div>';
|
|
2514
|
+
} else if (textContent) {
|
|
2515
|
+
responseHtml = `<div style="white-space: pre-wrap; font-family: inherit; font-size: 0.88rem; line-height: 1.5; color: #1e293b;">${escapeHtml(textContent)}</div>`;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
if (responseHtml) {
|
|
2519
|
+
html += `
|
|
2520
|
+
<div class="msg-bubble" style="background-color: #ecfdf5; border: 1px solid #05966920; border-radius: 8px; overflow: hidden; border-left: 4px solid #059669; box-shadow: 0 1px 2px rgba(0,0,0,0.02);">
|
|
2521
|
+
<div style="padding: 6px 12px; background-color: #05966908; border-bottom: 1px solid #0596690f; color: #059669; font-weight: bold; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; display: flex; justify-content: space-between; align-items: center;">
|
|
2522
|
+
<span>AI Response / Output</span>
|
|
2523
|
+
${payload.response.length > 1 ? `<span style="font-size: 0.7rem; background-color: #059669; color: white; padding: 1px 5px; border-radius: 4px;">Gen ${idx+1}</span>` : ''}
|
|
2524
|
+
</div>
|
|
2525
|
+
<div style="padding: 12px 15px;">
|
|
2526
|
+
${responseHtml}
|
|
2527
|
+
</div>
|
|
2528
|
+
</div>
|
|
2529
|
+
`;
|
|
2530
|
+
}
|
|
2531
|
+
});
|
|
2532
|
+
html += '</div>';
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// 5. Add custom media render blocks (screenshots etc.) if any match the parameters
|
|
2536
|
+
const mediaHtml = detectAndRenderMedia(payload);
|
|
2537
|
+
if (mediaHtml) {
|
|
2538
|
+
html += `<div style="margin-top: 15px;">${mediaHtml}</div>`;
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
// 6. Collapsible Raw JSON block at the bottom (lazily filled on first expand)
|
|
2542
|
+
html += `
|
|
2543
|
+
<details style="margin-top: 25px; border: 1px solid #e2e8f0; border-radius: 6px; overflow: hidden;" data-raw-json-id="${registerRawJsonPayload(payload)}" ontoggle="onRawJsonToggle(this)">
|
|
2544
|
+
<summary style="padding: 8px 12px; font-weight: bold; font-size: 0.8rem; color: #64748b; cursor: pointer; background-color: #f8fafc; outline: none; user-select: none;">
|
|
2545
|
+
⚙️ View Raw JSON Payload
|
|
2546
|
+
</summary>
|
|
2547
|
+
<div style="padding: 15px; border-top: 1px solid #e2e8f0; background-color: #0f172a; overflow-x: auto;">
|
|
2548
|
+
<pre style="margin: 0; font-size: 0.8rem; color: #f8fafc; font-family: monospace; white-space: pre-wrap; max-height: 350px; overflow-y: auto;"></pre>
|
|
2549
|
+
</div>
|
|
2550
|
+
</details>
|
|
2551
|
+
`;
|
|
2552
|
+
|
|
2553
|
+
return html;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
function renderToolPayload(payload) {
|
|
2557
|
+
let html = '';
|
|
2558
|
+
|
|
2559
|
+
if (payload.args) {
|
|
2560
|
+
html += `
|
|
2561
|
+
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 12px 15px; margin-bottom: 12px; border-left: 4px solid #64748b; box-shadow: 0 1px 2px rgba(0,0,0,0.02);">
|
|
2562
|
+
<h4 style="margin: 0 0 6px 0; color: #475569; font-size: 0.8rem; font-weight: bold; text-transform: uppercase; letter-spacing: 0.05em;">🔧 Tool Arguments</h4>
|
|
2563
|
+
<pre class="light-code" style="margin: 0; font-size: 0.82rem; color: #334155; font-family: monospace; white-space: pre-wrap; line-height: 1.4;">${escapeHtml(JSON.stringify(payload.args, null, 2))}</pre>
|
|
2564
|
+
</div>
|
|
2565
|
+
`;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
if (payload.result !== undefined && payload.result !== null) {
|
|
2569
|
+
const resultText = typeof payload.result === 'object' ? JSON.stringify(payload.result, null, 2) : String(payload.result);
|
|
2570
|
+
html += `
|
|
2571
|
+
<div style="background-color: #f0fdf4; border: 1px solid #dcfce7; border-radius: 6px; padding: 12px 15px; margin-bottom: 12px; border-left: 4px solid #16a34a; box-shadow: 0 1px 2px rgba(0,0,0,0.02);">
|
|
2572
|
+
<h4 style="margin: 0 0 6px 0; color: #16a34a; font-size: 0.8rem; font-weight: bold; text-transform: uppercase; letter-spacing: 0.05em;">🟢 Tool Result</h4>
|
|
2573
|
+
<pre class="light-code" style="margin: 0; font-size: 0.82rem; color: #14532d; font-family: monospace; white-space: pre-wrap; line-height: 1.4;">${escapeHtml(resultText)}</pre>
|
|
2574
|
+
</div>
|
|
2575
|
+
`;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
if (payload.error) {
|
|
2579
|
+
html += `
|
|
2580
|
+
<div style="background-color: #fef2f2; border: 1px solid #fee2e2; border-radius: 6px; padding: 12px 15px; margin-bottom: 12px; border-left: 4px solid #ef4444; box-shadow: 0 1px 2px rgba(0,0,0,0.02);">
|
|
2581
|
+
<h4 style="margin: 0 0 6px 0; color: #ef4444; font-size: 0.8rem; font-weight: bold; text-transform: uppercase; letter-spacing: 0.05em;">🔴 Tool Error</h4>
|
|
2582
|
+
<pre class="light-code" style="margin: 0; font-size: 0.82rem; color: #991b1b; font-family: monospace; white-space: pre-wrap; line-height: 1.4;">${escapeHtml(payload.error)}</pre>
|
|
2583
|
+
</div>
|
|
2584
|
+
`;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// Add custom media render blocks (screenshots etc.) if any match the parameters
|
|
2588
|
+
const mediaHtml = detectAndRenderMedia(payload);
|
|
2589
|
+
if (mediaHtml) {
|
|
2590
|
+
html += `<div style="margin-top: 15px;">${mediaHtml}</div>`;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// Collapsible Raw JSON block at the bottom (lazily filled on first expand)
|
|
2594
|
+
html += `
|
|
2595
|
+
<details style="margin-top: 15px; border: 1px solid #e2e8f0; border-radius: 6px; overflow: hidden;" data-raw-json-id="${registerRawJsonPayload(payload)}" ontoggle="onRawJsonToggle(this)">
|
|
2596
|
+
<summary style="padding: 6px 10px; font-weight: bold; font-size: 0.75rem; color: #64748b; cursor: pointer; background-color: #f8fafc; outline: none; user-select: none;">
|
|
2597
|
+
⚙️ View Raw JSON Payload
|
|
2598
|
+
</summary>
|
|
2599
|
+
<div style="padding: 10px; border-top: 1px solid #e2e8f0; background-color: #0f172a; overflow-x: auto;">
|
|
2600
|
+
<pre style="margin: 0; font-size: 0.8rem; color: #f8fafc; font-family: monospace; white-space: pre-wrap; max-height: 250px; overflow-y: auto;"></pre>
|
|
2601
|
+
</div>
|
|
2602
|
+
</details>
|
|
2603
|
+
`;
|
|
2604
|
+
|
|
2605
|
+
return html;
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
async function loadSteps(silent = false) {
|
|
2609
|
+
if (!currentSessionId) return;
|
|
2610
|
+
const container = document.getElementById('steps-container');
|
|
2611
|
+
if (!container) return;
|
|
2612
|
+
|
|
2613
|
+
const existingCards = container.querySelectorAll('.step-card');
|
|
2614
|
+
|
|
2615
|
+
if (!silent) {
|
|
2616
|
+
container.innerHTML = '<div style="padding: 30px; color: #718096; text-align: center; font-size: 1rem;"><div class="spinner" style="width: 20px; height: 20px; border: 2px solid #3182ce; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; display: inline-block; margin-right: 8px; vertical-align: middle;"></div>Loading steps timeline...</div>';
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
try {
|
|
2620
|
+
const res = await fetch(`/api/sessions/${currentSessionId}/steps`);
|
|
2621
|
+
if (!res.ok) throw new Error(await res.text());
|
|
2622
|
+
const steps = await res.json();
|
|
2623
|
+
|
|
2624
|
+
if (steps.length === 0) {
|
|
2625
|
+
container.innerHTML = '<div style="padding: 40px; color: #718096; text-align: center; font-size: 1.1rem;">👣 No steps recorded yet for this session.</div>';
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// Silent update optimization: if card count matches step count, return immediately to preserve DOM & expand state
|
|
2630
|
+
if (silent && existingCards.length === steps.length) {
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
// Append only new steps when updating silently
|
|
2635
|
+
let stepsToRender = steps;
|
|
2636
|
+
if (silent && existingCards.length > 0 && steps.length > existingCards.length) {
|
|
2637
|
+
stepsToRender = steps.slice(existingCards.length);
|
|
2638
|
+
} else {
|
|
2639
|
+
container.innerHTML = '';
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
stepsToRender.forEach(step => {
|
|
2643
|
+
const hasPreImage = step.pre_image_name && step.pre_image_name !== 'None' && step.pre_image_name !== 'null' && String(step.pre_image_name).trim() !== '';
|
|
2644
|
+
let hasPostImage = step.post_image_name && step.post_image_name !== 'None' && step.post_image_name !== 'null' && String(step.post_image_name).trim() !== '';
|
|
2645
|
+
// If pre and post are the exact same image, only show one (pre)
|
|
2646
|
+
if (hasPreImage && hasPostImage && step.pre_image_name === step.post_image_name) {
|
|
2647
|
+
hasPostImage = false;
|
|
2648
|
+
}
|
|
2649
|
+
const hasImages = hasPreImage || hasPostImage;
|
|
2650
|
+
|
|
2651
|
+
const stepCard = document.createElement('div');
|
|
2652
|
+
stepCard.className = 'step-card';
|
|
2653
|
+
stepCard.id = `step-card-${step.step_id}`;
|
|
2654
|
+
stepCard.style.cssText = 'background: white; border: 1px solid #e2e8f0; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.03); overflow: hidden;';
|
|
2655
|
+
|
|
2656
|
+
// Header HTML
|
|
2657
|
+
const headerHtml = `
|
|
2658
|
+
<div style="background-color: #f8fafc; border-bottom: 1px solid #e2e8f0; padding: 12px 20px; display: flex; justify-content: space-between; align-items: center;">
|
|
2659
|
+
<h3 style="margin: 0; color: #1e293b; font-size: 1.05rem; display: flex; align-items: center; gap: 10px;">
|
|
2660
|
+
<span style="background-color: #3182ce; color: white; border-radius: 50%; width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; font-size: 0.8rem; font-weight: bold;">${step.step_number}</span>
|
|
2661
|
+
<span>${escapeHtml(step.summary || 'Step Executing...')}</span>
|
|
2662
|
+
</h3>
|
|
2663
|
+
<span style="color: #64748b; font-size: 0.85rem; font-weight: 500;">${formatRelativeTime(step.timestamp)}</span>
|
|
2664
|
+
</div>
|
|
2665
|
+
`;
|
|
2666
|
+
|
|
2667
|
+
// Content panel grid
|
|
2668
|
+
let contentHtml = `<div style="padding: 20px; display: flex; flex-direction: column; gap: 16px;">`;
|
|
2669
|
+
contentHtml += `<div style="display: grid; grid-template-columns: ${hasImages ? '1fr 1fr' : '1fr'}; gap: 20px;">`;
|
|
2670
|
+
|
|
2671
|
+
// Left Column (Thoughts and Actions)
|
|
2672
|
+
contentHtml += `<div style="display: flex; flex-direction: column; gap: 12px;">`;
|
|
2673
|
+
|
|
2674
|
+
if (step.operator_native_thinking) {
|
|
2675
|
+
contentHtml += `
|
|
2676
|
+
<div class="thinking-box native" style="background-color: #f0fdf4; border-left: 4px solid #16a34a; border-radius: 6px; padding: 12px 15px; border: 1px solid #dcfce7; border-left-width: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.02);">
|
|
2677
|
+
<h4 style="margin: 0 0 6px 0; color: #16a34a; font-size: 0.85rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; display: flex; align-items: center; gap: 6px;">
|
|
2678
|
+
<span>🧠 Native Thinking Process</span>
|
|
2679
|
+
</h4>
|
|
2680
|
+
<div style="white-space: pre-wrap; font-size: 0.88rem; color: #1f2937; line-height: 1.6; font-family: inherit;">${escapeHtml(step.operator_native_thinking)}</div>
|
|
2681
|
+
</div>
|
|
2682
|
+
`;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
if (step.operator_raw_thinking && isHumanThinking(step.operator_raw_thinking)) {
|
|
2686
|
+
contentHtml += `
|
|
2687
|
+
<div class="thinking-box raw" style="background-color: #f8fafc; border-left: 4px solid #475569; border-radius: 6px; padding: 12px 15px; border: 1px solid #e2e8f0; border-left-width: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.02);">
|
|
2688
|
+
<h4 style="margin: 0 0 6px 0; color: #475569; font-size: 0.85rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; display: flex; align-items: center; gap: 6px;">
|
|
2689
|
+
<span>📝 Raw Thinking / Plan</span>
|
|
2690
|
+
</h4>
|
|
2691
|
+
<div style="white-space: pre-wrap; font-size: 0.88rem; color: #374151; line-height: 1.6; font-family: inherit;">${escapeHtml(step.operator_raw_thinking)}</div>
|
|
2692
|
+
</div>
|
|
2693
|
+
`;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
if (step.action_taken) {
|
|
2697
|
+
contentHtml += `
|
|
2698
|
+
<div style="background-color: #eff6ff; border: 1px solid #dbeafe; border-radius: 6px; padding: 12px 15px; border-left: 4px solid #2563eb;">
|
|
2699
|
+
<h4 style="margin: 0 0 6px 0; color: #1d4ed8; font-size: 0.8rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;">⚡ Action Executed</h4>
|
|
2700
|
+
<pre class="light-code" style="margin: 0; font-size: 0.82rem; color: #1e3a8a; font-family: monospace; white-space: pre-wrap; line-height: 1.4;">${escapeHtml(JSON.stringify(step.action_taken, null, 2))}</pre>
|
|
2701
|
+
</div>
|
|
2702
|
+
`;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
if (step.last_execution_result) {
|
|
2706
|
+
const isSuccess = step.last_execution_result.success !== false;
|
|
2707
|
+
const resColor = isSuccess ? '#16a34a' : '#dc2626';
|
|
2708
|
+
const resBg = isSuccess ? '#f0fdf4' : '#fef2f2';
|
|
2709
|
+
const resBorder = isSuccess ? '#dcfce7' : '#fee2e2';
|
|
2710
|
+
|
|
2711
|
+
contentHtml += `
|
|
2712
|
+
<div style="background-color: ${resBg}; border: 1px solid ${resBorder}; border-radius: 6px; padding: 12px 15px; border-left: 4px solid ${resColor};">
|
|
2713
|
+
<h4 style="margin: 0 0 6px 0; color: ${resColor}; font-size: 0.8rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;">✓ Execution/Validation Result</h4>
|
|
2714
|
+
<pre class="light-code" style="margin: 0; font-size: 0.82rem; color: ${isSuccess ? '#14532d' : '#7f1d1d'}; font-family: monospace; white-space: pre-wrap; line-height: 1.4;">${escapeHtml(JSON.stringify(step.last_execution_result, null, 2))}</pre>
|
|
2715
|
+
</div>
|
|
2716
|
+
`;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
contentHtml += `</div>`; // End Left Column
|
|
2720
|
+
|
|
2721
|
+
// Right Column (Screenshots)
|
|
2722
|
+
if (hasImages) {
|
|
2723
|
+
contentHtml += `<div style="display: flex; flex-direction: column; gap: 15px; align-items: center; justify-content: start;">`;
|
|
2724
|
+
contentHtml += `<div style="display: flex; gap: 12px; width: 100%; justify-content: center;">`;
|
|
2725
|
+
|
|
2726
|
+
if (hasPreImage) {
|
|
2727
|
+
const preSrc = (step.pre_image_name.startsWith('data:') || step.pre_image_name.startsWith('http') || step.pre_image_name.startsWith('/'))
|
|
2728
|
+
? step.pre_image_name
|
|
2729
|
+
: `/images/${step.pre_image_name}`;
|
|
2730
|
+
contentHtml += `
|
|
2731
|
+
<div style="flex: 1; text-align: center; display: flex; flex-direction: column; gap: 6px; max-width: ${hasPostImage ? '100%' : '360px'}; margin: 0 auto;">
|
|
2732
|
+
<span style="font-size: 0.75rem; font-weight: 600; color: #64748b;">${hasPostImage ? 'Pre-Action (Before)' : 'Screenshot'}</span>
|
|
2733
|
+
<div class="image-wrapper" style="border: 1px solid #e2e8f0; border-radius: 6px; overflow: hidden; background-color: #f8fafc; cursor: zoom-in; box-shadow: 0 1px 3px rgba(0,0,0,0.05); display: flex; align-items: center; justify-content: center;" onclick="zoomImage('${escapeHtml(preSrc)}')">
|
|
2734
|
+
<div style="position: relative; display: inline-block; max-width: 100%;">
|
|
2735
|
+
<img id="step-img-${escapeHtml(step.step_id)}" src="${escapeHtml(preSrc)}" style="max-width: 100%; max-height: 340px; object-fit: contain; display: block; margin: 0 auto; transition: transform 0.2s;"/>
|
|
2736
|
+
<div id="step-overlay-${escapeHtml(step.step_id)}" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none;"></div>
|
|
2737
|
+
</div>
|
|
2738
|
+
</div>
|
|
2739
|
+
</div>
|
|
2740
|
+
`;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
if (hasPostImage) {
|
|
2744
|
+
const postSrc = (step.post_image_name.startsWith('data:') || step.post_image_name.startsWith('http') || step.post_image_name.startsWith('/'))
|
|
2745
|
+
? step.post_image_name
|
|
2746
|
+
: `/images/${step.post_image_name}`;
|
|
2747
|
+
contentHtml += `
|
|
2748
|
+
<div style="flex: 1; text-align: center; display: flex; flex-direction: column; gap: 6px; max-width: ${hasPreImage ? '100%' : '360px'}; margin: 0 auto;">
|
|
2749
|
+
<span style="font-size: 0.75rem; font-weight: 600; color: #64748b;">Post-Action (After)</span>
|
|
2750
|
+
<div class="image-wrapper" style="border: 1px solid #e2e8f0; border-radius: 6px; overflow: hidden; background-color: #f8fafc; cursor: zoom-in; box-shadow: 0 1px 3px rgba(0,0,0,0.05);" onclick="zoomImage('${escapeHtml(postSrc)}')">
|
|
2751
|
+
<img src="${escapeHtml(postSrc)}" style="max-width: 100%; max-height: 340px; object-fit: contain; display: block; margin: 0 auto; transition: transform 0.2s;"/>
|
|
2752
|
+
</div>
|
|
2753
|
+
</div>
|
|
2754
|
+
`;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
contentHtml += `</div>`;
|
|
2758
|
+
contentHtml += `</div>`; // End Right Column
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
contentHtml += `</div>`; // End Grid Column
|
|
2762
|
+
|
|
2763
|
+
// Sub-Traces section (mini-expander)
|
|
2764
|
+
const isOpen = userExpandedStepIds.has(step.step_id);
|
|
2765
|
+
const isLoaded = stepTracesCache[step.step_id] !== undefined;
|
|
2766
|
+
|
|
2767
|
+
contentHtml += `
|
|
2768
|
+
<div class="step-traces-expander" style="border-top: 1px solid #f1f5f9; padding-top: 15px; margin-top: 5px;">
|
|
2769
|
+
<details ${isOpen ? 'open' : ''} ontoggle="onStepDetailsToggle(this, '${escapeHtml(step.step_id)}')">
|
|
2770
|
+
<summary style="font-size: 0.82rem; font-weight: 600; color: #475569; cursor: pointer; display: flex; align-items: center; justify-content: space-between; user-select: none; outline: none; padding-right: 5px;">
|
|
2771
|
+
<div style="display: flex; align-items: center; gap: 6px;">
|
|
2772
|
+
🔍 Click to inspect Step Details & Sub-Traces
|
|
2773
|
+
</div>
|
|
2774
|
+
<div onclick="event.stopPropagation();" style="display: flex; align-items: center; gap: 6px; font-weight: normal;">
|
|
2775
|
+
<label for="trace-filter-${escapeHtml(step.step_id)}" style="font-size: 0.75rem; color: #64748b;">Trace Type:</label>
|
|
2776
|
+
<select id="trace-filter-${escapeHtml(step.step_id)}" onchange="filterStepTraces('${escapeHtml(step.step_id)}', this.value)" onclick="event.stopPropagation();" style="font-size: 0.75rem; padding: 2px 8px; border-radius: 4px; border: 1px solid #cbd5e1; background: white; color: #334155; cursor: pointer; outline: none; font-weight: 500;">
|
|
2777
|
+
<option value="all">All</option>
|
|
2778
|
+
</select>
|
|
2779
|
+
</div>
|
|
2780
|
+
</summary>
|
|
2781
|
+
<div class="step-child-traces-list" id="step-traces-${escapeHtml(step.step_id)}" ${isLoaded ? 'data-loaded="true"' : ''} style="margin-top: 12px; padding: 12px 15px; border-left: 2px dashed #cbd5e1; background-color: #f8fafc; border-radius: 0 6px 6px 0; display: flex; flex-direction: column; gap: 8px;">
|
|
2782
|
+
<div style="font-size: 0.8rem; color: #64748b;">Loading step child traces...</div>
|
|
2783
|
+
</div>
|
|
2784
|
+
</details>
|
|
2785
|
+
</div>
|
|
2786
|
+
`;
|
|
2787
|
+
|
|
2788
|
+
contentHtml += `</div>`; // End Step Content Area
|
|
2789
|
+
|
|
2790
|
+
stepCard.innerHTML = headerHtml + contentHtml;
|
|
2791
|
+
container.appendChild(stepCard);
|
|
2792
|
+
|
|
2793
|
+
if (hasPreImage && step.action_taken) {
|
|
2794
|
+
setupImageCoordinates(`step-img-${step.step_id}`, `step-overlay-${step.step_id}`, step.action_taken);
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
if (isOpen) {
|
|
2798
|
+
const targetDiv = document.getElementById(`step-traces-${step.step_id}`);
|
|
2799
|
+
const details = targetDiv ? targetDiv.closest('details') : null;
|
|
2800
|
+
loadStepTraces(step.step_id, details);
|
|
2801
|
+
}
|
|
2802
|
+
});
|
|
2803
|
+
} catch (err) {
|
|
2804
|
+
container.innerHTML = `<div style="padding: 20px; color: #dc2626; text-align: center; font-weight: bold;">Error loading steps timeline: ${escapeHtml(err.message)}</div>`;
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
let stepTracesCache = {};
|
|
2809
|
+
let stepFilterMap = {};
|
|
2810
|
+
|
|
2811
|
+
function getUniqueTraceTypes(nodes) {
|
|
2812
|
+
const types = new Set();
|
|
2813
|
+
function collect(nodeList) {
|
|
2814
|
+
if (!nodeList) return;
|
|
2815
|
+
nodeList.forEach(node => {
|
|
2816
|
+
if (node.type) {
|
|
2817
|
+
types.add(node.type);
|
|
2818
|
+
}
|
|
2819
|
+
if (node.children && node.children.length > 0) {
|
|
2820
|
+
collect(node.children);
|
|
2821
|
+
}
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
collect(nodes);
|
|
2825
|
+
return Array.from(types).sort();
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
function populateTraceTypeDropdown(stepId, nodes) {
|
|
2829
|
+
const select = document.getElementById(`trace-filter-${stepId}`);
|
|
2830
|
+
if (!select) return;
|
|
2831
|
+
|
|
2832
|
+
const uniqueTypes = getUniqueTraceTypes(nodes);
|
|
2833
|
+
const currentVal = stepFilterMap[stepId] || 'all';
|
|
2834
|
+
|
|
2835
|
+
select.innerHTML = '<option value="all">All</option>';
|
|
2836
|
+
|
|
2837
|
+
uniqueTypes.forEach(type => {
|
|
2838
|
+
const opt = document.createElement('option');
|
|
2839
|
+
opt.value = type;
|
|
2840
|
+
const label = type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
2841
|
+
opt.textContent = label;
|
|
2842
|
+
select.appendChild(opt);
|
|
2843
|
+
});
|
|
2844
|
+
|
|
2845
|
+
select.value = uniqueTypes.includes(currentVal) ? currentVal : 'all';
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
function filterStepTraces(stepId, selectedType) {
|
|
2849
|
+
stepFilterMap[stepId] = selectedType;
|
|
2850
|
+
const targetDiv = document.getElementById(`step-traces-${stepId}`);
|
|
2851
|
+
if (!targetDiv || !stepTracesCache[stepId]) return;
|
|
2852
|
+
|
|
2853
|
+
targetDiv.innerHTML = '';
|
|
2854
|
+
renderMiniTree(stepTracesCache[stepId], targetDiv, 0, selectedType);
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
async function loadStepTraces(stepId, detailsElement, force = false) {
|
|
2858
|
+
const targetDiv = document.getElementById(`step-traces-${stepId}`);
|
|
2859
|
+
if (!targetDiv) return;
|
|
2860
|
+
if (!force && targetDiv.getAttribute('data-loaded') === 'true') return;
|
|
2861
|
+
|
|
2862
|
+
const currentFilter = stepFilterMap[stepId] || 'all';
|
|
2863
|
+
|
|
2864
|
+
if (!force && stepTracesCache[stepId]) {
|
|
2865
|
+
targetDiv.innerHTML = '';
|
|
2866
|
+
targetDiv.setAttribute('data-loaded', 'true');
|
|
2867
|
+
populateTraceTypeDropdown(stepId, stepTracesCache[stepId]);
|
|
2868
|
+
renderMiniTree(stepTracesCache[stepId], targetDiv, 0, currentFilter);
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
try {
|
|
2873
|
+
const res = await fetch(`/api/steps/${stepId}/traces?session_id=${currentSessionId || ''}`);
|
|
2874
|
+
if (!res.ok) {
|
|
2875
|
+
targetDiv.innerHTML = '<div style="font-size: 0.8rem; color: #64748b;">No direct tool or LLM traces associated with this step.</div>';
|
|
2876
|
+
targetDiv.setAttribute('data-loaded', 'true');
|
|
2877
|
+
populateTraceTypeDropdown(stepId, []);
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
const data = await res.json();
|
|
2881
|
+
|
|
2882
|
+
stepTracesCache[stepId] = data;
|
|
2883
|
+
targetDiv.innerHTML = '';
|
|
2884
|
+
targetDiv.setAttribute('data-loaded', 'true');
|
|
2885
|
+
|
|
2886
|
+
populateTraceTypeDropdown(stepId, data);
|
|
2887
|
+
|
|
2888
|
+
if (data.length === 0) {
|
|
2889
|
+
targetDiv.innerHTML = '<div style="font-size: 0.8rem; color: #64748b;">No direct tool or LLM traces associated with this step.</div>';
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
renderMiniTree(data, targetDiv, 0, currentFilter);
|
|
2894
|
+
|
|
2895
|
+
// Ensure streams are rendered for newly attached DOM elements
|
|
2896
|
+
for (let traceId in nodeStreams) {
|
|
2897
|
+
updateStreamDisplay(traceId);
|
|
2898
|
+
}
|
|
2899
|
+
} catch (err) {
|
|
2900
|
+
targetDiv.innerHTML = `<div style="font-size: 0.8rem; color: #dc2626;">Failed to load traces: ${escapeHtml(err.message)}</div>`;
|
|
2901
|
+
populateTraceTypeDropdown(stepId, []);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
function hasMatchingDescendant(children, filterType) {
|
|
2906
|
+
if (filterType === 'all' || !children) return false;
|
|
2907
|
+
for (const child of children) {
|
|
2908
|
+
if (child.type === filterType || hasMatchingDescendant(child.children, filterType)) {
|
|
2909
|
+
return true;
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
return false;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
function renderMiniTree(nodes, container, depth = 0, filterType = 'all') {
|
|
2916
|
+
nodes.forEach(node => {
|
|
2917
|
+
const selfMatches = (filterType === 'all' || node.type === filterType);
|
|
2918
|
+
const childMatches = hasMatchingDescendant(node.children, filterType);
|
|
2919
|
+
|
|
2920
|
+
if (!selfMatches && !childMatches) {
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
const nodeRow = document.createElement('div');
|
|
2925
|
+
nodeRow.style.marginLeft = `${depth * 16}px`;
|
|
2926
|
+
nodeRow.style.marginBottom = '6px';
|
|
2927
|
+
|
|
2928
|
+
if (node.type === 'raw_thinking' || node.type === 'thinking') {
|
|
2929
|
+
const thoughtText = extractThinkingText(node.payload);
|
|
2930
|
+
const isNative = node.type === 'thinking';
|
|
2931
|
+
const boxColor = isNative ? '#16a34a' : '#475569';
|
|
2932
|
+
const boxBg = isNative ? '#f0fdf4' : '#f8fafc';
|
|
2933
|
+
const boxBorder = isNative ? '#dcfce7' : '#e2e8f0';
|
|
2934
|
+
const label = isNative ? '🧠 Model Native Thinking' : '📝 Model Raw Thinking / Plan';
|
|
2935
|
+
|
|
2936
|
+
nodeRow.innerHTML = `
|
|
2937
|
+
<div style="background-color: ${boxBg}; border-left: 4px solid ${boxColor}; border-radius: 6px; padding: 12px 15px; border: 1px solid ${boxBorder}; border-left-width: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.02);">
|
|
2938
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
2939
|
+
<h4 style="margin: 0; color: ${boxColor}; font-size: 0.82rem; font-weight: 700; text-transform: uppercase; display: flex; align-items: center; gap: 6px;">
|
|
2940
|
+
<span>${label}</span>
|
|
2941
|
+
</h4>
|
|
2942
|
+
<div style="display: flex; align-items: center; gap: 8px; font-size: 0.75rem; color: #718096;">
|
|
2943
|
+
<span>${formatRelativeTime(node.timestamp)}</span>
|
|
2944
|
+
<span class="status-${escapeHtml(node.status)}">${escapeHtml(node.status)}</span>
|
|
2945
|
+
</div>
|
|
2946
|
+
</div>
|
|
2947
|
+
<div style="white-space: pre-wrap; font-size: 0.85rem; color: #1e293b; line-height: 1.6; font-family: inherit;">${escapeHtml(thoughtText || '(No thinking content)')}</div>
|
|
2948
|
+
</div>
|
|
2949
|
+
`;
|
|
2950
|
+
if (node.children && node.children.length > 0) {
|
|
2951
|
+
const childrenWrapper = document.createElement('div');
|
|
2952
|
+
renderMiniTree(node.children, childrenWrapper, depth + 1, filterType);
|
|
2953
|
+
nodeRow.appendChild(childrenWrapper);
|
|
2954
|
+
}
|
|
2955
|
+
container.appendChild(nodeRow);
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
nodeRow.style.padding = '8px 12px';
|
|
2960
|
+
nodeRow.style.backgroundColor = 'white';
|
|
2961
|
+
nodeRow.style.border = '1px solid #e2e8f0';
|
|
2962
|
+
nodeRow.style.borderRadius = '6px';
|
|
2963
|
+
nodeRow.style.boxShadow = '0 1px 2px rgba(0,0,0,0.01)';
|
|
2964
|
+
|
|
2965
|
+
const typePill = `<span class="pill type-${escapeHtml(node.type)}" style="font-size: 0.7rem; padding: 2px 6px; margin-right: 8px; border-radius: 4px; font-weight: bold; display: inline-block;">${escapeHtml(node.type)}</span>`;
|
|
2966
|
+
const statusColor = node.status === 'success' ? '#16a34a' : (node.status === 'failed' ? '#dc2626' : '#d97706');
|
|
2967
|
+
const statusDot = `<span style="color: ${statusColor}; font-weight: bold; margin-right: 8px; font-size: 0.78rem;">● ${escapeHtml(node.status)}</span>`;
|
|
2968
|
+
|
|
2969
|
+
const header = document.createElement('div');
|
|
2970
|
+
header.style.display = 'flex';
|
|
2971
|
+
header.style.justifyContent = 'space-between';
|
|
2972
|
+
header.style.alignItems = 'center';
|
|
2973
|
+
header.style.cursor = 'pointer';
|
|
2974
|
+
header.style.userSelect = 'none';
|
|
2975
|
+
|
|
2976
|
+
header.innerHTML = `
|
|
2977
|
+
<div style="display: flex; align-items: center;">
|
|
2978
|
+
${typePill}
|
|
2979
|
+
<span style="font-weight: bold; font-size: 0.82rem; color: #1e293b;">${escapeHtml(node.name)}</span>
|
|
2980
|
+
</div>
|
|
2981
|
+
<div style="font-size: 0.78rem; display: flex; align-items: center; gap: 8px;">
|
|
2982
|
+
<span style="color: #64748b; font-size: 0.75rem;">${formatRelativeTime(node.timestamp)}</span>
|
|
2983
|
+
${statusDot}
|
|
2984
|
+
${node.duration ? `<span style="color: #64748b;">${formatNodeDurationBreakdown(node)}</span>` : ''}
|
|
2985
|
+
</div>
|
|
2986
|
+
`;
|
|
2987
|
+
|
|
2988
|
+
const detailsContainer = document.createElement('div');
|
|
2989
|
+
detailsContainer.style.display = 'none';
|
|
2990
|
+
detailsContainer.style.marginTop = '10px';
|
|
2991
|
+
detailsContainer.style.borderTop = '1px solid #f1f5f9';
|
|
2992
|
+
detailsContainer.style.paddingTop = '10px';
|
|
2993
|
+
|
|
2994
|
+
header.onclick = async () => {
|
|
2995
|
+
if (detailsContainer.style.display === 'none') {
|
|
2996
|
+
if (!detailsContainer.getAttribute('data-loaded')) {
|
|
2997
|
+
detailsContainer.innerHTML = '<div style="font-size: 0.8rem; color: #64748b; padding: 10px 0;"><div class="spinner" style="width: 12px; height: 12px; border: 2px solid #64748b; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; display: inline-block; margin-right: 6px; vertical-align: middle;"></div>Loading full details...</div>';
|
|
2998
|
+
detailsContainer.style.display = 'block';
|
|
2999
|
+
try {
|
|
3000
|
+
const res = await fetch(`/api/traces/${node.trace_id}`);
|
|
3001
|
+
if (!res.ok) throw new Error(await res.text());
|
|
3002
|
+
const traceDetails = await res.json();
|
|
3003
|
+
detailsContainer.setAttribute('data-loaded', 'true');
|
|
3004
|
+
|
|
3005
|
+
if (traceDetails.type === 'llm_call') {
|
|
3006
|
+
detailsContainer.innerHTML = renderLLMCallPayload(traceDetails.payload);
|
|
3007
|
+
} else if (traceDetails.type === 'tool') {
|
|
3008
|
+
detailsContainer.innerHTML = renderToolPayload(traceDetails.payload);
|
|
3009
|
+
} else {
|
|
3010
|
+
detailsContainer.innerHTML = `
|
|
3011
|
+
<pre style="margin: 0; font-size: 0.8rem; color: #334155; font-family: monospace; white-space: pre-wrap; background-color: #f8fafc; padding: 10px; border-radius: 4px; border: 1px solid #e2e8f0; max-height: 250px; overflow-y: auto;">${escapeHtml(JSON.stringify(traceDetails.payload, null, 2))}</pre>
|
|
3012
|
+
`;
|
|
3013
|
+
}
|
|
3014
|
+
} catch (err) {
|
|
3015
|
+
detailsContainer.innerHTML = `<div style="font-size: 0.8rem; color: #dc2626;">Error: ${escapeHtml(err.message)}</div>`;
|
|
3016
|
+
}
|
|
3017
|
+
} else {
|
|
3018
|
+
detailsContainer.style.display = 'block';
|
|
3019
|
+
}
|
|
3020
|
+
} else {
|
|
3021
|
+
detailsContainer.style.display = 'none';
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
|
|
3025
|
+
nodeRow.appendChild(header);
|
|
3026
|
+
nodeRow.appendChild(detailsContainer);
|
|
3027
|
+
container.appendChild(nodeRow);
|
|
3028
|
+
|
|
3029
|
+
if (node.children && node.children.length > 0) {
|
|
3030
|
+
const childrenWrapper = document.createElement('div');
|
|
3031
|
+
renderMiniTree(node.children, childrenWrapper, depth + 1, filterType);
|
|
3032
|
+
container.appendChild(childrenWrapper);
|
|
3033
|
+
}
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
|
|
3038
|
+
// Incrementally render growing stream text: append only the new tail when the
|
|
3039
|
+
// target text extends what is already rendered; otherwise rewrite in full.
|
|
3040
|
+
function updateStreamPreContent(pre, fullText) {
|
|
3041
|
+
const renderedLen = pre._streamRenderedLen || 0;
|
|
3042
|
+
const tail = pre._streamTail || '';
|
|
3043
|
+
const canAppend = renderedLen > 0 && fullText.length >= renderedLen &&
|
|
3044
|
+
fullText.substring(renderedLen - tail.length, renderedLen) === tail;
|
|
3045
|
+
if (canAppend) {
|
|
3046
|
+
if (fullText.length > renderedLen) {
|
|
3047
|
+
pre.appendChild(document.createTextNode(fullText.substring(renderedLen)));
|
|
3048
|
+
}
|
|
3049
|
+
} else {
|
|
3050
|
+
pre.textContent = fullText;
|
|
3051
|
+
}
|
|
3052
|
+
pre._streamRenderedLen = fullText.length;
|
|
3053
|
+
pre._streamTail = fullText.substring(Math.max(0, fullText.length - 64));
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
// Coalesce auto-scroll to at most once per animation frame per element
|
|
3057
|
+
function scheduleStreamAutoScroll(pre) {
|
|
3058
|
+
if (pre._streamScrollScheduled) return;
|
|
3059
|
+
pre._streamScrollScheduled = true;
|
|
3060
|
+
requestAnimationFrame(() => {
|
|
3061
|
+
pre._streamScrollScheduled = false;
|
|
3062
|
+
pre.scrollTop = pre.scrollHeight;
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
function updateStreamDisplay(traceId) {
|
|
3067
|
+
if (!nodeStreams || !nodeStreams[traceId]) return;
|
|
3068
|
+
if (traceId === currentSessionId || traceId === 'session') return;
|
|
3069
|
+
|
|
3070
|
+
let targetDiv = document.getElementById(`node-div-${traceId}`);
|
|
3071
|
+
const streamData = nodeStreams[traceId];
|
|
3072
|
+
|
|
3073
|
+
if (!targetDiv && streamData && streamData.parentTraceId) {
|
|
3074
|
+
const parentDiv = document.getElementById(`node-div-${streamData.parentTraceId}`);
|
|
3075
|
+
if (parentDiv) {
|
|
3076
|
+
const runningLLMs = parentDiv.querySelectorAll('.node-type-llm_call[data-status="running"]');
|
|
3077
|
+
if (runningLLMs.length > 0) {
|
|
3078
|
+
targetDiv = runningLLMs[runningLLMs.length - 1];
|
|
3079
|
+
} else {
|
|
3080
|
+
targetDiv = document.createElement('div');
|
|
3081
|
+
targetDiv.className = 'tree-node node-type-llm_call placeholder-llm-call';
|
|
3082
|
+
targetDiv.id = `node-div-${traceId}`;
|
|
3083
|
+
targetDiv.setAttribute('data-status', 'running');
|
|
3084
|
+
targetDiv.innerHTML = `
|
|
3085
|
+
<div class="node-header type-llm_call">
|
|
3086
|
+
<div class="node-title">
|
|
3087
|
+
<span class="pill type-llm_call">llm_call</span>
|
|
3088
|
+
<span><strong>Generating response...</strong></span>
|
|
3089
|
+
</div>
|
|
3090
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
3091
|
+
<span class="status-running">running</span>
|
|
3092
|
+
</div>
|
|
3093
|
+
</div>
|
|
3094
|
+
`;
|
|
3095
|
+
parentDiv.appendChild(targetDiv);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
if (!targetDiv || !targetDiv.classList.contains('node-type-llm_call')) return;
|
|
3101
|
+
|
|
3102
|
+
const targetIsRunning = targetDiv.getAttribute('data-status') === 'running' || Boolean(targetDiv.querySelector('.status-running'));
|
|
3103
|
+
if (!targetIsRunning) {
|
|
3104
|
+
const existing = targetDiv.querySelector('.node-stream-container');
|
|
3105
|
+
if (existing) existing.remove();
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
let streamContainer = targetDiv.querySelector('.node-stream-container');
|
|
3110
|
+
|
|
3111
|
+
let text = '';
|
|
3112
|
+
let thinking = '';
|
|
3113
|
+
if (typeof streamData === 'string') {
|
|
3114
|
+
text = streamData;
|
|
3115
|
+
} else if (typeof streamData === 'object' && streamData !== null) {
|
|
3116
|
+
text = streamData.text || '';
|
|
3117
|
+
thinking = streamData.thinking || '';
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
if (!text && !thinking) {
|
|
3121
|
+
if (streamContainer) {
|
|
3122
|
+
streamContainer.style.display = 'none';
|
|
3123
|
+
}
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
if (!streamContainer) {
|
|
3128
|
+
streamContainer = document.createElement('div');
|
|
3129
|
+
streamContainer.className = 'node-stream-container';
|
|
3130
|
+
streamContainer.style.margin = '4px 0 10px 20px';
|
|
3131
|
+
streamContainer.style.borderLeft = '2px solid #4299e1';
|
|
3132
|
+
streamContainer.style.backgroundColor = '#2d3748';
|
|
3133
|
+
streamContainer.style.borderRadius = '0 6px 6px 0';
|
|
3134
|
+
streamContainer.style.overflow = 'hidden';
|
|
3135
|
+
streamContainer.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
|
|
3136
|
+
streamContainer.style.transition = 'border-color 0.3s ease, box-shadow 0.3s ease';
|
|
3137
|
+
|
|
3138
|
+
const streamLabel = document.createElement('div');
|
|
3139
|
+
streamLabel.style.backgroundColor = '#1a202c';
|
|
3140
|
+
streamLabel.style.color = '#a0aec0';
|
|
3141
|
+
streamLabel.style.padding = '4px 10px';
|
|
3142
|
+
streamLabel.style.fontSize = '0.75rem';
|
|
3143
|
+
streamLabel.style.fontWeight = 'bold';
|
|
3144
|
+
streamLabel.style.textTransform = 'uppercase';
|
|
3145
|
+
streamLabel.style.display = 'flex';
|
|
3146
|
+
streamLabel.style.alignItems = 'center';
|
|
3147
|
+
streamLabel.style.justifyContent = 'space-between';
|
|
3148
|
+
streamLabel.innerHTML = `<span>LLM Output Stream</span><span style="color: #4299e1;">● Live</span>`;
|
|
3149
|
+
streamContainer.appendChild(streamLabel);
|
|
3150
|
+
|
|
3151
|
+
// 1. Thinking Process Block
|
|
3152
|
+
const labelThinking = document.createElement('div');
|
|
3153
|
+
labelThinking.className = 'label-thinking';
|
|
3154
|
+
labelThinking.style.color = '#a0aec0';
|
|
3155
|
+
labelThinking.style.padding = '4px 12px';
|
|
3156
|
+
labelThinking.style.fontSize = '0.75rem';
|
|
3157
|
+
labelThinking.style.fontWeight = 'bold';
|
|
3158
|
+
labelThinking.style.borderBottom = '1px solid #1a202c';
|
|
3159
|
+
labelThinking.style.backgroundColor = '#262f3e';
|
|
3160
|
+
labelThinking.innerText = '🧠 Thinking Process';
|
|
3161
|
+
labelThinking.style.display = 'none';
|
|
3162
|
+
streamContainer.appendChild(labelThinking);
|
|
3163
|
+
|
|
3164
|
+
const preThinking = document.createElement('pre');
|
|
3165
|
+
preThinking.className = 'stream-thinking';
|
|
3166
|
+
preThinking.style.margin = '0';
|
|
3167
|
+
preThinking.style.padding = '8px 12px';
|
|
3168
|
+
preThinking.style.backgroundColor = '#232a37';
|
|
3169
|
+
preThinking.style.maxHeight = '150px';
|
|
3170
|
+
preThinking.style.overflowY = 'auto';
|
|
3171
|
+
preThinking.style.whiteSpace = 'pre-wrap';
|
|
3172
|
+
preThinking.style.fontSize = '0.8rem';
|
|
3173
|
+
preThinking.style.color = '#a0aec0';
|
|
3174
|
+
preThinking.style.fontStyle = 'italic';
|
|
3175
|
+
preThinking.style.fontFamily = "'Courier New', Courier, monospace";
|
|
3176
|
+
preThinking.style.display = 'none';
|
|
3177
|
+
preThinking.style.borderBottom = '1px solid #4a5568';
|
|
3178
|
+
streamContainer.appendChild(preThinking);
|
|
3179
|
+
|
|
3180
|
+
// 2. Output Block
|
|
3181
|
+
const labelText = document.createElement('div');
|
|
3182
|
+
labelText.className = 'label-text';
|
|
3183
|
+
labelText.style.color = '#a0aec0';
|
|
3184
|
+
labelText.style.padding = '4px 12px';
|
|
3185
|
+
labelText.style.fontSize = '0.75rem';
|
|
3186
|
+
labelText.style.fontWeight = 'bold';
|
|
3187
|
+
labelText.style.backgroundColor = '#262f3e';
|
|
3188
|
+
labelText.innerText = '💬 Output';
|
|
3189
|
+
labelText.style.display = 'none';
|
|
3190
|
+
streamContainer.appendChild(labelText);
|
|
3191
|
+
|
|
3192
|
+
const preText = document.createElement('pre');
|
|
3193
|
+
preText.className = 'stream-text';
|
|
3194
|
+
preText.style.margin = '0';
|
|
3195
|
+
preText.style.padding = '8px 12px';
|
|
3196
|
+
preText.style.backgroundColor = 'transparent';
|
|
3197
|
+
preText.style.maxHeight = '250px';
|
|
3198
|
+
preText.style.overflowY = 'auto';
|
|
3199
|
+
preText.style.whiteSpace = 'pre-wrap';
|
|
3200
|
+
preText.style.fontSize = '0.85rem';
|
|
3201
|
+
preText.style.color = '#e2e8f0';
|
|
3202
|
+
preText.style.fontFamily = "'Courier New', Courier, monospace";
|
|
3203
|
+
streamContainer.appendChild(preText);
|
|
3204
|
+
|
|
3205
|
+
targetDiv.appendChild(streamContainer);
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
streamContainer.style.display = 'block';
|
|
3209
|
+
|
|
3210
|
+
const preThinking = streamContainer.querySelector('.stream-thinking');
|
|
3211
|
+
const labelThinking = streamContainer.querySelector('.label-thinking');
|
|
3212
|
+
const preText = streamContainer.querySelector('.stream-text');
|
|
3213
|
+
const labelText = streamContainer.querySelector('.label-text');
|
|
3214
|
+
|
|
3215
|
+
if (thinking) {
|
|
3216
|
+
labelThinking.style.display = 'block';
|
|
3217
|
+
preThinking.style.display = 'block';
|
|
3218
|
+
updateStreamPreContent(preThinking, thinking);
|
|
3219
|
+
} else {
|
|
3220
|
+
labelThinking.style.display = 'none';
|
|
3221
|
+
preThinking.style.display = 'none';
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
if (text) {
|
|
3225
|
+
if (thinking) {
|
|
3226
|
+
labelText.style.display = 'block';
|
|
3227
|
+
} else {
|
|
3228
|
+
labelText.style.display = 'none';
|
|
3229
|
+
}
|
|
3230
|
+
updateStreamPreContent(preText, text);
|
|
3231
|
+
} else {
|
|
3232
|
+
labelText.style.display = 'none';
|
|
3233
|
+
updateStreamPreContent(preText, '');
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
const visiblePre = preText.style.display !== 'none' ? preText : preThinking;
|
|
3237
|
+
scheduleStreamAutoScroll(visiblePre);
|
|
3238
|
+
|
|
3239
|
+
streamContainer.classList.add('streaming-active');
|
|
3240
|
+
clearTimeout(streamContainer.streamTimeout);
|
|
3241
|
+
streamContainer.streamTimeout = setTimeout(() => streamContainer.classList.remove('streaming-active'), 500);
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
let treeLoadInFlight = false;
|
|
3245
|
+
let treeLoadPending = false;
|
|
3246
|
+
let treeReloadWhenVisible = false;
|
|
3247
|
+
|
|
3248
|
+
async function loadTree() {
|
|
3249
|
+
if (!currentSessionId) return;
|
|
3250
|
+
if (document.hidden) {
|
|
3251
|
+
// Skip rebuilding while the page is hidden; refresh once on visibilitychange
|
|
3252
|
+
treeReloadWhenVisible = true;
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
if (treeLoadInFlight) {
|
|
3256
|
+
// Coalesce concurrent requests: run one more pass after the current load finishes
|
|
3257
|
+
treeLoadPending = true;
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
treeLoadInFlight = true;
|
|
3261
|
+
try {
|
|
3262
|
+
if (activeNodeId) {
|
|
3263
|
+
try {
|
|
3264
|
+
const resDetails = await fetch(`/api/traces/${activeNodeId}`);
|
|
3265
|
+
if (resDetails.ok) {
|
|
3266
|
+
activeNodeDetails = await resDetails.json();
|
|
3267
|
+
}
|
|
3268
|
+
} catch (e) {
|
|
3269
|
+
console.error('Failed to update active node details', e);
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
const res = await fetch(`/api/sessions/${currentSessionId}/tree`);
|
|
3273
|
+
const data = await res.json();
|
|
3274
|
+
|
|
3275
|
+
function scanTraces(nodes) {
|
|
3276
|
+
if (!nodes) return;
|
|
3277
|
+
nodes.forEach(n => {
|
|
3278
|
+
if (n.trace_id && n.name) traceMap[n.trace_id] = n.name;
|
|
3279
|
+
if (n.children) scanTraces(n.children);
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
scanTraces(data);
|
|
3283
|
+
|
|
3284
|
+
const container = document.getElementById('tree-container');
|
|
3285
|
+
container.innerHTML = '';
|
|
3286
|
+
renderTree(data, container);
|
|
3287
|
+
|
|
3288
|
+
// Remove any leftover stream containers from completed nodes (scoped to the tree container)
|
|
3289
|
+
container.querySelectorAll('.tree-node:not([data-status="running"]) .node-stream-container, .node-type-tool .node-stream-container').forEach(el => el.remove());
|
|
3290
|
+
|
|
3291
|
+
for (let traceId in nodeStreams) {
|
|
3292
|
+
if (traceId === currentSessionId || traceId === 'session') continue;
|
|
3293
|
+
updateStreamDisplay(traceId);
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
try {
|
|
3297
|
+
const bgRes = await fetch(`/api/sessions/${currentSessionId}/background_tasks`);
|
|
3298
|
+
const bgData = await bgRes.json();
|
|
3299
|
+
updateBackgroundTasksUI(bgData);
|
|
3300
|
+
} catch (e) {}
|
|
3301
|
+
} catch (err) {
|
|
3302
|
+
console.error('Failed to load tree', err);
|
|
3303
|
+
} finally {
|
|
3304
|
+
treeLoadInFlight = false;
|
|
3305
|
+
if (treeLoadPending) {
|
|
3306
|
+
treeLoadPending = false;
|
|
3307
|
+
loadTree();
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
function flattenVisibleNodes(nodes, showLogs) {
|
|
3313
|
+
const allowedTypes = ['agent', 'tool', 'llm_call', 'node', 'action', 'thinking', 'raw_thinking'];
|
|
3314
|
+
let result = [];
|
|
3315
|
+
if (!nodes) return result;
|
|
3316
|
+
nodes.forEach(n => {
|
|
3317
|
+
if (showLogs || allowedTypes.includes(n.type)) {
|
|
3318
|
+
result.push(n);
|
|
3319
|
+
} else if (n.children && n.children.length > 0) {
|
|
3320
|
+
result = result.concat(flattenVisibleNodes(n.children, showLogs));
|
|
3321
|
+
}
|
|
3322
|
+
});
|
|
3323
|
+
return result;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
function renderTree(nodes, container) {
|
|
3327
|
+
const showLogs = document.getElementById('show-logs-check').checked;
|
|
3328
|
+
const visibleNodes = flattenVisibleNodes(nodes, showLogs);
|
|
3329
|
+
|
|
3330
|
+
// Temporal Clustering Algorithm: strictly isolate core agent nodes and cluster only compatible child level elements
|
|
3331
|
+
let temporalGroups = [];
|
|
3332
|
+
let currentGroup = [];
|
|
3333
|
+
|
|
3334
|
+
visibleNodes.forEach(node => {
|
|
3335
|
+
if (currentGroup.length === 0) {
|
|
3336
|
+
currentGroup.push(node);
|
|
3337
|
+
} else {
|
|
3338
|
+
const prev = currentGroup[currentGroup.length - 1];
|
|
3339
|
+
// Isolation rule: Core agent nodes MUST NEVER be clustered horizontally and always cascade vertically.
|
|
3340
|
+
const isClusterable = (node.type === 'llm_call' || node.type === 'tool') && node.type === prev.type;
|
|
3341
|
+
|
|
3342
|
+
if (isClusterable && Math.abs(node.timestamp - prev.timestamp) < 0.1) {
|
|
3343
|
+
currentGroup.push(node);
|
|
3344
|
+
} else {
|
|
3345
|
+
temporalGroups.push(currentGroup);
|
|
3346
|
+
currentGroup = [node];
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
});
|
|
3350
|
+
if (currentGroup.length > 0) {
|
|
3351
|
+
temporalGroups.push(currentGroup);
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
// Render groups based on actual temporal clustering
|
|
3355
|
+
const nodeIndexMap = new Map();
|
|
3356
|
+
visibleNodes.forEach((n, i) => nodeIndexMap.set(n, i));
|
|
3357
|
+
temporalGroups.forEach(group => {
|
|
3358
|
+
let targetContainer = container;
|
|
3359
|
+
if (group.length > 1) {
|
|
3360
|
+
const dash = document.createElement('div');
|
|
3361
|
+
dash.className = 'horizontal-dashboard';
|
|
3362
|
+
container.appendChild(dash);
|
|
3363
|
+
targetContainer = dash;
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
group.forEach(node => {
|
|
3367
|
+
// Find its absolute position in visibleNodes to support global ReAct stream consolidation
|
|
3368
|
+
const absIndex = nodeIndexMap.get(node);
|
|
3369
|
+
const isReActContinuation = node.type === 'llm_call' && absIndex > 0 && visibleNodes[absIndex - 1].type === 'tool';
|
|
3370
|
+
|
|
3371
|
+
const nodeDiv = document.createElement('div');
|
|
3372
|
+
nodeDiv.className = `tree-node node-type-${node.type}`;
|
|
3373
|
+
nodeDiv.id = `node-div-${node.trace_id}`;
|
|
3374
|
+
nodeDiv.setAttribute('data-status', node.status || '');
|
|
3375
|
+
|
|
3376
|
+
if (node.type === 'raw_thinking' || node.type === 'thinking') {
|
|
3377
|
+
const thoughtText = extractThinkingText(node.payload);
|
|
3378
|
+
const isNative = node.type === 'thinking';
|
|
3379
|
+
const boxColor = isNative ? '#16a34a' : '#475569';
|
|
3380
|
+
const boxBg = isNative ? '#f0fdf4' : '#f8fafc';
|
|
3381
|
+
const boxBorder = isNative ? '#dcfce7' : '#e2e8f0';
|
|
3382
|
+
const label = isNative ? '🧠 Model Native Thinking' : '📝 Model Raw Thinking / Plan';
|
|
3383
|
+
|
|
3384
|
+
nodeDiv.innerHTML = `
|
|
3385
|
+
<div style="background-color: ${boxBg}; border-left: 4px solid ${boxColor}; border-radius: 6px; padding: 12px 15px; border: 1px solid ${boxBorder}; border-left-width: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.02); margin: 6px 0;">
|
|
3386
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
3387
|
+
<h4 style="margin: 0; color: ${boxColor}; font-size: 0.85rem; font-weight: 700; text-transform: uppercase; display: flex; align-items: center; gap: 6px;">
|
|
3388
|
+
<span>${label}</span>
|
|
3389
|
+
</h4>
|
|
3390
|
+
<div style="display: flex; align-items: center; gap: 8px; font-size: 0.75rem; color: #718096;">
|
|
3391
|
+
<span>${formatRelativeTime(node.timestamp)}</span>
|
|
3392
|
+
<span class="status-${escapeHtml(node.status)}">${escapeHtml(node.status)}</span>
|
|
3393
|
+
</div>
|
|
3394
|
+
</div>
|
|
3395
|
+
<div style="white-space: pre-wrap; font-size: 0.88rem; color: #1e293b; line-height: 1.6; font-family: inherit;">${escapeHtml(thoughtText || '(No thinking content)')}</div>
|
|
3396
|
+
</div>
|
|
3397
|
+
`;
|
|
3398
|
+
|
|
3399
|
+
const visibleChildren = flattenVisibleNodes(node.children || [], showLogs);
|
|
3400
|
+
if (visibleChildren.length > 0) {
|
|
3401
|
+
const childrenContainer = document.createElement('div');
|
|
3402
|
+
renderTree(node.children, childrenContainer);
|
|
3403
|
+
nodeDiv.appendChild(childrenContainer);
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
targetContainer.appendChild(nodeDiv);
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
if (!isReActContinuation) {
|
|
3411
|
+
const header = document.createElement('div');
|
|
3412
|
+
header.className = `node-header type-${node.type} ${node.trace_id === activeNodeId ? 'active' : ''}`;
|
|
3413
|
+
header.onclick = (e) => {
|
|
3414
|
+
e.stopPropagation();
|
|
3415
|
+
selectNode(node.trace_id);
|
|
3416
|
+
};
|
|
3417
|
+
|
|
3418
|
+
const typePill = `<span class="pill type-${escapeHtml(node.type)}">${escapeHtml(node.type)}</span>`;
|
|
3419
|
+
const statusClass = `status-${node.status}`;
|
|
3420
|
+
|
|
3421
|
+
header.innerHTML = `
|
|
3422
|
+
<div class="node-title">
|
|
3423
|
+
${typePill}
|
|
3424
|
+
<span><strong>${escapeHtml(node.name)}</strong></span>
|
|
3425
|
+
</div>
|
|
3426
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
3427
|
+
<span style="color: #718096; font-size: 0.75rem;">${formatRelativeTime(node.timestamp)}</span>
|
|
3428
|
+
<span class="${escapeHtml(statusClass)}">${escapeHtml(node.status)}</span>
|
|
3429
|
+
${node.duration ? `<small style="color: #718096;">${formatNodeDurationBreakdown(node)}</small>` : ''}
|
|
3430
|
+
</div>
|
|
3431
|
+
`;
|
|
3432
|
+
|
|
3433
|
+
nodeDiv.appendChild(header);
|
|
3434
|
+
} else {
|
|
3435
|
+
const header = document.createElement('div');
|
|
3436
|
+
header.className = `node-header type-${node.type} ${node.trace_id === activeNodeId ? 'active' : ''}`;
|
|
3437
|
+
header.style.opacity = '0.8';
|
|
3438
|
+
header.onclick = (e) => {
|
|
3439
|
+
e.stopPropagation();
|
|
3440
|
+
selectNode(node.trace_id);
|
|
3441
|
+
};
|
|
3442
|
+
|
|
3443
|
+
const typePill = `<span class="pill type-${escapeHtml(node.type)}">↳ continuation</span>`;
|
|
3444
|
+
const statusClass = `status-${node.status}`;
|
|
3445
|
+
|
|
3446
|
+
header.innerHTML = `
|
|
3447
|
+
<div class="node-title">
|
|
3448
|
+
${typePill}
|
|
3449
|
+
<span style="font-style: italic; color: #718096;"><strong>${escapeHtml(node.name)}</strong></span>
|
|
3450
|
+
</div>
|
|
3451
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
3452
|
+
<span style="color: #718096; font-size: 0.75rem;">${formatRelativeTime(node.timestamp)}</span>
|
|
3453
|
+
<span class="${escapeHtml(statusClass)}">${escapeHtml(node.status)}</span>
|
|
3454
|
+
${node.duration ? `<small style="color: #718096;">${formatNodeDurationBreakdown(node)}</small>` : ''}
|
|
3455
|
+
</div>
|
|
3456
|
+
`;
|
|
3457
|
+
|
|
3458
|
+
nodeDiv.appendChild(header);
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
if (node.trace_id === activeNodeId && activeNodeDetails) {
|
|
3462
|
+
const detailsDiv = document.createElement('div');
|
|
3463
|
+
detailsDiv.className = 'detail-section';
|
|
3464
|
+
detailsDiv.style.margin = '10px 0';
|
|
3465
|
+
detailsDiv.innerHTML = `
|
|
3466
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
|
3467
|
+
<h3 style="margin: 0;">Details</h3>
|
|
3468
|
+
<a href="/api/traces/${escapeHtml(node.trace_id)}/download" target="_blank" style="font-size: 0.85rem; background-color: #2d3748; color: #fff; padding: 4px 8px; border-radius: 4px; text-decoration: none; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor='#4a5568'" onmouseout="this.style.backgroundColor='#2d3748'">📥 Download Raw Payload</a>
|
|
3469
|
+
</div>
|
|
3470
|
+
`;
|
|
3471
|
+
|
|
3472
|
+
if (activeNodeDetails.payload) {
|
|
3473
|
+
if (activeNodeDetails.type === 'llm_call') {
|
|
3474
|
+
const wrapper = document.createElement('div');
|
|
3475
|
+
wrapper.innerHTML = renderLLMCallPayload(activeNodeDetails.payload);
|
|
3476
|
+
detailsDiv.appendChild(wrapper);
|
|
3477
|
+
} else if (activeNodeDetails.type === 'tool') {
|
|
3478
|
+
const wrapper = document.createElement('div');
|
|
3479
|
+
wrapper.innerHTML = renderToolPayload(activeNodeDetails.payload);
|
|
3480
|
+
detailsDiv.appendChild(wrapper);
|
|
3481
|
+
} else {
|
|
3482
|
+
const pre = document.createElement('pre');
|
|
3483
|
+
pre.innerText = JSON.stringify(activeNodeDetails.payload, null, 2);
|
|
3484
|
+
detailsDiv.appendChild(pre);
|
|
3485
|
+
|
|
3486
|
+
const mediaHtml = detectAndRenderMedia(activeNodeDetails.payload);
|
|
3487
|
+
if (mediaHtml) {
|
|
3488
|
+
const mediaDiv = document.createElement('div');
|
|
3489
|
+
mediaDiv.innerHTML = mediaHtml;
|
|
3490
|
+
detailsDiv.appendChild(mediaDiv);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
nodeDiv.appendChild(detailsDiv);
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
const visibleChildren = flattenVisibleNodes(node.children, showLogs);
|
|
3498
|
+
if (visibleChildren.length > 0) {
|
|
3499
|
+
const childrenContainer = document.createElement('div');
|
|
3500
|
+
renderTree(node.children, childrenContainer);
|
|
3501
|
+
nodeDiv.appendChild(childrenContainer);
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
targetContainer.appendChild(nodeDiv);
|
|
3505
|
+
});
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
async function selectNode(traceId) {
|
|
3510
|
+
if (activeNodeId === traceId) {
|
|
3511
|
+
activeNodeId = null;
|
|
3512
|
+
activeNodeDetails = null;
|
|
3513
|
+
loadTree();
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
activeNodeId = traceId;
|
|
3517
|
+
|
|
3518
|
+
try {
|
|
3519
|
+
const res = await fetch(`/api/traces/${traceId}`);
|
|
3520
|
+
const data = await res.json();
|
|
3521
|
+
activeNodeDetails = data;
|
|
3522
|
+
// Refresh tree to update active class and show details inline
|
|
3523
|
+
loadTree();
|
|
3524
|
+
} catch (err) {
|
|
3525
|
+
console.error('Failed to load details', err);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
function detectAndRenderMedia(payload) {
|
|
3530
|
+
let html = '';
|
|
3531
|
+
|
|
3532
|
+
// Helper function to scan object recursively
|
|
3533
|
+
function scan(obj) {
|
|
3534
|
+
if (!obj) return;
|
|
3535
|
+
|
|
3536
|
+
if (typeof obj === 'string') {
|
|
3537
|
+
// Check for Base64 image
|
|
3538
|
+
if (obj.startsWith('data:image/jpeg;base64,') || obj.startsWith('data:image/png;base64,')) {
|
|
3539
|
+
html += `
|
|
3540
|
+
<div class="media-container" style="margin-top: 10px; text-align: center;">
|
|
3541
|
+
<span class="media-label" style="display: block; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 5px;">🖼️ Base64 Image</span>
|
|
3542
|
+
<div class="image-wrapper" style="border: 1px solid #e2e8f0; border-radius: 6px; background-color: #f8fafc; cursor: zoom-in; display: inline-flex; align-items: center; justify-content: center; overflow: hidden; max-height: 400px; box-shadow: 0 1px 3px rgba(0,0,0,0.03);">
|
|
3543
|
+
<img src="${escapeHtml(obj)}" alt="Base64 Image" style="max-height: 380px; max-width: 100%; object-fit: contain;" data-zoom-img/>
|
|
3544
|
+
</div>
|
|
3545
|
+
</div>`;
|
|
3546
|
+
}
|
|
3547
|
+
// Check for file:// URL (local sandbox files)
|
|
3548
|
+
else if (obj.startsWith('file://')) {
|
|
3549
|
+
html += `
|
|
3550
|
+
<div class="media-container" style="margin-top: 10px; text-align: center;">
|
|
3551
|
+
<span class="media-label" style="display: block; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 5px;">🖼️ Tool Output Screenshot</span>
|
|
3552
|
+
<div class="image-wrapper" style="border: 1px solid #e2e8f0; border-radius: 6px; background-color: #f8fafc; cursor: zoom-in; display: inline-flex; align-items: center; justify-content: center; overflow: hidden; max-height: 400px; box-shadow: 0 1px 3px rgba(0,0,0,0.03);">
|
|
3553
|
+
<img src="${escapeHtml(getSafeLocalUrl(obj))}" alt="Local Media" style="max-height: 380px; max-width: 100%; object-fit: contain;" onclick="zoomImage('${escapeHtml(obj)}')"/>
|
|
3554
|
+
</div>
|
|
3555
|
+
</div>`;
|
|
3556
|
+
}
|
|
3557
|
+
// Check for image:// reference
|
|
3558
|
+
else if (obj.startsWith('image://')) {
|
|
3559
|
+
const imageName = obj.replace('image://', '');
|
|
3560
|
+
const imgUrl = `/images/${imageName}`;
|
|
3561
|
+
html += `
|
|
3562
|
+
<div class="media-container" style="margin-top: 10px; text-align: center;">
|
|
3563
|
+
<span class="media-label" style="display: block; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 5px;">🖼️ Image Reference: ${escapeHtml(imageName)}</span>
|
|
3564
|
+
<div class="image-wrapper" style="border: 1px solid #e2e8f0; border-radius: 6px; background-color: #f8fafc; cursor: zoom-in; display: inline-flex; align-items: center; justify-content: center; overflow: hidden; max-height: 400px; box-shadow: 0 1px 3px rgba(0,0,0,0.03);">
|
|
3565
|
+
<img src="${escapeHtml(imgUrl)}" alt="Referenced Image" style="max-height: 380px; max-width: 100%; object-fit: contain;" onclick="zoomImage('${escapeHtml(imgUrl)}')" onerror="this.onerror=null; this.src=''; this.parentNode.style.display='none'; this.after('Image not found on server');"/>
|
|
3566
|
+
</div>
|
|
3567
|
+
</div>`;
|
|
3568
|
+
}
|
|
3569
|
+
// Extract any nested standard image/video paths inside the string
|
|
3570
|
+
else {
|
|
3571
|
+
const matches = obj.match(/[\w\-./\\]+\.(jpg|jpeg|png|mp4)/gi);
|
|
3572
|
+
if (matches) {
|
|
3573
|
+
matches.forEach(matchPath => {
|
|
3574
|
+
const ext = matchPath.split('.').pop().toLowerCase();
|
|
3575
|
+
if (ext === 'mp4') {
|
|
3576
|
+
html += `
|
|
3577
|
+
<div class="media-container">
|
|
3578
|
+
<span class="media-label">Video: ${escapeHtml(matchPath)}</span>
|
|
3579
|
+
<video controls>
|
|
3580
|
+
<source src="/videos/${encodeURIComponent(matchPath)}" type="video/mp4">
|
|
3581
|
+
Your browser does not support the video tag.
|
|
3582
|
+
</video>
|
|
3583
|
+
</div>`;
|
|
3584
|
+
} else {
|
|
3585
|
+
const imageName = matchPath.substring(matchPath.lastIndexOf('/') + 1);
|
|
3586
|
+
let imageUrl = `/images/${imageName}`;
|
|
3587
|
+
if (matchPath.startsWith('/') || matchPath.includes('\\') || matchPath.startsWith('file://')) {
|
|
3588
|
+
imageUrl = getSafeLocalUrl(matchPath);
|
|
3589
|
+
}
|
|
3590
|
+
html += `
|
|
3591
|
+
<div class="media-container" style="margin-top: 10px; text-align: center;">
|
|
3592
|
+
<span class="media-label" style="display: block; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 5px;">🖼️ Image Reference: ${escapeHtml(imageName)}</span>
|
|
3593
|
+
<div class="image-wrapper" style="border: 1px solid #e2e8f0; border-radius: 6px; background-color: #f8fafc; cursor: zoom-in; display: inline-flex; align-items: center; justify-content: center; overflow: hidden; max-height: 400px; box-shadow: 0 1px 3px rgba(0,0,0,0.03);">
|
|
3594
|
+
<img src="${escapeHtml(imageUrl)}" alt="Referenced Image" style="max-height: 380px; max-width: 100%; object-fit: contain;" onclick="zoomImage('${escapeHtml(imageUrl)}')" onerror="this.onerror=null; this.src=''; this.parentNode.style.display='none';"/>
|
|
3595
|
+
</div>
|
|
3596
|
+
</div>`;
|
|
3597
|
+
}
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
} else if (typeof obj === 'object') {
|
|
3602
|
+
for (let key in obj) {
|
|
3603
|
+
scan(obj[key]);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
scan(payload);
|
|
3609
|
+
return html;
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
let _latestKnownSessionId = null;
|
|
3613
|
+
let taskStatusCheckInFlight = false;
|
|
3614
|
+
async function checkTaskStatus() {
|
|
3615
|
+
// Skip when a previous poll is still pending, or while the page is hidden
|
|
3616
|
+
if (taskStatusCheckInFlight || document.hidden) return;
|
|
3617
|
+
taskStatusCheckInFlight = true;
|
|
3618
|
+
try {
|
|
3619
|
+
const res = await fetch('/api/status');
|
|
3620
|
+
const data = await res.json();
|
|
3621
|
+
const statusDiv = document.getElementById('run-status');
|
|
3622
|
+
if (data.status === 'running') {
|
|
3623
|
+
statusDiv.innerHTML = `<span style="color: #3182ce; font-weight: 600;">⚡ Active Task:</span> <span style="color: #2d3748;">${escapeHtml(data.goal)}</span> <small style="color: #a0aec0;">(PID: ${escapeHtml(data.pid)})</small>`;
|
|
3624
|
+
document.getElementById('resume-btn').style.display = 'none';
|
|
3625
|
+
} else if (data.status === 'completed') {
|
|
3626
|
+
const color = data.returncode === 0 ? '#38a169' : '#e53e3e';
|
|
3627
|
+
const text = data.returncode === 0 ? 'Successfully Completed' : `Failed (Exit ${escapeHtml(data.returncode)})`;
|
|
3628
|
+
statusDiv.innerHTML = `<span style="color: ${color}; font-weight: 600;">✓ Task ${text}:</span> <span style="color: #2d3748;">${escapeHtml(data.goal)}</span>`;
|
|
3629
|
+
document.getElementById('resume-btn').style.display = 'none';
|
|
3630
|
+
} else if (data.status === 'paused') {
|
|
3631
|
+
if (!statusDiv.innerText.includes('Task Paused')) {
|
|
3632
|
+
statusDiv.innerHTML = `<span style="color: #e53e3e; font-weight: 600;">⏸️ Task Paused:</span> <span style="color: #2d3748;">${escapeHtml(data.goal)}</span>`;
|
|
3633
|
+
}
|
|
3634
|
+
document.getElementById('resume-btn').style.display = 'block';
|
|
3635
|
+
} else {
|
|
3636
|
+
if (!statusDiv.innerHTML || statusDiv.innerText.includes('Active Task') || statusDiv.innerText.includes('Stopping task') || statusDiv.innerText.includes('Task Paused')) {
|
|
3637
|
+
statusDiv.innerHTML = '<span style="color: #718096;">No background task running</span>';
|
|
3638
|
+
}
|
|
3639
|
+
document.getElementById('resume-btn').style.display = 'none';
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
// Auto-refresh sessions list if a new active session is detected
|
|
3643
|
+
if (data.session_id && data.session_id !== _latestKnownSessionId) {
|
|
3644
|
+
const wasTrackingLatest = (currentSessionId === _latestKnownSessionId) || !currentSessionId;
|
|
3645
|
+
_latestKnownSessionId = data.session_id;
|
|
3646
|
+
loadSessions();
|
|
3647
|
+
|
|
3648
|
+
if (wasTrackingLatest && data.status === 'running') {
|
|
3649
|
+
// Auto-switch to the new session so the trace live-updates without refreshing
|
|
3650
|
+
selectSession(data.session_id, data.goal, Date.now() / 1000);
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
// Update background task bar only if the selected session matches the active/latest session
|
|
3655
|
+
if (data.session_id === currentSessionId && data.background_tasks) {
|
|
3656
|
+
updateBackgroundTasksUI(data.background_tasks);
|
|
3657
|
+
}
|
|
3658
|
+
} catch (e) {
|
|
3659
|
+
// ignore connection errors
|
|
3660
|
+
} finally {
|
|
3661
|
+
taskStatusCheckInFlight = false;
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
setInterval(checkTaskStatus, 2000);
|
|
3665
|
+
checkTaskStatus();
|
|
3666
|
+
|
|
3667
|
+
document.addEventListener('visibilitychange', function() {
|
|
3668
|
+
if (document.hidden) {
|
|
3669
|
+
// Persist any pending stream data immediately when the page is hidden
|
|
3670
|
+
flushNodeStreamsPersist();
|
|
3671
|
+
} else {
|
|
3672
|
+
if (treeReloadWhenVisible) {
|
|
3673
|
+
treeReloadWhenVisible = false;
|
|
3674
|
+
if (activeTab === 'traces' && currentSessionId) {
|
|
3675
|
+
loadTree();
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
// Resume polling immediately upon returning to the page
|
|
3679
|
+
checkTaskStatus();
|
|
3680
|
+
}
|
|
3681
|
+
});
|
|
3682
|
+
window.addEventListener('beforeunload', flushNodeStreamsPersist);
|
|
3683
|
+
|
|
3684
|
+
async function cleanupHistory() {
|
|
3685
|
+
if (!confirm("Are you sure you want to clean up all database history, images, and trace files? This action cannot be undone.")) {
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
try {
|
|
3689
|
+
const res = await fetch('/api/cleanup', { method: 'POST' });
|
|
3690
|
+
const data = await res.json();
|
|
3691
|
+
if (res.ok) {
|
|
3692
|
+
alert("Cleanup successful!");
|
|
3693
|
+
currentSessionId = null;
|
|
3694
|
+
document.getElementById('session-header').innerHTML = `<h1>Select a session from the sidebar</h1>`;
|
|
3695
|
+
document.getElementById('tree-container').innerHTML = '';
|
|
3696
|
+
document.getElementById('details-container').innerHTML = '';
|
|
3697
|
+
loadSessions();
|
|
3698
|
+
} else {
|
|
3699
|
+
alert(`Cleanup failed: ${data.detail || 'Unknown error'}`);
|
|
3700
|
+
}
|
|
3701
|
+
} catch (err) {
|
|
3702
|
+
alert(`Error during cleanup: ${err.message}`);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
// Initial load
|
|
3707
|
+
loadSessions();
|
|
3708
|
+
</script>
|
|
3709
|
+
|
|
3710
|
+
<!-- Image Zoom Modal -->
|
|
3711
|
+
<div id="zoom-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(15, 23, 42, 0.85); backdrop-filter: blur(4px); z-index: 10000; justify-content: center; align-items: center; cursor: zoom-out;" onclick="closeZoomModal()">
|
|
3712
|
+
<img id="zoom-modal-img" style="max-width: 90%; max-height: 90%; object-fit: contain; border: 2px solid #334155; border-radius: 8px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);"/>
|
|
3713
|
+
</div>
|
|
3714
|
+
|
|
3715
|
+
|
|
3716
|
+
<!-- Fixed Global Background Task Container -->
|
|
3717
|
+
<div id="global-task-container" style="position: fixed; bottom: 0; left: 391px; right: 0; z-index: 1000; background-color: #1a202c; box-shadow: 0 -4px 20px rgba(0,0,0,0.2); display: none; flex-direction: column; border-top: 1px solid #2d3748;">
|
|
3718
|
+
<!-- Panel Body (expandable drawer) -->
|
|
3719
|
+
<div id="global-task-panel" style="max-height: 450px; overflow-y: auto; padding: 20px; border-bottom: 1px solid #2d3748; display: none; background-color: #111827;">
|
|
3720
|
+
<h3 style="color: #e2e8f0; margin-top: 0; margin-bottom: 15px; font-size: 1.05rem; display: flex; align-items: center; gap: 8px;">
|
|
3721
|
+
<span class="pill" style="background-color: #d69e2e; color: #1a202c;">Async Tasks</span>
|
|
3722
|
+
<span>Background Tasks Tracker</span>
|
|
3723
|
+
</h3>
|
|
3724
|
+
<div id="background-tasks-list" style="display: flex; flex-direction: column; gap: 12px;">
|
|
3725
|
+
<!-- Task items dynamically injected -->
|
|
3726
|
+
</div>
|
|
3727
|
+
</div>
|
|
3728
|
+
<!-- Header Bar -->
|
|
3729
|
+
<div id="global-task-bar" style="padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; background-color: #1a202c; transition: background-color 0.2s;" onclick="toggleBackgroundTaskPanel()" onmouseover="this.style.backgroundColor='#2d3748'" onmouseout="this.style.backgroundColor='#1a202c'">
|
|
3730
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
3731
|
+
<span class="pill" style="background-color: #d69e2e; color: #1a202c;">Async Tasks</span>
|
|
3732
|
+
<span id="task-bar-text" style="font-family: monospace; color: #e2e8f0; font-size: 0.85rem;"></span>
|
|
3733
|
+
</div>
|
|
3734
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
3735
|
+
<div id="task-bar-spinner" class="spinner" style="width: 14px; height: 14px; border: 2px solid #e2e8f0; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; display: none;"></div>
|
|
3736
|
+
<span id="task-panel-toggle-icon" style="color: #94a3b8; font-size: 0.8rem; font-weight: bold;">▲ Expand</span>
|
|
3737
|
+
</div>
|
|
3738
|
+
</div>
|
|
3739
|
+
</div>
|
|
3740
|
+
<style>
|
|
3741
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
3742
|
+
</style>
|
|
3743
|
+
|
|
3744
|
+
<!-- Floating Mini Video Player -->
|
|
3745
|
+
<div id="floating-video-container" style="position: fixed; bottom: 60px; right: 20px; display: none; background: white; padding: 12px 15px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 1050; border: 1px solid #ccc;">
|
|
3746
|
+
<div style="display: flex; justify-content: space-between; align-items: center; gap: 15px;">
|
|
3747
|
+
<div style="font-weight: bold; font-size: 14px; color: #2c3e50; display: flex; align-items: center; gap: 6px;">📹 Run Video</div>
|
|
3748
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
3749
|
+
<button onclick="reloadSessionVideo()" style="cursor: pointer; background: none; border: none; font-size: 14px; outline: none; padding: 2px 4px;" title="Reload Video">🔄</button>
|
|
3750
|
+
<select id="video-speed" onchange="document.getElementById('global-video').playbackRate = parseFloat(this.value)" style="font-size: 12px; padding: 2px; border-radius: 4px; border: 1px solid #ccc; background: white;">
|
|
3751
|
+
<option value="1">1x</option>
|
|
3752
|
+
<option value="2">2x</option>
|
|
3753
|
+
<option value="4">4x</option>
|
|
3754
|
+
<option value="8">8x</option>
|
|
3755
|
+
</select>
|
|
3756
|
+
<button onclick="toggleVideoContent()" id="video-toggle-btn-icon" style="cursor: pointer; background: none; border: none; font-size: 14px; color: #2c3e50; outline: none; padding: 2px 4px;" title="Expand / Collapse">➕</button>
|
|
3757
|
+
</div>
|
|
3758
|
+
</div>
|
|
3759
|
+
<div id="video-content" style="position: relative; width: 320px; display: none; margin-top: 10px;">
|
|
3760
|
+
<video id="global-video" width="320" height="568" controls style="border-radius: 8px;">
|
|
3761
|
+
<source id="global-video-src" src="">
|
|
3762
|
+
</video>
|
|
3763
|
+
<div id="global-video-overlay" style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.7); color: white; padding: 4px 8px; font-family: monospace; font-size: 16px; border-radius: 4px; pointer-events: none;">
|
|
3764
|
+
ts: 0 s
|
|
3765
|
+
</div>
|
|
3766
|
+
<div id="video-error-box" style="display: none; background: #fff5f5; border: 1px solid #feb2b2; color: #c53030; padding: 20px 15px; border-radius: 8px; font-size: 0.9rem; text-align: center;">
|
|
3767
|
+
<div style="font-size: 1.5rem; margin-bottom: 8px;">⚠️</div>
|
|
3768
|
+
<strong style="display: block; margin-bottom: 4px;">No Recording Found</strong>
|
|
3769
|
+
<div id="video-error-text" style="font-size: 0.8rem; color: #742a2a; line-height: 1.4;">
|
|
3770
|
+
No video recording file was found for this session on disk or database records.
|
|
3771
|
+
</div>
|
|
3772
|
+
</div>
|
|
3773
|
+
</div>
|
|
3774
|
+
</div>
|
|
3775
|
+
<script>
|
|
3776
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
3777
|
+
const globalVideo = document.getElementById('global-video');
|
|
3778
|
+
const globalOverlay = document.getElementById('global-video-overlay');
|
|
3779
|
+
if (globalVideo && globalOverlay) {
|
|
3780
|
+
globalVideo.addEventListener('timeupdate', function() {
|
|
3781
|
+
globalOverlay.textContent = 'ts: ' + Math.floor(globalVideo.currentTime) + ' s';
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
});
|
|
3785
|
+
</script>
|
|
3786
|
+
</body>
|
|
3787
|
+
</html>
|