diff --git a/front_end/panels/ai_chat/BUILD.gn b/front_end/panels/ai_chat/BUILD.gn
index d0368fa2ac..ede2191611 100644
--- a/front_end/panels/ai_chat/BUILD.gn
+++ b/front_end/panels/ai_chat/BUILD.gn
@@ -40,6 +40,27 @@ devtools_module("ai_chat") {
"ui/ToolDescriptionFormatter.ts",
"ui/HelpDialog.ts",
"ui/SettingsDialog.ts",
+ "ui/settings/types.ts",
+ "ui/settings/constants.ts",
+ "ui/settings/i18n-strings.ts",
+ "ui/settings/utils/validation.ts",
+ "ui/settings/utils/storage.ts",
+ "ui/settings/utils/styles.ts",
+ "ui/settings/components/ModelSelectorFactory.ts",
+ "ui/settings/components/SettingsHeader.ts",
+ "ui/settings/components/SettingsFooter.ts",
+ "ui/settings/components/AdvancedToggle.ts",
+ "ui/settings/providers/BaseProviderSettings.ts",
+ "ui/settings/providers/OpenAISettings.ts",
+ "ui/settings/providers/LiteLLMSettings.ts",
+ "ui/settings/providers/GroqSettings.ts",
+ "ui/settings/providers/OpenRouterSettings.ts",
+ "ui/settings/providers/BrowserOperatorSettings.ts",
+ "ui/settings/advanced/MCPSettings.ts",
+ "ui/settings/advanced/BrowsingHistorySettings.ts",
+ "ui/settings/advanced/VectorDBSettings.ts",
+ "ui/settings/advanced/TracingSettings.ts",
+ "ui/settings/advanced/EvaluationSettings.ts",
"ui/PromptEditDialog.ts",
"ui/EvaluationDialog.ts",
"ui/WebAppCodeViewer.ts",
@@ -213,6 +234,27 @@ _ai_chat_sources = [
"ui/HelpDialog.ts",
"ui/PromptEditDialog.ts",
"ui/SettingsDialog.ts",
+ "ui/settings/types.ts",
+ "ui/settings/constants.ts",
+ "ui/settings/i18n-strings.ts",
+ "ui/settings/utils/validation.ts",
+ "ui/settings/utils/storage.ts",
+ "ui/settings/utils/styles.ts",
+ "ui/settings/components/ModelSelectorFactory.ts",
+ "ui/settings/components/SettingsHeader.ts",
+ "ui/settings/components/SettingsFooter.ts",
+ "ui/settings/components/AdvancedToggle.ts",
+ "ui/settings/providers/BaseProviderSettings.ts",
+ "ui/settings/providers/OpenAISettings.ts",
+ "ui/settings/providers/LiteLLMSettings.ts",
+ "ui/settings/providers/GroqSettings.ts",
+ "ui/settings/providers/OpenRouterSettings.ts",
+ "ui/settings/providers/BrowserOperatorSettings.ts",
+ "ui/settings/advanced/MCPSettings.ts",
+ "ui/settings/advanced/BrowsingHistorySettings.ts",
+ "ui/settings/advanced/VectorDBSettings.ts",
+ "ui/settings/advanced/TracingSettings.ts",
+ "ui/settings/advanced/EvaluationSettings.ts",
"ui/EvaluationDialog.ts",
"ui/WebAppCodeViewer.ts",
"ui/TodoListDisplay.ts",
diff --git a/front_end/panels/ai_chat/ui/SettingsDialog.ts b/front_end/panels/ai_chat/ui/SettingsDialog.ts
index a822ae7ebf..90530ca669 100644
--- a/front_end/panels/ai_chat/ui/SettingsDialog.ts
+++ b/front_end/panels/ai_chat/ui/SettingsDialog.ts
@@ -2,557 +2,51 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import * as i18n from '../../../core/i18n/i18n.js';
import * as UI from '../../../ui/legacy/legacy.js';
-import { getEvaluationConfig, setEvaluationConfig, isEvaluationEnabled, connectToEvaluationService, disconnectFromEvaluationService, getEvaluationClientId, isEvaluationConnected } from '../common/EvaluationConfig.js';
import { createLogger } from '../core/Logger.js';
import { LLMClient } from '../LLM/LLMClient.js';
-import { getTracingConfig, setTracingConfig, isTracingEnabled } from '../tracing/TracingConfig.js';
-import { getMCPConfig, setMCPConfig, isMCPEnabled, hasStoredAuthErrors, getStoredAuthErrors, clearStoredAuthError } from '../mcp/MCPConfig.js';
-import { MCPRegistry } from '../mcp/MCPRegistry.js';
-import { MCPConnectionsDialog } from './mcp/MCPConnectionsDialog.js';
-import { DEFAULT_PROVIDER_MODELS } from './AIChatPanel.js';
+// Import settings utilities
+import { i18nString, UIStrings } from './settings/i18n-strings.js';
+import { PROVIDER_SELECTION_KEY, MINI_MODEL_STORAGE_KEY, NANO_MODEL_STORAGE_KEY, ADVANCED_SETTINGS_ENABLED_KEY } from './settings/constants.js';
+import { applySettingsStyles } from './settings/utils/styles.js';
+import { isVectorDBEnabled } from './settings/utils/storage.js';
+import type { ModelOption, ProviderType, FetchLiteLLMModelsFunction, UpdateModelOptionsFunction, GetModelOptionsFunction, AddCustomModelOptionFunction, RemoveCustomModelOptionFunction } from './settings/types.js';
+
+// Re-export for backward compatibility
+export { isVectorDBEnabled };
+
+// Import provider settings classes
+import { OpenAISettings } from './settings/providers/OpenAISettings.js';
+import { LiteLLMSettings } from './settings/providers/LiteLLMSettings.js';
+import { GroqSettings } from './settings/providers/GroqSettings.js';
+import { OpenRouterSettings } from './settings/providers/OpenRouterSettings.js';
+import { BrowserOperatorSettings } from './settings/providers/BrowserOperatorSettings.js';
+
+// Import advanced feature settings classes
+import { MCPSettings } from './settings/advanced/MCPSettings.js';
+import { BrowsingHistorySettings } from './settings/advanced/BrowsingHistorySettings.js';
+import { VectorDBSettings } from './settings/advanced/VectorDBSettings.js';
+import { TracingSettings } from './settings/advanced/TracingSettings.js';
+import { EvaluationSettings } from './settings/advanced/EvaluationSettings.js';
+
import './model_selector/ModelSelector.js';
const logger = createLogger('SettingsDialog');
-// Model type definition
-interface ModelOption {
- value: string;
- label: string;
- type: 'openai' | 'litellm' | 'groq' | 'openrouter' | 'browseroperator';
-}
-
-// Local storage keys
-const MINI_MODEL_STORAGE_KEY = 'ai_chat_mini_model';
-const NANO_MODEL_STORAGE_KEY = 'ai_chat_nano_model';
-const LITELLM_ENDPOINT_KEY = 'ai_chat_litellm_endpoint';
-const LITELLM_API_KEY_STORAGE_KEY = 'ai_chat_litellm_api_key';
-const GROQ_API_KEY_STORAGE_KEY = 'ai_chat_groq_api_key';
-const OPENROUTER_API_KEY_STORAGE_KEY = 'ai_chat_openrouter_api_key';
-const PROVIDER_SELECTION_KEY = 'ai_chat_provider';
-
-// Cache constants
-const OPENROUTER_MODELS_CACHE_DURATION_MS = 60 * 60 * 1000; // 60 minutes
-// Vector DB configuration keys - Milvus format
-const VECTOR_DB_ENABLED_KEY = 'ai_chat_vector_db_enabled';
-const MILVUS_ENDPOINT_KEY = 'ai_chat_milvus_endpoint';
-const MILVUS_USERNAME_KEY = 'ai_chat_milvus_username';
-const MILVUS_PASSWORD_KEY = 'ai_chat_milvus_password';
-const MILVUS_COLLECTION_KEY = 'ai_chat_milvus_collection';
-const MILVUS_OPENAI_KEY = 'ai_chat_milvus_openai_key';
-
-// UI Strings
-const UIStrings = {
- /**
- *@description Settings dialog title
- */
- settings: 'Settings',
- /**
- *@description Provider selection label
- */
- providerLabel: 'Provider',
- /**
- *@description Provider selection hint
- */
- providerHint: 'Select which AI provider to use',
- /**
- *@description OpenAI provider option
- */
- openaiProvider: 'OpenAI',
- /**
- *@description LiteLLM provider option
- */
- litellmProvider: 'LiteLLM',
- /**
- *@description Groq provider option
- */
- groqProvider: 'Groq',
- /**
- *@description OpenRouter provider option
- */
- openrouterProvider: 'OpenRouter',
- /**
- *@description LiteLLM API Key label
- */
- liteLLMApiKey: 'LiteLLM API Key',
- /**
- *@description LiteLLM API Key hint
- */
- liteLLMApiKeyHint: 'Your LiteLLM API key for authentication (optional)',
- /**
- *@description LiteLLM endpoint label
- */
- litellmEndpointLabel: 'LiteLLM Endpoint',
- /**
- *@description LiteLLM endpoint hint
- */
- litellmEndpointHint: 'Enter the URL for your LiteLLM server (e.g., http://localhost:4000 or https://your-litellm-server.com)',
- /**
- *@description Groq API Key label
- */
- groqApiKeyLabel: 'Groq API Key',
- /**
- *@description Groq API Key hint
- */
- groqApiKeyHint: 'Your Groq API key for authentication',
- /**
- *@description Fetch Groq models button text
- */
- fetchGroqModelsButton: 'Fetch Groq Models',
- /**
- *@description OpenRouter API Key label
- */
- openrouterApiKeyLabel: 'OpenRouter API Key',
- /**
- *@description OpenRouter API Key hint
- */
- openrouterApiKeyHint: 'Your OpenRouter API key for authentication',
- /**
- *@description Fetch OpenRouter models button text
- */
- fetchOpenRouterModelsButton: 'Fetch OpenRouter Models',
- /**
- *@description BrowserOperator provider option
- */
- browseroperatorProvider: 'BrowserOperator',
- /**
- *@description BrowserOperator endpoint label
- */
- browseroperatorEndpointLabel: 'BrowserOperator Endpoint',
- /**
- *@description BrowserOperator endpoint hint
- */
- browseroperatorEndpointHint: 'URL for your BrowserOperator API server (e.g., http://localhost:8080/v1)',
- /**
- *@description BrowserOperator agent label
- */
- browseroperatorAgentLabel: 'Default Agent',
- /**
- *@description BrowserOperator agent hint
- */
- browseroperatorAgentHint: 'Agent for routing: deep-research (Cerebras), web-agent (OpenAI), code-assist (Groq), chat, fast, default',
- /**
- *@description Test BrowserOperator connection button
- */
- testBrowseroperatorConnection: 'Test Connection',
- /**
- *@description OpenAI API Key label
- */
- apiKeyLabel: 'OpenAI API Key',
- /**
- *@description OpenAI API Key hint
- */
- apiKeyHint: 'An OpenAI API key is required for OpenAI models (GPT-4.1, O4 Mini, etc.)',
- /**
- *@description Test button text
- */
- testButton: 'Test',
- /**
- *@description Add button text
- */
- addButton: 'Add',
- /**
- *@description Remove button text
- */
- removeButton: 'Remove',
- /**
- *@description Fetch models button text
- */
- fetchModelsButton: 'Fetch LiteLLM Models',
- /**
- *@description Fetching models status
- */
- fetchingModels: 'Fetching models...',
- /**
- *@description Wildcard models only message
- */
- wildcardModelsOnly: 'LiteLLM proxy returned wildcard model only. Please add custom models below.',
- /**
- *@description Wildcard and custom models message
- */
- wildcardAndCustomModels: 'Fetched wildcard model (custom models available)',
- /**
- *@description Wildcard and other models message with count
- */
- wildcardAndOtherModels: 'Fetched {PH1} models plus wildcard',
- /**
- *@description Fetched models message with count
- */
- fetchedModels: 'Fetched {PH1} models',
- /**
- *@description LiteLLM endpoint required error
- */
- endpointRequired: 'LiteLLM endpoint is required to test model',
- /**
- *@description Custom models label
- */
- customModelsLabel: 'Custom Models',
- /**
- *@description Custom models hint
- */
- customModelsHint: 'Add custom models one at a time.',
- /**
- *@description Mini model label
- */
- miniModelLabel: 'Mini Model',
- /**
- *@description Mini model description
- */
- miniModelDescription: 'Used for fast operations, tools, and sub-tasks',
- /**
- *@description Nano model label
- */
- nanoModelLabel: 'Nano Model',
- /**
- *@description Nano model description
- */
- nanoModelDescription: 'Used for very fast operations and simple tasks',
- /**
- *@description Default mini model option
- */
- defaultMiniOption: 'Use default (main model)',
- /**
- *@description Default nano model option
- */
- defaultNanoOption: 'Use default (mini model or main model)',
- /**
- *@description Browsing history section title
- */
- browsingHistoryTitle: 'Browsing History',
- /**
- *@description Browsing history description
- */
- browsingHistoryDescription: 'Your browsing history is stored locally to enable search by domains and keywords.',
- /**
- *@description Clear browsing history button
- */
- clearHistoryButton: 'Clear Browsing History',
- /**
- *@description History cleared message
- */
- historyCleared: 'Browsing history cleared successfully',
- /**
- *@description Important notice title
- */
- importantNotice: 'Important Notice',
- /**
- *@description Vector DB section label
- */
- vectorDBLabel: 'Vector Database Configuration',
- /**
- *@description Vector DB enabled label
- */
- vectorDBEnabled: 'Enable Vector Database',
- /**
- *@description Vector DB enabled hint
- */
- vectorDBEnabledHint: 'Enable Vector Database for semantic search of websites',
- /**
- *@description Milvus endpoint label
- */
- vectorDBEndpoint: 'Milvus Endpoint',
- /**
- *@description Milvus endpoint hint
- */
- vectorDBEndpointHint: 'Enter the URL for your Milvus server (e.g., http://localhost:19530 or https://your-milvus.com)',
- /**
- *@description Milvus username label
- */
- vectorDBApiKey: 'Milvus Username',
- /**
- *@description Milvus username hint
- */
- vectorDBApiKeyHint: 'For self-hosted: username (default: root). For Milvus Cloud: leave as root',
- /**
- *@description Vector DB collection label
- */
- vectorDBCollection: 'Collection Name',
- /**
- *@description Vector DB collection hint
- */
- vectorDBCollectionHint: 'Name of the collection to store websites (default: bookmarks)',
- /**
- *@description Milvus password/token label
- */
- milvusPassword: 'Password/API Token',
- /**
- *@description Milvus password/token hint
- */
- milvusPasswordHint: 'For self-hosted: password (default: Milvus). For Milvus Cloud: API token directly',
- /**
- *@description OpenAI API key for embeddings label
- */
- milvusOpenAIKey: 'OpenAI API Key (for embeddings)',
- /**
- *@description OpenAI API key for embeddings hint
- */
- milvusOpenAIKeyHint: 'Required for generating embeddings using OpenAI text-embedding-3-small model',
- /**
- *@description Test vector DB connection button
- */
- testVectorDBConnection: 'Test Connection',
- /**
- *@description Vector DB connection testing status
- */
- testingVectorDBConnection: 'Testing connection...',
- /**
- *@description Vector DB connection success message
- */
- vectorDBConnectionSuccess: 'Vector DB connection successful!',
- /**
- *@description Vector DB connection failed message
- */
- vectorDBConnectionFailed: 'Vector DB connection failed',
- /**
- *@description Tracing section title
- */
- tracingSection: 'Tracing Configuration',
- /**
- *@description Tracing enabled label
- */
- tracingEnabled: 'Enable Tracing',
- /**
- *@description Tracing enabled hint
- */
- tracingEnabledHint: 'Enable observability tracing for AI Chat interactions',
- /**
- *@description Langfuse endpoint label
- */
- langfuseEndpoint: 'Langfuse Endpoint',
- /**
- *@description Langfuse endpoint hint
- */
- langfuseEndpointHint: 'URL of your Langfuse server (e.g., http://localhost:3000)',
- /**
- *@description Langfuse public key label
- */
- langfusePublicKey: 'Langfuse Public Key',
- /**
- *@description Langfuse public key hint
- */
- langfusePublicKeyHint: 'Your Langfuse project public key (starts with pk-lf-)',
- /**
- *@description Langfuse secret key label
- */
- langfuseSecretKey: 'Langfuse Secret Key',
- /**
- *@description Langfuse secret key hint
- */
- langfuseSecretKeyHint: 'Your Langfuse project secret key (starts with sk-lf-)',
- /**
- *@description Test tracing button
- */
- testTracing: 'Test Connection',
- /**
- *@description Evaluation section title
- */
- evaluationSection: 'Evaluation Configuration',
- /**
- *@description Evaluation enabled label
- */
- evaluationEnabled: 'Enable Evaluation',
- /**
- *@description Evaluation enabled hint
- */
- evaluationEnabledHint: 'Enable evaluation service connection for AI Chat interactions',
- /**
- *@description Evaluation endpoint label
- */
- evaluationEndpoint: 'Evaluation Endpoint',
- /**
- *@description Evaluation endpoint hint
- */
- evaluationEndpointHint: 'WebSocket endpoint for the evaluation service (e.g., ws://localhost:8080)',
- /**
- *@description Evaluation secret key label
- */
- evaluationSecretKey: 'Evaluation Secret Key',
- /**
- *@description Evaluation secret key hint
- */
- evaluationSecretKeyHint: 'Secret key for authentication with the evaluation service (optional)',
- /**
- *@description Evaluation connection status
- */
- evaluationConnectionStatus: 'Connection Status',
- /**
- *@description MCP section title
- */
- mcpSection: 'MCP Integration',
- /**
- *@description MCP enabled label
- */
- mcpEnabled: 'Enable MCP Integration',
- /**
- *@description MCP enabled hint
- */
- mcpEnabledHint: 'Enable MCP client to discover and call tools via Model Context Protocol',
- /**
- *@description MCP connections header label
- */
- mcpConnectionsHeader: 'Connections',
- /**
- *@description MCP connections hint text
- */
- mcpConnectionsHint: 'Configure one or more MCP servers. OAuth flows use PKCE automatically.',
- /**
- *@description MCP manage connections button text
- */
- mcpManageConnections: 'Manage connections',
- /**
- *@description MCP refresh connections button text
- */
- mcpRefreshConnections: 'Reconnect all',
- /**
- *@description MCP individual reconnect button text
- */
- mcpReconnectButton: 'Reconnect',
- /**
- *@description MCP individual reconnect button text while in progress
- */
- mcpReconnectInProgress: 'Reconnecting…',
- /**
- *@description MCP individual reconnect button failure state text
- */
- mcpReconnectRetry: 'Retry reconnect',
- /**
- *@description MCP discovered tools label
- */
- mcpDiscoveredTools: 'Discovered Tools',
- /**
- *@description MCP discovered tools hint
- */
- mcpDiscoveredToolsHint: 'Select which MCP tools to make available to agents',
- /**
- *@description MCP no tools message
- */
- mcpNoTools: 'No tools discovered. Connect to an MCP server first.',
-
- /**
- *@description MCP tool mode label
- */
- mcpToolMode: 'Tool Selection Mode',
- /**
- *@description MCP tool mode hint
- */
- mcpToolModeHint: 'Choose how MCP tools are selected and surfaced to agents',
- /**
- *@description MCP tool mode all option
- */
- mcpToolModeAll: 'All Tools - Surface all available MCP tools (may impact performance)',
- /**
- *@description MCP tool mode router option
- */
- mcpToolModeRouter: 'Smart Router - Use LLM to select most relevant tools each turn (recommended)',
- /**
- *@description MCP tool mode meta option
- */
- mcpToolModeMeta: 'Meta Tools - Use mcp.search/mcp.invoke for dynamic discovery (best for large catalogs)',
- /**
- *@description MCP max tools per turn label
- */
- mcpMaxToolsPerTurn: 'Max Tools Per Turn',
- /**
- *@description MCP max tools per turn hint
- */
- mcpMaxToolsPerTurnHint: 'Maximum number of tools to surface to agents in a single turn (default: 20)',
- /**
- *@description MCP max MCP tools per turn label
- */
- mcpMaxMcpPerTurn: 'Max MCP Tools Per Turn',
- /**
- *@description MCP max MCP tools per turn hint
- */
- mcpMaxMcpPerTurnHint: 'Maximum number of MCP tools to include in tool selection (default: 8)',
- /**
- *@description MCP auth type label
- */
- mcpAuthType: 'Authentication Method',
- /**
- *@description MCP auth type hint
- */
- mcpAuthTypeHint: 'Choose how to authenticate with your MCP server',
- /**
- *@description MCP bearer option
- */
- mcpAuthBearer: 'Bearer token',
- /**
- *@description MCP OAuth option
- */
- mcpAuthOAuth: 'OAuth (redirect to provider)',
- /**
- *@description MCP OAuth client ID label
- */
- mcpOAuthClientId: 'OAuth Client ID',
- /**
- *@description MCP OAuth client ID hint
- */
- mcpOAuthClientIdHint: 'Pre-registered public client ID for this MCP server (no secret).',
- /**
- *@description MCP OAuth redirect URL label
- */
- mcpOAuthRedirect: 'OAuth Redirect URL',
- /**
- *@description MCP OAuth redirect URL hint
- */
- mcpOAuthRedirectHint: 'Must match the redirect URI registered with the provider (default: https://localhost:3000/callback).',
- /**
- *@description MCP OAuth scope label
- */
- mcpOAuthScope: 'OAuth Scope (optional)',
- /**
- *@description MCP OAuth scope hint
- */
- mcpOAuthScopeHint: 'Provider-specific scopes, space-separated. Leave empty if unsure.',
-};
-
-const str_ = i18n.i18n.registerUIStrings('panels/ai_chat/ui/SettingsDialog.ts', UIStrings);
-const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
-
-// Helper function to check if Vector DB is enabled
-export function isVectorDBEnabled(): boolean {
- return localStorage.getItem(VECTOR_DB_ENABLED_KEY) === 'true';
-}
-
export class SettingsDialog {
- // Variables to store direct references to model selectors
- static #openaiMiniModelSelect: any | null = null;
- static #openaiNanoModelSelect: any | null = null;
- static #litellmMiniModelSelect: any | null = null;
- static #litellmNanoModelSelect: any | null = null;
- static #groqMiniModelSelect: any | null = null;
- static #groqNanoModelSelect: any | null = null;
- static #openrouterMiniModelSelect: any | null = null;
- static #openrouterNanoModelSelect: any | null = null;
- static #browseroperatorMiniModelSelect: any | null = null;
- static #browseroperatorNanoModelSelect: any | null = null;
-
static async show(
selectedModel: string,
miniModel: string,
nanoModel: string,
onSettingsSaved: () => void,
- fetchLiteLLMModels: (apiKey: string|null, endpoint?: string) => Promise<{models: ModelOption[], hadWildcard: boolean}>,
- updateModelOptions: (litellmModels: ModelOption[], hadWildcard?: boolean) => void,
- getModelOptions: (provider?: 'openai' | 'litellm' | 'groq' | 'openrouter') => ModelOption[],
- addCustomModelOption: (modelName: string, modelType?: 'openai' | 'litellm' | 'groq' | 'openrouter') => ModelOption[],
- removeCustomModelOption: (modelName: string) => ModelOption[],
+ fetchLiteLLMModels: FetchLiteLLMModelsFunction,
+ updateModelOptions: UpdateModelOptionsFunction,
+ getModelOptions: GetModelOptionsFunction,
+ addCustomModelOption: AddCustomModelOptionFunction,
+ removeCustomModelOption: RemoveCustomModelOptionFunction,
): Promise {
-
- // Get all model options using the provided getModelOptions function
- const modelOptions = getModelOptions();
-
- // Count models by provider
- const openaiModels = modelOptions.filter(m => m.type === 'openai');
- const litellmModels = modelOptions.filter(m => m.type === 'litellm');
-
- // Reset selector references
- SettingsDialog.#openaiMiniModelSelect = null;
- SettingsDialog.#openaiNanoModelSelect = null;
- SettingsDialog.#litellmMiniModelSelect = null;
- SettingsDialog.#litellmNanoModelSelect = null;
+
// Create a settings dialog
const dialog = new UI.Dialog.Dialog();
dialog.setDimmed(true);
@@ -564,66 +58,66 @@ export class SettingsDialog {
contentDiv.className = 'settings-content';
contentDiv.style.overflowY = 'auto';
dialog.contentElement.appendChild(contentDiv);
-
+
// Create header
const headerDiv = document.createElement('div');
headerDiv.className = 'settings-header';
contentDiv.appendChild(headerDiv);
-
+
const title = document.createElement('h2');
title.className = 'settings-title';
title.textContent = i18nString(UIStrings.settings);
headerDiv.appendChild(title);
-
+
const closeButton = document.createElement('button');
closeButton.className = 'settings-close-button';
closeButton.setAttribute('aria-label', 'Close settings');
closeButton.textContent = '×';
closeButton.addEventListener('click', () => dialog.hide());
headerDiv.appendChild(closeButton);
-
+
// Add provider selection dropdown
const providerSection = document.createElement('div');
providerSection.className = 'provider-selection-section';
contentDiv.appendChild(providerSection);
-
+
const providerLabel = document.createElement('div');
providerLabel.className = 'settings-label';
providerLabel.textContent = i18nString(UIStrings.providerLabel);
providerSection.appendChild(providerLabel);
-
+
const providerHint = document.createElement('div');
providerHint.className = 'settings-hint';
providerHint.textContent = i18nString(UIStrings.providerHint);
providerSection.appendChild(providerHint);
-
+
// Use the stored provider from localStorage
- const currentProvider = (localStorage.getItem(PROVIDER_SELECTION_KEY) || 'openai') as 'openai' | 'litellm' | 'groq' | 'openrouter' | 'browseroperator';
-
+ const currentProvider = (localStorage.getItem(PROVIDER_SELECTION_KEY) || 'openai') as ProviderType;
+
// Create provider selection dropdown
const providerSelect = document.createElement('select');
providerSelect.className = 'settings-select provider-select';
providerSection.appendChild(providerSelect);
-
+
// Add options to the dropdown
const openaiOption = document.createElement('option');
openaiOption.value = 'openai';
openaiOption.textContent = i18nString(UIStrings.openaiProvider);
openaiOption.selected = currentProvider === 'openai';
providerSelect.appendChild(openaiOption);
-
+
const litellmOption = document.createElement('option');
litellmOption.value = 'litellm';
litellmOption.textContent = i18nString(UIStrings.litellmProvider);
litellmOption.selected = currentProvider === 'litellm';
providerSelect.appendChild(litellmOption);
-
+
const groqOption = document.createElement('option');
groqOption.value = 'groq';
groqOption.textContent = i18nString(UIStrings.groqProvider);
groqOption.selected = currentProvider === 'groq';
providerSelect.appendChild(groqOption);
-
+
const openrouterOption = document.createElement('option');
openrouterOption.value = 'openrouter';
openrouterOption.textContent = i18nString(UIStrings.openrouterProvider);
@@ -638,23 +132,23 @@ export class SettingsDialog {
// Ensure the select's value reflects the computed currentProvider
providerSelect.value = currentProvider;
-
+
// Create provider-specific content containers
const openaiContent = document.createElement('div');
openaiContent.className = 'provider-content openai-content';
openaiContent.style.display = currentProvider === 'openai' ? 'block' : 'none';
contentDiv.appendChild(openaiContent);
-
+
const litellmContent = document.createElement('div');
litellmContent.className = 'provider-content litellm-content';
litellmContent.style.display = currentProvider === 'litellm' ? 'block' : 'none';
contentDiv.appendChild(litellmContent);
-
+
const groqContent = document.createElement('div');
groqContent.className = 'provider-content groq-content';
groqContent.style.display = currentProvider === 'groq' ? 'block' : 'none';
contentDiv.appendChild(groqContent);
-
+
const openrouterContent = document.createElement('div');
openrouterContent.className = 'provider-content openrouter-content';
openrouterContent.style.display = currentProvider === 'openrouter' ? 'block' : 'none';
@@ -665,10 +159,68 @@ export class SettingsDialog {
browseroperatorContent.style.display = currentProvider === 'browseroperator' ? 'block' : 'none';
contentDiv.appendChild(browseroperatorContent);
+ // Instantiate provider settings classes
+ const openaiSettings = new OpenAISettings(
+ openaiContent,
+ getModelOptions,
+ addCustomModelOption,
+ removeCustomModelOption
+ );
+
+ const litellmSettings = new LiteLLMSettings(
+ litellmContent,
+ getModelOptions,
+ addCustomModelOption,
+ removeCustomModelOption,
+ updateModelOptions,
+ fetchLiteLLMModels
+ );
+
+ const groqSettings = new GroqSettings(
+ groqContent,
+ getModelOptions,
+ addCustomModelOption,
+ removeCustomModelOption,
+ updateModelOptions
+ );
+
+ const openrouterSettings = new OpenRouterSettings(
+ openrouterContent,
+ getModelOptions,
+ addCustomModelOption,
+ removeCustomModelOption,
+ updateModelOptions,
+ onSettingsSaved,
+ () => dialog.hide()
+ );
+
+ const browseroperatorSettings = new BrowserOperatorSettings(
+ browseroperatorContent,
+ getModelOptions,
+ addCustomModelOption,
+ removeCustomModelOption
+ );
+
+ // Render all providers (only visible one will be shown)
+ openaiSettings.render();
+ litellmSettings.render();
+ groqSettings.render();
+ openrouterSettings.render();
+ browseroperatorSettings.render();
+
+ // Store provider settings for later access
+ const providerSettings = new Map([
+ ['openai', openaiSettings],
+ ['litellm', litellmSettings],
+ ['groq', groqSettings],
+ ['openrouter', openrouterSettings],
+ ['browseroperator', browseroperatorSettings],
+ ]);
+
// Event listener for provider change
providerSelect.addEventListener('change', async () => {
- const selectedProvider = providerSelect.value;
-
+ const selectedProvider = providerSelect.value as ProviderType;
+
// Toggle visibility of provider content
openaiContent.style.display = selectedProvider === 'openai' ? 'block' : 'none';
litellmContent.style.display = selectedProvider === 'litellm' ? 'block' : 'none';
@@ -678,14 +230,15 @@ export class SettingsDialog {
// If switching to LiteLLM, fetch the latest models if endpoint is configured
if (selectedProvider === 'litellm') {
- const endpoint = litellmEndpointInput.value.trim();
- const liteLLMApiKey = litellmApiKeyInput.value.trim() || localStorage.getItem(LITELLM_API_KEY_STORAGE_KEY) || '';
-
+ const endpoint = localStorage.getItem('ai_chat_litellm_endpoint');
+ const liteLLMApiKey = localStorage.getItem('ai_chat_litellm_api_key') || '';
+
if (endpoint) {
try {
logger.debug('Fetching LiteLLM models after provider change...');
const { models: litellmModels, hadWildcard } = await fetchLiteLLMModels(liteLLMApiKey, endpoint);
updateModelOptions(litellmModels, hadWildcard);
+ litellmSettings.updateModelSelectors();
logger.debug('Successfully refreshed LiteLLM models after provider change');
} catch (error) {
logger.error('Failed to fetch LiteLLM models after provider change:', error);
@@ -693,8 +246,8 @@ export class SettingsDialog {
}
} else if (selectedProvider === 'groq') {
// If switching to Groq, fetch models if API key is configured
- const groqApiKey = groqApiKeyInput.value.trim() || localStorage.getItem('ai_chat_groq_api_key') || '';
-
+ const groqApiKey = localStorage.getItem('ai_chat_groq_api_key') || '';
+
if (groqApiKey) {
try {
logger.debug('Fetching Groq models after provider change...');
@@ -705,6 +258,7 @@ export class SettingsDialog {
type: 'groq' as const
}));
updateModelOptions(modelOptions, false);
+ groqSettings.updateModelSelectors();
logger.debug('Successfully refreshed Groq models after provider change');
} catch (error) {
logger.error('Failed to fetch Groq models after provider change:', error);
@@ -712,8 +266,8 @@ export class SettingsDialog {
}
} else if (selectedProvider === 'openrouter') {
// If switching to OpenRouter, fetch models if API key is configured
- const openrouterApiKey = openrouterApiKeyInput.value.trim() || localStorage.getItem('ai_chat_openrouter_api_key') || '';
-
+ const openrouterApiKey = localStorage.getItem('ai_chat_openrouter_api_key') || '';
+
if (openrouterApiKey) {
try {
logger.debug('Fetching OpenRouter models after provider change...');
@@ -727,2569 +281,110 @@ export class SettingsDialog {
// Persist cache alongside timestamp for consistency
localStorage.setItem('openrouter_models_cache', JSON.stringify(modelOptions));
localStorage.setItem('openrouter_models_cache_timestamp', Date.now().toString());
+ openrouterSettings.updateModelSelectors();
logger.debug('Successfully refreshed OpenRouter models after provider change');
} catch (error) {
logger.error('Failed to fetch OpenRouter models after provider change:', error);
}
}
}
-
- // Get model options filtered by the selected provider
- const availableModels = getModelOptions(selectedProvider as 'openai' | 'litellm' | 'groq' | 'openrouter');
-
-
- // Refresh model selectors based on new provider
- if (selectedProvider === 'openai') {
- // Use our reusable function to update OpenAI model selectors
- updateOpenAIModelSelectors();
- } else if (selectedProvider === 'litellm') {
- // Make sure LiteLLM selectors are updated
- updateLiteLLMModelSelectors();
- } else if (selectedProvider === 'groq') {
- // Update Groq selectors
- updateGroqModelSelectors();
- } else if (selectedProvider === 'openrouter') {
- // Update OpenRouter selectors
- await updateOpenRouterModelSelectors();
- }
- });
-
- // Setup OpenAI content
- const openaiSettingsSection = document.createElement('div');
- openaiSettingsSection.className = 'settings-section';
- openaiContent.appendChild(openaiSettingsSection);
-
- const apiKeyLabel = document.createElement('div');
- apiKeyLabel.className = 'settings-label';
- apiKeyLabel.textContent = i18nString(UIStrings.apiKeyLabel);
- openaiSettingsSection.appendChild(apiKeyLabel);
-
- const apiKeyHint = document.createElement('div');
- apiKeyHint.className = 'settings-hint';
- apiKeyHint.textContent = i18nString(UIStrings.apiKeyHint);
- openaiSettingsSection.appendChild(apiKeyHint);
-
- const settingsSavedApiKey = localStorage.getItem('ai_chat_api_key') || '';
- const settingsApiKeyInput = document.createElement('input');
- settingsApiKeyInput.className = 'settings-input';
- settingsApiKeyInput.type = 'password';
- settingsApiKeyInput.placeholder = 'Enter your OpenAI API key';
- settingsApiKeyInput.value = settingsSavedApiKey;
- openaiSettingsSection.appendChild(settingsApiKeyInput);
-
- const settingsApiKeyStatus = document.createElement('div');
- settingsApiKeyStatus.className = 'settings-status';
- settingsApiKeyStatus.style.display = 'none';
- openaiSettingsSection.appendChild(settingsApiKeyStatus);
-
- // Function to update OpenAI model selectors
- function updateOpenAIModelSelectors() {
-
- // Get the latest model options filtered for OpenAI provider
- const openaiModels = getModelOptions('openai');
-
- // Get valid models using generic helper
- const validMiniModel = getValidModelForProvider(miniModel, openaiModels, 'openai', 'mini');
- const validNanoModel = getValidModelForProvider(nanoModel, openaiModels, 'openai', 'nano');
-
- // Clear any existing model selectors
- const existingSelectors = openaiContent.querySelectorAll('.model-selection-section');
- existingSelectors.forEach(selector => selector.remove());
-
- // Create a new model selection section
- const openaiModelSection = document.createElement('div');
- openaiModelSection.className = 'settings-section model-selection-section';
- openaiContent.appendChild(openaiModelSection);
-
- const openaiModelSectionTitle = document.createElement('h3');
- openaiModelSectionTitle.className = 'settings-subtitle';
- openaiModelSectionTitle.textContent = 'Model Size Selection';
- openaiModelSection.appendChild(openaiModelSectionTitle);
-
-
- // No focus handler needed for OpenAI selectors as we don't need to fetch models on focus
-
- // Create OpenAI Mini Model selection and store reference
- SettingsDialog.#openaiMiniModelSelect = createModelSelector(
- openaiModelSection,
- i18nString(UIStrings.miniModelLabel),
- i18nString(UIStrings.miniModelDescription),
- 'mini-model-select',
- openaiModels,
- validMiniModel,
- i18nString(UIStrings.defaultMiniOption),
- undefined // No focus handler for OpenAI
- );
-
-
- // Create OpenAI Nano Model selection and store reference
- SettingsDialog.#openaiNanoModelSelect = createModelSelector(
- openaiModelSection,
- i18nString(UIStrings.nanoModelLabel),
- i18nString(UIStrings.nanoModelDescription),
- 'nano-model-select',
- openaiModels,
- validNanoModel,
- i18nString(UIStrings.defaultNanoOption),
- undefined // No focus handler for OpenAI
- );
-
- }
-
- // Initialize OpenAI model selectors
- updateOpenAIModelSelectors();
-
- // Setup LiteLLM content
- const litellmSettingsSection = document.createElement('div');
- litellmSettingsSection.className = 'settings-section';
- litellmContent.appendChild(litellmSettingsSection);
-
- // LiteLLM endpoint
- const litellmEndpointLabel = document.createElement('div');
- litellmEndpointLabel.className = 'settings-label';
- litellmEndpointLabel.textContent = i18nString(UIStrings.litellmEndpointLabel);
- litellmSettingsSection.appendChild(litellmEndpointLabel);
-
- const litellmEndpointHint = document.createElement('div');
- litellmEndpointHint.className = 'settings-hint';
- litellmEndpointHint.textContent = i18nString(UIStrings.litellmEndpointHint);
- litellmSettingsSection.appendChild(litellmEndpointHint);
-
- const settingsSavedLiteLLMEndpoint = localStorage.getItem(LITELLM_ENDPOINT_KEY) || '';
- const litellmEndpointInput = document.createElement('input');
- litellmEndpointInput.className = 'settings-input litellm-endpoint-input';
- litellmEndpointInput.type = 'text';
- litellmEndpointInput.placeholder = 'http://localhost:4000';
- litellmEndpointInput.value = settingsSavedLiteLLMEndpoint;
- litellmSettingsSection.appendChild(litellmEndpointInput);
-
- // LiteLLM API Key
- const litellmAPIKeyLabel = document.createElement('div');
- litellmAPIKeyLabel.className = 'settings-label';
- litellmAPIKeyLabel.textContent = i18nString(UIStrings.liteLLMApiKey);
- litellmSettingsSection.appendChild(litellmAPIKeyLabel);
-
- const litellmAPIKeyHint = document.createElement('div');
- litellmAPIKeyHint.className = 'settings-hint';
- litellmAPIKeyHint.textContent = i18nString(UIStrings.liteLLMApiKeyHint);
- litellmSettingsSection.appendChild(litellmAPIKeyHint);
-
- const settingsSavedLiteLLMApiKey = localStorage.getItem(LITELLM_API_KEY_STORAGE_KEY) || '';
- const litellmApiKeyInput = document.createElement('input');
- litellmApiKeyInput.className = 'settings-input litellm-api-key-input';
- litellmApiKeyInput.type = 'password';
- litellmApiKeyInput.placeholder = 'Enter your LiteLLM API key';
- litellmApiKeyInput.value = settingsSavedLiteLLMApiKey;
- litellmSettingsSection.appendChild(litellmApiKeyInput);
-
- // Create event handler function
- const updateFetchButtonState = () => {
- fetchModelsButton.disabled = !litellmEndpointInput.value.trim();
- };
-
- litellmEndpointInput.addEventListener('input', updateFetchButtonState);
-
- const fetchButtonContainer = document.createElement('div');
- fetchButtonContainer.className = 'fetch-button-container';
- litellmSettingsSection.appendChild(fetchButtonContainer);
-
- const fetchModelsButton = document.createElement('button');
- fetchModelsButton.className = 'settings-button';
- fetchModelsButton.setAttribute('type', 'button'); // Set explicit button type
- fetchModelsButton.textContent = i18nString(UIStrings.fetchModelsButton);
- fetchModelsButton.disabled = !litellmEndpointInput.value.trim();
- fetchButtonContainer.appendChild(fetchModelsButton);
-
- const fetchModelsStatus = document.createElement('div');
- fetchModelsStatus.className = 'settings-status';
- fetchModelsStatus.style.display = 'none';
- fetchButtonContainer.appendChild(fetchModelsStatus);
-
- // Add click handler for fetch models button
- fetchModelsButton.addEventListener('click', async () => {
- fetchModelsButton.disabled = true;
- fetchModelsStatus.textContent = i18nString(UIStrings.fetchingModels);
- fetchModelsStatus.style.display = 'block';
- fetchModelsStatus.style.backgroundColor = 'var(--color-accent-blue-background)';
- fetchModelsStatus.style.color = 'var(--color-accent-blue)';
-
- try {
- const endpoint = litellmEndpointInput.value;
- const liteLLMApiKey = litellmApiKeyInput.value || localStorage.getItem(LITELLM_API_KEY_STORAGE_KEY) || '';
-
- const { models: litellmModels, hadWildcard } = await fetchLiteLLMModels(liteLLMApiKey, endpoint || undefined);
- updateModelOptions(litellmModels, hadWildcard);
-
- // Get counts from centralized getModelOptions
- const allLiteLLMModels = getModelOptions('litellm');
- const actualModelCount = litellmModels.length;
- const hasCustomModels = allLiteLLMModels.length > actualModelCount;
-
- // Refresh existing model selectors with new options if they exist
- if (SettingsDialog.#litellmMiniModelSelect) {
- refreshModelSelectOptions(SettingsDialog.#litellmMiniModelSelect, allLiteLLMModels, miniModel, i18nString(UIStrings.defaultMiniOption));
- }
- if (SettingsDialog.#litellmNanoModelSelect) {
- refreshModelSelectOptions(SettingsDialog.#litellmNanoModelSelect, allLiteLLMModels, nanoModel, i18nString(UIStrings.defaultNanoOption));
- }
-
- if (hadWildcard && actualModelCount === 0 && !hasCustomModels) {
- fetchModelsStatus.textContent = i18nString(UIStrings.wildcardModelsOnly);
- fetchModelsStatus.style.backgroundColor = 'var(--color-accent-orange-background)';
- fetchModelsStatus.style.color = 'var(--color-accent-orange)';
- } else if (hadWildcard && actualModelCount === 0) {
- // Only wildcard was returned but we have custom models
- fetchModelsStatus.textContent = i18nString(UIStrings.wildcardAndCustomModels);
- fetchModelsStatus.style.backgroundColor = 'var(--color-accent-green-background)';
- fetchModelsStatus.style.color = 'var(--color-accent-green)';
- } else if (hadWildcard) {
- // Wildcard plus other models
- fetchModelsStatus.textContent = i18nString(UIStrings.wildcardAndOtherModels, {PH1: actualModelCount});
- fetchModelsStatus.style.backgroundColor = 'var(--color-accent-green-background)';
- fetchModelsStatus.style.color = 'var(--color-accent-green)';
- } else {
- // No wildcard, just regular models
- fetchModelsStatus.textContent = i18nString(UIStrings.fetchedModels, {PH1: actualModelCount});
- fetchModelsStatus.style.backgroundColor = 'var(--color-accent-green-background)';
- fetchModelsStatus.style.color = 'var(--color-accent-green)';
- }
-
- // Update LiteLLM model selections
- updateLiteLLMModelSelectors();
-
- } catch (error) {
- logger.error('Failed to fetch models:', error);
- fetchModelsStatus.textContent = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
- fetchModelsStatus.style.backgroundColor = 'var(--color-accent-red-background)';
- fetchModelsStatus.style.color = 'var(--color-accent-red)';
- } finally {
- updateFetchButtonState();
- setTimeout(() => {
- fetchModelsStatus.style.display = 'none';
- }, 3000);
- }
});
-
- // Custom model section with array support
- const customModelsSection = document.createElement('div');
- customModelsSection.className = 'custom-models-section';
- litellmContent.appendChild(customModelsSection);
-
- const customModelsLabel = document.createElement('div');
- customModelsLabel.className = 'settings-label';
- customModelsLabel.textContent = i18nString(UIStrings.customModelsLabel);
- customModelsSection.appendChild(customModelsLabel);
-
- const customModelsHint = document.createElement('div');
- customModelsHint.className = 'settings-hint';
- customModelsHint.textContent = i18nString(UIStrings.customModelsHint);
- customModelsSection.appendChild(customModelsHint);
-
- // Current custom models list
- const customModelsList = document.createElement('div');
- customModelsList.className = 'custom-models-list';
- customModelsSection.appendChild(customModelsList);
-
- // Helper function to refresh the model list in a select element
- function refreshModelSelectOptions(select: any, models: ModelOption[], currentValue: string, defaultLabel: string) {
- // Custom component path
- if (select && select.tagName && select.tagName.toLowerCase() === 'ai-model-selector') {
- const previousValue = select.value || select.selected || '';
- const opts = [{ value: '', label: defaultLabel }, ...models];
- select.options = opts;
- if (previousValue && opts.some((o: any) => o.value === previousValue)) {
- select.value = previousValue;
- } else if (currentValue && opts.some((o: any) => o.value === currentValue)) {
- select.value = currentValue;
- } else {
- select.value = '';
- }
- return;
- }
- // Native
`;
disclaimerSection.appendChild(disclaimerText);
-
- // Button container
+
+ // Create footer with buttons
+ const footer = document.createElement('div');
+ footer.className = 'settings-footer';
+ contentDiv.appendChild(footer);
+
const buttonContainer = document.createElement('div');
- buttonContainer.className = 'settings-footer';
- contentDiv.appendChild(buttonContainer);
-
- // Status message for save operation
+ buttonContainer.className = 'settings-buttons';
+ footer.appendChild(buttonContainer);
+
const saveStatusMessage = document.createElement('div');
saveStatusMessage.className = 'settings-status save-status';
saveStatusMessage.style.display = 'none';
- saveStatusMessage.style.marginRight = 'auto'; // Push to left
buttonContainer.appendChild(saveStatusMessage);
-
- // Cancel button
+
const cancelButton = document.createElement('button');
- cancelButton.textContent = 'Cancel';
+ cancelButton.textContent = i18nString(UIStrings.cancelButton);
cancelButton.className = 'settings-button cancel-button';
cancelButton.setAttribute('type', 'button');
cancelButton.addEventListener('click', () => dialog.hide());
buttonContainer.appendChild(cancelButton);
-
- // Save button
+
const saveButton = document.createElement('button');
- saveButton.textContent = 'Save';
+ saveButton.textContent = i18nString(UIStrings.saveButton);
saveButton.className = 'settings-button save-button';
saveButton.setAttribute('type', 'button');
buttonContainer.appendChild(saveButton);
-
+
saveButton.addEventListener('click', async () => {
// Disable save button while saving
saveButton.disabled = true;
-
+
// Show saving status
saveStatusMessage.textContent = 'Saving settings...';
saveStatusMessage.style.backgroundColor = 'var(--color-accent-blue-background)';
saveStatusMessage.style.color = 'var(--color-accent-blue)';
saveStatusMessage.style.display = 'block';
-
+
// Save provider selection
const selectedProvider = providerSelect.value;
localStorage.setItem(PROVIDER_SELECTION_KEY, selectedProvider);
-
- // Save OpenAI API key
- const newApiKey = settingsApiKeyInput.value.trim();
- if (newApiKey) {
- localStorage.setItem('ai_chat_api_key', newApiKey);
- } else {
- localStorage.removeItem('ai_chat_api_key');
- }
-
- // Save or remove LiteLLM API key
- const liteLLMApiKeyValue = litellmApiKeyInput.value.trim();
- if (liteLLMApiKeyValue) {
- localStorage.setItem(LITELLM_API_KEY_STORAGE_KEY, liteLLMApiKeyValue);
- } else {
- localStorage.removeItem(LITELLM_API_KEY_STORAGE_KEY);
- }
-
- // Save or remove LiteLLM endpoint
- const liteLLMEndpointValue = litellmEndpointInput.value.trim();
- if (liteLLMEndpointValue) {
- localStorage.setItem(LITELLM_ENDPOINT_KEY, liteLLMEndpointValue);
- } else {
- localStorage.removeItem(LITELLM_ENDPOINT_KEY);
- }
-
- // Save or remove Groq API key
- const groqApiKeyValue = groqApiKeyInput.value.trim();
- if (groqApiKeyValue) {
- localStorage.setItem(GROQ_API_KEY_STORAGE_KEY, groqApiKeyValue);
- } else {
- localStorage.removeItem(GROQ_API_KEY_STORAGE_KEY);
- }
-
- // Save or remove OpenRouter API key
- const openrouterApiKeyValue = openrouterApiKeyInput.value.trim();
- if (openrouterApiKeyValue) {
- localStorage.setItem(OPENROUTER_API_KEY_STORAGE_KEY, openrouterApiKeyValue);
- } else {
- localStorage.removeItem(OPENROUTER_API_KEY_STORAGE_KEY);
- }
-
- // BrowserOperator settings are hardcoded - no need to save endpoint or agent
- // Determine which mini/nano model selectors to use based on current provider
- let miniModelValue = '';
- let nanoModelValue = '';
-
- if (selectedProvider === 'openai') {
- // Get values from OpenAI selectors
- if (SettingsDialog.#openaiMiniModelSelect) {
- miniModelValue = SettingsDialog.#openaiMiniModelSelect.value;
- }
- if (SettingsDialog.#openaiNanoModelSelect) {
- nanoModelValue = SettingsDialog.#openaiNanoModelSelect.value;
- }
- } else if (selectedProvider === 'litellm') {
- // Get values from LiteLLM selectors
- if (SettingsDialog.#litellmMiniModelSelect) {
- miniModelValue = SettingsDialog.#litellmMiniModelSelect.value;
- }
- if (SettingsDialog.#litellmNanoModelSelect) {
- nanoModelValue = SettingsDialog.#litellmNanoModelSelect.value;
- }
- } else if (selectedProvider === 'groq') {
- // Get values from Groq selectors
- if (SettingsDialog.#groqMiniModelSelect) {
- miniModelValue = SettingsDialog.#groqMiniModelSelect.value;
- }
- if (SettingsDialog.#groqNanoModelSelect) {
- nanoModelValue = SettingsDialog.#groqNanoModelSelect.value;
- }
- } else if (selectedProvider === 'openrouter') {
- // Get values from OpenRouter selectors
- if (SettingsDialog.#openrouterMiniModelSelect) {
- miniModelValue = SettingsDialog.#openrouterMiniModelSelect.value;
- }
- if (SettingsDialog.#openrouterNanoModelSelect) {
- nanoModelValue = SettingsDialog.#openrouterNanoModelSelect.value;
- }
- } else if (selectedProvider === 'browseroperator') {
- // Get values from BrowserOperator selectors
- if (SettingsDialog.#browseroperatorMiniModelSelect) {
- miniModelValue = SettingsDialog.#browseroperatorMiniModelSelect.value;
- }
- if (SettingsDialog.#browseroperatorNanoModelSelect) {
- nanoModelValue = SettingsDialog.#browseroperatorNanoModelSelect.value;
+ // Save all provider settings
+ openaiSettings.save();
+ litellmSettings.save();
+ groqSettings.save();
+ openrouterSettings.save();
+ browseroperatorSettings.save();
+
+ // Save mini/nano model selections from current provider
+ const currentProviderSettings = providerSettings.get(selectedProvider as ProviderType);
+ if (currentProviderSettings) {
+ const miniModelValue = currentProviderSettings.getMiniModel();
+ const nanoModelValue = currentProviderSettings.getNanoModel();
+
+ logger.debug('Mini model value to save:', miniModelValue);
+ if (miniModelValue) {
+ localStorage.setItem(MINI_MODEL_STORAGE_KEY, miniModelValue);
+ } else {
+ localStorage.removeItem(MINI_MODEL_STORAGE_KEY);
}
- }
-
- // Save mini model if selected
- logger.debug('Mini model value to save:', miniModelValue);
- if (miniModelValue) {
- localStorage.setItem(MINI_MODEL_STORAGE_KEY, miniModelValue);
- } else {
- localStorage.removeItem(MINI_MODEL_STORAGE_KEY);
- }
-
- // Save nano model if selected
- logger.debug('Nano model value to save:', nanoModelValue);
- if (nanoModelValue) {
- localStorage.setItem(NANO_MODEL_STORAGE_KEY, nanoModelValue);
- } else {
- localStorage.removeItem(NANO_MODEL_STORAGE_KEY);
- }
-
- // Save tracing configuration
- if (tracingEnabledCheckbox.checked) {
- const endpoint = endpointInput.value.trim();
- const publicKey = publicKeyInput.value.trim();
- const secretKey = secretKeyInput.value.trim();
- if (endpoint && publicKey && secretKey) {
- setTracingConfig({
- provider: 'langfuse',
- endpoint,
- publicKey,
- secretKey
- });
+ logger.debug('Nano model value to save:', nanoModelValue);
+ if (nanoModelValue) {
+ localStorage.setItem(NANO_MODEL_STORAGE_KEY, nanoModelValue);
+ } else {
+ localStorage.removeItem(NANO_MODEL_STORAGE_KEY);
}
- } else {
- setTracingConfig({ provider: 'disabled' });
}
- // Save evaluation configuration
- setEvaluationConfig({
- enabled: evaluationEnabledCheckbox.checked,
- endpoint: evaluationEndpointInput.value.trim() || 'ws://localhost:8080',
- secretKey: evaluationSecretKeyInput.value.trim()
- });
+ // Save all advanced feature settings
+ vectorDBSettings.save();
+ tracingSettings.save();
+ evaluationSettings.save();
+ // browsingHistorySettings and mcpSettings don't have save() methods as they auto-save
- // MCP settings are auto-managed; no save from UI
-
logger.debug('Settings saved successfully');
logger.debug('Mini Model:', localStorage.getItem(MINI_MODEL_STORAGE_KEY));
logger.debug('Nano Model:', localStorage.getItem(NANO_MODEL_STORAGE_KEY));
logger.debug('Provider:', selectedProvider);
-
+
// Set success message and notify parent
saveStatusMessage.textContent = 'Settings saved successfully';
saveStatusMessage.style.backgroundColor = 'var(--color-accent-green-background)';
saveStatusMessage.style.color = 'var(--color-accent-green)';
saveStatusMessage.style.display = 'block';
-
+
onSettingsSaved();
-
+
setTimeout(() => {
dialog.hide();
}, 1500);
});
-
- // Advanced Settings Toggle Logic
- function toggleAdvancedSections(show: boolean): void {
- const display = show ? 'block' : 'none';
- historySection.style.display = display;
- vectorDBSection.style.display = display;
- tracingSection.style.display = display;
- evaluationSection.style.display = display;
-
- // Save state to localStorage
- localStorage.setItem(ADVANCED_SETTINGS_ENABLED_KEY, show.toString());
- }
-
- // Set initial state of advanced sections
- toggleAdvancedSections(advancedToggleCheckbox.checked);
-
- // Add event listener for toggle
- advancedToggleCheckbox.addEventListener('change', () => {
- toggleAdvancedSections(advancedToggleCheckbox.checked);
- });
-
- // Add styles
- const styleElement = document.createElement('style');
- styleElement.textContent = `
- .settings-dialog {
- color: var(--color-text-primary);
- background-color: var(--color-background);
- }
-
- .settings-content {
- padding: 0;
- max-width: 100%;
- }
-
- .settings-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 20px;
- border-bottom: 1px solid var(--color-details-hairline);
- }
-
- .settings-title {
- font-size: 18px;
- font-weight: 500;
- margin: 0;
- color: var(--color-text-primary);
- }
-
- .settings-close-button {
- background: none;
- border: none;
- font-size: 20px;
- cursor: pointer;
- color: var(--color-text-secondary);
- padding: 4px 8px;
- }
-
- .settings-close-button:hover {
- color: var(--color-text-primary);
- }
-
- .provider-selection-section {
- padding: 16px 20px;
- border-bottom: 1px solid var(--color-details-hairline);
- }
-
- .provider-select {
- margin-top: 8px;
- }
-
- .provider-content {
- padding: 16px 20px;
- border-bottom: 1px solid var(--color-details-hairline);
- }
-
- .settings-section {
- margin-bottom: 24px;
- }
-
- .settings-subtitle {
- font-size: 16px;
- font-weight: 500;
- margin: 0 0 12px 0;
- color: var(--color-text-primary);
- }
-
- .settings-label {
- font-size: 14px;
- font-weight: 500;
- margin-bottom: 6px;
- color: var(--color-text-primary);
- }
-
- .settings-hint {
- font-size: 12px;
- color: var(--color-text-secondary);
- margin-bottom: 8px;
- }
-
- .settings-description {
- font-size: 14px;
- color: var(--color-text-secondary);
- margin: 4px 0 12px 0;
- }
-
- .settings-input, .settings-select {
- width: 100%;
- padding: 8px 12px;
- border-radius: 4px;
- border: 1px solid var(--color-details-hairline);
- background-color: var(--color-background-elevation-2);
- color: var(--color-text-primary);
- font-size: 14px;
- box-sizing: border-box;
- height: 32px;
- }
-
- .settings-input:focus, .settings-select:focus {
- outline: none;
- border-color: var(--color-primary);
- box-shadow: 0 0 0 1px var(--color-primary-opacity-30);
- }
-
- .settings-status {
- padding: 8px 12px;
- border-radius: 4px;
- font-size: 13px;
- margin: 8px 0;
- }
-
- .fetch-button-container {
- display: flex;
- align-items: center;
- gap: 8px;
- margin: 12px 0;
- }
-
- .custom-models-section {
- margin-top: 16px;
- }
-
- .custom-models-list {
- margin-bottom: 16px;
- }
-
- .custom-model-row {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- padding: 6px 0;
- border-bottom: 1px solid var(--color-details-hairline);
- }
-
- .custom-model-name {
- flex: 1;
- font-size: 14px;
- }
-
- .new-model-row {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- }
-
- .custom-model-input {
- flex: 1;
- margin-bottom: 0;
- }
-
- /* Button spacing and layout */
- .button-group {
- display: flex;
- gap: 8px;
- align-items: center;
- }
-
- .test-status {
- font-size: 12px;
- margin-left: 4px;
- }
-
- .model-test-status {
- margin-top: 4px;
- }
-
- .model-selection-container {
- margin-bottom: 20px;
- }
-
- /* Model selector component styles (shared with chat view) */
- .model-selection-container ai-model-selector { display: block; width: 100%; }
- .model-selector.searchable { position: relative; }
- .model-select-trigger {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 6px 10px;
- border: 1px solid var(--color-details-hairline);
- border-radius: 6px;
- background: var(--color-background-elevation-1);
- cursor: pointer;
- width: 100%;
- font-size: 12px;
- color: var(--color-text-primary);
- transition: all 0.2s ease;
- box-sizing: border-box;
- }
- .model-select-trigger:hover:not(:disabled) { background: var(--color-background-elevation-2); border-color: #00a4fe; }
- .model-select-trigger:disabled { opacity: 0.6; cursor: not-allowed; background-color: var(--color-background-elevation-0); }
- .selected-model { flex: 1; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
- .dropdown-arrow { margin-left: 8px; font-size: 10px; color: var(--color-text-secondary); transition: transform 0.2s ease; }
- .model-dropdown { position: absolute; left: 0; right: 0; background: var(--color-background-elevation-1); border: 1px solid var(--color-details-hairline); border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000; max-height: 300px; overflow: hidden; }
- .model-dropdown.below { top: 100%; margin-top: 2px; }
- .model-dropdown.above { bottom: 100%; margin-bottom: 2px; }
- .model-search { width: 100%; padding: 8px 12px; border: none; border-bottom: 1px solid var(--color-details-hairline); outline: none; background: var(--color-background-elevation-1); color: var(--color-text-primary); font-size: 12px; box-sizing: border-box; }
- .model-search::placeholder { color: var(--color-text-secondary); }
- .model-options { max-height: 240px; overflow-y: auto; }
- .model-option { padding: 8px 12px; cursor: pointer; border-bottom: 1px solid var(--color-details-hairline); font-size: 12px; color: var(--color-text-primary); transition: background-color 0.2s ease; }
- .model-option:last-child { border-bottom: none; }
- .model-option:hover, .model-option.highlighted { background: #def1fb; }
- .model-option.selected { background: #00a4fe; color: white; }
- .model-option.no-results { color: var(--color-text-secondary); cursor: default; font-style: italic; }
- .model-option.no-results:hover { background: transparent; }
-
- .mini-model-description, .nano-model-description {
- font-size: 12px;
- font-style: italic;
- }
-
- .history-section {
- margin-top: 16px;
- padding: 16px 20px;
- border-bottom: 1px solid var(--color-details-hairline);
- }
-
- .disclaimer-section {
- background-color: var(--color-background-elevation-1);
- border-radius: 8px;
- padding: 16px 20px;
- margin: 16px 20px;
- border: 1px solid var(--color-details-hairline);
- }
-
- .disclaimer-warning {
- color: var(--color-accent-orange);
- margin-bottom: 8px;
- }
-
- .disclaimer-note {
- margin-bottom: 8px;
- }
-
- .disclaimer-footer {
- font-size: 12px;
- color: var(--color-text-secondary);
- margin-top: 8px;
- }
-
- .settings-footer {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- gap: 12px;
- padding: 16px 20px;
- border-top: 1px solid var(--color-details-hairline);
- }
-
- .save-status {
- margin: 0;
- font-size: 13px;
- padding: 6px 10px;
- }
-
- .settings-button {
- padding: 8px 16px;
- border-radius: 4px;
- font-size: 14px;
- cursor: pointer;
- transition: all 0.2s;
- font-family: inherit;
- background-color: var(--color-background-elevation-1);
- border: 1px solid var(--color-details-hairline);
- color: var(--color-text-primary);
- }
-
- .settings-button:hover {
- background-color: var(--color-background-elevation-2);
- }
-
- .settings-button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- /* Add button styling */
- .add-button {
- min-width: 60px;
- border-radius: 4px;
- font-size: 12px;
- }
-
- /* Icon button styling */
- .icon-button {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- border-radius: 50%;
- border: none;
- background: transparent;
- cursor: pointer;
- padding: 0;
- color: var(--color-text-secondary);
- transition: all 0.15s;
- }
-
- .icon-button:hover {
- background-color: var(--color-background-elevation-2);
- }
-
- /* Specific icon button hover states */
- .remove-button:hover {
- color: var(--color-accent-red);
- }
-
- .test-button:hover {
- color: var(--color-accent-green);
- }
-
- .trash-icon, .check-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- /* Add specific button styling */
- .add-button {
- background-color: var(--color-background-elevation-1);
- }
-
- .add-button:hover {
- background-color: var(--color-background-elevation-2);
- }
-
- /* Cancel button */
- .cancel-button {
- background-color: var(--color-background-elevation-1);
- border: 1px solid var(--color-details-hairline);
- color: var(--color-text-primary);
- }
-
- .cancel-button:hover {
- background-color: var(--color-background-elevation-2);
- }
-
- /* Save button */
- .save-button {
- background-color: var(--color-primary);
- border: 1px solid var(--color-primary);
- color: white;
- }
-
- .save-button:hover {
- background-color: var(--color-primary-variant);
- }
-
- .clear-button {
- margin-top: 8px;
- }
-
- /* Vector DB section styles */
- .vector-db-section {
- margin-top: 16px;
- padding: 16px 20px;
- border-bottom: 1px solid var(--color-details-hairline);
- }
-
- /* Tracing section styles */
- .tracing-section {
- margin-top: 16px;
- padding: 16px 20px;
- border-bottom: 1px solid var(--color-details-hairline);
- }
-
- .settings-section-title {
- font-size: 16px;
- font-weight: 500;
- color: var(--color-text-primary);
- margin: 0 0 16px 0;
- }
-
- .tracing-enabled-container {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- }
-
- .tracing-checkbox {
- margin: 0;
- }
-
- .tracing-label {
- font-weight: 500;
- color: var(--color-text-primary);
- cursor: pointer;
- }
-
- .tracing-config-container {
- margin-top: 16px;
- padding-left: 24px;
- border-left: 2px solid var(--color-details-hairline);
- }
- /* Apply tracing config visual style to Evaluation section */
- .evaluation-enabled-container {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- }
- .evaluation-checkbox { margin: 0; }
- .evaluation-label {
- font-weight: 500;
- color: var(--color-text-primary);
- cursor: pointer;
- }
- .evaluation-config-container {
- margin-top: 16px;
- padding-left: 24px;
- border-left: 2px solid var(--color-details-hairline);
- }
-
- /* Apply tracing config visual style to MCP section */
- .mcp-enabled-container {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- }
- .mcp-checkbox { margin: 0; }
- .mcp-label {
- font-weight: 500;
- color: var(--color-text-primary);
- cursor: pointer;
- }
- .mcp-config-container {
- margin-top: 16px;
- padding-left: 24px;
- border-left: 2px solid var(--color-details-hairline);
- }
+ // Apply styles
+ applySettingsStyles(dialog.contentElement);
- /* Advanced Settings Toggle styles */
- .advanced-settings-toggle-section {
- padding: 16px 20px;
- border-bottom: 1px solid var(--color-details-hairline);
- background-color: var(--color-background-highlight);
- }
-
- .advanced-settings-toggle-container {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- }
-
- .advanced-settings-checkbox {
- margin: 0;
- transform: scale(1.1);
- }
-
- .advanced-settings-label {
- font-weight: 500;
- color: var(--color-text-primary);
- cursor: pointer;
- font-size: 14px;
- }
-
- /* Advanced Settings Toggle styles */
- .advanced-settings-toggle-section {
- padding: 16px 20px;
- border-bottom: 1px solid var(--color-details-hairline);
- background-color: var(--color-background-highlight);
- }
-
- .advanced-settings-toggle-container {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- }
-
- .advanced-settings-checkbox {
- margin: 0;
- transform: scale(1.1);
- }
-
- .advanced-settings-label {
- font-weight: 500;
- color: var(--color-text-primary);
- cursor: pointer;
- font-size: 14px;
- }
-
- /* Evaluation section styles */
- .evaluation-section {
- margin-top: 16px;
- padding: 16px 20px;
- border-bottom: 1px solid var(--color-details-hairline);
- }
-
- .evaluation-buttons-container {
- display: flex;
- gap: 8px;
- margin-top: 16px;
- }
-
- .connect-button {
- background-color: var(--color-accent-blue-background);
- color: var(--color-accent-blue);
- border: 1px solid var(--color-accent-blue);
- }
-
- .connect-button:hover {
- background-color: var(--color-accent-blue);
- color: var(--color-background);
- }
-
- .connect-button:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
+ // Show the dialog
+ dialog.show();
- .mcp-section {
- margin-top: 16px;
- padding: 16px 20px;
- border-bottom: 1px solid var(--color-details-hairline);
- }
+ // Cleanup when dialog is hidden
+ dialog.contentElement.addEventListener('DOMNodeRemovedFromDocument', () => {
+ // Cleanup all advanced features that have cleanup methods
+ advancedFeatures.forEach(feature => {
+ if (feature.cleanup) {
+ feature.cleanup();
+ }
+ });
- @keyframes spin {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
- }
- `;
- dialog.contentElement.appendChild(styleElement);
-
- dialog.show();
-
- return Promise.resolve();
+ // Cleanup all provider settings that have cleanup methods
+ providerSettings.forEach(provider => {
+ if (provider.cleanup) {
+ provider.cleanup();
+ }
+ });
+ });
}
/**
- * Static method to update OpenRouter models programmatically (called after OAuth success)
+ * Static method to update OpenRouter models cache
+ * Called from AIChatPanel when OAuth credentials are available
*/
- static updateOpenRouterModels(openrouterModels: any[]): void {
- try {
- logger.debug('Updating OpenRouter models programmatically...', openrouterModels.length);
-
- // Convert OpenRouter models to ModelOption format
- const modelOptions: ModelOption[] = openrouterModels.map(model => ({
- value: model.id,
- label: model.name || model.id,
- type: 'openrouter' as const
- }));
-
- // Store models in localStorage cache for the model management system to pick up
- localStorage.setItem('openrouter_models_cache', JSON.stringify(modelOptions));
- localStorage.setItem('openrouter_models_cache_timestamp', Date.now().toString());
-
- // Dispatch event to notify model management system to refresh
- window.dispatchEvent(new CustomEvent('openrouter-models-updated', {
- detail: { models: modelOptions, source: 'oauth' }
- }));
-
- logger.debug('Successfully cached OpenRouter models and dispatched update event');
-
- } catch (error) {
- logger.error('Failed to update OpenRouter models programmatically:', error);
- }
- }
-}
+ static updateOpenRouterModels(openrouterModels: Array<{id: string, name?: string}>): void {
+ // Convert OpenRouter models to ModelOption format
+ const modelOptions: ModelOption[] = openrouterModels.map(model => ({
+ value: model.id,
+ label: model.name || model.id,
+ type: 'openrouter' as const
+ }));
-// Helper function to create a model selector (uses shared component)
-function createModelSelector(
- container: HTMLElement,
- labelText: string,
- description: string,
- selectorType: string, // semantic identifier
- modelOptions: ModelOption[],
- selectedModel: string,
- defaultOptionText: string,
- onFocus?: () => void // Optional callback for when the selector is opened/focused
-): HTMLElement {
- const modelContainer = document.createElement('div');
- modelContainer.className = 'model-selection-container';
- container.appendChild(modelContainer);
-
- const modelLabel = document.createElement('div');
- modelLabel.className = 'settings-label';
- modelLabel.textContent = labelText;
- modelContainer.appendChild(modelLabel);
-
- const modelDescription = document.createElement('div');
- modelDescription.className = 'settings-hint';
- modelDescription.textContent = description;
- modelContainer.appendChild(modelDescription);
-
- const selectorEl = document.createElement('ai-model-selector') as any;
- selectorEl.dataset.modelType = selectorType;
- selectorEl.options = [{ value: '', label: defaultOptionText }, ...modelOptions];
- selectorEl.selected = selectedModel || '';
- selectorEl.forceSearchable = true; // Ensure consistent UI in Settings
-
- // Expose a `.value` API similar to native