'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, } from "@phosphor-icons/react"; import NavMenu from "../components/navMenu/navMenu"; 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; } export 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 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"; 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 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 Manage your synced files Github Set Github repositories to index Notion

Sync your Notion pages. See the setup instructions

{!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
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)}`}
copyAPIKey(key.token)}/> 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 && ( ) || ( )}
); }