@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,1908 @@
|
|
|
1
|
+
// skills.js — Skills tab in the Memory modal.
|
|
2
|
+
//
|
|
3
|
+
// Skills are SKILL.md files (frontmatter + body) under data/skills/.
|
|
4
|
+
// This UI supports: list, search, view (read SKILL.md), edit (replace
|
|
5
|
+
// content), publish/draft toggle, delete, and "run as slash" via the
|
|
6
|
+
// /<skill-name> path.
|
|
7
|
+
|
|
8
|
+
import uiModule from './ui.js';
|
|
9
|
+
import * as spinnerModule from './spinner.js';
|
|
10
|
+
|
|
11
|
+
const API = window.location.origin;
|
|
12
|
+
let skills = [];
|
|
13
|
+
let builtinSkills = []; // read-only agent tool capabilities (TOOL_SECTIONS)
|
|
14
|
+
let loaded = false;
|
|
15
|
+
let _loadPromise = null;
|
|
16
|
+
|
|
17
|
+
function esc(s) { return uiModule.esc(String(s ?? '')); }
|
|
18
|
+
|
|
19
|
+
let _pendingFocusSkill = null;
|
|
20
|
+
let _cascadeNext = false; // set true to play the domino-in entrance on the next render
|
|
21
|
+
|
|
22
|
+
function _playSkillsCascade(container = document.getElementById('skills-list')) {
|
|
23
|
+
if (!container || !container.querySelector('.skill-card')) return false;
|
|
24
|
+
container.classList.remove('doclib-just-opened');
|
|
25
|
+
void container.offsetWidth;
|
|
26
|
+
container.classList.add('doclib-just-opened');
|
|
27
|
+
setTimeout(() => container.classList.remove('doclib-just-opened'), 900);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Cache of SKILL.md text by skill name, so expanding is instant (no async
|
|
32
|
+
// fetch + content-settle jump). Populated lazily on expand AND eagerly in
|
|
33
|
+
// the background for all visible cards right after render.
|
|
34
|
+
const _mdCache = new Map();
|
|
35
|
+
async function _fetchSkillMarkdown(name) {
|
|
36
|
+
if (_mdCache.has(name)) return _mdCache.get(name);
|
|
37
|
+
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`);
|
|
38
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
const md = data.markdown || '';
|
|
41
|
+
_mdCache.set(name, md);
|
|
42
|
+
return md;
|
|
43
|
+
}
|
|
44
|
+
// Background-load the markdown for every currently-rendered skill card so it
|
|
45
|
+
// is ready (in the card's <pre> + _mdLoaded) before the user expands it.
|
|
46
|
+
function _preloadVisibleMarkdown() {
|
|
47
|
+
document.querySelectorAll('#skills-list .skill-card[data-skill-name]').forEach(card => {
|
|
48
|
+
const name = card.dataset.skillName;
|
|
49
|
+
if (!name || card._mdLoaded) return;
|
|
50
|
+
const pre = card.querySelector('.skill-md-pre');
|
|
51
|
+
const apply = (md) => { if (pre) pre.textContent = md || '(empty)'; card._mdLoaded = true; card._md = md || ''; };
|
|
52
|
+
if (_mdCache.has(name)) { apply(_mdCache.get(name)); return; }
|
|
53
|
+
_fetchSkillMarkdown(name).then(apply).catch(() => {});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Collapsed skills sections ("user" / "builtin"), persisted so the
|
|
58
|
+
// choice survives reloads. Built-in defaults to collapsed (it's
|
|
59
|
+
// reference info, not the user's own skills).
|
|
60
|
+
const _collapsedSections = (() => {
|
|
61
|
+
try {
|
|
62
|
+
const raw = localStorage.getItem('skillsSectionsCollapsed');
|
|
63
|
+
if (raw) return new Set(JSON.parse(raw));
|
|
64
|
+
} catch (_) {}
|
|
65
|
+
return new Set(['builtin']);
|
|
66
|
+
})();
|
|
67
|
+
function _saveCollapsedSections() {
|
|
68
|
+
try { localStorage.setItem('skillsSectionsCollapsed', JSON.stringify([..._collapsedSections])); } catch (_) {}
|
|
69
|
+
}
|
|
70
|
+
function _applySectionCollapse(container) {
|
|
71
|
+
if (!container) return;
|
|
72
|
+
container.querySelectorAll('.skills-section-header').forEach(h => {
|
|
73
|
+
h.classList.toggle('collapsed', _collapsedSections.has(h.dataset.section));
|
|
74
|
+
});
|
|
75
|
+
container.querySelectorAll('.doclib-card[data-skill-section]').forEach(c => {
|
|
76
|
+
c.classList.toggle('skill-card-section-hidden', _collapsedSections.has(c.dataset.skillSection));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function loadSkills(cascade = false) {
|
|
81
|
+
// Play the domino-in entrance on this load (set when the tab is opened,
|
|
82
|
+
// not for the silent re-loads after an edit/delete).
|
|
83
|
+
if (cascade) _cascadeNext = true;
|
|
84
|
+
if (cascade && loaded && !_loadPromise && _playSkillsCascade()) {
|
|
85
|
+
_cascadeNext = false;
|
|
86
|
+
updateCount();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (_loadPromise) return _loadPromise;
|
|
90
|
+
_loadPromise = (async () => {
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(`${API}/api/skills`);
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
skills = data.skills || [];
|
|
95
|
+
_loadSkillApprovalThreshold();
|
|
96
|
+
// Built-in capabilities are no longer surfaced in the Skills menu.
|
|
97
|
+
loaded = true;
|
|
98
|
+
renderSkillsList();
|
|
99
|
+
updateCount();
|
|
100
|
+
if (_pendingFocusSkill) {
|
|
101
|
+
_focusSkillRow(_pendingFocusSkill);
|
|
102
|
+
_pendingFocusSkill = null;
|
|
103
|
+
}
|
|
104
|
+
// If a background audit is running, re-show its progress panel.
|
|
105
|
+
if (!_auditPoll) {
|
|
106
|
+
_fetchAuditStatus().then(st => {
|
|
107
|
+
if (st.status === 'running') _auditAllSkills();
|
|
108
|
+
}).catch(() => {});
|
|
109
|
+
}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.error('Failed to load skills:', e);
|
|
112
|
+
} finally {
|
|
113
|
+
_loadPromise = null;
|
|
114
|
+
}
|
|
115
|
+
})();
|
|
116
|
+
return _loadPromise;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _focusSkillRow(name) {
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
const card = document.querySelector(`.skill-card[data-skill-name="${CSS.escape(name)}"]`);
|
|
122
|
+
if (!card) return;
|
|
123
|
+
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
124
|
+
card.classList.add('skill-row-flash');
|
|
125
|
+
setTimeout(() => card.classList.remove('skill-row-flash'), 2000);
|
|
126
|
+
// Expand it so the linked skill opens to its SKILL.md directly.
|
|
127
|
+
_expandSkillCard(card, name);
|
|
128
|
+
}, 200);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Open the Memory modal → Skills tab → focus a specific skill row.
|
|
132
|
+
// Used by the chat anchor-link delegate ([name](#skill-<name>)).
|
|
133
|
+
export function openSkill(name) {
|
|
134
|
+
_pendingFocusSkill = name || null;
|
|
135
|
+
// Open the memory modal if not already open.
|
|
136
|
+
const memBtn = document.getElementById('tool-memory-btn');
|
|
137
|
+
if (memBtn) memBtn.click();
|
|
138
|
+
// Switch to the skills tab (triggers lazy loadSkills()).
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
const tab = document.querySelector('.memory-tab[data-memory-tab="skills"]');
|
|
141
|
+
if (tab) tab.click();
|
|
142
|
+
else loadSkills(); // fallback if tab structure differs
|
|
143
|
+
}, 120);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let _skillsSort = 'confidence';
|
|
147
|
+
let _showDraftsOnly = false;
|
|
148
|
+
let _showPublishedOnly = false;
|
|
149
|
+
let _confMax = null; // confidence ceiling filter (%, e.g. 90 = show ≤90%); null = off
|
|
150
|
+
let _selectMode = false;
|
|
151
|
+
const _selectedNames = new Set();
|
|
152
|
+
let _skillApprovalThreshold = 0.85;
|
|
153
|
+
|
|
154
|
+
function updateCount() {
|
|
155
|
+
const el = document.getElementById('skills-count');
|
|
156
|
+
if (el) el.textContent = skills.length || '0';
|
|
157
|
+
const elH = document.getElementById('skills-count-h2');
|
|
158
|
+
if (elH) elH.textContent = skills.length + ' skill' + (skills.length === 1 ? '' : 's');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _sortSkills(list) {
|
|
162
|
+
const arr = list.slice();
|
|
163
|
+
if (_skillsSort === 'confidence') {
|
|
164
|
+
arr.sort((a, b) => (b.confidence || 0) - (a.confidence || 0) || (a.name || '').localeCompare(b.name || ''));
|
|
165
|
+
} else if (_skillsSort === 'uses') {
|
|
166
|
+
arr.sort((a, b) => (b.uses || 0) - (a.uses || 0) || (a.name || '').localeCompare(b.name || ''));
|
|
167
|
+
} else if (_skillsSort === 'recent') {
|
|
168
|
+
arr.sort((a, b) => (b.updated_at || b.created_at || 0) - (a.updated_at || a.created_at || 0));
|
|
169
|
+
} else {
|
|
170
|
+
arr.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
|
171
|
+
}
|
|
172
|
+
return arr;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function _matches(sk, query) {
|
|
176
|
+
const q = query.toLowerCase();
|
|
177
|
+
return (
|
|
178
|
+
(sk.name || '').toLowerCase().includes(q) ||
|
|
179
|
+
(sk.description || '').toLowerCase().includes(q) ||
|
|
180
|
+
(sk.when_to_use || sk.problem || '').toLowerCase().includes(q) ||
|
|
181
|
+
(sk.category || '').toLowerCase().includes(q) ||
|
|
182
|
+
(sk.tags || []).some(t => (t || '').toLowerCase().includes(q))
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function _statusPill(sk) {
|
|
187
|
+
const s = sk.status || (sk._legacy ? 'legacy' : 'draft');
|
|
188
|
+
if (s === 'published') return '<span class="memory-cat-badge skill-status-pill" data-status="published" style="background:color-mix(in srgb, var(--accent, #4ade80) 30%, transparent)">published</span>';
|
|
189
|
+
if (s === 'draft') return '<span class="memory-cat-badge skill-status-pill" data-status="draft" style="background:color-mix(in srgb, var(--fg) 14%, transparent)">draft</span>';
|
|
190
|
+
return `<span class="memory-cat-badge skill-status-pill" data-status="${esc(s)}" style="opacity:0.6">${esc(s)}</span>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Show a "teacher" badge for skills written by the auto-escalation
|
|
194
|
+
// teacher loop. Lets the user tell at-a-glance which procedures were
|
|
195
|
+
// hand-authored vs auto-generated so they can audit (and demote /
|
|
196
|
+
// edit / publish) before trusting them.
|
|
197
|
+
function _sourcePill(sk) {
|
|
198
|
+
if (sk.source !== 'teacher-escalation') return '';
|
|
199
|
+
const teacher = sk.teacher_model || 'teacher';
|
|
200
|
+
return `<span class="memory-cat-badge" title="Created by teacher escalation: ${esc(teacher)}" style="background:color-mix(in srgb, var(--color-warning, #f0ad4e) 22%, transparent);">teacher-created</span>`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function _modelShortName(model) {
|
|
204
|
+
return String(model || '').split('/').filter(Boolean).pop() || String(model || '');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function _skillTokens(sk) {
|
|
208
|
+
return new Set(String([
|
|
209
|
+
sk.name || '',
|
|
210
|
+
sk.description || '',
|
|
211
|
+
sk.when_to_use || '',
|
|
212
|
+
...(sk.tags || []),
|
|
213
|
+
].join(' ')).toLowerCase()
|
|
214
|
+
.replace(/-\d+\b/g, '')
|
|
215
|
+
.split(/[^a-z0-9]+/)
|
|
216
|
+
.filter(t => t.length > 2 && !['the', 'and', 'with', 'for', 'from', 'using'].includes(t)));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function _skillSimilarity(a, b) {
|
|
220
|
+
const A = _skillTokens(a), B = _skillTokens(b);
|
|
221
|
+
if (!A.size || !B.size) return 0;
|
|
222
|
+
let inter = 0;
|
|
223
|
+
for (const t of A) if (B.has(t)) inter++;
|
|
224
|
+
return inter / (A.size + B.size - inter);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function _baseSkillName(name) {
|
|
228
|
+
return String(name || '').replace(/-\d+$/, '');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function _scoreDuplicateKeeper(sk) {
|
|
232
|
+
return [
|
|
233
|
+
(sk.status === 'published') ? 100000 : 0,
|
|
234
|
+
(sk.uses || 0) * 100,
|
|
235
|
+
Math.round((sk.confidence || 0) * 100),
|
|
236
|
+
sk.audit_by_teacher ? -5 : 0,
|
|
237
|
+
-String(sk.name || '').length / 1000,
|
|
238
|
+
].reduce((a, b) => a + b, 0);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function _duplicateMeta(list) {
|
|
242
|
+
const parent = new Map();
|
|
243
|
+
const names = list.map(s => s.name || s.id).filter(Boolean);
|
|
244
|
+
names.forEach(n => parent.set(n, n));
|
|
245
|
+
const find = (x) => {
|
|
246
|
+
let p = parent.get(x) || x;
|
|
247
|
+
while (p !== parent.get(p)) p = parent.get(p);
|
|
248
|
+
return p;
|
|
249
|
+
};
|
|
250
|
+
const unite = (a, b) => {
|
|
251
|
+
const pa = find(a), pb = find(b);
|
|
252
|
+
if (pa !== pb) parent.set(pb, pa);
|
|
253
|
+
};
|
|
254
|
+
for (let i = 0; i < list.length; i++) {
|
|
255
|
+
for (let j = i + 1; j < list.length; j++) {
|
|
256
|
+
const a = list[i], b = list[j];
|
|
257
|
+
const an = a.name || a.id, bn = b.name || b.id;
|
|
258
|
+
if (!an || !bn) continue;
|
|
259
|
+
if (_baseSkillName(an) === _baseSkillName(bn) || _skillSimilarity(a, b) >= 0.38) {
|
|
260
|
+
unite(an, bn);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const groups = new Map();
|
|
265
|
+
for (const sk of list) {
|
|
266
|
+
const n = sk.name || sk.id;
|
|
267
|
+
if (!n) continue;
|
|
268
|
+
const root = find(n);
|
|
269
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
270
|
+
groups.get(root).push(sk);
|
|
271
|
+
}
|
|
272
|
+
const meta = new Map();
|
|
273
|
+
let idx = 1;
|
|
274
|
+
for (const group of groups.values()) {
|
|
275
|
+
if (group.length < 2) continue;
|
|
276
|
+
const sorted = group.slice().sort((a, b) => _scoreDuplicateKeeper(b) - _scoreDuplicateKeeper(a));
|
|
277
|
+
const keep = sorted[0].name || sorted[0].id;
|
|
278
|
+
const groupNames = sorted.map(s => s.name || s.id).filter(Boolean);
|
|
279
|
+
for (const sk of sorted) {
|
|
280
|
+
const n = sk.name || sk.id;
|
|
281
|
+
meta.set(n, { group: idx, keep: n === keep, keepName: keep, names: groupNames });
|
|
282
|
+
}
|
|
283
|
+
idx++;
|
|
284
|
+
}
|
|
285
|
+
return meta;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function _auditModelPills(sk) {
|
|
289
|
+
const worker = sk.audit_worker_model || '';
|
|
290
|
+
const teacher = sk.audit_teacher_model || '';
|
|
291
|
+
let html = '';
|
|
292
|
+
if (worker) {
|
|
293
|
+
html += `<span class="memory-cat-badge skill-model-pill skill-model-student" title="Last audited by default audit model: ${esc(worker)}">audit</span>`;
|
|
294
|
+
}
|
|
295
|
+
if (sk.audit_by_teacher || teacher) {
|
|
296
|
+
const title = teacher
|
|
297
|
+
? `Teacher rewrote this skill; audit model passed after the rewrite. Teacher: ${teacher}`
|
|
298
|
+
: 'Teacher rewrote this skill; audit model passed after the rewrite.';
|
|
299
|
+
html += `<span class="memory-cat-badge skill-model-pill skill-model-teacher" title="${esc(title)}">teacher-fixed</span>`;
|
|
300
|
+
}
|
|
301
|
+
return html;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function _necessityKind(sk) {
|
|
305
|
+
const nec = sk && sk.necessity;
|
|
306
|
+
if (sk && sk._duplicateGroup) return 'duplicate';
|
|
307
|
+
if (!nec || nec.necessary !== false) return null;
|
|
308
|
+
const reason = String(nec.reason || '').toLowerCase();
|
|
309
|
+
const redundant = (nec.redundant_with || []).filter(Boolean);
|
|
310
|
+
if (redundant.length || /duplicat|redundan|overlap|same skill|same procedure/.test(reason)) return 'duplicate';
|
|
311
|
+
if (/trivial|generic|capable assistant|without a saved|not need|unnecessary/.test(reason)) return 'trivial';
|
|
312
|
+
return 'irrelevant';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function _necessityPill(sk) {
|
|
316
|
+
const kind = _necessityKind(sk);
|
|
317
|
+
if (!kind) return '';
|
|
318
|
+
const nec = sk.necessity || {};
|
|
319
|
+
const dup = (nec.redundant_with || []).filter(Boolean);
|
|
320
|
+
const label = kind === 'duplicate' ? (sk._duplicateGroup ? `duplicate #${sk._duplicateGroup}` : 'duplicate')
|
|
321
|
+
: kind === 'trivial' ? 'generic'
|
|
322
|
+
: 'possibly-irrelevant';
|
|
323
|
+
const group = sk._duplicateNames || [];
|
|
324
|
+
const why = sk._duplicateGroup
|
|
325
|
+
? `Duplicate group #${sk._duplicateGroup}. Recommended keep: ${sk._duplicateKeepName}. Group: ${group.join(', ')}`
|
|
326
|
+
: (nec.reason || 'May not be worth keeping') + (dup.length ? ' | overlaps: ' + dup.join(', ') : '');
|
|
327
|
+
return `<span class="memory-cat-badge skill-necessity-pill skill-necessity-${kind}" title="${esc(why)}">${label}</span>`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function _duplicatePriorityPill(sk) {
|
|
331
|
+
if (!sk._duplicateGroup) return '';
|
|
332
|
+
if (sk._duplicateKeep) {
|
|
333
|
+
return `<span class="memory-cat-badge skill-duplicate-keep" title="Best duplicate candidate by published status, uses, confidence, and specificity">recommended</span>`;
|
|
334
|
+
}
|
|
335
|
+
return `<span class="memory-cat-badge skill-duplicate-lower" title="Lower-priority duplicate. Suggested keeper: ${esc(sk._duplicateKeepName || '')}">lower-priority</span>`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Verified-by-test indicators shown next to the confidence %. A check when a
|
|
339
|
+
// test/audit run passed; a graduation-cap when the teacher model had to
|
|
340
|
+
// rewrite the skill to make it pass. SVG (no Unicode emoji).
|
|
341
|
+
function _auditMarks(sk) {
|
|
342
|
+
let html = '';
|
|
343
|
+
if (sk.audit_verdict === 'pass') {
|
|
344
|
+
html += `<span class="skill-verified" title="Passed an automated test"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>`;
|
|
345
|
+
}
|
|
346
|
+
if (sk.audit_by_teacher) {
|
|
347
|
+
const teacher = sk.audit_teacher_model ? `: ${sk.audit_teacher_model}` : '';
|
|
348
|
+
html += `<span class="skill-teachermark" title="Teacher rewrote this skill; audit model passed after the rewrite${esc(teacher)}"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10L12 5 2 10l10 5 10-5z"/><path d="M6 12v5c0 1 3 2 6 2s6-1 6-2v-5"/></svg></span>`;
|
|
349
|
+
}
|
|
350
|
+
return html;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Audit verdict dot — removed at user request. The ✓ check-mark next to the
|
|
354
|
+
// confidence % still indicates a pass. Stub returns empty so the surrounding
|
|
355
|
+
// header HTML still composes without changing other layout.
|
|
356
|
+
function _auditDot(sk) { return ''; }
|
|
357
|
+
|
|
358
|
+
function _isDraftsFilter() { return !!_showDraftsOnly; }
|
|
359
|
+
|
|
360
|
+
// Confidence → colour. 90%+ is solidly green, scaling down through
|
|
361
|
+
// yellow/orange to red at 50% and below (hue 120→0 over 90→50).
|
|
362
|
+
function _confColor(conf) {
|
|
363
|
+
const hue = Math.max(0, Math.min(120, ((conf - 50) / 40) * 120));
|
|
364
|
+
return `hsl(${Math.round(hue)}, 70%, 42%)`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Shared action icons (collapsed kebab menu + expanded footer use the same).
|
|
368
|
+
const _ICON = {
|
|
369
|
+
del: '<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>',
|
|
370
|
+
edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>',
|
|
371
|
+
approve: '<polyline points="20 6 9 17 4 12"/>',
|
|
372
|
+
unpublish: '<path d="M5 12l5 5L20 7"/>',
|
|
373
|
+
test: '<polygon points="5 3 19 12 5 21 5 3"/>',
|
|
374
|
+
};
|
|
375
|
+
function _svg(paths, { fill = 'none', size = 13 } = {}) {
|
|
376
|
+
const stroke = fill === 'currentColor' ? '' : 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
|
377
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="${fill}" ${stroke} style="vertical-align:-2px;flex-shrink:0;">${paths}</svg>`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Kebab dropdown for a collapsed skill card — same actions + icons as the
|
|
381
|
+
// expanded footer (Publish/Unpublish · Edit · Delete).
|
|
382
|
+
function _openSkillMenu(btn, card, sk, name, isPublished) {
|
|
383
|
+
document.querySelectorAll('.skill-kebab-menu').forEach(m => m.remove());
|
|
384
|
+
const menu = document.createElement('div');
|
|
385
|
+
menu.className = 'skill-kebab-menu';
|
|
386
|
+
const mk = (paths, label, opts, onClick) => {
|
|
387
|
+
const item = document.createElement('button');
|
|
388
|
+
item.className = 'skill-kebab-item' + (opts && opts.danger ? ' danger' : '');
|
|
389
|
+
item.innerHTML = _svg(paths, opts) + `<span>${label}</span>`;
|
|
390
|
+
item.addEventListener('click', (e) => { e.stopPropagation(); menu.remove(); onClick(); });
|
|
391
|
+
menu.appendChild(item);
|
|
392
|
+
};
|
|
393
|
+
if (isPublished) mk(_ICON.unpublish, 'Unpublish', {}, () => _setSkillStatus(name, 'draft'));
|
|
394
|
+
else mk(_ICON.approve, 'Publish', {}, () => _setSkillStatus(name, 'published'));
|
|
395
|
+
mk(_ICON.edit, 'Edit', {}, async () => {
|
|
396
|
+
if (!card.classList.contains('doclib-card-expanded')) await _expandSkillCard(card, name);
|
|
397
|
+
_toggleSkillEdit(card, name);
|
|
398
|
+
});
|
|
399
|
+
mk(_ICON.test, 'Test', {}, () => _testSkill(card, name));
|
|
400
|
+
// Audit kicks off the bulk audit-all loop (test → judge → fix → retry → demote).
|
|
401
|
+
// Starts at the top of the list and walks down.
|
|
402
|
+
mk(_ICON.test, 'Audit', {}, () => _auditAllSkills());
|
|
403
|
+
mk(_ICON.del, 'Delete', { danger: true }, () => _deleteSkill(name, card));
|
|
404
|
+
|
|
405
|
+
// Select — enters bulk-select mode and pre-selects this skill. Same pattern
|
|
406
|
+
// as the email/documents/brain Select item, with the email bullet icon.
|
|
407
|
+
const selItem = document.createElement('button');
|
|
408
|
+
selItem.className = 'skill-kebab-item';
|
|
409
|
+
selItem.innerHTML = '<span style="display:inline-flex;width:14px;height:14px;align-items:center;justify-content:center;"><span style="font-size:16px;line-height:1;">●</span></span><span>Select</span>';
|
|
410
|
+
selItem.addEventListener('click', (e) => {
|
|
411
|
+
e.stopPropagation();
|
|
412
|
+
menu.remove();
|
|
413
|
+
if (!_selectMode) _enterSelectMode();
|
|
414
|
+
_selectedNames.add(name);
|
|
415
|
+
renderSkillsList();
|
|
416
|
+
});
|
|
417
|
+
menu.appendChild(selItem);
|
|
418
|
+
|
|
419
|
+
// Mobile-only Cancel — mirrors the email/documents/brain popup pattern.
|
|
420
|
+
// CSS hides `.dropdown-cancel-mobile` on desktop where outside-click
|
|
421
|
+
// already dismisses cleanly.
|
|
422
|
+
const cancelItem = document.createElement('button');
|
|
423
|
+
cancelItem.className = 'skill-kebab-item dropdown-cancel-mobile';
|
|
424
|
+
cancelItem.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg><span>Cancel</span>';
|
|
425
|
+
cancelItem.addEventListener('click', (e) => { e.stopPropagation(); menu.remove(); });
|
|
426
|
+
menu.appendChild(cancelItem);
|
|
427
|
+
|
|
428
|
+
document.body.appendChild(menu);
|
|
429
|
+
const r = btn.getBoundingClientRect();
|
|
430
|
+
menu.style.top = (r.bottom + 4) + 'px';
|
|
431
|
+
menu.style.right = Math.max(6, window.innerWidth - r.right) + 'px';
|
|
432
|
+
// Keep it on-screen (mobile): flip above the button if it would overflow the
|
|
433
|
+
// bottom, clamp the left edge, and cap the height as a last resort.
|
|
434
|
+
const mr = menu.getBoundingClientRect();
|
|
435
|
+
if (mr.bottom > window.innerHeight - 6) {
|
|
436
|
+
menu.style.top = Math.max(6, r.top - mr.height - 4) + 'px';
|
|
437
|
+
}
|
|
438
|
+
if (mr.left < 6) {
|
|
439
|
+
menu.style.right = Math.max(6, window.innerWidth - 6 - mr.width) + 'px';
|
|
440
|
+
}
|
|
441
|
+
const mr2 = menu.getBoundingClientRect();
|
|
442
|
+
if (mr2.bottom > window.innerHeight - 6) {
|
|
443
|
+
menu.style.maxHeight = Math.max(80, window.innerHeight - 12 - mr2.top) + 'px';
|
|
444
|
+
menu.style.overflowY = 'auto';
|
|
445
|
+
}
|
|
446
|
+
const close = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('click', close, true); } };
|
|
447
|
+
setTimeout(() => document.addEventListener('click', close, true), 0);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Cards for the agent's built-in tool capabilities (from
|
|
451
|
+
// /api/skills/builtin → TOOL_SECTIONS). Expandable to preview the
|
|
452
|
+
// instruction block; editable with a warning + a revert-to-default
|
|
453
|
+
// button (overrides stored in settings, applied to the prompt).
|
|
454
|
+
function _buildBuiltinCards() {
|
|
455
|
+
return builtinSkills.map(b => {
|
|
456
|
+
const card = document.createElement('div');
|
|
457
|
+
card.className = 'doclib-card skill-card skill-builtin-card';
|
|
458
|
+
card.dataset.builtinName = b.name;
|
|
459
|
+
|
|
460
|
+
const header = document.createElement('div');
|
|
461
|
+
header.className = 'doclib-card-header skill-card-header';
|
|
462
|
+
header.innerHTML = `
|
|
463
|
+
<span class="skill-conf-dot" style="display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--accent, var(--red));flex-shrink:0;margin-right:6px;opacity:0.55;"></span>
|
|
464
|
+
<div style="flex:1;min-width:0;overflow:hidden;">
|
|
465
|
+
<div class="doclib-card-title" style="display:flex;align-items:center;gap:6px;min-width:0;">
|
|
466
|
+
<code style="font-weight:600;font-size:0.9em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:1;min-width:0;">${esc(b.name)}</code>
|
|
467
|
+
<span class="memory-cat-badge" style="background:color-mix(in srgb, var(--fg) 14%, transparent)">built-in</span>
|
|
468
|
+
${b.is_overridden ? '<span class="memory-cat-badge" title="You have edited this built-in capability" style="background:color-mix(in srgb, var(--color-warning, #f0ad4e) 30%, transparent);">edited</span>' : ''}
|
|
469
|
+
</div>
|
|
470
|
+
${b.description ? `<div class="doclib-card-session" title="${esc(b.description)}" style="font-size:10px;opacity:0.55;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${esc(b.description)}</div>` : ''}
|
|
471
|
+
</div>
|
|
472
|
+
<span class="doclib-card-chevron"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
473
|
+
`;
|
|
474
|
+
card.appendChild(header);
|
|
475
|
+
|
|
476
|
+
const preview = document.createElement('div');
|
|
477
|
+
preview.className = 'doclib-card-preview skill-card-preview';
|
|
478
|
+
// Warning banner — editing a built-in changes how the assistant uses a native tool.
|
|
479
|
+
const warn = document.createElement('div');
|
|
480
|
+
warn.className = 'skill-builtin-warn';
|
|
481
|
+
warn.innerHTML = '⚠ This is a built-in capability. Editing changes how the assistant is instructed to use this native tool — it can break or alter core behaviour. Use Revert to restore the shipped default.';
|
|
482
|
+
preview.appendChild(warn);
|
|
483
|
+
const pre = document.createElement('pre');
|
|
484
|
+
pre.className = 'skill-md-pre';
|
|
485
|
+
pre.textContent = ''; // filled on expand
|
|
486
|
+
preview.appendChild(pre);
|
|
487
|
+
|
|
488
|
+
// Footer: Revert (left, only meaningful when overridden) · Edit/Save (right).
|
|
489
|
+
const actions = document.createElement('div');
|
|
490
|
+
actions.className = 'doclib-card-expanded-actions';
|
|
491
|
+
|
|
492
|
+
const revertBtn = document.createElement('button');
|
|
493
|
+
revertBtn.className = 'doclib-card-text-btn doclib-card-action-btn doclib-card-text-btn-danger';
|
|
494
|
+
revertBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>Revert';
|
|
495
|
+
revertBtn.title = 'Restore the original shipped instructions';
|
|
496
|
+
revertBtn.addEventListener('click', (e) => { e.stopPropagation(); _revertBuiltin(b.name); });
|
|
497
|
+
|
|
498
|
+
const editBtn = document.createElement('button');
|
|
499
|
+
editBtn.className = 'doclib-card-text-btn doclib-card-action-btn';
|
|
500
|
+
editBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Edit';
|
|
501
|
+
editBtn.addEventListener('click', (e) => { e.stopPropagation(); _toggleBuiltinEdit(card, b.name); });
|
|
502
|
+
|
|
503
|
+
const rightGroup = document.createElement('div');
|
|
504
|
+
rightGroup.className = 'doclib-action-group';
|
|
505
|
+
const btnRow = document.createElement('div');
|
|
506
|
+
btnRow.className = 'doclib-action-btn-row';
|
|
507
|
+
btnRow.appendChild(editBtn);
|
|
508
|
+
rightGroup.appendChild(btnRow);
|
|
509
|
+
|
|
510
|
+
actions.appendChild(revertBtn);
|
|
511
|
+
actions.appendChild(rightGroup);
|
|
512
|
+
preview.appendChild(actions);
|
|
513
|
+
card.appendChild(preview);
|
|
514
|
+
|
|
515
|
+
card.addEventListener('click', (e) => {
|
|
516
|
+
if (e.target.closest('button, input, textarea')) return;
|
|
517
|
+
_expandBuiltinCard(card, b.name);
|
|
518
|
+
});
|
|
519
|
+
return card;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function _expandBuiltinCard(card, name) {
|
|
524
|
+
const grid = card.closest('.doclib-grid');
|
|
525
|
+
if (card.classList.contains('doclib-card-expanded')) {
|
|
526
|
+
card.classList.remove('doclib-card-expanded');
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (grid) grid.querySelectorAll('.doclib-card-expanded').forEach(c => c.classList.remove('doclib-card-expanded'));
|
|
530
|
+
card.classList.add('doclib-card-expanded');
|
|
531
|
+
if (grid) grid.scrollTop = 0;
|
|
532
|
+
const pre = card.querySelector('.skill-md-pre');
|
|
533
|
+
if (pre && !card._loaded) {
|
|
534
|
+
pre.textContent = 'Loading…';
|
|
535
|
+
try {
|
|
536
|
+
const res = await fetch(`${API}/api/skills/builtin/${encodeURIComponent(name)}`);
|
|
537
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
538
|
+
const data = await res.json();
|
|
539
|
+
pre.textContent = data.text || '(empty)';
|
|
540
|
+
card._loaded = true;
|
|
541
|
+
card._text = data.text || '';
|
|
542
|
+
card._default = data.default || '';
|
|
543
|
+
} catch (e) {
|
|
544
|
+
pre.textContent = 'Failed to load.';
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function _toggleBuiltinEdit(card, name) {
|
|
550
|
+
const preview = card.querySelector('.skill-card-preview');
|
|
551
|
+
if (!preview) return;
|
|
552
|
+
if (preview.querySelector('.skill-md-editor')) { _saveBuiltinEdit(card, name); return; }
|
|
553
|
+
const pre = preview.querySelector('.skill-md-pre');
|
|
554
|
+
const ta = document.createElement('textarea');
|
|
555
|
+
ta.className = 'skill-md-editor';
|
|
556
|
+
ta.spellcheck = false;
|
|
557
|
+
ta.value = (card._text != null ? card._text : (pre ? pre.textContent : '')) || '';
|
|
558
|
+
ta.addEventListener('click', (e) => e.stopPropagation());
|
|
559
|
+
if (pre) pre.style.display = 'none';
|
|
560
|
+
preview.insertBefore(ta, preview.querySelector('.doclib-card-expanded-actions'));
|
|
561
|
+
ta.focus();
|
|
562
|
+
const editBtn = [...preview.querySelectorAll('.doclib-card-action-btn')].find(b => /Edit|Save/.test(b.textContent));
|
|
563
|
+
if (editBtn) editBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>Save';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function _saveBuiltinEdit(card, name) {
|
|
567
|
+
const ta = card.querySelector('.skill-md-editor');
|
|
568
|
+
if (!ta) return;
|
|
569
|
+
try {
|
|
570
|
+
const res = await fetch(`${API}/api/skills/builtin/${encodeURIComponent(name)}`, {
|
|
571
|
+
method: 'PUT',
|
|
572
|
+
headers: { 'Content-Type': 'application/json' },
|
|
573
|
+
body: JSON.stringify({ text: ta.value }),
|
|
574
|
+
});
|
|
575
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
576
|
+
uiModule.showToast('Built-in capability updated');
|
|
577
|
+
builtinSkills = []; // force reload of built-in list (refreshes "edited" badge)
|
|
578
|
+
await loadSkills();
|
|
579
|
+
} catch (e) { uiModule.showError('Save failed: ' + e.message); }
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function _revertBuiltin(name) {
|
|
583
|
+
if (!(await uiModule.styledConfirm(`Revert "${name}" to its original built-in instructions?`, { confirmText: 'Revert', danger: true }))) return;
|
|
584
|
+
try {
|
|
585
|
+
const res = await fetch(`${API}/api/skills/builtin/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
586
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
587
|
+
uiModule.showToast('Reverted to default');
|
|
588
|
+
builtinSkills = [];
|
|
589
|
+
await loadSkills();
|
|
590
|
+
} catch (e) { uiModule.showError('Revert failed: ' + e.message); }
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function _getFilteredSkills() {
|
|
594
|
+
const query = (document.getElementById('skills-search')?.value || '').toLowerCase();
|
|
595
|
+
let filtered = query ? skills.filter(sk => _matches(sk, query)) : skills;
|
|
596
|
+
if (_showDraftsOnly) {
|
|
597
|
+
filtered = filtered.filter(sk => (sk.status || 'draft') !== 'published');
|
|
598
|
+
}
|
|
599
|
+
if (_showPublishedOnly) {
|
|
600
|
+
filtered = filtered.filter(sk => (sk.status || 'draft') === 'published');
|
|
601
|
+
}
|
|
602
|
+
if (_confMax != null) {
|
|
603
|
+
// "≤ X%" — surface the lower-confidence skills that may need review.
|
|
604
|
+
filtered = filtered.filter(sk => Math.round((sk.confidence || 0) * 100) <= _confMax);
|
|
605
|
+
}
|
|
606
|
+
return _sortSkills(filtered);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function renderSkillsList() {
|
|
610
|
+
const container = document.getElementById('skills-list');
|
|
611
|
+
if (!container) return;
|
|
612
|
+
// Re-render rebuilds the cards (none expanded), so clear the expand flag
|
|
613
|
+
// on the admin-card or it would keep the toolbar hidden with nothing open.
|
|
614
|
+
container.closest('.admin-card')?.classList.remove('skills-has-expanded');
|
|
615
|
+
|
|
616
|
+
const sorted = _getFilteredSkills();
|
|
617
|
+
// Built-in capabilities show as their own read-only section (skipped when
|
|
618
|
+
// the user is filtering to drafts, since built-ins aren't drafts).
|
|
619
|
+
// Skills menu shows the user's own skills only (built-in capabilities
|
|
620
|
+
// are intentionally not surfaced here).
|
|
621
|
+
const showBuiltin = false;
|
|
622
|
+
|
|
623
|
+
if (!sorted.length && !showBuiltin) {
|
|
624
|
+
container.innerHTML = `<div style="text-align:center;opacity:0.4;padding:24px 0;font-size:11px;">${loaded ? 'No skills yet, use agent for it to auto extract them.' : 'Loading…'}</div>`;
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Library-style cards: a compact bar that expands in-place to show the
|
|
629
|
+
// SKILL.md, with a footer (Delete left; Edit / Run / Approve right).
|
|
630
|
+
// Reuses the proven .doclib-card / .doclib-card-preview /
|
|
631
|
+
// .doclib-card-expanded-actions markup so the desktop+mobile expand +
|
|
632
|
+
// footer behaviour matches the document/chat library exactly.
|
|
633
|
+
//
|
|
634
|
+
// #skills-list itself becomes the .doclib-grid (rather than a nested
|
|
635
|
+
// grid) so the global "hide non-grid children when a card is expanded"
|
|
636
|
+
// rule (.admin-card:has(.doclib-card-expanded) > *:not(.doclib-grid))
|
|
637
|
+
// doesn't hide the list container along with everything else.
|
|
638
|
+
container.classList.add('doclib-grid');
|
|
639
|
+
const cards = [];
|
|
640
|
+
const dupeMeta = _duplicateMeta(sorted);
|
|
641
|
+
|
|
642
|
+
for (const sk of sorted) {
|
|
643
|
+
const name = sk.name || sk.id;
|
|
644
|
+
const dm = dupeMeta.get(name);
|
|
645
|
+
if (dm) {
|
|
646
|
+
sk._duplicateGroup = dm.group;
|
|
647
|
+
sk._duplicateKeep = dm.keep;
|
|
648
|
+
sk._duplicateKeepName = dm.keepName;
|
|
649
|
+
sk._duplicateNames = dm.names;
|
|
650
|
+
} else {
|
|
651
|
+
delete sk._duplicateGroup;
|
|
652
|
+
delete sk._duplicateKeep;
|
|
653
|
+
delete sk._duplicateKeepName;
|
|
654
|
+
delete sk._duplicateNames;
|
|
655
|
+
}
|
|
656
|
+
const conf = Math.round((sk.confidence || 0) * 100);
|
|
657
|
+
const uses = sk.uses || 0;
|
|
658
|
+
const isPublished = (sk.status === 'published');
|
|
659
|
+
const confColor = _confColor(conf);
|
|
660
|
+
|
|
661
|
+
const card = document.createElement('div');
|
|
662
|
+
card.className = 'doclib-card skill-card';
|
|
663
|
+
card.dataset.skillName = name;
|
|
664
|
+
card.dataset.skillStatus = sk.status || 'draft';
|
|
665
|
+
|
|
666
|
+
const checked = _selectedNames.has(name) ? 'checked' : '';
|
|
667
|
+
const cbHtml = _selectMode
|
|
668
|
+
? `<input type="checkbox" class="memory-select-cb skill-select-cb" data-name="${esc(name)}" ${checked} style="margin-right:6px;flex-shrink:0;cursor:pointer;" />`
|
|
669
|
+
: '';
|
|
670
|
+
|
|
671
|
+
// Collapsed header bar: dot · name (wraps) · [pills (right) · stats · menu].
|
|
672
|
+
const header = document.createElement('div');
|
|
673
|
+
header.className = 'doclib-card-header skill-card-header';
|
|
674
|
+
header.innerHTML = `
|
|
675
|
+
${cbHtml}
|
|
676
|
+
${_auditDot(sk)}
|
|
677
|
+
<div class="skill-card-textcol">
|
|
678
|
+
<code class="skill-card-name">${esc(name)}</code>
|
|
679
|
+
${sk.description ? `<div class="skill-card-desc">${esc(sk.description)}</div>` : ''}
|
|
680
|
+
</div>
|
|
681
|
+
<div class="skill-card-right">
|
|
682
|
+
${_statusPill(sk)}
|
|
683
|
+
${_sourcePill(sk)}
|
|
684
|
+
${_auditModelPills(sk)}
|
|
685
|
+
${_necessityPill(sk)}
|
|
686
|
+
${_duplicatePriorityPill(sk)}
|
|
687
|
+
<span class="skill-stats">${_auditMarks(sk)}<span class="skill-conf" style="color:${confColor};">${conf}%</span> · ${uses}u</span>
|
|
688
|
+
<span class="skill-chevron-up" title="Collapse"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></span>
|
|
689
|
+
<button class="skill-kebab-btn" title="Actions" aria-label="Actions"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/></svg></button>
|
|
690
|
+
</div>
|
|
691
|
+
`;
|
|
692
|
+
card.appendChild(header);
|
|
693
|
+
|
|
694
|
+
// Kebab dropdown (collapsed-bar quick actions: same set + icons as the
|
|
695
|
+
// expanded footer). Clicking the kebab opens it; it doesn't expand.
|
|
696
|
+
header.querySelector('.skill-kebab-btn').addEventListener('click', (e) => {
|
|
697
|
+
e.stopPropagation();
|
|
698
|
+
_openSkillMenu(e.currentTarget, card, sk, name, isPublished);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// Preview (hidden until expanded) — SKILL.md goes here + footer.
|
|
702
|
+
const preview = document.createElement('div');
|
|
703
|
+
preview.className = 'doclib-card-preview skill-card-preview';
|
|
704
|
+
const pre = document.createElement('pre');
|
|
705
|
+
pre.className = 'skill-md-pre';
|
|
706
|
+
pre.textContent = ''; // filled on expand
|
|
707
|
+
preview.appendChild(pre);
|
|
708
|
+
|
|
709
|
+
// Footer: Approve/Unpublish on the left, destructive delete on the right.
|
|
710
|
+
const actions = document.createElement('div');
|
|
711
|
+
actions.className = 'doclib-card-expanded-actions';
|
|
712
|
+
|
|
713
|
+
const delBtn = document.createElement('button');
|
|
714
|
+
delBtn.className = 'doclib-card-text-btn doclib-card-action-btn doclib-card-text-btn-danger';
|
|
715
|
+
delBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>Delete';
|
|
716
|
+
delBtn.addEventListener('click', (e) => { e.stopPropagation(); _deleteSkill(name, card); });
|
|
717
|
+
|
|
718
|
+
const editBtn = document.createElement('button');
|
|
719
|
+
editBtn.className = 'doclib-card-text-btn doclib-card-action-btn';
|
|
720
|
+
editBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Edit';
|
|
721
|
+
editBtn.addEventListener('click', (e) => { e.stopPropagation(); _toggleSkillEdit(card, name); });
|
|
722
|
+
|
|
723
|
+
const pubBtn = document.createElement('button');
|
|
724
|
+
pubBtn.className = 'doclib-card-text-btn doclib-card-action-btn';
|
|
725
|
+
if (isPublished) {
|
|
726
|
+
pubBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg>Unpublish';
|
|
727
|
+
pubBtn.title = 'Move back to draft';
|
|
728
|
+
pubBtn.addEventListener('click', (e) => { e.stopPropagation(); _setSkillStatus(name, 'draft'); });
|
|
729
|
+
} else {
|
|
730
|
+
pubBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Publish';
|
|
731
|
+
pubBtn.title = 'Publish — appears in the skills index';
|
|
732
|
+
pubBtn.style.color = 'var(--color-success, #4caf50)';
|
|
733
|
+
pubBtn.addEventListener('click', (e) => { e.stopPropagation(); _setSkillStatus(name, 'published'); });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Test/audit this one skill — same action that's in the kebab, surfaced in
|
|
737
|
+
// the footer too so it's not buried under the "⋯" menu.
|
|
738
|
+
const testBtn = document.createElement('button');
|
|
739
|
+
testBtn.className = 'doclib-card-text-btn doclib-card-action-btn';
|
|
740
|
+
testBtn.innerHTML = _svg(_ICON.test, { size: 11 }) + 'Test';
|
|
741
|
+
testBtn.title = 'Test this skill — run it + AI judge';
|
|
742
|
+
testBtn.addEventListener('click', (e) => {
|
|
743
|
+
e.stopPropagation();
|
|
744
|
+
// Immediate visual feedback: previously the click looked like nothing
|
|
745
|
+
// happened because _testSkill awaits a status fetch before overwriting
|
|
746
|
+
// the preview — so users would tap a second time. Mark the button as
|
|
747
|
+
// pending right away so the first tap is obviously registered.
|
|
748
|
+
if (testBtn.dataset.busy === '1') return; // also dedupe rapid double-tap
|
|
749
|
+
testBtn.dataset.busy = '1';
|
|
750
|
+
testBtn.disabled = true;
|
|
751
|
+
const _origHTML = testBtn.innerHTML;
|
|
752
|
+
testBtn.innerHTML = _svg(_ICON.test, { size: 11 }) + 'Starting…';
|
|
753
|
+
Promise.resolve(_testSkill(card, name)).finally(() => {
|
|
754
|
+
// The preview gets overwritten by _testSkill, which removes the
|
|
755
|
+
// testBtn from the DOM. The cleanup below only matters if the
|
|
756
|
+
// button still exists (e.g. _testSkill bailed early).
|
|
757
|
+
if (document.body.contains(testBtn)) {
|
|
758
|
+
testBtn.disabled = false;
|
|
759
|
+
testBtn.dataset.busy = '';
|
|
760
|
+
testBtn.innerHTML = _origHTML;
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const rightGroup = document.createElement('div');
|
|
766
|
+
rightGroup.className = 'doclib-action-group';
|
|
767
|
+
const btnRow = document.createElement('div');
|
|
768
|
+
btnRow.className = 'doclib-action-btn-row';
|
|
769
|
+
btnRow.appendChild(testBtn);
|
|
770
|
+
btnRow.appendChild(editBtn);
|
|
771
|
+
btnRow.appendChild(delBtn);
|
|
772
|
+
rightGroup.appendChild(btnRow);
|
|
773
|
+
|
|
774
|
+
actions.appendChild(pubBtn);
|
|
775
|
+
actions.appendChild(rightGroup);
|
|
776
|
+
preview.appendChild(actions);
|
|
777
|
+
card.appendChild(preview);
|
|
778
|
+
|
|
779
|
+
// Click to expand/collapse (unless in select mode → toggle checkbox).
|
|
780
|
+
card.addEventListener('click', (e) => {
|
|
781
|
+
if (card._suppressNextClick) { card._suppressNextClick = false; return; }
|
|
782
|
+
if (e.target.closest('button, input, textarea')) return;
|
|
783
|
+
if (_selectMode) {
|
|
784
|
+
const cb = card.querySelector('.skill-select-cb');
|
|
785
|
+
if (cb) { cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); }
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
_expandSkillCard(card, name);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// Long-press anywhere on the card opens the kebab dropdown — mirrors the
|
|
792
|
+
// documents library + brain memory pattern. Skip when touch starts on a
|
|
793
|
+
// button/input so per-control handlers keep working.
|
|
794
|
+
{
|
|
795
|
+
const kebab = header.querySelector('.skill-kebab-btn');
|
|
796
|
+
let hold = null;
|
|
797
|
+
let start = null;
|
|
798
|
+
const _lpCancel = () => { if (hold) { clearTimeout(hold); hold = null; } start = null; };
|
|
799
|
+
card.addEventListener('pointerdown', (e) => {
|
|
800
|
+
if (e.target.closest('.skill-kebab-btn, .skill-select-cb, button, input, textarea')) return;
|
|
801
|
+
start = { x: e.clientX, y: e.clientY };
|
|
802
|
+
hold = setTimeout(() => {
|
|
803
|
+
hold = null;
|
|
804
|
+
card._suppressNextClick = true;
|
|
805
|
+
setTimeout(() => { card._suppressNextClick = false; }, 400);
|
|
806
|
+
if (navigator.vibrate) try { navigator.vibrate(15); } catch {}
|
|
807
|
+
if (kebab) kebab.click();
|
|
808
|
+
}, 500);
|
|
809
|
+
});
|
|
810
|
+
card.addEventListener('pointermove', (e) => {
|
|
811
|
+
if (!start) return;
|
|
812
|
+
if (Math.hypot(e.clientX - start.x, e.clientY - start.y) > 10) _lpCancel();
|
|
813
|
+
});
|
|
814
|
+
card.addEventListener('pointerup', _lpCancel);
|
|
815
|
+
card.addEventListener('pointercancel', _lpCancel);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
cards.push(card);
|
|
819
|
+
}
|
|
820
|
+
container.innerHTML = '';
|
|
821
|
+
|
|
822
|
+
// Two collapsible sections — "Your skills" and "Built-in". Headers and
|
|
823
|
+
// cards are all DIRECT children of the grid (cards tagged with
|
|
824
|
+
// data-skill-section) so the global expand rule — which hides sibling
|
|
825
|
+
// .doclib-card elements by direct-child selector — keeps working.
|
|
826
|
+
// Collapse just toggles display on the tagged cards.
|
|
827
|
+
const _mkSectionHeader = (sectionId, title, count) => {
|
|
828
|
+
const collapsed = _collapsedSections.has(sectionId);
|
|
829
|
+
const hdr = document.createElement('div');
|
|
830
|
+
hdr.className = 'skills-section-label skills-section-header' + (collapsed ? ' collapsed' : '');
|
|
831
|
+
hdr.dataset.section = sectionId;
|
|
832
|
+
hdr.innerHTML =
|
|
833
|
+
`<svg class="skills-section-chevron" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>` +
|
|
834
|
+
`<span>${esc(title)}</span>` +
|
|
835
|
+
`<span class="skills-section-count">${count}</span>`;
|
|
836
|
+
hdr.addEventListener('click', () => {
|
|
837
|
+
if (_collapsedSections.has(sectionId)) _collapsedSections.delete(sectionId);
|
|
838
|
+
else _collapsedSections.add(sectionId);
|
|
839
|
+
_saveCollapsedSections();
|
|
840
|
+
_applySectionCollapse(container);
|
|
841
|
+
});
|
|
842
|
+
return hdr;
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// "Your skills" section — show the header only when there's also a
|
|
846
|
+
// built-in section to distinguish from (otherwise it's just the list).
|
|
847
|
+
if (cards.length) {
|
|
848
|
+
if (showBuiltin) container.appendChild(_mkSectionHeader('user', 'Your skills', cards.length));
|
|
849
|
+
cards.forEach(c => { c.dataset.skillSection = 'user'; container.appendChild(c); });
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Built-in capabilities — read-only cards (the agent's native tools).
|
|
853
|
+
if (showBuiltin) {
|
|
854
|
+
const builtinCards = _buildBuiltinCards();
|
|
855
|
+
container.appendChild(_mkSectionHeader('builtin', 'Built-in capabilities', builtinCards.length));
|
|
856
|
+
builtinCards.forEach(c => { c.dataset.skillSection = 'builtin'; container.appendChild(c); });
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
_applySectionCollapse(container);
|
|
860
|
+
|
|
861
|
+
// Domino-in cascade when the Skills tab is (re)opened — same sleek
|
|
862
|
+
// staggered entrance the document/chat library uses (.doclib-just-opened
|
|
863
|
+
// → section-domino-in on each .doclib-card child). Only consumes the flag
|
|
864
|
+
// set on tab-open, so search/sort/edit re-renders stay instant.
|
|
865
|
+
if (_cascadeNext && cards.length) {
|
|
866
|
+
_cascadeNext = false;
|
|
867
|
+
_playSkillsCascade(container);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Select-mode checkbox wiring (card-body click is handled in the card's
|
|
871
|
+
// own click listener above).
|
|
872
|
+
if (_selectMode) {
|
|
873
|
+
container.querySelectorAll('.skill-select-cb').forEach(cb => {
|
|
874
|
+
cb.addEventListener('change', () => {
|
|
875
|
+
const name = cb.dataset.name;
|
|
876
|
+
if (cb.checked) _selectedNames.add(name); else _selectedNames.delete(name);
|
|
877
|
+
const all = document.getElementById('skills-select-all');
|
|
878
|
+
if (all) {
|
|
879
|
+
const visible = _getFilteredSkills().map(s => s.name || s.id);
|
|
880
|
+
all.checked = visible.length > 0 && visible.every(n => _selectedNames.has(n));
|
|
881
|
+
}
|
|
882
|
+
_updateBulkBar();
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Background-load the visible skills' SKILL.md so expanding any of them is
|
|
888
|
+
// instant (no first-time async fetch → no jump). Deferred so it never
|
|
889
|
+
// competes with the render/cascade paint.
|
|
890
|
+
setTimeout(_preloadVisibleMarkdown, 0);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ---- Card expand / edit / actions ----
|
|
894
|
+
|
|
895
|
+
// Collapse an expanded skill card: drop the class AND clear the inline
|
|
896
|
+
// heights skills.js pinned on the card/preview/<pre> (otherwise a collapsed
|
|
897
|
+
// card keeps its full expanded height) and detach its resize listener.
|
|
898
|
+
function _collapseSkillCardEl(c) {
|
|
899
|
+
c.classList.remove('doclib-card-expanded', 'skill-expand-instant');
|
|
900
|
+
c.style.removeProperty('height');
|
|
901
|
+
const pv = c.querySelector('.doclib-card-preview');
|
|
902
|
+
const pr = c.querySelector('.skill-md-pre') || c.querySelector('.skill-md-editor');
|
|
903
|
+
if (pv) { pv.style.removeProperty('height'); pv.style.removeProperty('flex'); pv.style.removeProperty('max-height'); }
|
|
904
|
+
if (pr) { pr.style.removeProperty('height'); pr.style.removeProperty('flex'); }
|
|
905
|
+
if (c._fillH) window.removeEventListener('resize', c._fillH);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function _expandSkillCard(card, name) {
|
|
909
|
+
const grid = card.closest('.doclib-grid');
|
|
910
|
+
const adminCard = card.closest('.admin-card');
|
|
911
|
+
// Toggle collapse if already open.
|
|
912
|
+
if (card.classList.contains('doclib-card-expanded')) {
|
|
913
|
+
_collapseSkillCardEl(card);
|
|
914
|
+
if (adminCard) adminCard.classList.remove('skills-has-expanded');
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
// Were we already showing another expanded card? If so this is a SWITCH,
|
|
918
|
+
// not a fresh open — skip the fade-in. The fade reveals the previous card
|
|
919
|
+
// collapsing behind the new (semi-transparent) one, which read as a jump.
|
|
920
|
+
const switching = !!(grid && grid.querySelector('.doclib-card-expanded'));
|
|
921
|
+
// Collapse any other expanded sibling (full cleanup, not just the class).
|
|
922
|
+
if (grid) grid.querySelectorAll('.doclib-card-expanded').forEach(_collapseSkillCardEl);
|
|
923
|
+
card.classList.add('doclib-card-expanded');
|
|
924
|
+
if (switching) card.classList.add('skill-expand-instant');
|
|
925
|
+
// Explicit class on the admin-card so CSS doesn't depend on :has()
|
|
926
|
+
// (Firefox mobile builds without :has left the expand at ~50%).
|
|
927
|
+
if (adminCard) adminCard.classList.add('skills-has-expanded');
|
|
928
|
+
if (grid) grid.scrollTop = 0;
|
|
929
|
+
|
|
930
|
+
// Firefox doesn't treat the absolutely-positioned card's stretched height
|
|
931
|
+
// (inset:0) or height:100% as DEFINITE, so grid/flex children won't fill.
|
|
932
|
+
// Pin an explicit px height = the card's already-rendered height. A px
|
|
933
|
+
// value is unambiguously definite, so the preview + <pre> finally fill.
|
|
934
|
+
card._fillH = () => {
|
|
935
|
+
// Reset any prior inline heights so we measure the natural box first
|
|
936
|
+
// (and so switching desktop<->mobile never leaves stale px values).
|
|
937
|
+
card.style.removeProperty('height');
|
|
938
|
+
const preview = card.querySelector('.doclib-card-preview');
|
|
939
|
+
const header = card.querySelector('.skill-card-header');
|
|
940
|
+
const pre = card.querySelector('.skill-md-pre') || card.querySelector('.skill-md-editor');
|
|
941
|
+
if (preview) { preview.style.removeProperty('height'); preview.style.removeProperty('flex'); preview.style.removeProperty('max-height'); }
|
|
942
|
+
if (pre) { pre.style.removeProperty('height'); pre.style.removeProperty('flex'); }
|
|
943
|
+
|
|
944
|
+
// The px-pinning is ONLY for the mobile layout (position:absolute fill,
|
|
945
|
+
// where Firefox won't propagate a definite height). On desktop the card
|
|
946
|
+
// expands via normal flex/flow — pinning measured heights there just
|
|
947
|
+
// under-sizes it. So bail on desktop and let the CSS handle it.
|
|
948
|
+
if (!window.matchMedia('(max-width: 768px)').matches) return;
|
|
949
|
+
|
|
950
|
+
const cardH = card.getBoundingClientRect().height;
|
|
951
|
+
if (cardH <= 0) return;
|
|
952
|
+
card.style.setProperty('height', cardH + 'px', 'important');
|
|
953
|
+
if (!preview) return;
|
|
954
|
+
|
|
955
|
+
const px = (el, prop) => parseFloat(getComputedStyle(el)[prop]) || 0;
|
|
956
|
+
const headerH = header ? header.getBoundingClientRect().height : 0;
|
|
957
|
+
const cardPad = px(card, 'paddingTop') + px(card, 'paddingBottom');
|
|
958
|
+
const previewH = Math.max(0, cardH - headerH - cardPad);
|
|
959
|
+
// Force the preview to an explicit height (flex:none so nothing fights it).
|
|
960
|
+
// A max-height (~335px, resolved from a % rule) was capping it — clear it.
|
|
961
|
+
preview.style.setProperty('flex', '0 0 auto', 'important');
|
|
962
|
+
preview.style.setProperty('max-height', 'none', 'important');
|
|
963
|
+
preview.style.setProperty('height', previewH + 'px', 'important');
|
|
964
|
+
|
|
965
|
+
if (pre) {
|
|
966
|
+
// Pre = preview height minus its non-pre siblings (footer, warn banner).
|
|
967
|
+
const prevPad = px(preview, 'paddingTop') + px(preview, 'paddingBottom');
|
|
968
|
+
let siblings = 0;
|
|
969
|
+
for (const child of preview.children) {
|
|
970
|
+
if (child !== pre) siblings += child.getBoundingClientRect().height;
|
|
971
|
+
}
|
|
972
|
+
const preH = Math.max(0, previewH - prevPad - siblings);
|
|
973
|
+
pre.style.setProperty('height', preH + 'px', 'important');
|
|
974
|
+
pre.style.setProperty('flex', '0 0 auto', 'important');
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
// Size SYNCHRONOUSLY (not in rAF) so the pinned heights are in place before
|
|
978
|
+
// the browser's first paint of the expanded card. Running it a frame later
|
|
979
|
+
// let the first frame paint at content-height, then snap — the "explosion"
|
|
980
|
+
// that showed on the first expand (when the SKILL.md was still loading).
|
|
981
|
+
card._fillH();
|
|
982
|
+
window.addEventListener('resize', card._fillH);
|
|
983
|
+
|
|
984
|
+
const pre = card.querySelector('.skill-md-pre');
|
|
985
|
+
if (pre && !card._mdLoaded) {
|
|
986
|
+
// Use the cache when available (the bg preload usually has it already),
|
|
987
|
+
// so the content is in place synchronously — no async settle/jump.
|
|
988
|
+
if (_mdCache.has(name)) {
|
|
989
|
+
const md = _mdCache.get(name);
|
|
990
|
+
pre.textContent = md || '(empty)';
|
|
991
|
+
card._mdLoaded = true;
|
|
992
|
+
card._md = md || '';
|
|
993
|
+
} else {
|
|
994
|
+
pre.textContent = 'Loading…';
|
|
995
|
+
try {
|
|
996
|
+
const md = await _fetchSkillMarkdown(name);
|
|
997
|
+
pre.textContent = md || '(empty)';
|
|
998
|
+
card._mdLoaded = true;
|
|
999
|
+
card._md = md;
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
pre.textContent = 'Failed to load SKILL.md';
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Swap the read-only <pre> for an editable <textarea> (and back). The
|
|
1008
|
+
// Edit button toggles; a Save button commits via the markdown endpoint.
|
|
1009
|
+
function _toggleSkillEdit(card, name) {
|
|
1010
|
+
const preview = card.querySelector('.skill-card-preview');
|
|
1011
|
+
if (!preview) return;
|
|
1012
|
+
const existing = preview.querySelector('.skill-md-editor');
|
|
1013
|
+
if (existing) {
|
|
1014
|
+
// Already editing — treat Edit as Save.
|
|
1015
|
+
_saveSkillEdit(card, name);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const pre = preview.querySelector('.skill-md-pre');
|
|
1019
|
+
const ta = document.createElement('textarea');
|
|
1020
|
+
ta.className = 'skill-md-editor';
|
|
1021
|
+
ta.spellcheck = false;
|
|
1022
|
+
ta.value = (card._md != null ? card._md : (pre ? pre.textContent : '')) || '';
|
|
1023
|
+
ta.addEventListener('click', (e) => e.stopPropagation());
|
|
1024
|
+
if (pre) pre.style.display = 'none';
|
|
1025
|
+
preview.insertBefore(ta, preview.querySelector('.doclib-card-expanded-actions'));
|
|
1026
|
+
ta.focus();
|
|
1027
|
+
// Flip the Edit button label to "Save".
|
|
1028
|
+
const editBtn = [...preview.querySelectorAll('.doclib-card-action-btn')].find(b => /Edit|Save/.test(b.textContent));
|
|
1029
|
+
if (editBtn) editBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>Save';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async function _saveSkillEdit(card, name) {
|
|
1033
|
+
const preview = card.querySelector('.skill-card-preview');
|
|
1034
|
+
const ta = preview?.querySelector('.skill-md-editor');
|
|
1035
|
+
if (!ta) return;
|
|
1036
|
+
try {
|
|
1037
|
+
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`, {
|
|
1038
|
+
method: 'POST',
|
|
1039
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1040
|
+
body: JSON.stringify({ markdown: ta.value }),
|
|
1041
|
+
});
|
|
1042
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1043
|
+
// Refresh the cached markdown so the preload/expand show the new text.
|
|
1044
|
+
_mdCache.set(name, ta.value);
|
|
1045
|
+
uiModule.showToast('Saved');
|
|
1046
|
+
await loadSkills(); // re-render (frontmatter changes like name/status may have changed)
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
uiModule.showError('Save failed: ' + e.message);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async function _deleteSkill(name, card = null) {
|
|
1053
|
+
if (!(await uiModule.styledConfirm(`Delete skill "${name}"? This removes the SKILL.md.`, { confirmText: 'Delete', danger: true }))) return;
|
|
1054
|
+
// Locate the card if the caller didn't hand one over, so we can collapse it
|
|
1055
|
+
// away gracefully (same fade+shrink as the document library) instead of
|
|
1056
|
+
// re-rendering the whole list.
|
|
1057
|
+
if (!card) {
|
|
1058
|
+
card = [...document.querySelectorAll('.skill-card')]
|
|
1059
|
+
.find(c => { const n = c.querySelector('.skill-card-name'); return n && n.textContent === name; }) || null;
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
await fetch(`${API}/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
1063
|
+
_mdCache.delete(name);
|
|
1064
|
+
if (card) {
|
|
1065
|
+
if (card._testPoll) { clearInterval(card._testPoll); card._testPoll = null; }
|
|
1066
|
+
_setCardRunning(card, false);
|
|
1067
|
+
card.classList.add('doclib-card-deleting');
|
|
1068
|
+
card.addEventListener('transitionend', () => card.remove(), { once: true });
|
|
1069
|
+
setTimeout(() => { if (card.parentElement) card.remove(); }, 400);
|
|
1070
|
+
} else {
|
|
1071
|
+
await loadSkills();
|
|
1072
|
+
}
|
|
1073
|
+
uiModule.showToast('Skill deleted');
|
|
1074
|
+
} catch (e) { uiModule.showError('Delete failed: ' + e.message); }
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function _setSkillStatus(name, status) {
|
|
1078
|
+
try {
|
|
1079
|
+
await fetch(`${API}/api/skills/${encodeURIComponent(name)}`, {
|
|
1080
|
+
method: 'PUT',
|
|
1081
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1082
|
+
body: JSON.stringify({ status }),
|
|
1083
|
+
});
|
|
1084
|
+
await loadSkills();
|
|
1085
|
+
uiModule.showToast(status === 'published' ? 'Skill approved' : 'Skill moved to draft');
|
|
1086
|
+
} catch (e) { uiModule.showError('Update failed: ' + e.message); }
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// ---- Test a skill (sandbox agent run + AI eval) ----
|
|
1090
|
+
|
|
1091
|
+
async function _fetchTestStatus(name) {
|
|
1092
|
+
try {
|
|
1093
|
+
const r = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/test-status`);
|
|
1094
|
+
return r.ok ? await r.json() : { status: 'none' };
|
|
1095
|
+
} catch { return { status: 'none' }; }
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function _renderTestLog(logEl, verdictEl, job, card, name) {
|
|
1099
|
+
if (!logEl) return;
|
|
1100
|
+
logEl.innerHTML = '';
|
|
1101
|
+
const add = (txt, cls) => { const d = document.createElement('div'); if (cls) d.className = cls; d.textContent = txt; logEl.appendChild(d); };
|
|
1102
|
+
for (const ev of (job.log || [])) {
|
|
1103
|
+
if (ev.type === 'skill_test_start') { add('Task: ' + ev.task, 'skill-test-task'); add('Model: ' + ev.model, 'skill-test-meta'); }
|
|
1104
|
+
else if (ev.type === 'agent_step') add('— round ' + ev.round + ' —', 'skill-test-round');
|
|
1105
|
+
else if (ev.type === 'tool_start') add('▸ ' + ev.tool + ' ' + String(ev.command || '').slice(0, 200), 'skill-test-tool');
|
|
1106
|
+
else if (ev.type === 'tool_output') add(String(ev.output || '').slice(0, 500), 'skill-test-out');
|
|
1107
|
+
else if (ev.type === 'say') add(ev.text || '', 'skill-test-say');
|
|
1108
|
+
else if (ev.type === 'evaluating') add('Evaluating run…', 'skill-test-meta');
|
|
1109
|
+
else if (ev.type === 'error') add('Error: ' + (ev.error || 'run failed'), 'skill-test-err');
|
|
1110
|
+
}
|
|
1111
|
+
if (job.status === 'running') add('…running (you can close this — it keeps going)', 'skill-test-meta');
|
|
1112
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
1113
|
+
if (job.status === 'done' && job.verdict) _renderTestVerdict(verdictEl, job.verdict, card, name);
|
|
1114
|
+
else if (verdictEl) verdictEl.innerHTML = '';
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// `force` = start a fresh run even if a finished result already exists (Retry).
|
|
1118
|
+
async function _testSkill(card, name, force = false) {
|
|
1119
|
+
if (!card.classList.contains('doclib-card-expanded')) await _expandSkillCard(card, name);
|
|
1120
|
+
const preview = card.querySelector('.skill-card-preview');
|
|
1121
|
+
if (!preview) return;
|
|
1122
|
+
preview.innerHTML =
|
|
1123
|
+
'<div class="skill-test"><div class="skill-test-log"></div>' +
|
|
1124
|
+
'<div class="skill-test-verdict"></div></div>';
|
|
1125
|
+
const logEl = preview.querySelector('.skill-test-log');
|
|
1126
|
+
const verdictEl = preview.querySelector('.skill-test-verdict');
|
|
1127
|
+
if (card._testPoll) { clearInterval(card._testPoll); card._testPoll = null; }
|
|
1128
|
+
|
|
1129
|
+
// Attach to an existing job unless forcing a fresh run.
|
|
1130
|
+
let job = force ? { status: 'none' } : await _fetchTestStatus(name);
|
|
1131
|
+
|
|
1132
|
+
if (job.status === 'none') {
|
|
1133
|
+
logEl.innerHTML = '<div class="skill-test-meta">Starting test…</div>';
|
|
1134
|
+
let model = '', endpoint_url = '';
|
|
1135
|
+
try {
|
|
1136
|
+
const sm = window.sessionModule;
|
|
1137
|
+
model = (sm && sm.getCurrentModel && sm.getCurrentModel()) || '';
|
|
1138
|
+
endpoint_url = (sm && sm.getCurrentEndpointUrl && sm.getCurrentEndpointUrl()) || '';
|
|
1139
|
+
} catch (_) {}
|
|
1140
|
+
try {
|
|
1141
|
+
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/test`, {
|
|
1142
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1143
|
+
body: JSON.stringify({ model, endpoint_url }),
|
|
1144
|
+
});
|
|
1145
|
+
if (!res.ok) { logEl.innerHTML = '<div class="skill-test-err">Test failed: HTTP ' + res.status + '</div>'; return; }
|
|
1146
|
+
} catch (e) { logEl.innerHTML = '<div class="skill-test-err">Test failed: ' + (e.message || e) + '</div>'; return; }
|
|
1147
|
+
job = await _fetchTestStatus(name);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
_renderTestLog(logEl, verdictEl, job, card, name);
|
|
1151
|
+
_setCardRunning(card, job.status === 'running');
|
|
1152
|
+
|
|
1153
|
+
if (job.status === 'running') {
|
|
1154
|
+
card._testPoll = setInterval(async () => {
|
|
1155
|
+
// Keep polling even if the card is collapsed (the test runs server-side);
|
|
1156
|
+
// only stop once the card itself is gone from the DOM.
|
|
1157
|
+
if (!document.body.contains(card)) { clearInterval(card._testPoll); card._testPoll = null; _setCardRunning(card, false); return; }
|
|
1158
|
+
const s = await _fetchTestStatus(name);
|
|
1159
|
+
// Update the expanded log only while it's still on screen.
|
|
1160
|
+
if (document.body.contains(logEl)) _renderTestLog(logEl, verdictEl, s, card, name);
|
|
1161
|
+
if (s.status !== 'running') {
|
|
1162
|
+
clearInterval(card._testPoll); card._testPoll = null;
|
|
1163
|
+
_setCardRunning(card, false);
|
|
1164
|
+
// If the log isn't visible (card was collapsed), still update the
|
|
1165
|
+
// header dot/% so the result shows on the folded card.
|
|
1166
|
+
if (!document.body.contains(logEl) && s.verdict && s.verdict.verdict) {
|
|
1167
|
+
_applyVerdictToHeader(card, s.verdict.verdict);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}, 1300);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Show/hide the app-wide whirlpool spinner next to the skill name while a test
|
|
1175
|
+
// is in flight. Works on the collapsed header too, since we inject a real DOM
|
|
1176
|
+
// element rather than a CSS pseudo on a class.
|
|
1177
|
+
function _setCardRunning(card, on) {
|
|
1178
|
+
if (!card) return;
|
|
1179
|
+
card.classList.toggle('skill-test-running', !!on);
|
|
1180
|
+
if (on) {
|
|
1181
|
+
if (card._testSpinner) return;
|
|
1182
|
+
const nameEl = card.querySelector('.skill-card-name');
|
|
1183
|
+
if (!nameEl) return;
|
|
1184
|
+
const wp = spinnerModule.createWhirlpool(12);
|
|
1185
|
+
wp.element.style.cssText = 'display:inline-flex;width:12px;height:12px;margin:0 0 0 7px;vertical-align:middle;flex-shrink:0;';
|
|
1186
|
+
// Append INSIDE the <code> name (inline-flow), not after it. The textcol
|
|
1187
|
+
// is a flex column, so a sibling-after lands on its own line — putting
|
|
1188
|
+
// the spinner inside the inline code keeps it on the title row.
|
|
1189
|
+
nameEl.appendChild(wp.element);
|
|
1190
|
+
card._testSpinner = wp;
|
|
1191
|
+
} else if (card._testSpinner) {
|
|
1192
|
+
try { card._testSpinner.destroy(); } catch (_) {}
|
|
1193
|
+
if (card._testSpinner.element && card._testSpinner.element.parentElement) {
|
|
1194
|
+
card._testSpinner.element.remove();
|
|
1195
|
+
}
|
|
1196
|
+
card._testSpinner = null;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Reflect a test/audit verdict on the (possibly collapsed) card header without
|
|
1201
|
+
// a full reload: the glowing audit dot, the confidence %, and the pass check.
|
|
1202
|
+
// Works whether the card is expanded or folded so a test that finishes after
|
|
1203
|
+
// you collapse still updates the card.
|
|
1204
|
+
function _applyVerdictToHeader(card, verdict) {
|
|
1205
|
+
if (!card || !verdict) return;
|
|
1206
|
+
const dotColor = {
|
|
1207
|
+
pass: 'var(--color-success, #4ade80)',
|
|
1208
|
+
needs_work: 'var(--color-warning, #f0ad4e)',
|
|
1209
|
+
inconclusive: 'var(--color-warning, #f0ad4e)',
|
|
1210
|
+
fail: 'var(--color-danger, #e06c75)',
|
|
1211
|
+
}[verdict];
|
|
1212
|
+
// Audit dot removed at user request — strip any pre-existing one so the
|
|
1213
|
+
// post-audit live update doesn't leave a stale dot from an old render.
|
|
1214
|
+
const header = card.querySelector('.skill-card-header');
|
|
1215
|
+
if (header) {
|
|
1216
|
+
header.querySelectorAll('.skill-audit-dot').forEach(n => n.remove());
|
|
1217
|
+
}
|
|
1218
|
+
const newConf = { pass: 95, needs_work: 60, fail: 40 }[verdict];
|
|
1219
|
+
const statsEl = card.querySelector('.skill-stats');
|
|
1220
|
+
if (statsEl && newConf != null) {
|
|
1221
|
+
const confEl = statsEl.querySelector('.skill-conf');
|
|
1222
|
+
if (confEl) { confEl.textContent = newConf + '%'; confEl.style.color = _confColor(newConf); }
|
|
1223
|
+
}
|
|
1224
|
+
// Fold the verdict into the status (draft / published) pill — colour the
|
|
1225
|
+
// pill itself and append a tiny check/warn/cross glyph so the audit result
|
|
1226
|
+
// lives next to the label instead of dangling in the stats row.
|
|
1227
|
+
const pill = card.querySelector('.skill-status-pill');
|
|
1228
|
+
if (pill) {
|
|
1229
|
+
// Inline glyphs for the per-verdict pill — appear next to the "checked"
|
|
1230
|
+
// label so the verdict reads as a real badge.
|
|
1231
|
+
const ICON = {
|
|
1232
|
+
pass: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><polyline points="20 6 9 17 4 12"/></svg>',
|
|
1233
|
+
needs_work: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="17" x2="12" y2="17"/></svg>',
|
|
1234
|
+
inconclusive: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="17" x2="12" y2="17"/></svg>',
|
|
1235
|
+
fail: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
|
|
1236
|
+
}[verdict];
|
|
1237
|
+
// Wash the pill's bg + tint the text so a glance at the badge tells you
|
|
1238
|
+
// pass/needs-work/fail without expanding the card.
|
|
1239
|
+
const tint = {
|
|
1240
|
+
pass: { bg: 'color-mix(in srgb, var(--color-success, #4ade80) 30%, transparent)', fg: 'var(--color-success, #4ade80)' },
|
|
1241
|
+
needs_work: { bg: 'color-mix(in srgb, var(--color-warning, #f0ad4e) 30%, transparent)', fg: 'var(--color-warning, #f0ad4e)' },
|
|
1242
|
+
inconclusive: { bg: 'color-mix(in srgb, var(--color-warning, #f0ad4e) 30%, transparent)', fg: 'var(--color-warning, #f0ad4e)' },
|
|
1243
|
+
fail: { bg: 'color-mix(in srgb, var(--color-danger, #e06c75) 30%, transparent)', fg: 'var(--color-danger, #e06c75)' },
|
|
1244
|
+
}[verdict];
|
|
1245
|
+
// The status pill (draft / published) keeps its own colours now — the
|
|
1246
|
+
// verdict lives in a separate "checked" pill that's inserted next to it.
|
|
1247
|
+
// Remove any prior audit glyph (was previously inserted inside the pill;
|
|
1248
|
+
// now scrub both the in-pill and sibling locations on every refresh).
|
|
1249
|
+
pill.querySelectorAll('.skill-pill-verdict').forEach(n => n.remove());
|
|
1250
|
+
if (pill.parentElement) {
|
|
1251
|
+
pill.parentElement.querySelectorAll(':scope > .skill-pill-verdict').forEach(n => n.remove());
|
|
1252
|
+
}
|
|
1253
|
+
if (ICON) {
|
|
1254
|
+
// Full "checked" pill badge — sits LEFT of the draft/published pill,
|
|
1255
|
+
// styled like the other memory-cat-badges so it reads as a real chip.
|
|
1256
|
+
const span = document.createElement('span');
|
|
1257
|
+
span.className = 'memory-cat-badge skill-pill-verdict';
|
|
1258
|
+
span.title = 'Audited: ' + verdict.replace(/_/g, ' ');
|
|
1259
|
+
span.innerHTML = ICON + '<span>checked</span>';
|
|
1260
|
+
if (tint) {
|
|
1261
|
+
span.style.background = tint.bg;
|
|
1262
|
+
span.style.color = tint.fg;
|
|
1263
|
+
}
|
|
1264
|
+
if (pill.parentElement) {
|
|
1265
|
+
pill.parentElement.insertBefore(span, pill);
|
|
1266
|
+
} else {
|
|
1267
|
+
pill.insertAdjacentElement('beforebegin', span);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
// Old free-floating .skill-verified check (next to confidence %) is no
|
|
1272
|
+
// longer added — the pill carries the verdict glyph now. Remove a stale
|
|
1273
|
+
// one in case the card was rendered by an earlier build.
|
|
1274
|
+
card.querySelectorAll('.skill-verified').forEach(n => n.remove());
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function _renderTestVerdict(el, v, card, name) {
|
|
1278
|
+
if (!el) return;
|
|
1279
|
+
const verdict = (v && v.verdict) || 'unknown';
|
|
1280
|
+
const cls = { pass: 'ok', needs_work: 'warn', fail: 'bad', inconclusive: 'unknown' }[verdict] || 'unknown';
|
|
1281
|
+
const label = { pass: 'PASS', needs_work: 'NEEDS WORK', fail: 'FAIL', inconclusive: 'INCONCLUSIVE', unknown: 'UNCLEAR' }[verdict] || 'UNCLEAR';
|
|
1282
|
+
const conf = v && typeof v.confidence === 'number' ? Math.round(v.confidence * 100) + '%' : '';
|
|
1283
|
+
const issues = Array.isArray(v && v.issues) ? v.issues : [];
|
|
1284
|
+
// Reflect the skill's current state: if it's already published, the button
|
|
1285
|
+
// confirms "Approved" (click to unpublish) rather than offering to approve.
|
|
1286
|
+
const isPub = card && card.dataset && card.dataset.skillStatus === 'published';
|
|
1287
|
+
const approveLabel = isPub ? 'Approved' : 'Approve';
|
|
1288
|
+
const approveCls = 'skill-eval-approve' + (isPub ? ' is-approved' : (verdict === 'pass' ? ' suggested' : ''));
|
|
1289
|
+
const approveTitle = isPub ? 'Already approved — click to unpublish' : 'Publish — appears in the skills index';
|
|
1290
|
+
el.innerHTML =
|
|
1291
|
+
'<div class="skill-eval-head"><span class="skill-eval-badge skill-eval-' + cls + '">' + label + (conf ? ' · ' + conf : '') + '</span>' +
|
|
1292
|
+
'<span class="skill-eval-summary">' + esc((v && v.summary) || '') + '</span></div>' +
|
|
1293
|
+
(issues.length ? '<ul class="skill-eval-issues">' + issues.map(i => '<li>' + esc(i) + '</li>').join('') + '</ul>' : '') +
|
|
1294
|
+
'<div class="doclib-card-expanded-actions skill-eval-actions-wrap">' +
|
|
1295
|
+
'<button class="doclib-card-text-btn doclib-card-action-btn ' + approveCls + '" data-act="approve" title="' + approveTitle + '">' + approveLabel + '</button>' +
|
|
1296
|
+
'<div class="doclib-action-group"><div class="doclib-action-btn-row">' +
|
|
1297
|
+
'<button class="doclib-card-text-btn doclib-card-action-btn" data-act="retry" title="Run the test again">Retry</button>' +
|
|
1298
|
+
'<button class="doclib-card-text-btn doclib-card-action-btn" data-act="copy" title="Copy the run output + verdict">Copy</button>' +
|
|
1299
|
+
'<button class="doclib-card-text-btn doclib-card-action-btn" data-act="edit">Edit</button>' +
|
|
1300
|
+
'<button class="doclib-card-text-btn doclib-card-action-btn doclib-card-text-btn-danger" data-act="del"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>Delete</button>' +
|
|
1301
|
+
'</div></div>' +
|
|
1302
|
+
'</div>';
|
|
1303
|
+
_applyVerdictToHeader(card, verdict);
|
|
1304
|
+
el.querySelector('[data-act="approve"]')?.addEventListener('click', async (e) => {
|
|
1305
|
+
e.stopPropagation();
|
|
1306
|
+
const nowPub = card.dataset.skillStatus === 'published';
|
|
1307
|
+
await _setSkillStatus(name, nowPub ? 'draft' : 'published');
|
|
1308
|
+
// _setSkillStatus reloads the list, but if this card survives, relabel it.
|
|
1309
|
+
card.dataset.skillStatus = nowPub ? 'draft' : 'published';
|
|
1310
|
+
const btn = el.querySelector('[data-act="approve"]');
|
|
1311
|
+
if (btn) {
|
|
1312
|
+
const pub = card.dataset.skillStatus === 'published';
|
|
1313
|
+
btn.textContent = pub ? 'Approved' : 'Approve';
|
|
1314
|
+
btn.title = pub ? 'Already approved — click to unpublish' : 'Publish — appears in the skills index';
|
|
1315
|
+
btn.classList.toggle('is-approved', pub);
|
|
1316
|
+
btn.classList.toggle('suggested', !pub && verdict === 'pass');
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
el.querySelector('[data-act="del"]')?.addEventListener('click', (e) => { e.stopPropagation(); _deleteSkill(name, card); });
|
|
1320
|
+
el.querySelector('[data-act="edit"]')?.addEventListener('click', (e) => { e.stopPropagation(); _toggleSkillEdit(card, name); });
|
|
1321
|
+
el.querySelector('[data-act="retry"]')?.addEventListener('click', (e) => { e.stopPropagation(); _testSkill(card, name, true); });
|
|
1322
|
+
el.querySelector('[data-act="copy"]')?.addEventListener('click', (e) => {
|
|
1323
|
+
e.stopPropagation();
|
|
1324
|
+
const logEl = card.querySelector('.skill-test-log');
|
|
1325
|
+
const issuesTxt = issues.length ? '\nIssues:\n- ' + issues.join('\n- ') : '';
|
|
1326
|
+
const text = (logEl ? logEl.innerText.trim() + '\n\n' : '') +
|
|
1327
|
+
'=== Eval: ' + label + (conf ? ' (' + conf + ')' : '') + ' ===\n' + ((v && v.summary) || '') + issuesTxt;
|
|
1328
|
+
// Shared helper falls back to execCommand on plain HTTP (navigator.clipboard
|
|
1329
|
+
// is unavailable in non-secure contexts, which is why the raw call failed).
|
|
1330
|
+
uiModule.copyToClipboard(text);
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// ---- Audit all skills (autonomous: test → fix → retry → teacher → flag) ----
|
|
1335
|
+
|
|
1336
|
+
let _auditPoll = null;
|
|
1337
|
+
let _auditSeenResults = 0;
|
|
1338
|
+
|
|
1339
|
+
function _confirmAuditSkills(label) {
|
|
1340
|
+
return new Promise(resolve => {
|
|
1341
|
+
let overlay = document.getElementById('skills-audit-confirm-overlay');
|
|
1342
|
+
if (!overlay) {
|
|
1343
|
+
overlay = document.createElement('div');
|
|
1344
|
+
overlay.id = 'skills-audit-confirm-overlay';
|
|
1345
|
+
overlay.className = 'modal';
|
|
1346
|
+
overlay.innerHTML =
|
|
1347
|
+
'<div class="modal-content styled-confirm-box">' +
|
|
1348
|
+
'<div class="modal-header"><h4>Audit Skills</h4></div>' +
|
|
1349
|
+
'<div class="modal-body">' +
|
|
1350
|
+
'<p id="skills-audit-confirm-msg"></p>' +
|
|
1351
|
+
'<label class="memory-bulk-check-all" style="margin-top:10px;display:inline-flex;align-items:center;gap:7px;">' +
|
|
1352
|
+
'<input type="checkbox" id="skills-audit-skip-audited" checked />' +
|
|
1353
|
+
'<span>Skip already audited</span>' +
|
|
1354
|
+
'</label>' +
|
|
1355
|
+
'</div>' +
|
|
1356
|
+
'<div class="modal-footer">' +
|
|
1357
|
+
'<button id="skills-audit-confirm-cancel" class="confirm-btn confirm-btn-secondary">Cancel</button>' +
|
|
1358
|
+
'<button id="skills-audit-confirm-ok" class="confirm-btn confirm-btn-primary">Audit</button>' +
|
|
1359
|
+
'</div>' +
|
|
1360
|
+
'</div>';
|
|
1361
|
+
document.body.appendChild(overlay);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const msg = overlay.querySelector('#skills-audit-confirm-msg');
|
|
1365
|
+
const skip = overlay.querySelector('#skills-audit-skip-audited');
|
|
1366
|
+
const okBtn = overlay.querySelector('#skills-audit-confirm-ok');
|
|
1367
|
+
const cancelBtn = overlay.querySelector('#skills-audit-confirm-cancel');
|
|
1368
|
+
msg.textContent = `Audit ${label}? Each is tested from top to bottom, then published or moved to draft using your auto-approve confidence threshold.`;
|
|
1369
|
+
skip.checked = true;
|
|
1370
|
+
overlay.classList.remove('hidden');
|
|
1371
|
+
overlay.style.display = '';
|
|
1372
|
+
|
|
1373
|
+
function cleanup(result) {
|
|
1374
|
+
overlay.classList.add('hidden');
|
|
1375
|
+
overlay.style.display = 'none';
|
|
1376
|
+
okBtn.removeEventListener('click', onOk);
|
|
1377
|
+
cancelBtn.removeEventListener('click', onCancel);
|
|
1378
|
+
overlay.removeEventListener('click', onBackdrop);
|
|
1379
|
+
document.removeEventListener('keydown', onKey);
|
|
1380
|
+
resolve(result);
|
|
1381
|
+
}
|
|
1382
|
+
function onOk() { cleanup({ ok: true, skipAudited: !!skip.checked }); }
|
|
1383
|
+
function onCancel() { cleanup({ ok: false, skipAudited: false }); }
|
|
1384
|
+
function onBackdrop(e) { if (e.target === overlay) onCancel(); }
|
|
1385
|
+
function onKey(e) {
|
|
1386
|
+
if (e.key === 'Escape') {
|
|
1387
|
+
e.preventDefault();
|
|
1388
|
+
e.stopPropagation();
|
|
1389
|
+
cleanup({ ok: false, skipAudited: false });
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
okBtn.addEventListener('click', onOk);
|
|
1394
|
+
cancelBtn.addEventListener('click', onCancel);
|
|
1395
|
+
overlay.addEventListener('click', onBackdrop);
|
|
1396
|
+
document.addEventListener('keydown', onKey);
|
|
1397
|
+
okBtn.focus();
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async function _auditAllSkills(opts = {}) {
|
|
1402
|
+
const panel = document.getElementById('skills-audit-panel');
|
|
1403
|
+
if (!panel) return;
|
|
1404
|
+
// If a run is already going, just (re)attach to it.
|
|
1405
|
+
let st = await _fetchAuditStatus();
|
|
1406
|
+
if (st.status !== 'running') {
|
|
1407
|
+
const explicitNames = Array.isArray(opts.names) ? opts.names.filter(Boolean) : null;
|
|
1408
|
+
const visibleNames = _getFilteredSkills()
|
|
1409
|
+
.map(sk => sk.name || sk.id)
|
|
1410
|
+
.filter(Boolean);
|
|
1411
|
+
const names = explicitNames || visibleNames;
|
|
1412
|
+
const label = explicitNames
|
|
1413
|
+
? `${names.length} selected ${names.length === 1 ? 'skill' : 'skills'}`
|
|
1414
|
+
: `${names.length} visible ${names.length === 1 ? 'skill' : 'skills'}`;
|
|
1415
|
+
if (!names.length) {
|
|
1416
|
+
uiModule.showToast(explicitNames ? 'No selected skills to audit' : 'No visible skills to audit');
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const confirmed = await _confirmAuditSkills(label);
|
|
1420
|
+
if (!confirmed.ok) return;
|
|
1421
|
+
try {
|
|
1422
|
+
const r = await fetch(`${API}/api/skills/audit-all`, {
|
|
1423
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1424
|
+
body: JSON.stringify({ scope: explicitNames ? 'selected' : 'all', names, skip_audited: confirmed.skipAudited }),
|
|
1425
|
+
});
|
|
1426
|
+
if (!r.ok) { uiModule.showError('Audit failed to start (HTTP ' + r.status + ')'); return; }
|
|
1427
|
+
st = await _fetchAuditStatus();
|
|
1428
|
+
} catch (e) { uiModule.showError('Audit failed: ' + (e.message || e)); return; }
|
|
1429
|
+
_auditSeenResults = 0;
|
|
1430
|
+
}
|
|
1431
|
+
panel.classList.remove('hidden');
|
|
1432
|
+
_auditSeenResults = Math.min(_auditSeenResults, (st.results || []).length);
|
|
1433
|
+
_renderAuditPanel(panel, st);
|
|
1434
|
+
_applyAuditResults(st);
|
|
1435
|
+
_highlightAuditCard(st.status === 'running' ? st.current : null);
|
|
1436
|
+
if (_auditPoll) clearInterval(_auditPoll);
|
|
1437
|
+
if (st.status === 'running') {
|
|
1438
|
+
_auditPoll = setInterval(async () => {
|
|
1439
|
+
const s = await _fetchAuditStatus();
|
|
1440
|
+
_renderAuditPanel(panel, s);
|
|
1441
|
+
_applyAuditResults(s);
|
|
1442
|
+
_highlightAuditCard(s.status === 'running' ? s.current : null);
|
|
1443
|
+
if (s.status !== 'running') {
|
|
1444
|
+
clearInterval(_auditPoll); _auditPoll = null;
|
|
1445
|
+
_highlightAuditCard(null);
|
|
1446
|
+
loadSkills(); // refresh statuses (some may have been demoted/edited)
|
|
1447
|
+
}
|
|
1448
|
+
}, 1500);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function _findSkillCard(name) {
|
|
1453
|
+
if (!name) return null;
|
|
1454
|
+
return [...document.querySelectorAll('.skill-card[data-skill-name]')]
|
|
1455
|
+
.find(c => c.dataset.skillName === name) || null;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function _mergeSkillState(state) {
|
|
1459
|
+
if (!state || !state.name) return;
|
|
1460
|
+
const idx = skills.findIndex(s => (s.name || s.id) === state.name);
|
|
1461
|
+
if (idx >= 0) skills[idx] = { ...skills[idx], ...state };
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function _applySkillStateToHeader(card, state, fallbackVerdict) {
|
|
1465
|
+
if (!card) return;
|
|
1466
|
+
const verdict = state?.audit_verdict || fallbackVerdict;
|
|
1467
|
+
if (verdict) _applyVerdictToHeader(card, verdict);
|
|
1468
|
+
if (state && typeof state.confidence === 'number') {
|
|
1469
|
+
const conf = Math.round(state.confidence * 100);
|
|
1470
|
+
const confEl = card.querySelector('.skill-conf');
|
|
1471
|
+
if (confEl) { confEl.textContent = conf + '%'; confEl.style.color = _confColor(conf); }
|
|
1472
|
+
}
|
|
1473
|
+
if (state?.status) {
|
|
1474
|
+
card.dataset.skillStatus = state.status;
|
|
1475
|
+
const oldPill = card.querySelector('.skill-status-pill');
|
|
1476
|
+
if (oldPill) {
|
|
1477
|
+
const wrap = document.createElement('span');
|
|
1478
|
+
wrap.innerHTML = _statusPill(state);
|
|
1479
|
+
const next = wrap.firstElementChild;
|
|
1480
|
+
if (next) oldPill.replaceWith(next);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const right = card.querySelector('.skill-card-right');
|
|
1484
|
+
if (right && state) {
|
|
1485
|
+
right.querySelectorAll('.skill-model-pill, .skill-necessity-pill').forEach(n => n.remove());
|
|
1486
|
+
const stats = right.querySelector('.skill-stats');
|
|
1487
|
+
const wrap = document.createElement('span');
|
|
1488
|
+
wrap.innerHTML = _auditModelPills(state) + _necessityPill(state);
|
|
1489
|
+
[...wrap.children].forEach(p => {
|
|
1490
|
+
if (stats) right.insertBefore(p, stats);
|
|
1491
|
+
else right.appendChild(p);
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function _applyAuditResults(st) {
|
|
1497
|
+
const results = st && Array.isArray(st.results) ? st.results : [];
|
|
1498
|
+
if (!results.length) return;
|
|
1499
|
+
for (const r of results.slice(_auditSeenResults)) {
|
|
1500
|
+
const name = r && r.skill;
|
|
1501
|
+
if (!name) continue;
|
|
1502
|
+
const state = r.skill_state || null;
|
|
1503
|
+
_mergeSkillState(state);
|
|
1504
|
+
const verdict = state?.audit_verdict || r.verdict?.verdict || (r.result === 'flagged' ? 'fail' : null);
|
|
1505
|
+
_applySkillStateToHeader(_findSkillCard(name), state, verdict);
|
|
1506
|
+
}
|
|
1507
|
+
_auditSeenResults = results.length;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Make the card currently being audited glow, so it's obvious which one the
|
|
1511
|
+
// "Audit now" run is processing. Pass null to clear all highlights.
|
|
1512
|
+
function _highlightAuditCard(name) {
|
|
1513
|
+
document.querySelectorAll('.skill-card.skill-audit-active')
|
|
1514
|
+
.forEach(c => { c.classList.remove('skill-audit-active'); _setCardRunning(c, false); });
|
|
1515
|
+
if (!name) return;
|
|
1516
|
+
const card = _findSkillCard(name);
|
|
1517
|
+
if (card) {
|
|
1518
|
+
card.classList.add('skill-audit-active');
|
|
1519
|
+
_setCardRunning(card, true);
|
|
1520
|
+
card.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
async function _fetchAuditStatus() {
|
|
1525
|
+
try {
|
|
1526
|
+
const r = await fetch(`${API}/api/skills/audit-all/status`);
|
|
1527
|
+
return r.ok ? await r.json() : { status: 'none' };
|
|
1528
|
+
} catch { return { status: 'none' }; }
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function _renderAuditPanel(panel, st) {
|
|
1532
|
+
if (st.status === 'none') { panel.classList.add('hidden'); panel.innerHTML = ''; return; }
|
|
1533
|
+
const done = st.done || 0, total = st.total || 0;
|
|
1534
|
+
const pct = total ? Math.round((done / total) * 100) : 0;
|
|
1535
|
+
const counts = {};
|
|
1536
|
+
for (const r of (st.results || [])) counts[r.result] = (counts[r.result] || 0) + 1;
|
|
1537
|
+
const summary = Object.entries(counts).map(([k, v]) => v + ' ' + k.replace(/_/g, ' ')).join(' · ');
|
|
1538
|
+
const running = st.status === 'running';
|
|
1539
|
+
const cancelled = st.status === 'cancelled';
|
|
1540
|
+
const head = running
|
|
1541
|
+
? `Auditing ${done}/${total}${st.current ? ' — ' + esc(st.current) : ''}`
|
|
1542
|
+
: cancelled
|
|
1543
|
+
? `Audit cancelled — ${done}/${total}`
|
|
1544
|
+
: `Audit complete — ${total} skill${total === 1 ? '' : 's'}`;
|
|
1545
|
+
panel.innerHTML =
|
|
1546
|
+
'<div class="skills-audit-head">' +
|
|
1547
|
+
'<span class="skills-audit-title-wrap" style="display:inline-flex;align-items:center;gap:8px;">' +
|
|
1548
|
+
'<span class="skills-audit-title">' + head + '</span>' +
|
|
1549
|
+
'</span>' +
|
|
1550
|
+
(running
|
|
1551
|
+
? '<button class="memory-toolbar-btn" data-act="audit-cancel">Cancel</button>'
|
|
1552
|
+
: '<button class="memory-toolbar-btn" data-act="audit-close">Close</button>') +
|
|
1553
|
+
'</div>' +
|
|
1554
|
+
'<div class="skills-audit-bar"><div class="skills-audit-fill" style="width:' + pct + '%"></div></div>' +
|
|
1555
|
+
(summary ? '<div class="skills-audit-summary">' + esc(summary) + (st.teacher ? ' · teacher: ' + esc(st.teacher) : '') + '</div>' : '') +
|
|
1556
|
+
'<div class="skills-audit-log">' + (st.log || []).slice(-40).map(l => '<div>' + esc(l) + '</div>').join('') + '</div>';
|
|
1557
|
+
// Whirlpool sits next to the title while the audit is actually running.
|
|
1558
|
+
if (running) {
|
|
1559
|
+
const titleWrap = panel.querySelector('.skills-audit-title-wrap');
|
|
1560
|
+
if (titleWrap) {
|
|
1561
|
+
const wp = spinnerModule.createWhirlpool(12);
|
|
1562
|
+
wp.element.style.cssText = 'display:inline-flex;width:12px;height:12px;margin:0;vertical-align:middle;flex-shrink:0;';
|
|
1563
|
+
titleWrap.appendChild(wp.element);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
const cancel = panel.querySelector('[data-act="audit-cancel"]');
|
|
1567
|
+
if (cancel) cancel.addEventListener('click', async (e) => {
|
|
1568
|
+
e.stopPropagation();
|
|
1569
|
+
cancel.disabled = true;
|
|
1570
|
+
cancel.textContent = 'Cancelling...';
|
|
1571
|
+
try {
|
|
1572
|
+
await fetch(`${API}/api/skills/audit-all/cancel`, { method: 'POST', credentials: 'same-origin' });
|
|
1573
|
+
const s = await _fetchAuditStatus();
|
|
1574
|
+
_renderAuditPanel(panel, { ...s, status: s.status === 'none' ? 'cancelled' : s.status });
|
|
1575
|
+
_highlightAuditCard(null);
|
|
1576
|
+
} catch {
|
|
1577
|
+
cancel.disabled = false;
|
|
1578
|
+
cancel.textContent = 'Cancel';
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
const close = panel.querySelector('[data-act="audit-close"]');
|
|
1582
|
+
if (close) close.addEventListener('click', () => { panel.classList.add('hidden'); panel.innerHTML = ''; });
|
|
1583
|
+
const logEl = panel.querySelector('.skills-audit-log');
|
|
1584
|
+
if (logEl) logEl.scrollTop = logEl.scrollHeight;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// ---- Select mode / bulk actions ----
|
|
1588
|
+
|
|
1589
|
+
function _enterSelectMode() {
|
|
1590
|
+
_selectMode = true;
|
|
1591
|
+
_selectedNames.clear();
|
|
1592
|
+
const bar = document.getElementById('skills-bulk-bar');
|
|
1593
|
+
const btn = document.getElementById('skills-select-btn');
|
|
1594
|
+
if (bar) bar.classList.remove('hidden');
|
|
1595
|
+
if (btn) { btn.classList.add('active'); btn.textContent = 'Cancel'; }
|
|
1596
|
+
_updateBulkBar();
|
|
1597
|
+
renderSkillsList();
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function _exitSelectMode() {
|
|
1601
|
+
_selectMode = false;
|
|
1602
|
+
_selectedNames.clear();
|
|
1603
|
+
const bar = document.getElementById('skills-bulk-bar');
|
|
1604
|
+
const btn = document.getElementById('skills-select-btn');
|
|
1605
|
+
const all = document.getElementById('skills-select-all');
|
|
1606
|
+
if (bar) bar.classList.add('hidden');
|
|
1607
|
+
if (btn) { btn.classList.remove('active'); btn.textContent = 'Select'; }
|
|
1608
|
+
if (all) all.checked = false;
|
|
1609
|
+
renderSkillsList();
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function _updateBulkBar() {
|
|
1613
|
+
const countEl = document.getElementById('skills-selected-count');
|
|
1614
|
+
const delBtn = document.getElementById('skills-bulk-delete');
|
|
1615
|
+
const delNonPassingBtn = document.getElementById('skills-bulk-delete-nonpassing');
|
|
1616
|
+
const pubBtn = document.getElementById('skills-bulk-publish');
|
|
1617
|
+
const auditBtn = document.getElementById('skills-bulk-audit');
|
|
1618
|
+
if (countEl) countEl.textContent = `${_selectedNames.size} Selected`;
|
|
1619
|
+
if (delBtn) delBtn.disabled = _selectedNames.size === 0;
|
|
1620
|
+
if (auditBtn) auditBtn.disabled = _selectedNames.size === 0;
|
|
1621
|
+
if (delNonPassingBtn) {
|
|
1622
|
+
const count = _selectedNonPassingSkills().length;
|
|
1623
|
+
delNonPassingBtn.disabled = count === 0;
|
|
1624
|
+
delNonPassingBtn.title = count
|
|
1625
|
+
? `Delete ${count} selected non-passing ${count === 1 ? 'skill' : 'skills'}`
|
|
1626
|
+
: 'No selected non-passing skills';
|
|
1627
|
+
}
|
|
1628
|
+
// Approve is only meaningful when at least one selected skill is still a draft.
|
|
1629
|
+
const anyDraft = [..._selectedNames].some(n => {
|
|
1630
|
+
const sk = skills.find(s => (s.name || s.id) === n);
|
|
1631
|
+
return sk && (sk.status || 'draft') !== 'published';
|
|
1632
|
+
});
|
|
1633
|
+
if (pubBtn) pubBtn.disabled = !anyDraft;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function _toggleSelectAll() {
|
|
1637
|
+
const all = document.getElementById('skills-select-all');
|
|
1638
|
+
if (!all) return;
|
|
1639
|
+
const visible = _getFilteredSkills().map(s => s.name || s.id);
|
|
1640
|
+
if (all.checked) visible.forEach(n => _selectedNames.add(n));
|
|
1641
|
+
else visible.forEach(n => _selectedNames.delete(n));
|
|
1642
|
+
_updateBulkBar();
|
|
1643
|
+
renderSkillsList();
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
async function _bulkDelete() {
|
|
1647
|
+
if (!_selectedNames.size) return;
|
|
1648
|
+
const n = _selectedNames.size;
|
|
1649
|
+
const ok = await uiModule.styledConfirm(
|
|
1650
|
+
`Delete ${n} ${n === 1 ? 'skill' : 'skills'}? This removes their SKILL.md files.`,
|
|
1651
|
+
{ confirmText: 'Delete', danger: true }
|
|
1652
|
+
);
|
|
1653
|
+
if (!ok) return;
|
|
1654
|
+
let deleted = 0;
|
|
1655
|
+
const deletedNames = [];
|
|
1656
|
+
for (const name of _selectedNames) {
|
|
1657
|
+
try {
|
|
1658
|
+
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
1659
|
+
if (res.ok) {
|
|
1660
|
+
deleted++;
|
|
1661
|
+
deletedNames.push(name);
|
|
1662
|
+
}
|
|
1663
|
+
} catch {}
|
|
1664
|
+
}
|
|
1665
|
+
for (const name of deletedNames) {
|
|
1666
|
+
const card = document.querySelector(`.skill-card[data-skill-name="${CSS.escape(name)}"]`);
|
|
1667
|
+
if (card) card.classList.add('doclib-card-deleting');
|
|
1668
|
+
}
|
|
1669
|
+
if (deletedNames.length) await new Promise(resolve => setTimeout(resolve, 320));
|
|
1670
|
+
_exitSelectMode();
|
|
1671
|
+
await loadSkills();
|
|
1672
|
+
uiModule.showToast(`Deleted ${deleted}`);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
async function _loadSkillApprovalThreshold() {
|
|
1676
|
+
try {
|
|
1677
|
+
const res = await fetch(`${API}/api/prefs`, { credentials: 'same-origin' });
|
|
1678
|
+
if (!res.ok) return;
|
|
1679
|
+
const prefs = await res.json();
|
|
1680
|
+
const raw = prefs.skill_min_confidence ?? prefs.skill_autosave_min_confidence;
|
|
1681
|
+
const val = Number(raw);
|
|
1682
|
+
if (Number.isFinite(val)) _skillApprovalThreshold = Math.max(0, Math.min(1, val));
|
|
1683
|
+
} catch {}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function _selectedNonPassingSkills() {
|
|
1687
|
+
const selected = new Set(_selectedNames);
|
|
1688
|
+
return skills.filter(sk => {
|
|
1689
|
+
const name = sk.name || sk.id;
|
|
1690
|
+
if (!selected.has(name)) return false;
|
|
1691
|
+
const conf = Number(sk.confidence || 0);
|
|
1692
|
+
const necessity = _necessityKind(sk);
|
|
1693
|
+
if (necessity === 'duplicate' || necessity === 'trivial' || necessity === 'irrelevant') return true;
|
|
1694
|
+
if ((sk.audit_verdict || '') !== 'pass') return true;
|
|
1695
|
+
return conf < _skillApprovalThreshold;
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
async function _bulkDeleteNonPassing() {
|
|
1700
|
+
const targets = _selectedNonPassingSkills();
|
|
1701
|
+
if (!targets.length) {
|
|
1702
|
+
uiModule.showToast('No selected non-passing skills');
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const thresholdPct = Math.round(_skillApprovalThreshold * 100);
|
|
1706
|
+
const names = targets.map(sk => sk.name || sk.id).filter(Boolean);
|
|
1707
|
+
const ok = await uiModule.styledConfirm(
|
|
1708
|
+
`Delete ${names.length} selected non-passing ${names.length === 1 ? 'skill' : 'skills'}? This removes duplicates, generic/irrelevant skills, failed audits, and anything below ${thresholdPct}%.`,
|
|
1709
|
+
{ confirmText: 'Delete non passing', danger: true }
|
|
1710
|
+
);
|
|
1711
|
+
if (!ok) return;
|
|
1712
|
+
let deleted = 0;
|
|
1713
|
+
const deletedNames = [];
|
|
1714
|
+
for (const name of names) {
|
|
1715
|
+
try {
|
|
1716
|
+
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
1717
|
+
if (res.ok) {
|
|
1718
|
+
deleted++;
|
|
1719
|
+
deletedNames.push(name);
|
|
1720
|
+
_mdCache.delete(name);
|
|
1721
|
+
}
|
|
1722
|
+
} catch {}
|
|
1723
|
+
}
|
|
1724
|
+
for (const name of deletedNames) {
|
|
1725
|
+
const card = document.querySelector(`.skill-card[data-skill-name="${CSS.escape(name)}"]`);
|
|
1726
|
+
if (card) card.classList.add('doclib-card-deleting');
|
|
1727
|
+
}
|
|
1728
|
+
if (deletedNames.length) await new Promise(resolve => setTimeout(resolve, 320));
|
|
1729
|
+
_exitSelectMode();
|
|
1730
|
+
await loadSkills();
|
|
1731
|
+
uiModule.showToast(`Deleted ${deleted} non-passing`);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
async function _bulkApprove() {
|
|
1735
|
+
if (!_selectedNames.size) return;
|
|
1736
|
+
let published = 0;
|
|
1737
|
+
for (const name of _selectedNames) {
|
|
1738
|
+
const sk = skills.find(s => (s.name || s.id) === name);
|
|
1739
|
+
if (sk && sk.status === 'published') continue;
|
|
1740
|
+
try {
|
|
1741
|
+
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}`, {
|
|
1742
|
+
method: 'PUT',
|
|
1743
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1744
|
+
body: JSON.stringify({ status: 'published' }),
|
|
1745
|
+
});
|
|
1746
|
+
if (res.ok) published++;
|
|
1747
|
+
} catch {}
|
|
1748
|
+
}
|
|
1749
|
+
_exitSelectMode();
|
|
1750
|
+
await loadSkills();
|
|
1751
|
+
uiModule.showToast(`Published ${published}`);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
async function _bulkAudit() {
|
|
1755
|
+
if (!_selectedNames.size) return;
|
|
1756
|
+
const selected = new Set(_selectedNames);
|
|
1757
|
+
const ordered = _getFilteredSkills()
|
|
1758
|
+
.map(sk => sk.name || sk.id)
|
|
1759
|
+
.filter(n => selected.has(n));
|
|
1760
|
+
_exitSelectMode();
|
|
1761
|
+
await _auditAllSkills({ names: ordered });
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
async function _showSkillSource(name) {
|
|
1765
|
+
let md = '';
|
|
1766
|
+
try {
|
|
1767
|
+
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`);
|
|
1768
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1769
|
+
const data = await res.json();
|
|
1770
|
+
md = data.markdown || '';
|
|
1771
|
+
} catch (e) {
|
|
1772
|
+
uiModule.showError('Failed to load SKILL.md');
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Lightweight modal — reuses the .modal CSS the rest of the app uses.
|
|
1777
|
+
const wrap = document.createElement('div');
|
|
1778
|
+
wrap.className = 'modal';
|
|
1779
|
+
wrap.style.display = 'block';
|
|
1780
|
+
wrap.innerHTML = `
|
|
1781
|
+
<div class="modal-content" style="max-width:760px;max-height:85vh;display:flex;flex-direction:column">
|
|
1782
|
+
<div class="modal-header">
|
|
1783
|
+
<h4>SKILL.md — <code>${esc(name)}</code></h4>
|
|
1784
|
+
<span style="flex:1"></span>
|
|
1785
|
+
<button class="memory-toolbar-btn" id="skill-save-btn">Save</button>
|
|
1786
|
+
<button class="close-btn" id="skill-md-close">✖</button>
|
|
1787
|
+
</div>
|
|
1788
|
+
<div class="modal-body" style="display:flex;flex-direction:column;gap:8px">
|
|
1789
|
+
<textarea id="skill-md-textarea" spellcheck="false" style="flex:1;min-height:50vh;width:100%;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--fg);box-sizing:border-box"></textarea>
|
|
1790
|
+
<p class="memory-desc" style="margin:0">Edit the frontmatter and body directly. Save replaces the file via PUT /api/skills/{name}.</p>
|
|
1791
|
+
</div>
|
|
1792
|
+
</div>
|
|
1793
|
+
`;
|
|
1794
|
+
document.body.appendChild(wrap);
|
|
1795
|
+
const ta = wrap.querySelector('#skill-md-textarea');
|
|
1796
|
+
ta.value = md;
|
|
1797
|
+
wrap.querySelector('#skill-md-close').addEventListener('click', () => wrap.remove());
|
|
1798
|
+
wrap.addEventListener('click', (e) => { if (e.target === wrap) wrap.remove(); });
|
|
1799
|
+
wrap.querySelector('#skill-save-btn').addEventListener('click', async () => {
|
|
1800
|
+
try {
|
|
1801
|
+
// We use the manage_skills-style edit by going through PUT with a
|
|
1802
|
+
// single 'content' field. The route doesn't accept that yet — use the
|
|
1803
|
+
// tool call instead. We have a /api/skills/{name} PUT for fields, but
|
|
1804
|
+
// a full SKILL.md replace is simpler via the parsed-then-PUT approach
|
|
1805
|
+
// below: parse client-side by uploading via the tool route.
|
|
1806
|
+
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`, {
|
|
1807
|
+
method: 'POST',
|
|
1808
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1809
|
+
body: JSON.stringify({ markdown: ta.value }),
|
|
1810
|
+
});
|
|
1811
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1812
|
+
uiModule.showToast('Saved');
|
|
1813
|
+
wrap.remove();
|
|
1814
|
+
await loadSkills();
|
|
1815
|
+
} catch (e) {
|
|
1816
|
+
uiModule.showError('Save failed: ' + e.message);
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
async function addSkill() {
|
|
1822
|
+
const name = document.getElementById('new-skill-name')?.value.trim()
|
|
1823
|
+
|| document.getElementById('new-skill-title')?.value.trim();
|
|
1824
|
+
const description = document.getElementById('new-skill-description')?.value.trim()
|
|
1825
|
+
|| document.getElementById('new-skill-title')?.value.trim();
|
|
1826
|
+
const whenToUse = document.getElementById('new-skill-when')?.value.trim()
|
|
1827
|
+
|| document.getElementById('new-skill-problem')?.value.trim() || '';
|
|
1828
|
+
const procedureRaw = document.getElementById('new-skill-procedure')?.value.trim()
|
|
1829
|
+
|| document.getElementById('new-skill-solution')?.value.trim() || '';
|
|
1830
|
+
const tagsRaw = document.getElementById('new-skill-tags')?.value.trim();
|
|
1831
|
+
const category = document.getElementById('new-skill-category')?.value.trim() || 'general';
|
|
1832
|
+
|
|
1833
|
+
if (!description && !name) {
|
|
1834
|
+
uiModule.showError('Description (or name) is required');
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
const procedure = procedureRaw
|
|
1838
|
+
? procedureRaw.split('\n').map(s => s.replace(/^\s*(?:[-*]|\d+[.)])\s+/, '').trim()).filter(Boolean)
|
|
1839
|
+
: [];
|
|
1840
|
+
const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
1841
|
+
|
|
1842
|
+
try {
|
|
1843
|
+
const res = await fetch(`${API}/api/skills/add`, {
|
|
1844
|
+
method: 'POST',
|
|
1845
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1846
|
+
body: JSON.stringify({
|
|
1847
|
+
name: name || undefined,
|
|
1848
|
+
description,
|
|
1849
|
+
category,
|
|
1850
|
+
when_to_use: whenToUse,
|
|
1851
|
+
procedure,
|
|
1852
|
+
tags,
|
|
1853
|
+
status: 'draft',
|
|
1854
|
+
}),
|
|
1855
|
+
});
|
|
1856
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1857
|
+
['new-skill-name', 'new-skill-title', 'new-skill-description', 'new-skill-when',
|
|
1858
|
+
'new-skill-problem', 'new-skill-procedure', 'new-skill-solution', 'new-skill-tags',
|
|
1859
|
+
'new-skill-category']
|
|
1860
|
+
.forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });
|
|
1861
|
+
await loadSkills();
|
|
1862
|
+
uiModule.showToast('Skill added (draft)');
|
|
1863
|
+
} catch (err) {
|
|
1864
|
+
uiModule.showError('Failed to add skill: ' + err.message);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1869
|
+
document.getElementById('add-skill-btn')?.addEventListener('click', addSkill);
|
|
1870
|
+
document.getElementById('skills-search')?.addEventListener('input', renderSkillsList);
|
|
1871
|
+
document.getElementById('skills-sort')?.addEventListener('change', (e) => {
|
|
1872
|
+
// Dropdown holds two optgroups: Sort (sort:<key>) and Filter (filter:<key>).
|
|
1873
|
+
// Picking a sort option leaves the filter alone, and vice-versa.
|
|
1874
|
+
const v = e.target.value || '';
|
|
1875
|
+
if (v.startsWith('sort:')) {
|
|
1876
|
+
_skillsSort = v.slice(5);
|
|
1877
|
+
} else if (v.startsWith('filter:')) {
|
|
1878
|
+
const f = v.slice(7);
|
|
1879
|
+
if (f === 'all') { _showDraftsOnly = false; _showPublishedOnly = false; _confMax = null; }
|
|
1880
|
+
else if (f === 'drafts') { _showDraftsOnly = true; _showPublishedOnly = false; _confMax = null; }
|
|
1881
|
+
else if (f === 'published') { _showPublishedOnly = true; _showDraftsOnly = false; _confMax = null; }
|
|
1882
|
+
else if (f.startsWith('conf')) { _showDraftsOnly = false; _showPublishedOnly = false; _confMax = parseInt(f.slice(4), 10) || null; }
|
|
1883
|
+
}
|
|
1884
|
+
renderSkillsList();
|
|
1885
|
+
});
|
|
1886
|
+
document.getElementById('skills-select-btn')?.addEventListener('click', () => {
|
|
1887
|
+
if (_selectMode) _exitSelectMode(); else _enterSelectMode();
|
|
1888
|
+
});
|
|
1889
|
+
document.getElementById('skills-audit-btn')?.addEventListener('click', _auditAllSkills);
|
|
1890
|
+
document.getElementById('skills-select-all')?.addEventListener('change', _toggleSelectAll);
|
|
1891
|
+
document.getElementById('skills-bulk-cancel')?.addEventListener('click', _exitSelectMode);
|
|
1892
|
+
document.getElementById('skills-bulk-audit')?.addEventListener('click', _bulkAudit);
|
|
1893
|
+
document.getElementById('skills-bulk-delete')?.addEventListener('click', _bulkDelete);
|
|
1894
|
+
document.getElementById('skills-bulk-delete-nonpassing')?.addEventListener('click', _bulkDeleteNonPassing);
|
|
1895
|
+
document.getElementById('skills-bulk-publish')?.addEventListener('click', _bulkApprove);
|
|
1896
|
+
document.getElementById('new-skill-title')?.addEventListener('keydown', (e) => {
|
|
1897
|
+
if (e.key === 'Enter') addSkill();
|
|
1898
|
+
});
|
|
1899
|
+
document.getElementById('new-skill-name')?.addEventListener('keydown', (e) => {
|
|
1900
|
+
if (e.key === 'Enter') addSkill();
|
|
1901
|
+
});
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
export default { loadSkills, openSkill };
|
|
1905
|
+
|
|
1906
|
+
// Populate the Skills badge on first load so the count is right before the
|
|
1907
|
+
// user clicks into the tab. Cheap fetch — same as the lazy path.
|
|
1908
|
+
document.addEventListener('DOMContentLoaded', () => { loadSkills(); });
|