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" interface ProfileSettingsProps {} export const ProfileSettings: FC = ({}) => { const { profile, setProfile, envKeyMap, setAvailableHostedModels, setAvailableOpenRouterModels, availableOpenRouterModels } = useContext(ChatbotUIContext) const router = useRouter() 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() router.push("/login") 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) => { if (e.key === "Enter") { buttonRef.current?.click() } } if (!profile) return null return ( {profile.image_url ? ( {"Image"} ) : ( )}
User Settings
Profile API Keys
{username !== profile.username ? ( usernameAvailable ? (
AVAILABLE
) : (
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)} /> )}
Download Chatbot UI 1.0 data as JSON. Import coming soon!
} trigger={ } />
) }