import { ChatbotUIContext } from "@/context/context" import { PROFILE_CONTEXT_MAX, PROFILE_DISPLAY_NAME_MAX, PROFILE_USERNAME_MAX, PROFILE_USERNAME_MIN } from "@/db/limits" import { updateProfile } from "@/db/profile" import { uploadProfileImage } from "@/db/storage/profile-images" import { exportLocalStorageAsJSON } from "@/lib/export-old-data" import { fetchOpenRouterModels } from "@/lib/models/fetch-models" import { LLM_LIST_MAP } from "@/lib/models/llm/llm-list" import { supabase } from "@/lib/supabase/browser-client" import { cn } from "@/lib/utils" import { OpenRouterLLM } from "@/types" import { IconCircleCheckFilled, IconCircleXFilled, IconFileDownload, IconLoader2, IconLogout, IconUser } from "@tabler/icons-react" import Image from "next/image" import { useRouter } from "next/navigation" import { FC, useCallback, useContext, useRef, useState } from "react" import { toast } from "sonner" import { SIDEBAR_ICON_SIZE } from "../sidebar/sidebar-switcher" import { Button } from "../ui/button" import ImagePicker from "../ui/image-picker" import { Input } from "../ui/input" import { Label } from "../ui/label" import { LimitDisplay } from "../ui/limit-display" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "../ui/sheet" import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs" import { TextareaAutosize } from "../ui/textarea-autosize" import { WithTooltip } from "../ui/with-tooltip" import { ThemeSwitcher } from "./theme-switcher" import { usePathname } from "next/navigation" // 导入 usePathname import { useTranslation } from "react-i18next"; // 引入useTranslation用于国际化 import i18nConfig from "@/i18nConfig" interface ProfileSettingsProps {} export const ProfileSettings: FC = ({}) => { const { t } = useTranslation(); // 使用t函数来获取翻译文本 const { profile, setProfile, envKeyMap, setAvailableHostedModels, setAvailableOpenRouterModels, availableOpenRouterModels } = useContext(ChatbotUIContext) const router = useRouter() const pathname = usePathname() // 获取当前路径 // 提取当前路径中的 locale 部分 const locale = pathname.split("/")[1] || "en" const buttonRef = useRef(null) const [isOpen, setIsOpen] = useState(false) const [displayName, setDisplayName] = useState(profile?.display_name || "") const [username, setUsername] = useState(profile?.username || "") const [usernameAvailable, setUsernameAvailable] = useState(true) const [loadingUsername, setLoadingUsername] = useState(false) const [profileImageSrc, setProfileImageSrc] = useState( profile?.image_url || "" ) const [profileImageFile, setProfileImageFile] = useState(null) const [profileInstructions, setProfileInstructions] = useState( profile?.profile_context || "" ) const [useAzureOpenai, setUseAzureOpenai] = useState( profile?.use_azure_openai ) const [openaiAPIKey, setOpenaiAPIKey] = useState( profile?.openai_api_key || "" ) const [openaiOrgID, setOpenaiOrgID] = useState( profile?.openai_organization_id || "" ) const [azureOpenaiAPIKey, setAzureOpenaiAPIKey] = useState( profile?.azure_openai_api_key || "" ) const [azureOpenaiEndpoint, setAzureOpenaiEndpoint] = useState( profile?.azure_openai_endpoint || "" ) const [azureOpenai35TurboID, setAzureOpenai35TurboID] = useState( profile?.azure_openai_35_turbo_id || "" ) const [azureOpenai45TurboID, setAzureOpenai45TurboID] = useState( profile?.azure_openai_45_turbo_id || "" ) const [azureOpenai45VisionID, setAzureOpenai45VisionID] = useState( profile?.azure_openai_45_vision_id || "" ) const [azureEmbeddingsID, setAzureEmbeddingsID] = useState( profile?.azure_openai_embeddings_id || "" ) const [anthropicAPIKey, setAnthropicAPIKey] = useState( profile?.anthropic_api_key || "" ) const [googleGeminiAPIKey, setGoogleGeminiAPIKey] = useState( profile?.google_gemini_api_key || "" ) const [mistralAPIKey, setMistralAPIKey] = useState( profile?.mistral_api_key || "" ) const [groqAPIKey, setGroqAPIKey] = useState(profile?.groq_api_key || "") const [perplexityAPIKey, setPerplexityAPIKey] = useState( profile?.perplexity_api_key || "" ) const [openrouterAPIKey, setOpenrouterAPIKey] = useState( profile?.openrouter_api_key || "" ) // const handleSignOut = async () => { // await supabase.auth.signOut() // // ✅ 清除 localStorage 中的语言偏好 // if (typeof window !== "undefined") { // localStorage.removeItem("preferred-language") // } // // ✅ 清除 Cookie 中的语言偏好(设置 max-age=0 立即过期) // document.cookie = "preferred-language=; path=/; max-age=0" // const pathSegments = pathname.split("/").filter(Boolean) // const locales = i18nConfig.locales // const defaultLocale = i18nConfig.defaultLocale // let locale: (typeof locales)[number] = defaultLocale // const segment = pathSegments[0] as (typeof locales)[number] // if (locales.includes(segment)) { // locale = segment // } // const homePath = locale === defaultLocale ? "/" : `/${locale}` // router.push(homePath) // router.refresh() // return // } const handleSignOut = async () => { try { // 调用 Supabase 注销 await supabase.auth.signOut(); } catch (error) { console.error("Sign-out error:", error); // 在错误发生时显示反馈或进行其他处理 return; } // ✅ 清除 localStorage 中的语言偏好 if (typeof window !== "undefined") { localStorage.removeItem("preferred-language"); } // ✅ 清除 Cookie 中的语言偏好(设置 max-age=0 立即过期) document.cookie = "preferred-language=; path=/; max-age=0"; // 清除相关 cookies,例如 access_token 和 refresh_token document.cookie = "access_token=; path=/; max-age=0"; document.cookie = "refresh_token=; path=/; max-age=0"; const pathSegments = pathname.split("/").filter(Boolean) const locales = i18nConfig.locales const defaultLocale = i18nConfig.defaultLocale let locale: (typeof locales)[number] = defaultLocale const segment = pathSegments[0] as (typeof locales)[number] if (locales.includes(segment)) { locale = segment } const homePath = locale === defaultLocale ? "/" : `/${locale}` console.log("...........[profile-setting.tsx]") router.push(homePath) router.refresh() return }; const handleSave = async () => { if (!profile) return let profileImageUrl = profile.image_url let profileImagePath = "" if (profileImageFile) { const { path, url } = await uploadProfileImage(profile, profileImageFile) profileImageUrl = url ?? profileImageUrl profileImagePath = path } const updatedProfile = await updateProfile(profile.id, { ...profile, display_name: displayName, username, profile_context: profileInstructions, image_url: profileImageUrl, image_path: profileImagePath, openai_api_key: openaiAPIKey, openai_organization_id: openaiOrgID, anthropic_api_key: anthropicAPIKey, google_gemini_api_key: googleGeminiAPIKey, mistral_api_key: mistralAPIKey, groq_api_key: groqAPIKey, perplexity_api_key: perplexityAPIKey, use_azure_openai: useAzureOpenai, azure_openai_api_key: azureOpenaiAPIKey, azure_openai_endpoint: azureOpenaiEndpoint, azure_openai_35_turbo_id: azureOpenai35TurboID, azure_openai_45_turbo_id: azureOpenai45TurboID, azure_openai_45_vision_id: azureOpenai45VisionID, azure_openai_embeddings_id: azureEmbeddingsID, openrouter_api_key: openrouterAPIKey }) setProfile(updatedProfile) // toast.success("Profile updated!"){t("profile.profileUpdated")} toast.success(t("profile.profileUpdated")) const providers = [ "openai", "google", "azure", "anthropic", "mistral", "groq", "perplexity", "openrouter" ] providers.forEach(async provider => { let providerKey: keyof typeof profile if (provider === "google") { providerKey = "google_gemini_api_key" } else if (provider === "azure") { providerKey = "azure_openai_api_key" } else { providerKey = `${provider}_api_key` as keyof typeof profile } const models = LLM_LIST_MAP[provider] const envKeyActive = envKeyMap[provider] if (!envKeyActive) { const hasApiKey = !!updatedProfile[providerKey] if (provider === "openrouter") { if (hasApiKey && availableOpenRouterModels.length === 0) { const openrouterModels: OpenRouterLLM[] = await fetchOpenRouterModels() setAvailableOpenRouterModels(prev => { const newModels = openrouterModels.filter( model => !prev.some(prevModel => prevModel.modelId === model.modelId) ) return [...prev, ...newModels] }) } else { setAvailableOpenRouterModels([]) } } else { if (hasApiKey && Array.isArray(models)) { setAvailableHostedModels(prev => { const newModels = models.filter( model => !prev.some(prevModel => prevModel.modelId === model.modelId) ) return [...prev, ...newModels] }) } else if (!hasApiKey && Array.isArray(models)) { setAvailableHostedModels(prev => prev.filter(model => !models.includes(model)) ) } } } }) setIsOpen(false) } const debounce = (func: (...args: any[]) => void, wait: number) => { let timeout: NodeJS.Timeout | null return (...args: any[]) => { const later = () => { if (timeout) clearTimeout(timeout) func(...args) } if (timeout) clearTimeout(timeout) timeout = setTimeout(later, wait) } } const checkUsernameAvailability = useCallback( debounce(async (username: string) => { if (!username) return if (username.length < PROFILE_USERNAME_MIN) { setUsernameAvailable(false) return } if (username.length > PROFILE_USERNAME_MAX) { setUsernameAvailable(false) return } const usernameRegex = /^[a-zA-Z0-9_]+$/ if (!usernameRegex.test(username)) { setUsernameAvailable(false) toast.error( "Username must be letters, numbers, or underscores only - no other characters or spacing allowed." ) return } setLoadingUsername(true) const response = await fetch(`/api/username/available`, { method: "POST", body: JSON.stringify({ username }) }) const data = await response.json() const isAvailable = data.isAvailable setUsernameAvailable(isAvailable) if (username === profile?.username) { setUsernameAvailable(true) } setLoadingUsername(false) }, 500), [] ) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { buttonRef.current?.click() } } if (!profile) return null return ( {profile.image_url ? ( {t("profile.imageAlt")} ) : ( )}
{t("profile.settingsTitle")}
{t("profile.profileTab")} {t("profile.apiKeysTab")}
{username !== profile.username ? ( usernameAvailable ? (
{t("profile.available")}
) : (
{t("profile.unavailable")}
) ) : null}
{ setUsername(e.target.value) checkUsernameAvailability(e.target.value) }} minLength={PROFILE_USERNAME_MIN} maxLength={PROFILE_USERNAME_MAX} /> {username !== profile.username ? (
{loadingUsername ? ( ) : usernameAvailable ? ( ) : ( )}
) : null}
setDisplayName(e.target.value)} maxLength={PROFILE_DISPLAY_NAME_MAX} />
{useAzureOpenai ? ( <> {envKeyMap["azure"] ? ( ) : ( setAzureOpenaiAPIKey(e.target.value)} /> )} ) : ( <> {envKeyMap["openai"] ? ( ) : ( setOpenaiAPIKey(e.target.value)} /> )} )}
{useAzureOpenai ? ( <> {
{envKeyMap["azure_openai_endpoint"] ? ( ) : ( <> setAzureOpenaiEndpoint(e.target.value) } /> )}
} {
{envKeyMap["azure_gpt_35_turbo_name"] ? ( ) : ( <> setAzureOpenai35TurboID(e.target.value) } /> )}
} {
{envKeyMap["azure_gpt_45_turbo_name"] ? ( ) : ( <> setAzureOpenai45TurboID(e.target.value) } /> )}
} {
{envKeyMap["azure_gpt_45_vision_name"] ? ( ) : ( <> setAzureOpenai45VisionID(e.target.value) } /> )}
} {
{envKeyMap["azure_embeddings_name"] ? ( ) : ( <> setAzureEmbeddingsID(e.target.value) } /> )}
} ) : ( <>
{envKeyMap["openai_organization_id"] ? ( ) : ( <> setOpenaiOrgID(e.target.value)} /> )}
)}
{envKeyMap["anthropic"] ? ( ) : ( <> setAnthropicAPIKey(e.target.value)} /> )}
{envKeyMap["google"] ? ( ) : ( <> setGoogleGeminiAPIKey(e.target.value)} /> )}
{envKeyMap["mistral"] ? ( ) : ( <> setMistralAPIKey(e.target.value)} /> )}
{envKeyMap["groq"] ? ( ) : ( <> setGroqAPIKey(e.target.value)} /> )}
{envKeyMap["perplexity"] ? ( ) : ( <> setPerplexityAPIKey(e.target.value)} /> )}
{envKeyMap["openrouter"] ? ( ) : ( <> setOpenrouterAPIKey(e.target.value)} /> )}
{t("profile.downloadTooltip")}
} trigger={ } />
) }