diff --git a/src/App.tsx b/src/App.tsx index b896d782..d33aedb8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -162,6 +162,10 @@ const hardcodedConfig: AppConfig = { soundEnabled: false, devMode: false, }, + aiBaseURL: 'https://ai.shakespeare.diy/v1', + aiApiKey: '', + aiModel: 'grok-4.1-fast', + aiSystemPrompt: '', }; /** diff --git a/src/components/AdvancedSettings.tsx b/src/components/AdvancedSettings.tsx index 157c13df..04bcd3ec 100644 --- a/src/components/AdvancedSettings.tsx +++ b/src/components/AdvancedSettings.tsx @@ -1,10 +1,9 @@ import { useState, useEffect } from 'react'; -import { ChevronDown, ChevronUp, Bot, Bug, RotateCcw, AlertTriangle } from 'lucide-react'; +import { ChevronDown, ChevronUp, RotateCcw, AlertTriangle, Eye, EyeOff } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; import { RequestToVanishDialog } from '@/components/RequestToVanishDialog'; @@ -12,10 +11,11 @@ import { useAppContext } from '@/hooks/useAppContext'; import { useToast } from '@/hooks/useToast'; import { useEncryptedSettings } from '@/hooks/useEncryptedSettings'; import { useCurrentUser } from '@/hooks/useCurrentUser'; -import { useShakespeare, sortModelsByCost } from '@/hooks/useShakespeare'; import { DEFAULT_SYSTEM_PROMPT_TEMPLATE } from '@/lib/aiChatSystemPrompt'; -import type { Model } from '@/hooks/useShakespeare'; +/** Hardcoded default values for Agent provider fields. Used for reset buttons. */ +const DEFAULT_AI_BASE_URL = 'https://ai.shakespeare.diy/v1'; +const DEFAULT_AI_MODEL = 'grok-4.1-fast'; /** The build-time default DSN from the environment variable. */ const DEFAULT_SENTRY_DSN = import.meta.env.VITE_SENTRY_DSN || ''; @@ -35,25 +35,73 @@ export function AdvancedSettings() { const [linkPreviewUrl, setLinkPreviewUrl] = useState(config.linkPreviewUrl); const [corsProxy, setCorsProxy] = useState(config.corsProxy); const [sentryDsn, setSentryDsn] = useState(config.sentryDsn); - const { getAvailableModels } = useShakespeare(); - const [aiModels, setAiModels] = useState([]); - const [aiModelsLoading, setAiModelsLoading] = useState(false); + const [baseUrlDraft, setBaseUrlDraft] = useState(config.aiBaseURL); + const [apiKeyDraft, setApiKeyDraft] = useState(config.aiApiKey); + const [modelDraft, setModelDraft] = useState(config.aiModel); + const [showApiKey, setShowApiKey] = useState(false); const [systemPromptDraft, setSystemPromptDraft] = useState(config.aiSystemPrompt || DEFAULT_SYSTEM_PROMPT_TEMPLATE); - // Fetch models lazily when the AI section is opened - useEffect(() => { - if (!aiOpen || !user || aiModels.length > 0) return; - let cancelled = false; - setAiModelsLoading(true); - getAvailableModels() - .then((response) => { - if (cancelled) return; - setAiModels(sortModelsByCost(response.data)); - }) - .catch(() => {}) - .finally(() => { if (!cancelled) setAiModelsLoading(false); }); - return () => { cancelled = true; }; - }, [aiOpen, user, aiModels.length, getAvailableModels]); + useEffect(() => { setBaseUrlDraft(config.aiBaseURL); }, [config.aiBaseURL]); + useEffect(() => { setApiKeyDraft(config.aiApiKey); }, [config.aiApiKey]); + useEffect(() => { setModelDraft(config.aiModel); }, [config.aiModel]); + + const commitBaseUrl = () => { + const trimmed = baseUrlDraft.trim().replace(/\/+$/, ''); + if (!trimmed) { + setBaseUrlDraft(DEFAULT_AI_BASE_URL); + if (config.aiBaseURL !== DEFAULT_AI_BASE_URL) { + updateConfig((current) => ({ ...current, aiBaseURL: DEFAULT_AI_BASE_URL })); + toast({ title: 'Base URL reset to default' }); + } + return; + } + if (trimmed !== config.aiBaseURL) { + updateConfig((current) => ({ ...current, aiBaseURL: trimmed })); + toast({ title: 'AI base URL updated' }); + } + }; + + const commitApiKey = () => { + const trimmed = apiKeyDraft.trim(); + if (trimmed !== config.aiApiKey) { + updateConfig((current) => ({ ...current, aiApiKey: trimmed })); + toast({ title: trimmed ? 'API key updated' : 'API key cleared (using NIP-98 auth)' }); + } + }; + + const commitModel = () => { + const trimmed = modelDraft.trim(); + if (!trimmed) { + setModelDraft(DEFAULT_AI_MODEL); + if (config.aiModel !== DEFAULT_AI_MODEL) { + updateConfig((current) => ({ ...current, aiModel: DEFAULT_AI_MODEL })); + toast({ title: 'AI model reset to default' }); + } + return; + } + if (trimmed !== config.aiModel) { + updateConfig((current) => ({ ...current, aiModel: trimmed })); + toast({ title: 'AI model updated' }); + } + }; + + const resetProviderDefaults = () => { + setBaseUrlDraft(DEFAULT_AI_BASE_URL); + setApiKeyDraft(''); + setModelDraft(DEFAULT_AI_MODEL); + updateConfig((current) => ({ + ...current, + aiBaseURL: DEFAULT_AI_BASE_URL, + aiApiKey: '', + aiModel: DEFAULT_AI_MODEL, + })); + toast({ title: 'Provider settings reset to defaults' }); + }; + + const providerIsDefault = + config.aiBaseURL === DEFAULT_AI_BASE_URL && + config.aiApiKey === '' && + config.aiModel === DEFAULT_AI_MODEL; const handleStatsPubkeyChange = (value: string) => { setStatsPubkey(value); @@ -68,6 +116,156 @@ export function AdvancedSettings() { return (
+ {/* Agent Section */} +
+ + + + + +
+ + {/* AI Base URL */} +
+ +

+ OpenAI-compatible /v1 endpoint. An API key is required for endpoints that don't support NIP-98 auth. +

+ setBaseUrlDraft(e.target.value)} + onBlur={commitBaseUrl} + placeholder={DEFAULT_AI_BASE_URL} + className="font-mono text-base md:text-sm" + autoComplete="off" + spellCheck={false} + /> +
+ + {/* API Key */} +
+ +

+ Optional. Required for endpoints that use standard API-key auth (e.g. OpenAI, Anthropic, OpenRouter). +

+
+ setApiKeyDraft(e.target.value)} + onBlur={commitApiKey} + placeholder="Leave empty to use NIP-98 auth" + className="font-mono text-base md:text-sm" + autoComplete="off" + spellCheck={false} + /> + +
+
+ + {/* AI Model */} +
+ +

+ Model ID sent to the provider (e.g. grok-4.1-fast, claude-opus-4.6, gpt-4o). +

+ setModelDraft(e.target.value)} + onBlur={commitModel} + placeholder={DEFAULT_AI_MODEL} + className="font-mono text-base md:text-sm" + autoComplete="off" + spellCheck={false} + /> + {!providerIsDefault && ( + + )} +
+ + {/* AI System Prompt */} +
+ +

+ The base system prompt sent to the AI. Supports {'{{SAVED_FEEDS}}'} and {'{{USER_IDENTITY}}'} placeholders. +

+