'use client' import styles from "./sidePanel.module.css"; import { useEffect, useState } from "react"; import { UserProfile, useAuthenticatedData } from "@/app/common/auth"; import Link from "next/link"; import useSWR from "swr"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { InlineLoading } from "../loading/loading"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"; import { ScrollArea } from "@/components/ui/scroll-area"; import { ArrowRight, ArrowLeft, ArrowDown, Spinner, Check, FolderPlus, DotsThreeVertical, House, StackPlus, UserCirclePlus, Sidebar, NotePencil } from "@phosphor-icons/react"; interface ChatHistory { conversation_id: string; slug: string; agent_name: string; agent_avatar: string; compressed: boolean; created: string; showSidePanel: (isEnabled: boolean) => void; } import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Pencil, Trash, Share } from "@phosphor-icons/react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; import { modifyFileFilterForConversation } from "@/app/common/chatFunctions"; import { ScrollAreaScrollbar } from "@radix-ui/react-scroll-area"; import { KhojLogo, KhojLogoType } from "@/app/components/logo/khogLogo"; import NavMenu from "../navMenu/navMenu"; // Define a fetcher function const fetcher = (url: string) => fetch(url).then((res) => res.json()); interface GroupedChatHistory { [key: string]: ChatHistory[]; } function renameConversation(conversationId: string, newTitle: string) { const editUrl = `/api/chat/title?client=web&conversation_id=${conversationId}&title=${newTitle}`; fetch(editUrl, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, }) .then(response => response.json()) .then(data => { }) .catch(err => { console.error(err); return; }); } function shareConversation(conversationId: string, setShareUrl: (url: string) => void) { const shareUrl = `/api/chat/share?client=web&conversation_id=${conversationId}`; fetch(shareUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, }) .then(response => response.json()) .then(data => { setShareUrl(data.url); }) .catch(err => { console.error(err); return; }); } function deleteConversation(conversationId: string) { const deleteUrl = `/api/chat/history?client=web&conversation_id=${conversationId}`; fetch(deleteUrl, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, }) .then(response => response.json()) .then(data => { }) .catch(err => { console.error(err); return; }); } interface FilesMenuProps { conversationId: string | null; uploadedFiles: string[]; isMobileWidth: boolean; } function FilesMenu(props: FilesMenuProps) { // Use SWR to fetch files const { data: files, error } = useSWR(props.conversationId ? '/api/content/computer' : null, fetcher); const { data: selectedFiles, error: selectedFilesError } = useSWR(props.conversationId ? `/api/chat/conversation/file-filters/${props.conversationId}` : null, fetcher); const [isOpen, setIsOpen] = useState(false); const [unfilteredFiles, setUnfilteredFiles] = useState([]); const [addedFiles, setAddedFiles] = useState([]); useEffect(() => { if (!files) return; // First, sort lexically files.sort(); let sortedFiles = files; if (addedFiles) { console.log("addedFiles in useeffect hook", addedFiles); sortedFiles = addedFiles.concat(sortedFiles.filter((filename: string) => !addedFiles.includes(filename))); } setUnfilteredFiles(sortedFiles); }, [files, addedFiles]); useEffect(() => { for (const file of props.uploadedFiles) { setAddedFiles((addedFiles) => [...addedFiles, file]); } }, [props.uploadedFiles]); useEffect(() => { if (selectedFiles) { setAddedFiles(selectedFiles); } }, [selectedFiles]); const removeAllFiles = () => { modifyFileFilterForConversation(props.conversationId, addedFiles, setAddedFiles, 'remove'); } const addAllFiles = () => { modifyFileFilterForConversation(props.conversationId, unfilteredFiles, setAddedFiles, 'add'); } if (!props.conversationId) return (<>); if (error) return
Failed to load files
; if (selectedFilesError) return
Failed to load selected files
; if (!files) return ; if (!selectedFiles) return ; const FilesMenuCommandBox = () => { return ( No results found. Try advanced search. { removeAllFiles(); }} > Clear all { addAllFiles(); }} > Select all {unfilteredFiles.map((filename: string) => ( addedFiles && addedFiles.includes(filename) ? { modifyFileFilterForConversation(props.conversationId, [value], setAddedFiles, 'remove'); }} > {filename} : { modifyFileFilterForConversation(props.conversationId, [value], setAddedFiles, 'add'); }} > {filename} ))} ); } if (props.isMobileWidth) { return ( <> Manage Files Files Manage files for this conversation
); } return ( <>

Manage Context

Using {addedFiles.length == 0 ? files.length : addedFiles.length} files

) } interface SessionsAndFilesProps { setEnabled: (enabled: boolean) => void; subsetOrganizedData: GroupedChatHistory | null; organizedData: GroupedChatHistory | null; data: ChatHistory[] | null; userProfile: UserProfile | null; conversationId: string | null; uploadedFiles: string[]; isMobileWidth: boolean; } function SessionsAndFiles(props: SessionsAndFilesProps) { return ( <>
{props.subsetOrganizedData != null && Object.keys(props.subsetOrganizedData).filter(tg => tg !== "All Time").map((timeGrouping) => (
{timeGrouping}
{props.subsetOrganizedData && props.subsetOrganizedData[timeGrouping].map((chatHistory) => ( ))}
))}
{ (props.data && props.data.length > 5) && ( ) }
) } interface ChatSessionActionMenuProps { conversationId: string; setTitle: (title: string) => void; } function ChatSessionActionMenu(props: ChatSessionActionMenuProps) { const [renamedTitle, setRenamedTitle] = useState(''); const [isRenaming, setIsRenaming] = useState(false); const [isSharing, setIsSharing] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [shareUrl, setShareUrl] = useState(''); const [showShareUrl, setShowShareUrl] = useState(false); const [isOpen, setIsOpen] = useState(false); useEffect(() => { if (isSharing) { shareConversation(props.conversationId, setShareUrl); setShowShareUrl(true); setIsSharing(false); } }, [isSharing, props.conversationId]); if (isRenaming) { return ( setIsRenaming(open)}> Set a new title for the conversation This will help you identify the conversation easily, and also help you search for it later. setRenamedTitle(e.target.value)} /> ) } if (isSharing || showShareUrl) { if (shareUrl) { navigator.clipboard.writeText(shareUrl); } return ( { setShowShareUrl(open) setIsSharing(open) }}> Conversation Share URL Sharing this chat session will allow anyone with a link to view the conversation. { !showShareUrl && } { showShareUrl && } ) } if (isDeleting) { return ( setIsDeleting(open)}> Delete Conversation Are you sure you want to delete this conversation? This action cannot be undone. Cancel { deleteConversation(props.conversationId); setIsDeleting(false); var currConversationId = parseInt(new URLSearchParams(window.location.search).get('conversationId') || "0"); //edge case for deleting current conversation if (currConversationId === parseInt(props.conversationId)) { window.location.href = '/'; } //reload side panel setTimeout(() => { window.location.reload(); }, 1000); }} className="bg-rose-500 hover:bg-rose-600">Delete ) } return ( setIsOpen(open)} open={isOpen}> ) } function ChatSession(props: ChatHistory) { const [isHovered, setIsHovered] = useState(false); const [title, setTitle] = useState(props.slug || "New Conversation 🌱"); var currConversationId = parseInt(new URLSearchParams(window.location.search).get('conversationId') || "-1"); return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} key={props.conversation_id} className={`${styles.session} ${props.compressed ? styles.compressed : '!max-w-full'} ${isHovered ? `${styles.sessionHover}` : ''} ${currConversationId === parseInt(props.conversation_id) && currConversationId != -1 ? "dark:bg-neutral-800 bg-white" : ""}`}> props.showSidePanel(false)}>

{title}

); } interface ChatSessionsModalProps { data: GroupedChatHistory | null; showSidePanel: (isEnabled: boolean) => void; } function ChatSessionsModal({ data, showSidePanel }: ChatSessionsModalProps) { return ( See All All Conversations {data && Object.keys(data).map((timeGrouping) => (
{timeGrouping}
{data[timeGrouping].map((chatHistory) => ( ))}
))}
); } const fetchChatHistory = async (url: string) => { const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); return response.json(); }; export const useChatSessionsFetchRequest = (url: string) => { const { data, error } = useSWR(url, fetchChatHistory); return { data, isLoading: !error && !data, isError: error, }; }; interface SidePanelProps { conversationId: string | null; uploadedFiles: string[]; isMobileWidth: boolean; } export default function SidePanel(props: SidePanelProps) { const [data, setData] = useState(null); const [organizedData, setOrganizedData] = useState(null); const [subsetOrganizedData, setSubsetOrganizedData] = useState(null); const [enabled, setEnabled] = useState(false); const authenticatedData = useAuthenticatedData(); const { data: chatSessions } = useChatSessionsFetchRequest(authenticatedData ? `/api/chat/sessions` : ''); useEffect(() => { if (chatSessions) { setData(chatSessions); const groupedData: GroupedChatHistory = {}; const subsetOrganizedData: GroupedChatHistory = {}; let numAdded = 0; const currentDate = new Date(); chatSessions.forEach((chatHistory) => { const chatDate = new Date(chatHistory.created); const diffTime = Math.abs(currentDate.getTime() - chatDate.getTime()); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); const timeGrouping = diffDays < 7 ? 'Recent' : diffDays < 30 ? 'Last Month' : 'All Time'; if (!groupedData[timeGrouping]) { groupedData[timeGrouping] = []; } groupedData[timeGrouping].push(chatHistory); // Add to subsetOrganizedData if less than 8 if (numAdded < 8) { if (!subsetOrganizedData[timeGrouping]) { subsetOrganizedData[timeGrouping] = []; } subsetOrganizedData[timeGrouping].push(chatHistory); numAdded++; } }); setSubsetOrganizedData(subsetOrganizedData); setOrganizedData(groupedData); } }, [chatSessions]); return (
{ props.isMobileWidth ? { if (!enabled) setEnabled(false); setEnabled(open); } }> Sessions and Files View all conversation sessions and manage conversation file filters { authenticatedData ?
:
{/* Redirect to login page */}
}
:
{enabled ? : }
} { props.isMobileWidth && } { props.isMobileWidth && }
{ authenticatedData && !props.isMobileWidth && enabled &&
} { !authenticatedData && enabled && !props.isMobileWidth &&
{/* Redirect to login page */}
}
); }