2024-07-25 05:06:30 +00:00
'use client'
2024-07-16 14:36:50 +00:00
import styles from "./settings.module.css" ;
2024-08-01 11:14:17 +00:00
import "intl-tel-input/styles" ;
2024-07-16 14:36:50 +00:00
import { Suspense , useEffect , useState } from "react" ;
2024-07-18 00:07:19 +00:00
import { useToast } from "@/components/ui/use-toast"
2024-07-16 14:36:50 +00:00
2024-07-25 05:06:30 +00:00
import { useUserConfig , ModelOptions , UserConfig } from "../common/auth" ;
2024-07-30 12:47:45 +00:00
import { toTitleCase } from "../common/utils" ;
2024-07-25 10:25:33 +00:00
import { isValidPhoneNumber } from 'libphonenumber-js' ;
2024-07-16 14:36:50 +00:00
import { Button } from "@/components/ui/button" ;
2024-07-26 09:08:45 +00:00
import { InputOTP , InputOTPGroup , InputOTPSlot } from "@/components/ui/input-otp" ;
import { Input } from "@/components/ui/input" ;
2024-07-16 14:36:50 +00:00
import {
Card ,
CardContent ,
CardFooter ,
CardHeader ,
} from "@/components/ui/card" ;
2024-07-17 12:32:51 +00:00
import {
2024-07-26 09:08:45 +00:00
DropdownMenu ,
DropdownMenuContent ,
DropdownMenuRadioGroup ,
DropdownMenuRadioItem ,
DropdownMenuTrigger ,
2024-07-17 12:32:51 +00:00
} from "@/components/ui/dropdown-menu"
2024-07-24 12:47:21 +00:00
import {
Table ,
TableBody ,
TableCell ,
TableRow ,
2024-07-26 09:08:45 +00:00
} from "@/components/ui/table"
2024-07-28 14:14:41 +00:00
import {
CommandInput ,
CommandList ,
CommandEmpty ,
CommandGroup ,
CommandItem ,
CommandDialog
} from "@/components/ui/command" ;
2024-07-17 12:32:51 +00:00
2024-07-25 05:06:30 +00:00
import {
ArrowRight ,
ChatCircleText ,
Key ,
Palette ,
SpeakerHigh ,
UserCircle ,
FileMagnifyingGlass ,
Trash ,
Copy ,
CreditCard ,
CheckCircle ,
2024-07-25 05:07:48 +00:00
NotionLogo ,
GithubLogo ,
2024-07-25 10:25:33 +00:00
Files ,
WhatsappLogo ,
2024-07-26 09:08:45 +00:00
ExclamationMark ,
Plugs ,
CloudSlash ,
Laptop ,
Plus ,
FloppyDisk ,
PlugsConnected ,
ArrowCircleUp ,
2024-07-28 09:41:57 +00:00
ArrowCircleDown ,
2024-07-28 12:35:22 +00:00
ArrowsClockwise ,
2024-07-28 14:14:41 +00:00
Check ,
2024-07-25 05:06:30 +00:00
} from "@phosphor-icons/react" ;
2024-07-17 12:32:51 +00:00
2024-07-16 14:36:50 +00:00
import NavMenu from "../components/navMenu/navMenu" ;
import SidePanel from "../components/sidePanel/chatHistorySidePanel" ;
import Loading from "../components/loading/loading" ;
2024-08-01 11:14:17 +00:00
import IntlTelInput from 'intl-tel-input/react' ;
2024-07-16 14:36:50 +00:00
2024-07-28 14:14:41 +00:00
const ManageFilesModal : React.FC < { onClose : ( ) = > void } > = ( { onClose } ) = > {
const [ syncedFiles , setSyncedFiles ] = useState < string [ ] > ( [ ] ) ;
const [ selectedFiles , setSelectedFiles ] = useState < string [ ] > ( [ ] ) ;
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 (
< CommandDialog open = { true } onOpenChange = { onClose } >
< div className = "flex flex-col h-full" >
< div className = "flex-none p-4 bg-background border-b" >
< CommandInput
placeholder = "Find synced files"
value = { searchQuery }
onValueChange = { setSearchQuery }
/ >
< / div >
< div className = "flex-grow overflow-auto" >
< CommandList >
< CommandEmpty > No such files synced . < / CommandEmpty >
< CommandGroup heading = "Synced files" >
{ filteredFiles . map ( ( filename : string ) = > (
< CommandItem
key = { filename }
value = { filename }
onSelect = { ( value ) = > {
setSelectedFiles ( prev = >
prev . includes ( value )
? prev . filter ( f = > f !== value )
: [ . . . prev , value ]
) ;
} }
>
< div className = "flex items-center justify-between w-full" >
< div className = { ` flex items-center ${ selectedFiles . includes ( filename ) ? 'font-semibold' : '' } ` } >
{ selectedFiles . includes ( filename ) && < Check className = "h-4 w-4 mr-2" / > }
< span className = "break-all" > { filename } < / span >
< / div >
< Button
variant = "outline"
size = "sm"
onClick = { ( ) = > deleteFile ( filename ) }
className = "ml-auto"
>
< Trash className = "h-4 w-4" / >
< / Button >
< / div >
< / CommandItem >
) ) }
< / CommandGroup >
< / CommandList >
< / div >
< div className = "flex-none p-4 bg-background border-t" >
< div className = "flex justify-between" >
< Button
variant = "outline"
size = "sm"
onClick = { deleteSelected }
className = "mr-2"
>
< Trash className = "h-4 w-4 mr-2" / >
2024-07-30 07:11:39 +00:00
{ selectedFiles . length > 0 ? ` Delect Selected ( ${ selectedFiles . length } ) ` : "Delete All" }
2024-07-28 14:14:41 +00:00
< / Button >
< / div >
< / div >
< / div >
< / CommandDialog >
) ;
}
2024-07-17 12:32:51 +00:00
interface DropdownComponentProps {
items : ModelOptions [ ] ;
selected : number ;
2024-07-18 00:07:19 +00:00
callbackFunc : ( value : string ) = > Promise < void > ;
2024-07-16 14:36:50 +00:00
}
2024-07-18 00:07:19 +00:00
const DropdownComponent : React.FC < DropdownComponentProps > = ( { items , selected , callbackFunc } ) = > {
2024-07-17 12:32:51 +00:00
const [ position , setPosition ] = useState ( selected ? . toString ( ) ? ? "0" ) ;
return ! ! selected && (
< div className = "overflow-hidden" >
< DropdownMenu >
< DropdownMenuTrigger asChild className = "w-full" >
2024-07-26 09:08:45 +00:00
< Button variant = "outline" className = "justify-start py-6" >
2024-07-30 07:11:39 +00:00
{ items . find ( item = > item . id . toString ( ) === position ) ? . name }
2024-07-17 12:32:51 +00:00
< / Button >
< / DropdownMenuTrigger >
< DropdownMenuContent >
2024-07-18 00:07:19 +00:00
< DropdownMenuRadioGroup
2024-07-30 07:11:39 +00:00
value = { position }
2024-07-18 00:07:19 +00:00
onValueChange = { async ( value ) = > { setPosition ( value ) ; await callbackFunc ( value ) ; } }
>
2024-07-17 12:32:51 +00:00
{ items . map ( ( item ) = > (
2024-07-25 05:07:48 +00:00
< DropdownMenuRadioItem key = { item . id . toString ( ) } value = { item . id . toString ( ) } >
2024-07-17 12:32:51 +00:00
{ item . name }
< / DropdownMenuRadioItem >
) ) }
< / DropdownMenuRadioGroup >
< / DropdownMenuContent >
< / DropdownMenu >
< / div >
2024-07-16 14:36:50 +00:00
) ;
2024-07-17 12:32:51 +00:00
}
2024-07-16 14:36:50 +00:00
2024-07-24 12:47:21 +00:00
interface TokenObject {
token : string ;
name : string ;
}
export const useApiKeys = ( ) = > {
const [ apiKeys , setApiKeys ] = useState < TokenObject [ ] > ( [ ] ) ;
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 ,
} ;
} ;
2024-07-25 10:25:33 +00:00
enum PhoneNumberValidationState {
Setup = "setup" ,
SendOTP = "otp" ,
VerifyOTP = "verify" ,
Verified = "verified" ,
}
2024-07-24 12:47:21 +00:00
2024-07-16 14:36:50 +00:00
export default function SettingsView() {
const [ title , setTitle ] = useState ( "Settings" ) ;
const [ isMobileWidth , setIsMobileWidth ] = useState ( false ) ;
2024-07-24 12:47:21 +00:00
const { apiKeys , generateAPIKey , copyAPIKey , deleteAPIKey } = useApiKeys ( ) ;
2024-07-25 05:06:30 +00:00
const initialUserConfig = useUserConfig ( true ) ;
const [ userConfig , setUserConfig ] = useState < UserConfig | null > ( null ) ;
2024-07-28 08:16:48 +00:00
const [ name , setName ] = useState < string | undefined > ( undefined ) ;
2024-07-28 12:35:22 +00:00
const [ notionToken , setNotionToken ] = useState < string | null > ( null ) ;
2024-08-01 11:14:17 +00:00
const [ phoneNumber , setPhoneNumber ] = useState < string | undefined > ( undefined ) ;
2024-07-25 10:25:33 +00:00
const [ otp , setOTP ] = useState ( "" ) ;
const [ numberValidationState , setNumberValidationState ] = useState < PhoneNumberValidationState > ( PhoneNumberValidationState . Verified ) ;
2024-07-28 14:14:41 +00:00
const [ isManageFilesModalOpen , setIsManageFilesModalOpen ] = useState ( false ) ;
2024-07-18 00:07:19 +00:00
const { toast } = useToast ( ) ;
2024-07-25 15:17:34 +00:00
const cardClassName = "w-full lg:w-1/3 grid grid-flow-column border border-gray-300 shadow-md rounded-lg" ;
2024-07-25 05:06:30 +00:00
2024-07-25 10:25:33 +00:00
useEffect ( ( ) = > {
setUserConfig ( initialUserConfig ) ;
2024-08-01 11:14:17 +00:00
setPhoneNumber ( initialUserConfig ? . phone_number ) ;
2024-07-25 10:25:33 +00:00
setNumberValidationState (
initialUserConfig ? . is_phone_number_verified
? PhoneNumberValidationState . Verified
: initialUserConfig ? . phone_number
? PhoneNumberValidationState . SendOTP
: PhoneNumberValidationState . Setup
) ;
2024-07-28 08:16:48 +00:00
setName ( initialUserConfig ? . given_name ) ;
2024-07-28 12:35:22 +00:00
setNotionToken ( initialUserConfig ? . notion_token ? ? null ) ;
2024-07-25 10:25:33 +00:00
} , [ initialUserConfig ] ) ;
2024-07-16 14:36:50 +00:00
useEffect ( ( ) = > {
setIsMobileWidth ( window . innerWidth < 786 ) ;
const handleResize = ( ) = > setIsMobileWidth ( window . innerWidth < 786 ) ;
window . addEventListener ( 'resize' , handleResize ) ;
return ( ) = > window . removeEventListener ( 'resize' , handleResize ) ;
} , [ ] ) ;
2024-07-25 12:49:58 +00:00
const sendOTP = async ( ) = > {
try {
2024-08-01 11:14:17 +00:00
const response = await fetch ( ` /api/phone?phone_number= ${ phoneNumber } ` , {
2024-07-25 12:49:58 +00:00
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" ,
} ) ;
}
} ;
2024-07-25 05:06:30 +00:00
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 ( {
2024-07-28 09:41:57 +00:00
title : "💳 Subscription" ,
2024-07-25 05:06:30 +00:00
description : userConfig?.subscription_state === "unsubscribed" ? "Your subscription was cancelled" : "Your Futurist subscription has been renewed" ,
} ) ;
} catch ( error ) {
console . error ( 'Error changing subscription:' , error ) ;
toast ( {
2024-07-28 09:41:57 +00:00
title : "💳 Subscription" ,
2024-07-25 05:06:30 +00:00
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" ,
} ) ;
2024-07-30 12:47:45 +00:00
}
2024-07-25 05:06:30 +00:00
} ;
2024-07-28 08:16:48 +00:00
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" ,
2024-07-28 12:35:22 +00:00
description : "Failed to update name. Try again or contact team@khoj.dev" ,
2024-07-28 08:16:48 +00:00
} ) ;
}
}
2024-07-18 00:07:19 +00:00
const updateModel = ( name : string ) = > async ( id : string ) = > {
2024-07-30 12:47:45 +00:00
if ( ! userConfig ? . is_active && name !== "search" ) return ;
2024-07-18 00:07:19 +00:00
try {
const response = await fetch ( ` /api/model/ ${ name } ?id= ` + id , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
}
} ) ;
2024-07-25 05:06:30 +00:00
if ( ! response . ok ) throw new Error ( 'Failed to update model' ) ;
toast ( {
2024-07-30 12:47:45 +00:00
title : ` ✅ Updated ${ toTitleCase ( name ) } Model ` ,
2024-07-25 05:06:30 +00:00
} ) ;
2024-07-18 00:07:19 +00:00
} catch ( error ) {
2024-07-25 05:06:30 +00:00
console . error ( ` Failed to update ${ name } model: ` , error ) ;
2024-07-18 00:07:19 +00:00
toast ( {
2024-07-30 12:47:45 +00:00
description : ` ❌ Failed to update ${ toTitleCase ( name ) } model. Try again. ` ,
2024-07-18 00:07:19 +00:00
variant : "destructive" ,
} ) ;
}
} ;
2024-07-28 12:35:22 +00:00
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 } ` , {
2024-07-29 17:12:43 +00:00
method : 'PATCH' ,
2024-07-28 12:35:22 +00:00
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 ;
2024-07-30 07:11:39 +00:00
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 ;
}
2024-07-28 12:35:22 +00:00
setUserConfig ( newUserConfig ) ;
}
// Notify user about disconnecting content source
2024-07-30 07:11:39 +00:00
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. ` ,
} ) ;
}
2024-07-28 12:35:22 +00:00
} 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 ` ,
} ) ;
}
}
2024-07-18 00:07:19 +00:00
if ( ! userConfig ) return < Loading / > ;
return (
2024-07-16 14:36:50 +00:00
< div id = "page" className = { styles . page } >
< title >
{ title }
< / title >
< div className = { styles . sidePanel } >
< SidePanel
webSocketConnected = { true }
conversationId = { null }
uploadedFiles = { [ ] }
isMobileWidth = { isMobileWidth }
/ >
< / div >
< div className = { styles . content } >
< NavMenu selected = "Settings" title = "Settings" showLogo = { true } / >
< div className = { styles . contentBody } >
< Suspense fallback = { < Loading / > } >
2024-07-25 15:17:34 +00:00
< div id = "content" className = "grid grid-flow-column sm:grid-flow-row gap-16 m-8" >
2024-07-16 14:36:50 +00:00
< div className = "section grid gap-8" >
2024-07-26 09:08:45 +00:00
< div className = "text-2xl" > Profile < / div >
2024-07-16 14:36:50 +00:00
< div className = "cards flex flex-wrap gap-16" >
< Card className = { cardClassName } >
2024-07-17 17:21:03 +00:00
< CardHeader className = "text-xl flex flex-row" > < UserCircle className = "h-7 w-7 mr-2" / > Name < / CardHeader >
2024-07-17 12:32:51 +00:00
< CardContent className = "overflow-hidden" >
2024-07-26 09:08:45 +00:00
< p className = "pb-4 text-gray-400" > What should Khoj refer to you as ? < / p >
2024-07-28 08:16:48 +00:00
< Input
type = "text"
onChange = { ( e ) = > setName ( e . target . value ) }
value = { name }
className = "w-full border border-gray-300 rounded-lg p-4 py-6"
/ >
2024-07-16 14:36:50 +00:00
< / CardContent >
2024-07-17 12:32:51 +00:00
< CardFooter className = "flex flex-wrap gap-4" >
2024-07-28 08:16:48 +00:00
< Button
variant = "outline"
size = "sm"
onClick = { saveName }
disabled = { name === userConfig . given_name } >
< FloppyDisk className = "h-5 w-5 inline mr-2" / >
Save
< / Button >
2024-07-16 14:36:50 +00:00
< / CardFooter >
< / Card >
2024-07-28 09:41:57 +00:00
< Card id = "billing" className = { cardClassName } >
< CardHeader className = "text-xl flex flex-row" >
< CreditCard className = "h-7 w-7 mr-2" / >
Subscription
< / CardHeader >
< CardContent className = "grid gap-2 overflow-hidden" >
< p className = "text-gray-400" > Current Plan < / p >
{ userConfig . subscription_state === "trial" && (
< >
< p className = "text-xl text-primary/80" > Futurist ( Trial ) < / p >
< p className = "text-gray-400" > You are on a 14 day trial of the Khoj Futurist plan . Check < a href = "https://khoj.dev/pricing" target = "_blank" > pricing page < / a > to compare plans . < / p >
< / >
) || userConfig . subscription_state === "subscribed" && (
< >
< p className = "text-xl text-primary/80" > Futurist < / p >
< p className = "text-gray-400" > Subscription < b > renews < / b > on < b > { userConfig . subscription_renewal_date } < / b > < / p >
< / >
) || userConfig . subscription_state === "unsubscribed" && (
< >
< p className = "text-xl" > Futurist < / p >
< p className = "text-gray-400" > Subscription < b > ends < / b > on < b > { userConfig . subscription_renewal_date } < / b > < / p >
< / >
) || userConfig . subscription_state === "expired" && (
< >
< p className = "text-xl" > Free Plan < / p >
{ userConfig . subscription_renewal_date && (
< p className = "text-gray-400" > Subscription < b > expired < / b > on < b > { userConfig . subscription_renewal_date } < / b > < / p >
) || (
< p className = "text-gray-400" > Check < a href = "https://khoj.dev/pricing" target = "_blank" > pricing page < / a > to compare plans . < / p >
) }
< / >
) }
< / CardContent >
< CardFooter className = "flex flex-wrap gap-4" >
{ ( userConfig . subscription_state == "subscribed" ) && (
< Button
variant = "outline"
className = "hover:text-red-400"
onClick = { ( ) = > setSubscription ( "cancel" ) }
>
< ArrowCircleDown className = "h-5 w-5 mr-2" / > Unsubscribe
< / Button >
) || ( userConfig . subscription_state == "unsubscribed" ) && (
< Button
variant = "outline"
className = "text-primary/80 hover:text-primary"
onClick = { ( ) = > setSubscription ( "resubscribe" ) }
>
< ArrowCircleUp weight = "bold" className = "h-5 w-5 mr-2" / > Resubscribe
< / Button >
) || (
< Button
variant = "outline"
className = "text-primary/80 hover:text-primary"
onClick = { ( ) = > window . open ( ` ${ userConfig . khoj_cloud_subscription_url } ?prefilled_email= ${ userConfig . username } ` , '_blank' , 'noopener,noreferrer' ) }
>
< ArrowCircleUp weight = "bold" className = "h-5 w-5 mr-2" / > Subscribe
< / Button >
) }
< / CardFooter >
< / Card >
2024-07-16 14:36:50 +00:00
< / div >
< / div >
2024-07-28 14:14:41 +00:00
{ isManageFilesModalOpen && < ManageFilesModal onClose = { ( ) = > setIsManageFilesModalOpen ( false ) } / > }
2024-07-16 14:36:50 +00:00
< div className = "section grid gap-8" >
2024-07-26 09:08:45 +00:00
< div className = "text-2xl" > Content < / div >
2024-07-16 14:36:50 +00:00
< div className = "cards flex flex-wrap gap-16" >
< Card className = { cardClassName } >
2024-07-26 09:08:45 +00:00
< CardHeader className = "text-xl flex flex-row text-2xl" > < Laptop className = "h-8 w-8 mr-2" / > Files < / CardHeader >
< CardContent className = "overflow-hidden pb-12 text-gray-400" >
2024-07-16 14:36:50 +00:00
Manage your synced files
< / CardContent >
2024-07-17 12:32:51 +00:00
< CardFooter className = "flex flex-wrap gap-4" >
2024-07-28 14:14:41 +00:00
< Button variant = "outline" size = "sm" onClick = { ( ) = > setIsManageFilesModalOpen ( true ) } >
2024-07-30 13:21:33 +00:00
< >
< Files className = "h-5 w-5 inline mr-1" / > Manage
< / >
2024-07-26 09:08:45 +00:00
< / Button >
2024-07-30 07:11:39 +00:00
< Button
variant = "outline"
size = "sm"
className = { ` ${ userConfig . enabled_content_source . computer || "hidden" } ` }
onClick = { ( ) = > disconnectContent ( "computer" ) }
>
2024-07-26 09:08:45 +00:00
< CloudSlash className = "h-5 w-5 inline mr-1" / > Disable
< / Button >
2024-07-16 14:36:50 +00:00
< / CardFooter >
< / Card >
2024-07-30 13:21:33 +00:00
< Card className = { ` ${ cardClassName } hidden ` } >
2024-07-26 09:08:45 +00:00
< CardHeader className = "text-xl flex flex-row text-2xl" > < GithubLogo className = "h-8 w-8 mr-2" / > Github < / CardHeader >
< CardContent className = "overflow-hidden pb-12 text-gray-400" >
Set Github repositories to index
2024-07-16 14:36:50 +00:00
< / CardContent >
2024-07-17 12:32:51 +00:00
< CardFooter className = "flex flex-wrap gap-4" >
2024-07-26 09:08:45 +00:00
< Button variant = "outline" size = "sm" >
{ userConfig . enabled_content_source . github && (
< >
< Files className = "h-5 w-5 inline mr-1" / > Manage
< / >
) || (
< >
< Plugs className = "h-5 w-5 inline mr-1" / > Connect
< / >
) }
< / Button >
< Button variant = "outline" size = "sm" className = { ` ${ userConfig . enabled_content_source . github || "hidden" } ` } >
< CloudSlash className = "h-5 w-5 inline mr-1" / > Disable
< / Button >
2024-07-16 14:36:50 +00:00
< / CardFooter >
< / Card >
< Card className = { cardClassName } >
2024-07-25 05:07:48 +00:00
< CardHeader className = "text-xl flex flex-row" > < NotionLogo className = "h-7 w-7 mr-2" / > Notion < / CardHeader >
2024-07-28 12:35:22 +00:00
< CardContent className = "grid gap-4" >
< p className = "text-gray-400" > Sync your Notion pages . See the < a href = "https://docs.khoj.dev/data-sources/notion_integration/" > setup instructions < / a > < / p >
{ ! userConfig . notion_oauth_url && (
< Input
onChange = { ( e ) = > setNotionToken ( e . target . value ) }
value = { notionToken || "" }
2024-07-30 07:11:39 +00:00
placeholder = "Enter API Key of your Khoj integration on Notion"
2024-07-28 12:35:22 +00:00
className = "w-full border border-gray-300 rounded-lg px-4 py-6"
/ >
) }
2024-07-16 14:36:50 +00:00
< / CardContent >
2024-07-17 12:32:51 +00:00
< CardFooter className = "flex flex-wrap gap-4" >
2024-07-28 12:35:22 +00:00
{ (
/* Show connect to notion button if notion oauth url setup and user disconnected*/
userConfig . notion_oauth_url && ! userConfig . enabled_content_source . notion
?
< Button variant = "outline" size = "sm" onClick = { ( ) = > { window . open ( userConfig . notion_oauth_url ) } } >
2024-07-26 09:08:45 +00:00
< Plugs className = "h-5 w-5 inline mr-1" / > Connect
2024-07-28 12:35:22 +00:00
< / Button >
/* Show sync button if user connected to notion and API key unchanged */
: userConfig . enabled_content_source . notion && notionToken === userConfig . notion_token
?
< Button variant = "outline" size = "sm" onClick = { ( ) = > syncContent ( "notion" ) } >
< ArrowsClockwise className = "h-5 w-5 inline mr-1" / > Sync
< / Button >
/* Show set API key button notion oauth url not set setup */
: ! userConfig . notion_oauth_url
?
< Button variant = "outline" size = "sm" onClick = { saveNotionToken } disabled = { notionToken === userConfig . notion_token } >
< FloppyDisk className = "h-5 w-5 inline mr-1" / >
{ userConfig . enabled_content_source . notion && "Update API Key" || "Set API Key" }
< / Button >
: < > < / >
2024-07-26 09:08:45 +00:00
) }
2024-07-28 12:35:22 +00:00
< Button
variant = "outline"
size = "sm"
2024-07-28 14:14:41 +00:00
className = { ` ${ userConfig . notion_token || "hidden" } ` }
2024-07-28 12:35:22 +00:00
onClick = { ( ) = > disconnectContent ( "notion" ) }
>
< CloudSlash className = "h-5 w-5 inline mr-1" / > Disconnect
2024-07-26 09:08:45 +00:00
< / Button >
2024-07-16 14:36:50 +00:00
< / CardFooter >
< / Card >
2024-07-17 12:32:51 +00:00
< / div >
< / div >
< div className = "section grid gap-8" >
2024-07-26 09:08:45 +00:00
< div className = "text-2xl" > Models < / div >
2024-07-17 12:32:51 +00:00
< div className = "cards flex flex-wrap gap-16" >
2024-07-26 09:08:45 +00:00
{ userConfig . chat_model_options . length > 0 && (
2024-07-17 12:32:51 +00:00
< Card className = { cardClassName } >
2024-07-17 17:21:03 +00:00
< CardHeader className = "text-xl flex flex-row" > < ChatCircleText className = "h-7 w-7 mr-2" / > Chat < / CardHeader >
2024-07-26 09:08:45 +00:00
< CardContent className = "overflow-hidden pb-12 grid gap-8" >
< p className = "text-gray-400" > Pick the chat model to generate text responses < / p >
2024-07-17 12:32:51 +00:00
< DropdownComponent
items = { userConfig . chat_model_options }
selected = { userConfig . selected_chat_model_config }
2024-07-18 00:07:19 +00:00
callbackFunc = { updateModel ( "chat" ) }
/ >
< / CardContent >
2024-07-26 09:08:45 +00:00
< CardFooter className = "flex flex-wrap gap-4" >
{ ! userConfig . is_active && (
< p className = "text-gray-400" > Subscribe to switch model < / p >
) }
< / CardFooter >
< / Card >
) }
{ userConfig . search_model_options . length > 0 && (
2024-07-18 00:07:19 +00:00
< Card className = { cardClassName } >
< CardHeader className = "text-xl flex flex-row" > < FileMagnifyingGlass className = "h-7 w-7 mr-2" / > Search < / CardHeader >
2024-07-26 09:08:45 +00:00
< CardContent className = "overflow-hidden pb-12 grid gap-8" >
< p className = "text-gray-400" > Pick the search model to find your documents < / p >
2024-07-18 00:07:19 +00:00
< DropdownComponent
items = { userConfig . search_model_options }
selected = { userConfig . selected_search_model_config }
callbackFunc = { updateModel ( "search" ) }
2024-07-17 12:32:51 +00:00
/ >
< / CardContent >
2024-07-26 09:08:45 +00:00
< CardFooter className = "flex flex-wrap gap-4" >
< / CardFooter >
2024-07-17 12:32:51 +00:00
< / Card >
2024-07-26 09:08:45 +00:00
) }
{ userConfig . paint_model_options . length > 0 && (
2024-07-17 12:32:51 +00:00
< Card className = { cardClassName } >
2024-07-17 17:21:03 +00:00
< CardHeader className = "text-xl flex flex-row" > < Palette className = "h-7 w-7 mr-2" / > Paint < / CardHeader >
2024-07-26 09:08:45 +00:00
< CardContent className = "overflow-hidden pb-12 grid gap-8" >
< p className = "text-gray-400" > Pick the paint model to generate image responses < / p >
2024-07-17 12:32:51 +00:00
< DropdownComponent
items = { userConfig . paint_model_options }
selected = { userConfig . selected_paint_model_config }
2024-07-18 00:07:19 +00:00
callbackFunc = { updateModel ( "paint" ) }
2024-07-17 12:32:51 +00:00
/ >
< / CardContent >
2024-07-26 09:08:45 +00:00
< CardFooter className = "flex flex-wrap gap-4" >
{ ! userConfig . is_active && (
< p className = "text-gray-400" > Subscribe to switch model < / p >
) }
< / CardFooter >
2024-07-17 12:32:51 +00:00
< / Card >
2024-07-26 09:08:45 +00:00
) }
{ userConfig . voice_model_options . length > 0 && (
2024-07-17 12:32:51 +00:00
< Card className = { cardClassName } >
2024-07-17 17:21:03 +00:00
< CardHeader className = "text-xl flex flex-row" > < SpeakerHigh className = "h-7 w-7 mr-2" / > Voice < / CardHeader >
2024-07-26 09:08:45 +00:00
< CardContent className = "overflow-hidden pb-12 grid gap-8" >
< p className = "text-gray-400" > Pick the voice model to generate speech responses < / p >
2024-07-17 12:32:51 +00:00
< DropdownComponent
items = { userConfig . voice_model_options }
selected = { userConfig . selected_voice_model_config }
2024-07-18 00:07:19 +00:00
callbackFunc = { updateModel ( "voice" ) }
2024-07-17 12:32:51 +00:00
/ >
2024-07-16 14:36:50 +00:00
< / CardContent >
2024-07-26 09:08:45 +00:00
< CardFooter className = "flex flex-wrap gap-4" >
{ ! userConfig . is_active && (
< p className = "text-gray-400" > Subscribe to switch model < / p >
) }
< / CardFooter >
2024-07-16 14:36:50 +00:00
< / Card >
2024-07-26 09:08:45 +00:00
) }
2024-07-16 14:36:50 +00:00
< / div >
< / div >
2024-07-24 12:47:21 +00:00
< div className = "section grid gap-8" >
2024-07-26 09:08:45 +00:00
< div className = "text-2xl" > Clients < / div >
< div className = "cards flex flex-wrap gap-8" >
2024-07-24 12:47:21 +00:00
< Card className = "grid grid-flow-column border border-gray-300 shadow-md rounded-lg" >
2024-07-26 09:08:45 +00:00
< CardHeader className = "text-xl grid grid-flow-col grid-cols-[1fr_auto] pb-0" >
< span className = "flex flex-wrap" >
< Key className = "h-7 w-7 mr-2" / > API Keys
< / span >
< Button variant = "secondary" className = "!mt-0" onClick = { generateAPIKey } >
< Plus weight = "bold" className = 'h-5 w-5 mr-2' / > Generate Key
< / Button >
< / CardHeader >
< CardContent className = "overflow-hidden grid gap-6" >
2024-07-24 12:47:21 +00:00
< p className = "text-md text-gray-400" >
2024-07-26 09:08:45 +00:00
Access Khoj from the < a href = "https://docs.khoj.dev/clients/Desktop" target = "_blank" > Desktop < / a > , < a href = "https://docs.khoj.dev/clients/Obsidian" > Obsidian < / a > , < a href = "https://docs.khoj.dev/clients/Emacs" > Emacs < / a > apps and more .
2024-07-24 12:47:21 +00:00
< / p >
< Table >
< TableBody >
{ apiKeys . map ( ( key ) = > (
< TableRow key = { key . token } >
2024-07-26 09:08:45 +00:00
< TableCell className = "pl-0 py-3" > { key . name } < / TableCell >
< TableCell className = "grid grid-flow-col grid-cols-[1fr_auto] bg-secondary rounded-xl p-3" >
< span >
{ ` ${ key . token . slice ( 0 , 6 ) } ... ${ key . token . slice ( - 4 ) } ` }
< / span >
< div className = "grid grid-flow-col" >
< Copy weight = "bold" className = "h-4 w-4 mr-2 hover:bg-primary/40" onClick = { ( ) = > copyAPIKey ( key . token ) } / >
< Trash weight = "bold" className = 'h-4 w-4 mr-2 md:ml-4 text-red-400 hover:bg-primary/40' onClick = { ( ) = > deleteAPIKey ( key . token ) } / >
< / div >
2024-07-24 12:47:21 +00:00
< / TableCell >
< / TableRow >
) ) }
< / TableBody >
< / Table >
< / CardContent >
< CardFooter className = "flex flex-wrap gap-4" >
< / CardFooter >
< / Card >
2024-07-25 10:25:33 +00:00
< Card className = { cardClassName } >
< CardHeader className = "text-xl flex flex-row" >
< WhatsappLogo className = "h-7 w-7 mr-2" / >
Chat on Whatsapp
{ numberValidationState === PhoneNumberValidationState . Verified && (
< CheckCircle weight = "bold" className = "h-4 w-4 ml-1 text-green-400" / >
) || numberValidationState !== PhoneNumberValidationState . Setup && (
< ExclamationMark weight = "bold" className = "h-4 w-4 ml-1 text-yellow-400" / >
) }
< / CardHeader >
< CardContent className = "grid gap-4" >
2024-07-26 09:08:45 +00:00
< p className = "text-gray-400" >
2024-07-25 10:25:33 +00:00
Connect your number to chat with Khoj on WhatsApp . Learn more about the integration < a href = "https://docs.khoj.dev/clients/whatsapp" > here < / a > .
< / p >
< div >
2024-08-01 11:14:17 +00:00
< IntlTelInput
initialValue = { phoneNumber || "" }
onChangeNumber = { setPhoneNumber }
disabled = { numberValidationState === PhoneNumberValidationState . Verified || numberValidationState === PhoneNumberValidationState . VerifyOTP }
initOptions = { {
separateDialCode : true ,
initialCountry : "us" ,
utilsScript : "https://assets.khoj.dev/intl-tel-input%4023.8.0_build_js_utils.js" ,
} }
/ >
2024-07-25 10:25:33 +00:00
{ numberValidationState === PhoneNumberValidationState . VerifyOTP && (
2024-07-26 09:08:45 +00:00
< >
2024-08-01 11:14:17 +00:00
< p > { ` Enter the OTP sent to your number: ${ phoneNumber } ` } < / p >
2024-07-25 10:25:33 +00:00
< InputOTP
autoFocus = { true }
maxLength = { 6 }
2024-07-26 09:08:45 +00:00
value = { otp || "" }
2024-07-25 10:25:33 +00:00
onChange = { setOTP }
2024-08-01 11:14:17 +00:00
onComplete = { ( ) = > setNumberValidationState ( PhoneNumberValidationState . VerifyOTP ) }
2024-07-25 10:25:33 +00:00
>
< InputOTPGroup >
< InputOTPSlot index = { 0 } / >
< InputOTPSlot index = { 1 } / >
< InputOTPSlot index = { 2 } / >
< InputOTPSlot index = { 3 } / >
< InputOTPSlot index = { 4 } / >
< InputOTPSlot index = { 5 } / >
< / InputOTPGroup >
< / InputOTP >
2024-07-26 09:08:45 +00:00
< / >
2024-07-25 10:25:33 +00:00
) }
< / div >
< / CardContent >
< CardFooter className = "flex flex-wrap gap-4" >
{ numberValidationState === PhoneNumberValidationState . VerifyOTP && (
< Button
variant = "outline"
2024-07-25 12:49:58 +00:00
onClick = { verifyOTP }
2024-07-25 10:25:33 +00:00
>
Verify
< / Button >
) || (
< Button
variant = "outline"
2024-08-01 11:14:17 +00:00
disabled = { ! phoneNumber || ( phoneNumber === userConfig . phone_number && numberValidationState === PhoneNumberValidationState . Verified ) || ! isValidPhoneNumber ( phoneNumber ) }
2024-07-25 12:49:58 +00:00
onClick = { sendOTP }
2024-07-25 10:25:33 +00:00
>
2024-07-26 09:08:45 +00:00
{ ! userConfig . phone_number
? ( < > < Plugs className = "inline mr-2" / > Setup Whatsapp < / > )
2024-08-01 11:14:17 +00:00
: ! phoneNumber || ( phoneNumber === userConfig . phone_number && numberValidationState === PhoneNumberValidationState . Verified ) || ! isValidPhoneNumber ( phoneNumber )
2024-07-26 09:08:45 +00:00
? ( < > < PlugsConnected className = "inline mr-2 text-green-400" / > Switch Number < / > )
2024-08-01 11:14:17 +00:00
: ( < > Send OTP < ArrowRight className = "inline ml-2" weight = "bold" / > < / > )
2024-07-26 09:08:45 +00:00
}
2024-07-25 10:25:33 +00:00
< / Button >
) }
< / CardFooter >
< / Card >
2024-07-24 12:47:21 +00:00
< / div >
< / div >
2024-07-16 14:36:50 +00:00
< / div >
< / Suspense >
< / div >
< / div >
< / div >
) ;
}