"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 } from "../common/auth"; import { toTitleCase } 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, SpeakerHigh, UserCircle, FileMagnifyingGlass, 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 SidePanel from "../components/sidePanel/chatHistorySidePanel"; import Loading from "../components/loading/loading"; import IntlTelInput from "intl-tel-input/react"; const ManageFilesModal: React.FC<{ onClose: () => void }> = ({ onClose }) => { const [syncedFiles, setSyncedFiles] = useState([]); const [selectedFiles, setSelectedFiles] = useState([]); const [searchQuery, setSearchQuery] = useState(""); 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(); }, []); const filteredFiles = syncedFiles.filter((file) => file.toLowerCase().includes(searchQuery.toLowerCase()), ); const deleteSelected = async () => { let filesToDelete = selectedFiles.length > 0 ? selectedFiles : filteredFiles; console.log("Delete selected files", filesToDelete); if (filesToDelete.length === 0) { console.log("No files to delete"); 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([]); console.log("Deleted files:", filesToDelete); } catch (error) { console.error("Error deleting files:", error); } }; const deleteFile = async (filename: string) => { console.log("Delete selected file", filename); 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)); console.log("Deleted file:", filename); } catch (error) { console.error("Error deleting file:", error); } }; return (
No such files synced. {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 [title, setTitle] = useState("Settings"); const [isMobileWidth, setIsMobileWidth] = useState(false); 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 cardClassName = "w-full lg:w-1/3 grid grid-flow-column border border-gray-300 shadow-md rounded-lg bg-gradient-to-b from-background to-gray-50 dark:to-gray-950"; 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]); useEffect(() => { setIsMobileWidth(window.innerWidth < 786); const handleResize = () => setIsMobileWidth(window.innerWidth < 786); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); 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?email=${userConfig?.username}&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" ? "unsubscribed" : "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 = (name: string) => async (id: string) => { if (!userConfig?.is_active && name !== "search") 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 (
{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 14 day trial of the Khoj Futurist plan. 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" && ( <>

Free Plan

{(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" && ( )) || ( )}
{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.search_model_options.length > 0 && ( Search

Pick the search model to find your documents

)} {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 && ( )}
); }