"use client"; import styles from "./settings.module.css"; import "intl-tel-input/styles"; import { Suspense, useEffect, useState } from "react"; import { useToast } from "@/components/ui/use-toast"; import { useUserConfig, ModelOptions, UserConfig, SubscriptionStates, isUserSubscribed } from "../common/auth"; import { toTitleCase, useIsMobileWidth } from "../common/utils"; import { isValidPhoneNumber } from "libphonenumber-js"; import { Button } from "@/components/ui/button"; import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; import { Switch } from "@/components/ui/switch"; import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; import { ArrowRight, ChatCircleText, Key, Palette, UserCircle, Trash, Copy, CreditCard, CheckCircle, NotionLogo, GithubLogo, Files, WhatsappLogo, ExclamationMark, Plugs, CloudSlash, Laptop, Plus, FloppyDisk, PlugsConnected, ArrowCircleUp, ArrowCircleDown, ArrowsClockwise, CaretDown, Waveform, MagnifyingGlass, Brain, EyeSlash, Eye, Download, TrashSimple, } from "@phosphor-icons/react"; import Loading from "../components/loading/loading"; import IntlTelInput from "intl-tel-input/react"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { AppSidebar } from "../components/appSidebar/appSidebar"; import { UserMemory, UserMemorySchema } from "../components/userMemory/userMemory"; import { Separator } from "@/components/ui/separator"; import { KhojLogoType } from "../components/logo/khojLogo"; import { Progress } from "@/components/ui/progress"; import JSZip from "jszip"; import { saveAs } from "file-saver"; interface DropdownComponentProps { items: ModelOptions[]; selected: number; isActive?: boolean; callbackFunc: (value: string) => Promise; } const DropdownComponent: React.FC = ({ items, selected, isActive, callbackFunc, }) => { const [position, setPosition] = useState(selected?.toString() ?? "0"); return ( !!selected && (
{ setPosition(value); await callbackFunc(value); }} > {items.map((item) => ( {item.name}{" "} {item.tier === "standard" && ( (Futurist) )} ))}
) ); }; interface TokenObject { token: string; name: string; } const useApiKeys = () => { const [apiKeys, setApiKeys] = useState([]); const { toast } = useToast(); const generateAPIKey = async () => { try { const response = await fetch(`/auth/token`, { method: "POST", headers: { "Content-Type": "application/json", }, }); const tokenObj = await response.json(); setApiKeys((prevKeys) => [...prevKeys, tokenObj]); } catch (error) { console.error("Error generating API key:", error); } }; const copyAPIKey = async (token: string) => { try { await navigator.clipboard.writeText(token); toast({ title: "🔑 API Key", description: "Copied to clipboard", }); } catch (error) { console.error("Error copying API key:", error); } }; const deleteAPIKey = async (token: string) => { try { const response = await fetch(`/auth/token?token=${token}`, { method: "DELETE" }); if (response.ok) { setApiKeys((prevKeys) => prevKeys.filter((key) => key.token !== token)); } } catch (error) { console.error("Error deleting API key:", error); } }; const listApiKeys = async () => { try { const response = await fetch(`/auth/token`); const tokens = await response.json(); if (tokens?.length > 0) { setApiKeys(tokens); } } catch (error) { console.error("Error listing API keys:", error); } }; useEffect(() => { listApiKeys(); }, []); return { apiKeys, generateAPIKey, copyAPIKey, deleteAPIKey, }; }; function ApiKeyCard() { const { apiKeys, generateAPIKey, copyAPIKey, deleteAPIKey } = useApiKeys(); const [visibleApiKeys, setVisibleApiKeys] = useState>(new Set()); const { toast } = useToast(); return ( API Keys

Access Khoj from the{" "} Desktop , Obsidian,{" "} Emacs apps and more.

{apiKeys.map((key) => ( {key.name} {visibleApiKeys.has(key.token) ? key.token : `${key.token.slice(0, 6)}...${key.token.slice(-4)}`}
{visibleApiKeys.has(key.token) ? ( setVisibleApiKeys((prev) => { const next = new Set(prev); next.delete(key.token); return next; }) } /> ) : ( setVisibleApiKeys( new Set([...visibleApiKeys, key.token]), ) } /> )} { toast({ title: `🔑 Copied API Key: ${key.name}`, description: `Set this API key in the Khoj apps you want to connect to this Khoj account`, }); copyAPIKey(key.token); }} /> { toast({ title: `🔑 Deleted API Key: ${key.name}`, description: `Apps using this API key will no longer connect to this Khoj account`, }); deleteAPIKey(key.token); }} />
))}
); } enum PhoneNumberValidationState { Setup = "setup", SendOTP = "otp", VerifyOTP = "verify", Verified = "verified", } export default function SettingsView() { const { data: initialUserConfig } = useUserConfig(true); const [userConfig, setUserConfig] = useState(null); const [name, setName] = useState(undefined); const [notionToken, setNotionToken] = useState(null); const [phoneNumber, setPhoneNumber] = useState(undefined); const [otp, setOTP] = useState(""); const [numberValidationState, setNumberValidationState] = useState( PhoneNumberValidationState.Verified, ); const [memories, setMemories] = useState([]); const [enableMemory, setEnableMemory] = useState(true); const [serverMemoryMode, setServerMemoryMode] = useState("enabled_default_on"); const [isExporting, setIsExporting] = useState(false); const [exportProgress, setExportProgress] = useState(0); const [exportedConversations, setExportedConversations] = useState(0); const [totalConversations, setTotalConversations] = useState(0); const { toast } = useToast(); const isMobileWidth = useIsMobileWidth(); const title = "Settings"; const cardClassName = "w-full lg:w-5/12 grid grid-flow-column border border-gray-300 shadow-md rounded-lg border dark:border-none border-opacity-50 dark:bg-muted"; useEffect(() => { setUserConfig(initialUserConfig); setPhoneNumber(initialUserConfig?.phone_number); setNumberValidationState( initialUserConfig?.is_phone_number_verified ? PhoneNumberValidationState.Verified : initialUserConfig?.phone_number ? PhoneNumberValidationState.SendOTP : PhoneNumberValidationState.Setup, ); setName(initialUserConfig?.given_name); setNotionToken(initialUserConfig?.notion_token ?? null); setEnableMemory(initialUserConfig?.enable_memory ?? true); setServerMemoryMode(initialUserConfig?.server_memory_mode ?? "enabled_default_on"); }, [initialUserConfig]); const sendOTP = async () => { try { const response = await fetch(`/api/phone?phone_number=${phoneNumber}`, { method: "POST", headers: { "Content-Type": "application/json", }, }); if (!response.ok) throw new Error("Failed to send OTP"); setNumberValidationState(PhoneNumberValidationState.VerifyOTP); } catch (error) { console.error("Error sending OTP:", error); toast({ title: "📱 Phone", description: "Failed to send OTP. Try again or contact us at team@khoj.dev", }); } }; const verifyOTP = async () => { try { const response = await fetch(`/api/phone/verify?code=${otp}`, { method: "POST", headers: { "Content-Type": "application/json", }, }); if (!response.ok) throw new Error("Failed to verify OTP"); setNumberValidationState(PhoneNumberValidationState.Verified); toast({ title: "📱 Phone", description: "Phone number verified", }); } catch (error) { console.error("Error verifying OTP:", error); toast({ title: "📱 Phone", description: "Failed to verify OTP. Try again or contact us at team@khoj.dev", }); } }; const disconnectNumber = async () => { try { const response = await fetch(`/api/phone`, { method: "DELETE", headers: { "Content-Type": "application/json", }, }); if (!response.ok) throw new Error("Failed to disconnect phone number"); setPhoneNumber(undefined); setNumberValidationState(PhoneNumberValidationState.Setup); toast({ title: "📱 Phone", description: "Phone number disconnected", }); } catch (error) { console.error("Error disconnecting phone number:", error); toast({ title: "📱 Phone", description: "Failed to disconnect phone number. Try again or contact us at team@khoj.dev", }); } }; const setSubscription = async (state: string) => { try { const url = `/api/subscription?operation=${state}`; const response = await fetch(url, { method: "PATCH", headers: { "Content-Type": "application/json", }, }); if (!response.ok) throw new Error("Failed to change subscription"); // Set updated user settings if (userConfig) { let newUserConfig = userConfig; newUserConfig.subscription_state = state === "cancel" ? SubscriptionStates.UNSUBSCRIBED : SubscriptionStates.SUBSCRIBED; setUserConfig(newUserConfig); } // Notify user of subscription change toast({ title: "💳 Subscription", description: userConfig?.subscription_state === "unsubscribed" ? "Your subscription was cancelled" : "Your Futurist subscription has been renewed", }); } catch (error) { console.error("Error changing subscription:", error); toast({ title: "💳 Subscription", description: state === "cancel" ? "Failed to cancel subscription. Try again or contact us at team@khoj.dev" : "Failed to renew subscription. Try again or contact us at team@khoj.dev", }); } }; const saveName = async () => { if (!name) return; try { const response = await fetch(`/api/user/name?name=${name}`, { method: "PATCH", headers: { "Content-Type": "application/json", }, }); if (!response.ok) throw new Error("Failed to update name"); // Set updated user settings if (userConfig) { let newUserConfig = userConfig; newUserConfig.given_name = name; setUserConfig(newUserConfig); } // Notify user of name change toast({ title: `✅ Updated Profile`, description: `You name has been updated to ${name}`, }); } catch (error) { console.error("Error updating name:", error); toast({ title: "⚠️ Failed to Update Profile", description: "Failed to update name. Try again or contact team@khoj.dev", }); } }; const updateModel = (modelType: string) => async (id: string) => { // Get the selected model from the options const modelOptions = modelType === "chat" ? userConfig?.chat_model_options : modelType === "paint" ? userConfig?.paint_model_options : userConfig?.voice_model_options; const selectedModel = modelOptions?.find((model) => model.id.toString() === id); const modelName = selectedModel?.name; // Check if the model is free tier or if the user is active if (!userConfig?.is_active && selectedModel?.tier !== "free") { toast({ title: `Model Update`, description: `Subscribe to switch ${modelType} model to ${modelName}.`, variant: "destructive", }); return; } try { const response = await fetch(`/api/model/${modelType}?id=` + id, { method: "POST", headers: { "Content-Type": "application/json", }, }); if (!response.ok) throw new Error(`Failed to switch ${modelType} model to ${modelName}`); toast({ title: `✅ Switched ${modelType} model to ${modelName}`, }); } catch (error) { console.error(`Failed to update ${modelType} model to ${modelName}:`, error); toast({ description: `❌ Failed to switch ${modelType} model to ${modelName}. Try again.`, variant: "destructive", }); } }; const exportChats = async () => { try { setIsExporting(true); // Get total conversation count const statsResponse = await fetch("/api/chat/stats"); const stats = await statsResponse.json(); const total = stats.num_conversations; setTotalConversations(total); // Create zip file const zip = new JSZip(); const conversations = []; // Fetch all conversations in batches of 10 for (let page = 0; page * 10 < total; page++) { const response = await fetch(`/api/chat/export?page=${page}`); const data = await response.json(); conversations.push(...data); setExportedConversations((page + 1) * 10); setExportProgress((((page + 1) * 10) / total) * 100); } // Add conversations to zip zip.file("conversations.json", JSON.stringify(conversations, null, 2)); // Generate and download zip const content = await zip.generateAsync({ type: "blob" }); saveAs(content, "khoj-conversations.zip"); toast({ title: "Export Complete", description: `Successfully exported ${conversations.length} conversations`, }); } catch (error) { console.error("Error exporting chats:", error); toast({ title: "Export Failed", description: "Failed to export chats. Please try again.", variant: "destructive", }); } finally { setIsExporting(false); setExportProgress(0); setExportedConversations(0); setTotalConversations(0); } }; const saveNotionToken = async () => { if (!notionToken) return; // Save Notion API key to server try { const response = await fetch(`/api/content/notion`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ token: notionToken }), }); if (!response.ok) throw new Error("Failed to save Notion API key"); // Set updated user settings if (userConfig) { let newUserConfig = userConfig; newUserConfig.notion_token = notionToken; setUserConfig(newUserConfig); } // Notify user of Notion API key save toast({ title: `✅ Saved Notion Settings`, description: `You Notion API key has been saved.`, }); } catch (error) { console.error("Error updating name:", error); toast({ title: "⚠️ Failed to Save Notion Settings", description: "Failed to save Notion API key. Try again or contact team@khoj.dev", }); } }; const fetchMemories = async () => { try { console.log("Fetching memories..."); const response = await fetch('/api/memories'); if (!response.ok) throw new Error('Failed to fetch memories'); const data = await response.json(); setMemories(data); } catch (error) { console.error('Error fetching memories:', error); toast({ title: "Error", description: "Failed to fetch memories. Please try again.", variant: "destructive" }); } }; const handleDeleteMemory = async (id: number) => { try { const response = await fetch(`/api/memories/${id}`, { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to delete memory'); setMemories(memories.filter(memory => memory.id !== id)); } catch (error) { console.error('Error deleting memory:', error); toast({ title: "Error", description: "Failed to delete memory. Please try again.", variant: "destructive" }); } }; const handleUpdateMemory = async (id: number, raw: string) => { try { const response = await fetch(`/api/memories/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ raw, memory_id: id }), }); if (!response.ok) throw new Error('Failed to update memory'); const updatedMemory: UserMemorySchema = await response.json(); setMemories(memories.map(memory => memory.id === id ? updatedMemory : memory )); } catch (error) { console.error('Error updating memory:', error); toast({ title: "Error", description: "Failed to update memory. Please try again.", variant: "destructive" }); } }; const handleToggleMemory = async (enabled: boolean) => { try { const response = await fetch(`/api/user/memory?enable_memory=${enabled}`, { method: 'PATCH', }); if (!response.ok) throw new Error('Failed to update memory setting'); setEnableMemory(enabled); toast({ title: enabled ? "Memory enabled" : "Memory disabled", description: enabled ? "Khoj will learn and remember from your conversations." : "Khoj will no longer learn or remember from your conversations.", }); } catch (error) { console.error('Error toggling memory:', error); toast({ title: "Error", description: "Failed to update memory setting. Please try again.", variant: "destructive" }); } }; const syncContent = async (type: string) => { try { const response = await fetch(`/api/content?t=${type}`, { method: "PATCH", headers: { "Content-Type": "application/json", }, }); if (!response.ok) throw new Error(`Failed to sync content from ${type}`); toast({ title: `🔄 Syncing ${type}`, description: `Your ${type} content is being synced.`, }); } catch (error) { console.error("Error syncing content:", error); toast({ title: `⚠️ Failed to Sync ${type}`, description: `Failed to sync ${type} content. Try again or contact team@khoj.dev`, }); } }; const disconnectContent = async (source: string) => { try { const response = await fetch(`/api/content/source/${source}`, { method: "DELETE", headers: { "Content-Type": "application/json", }, }); if (!response.ok) throw new Error(`Failed to disconnect ${source}`); // Set updated user settings if (userConfig) { let newUserConfig = userConfig; if (source === "computer") { newUserConfig.enabled_content_source.computer = false; } else if (source === "notion") { newUserConfig.enabled_content_source.notion = false; newUserConfig.notion_token = null; setNotionToken(newUserConfig.notion_token); } else if (source === "github") { newUserConfig.enabled_content_source.github = false; } setUserConfig(newUserConfig); } // Notify user about disconnecting content source if (source === "computer") { toast({ title: `✅ Deleted Synced Files`, description: "Your synced documents have been deleted.", }); } else { toast({ title: `✅ Disconnected ${source}`, description: `Your ${source} integration to Khoj has been disconnected.`, }); } } catch (error) { console.error(`Error disconnecting ${source}:`, error); toast({ title: `⚠️ Failed to Disconnect ${source}`, description: `Failed to disconnect from ${source}. Try again or contact team@khoj.dev`, }); } }; if (!userConfig) return ; return (
{isMobileWidth ? ( ) : (

Settings

)}
{title}
}>
Profile
Name

What should Khoj refer to you as?

setName(e.target.value)} value={name || ""} className="w-full border border-gray-300 rounded-lg p-4 py-6" />
{isUserSubscribed(userConfig) && ( Subscription

Current Plan

{(userConfig.subscription_state === "trial" && ( <>

Futurist (Trial)

You are on a{" "} {userConfig.length_of_free_trial}{" "} day trial of the Khoj Futurist plan. Your trial ends on{" "} { userConfig.subscription_renewal_date } . Check{" "} pricing page {" "} to compare plans.

)) || (userConfig.subscription_state === "subscribed" && ( <>

Futurist

Subscription renews on{" "} { userConfig.subscription_renewal_date }

)) || (userConfig.subscription_state === "unsubscribed" && ( <>

Futurist

Subscription ends on{" "} { userConfig.subscription_renewal_date }

))}
{(userConfig.subscription_state == "subscribed" && ( )) || (userConfig.subscription_state == "unsubscribed" && ( )) || ( )}
)}
Content
Knowledge Base {userConfig.enabled_content_source.computer && ( )} Manage and search through your digital brain. Github Set Github repositories to index Notion {userConfig.enabled_content_source.notion && ( )}

Sync your Notion workspace.

{!userConfig.notion_oauth_url && ( setNotionToken(e.target.value) } value={notionToken || ""} placeholder="Enter API Key of your Khoj integration on Notion" className="w-full border border-gray-300 rounded-lg px-4 py-6" /> )}
{ /* Show connect to notion button if notion oauth url setup and user disconnected*/ userConfig.notion_oauth_url && !userConfig.enabled_content_source .notion ? ( ) : /* Show sync button if user connected to notion and API key unchanged */ userConfig.enabled_content_source.notion && notionToken === userConfig.notion_token ? ( ) : /* Show set API key button notion oauth url not set setup */ !userConfig.notion_oauth_url ? ( ) : ( <> ) }
Models
{userConfig.chat_model_options.length > 0 && ( Chat

Pick the chat model to generate text responses

{!userConfig.is_active && (

{userConfig.chat_model_options.some( (model) => model.tier === "free", ) ? "Free models available" : "Subscribe to switch model"}

)}
)} {userConfig.paint_model_options.length > 0 && ( Paint

Pick the paint model to generate image responses

{!userConfig.is_active && (

{userConfig.paint_model_options.some( (model) => model.tier === "free", ) ? "Free models available" : "Subscribe to switch model"}

)}
)} {userConfig.voice_model_options.length > 0 && ( Voice

Pick the voice model to generate speech responses

{!userConfig.is_active && (

{userConfig.voice_model_options.some( (model) => model.tier === "free", ) ? "Free models available" : "Subscribe to switch model"}

)}
)}
Clients
{!userConfig.anonymous_mode && }
Account
Export Data

Download all your chat conversations

{exportProgress > 0 && (

Exported {exportedConversations} of{" "} {totalConversations} conversations

)}
Memories

View and manage your long-term memories

handleToggleMemory(checked)} disabled={serverMemoryMode === "disabled"} />
{serverMemoryMode === "disabled" && (

Memory has been disabled by the server administrator.

)}
open && fetchMemories()}> Your Memories
{memories.map((memory) => ( ))} {memories.length === 0 && (

No memories found

)}
Delete Account

This will delete all your account data, including conversations, agents, and any assets you{"'"}ve generated. Be sure to export before you do this if you want to keep your information.

Are you absolutely sure? This action is irreversible. This will permanently delete your account and remove all your data from our servers. Cancel { try { const response = await fetch( "/api/self", { method: "DELETE", }, ); if (!response.ok) throw new Error( "Failed to delete account", ); toast({ title: "Account Deleted", description: "Your account has been successfully deleted.", }); // Redirect to home page after successful deletion window.location.href = "/"; } catch (error) { console.error( "Error deleting account:", error, ); toast({ title: "Error", description: "Failed to delete account. Please try again or contact support.", variant: "destructive", }); } }} > Delete Account
); }