"use client"; import styles from "./settings.module.css"; import "intl-tel-input/styles"; import { Suspense, useEffect, useRef, useState } from "react"; import { useToast } from "@/components/ui/use-toast"; import { useUserConfig, ModelOptions, UserConfig, SubscriptionStates } 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 { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; import { CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandDialog, } from "@/components/ui/command"; import { ArrowRight, ChatCircleText, Key, Palette, UserCircle, Trash, Copy, CreditCard, CheckCircle, NotionLogo, GithubLogo, Files, WhatsappLogo, ExclamationMark, Plugs, CloudSlash, Laptop, Plus, FloppyDisk, PlugsConnected, ArrowCircleUp, ArrowCircleDown, ArrowsClockwise, Check, CaretDown, Waveform, } from "@phosphor-icons/react"; import Loading from "../components/loading/loading"; import IntlTelInput from "intl-tel-input/react"; import { uploadDataForIndexing } from "../common/chatFunctions"; import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Progress } from "@/components/ui/progress"; import Link from "next/link"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { AppSidebar } from "../components/appSidebar/appSidebar"; import { Separator } from "@/components/ui/separator"; import { KhojLogoType } from "../components/logo/khojLogo"; const ManageFilesModal: React.FC<{ onClose: () => void }> = ({ onClose }) => { const [syncedFiles, setSyncedFiles] = useState([]); const [selectedFiles, setSelectedFiles] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [isDragAndDropping, setIsDragAndDropping] = useState(false); const [warning, setWarning] = useState(null); const [error, setError] = useState(null); const [uploading, setUploading] = useState(false); const [progressValue, setProgressValue] = useState(0); const [uploadedFiles, setUploadedFiles] = useState([]); const fileInputRef = useRef(null); useEffect(() => { if (!uploading) { setProgressValue(0); } if (uploading) { const interval = setInterval(() => { setProgressValue((prev) => { const increment = Math.floor(Math.random() * 5) + 1; // Generates a random number between 1 and 5 const nextValue = prev + increment; return nextValue < 100 ? nextValue : 100; // Ensures progress does not exceed 100 }); }, 800); return () => clearInterval(interval); } }, [uploading]); useEffect(() => { const fetchFiles = async () => { try { const response = await fetch("/api/content/computer"); if (!response.ok) throw new Error("Failed to fetch files"); // Extract resonse const syncedFiles = await response.json(); // Validate response if (Array.isArray(syncedFiles)) { // Set synced files state setSyncedFiles(syncedFiles.toSorted()); } else { console.error("Unexpected data format from API"); } } catch (error) { console.error("Error fetching files:", error); } }; fetchFiles(); }, [uploadedFiles]); const filteredFiles = syncedFiles.filter((file) => file.toLowerCase().includes(searchQuery.toLowerCase()), ); const deleteSelected = async () => { let filesToDelete = selectedFiles.length > 0 ? selectedFiles : filteredFiles; if (filesToDelete.length === 0) { return; } try { const response = await fetch("/api/content/files", { method: "DELETE", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ files: filesToDelete }), }); if (!response.ok) throw new Error("Failed to delete files"); // Update the syncedFiles state setSyncedFiles((prevFiles) => prevFiles.filter((file) => !filesToDelete.includes(file)), ); // Reset selectedFiles setSelectedFiles([]); } catch (error) { console.error("Error deleting files:", error); } }; const deleteFile = async (filename: string) => { try { const response = await fetch( `/api/content/file?filename=${encodeURIComponent(filename)}`, { method: "DELETE", headers: { "Content-Type": "application/json", }, }, ); if (!response.ok) throw new Error("Failed to delete file"); // Update the syncedFiles state setSyncedFiles((prevFiles) => prevFiles.filter((file) => file !== filename)); // Remove the file from selectedFiles if it's there setSelectedFiles((prevSelected) => prevSelected.filter((file) => file !== filename)); } catch (error) { console.error("Error deleting file:", error); } }; function handleDragOver(event: React.DragEvent) { event.preventDefault(); setIsDragAndDropping(true); } function handleDragLeave(event: React.DragEvent) { event.preventDefault(); setIsDragAndDropping(false); } function handleDragAndDropFiles(event: React.DragEvent) { event.preventDefault(); setIsDragAndDropping(false); if (!event.dataTransfer.files) return; uploadFiles(event.dataTransfer.files); } function openFileInput() { if (fileInputRef && fileInputRef.current) { fileInputRef.current.click(); } } function handleFileChange(event: React.ChangeEvent) { if (!event.target.files) return; uploadFiles(event.target.files); } function uploadFiles(files: FileList) { uploadDataForIndexing(files, setWarning, setUploading, setError, setUploadedFiles); } return ( Alert {warning || error} { setWarning(null); setError(null); setUploading(false); }} > Close
Upload files {uploading && ( )}
{isDragAndDropping ? (
Drop files to upload
) : (
Drag and drop files here
)}
{syncedFiles.length === 0 ? (
No files synced
) : (
Could not find a good match. Need advanced search? Click here.
)}
{filteredFiles.map((filename: string) => ( { setSelectedFiles((prev) => prev.includes(value) ? prev.filter((f) => f !== value) : [...prev, value], ); }} >
{selectedFiles.includes(filename) && ( )} {filename}
))}
); }; interface DropdownComponentProps { items: ModelOptions[]; selected: number; callbackFunc: (value: string) => Promise; } const DropdownComponent: React.FC = ({ items, selected, callbackFunc }) => { const [position, setPosition] = useState(selected?.toString() ?? "0"); return ( !!selected && (
{ setPosition(value); await callbackFunc(value); }} > {items.map((item) => ( {item.name} ))}
) ); }; 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, }; }; enum PhoneNumberValidationState { Setup = "setup", SendOTP = "otp", VerifyOTP = "verify", Verified = "verified", } export default function SettingsView() { const { apiKeys, generateAPIKey, copyAPIKey, deleteAPIKey } = useApiKeys(); const { userConfig: 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 [isManageFilesModalOpen, setIsManageFilesModalOpen] = useState(false); const { toast } = useToast(); const isMobileWidth = useIsMobileWidth(); const title = "Settings"; const cardClassName = "w-full lg:w-1/3 grid grid-flow-column border border-gray-300 shadow-md rounded-lg border dark:border-none dark:bg-muted border-opacity-50"; 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); }, [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 enableFreeTrial = async () => { const formatDate = (dateString: Date) => { const date = new Date(dateString); return new Intl.DateTimeFormat("en-US", { day: "2-digit", month: "short", year: "numeric", }).format(date); }; try { const response = await fetch(`/api/subscription/trial`, { method: "POST", }); if (!response.ok) throw new Error("Failed to enable free trial"); const responseBody = await response.json(); // Set updated user settings if (responseBody.trial_enabled && userConfig) { let newUserConfig = userConfig; newUserConfig.subscription_state = SubscriptionStates.TRIAL; const renewalDate = new Date( Date.now() + userConfig.length_of_free_trial * 24 * 60 * 60 * 1000, ); newUserConfig.subscription_renewal_date = formatDate(renewalDate); newUserConfig.subscription_enabled_trial_at = new Date().toISOString(); setUserConfig(newUserConfig); // Notify user of free trial toast({ title: "🎉 Trial Enabled", description: `Your free trial will end on ${newUserConfig.subscription_renewal_date}`, }); } } catch (error) { console.error("Error enabling free trial:", error); toast({ title: "⚠️ Failed to Enable Free Trial", description: "Failed to enable free trial. 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 = (name: string) => async (id: string) => { if (!userConfig?.is_active) { toast({ title: `Model Update`, description: `You need to be subscribed to update ${name} models`, variant: "destructive", }); return; } try { const response = await fetch(`/api/model/${name}?id=` + id, { method: "POST", headers: { "Content-Type": "application/json", }, }); if (!response.ok) throw new Error("Failed to update model"); toast({ title: `✅ Updated ${toTitleCase(name)} Model`, }); } catch (error) { console.error(`Failed to update ${name} model:`, error); toast({ description: `❌ Failed to update ${toTitleCase(name)} model. Try again.`, variant: "destructive", }); } }; 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 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 (type: string) => { try { const response = await fetch(`/api/content/${type}`, { method: "DELETE", headers: { "Content-Type": "application/json", }, }); if (!response.ok) throw new Error(`Failed to disconnect ${type}`); // Set updated user settings if (userConfig) { let newUserConfig = userConfig; if (type === "computer") { newUserConfig.enabled_content_source.computer = false; } else if (type === "notion") { newUserConfig.enabled_content_source.notion = false; newUserConfig.notion_token = null; setNotionToken(newUserConfig.notion_token); } else if (type === "github") { newUserConfig.enabled_content_source.github = false; } setUserConfig(newUserConfig); } // Notify user about disconnecting content source if (type === "computer") { toast({ title: `✅ Deleted Synced Files`, description: "Your synced documents have been deleted.", }); } else { toast({ title: `✅ Disconnected ${type}`, description: `Your ${type} integration to Khoj has been disconnected.`, }); } } catch (error) { console.error(`Error disconnecting ${type}:`, error); toast({ title: `⚠️ Failed to Disconnect ${type}`, description: `Failed to disconnect from ${type}. 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" />
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 === "expired" && ( <>

Humanist

{(userConfig.subscription_renewal_date && (

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

)) || (

Check{" "} pricing page {" "} to compare plans.

)} ))}
{(userConfig.subscription_state == "subscribed" && ( )) || (userConfig.subscription_state == "unsubscribed" && ( )) || (userConfig.subscription_enabled_trial_at && ( )) || ( )}
{isManageFilesModalOpen && ( setIsManageFilesModalOpen(false)} /> )}
Content
Files {userConfig.enabled_content_source.computer && ( )} Manage your synced files 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 && (

Subscribe to switch model

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

Pick the paint model to generate image responses

{!userConfig.is_active && (

Subscribe to switch model

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

Pick the voice model to generate speech responses

{!userConfig.is_active && (

Subscribe to switch model

)}
)}
Clients
{!userConfig.anonymous_mode && ( API Keys

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

{apiKeys.map((key) => ( {key.name} {`${key.token.slice(0, 6)}...${key.token.slice(-4)}`}
{ 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, ); }} />
))}
)} Chat on Whatsapp {(numberValidationState === PhoneNumberValidationState.Verified && ( )) || (numberValidationState !== PhoneNumberValidationState.Setup && ( ))}

Connect your number to chat with Khoj on WhatsApp. Learn more about the integration{" "} here .

{numberValidationState === PhoneNumberValidationState.VerifyOTP && ( <>

{`Enter the OTP sent to your number: ${phoneNumber}`}

setNumberValidationState( PhoneNumberValidationState.VerifyOTP, ) } > )}
{(numberValidationState === PhoneNumberValidationState.VerifyOTP && ( )) || ( )} {numberValidationState === PhoneNumberValidationState.Verified && ( )}
); }