@fraziym/axoniz 0.1.0
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/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/agents/definition.d.ts +15 -0
- package/dist/agents/definition.d.ts.map +1 -0
- package/dist/agents/definition.js +41 -0
- package/dist/agents/definition.js.map +1 -0
- package/dist/agents/index.d.ts +12 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +11 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/registry.d.ts +17 -0
- package/dist/agents/registry.d.ts.map +1 -0
- package/dist/agents/registry.js +23 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/agents/specialized.d.ts +31 -0
- package/dist/agents/specialized.d.ts.map +1 -0
- package/dist/agents/specialized.js +75 -0
- package/dist/agents/specialized.js.map +1 -0
- package/dist/awareness/index.d.ts +14 -0
- package/dist/awareness/index.d.ts.map +1 -0
- package/dist/awareness/index.js +22 -0
- package/dist/awareness/index.js.map +1 -0
- package/dist/awareness/service.d.ts +180 -0
- package/dist/awareness/service.d.ts.map +1 -0
- package/dist/awareness/service.js +789 -0
- package/dist/awareness/service.js.map +1 -0
- package/dist/cli/entry.d.ts +13 -0
- package/dist/cli/entry.d.ts.map +1 -0
- package/dist/cli/entry.js +35 -0
- package/dist/cli/entry.js.map +1 -0
- package/dist/comms/telegram.d.ts +41 -0
- package/dist/comms/telegram.d.ts.map +1 -0
- package/dist/comms/telegram.js +332 -0
- package/dist/comms/telegram.js.map +1 -0
- package/dist/core/agent.d.ts +252 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +1399 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/auth.d.ts +23 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +113 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/authority.d.ts +118 -0
- package/dist/core/authority.d.ts.map +1 -0
- package/dist/core/authority.js +402 -0
- package/dist/core/authority.js.map +1 -0
- package/dist/core/backend/async_queue.d.ts +17 -0
- package/dist/core/backend/async_queue.d.ts.map +1 -0
- package/dist/core/backend/async_queue.js +66 -0
- package/dist/core/backend/async_queue.js.map +1 -0
- package/dist/core/backend/index.d.ts +24 -0
- package/dist/core/backend/index.d.ts.map +1 -0
- package/dist/core/backend/index.js +128 -0
- package/dist/core/backend/index.js.map +1 -0
- package/dist/core/backend/llama_server.d.ts +39 -0
- package/dist/core/backend/llama_server.d.ts.map +1 -0
- package/dist/core/backend/llama_server.js +182 -0
- package/dist/core/backend/llama_server.js.map +1 -0
- package/dist/core/backend/llamacpp.d.ts +40 -0
- package/dist/core/backend/llamacpp.d.ts.map +1 -0
- package/dist/core/backend/llamacpp.js +200 -0
- package/dist/core/backend/llamacpp.js.map +1 -0
- package/dist/core/backend/lmstudio.d.ts +53 -0
- package/dist/core/backend/lmstudio.d.ts.map +1 -0
- package/dist/core/backend/lmstudio.js +274 -0
- package/dist/core/backend/lmstudio.js.map +1 -0
- package/dist/core/backend/ollama.d.ts +25 -0
- package/dist/core/backend/ollama.d.ts.map +1 -0
- package/dist/core/backend/ollama.js +86 -0
- package/dist/core/backend/ollama.js.map +1 -0
- package/dist/core/backend/openai.d.ts +27 -0
- package/dist/core/backend/openai.d.ts.map +1 -0
- package/dist/core/backend/openai.js +84 -0
- package/dist/core/backend/openai.js.map +1 -0
- package/dist/core/backend/types.d.ts +73 -0
- package/dist/core/backend/types.d.ts.map +1 -0
- package/dist/core/backend/types.js +137 -0
- package/dist/core/backend/types.js.map +1 -0
- package/dist/core/chat_store.d.ts +77 -0
- package/dist/core/chat_store.d.ts.map +1 -0
- package/dist/core/chat_store.js +332 -0
- package/dist/core/chat_store.js.map +1 -0
- package/dist/core/cli.d.ts +82 -0
- package/dist/core/cli.d.ts.map +1 -0
- package/dist/core/cli.js +602 -0
- package/dist/core/cli.js.map +1 -0
- package/dist/core/config.d.ts +127 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +295 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/debug.d.ts +8 -0
- package/dist/core/debug.d.ts.map +1 -0
- package/dist/core/debug.js +8 -0
- package/dist/core/debug.js.map +1 -0
- package/dist/core/downloader.d.ts +31 -0
- package/dist/core/downloader.d.ts.map +1 -0
- package/dist/core/downloader.js +180 -0
- package/dist/core/downloader.js.map +1 -0
- package/dist/core/errors.d.ts +80 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +134 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/extras.d.ts +160 -0
- package/dist/core/extras.d.ts.map +1 -0
- package/dist/core/extras.js +553 -0
- package/dist/core/extras.js.map +1 -0
- package/dist/core/health.d.ts +27 -0
- package/dist/core/health.d.ts.map +1 -0
- package/dist/core/health.js +96 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/history.d.ts +50 -0
- package/dist/core/history.d.ts.map +1 -0
- package/dist/core/history.js +276 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/intelligence/ast_index.d.ts +255 -0
- package/dist/core/intelligence/ast_index.d.ts.map +1 -0
- package/dist/core/intelligence/ast_index.js +1369 -0
- package/dist/core/intelligence/ast_index.js.map +1 -0
- package/dist/core/intelligence/confidence.d.ts +113 -0
- package/dist/core/intelligence/confidence.d.ts.map +1 -0
- package/dist/core/intelligence/confidence.js +298 -0
- package/dist/core/intelligence/confidence.js.map +1 -0
- package/dist/core/intelligence/context_compressor.d.ts +80 -0
- package/dist/core/intelligence/context_compressor.d.ts.map +1 -0
- package/dist/core/intelligence/context_compressor.js +245 -0
- package/dist/core/intelligence/context_compressor.js.map +1 -0
- package/dist/core/intelligence/daemon.d.ts +247 -0
- package/dist/core/intelligence/daemon.d.ts.map +1 -0
- package/dist/core/intelligence/daemon.js +842 -0
- package/dist/core/intelligence/daemon.js.map +1 -0
- package/dist/core/intelligence/index.d.ts +60 -0
- package/dist/core/intelligence/index.d.ts.map +1 -0
- package/dist/core/intelligence/index.js +101 -0
- package/dist/core/intelligence/index.js.map +1 -0
- package/dist/core/intelligence/predictor.d.ts +246 -0
- package/dist/core/intelligence/predictor.d.ts.map +1 -0
- package/dist/core/intelligence/predictor.js +590 -0
- package/dist/core/intelligence/predictor.js.map +1 -0
- package/dist/core/intelligence/reflex.d.ts +84 -0
- package/dist/core/intelligence/reflex.d.ts.map +1 -0
- package/dist/core/intelligence/reflex.js +344 -0
- package/dist/core/intelligence/reflex.js.map +1 -0
- package/dist/core/intelligence/self_correction.d.ts +121 -0
- package/dist/core/intelligence/self_correction.d.ts.map +1 -0
- package/dist/core/intelligence/self_correction.js +327 -0
- package/dist/core/intelligence/self_correction.js.map +1 -0
- package/dist/core/intelligence/skill_distiller.d.ts +100 -0
- package/dist/core/intelligence/skill_distiller.d.ts.map +1 -0
- package/dist/core/intelligence/skill_distiller.js +246 -0
- package/dist/core/intelligence/skill_distiller.js.map +1 -0
- package/dist/core/intelligence/swarm.d.ts +330 -0
- package/dist/core/intelligence/swarm.d.ts.map +1 -0
- package/dist/core/intelligence/swarm.js +1064 -0
- package/dist/core/intelligence/swarm.js.map +1 -0
- package/dist/core/intelligence/trajectory.d.ts +107 -0
- package/dist/core/intelligence/trajectory.d.ts.map +1 -0
- package/dist/core/intelligence/trajectory.js +330 -0
- package/dist/core/intelligence/trajectory.js.map +1 -0
- package/dist/core/logger.d.ts +111 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +640 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/loop.d.ts +123 -0
- package/dist/core/loop.d.ts.map +1 -0
- package/dist/core/loop.js +423 -0
- package/dist/core/loop.js.map +1 -0
- package/dist/core/memory.d.ts +81 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +328 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/metrics.d.ts +29 -0
- package/dist/core/metrics.d.ts.map +1 -0
- package/dist/core/metrics.js +109 -0
- package/dist/core/metrics.js.map +1 -0
- package/dist/core/models.d.ts +51 -0
- package/dist/core/models.d.ts.map +1 -0
- package/dist/core/models.js +146 -0
- package/dist/core/models.js.map +1 -0
- package/dist/core/optimizer.d.ts +34 -0
- package/dist/core/optimizer.d.ts.map +1 -0
- package/dist/core/optimizer.js +131 -0
- package/dist/core/optimizer.js.map +1 -0
- package/dist/core/persona.d.ts +35 -0
- package/dist/core/persona.d.ts.map +1 -0
- package/dist/core/persona.js +126 -0
- package/dist/core/persona.js.map +1 -0
- package/dist/core/rate_limit.d.ts +23 -0
- package/dist/core/rate_limit.d.ts.map +1 -0
- package/dist/core/rate_limit.js +62 -0
- package/dist/core/rate_limit.js.map +1 -0
- package/dist/core/runner.d.ts +63 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +671 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/stream_parser.d.ts +43 -0
- package/dist/core/stream_parser.d.ts.map +1 -0
- package/dist/core/stream_parser.js +140 -0
- package/dist/core/stream_parser.js.map +1 -0
- package/dist/core/tool_schemas.d.ts +26 -0
- package/dist/core/tool_schemas.d.ts.map +1 -0
- package/dist/core/tool_schemas.js +124 -0
- package/dist/core/tool_schemas.js.map +1 -0
- package/dist/core/which.d.ts +2 -0
- package/dist/core/which.d.ts.map +1 -0
- package/dist/core/which.js +35 -0
- package/dist/core/which.js.map +1 -0
- package/dist/goals/index.d.ts +9 -0
- package/dist/goals/index.d.ts.map +1 -0
- package/dist/goals/index.js +17 -0
- package/dist/goals/index.js.map +1 -0
- package/dist/goals/service.d.ts +115 -0
- package/dist/goals/service.d.ts.map +1 -0
- package/dist/goals/service.js +415 -0
- package/dist/goals/service.js.map +1 -0
- package/dist/goals/types.d.ts +47 -0
- package/dist/goals/types.d.ts.map +1 -0
- package/dist/goals/types.js +66 -0
- package/dist/goals/types.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/autonomous_startup.d.ts +51 -0
- package/dist/integrations/autonomous_startup.d.ts.map +1 -0
- package/dist/integrations/autonomous_startup.js +419 -0
- package/dist/integrations/autonomous_startup.js.map +1 -0
- package/dist/integrations/goose.d.ts +105 -0
- package/dist/integrations/goose.d.ts.map +1 -0
- package/dist/integrations/goose.js +408 -0
- package/dist/integrations/goose.js.map +1 -0
- package/dist/integrations/index.d.ts +21 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +17 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/mcp.d.ts +73 -0
- package/dist/integrations/mcp.d.ts.map +1 -0
- package/dist/integrations/mcp.js +307 -0
- package/dist/integrations/mcp.js.map +1 -0
- package/dist/integrations/mempalace.d.ts +18 -0
- package/dist/integrations/mempalace.d.ts.map +1 -0
- package/dist/integrations/mempalace.js +25 -0
- package/dist/integrations/mempalace.js.map +1 -0
- package/dist/integrations/multi_agent.d.ts +82 -0
- package/dist/integrations/multi_agent.d.ts.map +1 -0
- package/dist/integrations/multi_agent.js +280 -0
- package/dist/integrations/multi_agent.js.map +1 -0
- package/dist/integrations/unified_memory.d.ts +415 -0
- package/dist/integrations/unified_memory.d.ts.map +1 -0
- package/dist/integrations/unified_memory.js +1489 -0
- package/dist/integrations/unified_memory.js.map +1 -0
- package/dist/roles/beru.yaml +48 -0
- package/dist/sidecar/client.d.ts +63 -0
- package/dist/sidecar/client.d.ts.map +1 -0
- package/dist/sidecar/client.js +222 -0
- package/dist/sidecar/client.js.map +1 -0
- package/dist/sidecar/index.d.ts +7 -0
- package/dist/sidecar/index.d.ts.map +1 -0
- package/dist/sidecar/index.js +6 -0
- package/dist/sidecar/index.js.map +1 -0
- package/dist/startup.d.ts +22 -0
- package/dist/startup.d.ts.map +1 -0
- package/dist/startup.js +194 -0
- package/dist/startup.js.map +1 -0
- package/dist/tools/_internal.d.ts +90 -0
- package/dist/tools/_internal.d.ts.map +1 -0
- package/dist/tools/_internal.js +375 -0
- package/dist/tools/_internal.js.map +1 -0
- package/dist/tools/_smoke.d.ts +2 -0
- package/dist/tools/_smoke.d.ts.map +1 -0
- package/dist/tools/_smoke.js +203 -0
- package/dist/tools/_smoke.js.map +1 -0
- package/dist/tools/axodex_tools.d.ts +51 -0
- package/dist/tools/axodex_tools.d.ts.map +1 -0
- package/dist/tools/axodex_tools.js +181 -0
- package/dist/tools/axodex_tools.js.map +1 -0
- package/dist/tools/code_tools.d.ts +33 -0
- package/dist/tools/code_tools.d.ts.map +1 -0
- package/dist/tools/code_tools.js +191 -0
- package/dist/tools/code_tools.js.map +1 -0
- package/dist/tools/computer_tools.d.ts +59 -0
- package/dist/tools/computer_tools.d.ts.map +1 -0
- package/dist/tools/computer_tools.js +348 -0
- package/dist/tools/computer_tools.js.map +1 -0
- package/dist/tools/file_tools.d.ts +51 -0
- package/dist/tools/file_tools.d.ts.map +1 -0
- package/dist/tools/file_tools.js +424 -0
- package/dist/tools/file_tools.js.map +1 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +13 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/shell_tools.d.ts +39 -0
- package/dist/tools/shell_tools.d.ts.map +1 -0
- package/dist/tools/shell_tools.js +163 -0
- package/dist/tools/shell_tools.js.map +1 -0
- package/dist/tools/web_tools.d.ts +31 -0
- package/dist/tools/web_tools.d.ts.map +1 -0
- package/dist/tools/web_tools.js +117 -0
- package/dist/tools/web_tools.js.map +1 -0
- package/dist/voice/index.d.ts +41 -0
- package/dist/voice/index.d.ts.map +1 -0
- package/dist/voice/index.js +58 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/light_daemon.d.ts +197 -0
- package/dist/voice/light_daemon.d.ts.map +1 -0
- package/dist/voice/light_daemon.js +746 -0
- package/dist/voice/light_daemon.js.map +1 -0
- package/dist/voice/setup.d.ts +63 -0
- package/dist/voice/setup.d.ts.map +1 -0
- package/dist/voice/setup.js +443 -0
- package/dist/voice/setup.js.map +1 -0
- package/dist/voice/stt.d.ts +148 -0
- package/dist/voice/stt.d.ts.map +1 -0
- package/dist/voice/stt.js +361 -0
- package/dist/voice/stt.js.map +1 -0
- package/dist/voice/tts.d.ts +168 -0
- package/dist/voice/tts.d.ts.map +1 -0
- package/dist/voice/tts.js +698 -0
- package/dist/voice/tts.js.map +1 -0
- package/dist/voice/voice_loop.d.ts +101 -0
- package/dist/voice/voice_loop.d.ts.map +1 -0
- package/dist/voice/voice_loop.js +463 -0
- package/dist/voice/voice_loop.js.map +1 -0
- package/dist/voice/wake_word.d.ts +153 -0
- package/dist/voice/wake_word.d.ts.map +1 -0
- package/dist/voice/wake_word.js +458 -0
- package/dist/voice/wake_word.js.map +1 -0
- package/dist/voice/wav.d.ts +99 -0
- package/dist/voice/wav.d.ts.map +1 -0
- package/dist/voice/wav.js +306 -0
- package/dist/voice/wav.js.map +1 -0
- package/dist/web/broker.d.ts +33 -0
- package/dist/web/broker.d.ts.map +1 -0
- package/dist/web/broker.js +58 -0
- package/dist/web/broker.js.map +1 -0
- package/dist/web/open_browser.d.ts +2 -0
- package/dist/web/open_browser.d.ts.map +1 -0
- package/dist/web/open_browser.js +42 -0
- package/dist/web/open_browser.js.map +1 -0
- package/dist/web/server.d.ts +54 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +831 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/static/app.js +4121 -0
- package/dist/web/static/index.html +2281 -0
- package/dist/web/static/js/MODULE_SUMMARY.md +124 -0
- package/dist/web/static/js/a11y.js +165 -0
- package/dist/web/static/js/admin.js +2145 -0
- package/dist/web/static/js/assistant.js +475 -0
- package/dist/web/static/js/calendar/reminders.js +114 -0
- package/dist/web/static/js/calendar/utils.js +166 -0
- package/dist/web/static/js/calendar.js +3348 -0
- package/dist/web/static/js/censor.js +356 -0
- package/dist/web/static/js/chat.js +4546 -0
- package/dist/web/static/js/chatRenderer.js +2294 -0
- package/dist/web/static/js/chatStream.js +276 -0
- package/dist/web/static/js/codeRunner.js +398 -0
- package/dist/web/static/js/color/hex.js +14 -0
- package/dist/web/static/js/colorPicker.js +453 -0
- package/dist/web/static/js/compare/icons.js +77 -0
- package/dist/web/static/js/compare/index.js +1482 -0
- package/dist/web/static/js/compare/models.js +104 -0
- package/dist/web/static/js/compare/panes.js +817 -0
- package/dist/web/static/js/compare/probe.js +78 -0
- package/dist/web/static/js/compare/scoreboard.js +223 -0
- package/dist/web/static/js/compare/selector.js +1335 -0
- package/dist/web/static/js/compare/state.js +58 -0
- package/dist/web/static/js/compare/stream.js +695 -0
- package/dist/web/static/js/compare/vote.js +254 -0
- package/dist/web/static/js/cookbook-diagnosis.js +694 -0
- package/dist/web/static/js/cookbook-hwfit.js +1888 -0
- package/dist/web/static/js/cookbook.js +2093 -0
- package/dist/web/static/js/cookbookDownload.js +633 -0
- package/dist/web/static/js/cookbookProgressSignal.js +29 -0
- package/dist/web/static/js/cookbookRunning.js +3506 -0
- package/dist/web/static/js/cookbookServe.js +2128 -0
- package/dist/web/static/js/document.js +9731 -0
- package/dist/web/static/js/documentLibrary.js +3376 -0
- package/dist/web/static/js/dragSort.js +265 -0
- package/dist/web/static/js/editor/ai-inpaint.js +374 -0
- package/dist/web/static/js/editor/ai-models.js +273 -0
- package/dist/web/static/js/editor/ai-rembg.js +230 -0
- package/dist/web/static/js/editor/ai-tool-runner.js +147 -0
- package/dist/web/static/js/editor/ai-tools-misc.js +216 -0
- package/dist/web/static/js/editor/build/controls.js +366 -0
- package/dist/web/static/js/editor/build/popups.js +112 -0
- package/dist/web/static/js/editor/build/right-panel.js +200 -0
- package/dist/web/static/js/editor/build/toolbar.js +73 -0
- package/dist/web/static/js/editor/build/topbar.js +131 -0
- package/dist/web/static/js/editor/build/transform-popup.js +109 -0
- package/dist/web/static/js/editor/canvas-coords.js +21 -0
- package/dist/web/static/js/editor/canvas-events.js +197 -0
- package/dist/web/static/js/editor/canvas-transforms.js +132 -0
- package/dist/web/static/js/editor/checkerboard.js +24 -0
- package/dist/web/static/js/editor/clipboard-and-drop.js +133 -0
- package/dist/web/static/js/editor/composite-helpers.js +83 -0
- package/dist/web/static/js/editor/filters/blur.js +118 -0
- package/dist/web/static/js/editor/filters/edge-feather.js +70 -0
- package/dist/web/static/js/editor/fx/adj-popup.js +677 -0
- package/dist/web/static/js/editor/fx/filter-string.js +38 -0
- package/dist/web/static/js/editor/fx/histogram.js +67 -0
- package/dist/web/static/js/editor/fx/pixel-pass.js +249 -0
- package/dist/web/static/js/editor/harmonize-masks.js +116 -0
- package/dist/web/static/js/editor/history-panel.js +176 -0
- package/dist/web/static/js/editor/keyboard-shortcuts.js +271 -0
- package/dist/web/static/js/editor/layer-helpers.js +131 -0
- package/dist/web/static/js/editor/layer-panel.js +600 -0
- package/dist/web/static/js/editor/mask-utils.js +91 -0
- package/dist/web/static/js/editor/shortcuts-popover.js +149 -0
- package/dist/web/static/js/editor/slider-ux.js +191 -0
- package/dist/web/static/js/editor/snap.js +107 -0
- package/dist/web/static/js/editor/state.js +257 -0
- package/dist/web/static/js/editor/stroke-pipeline.js +162 -0
- package/dist/web/static/js/editor/stroke-tool-sliders.js +64 -0
- package/dist/web/static/js/editor/tools/clone.js +81 -0
- package/dist/web/static/js/editor/tools/crop.js +137 -0
- package/dist/web/static/js/editor/tools/flood-fill.js +72 -0
- package/dist/web/static/js/editor/tools/lasso-mask.js +171 -0
- package/dist/web/static/js/editor/tools/lasso.js +65 -0
- package/dist/web/static/js/editor/tools/move.js +79 -0
- package/dist/web/static/js/editor/tools/stroke.js +123 -0
- package/dist/web/static/js/editor/tools/transform-drag.js +174 -0
- package/dist/web/static/js/editor/tools/transform-handles.js +271 -0
- package/dist/web/static/js/editor/tools/transform-session.js +381 -0
- package/dist/web/static/js/editor/tools/wand.js +46 -0
- package/dist/web/static/js/editor/wire-import.js +148 -0
- package/dist/web/static/js/editor/wire-inpaint-controls.js +164 -0
- package/dist/web/static/js/editor/wire-merge-buttons.js +103 -0
- package/dist/web/static/js/editor/wire-selection-controls.js +170 -0
- package/dist/web/static/js/editor/wire-topbar-menus.js +174 -0
- package/dist/web/static/js/editor/wire-topbar-overflow.js +49 -0
- package/dist/web/static/js/editor/wire-topbar.js +188 -0
- package/dist/web/static/js/emailInbox.js +1245 -0
- package/dist/web/static/js/emailLibrary/replyRecipients.js +27 -0
- package/dist/web/static/js/emailLibrary/signatureFold.js +339 -0
- package/dist/web/static/js/emailLibrary/state.js +34 -0
- package/dist/web/static/js/emailLibrary/utils.js +202 -0
- package/dist/web/static/js/emailLibrary.js +5217 -0
- package/dist/web/static/js/emojiPicker.js +311 -0
- package/dist/web/static/js/escMenuStack.js +102 -0
- package/dist/web/static/js/fileHandler.js +295 -0
- package/dist/web/static/js/gallery.js +2835 -0
- package/dist/web/static/js/galleryEditor.js +3798 -0
- package/dist/web/static/js/group.js +955 -0
- package/dist/web/static/js/init.js +405 -0
- package/dist/web/static/js/keyboard-shortcuts.js +292 -0
- package/dist/web/static/js/langIcons.js +187 -0
- package/dist/web/static/js/markdown/tableRow.js +19 -0
- package/dist/web/static/js/markdown.js +806 -0
- package/dist/web/static/js/memory.js +1418 -0
- package/dist/web/static/js/modalManager.js +1552 -0
- package/dist/web/static/js/modalSnap.js +782 -0
- package/dist/web/static/js/model/matchKey.js +19 -0
- package/dist/web/static/js/modelPicker.js +695 -0
- package/dist/web/static/js/modelSort.js +33 -0
- package/dist/web/static/js/models.js +642 -0
- package/dist/web/static/js/notes.js +5054 -0
- package/dist/web/static/js/package.json +1 -0
- package/dist/web/static/js/platform.js +47 -0
- package/dist/web/static/js/presets.js +1110 -0
- package/dist/web/static/js/providers.js +93 -0
- package/dist/web/static/js/rag.js +177 -0
- package/dist/web/static/js/research/jobs.js +335 -0
- package/dist/web/static/js/research/panel.js +1233 -0
- package/dist/web/static/js/researchSynapse.js +225 -0
- package/dist/web/static/js/search-chat.js +201 -0
- package/dist/web/static/js/search.js +52 -0
- package/dist/web/static/js/section-management.js +260 -0
- package/dist/web/static/js/sessions.js +3108 -0
- package/dist/web/static/js/settings.js +4991 -0
- package/dist/web/static/js/sidebar-layout.js +573 -0
- package/dist/web/static/js/signature.js +504 -0
- package/dist/web/static/js/skills.js +1908 -0
- package/dist/web/static/js/slashAutocomplete.js +267 -0
- package/dist/web/static/js/slashCommands.js +6099 -0
- package/dist/web/static/js/spinner.js +406 -0
- package/dist/web/static/js/storage.js +123 -0
- package/dist/web/static/js/tasks.js +2699 -0
- package/dist/web/static/js/theme.js +2086 -0
- package/dist/web/static/js/tileManager.js +386 -0
- package/dist/web/static/js/tourAutoplay.js +133 -0
- package/dist/web/static/js/tourHints.js +179 -0
- package/dist/web/static/js/tts-ai.js +521 -0
- package/dist/web/static/js/ui.js +1288 -0
- package/dist/web/static/js/util/ordinal.js +13 -0
- package/dist/web/static/js/voiceRecorder.js +283 -0
- package/dist/web/static/js/windowDrag.js +326 -0
- package/dist/web/static/js/windowResize.js +233 -0
- package/dist/web/static/landing.html +824 -0
- package/dist/web/static/lib/docx.umd.min.js +20116 -0
- package/dist/web/static/lib/highlight.min.js +1213 -0
- package/dist/web/static/lib/html2pdf.bundle.min.js +3 -0
- package/dist/web/static/lib/mammoth.browser.min.js +21 -0
- package/dist/web/static/lib/qrcode.min.js +8 -0
- package/dist/web/static/lib/xlsx.full.min.js +24 -0
- package/dist/web/static/login.html +582 -0
- package/dist/web/static/manifest.json +15 -0
- package/dist/web/static/style.css +35733 -0
- package/dist/web/static/sw.js +144 -0
- package/dist/workflows/engine.d.ts +162 -0
- package/dist/workflows/engine.d.ts.map +1 -0
- package/dist/workflows/engine.js +625 -0
- package/dist/workflows/engine.js.map +1 -0
- package/dist/workflows/index.d.ts +8 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +14 -0
- package/dist/workflows/index.js.map +1 -0
- package/package.json +112 -0
|
@@ -0,0 +1,1335 @@
|
|
|
1
|
+
// compare/selector.js — model selection modal
|
|
2
|
+
import state from './state.js';
|
|
3
|
+
import Storage from '../storage.js';
|
|
4
|
+
import { fetchModels, _persistSelections, getExcludedModels } from './models.js';
|
|
5
|
+
import { showScoreboard } from './scoreboard.js';
|
|
6
|
+
import { EYE_OPEN, EYE_CLOSED, ICON_DICE, ICON_PARALLEL, ICON_SEQUENTIAL, SAVE_ICON, WAVE_FRAMES, CHAT_ICON } from './icons.js';
|
|
7
|
+
import { _clearProbeWaves } from './probe.js';
|
|
8
|
+
import uiModule from '../ui.js';
|
|
9
|
+
import spinnerModule from '../spinner.js';
|
|
10
|
+
import themeModule from '../theme.js';
|
|
11
|
+
|
|
12
|
+
const escapeHtml = uiModule.esc;
|
|
13
|
+
|
|
14
|
+
// Match the Deep Research "Start" button (play icon + "Start", styled by
|
|
15
|
+
// .research-start-btn) so the two primary actions look identical.
|
|
16
|
+
const _CMP_PLAY_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5,3 19,12 5,21"/></svg>';
|
|
17
|
+
const _CMP_START_LABEL = _CMP_PLAY_ICON + ' Start';
|
|
18
|
+
|
|
19
|
+
/** Slot label: letters (A, B) in parallel, numbers (1, 2) in sequential */
|
|
20
|
+
function _slotChar(i) { return state._parallel ? String.fromCharCode(65 + i) : String(i + 1); }
|
|
21
|
+
|
|
22
|
+
/** Sync the Compare toolbar indicator button state. */
|
|
23
|
+
function _syncToolbarIndicator(active) {
|
|
24
|
+
// The old red-accent "Compare active — click to deactivate" chip is no longer
|
|
25
|
+
// shown — Compare is exited from its own header bar, so the input-bar tool
|
|
26
|
+
// indicator was redundant. Keep it hidden regardless of state.
|
|
27
|
+
const indicator = document.getElementById('compare-indicator-btn');
|
|
28
|
+
if (indicator) {
|
|
29
|
+
indicator.style.display = 'none';
|
|
30
|
+
indicator.classList.remove('active');
|
|
31
|
+
}
|
|
32
|
+
// Notify app.js to update the plus-dot indicator
|
|
33
|
+
document.dispatchEvent(new CustomEvent('overflow-state-change'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Disable tool toggles (web, bash, RAG, research) for clean comparison. */
|
|
37
|
+
function disableToolToggles() {
|
|
38
|
+
const ids = ['web-toggle', 'bash-toggle', 'rag-toggle', 'research-toggle'];
|
|
39
|
+
state._savedToggles = {};
|
|
40
|
+
ids.forEach(id => {
|
|
41
|
+
const chk = document.getElementById(id);
|
|
42
|
+
if (chk) {
|
|
43
|
+
state._savedToggles[id] = chk.checked;
|
|
44
|
+
if (chk.checked) { chk.checked = false; chk.dispatchEvent(new Event('change')); }
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Restore tool toggles to pre-compare state. */
|
|
50
|
+
function restoreToolToggles() {
|
|
51
|
+
if (!state._savedToggles) return;
|
|
52
|
+
Object.entries(state._savedToggles).forEach(([id, wasChecked]) => {
|
|
53
|
+
const chk = document.getElementById(id);
|
|
54
|
+
if (chk && wasChecked && !chk.checked) { chk.checked = true; chk.dispatchEvent(new Event('change')); }
|
|
55
|
+
});
|
|
56
|
+
state._savedToggles = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Show model selection modal with dynamic model list + toggles. */
|
|
60
|
+
async function showModelSelector() {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
let models = [];
|
|
63
|
+
let _modelsLoaded = false;
|
|
64
|
+
|
|
65
|
+
const overlay = document.createElement('div');
|
|
66
|
+
overlay.id = 'compare-model-overlay';
|
|
67
|
+
overlay.className = 'modal';
|
|
68
|
+
|
|
69
|
+
const content = document.createElement('div');
|
|
70
|
+
content.className = 'modal-content';
|
|
71
|
+
content.style.width = 'min(520px, 92vw)';
|
|
72
|
+
|
|
73
|
+
// ── Header (draggable) ──
|
|
74
|
+
const header = document.createElement('div');
|
|
75
|
+
header.className = 'modal-header';
|
|
76
|
+
|
|
77
|
+
const title = document.createElement('h4');
|
|
78
|
+
title.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><path d="M11 18H8a2 2 0 0 1-2-2V9"/></svg>Model Comparison';
|
|
79
|
+
// Absorb the free space so the injected minimize (_) and close (✕) cluster
|
|
80
|
+
// together on the right instead of being spread apart by space-between.
|
|
81
|
+
title.style.marginRight = 'auto';
|
|
82
|
+
header.appendChild(title);
|
|
83
|
+
|
|
84
|
+
// Minimize (_) + Close (✕) grouped in one wrapper so they're always
|
|
85
|
+
// adjacent on the right (the auto-injected minimize otherwise drifted
|
|
86
|
+
// away from the close). The minimize carries .minimize-btn so the modal
|
|
87
|
+
// manager wires it instead of injecting a second one.
|
|
88
|
+
const headerCtrls = document.createElement('div');
|
|
89
|
+
headerCtrls.style.cssText = 'display:flex;align-items:center;gap:6px;flex-shrink:0;';
|
|
90
|
+
|
|
91
|
+
const headerMinBtn = document.createElement('button');
|
|
92
|
+
headerMinBtn.type = 'button';
|
|
93
|
+
headerMinBtn.className = 'modal-minimize-btn minimize-btn';
|
|
94
|
+
headerMinBtn.title = 'Minimize';
|
|
95
|
+
headerMinBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="5" y1="18" x2="19" y2="18"/></svg>';
|
|
96
|
+
headerMinBtn.style.margin = '0';
|
|
97
|
+
|
|
98
|
+
const headerCloseBtn = document.createElement('button');
|
|
99
|
+
headerCloseBtn.className = 'close-btn';
|
|
100
|
+
headerCloseBtn.innerHTML = '✖';
|
|
101
|
+
headerCloseBtn.style.cssText = 'flex-shrink:0;margin:0;';
|
|
102
|
+
headerCloseBtn.addEventListener('click', () => cleanup(false));
|
|
103
|
+
|
|
104
|
+
headerCtrls.appendChild(headerMinBtn);
|
|
105
|
+
headerCtrls.appendChild(headerCloseBtn);
|
|
106
|
+
|
|
107
|
+
// Toggle icons container
|
|
108
|
+
const toggleRow = document.createElement('div');
|
|
109
|
+
toggleRow.style.cssText = 'display:flex;gap:4px;align-items:flex-start;margin-left:auto;margin-right:8px;';
|
|
110
|
+
|
|
111
|
+
function _toggleLabel(text) {
|
|
112
|
+
return '<span class="compare-toggle-label">' + text + '</span>';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
state._blindMode = true;
|
|
116
|
+
const blindBtn = document.createElement('button');
|
|
117
|
+
blindBtn.type = 'button';
|
|
118
|
+
blindBtn.className = 'compare-blind-toggle active';
|
|
119
|
+
blindBtn.title = 'Blind Mode — hide model names until you vote';
|
|
120
|
+
blindBtn.innerHTML = EYE_CLOSED + _toggleLabel('Blind');
|
|
121
|
+
blindBtn.addEventListener('click', () => {
|
|
122
|
+
state._blindMode = !state._blindMode;
|
|
123
|
+
blindBtn.classList.toggle('active', state._blindMode);
|
|
124
|
+
blindBtn.innerHTML = (state._blindMode ? EYE_CLOSED : EYE_OPEN) + _toggleLabel('Blind');
|
|
125
|
+
// Turning off blind mode reveals shuffled models
|
|
126
|
+
if (!state._blindMode && _shuffled) {
|
|
127
|
+
_shuffled = false;
|
|
128
|
+
diceBtn.classList.remove('active');
|
|
129
|
+
}
|
|
130
|
+
renderModelRows();
|
|
131
|
+
// Mobile hides the button labels — surface the new state as a toast.
|
|
132
|
+
uiModule.showToast('Mode: Blind ' + (state._blindMode ? 'on' : 'off'));
|
|
133
|
+
_updateModeLabel();
|
|
134
|
+
_setModeHint(state._blindMode
|
|
135
|
+
? '<span style="color:var(--color-blind-orange)">Blind mode</span>: model names stay hidden until you vote.'
|
|
136
|
+
: '<span style="color:var(--color-blind-orange)">Blind mode off</span>: model names are shown.');
|
|
137
|
+
});
|
|
138
|
+
toggleRow.appendChild(blindBtn);
|
|
139
|
+
|
|
140
|
+
// Parallel / Sequential toggle — right after blind
|
|
141
|
+
state._parallel = true;
|
|
142
|
+
const parallelBtn = document.createElement('button');
|
|
143
|
+
parallelBtn.type = 'button';
|
|
144
|
+
parallelBtn.className = 'compare-parallel-toggle active';
|
|
145
|
+
parallelBtn.title = 'Parallel — run all models at once vs one at a time';
|
|
146
|
+
parallelBtn.innerHTML = ICON_PARALLEL + _toggleLabel('Parallel');
|
|
147
|
+
parallelBtn.addEventListener('click', () => {
|
|
148
|
+
state._parallel = !state._parallel;
|
|
149
|
+
parallelBtn.classList.toggle('active', state._parallel);
|
|
150
|
+
parallelBtn.innerHTML = (state._parallel ? ICON_PARALLEL : ICON_SEQUENTIAL) + _toggleLabel(state._parallel ? 'Parallel' : 'Sequential');
|
|
151
|
+
parallelBtn.title = state._parallel ? 'Switch to one at a time' : 'Run side by side';
|
|
152
|
+
renderModelRows();
|
|
153
|
+
uiModule.showToast('Mode: ' + (state._parallel ? 'Parallel' : 'Sequential'));
|
|
154
|
+
_updateModeLabel();
|
|
155
|
+
_setModeHint(state._parallel
|
|
156
|
+
? '<span style="color:#5b8def">Parallel</span>: all models answer at once, side by side.'
|
|
157
|
+
: '<span style="color:#e0a050">Sequential</span>: models answer one at a time.');
|
|
158
|
+
});
|
|
159
|
+
toggleRow.appendChild(parallelBtn);
|
|
160
|
+
|
|
161
|
+
// Dice / shuffle button — next to blind toggle
|
|
162
|
+
const diceBtn = document.createElement('button');
|
|
163
|
+
diceBtn.type = 'button';
|
|
164
|
+
diceBtn.className = 'compare-dice-toggle';
|
|
165
|
+
diceBtn.title = 'Shuffle — randomly pick models for each slot';
|
|
166
|
+
diceBtn.innerHTML = ICON_DICE + _toggleLabel('Shuffle');
|
|
167
|
+
diceBtn.addEventListener('click', () => {
|
|
168
|
+
if (!_modelsLoaded) return;
|
|
169
|
+
// Toggle off if already shuffled
|
|
170
|
+
if (_shuffled) {
|
|
171
|
+
_shuffled = false;
|
|
172
|
+
diceBtn.classList.remove('active');
|
|
173
|
+
renderModelRows();
|
|
174
|
+
uiModule.showToast('Mode: Shuffle off');
|
|
175
|
+
_updateModeLabel();
|
|
176
|
+
_setModeHint('<span style="color:var(--red)">Shuffle off</span>: choose the models yourself.');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Randomly pick models from filtered list for each slot
|
|
180
|
+
const excluded = getExcludedModels();
|
|
181
|
+
const pool = filteredModels().filter(m => !excluded.includes(m.id)).slice();
|
|
182
|
+
if (pool.length === 0) return;
|
|
183
|
+
for (let i = pool.length - 1; i > 0; i--) {
|
|
184
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
185
|
+
[pool[i], pool[j]] = [pool[j], pool[i]];
|
|
186
|
+
}
|
|
187
|
+
for (let i = 0; i < selections.length; i++) {
|
|
188
|
+
const m = pool[i % pool.length];
|
|
189
|
+
selections[i] = { model: m.id, endpoint: m.url, endpointId: m.endpointId, name: m.name, endpointName: m.endpointName || '' };
|
|
190
|
+
}
|
|
191
|
+
_shuffled = true;
|
|
192
|
+
// Auto-enable blind mode so picks stay hidden
|
|
193
|
+
if (!state._blindMode) {
|
|
194
|
+
state._blindMode = true;
|
|
195
|
+
blindBtn.classList.add('active');
|
|
196
|
+
blindBtn.innerHTML = EYE_CLOSED + _toggleLabel('Blind');
|
|
197
|
+
}
|
|
198
|
+
renderModelRows();
|
|
199
|
+
uiModule.showToast(state._blindMode ? 'Mode: Shuffle on · Blind on' : 'Mode: Shuffle on');
|
|
200
|
+
_updateModeLabel();
|
|
201
|
+
_setModeHint('<span style="color:var(--red)">Shuffle</span>: random models picked for each slot (auto-hidden).');
|
|
202
|
+
// Show active state + spin only the dice icon
|
|
203
|
+
diceBtn.classList.add('active');
|
|
204
|
+
const diceSvg = diceBtn.querySelector('svg');
|
|
205
|
+
if (diceSvg) {
|
|
206
|
+
diceSvg.style.transition = 'transform 0.3s ease';
|
|
207
|
+
diceSvg.style.transform = 'rotate(360deg)';
|
|
208
|
+
setTimeout(() => { diceSvg.style.transition = ''; diceSvg.style.transform = ''; }, 300);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
toggleRow.appendChild(diceBtn);
|
|
212
|
+
|
|
213
|
+
// (Pre-round "Shuffle models?" reminder removed at the user's request — the
|
|
214
|
+
// running-state panes still show their own shuffle nudge.)
|
|
215
|
+
function _remindShuffle() { /* no-op in the selector */ }
|
|
216
|
+
|
|
217
|
+
state._continueChat = false;
|
|
218
|
+
|
|
219
|
+
state._saveOnClose = false;
|
|
220
|
+
const saveBtn = document.createElement('button');
|
|
221
|
+
saveBtn.type = 'button';
|
|
222
|
+
saveBtn.className = 'compare-save-toggle';
|
|
223
|
+
saveBtn.title = 'Save — keep sessions after closing compare';
|
|
224
|
+
saveBtn.innerHTML = SAVE_ICON + _toggleLabel('Save');
|
|
225
|
+
saveBtn.addEventListener('click', () => {
|
|
226
|
+
state._saveOnClose = !state._saveOnClose;
|
|
227
|
+
saveBtn.classList.toggle('active', state._saveOnClose);
|
|
228
|
+
uiModule.showToast('Mode: Save ' + (state._saveOnClose ? 'on' : 'off'));
|
|
229
|
+
_updateModeLabel();
|
|
230
|
+
_setModeHint(state._saveOnClose
|
|
231
|
+
? '<span style="color:var(--color-save-green)">Save</span>: keep these sessions after you close Compare.'
|
|
232
|
+
: '<span style="color:var(--color-save-green)">Save off</span>: sessions are discarded when you close Compare.');
|
|
233
|
+
});
|
|
234
|
+
toggleRow.appendChild(saveBtn);
|
|
235
|
+
|
|
236
|
+
// Reset button
|
|
237
|
+
const resetBtn = document.createElement('button');
|
|
238
|
+
resetBtn.type = 'button';
|
|
239
|
+
resetBtn.className = 'compare-reset-toggle';
|
|
240
|
+
resetBtn.title = 'Reset — restore all defaults';
|
|
241
|
+
resetBtn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>' + _toggleLabel('Reset');
|
|
242
|
+
resetBtn.addEventListener('click', () => {
|
|
243
|
+
state._blindMode = true;
|
|
244
|
+
blindBtn.classList.add('active');
|
|
245
|
+
blindBtn.innerHTML = EYE_CLOSED + _toggleLabel('Blind');
|
|
246
|
+
_shuffled = false;
|
|
247
|
+
diceBtn.classList.remove('active');
|
|
248
|
+
state._continueChat = false;
|
|
249
|
+
state._saveOnClose = false;
|
|
250
|
+
saveBtn.classList.remove('active');
|
|
251
|
+
state._parallel = true;
|
|
252
|
+
parallelBtn.classList.add('active');
|
|
253
|
+
parallelBtn.innerHTML = ICON_PARALLEL + _toggleLabel('Parallel');
|
|
254
|
+
selections = [null, null];
|
|
255
|
+
renderModelRows();
|
|
256
|
+
});
|
|
257
|
+
toggleRow.appendChild(resetBtn);
|
|
258
|
+
|
|
259
|
+
header.appendChild(headerCtrls);
|
|
260
|
+
|
|
261
|
+
content.appendChild(header);
|
|
262
|
+
|
|
263
|
+
// ── Body ──
|
|
264
|
+
const body = document.createElement('div');
|
|
265
|
+
body.className = 'modal-body';
|
|
266
|
+
body.style.padding = '12px 16px';
|
|
267
|
+
|
|
268
|
+
const desc = document.createElement('p');
|
|
269
|
+
desc.style.cssText = 'color:color-mix(in srgb, var(--fg) 55%, transparent);font-size:0.85em;margin:0 0 12px;';
|
|
270
|
+
desc.textContent = 'Select models to compare side-by-side. Send the same prompt to all.';
|
|
271
|
+
body.appendChild(desc);
|
|
272
|
+
|
|
273
|
+
// Options row
|
|
274
|
+
toggleRow.style.cssText = 'display:flex;gap:4px;align-items:flex-start;flex-wrap:wrap;';
|
|
275
|
+
const modeWrap = document.createElement('div');
|
|
276
|
+
modeWrap.className = 'compare-section';
|
|
277
|
+
const modeLabel = document.createElement('div');
|
|
278
|
+
modeLabel.className = 'compare-section-label';
|
|
279
|
+
// The active modes (+colors) are appended in a span shown only on mobile,
|
|
280
|
+
// where the toggle text labels are hidden so the icons alone are ambiguous.
|
|
281
|
+
modeLabel.innerHTML = 'Mode: <span class="compare-mode-current"></span>';
|
|
282
|
+
modeWrap.appendChild(modeLabel);
|
|
283
|
+
modeWrap.appendChild(toggleRow);
|
|
284
|
+
// Contextual one-liner describing the mode you just toggled.
|
|
285
|
+
const modeHint = document.createElement('div');
|
|
286
|
+
modeHint.className = 'compare-mode-hint';
|
|
287
|
+
modeWrap.appendChild(modeHint);
|
|
288
|
+
function _setModeHint(html) { modeHint.innerHTML = html || ''; }
|
|
289
|
+
body.appendChild(modeWrap);
|
|
290
|
+
|
|
291
|
+
// Reflect the active modes in the "Mode:" label, each in its icon's color.
|
|
292
|
+
function _updateModeLabel() {
|
|
293
|
+
const cur = modeLabel.querySelector('.compare-mode-current');
|
|
294
|
+
if (!cur) return;
|
|
295
|
+
const parts = [];
|
|
296
|
+
if (state._blindMode) parts.push('<span style="color:var(--color-blind-orange)">Blind</span>');
|
|
297
|
+
parts.push(state._parallel
|
|
298
|
+
? '<span style="color:#5b8def">Parallel</span>'
|
|
299
|
+
: '<span style="color:#e0a050">Sequential</span>');
|
|
300
|
+
if (_shuffled) parts.push('<span style="color:var(--red)">Shuffle</span>');
|
|
301
|
+
if (state._saveOnClose) parts.push('<span style="color:var(--color-save-green)">Save</span>');
|
|
302
|
+
cur.innerHTML = parts.join(', ');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Type tabs (Chat / Agent / Search / Research) ──
|
|
306
|
+
state._compareMode = 'chat';
|
|
307
|
+
const typeWrap = document.createElement('div');
|
|
308
|
+
typeWrap.className = 'compare-section';
|
|
309
|
+
const typeLabel = document.createElement('div');
|
|
310
|
+
typeLabel.className = 'compare-section-label';
|
|
311
|
+
// The active type name (+icon) is appended in a span shown only on mobile,
|
|
312
|
+
// where the tab text labels are hidden so the icons alone are ambiguous.
|
|
313
|
+
typeLabel.innerHTML = 'Type: <span class="compare-type-current"></span>';
|
|
314
|
+
typeWrap.appendChild(typeLabel);
|
|
315
|
+
const tabBar = document.createElement('div');
|
|
316
|
+
tabBar.className = 'compare-mode-tabs compare-type-tabs';
|
|
317
|
+
// Agent — shell prompt `>_` (matches the bash-toggle-btn icon in the composer)
|
|
318
|
+
const _ICON_AGENT = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>';
|
|
319
|
+
const _ICON_SEARCH = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>';
|
|
320
|
+
// Research — magnifying glass with `+` (matches the sidebar Deep Research icon)
|
|
321
|
+
const _ICON_RESEARCH = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>';
|
|
322
|
+
const _modes = [
|
|
323
|
+
{ id: 'chat', label: 'Chat', icon: CHAT_ICON },
|
|
324
|
+
{ id: 'agent', label: 'Agent', icon: _ICON_AGENT },
|
|
325
|
+
{ id: 'search', label: 'Search', icon: _ICON_SEARCH },
|
|
326
|
+
{ id: 'research', label: 'Research', icon: _ICON_RESEARCH },
|
|
327
|
+
];
|
|
328
|
+
_modes.forEach(m => {
|
|
329
|
+
const tab = document.createElement('button');
|
|
330
|
+
tab.type = 'button';
|
|
331
|
+
tab.className = 'compare-mode-tab' + (m.id === 'chat' ? ' active' : '');
|
|
332
|
+
tab.innerHTML = m.icon + '<span class="compare-toggle-label">' + m.label + '</span>';
|
|
333
|
+
tab.dataset.mode = m.id;
|
|
334
|
+
tab.addEventListener('click', () => setModeTab(m.id));
|
|
335
|
+
tabBar.appendChild(tab);
|
|
336
|
+
});
|
|
337
|
+
// Reflect the active type in the "Type:" label (icon + name) for mobile.
|
|
338
|
+
function _updateTypeLabel(mode) {
|
|
339
|
+
const cur = typeLabel.querySelector('.compare-type-current');
|
|
340
|
+
const m = _modes.find(x => x.id === mode);
|
|
341
|
+
if (cur && m) cur.innerHTML = m.icon + '<span>' + m.label + '</span>';
|
|
342
|
+
}
|
|
343
|
+
_updateTypeLabel('chat');
|
|
344
|
+
typeWrap.appendChild(tabBar);
|
|
345
|
+
body.appendChild(typeWrap);
|
|
346
|
+
|
|
347
|
+
// Per-tab selection memory
|
|
348
|
+
const _tabSelections = { chat: null, agent: null, search: null, research: null };
|
|
349
|
+
|
|
350
|
+
function setModeTab(mode) {
|
|
351
|
+
if (!_modelsLoaded) return;
|
|
352
|
+
// Save current tab's selections before switching
|
|
353
|
+
_tabSelections[state._compareMode] = selections.map(s => s ? { ...s } : null);
|
|
354
|
+
state._compareMode = mode;
|
|
355
|
+
tabBar.querySelectorAll('.compare-mode-tab').forEach(t => t.classList.remove('active'));
|
|
356
|
+
const activeTab = tabBar.querySelector(`[data-mode="${mode}"]`);
|
|
357
|
+
if (activeTab) activeTab.classList.add('active');
|
|
358
|
+
_updateTypeLabel(mode);
|
|
359
|
+
_shuffled = false;
|
|
360
|
+
diceBtn.classList.remove('active');
|
|
361
|
+
// Search and Research default to sequential; others default to parallel
|
|
362
|
+
if (mode === 'search' || mode === 'research') {
|
|
363
|
+
state._parallel = false;
|
|
364
|
+
parallelBtn.classList.remove('active');
|
|
365
|
+
parallelBtn.innerHTML = ICON_SEQUENTIAL + _toggleLabel('Sequential');
|
|
366
|
+
} else {
|
|
367
|
+
state._parallel = true;
|
|
368
|
+
parallelBtn.classList.add('active');
|
|
369
|
+
parallelBtn.innerHTML = ICON_PARALLEL + _toggleLabel('Parallel');
|
|
370
|
+
}
|
|
371
|
+
// Restore saved selections for this tab, or default
|
|
372
|
+
selections = _tabSelections[mode] ? _tabSelections[mode].slice() : [null, null];
|
|
373
|
+
_updateModeLabel();
|
|
374
|
+
_setModeHint('');
|
|
375
|
+
renderModelRows();
|
|
376
|
+
}
|
|
377
|
+
// Tab click listeners are set in the loop above
|
|
378
|
+
|
|
379
|
+
// ── Model list ──
|
|
380
|
+
const listContainer = document.createElement('div');
|
|
381
|
+
body.appendChild(listContainer);
|
|
382
|
+
|
|
383
|
+
// Show loading state immediately with spinner
|
|
384
|
+
const _loadingDiv = document.createElement('div');
|
|
385
|
+
_loadingDiv.style.cssText = 'color:color-mix(in srgb, var(--fg) 40%, transparent);font-size:0.85em;padding:12px 0;text-align:left;';
|
|
386
|
+
if (spinnerModule) {
|
|
387
|
+
const _loadSpinner = spinnerModule.create('Loading models', 'right');
|
|
388
|
+
_loadingDiv.appendChild(_loadSpinner.createElement());
|
|
389
|
+
_loadSpinner.start();
|
|
390
|
+
} else {
|
|
391
|
+
_loadingDiv.textContent = 'Loading models\u2026';
|
|
392
|
+
}
|
|
393
|
+
listContainer.appendChild(_loadingDiv);
|
|
394
|
+
|
|
395
|
+
// Restore last used selections from storage (per-mode)
|
|
396
|
+
const _selKey = 'zero-compare-selections-' + (state._compareMode || 'chat');
|
|
397
|
+
let selections = Storage.getJSON(_selKey) || Storage.getJSON('zero-compare-selections') || [];
|
|
398
|
+
// Restore synthesis models for search/research
|
|
399
|
+
if (state._compareMode === 'search' || state._compareMode === 'research') {
|
|
400
|
+
const savedSynth = Storage.getJSON('zero-compare-synth-' + state._compareMode);
|
|
401
|
+
if (savedSynth) state._searchSynthModels = savedSynth;
|
|
402
|
+
}
|
|
403
|
+
// Validate saved selections against available models (done after models load)
|
|
404
|
+
let _needsValidation = selections.length > 0;
|
|
405
|
+
let addBtn = null;
|
|
406
|
+
let _shuffled = false;
|
|
407
|
+
_updateModeLabel(); // initial readout (Blind + Parallel on by default)
|
|
408
|
+
|
|
409
|
+
function filteredModels() {
|
|
410
|
+
// Agent and Research modes use chat models
|
|
411
|
+
const effectiveType = (state._compareMode === 'agent' || state._compareMode === 'research') ? 'chat' : state._compareMode;
|
|
412
|
+
return models.filter(m => m.type === effectiveType);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function buildOption(m) {
|
|
416
|
+
return {
|
|
417
|
+
val: JSON.stringify({ model: m.id, endpoint: m.url, endpointId: m.endpointId, name: m.name, endpointName: m.endpointName || '' }),
|
|
418
|
+
label: m.endpointName ? `${m.name} (${m.endpointName})` : m.name,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Build a searchable model picker (used when >5 models) */
|
|
423
|
+
function _buildSearchablePicker(modelList, currentSel, slotIdx, onSelect) {
|
|
424
|
+
const wrap = document.createElement('div');
|
|
425
|
+
wrap.style.cssText = 'flex:1;position:relative;';
|
|
426
|
+
|
|
427
|
+
const input = document.createElement('input');
|
|
428
|
+
input.type = 'text';
|
|
429
|
+
input.placeholder = 'Search models\u2026';
|
|
430
|
+
input.className = 'cmp-form-control';
|
|
431
|
+
input.style.cssText = 'width:100%;box-sizing:border-box;';
|
|
432
|
+
// Mobile: suppress the on-screen keyboard so tapping the picker
|
|
433
|
+
// opens the dropdown but doesn't shove a keyboard up over the list.
|
|
434
|
+
// (Matches the +Model dropdown's mobile behavior.)
|
|
435
|
+
if (window.innerWidth <= 768) {
|
|
436
|
+
input.setAttribute('inputmode', 'none');
|
|
437
|
+
input.setAttribute('readonly', 'readonly');
|
|
438
|
+
}
|
|
439
|
+
if (currentSel) {
|
|
440
|
+
const m = modelList.find(m => m.id === currentSel.model && m.url === currentSel.endpoint)
|
|
441
|
+
|| modelList.find(m => m.id === currentSel.model);
|
|
442
|
+
if (m) input.value = buildOption(m).label;
|
|
443
|
+
} else {
|
|
444
|
+
const fallback = modelList[Math.min(slotIdx, modelList.length - 1)];
|
|
445
|
+
if (fallback) input.value = buildOption(fallback).label;
|
|
446
|
+
}
|
|
447
|
+
wrap.appendChild(input);
|
|
448
|
+
|
|
449
|
+
const dropdown = document.createElement('div');
|
|
450
|
+
dropdown.className = 'cmp-picker-dropdown';
|
|
451
|
+
// Appended to document.body (NOT wrap) and position:fixed so it escapes
|
|
452
|
+
// both the modal's overflow clipping AND any transform on the modal-content
|
|
453
|
+
// (a transformed ancestor makes position:fixed clip to it — which was why
|
|
454
|
+
// the dropdown kept cropping under the next row). Coords set in _placeDropdown.
|
|
455
|
+
dropdown.style.cssText = 'display:none;position:fixed;max-height:200px;overflow-y:auto;background:var(--panel);border:1px solid var(--border);border-radius:6px;z-index:100000;box-shadow:0 4px 12px rgba(0,0,0,0.2);';
|
|
456
|
+
document.body.appendChild(dropdown);
|
|
457
|
+
|
|
458
|
+
function renderItems(query) {
|
|
459
|
+
dropdown.innerHTML = '';
|
|
460
|
+
const q = (query || '').toLowerCase();
|
|
461
|
+
const matches = modelList.filter(m => {
|
|
462
|
+
const label = buildOption(m).label.toLowerCase();
|
|
463
|
+
return !q || label.includes(q);
|
|
464
|
+
});
|
|
465
|
+
if (matches.length === 0) {
|
|
466
|
+
const empty = document.createElement('div');
|
|
467
|
+
empty.style.cssText = 'padding:8px 12px;color:color-mix(in srgb, var(--fg) 40%, transparent);font-size:0.82em;font-style:italic;';
|
|
468
|
+
empty.textContent = 'No matches';
|
|
469
|
+
dropdown.appendChild(empty);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
matches.forEach(m => {
|
|
473
|
+
const opt = buildOption(m);
|
|
474
|
+
const item = document.createElement('div');
|
|
475
|
+
item.style.cssText = 'padding:6px 12px;cursor:pointer;font-size:0.85em;transition:background 0.08s;';
|
|
476
|
+
item.textContent = opt.label;
|
|
477
|
+
const isSelected = currentSel && currentSel.model === m.id && (currentSel.endpoint === m.url || !modelList.some(o => o.id === m.id && o !== m));
|
|
478
|
+
if (isSelected) item.style.background = 'color-mix(in srgb, var(--fg) 8%, transparent)';
|
|
479
|
+
item.addEventListener('mouseenter', () => { item.style.background = 'color-mix(in srgb, var(--fg) 10%, transparent)'; });
|
|
480
|
+
item.addEventListener('mouseleave', () => { item.style.background = isSelected ? 'color-mix(in srgb, var(--fg) 8%, transparent)' : ''; });
|
|
481
|
+
item.addEventListener('click', () => {
|
|
482
|
+
const chosen = { model: m.id, endpoint: m.url, endpointId: m.endpointId, name: m.name, endpointName: m.endpointName || '' };
|
|
483
|
+
input.value = opt.label;
|
|
484
|
+
currentSel = chosen;
|
|
485
|
+
onSelect(chosen);
|
|
486
|
+
dropdown.style.display = 'none';
|
|
487
|
+
input.blur();
|
|
488
|
+
});
|
|
489
|
+
dropdown.appendChild(item);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Position the dropdown either below or above the input depending
|
|
494
|
+
// on which side has more room — otherwise on a mobile bottom-sheet
|
|
495
|
+
// a picker near the bottom of the screen would open downward and
|
|
496
|
+
// either clip past the modal or extend off the viewport.
|
|
497
|
+
const _placeDropdown = () => {
|
|
498
|
+
const inRect = input.getBoundingClientRect();
|
|
499
|
+
const vh = window.innerHeight;
|
|
500
|
+
const vw = window.innerWidth;
|
|
501
|
+
const below = vh - inRect.bottom;
|
|
502
|
+
const above = inRect.top;
|
|
503
|
+
const flipUp = below < 220 && above > below;
|
|
504
|
+
// Horizontal: align to the input but clamp inside the viewport so it
|
|
505
|
+
// never runs off the screen edge on mobile.
|
|
506
|
+
const width = Math.min(inRect.width, vw - 16);
|
|
507
|
+
let left = inRect.left;
|
|
508
|
+
if (left + width > vw - 8) left = vw - 8 - width;
|
|
509
|
+
if (left < 8) left = 8;
|
|
510
|
+
dropdown.style.left = left + 'px';
|
|
511
|
+
dropdown.style.width = width + 'px';
|
|
512
|
+
// Vertical: flip above/below based on available room (fixed coords).
|
|
513
|
+
if (flipUp) {
|
|
514
|
+
dropdown.style.top = 'auto';
|
|
515
|
+
dropdown.style.bottom = (vh - inRect.top + 2) + 'px';
|
|
516
|
+
dropdown.style.maxHeight = Math.max(120, Math.min(280, above - 16)) + 'px';
|
|
517
|
+
} else {
|
|
518
|
+
dropdown.style.bottom = 'auto';
|
|
519
|
+
dropdown.style.top = (inRect.bottom + 2) + 'px';
|
|
520
|
+
dropdown.style.maxHeight = Math.max(120, Math.min(280, below - 16)) + 'px';
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
input.addEventListener('focus', () => {
|
|
524
|
+
input.value = '';
|
|
525
|
+
renderItems('');
|
|
526
|
+
dropdown.style.display = '';
|
|
527
|
+
_placeDropdown();
|
|
528
|
+
});
|
|
529
|
+
input.addEventListener('input', () => {
|
|
530
|
+
renderItems(input.value);
|
|
531
|
+
dropdown.style.display = '';
|
|
532
|
+
_placeDropdown();
|
|
533
|
+
});
|
|
534
|
+
input.addEventListener('keydown', (e) => {
|
|
535
|
+
if (e.key === 'Enter') {
|
|
536
|
+
e.preventDefault();
|
|
537
|
+
const first = dropdown.querySelector('div[style*="cursor:pointer"]');
|
|
538
|
+
if (first) first.click();
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
// Close on outside click. The dropdown lives in document.body, so check
|
|
542
|
+
// both wrap and dropdown; and tear the dropdown down when the picker row
|
|
543
|
+
// is removed from the DOM (rebuild) so it doesn't orphan in the body.
|
|
544
|
+
function _closeHandler(e) {
|
|
545
|
+
if (!wrap.contains(e.target) && !dropdown.contains(e.target)) {
|
|
546
|
+
dropdown.style.display = 'none';
|
|
547
|
+
if (currentSel) {
|
|
548
|
+
const m = modelList.find(m => m.id === currentSel.model && m.url === currentSel.endpoint);
|
|
549
|
+
if (m) input.value = buildOption(m).label;
|
|
550
|
+
}
|
|
551
|
+
if (!wrap.isConnected) {
|
|
552
|
+
dropdown.remove();
|
|
553
|
+
document.removeEventListener('click', _closeHandler, true);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
setTimeout(() => document.addEventListener('click', _closeHandler, true), 0);
|
|
558
|
+
|
|
559
|
+
return wrap;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function renderModelRows() {
|
|
563
|
+
if (!_modelsLoaded) return;
|
|
564
|
+
// The picker dropdowns live in document.body (to escape modal clipping);
|
|
565
|
+
// clear any leftovers before rebuilding the rows so they don't orphan.
|
|
566
|
+
document.querySelectorAll('.cmp-picker-dropdown').forEach(d => d.remove());
|
|
567
|
+
|
|
568
|
+
// ── Search mode: show provider dropdowns ──
|
|
569
|
+
if (state._compareMode === 'search') {
|
|
570
|
+
listContainer.innerHTML = '';
|
|
571
|
+
if (!state._cachedProviders) {
|
|
572
|
+
listContainer.innerHTML = '<div style="color:color-mix(in srgb, var(--fg) 40%, transparent);font-size:0.85em;padding:12px 0;text-align:left;">Loading search providers\u2026</div>';
|
|
573
|
+
fetch(`${state.API_BASE}/api/search/providers`).then(r => r.json()).then(providers => {
|
|
574
|
+
state._cachedProviders = providers;
|
|
575
|
+
renderModelRows();
|
|
576
|
+
}).catch(() => {
|
|
577
|
+
listContainer.innerHTML = '<div style="color:var(--color-error);font-size:0.85em;padding:12px 0;">Failed to load search providers</div>';
|
|
578
|
+
});
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const available = state._cachedProviders.filter(p => p.available);
|
|
582
|
+
if (available.length === 0) {
|
|
583
|
+
listContainer.innerHTML = '<div style="color:color-mix(in srgb, var(--fg) 40%, transparent);font-size:0.85em;padding:12px 0;text-align:center;font-style:italic;">No search providers configured</div>';
|
|
584
|
+
if (addBtn) addBtn.style.display = 'none';
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
// Ensure per-pane synth model array matches selections length
|
|
588
|
+
if (!state._searchSynthModels) state._searchSynthModels = [];
|
|
589
|
+
while (state._searchSynthModels.length < selections.length) state._searchSynthModels.push(null);
|
|
590
|
+
|
|
591
|
+
const chatModels = state._cachedModels.filter(m => m.type === 'chat');
|
|
592
|
+
const _seqStepS = !state._parallel ? Math.min(20, Math.floor(80 / Math.max(selections.length, 1))) : 0;
|
|
593
|
+
|
|
594
|
+
selections.forEach((sel, idx) => {
|
|
595
|
+
const row = document.createElement('div');
|
|
596
|
+
row.className = 'cmp-model-row';
|
|
597
|
+
if (_seqStepS) row.style.marginLeft = (idx * _seqStepS) + 'px';
|
|
598
|
+
|
|
599
|
+
// Left label: number/letter or blind eye icon
|
|
600
|
+
const lbl = document.createElement('span');
|
|
601
|
+
lbl.className = 'cmp-row-label';
|
|
602
|
+
if (state._blindMode) {
|
|
603
|
+
lbl.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><line x1="8" y1="16" x2="16" y2="8"/><line x1="8" y1="8" x2="16" y2="16"/></svg>';
|
|
604
|
+
} else {
|
|
605
|
+
lbl.textContent = _slotChar(idx);
|
|
606
|
+
}
|
|
607
|
+
row.appendChild(lbl);
|
|
608
|
+
|
|
609
|
+
// Model picker (synthesis LLM) — searchable for large lists
|
|
610
|
+
if (!state._searchSynthModels[idx] && chatModels.length > 0) {
|
|
611
|
+
const fb = chatModels[Math.min(idx, chatModels.length - 1)];
|
|
612
|
+
state._searchSynthModels[idx] = { model: fb.id, endpoint: fb.url, endpointId: fb.endpointId, name: fb.name };
|
|
613
|
+
}
|
|
614
|
+
if (chatModels.length >= 5) {
|
|
615
|
+
const picker = _buildSearchablePicker(chatModels, state._searchSynthModels[idx], idx, (chosen) => {
|
|
616
|
+
state._searchSynthModels[idx] = chosen;
|
|
617
|
+
});
|
|
618
|
+
row.appendChild(picker);
|
|
619
|
+
} else {
|
|
620
|
+
const modelSelect = document.createElement('select');
|
|
621
|
+
modelSelect.className = 'cmp-form-control';
|
|
622
|
+
modelSelect.style.flex = '1';
|
|
623
|
+
chatModels.forEach(m => {
|
|
624
|
+
const opt = document.createElement('option');
|
|
625
|
+
opt.value = JSON.stringify({ model: m.id, endpoint: m.url, endpointId: m.endpointId, name: m.name, endpointName: m.endpointName || '' });
|
|
626
|
+
opt.textContent = m.endpointName ? `${m.name} (${m.endpointName})` : m.name;
|
|
627
|
+
if (state._searchSynthModels[idx] && state._searchSynthModels[idx].model === m.id) opt.selected = true;
|
|
628
|
+
modelSelect.appendChild(opt);
|
|
629
|
+
});
|
|
630
|
+
modelSelect.addEventListener('change', () => {
|
|
631
|
+
try { state._searchSynthModels[idx] = JSON.parse(modelSelect.value); } catch (e) {}
|
|
632
|
+
});
|
|
633
|
+
try { if (!state._searchSynthModels[idx]) state._searchSynthModels[idx] = JSON.parse(modelSelect.value); } catch (e) {}
|
|
634
|
+
row.appendChild(modelSelect);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Search provider picker (smaller)
|
|
638
|
+
const provSelect = document.createElement('select');
|
|
639
|
+
provSelect.className = 'cmp-form-control cmp-prov-select';
|
|
640
|
+
available.forEach((p, pi) => {
|
|
641
|
+
const optEl = document.createElement('option');
|
|
642
|
+
optEl.value = JSON.stringify({ model: p.id, endpoint: '', endpointId: null, name: p.label, searchProvider: p.id });
|
|
643
|
+
optEl.textContent = p.label;
|
|
644
|
+
if (sel && sel.model === p.id) optEl.selected = true;
|
|
645
|
+
else if (!sel && pi === Math.min(idx, available.length - 1)) optEl.selected = true;
|
|
646
|
+
provSelect.appendChild(optEl);
|
|
647
|
+
});
|
|
648
|
+
provSelect.addEventListener('change', () => {
|
|
649
|
+
try { selections[idx] = JSON.parse(provSelect.value); } catch (e) {}
|
|
650
|
+
});
|
|
651
|
+
try { if (!selections[idx]) selections[idx] = JSON.parse(provSelect.value); } catch (e) {}
|
|
652
|
+
row.appendChild(provSelect);
|
|
653
|
+
|
|
654
|
+
// X remove button when >2 slots
|
|
655
|
+
if (selections.length > 2) {
|
|
656
|
+
const rmBtn = document.createElement('button');
|
|
657
|
+
rmBtn.type = 'button';
|
|
658
|
+
rmBtn.textContent = '\u00d7';
|
|
659
|
+
rmBtn.className = 'cmp-rm-btn';
|
|
660
|
+
rmBtn.addEventListener('mouseenter', () => { rmBtn.style.opacity = '1'; rmBtn.style.color = 'var(--color-error)'; });
|
|
661
|
+
rmBtn.addEventListener('mouseleave', () => { rmBtn.style.opacity = '0.3'; rmBtn.style.color = 'var(--fg)'; });
|
|
662
|
+
rmBtn.addEventListener('click', () => { selections.splice(idx, 1); state._searchSynthModels.splice(idx, 1); renderModelRows(); });
|
|
663
|
+
row.appendChild(rmBtn);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
listContainer.appendChild(row);
|
|
667
|
+
});
|
|
668
|
+
if (addBtn) addBtn.style.display = selections.length >= 8 ? 'none' : '';
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ── Chat / Image / Agent / Research mode: show model dropdowns ──
|
|
673
|
+
const filtered = filteredModels();
|
|
674
|
+
listContainer.innerHTML = '';
|
|
675
|
+
|
|
676
|
+
// Research mode needs search providers too — fetch if not cached
|
|
677
|
+
const needsProviders = state._compareMode === 'research';
|
|
678
|
+
if (needsProviders && !state._cachedProviders) {
|
|
679
|
+
listContainer.innerHTML = '<div style="color:color-mix(in srgb, var(--fg) 40%, transparent);font-size:0.85em;padding:12px 0;">Loading search providers\u2026</div>';
|
|
680
|
+
fetch(`${state.API_BASE}/api/search/providers`).then(r => r.json()).then(providers => {
|
|
681
|
+
state._cachedProviders = providers;
|
|
682
|
+
renderModelRows();
|
|
683
|
+
}).catch(() => {
|
|
684
|
+
state._cachedProviders = [];
|
|
685
|
+
renderModelRows();
|
|
686
|
+
});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (filtered.length === 0) {
|
|
691
|
+
const empty = document.createElement('div');
|
|
692
|
+
empty.style.cssText = 'color:color-mix(in srgb, var(--fg) 40%, transparent);font-size:0.85em;padding:12px 0;text-align:center;font-style:italic;';
|
|
693
|
+
empty.textContent = 'No ' + state._compareMode + ' models available';
|
|
694
|
+
listContainer.appendChild(empty);
|
|
695
|
+
if (addBtn) addBtn.style.display = 'none';
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Research: ensure per-pane provider array
|
|
700
|
+
const researchProviders = needsProviders && state._cachedProviders ? state._cachedProviders.filter(p => p.available) : [];
|
|
701
|
+
if (!state._searchSynthModels) state._searchSynthModels = [];
|
|
702
|
+
while (state._searchSynthModels.length < selections.length) state._searchSynthModels.push(null);
|
|
703
|
+
|
|
704
|
+
const _seqStep = !state._parallel ? Math.min(20, Math.floor(80 / Math.max(selections.length, 1))) : 0;
|
|
705
|
+
selections.forEach((sel, idx) => {
|
|
706
|
+
const row = document.createElement('div');
|
|
707
|
+
row.className = 'cmp-model-row';
|
|
708
|
+
if (_seqStep) row.style.marginLeft = (idx * _seqStep) + 'px';
|
|
709
|
+
|
|
710
|
+
// Left label: number/letter or blind eye icon
|
|
711
|
+
const lbl = document.createElement('span');
|
|
712
|
+
lbl.className = 'cmp-row-label';
|
|
713
|
+
if (state._blindMode) {
|
|
714
|
+
lbl.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><line x1="8" y1="16" x2="16" y2="8"/><line x1="8" y1="8" x2="16" y2="16"/></svg>';
|
|
715
|
+
} else {
|
|
716
|
+
lbl.textContent = _slotChar(idx);
|
|
717
|
+
}
|
|
718
|
+
row.appendChild(lbl);
|
|
719
|
+
|
|
720
|
+
if (_shuffled) {
|
|
721
|
+
const mask = document.createElement('div');
|
|
722
|
+
mask.className = 'cmp-form-control';
|
|
723
|
+
mask.style.cssText = 'flex:1;opacity:0.4;font-style:italic;';
|
|
724
|
+
mask.textContent = 'Hidden';
|
|
725
|
+
row.appendChild(mask);
|
|
726
|
+
} else if (filtered.length >= 5) {
|
|
727
|
+
const picker = _buildSearchablePicker(filtered, sel, idx, (chosen) => {
|
|
728
|
+
selections[idx] = chosen;
|
|
729
|
+
_remindShuffle();
|
|
730
|
+
});
|
|
731
|
+
if (!selections[idx]) {
|
|
732
|
+
const fallback = filtered[Math.min(idx, filtered.length - 1)];
|
|
733
|
+
selections[idx] = { model: fallback.id, endpoint: fallback.url, endpointId: fallback.endpointId, name: fallback.name };
|
|
734
|
+
}
|
|
735
|
+
row.appendChild(picker);
|
|
736
|
+
} else {
|
|
737
|
+
const select = document.createElement('select');
|
|
738
|
+
select.className = 'cmp-form-control';
|
|
739
|
+
select.style.flex = '1';
|
|
740
|
+
filtered.forEach((m, mi) => {
|
|
741
|
+
const opt = buildOption(m);
|
|
742
|
+
const optEl = document.createElement('option');
|
|
743
|
+
optEl.value = opt.val;
|
|
744
|
+
optEl.textContent = opt.label;
|
|
745
|
+
if (sel && sel.model === m.id && (sel.endpoint === m.url || !filtered.some(o => o.id === m.id && o !== m))) optEl.selected = true;
|
|
746
|
+
else if (!sel && mi === Math.min(idx, filtered.length - 1)) optEl.selected = true;
|
|
747
|
+
select.appendChild(optEl);
|
|
748
|
+
});
|
|
749
|
+
select.addEventListener('change', () => {
|
|
750
|
+
try { selections[idx] = JSON.parse(select.value); } catch (e) { console.warn('Compare model select parse failed:', e); }
|
|
751
|
+
_remindShuffle();
|
|
752
|
+
});
|
|
753
|
+
try { if (!selections[idx]) selections[idx] = JSON.parse(select.value); } catch (e) { console.warn('Compare model init parse failed:', e); }
|
|
754
|
+
row.appendChild(select);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Research mode: search provider picker next to model
|
|
758
|
+
if (needsProviders && researchProviders.length > 0 && !_shuffled) {
|
|
759
|
+
const provSelect = document.createElement('select');
|
|
760
|
+
provSelect.className = 'cmp-form-control cmp-prov-select';
|
|
761
|
+
provSelect.title = 'Search provider';
|
|
762
|
+
researchProviders.forEach((p, pi) => {
|
|
763
|
+
const optEl = document.createElement('option');
|
|
764
|
+
optEl.value = p.id;
|
|
765
|
+
optEl.textContent = p.label;
|
|
766
|
+
if (state._searchSynthModels[idx] && state._searchSynthModels[idx] === p.id) optEl.selected = true;
|
|
767
|
+
else if (!state._searchSynthModels[idx] && pi === 0) optEl.selected = true;
|
|
768
|
+
provSelect.appendChild(optEl);
|
|
769
|
+
});
|
|
770
|
+
provSelect.addEventListener('change', () => { state._searchSynthModels[idx] = provSelect.value; });
|
|
771
|
+
if (!state._searchSynthModels[idx]) state._searchSynthModels[idx] = provSelect.value;
|
|
772
|
+
row.appendChild(provSelect);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// X remove button when >2 slots
|
|
776
|
+
if (selections.length > 2) {
|
|
777
|
+
const rmBtn = document.createElement('button');
|
|
778
|
+
rmBtn.type = 'button';
|
|
779
|
+
rmBtn.textContent = '\u00d7';
|
|
780
|
+
rmBtn.className = 'cmp-rm-btn';
|
|
781
|
+
rmBtn.addEventListener('mouseenter', () => { rmBtn.style.opacity = '1'; rmBtn.style.color = 'var(--color-error)'; });
|
|
782
|
+
rmBtn.addEventListener('mouseleave', () => { rmBtn.style.opacity = '0.3'; rmBtn.style.color = 'var(--fg)'; });
|
|
783
|
+
rmBtn.addEventListener('click', () => { selections.splice(idx, 1); if (state._searchSynthModels.length > idx) state._searchSynthModels.splice(idx, 1); renderModelRows(); });
|
|
784
|
+
row.appendChild(rmBtn);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
listContainer.appendChild(row);
|
|
788
|
+
});
|
|
789
|
+
if (addBtn) addBtn.style.display = (selections.length >= 8) ? 'none' : '';
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Default to 2 empty slots if no saved selections
|
|
793
|
+
if (!selections.length || !selections.some(s => s !== null)) selections = [null, null];
|
|
794
|
+
|
|
795
|
+
addBtn = document.createElement('button');
|
|
796
|
+
addBtn.type = 'button';
|
|
797
|
+
addBtn.style.cssText = 'display:none;align-items:center;gap:6px;background:none;border:1px dashed var(--border);color:var(--fg);border-radius:6px;cursor:pointer;padding:6px 12px;font-size:0.82em;opacity:0.6;transition:all 0.15s;margin-bottom:16px;width:100%;justify-content:center;';
|
|
798
|
+
addBtn.textContent = '+ Add Model';
|
|
799
|
+
addBtn.addEventListener('mouseenter', () => { addBtn.style.opacity = '1'; });
|
|
800
|
+
addBtn.addEventListener('mouseleave', () => { addBtn.style.opacity = '0.6'; });
|
|
801
|
+
addBtn.addEventListener('click', () => {
|
|
802
|
+
if (selections.length >= 8) return;
|
|
803
|
+
if (_shuffled) {
|
|
804
|
+
// In shuffle mode every slot is a hidden, randomly-picked model — so a
|
|
805
|
+
// new slot must get a random pool model too, not an empty picker.
|
|
806
|
+
const excluded = getExcludedModels();
|
|
807
|
+
const used = new Set(selections.filter(Boolean).map(s => s.model + '|' + s.endpoint));
|
|
808
|
+
const pool = filteredModels().filter(m => !excluded.includes(m.id));
|
|
809
|
+
const fresh = pool.filter(m => !used.has(m.id + '|' + m.url));
|
|
810
|
+
const src = fresh.length ? fresh : pool;
|
|
811
|
+
const pick = src.length ? src[Math.floor(Math.random() * src.length)] : null;
|
|
812
|
+
selections.push(pick ? { model: pick.id, endpoint: pick.url, endpointId: pick.endpointId, name: pick.name, endpointName: pick.endpointName || '' } : null);
|
|
813
|
+
} else {
|
|
814
|
+
selections.push(null);
|
|
815
|
+
}
|
|
816
|
+
renderModelRows();
|
|
817
|
+
_remindShuffle();
|
|
818
|
+
});
|
|
819
|
+
body.appendChild(addBtn);
|
|
820
|
+
|
|
821
|
+
// ── Timeout input ──
|
|
822
|
+
const timeoutRow = document.createElement('div');
|
|
823
|
+
timeoutRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:8px;';
|
|
824
|
+
const timeoutLabel = document.createElement('span');
|
|
825
|
+
timeoutLabel.style.cssText = 'color:color-mix(in srgb, var(--fg) 55%, transparent);font-size:0.82em;';
|
|
826
|
+
timeoutLabel.textContent = 'Timeout:';
|
|
827
|
+
const timeoutInput = document.createElement('input');
|
|
828
|
+
timeoutInput.type = 'number';
|
|
829
|
+
timeoutInput.min = '5';
|
|
830
|
+
timeoutInput.max = '300';
|
|
831
|
+
timeoutInput.value = String(state._timeout);
|
|
832
|
+
timeoutInput.style.cssText = 'width:60px;padding:4px 8px;background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:4px;font-size:0.82em;text-align:center;-moz-appearance:textfield;';
|
|
833
|
+
const timeoutSuffix = document.createElement('span');
|
|
834
|
+
timeoutSuffix.style.cssText = 'color:color-mix(in srgb, var(--fg) 55%, transparent);font-size:0.82em;';
|
|
835
|
+
timeoutSuffix.textContent = 'seconds';
|
|
836
|
+
timeoutRow.appendChild(timeoutLabel);
|
|
837
|
+
timeoutRow.appendChild(timeoutInput);
|
|
838
|
+
timeoutRow.appendChild(timeoutSuffix);
|
|
839
|
+
|
|
840
|
+
// Scoreboard button
|
|
841
|
+
const scoreBtn = document.createElement('button');
|
|
842
|
+
scoreBtn.type = 'button';
|
|
843
|
+
scoreBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:4px;"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>Scoreboard';
|
|
844
|
+
scoreBtn.style.cssText = 'margin-left:auto;padding:4px 10px;background:transparent;color:var(--fg);border:1px solid var(--border);border-radius:4px;cursor:pointer;font-size:0.82em;opacity:0.7;position:relative;top:-5px;';
|
|
845
|
+
scoreBtn.addEventListener('mouseenter', () => { scoreBtn.style.opacity = '1'; });
|
|
846
|
+
scoreBtn.addEventListener('mouseleave', () => { scoreBtn.style.opacity = '0.7'; });
|
|
847
|
+
scoreBtn.addEventListener('click', () => showScoreboard());
|
|
848
|
+
timeoutRow.appendChild(scoreBtn);
|
|
849
|
+
|
|
850
|
+
body.appendChild(timeoutRow);
|
|
851
|
+
|
|
852
|
+
content.appendChild(body);
|
|
853
|
+
|
|
854
|
+
// ── Footer with action buttons ──
|
|
855
|
+
const footer = document.createElement('div');
|
|
856
|
+
footer.className = 'modal-footer';
|
|
857
|
+
footer.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;padding:14px 16px 10px;border-top:1px solid var(--border);';
|
|
858
|
+
// Cancel button removed — the overlay's X / outside-click / Esc all
|
|
859
|
+
// dismiss the popup, so the footer Cancel was redundant.
|
|
860
|
+
const startBtn = document.createElement('button');
|
|
861
|
+
startBtn.innerHTML = _CMP_START_LABEL;
|
|
862
|
+
startBtn.className = 'research-start-btn';
|
|
863
|
+
startBtn.disabled = true;
|
|
864
|
+
// Pin to the same 30px box as Cancel so both buttons sit on the same line.
|
|
865
|
+
startBtn.style.cssText = 'opacity:0.4;height:30px;box-sizing:border-box;align-items:center;';
|
|
866
|
+
footer.appendChild(startBtn);
|
|
867
|
+
content.appendChild(footer);
|
|
868
|
+
|
|
869
|
+
overlay.appendChild(content);
|
|
870
|
+
document.body.appendChild(overlay);
|
|
871
|
+
|
|
872
|
+
// Make draggable via header
|
|
873
|
+
if (themeModule && themeModule.makeDraggable) {
|
|
874
|
+
themeModule.makeDraggable(content, header);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function cleanup(result) {
|
|
878
|
+
overlay.remove();
|
|
879
|
+
// Remove any body-appended picker dropdowns so they don't orphan.
|
|
880
|
+
document.querySelectorAll('.cmp-picker-dropdown').forEach(d => d.remove());
|
|
881
|
+
if (result) {
|
|
882
|
+
state._selectedModels = selections.filter(Boolean);
|
|
883
|
+
state._timeout = Math.max(5, parseInt(timeoutInput.value) || 30);
|
|
884
|
+
// Persist selections for next time (save filtered, non-null entries)
|
|
885
|
+
_persistSelections();
|
|
886
|
+
}
|
|
887
|
+
resolve(result);
|
|
888
|
+
}
|
|
889
|
+
// (cancelBtn removed — overlay X / outside-click / Esc still call cleanup)
|
|
890
|
+
startBtn.addEventListener('click', async () => {
|
|
891
|
+
if (!_modelsLoaded) return;
|
|
892
|
+
let selected = selections.filter(Boolean);
|
|
893
|
+
// Auto-populate any null selections from available models
|
|
894
|
+
if (selected.length < selections.length) {
|
|
895
|
+
const avail = state._compareMode === 'search' ? [] : filteredModels();
|
|
896
|
+
selections.forEach((s, i) => {
|
|
897
|
+
if (!s && avail.length > 0) {
|
|
898
|
+
const fb = avail[Math.min(i, avail.length - 1)];
|
|
899
|
+
selections[i] = { model: fb.id, endpoint: fb.url, endpointId: fb.endpointId, name: fb.name };
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
selected = selections.filter(Boolean);
|
|
903
|
+
}
|
|
904
|
+
if (selected.length < 1) return;
|
|
905
|
+
|
|
906
|
+
// For search mode, probe the synthesis LLM models instead of providers
|
|
907
|
+
const modelsToProbe = (state._compareMode === 'search')
|
|
908
|
+
? (state._searchSynthModels || []).filter(Boolean)
|
|
909
|
+
: selected;
|
|
910
|
+
if (modelsToProbe.length < 1) { cleanup(true); return; }
|
|
911
|
+
|
|
912
|
+
// ── Skip probe if all models already probed, go straight to start ──
|
|
913
|
+
const allAlreadyProbed = modelsToProbe.every(m => state._probed.has(m.model));
|
|
914
|
+
if (allAlreadyProbed) { cleanup(true); return; }
|
|
915
|
+
|
|
916
|
+
// ── Check selected models before starting ──
|
|
917
|
+
startBtn.disabled = true;
|
|
918
|
+
startBtn.style.opacity = '0.6';
|
|
919
|
+
|
|
920
|
+
const isBlind = state._blindMode || _shuffled;
|
|
921
|
+
|
|
922
|
+
// Show probe overlay as a fixed modal
|
|
923
|
+
const probeOverlay = document.createElement('div');
|
|
924
|
+
probeOverlay.className = 'compare-probe-overlay';
|
|
925
|
+
const probeCard = document.createElement('div');
|
|
926
|
+
probeCard.className = 'compare-probe-card';
|
|
927
|
+
probeCard.innerHTML = '<div class="compare-probe-title">Checking models...</div>';
|
|
928
|
+
let _probeSkipped = false;
|
|
929
|
+
const probeList = document.createElement('div');
|
|
930
|
+
probeList.className = 'compare-probe-list';
|
|
931
|
+
modelsToProbe.forEach((m, i) => {
|
|
932
|
+
const row = document.createElement('div');
|
|
933
|
+
row.className = 'compare-probe-row';
|
|
934
|
+
row.dataset.model = m.model;
|
|
935
|
+
row.dataset.idx = i;
|
|
936
|
+
// In blind mode, hide name until failure — only show slot letter
|
|
937
|
+
const name = m.name || m.model.split('/').pop();
|
|
938
|
+
const displayName = isBlind ? `Model ${_slotChar(i)}` : escapeHtml(name);
|
|
939
|
+
row._realName = name;
|
|
940
|
+
row.innerHTML = `<span class="compare-probe-spinner">▁▂▃</span><span class="compare-probe-name">${displayName}</span><span class="compare-probe-status"></span>`;
|
|
941
|
+
const waveEl = row.querySelector('.compare-probe-spinner');
|
|
942
|
+
const waveFrames = WAVE_FRAMES;
|
|
943
|
+
let waveIdx = 0;
|
|
944
|
+
row._waveInterval = setInterval(() => {
|
|
945
|
+
waveIdx = (waveIdx + 1) % waveFrames.length;
|
|
946
|
+
if (waveEl && !waveEl.classList.contains('ok') && !waveEl.classList.contains('fail')) {
|
|
947
|
+
waveEl.textContent = waveFrames[waveIdx];
|
|
948
|
+
}
|
|
949
|
+
}, 100);
|
|
950
|
+
probeList.appendChild(row);
|
|
951
|
+
});
|
|
952
|
+
probeCard.appendChild(probeList);
|
|
953
|
+
const skipBtn = document.createElement('button');
|
|
954
|
+
skipBtn.textContent = 'Skip';
|
|
955
|
+
skipBtn.className = 'cmp-btn-secondary';
|
|
956
|
+
skipBtn.style.cssText = 'padding:4px 14px;font-size:11px;opacity:0.5;transition:opacity 0.15s;margin-top:8px;';
|
|
957
|
+
skipBtn.addEventListener('mouseenter', () => { skipBtn.style.opacity = '1'; });
|
|
958
|
+
skipBtn.addEventListener('mouseleave', () => { skipBtn.style.opacity = '0.5'; });
|
|
959
|
+
skipBtn.addEventListener('click', () => {
|
|
960
|
+
_probeSkipped = true;
|
|
961
|
+
_clearProbeWaves();
|
|
962
|
+
probeOverlay.remove();
|
|
963
|
+
cleanup(true);
|
|
964
|
+
});
|
|
965
|
+
probeCard.appendChild(skipBtn);
|
|
966
|
+
probeOverlay.appendChild(probeCard);
|
|
967
|
+
// The CSS z-index for .compare-probe-overlay is 300, but modalManager
|
|
968
|
+
// bumps each opened tool modal above that on every focus (_modalTopZ
|
|
969
|
+
// starts at 300 and increments). So the compare modal often ends up
|
|
970
|
+
// ABOVE the probe overlay, hiding it. Recompute from the compare
|
|
971
|
+
// modal's current effective z-index so probe always sits one above.
|
|
972
|
+
const _cmpModal = document.getElementById('compare-model-overlay');
|
|
973
|
+
if (_cmpModal) {
|
|
974
|
+
const _cmpZ = parseInt(getComputedStyle(_cmpModal).zIndex, 10) || 0;
|
|
975
|
+
probeOverlay.style.setProperty('z-index', String(_cmpZ + 1), 'important');
|
|
976
|
+
}
|
|
977
|
+
document.body.appendChild(probeOverlay);
|
|
978
|
+
|
|
979
|
+
// ESC to close probe overlay (stopPropagation prevents closing model selector too)
|
|
980
|
+
const _probeEsc = (e) => {
|
|
981
|
+
if (e.key === 'Escape') {
|
|
982
|
+
e.stopPropagation();
|
|
983
|
+
e.preventDefault();
|
|
984
|
+
_probeSkipped = true;
|
|
985
|
+
_clearProbeWaves();
|
|
986
|
+
probeOverlay.remove();
|
|
987
|
+
document.removeEventListener('keydown', _probeEsc, false);
|
|
988
|
+
startBtn.disabled = false;
|
|
989
|
+
startBtn.innerHTML = _CMP_START_LABEL;
|
|
990
|
+
startBtn.style.opacity = '1';
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
document.addEventListener('keydown', _probeEsc, false);
|
|
994
|
+
|
|
995
|
+
// Helper: probe a single model (skip image models — they use a different API)
|
|
996
|
+
const _imageModelPrefixes = ['dall-e', 'gpt-image', 'chatgpt-image', 'stable-diffusion', 'sdxl', 'flux', 'midjourney'];
|
|
997
|
+
function _isImageModel(modelId) {
|
|
998
|
+
const lower = (modelId || '').toLowerCase();
|
|
999
|
+
return _imageModelPrefixes.some(p => lower.includes(p));
|
|
1000
|
+
}
|
|
1001
|
+
async function _probeOne(m) {
|
|
1002
|
+
if (_isImageModel(m.model)) {
|
|
1003
|
+
return { status: 'ok', model: m.model, skipped: true, skipReason: 'Image' };
|
|
1004
|
+
}
|
|
1005
|
+
// Search mode — probe the LLM model normally (don't skip)
|
|
1006
|
+
if (state._compareMode === 'search' && !m.model) {
|
|
1007
|
+
return { status: 'ok', model: m.model, skipped: true, skipReason: 'No model' };
|
|
1008
|
+
}
|
|
1009
|
+
const res = await fetch(`${state.API_BASE}/api/probe-selected`, {
|
|
1010
|
+
method: 'POST', credentials: 'same-origin',
|
|
1011
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1012
|
+
body: JSON.stringify({ models: [{ endpoint_id: m.endpointId || '', model: m.model, endpoint: m.endpoint || '', with_tools: state._compareMode === 'agent' }] }),
|
|
1013
|
+
});
|
|
1014
|
+
const data = await res.json();
|
|
1015
|
+
return (data.results || [])[0] || { status: 'fail', error: 'No response' };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Helper: update a probe row's visual state
|
|
1019
|
+
function _updateRow(idx, result) {
|
|
1020
|
+
const row = probeList.querySelector(`[data-idx="${idx}"]`);
|
|
1021
|
+
if (!row) return;
|
|
1022
|
+
// Stop wave animation
|
|
1023
|
+
if (row._waveInterval) { clearInterval(row._waveInterval); row._waveInterval = null; }
|
|
1024
|
+
const spinner = row.querySelector('.compare-probe-spinner');
|
|
1025
|
+
const status = row.querySelector('.compare-probe-status');
|
|
1026
|
+
if (result.status === 'ok') {
|
|
1027
|
+
spinner.textContent = '\u2713';
|
|
1028
|
+
spinner.classList.remove('fail');
|
|
1029
|
+
spinner.classList.add('ok');
|
|
1030
|
+
status.textContent = result.skipped ? (result.skipReason || 'Skipped') : (result.latency_ms ? `${result.latency_ms}ms` : 'OK');
|
|
1031
|
+
status.classList.remove('fail');
|
|
1032
|
+
status.classList.add('ok');
|
|
1033
|
+
row.classList.remove('fail');
|
|
1034
|
+
// Track as probed
|
|
1035
|
+
if (result.model) state._probed.add(result.model);
|
|
1036
|
+
} else {
|
|
1037
|
+
spinner.textContent = '\u2717';
|
|
1038
|
+
spinner.classList.remove('ok');
|
|
1039
|
+
spinner.classList.add('fail');
|
|
1040
|
+
status.textContent = '';
|
|
1041
|
+
status.classList.remove('ok');
|
|
1042
|
+
row.classList.add('fail');
|
|
1043
|
+
// Reveal real model name on failure (even in blind mode)
|
|
1044
|
+
if (isBlind && row._realName) {
|
|
1045
|
+
const nameEl = row.querySelector('.compare-probe-name');
|
|
1046
|
+
if (nameEl) nameEl.textContent = row._realName;
|
|
1047
|
+
}
|
|
1048
|
+
// Remove old detail/actions if retrying
|
|
1049
|
+
const oldDetail = row.nextElementSibling;
|
|
1050
|
+
if (oldDetail && oldDetail.classList.contains('compare-probe-detail')) oldDetail.remove();
|
|
1051
|
+
// Error + actions below the row
|
|
1052
|
+
const detail = document.createElement('div');
|
|
1053
|
+
detail.className = 'compare-probe-detail';
|
|
1054
|
+
detail.style.cssText = 'grid-column:1/-1;display:flex;align-items:flex-start;gap:6px;padding:4px 10px 6px;font-size:10px;opacity:0.6;background:color-mix(in srgb, var(--color-error, #f44) 5%, transparent);border-radius:4px;margin-top:-2px;';
|
|
1055
|
+
const errSpan = document.createElement('span');
|
|
1056
|
+
// Truncate long error messages
|
|
1057
|
+
const errText = (result.error || 'Failed');
|
|
1058
|
+
errSpan.textContent = errText.length > 80 ? errText.slice(0, 80) + '...' : errText;
|
|
1059
|
+
errSpan.title = errText;
|
|
1060
|
+
errSpan.style.cssText = 'flex:1;line-height:1.4;';
|
|
1061
|
+
detail.appendChild(errSpan);
|
|
1062
|
+
// Track timeout for retry doubling
|
|
1063
|
+
if (!row._probeTimeout) row._probeTimeout = 15000;
|
|
1064
|
+
if (result.error === 'Timeout') row._probeTimeout = Math.min(row._probeTimeout * 2, 120000);
|
|
1065
|
+
const retryBtn = document.createElement('button');
|
|
1066
|
+
retryBtn.className = 'compare-probe-action-btn';
|
|
1067
|
+
const retryLabel = result.error === 'Timeout' ? `Retry ${Math.round(row._probeTimeout / 1000)}s` : 'Retry';
|
|
1068
|
+
retryBtn.textContent = retryLabel;
|
|
1069
|
+
retryBtn.addEventListener('click', async (e) => {
|
|
1070
|
+
e.stopPropagation();
|
|
1071
|
+
detail.remove();
|
|
1072
|
+
if (isBlind) {
|
|
1073
|
+
const nameEl = row.querySelector('.compare-probe-name');
|
|
1074
|
+
if (nameEl) nameEl.textContent = `Model ${_slotChar(idx)}`;
|
|
1075
|
+
}
|
|
1076
|
+
const waveFrames2 = WAVE_FRAMES;
|
|
1077
|
+
let w2 = 0;
|
|
1078
|
+
spinner.classList.remove('ok', 'fail');
|
|
1079
|
+
spinner.style.color = '';
|
|
1080
|
+
row._waveInterval = setInterval(() => { w2 = (w2 + 1) % waveFrames2.length; spinner.textContent = waveFrames2[w2]; }, 100);
|
|
1081
|
+
row.classList.remove('fail');
|
|
1082
|
+
const r2 = await Promise.race([_probeOne(modelsToProbe[idx]), new Promise(r => setTimeout(() => r({ status: 'fail', error: 'Timeout' }), row._probeTimeout))]);
|
|
1083
|
+
_updateRow(idx, r2);
|
|
1084
|
+
});
|
|
1085
|
+
const swapBtn = document.createElement('button');
|
|
1086
|
+
swapBtn.className = 'compare-probe-action-btn';
|
|
1087
|
+
swapBtn.textContent = 'Swap';
|
|
1088
|
+
swapBtn.addEventListener('click', (e) => {
|
|
1089
|
+
e.stopPropagation();
|
|
1090
|
+
_clearProbeWaves();
|
|
1091
|
+
probeOverlay.remove();
|
|
1092
|
+
_probeSkipped = true;
|
|
1093
|
+
startBtn.disabled = false;
|
|
1094
|
+
startBtn.innerHTML = _CMP_START_LABEL;
|
|
1095
|
+
startBtn.style.opacity = '1';
|
|
1096
|
+
});
|
|
1097
|
+
detail.appendChild(retryBtn);
|
|
1098
|
+
detail.appendChild(swapBtn);
|
|
1099
|
+
row.after(detail);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
try {
|
|
1104
|
+
// Probe all in parallel (with 15s timeout per model)
|
|
1105
|
+
const results = await Promise.all(modelsToProbe.map(m =>
|
|
1106
|
+
Promise.race([
|
|
1107
|
+
_probeOne(m),
|
|
1108
|
+
new Promise(r => setTimeout(() => r({ status: 'fail', error: 'Timeout' }), 15000))
|
|
1109
|
+
])
|
|
1110
|
+
));
|
|
1111
|
+
if (_probeSkipped) return;
|
|
1112
|
+
let allOk = true;
|
|
1113
|
+
let failCount = 0;
|
|
1114
|
+
|
|
1115
|
+
for (let i = 0; i < results.length; i++) {
|
|
1116
|
+
_updateRow(i, results[i]);
|
|
1117
|
+
if (results[i].status !== 'ok') {
|
|
1118
|
+
allOk = false;
|
|
1119
|
+
failCount++;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// In shuffle/blind mode: auto-swap failed models silently (not for search/research)
|
|
1124
|
+
if (!allOk && _shuffled && state._compareMode !== 'search' && state._compareMode !== 'research') {
|
|
1125
|
+
const excluded = getExcludedModels();
|
|
1126
|
+
const usedModels = new Set(selections.filter(Boolean).map(m => m.model));
|
|
1127
|
+
const pool = filteredModels().filter(m => !excluded.includes(m.id) && !usedModels.has(m.id));
|
|
1128
|
+
let poolIdx = 0;
|
|
1129
|
+
|
|
1130
|
+
for (let i = 0; i < results.length; i++) {
|
|
1131
|
+
if (results[i].status !== 'ok') {
|
|
1132
|
+
const row = probeList.querySelector(`[data-idx="${i}"]`);
|
|
1133
|
+
// Restart wave in red to show swapping
|
|
1134
|
+
if (row) {
|
|
1135
|
+
const spinner = row.querySelector('.compare-probe-spinner');
|
|
1136
|
+
const status = row.querySelector('.compare-probe-status');
|
|
1137
|
+
if (spinner) {
|
|
1138
|
+
spinner.classList.remove('ok', 'fail');
|
|
1139
|
+
spinner.style.color = 'var(--color-error, #f44)';
|
|
1140
|
+
const waveFrames = WAVE_FRAMES;
|
|
1141
|
+
let wIdx = 0;
|
|
1142
|
+
row._waveInterval = setInterval(() => { wIdx = (wIdx + 1) % waveFrames.length; spinner.textContent = waveFrames[wIdx]; }, 100);
|
|
1143
|
+
}
|
|
1144
|
+
if (status) status.textContent = 'Swapping...';
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Try up to 3 replacements with 10s timeout each
|
|
1148
|
+
let swapped = false;
|
|
1149
|
+
for (let attempt = 0; attempt < 3 && poolIdx < pool.length; attempt++) {
|
|
1150
|
+
const replacement = pool[poolIdx++];
|
|
1151
|
+
const probePromise = _probeOne({ model: replacement.id, endpoint: replacement.url, endpointId: replacement.endpointId });
|
|
1152
|
+
const timeoutPromise = new Promise(r => setTimeout(() => r({ status: 'timeout', error: 'Swap timed out' }), 10000));
|
|
1153
|
+
const probeResult = await Promise.race([probePromise, timeoutPromise]);
|
|
1154
|
+
if (probeResult.status === 'ok') {
|
|
1155
|
+
selections[i] = { model: replacement.id, endpoint: replacement.url, endpointId: replacement.endpointId, name: replacement.name };
|
|
1156
|
+
usedModels.add(replacement.id);
|
|
1157
|
+
if (row && row._waveInterval) { clearInterval(row._waveInterval); row._waveInterval = null; }
|
|
1158
|
+
_updateRow(i, probeResult);
|
|
1159
|
+
swapped = true;
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (!swapped) {
|
|
1164
|
+
if (row && row._waveInterval) { clearInterval(row._waveInterval); row._waveInterval = null; }
|
|
1165
|
+
if (row) {
|
|
1166
|
+
const spinner = row.querySelector('.compare-probe-spinner');
|
|
1167
|
+
const status = row.querySelector('.compare-probe-status');
|
|
1168
|
+
if (spinner) { spinner.textContent = '\u2717'; spinner.classList.add('fail'); spinner.style.color = ''; }
|
|
1169
|
+
if (status) { status.textContent = 'No replacement'; }
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Re-check if all are ok now
|
|
1176
|
+
const finalToProbe = (state._compareMode === 'search') ? (state._searchSynthModels || []).filter(Boolean) : selections.filter(Boolean);
|
|
1177
|
+
const finalResults = await Promise.all(finalToProbe.map(m => _probeOne(m)));
|
|
1178
|
+
allOk = finalResults.every(r => r.status === 'ok');
|
|
1179
|
+
failCount = finalResults.filter(r => r.status !== 'ok').length;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// ── Phase 2: For search/research, also check search providers ──
|
|
1183
|
+
if (allOk && (state._compareMode === 'search' || state._compareMode === 'research')) {
|
|
1184
|
+
const providers = state._compareMode === 'search'
|
|
1185
|
+
? selected.map(s => ({ id: s.model, label: s.name }))
|
|
1186
|
+
: (state._searchSynthModels || []).map(p => typeof p === 'string' ? { id: p, label: p } : null).filter(Boolean);
|
|
1187
|
+
|
|
1188
|
+
if (providers.length > 0) {
|
|
1189
|
+
const titleEl = probeOverlay.querySelector('.compare-probe-title');
|
|
1190
|
+
titleEl.textContent = 'Checking search providers...';
|
|
1191
|
+
|
|
1192
|
+
// Add provider rows
|
|
1193
|
+
const providerRows = [];
|
|
1194
|
+
providers.forEach((p, i) => {
|
|
1195
|
+
const row = document.createElement('div');
|
|
1196
|
+
row.className = 'compare-probe-row';
|
|
1197
|
+
row.dataset.idx = 'p' + i;
|
|
1198
|
+
row.innerHTML = `<span class="compare-probe-spinner">▁▂▃</span><span class="compare-probe-name">${p.label || p.id}</span><span class="compare-probe-status"></span>`;
|
|
1199
|
+
const waveEl = row.querySelector('.compare-probe-spinner');
|
|
1200
|
+
const waveFrames = WAVE_FRAMES;
|
|
1201
|
+
let wIdx = 0;
|
|
1202
|
+
row._waveInterval = setInterval(() => {
|
|
1203
|
+
wIdx = (wIdx + 1) % waveFrames.length;
|
|
1204
|
+
if (waveEl && !waveEl.classList.contains('ok') && !waveEl.classList.contains('fail')) waveEl.textContent = waveFrames[wIdx];
|
|
1205
|
+
}, 100);
|
|
1206
|
+
probeList.appendChild(row);
|
|
1207
|
+
providerRows.push(row);
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
// Probe each provider with a test query
|
|
1211
|
+
const provResults = await Promise.all(providers.map(async (p) => {
|
|
1212
|
+
try {
|
|
1213
|
+
const fd = new FormData();
|
|
1214
|
+
fd.append('query', 'test');
|
|
1215
|
+
fd.append('provider', p.id);
|
|
1216
|
+
fd.append('count', '1');
|
|
1217
|
+
const r = await fetch(`${state.API_BASE}/api/search/query`, { method: 'POST', body: fd, credentials: 'same-origin' });
|
|
1218
|
+
const d = await r.json();
|
|
1219
|
+
return { status: d.error ? 'fail' : 'ok', error: d.error };
|
|
1220
|
+
} catch (e) {
|
|
1221
|
+
return { status: 'fail', error: e.message };
|
|
1222
|
+
}
|
|
1223
|
+
}));
|
|
1224
|
+
|
|
1225
|
+
let searchAllOk = true;
|
|
1226
|
+
provResults.forEach((result, i) => {
|
|
1227
|
+
const row = providerRows[i];
|
|
1228
|
+
if (row._waveInterval) { clearInterval(row._waveInterval); row._waveInterval = null; }
|
|
1229
|
+
const spinner = row.querySelector('.compare-probe-spinner');
|
|
1230
|
+
const status = row.querySelector('.compare-probe-status');
|
|
1231
|
+
if (result.status === 'ok') {
|
|
1232
|
+
spinner.textContent = '\u2713'; spinner.classList.add('ok');
|
|
1233
|
+
status.textContent = 'OK'; status.classList.add('ok');
|
|
1234
|
+
} else {
|
|
1235
|
+
spinner.textContent = '\u2717'; spinner.classList.add('fail');
|
|
1236
|
+
status.textContent = result.error || 'Failed'; status.classList.add('fail');
|
|
1237
|
+
row.classList.add('fail');
|
|
1238
|
+
searchAllOk = false;
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
if (!searchAllOk) {
|
|
1243
|
+
allOk = false;
|
|
1244
|
+
failCount += provResults.filter(r => r.status !== 'ok').length;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (allOk) {
|
|
1250
|
+
// Don't hide the Skip button here — collapsing its space made the
|
|
1251
|
+
// card shrink and the title + rows jump ("quick cut"). On success the
|
|
1252
|
+
// whole overlay fades out a moment later, so just leave it in place.
|
|
1253
|
+
probeOverlay.querySelector('.compare-probe-title').textContent = 'All ready!';
|
|
1254
|
+
setTimeout(() => {
|
|
1255
|
+
probeOverlay.style.transition = 'opacity 0.3s ease';
|
|
1256
|
+
probeOverlay.style.opacity = '0';
|
|
1257
|
+
setTimeout(() => { _clearProbeWaves(); probeOverlay.remove(); cleanup(true); if (window._updateCheckBtnState) window._updateCheckBtnState(); }, 300);
|
|
1258
|
+
}, 400);
|
|
1259
|
+
} else {
|
|
1260
|
+
// Failed — the Skip button is replaced by the Go Back / Start Anyway row.
|
|
1261
|
+
skipBtn.style.display = 'none';
|
|
1262
|
+
// Some failed — show which ones
|
|
1263
|
+
const failedNames = [];
|
|
1264
|
+
probeList.querySelectorAll('.compare-probe-row.fail').forEach(row => {
|
|
1265
|
+
failedNames.push(row.querySelector('.compare-probe-name').textContent);
|
|
1266
|
+
});
|
|
1267
|
+
const titleEl = probeOverlay.querySelector('.compare-probe-title');
|
|
1268
|
+
titleEl.textContent = failedNames.length <= 2
|
|
1269
|
+
? failedNames.join(' & ') + ' failed'
|
|
1270
|
+
: `${failCount} models failed`;
|
|
1271
|
+
const btnRow = document.createElement('div');
|
|
1272
|
+
btnRow.style.cssText = 'display:flex;gap:8px;justify-content:center;margin-top:12px;';
|
|
1273
|
+
const goBackBtn = document.createElement('button');
|
|
1274
|
+
goBackBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><polyline points="15 18 9 12 15 6"/></svg>Go Back';
|
|
1275
|
+
goBackBtn.className = 'cmp-btn-secondary';
|
|
1276
|
+
goBackBtn.style.cssText = 'padding:5px 12px;font-size:12px;display:inline-flex;align-items:center;';
|
|
1277
|
+
goBackBtn.addEventListener('click', () => { _clearProbeWaves(); probeOverlay.remove(); startBtn.disabled = false; startBtn.innerHTML = _CMP_START_LABEL; startBtn.style.opacity = '1'; });
|
|
1278
|
+
const startAnywayBtn = document.createElement('button');
|
|
1279
|
+
startAnywayBtn.textContent = 'Start Anyway';
|
|
1280
|
+
startAnywayBtn.className = 'cmp-btn-primary';
|
|
1281
|
+
startAnywayBtn.style.cssText = 'padding:5px 12px;font-size:12px;';
|
|
1282
|
+
startAnywayBtn.addEventListener('click', () => { _clearProbeWaves(); probeOverlay.remove(); cleanup(true); });
|
|
1283
|
+
btnRow.appendChild(goBackBtn);
|
|
1284
|
+
btnRow.appendChild(startAnywayBtn);
|
|
1285
|
+
probeCard.appendChild(btnRow);
|
|
1286
|
+
}
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
// Probe failed entirely — let user start anyway
|
|
1289
|
+
console.error('Compare probe error:', e);
|
|
1290
|
+
_clearProbeWaves();
|
|
1291
|
+
probeOverlay.remove();
|
|
1292
|
+
startBtn.disabled = false;
|
|
1293
|
+
startBtn.innerHTML = _CMP_START_LABEL;
|
|
1294
|
+
startBtn.style.opacity = '1';
|
|
1295
|
+
cleanup(true);
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
// ── Fetch models in background ──
|
|
1300
|
+
fetchModels().then(fetched => {
|
|
1301
|
+
models = fetched;
|
|
1302
|
+
state._cachedModels = fetched;
|
|
1303
|
+
_modelsLoaded = true;
|
|
1304
|
+
if (models.length < 1) {
|
|
1305
|
+
listContainer.innerHTML = '<div style="color:var(--color-error);font-size:0.85em;padding:12px 0;text-align:center;">No models available</div>';
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
// Validate saved selections against available models
|
|
1309
|
+
if (_needsValidation && selections.length > 0) {
|
|
1310
|
+
selections = selections.map(sel => {
|
|
1311
|
+
if (!sel) return null;
|
|
1312
|
+
// Prefer exact match (model + endpoint), fall back to model ID only
|
|
1313
|
+
const exact = models.find(m => m.id === sel.model && m.url === sel.endpoint);
|
|
1314
|
+
if (exact) return { ...sel, endpoint: exact.url, endpointId: exact.endpointId, endpointName: exact.endpointName || sel.endpointName || '' };
|
|
1315
|
+
const byId = models.find(m => m.id === sel.model);
|
|
1316
|
+
if (byId) return { model: byId.id, endpoint: byId.url, endpointId: byId.endpointId, name: byId.name, endpointName: byId.endpointName || '' };
|
|
1317
|
+
return null;
|
|
1318
|
+
});
|
|
1319
|
+
// Keep nulls in place so slot positions are preserved
|
|
1320
|
+
if (!selections.some(s => s !== null)) selections = [null, null];
|
|
1321
|
+
_needsValidation = false;
|
|
1322
|
+
}
|
|
1323
|
+
if (!selections.length) selections = [null, null];
|
|
1324
|
+
startBtn.disabled = false;
|
|
1325
|
+
startBtn.style.opacity = '1';
|
|
1326
|
+
addBtn.style.display = 'flex';
|
|
1327
|
+
renderModelRows();
|
|
1328
|
+
}).catch(e => {
|
|
1329
|
+
console.error('Failed to fetch models for compare:', e);
|
|
1330
|
+
listContainer.innerHTML = '<div style="color:var(--color-error);font-size:0.85em;padding:12px 0;text-align:center;">Failed to load models</div>';
|
|
1331
|
+
});
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
export { showModelSelector, disableToolToggles, restoreToolToggles, _syncToolbarIndicator };
|