"use client"; import { useLayoutEffect, 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 { 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, SEARXNG_BASE_URL, } from "@/constants/urls"; import locales from "@/constants/locales"; 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(), 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(), searxngApiProxy: z.string().optional(), searxngScope: z.string().optional(), parallelSearch: z.number().min(1).max(5), searchMaxResult: z.number().min(1).max(10), 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(), }); 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) { update(values); onClose(); } const fetchModelList = useCallback(async () => { const { provider } = useSettingStore.getState(); try { setIsRefreshing(true); await refresh(provider); } 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 }); await fetchModelList(); } 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(); }, }, }); } useLayoutEffect(() => { if (open && !preLoading) { preLoading = true; fetchModelList(); } }, [open, fetchModelList]); useLayoutEffect(() => { if (open && mode === "") { const { apiKey, accessPassword, update } = useSettingStore.getState(); const requestMode = !apiKey && 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") ) } /> )} />
( {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")} *
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.apiUrlLabel")} )} /> ( {t("setting.searchScope")} )} />
( {t("setting.parallelSearch")}
field.onChange(values[0]) } /> {field.value}
)} /> ( {t("setting.searchResults")}
field.onChange(values[0]) } /> {field.value}
)} />
( {t("setting.language")} )} /> ( {t("theme")} )} /> ( {t("setting.debug")} )} /> {pwaInstall ? (
) : null}
( {t("setting.references")} )} /> ( {t("setting.citationImage")} )} /> ( {t("setting.textOutputMode")} )} /> ( {t("setting.useLocalResource")} )} />
); } export default Setting;