From 4ff019e9cf7d5bc2fa2430da1cfd6b99ff15bd64 Mon Sep 17 00:00:00 2001 From: Lemon Date: Mon, 30 Mar 2026 23:44:52 -0700 Subject: [PATCH] Move AI model selector from Dork chat to Settings > Advanced --- src/App.tsx | 1 + src/components/AdvancedSettings.tsx | 94 ++++++++++++++++++++++++++++- src/contexts/AppContext.ts | 2 + src/hooks/useAIChatSession.ts | 34 ++++------- src/pages/AIChatPage.tsx | 27 +-------- 5 files changed, 109 insertions(+), 49 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7e48f1ae..97960f56 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -156,6 +156,7 @@ const hardcodedConfig: AppConfig = { { id: 'hot-posts' }, { id: 'wikipedia' }, ], + aiModel: '', }; /** diff --git a/src/components/AdvancedSettings.tsx b/src/components/AdvancedSettings.tsx index 65927f98..ba1865ed 100644 --- a/src/components/AdvancedSettings.tsx +++ b/src/components/AdvancedSettings.tsx @@ -1,12 +1,14 @@ -import { useState } from 'react'; -import { ChevronDown, ChevronUp, Bug, RotateCcw, AlertTriangle } from 'lucide-react'; +import { useState, useEffect } from 'react'; +import { ChevronDown, ChevronUp, Bot, Bug, RotateCcw, AlertTriangle } 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 { RequestToVanishDialog } from '@/components/RequestToVanishDialog'; import { useAppContext } from '@/hooks/useAppContext'; +import { useShakespeare, type Model } from '@/hooks/useShakespeare'; import { useToast } from '@/hooks/useToast'; import { useEncryptedSettings } from '@/hooks/useEncryptedSettings'; import { useCurrentUser } from '@/hooks/useCurrentUser'; @@ -19,6 +21,10 @@ export function AdvancedSettings() { const { toast } = useToast(); const { updateSettings } = useEncryptedSettings(); const { user } = useCurrentUser(); + const { getAvailableModels } = useShakespeare(); + const [aiOpen, setAiOpen] = useState(false); + const [aiModels, setAiModels] = useState([]); + const [aiModelsLoading, setAiModelsLoading] = useState(false); const [systemOpen, setSystemOpen] = useState(true); const [sentryOpen, setSentryOpen] = useState(false); const [dangerOpen, setDangerOpen] = useState(false); @@ -29,6 +35,26 @@ export function AdvancedSettings() { const [corsProxy, setCorsProxy] = useState(config.corsProxy); const [sentryDsn, setSentryDsn] = useState(config.sentryDsn); + // Fetch AI models when the section opens + useEffect(() => { + if (!aiOpen || !user || aiModels.length > 0) return; + let cancelled = false; + setAiModelsLoading(true); + getAvailableModels() + .then((response) => { + if (cancelled) return; + const sorted = response.data.sort((a, b) => { + const costA = parseFloat(a.pricing.prompt) + parseFloat(a.pricing.completion); + const costB = parseFloat(b.pricing.prompt) + parseFloat(b.pricing.completion); + return costA - costB; + }); + setAiModels(sorted); + }) + .catch(() => {}) + .finally(() => { if (!cancelled) setAiModelsLoading(false); }); + return () => { cancelled = true; }; + }, [aiOpen, user, aiModels.length, getAvailableModels]); + const handleStatsPubkeyChange = (value: string) => { setStatsPubkey(value); if (value.length === 64 && /^[0-9a-f]{64}$/i.test(value)) { @@ -42,6 +68,70 @@ export function AdvancedSettings() { return (
+ {/* Dork AI Section */} + {user && ( +
+ + + + + +
+
+ + +

+ Choose which AI model Dork uses for chat responses. +

+
+
+
+
+
+ )} + {/* System Section (includes Stats Source) */}
diff --git a/src/contexts/AppContext.ts b/src/contexts/AppContext.ts index 48243b6b..b071ad80 100644 --- a/src/contexts/AppContext.ts +++ b/src/contexts/AppContext.ts @@ -247,6 +247,8 @@ export interface AppConfig { sandboxDomain: string; /** Ordered list of right sidebar widget configs. Each entry is a widget type ID with optional display settings. */ sidebarWidgets: WidgetConfig[]; + /** Selected AI model ID for Dork chat. Empty string means "use cheapest available". */ + aiModel: string; } /** Configuration for a single widget in the right sidebar. */ diff --git a/src/hooks/useAIChatSession.ts b/src/hooks/useAIChatSession.ts index 4a529602..91518ef6 100644 --- a/src/hooks/useAIChatSession.ts +++ b/src/hooks/useAIChatSession.ts @@ -1,6 +1,7 @@ import { useState, useRef, useEffect, useCallback, useMemo } from 'react'; -import { useShakespeare, type ChatMessage, type Model } from '@/hooks/useShakespeare'; +import { useShakespeare, type ChatMessage } from '@/hooks/useShakespeare'; import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useAppContext } from '@/hooks/useAppContext'; import { useAIChatTools } from '@/hooks/useAIChatTools'; import { TOOLS, type DisplayMessage, type ToolCall } from '@/lib/aiChatTools'; import { SYSTEM_PROMPT } from '@/lib/aiChatSystemPrompt'; @@ -40,15 +41,17 @@ function saveMessages(messages: DisplayMessage[]): void { export function useAIChatSession() { const { user } = useCurrentUser(); + const { config } = useAppContext(); const { sendChatMessage, getAvailableModels, getCredits, isLoading: apiLoading, error: apiError, clearError } = useShakespeare(); const { executeToolCall } = useAIChatTools(); const [messages, setMessages] = useState(loadMessages); const [input, setInput] = useState(''); const [isStreaming, setIsStreaming] = useState(false); - const [models, setModels] = useState([]); - const [selectedModel, setSelectedModel] = useState(''); - const [modelsLoading, setModelsLoading] = useState(false); + + // Resolve the effective model: config value, or fetch the cheapest as default + const [defaultModel, setDefaultModel] = useState(''); + const selectedModel = config.aiModel || defaultModel; const messagesEndRef = useRef(null); const abortRef = useRef(null); @@ -63,13 +66,11 @@ export function useAIChatSession() { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); - // Fetch available models on mount + // Fetch cheapest model as fallback when no model is configured useEffect(() => { - if (!user) return; + if (!user || config.aiModel) return; let cancelled = false; - setModelsLoading(true); - getAvailableModels() .then((response) => { if (cancelled) return; @@ -78,20 +79,14 @@ export function useAIChatSession() { const costB = parseFloat(b.pricing.prompt) + parseFloat(b.pricing.completion); return costA - costB; }); - setModels(sorted); - if (sorted.length > 0 && !selectedModel) { - setSelectedModel(sorted[0].id); + if (sorted.length > 0) { + setDefaultModel(sorted[0].id); } }) - .catch((err) => { - if (!cancelled) console.error('Failed to fetch models:', err); - }) - .finally(() => { - if (!cancelled) setModelsLoading(false); - }); + .catch(() => {}); return () => { cancelled = true; }; - }, [user, getAvailableModels]); // eslint-disable-line react-hooks/exhaustive-deps + }, [user, config.aiModel, getAvailableModels]); // Build the chat messages array for the API const buildApiMessages = useCallback((displayMsgs: DisplayMessage[]): ChatMessage[] => { @@ -271,10 +266,7 @@ export function useAIChatSession() { input, setInput, isStreaming, - models, selectedModel, - setSelectedModel, - modelsLoading, apiLoading, apiError, messagesEndRef, diff --git a/src/pages/AIChatPage.tsx b/src/pages/AIChatPage.tsx index 1cd2d0ed..2abac78b 100644 --- a/src/pages/AIChatPage.tsx +++ b/src/pages/AIChatPage.tsx @@ -15,7 +15,6 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { cn } from '@/lib/utils'; import { useLayoutOptions } from '@/contexts/LayoutContext'; @@ -27,8 +26,7 @@ export function AIChatPage() { const { config } = useAppContext(); const { user } = useCurrentUser(); const { - messages, input, setInput, isStreaming, - models, selectedModel, setSelectedModel, modelsLoading, + messages, input, setInput, isStreaming, selectedModel, apiLoading, apiError, messagesEndRef, handleSend, handleStop, handleKeyDown, handleClear, getCredits, } = useAIChatSession(); @@ -63,29 +61,6 @@ export function AIChatPage() { } className="shrink-0 py-3">
-