agent-orcha 0.0.5 → 0.0.8
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/README.md +250 -1275
- package/dist/lib/agents/agent-executor.d.ts +4 -2
- package/dist/lib/agents/agent-executor.d.ts.map +1 -1
- package/dist/lib/agents/agent-executor.js +85 -53
- package/dist/lib/agents/agent-executor.js.map +1 -1
- package/dist/lib/agents/agent-loader.d.ts +3 -0
- package/dist/lib/agents/agent-loader.d.ts.map +1 -1
- package/dist/lib/agents/agent-loader.js +10 -1
- package/dist/lib/agents/agent-loader.js.map +1 -1
- package/dist/lib/agents/react-loop.d.ts.map +1 -1
- package/dist/lib/agents/react-loop.js +207 -142
- package/dist/lib/agents/react-loop.js.map +1 -1
- package/dist/lib/agents/types.d.ts +181 -18
- package/dist/lib/agents/types.d.ts.map +1 -1
- package/dist/lib/agents/types.js +18 -2
- package/dist/lib/agents/types.js.map +1 -1
- package/dist/lib/functions/function-loader.d.ts +2 -0
- package/dist/lib/functions/function-loader.d.ts.map +1 -1
- package/dist/lib/functions/function-loader.js +10 -0
- package/dist/lib/functions/function-loader.js.map +1 -1
- package/dist/lib/functions/simple-function-wrapper.js +3 -3
- package/dist/lib/functions/simple-function-wrapper.js.map +1 -1
- package/dist/lib/integrations/email.d.ts +38 -0
- package/dist/lib/integrations/email.d.ts.map +1 -0
- package/dist/lib/integrations/email.js +249 -0
- package/dist/lib/integrations/email.js.map +1 -0
- package/dist/lib/integrations/integration-manager.d.ts +5 -0
- package/dist/lib/integrations/integration-manager.d.ts.map +1 -1
- package/dist/lib/integrations/integration-manager.js +53 -3
- package/dist/lib/integrations/integration-manager.js.map +1 -1
- package/dist/lib/integrations/types.d.ts +187 -4
- package/dist/lib/integrations/types.d.ts.map +1 -1
- package/dist/lib/integrations/types.js +24 -1
- package/dist/lib/integrations/types.js.map +1 -1
- package/dist/lib/knowledge/knowledge-store.d.ts +7 -1
- package/dist/lib/knowledge/knowledge-store.d.ts.map +1 -1
- package/dist/lib/knowledge/knowledge-store.js +96 -8
- package/dist/lib/knowledge/knowledge-store.js.map +1 -1
- package/dist/lib/knowledge/loaders/file-loaders.d.ts +8 -3
- package/dist/lib/knowledge/loaders/file-loaders.d.ts.map +1 -1
- package/dist/lib/knowledge/loaders/file-loaders.js +96 -75
- package/dist/lib/knowledge/loaders/file-loaders.js.map +1 -1
- package/dist/lib/knowledge/loaders/web-loader.d.ts +12 -3
- package/dist/lib/knowledge/loaders/web-loader.d.ts.map +1 -1
- package/dist/lib/knowledge/loaders/web-loader.js +56 -22
- package/dist/lib/knowledge/loaders/web-loader.js.map +1 -1
- package/dist/lib/knowledge/sqlite-store.d.ts.map +1 -1
- package/dist/lib/knowledge/sqlite-store.js +19 -10
- package/dist/lib/knowledge/sqlite-store.js.map +1 -1
- package/dist/lib/knowledge/types.d.ts +69 -33
- package/dist/lib/knowledge/types.d.ts.map +1 -1
- package/dist/lib/knowledge/types.js +18 -3
- package/dist/lib/knowledge/types.js.map +1 -1
- package/dist/lib/llm/index.d.ts +1 -1
- package/dist/lib/llm/index.d.ts.map +1 -1
- package/dist/lib/llm/index.js +1 -1
- package/dist/lib/llm/index.js.map +1 -1
- package/dist/lib/llm/llm-call-logger.d.ts +3 -1
- package/dist/lib/llm/llm-call-logger.d.ts.map +1 -1
- package/dist/lib/llm/llm-call-logger.js +31 -26
- package/dist/lib/llm/llm-call-logger.js.map +1 -1
- package/dist/lib/llm/llm-config.d.ts +59 -8
- package/dist/lib/llm/llm-config.d.ts.map +1 -1
- package/dist/lib/llm/llm-config.js +163 -17
- package/dist/lib/llm/llm-config.js.map +1 -1
- package/dist/lib/llm/llm-factory.d.ts +1 -2
- package/dist/lib/llm/llm-factory.d.ts.map +1 -1
- package/dist/lib/llm/llm-factory.js +44 -8
- package/dist/lib/llm/llm-factory.js.map +1 -1
- package/dist/lib/llm/providers/anthropic-chat-model.d.ts +5 -1
- package/dist/lib/llm/providers/anthropic-chat-model.d.ts.map +1 -1
- package/dist/lib/llm/providers/anthropic-chat-model.js +118 -42
- package/dist/lib/llm/providers/anthropic-chat-model.js.map +1 -1
- package/dist/lib/llm/providers/gemini-chat-model.d.ts +3 -2
- package/dist/lib/llm/providers/gemini-chat-model.d.ts.map +1 -1
- package/dist/lib/llm/providers/gemini-chat-model.js +83 -24
- package/dist/lib/llm/providers/gemini-chat-model.js.map +1 -1
- package/dist/lib/llm/providers/openai-chat-model.d.ts +20 -1
- package/dist/lib/llm/providers/openai-chat-model.d.ts.map +1 -1
- package/dist/lib/llm/providers/openai-chat-model.js +265 -32
- package/dist/lib/llm/providers/openai-chat-model.js.map +1 -1
- package/dist/lib/llm/providers/openai-embeddings.d.ts.map +1 -1
- package/dist/lib/llm/providers/openai-embeddings.js +41 -10
- package/dist/lib/llm/providers/openai-embeddings.js.map +1 -1
- package/dist/lib/local-llm/binary-manager.d.ts +66 -0
- package/dist/lib/local-llm/binary-manager.d.ts.map +1 -0
- package/dist/lib/local-llm/binary-manager.js +441 -0
- package/dist/lib/local-llm/binary-manager.js.map +1 -0
- package/dist/lib/local-llm/engine-interface.d.ts +47 -0
- package/dist/lib/local-llm/engine-interface.d.ts.map +1 -0
- package/dist/lib/local-llm/engine-interface.js +2 -0
- package/dist/lib/local-llm/engine-interface.js.map +1 -0
- package/dist/lib/local-llm/engine-registry.d.ts +20 -0
- package/dist/lib/local-llm/engine-registry.d.ts.map +1 -0
- package/dist/lib/local-llm/engine-registry.js +56 -0
- package/dist/lib/local-llm/engine-registry.js.map +1 -0
- package/dist/lib/local-llm/engines/llama-cpp-engine.d.ts +31 -0
- package/dist/lib/local-llm/engines/llama-cpp-engine.d.ts.map +1 -0
- package/dist/lib/local-llm/engines/llama-cpp-engine.js +164 -0
- package/dist/lib/local-llm/engines/llama-cpp-engine.js.map +1 -0
- package/dist/lib/local-llm/engines/mlx-serve-engine.d.ts +31 -0
- package/dist/lib/local-llm/engines/mlx-serve-engine.d.ts.map +1 -0
- package/dist/lib/local-llm/engines/mlx-serve-engine.js +161 -0
- package/dist/lib/local-llm/engines/mlx-serve-engine.js.map +1 -0
- package/dist/lib/local-llm/gguf-reader.d.ts +20 -0
- package/dist/lib/local-llm/gguf-reader.d.ts.map +1 -0
- package/dist/lib/local-llm/gguf-reader.js +190 -0
- package/dist/lib/local-llm/gguf-reader.js.map +1 -0
- package/dist/lib/local-llm/index.d.ts +9 -0
- package/dist/lib/local-llm/index.d.ts.map +1 -0
- package/dist/lib/local-llm/index.js +6 -0
- package/dist/lib/local-llm/index.js.map +1 -0
- package/dist/lib/local-llm/llama-server-process.d.ts +42 -0
- package/dist/lib/local-llm/llama-server-process.d.ts.map +1 -0
- package/dist/lib/local-llm/llama-server-process.js +237 -0
- package/dist/lib/local-llm/llama-server-process.js.map +1 -0
- package/dist/lib/local-llm/mlx-binary-manager.d.ts +33 -0
- package/dist/lib/local-llm/mlx-binary-manager.d.ts.map +1 -0
- package/dist/lib/local-llm/mlx-binary-manager.js +211 -0
- package/dist/lib/local-llm/mlx-binary-manager.js.map +1 -0
- package/dist/lib/local-llm/mlx-server-process.d.ts +26 -0
- package/dist/lib/local-llm/mlx-server-process.d.ts.map +1 -0
- package/dist/lib/local-llm/mlx-server-process.js +210 -0
- package/dist/lib/local-llm/mlx-server-process.js.map +1 -0
- package/dist/lib/local-llm/model-manager.d.ts +33 -0
- package/dist/lib/local-llm/model-manager.d.ts.map +1 -0
- package/dist/lib/local-llm/model-manager.js +591 -0
- package/dist/lib/local-llm/model-manager.js.map +1 -0
- package/dist/lib/local-llm/types.d.ts +51 -0
- package/dist/lib/local-llm/types.d.ts.map +1 -0
- package/dist/lib/local-llm/types.js +2 -0
- package/dist/lib/local-llm/types.js.map +1 -0
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +68 -6
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/mcp/mcp-client.d.ts.map +1 -1
- package/dist/lib/mcp/mcp-client.js +5 -3
- package/dist/lib/mcp/mcp-client.js.map +1 -1
- package/dist/lib/mcp/types.d.ts +0 -9
- package/dist/lib/mcp/types.d.ts.map +1 -1
- package/dist/lib/mcp/types.js +1 -2
- package/dist/lib/mcp/types.js.map +1 -1
- package/dist/lib/memory/memory-manager.d.ts +1 -0
- package/dist/lib/memory/memory-manager.d.ts.map +1 -1
- package/dist/lib/memory/memory-manager.js +9 -0
- package/dist/lib/memory/memory-manager.js.map +1 -1
- package/dist/lib/orchestrator.d.ts +11 -8
- package/dist/lib/orchestrator.d.ts.map +1 -1
- package/dist/lib/orchestrator.js +246 -5
- package/dist/lib/orchestrator.js.map +1 -1
- package/dist/lib/sandbox/cdp-client.d.ts +15 -0
- package/dist/lib/sandbox/cdp-client.d.ts.map +1 -0
- package/dist/lib/sandbox/cdp-client.js +139 -0
- package/dist/lib/sandbox/cdp-client.js.map +1 -0
- package/dist/lib/sandbox/html-to-markdown.d.ts +9 -1
- package/dist/lib/sandbox/html-to-markdown.d.ts.map +1 -1
- package/dist/lib/sandbox/html-to-markdown.js +67 -10
- package/dist/lib/sandbox/html-to-markdown.js.map +1 -1
- package/dist/lib/sandbox/index.d.ts +6 -0
- package/dist/lib/sandbox/index.d.ts.map +1 -1
- package/dist/lib/sandbox/index.js +5 -0
- package/dist/lib/sandbox/index.js.map +1 -1
- package/dist/lib/sandbox/page-readiness.d.ts +37 -0
- package/dist/lib/sandbox/page-readiness.d.ts.map +1 -0
- package/dist/lib/sandbox/page-readiness.js +268 -0
- package/dist/lib/sandbox/page-readiness.js.map +1 -0
- package/dist/lib/sandbox/sandbox-browser.d.ts +4 -0
- package/dist/lib/sandbox/sandbox-browser.d.ts.map +1 -0
- package/dist/lib/sandbox/sandbox-browser.js +316 -0
- package/dist/lib/sandbox/sandbox-browser.js.map +1 -0
- package/dist/lib/sandbox/sandbox-container.d.ts +39 -0
- package/dist/lib/sandbox/sandbox-container.d.ts.map +1 -0
- package/dist/lib/sandbox/sandbox-container.js +176 -0
- package/dist/lib/sandbox/sandbox-container.js.map +1 -0
- package/dist/lib/sandbox/sandbox-file.d.ts +4 -0
- package/dist/lib/sandbox/sandbox-file.d.ts.map +1 -0
- package/dist/lib/sandbox/sandbox-file.js +169 -0
- package/dist/lib/sandbox/sandbox-file.js.map +1 -0
- package/dist/lib/sandbox/sandbox-shell.d.ts +5 -0
- package/dist/lib/sandbox/sandbox-shell.d.ts.map +1 -0
- package/dist/lib/sandbox/sandbox-shell.js +111 -0
- package/dist/lib/sandbox/sandbox-shell.js.map +1 -0
- package/dist/lib/sandbox/sandbox-web.d.ts.map +1 -1
- package/dist/lib/sandbox/sandbox-web.js +64 -24
- package/dist/lib/sandbox/sandbox-web.js.map +1 -1
- package/dist/lib/sandbox/types.d.ts +9 -0
- package/dist/lib/sandbox/types.d.ts.map +1 -1
- package/dist/lib/sandbox/types.js +1 -0
- package/dist/lib/sandbox/types.js.map +1 -1
- package/dist/lib/sandbox/vision-browser.d.ts +4 -0
- package/dist/lib/sandbox/vision-browser.d.ts.map +1 -0
- package/dist/lib/sandbox/vision-browser.js +298 -0
- package/dist/lib/sandbox/vision-browser.js.map +1 -0
- package/dist/lib/sea/app-window.d.ts +7 -0
- package/dist/lib/sea/app-window.d.ts.map +1 -0
- package/dist/lib/sea/app-window.js +95 -0
- package/dist/lib/sea/app-window.js.map +1 -0
- package/dist/lib/sea/bootstrap.d.ts +18 -0
- package/dist/lib/sea/bootstrap.d.ts.map +1 -0
- package/dist/lib/sea/bootstrap.js +103 -0
- package/dist/lib/sea/bootstrap.js.map +1 -0
- package/dist/lib/sea/sqlite-vec-shim.d.ts +3 -0
- package/dist/lib/sea/sqlite-vec-shim.d.ts.map +1 -0
- package/dist/lib/sea/sqlite-vec-shim.js +10 -0
- package/dist/lib/sea/sqlite-vec-shim.js.map +1 -0
- package/dist/lib/skills/skill-loader.d.ts +2 -0
- package/dist/lib/skills/skill-loader.d.ts.map +1 -1
- package/dist/lib/skills/skill-loader.js +12 -1
- package/dist/lib/skills/skill-loader.js.map +1 -1
- package/dist/lib/tasks/task-manager.d.ts +3 -1
- package/dist/lib/tasks/task-manager.d.ts.map +1 -1
- package/dist/lib/tasks/task-manager.js +11 -0
- package/dist/lib/tasks/task-manager.js.map +1 -1
- package/dist/lib/tasks/task-store.d.ts +1 -1
- package/dist/lib/tasks/task-store.d.ts.map +1 -1
- package/dist/lib/tasks/task-store.js.map +1 -1
- package/dist/lib/tasks/types.d.ts +18 -0
- package/dist/lib/tasks/types.d.ts.map +1 -1
- package/dist/lib/tools/built-in/integration-tools.d.ts +4 -0
- package/dist/lib/tools/built-in/integration-tools.d.ts.map +1 -0
- package/dist/lib/tools/built-in/integration-tools.js +47 -0
- package/dist/lib/tools/built-in/integration-tools.js.map +1 -0
- package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.d.ts +1 -2
- package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.d.ts.map +1 -1
- package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.js +17 -17
- package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.js.map +1 -1
- package/dist/lib/tools/built-in/knowledge-graph-schema.tool.d.ts.map +1 -1
- package/dist/lib/tools/built-in/knowledge-graph-schema.tool.js +2 -4
- package/dist/lib/tools/built-in/knowledge-graph-schema.tool.js.map +1 -1
- package/dist/lib/tools/built-in/knowledge-search.tool.js +4 -4
- package/dist/lib/tools/built-in/knowledge-search.tool.js.map +1 -1
- package/dist/lib/tools/built-in/knowledge-sql.tool.d.ts.map +1 -1
- package/dist/lib/tools/built-in/knowledge-sql.tool.js +74 -40
- package/dist/lib/tools/built-in/knowledge-sql.tool.js.map +1 -1
- package/dist/lib/tools/built-in/knowledge-tools-factory.js +2 -2
- package/dist/lib/tools/built-in/knowledge-tools-factory.js.map +1 -1
- package/dist/lib/tools/built-in/knowledge-traverse.tool.d.ts +1 -2
- package/dist/lib/tools/built-in/knowledge-traverse.tool.d.ts.map +1 -1
- package/dist/lib/tools/built-in/knowledge-traverse.tool.js +5 -11
- package/dist/lib/tools/built-in/knowledge-traverse.tool.js.map +1 -1
- package/dist/lib/tools/built-in/query-validators.d.ts.map +1 -1
- package/dist/lib/tools/built-in/query-validators.js +4 -0
- package/dist/lib/tools/built-in/query-validators.js.map +1 -1
- package/dist/lib/tools/workspace/workspace-tools.d.ts +1 -0
- package/dist/lib/tools/workspace/workspace-tools.d.ts.map +1 -1
- package/dist/lib/tools/workspace/workspace-tools.js +44 -4
- package/dist/lib/tools/workspace/workspace-tools.js.map +1 -1
- package/dist/lib/triggers/cron-trigger.d.ts +1 -1
- package/dist/lib/triggers/cron-trigger.d.ts.map +1 -1
- package/dist/lib/triggers/cron-trigger.js.map +1 -1
- package/dist/lib/triggers/trigger-manager.d.ts +1 -0
- package/dist/lib/triggers/trigger-manager.d.ts.map +1 -1
- package/dist/lib/triggers/trigger-manager.js +26 -0
- package/dist/lib/triggers/trigger-manager.js.map +1 -1
- package/dist/lib/triggers/webhook-trigger.d.ts +1 -1
- package/dist/lib/triggers/webhook-trigger.d.ts.map +1 -1
- package/dist/lib/triggers/webhook-trigger.js.map +1 -1
- package/dist/lib/types/llm-types.d.ts +22 -4
- package/dist/lib/types/llm-types.d.ts.map +1 -1
- package/dist/lib/types/llm-types.js +50 -0
- package/dist/lib/types/llm-types.js.map +1 -1
- package/dist/lib/types/tool-factory.d.ts +2 -2
- package/dist/lib/types/tool-factory.d.ts.map +1 -1
- package/dist/lib/types/tool-factory.js +9 -2
- package/dist/lib/types/tool-factory.js.map +1 -1
- package/dist/lib/utils/document-extract.d.ts +10 -0
- package/dist/lib/utils/document-extract.d.ts.map +1 -0
- package/dist/lib/utils/document-extract.js +149 -0
- package/dist/lib/utils/document-extract.js.map +1 -0
- package/dist/lib/utils/env-substitution.d.ts +6 -0
- package/dist/lib/utils/env-substitution.d.ts.map +1 -0
- package/dist/lib/utils/env-substitution.js +15 -0
- package/dist/lib/utils/env-substitution.js.map +1 -0
- package/dist/lib/workflows/react-workflow-executor.d.ts.map +1 -1
- package/dist/lib/workflows/react-workflow-executor.js +23 -17
- package/dist/lib/workflows/react-workflow-executor.js.map +1 -1
- package/dist/lib/workflows/types.d.ts +81 -55
- package/dist/lib/workflows/types.d.ts.map +1 -1
- package/dist/lib/workflows/types.js +10 -0
- package/dist/lib/workflows/types.js.map +1 -1
- package/dist/lib/workflows/workflow-loader.d.ts +3 -0
- package/dist/lib/workflows/workflow-loader.d.ts.map +1 -1
- package/dist/lib/workflows/workflow-loader.js +10 -1
- package/dist/lib/workflows/workflow-loader.js.map +1 -1
- package/dist/public/assets/logo.png +0 -0
- package/dist/public/chat.html +39 -0
- package/dist/public/index.html +6 -176
- package/dist/public/src/components/AgentComposer.js +807 -0
- package/dist/public/src/components/AgentsView.js +1812 -508
- package/dist/public/src/components/AppRoot.js +125 -38
- package/dist/public/src/components/GraphView.js +382 -300
- package/dist/public/src/components/IdeView.js +277 -86
- package/dist/public/src/components/KnowledgeView.js +94 -130
- package/dist/public/src/components/LlmView.js +15 -19
- package/dist/public/src/components/LocalLlmView.js +2440 -0
- package/dist/public/src/components/LogViewer.js +155 -0
- package/dist/public/src/components/McpView.js +41 -49
- package/dist/public/src/components/MonitorView.js +174 -83
- package/dist/public/src/components/NavBar.js +16 -26
- package/dist/public/src/components/StandaloneChat.js +875 -0
- package/dist/public/src/services/ApiService.js +203 -4
- package/dist/public/src/services/SessionStore.js +86 -0
- package/dist/public/src/services/StreamManager.js +183 -0
- package/dist/public/src/store.js +1 -3
- package/dist/public/src/utils/card.js +21 -0
- package/dist/public/src/utils/markdown.js +7 -0
- package/dist/public/styles.css +2777 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +7 -1
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/start.d.ts.map +1 -1
- package/dist/src/cli/commands/start.js +28 -5
- package/dist/src/cli/commands/start.js.map +1 -1
- package/dist/src/cli/index.js +19 -5
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.js +7 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/middleware/auth.d.ts.map +1 -1
- package/dist/src/middleware/auth.js +28 -6
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/middleware/rate-limit.d.ts +8 -0
- package/dist/src/middleware/rate-limit.d.ts.map +1 -0
- package/dist/src/middleware/rate-limit.js +21 -0
- package/dist/src/middleware/rate-limit.js.map +1 -0
- package/dist/src/routes/agents.route.d.ts.map +1 -1
- package/dist/src/routes/agents.route.js +138 -10
- package/dist/src/routes/agents.route.js.map +1 -1
- package/dist/src/routes/chat.route.d.ts +3 -0
- package/dist/src/routes/chat.route.d.ts.map +1 -0
- package/dist/src/routes/chat.route.js +156 -0
- package/dist/src/routes/chat.route.js.map +1 -0
- package/dist/src/routes/files.route.d.ts.map +1 -1
- package/dist/src/routes/files.route.js +37 -2
- package/dist/src/routes/files.route.js.map +1 -1
- package/dist/src/routes/llm.route.d.ts.map +1 -1
- package/dist/src/routes/llm.route.js +263 -8
- package/dist/src/routes/llm.route.js.map +1 -1
- package/dist/src/routes/local-llm.route.d.ts +3 -0
- package/dist/src/routes/local-llm.route.d.ts.map +1 -0
- package/dist/src/routes/local-llm.route.js +688 -0
- package/dist/src/routes/local-llm.route.js.map +1 -0
- package/dist/src/routes/logs.route.d.ts +3 -0
- package/dist/src/routes/logs.route.d.ts.map +1 -0
- package/dist/src/routes/logs.route.js +24 -0
- package/dist/src/routes/logs.route.js.map +1 -0
- package/dist/src/routes/tasks.route.d.ts.map +1 -1
- package/dist/src/routes/tasks.route.js +15 -1
- package/dist/src/routes/tasks.route.js.map +1 -1
- package/dist/src/routes/vnc.route.d.ts +12 -0
- package/dist/src/routes/vnc.route.d.ts.map +1 -0
- package/dist/src/routes/vnc.route.js +74 -0
- package/dist/src/routes/vnc.route.js.map +1 -0
- package/dist/src/routes/workflows.route.d.ts.map +1 -1
- package/dist/src/routes/workflows.route.js +24 -0
- package/dist/src/routes/workflows.route.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +29 -3
- package/dist/src/server.js.map +1 -1
- package/dist/templates/Demo.md +152 -0
- package/dist/templates/README.md +12 -3
- package/dist/templates/agents/actor.agent.yaml +34 -0
- package/dist/templates/agents/architect.agent.yaml +20 -13
- package/dist/templates/agents/chatbot.agent.yaml +23 -27
- package/dist/templates/agents/corporate.agent.yaml +64 -0
- package/dist/templates/agents/functions.agent.yaml +29 -0
- package/dist/templates/agents/investment-analyst.agent.yaml +79 -0
- package/dist/templates/agents/music-librarian.agent.yaml +46 -0
- package/dist/templates/agents/network-security.agent.yaml +81 -0
- package/dist/templates/agents/transport-security.agent.yaml +69 -0
- package/dist/templates/agents/web-engineer.agent.yaml +98 -0
- package/dist/templates/agents/web-pilot.agent.yaml +57 -0
- package/dist/templates/knowledge/music-store/LICENSE.md +11 -0
- package/dist/templates/knowledge/music-store/musicstore.sqlite +0 -0
- package/dist/templates/knowledge/music-store/tables.png +0 -0
- package/dist/templates/knowledge/music-store.knowledge.yaml +138 -0
- package/dist/templates/knowledge/org-chart/personnel.csv +21 -21
- package/dist/templates/knowledge/org-chart.knowledge.yaml +4 -0
- package/dist/templates/knowledge/patient-records.knowledge.yaml +20 -0
- package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_0.pdf +0 -0
- package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_1.pdf +0 -0
- package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_10.pdf +0 -0
- package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_11.pdf +0 -0
- package/dist/templates/knowledge/pet-store.knowledge.yaml +3 -0
- package/dist/templates/knowledge/security-incidents/incidents.json +55935 -0
- package/dist/templates/knowledge/security-incidents.knowledge.yaml +46 -0
- package/dist/templates/knowledge/{example.knowledge.yaml → transcripts.knowledge.yaml} +9 -5
- package/dist/templates/knowledge/transport-ot/systems.csv +117 -0
- package/dist/templates/knowledge/transport-ot.knowledge.yaml +55 -0
- package/dist/templates/knowledge/web-docs.knowledge.yaml +1 -1
- package/dist/templates/llm.json +62 -22
- package/dist/templates/mcp.json +7 -4
- package/dist/templates/skills/orcha-builder/SKILL.md +148 -215
- package/dist/templates/skills/pii-guard/SKILL.md +22 -0
- package/dist/templates/skills/sandbox/SKILL.md +25 -48
- package/dist/templates/skills/web-pilot/SKILL.md +51 -0
- package/dist/templates/workflows/example.workflow.yaml +27 -35
- package/dist/templates/workflows/react-example.workflow.yaml +14 -19
- package/dist/templates/workflows/team-chat.workflow.yaml +47 -0
- package/package.json +17 -4
- package/dist/public/src/components/SkillsView.js +0 -137
- package/dist/public/src/components/WorkflowsView.js +0 -416
- package/dist/templates/agents/knowledge-broker.agent.yaml +0 -39
- package/dist/templates/agents/sandbox.agent.yaml +0 -56
|
@@ -0,0 +1,2440 @@
|
|
|
1
|
+
|
|
2
|
+
import { Component } from '../utils/Component.js';
|
|
3
|
+
import { api } from '../services/ApiService.js';
|
|
4
|
+
|
|
5
|
+
function formatBytes(bytes) {
|
|
6
|
+
if (!bytes) return '0 B';
|
|
7
|
+
if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`;
|
|
8
|
+
if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`;
|
|
9
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
10
|
+
return `${bytes} B`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function escapeHtml(text) {
|
|
14
|
+
if (!text) return '';
|
|
15
|
+
return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function timeAgo(dateStr) {
|
|
19
|
+
const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
|
|
20
|
+
if (seconds < 60) return 'just now';
|
|
21
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
22
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
23
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Model family patterns for capability detection.
|
|
27
|
+
// HuggingFace tags are unreliable — most GGUF repos don't tag capabilities.
|
|
28
|
+
// We combine explicit tags, pipeline_tag, and model name heuristics.
|
|
29
|
+
const TOOL_TAGS = ['tool-calling', 'function-calling', 'tool_use', 'tool-use'];
|
|
30
|
+
const TOOL_NAME_PATTERNS = [
|
|
31
|
+
/qwen[23]/i, /qwen3\.5/i, /llama.?3\.[1-9]/i, /mistral/i, /phi.?[4-9]/i,
|
|
32
|
+
/functionary/i, /hermes/i, /command.?r/i, /glm/i, /gemma.?[2-9]/i,
|
|
33
|
+
/nemotron/i, /granite/i,
|
|
34
|
+
];
|
|
35
|
+
const VISION_PIPELINE_TAGS = ['image-text-to-text', 'image-to-text'];
|
|
36
|
+
const VISION_NAME_PATTERNS = [
|
|
37
|
+
/vision/i, /llava/i, /pixtral/i, /qwen3\.5/i,
|
|
38
|
+
];
|
|
39
|
+
const REASONING_NAME_PATTERNS = [
|
|
40
|
+
/deepseek.?r1/i, /qwq/i, /o[134]-/i, /reasoning/i,
|
|
41
|
+
/think/i, /r1.?distill/i, /qwen3/i,
|
|
42
|
+
];
|
|
43
|
+
// Qwen3+ has built-in thinking mode (enable_thinking), including Qwen3.5
|
|
44
|
+
const REASONING_TAG_PATTERNS = [/qwen3/i];
|
|
45
|
+
|
|
46
|
+
const PROVIDERS = ['local', 'openai', 'anthropic', 'gemini'];
|
|
47
|
+
|
|
48
|
+
// Brand SVG icons (viewBox 0 0 24 24, uses currentColor)
|
|
49
|
+
const BRAND_SVGS = {
|
|
50
|
+
openai: `<svg viewBox="0 0 24 24" fill="currentColor" class="llm-brand-icon"><path d="M22.28 9.82a5.98 5.98 0 0 0-.52-4.91 6.05 6.05 0 0 0-6.51-2.9A6.07 6.07 0 0 0 4.98 4.18a5.98 5.98 0 0 0-4 2.9 6.05 6.05 0 0 0 .74 7.1 5.98 5.98 0 0 0 .51 4.91 6.05 6.05 0 0 0 6.52 2.9A5.98 5.98 0 0 0 13.26 24a6.06 6.06 0 0 0 5.77-4.21 5.99 5.99 0 0 0 4-2.9 6.06 6.06 0 0 0-.75-7.07zm-9.02 12.61a4.48 4.48 0 0 1-2.88-1.04l.14-.08 4.78-2.76a.8.8 0 0 0 .39-.68v-6.74l2.02 1.17a.07.07 0 0 1 .04.05v5.58a4.5 4.5 0 0 1-4.49 4.5zM3.6 18.3a4.47 4.47 0 0 1-.54-3.01l.14.08 4.78 2.76a.77.77 0 0 0 .78 0l5.84-3.37v2.33a.08.08 0 0 1-.03.06l-4.84 2.79a4.5 4.5 0 0 1-6.14-1.65zM2.34 7.9a4.49 4.49 0 0 1 2.37-1.97V11.6a.77.77 0 0 0 .39.68l5.81 3.35-2.02 1.17a.08.08 0 0 1-.07 0L4.02 14.01A4.5 4.5 0 0 1 2.34 7.87zm16.6 3.86L13.1 8.36l2.02-1.16a.08.08 0 0 1 .07 0l4.83 2.79a4.49 4.49 0 0 1-.68 8.1V12.42a.79.79 0 0 0-.41-.68zm2.01-3.02l-.14-.09-4.77-2.78a.78.78 0 0 0-.79 0L9.41 9.23V6.9a.07.07 0 0 1 .03-.06l4.83-2.79a4.5 4.5 0 0 1 6.68 4.66zM8.31 12.86l-2.02-1.16a.08.08 0 0 1-.04-.06V6.07a4.5 4.5 0 0 1 7.38-3.45l-.14.08-4.78 2.76a.8.8 0 0 0-.39.68zm1.1-2.36l2.6-1.5 2.6 1.5v3l-2.6 1.5-2.6-1.5z"/></svg>`,
|
|
51
|
+
anthropic: `<svg viewBox="0 0 24 24" fill="currentColor" class="llm-brand-icon"><path d="M13.83 3.52h3.6L24 20.48h-3.6l-6.57-16.96zm-7.26 0h3.6l6.57 16.96h-3.6L6.57 3.52z"/></svg>`,
|
|
52
|
+
gemini: `<svg viewBox="0 0 24 24" fill="currentColor" class="llm-brand-icon"><path d="M12 0C12 6.63 6.63 12 0 12c6.63 0 12 5.37 12 12 0-6.63 5.37-12 12-12C17.37 12 12 6.63 12 0z"/></svg>`,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function providerIcon(provider) {
|
|
56
|
+
if (BRAND_SVGS[provider]) return BRAND_SVGS[provider];
|
|
57
|
+
return `<i class="fas fa-server"></i>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PROVIDER_META = {
|
|
61
|
+
local: { label: 'Local', color: 'amber' },
|
|
62
|
+
openai: { label: 'OpenAI', color: 'green' },
|
|
63
|
+
anthropic: { label: 'Anthropic', color: 'purple' },
|
|
64
|
+
gemini: { label: 'Google', color: 'blue' },
|
|
65
|
+
};
|
|
66
|
+
const POPULAR_MODELS = {
|
|
67
|
+
openai: ['gpt-5.4', 'gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'o4-mini', 'o3', 'o3-mini', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4o', 'gpt-4o-mini'],
|
|
68
|
+
anthropic: ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5-20251001', 'claude-sonnet-4-5', 'claude-opus-4-5'],
|
|
69
|
+
gemini: ['gemini-3.1-pro-preview', 'gemini-3-flash-preview', 'gemini-3.1-flash-lite-preview', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'],
|
|
70
|
+
};
|
|
71
|
+
const POPULAR_EMBEDDINGS = {
|
|
72
|
+
openai: ['text-embedding-3-small', 'text-embedding-3-large'],
|
|
73
|
+
gemini: ['gemini-embedding-001', 'text-embedding-004'],
|
|
74
|
+
};
|
|
75
|
+
const PROVIDER_ENV_NAMES = {
|
|
76
|
+
openai: 'OPENAI_API_KEY',
|
|
77
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
78
|
+
gemini: 'GOOGLE_API_KEY',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const RECOMMENDED_MODELS_GGUF = [
|
|
82
|
+
{ repo: 'unsloth/Qwen3.5-9B-GGUF', file: 'Qwen3.5-9B-Q4_K_M.gguf', label: 'Qwen3.5-9B-Q4_K_M', desc: 'Chat model with tool calling, vision, and reasoning. Great all-rounder for local use.', size: '~5.3 GB', icon: 'fa-comments', color: 'amber', type: 'gguf' },
|
|
83
|
+
{ repo: 'nomic-ai/nomic-embed-text-v1.5-GGUF', file: 'nomic-embed-text-v1.5.Q4_K_M.gguf', label: 'nomic-embed-text-v1.5-Q4_K_M', desc: 'Embedding model for knowledge stores. Required for local RAG pipelines.', size: '~80 MB', icon: 'fa-vector-square', color: 'blue', type: 'gguf' },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const RECOMMENDED_MODELS_MLX = [
|
|
87
|
+
{ repo: 'mlx-community/Qwen3.5-9B-4bit', file: '__mlx_repo__', label: 'Qwen3.5-9B-4bit (MLX)', desc: 'MLX-optimized for Apple Silicon. Recommended for Mac.', size: '~5 GB', icon: 'fa-apple', color: 'amber', type: 'mlx' },
|
|
88
|
+
{ repo: 'mlx-community/all-MiniLM-L6-v2-4bit', file: '__mlx_repo__', label: 'all-MiniLM-L6-v2-4bit (MLX)', desc: 'Fast, lightweight embedding model (22M params, 384 dims). Recommended for local RAG.', size: '~15 MB', icon: 'fa-vector-square', color: 'blue', type: 'mlx' },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
function isAppleSilicon(status) {
|
|
92
|
+
return status?.platform === 'darwin' && status?.arch === 'arm64';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getRecommendedModels(status) {
|
|
96
|
+
return isAppleSilicon(status) ? RECOMMENDED_MODELS_MLX : RECOMMENDED_MODELS_GGUF;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function detectCapabilities(result) {
|
|
100
|
+
const { tags, pipelineTag, modelName, repoId } = result;
|
|
101
|
+
const name = `${repoId} ${modelName}`;
|
|
102
|
+
const lowerTags = (tags || []).map(t => t.toLowerCase());
|
|
103
|
+
|
|
104
|
+
const vision = VISION_PIPELINE_TAGS.includes(pipelineTag)
|
|
105
|
+
|| lowerTags.some(t => VISION_PIPELINE_TAGS.includes(t))
|
|
106
|
+
|| VISION_NAME_PATTERNS.some(p => p.test(name));
|
|
107
|
+
|
|
108
|
+
const tools = lowerTags.some(t => TOOL_TAGS.includes(t))
|
|
109
|
+
|| TOOL_NAME_PATTERNS.some(p => p.test(name));
|
|
110
|
+
|
|
111
|
+
const reasoning = REASONING_NAME_PATTERNS.some(p => p.test(name))
|
|
112
|
+
|| REASONING_TAG_PATTERNS.some(p => lowerTags.some(t => p.test(t)));
|
|
113
|
+
|
|
114
|
+
return { vision, tools, reasoning };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function detectCapabilitiesFromFile(model) {
|
|
118
|
+
const name = `${model.repo || ''} ${model.fileName}`;
|
|
119
|
+
return {
|
|
120
|
+
tools: TOOL_NAME_PATTERNS.some(p => p.test(name)),
|
|
121
|
+
vision: VISION_NAME_PATTERNS.some(p => p.test(name)),
|
|
122
|
+
reasoning: REASONING_NAME_PATTERNS.some(p => p.test(name))
|
|
123
|
+
|| REASONING_TAG_PATTERNS.some(p => p.test(name)),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function capabilityBadges(caps) {
|
|
128
|
+
const badges = [];
|
|
129
|
+
if (caps.tools) badges.push('<span class="cap-badge cap-badge-tools" title="Tool calling"><i class="fas fa-wrench mr-1"></i>tools</span>');
|
|
130
|
+
if (caps.vision) badges.push('<span class="cap-badge cap-badge-vision" title="Vision"><i class="fas fa-eye mr-1"></i>vision</span>');
|
|
131
|
+
if (caps.reasoning) badges.push('<span class="cap-badge cap-badge-think" title="Reasoning"><i class="fas fa-brain mr-1"></i>think</span>');
|
|
132
|
+
return badges.join(' ');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const ENGINE_LABELS = {
|
|
136
|
+
'llama-cpp': 'llama-cpp',
|
|
137
|
+
'mlx-serve': 'mlx-serve',
|
|
138
|
+
'ollama': 'Ollama',
|
|
139
|
+
'lmstudio': 'LM Studio',
|
|
140
|
+
};
|
|
141
|
+
const ENGINE_ICONS = {
|
|
142
|
+
'llama-cpp': '<i class="fas fa-server"></i>',
|
|
143
|
+
'mlx-serve': '<i class="fab fa-apple"></i>',
|
|
144
|
+
'ollama': '<i class="fas fa-cube"></i>',
|
|
145
|
+
'lmstudio': '<i class="fas fa-flask"></i>',
|
|
146
|
+
};
|
|
147
|
+
const MANAGED_ENGINES = ['llama-cpp', 'mlx-serve'];
|
|
148
|
+
const EXTERNAL_ENGINES = ['ollama', 'lmstudio'];
|
|
149
|
+
|
|
150
|
+
export class LocalLlmView extends Component {
|
|
151
|
+
constructor() {
|
|
152
|
+
super();
|
|
153
|
+
this.status = null;
|
|
154
|
+
this.models = [];
|
|
155
|
+
this.searchResults = [];
|
|
156
|
+
this.activeDownloads = new Map(); // EventSource connections (UI only)
|
|
157
|
+
this.downloadPollTimer = null;
|
|
158
|
+
this.systemRamBytes = 0;
|
|
159
|
+
this.updateInfo = null;
|
|
160
|
+
this.mlxUpdateInfo = null;
|
|
161
|
+
this.activeProvider = 'local';
|
|
162
|
+
this.llmConfig = null;
|
|
163
|
+
this._browseFormat = 'gguf';
|
|
164
|
+
this._selectedEngine = null; // auto-detect from status
|
|
165
|
+
this._engines = null; // cached engine probe result
|
|
166
|
+
this._engineUrls = {}; // custom base URLs for external engines
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Resolve a config section's default pointer to the actual config object.
|
|
171
|
+
* In the new pointer format, `default` is a string key pointing to another entry.
|
|
172
|
+
* @param {'models'|'embeddings'} section
|
|
173
|
+
* @returns {object|null} The resolved config object, or null
|
|
174
|
+
*/
|
|
175
|
+
_resolveDefault(section) {
|
|
176
|
+
const sectionData = this.llmConfig?.[section];
|
|
177
|
+
if (!sectionData) return null;
|
|
178
|
+
let val = sectionData['default'];
|
|
179
|
+
// Dereference string pointer (one level)
|
|
180
|
+
if (typeof val === 'string') {
|
|
181
|
+
val = sectionData[val];
|
|
182
|
+
}
|
|
183
|
+
return (val && typeof val === 'object') ? val : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get the resolved key name that 'default' points to.
|
|
188
|
+
* @param {'models'|'embeddings'} section
|
|
189
|
+
* @returns {string|null}
|
|
190
|
+
*/
|
|
191
|
+
_resolveDefaultKey(section) {
|
|
192
|
+
const sectionData = this.llmConfig?.[section];
|
|
193
|
+
if (!sectionData) return null;
|
|
194
|
+
const val = sectionData['default'];
|
|
195
|
+
if (typeof val === 'string') return val;
|
|
196
|
+
return 'default';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async connectedCallback() {
|
|
200
|
+
super.connectedCallback();
|
|
201
|
+
await this.loadLlmConfig();
|
|
202
|
+
await this.refresh();
|
|
203
|
+
this.pollActiveDownloads();
|
|
204
|
+
this.loadInterruptedDownloads();
|
|
205
|
+
this.loadEngines();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
disconnectedCallback() {
|
|
209
|
+
// Only close SSE connections — server downloads continue
|
|
210
|
+
for (const es of this.activeDownloads.values()) {
|
|
211
|
+
es.close();
|
|
212
|
+
}
|
|
213
|
+
this.activeDownloads.clear();
|
|
214
|
+
if (this.downloadPollTimer) {
|
|
215
|
+
clearInterval(this.downloadPollTimer);
|
|
216
|
+
this.downloadPollTimer = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async refresh() {
|
|
221
|
+
await Promise.all([this.loadStatus(), this.loadModels()]);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
startDownloadPolling() {
|
|
225
|
+
if (this.downloadPollTimer) return; // already polling
|
|
226
|
+
this.downloadPollTimer = setInterval(async () => {
|
|
227
|
+
try {
|
|
228
|
+
const current = await api.getActiveDownloads();
|
|
229
|
+
if (current.length === 0) {
|
|
230
|
+
clearInterval(this.downloadPollTimer);
|
|
231
|
+
this.downloadPollTimer = null;
|
|
232
|
+
this.renderActiveDownloads([]);
|
|
233
|
+
this.loadModels();
|
|
234
|
+
} else {
|
|
235
|
+
this.renderActiveDownloads(current);
|
|
236
|
+
}
|
|
237
|
+
} catch { /* ignore */ }
|
|
238
|
+
}, 1000);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async pollActiveDownloads() {
|
|
242
|
+
try {
|
|
243
|
+
const downloads = await api.getActiveDownloads();
|
|
244
|
+
if (downloads.length > 0) {
|
|
245
|
+
this.renderActiveDownloads(downloads);
|
|
246
|
+
this.startDownloadPolling();
|
|
247
|
+
}
|
|
248
|
+
} catch { /* ignore */ }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async loadInterruptedDownloads() {
|
|
252
|
+
const container = this.querySelector('#interruptedDownloads');
|
|
253
|
+
if (!container) return;
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const interrupted = await api.getInterruptedDownloads();
|
|
257
|
+
if (!interrupted.length) {
|
|
258
|
+
container.innerHTML = '';
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
container.innerHTML = interrupted.map(d => `
|
|
263
|
+
<div class="llm-alert llm-alert-warning" data-interrupted="${escapeHtml(d.fileName)}">
|
|
264
|
+
<i class="fas fa-pause-circle text-amber text-sm"></i>
|
|
265
|
+
<div class="min-w-0 flex-1">
|
|
266
|
+
<div class="text-sm text-primary truncate">${escapeHtml(d.fileName)}</div>
|
|
267
|
+
<div class="text-xs text-muted">${d.repo ? escapeHtml(d.repo) + ' · ' : ''}${formatBytes(d.downloadedBytes)} downloaded</div>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="flex items-center gap-2 flex-shrink-0">
|
|
270
|
+
<button class="resume-btn btn btn-amber btn-sm" data-repo="${escapeHtml(d.repo || '')}" data-file="${escapeHtml(d.fileName)}">
|
|
271
|
+
<i class="fas fa-play mr-1"></i>Resume
|
|
272
|
+
</button>
|
|
273
|
+
<button class="discard-btn btn btn-danger btn-sm" data-file="${escapeHtml(d.fileName)}">
|
|
274
|
+
<i class="fas fa-trash-alt mr-1"></i>Discard
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
`).join('');
|
|
279
|
+
|
|
280
|
+
container.querySelectorAll('.resume-btn').forEach(btn => {
|
|
281
|
+
btn.addEventListener('click', () => {
|
|
282
|
+
const repo = btn.dataset.repo;
|
|
283
|
+
const fileName = btn.dataset.file;
|
|
284
|
+
if (!repo) return;
|
|
285
|
+
const row = btn.closest('[data-interrupted]');
|
|
286
|
+
if (row) row.remove();
|
|
287
|
+
this.downloadModel(repo, fileName, null);
|
|
288
|
+
this.startDownloadPolling();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
container.querySelectorAll('.discard-btn').forEach(btn => {
|
|
293
|
+
btn.addEventListener('click', async () => {
|
|
294
|
+
const fileName = btn.dataset.file;
|
|
295
|
+
try {
|
|
296
|
+
await api.deleteInterruptedDownload(fileName);
|
|
297
|
+
const row = btn.closest('[data-interrupted]');
|
|
298
|
+
if (row) row.remove();
|
|
299
|
+
} catch (e) {
|
|
300
|
+
console.error('Failed to discard download:', e);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
} catch {
|
|
305
|
+
// ignore
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
renderActiveDownloads(downloads) {
|
|
310
|
+
const container = this.querySelector('#activeDownloads');
|
|
311
|
+
if (!container) return;
|
|
312
|
+
|
|
313
|
+
if (!downloads.length) {
|
|
314
|
+
container.innerHTML = '';
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
container.innerHTML = downloads.map(d => `
|
|
319
|
+
<div class="llm-alert llm-alert-amber">
|
|
320
|
+
<i class="fas fa-spinner fa-spin text-amber text-sm"></i>
|
|
321
|
+
<div class="min-w-0 flex-1">
|
|
322
|
+
<div class="text-sm text-primary truncate">${escapeHtml(d.fileName)}</div>
|
|
323
|
+
<div class="text-xs text-muted truncate">${escapeHtml(d.repo)}</div>
|
|
324
|
+
</div>
|
|
325
|
+
<div class="flex items-center gap-2 flex-shrink-0">
|
|
326
|
+
<div class="llm-download-bar">
|
|
327
|
+
<div class="llm-download-fill llm-download-fill-amber" style="width: ${d.progress.percent}%"></div>
|
|
328
|
+
</div>
|
|
329
|
+
<span class="text-xs text-muted font-mono text-right">${d.progress.percent}%</span>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
`).join('');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async loadStatus() {
|
|
336
|
+
try {
|
|
337
|
+
this.status = await api.getLocalLlmStatus();
|
|
338
|
+
this.systemRamBytes = this.status.systemRamBytes || 0;
|
|
339
|
+
// Re-render engine tabs if available (status may update engine availability)
|
|
340
|
+
if (this._engines) this.renderEngineTabs();
|
|
341
|
+
this.renderStatus();
|
|
342
|
+
} catch (e) {
|
|
343
|
+
console.error('Failed to load local LLM status:', e);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async loadModels() {
|
|
348
|
+
try {
|
|
349
|
+
this.models = await api.getLocalLlmModels();
|
|
350
|
+
this.renderModels();
|
|
351
|
+
this.renderRecommendations();
|
|
352
|
+
// Re-render status since it depends on models for capability detection
|
|
353
|
+
if (this.status) this.renderStatus();
|
|
354
|
+
} catch (e) {
|
|
355
|
+
console.error('Failed to load local models:', e);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async loadEngines() {
|
|
360
|
+
try {
|
|
361
|
+
const [engines, urls] = await Promise.all([api.getEngines(), api.getEngineUrls()]);
|
|
362
|
+
this._engines = engines;
|
|
363
|
+
this._engineUrls = urls || {};
|
|
364
|
+
// Auto-detect initial engine from current default config
|
|
365
|
+
if (!this._selectedEngine) {
|
|
366
|
+
const configEngine = this.status?.defaultEngine || this._resolveDefault('models')?.engine;
|
|
367
|
+
if (configEngine) {
|
|
368
|
+
this._selectedEngine = configEngine;
|
|
369
|
+
} else {
|
|
370
|
+
this._selectedEngine = 'llama-cpp';
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
this.renderEngineTabs();
|
|
374
|
+
this.renderEngineContent();
|
|
375
|
+
} catch (e) {
|
|
376
|
+
console.error('Failed to load engines:', e);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
renderEngineTabs() {
|
|
381
|
+
const container = this.querySelector('#engineTabs');
|
|
382
|
+
if (!container || !this._engines) return;
|
|
383
|
+
|
|
384
|
+
const isMacHost = this.status?.platform === 'darwin';
|
|
385
|
+
const configDefaultEngine = this._resolveDefault('models')?.engine || this.status?.defaultEngine || null;
|
|
386
|
+
const engines = ['llama-cpp', 'mlx-serve', 'ollama', 'lmstudio']
|
|
387
|
+
.filter(eng => eng !== 'mlx-serve' || isMacHost);
|
|
388
|
+
container.innerHTML = engines.map(eng => {
|
|
389
|
+
const available = this._engines[eng]?.available;
|
|
390
|
+
const isActive = this._selectedEngine === eng;
|
|
391
|
+
const isExternal = EXTERNAL_ENGINES.includes(eng);
|
|
392
|
+
const isDefault = eng === configDefaultEngine;
|
|
393
|
+
const statusDot = isExternal
|
|
394
|
+
? `<span class="engine-status ${available ? 'connected' : 'disconnected'}"></span>`
|
|
395
|
+
: '';
|
|
396
|
+
const defaultBadge = isDefault
|
|
397
|
+
? '<span class="badge badge-green text-2xs">default</span>'
|
|
398
|
+
: '';
|
|
399
|
+
const experimentalBadge = eng === 'mlx-serve'
|
|
400
|
+
? '<span class="badge badge-amber text-2xs">experimental</span>'
|
|
401
|
+
: '';
|
|
402
|
+
return `
|
|
403
|
+
<button class="llm-engine-tab ${isActive ? 'active' : ''} ${!available ? 'unavailable' : ''}"
|
|
404
|
+
data-engine="${eng}" ${!available ? 'title="Not detected / Not running"' : ''}>
|
|
405
|
+
${ENGINE_ICONS[eng]}
|
|
406
|
+
<span>${ENGINE_LABELS[eng]}</span>
|
|
407
|
+
${experimentalBadge}
|
|
408
|
+
${defaultBadge}
|
|
409
|
+
${statusDot}
|
|
410
|
+
</button>`;
|
|
411
|
+
}).join('');
|
|
412
|
+
|
|
413
|
+
container.querySelectorAll('.llm-engine-tab').forEach(btn => {
|
|
414
|
+
btn.addEventListener('click', () => {
|
|
415
|
+
const eng = btn.dataset.engine;
|
|
416
|
+
// Allow selecting unavailable engines (they'll show "not detected")
|
|
417
|
+
this._selectedEngine = eng;
|
|
418
|
+
this.renderEngineTabs();
|
|
419
|
+
this.renderEngineContent();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
renderEngineContent() {
|
|
425
|
+
const eng = this._selectedEngine;
|
|
426
|
+
if (!eng) return;
|
|
427
|
+
|
|
428
|
+
const isExternal = EXTERNAL_ENGINES.includes(eng);
|
|
429
|
+
const modelsSection = this.querySelector('#managedModelsSection');
|
|
430
|
+
const hfSection = this.querySelector('#hfSection');
|
|
431
|
+
const recSection = this.querySelector('#recommendedSection');
|
|
432
|
+
const extSection = this.querySelector('#externalModelsSection');
|
|
433
|
+
|
|
434
|
+
// Status bar is always visible — renderStatus handles all engines
|
|
435
|
+
this.renderStatus();
|
|
436
|
+
|
|
437
|
+
if (isExternal) {
|
|
438
|
+
if (modelsSection) modelsSection.classList.add('hidden');
|
|
439
|
+
if (hfSection) hfSection.classList.add('hidden');
|
|
440
|
+
if (recSection) recSection.classList.add('hidden');
|
|
441
|
+
if (extSection) extSection.classList.remove('hidden');
|
|
442
|
+
this.renderExternalModels();
|
|
443
|
+
} else {
|
|
444
|
+
if (modelsSection) modelsSection.classList.remove('hidden');
|
|
445
|
+
if (hfSection) hfSection.classList.remove('hidden');
|
|
446
|
+
if (extSection) extSection.classList.add('hidden');
|
|
447
|
+
this.renderModels();
|
|
448
|
+
this.renderRecommendations();
|
|
449
|
+
// Lock HF format to match engine and reset results
|
|
450
|
+
const formatSelect = this.querySelector('#hfFormatSelect');
|
|
451
|
+
if (formatSelect) {
|
|
452
|
+
const newFormat = eng === 'mlx-serve' ? 'mlx' : 'gguf';
|
|
453
|
+
if (this._browseFormat !== newFormat) {
|
|
454
|
+
formatSelect.value = newFormat;
|
|
455
|
+
this._browseFormat = newFormat;
|
|
456
|
+
this.searchResults = [];
|
|
457
|
+
const hfResults = this.querySelector('#hfResults');
|
|
458
|
+
if (hfResults) {
|
|
459
|
+
hfResults.innerHTML = `
|
|
460
|
+
<div class="text-muted text-center py-8 text-sm">
|
|
461
|
+
<i class="fas fa-cube text-2xl mb-3 block text-muted"></i>
|
|
462
|
+
Search HuggingFace to find and download ${newFormat.toUpperCase()} models
|
|
463
|
+
</div>`;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
formatSelect.disabled = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
renderExternalModels() {
|
|
472
|
+
const container = this.querySelector('#externalModelsSection');
|
|
473
|
+
if (!container) return;
|
|
474
|
+
|
|
475
|
+
const eng = this._selectedEngine;
|
|
476
|
+
const engineData = this._engines?.[eng];
|
|
477
|
+
const available = engineData?.available;
|
|
478
|
+
const models = engineData?.models || [];
|
|
479
|
+
const label = ENGINE_LABELS[eng];
|
|
480
|
+
|
|
481
|
+
if (!available) {
|
|
482
|
+
container.innerHTML = '';
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const currentDefault = this._resolveDefault('models');
|
|
487
|
+
const currentEmbDefault = this._resolveDefault('embeddings');
|
|
488
|
+
const isActiveChat = currentDefault?.engine === eng;
|
|
489
|
+
const isActiveEmb = currentEmbDefault?.engine === eng;
|
|
490
|
+
const activeChatModel = isActiveChat ? currentDefault?.model : null;
|
|
491
|
+
const activeEmbModel = isActiveEmb ? currentEmbDefault?.model : null;
|
|
492
|
+
|
|
493
|
+
let html = '';
|
|
494
|
+
if (!models.length) {
|
|
495
|
+
html = `<div class="text-muted text-center py-8 text-sm">No models loaded in ${label}.</div>`;
|
|
496
|
+
} else {
|
|
497
|
+
const totalRam = this.systemRamBytes;
|
|
498
|
+
html = `
|
|
499
|
+
<h3 class="section-title mb-3">Available Models</h3>
|
|
500
|
+
<div class="llm-model-grid">
|
|
501
|
+
${models.map(m => {
|
|
502
|
+
const name = m.name;
|
|
503
|
+
const isChatActive = activeChatModel === name;
|
|
504
|
+
const isEmbActive = activeEmbModel === name;
|
|
505
|
+
const isLoaded = !!m.loaded;
|
|
506
|
+
|
|
507
|
+
const caps = this._detectExternalCaps(m, eng);
|
|
508
|
+
const badges = capabilityBadges(caps);
|
|
509
|
+
const looksLikeEmbed = caps.embedding;
|
|
510
|
+
|
|
511
|
+
const sizeStr = m.size ? formatBytes(m.size) : '';
|
|
512
|
+
const tooLarge = !this._isEngineRemote(eng) && totalRam && m.size && m.size > totalRam;
|
|
513
|
+
|
|
514
|
+
const metaParts = [];
|
|
515
|
+
if (m.parameterSize) metaParts.push(m.parameterSize);
|
|
516
|
+
if (m.quantization) metaParts.push(m.quantization);
|
|
517
|
+
if (m.family) metaParts.push(m.family);
|
|
518
|
+
if (m.arch) metaParts.push(m.arch);
|
|
519
|
+
if (m.maxContextLength) metaParts.push(`${(m.maxContextLength / 1024).toFixed(0)}K ctx`);
|
|
520
|
+
const metaStr = metaParts.join(' · ');
|
|
521
|
+
|
|
522
|
+
// Card highlights as active based on config; button reflects loaded state
|
|
523
|
+
const cardCls = isChatActive ? 'llm-model-card active-chat' : isEmbActive ? 'llm-model-card active-emb' : 'llm-model-card';
|
|
524
|
+
|
|
525
|
+
// Determine the action button/badge
|
|
526
|
+
let actionHtml;
|
|
527
|
+
if (looksLikeEmbed) {
|
|
528
|
+
if (isEmbActive && isLoaded) {
|
|
529
|
+
actionHtml = '<span class="badge badge-blue">Embedding</span>';
|
|
530
|
+
} else if (isEmbActive && !isLoaded) {
|
|
531
|
+
actionHtml = `<button class="ext-activate-emb btn btn-blue btn-sm" data-model="${escapeHtml(name)}">
|
|
532
|
+
<i class="fas fa-redo mr-1"></i>Reload
|
|
533
|
+
</button>`;
|
|
534
|
+
} else {
|
|
535
|
+
actionHtml = `<button class="ext-activate-emb btn btn-blue btn-sm" data-model="${escapeHtml(name)}">
|
|
536
|
+
<i class="fas fa-vector-square mr-1"></i>Embed
|
|
537
|
+
</button>`;
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
if (isChatActive && isLoaded) {
|
|
541
|
+
actionHtml = '<span class="badge badge-amber">Active</span>';
|
|
542
|
+
} else if (isChatActive && !isLoaded) {
|
|
543
|
+
actionHtml = `<button class="ext-activate-chat btn btn-amber btn-sm" data-model="${escapeHtml(name)}">
|
|
544
|
+
<i class="fas fa-redo mr-1"></i>Reload
|
|
545
|
+
</button>`;
|
|
546
|
+
} else {
|
|
547
|
+
actionHtml = `<button class="ext-activate-chat btn btn-amber btn-sm" data-model="${escapeHtml(name)}">
|
|
548
|
+
<i class="fas fa-play mr-1"></i>Activate
|
|
549
|
+
</button>`;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return `
|
|
554
|
+
<div class="${cardCls}">
|
|
555
|
+
<div class="flex items-start justify-between mb-3">
|
|
556
|
+
<div class="min-w-0 flex-1">
|
|
557
|
+
<div class="flex items-center gap-2 mb-1">
|
|
558
|
+
${isChatActive ? `<i class="fas fa-circle ${isLoaded ? 'text-amber' : 'text-muted'} text-2xs" ${!isLoaded ? 'title="Not loaded on server"' : ''}></i>` : ''}
|
|
559
|
+
${isEmbActive ? `<i class="fas fa-circle ${isLoaded ? 'text-blue' : 'text-muted'} text-2xs" ${!isLoaded ? 'title="Not loaded on server"' : ''}></i>` : ''}
|
|
560
|
+
<span class="font-medium text-primary text-sm truncate">${escapeHtml(name)}</span>
|
|
561
|
+
${m.format ? `<span class="badge badge-${m.format === 'mlx' ? 'green' : 'amber'} text-2xs">${escapeHtml(m.format.toUpperCase())}</span>` : ''}
|
|
562
|
+
</div>
|
|
563
|
+
${metaStr ? `<div class="text-xs text-muted">${escapeHtml(metaStr)}</div>` : ''}
|
|
564
|
+
${badges ? `<div class="flex items-center gap-1 mt-1">${badges}</div>` : ''}
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
<div class="flex items-center justify-between">
|
|
568
|
+
<div class="flex items-center gap-3 text-xs text-muted">
|
|
569
|
+
${sizeStr ? `<span><i class="fas fa-hard-drive mr-1"></i>${sizeStr}</span>` : ''}
|
|
570
|
+
${tooLarge ? `<span class="text-red"><i class="fas fa-memory mr-1"></i>won't fit</span>` : ''}
|
|
571
|
+
</div>
|
|
572
|
+
<div class="flex items-center gap-2">
|
|
573
|
+
${actionHtml}
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>`;
|
|
577
|
+
}).join('')}
|
|
578
|
+
</div>`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
container.innerHTML = html;
|
|
582
|
+
|
|
583
|
+
container.querySelectorAll('.ext-activate-chat').forEach(btn => {
|
|
584
|
+
btn.addEventListener('click', async () => {
|
|
585
|
+
btn.disabled = true;
|
|
586
|
+
btn.innerHTML = '<span class="spinner-sm"></span> Activating...';
|
|
587
|
+
try {
|
|
588
|
+
await api.activateEngine(eng, btn.dataset.model, 'chat');
|
|
589
|
+
await this.loadLlmConfig();
|
|
590
|
+
await this.loadEngines();
|
|
591
|
+
} catch (e) {
|
|
592
|
+
console.error('Failed to activate external model:', e);
|
|
593
|
+
btn.disabled = false;
|
|
594
|
+
btn.innerHTML = '<i class="fas fa-play mr-1"></i>Activate';
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
container.querySelectorAll('.ext-activate-emb').forEach(btn => {
|
|
600
|
+
btn.addEventListener('click', async () => {
|
|
601
|
+
btn.disabled = true;
|
|
602
|
+
btn.innerHTML = '<span class="spinner-sm"></span> Activating...';
|
|
603
|
+
try {
|
|
604
|
+
await api.activateEngine(eng, btn.dataset.model, 'embedding');
|
|
605
|
+
await this.loadLlmConfig();
|
|
606
|
+
await this.loadEngines();
|
|
607
|
+
} catch (e) {
|
|
608
|
+
console.error('Failed to activate external embedding:', e);
|
|
609
|
+
btn.disabled = false;
|
|
610
|
+
btn.innerHTML = '<i class="fas fa-vector-square mr-1"></i>Embed';
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
_isEngineRemote(eng) {
|
|
617
|
+
const defaultUrls = { ollama: 'http://localhost:11434', lmstudio: 'http://localhost:1234' };
|
|
618
|
+
const url = this._engineUrls?.[eng] || defaultUrls[eng] || '';
|
|
619
|
+
try {
|
|
620
|
+
const host = new URL(url).hostname;
|
|
621
|
+
return host !== 'localhost' && host !== '127.0.0.1' && host !== '::1';
|
|
622
|
+
} catch { return false; }
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
_detectExternalCaps(model, engine) {
|
|
626
|
+
const name = model.name || '';
|
|
627
|
+
const caps = model.capabilities || [];
|
|
628
|
+
const type = model.type || ''; // LM Studio: vlm, llm, embeddings
|
|
629
|
+
// When API provides capability data, trust it over name heuristics
|
|
630
|
+
const hasApiCaps = caps.length > 0 || !!type;
|
|
631
|
+
|
|
632
|
+
// Embedding detection: API capabilities, LM Studio type, or name heuristics
|
|
633
|
+
// Always use name heuristics for embedding — LM Studio doesn't flag embedding models
|
|
634
|
+
const embedding = caps.includes('embedding') || caps.includes('embeddings')
|
|
635
|
+
|| type === 'embedding' || type === 'embeddings'
|
|
636
|
+
|| /embed|MiniLM|bge-|e5-|gte-|nomic/i.test(name);
|
|
637
|
+
|
|
638
|
+
// Tools: API capability, fall back to name heuristics only without API data
|
|
639
|
+
const tools = caps.includes('tools') || caps.includes('tool_use')
|
|
640
|
+
|| (!hasApiCaps && TOOL_NAME_PATTERNS.some(p => p.test(name)));
|
|
641
|
+
|
|
642
|
+
// Vision: API capability, LM Studio type, or name heuristics as fallback
|
|
643
|
+
const vision = caps.includes('vision')
|
|
644
|
+
|| type === 'vlm'
|
|
645
|
+
|| (!hasApiCaps && VISION_NAME_PATTERNS.some(p => p.test(name)));
|
|
646
|
+
|
|
647
|
+
// Reasoning: API capability or name heuristics as fallback
|
|
648
|
+
const reasoning = caps.includes('thinking') || caps.includes('reasoning')
|
|
649
|
+
|| (!hasApiCaps && REASONING_NAME_PATTERNS.some(p => p.test(name)));
|
|
650
|
+
|
|
651
|
+
return { tools, vision, reasoning, embedding };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
_renderExternalStatus(container) {
|
|
655
|
+
const eng = this._selectedEngine;
|
|
656
|
+
const engineData = this._engines?.[eng];
|
|
657
|
+
const available = engineData?.available;
|
|
658
|
+
const running = engineData?.running || [];
|
|
659
|
+
const label = ENGINE_LABELS[eng];
|
|
660
|
+
const totalRam = this.systemRamBytes;
|
|
661
|
+
const freeRam = this.status?.freeRamBytes || 0;
|
|
662
|
+
|
|
663
|
+
const currentDefault = this._resolveDefault('models');
|
|
664
|
+
const currentEmbDefault = this._resolveDefault('embeddings');
|
|
665
|
+
const activeChatModel = currentDefault?.engine === eng ? currentDefault?.model : null;
|
|
666
|
+
const activeEmbModel = currentEmbDefault?.engine === eng ? currentEmbDefault?.model : null;
|
|
667
|
+
const currentCtx = currentDefault?.contextSize || 8192;
|
|
668
|
+
const currentMaxTokens = currentDefault?.maxTokens || 4096;
|
|
669
|
+
|
|
670
|
+
// Cross-reference config with server loaded state
|
|
671
|
+
const models = engineData?.models || [];
|
|
672
|
+
const activeModelData = activeChatModel ? models.find(m => m.name === activeChatModel) : null;
|
|
673
|
+
const isChatLoaded = !!activeModelData?.loaded;
|
|
674
|
+
const maxCtxFromApi = activeModelData?.maxContextLength || null; // LM Studio provides this
|
|
675
|
+
|
|
676
|
+
// Detect if engine is on a remote host (non-localhost base URL)
|
|
677
|
+
const defaultUrls = { ollama: 'http://localhost:11434', lmstudio: 'http://localhost:1234' };
|
|
678
|
+
const effectiveUrl = this._engineUrls?.[eng] || defaultUrls[eng] || '';
|
|
679
|
+
const isRemote = this._isEngineRemote(eng);
|
|
680
|
+
|
|
681
|
+
// Calculate total VRAM used by running models (Ollama provides this)
|
|
682
|
+
const totalVram = running.reduce((sum, r) => sum + (r.sizeVram || 0), 0);
|
|
683
|
+
|
|
684
|
+
let html = `<div class="llm-server-panel">`;
|
|
685
|
+
|
|
686
|
+
// --- Header ---
|
|
687
|
+
html += `
|
|
688
|
+
<div class="llm-server-header">
|
|
689
|
+
<div class="flex items-center gap-2">
|
|
690
|
+
${ENGINE_ICONS[eng]}
|
|
691
|
+
<span class="text-sm font-semibold text-primary">${label}</span>
|
|
692
|
+
<span class="${available ? 'llm-pulse llm-pulse-green' : 'llm-pulse-off'}"></span>
|
|
693
|
+
<span class="text-xs ${available ? 'text-green' : 'text-red'}">${available ? 'Connected' : 'Not detected / Not running'}</span>
|
|
694
|
+
</div>
|
|
695
|
+
<button id="refreshEnginesBtn" class="btn-ghost text-xs flex-shrink-0">
|
|
696
|
+
<i class="fas fa-sync-alt mr-1"></i>Refresh
|
|
697
|
+
</button>
|
|
698
|
+
</div>`;
|
|
699
|
+
|
|
700
|
+
// --- Base URL override ---
|
|
701
|
+
const currentUrl = this._engineUrls?.[eng] || '';
|
|
702
|
+
html += `
|
|
703
|
+
<div class="llm-server-section">
|
|
704
|
+
<div class="llm-section-content flex items-center gap-2">
|
|
705
|
+
<label class="text-xs text-muted flex-shrink-0" for="engineUrlInput">Base URL</label>
|
|
706
|
+
<input type="text" id="engineUrlInput" class="input input-sm flex-1 font-mono text-xs"
|
|
707
|
+
value="${escapeHtml(currentUrl)}" placeholder="${defaultUrls[eng] || ''}" />
|
|
708
|
+
<button id="engineUrlSaveBtn" class="btn btn-accent btn-sm hidden">
|
|
709
|
+
<i class="fas fa-save mr-1"></i>Save
|
|
710
|
+
</button>
|
|
711
|
+
<button id="engineUrlResetBtn" class="btn-ghost text-xs ${currentUrl === defaultUrls[eng] || !currentUrl ? 'hidden' : ''}" title="Reset to default">
|
|
712
|
+
<i class="fas fa-undo"></i>
|
|
713
|
+
</button>
|
|
714
|
+
</div>
|
|
715
|
+
</div>`;
|
|
716
|
+
|
|
717
|
+
if (!available) {
|
|
718
|
+
html += `
|
|
719
|
+
<div class="llm-section-content flex items-center gap-3 py-2">
|
|
720
|
+
<span class="text-sm text-muted">Make sure ${label} is running${isRemote ? ` at ${escapeHtml(effectiveUrl)}` : ' on your machine'}</span>
|
|
721
|
+
</div>`;
|
|
722
|
+
} else {
|
|
723
|
+
// --- Server details (RAM + models loaded) ---
|
|
724
|
+
// Only show local system RAM when the engine is running on localhost
|
|
725
|
+
if (!isRemote) {
|
|
726
|
+
const ramUsedPct = totalRam ? Math.round(((totalRam - freeRam) / totalRam) * 100) : 0;
|
|
727
|
+
const ramBarCls = ramUsedPct > 80 ? 'llm-mem-red' : ramUsedPct > 60 ? 'llm-mem-amber' : 'llm-mem-green';
|
|
728
|
+
html += `
|
|
729
|
+
<div class="llm-server-details">
|
|
730
|
+
<span title="System RAM"><i class="fas fa-memory mr-1 llm-icon-dim"></i>${formatBytes(totalRam - freeRam)} / ${formatBytes(totalRam)} RAM</span>
|
|
731
|
+
${totalVram ? `<span title="VRAM used by loaded models"><i class="fas fa-bolt mr-1 llm-icon-dim"></i>${formatBytes(totalVram)} VRAM</span>` : ''}
|
|
732
|
+
<span title="Models loaded"><i class="fas fa-cube mr-1 llm-icon-dim"></i>${running.length} loaded</span>
|
|
733
|
+
</div>`;
|
|
734
|
+
|
|
735
|
+
if (totalRam) {
|
|
736
|
+
html += `
|
|
737
|
+
<div class="llm-mem-bar" title="${ramUsedPct}% RAM used">
|
|
738
|
+
<div class="llm-mem-fill ${ramBarCls}" style="width: ${ramUsedPct}%"></div>
|
|
739
|
+
</div>`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// --- GPU VRAM bar (NVIDIA discrete GPUs) ---
|
|
743
|
+
const gpuVram = this.status?.gpuVram;
|
|
744
|
+
if (gpuVram) {
|
|
745
|
+
const vramPct = Math.round((gpuVram.usedBytes / gpuVram.totalBytes) * 100);
|
|
746
|
+
const vramBarCls = vramPct > 80 ? 'llm-mem-red' : vramPct > 60 ? 'llm-mem-amber' : 'llm-mem-green';
|
|
747
|
+
html += `
|
|
748
|
+
<div class="llm-server-details">
|
|
749
|
+
<span title="GPU VRAM"><i class="fas fa-microchip mr-1 llm-icon-dim"></i>${formatBytes(gpuVram.usedBytes)} / ${formatBytes(gpuVram.totalBytes)} VRAM</span>
|
|
750
|
+
</div>
|
|
751
|
+
<div class="llm-mem-bar" title="${vramPct}% VRAM used">
|
|
752
|
+
<div class="llm-mem-fill ${vramBarCls}" style="width: ${vramPct}%"></div>
|
|
753
|
+
</div>`;
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
// Remote engine: only show model count and VRAM from API data
|
|
757
|
+
html += `
|
|
758
|
+
<div class="llm-server-details">
|
|
759
|
+
${totalVram ? `<span title="VRAM used by loaded models"><i class="fas fa-bolt mr-1 llm-icon-dim"></i>${formatBytes(totalVram)} VRAM</span>` : ''}
|
|
760
|
+
<span title="Models loaded"><i class="fas fa-cube mr-1 llm-icon-dim"></i>${running.length} loaded</span>
|
|
761
|
+
</div>`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// --- Running models with unload ---
|
|
765
|
+
if (running.length) {
|
|
766
|
+
html += running.map(r => {
|
|
767
|
+
const vramStr = r.sizeVram ? formatBytes(r.sizeVram) : '';
|
|
768
|
+
const ctxStr = r.contextLength ? `${(r.contextLength / 1024).toFixed(0)}K ctx` : '';
|
|
769
|
+
const sizeStr = r.size ? formatBytes(r.size) : '';
|
|
770
|
+
const meta = [sizeStr, vramStr, ctxStr].filter(Boolean).join(' · ');
|
|
771
|
+
return `
|
|
772
|
+
<div class="llm-server-section">
|
|
773
|
+
<div class="llm-section-content flex items-center justify-between">
|
|
774
|
+
<div class="flex items-center gap-3">
|
|
775
|
+
<span class="llm-pulse llm-pulse-green"></span>
|
|
776
|
+
<span class="badge badge-amber font-mono">${escapeHtml(r.name)}</span>
|
|
777
|
+
${meta ? `<span class="text-xs text-muted">${meta}</span>` : ''}
|
|
778
|
+
</div>
|
|
779
|
+
<button class="unload-btn btn btn-danger btn-sm" data-model="${escapeHtml(r.name)}"
|
|
780
|
+
${r.instanceId ? `data-instance-id="${escapeHtml(r.instanceId)}"` : ''}>
|
|
781
|
+
<i class="fas fa-eject mr-1"></i>Unload
|
|
782
|
+
</button>
|
|
783
|
+
</div>
|
|
784
|
+
</div>`;
|
|
785
|
+
}).join('');
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// --- Chat model config (sliders) ---
|
|
789
|
+
html += `<div class="llm-server-section">`;
|
|
790
|
+
if (activeChatModel) {
|
|
791
|
+
html += `
|
|
792
|
+
<div class="llm-section-content space-y-2">
|
|
793
|
+
<div class="flex items-center gap-3">
|
|
794
|
+
<i class="fas fa-comments text-amber text-xs"></i>
|
|
795
|
+
<span class="text-sm text-primary">Chat Config</span>
|
|
796
|
+
<span class="text-xs text-muted font-mono">${escapeHtml(activeChatModel)}</span>
|
|
797
|
+
${!isChatLoaded ? '<span class="text-2xs text-red">not loaded</span>' : ''}
|
|
798
|
+
</div>
|
|
799
|
+
<div class="llm-sliders-section">
|
|
800
|
+
<div class="llm-slider-row">
|
|
801
|
+
<label class="llm-slider-label">
|
|
802
|
+
<span>Context Size</span>
|
|
803
|
+
<span id="extCtxValue" class="font-mono">${currentCtx >= 1024 ? `${(currentCtx / 1024).toFixed(0)}K` : currentCtx}</span>
|
|
804
|
+
</label>
|
|
805
|
+
<input type="range" id="extCtxSlider" class="llm-range"
|
|
806
|
+
data-orig="${currentCtx}"
|
|
807
|
+
min="2048" max="${maxCtxFromApi || 131072}" step="1024" value="${Math.min(currentCtx, maxCtxFromApi || 131072)}" />
|
|
808
|
+
<div class="llm-slider-meta">
|
|
809
|
+
<span>2K</span>
|
|
810
|
+
<span class="text-2xs text-muted">${eng === 'ollama' ? 'Sent as num_ctx per request' : 'Reloads model in LM Studio'}</span>
|
|
811
|
+
<span>${maxCtxFromApi ? `${(maxCtxFromApi / 1024).toFixed(0)}K` : '128K'}</span>
|
|
812
|
+
</div>
|
|
813
|
+
</div>
|
|
814
|
+
<div class="llm-slider-row">
|
|
815
|
+
<label class="llm-slider-label">
|
|
816
|
+
<span>Max Tokens</span>
|
|
817
|
+
<span id="extMaxTokValue" class="font-mono">${currentMaxTokens.toLocaleString()}</span>
|
|
818
|
+
</label>
|
|
819
|
+
<input type="range" id="extMaxTokSlider" class="llm-range"
|
|
820
|
+
data-orig="${currentMaxTokens}"
|
|
821
|
+
min="256" max="16384" step="256" value="${currentMaxTokens}" />
|
|
822
|
+
<div class="llm-slider-meta">
|
|
823
|
+
<span>256</span>
|
|
824
|
+
<span>16K</span>
|
|
825
|
+
</div>
|
|
826
|
+
</div>
|
|
827
|
+
<button id="extApplyBtn" class="btn btn-accent btn-sm hidden">
|
|
828
|
+
<i class="fas fa-save mr-1"></i>Apply
|
|
829
|
+
</button>
|
|
830
|
+
</div>
|
|
831
|
+
</div>`;
|
|
832
|
+
} else {
|
|
833
|
+
html += `
|
|
834
|
+
<div class="llm-section-content flex items-center gap-3">
|
|
835
|
+
<span class="llm-pulse-off"></span>
|
|
836
|
+
<span class="text-sm text-secondary">Chat Model</span>
|
|
837
|
+
<span class="text-xs text-muted">Activate a model below</span>
|
|
838
|
+
</div>`;
|
|
839
|
+
}
|
|
840
|
+
html += `</div>`;
|
|
841
|
+
|
|
842
|
+
// --- Embedding model ---
|
|
843
|
+
if (activeEmbModel) {
|
|
844
|
+
html += `
|
|
845
|
+
<div class="llm-server-section">
|
|
846
|
+
<div class="llm-section-content flex items-center gap-3">
|
|
847
|
+
<span class="llm-pulse llm-pulse-blue"></span>
|
|
848
|
+
<span class="text-sm text-primary">Embedding Model</span>
|
|
849
|
+
<span class="badge badge-blue font-mono">${escapeHtml(activeEmbModel)}</span>
|
|
850
|
+
</div>
|
|
851
|
+
</div>`;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
html += `</div>`;
|
|
856
|
+
container.innerHTML = html;
|
|
857
|
+
|
|
858
|
+
// --- Wire events ---
|
|
859
|
+
container.querySelector('#refreshEnginesBtn')?.addEventListener('click', () => this.loadEngines());
|
|
860
|
+
|
|
861
|
+
// Base URL input
|
|
862
|
+
const urlInput = container.querySelector('#engineUrlInput');
|
|
863
|
+
const urlSaveBtn = container.querySelector('#engineUrlSaveBtn');
|
|
864
|
+
const urlResetBtn = container.querySelector('#engineUrlResetBtn');
|
|
865
|
+
if (urlInput) {
|
|
866
|
+
const origUrl = urlInput.value;
|
|
867
|
+
urlInput.addEventListener('input', () => {
|
|
868
|
+
const changed = urlInput.value.replace(/\/+$/, '') !== origUrl;
|
|
869
|
+
urlSaveBtn?.classList.toggle('hidden', !changed);
|
|
870
|
+
});
|
|
871
|
+
urlInput.addEventListener('keydown', (e) => {
|
|
872
|
+
if (e.key === 'Enter') urlSaveBtn?.click();
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
urlSaveBtn?.addEventListener('click', async () => {
|
|
876
|
+
const newUrl = urlInput.value.trim();
|
|
877
|
+
if (!newUrl) return;
|
|
878
|
+
urlSaveBtn.disabled = true;
|
|
879
|
+
urlSaveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>Saving...';
|
|
880
|
+
try {
|
|
881
|
+
await api.setEngineUrl(eng, newUrl);
|
|
882
|
+
await this.loadEngines();
|
|
883
|
+
} catch (e) {
|
|
884
|
+
console.error('Failed to set engine URL:', e);
|
|
885
|
+
urlSaveBtn.disabled = false;
|
|
886
|
+
urlSaveBtn.innerHTML = '<i class="fas fa-save mr-1"></i>Save';
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
urlResetBtn?.addEventListener('click', async () => {
|
|
890
|
+
const defaults = { ollama: 'http://localhost:11434', lmstudio: 'http://localhost:1234' };
|
|
891
|
+
urlResetBtn.disabled = true;
|
|
892
|
+
try {
|
|
893
|
+
await api.setEngineUrl(eng, defaults[eng]);
|
|
894
|
+
await this.loadEngines();
|
|
895
|
+
} catch (e) {
|
|
896
|
+
console.error('Failed to reset engine URL:', e);
|
|
897
|
+
urlResetBtn.disabled = false;
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Unload buttons
|
|
902
|
+
container.querySelectorAll('.unload-btn').forEach(btn => {
|
|
903
|
+
btn.addEventListener('click', async () => {
|
|
904
|
+
btn.disabled = true;
|
|
905
|
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>Unloading...';
|
|
906
|
+
try {
|
|
907
|
+
await api.unloadEngineModel(eng, btn.dataset.model, btn.dataset.instanceId);
|
|
908
|
+
await this.loadEngines();
|
|
909
|
+
} catch (e) {
|
|
910
|
+
console.error('Failed to unload model:', e);
|
|
911
|
+
btn.disabled = false;
|
|
912
|
+
btn.innerHTML = '<i class="fas fa-eject mr-1"></i>Unload';
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// Slider wiring
|
|
918
|
+
const extCtxSlider = container.querySelector('#extCtxSlider');
|
|
919
|
+
const extMaxTokSlider = container.querySelector('#extMaxTokSlider');
|
|
920
|
+
const extApplyBtn = container.querySelector('#extApplyBtn');
|
|
921
|
+
|
|
922
|
+
const updateApplyVisibility = () => {
|
|
923
|
+
const ctxChanged = extCtxSlider && extCtxSlider.value !== extCtxSlider.dataset.orig;
|
|
924
|
+
const tokChanged = extMaxTokSlider && extMaxTokSlider.value !== extMaxTokSlider.dataset.orig;
|
|
925
|
+
extApplyBtn?.classList.toggle('hidden', !ctxChanged && !tokChanged);
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
if (extCtxSlider) {
|
|
929
|
+
const ctxMax = parseInt(extCtxSlider.max);
|
|
930
|
+
const updateFill = () => {
|
|
931
|
+
const pos = ((parseInt(extCtxSlider.value) - 2048) / (ctxMax - 2048)) * 100;
|
|
932
|
+
extCtxSlider.style.setProperty('--range-color', 'var(--green)');
|
|
933
|
+
extCtxSlider.style.setProperty('--range-fill', `${pos}%`);
|
|
934
|
+
};
|
|
935
|
+
updateFill();
|
|
936
|
+
extCtxSlider.addEventListener('input', () => {
|
|
937
|
+
const val = parseInt(extCtxSlider.value);
|
|
938
|
+
container.querySelector('#extCtxValue').textContent = val >= 1024 ? `${(val / 1024).toFixed(0)}K` : val;
|
|
939
|
+
updateFill();
|
|
940
|
+
updateApplyVisibility();
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
if (extMaxTokSlider) {
|
|
944
|
+
const updateFill = () => {
|
|
945
|
+
const pos = ((parseInt(extMaxTokSlider.value) - 256) / (16384 - 256)) * 100;
|
|
946
|
+
extMaxTokSlider.style.setProperty('--range-color', 'var(--green)');
|
|
947
|
+
extMaxTokSlider.style.setProperty('--range-fill', `${pos}%`);
|
|
948
|
+
};
|
|
949
|
+
updateFill();
|
|
950
|
+
extMaxTokSlider.addEventListener('input', () => {
|
|
951
|
+
container.querySelector('#extMaxTokValue').textContent = parseInt(extMaxTokSlider.value).toLocaleString();
|
|
952
|
+
updateFill();
|
|
953
|
+
updateApplyVisibility();
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
extApplyBtn?.addEventListener('click', async () => {
|
|
958
|
+
extApplyBtn.disabled = true;
|
|
959
|
+
extApplyBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>Applying...';
|
|
960
|
+
try {
|
|
961
|
+
const newCtx = extCtxSlider ? parseInt(extCtxSlider.value) : currentCtx;
|
|
962
|
+
const newMaxTokens = extMaxTokSlider ? parseInt(extMaxTokSlider.value) : currentMaxTokens;
|
|
963
|
+
|
|
964
|
+
await api.setEngineContext(newCtx);
|
|
965
|
+
|
|
966
|
+
const resolvedKey = this._resolveDefaultKey('models');
|
|
967
|
+
const existing = this._resolveDefault('models') || {};
|
|
968
|
+
await api.saveLlmModel(resolvedKey, {
|
|
969
|
+
...existing,
|
|
970
|
+
provider: existing.provider || 'local',
|
|
971
|
+
maxTokens: newMaxTokens,
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
await this.loadLlmConfig();
|
|
975
|
+
if (extCtxSlider) extCtxSlider.dataset.orig = extCtxSlider.value;
|
|
976
|
+
if (extMaxTokSlider) extMaxTokSlider.dataset.orig = extMaxTokSlider.value;
|
|
977
|
+
extApplyBtn.classList.add('hidden');
|
|
978
|
+
} catch (e) {
|
|
979
|
+
console.error('Failed to apply settings:', e);
|
|
980
|
+
} finally {
|
|
981
|
+
extApplyBtn.disabled = false;
|
|
982
|
+
extApplyBtn.innerHTML = '<i class="fas fa-save mr-1"></i>Apply';
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
renderStatus() {
|
|
988
|
+
const container = this.querySelector('#statusBar');
|
|
989
|
+
if (!container) return;
|
|
990
|
+
|
|
991
|
+
// External engines get their own status panel
|
|
992
|
+
if (EXTERNAL_ENGINES.includes(this._selectedEngine)) {
|
|
993
|
+
this._renderExternalStatus(container);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (!this.status) return;
|
|
998
|
+
|
|
999
|
+
const isMlx = this._selectedEngine === 'mlx-serve';
|
|
1000
|
+
// Get per-engine status from the engines map
|
|
1001
|
+
const engineStatus = this.status.engines?.[this._selectedEngine];
|
|
1002
|
+
const chatStatus = engineStatus?.chat || {};
|
|
1003
|
+
const embeddingStatus = engineStatus?.embedding || {};
|
|
1004
|
+
|
|
1005
|
+
const running = chatStatus.running || false;
|
|
1006
|
+
const activeModel = running ? chatStatus.activeModel : null;
|
|
1007
|
+
// Show embedding if this engine has an active embedding server
|
|
1008
|
+
const embedding = embeddingStatus.running ? embeddingStatus : null;
|
|
1009
|
+
const version = isMlx ? this.status.mlxVersion : this.status.llamaVersion;
|
|
1010
|
+
const versionLabel = isMlx
|
|
1011
|
+
? (version ? `v${escapeHtml(version)}` : '')
|
|
1012
|
+
: (version ? `b${escapeHtml(version.match(/^(\d+)/)?.[1] || version)}` : '');
|
|
1013
|
+
const binarySource = isMlx ? this.status.mlxBinarySource : this.status.binarySource;
|
|
1014
|
+
const gpuName = this.status.gpu?.name;
|
|
1015
|
+
const gpuAccel = this.status.gpu?.accel || 'none';
|
|
1016
|
+
const updateInfo = isMlx ? this.mlxUpdateInfo : this.updateInfo;
|
|
1017
|
+
const hasUpdate = updateInfo?.available === true;
|
|
1018
|
+
const daysLabel = hasUpdate && updateInfo.daysNewer != null
|
|
1019
|
+
? (updateInfo.daysNewer === 0 ? 'today' : `${updateInfo.daysNewer}d ago`)
|
|
1020
|
+
: '';
|
|
1021
|
+
|
|
1022
|
+
const accelLabels = { 'none': 'CPU', 'metal': 'Metal', 'vulkan': 'Vulkan', 'cuda-12.4': 'CUDA 12.4', 'cuda-13.1': 'CUDA 13.1' };
|
|
1023
|
+
const runtimeLabel = gpuName
|
|
1024
|
+
? `${gpuName} (${accelLabels[gpuAccel] || gpuAccel})`
|
|
1025
|
+
: (accelLabels[gpuAccel] || gpuAccel);
|
|
1026
|
+
|
|
1027
|
+
// --- Server header ---
|
|
1028
|
+
let html = `<div class="llm-server-panel">`;
|
|
1029
|
+
|
|
1030
|
+
// Server info header
|
|
1031
|
+
const configDefaultEngine = this._resolveDefault('models')?.engine || this.status.defaultEngine || null;
|
|
1032
|
+
const isDefaultEngine = this._selectedEngine === configDefaultEngine;
|
|
1033
|
+
|
|
1034
|
+
html += `
|
|
1035
|
+
<div class="llm-server-header">
|
|
1036
|
+
<div class="flex items-center gap-2">
|
|
1037
|
+
<i class="fas fa-server text-amber text-xs"></i>
|
|
1038
|
+
<span class="text-sm font-semibold text-primary">${isMlx ? 'mlx-serve' : 'llama-server'}</span>
|
|
1039
|
+
${versionLabel ? `<span class="text-xs text-muted font-mono">${versionLabel}</span>` : ''}
|
|
1040
|
+
<span class="${running ? 'llm-pulse llm-pulse-green' : 'llm-pulse-off'}"></span>
|
|
1041
|
+
<span class="text-xs ${running ? 'text-green' : 'text-muted'}">${running ? 'Running' : 'Stopped'}</span>
|
|
1042
|
+
${isDefaultEngine ? '<span class="badge badge-green text-2xs">default</span>' : ''}
|
|
1043
|
+
</div>
|
|
1044
|
+
${binarySource === 'managed' ? (
|
|
1045
|
+
hasUpdate ? `
|
|
1046
|
+
<button id="updateBinaryBtn" class="btn btn-indigo btn-sm flex-shrink-0" title="Latest: ${escapeHtml(updateInfo.latestTag || '')}">
|
|
1047
|
+
<i class="fas fa-arrows-rotate mr-1"></i>Update${daysLabel ? ` <span class="text-muted">(${daysLabel})</span>` : ''}
|
|
1048
|
+
</button>`
|
|
1049
|
+
: !updateInfo ? `
|
|
1050
|
+
<button id="checkUpdateBtn" class="btn-ghost text-xs flex-shrink-0">
|
|
1051
|
+
<i class="fas fa-arrows-rotate mr-1"></i>Check for updates
|
|
1052
|
+
</button>` : ''
|
|
1053
|
+
) : ''}
|
|
1054
|
+
</div>`;
|
|
1055
|
+
|
|
1056
|
+
// Server details row
|
|
1057
|
+
html += `
|
|
1058
|
+
<div class="llm-server-details">
|
|
1059
|
+
<span title="Runtime"><i class="fas fa-bolt mr-1 llm-icon-dim"></i>${escapeHtml(runtimeLabel)}</span>
|
|
1060
|
+
<span title="Server type"><i class="fas fa-comments mr-1 llm-icon-dim"></i>Chat Completions</span>
|
|
1061
|
+
${running && chatStatus.port ? `
|
|
1062
|
+
<span class="flex items-center gap-1" title="OpenAI-compatible API endpoint">
|
|
1063
|
+
<i class="fas fa-link mr-1 llm-icon-dim"></i>
|
|
1064
|
+
<code class="font-mono text-secondary">http://127.0.0.1:${chatStatus.port}/v1</code>
|
|
1065
|
+
<button class="copy-url-btn text-muted transition-colors" data-url="http://127.0.0.1:${chatStatus.port}/v1" title="Copy URL">
|
|
1066
|
+
<i class="fas fa-copy text-2xs"></i>
|
|
1067
|
+
</button>
|
|
1068
|
+
</span>` : `<span><i class="fas fa-link mr-1 llm-icon-dim"></i><span class="text-muted">Not running</span></span>`}
|
|
1069
|
+
</div>`;
|
|
1070
|
+
|
|
1071
|
+
// --- System RAM bar ---
|
|
1072
|
+
const totalRamManaged = this.status.systemRamBytes;
|
|
1073
|
+
const freeRamManaged = this.status.freeRamBytes;
|
|
1074
|
+
if (totalRamManaged) {
|
|
1075
|
+
const ramUsedPct = Math.round(((totalRamManaged - freeRamManaged) / totalRamManaged) * 100);
|
|
1076
|
+
const ramBarCls = ramUsedPct > 80 ? 'llm-mem-red' : ramUsedPct > 60 ? 'llm-mem-amber' : 'llm-mem-green';
|
|
1077
|
+
html += `
|
|
1078
|
+
<div class="llm-server-details">
|
|
1079
|
+
<span title="System RAM"><i class="fas fa-memory mr-1 llm-icon-dim"></i>${formatBytes(totalRamManaged - freeRamManaged)} / ${formatBytes(totalRamManaged)} RAM</span>
|
|
1080
|
+
</div>
|
|
1081
|
+
<div class="llm-mem-bar" title="${ramUsedPct}% RAM used">
|
|
1082
|
+
<div class="llm-mem-fill ${ramBarCls}" style="width: ${ramUsedPct}%"></div>
|
|
1083
|
+
</div>`;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// --- GPU VRAM bar (NVIDIA discrete GPUs) ---
|
|
1087
|
+
const gpuVram = this.status.gpuVram;
|
|
1088
|
+
if (gpuVram) {
|
|
1089
|
+
const vramPct = Math.round((gpuVram.usedBytes / gpuVram.totalBytes) * 100);
|
|
1090
|
+
const vramBarCls = vramPct > 80 ? 'llm-mem-red' : vramPct > 60 ? 'llm-mem-amber' : 'llm-mem-green';
|
|
1091
|
+
html += `
|
|
1092
|
+
<div class="llm-server-details">
|
|
1093
|
+
<span title="GPU VRAM"><i class="fas fa-microchip mr-1 llm-icon-dim"></i>${formatBytes(gpuVram.usedBytes)} / ${formatBytes(gpuVram.totalBytes)} VRAM</span>
|
|
1094
|
+
</div>
|
|
1095
|
+
<div class="llm-mem-bar" title="${vramPct}% VRAM used">
|
|
1096
|
+
<div class="llm-mem-fill ${vramBarCls}" style="width: ${vramPct}%"></div>
|
|
1097
|
+
</div>`;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// --- Chat model (child) ---
|
|
1101
|
+
html += `<div class="llm-server-section">`;
|
|
1102
|
+
if (running) {
|
|
1103
|
+
const modelName = activeModel ? activeModel.split('/').pop() : 'Unknown';
|
|
1104
|
+
const mem = chatStatus.memoryEstimate;
|
|
1105
|
+
const totalRam = this.status.systemRamBytes;
|
|
1106
|
+
const ctxSize = chatStatus.contextSize;
|
|
1107
|
+
const memPct = mem && totalRam ? Math.round((mem.totalBytes / totalRam) * 100) : null;
|
|
1108
|
+
const memBarCls = memPct > 80 ? 'llm-mem-red' : memPct > 60 ? 'llm-mem-amber' : 'llm-mem-green';
|
|
1109
|
+
const kvPerToken = mem && ctxSize ? mem.kvCacheBytes / ctxSize : 0;
|
|
1110
|
+
const resolvedDefault = this._resolveDefault('models');
|
|
1111
|
+
const currentMaxTokens = resolvedDefault?.maxTokens || 4096;
|
|
1112
|
+
const currentReasoningBudget = resolvedDefault?.reasoningBudget || 0;
|
|
1113
|
+
const activeModelObj = this.models.find(m => m.filePath === activeModel);
|
|
1114
|
+
const modelCaps = activeModelObj ? detectCapabilitiesFromFile(activeModelObj) : { reasoning: false };
|
|
1115
|
+
const thinkingEnabled = currentReasoningBudget > 0;
|
|
1116
|
+
|
|
1117
|
+
html += `
|
|
1118
|
+
<div class="llm-section-content space-y-2">
|
|
1119
|
+
<div class="flex items-center justify-between">
|
|
1120
|
+
<div class="flex items-center gap-3">
|
|
1121
|
+
<span class="llm-pulse llm-pulse-green"></span>
|
|
1122
|
+
<span class="text-sm text-primary">Chat Model</span>
|
|
1123
|
+
<span class="badge badge-amber font-mono">${escapeHtml(modelName)}</span>
|
|
1124
|
+
${ctxSize ? `<span class="text-xs text-muted">${(ctxSize / 1024).toFixed(0)}K ctx</span>` : ''}
|
|
1125
|
+
</div>
|
|
1126
|
+
<button id="stopBtn" class="btn btn-danger btn-sm">
|
|
1127
|
+
<i class="fas fa-stop mr-1"></i>Stop
|
|
1128
|
+
</button>
|
|
1129
|
+
</div>
|
|
1130
|
+
${mem ? `
|
|
1131
|
+
<div class="flex items-center gap-3">
|
|
1132
|
+
<div class="llm-mem-bar">
|
|
1133
|
+
<div class="llm-mem-fill ${memBarCls}" style="width: ${memPct}%"></div>
|
|
1134
|
+
</div>
|
|
1135
|
+
<div class="flex items-center gap-3 text-xs text-muted flex-shrink-0">
|
|
1136
|
+
<span title="Model weights"><i class="fas fa-cube mr-1"></i>${formatBytes(mem.modelBytes)}</span>
|
|
1137
|
+
<span title="KV cache"><i class="fas fa-memory mr-1"></i>${formatBytes(mem.kvCacheBytes)}</span>
|
|
1138
|
+
<span title="Total estimated / System RAM">${formatBytes(mem.totalBytes)} / ${formatBytes(totalRam)}</span>
|
|
1139
|
+
</div>
|
|
1140
|
+
</div>` : ''}
|
|
1141
|
+
<div class="llm-sliders-section">
|
|
1142
|
+
<div class="llm-slider-row">
|
|
1143
|
+
<label class="llm-slider-label">
|
|
1144
|
+
<span>Context Size</span>
|
|
1145
|
+
<span id="ctxValue" class="font-mono">${ctxSize >= 1024 ? `${(ctxSize / 1024).toFixed(0)}K` : ctxSize}</span>
|
|
1146
|
+
</label>
|
|
1147
|
+
<input type="range" id="ctxSlider" class="llm-range"
|
|
1148
|
+
data-kv-per-token="${kvPerToken}" data-model-bytes="${mem?.modelBytes || 0}" data-total-ram="${totalRam || 0}"
|
|
1149
|
+
data-orig="${ctxSize || 8192}"
|
|
1150
|
+
min="2048" max="131072" step="1024" value="${ctxSize || 8192}" />
|
|
1151
|
+
<div class="llm-slider-meta">
|
|
1152
|
+
<span>2K</span>
|
|
1153
|
+
<span id="ctxMemEstimate" class="font-mono">${formatBytes(mem?.kvCacheBytes || 0)} KV + ${formatBytes(mem?.modelBytes || 0)} model = ${formatBytes(mem?.totalBytes || 0)} / ${formatBytes(totalRam)}</span>
|
|
1154
|
+
<span>128K</span>
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
<div class="llm-slider-row">
|
|
1158
|
+
<label class="llm-slider-label">
|
|
1159
|
+
<span>Max Tokens</span>
|
|
1160
|
+
<span id="maxTokValue" class="font-mono">${currentMaxTokens.toLocaleString()}</span>
|
|
1161
|
+
</label>
|
|
1162
|
+
<input type="range" id="maxTokSlider" class="llm-range"
|
|
1163
|
+
data-orig="${currentMaxTokens}"
|
|
1164
|
+
min="256" max="16384" step="256" value="${currentMaxTokens}" />
|
|
1165
|
+
<div class="llm-slider-meta">
|
|
1166
|
+
<span>256</span>
|
|
1167
|
+
<span>16K</span>
|
|
1168
|
+
</div>
|
|
1169
|
+
</div>
|
|
1170
|
+
${modelCaps.reasoning ? `
|
|
1171
|
+
<div class="llm-thinking-row">
|
|
1172
|
+
<div class="flex items-center justify-between">
|
|
1173
|
+
<label class="llm-slider-label flex items-center gap-2">
|
|
1174
|
+
<span><i class="fas fa-brain text-purple mr-1"></i>Thinking</span>
|
|
1175
|
+
<span id="thinkingValue" class="font-mono text-xs ${thinkingEnabled ? (currentReasoningBudget > 256 ? 'text-red' : 'text-purple') : 'text-muted'}">${thinkingEnabled ? currentReasoningBudget.toLocaleString() + ' tokens' : 'Off'}</span>
|
|
1176
|
+
</label>
|
|
1177
|
+
<label class="llm-toggle">
|
|
1178
|
+
<input type="checkbox" id="thinkingToggle" data-orig="${currentReasoningBudget}" ${thinkingEnabled ? 'checked' : ''} />
|
|
1179
|
+
<span class="llm-toggle-slider"></span>
|
|
1180
|
+
</label>
|
|
1181
|
+
</div>
|
|
1182
|
+
<div id="thinkingSliderWrap" class="${thinkingEnabled ? '' : 'hidden'}">
|
|
1183
|
+
<input type="range" id="thinkingSlider" class="llm-range"
|
|
1184
|
+
data-orig="${currentReasoningBudget || 128}"
|
|
1185
|
+
min="128" max="1024" step="128" value="${currentReasoningBudget || 128}" />
|
|
1186
|
+
<div class="llm-slider-meta">
|
|
1187
|
+
<span>128</span>
|
|
1188
|
+
<span class="text-2xs text-muted">Requires server restart</span>
|
|
1189
|
+
<span>1K</span>
|
|
1190
|
+
</div>
|
|
1191
|
+
</div>
|
|
1192
|
+
</div>` : ''}
|
|
1193
|
+
<div id="sliderWarning" class="llm-slider-warning hidden"></div>
|
|
1194
|
+
<button id="applySettingsBtn" class="btn btn-accent btn-sm hidden">
|
|
1195
|
+
<i class="fas fa-save mr-1"></i>Apply
|
|
1196
|
+
</button>
|
|
1197
|
+
</div>
|
|
1198
|
+
</div>`;
|
|
1199
|
+
} else {
|
|
1200
|
+
const lastModel = this.status.lastActiveModel;
|
|
1201
|
+
const lastModelName = lastModel ? this.models.find(m => m.id === lastModel) : null;
|
|
1202
|
+
html += `
|
|
1203
|
+
<div class="llm-section-content flex items-center justify-between">
|
|
1204
|
+
<div class="flex items-center gap-3">
|
|
1205
|
+
<span class="llm-pulse-off"></span>
|
|
1206
|
+
<span class="text-sm text-secondary">Chat Model</span>
|
|
1207
|
+
${lastModelName
|
|
1208
|
+
? `<span class="text-xs text-muted">${escapeHtml(lastModelName.fileName)}</span>`
|
|
1209
|
+
: '<span class="text-xs text-muted">Activate a model to start</span>'}
|
|
1210
|
+
</div>
|
|
1211
|
+
${lastModelName
|
|
1212
|
+
? `<button id="startLastBtn" class="btn btn-amber btn-sm" data-id="${escapeHtml(lastModel)}">
|
|
1213
|
+
<i class="fas fa-play mr-1"></i>Start
|
|
1214
|
+
</button>`
|
|
1215
|
+
: ''}
|
|
1216
|
+
</div>`;
|
|
1217
|
+
}
|
|
1218
|
+
html += `</div>`;
|
|
1219
|
+
|
|
1220
|
+
// --- Embedding model (child) ---
|
|
1221
|
+
if (embedding?.running) {
|
|
1222
|
+
const embName = embedding.activeModel ? embedding.activeModel.split('/').pop() : 'Unknown';
|
|
1223
|
+
html += `
|
|
1224
|
+
<div class="llm-server-section">
|
|
1225
|
+
<div class="llm-section-content flex items-center justify-between">
|
|
1226
|
+
<div class="flex items-center gap-3">
|
|
1227
|
+
<span class="llm-pulse llm-pulse-blue"></span>
|
|
1228
|
+
<span class="text-sm text-primary">Embedding Model</span>
|
|
1229
|
+
<span class="badge badge-blue font-mono">${escapeHtml(embName)}</span>
|
|
1230
|
+
</div>
|
|
1231
|
+
<button id="stopEmbBtn" class="btn btn-danger btn-sm">
|
|
1232
|
+
<i class="fas fa-stop mr-1"></i>Stop
|
|
1233
|
+
</button>
|
|
1234
|
+
</div>
|
|
1235
|
+
</div>`;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
html += `</div>`; // close outer container
|
|
1239
|
+
|
|
1240
|
+
container.innerHTML = html;
|
|
1241
|
+
this.querySelector('#stopBtn')?.addEventListener('click', () => this.stopServer());
|
|
1242
|
+
this.querySelector('#stopEmbBtn')?.addEventListener('click', () => this.stopEmbedding());
|
|
1243
|
+
this.querySelector('#startLastBtn')?.addEventListener('click', (e) => this.activateModel(e.currentTarget.dataset.id));
|
|
1244
|
+
this.querySelector('.copy-url-btn')?.addEventListener('click', (e) => {
|
|
1245
|
+
navigator.clipboard.writeText(e.currentTarget.dataset.url);
|
|
1246
|
+
const icon = e.currentTarget.querySelector('i');
|
|
1247
|
+
icon.className = 'fas fa-check text-2xs text-green';
|
|
1248
|
+
setTimeout(() => { icon.className = 'fas fa-copy text-2xs'; }, 1500);
|
|
1249
|
+
});
|
|
1250
|
+
this.querySelector('#updateBinaryBtn')?.addEventListener('click', () => this.updateBinary());
|
|
1251
|
+
this.querySelector('#checkUpdateBtn')?.addEventListener('click', () => this.checkForUpdate());
|
|
1252
|
+
|
|
1253
|
+
const ctxSlider = this.querySelector('#ctxSlider');
|
|
1254
|
+
const maxTokSlider = this.querySelector('#maxTokSlider');
|
|
1255
|
+
const applyBtn = this.querySelector('#applySettingsBtn');
|
|
1256
|
+
const warningEl = this.querySelector('#sliderWarning');
|
|
1257
|
+
|
|
1258
|
+
const updateSliderTrack = (slider, pct) => {
|
|
1259
|
+
const color = pct > 80 ? 'var(--red-400)' : pct > 60 ? 'var(--amber-400)' : 'var(--green)';
|
|
1260
|
+
const pos = ((parseInt(slider.value) - slider.min) / (slider.max - slider.min)) * 100;
|
|
1261
|
+
slider.style.setProperty('--range-color', color);
|
|
1262
|
+
slider.style.setProperty('--range-fill', `${pos}%`);
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
const thinkingToggle = this.querySelector('#thinkingToggle');
|
|
1266
|
+
const thinkingSlider = this.querySelector('#thinkingSlider');
|
|
1267
|
+
const thinkingSliderWrap = this.querySelector('#thinkingSliderWrap');
|
|
1268
|
+
|
|
1269
|
+
const getThinkingBudget = () => {
|
|
1270
|
+
if (!thinkingToggle) return 0;
|
|
1271
|
+
return thinkingToggle.checked ? parseInt(thinkingSlider?.value || '128') : 0;
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
const updateApplyVisibility = () => {
|
|
1275
|
+
const ctxChanged = ctxSlider && ctxSlider.value !== ctxSlider.dataset.orig;
|
|
1276
|
+
const tokChanged = maxTokSlider && maxTokSlider.value !== maxTokSlider.dataset.orig;
|
|
1277
|
+
const thinkingChanged = thinkingToggle && String(getThinkingBudget()) !== thinkingToggle.dataset.orig;
|
|
1278
|
+
const changed = ctxChanged || tokChanged || thinkingChanged;
|
|
1279
|
+
applyBtn?.classList.toggle('hidden', !changed);
|
|
1280
|
+
if (!changed && warningEl) { warningEl.classList.add('hidden'); warningEl.textContent = ''; }
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
const computeRamPct = () => {
|
|
1284
|
+
if (!ctxSlider) return 0;
|
|
1285
|
+
const kvPT = parseFloat(ctxSlider.dataset.kvPerToken);
|
|
1286
|
+
const modelB = parseInt(ctxSlider.dataset.modelBytes);
|
|
1287
|
+
const ram = parseInt(ctxSlider.dataset.totalRam);
|
|
1288
|
+
const newKv = parseInt(ctxSlider.value) * kvPT;
|
|
1289
|
+
return ram ? Math.round((modelB + newKv) / ram * 100) : 0;
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
const updateWarning = (pct) => {
|
|
1293
|
+
if (!warningEl) return;
|
|
1294
|
+
if (pct > 90) {
|
|
1295
|
+
warningEl.textContent = 'Exceeds available RAM — will cause heavy swapping and very slow performance';
|
|
1296
|
+
warningEl.className = 'llm-slider-warning llm-slider-warning-red';
|
|
1297
|
+
} else if (pct > 75) {
|
|
1298
|
+
warningEl.textContent = 'High memory usage — may cause swapping and reduced performance';
|
|
1299
|
+
warningEl.className = 'llm-slider-warning llm-slider-warning-amber';
|
|
1300
|
+
} else {
|
|
1301
|
+
warningEl.classList.add('hidden');
|
|
1302
|
+
warningEl.textContent = '';
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
// Set initial track fill
|
|
1307
|
+
if (ctxSlider) { updateSliderTrack(ctxSlider, computeRamPct()); }
|
|
1308
|
+
if (maxTokSlider) {
|
|
1309
|
+
const orig = parseInt(maxTokSlider.dataset.orig);
|
|
1310
|
+
const pos = ((orig - 256) / (16384 - 256)) * 100;
|
|
1311
|
+
maxTokSlider.style.setProperty('--range-color', 'var(--green)');
|
|
1312
|
+
maxTokSlider.style.setProperty('--range-fill', `${pos}%`);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
ctxSlider?.addEventListener('input', () => {
|
|
1316
|
+
const val = parseInt(ctxSlider.value);
|
|
1317
|
+
const kvPT = parseFloat(ctxSlider.dataset.kvPerToken);
|
|
1318
|
+
const modelB = parseInt(ctxSlider.dataset.modelBytes);
|
|
1319
|
+
const ram = parseInt(ctxSlider.dataset.totalRam);
|
|
1320
|
+
const newKv = val * kvPT;
|
|
1321
|
+
const newTotal = modelB + newKv;
|
|
1322
|
+
this.querySelector('#ctxValue').textContent = val >= 1024 ? `${(val / 1024).toFixed(0)}K` : val;
|
|
1323
|
+
this.querySelector('#ctxMemEstimate').textContent = `${formatBytes(newKv)} KV + ${formatBytes(modelB)} model = ${formatBytes(newTotal)} / ${formatBytes(ram)}`;
|
|
1324
|
+
const pct = computeRamPct();
|
|
1325
|
+
updateSliderTrack(ctxSlider, pct);
|
|
1326
|
+
updateWarning(pct);
|
|
1327
|
+
updateApplyVisibility();
|
|
1328
|
+
});
|
|
1329
|
+
maxTokSlider?.addEventListener('input', () => {
|
|
1330
|
+
this.querySelector('#maxTokValue').textContent = parseInt(maxTokSlider.value).toLocaleString();
|
|
1331
|
+
const pos = ((parseInt(maxTokSlider.value) - 256) / (16384 - 256)) * 100;
|
|
1332
|
+
maxTokSlider.style.setProperty('--range-fill', `${pos}%`);
|
|
1333
|
+
updateApplyVisibility();
|
|
1334
|
+
});
|
|
1335
|
+
const thinkingColor = (val) => val > 256 ? 'var(--red-400, #f87171)' : 'var(--purple-400, #a855f7)';
|
|
1336
|
+
const thinkingLabelClass = (val) => val > 256 ? 'font-mono text-xs text-red' : 'font-mono text-xs text-purple';
|
|
1337
|
+
|
|
1338
|
+
thinkingToggle?.addEventListener('change', () => {
|
|
1339
|
+
const on = thinkingToggle.checked;
|
|
1340
|
+
thinkingSliderWrap?.classList.toggle('hidden', !on);
|
|
1341
|
+
const label = this.querySelector('#thinkingValue');
|
|
1342
|
+
if (label) {
|
|
1343
|
+
if (on) {
|
|
1344
|
+
const val = parseInt(thinkingSlider?.value || '128');
|
|
1345
|
+
label.textContent = val.toLocaleString() + ' tokens';
|
|
1346
|
+
label.className = thinkingLabelClass(val);
|
|
1347
|
+
} else {
|
|
1348
|
+
label.textContent = 'Off';
|
|
1349
|
+
label.className = 'font-mono text-xs text-muted';
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
updateApplyVisibility();
|
|
1353
|
+
});
|
|
1354
|
+
thinkingSlider?.addEventListener('input', () => {
|
|
1355
|
+
const val = parseInt(thinkingSlider.value);
|
|
1356
|
+
const label = this.querySelector('#thinkingValue');
|
|
1357
|
+
if (label) {
|
|
1358
|
+
label.textContent = val.toLocaleString() + ' tokens';
|
|
1359
|
+
label.className = thinkingLabelClass(val);
|
|
1360
|
+
}
|
|
1361
|
+
const pos = ((val - 128) / (1024 - 128)) * 100;
|
|
1362
|
+
thinkingSlider.style.setProperty('--range-color', thinkingColor(val));
|
|
1363
|
+
thinkingSlider.style.setProperty('--range-fill', `${pos}%`);
|
|
1364
|
+
updateApplyVisibility();
|
|
1365
|
+
});
|
|
1366
|
+
// Set initial track fill for thinking slider
|
|
1367
|
+
if (thinkingSlider && thinkingToggle?.checked) {
|
|
1368
|
+
const val = parseInt(thinkingSlider.value);
|
|
1369
|
+
const pos = ((val - 128) / (1024 - 128)) * 100;
|
|
1370
|
+
thinkingSlider.style.setProperty('--range-color', thinkingColor(val));
|
|
1371
|
+
thinkingSlider.style.setProperty('--range-fill', `${pos}%`);
|
|
1372
|
+
}
|
|
1373
|
+
applyBtn?.addEventListener('click', () => this.applyModelSettings());
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
async checkForUpdate() {
|
|
1377
|
+
const btn = this.querySelector('#checkUpdateBtn');
|
|
1378
|
+
if (btn) {
|
|
1379
|
+
btn.disabled = true;
|
|
1380
|
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>Checking...';
|
|
1381
|
+
}
|
|
1382
|
+
try {
|
|
1383
|
+
const isMlx = this._selectedEngine === 'mlx-serve';
|
|
1384
|
+
if (isMlx) {
|
|
1385
|
+
this.mlxUpdateInfo = await api.checkMlxUpdate();
|
|
1386
|
+
} else {
|
|
1387
|
+
this.updateInfo = await api.checkLlamaUpdate();
|
|
1388
|
+
}
|
|
1389
|
+
this.renderStatus();
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
console.error('Failed to check for updates:', e);
|
|
1392
|
+
if (btn) {
|
|
1393
|
+
btn.disabled = false;
|
|
1394
|
+
btn.innerHTML = '<i class="fas fa-exclamation-circle mr-1 text-red"></i>Failed';
|
|
1395
|
+
setTimeout(() => { btn.innerHTML = '<i class="fas fa-arrows-rotate mr-1"></i>Check for updates'; }, 3000);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
async applyModelSettings() {
|
|
1401
|
+
const ctxSlider = this.querySelector('#ctxSlider');
|
|
1402
|
+
const maxTokSlider = this.querySelector('#maxTokSlider');
|
|
1403
|
+
const btn = this.querySelector('#applySettingsBtn');
|
|
1404
|
+
if (!ctxSlider || !maxTokSlider) return;
|
|
1405
|
+
|
|
1406
|
+
const newCtx = parseInt(ctxSlider.value);
|
|
1407
|
+
const newMaxTokens = parseInt(maxTokSlider.value);
|
|
1408
|
+
const engineSt = this.status?.engines?.[this._selectedEngine];
|
|
1409
|
+
const currentCtx = engineSt?.chat?.contextSize;
|
|
1410
|
+
const ctxChanged = newCtx !== currentCtx;
|
|
1411
|
+
|
|
1412
|
+
const thinkingToggle = this.querySelector('#thinkingToggle');
|
|
1413
|
+
const thinkingSlider = this.querySelector('#thinkingSlider');
|
|
1414
|
+
const newReasoningBudget = thinkingToggle ? (thinkingToggle.checked ? parseInt(thinkingSlider?.value || '128') : 0) : undefined;
|
|
1415
|
+
const existingBudget = this._resolveDefault('models')?.reasoningBudget || 0;
|
|
1416
|
+
const reasoningChanged = newReasoningBudget !== undefined && newReasoningBudget !== existingBudget;
|
|
1417
|
+
const needsRestart = ctxChanged || reasoningChanged;
|
|
1418
|
+
|
|
1419
|
+
if (btn) { btn.disabled = true; btn.innerHTML = `<i class="fas fa-spinner fa-spin mr-1"></i>${needsRestart ? 'Restarting...' : 'Saving...'}`; }
|
|
1420
|
+
|
|
1421
|
+
try {
|
|
1422
|
+
const resolvedKey = this._resolveDefaultKey('models');
|
|
1423
|
+
const existing = this._resolveDefault('models') || {};
|
|
1424
|
+
await api.saveLlmModel(resolvedKey, {
|
|
1425
|
+
provider: existing.provider || existing._provider || 'local',
|
|
1426
|
+
model: existing.model,
|
|
1427
|
+
contextSize: newCtx,
|
|
1428
|
+
maxTokens: newMaxTokens,
|
|
1429
|
+
...(newReasoningBudget !== undefined ? { reasoningBudget: newReasoningBudget } : existing.reasoningBudget != null ? { reasoningBudget: existing.reasoningBudget } : {}),
|
|
1430
|
+
...(existing.temperature != null ? { temperature: existing.temperature } : {}),
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
if (needsRestart) {
|
|
1434
|
+
await api.stopLocalLlm(this._selectedEngine);
|
|
1435
|
+
const activeModelPath = this.status?.engines?.[this._selectedEngine]?.chat?.activeModel;
|
|
1436
|
+
const model = this.models.find(m => activeModelPath === m.filePath);
|
|
1437
|
+
if (model) {
|
|
1438
|
+
await api.activateLocalModel(model.id);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
await this.loadLlmConfig();
|
|
1443
|
+
await this.refresh();
|
|
1444
|
+
} catch (e) {
|
|
1445
|
+
console.error('Failed to apply settings:', e);
|
|
1446
|
+
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-save mr-1"></i>Apply'; }
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async updateBinary() {
|
|
1451
|
+
const btn = this.querySelector('#updateBinaryBtn');
|
|
1452
|
+
if (btn) {
|
|
1453
|
+
btn.disabled = true;
|
|
1454
|
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>Updating...';
|
|
1455
|
+
}
|
|
1456
|
+
try {
|
|
1457
|
+
const isMlx = this._selectedEngine === 'mlx-serve';
|
|
1458
|
+
if (isMlx) {
|
|
1459
|
+
await api.updateMlxBinary();
|
|
1460
|
+
this.mlxUpdateInfo = null;
|
|
1461
|
+
} else {
|
|
1462
|
+
await api.updateLlamaBinary();
|
|
1463
|
+
this.updateInfo = null;
|
|
1464
|
+
}
|
|
1465
|
+
await this.refresh();
|
|
1466
|
+
} catch (e) {
|
|
1467
|
+
console.error(`Failed to update ${this._selectedEngine === 'mlx-serve' ? 'mlx-serve' : 'llama-server'}:`, e);
|
|
1468
|
+
if (btn) {
|
|
1469
|
+
btn.disabled = false;
|
|
1470
|
+
btn.innerHTML = '<i class="fas fa-exclamation-circle mr-1 text-red"></i>Failed';
|
|
1471
|
+
setTimeout(() => { btn.innerHTML = '<i class="fas fa-arrows-rotate mr-1"></i>Update'; }, 3000);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
async loadLlmConfig() {
|
|
1477
|
+
try {
|
|
1478
|
+
this.llmConfig = await api.getLlmConfig();
|
|
1479
|
+
const defaultModel = this._resolveDefault('models');
|
|
1480
|
+
if (defaultModel?._provider) {
|
|
1481
|
+
this.activeProvider = defaultModel._provider;
|
|
1482
|
+
}
|
|
1483
|
+
this.renderProviderTabs();
|
|
1484
|
+
this.switchProviderView(this.activeProvider);
|
|
1485
|
+
} catch (e) {
|
|
1486
|
+
console.error('Failed to load LLM config:', e);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
renderProviderTabs() {
|
|
1491
|
+
const container = this.querySelector('#providerTabs');
|
|
1492
|
+
if (!container) return;
|
|
1493
|
+
|
|
1494
|
+
const defaultProvider = this._resolveDefault('models')?._provider || 'local';
|
|
1495
|
+
|
|
1496
|
+
container.innerHTML = PROVIDERS.map(p => {
|
|
1497
|
+
const meta = PROVIDER_META[p];
|
|
1498
|
+
const isActive = this.activeProvider === p;
|
|
1499
|
+
const isDefault = defaultProvider === p;
|
|
1500
|
+
return `
|
|
1501
|
+
<button class="llm-provider-tab ${isActive ? 'active' : ''}" data-provider="${p}">
|
|
1502
|
+
<span class="text-${meta.color}">${providerIcon(p)}</span>
|
|
1503
|
+
<span>${meta.label}</span>
|
|
1504
|
+
${isDefault ? '<span class="default-dot" title="Default provider"></span>' : ''}
|
|
1505
|
+
</button>`;
|
|
1506
|
+
}).join('');
|
|
1507
|
+
|
|
1508
|
+
container.querySelectorAll('.llm-provider-tab').forEach(btn => {
|
|
1509
|
+
btn.addEventListener('click', () => {
|
|
1510
|
+
this.activeProvider = btn.dataset.provider;
|
|
1511
|
+
this.renderProviderTabs();
|
|
1512
|
+
this.switchProviderView(this.activeProvider);
|
|
1513
|
+
});
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
switchProviderView(provider) {
|
|
1518
|
+
const localEl = this.querySelector('#localContent');
|
|
1519
|
+
const cloudEl = this.querySelector('#cloudContent');
|
|
1520
|
+
if (!localEl || !cloudEl) return;
|
|
1521
|
+
|
|
1522
|
+
if (provider === 'local') {
|
|
1523
|
+
localEl.classList.remove('hidden');
|
|
1524
|
+
cloudEl.classList.add('hidden');
|
|
1525
|
+
} else {
|
|
1526
|
+
localEl.classList.add('hidden');
|
|
1527
|
+
cloudEl.classList.remove('hidden');
|
|
1528
|
+
this.renderCloudConfig(provider);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
renderCloudConfig(provider) {
|
|
1533
|
+
const container = this.querySelector('#cloudContent');
|
|
1534
|
+
if (!container) return;
|
|
1535
|
+
|
|
1536
|
+
const meta = PROVIDER_META[provider];
|
|
1537
|
+
const models = POPULAR_MODELS[provider] || [];
|
|
1538
|
+
const embeddings = POPULAR_EMBEDDINGS[provider] || [];
|
|
1539
|
+
const config = this.llmConfig;
|
|
1540
|
+
const defaultModel = this._resolveDefault('models');
|
|
1541
|
+
const isDefault = defaultModel?._provider === provider;
|
|
1542
|
+
const defaultEmbedding = this._resolveDefault('embeddings');
|
|
1543
|
+
|
|
1544
|
+
// Find the config entry for this provider (check default first, then named entries)
|
|
1545
|
+
let modelEntry = null;
|
|
1546
|
+
let modelEntryName = null;
|
|
1547
|
+
if (isDefault) {
|
|
1548
|
+
modelEntry = defaultModel;
|
|
1549
|
+
modelEntryName = this._resolveDefaultKey('models');
|
|
1550
|
+
} else {
|
|
1551
|
+
// Check named entries (skip string pointers)
|
|
1552
|
+
for (const [name, m] of Object.entries(config?.models || {})) {
|
|
1553
|
+
if (typeof m === 'string') continue;
|
|
1554
|
+
if (m._provider === provider && name !== 'default') {
|
|
1555
|
+
modelEntry = m;
|
|
1556
|
+
modelEntryName = name;
|
|
1557
|
+
break;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const currentModel = modelEntry?.model || models[0] || '';
|
|
1563
|
+
const hasEnvKey = modelEntry?._hasEnvKey || false;
|
|
1564
|
+
const isEnvRef = modelEntry?.apiKey && /^\$\{.+\}$/.test(modelEntry.apiKey);
|
|
1565
|
+
const hasConfigKey = isEnvRef ? false : (modelEntry?.apiKey && !modelEntry.apiKey.startsWith('••••') ? false : !!modelEntry?.apiKey);
|
|
1566
|
+
const envVarName = PROVIDER_ENV_NAMES[provider] || '';
|
|
1567
|
+
|
|
1568
|
+
// Key status
|
|
1569
|
+
let keyStatusHtml = '';
|
|
1570
|
+
if (hasConfigKey) {
|
|
1571
|
+
keyStatusHtml = `<span class="llm-key-status llm-key-status-set"><i class="fas fa-check"></i>Set in config</span>`;
|
|
1572
|
+
} else if (isEnvRef || hasEnvKey) {
|
|
1573
|
+
keyStatusHtml = `<span class="llm-key-status llm-key-status-env"><i class="fas fa-leaf"></i>Using ${envVarName}</span>`;
|
|
1574
|
+
} else {
|
|
1575
|
+
keyStatusHtml = `<span class="llm-key-status llm-key-status-missing"><i class="fas fa-exclamation-triangle"></i>Not configured</span>`;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// Model select options
|
|
1579
|
+
const isCustomModel = currentModel && !models.includes(currentModel);
|
|
1580
|
+
const modelOptions = models.map(m =>
|
|
1581
|
+
`<option value="${escapeHtml(m)}" ${m === currentModel ? 'selected' : ''}>${escapeHtml(m)}</option>`
|
|
1582
|
+
).join('');
|
|
1583
|
+
|
|
1584
|
+
// Embedding select options (for providers that have them)
|
|
1585
|
+
const embSection = embeddings.length > 0 ? (() => {
|
|
1586
|
+
const isEmbDefault = defaultEmbedding?.provider === provider;
|
|
1587
|
+
const currentEmb = isEmbDefault ? defaultEmbedding?.model : embeddings[0];
|
|
1588
|
+
const isCustomEmb = currentEmb && !embeddings.includes(currentEmb);
|
|
1589
|
+
const embOptions = embeddings.map(m =>
|
|
1590
|
+
`<option value="${escapeHtml(m)}" ${m === currentEmb ? 'selected' : ''}>${escapeHtml(m)}</option>`
|
|
1591
|
+
).join('');
|
|
1592
|
+
return `
|
|
1593
|
+
<div class="llm-form-group">
|
|
1594
|
+
<label class="llm-form-label">Embedding Model</label>
|
|
1595
|
+
<div class="llm-form-row">
|
|
1596
|
+
<select id="cloudEmbModel" class="select">${embOptions}<option value="" ${isCustomEmb ? 'selected' : ''}>Custom...</option></select>
|
|
1597
|
+
<input id="cloudEmbModelCustom" type="text" class="input ${isCustomEmb ? '' : 'hidden'}"
|
|
1598
|
+
placeholder="Custom embedding model name" value="${isCustomEmb ? escapeHtml(currentEmb) : ''}" />
|
|
1599
|
+
</div>
|
|
1600
|
+
</div>`;
|
|
1601
|
+
})() : '';
|
|
1602
|
+
|
|
1603
|
+
container.innerHTML = `
|
|
1604
|
+
<div class="llm-config-card">
|
|
1605
|
+
<div class="flex items-center justify-between mb-4">
|
|
1606
|
+
<div class="flex items-center gap-2">
|
|
1607
|
+
<span class="text-${meta.color}">${providerIcon(provider)}</span>
|
|
1608
|
+
<span class="font-semibold text-primary">${meta.label} Configuration</span>
|
|
1609
|
+
</div>
|
|
1610
|
+
${isDefault
|
|
1611
|
+
? `<span class="badge badge-green"><i class="fas fa-check mr-1"></i>Default</span>`
|
|
1612
|
+
: `<button id="setDefaultBtn" class="btn btn-accent btn-sm"><i class="fas fa-star mr-1"></i>Set as Default</button>`}
|
|
1613
|
+
</div>
|
|
1614
|
+
|
|
1615
|
+
<div class="llm-cloud-form">
|
|
1616
|
+
<div class="llm-form-group">
|
|
1617
|
+
<div class="flex items-center justify-between">
|
|
1618
|
+
<label class="llm-form-label">API Key</label>
|
|
1619
|
+
${keyStatusHtml}
|
|
1620
|
+
</div>
|
|
1621
|
+
<input id="cloudApiKey" type="password" class="input" placeholder="${envVarName ? `Or set ${envVarName} env var` : 'Enter API key'}"
|
|
1622
|
+
value="${hasConfigKey ? (modelEntry?.apiKey || '') : ''}" />
|
|
1623
|
+
${envVarName ? `<span class="llm-form-hint">Environment variable: ${envVarName}</span>` : ''}
|
|
1624
|
+
</div>
|
|
1625
|
+
|
|
1626
|
+
<div class="llm-form-group">
|
|
1627
|
+
<label class="llm-form-label">Chat Model</label>
|
|
1628
|
+
<div class="llm-form-row">
|
|
1629
|
+
<select id="cloudModel" class="select">${modelOptions}<option value="" ${isCustomModel ? 'selected' : ''}>Custom...</option></select>
|
|
1630
|
+
<input id="cloudModelCustom" type="text" class="input ${isCustomModel ? '' : 'hidden'}"
|
|
1631
|
+
placeholder="Custom model name" value="${isCustomModel ? escapeHtml(currentModel) : ''}" />
|
|
1632
|
+
</div>
|
|
1633
|
+
</div>
|
|
1634
|
+
|
|
1635
|
+
${embSection}
|
|
1636
|
+
|
|
1637
|
+
<div class="llm-form-group">
|
|
1638
|
+
<label class="llm-form-label">Advanced</label>
|
|
1639
|
+
<div class="llm-form-row">
|
|
1640
|
+
<div class="llm-form-group">
|
|
1641
|
+
<label class="llm-form-label">Temperature</label>
|
|
1642
|
+
<input id="cloudTemp" type="number" class="input" min="0" max="2" step="0.1"
|
|
1643
|
+
placeholder="Default" value="${modelEntry?.temperature ?? ''}" />
|
|
1644
|
+
</div>
|
|
1645
|
+
<div class="llm-form-group">
|
|
1646
|
+
<label class="llm-form-label">Max Tokens</label>
|
|
1647
|
+
<input id="cloudMaxTokens" type="number" class="input" min="1"
|
|
1648
|
+
placeholder="Default" value="${modelEntry?.maxTokens ?? ''}" />
|
|
1649
|
+
</div>
|
|
1650
|
+
</div>
|
|
1651
|
+
${provider === 'anthropic' ? `
|
|
1652
|
+
<div class="llm-form-group">
|
|
1653
|
+
<label class="llm-form-label">Thinking Budget</label>
|
|
1654
|
+
<input id="cloudThinkingBudget" type="number" class="input" min="0"
|
|
1655
|
+
placeholder="0 = disabled" value="${modelEntry?.thinkingBudget ?? ''}" />
|
|
1656
|
+
</div>` : ''}
|
|
1657
|
+
<div class="llm-form-group">
|
|
1658
|
+
<label class="llm-form-label">Base URL</label>
|
|
1659
|
+
<input id="cloudBaseUrl" type="text" class="input"
|
|
1660
|
+
placeholder="Default (leave empty for standard API)" value="${modelEntry?.baseUrl ? escapeHtml(modelEntry.baseUrl) : ''}" />
|
|
1661
|
+
</div>
|
|
1662
|
+
</div>
|
|
1663
|
+
|
|
1664
|
+
<div id="cloudSaveStatus" class="text-xs text-muted"></div>
|
|
1665
|
+
|
|
1666
|
+
<div class="flex items-center gap-2">
|
|
1667
|
+
<button id="cloudSaveBtn" class="btn btn-accent">
|
|
1668
|
+
<i class="fas fa-save mr-1"></i>Save Configuration
|
|
1669
|
+
</button>
|
|
1670
|
+
</div>
|
|
1671
|
+
</div>
|
|
1672
|
+
</div>
|
|
1673
|
+
|
|
1674
|
+
${this.renderNamedConfigs(provider)}
|
|
1675
|
+
`;
|
|
1676
|
+
|
|
1677
|
+
// Wire up events
|
|
1678
|
+
const modelSelect = container.querySelector('#cloudModel');
|
|
1679
|
+
const modelCustom = container.querySelector('#cloudModelCustom');
|
|
1680
|
+
modelSelect?.addEventListener('change', () => {
|
|
1681
|
+
modelCustom.classList.toggle('hidden', modelSelect.value !== '');
|
|
1682
|
+
if (modelSelect.value === '') modelCustom.focus();
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
const embSelect = container.querySelector('#cloudEmbModel');
|
|
1686
|
+
const embCustom = container.querySelector('#cloudEmbModelCustom');
|
|
1687
|
+
embSelect?.addEventListener('change', () => {
|
|
1688
|
+
embCustom?.classList.toggle('hidden', embSelect.value !== '');
|
|
1689
|
+
if (embSelect.value === '') embCustom?.focus();
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
container.querySelector('#cloudSaveBtn')?.addEventListener('click', () => this.saveCloudConfig(provider));
|
|
1693
|
+
container.querySelector('#setDefaultBtn')?.addEventListener('click', () => this.setProviderAsDefault(provider));
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
renderNamedConfigs(excludeProvider) {
|
|
1697
|
+
if (!this.llmConfig?.models) return '';
|
|
1698
|
+
const entries = Object.entries(this.llmConfig.models)
|
|
1699
|
+
.filter(([name, m]) => name !== 'default' && typeof m !== 'string' && m._provider !== excludeProvider);
|
|
1700
|
+
if (!entries.length) return '';
|
|
1701
|
+
|
|
1702
|
+
const rows = entries.map(([name, m]) => `
|
|
1703
|
+
<div class="llm-named-row">
|
|
1704
|
+
<span class="name">${escapeHtml(name)}</span>
|
|
1705
|
+
<span class="badge badge-${PROVIDER_META[m._provider]?.color || 'gray'}">${PROVIDER_META[m._provider]?.label || m._provider}</span>
|
|
1706
|
+
<span class="model">${escapeHtml(m.model)}</span>
|
|
1707
|
+
</div>`).join('');
|
|
1708
|
+
|
|
1709
|
+
return `
|
|
1710
|
+
<div class="mt-4">
|
|
1711
|
+
<h4 class="text-xs font-medium text-muted mb-2">Other Named Configs</h4>
|
|
1712
|
+
<div class="llm-named-configs">${rows}</div>
|
|
1713
|
+
</div>`;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
async saveCloudConfig(provider) {
|
|
1717
|
+
const btn = this.querySelector('#cloudSaveBtn');
|
|
1718
|
+
const statusEl = this.querySelector('#cloudSaveStatus');
|
|
1719
|
+
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>Saving...'; }
|
|
1720
|
+
|
|
1721
|
+
try {
|
|
1722
|
+
const modelSelect = this.querySelector('#cloudModel');
|
|
1723
|
+
const modelCustom = this.querySelector('#cloudModelCustom');
|
|
1724
|
+
const model = modelSelect?.value || modelCustom?.value?.trim();
|
|
1725
|
+
if (!model) throw new Error('Please select or enter a model name');
|
|
1726
|
+
|
|
1727
|
+
const apiKey = this.querySelector('#cloudApiKey')?.value?.trim() || (PROVIDER_ENV_NAMES[provider] ? `\${${PROVIDER_ENV_NAMES[provider]}}` : undefined);
|
|
1728
|
+
const temp = this.querySelector('#cloudTemp')?.value;
|
|
1729
|
+
const maxTokens = this.querySelector('#cloudMaxTokens')?.value;
|
|
1730
|
+
const baseUrl = this.querySelector('#cloudBaseUrl')?.value?.trim() || undefined;
|
|
1731
|
+
const thinkingBudget = this.querySelector('#cloudThinkingBudget')?.value;
|
|
1732
|
+
|
|
1733
|
+
const config = {
|
|
1734
|
+
provider,
|
|
1735
|
+
model,
|
|
1736
|
+
...(apiKey ? { apiKey } : {}),
|
|
1737
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
1738
|
+
...(temp !== '' && temp != null ? { temperature: parseFloat(temp) } : {}),
|
|
1739
|
+
...(maxTokens !== '' && maxTokens != null ? { maxTokens: parseInt(maxTokens) } : {}),
|
|
1740
|
+
...(thinkingBudget !== '' && thinkingBudget != null ? { thinkingBudget: parseInt(thinkingBudget) } : {}),
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
// Write config to the provider's named key, then set default pointer
|
|
1744
|
+
await api.saveLlmModel(provider, config);
|
|
1745
|
+
await api.saveLlmModel('default', { _pointer: provider });
|
|
1746
|
+
|
|
1747
|
+
// Save embedding config if applicable
|
|
1748
|
+
const embSelect = this.querySelector('#cloudEmbModel');
|
|
1749
|
+
const embCustom = this.querySelector('#cloudEmbModelCustom');
|
|
1750
|
+
const embModel = embSelect?.value || embCustom?.value?.trim();
|
|
1751
|
+
if (embModel) {
|
|
1752
|
+
await api.saveLlmEmbedding(provider, {
|
|
1753
|
+
provider,
|
|
1754
|
+
model: embModel,
|
|
1755
|
+
...(apiKey ? { apiKey } : {}),
|
|
1756
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
1757
|
+
});
|
|
1758
|
+
await api.saveLlmEmbedding('default', { _pointer: provider });
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
await this.loadLlmConfig();
|
|
1762
|
+
if (statusEl) { statusEl.innerHTML = '<i class="fas fa-check text-green mr-1"></i>Saved'; }
|
|
1763
|
+
setTimeout(() => { if (statusEl) statusEl.innerHTML = ''; }, 3000);
|
|
1764
|
+
} catch (e) {
|
|
1765
|
+
console.error('Failed to save config:', e);
|
|
1766
|
+
if (statusEl) { statusEl.innerHTML = `<i class="fas fa-exclamation-circle text-red mr-1"></i>${escapeHtml(e.message)}`; }
|
|
1767
|
+
} finally {
|
|
1768
|
+
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-save mr-1"></i>Save Configuration'; }
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
async setProviderAsDefault(provider) {
|
|
1773
|
+
const btn = this.querySelector('#setDefaultBtn');
|
|
1774
|
+
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>Switching...'; }
|
|
1775
|
+
|
|
1776
|
+
try {
|
|
1777
|
+
// Find the config for this provider
|
|
1778
|
+
const modelSelect = this.querySelector('#cloudModel');
|
|
1779
|
+
const modelCustom = this.querySelector('#cloudModelCustom');
|
|
1780
|
+
const model = modelSelect?.value || modelCustom?.value?.trim();
|
|
1781
|
+
if (!model) throw new Error('Please select or enter a model name first');
|
|
1782
|
+
|
|
1783
|
+
const apiKey = this.querySelector('#cloudApiKey')?.value?.trim() || (PROVIDER_ENV_NAMES[provider] ? `\${${PROVIDER_ENV_NAMES[provider]}}` : undefined);
|
|
1784
|
+
const temp = this.querySelector('#cloudTemp')?.value;
|
|
1785
|
+
const maxTokens = this.querySelector('#cloudMaxTokens')?.value;
|
|
1786
|
+
const baseUrl = this.querySelector('#cloudBaseUrl')?.value?.trim() || undefined;
|
|
1787
|
+
const thinkingBudget = this.querySelector('#cloudThinkingBudget')?.value;
|
|
1788
|
+
|
|
1789
|
+
// Save config to provider's named key, then set default pointer
|
|
1790
|
+
await api.saveLlmModel(provider, {
|
|
1791
|
+
provider,
|
|
1792
|
+
model,
|
|
1793
|
+
...(apiKey ? { apiKey } : {}),
|
|
1794
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
1795
|
+
...(temp !== '' && temp != null ? { temperature: parseFloat(temp) } : {}),
|
|
1796
|
+
...(maxTokens !== '' && maxTokens != null ? { maxTokens: parseInt(maxTokens) } : {}),
|
|
1797
|
+
...(thinkingBudget !== '' && thinkingBudget != null ? { thinkingBudget: parseInt(thinkingBudget) } : {}),
|
|
1798
|
+
});
|
|
1799
|
+
await api.saveLlmModel('default', { _pointer: provider });
|
|
1800
|
+
|
|
1801
|
+
await this.loadLlmConfig();
|
|
1802
|
+
} catch (e) {
|
|
1803
|
+
console.error('Failed to set default:', e);
|
|
1804
|
+
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-star mr-1"></i>Set as Default'; }
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
renderModels() {
|
|
1809
|
+
const container = this.querySelector('#modelsGrid');
|
|
1810
|
+
if (!container) return;
|
|
1811
|
+
|
|
1812
|
+
if (!this.models.length) {
|
|
1813
|
+
container.innerHTML = `
|
|
1814
|
+
<div class="col-span-full text-muted text-center py-8">
|
|
1815
|
+
<i class="fas fa-box-open text-4xl mb-4 block text-muted"></i>
|
|
1816
|
+
<p class="text-lg mb-2">No models downloaded</p>
|
|
1817
|
+
<p class="text-sm">Search HuggingFace below, or download a recommended model</p>
|
|
1818
|
+
</div>`;
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// Collect active models across all engines
|
|
1823
|
+
let activeModelPath = null;
|
|
1824
|
+
let activeEmbPath = null;
|
|
1825
|
+
if (this.status?.engines) {
|
|
1826
|
+
for (const eng of Object.values(this.status.engines)) {
|
|
1827
|
+
if (eng.chat?.running && eng.chat.activeModel) activeModelPath = activeModelPath || eng.chat.activeModel;
|
|
1828
|
+
if (eng.embedding?.running && eng.embedding.activeModel) activeEmbPath = activeEmbPath || eng.embedding.activeModel;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const selectedEng = this._selectedEngine;
|
|
1833
|
+
|
|
1834
|
+
// Filter to only show models matching the selected engine
|
|
1835
|
+
const filteredModels = this.models.filter(model => {
|
|
1836
|
+
if (!selectedEng || !MANAGED_ENGINES.includes(selectedEng)) return true;
|
|
1837
|
+
const modelEngine = model.type === 'mlx' ? 'mlx-serve' : 'llama-cpp';
|
|
1838
|
+
return modelEngine === selectedEng;
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
if (!filteredModels.length) {
|
|
1842
|
+
const formatLabel = selectedEng === 'mlx-serve' ? 'MLX' : 'GGUF';
|
|
1843
|
+
container.innerHTML = `
|
|
1844
|
+
<div class="col-span-full text-muted text-center py-8">
|
|
1845
|
+
<i class="fas fa-box-open text-4xl mb-4 block text-muted"></i>
|
|
1846
|
+
<p class="text-lg mb-2">No ${formatLabel} models downloaded</p>
|
|
1847
|
+
<p class="text-sm">Search HuggingFace below, or download a recommended model</p>
|
|
1848
|
+
</div>`;
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
container.innerHTML = filteredModels.map(model => {
|
|
1853
|
+
const isChat = activeModelPath && activeModelPath === model.filePath;
|
|
1854
|
+
const isEmb = activeEmbPath && activeEmbPath === model.filePath;
|
|
1855
|
+
const looksLikeEmbedding = /embed|MiniLM/i.test(model.fileName);
|
|
1856
|
+
|
|
1857
|
+
const cardCls = isChat ? 'llm-model-card active-chat' : isEmb ? 'llm-model-card active-emb' : 'llm-model-card';
|
|
1858
|
+
const caps = looksLikeEmbedding ? null : detectCapabilitiesFromFile(model);
|
|
1859
|
+
const badges = caps ? capabilityBadges(caps) : '';
|
|
1860
|
+
|
|
1861
|
+
return `
|
|
1862
|
+
<div class="${cardCls}" data-model-id="${escapeHtml(model.id)}">
|
|
1863
|
+
<div class="flex items-start justify-between mb-3">
|
|
1864
|
+
<div class="min-w-0 flex-1">
|
|
1865
|
+
<div class="flex items-center gap-2 mb-1">
|
|
1866
|
+
${isChat ? '<i class="fas fa-circle text-amber text-2xs"></i>' : ''}
|
|
1867
|
+
${isEmb ? '<i class="fas fa-circle text-blue text-2xs"></i>' : ''}
|
|
1868
|
+
<span class="font-medium text-primary text-sm truncate">${escapeHtml(model.fileName)}</span>
|
|
1869
|
+
<span class="badge badge-${model.type === 'mlx' ? 'green' : 'amber'} text-2xs">${(model.type || 'gguf').toUpperCase()}</span>
|
|
1870
|
+
</div>
|
|
1871
|
+
${model.repo ? `<div class="text-xs text-muted truncate">${escapeHtml(model.repo)}</div>` : ''}
|
|
1872
|
+
${badges ? `<div class="flex items-center gap-1 mt-1">${badges}</div>` : ''}
|
|
1873
|
+
</div>
|
|
1874
|
+
</div>
|
|
1875
|
+
<div class="flex items-center justify-between">
|
|
1876
|
+
<div class="flex items-center gap-3 text-xs text-muted">
|
|
1877
|
+
<span><i class="fas fa-hard-drive mr-1"></i>${formatBytes(model.sizeBytes)}</span>
|
|
1878
|
+
<span>${timeAgo(model.downloadedAt)}</span>
|
|
1879
|
+
</div>
|
|
1880
|
+
<div class="flex items-center gap-2">
|
|
1881
|
+
${looksLikeEmbedding
|
|
1882
|
+
? (isEmb
|
|
1883
|
+
? '<span class="badge badge-blue">Embedding</span>'
|
|
1884
|
+
: `<button class="activate-emb-btn btn btn-blue btn-sm" data-id="${escapeHtml(model.id)}" title="Activate as embedding model">
|
|
1885
|
+
<i class="fas fa-vector-square mr-1"></i>Embed
|
|
1886
|
+
</button>`)
|
|
1887
|
+
: (isChat
|
|
1888
|
+
? '<span class="badge badge-amber">Active</span>'
|
|
1889
|
+
: `<button class="activate-btn btn btn-amber btn-sm" data-id="${escapeHtml(model.id)}" title="Activate as chat model">
|
|
1890
|
+
<i class="fas fa-play mr-1"></i>Activate
|
|
1891
|
+
</button>`)
|
|
1892
|
+
}
|
|
1893
|
+
${!isChat && !isEmb
|
|
1894
|
+
? `<button class="delete-btn text-xs text-muted transition-colors" data-id="${escapeHtml(model.id)}" title="Delete model">
|
|
1895
|
+
<i class="fas fa-trash-alt"></i>
|
|
1896
|
+
</button>`
|
|
1897
|
+
: ''
|
|
1898
|
+
}
|
|
1899
|
+
</div>
|
|
1900
|
+
</div>
|
|
1901
|
+
</div>`;
|
|
1902
|
+
}).join('');
|
|
1903
|
+
|
|
1904
|
+
container.querySelectorAll('.activate-btn').forEach(btn => {
|
|
1905
|
+
btn.addEventListener('click', (e) => {
|
|
1906
|
+
e.stopPropagation();
|
|
1907
|
+
this.activateModel(btn.dataset.id);
|
|
1908
|
+
});
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
container.querySelectorAll('.activate-emb-btn').forEach(btn => {
|
|
1912
|
+
btn.addEventListener('click', (e) => {
|
|
1913
|
+
e.stopPropagation();
|
|
1914
|
+
this.activateEmbedding(btn.dataset.id);
|
|
1915
|
+
});
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
container.querySelectorAll('.delete-btn').forEach(btn => {
|
|
1919
|
+
btn.addEventListener('click', (e) => {
|
|
1920
|
+
e.stopPropagation();
|
|
1921
|
+
this.deleteModel(btn.dataset.id);
|
|
1922
|
+
});
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
renderRecommendations() {
|
|
1927
|
+
const container = this.querySelector('#recommendedSection');
|
|
1928
|
+
if (!container) return;
|
|
1929
|
+
|
|
1930
|
+
const recommended = getRecommendedModels(this.status);
|
|
1931
|
+
const pending = recommended.filter(r => !this.isModelDownloaded(r.repo, r.file));
|
|
1932
|
+
if (!pending.length) {
|
|
1933
|
+
container.innerHTML = '';
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
container.innerHTML = `
|
|
1938
|
+
<h3 class="section-title mb-3">Recommended Models</h3>
|
|
1939
|
+
<div class="llm-rec-grid">
|
|
1940
|
+
${pending.map(r => {
|
|
1941
|
+
const downloadId = r.type === 'mlx' ? `mlx:${r.repo}` : `${r.repo}/${r.file}`;
|
|
1942
|
+
const isDownloading = this.activeDownloads.has(downloadId);
|
|
1943
|
+
return `
|
|
1944
|
+
<div class="llm-rec-card llm-rec-card-${r.color}">
|
|
1945
|
+
<div class="flex items-center gap-2 mb-2">
|
|
1946
|
+
<i class="fa${r.icon === 'fa-apple' ? 'b' : 's'} ${r.icon} text-${r.color} text-sm"></i>
|
|
1947
|
+
<span class="font-medium text-primary text-sm">${escapeHtml(r.label)}</span>
|
|
1948
|
+
<span class="badge badge-${r.type === 'mlx' ? 'green' : 'amber'} text-2xs">${r.type.toUpperCase()}</span>
|
|
1949
|
+
</div>
|
|
1950
|
+
<p class="text-xs text-muted mb-3">${escapeHtml(r.desc)}</p>
|
|
1951
|
+
<div class="flex items-center justify-between">
|
|
1952
|
+
<span class="text-xs text-muted">${r.size}</span>
|
|
1953
|
+
<button class="rec-download-btn btn btn-${r.color} btn-sm"
|
|
1954
|
+
data-repo="${escapeHtml(r.repo)}" data-file="${escapeHtml(r.file)}" data-type="${r.type}" ${isDownloading ? 'disabled' : ''}>
|
|
1955
|
+
${isDownloading
|
|
1956
|
+
? '<i class="fas fa-spinner fa-spin mr-1"></i>Downloading...'
|
|
1957
|
+
: '<i class="fas fa-download mr-1"></i>Download'}
|
|
1958
|
+
</button>
|
|
1959
|
+
</div>
|
|
1960
|
+
</div>`;
|
|
1961
|
+
}).join('')}
|
|
1962
|
+
</div>`;
|
|
1963
|
+
|
|
1964
|
+
container.querySelectorAll('.rec-download-btn').forEach(btn => {
|
|
1965
|
+
btn.addEventListener('click', () => {
|
|
1966
|
+
btn.disabled = true;
|
|
1967
|
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>Downloading...';
|
|
1968
|
+
this.downloadModel(btn.dataset.repo, btn.dataset.file, null, btn.dataset.type);
|
|
1969
|
+
this.startDownloadPolling();
|
|
1970
|
+
});
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
async activateModel(id) {
|
|
1975
|
+
const gridBtn = this.querySelector(`.activate-btn[data-id="${id}"]`);
|
|
1976
|
+
const startBtn = this.querySelector('#startLastBtn');
|
|
1977
|
+
const card = gridBtn?.closest('[data-model-id]');
|
|
1978
|
+
const spinnerHtml = '<span class="spinner-sm"></span>';
|
|
1979
|
+
if (gridBtn) {
|
|
1980
|
+
gridBtn.disabled = true;
|
|
1981
|
+
gridBtn.innerHTML = `${spinnerHtml} Starting...`;
|
|
1982
|
+
}
|
|
1983
|
+
if (startBtn) {
|
|
1984
|
+
startBtn.disabled = true;
|
|
1985
|
+
startBtn.innerHTML = `${spinnerHtml} Starting...`;
|
|
1986
|
+
}
|
|
1987
|
+
// Clear any previous error
|
|
1988
|
+
card?.querySelector('.activate-error')?.remove();
|
|
1989
|
+
|
|
1990
|
+
try {
|
|
1991
|
+
const result = await api.activateLocalModel(id);
|
|
1992
|
+
if (result.error) {
|
|
1993
|
+
throw new Error(result.error);
|
|
1994
|
+
}
|
|
1995
|
+
await this.loadLlmConfig();
|
|
1996
|
+
await this.refresh();
|
|
1997
|
+
this.renderEngineTabs();
|
|
1998
|
+
} catch (e) {
|
|
1999
|
+
console.error('Failed to activate model:', e);
|
|
2000
|
+
if (gridBtn) {
|
|
2001
|
+
gridBtn.disabled = false;
|
|
2002
|
+
gridBtn.innerHTML = '<i class="fas fa-play mr-1"></i>Activate';
|
|
2003
|
+
}
|
|
2004
|
+
if (startBtn) {
|
|
2005
|
+
startBtn.disabled = false;
|
|
2006
|
+
startBtn.innerHTML = '<i class="fas fa-play mr-1"></i>Start';
|
|
2007
|
+
}
|
|
2008
|
+
if (card) {
|
|
2009
|
+
const errorEl = document.createElement('div');
|
|
2010
|
+
errorEl.className = 'activate-error';
|
|
2011
|
+
errorEl.innerHTML = `<i class="fas fa-exclamation-circle mr-1"></i>${escapeHtml(e.message)}`;
|
|
2012
|
+
card.appendChild(errorEl);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
async deleteModel(id) {
|
|
2018
|
+
if (!confirm('Delete this model? This cannot be undone.')) return;
|
|
2019
|
+
|
|
2020
|
+
try {
|
|
2021
|
+
await api.deleteLocalModel(id);
|
|
2022
|
+
await this.loadModels();
|
|
2023
|
+
} catch (e) {
|
|
2024
|
+
console.error('Failed to delete model:', e);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
async activateEmbedding(id) {
|
|
2029
|
+
const btn = this.querySelector(`.activate-emb-btn[data-id="${id}"]`);
|
|
2030
|
+
const card = btn?.closest('[data-model-id]');
|
|
2031
|
+
if (btn) {
|
|
2032
|
+
btn.disabled = true;
|
|
2033
|
+
btn.innerHTML = '<span class="spinner-sm"></span> Loading...';
|
|
2034
|
+
}
|
|
2035
|
+
card?.querySelector('.activate-error')?.remove();
|
|
2036
|
+
|
|
2037
|
+
try {
|
|
2038
|
+
const result = await api.activateLocalEmbedding(id);
|
|
2039
|
+
if (result.error) {
|
|
2040
|
+
throw new Error(result.error);
|
|
2041
|
+
}
|
|
2042
|
+
await this.refresh();
|
|
2043
|
+
} catch (e) {
|
|
2044
|
+
console.error('Failed to activate embedding model:', e);
|
|
2045
|
+
if (btn) {
|
|
2046
|
+
btn.disabled = false;
|
|
2047
|
+
btn.innerHTML = '<i class="fas fa-vector-square mr-1"></i>Embed';
|
|
2048
|
+
}
|
|
2049
|
+
if (card) {
|
|
2050
|
+
const errorEl = document.createElement('div');
|
|
2051
|
+
errorEl.className = 'activate-error';
|
|
2052
|
+
errorEl.innerHTML = `<i class="fas fa-exclamation-circle mr-1"></i>${escapeHtml(e.message)}`;
|
|
2053
|
+
card.appendChild(errorEl);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
async stopServer() {
|
|
2059
|
+
const btn = this.querySelector('#stopBtn');
|
|
2060
|
+
if (btn) {
|
|
2061
|
+
btn.disabled = true;
|
|
2062
|
+
btn.textContent = 'Stopping...';
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
try {
|
|
2066
|
+
await api.stopLocalLlm(this._selectedEngine);
|
|
2067
|
+
await this.refresh();
|
|
2068
|
+
} catch (e) {
|
|
2069
|
+
console.error('Failed to stop server:', e);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
async stopEmbedding() {
|
|
2074
|
+
const btn = this.querySelector('#stopEmbBtn');
|
|
2075
|
+
if (btn) {
|
|
2076
|
+
btn.disabled = true;
|
|
2077
|
+
btn.textContent = 'Stopping...';
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
try {
|
|
2081
|
+
await api.stopLocalEmbedding(this._selectedEngine);
|
|
2082
|
+
await this.refresh();
|
|
2083
|
+
} catch (e) {
|
|
2084
|
+
console.error('Failed to stop embedding:', e);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
async searchHuggingFace() {
|
|
2089
|
+
const input = this.querySelector('#hfSearchInput');
|
|
2090
|
+
const query = input?.value?.trim();
|
|
2091
|
+
if (!query) return;
|
|
2092
|
+
|
|
2093
|
+
const formatSelect = this.querySelector('#hfFormatSelect');
|
|
2094
|
+
this._browseFormat = formatSelect?.value || 'gguf';
|
|
2095
|
+
|
|
2096
|
+
const container = this.querySelector('#hfResults');
|
|
2097
|
+
const btn = this.querySelector('#hfSearchBtn');
|
|
2098
|
+
container.innerHTML = '<div class="text-muted text-center py-8"><i class="fas fa-spinner fa-spin mr-2"></i>Searching HuggingFace...</div>';
|
|
2099
|
+
btn.disabled = true;
|
|
2100
|
+
|
|
2101
|
+
try {
|
|
2102
|
+
this.searchResults = await api.browseHuggingFace(query, 10, this._browseFormat);
|
|
2103
|
+
this.renderSearchResults();
|
|
2104
|
+
} catch (e) {
|
|
2105
|
+
container.innerHTML = `<div class="text-red text-center py-4">Error: ${escapeHtml(e.message)}</div>`;
|
|
2106
|
+
} finally {
|
|
2107
|
+
btn.disabled = false;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
selectedFileForRow(idx) {
|
|
2112
|
+
const select = this.querySelector(`#gguf-select-${idx}`);
|
|
2113
|
+
if (!select) return null;
|
|
2114
|
+
const fileName = select.value;
|
|
2115
|
+
const result = this.searchResults[idx];
|
|
2116
|
+
return result?.ggufFiles.find(f => f.fileName === fileName) ?? null;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
updateRowDownloadState(idx) {
|
|
2120
|
+
const btn = this.querySelector(`.download-btn[data-idx="${idx}"]`);
|
|
2121
|
+
if (!btn) return;
|
|
2122
|
+
const result = this.searchResults[idx];
|
|
2123
|
+
const file = this.selectedFileForRow(idx);
|
|
2124
|
+
if (!result || !file) return;
|
|
2125
|
+
|
|
2126
|
+
const downloaded = this.isModelDownloaded(result.repoId, file.fileName);
|
|
2127
|
+
const downloadId = `${result.repoId}/${file.fileName}`;
|
|
2128
|
+
const isDownloading = this.activeDownloads.has(downloadId);
|
|
2129
|
+
|
|
2130
|
+
// Always clear download-in-progress classes first
|
|
2131
|
+
btn.classList.remove('opacity-50', 'pointer-events-none');
|
|
2132
|
+
|
|
2133
|
+
if (downloaded) {
|
|
2134
|
+
btn.disabled = true;
|
|
2135
|
+
btn.classList.remove('btn-amber');
|
|
2136
|
+
btn.classList.add('btn-green', 'cursor-not-allowed');
|
|
2137
|
+
btn.innerHTML = '<i class="fas fa-check mr-1"></i>Downloaded';
|
|
2138
|
+
} else if (isDownloading) {
|
|
2139
|
+
btn.classList.add('opacity-50', 'pointer-events-none');
|
|
2140
|
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
|
2141
|
+
} else {
|
|
2142
|
+
btn.disabled = false;
|
|
2143
|
+
btn.classList.remove('btn-green', 'cursor-not-allowed');
|
|
2144
|
+
btn.classList.add('btn-amber');
|
|
2145
|
+
btn.innerHTML = '<i class="fas fa-download mr-1"></i>Download';
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
updateRowWarning(idx) {
|
|
2150
|
+
const warning = this.querySelector(`.ram-warning[data-idx="${idx}"]`);
|
|
2151
|
+
if (!warning) return;
|
|
2152
|
+
const file = this.selectedFileForRow(idx);
|
|
2153
|
+
if (file && this.systemRamBytes > 0 && file.sizeBytes > this.systemRamBytes) {
|
|
2154
|
+
warning.classList.remove('hidden');
|
|
2155
|
+
} else {
|
|
2156
|
+
warning.classList.add('hidden');
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
isModelDownloaded(repoId, fileName) {
|
|
2161
|
+
// For MLX repos, match by repo ID (fileName is __mlx_repo__ sentinel)
|
|
2162
|
+
if (fileName === '__mlx_repo__') {
|
|
2163
|
+
return this.models.some(m => m.repo === repoId && m.type === 'mlx');
|
|
2164
|
+
}
|
|
2165
|
+
return this.models.some(m => m.repo === repoId && m.fileName === fileName);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
renderSearchResults() {
|
|
2169
|
+
const container = this.querySelector('#hfResults');
|
|
2170
|
+
if (!container) return;
|
|
2171
|
+
|
|
2172
|
+
const isMlx = this._browseFormat === 'mlx';
|
|
2173
|
+
const emptyLabel = isMlx ? 'MLX' : 'GGUF';
|
|
2174
|
+
|
|
2175
|
+
if (!this.searchResults.length) {
|
|
2176
|
+
container.innerHTML = `<div class="text-muted text-center py-8">No ${emptyLabel} models found for this query.</div>`;
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// Filter out results with no files
|
|
2181
|
+
const resultsWithFiles = this.searchResults.filter(r => r.ggufFiles.length > 0);
|
|
2182
|
+
if (!resultsWithFiles.length) {
|
|
2183
|
+
container.innerHTML = `<div class="text-muted text-center py-8">No ${emptyLabel} files found in the results.</div>`;
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
const ram = this.systemRamBytes;
|
|
2188
|
+
|
|
2189
|
+
const rows = this.searchResults.map((result, idx) => {
|
|
2190
|
+
if (result.ggufFiles.length === 0) return '';
|
|
2191
|
+
|
|
2192
|
+
const caps = detectCapabilities(result);
|
|
2193
|
+
const capIcons = [
|
|
2194
|
+
caps.tools ? '<i class="fas fa-wrench text-green" title="Tool calling"></i>' : '<i class="fas fa-wrench text-muted" title="No tool calling"></i>',
|
|
2195
|
+
caps.vision ? '<i class="fas fa-eye text-blue" title="Vision"></i>' : '<i class="fas fa-eye text-muted" title="No vision"></i>',
|
|
2196
|
+
caps.reasoning ? '<i class="fas fa-brain text-purple" title="Reasoning / Thinking"></i>' : '<i class="fas fa-brain text-muted" title="No reasoning"></i>',
|
|
2197
|
+
].join('');
|
|
2198
|
+
|
|
2199
|
+
if (isMlx) {
|
|
2200
|
+
// MLX: single entry per repo, no file select
|
|
2201
|
+
const totalSize = result.ggufFiles[0]?.sizeBytes || 0;
|
|
2202
|
+
const downloaded = this.isModelDownloaded(result.repoId, '__mlx_repo__');
|
|
2203
|
+
const tooLarge = ram > 0 && totalSize > ram;
|
|
2204
|
+
|
|
2205
|
+
return `
|
|
2206
|
+
<div class="hf-result-row" data-idx="${idx}">
|
|
2207
|
+
<div class="min-w-0 flex-shrink-0">
|
|
2208
|
+
<div class="font-medium text-primary text-sm truncate" title="${escapeHtml(result.repoId)}">${escapeHtml(result.modelName)}</div>
|
|
2209
|
+
<div class="text-xs text-muted truncate">${escapeHtml(result.author)}</div>
|
|
2210
|
+
</div>
|
|
2211
|
+
<div class="flex items-center gap-2 text-xs flex-shrink-0">
|
|
2212
|
+
<span class="text-muted" title="Downloads"><i class="fas fa-download mr-1"></i>${result.downloads?.toLocaleString() ?? 0}</span>
|
|
2213
|
+
<span class="flex items-center gap-1">${capIcons}</span>
|
|
2214
|
+
</div>
|
|
2215
|
+
<span class="text-xs text-muted">${formatBytes(totalSize)}</span>
|
|
2216
|
+
${tooLarge ? '<span class="ram-warning ram-warning-badge" title="Exceeds system RAM"><i class="fas fa-memory mr-1"></i>won\'t fit</span>' : ''}
|
|
2217
|
+
<div class="flex items-center gap-2 flex-shrink-0">
|
|
2218
|
+
<button class="download-btn btn btn-sm ${downloaded ? 'btn-green cursor-not-allowed' : 'btn-amber'}" data-idx="${idx}" data-type="mlx" ${downloaded ? 'disabled' : ''}>
|
|
2219
|
+
${downloaded ? '<i class="fas fa-check mr-1"></i>Downloaded' : '<i class="fas fa-download mr-1"></i>Download'}
|
|
2220
|
+
</button>
|
|
2221
|
+
</div>
|
|
2222
|
+
</div>`;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// GGUF: file select
|
|
2226
|
+
const options = result.ggufFiles.map(f =>
|
|
2227
|
+
`<option value="${escapeHtml(f.fileName)}" data-size="${f.sizeBytes}">${escapeHtml(f.fileName)} (${formatBytes(f.sizeBytes)})</option>`
|
|
2228
|
+
).join('');
|
|
2229
|
+
|
|
2230
|
+
const firstFile = result.ggufFiles[0];
|
|
2231
|
+
const firstTooLarge = ram > 0 && firstFile.sizeBytes > ram;
|
|
2232
|
+
const firstDownloaded = this.isModelDownloaded(result.repoId, firstFile.fileName);
|
|
2233
|
+
|
|
2234
|
+
return `
|
|
2235
|
+
<div class="hf-result-row" data-idx="${idx}">
|
|
2236
|
+
<div class="min-w-0 flex-shrink-0">
|
|
2237
|
+
<div class="font-medium text-primary text-sm truncate" title="${escapeHtml(result.repoId)}">${escapeHtml(result.modelName)}</div>
|
|
2238
|
+
<div class="text-xs text-muted truncate">${escapeHtml(result.author)}</div>
|
|
2239
|
+
</div>
|
|
2240
|
+
<div class="flex items-center gap-2 text-xs flex-shrink-0">
|
|
2241
|
+
<span class="text-muted" title="Downloads"><i class="fas fa-download mr-1"></i>${result.downloads?.toLocaleString() ?? 0}</span>
|
|
2242
|
+
<span class="flex items-center gap-1">${capIcons}</span>
|
|
2243
|
+
</div>
|
|
2244
|
+
<select class="gguf-select hf-select" id="gguf-select-${idx}">
|
|
2245
|
+
${options}
|
|
2246
|
+
</select>
|
|
2247
|
+
<span class="ram-warning ram-warning-badge ${firstTooLarge ? '' : 'hidden'}" data-idx="${idx}" title="File size exceeds system RAM (${formatBytes(ram)})"><i class="fas fa-memory mr-1"></i>won't fit</span>
|
|
2248
|
+
<div class="flex items-center gap-2 flex-shrink-0">
|
|
2249
|
+
<button class="download-btn btn btn-sm ${firstDownloaded
|
|
2250
|
+
? 'btn-green cursor-not-allowed'
|
|
2251
|
+
: 'btn-amber'}" data-idx="${idx}" ${firstDownloaded ? 'disabled' : ''}>
|
|
2252
|
+
${firstDownloaded
|
|
2253
|
+
? '<i class="fas fa-check mr-1"></i>Downloaded'
|
|
2254
|
+
: '<i class="fas fa-download mr-1"></i>Download'}
|
|
2255
|
+
</button>
|
|
2256
|
+
</div>
|
|
2257
|
+
</div>`;
|
|
2258
|
+
}).join('');
|
|
2259
|
+
|
|
2260
|
+
container.innerHTML = `<div class="space-y-2">${rows}</div>`;
|
|
2261
|
+
|
|
2262
|
+
// Update RAM warning and download button when select changes (GGUF only)
|
|
2263
|
+
container.querySelectorAll('.gguf-select').forEach(select => {
|
|
2264
|
+
select.addEventListener('change', () => {
|
|
2265
|
+
const idx = parseInt(select.id.replace('gguf-select-', ''));
|
|
2266
|
+
this.updateRowWarning(idx);
|
|
2267
|
+
this.updateRowDownloadState(idx);
|
|
2268
|
+
});
|
|
2269
|
+
});
|
|
2270
|
+
|
|
2271
|
+
container.querySelectorAll('.download-btn').forEach(btn => {
|
|
2272
|
+
btn.addEventListener('click', () => {
|
|
2273
|
+
const idx = parseInt(btn.dataset.idx);
|
|
2274
|
+
const result = this.searchResults[idx];
|
|
2275
|
+
const dlType = btn.dataset.type || 'gguf';
|
|
2276
|
+
|
|
2277
|
+
if (dlType === 'mlx') {
|
|
2278
|
+
this.downloadModel(result.repoId, '__mlx_repo__', idx, 'mlx');
|
|
2279
|
+
} else {
|
|
2280
|
+
const select = container.querySelector(`#gguf-select-${idx}`);
|
|
2281
|
+
const fileName = select?.value;
|
|
2282
|
+
if (result && fileName) {
|
|
2283
|
+
this.downloadModel(result.repoId, fileName, idx);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
this.startDownloadPolling();
|
|
2287
|
+
});
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
downloadModel(repo, fileName, rowIdx, type = 'gguf') {
|
|
2292
|
+
const downloadId = type === 'mlx' ? `mlx:${repo}` : `${repo}/${fileName}`;
|
|
2293
|
+
if (this.activeDownloads.has(downloadId)) return;
|
|
2294
|
+
|
|
2295
|
+
const es = api.downloadLocalModel(repo, fileName, type);
|
|
2296
|
+
this.activeDownloads.set(downloadId, es);
|
|
2297
|
+
|
|
2298
|
+
// Update button state for the search result row (if any)
|
|
2299
|
+
if (rowIdx != null) this.updateRowDownloadState(rowIdx);
|
|
2300
|
+
|
|
2301
|
+
es.onmessage = (event) => {
|
|
2302
|
+
try {
|
|
2303
|
+
const data = JSON.parse(event.data);
|
|
2304
|
+
|
|
2305
|
+
if (data.type === 'complete') {
|
|
2306
|
+
this.cleanupDownload(downloadId);
|
|
2307
|
+
if (rowIdx != null) this.updateRowDownloadState(rowIdx);
|
|
2308
|
+
this.loadModels();
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
if (data.type === 'error') {
|
|
2312
|
+
console.error('Download error:', data.error);
|
|
2313
|
+
this.cleanupDownload(downloadId);
|
|
2314
|
+
if (rowIdx != null) this.updateRowDownloadState(rowIdx);
|
|
2315
|
+
}
|
|
2316
|
+
} catch {
|
|
2317
|
+
// ignore parse errors
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
es.onerror = () => {
|
|
2322
|
+
this.cleanupDownload(downloadId);
|
|
2323
|
+
if (rowIdx != null) this.updateRowDownloadState(rowIdx);
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
cleanupDownload(downloadId) {
|
|
2328
|
+
const es = this.activeDownloads.get(downloadId);
|
|
2329
|
+
if (es) {
|
|
2330
|
+
es.close();
|
|
2331
|
+
this.activeDownloads.delete(downloadId);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
postRender() {
|
|
2336
|
+
this.querySelector('#refreshBtn')?.addEventListener('click', async () => {
|
|
2337
|
+
await this.loadLlmConfig();
|
|
2338
|
+
await this.refresh();
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
this.querySelector('#hfSearchBtn')?.addEventListener('click', () => this.searchHuggingFace());
|
|
2342
|
+
this.querySelector('#hfSearchInput')?.addEventListener('keydown', (e) => {
|
|
2343
|
+
if (e.key === 'Enter') this.searchHuggingFace();
|
|
2344
|
+
});
|
|
2345
|
+
|
|
2346
|
+
// Default to MLX format on Apple Silicon
|
|
2347
|
+
if (isAppleSilicon(this.status)) {
|
|
2348
|
+
const formatSelect = this.querySelector('#hfFormatSelect');
|
|
2349
|
+
if (formatSelect) formatSelect.value = 'mlx';
|
|
2350
|
+
this._browseFormat = 'mlx';
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
template() {
|
|
2355
|
+
return `
|
|
2356
|
+
<div class="space-y-6 h-full overflow-y-auto pb-6 custom-scrollbar view-panel">
|
|
2357
|
+
<div class="flex items-center justify-between border-b pb-4">
|
|
2358
|
+
<div>
|
|
2359
|
+
<h2 class="text-lg font-semibold text-primary">LLM Configuration</h2>
|
|
2360
|
+
<p class="text-xs text-muted mt-1">Configure your AI model provider</p>
|
|
2361
|
+
</div>
|
|
2362
|
+
<button id="refreshBtn" class="btn-ghost" title="Refresh">
|
|
2363
|
+
<i class="fas fa-sync-alt text-sm"></i>
|
|
2364
|
+
</button>
|
|
2365
|
+
</div>
|
|
2366
|
+
|
|
2367
|
+
<!-- Provider Tabs -->
|
|
2368
|
+
<div id="providerTabs" class="llm-provider-tabs"></div>
|
|
2369
|
+
|
|
2370
|
+
<!-- Cloud Provider Config (hidden by default) -->
|
|
2371
|
+
<div id="cloudContent" class="hidden"></div>
|
|
2372
|
+
|
|
2373
|
+
<!-- Local LLM Content -->
|
|
2374
|
+
<div id="localContent">
|
|
2375
|
+
<!-- Engine Tabs -->
|
|
2376
|
+
<div id="engineTabs" class="llm-engine-tabs"></div>
|
|
2377
|
+
|
|
2378
|
+
<!-- Status Bar (managed engines) -->
|
|
2379
|
+
<div id="statusBar" class="mb-6">
|
|
2380
|
+
<div class="llm-alert">
|
|
2381
|
+
<i class="fas fa-spinner fa-spin text-muted text-sm"></i>
|
|
2382
|
+
<span class="text-sm text-secondary">Loading...</span>
|
|
2383
|
+
</div>
|
|
2384
|
+
</div>
|
|
2385
|
+
|
|
2386
|
+
<!-- External Models (ollama/lmstudio) -->
|
|
2387
|
+
<div id="externalModelsSection" class="hidden mb-6"></div>
|
|
2388
|
+
|
|
2389
|
+
<!-- Active / Interrupted Downloads -->
|
|
2390
|
+
<div id="activeDownloads" class="mb-4"></div>
|
|
2391
|
+
<div id="interruptedDownloads" class="mb-4"></div>
|
|
2392
|
+
|
|
2393
|
+
<!-- Downloaded Models (managed engines) -->
|
|
2394
|
+
<div id="managedModelsSection" class="mb-6">
|
|
2395
|
+
<h3 class="section-title mb-3">Downloaded Models</h3>
|
|
2396
|
+
<div id="modelsGrid" class="llm-model-grid">
|
|
2397
|
+
<div class="text-muted text-center py-8 col-span-full">Loading...</div>
|
|
2398
|
+
</div>
|
|
2399
|
+
</div>
|
|
2400
|
+
|
|
2401
|
+
<!-- Recommended Models (shown when not all are downloaded) -->
|
|
2402
|
+
<div id="recommendedSection" class="mb-6"></div>
|
|
2403
|
+
|
|
2404
|
+
<!-- HuggingFace Browser (managed engines only) -->
|
|
2405
|
+
<div id="hfSection" class="border-t pt-4">
|
|
2406
|
+
<h3 class="section-title mb-3">HuggingFace Browser</h3>
|
|
2407
|
+
<div class="flex gap-2 mb-2">
|
|
2408
|
+
<input id="hfSearchInput" type="text" placeholder="Search models (e.g. Qwen3, Llama, Phi)..."
|
|
2409
|
+
class="input flex-1" />
|
|
2410
|
+
<select id="hfFormatSelect" class="input" style="width: auto; min-width: 90px;">
|
|
2411
|
+
<option value="gguf">GGUF</option>
|
|
2412
|
+
<option value="mlx">MLX</option>
|
|
2413
|
+
</select>
|
|
2414
|
+
<button id="hfSearchBtn" class="btn btn-accent flex-shrink-0">
|
|
2415
|
+
<i class="fas fa-search mr-1"></i>Search
|
|
2416
|
+
</button>
|
|
2417
|
+
</div>
|
|
2418
|
+
<p class="text-xs text-muted mb-4">
|
|
2419
|
+
<i class="fas fa-wrench text-green mr-1"></i>tool calling
|
|
2420
|
+
<span class="mx-2">|</span>
|
|
2421
|
+
<i class="fas fa-eye text-blue mr-1"></i>vision
|
|
2422
|
+
<span class="mx-2">|</span>
|
|
2423
|
+
<i class="fas fa-brain text-purple mr-1"></i>reasoning
|
|
2424
|
+
<span class="mx-2">|</span>
|
|
2425
|
+
<span class="text-muted">gray = not supported</span>
|
|
2426
|
+
</p>
|
|
2427
|
+
<div id="hfResults">
|
|
2428
|
+
<div class="text-muted text-center py-8 text-sm">
|
|
2429
|
+
<i class="fas fa-cube text-2xl mb-3 block text-muted"></i>
|
|
2430
|
+
Search HuggingFace to find and download models
|
|
2431
|
+
</div>
|
|
2432
|
+
</div>
|
|
2433
|
+
</div>
|
|
2434
|
+
</div>
|
|
2435
|
+
</div>
|
|
2436
|
+
`;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
customElements.define('local-llm-view', LocalLlmView);
|