diff --git a/frontend-modern/src/components/Settings/AISettings.tsx b/frontend-modern/src/components/Settings/AISettings.tsx index 18130cc..a2c000d 100644 --- a/frontend-modern/src/components/Settings/AISettings.tsx +++ b/frontend-modern/src/components/Settings/AISettings.tsx @@ -72,6 +72,13 @@ export const AISettings: Component = () => { // Auto-fix acknowledgement state (not persisted - must acknowledge each session) const [autoFixAcknowledged, setAutoFixAcknowledged] = createSignal(false); + // First-time setup modal state + const [showSetupModal, setShowSetupModal] = createSignal(false); + const [setupProvider, setSetupProvider] = createSignal<'anthropic' | 'openai' | 'deepseek' | 'ollama'>('anthropic'); + const [setupApiKey, setSetupApiKey] = createSignal(''); + const [setupOllamaUrl, setSetupOllamaUrl] = createSignal('http://localhost:11434'); + const [setupSaving, setSetupSaving] = createSignal(false); + const [form, setForm] = createStore({ enabled: false, provider: 'anthropic' as AIProvider, // Legacy - kept for compatibility @@ -413,871 +420,1072 @@ export const AISettings: Component = () => { // Legacy helper functions removed - multi-provider accordions handle all provider-specific UI return ( - -
-
-
- - - -
- - { - const newValue = event.currentTarget.checked; - setForm('enabled', newValue); - // Auto-save the enabled toggle immediately - try { - const updated = await AIAPI.updateSettings({ enabled: newValue }); - setSettings(updated); - notificationStore.success(newValue ? 'AI Assistant enabled' : 'AI Assistant disabled'); - } catch (error) { - // Revert on failure - setForm('enabled', !newValue); - logger.error('[AISettings] Failed to toggle AI:', error); - const message = error instanceof Error ? error.message : 'Failed to update AI setting'; - notificationStore.error(message); - } - }} - disabled={loading() || saving()} - containerClass="items-center gap-2" - label={ - - {form.enabled ? 'Enabled' : 'Disabled'} - - } - /> -
-
- -
-
-

AI Assistant helps you:

-
    -
  • Diagnose infrastructure issues with context-aware analysis
  • -
  • Get remediation suggestions based on your specific environment
  • -
  • Understand alerts and metrics with plain-language explanations
  • -
-
- - -
- - Loading AI settings... -
-
- - -
- {/* Model Selection */} -
-
- - -
- 0} fallback={ - setForm('model', e.currentTarget.value)} - placeholder="Configure a provider below to see available models" - class={controlClass()} - disabled={saving()} + <> + +
+
+
+ + - }> - - -

- Main model used when no specific override is set. {availableModels().length === 0 && 'Save API key and refresh to see available models.'} -

+
+ + {/* Toggle with first-time setup flow */} + {(() => { + const s = settings(); + const hasConfiguredProvider = s && (s.anthropic_configured || s.openai_configured || s.deepseek_configured || s.ollama_configured); - {/* Chat Model Override */} -
- - 0} fallback={ - setForm('chatModel', e.currentTarget.value)} - placeholder="Leave empty to use default model" - class={controlClass()} - disabled={saving()} - /> - }> - - -

- Model for interactive AI chat. Use a more capable model for complex reasoning. -

-
- - {/* Patrol Model Override */} -
- - 0} fallback={ - setForm('patrolModel', e.currentTarget.value)} - placeholder="Leave empty to use default model" - class={controlClass()} - disabled={saving()} - /> - }> - - -

- Model for background patrol analysis. Use a cheaper/faster model to save tokens. -

-
- - {/* AI Provider Configuration - Configure API keys for all providers */} -
-
-

- - - - AI Provider Configuration -

-

- Configure API keys for each AI provider you want to use. Models from all configured providers will appear in the model selectors. -

-
- - {/* Provider Accordions */} -
- {/* Anthropic */} -
- - -
- setForm('anthropicApiKey', e.currentTarget.value)} - placeholder={settings()?.anthropic_configured ? '••••••••••• (configured)' : 'sk-ant-...'} - class={controlClass()} - disabled={saving()} - /> -
-

- Get API key → -

- -
- - -
-
-
- -

- {providerTestResult()?.message} -

-
-
-
-
- - {/* OpenAI */} -
- - -
- setForm('openaiApiKey', e.currentTarget.value)} - placeholder={settings()?.openai_configured ? '••••••••••• (configured)' : 'sk-...'} - class={controlClass()} - disabled={saving()} - /> - setForm('openaiBaseUrl', e.currentTarget.value)} - placeholder="Custom base URL (optional, for Azure OpenAI)" - class={controlClass()} - disabled={saving()} - /> -
-

- Get API key → -

- -
- - -
-
-
- -

- {providerTestResult()?.message} -

-
-
-
-
- - {/* DeepSeek */} -
- - -
- setForm('deepseekApiKey', e.currentTarget.value)} - placeholder={settings()?.deepseek_configured ? '••••••••••• (configured)' : 'sk-...'} - class={controlClass()} - disabled={saving()} - /> -
-

- Get API key → -

- -
- - -
-
-
- -

- {providerTestResult()?.message} -

-
-
-
-
- - {/* Ollama */} -
- - -
- setForm('ollamaBaseUrl', e.currentTarget.value)} - placeholder="http://localhost:11434" - class={controlClass()} - disabled={saving()} - /> -
-

- Learn about Ollama → - · Free & local -

- -
- - -
-
-
- -

- {providerTestResult()?.message} -

-
-
-
-
-
-
- - {/* Autonomous Mode */} -
-
-
- -

- {form.autonomousMode - ? 'AI will execute all commands without asking for approval. Only enable if you trust your configured model.' - : 'AI will ask for approval before running commands that modify your system. Read-only commands (like df, ps, docker stats) run automatically.'} -

-
+ return ( setForm('autonomousMode', event.currentTarget.checked)} - disabled={saving()} - /> -
-
- - {/* AI Patrol & Efficiency Settings */} -
-
- -

- Configure how AI monitors your infrastructure. Balance between coverage and token usage. -

-
- - {/* Patrol Interval */} -
-
- -
- { - const value = parseInt(e.currentTarget.value, 10); - if (!isNaN(value)) { - setForm('patrolIntervalMinutes', Math.max(0, value)); - } - }} - min={0} - max={10080} - step={15} - disabled={saving()} - style={{ width: '120px' }} - /> - - {form.patrolIntervalMinutes === 0 - ? 'Disabled' - : form.patrolIntervalMinutes >= 60 - ? `${Math.floor(form.patrolIntervalMinutes / 60)}h ${form.patrolIntervalMinutes % 60 > 0 ? `${form.patrolIntervalMinutes % 60}m` : ''}` - : `${form.patrolIntervalMinutes}m`} + checked={form.enabled} + onChange={async (event) => { + const newValue = event.currentTarget.checked; + // Show setup modal if trying to enable without a configured provider + if (newValue && !hasConfiguredProvider) { + event.currentTarget.checked = false; + setShowSetupModal(true); + return; + } + setForm('enabled', newValue); + // Auto-save the enabled toggle immediately + try { + const updated = await AIAPI.updateSettings({ enabled: newValue }); + setSettings(updated); + notificationStore.success(newValue ? 'AI Assistant enabled' : 'AI Assistant disabled'); + } catch (error) { + // Revert on failure + setForm('enabled', !newValue); + logger.error('[AISettings] Failed to toggle AI:', error); + const message = error instanceof Error ? error.message : 'Failed to update AI setting'; + notificationStore.error(message); + } + }} + disabled={loading() || saving()} + containerClass="items-center gap-2" + label={ + + {form.enabled ? 'Enabled' : 'Disabled'} -
-

- Set to 0 to disable scheduled patrol. Minimum 10 minutes when enabled. -

-
- - {/* Alert-Triggered Analysis Toggle */} -
-
- -

- When enabled, AI automatically analyzes specific resources when alerts fire. - Uses minimal tokens since it only analyzes affected resources. -

-
- setForm('alertTriggeredAnalysis', event.currentTarget.checked)} - disabled={saving()} - /> -
- - {/* Auto-Fix Mode Toggle */} -
-
- -

- When enabled, patrol can attempt automatic remediation of issues. - When disabled (default), patrol only observes and reports - it won't make changes. -

-
- { - // Can only enable if acknowledged - if (event.currentTarget.checked && !autoFixAcknowledged()) { - return; // Prevent enabling without acknowledgement - } - setForm('patrolAutoFix', event.currentTarget.checked); - }} - disabled={saving() || (!form.patrolAutoFix && !autoFixAcknowledged())} - /> -
- - {/* Auto-Fix Warning & Acknowledgement - Simplified inline flow */} - -
-
-
- - - -
-
-

Enable Auto-Fix Mode?

-
-
- - AI executes remediation commands without approval -
-
- - Actions may be irreversible (restarts, cache clears, etc.) -
-
- - Test in staging/dev environments first -
-
- -
-
-
-
- - {/* Warning when enabled */} - -
-
- - - -
-

⚠️ Auto-Fix is ENABLED

-

AI patrol will automatically attempt to fix issues without asking for approval. Review findings regularly.

-
-
-
- - {/* Auto-Fix Model Selector - shown when auto-fix is enabled */} -
- - 0} fallback={ - setForm('autoFixModel', e.currentTarget.value)} - placeholder="Leave empty to use patrol model" - class={controlClass()} - disabled={saving()} - /> - }> - - -

- Model for automatic remediation. Use a more capable model for better fix accuracy. -

-
-
-
-
- - {/* AI Cost Controls - Prominent positioning */} -
-
-
- - - -
-
- -

- Set a budget alert for cross-provider cost tracking -

-
-
- -
-
- -
- $ - setForm('costBudgetUSD30d', e.currentTarget.value)} - min={0} - step={1} - placeholder="0" - disabled={saving()} - /> -
-
-
- 0}> -
-
- Daily equivalent - ${(parseFloat(form.costBudgetUSD30d) / 30).toFixed(2)}/day -
-
- Weekly equivalent - ${(parseFloat(form.costBudgetUSD30d) / 4.3).toFixed(2)}/week -
-
-
- -
- 💡 Set a budget to get proactive alerts before overspending -
-
-
-
-

- This is a cross-provider estimate. Provider dashboards are the source of truth for billing. -

-
- + } + /> + ); + })()} +
+
+ +
+

AI Assistant helps you:

+
    +
  • Diagnose infrastructure issues with context-aware analysis
  • +
  • Get remediation suggestions based on your specific environment
  • +
  • Understand alerts and metrics with plain-language explanations
  • +
- {/* Status indicator */} - -
-
-
- - {settings()?.configured - ? `Ready • ${settings()?.configured_providers?.length || 0} provider${(settings()?.configured_providers?.length || 0) !== 1 ? 's' : ''} • ${availableModels().length} models` - : 'Configure at least one AI provider above to enable AI features'} - - - - • Default: {settings()?.model?.split(':').pop() || settings()?.model} - - -
+ +
+ + Loading AI settings...
- {/* Actions */} -
- - +
+ 0} fallback={ + setForm('model', e.currentTarget.value)} + placeholder="Configure a provider below to see available models" + class={controlClass()} + disabled={saving()} + /> + }> + + +

+ Main model used when no specific override is set. {availableModels().length === 0 && 'Save API key and refresh to see available models.'} +

+
+ + {/* Chat Model Override */} +
+ + 0} fallback={ + setForm('chatModel', e.currentTarget.value)} + placeholder="Leave empty to use default model" + class={controlClass()} + disabled={saving()} + /> + }> + + +

+ Model for interactive AI chat. Use a more capable model for complex reasoning. +

+
+ + {/* Patrol Model Override */} +
+ + 0} fallback={ + setForm('patrolModel', e.currentTarget.value)} + placeholder="Leave empty to use default model" + class={controlClass()} + disabled={saving()} + /> + }> + + +

+ Model for background patrol analysis. Use a cheaper/faster model to save tokens. +

+
+ + {/* AI Provider Configuration - Configure API keys for all providers */} +
+
+

+ + + + AI Provider Configuration +

+

+ Configure API keys for each AI provider you want to use. Models from all configured providers will appear in the model selectors. +

+
+ + {/* Provider Accordions */} +
+ {/* Anthropic */} +
+ + +
+ setForm('anthropicApiKey', e.currentTarget.value)} + placeholder={settings()?.anthropic_configured ? '••••••••••• (configured)' : 'sk-ant-...'} + class={controlClass()} + disabled={saving()} + /> +
+

+ Get API key → +

+ +
+ + +
+
+
+ +

+ {providerTestResult()?.message} +

+
+
+
+
+ + {/* OpenAI */} +
+ + +
+ setForm('openaiApiKey', e.currentTarget.value)} + placeholder={settings()?.openai_configured ? '••••••••••• (configured)' : 'sk-...'} + class={controlClass()} + disabled={saving()} + /> + setForm('openaiBaseUrl', e.currentTarget.value)} + placeholder="Custom base URL (optional, for Azure OpenAI)" + class={controlClass()} + disabled={saving()} + /> +
+

+ Get API key → +

+ +
+ + +
+
+
+ +

+ {providerTestResult()?.message} +

+
+
+
+
+ + {/* DeepSeek */} +
+ + +
+ setForm('deepseekApiKey', e.currentTarget.value)} + placeholder={settings()?.deepseek_configured ? '••••••••••• (configured)' : 'sk-...'} + class={controlClass()} + disabled={saving()} + /> +
+

+ Get API key → +

+ +
+ + +
+
+
+ +

+ {providerTestResult()?.message} +

+
+
+
+
+ + {/* Ollama */} +
+ + +
+ setForm('ollamaBaseUrl', e.currentTarget.value)} + placeholder="http://localhost:11434" + class={controlClass()} + disabled={saving()} + /> +
+

+ Learn about Ollama → + · Free & local +

+ +
+ + +
+
+
+ +

+ {providerTestResult()?.message} +

+
+
+
+
+
+
+ + {/* Autonomous Mode */} +
+
+
+ +

+ {form.autonomousMode + ? 'AI will execute all commands without asking for approval. Only enable if you trust your configured model.' + : 'AI will ask for approval before running commands that modify your system. Read-only commands (like df, ps, docker stats) run automatically.'} +

+
+ setForm('autonomousMode', event.currentTarget.checked)} + disabled={saving()} + /> +
+
+ + {/* AI Patrol & Efficiency Settings */} +
+
+ +

+ Configure how AI monitors your infrastructure. Balance between coverage and token usage. +

+
+ + {/* Patrol Interval */} +
+
+ +
+ { + const value = parseInt(e.currentTarget.value, 10); + if (!isNaN(value)) { + setForm('patrolIntervalMinutes', Math.max(0, value)); + } + }} + min={0} + max={10080} + step={15} + disabled={saving()} + style={{ width: '120px' }} + /> + + {form.patrolIntervalMinutes === 0 + ? 'Disabled' + : form.patrolIntervalMinutes >= 60 + ? `${Math.floor(form.patrolIntervalMinutes / 60)}h ${form.patrolIntervalMinutes % 60 > 0 ? `${form.patrolIntervalMinutes % 60}m` : ''}` + : `${form.patrolIntervalMinutes}m`} + +
+

+ Set to 0 to disable scheduled patrol. Minimum 10 minutes when enabled. +

+
+ + {/* Alert-Triggered Analysis Toggle */} +
+
+ +

+ When enabled, AI automatically analyzes specific resources when alerts fire. + Uses minimal tokens since it only analyzes affected resources. +

+
+ setForm('alertTriggeredAnalysis', event.currentTarget.checked)} + disabled={saving()} + /> +
+ + {/* Auto-Fix Mode Toggle */} +
+
+ +

+ When enabled, patrol can attempt automatic remediation of issues. + When disabled (default), patrol only observes and reports - it won't make changes. +

+
+ { + // Can only enable if acknowledged + if (event.currentTarget.checked && !autoFixAcknowledged()) { + return; // Prevent enabling without acknowledgement + } + setForm('patrolAutoFix', event.currentTarget.checked); + }} + disabled={saving() || (!form.patrolAutoFix && !autoFixAcknowledged())} + /> +
+ + {/* Auto-Fix Warning & Acknowledgement - Simplified inline flow */} + +
+
+
+ + + +
+
+

Enable Auto-Fix Mode?

+
+
+ + AI executes remediation commands without approval +
+
+ + Actions may be irreversible (restarts, cache clears, etc.) +
+
+ + Test in staging/dev environments first +
+
+ +
+
+
+
+ + {/* Warning when enabled */} + +
+
+ + + +
+

⚠️ Auto-Fix is ENABLED

+

AI patrol will automatically attempt to fix issues without asking for approval. Review findings regularly.

+
+
+
+ + {/* Auto-Fix Model Selector - shown when auto-fix is enabled */} +
+ + 0} fallback={ + setForm('autoFixModel', e.currentTarget.value)} + placeholder="Leave empty to use patrol model" + class={controlClass()} + disabled={saving()} + /> + }> + + +

+ Model for automatic remediation. Use a more capable model for better fix accuracy. +

+
+
+
+
+ + {/* AI Cost Controls - Prominent positioning */} +
+
+
+ + + +
+
+ +

+ Set a budget alert for cross-provider cost tracking +

+
+
+ +
+
+ +
+ $ + setForm('costBudgetUSD30d', e.currentTarget.value)} + min={0} + step={1} + placeholder="0" + disabled={saving()} + /> +
+
+
+ 0}> +
+
+ Daily equivalent + ${(parseFloat(form.costBudgetUSD30d) / 30).toFixed(2)}/day +
+
+ Weekly equivalent + ${(parseFloat(form.costBudgetUSD30d) / 4.3).toFixed(2)}/week +
+
+
+ +
+ 💡 Set a budget to get proactive alerts before overspending +
+
+
+
+

+ This is a cross-provider estimate. Provider dashboards are the source of truth for billing. +

+
+ + +
+ + {/* Status indicator */} + +
- {testing() ? 'Testing...' : 'Test Connection'} - +
+
+ + {settings()?.configured + ? `Ready • ${settings()?.configured_providers?.length || 0} provider${(settings()?.configured_providers?.length || 0) !== 1 ? 's' : ''} • ${availableModels().length} models` + : 'Configure at least one AI provider above to enable AI features'} + + + + • Default: {settings()?.model?.split(':').pop() || settings()?.model} + + +
+
-
+ + {/* Actions */} +
+ + + +
+ + +
+
+ + + + + {/* First-time Setup Modal */} + +
+
+ {/* Header */} +
+

Set Up AI Assistant

+

Choose a provider to get started

+
+ + {/* Provider Selection */} +
+
+ + + + +
+ + {/* API Key / URL Input */} + + + setSetupApiKey(e.currentTarget.value)} + placeholder={setupProvider() === 'anthropic' ? 'sk-ant-...' : 'sk-...'} + class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +

+ + Get your API key → + +

+
+ }> +
+ + setSetupOllamaUrl(e.currentTarget.value)} + placeholder="http://localhost:11434" + class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +

+ Ollama runs locally - no API key needed +

+
+ +
+ + {/* Footer */} +
-
- - +
+ + ); }; diff --git a/internal/ai/providers/ollama.go b/internal/ai/providers/ollama.go index 34eead3..cec8422 100644 --- a/internal/ai/providers/ollama.go +++ b/internal/ai/providers/ollama.go @@ -95,6 +95,10 @@ func (c *OllamaClient) Chat(ctx context.Context, req ChatRequest) (*ChatResponse // Use provided model or fall back to client default model := req.Model + // Strip "ollama:" prefix if present - callers may pass the full "provider:model" string + if strings.HasPrefix(model, "ollama:") { + model = strings.TrimPrefix(model, "ollama:") + } if model == "" { model = c.model } diff --git a/internal/api/ai_handlers.go b/internal/api/ai_handlers.go index f9c3663..19e7547 100644 --- a/internal/api/ai_handlers.go +++ b/internal/api/ai_handlers.go @@ -359,27 +359,41 @@ func (h *AISettingsHandler) HandleUpdateAISettings(w http.ResponseWriter, r *htt settings.CustomContext = strings.TrimSpace(*req.CustomContext) } + // Handle multi-provider credentials FIRST - before enabled check + // This allows the setup flow to send API key + enabled:true together + // Clear flags take priority over setting new values + if req.ClearAnthropicKey != nil && *req.ClearAnthropicKey { + settings.AnthropicAPIKey = "" + } else if req.AnthropicAPIKey != nil { + settings.AnthropicAPIKey = strings.TrimSpace(*req.AnthropicAPIKey) + } + if req.ClearOpenAIKey != nil && *req.ClearOpenAIKey { + settings.OpenAIAPIKey = "" + } else if req.OpenAIAPIKey != nil { + settings.OpenAIAPIKey = strings.TrimSpace(*req.OpenAIAPIKey) + } + if req.ClearDeepSeekKey != nil && *req.ClearDeepSeekKey { + settings.DeepSeekAPIKey = "" + } else if req.DeepSeekAPIKey != nil { + settings.DeepSeekAPIKey = strings.TrimSpace(*req.DeepSeekAPIKey) + } + if req.ClearOllamaURL != nil && *req.ClearOllamaURL { + settings.OllamaBaseURL = "" + } else if req.OllamaBaseURL != nil { + settings.OllamaBaseURL = strings.TrimSpace(*req.OllamaBaseURL) + } + if req.OpenAIBaseURL != nil { + settings.OpenAIBaseURL = strings.TrimSpace(*req.OpenAIBaseURL) + } + if req.Enabled != nil { // Only allow enabling if at least one provider is configured if *req.Enabled { configuredProviders := settings.GetConfiguredProviders() if len(configuredProviders) == 0 { - // Fall back to legacy validation for backwards compatibility - switch settings.Provider { - case config.AIProviderAnthropic, config.AIProviderOpenAI, config.AIProviderDeepSeek: - if settings.APIKey == "" { - http.Error(w, "Cannot enable AI: configure at least one AI provider first", http.StatusBadRequest) - return - } - case config.AIProviderOllama: - // Ollama doesn't need API key, but needs base URL (or will use default) - if settings.BaseURL == "" { - settings.BaseURL = config.DefaultOllamaBaseURL - } - default: - http.Error(w, "Cannot enable AI: configure at least one AI provider first", http.StatusBadRequest) - return - } + // No providers configured - give a helpful error + http.Error(w, "Please configure an AI provider (API key or Ollama URL) before enabling AI", http.StatusBadRequest) + return } // If we have configured providers, we're good to enable } @@ -429,32 +443,6 @@ func (h *AISettingsHandler) HandleUpdateAISettings(w http.ResponseWriter, r *htt settings.AlertTriggeredAnalysis = *req.AlertTriggeredAnalysis } - // Handle multi-provider credentials - // Clear flags take priority over setting new values - if req.ClearAnthropicKey != nil && *req.ClearAnthropicKey { - settings.AnthropicAPIKey = "" - } else if req.AnthropicAPIKey != nil { - settings.AnthropicAPIKey = strings.TrimSpace(*req.AnthropicAPIKey) - } - if req.ClearOpenAIKey != nil && *req.ClearOpenAIKey { - settings.OpenAIAPIKey = "" - } else if req.OpenAIAPIKey != nil { - settings.OpenAIAPIKey = strings.TrimSpace(*req.OpenAIAPIKey) - } - if req.ClearDeepSeekKey != nil && *req.ClearDeepSeekKey { - settings.DeepSeekAPIKey = "" - } else if req.DeepSeekAPIKey != nil { - settings.DeepSeekAPIKey = strings.TrimSpace(*req.DeepSeekAPIKey) - } - if req.ClearOllamaURL != nil && *req.ClearOllamaURL { - settings.OllamaBaseURL = "" - } else if req.OllamaBaseURL != nil { - settings.OllamaBaseURL = strings.TrimSpace(*req.OllamaBaseURL) - } - if req.OpenAIBaseURL != nil { - settings.OpenAIBaseURL = strings.TrimSpace(*req.OpenAIBaseURL) - } - // Save settings if err := h.persistence.SaveAIConfig(*settings); err != nil { log.Error().Err(err).Msg("Failed to save AI settings")