"use client"; import styles from "./sidePanel.module.css"; import { useEffect, useMemo, useState } from "react"; import { mutate } from "swr"; 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, FunnelSimple, MagnifyingGlass, ChatsCircle, } from "@phosphor-icons/react"; interface ChatHistory { conversation_id: string; slug: string; agent_name: string; agent_icon: string; agent_color: string; compressed: boolean; created: string; updated: string; } import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, 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, } from "@/components/ui/alert-dialog"; import { modifyFileFilterForConversation } from "@/app/common/chatFunctions"; import { ScrollAreaScrollbar } from "@radix-ui/react-scroll-area"; import { getIconFromIconName } from "@/app/common/iconUtils"; import { SidebarGroup, SidebarGroupLabel, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, } from "@/components/ui/sidebar"; // Define a fetcher function const fetcher = (url: string) => fetch(url).then((res) => { if (!res.ok) throw new Error(`Failed to call API at ${url} with error ${res.statusText}`); return 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(); mutate("/api/chat/sessions"); }) .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("/api/content/computer", 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([]); const usingConversationContext = props.conversationId !== null; useEffect(() => { if (!files) return; let sortedUniqueFiles = Array.from(new Set(files)).sort(); if (Array.isArray(addedFiles)) { sortedUniqueFiles = addedFiles.concat( sortedUniqueFiles.filter((filename: string) => !addedFiles.includes(filename)), ); } setUnfilteredFiles(sortedUniqueFiles); }, [files, addedFiles]); useEffect(() => { for (const file of props.uploadedFiles) { setAddedFiles((addedFiles) => [...addedFiles, file]); } }, [props.uploadedFiles]); useEffect(() => { if (Array.isArray(selectedFiles)) { setAddedFiles(selectedFiles); } else { setAddedFiles([]); } }, [selectedFiles]); const removeAllFiles = () => { modifyFileFilterForConversation(props.conversationId, addedFiles, setAddedFiles, "remove"); }; const addAllFiles = () => { modifyFileFilterForConversation( props.conversationId, unfilteredFiles, setAddedFiles, "add", ); }; if (error) return
Failed to load files
; if (selectedFilesError) return
Failed to load selected files
; if (!files) return ; if (!selectedFiles && props.conversationId) return ; const FilesMenuCommandBox = () => { return ( No results found. Try advanced search. {usingConversationContext && ( { removeAllFiles(); }} > Clear all { addAllFiles(); }} > Select all )} Settings {addedFiles.length == 0 && ( Upload documents )} {unfilteredFiles.map((filename: string) => Array.isArray(addedFiles) && addedFiles.includes(filename) ? ( { if (!usingConversationContext) return; modifyFileFilterForConversation( props.conversationId, [value], setAddedFiles, "remove", ); }} > {filename} ) : ( { if (!usingConversationContext) return; modifyFileFilterForConversation( props.conversationId, [value], setAddedFiles, "add", ); }} > {filename} ), )} ); }; if (props.isMobileWidth) { return ( <> Manage Files Files {usingConversationContext ? "Manage files for this conversation" : "Shared files"}
); } return ( <>

{usingConversationContext ? "Manage Context" : "Files"}

{usingConversationContext ? ( Using{" "} {addedFiles.length == 0 ? files.length : addedFiles.length}{" "} files ) : ( Shared{" "} {addedFiles.length == 0 ? files.length : addedFiles.length}{" "} files )}

); } interface SessionsAndFilesProps { subsetOrganizedData: GroupedChatHistory | null; organizedData: GroupedChatHistory | null; data: ChatHistory[] | null; userProfile: UserProfile | null; conversationId: string | null; uploadedFiles: string[]; isMobileWidth: boolean; sideBarOpen: boolean; } function SessionsAndFiles(props: SessionsAndFilesProps) { return (
{props.sideBarOpen && (
{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 && ( )}
); } export interface ChatSessionActionMenuProps { conversationId: string; setTitle: (title: string) => void; sizing?: "sm" | "md" | "lg"; } export 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); }} className="bg-rose-500 hover:bg-rose-600" > Delete ); } function sizeClass() { switch (props.sizing) { case "sm": return "h-4 w-4"; case "md": return "h-6 w-6"; case "lg": return "h-8 w-8"; default: return "h-4 w-4"; } } const size = sizeClass(); return (
{(props.sizing === "lg" || props.sizing === "md") && ( )} setIsOpen(open)} open={isOpen}> {props.sizing === "lg" || props.sizing === "md" ? ( ) : ( )} setIsRenaming(true)}> Rename {props.sizing === "sm" && ( setIsSharing(true)}> Share )} setIsDeleting(true)}> Delete
); } function ChatSession(props: ChatHistory) { const [isHovered, setIsHovered] = useState(false); const [title, setTitle] = useState(props.slug || "New Conversation 🌱"); var currConversationId = 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 === props.conversation_id && currConversationId != "-1" ? "dark:bg-neutral-800 bg-white" : ""}`} >

{title}

); } interface ChatSessionsModalProps { data: GroupedChatHistory | null; sideBarOpen: boolean; } interface AgentStyle { color: string; icon: string; } function ChatSessionsModal({ data, sideBarOpen }: ChatSessionsModalProps) { const [agentsFilter, setAgentsFilter] = useState([]); const [agentOptions, setAgentOptions] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [agentNameToStyleMap, setAgentNameToStyleMap] = useState>({}); useEffect(() => { if (data) { const agents: string[] = []; let agentNameToStyleMapLocal: Record = {}; Object.keys(data).forEach((timeGrouping) => { data[timeGrouping].forEach((chatHistory) => { if (!agents.includes(chatHistory.agent_name) && chatHistory.agent_name) { agents.push(chatHistory.agent_name); agentNameToStyleMapLocal = { ...agentNameToStyleMapLocal, [chatHistory.agent_name]: { color: chatHistory.agent_color, icon: chatHistory.agent_icon, }, }; } }); }); setAgentNameToStyleMap(agentNameToStyleMapLocal); setAgentOptions(agents); } }, [data]); // Memoize the filtered results const filteredData = useMemo(() => { if (!data) return null; // Early return if no filters active if (agentsFilter.length === 0 && searchQuery.length === 0) { return data; } const filtered: GroupedChatHistory = {}; const agentSet = new Set(agentsFilter); const searchLower = searchQuery.toLowerCase(); for (const timeGrouping in data) { const matches = data[timeGrouping].filter((chatHistory) => { // Early return for agent filter if (agentsFilter.length > 0 && !agentSet.has(chatHistory.agent_name)) { return false; } // Early return for search query if (searchQuery && !chatHistory.slug?.toLowerCase().includes(searchLower)) { return false; } return true; }); if (matches.length > 0) { filtered[timeGrouping] = matches; } } return filtered; }, [data, agentsFilter, searchQuery]); return ( {sideBarOpen ? "All Conversations" : ""} All Conversations
{ setSearchQuery(e.target.value); }} placeholder="Search conversations" /> Agents {agentOptions.map((agent) => ( e.preventDefault()} checked={agentsFilter.includes(agent)} onCheckedChange={(checked) => { if (checked) { setAgentsFilter([...agentsFilter, agent]); } else { setAgentsFilter( agentsFilter.filter((a) => a !== agent), ); } }} >
{getIconFromIconName( agentNameToStyleMap[agent]?.icon, agentNameToStyleMap[agent]?.color, )}
{agent}
))}
{filteredData && Object.keys(filteredData).map((timeGrouping) => (
{timeGrouping}
{filteredData[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, isLoading, error } = useSWR(url, fetchChatHistory); return { data, isLoading, error }; }; interface SidePanelProps { conversationId: string | null; uploadedFiles: string[]; isMobileWidth: boolean; sideBarOpen: boolean; } export default function AllConversations(props: SidePanelProps) { const [data, setData] = useState(null); const [organizedData, setOrganizedData] = useState(null); const [subsetOrganizedData, setSubsetOrganizedData] = useState(null); const { data: authenticatedData, error: authenticationError, isLoading: authenticationLoading, } = useAuthenticatedData(); const { data: chatSessions, isLoading } = 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((chatSessionMetadata) => { const chatDate = new Date(chatSessionMetadata.updated); 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(chatSessionMetadata); // Add to subsetOrganizedData if less than 8 if (numAdded < 8) { if (!subsetOrganizedData[timeGrouping]) { subsetOrganizedData[timeGrouping] = []; } subsetOrganizedData[timeGrouping].push(chatSessionMetadata); numAdded++; } }); setSubsetOrganizedData(subsetOrganizedData); setOrganizedData(groupedData); } }, [chatSessions]); if (isLoading) { return ( Conversations {Array.from({ length: 5 }).map((_, index) => ( ))} ); } return (
{authenticatedData && ( <> Conversations
{props.sideBarOpen && ( )} )}
); }