Move AI model selector from Dork chat to Settings > Advanced
This commit is contained in:
@@ -156,6 +156,7 @@ const hardcodedConfig: AppConfig = {
|
||||
{ id: 'hot-posts' },
|
||||
{ id: 'wikipedia' },
|
||||
],
|
||||
aiModel: '',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<Model[]>([]);
|
||||
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 (
|
||||
<div>
|
||||
{/* Dork AI Section */}
|
||||
{user && (
|
||||
<div>
|
||||
<Collapsible open={aiOpen} onOpenChange={setAiOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="relative w-full justify-between px-3 py-3.5 h-auto hover:bg-muted/20 hover:text-foreground rounded-none"
|
||||
>
|
||||
<span className="flex items-center gap-2 text-base font-semibold">
|
||||
<Bot className="h-4 w-4" />
|
||||
Dork
|
||||
</span>
|
||||
{aiOpen ? (
|
||||
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="px-4 py-4 space-y-4 border-b border-border">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ai-model">Model</Label>
|
||||
<Select
|
||||
value={config.aiModel || (aiModels.length > 0 ? aiModels[0].id : '')}
|
||||
onValueChange={(value) => {
|
||||
updateConfig(() => ({ aiModel: value }));
|
||||
toast({ title: 'AI model updated' });
|
||||
}}
|
||||
disabled={aiModelsLoading || aiModels.length === 0}
|
||||
>
|
||||
<SelectTrigger id="ai-model">
|
||||
<SelectValue placeholder={aiModelsLoading ? 'Loading models...' : 'Select model'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{aiModels.map((model) => {
|
||||
const totalCost = parseFloat(model.pricing.prompt) + parseFloat(model.pricing.completion);
|
||||
const isFree = totalCost === 0;
|
||||
return (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
<span className="flex items-center gap-1.5">
|
||||
{model.name}
|
||||
{isFree && (
|
||||
<span className="text-[10px] font-medium text-green-600 dark:text-green-400 bg-green-500/10 px-1 rounded">
|
||||
FREE
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose which AI model Dork uses for chat responses.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* System Section (includes Stats Source) */}
|
||||
<div>
|
||||
<Collapsible open={systemOpen} onOpenChange={setSystemOpen}>
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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<DisplayMessage[]>(loadMessages);
|
||||
const [input, setInput] = useState('');
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [models, setModels] = useState<Model[]>([]);
|
||||
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<HTMLDivElement>(null);
|
||||
const abortRef = useRef<AbortController | null>(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,
|
||||
|
||||
@@ -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() {
|
||||
<PageHeader title="Dork" icon={<Bot className="size-5" />} className="shrink-0 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<CreditsBadge getCredits={getCredits} />
|
||||
<Select value={selectedModel} onValueChange={setSelectedModel} disabled={modelsLoading}>
|
||||
<SelectTrigger className="h-8 min-w-0 text-base md:text-xs">
|
||||
<SelectValue placeholder={modelsLoading ? 'Loading...' : 'Select model'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{models.map((model) => {
|
||||
const totalCost = parseFloat(model.pricing.prompt) + parseFloat(model.pricing.completion);
|
||||
const isFree = totalCost === 0;
|
||||
return (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
<span className="flex items-center gap-1.5">
|
||||
{model.name}
|
||||
{isFree && (
|
||||
<span className="text-[10px] font-medium text-green-600 dark:text-green-400 bg-green-500/10 px-1 rounded">
|
||||
FREE
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
||||
Reference in New Issue
Block a user