843 lines
28 KiB
TypeScript
843 lines
28 KiB
TypeScript
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<ProfileSettingsProps> = ({}) => {
|
||
|
||
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<HTMLButtonElement>(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<File | null>(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!")
|
||
|
||
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<HTMLDivElement>) => {
|
||
if (e.key === "Enter") {
|
||
buttonRef.current?.click()
|
||
}
|
||
}
|
||
|
||
if (!profile) return null
|
||
|
||
return (
|
||
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
||
<SheetTrigger asChild>
|
||
{profile.image_url ? (
|
||
<Image
|
||
className="mt-2 size-[34px] cursor-pointer rounded hover:opacity-50"
|
||
src={profile.image_url + "?" + new Date().getTime()}
|
||
height={34}
|
||
width={34}
|
||
alt={t("profile.imageAlt")}
|
||
//alt={"Image"}
|
||
/>
|
||
) : (
|
||
<Button size="icon" variant="ghost">
|
||
<IconUser size={SIDEBAR_ICON_SIZE} />
|
||
</Button>
|
||
)}
|
||
</SheetTrigger>
|
||
|
||
<SheetContent
|
||
className="flex flex-col justify-between"
|
||
side="left"
|
||
onKeyDown={handleKeyDown}
|
||
>
|
||
<div className="grow overflow-auto">
|
||
<SheetHeader>
|
||
<SheetTitle className="flex items-center justify-between space-x-2">
|
||
<div>{t("profile.settingsTitle")}</div>
|
||
|
||
<Button
|
||
tabIndex={-1}
|
||
className="text-xs"
|
||
size="sm"
|
||
onClick={handleSignOut}
|
||
>
|
||
<IconLogout className="mr-1" size={20} />
|
||
{t("profile.logout")}
|
||
</Button>
|
||
</SheetTitle>
|
||
</SheetHeader>
|
||
|
||
<Tabs defaultValue="profile">
|
||
<TabsList className="mt-4 grid w-full grid-cols-2">
|
||
<TabsTrigger value="profile">{t("profile.profileTab")}</TabsTrigger>
|
||
<TabsTrigger value="keys">{t("profile.apiKeysTab")}</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent className="mt-4 space-y-4" value="profile">
|
||
<div className="space-y-1">
|
||
<div className="flex items-center space-x-2">
|
||
<Label>{t("profile.usernameLabel")}</Label>
|
||
|
||
<div className="text-xs">
|
||
{username !== profile.username ? (
|
||
usernameAvailable ? (
|
||
<div className="text-green-500">{t("profile.available")}</div>
|
||
) : (
|
||
<div className="text-red-500">{t("profile.unavailable")}</div>
|
||
)
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="relative">
|
||
<Input
|
||
className="pr-10"
|
||
placeholder={t("profile.usernamePlaceholder")}
|
||
value={username}
|
||
onChange={e => {
|
||
setUsername(e.target.value)
|
||
checkUsernameAvailability(e.target.value)
|
||
}}
|
||
minLength={PROFILE_USERNAME_MIN}
|
||
maxLength={PROFILE_USERNAME_MAX}
|
||
/>
|
||
|
||
{username !== profile.username ? (
|
||
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
|
||
{loadingUsername ? (
|
||
<IconLoader2 className="animate-spin" />
|
||
) : usernameAvailable ? (
|
||
<IconCircleCheckFilled className="text-green-500" />
|
||
) : (
|
||
<IconCircleXFilled className="text-red-500" />
|
||
)}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
<LimitDisplay
|
||
used={username.length}
|
||
limit={PROFILE_USERNAME_MAX}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<Label>{t("profile.profileImageLabel")}</Label>
|
||
|
||
<ImagePicker
|
||
src={profileImageSrc}
|
||
image={profileImageFile}
|
||
height={50}
|
||
width={50}
|
||
onSrcChange={setProfileImageSrc}
|
||
onImageChange={setProfileImageFile}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<Label>{t("profile.chatDisplayName")}</Label>
|
||
|
||
<Input
|
||
placeholder={t("profile.chatDisplayNamePlaceholder")}
|
||
value={displayName}
|
||
onChange={e => setDisplayName(e.target.value)}
|
||
maxLength={PROFILE_DISPLAY_NAME_MAX}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<Label className="text-sm">
|
||
{t("profile.instructionsLabel")}
|
||
</Label>
|
||
|
||
<TextareaAutosize
|
||
value={profileInstructions}
|
||
onValueChange={setProfileInstructions}
|
||
placeholder={t("profile.instructionsPlaceholder")}
|
||
minRows={6}
|
||
maxRows={10}
|
||
/>
|
||
|
||
<LimitDisplay
|
||
used={profileInstructions.length}
|
||
limit={PROFILE_CONTEXT_MAX}
|
||
/>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent className="mt-4 space-y-4" value="keys">
|
||
<div className="mt-5 space-y-2">
|
||
<Label className="flex items-center">
|
||
{useAzureOpenai
|
||
? envKeyMap["azure"]
|
||
? ""
|
||
: t("setup.azureOpenaiApiKey")
|
||
: envKeyMap["openai"]
|
||
? ""
|
||
: t("setup.openaiApiKey")}
|
||
|
||
<Button
|
||
className={cn(
|
||
"h-[18px] w-[150px] text-[11px]",
|
||
(useAzureOpenai && !envKeyMap["azure"]) ||
|
||
(!useAzureOpenai && !envKeyMap["openai"])
|
||
? "ml-3"
|
||
: "mb-3"
|
||
)}
|
||
onClick={() => setUseAzureOpenai(!useAzureOpenai)}
|
||
>
|
||
{useAzureOpenai
|
||
? t("profile.switchToStandardOpenAI")
|
||
: t("profile.switchToAzureOpenAI")}
|
||
</Button>
|
||
</Label>
|
||
|
||
{useAzureOpenai ? (
|
||
<>
|
||
{envKeyMap["azure"] ? (
|
||
<Label>{t("profile.azureOpenAIKeySetByAdmin")}</Label>
|
||
) : (
|
||
<Input
|
||
placeholder={t("setup.azureOpenaiApiKey")}
|
||
type="password"
|
||
value={azureOpenaiAPIKey}
|
||
onChange={e => setAzureOpenaiAPIKey(e.target.value)}
|
||
/>
|
||
)}
|
||
</>
|
||
) : (
|
||
<>
|
||
{envKeyMap["openai"] ? (
|
||
<Label>{t("profile.openAIAPIKeySetByAdmin")}</Label>
|
||
) : (
|
||
<Input
|
||
placeholder={t("setup.openaiApiKey")}
|
||
type="password"
|
||
value={openaiAPIKey}
|
||
onChange={e => setOpenaiAPIKey(e.target.value)}
|
||
/>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="ml-8 space-y-3">
|
||
{useAzureOpenai ? (
|
||
<>
|
||
{
|
||
<div className="space-y-1">
|
||
{envKeyMap["azure_openai_endpoint"] ? (
|
||
<Label className="text-xs">
|
||
<Label>{t("profile.azureEndpointSetByAdmin")}</Label>
|
||
</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.azureEndpointLabel")}</Label>
|
||
|
||
<Input
|
||
placeholder={t("profile.azureEndpointPlaceholder")}
|
||
value={azureOpenaiEndpoint}
|
||
onChange={e =>
|
||
setAzureOpenaiEndpoint(e.target.value)
|
||
}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
}
|
||
|
||
{
|
||
<div className="space-y-1">
|
||
{envKeyMap["azure_gpt_35_turbo_name"] ? (
|
||
<Label className="text-xs">
|
||
{t("profile.azureGpt35TurboDeploymentNameSetByAdmin")}
|
||
</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.azureGpt35TurboDeploymentName")}</Label>
|
||
|
||
<Input
|
||
placeholder={t("profile.azureGpt35TurboDeploymentNamePlaceholder")}
|
||
value={azureOpenai35TurboID}
|
||
onChange={e =>
|
||
setAzureOpenai35TurboID(e.target.value)
|
||
}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
}
|
||
|
||
{
|
||
<div className="space-y-1">
|
||
{envKeyMap["azure_gpt_45_turbo_name"] ? (
|
||
<Label className="text-xs">
|
||
{t("profile.azureGpt45TurboDeploymentNameSetByAdmin")}
|
||
</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.azureGpt45TurboDeploymentName")}</Label>
|
||
|
||
<Input
|
||
placeholder={t("profile.azureGpt45TurboDeploymentNamePlaceholder")}
|
||
value={azureOpenai45TurboID}
|
||
onChange={e =>
|
||
setAzureOpenai45TurboID(e.target.value)
|
||
}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
}
|
||
|
||
{
|
||
<div className="space-y-1">
|
||
{envKeyMap["azure_gpt_45_vision_name"] ? (
|
||
<Label className="text-xs">
|
||
{t("profile.azureGpt45VisionDeploymentNameSetByAdmin")}
|
||
</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.azureGpt45VisionDeploymentName")}</Label>
|
||
|
||
<Input
|
||
placeholder={t("profile.azureGpt45VisionDeploymentNamePlaceholder")}
|
||
value={azureOpenai45VisionID}
|
||
onChange={e =>
|
||
setAzureOpenai45VisionID(e.target.value)
|
||
}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
}
|
||
|
||
{
|
||
<div className="space-y-1">
|
||
{envKeyMap["azure_embeddings_name"] ? (
|
||
<Label className="text-xs">
|
||
{t("profile.azureEmbeddingsDeploymentNameSetByAdmin")}
|
||
</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.azureEmbeddingsDeploymentName")}</Label>
|
||
|
||
<Input
|
||
placeholder={t("profile.azureEmbeddingsDeploymentNamePlaceholder")}
|
||
value={azureEmbeddingsID}
|
||
onChange={e =>
|
||
setAzureEmbeddingsID(e.target.value)
|
||
}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
}
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className="space-y-1">
|
||
{envKeyMap["openai_organization_id"] ? (
|
||
<Label className="text-xs">
|
||
{t("profile.openaiOrgIdSetByAdmin")}
|
||
</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.openaiOrgIdLabel")}</Label>
|
||
|
||
<Input
|
||
placeholder={t("profile.openaiOrgIdPlaceholder")}
|
||
disabled={
|
||
!!process.env.NEXT_PUBLIC_OPENAI_ORGANIZATION_ID
|
||
}
|
||
type="password"
|
||
value={openaiOrgID}
|
||
onChange={e => setOpenaiOrgID(e.target.value)}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
{envKeyMap["anthropic"] ? (
|
||
<Label>{t("profile.anthropicApiKeySetByAdmin")}</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.anthropicApiKeyLabel")}</Label>
|
||
<Input
|
||
placeholder={t("profile.anthropicApiKeyPlaceholder")}
|
||
type="password"
|
||
value={anthropicAPIKey}
|
||
onChange={e => setAnthropicAPIKey(e.target.value)}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
{envKeyMap["google"] ? (
|
||
<Label>{t("profile.geminiAPIKeySetByAdmin")}</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.googleGeminiApiKeyLabel")}</Label>
|
||
<Input
|
||
placeholder={t("profile.googleGeminiApiKeyPlaceholder")}
|
||
type="password"
|
||
value={googleGeminiAPIKey}
|
||
onChange={e => setGoogleGeminiAPIKey(e.target.value)}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
{envKeyMap["mistral"] ? (
|
||
<Label>{t("profile.mistralAPIKeySetByAdmin")}</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.mistralApiKeyLabel")}</Label>
|
||
<Input
|
||
placeholder={t("profile.mistralApiKeyPlaceholder")}
|
||
type="password"
|
||
value={mistralAPIKey}
|
||
onChange={e => setMistralAPIKey(e.target.value)}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
{envKeyMap["groq"] ? (
|
||
<Label>{t("profile.groqAPIKeySetByAdmin")}</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.groqApiKeyLabel")}</Label>
|
||
<Input
|
||
placeholder={t("profile.groqApiKeyPlaceholder")}
|
||
type="password"
|
||
value={groqAPIKey}
|
||
onChange={e => setGroqAPIKey(e.target.value)}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
{envKeyMap["perplexity"] ? (
|
||
<Label>{t("profile.perplexityAPIKeySetByAdmin")}</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.perplexityApiKeyLabel")}</Label>
|
||
<Input
|
||
placeholder={t("profile.perplexityApiKeyPlaceholder")}
|
||
type="password"
|
||
value={perplexityAPIKey}
|
||
onChange={e => setPerplexityAPIKey(e.target.value)}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
{envKeyMap["openrouter"] ? (
|
||
<Label>{t("profile.openRouterAPIKeySetByAdmin")}</Label>
|
||
) : (
|
||
<>
|
||
<Label>{t("profile.openRouterApiKeyLabel")}</Label>
|
||
<Input
|
||
placeholder={t("profile.openRouterApiKeyPlaceholder")}
|
||
type="password"
|
||
value={openrouterAPIKey}
|
||
onChange={e => setOpenrouterAPIKey(e.target.value)}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
|
||
<div className="mt-6 flex items-center">
|
||
<div className="flex items-center space-x-1">
|
||
<ThemeSwitcher />
|
||
|
||
<WithTooltip
|
||
display={
|
||
<div>
|
||
{t("profile.downloadTooltip")}
|
||
</div>
|
||
}
|
||
trigger={
|
||
<IconFileDownload
|
||
className="cursor-pointer hover:opacity-50"
|
||
size={32}
|
||
onClick={exportLocalStorageAsJSON}
|
||
/>
|
||
}
|
||
/>
|
||
</div>
|
||
|
||
<div className="ml-auto space-x-2">
|
||
<Button variant="ghost" onClick={() => setIsOpen(false)}>
|
||
{t("profile.cancel")} {/* 取消按钮 */}
|
||
</Button>
|
||
|
||
<Button ref={buttonRef} onClick={handleSave}>
|
||
{t("profile.save")} {/* 保存按钮 */}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</SheetContent>
|
||
</Sheet>
|
||
)
|
||
}
|