"use client"; import { useEffect, useState, useCallback, useMemo, type ReactNode, } from "react"; import { useTranslation } from "react-i18next"; import { usePWAInstall } from "react-use-pwa-install"; import { RefreshCw, CircleHelp, MonitorDown } from "lucide-react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { toast } from "sonner"; import { Password } from "@/components/Internal/PasswordInput"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogFooter, DialogTitle, } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Form, FormControl, FormField, FormItem, FormLabel, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectLabel, SelectValue, } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Slider } from "@/components/ui/slider"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import useModel from "@/hooks/useModelList"; import { useSettingStore } from "@/store/setting"; import { GEMINI_BASE_URL, OPENROUTER_BASE_URL, OPENAI_BASE_URL, ANTHROPIC_BASE_URL, DEEPSEEK_BASE_URL, XAI_BASE_URL, MISTRAL_BASE_URL, POLLINATIONS_BASE_URL, OLLAMA_BASE_URL, TAVILY_BASE_URL, FIRECRAWL_BASE_URL, EXA_BASE_URL, BOCHA_BASE_URL, BRAVE_BASE_URL, SEARXNG_BASE_URL, } from "@/constants/urls"; import locales from "@/constants/locales"; import { parseDeepResearchPromptOverrides } from "@/constants/prompts"; import { filterThinkingModelList, filterNetworkingModelList, filterOpenRouterModelList, filterDeepSeekModelList, filterOpenAIModelList, filterMistralModelList, filterPollinationsModelList, getCustomModelList, } from "@/utils/model"; import { researchStore } from "@/utils/storage"; import { cn } from "@/utils/style"; import { omit, capitalize } from "radash"; type SettingProps = { open: boolean; onClose: () => void; }; const BUILD_MODE = process.env.NEXT_PUBLIC_BUILD_MODE; const VERSION = process.env.NEXT_PUBLIC_VERSION; const DISABLED_AI_PROVIDER = process.env.NEXT_PUBLIC_DISABLED_AI_PROVIDER || ""; const DISABLED_SEARCH_PROVIDER = process.env.NEXT_PUBLIC_DISABLED_SEARCH_PROVIDER || ""; const MODEL_LIST = process.env.NEXT_PUBLIC_MODEL_LIST || ""; const formSchema = z.object({ provider: z.string(), mode: z.string().optional(), apiKey: z.string().optional(), apiProxy: z.string().optional(), thinkingModel: z.string().optional(), networkingModel: z.string().optional(), googleVertexProject: z.string().optional(), googleVertexLocation: z.string().optional(), googleClientEmail: z.string().optional(), googlePrivateKey: z.string().optional(), googlePrivateKeyId: z.string().optional(), googleVertexThinkingModel: z.string().optional(), googleVertexNetworkingModel: z.string().optional(), openRouterApiKey: z.string().optional(), openRouterApiProxy: z.string().optional(), openRouterThinkingModel: z.string().optional(), openRouterNetworkingModel: z.string().optional(), openAIApiKey: z.string().optional(), openAIApiProxy: z.string().optional(), openAIThinkingModel: z.string().optional(), openAINetworkingModel: z.string().optional(), anthropicApiKey: z.string().optional(), anthropicApiProxy: z.string().optional(), anthropicThinkingModel: z.string().optional(), anthropicNetworkingModel: z.string().optional(), deepseekApiKey: z.string().optional(), deepseekApiProxy: z.string().optional(), deepseekThinkingModel: z.string().optional(), deepseekNetworkingModel: z.string().optional(), xAIApiKey: z.string().optional(), xAIApiProxy: z.string().optional(), xAIThinkingModel: z.string().optional(), xAINetworkingModel: z.string().optional(), mistralApiKey: z.string().optional(), mistralApiProxy: z.string().optional(), mistralThinkingModel: z.string().optional(), mistralNetworkingModel: z.string().optional(), azureApiKey: z.string().optional(), azureResourceName: z.string().optional(), azureApiVersion: z.string().optional(), azureThinkingModel: z.string().optional(), azureNetworkingModel: z.string().optional(), openAICompatibleApiKey: z.string().optional(), openAICompatibleApiProxy: z.string().optional(), openAICompatibleThinkingModel: z.string().optional(), openAICompatibleNetworkingModel: z.string().optional(), pollinationsApiProxy: z.string().optional(), pollinationsThinkingModel: z.string().optional(), pollinationsNetworkingModel: z.string().optional(), ollamaApiProxy: z.string().optional(), ollamaThinkingModel: z.string().optional(), ollamaNetworkingModel: z.string().optional(), accessPassword: z.string().optional(), enableSearch: z.string(), searchProvider: z.string().optional(), tavilyApiKey: z.string().optional(), tavilyApiProxy: z.string().optional(), tavilyScope: z.string().optional(), firecrawlApiKey: z.string().optional(), firecrawlApiProxy: z.string().optional(), exaApiKey: z.string().optional(), exaApiProxy: z.string().optional(), exaScope: z.string().optional(), bochaApiKey: z.string().optional(), bochaApiProxy: z.string().optional(), braveApiKey: z.string().optional(), braveApiProxy: z.string().optional(), searxngApiProxy: z.string().optional(), searxngScope: z.string().optional(), parallelSearch: z.number().min(1).max(5), autoReviewRounds: z.number().min(0).max(5), maxCollectionTopics: z.number().min(1).max(20), searchMaxResult: z.number().min(1).max(10), searchIncludeDomains: z.string().optional(), searchExcludeDomains: z.string().optional(), language: z.string().optional(), theme: z.string().optional(), debug: z.enum(["enable", "disable"]).optional(), references: z.enum(["enable", "disable"]).optional(), citationImage: z.enum(["enable", "disable"]).optional(), smoothTextStreamType: z.enum(["character", "word", "line"]).optional(), onlyUseLocalResource: z.enum(["enable", "disable"]).optional(), useFileFormatResource: z.enum(["enable", "disable"]).optional(), reportStyle: z .enum(["balanced", "executive", "technical", "concise"]) .optional(), reportLength: z.enum(["brief", "standard", "comprehensive"]).optional(), deepResearchPromptOverrides: z.string().optional(), }); function convertModelName(name: string) { return name .replaceAll("/", "-") .split("-") .map((word) => capitalize(word)) .join(" "); } let preLoading = false; function HelpTip({ children, tip }: { children: ReactNode; tip: string }) { const [open, setOpen] = useState(false); const handleOpen = () => { setOpen(true); setTimeout(() => { setOpen(false); }, 2000); }; return (
{children} setOpen(opened)}> { ev.preventDefault(); ev.stopPropagation(); handleOpen(); }} />

{tip}

); } function Setting({ open, onClose }: SettingProps) { const { t } = useTranslation(); const { mode, provider, searchProvider, update } = useSettingStore(); const { modelList, refresh } = useModel(); const pwaInstall = usePWAInstall(); const [isRefreshing, setIsRefreshing] = useState(false); const thinkingModelList = useMemo(() => { const { provider } = useSettingStore.getState(); if (provider === "google") { return filterThinkingModelList(modelList); } else if (provider === "openrouter") { return filterOpenRouterModelList(modelList); } else if (provider === "deepseek") { return filterDeepSeekModelList(modelList); } else if (provider === "mistral") { return filterMistralModelList(modelList); } else if (provider === "pollinations") { return filterPollinationsModelList(modelList); } return [[], modelList]; }, [modelList]); const networkingModelList = useMemo(() => { const { provider } = useSettingStore.getState(); if (provider === "google") { return filterNetworkingModelList(modelList); } else if (provider === "openrouter") { return filterOpenRouterModelList(modelList); } else if (provider === "openai") { return filterOpenAIModelList(modelList); } else if (provider === "mistral") { return filterMistralModelList(modelList); } else if (provider === "pollinations") { return filterPollinationsModelList(modelList); } return [[], modelList]; }, [modelList]); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: async () => { return new Promise((resolve) => { const state = useSettingStore.getState(); resolve({ ...omit(state, ["update"]) }); }); }, }); const isDisabledAIProvider = useCallback( (provider: string) => { const disabledAIProviders = mode === "proxy" && DISABLED_AI_PROVIDER.length > 0 ? DISABLED_AI_PROVIDER.split(",") : []; return disabledAIProviders.includes(provider); }, [mode], ); const isDisabledAIModel = useCallback( (model: string) => { if (mode === "local") return false; const { availableModelList, disabledModelList } = getCustomModelList( MODEL_LIST.length > 0 ? MODEL_LIST.split(",") : [], ); const isAvailableModel = availableModelList.some( (availableModel) => availableModel === model, ); if (isAvailableModel) return false; if (disabledModelList.includes("all")) return true; return disabledModelList.some((disabledModel) => disabledModel === model); }, [mode], ); const isDisabledSearchProvider = useCallback( (provider: string) => { const disabledSearchProviders = mode === "proxy" && DISABLED_SEARCH_PROVIDER.length > 0 ? DISABLED_SEARCH_PROVIDER.split(",") : []; return disabledSearchProviders.includes(provider); }, [mode], ); const installPWA = async () => { if ("serviceWorker" in navigator) { await window.serwist?.register(); } if (pwaInstall) await pwaInstall(); }; function handleClose(open: boolean) { if (!open) onClose(); } function handleSubmit(values: z.infer) { try { parseDeepResearchPromptOverrides(values.deepResearchPromptOverrides); } catch { toast.error(t("setting.promptOverridesInvalid")); return; } update(values); onClose(); } const fetchModelList = useCallback(async () => { const { provider } = useSettingStore.getState(); try { setIsRefreshing(true); await refresh(provider); } catch (e) { console.error("Failed to fetch model list:", e); } finally { setIsRefreshing(false); } }, [refresh]); function handleModeChange(mode: string) { update({ mode }); } async function handleProviderChange(provider: string) { update({ provider }); await fetchModelList(); } async function handleSearchProviderChange(searchProvider: string) { update({ searchProvider }); } async function updateSetting(key: string, value?: string | number) { update({ [key]: value }); try { await fetchModelList(); } catch (e) { console.error("Failed to update setting and fetch models:", e); } } function handleReset() { toast.warning(t("setting.resetSetting"), { description: t("setting.resetSettingWarning"), duration: 5000, action: { label: t("setting.confirm"), onClick: async () => { const { reset } = useSettingStore.getState(); reset(); await researchStore.clear(); }, }, }); } useEffect(() => { if (open && !preLoading) { preLoading = true; fetchModelList(); } }, [open, fetchModelList]); useEffect(() => { if (open && mode === "") { const { accessPassword, update } = useSettingStore.getState(); const requestMode = accessPassword ? "proxy" : "local"; update({ mode: requestMode }); form.setValue("mode", requestMode); } }, [open, mode, form]); return ( {t("setting.title")} {t("setting.description")}
{t("setting.model")} {t("setting.search")} {t("setting.general")} {t("setting.experimental")}
( {t("setting.mode")} )} />
( {t("setting.provider")} )} />
( {t("setting.apiKeyLabel")} * updateSetting( "apiKey", form.getValues("apiKey"), ) } /> )} /> ( {t("setting.apiUrlLabel")} updateSetting( "apiProxy", form.getValues("apiProxy"), ) } /> )} />
( Project * updateSetting( "googleVertexProject", form.getValues("googleVertexProject"), ) } /> )} /> ( Location * updateSetting( "googleVertexLocation", form.getValues("googleVertexLocation"), ) } /> )} /> ( Client Email updateSetting( "googleClientEmail", form.getValues("googleClientEmail"), ) } /> )} /> ( Private Key updateSetting( "googlePrivateKey", form.getValues("googlePrivateKey"), ) } /> )} /> ( Private Key ID updateSetting( "googlePrivateKeyId", form.getValues("googlePrivateKeyId"), ) } /> )} />
( {t("setting.apiKeyLabel")} * updateSetting( "openRouterApiKey", form.getValues("openRouterApiKey"), ) } /> )} /> ( {t("setting.apiUrlLabel")} updateSetting( "openRouterApiProxy", form.getValues("openRouterApiProxy"), ) } /> )} />
( {t("setting.apiKeyLabel")} * updateSetting( "openAIApiKey", form.getValues("openAIApiKey"), ) } /> )} /> ( {t("setting.apiUrlLabel")} updateSetting( "openAIApiProxy", form.getValues("openAIApiProxy"), ) } /> )} />
( {t("setting.apiKeyLabel")} * updateSetting( "anthropicApiKey", form.getValues("anthropicApiKey"), ) } /> )} /> ( {t("setting.apiUrlLabel")} updateSetting( "anthropicApiProxy", form.getValues("anthropicApiProxy"), ) } /> )} />
( {t("setting.apiKeyLabel")} * updateSetting( "deepseekApiKey", form.getValues("deepseekApiKey"), ) } /> )} /> ( {t("setting.apiUrlLabel")} updateSetting( "deepseekApiProxy", form.getValues("deepseekApiProxy"), ) } /> )} />
( {t("setting.apiKeyLabel")} * updateSetting( "xAIApiKey", form.getValues("xAIApiKey"), ) } /> )} /> ( {t("setting.apiUrlLabel")} updateSetting( "xAIApiProxy", form.getValues("xAIApiProxy"), ) } /> )} />
( {t("setting.apiKeyLabel")} * updateSetting( "mistralApiKey", form.getValues("mistralApiKey"), ) } /> )} /> ( {t("setting.apiUrlLabel")} updateSetting( "mistralApiProxy", form.getValues("mistralApiProxy"), ) } /> )} />
( {t("setting.apiKeyLabel")} * updateSetting( "azureApiKey", form.getValues("azureApiKey"), ) } /> )} /> ( {t("setting.resourceNameLabel")} * updateSetting( "azureResourceName", form.getValues("azureResourceName"), ) } /> )} /> ( {t("setting.apiVersionLabel")} updateSetting( "azureApiVersion", form.getValues("azureApiVersion"), ) } /> )} />
( {t("setting.apiKeyLabel")} * updateSetting( "openAICompatibleApiKey", form.getValues("openAICompatibleApiKey"), ) } /> )} /> ( {t("setting.apiUrlLabel")} updateSetting( "openAICompatibleApiProxy", form.getValues("openAICompatibleApiProxy"), ) } /> )} />
( {t("setting.apiUrlLabel")} updateSetting( "pollinationsApiProxy", form.getValues("pollinationsApiProxy"), ) } /> )} />
( {t("setting.apiUrlLabel")} updateSetting( "ollamaApiProxy", form.getValues("ollamaApiProxy"), ) } /> )} />
( {t("setting.accessPassword")} * updateSetting( "accessPassword", form.getValues("accessPassword"), ) } /> )} />
( {t("setting.thinkingModel")} *
)} /> ( {t("setting.networkingModel")} *
)} />
( {t("setting.thinkingModel")} *
)} /> ( {t("setting.networkingModel")} *
)} />
( {t("setting.thinkingModel")} *
)} /> ( {t("setting.networkingModel")} *
)} />
( {t("setting.thinkingModel")} *
)} /> ( {t("setting.networkingModel")} *
)} />
( {t("setting.thinkingModel")} *
)} /> ( {t("setting.networkingModel")} *
)} />
( {t("setting.thinkingModel")} *
)} /> ( {t("setting.networkingModel")} *
)} />
( {t("setting.thinkingModel")} *
)} /> ( {t("setting.networkingModel")} *
)} />
( {t("setting.thinkingModel")} *
)} /> ( {t("setting.networkingModel")} *
)} />
( {t("setting.thinkingModel")} *
)} /> ( {t("setting.networkingModel")} *
)} />
( {t("setting.thinkingModel")} *
0, })} placeholder={t("setting.modelListPlaceholder")} {...field} />
)} /> ( {t("setting.networkingModel")} *
0, })} placeholder={t("setting.modelListPlaceholder")} {...field} />
)} />
( {t("setting.thinkingModel")} *
)} /> ( {t("setting.networkingModel")} *
)} />
( {t("setting.thinkingModel")} *
0, })} placeholder={t("setting.modelListPlaceholder")} {...field} />
)} /> ( {t("setting.networkingModel")} *
0, })} placeholder={t("setting.modelListPlaceholder")} {...field} />
)} />
( {t("setting.webSearch")} )} /> ( {t("setting.searchProvider")} )} />
( {t("setting.apiKeyLabel")} * )} /> ( {t("setting.apiUrlLabel")} )} /> ( {t("setting.searchScope")} )} />
( {t("setting.apiKeyLabel")} * )} /> ( {t("setting.apiUrlLabel")} )} />
( {t("setting.apiKeyLabel")} * )} /> ( {t("setting.apiUrlLabel")} )} /> ( {t("setting.searchScope")} )} />
( {t("setting.apiKeyLabel")} * )} /> ( {t("setting.apiUrlLabel")} )} />
( {t("setting.apiKeyLabel")} * )} /> ( {t("setting.apiUrlLabel")} )} />
( {t("setting.apiUrlLabel")} )} /> ( {t("setting.searchScope")} )} />
( {t("setting.parallelSearch")}
field.onChange(values[0]) } /> {field.value}
)} /> ( {t("setting.autoReviewRounds")}
field.onChange(values[0]) } /> {field.value}
)} /> ( {t("setting.maxCollectionTopics")}
field.onChange(values[0]) } /> {field.value}
)} /> ( {t("setting.searchResults")}
field.onChange(values[0]) } /> {field.value}
)} /> ( {t("setting.searchIncludeDomains")}