mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-02 21:19:12 +00:00
997 lines
38 KiB
TypeScript
997 lines
38 KiB
TypeScript
"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<string[]>("/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<string[]>([]);
|
|
const [addedFiles, setAddedFiles] = useState<string[]>([]);
|
|
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 <div>Failed to load files</div>;
|
|
if (selectedFilesError) return <div>Failed to load selected files</div>;
|
|
if (!files) return <InlineLoading />;
|
|
if (!selectedFiles && props.conversationId) return <InlineLoading />;
|
|
|
|
const FilesMenuCommandBox = () => {
|
|
return (
|
|
<Command>
|
|
<CommandInput placeholder="Find file" />
|
|
<CommandList>
|
|
<CommandEmpty>
|
|
No results found. <Link href="/search">Try advanced search</Link>.
|
|
</CommandEmpty>
|
|
{usingConversationContext && (
|
|
<CommandGroup heading="Quick">
|
|
<CommandItem
|
|
onSelect={() => {
|
|
removeAllFiles();
|
|
}}
|
|
>
|
|
<Trash className="h-4 w-4 mr-2" />
|
|
<span>Clear all</span>
|
|
</CommandItem>
|
|
<CommandItem
|
|
onSelect={() => {
|
|
addAllFiles();
|
|
}}
|
|
>
|
|
<FolderPlus className="h-4 w-4 mr-2" />
|
|
<span>Select all</span>
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
)}
|
|
<CommandGroup heading="Manage files">
|
|
<CommandItem>
|
|
<Link href="/settings">Settings</Link>
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
<CommandGroup
|
|
heading={`${usingConversationContext ? "Configure files" : "Synced files"}`}
|
|
>
|
|
{addedFiles.length == 0 && (
|
|
<CommandItem>
|
|
<Link href="/settings">Upload documents</Link>
|
|
</CommandItem>
|
|
)}
|
|
{unfilteredFiles.map((filename: string) =>
|
|
Array.isArray(addedFiles) && addedFiles.includes(filename) ? (
|
|
<CommandItem
|
|
key={filename}
|
|
value={filename}
|
|
className="bg-accent text-accent-foreground mb-1"
|
|
onSelect={(value) => {
|
|
if (!usingConversationContext) return;
|
|
|
|
modifyFileFilterForConversation(
|
|
props.conversationId,
|
|
[value],
|
|
setAddedFiles,
|
|
"remove",
|
|
);
|
|
}}
|
|
>
|
|
<Check className="h-4 w-4 mr-2" />
|
|
<span className="break-all">{filename}</span>
|
|
</CommandItem>
|
|
) : (
|
|
<CommandItem
|
|
key={filename}
|
|
className="mb-1"
|
|
value={filename}
|
|
onSelect={(value) => {
|
|
if (!usingConversationContext) return;
|
|
|
|
modifyFileFilterForConversation(
|
|
props.conversationId,
|
|
[value],
|
|
setAddedFiles,
|
|
"add",
|
|
);
|
|
}}
|
|
>
|
|
<span className="break-all">{filename}</span>
|
|
</CommandItem>
|
|
),
|
|
)}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
);
|
|
};
|
|
|
|
if (props.isMobileWidth) {
|
|
return (
|
|
<>
|
|
<Drawer>
|
|
<DrawerTrigger className="bg-background border border-muted p-4 rounded-2xl my-8 text-left inline-flex items-center justify-between w-full">
|
|
Manage Files <ArrowRight className="h-4 w-4 mx-2" />
|
|
</DrawerTrigger>
|
|
<DrawerContent>
|
|
<DrawerHeader>
|
|
<DrawerTitle>Files</DrawerTitle>
|
|
<DrawerDescription>
|
|
{usingConversationContext
|
|
? "Manage files for this conversation"
|
|
: "Shared files"}
|
|
</DrawerDescription>
|
|
</DrawerHeader>
|
|
<div className={`${styles.panelWrapper}`}>
|
|
<FilesMenuCommandBox />
|
|
</div>
|
|
<DrawerFooter>
|
|
<DrawerClose>
|
|
<Button variant="outline">Done</Button>
|
|
</DrawerClose>
|
|
</DrawerFooter>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
<PopoverTrigger asChild>
|
|
<div className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl my-8">
|
|
<div className="flex items-center justify-between space-x-4">
|
|
<h4 className="text-sm font-semibold">
|
|
{usingConversationContext ? "Manage Context" : "Files"}
|
|
<p>
|
|
{usingConversationContext ? (
|
|
<span className="text-muted-foreground text-xs">
|
|
Using{" "}
|
|
{addedFiles.length == 0
|
|
? files.length
|
|
: addedFiles.length}{" "}
|
|
files
|
|
</span>
|
|
) : (
|
|
<span className="text-muted-foreground text-xs">
|
|
Shared{" "}
|
|
{addedFiles.length == 0
|
|
? files.length
|
|
: addedFiles.length}{" "}
|
|
files
|
|
</span>
|
|
)}
|
|
</p>
|
|
</h4>
|
|
<Button variant="ghost" size="sm" className="w-9 p-0">
|
|
{isOpen ? (
|
|
<ArrowDown className="h-4 w-4" />
|
|
) : (
|
|
<ArrowRight className="h-4 w-4" />
|
|
)}
|
|
<span className="sr-only">Toggle</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</PopoverTrigger>
|
|
<PopoverContent className={`mx-2`}>
|
|
<FilesMenuCommandBox />
|
|
</PopoverContent>
|
|
</Popover>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<div>
|
|
{props.sideBarOpen && (
|
|
<ScrollArea>
|
|
<ScrollAreaScrollbar
|
|
orientation="vertical"
|
|
className="h-full w-2.5 border-l border-l-transparent p-[1px]"
|
|
/>
|
|
<div className="p-0 m-0">
|
|
{props.subsetOrganizedData != null &&
|
|
Object.keys(props.subsetOrganizedData)
|
|
.filter((tg) => tg !== "All Time")
|
|
.map((timeGrouping) => (
|
|
<div key={timeGrouping} className={`my-1`}>
|
|
<div className={`text-muted-foreground text-xs p-[0.5rem]`}>
|
|
{timeGrouping}
|
|
</div>
|
|
{props.subsetOrganizedData &&
|
|
props.subsetOrganizedData[timeGrouping].map(
|
|
(chatHistory) => (
|
|
<ChatSession
|
|
updated={chatHistory.updated}
|
|
created={chatHistory.created}
|
|
compressed={true}
|
|
key={chatHistory.conversation_id}
|
|
conversation_id={
|
|
chatHistory.conversation_id
|
|
}
|
|
slug={chatHistory.slug}
|
|
agent_name={chatHistory.agent_name}
|
|
agent_color={chatHistory.agent_color}
|
|
agent_icon={chatHistory.agent_icon}
|
|
/>
|
|
),
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
)}
|
|
{props.data && props.data.length > 5 && (
|
|
<ChatSessionsModal data={props.organizedData} sideBarOpen={props.sideBarOpen} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<Dialog open={isRenaming} onOpenChange={(open) => setIsRenaming(open)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Set a new title for the conversation</DialogTitle>
|
|
<DialogDescription>
|
|
This will help you identify the conversation easily, and also help you
|
|
search for it later.
|
|
</DialogDescription>
|
|
<Input
|
|
value={renamedTitle}
|
|
onChange={(e) => setRenamedTitle(e.target.value)}
|
|
/>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button
|
|
onClick={() => {
|
|
renameConversation(props.conversationId, renamedTitle);
|
|
props.setTitle(renamedTitle);
|
|
setIsRenaming(false);
|
|
}}
|
|
type="submit"
|
|
>
|
|
Rename
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
if (isSharing || showShareUrl) {
|
|
if (shareUrl) {
|
|
navigator.clipboard.writeText(shareUrl);
|
|
}
|
|
return (
|
|
<Dialog
|
|
open={isSharing || showShareUrl}
|
|
onOpenChange={(open) => {
|
|
setShowShareUrl(open);
|
|
setIsSharing(open);
|
|
}}
|
|
>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Conversation Share URL</DialogTitle>
|
|
<DialogDescription>
|
|
Sharing this chat session will allow anyone with a link to view the
|
|
conversation.
|
|
<Input
|
|
className="w-full bg-accent text-accent-foreground rounded-md p-2 mt-2"
|
|
value={shareUrl}
|
|
readOnly={true}
|
|
/>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
{!showShareUrl && (
|
|
<Button
|
|
onClick={() => {
|
|
shareConversation(props.conversationId, setShareUrl);
|
|
setShowShareUrl(true);
|
|
}}
|
|
className="bg-orange-500"
|
|
disabled
|
|
>
|
|
<Spinner className="mr-2 h-4 w-4 animate-spin" />
|
|
Sharing
|
|
</Button>
|
|
)}
|
|
{showShareUrl && (
|
|
<Button
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(shareUrl);
|
|
}}
|
|
variant={"default"}
|
|
>
|
|
Copy
|
|
</Button>
|
|
)}
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
if (isDeleting) {
|
|
return (
|
|
<AlertDialog open={isDeleting} onOpenChange={(open) => setIsDeleting(open)}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete Conversation</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to delete this conversation? This action cannot be
|
|
undone.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => {
|
|
deleteConversation(props.conversationId);
|
|
setIsDeleting(false);
|
|
}}
|
|
className="bg-rose-500 hover:bg-rose-600"
|
|
>
|
|
Delete
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<div className="flex items-center gap-2">
|
|
{(props.sizing === "lg" || props.sizing === "md") && (
|
|
<Button
|
|
className="p-0 text-sm h-auto"
|
|
variant={"ghost"}
|
|
onClick={() => setIsSharing(true)}
|
|
>
|
|
<Share className={`${size}`} />
|
|
</Button>
|
|
)}
|
|
<DropdownMenu onOpenChange={(open) => setIsOpen(open)} open={isOpen}>
|
|
<DropdownMenuTrigger asChild>
|
|
{props.sizing === "lg" || props.sizing === "md" ? (
|
|
<Button className="p-0 text-sm h-auto" variant={"ghost"}>
|
|
<DotsThreeVertical className={`${size}`} />
|
|
</Button>
|
|
) : (
|
|
<SidebarMenuAction asChild>
|
|
<DotsThreeVertical className={`${size}`} />
|
|
</SidebarMenuAction>
|
|
)}
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent side="right" align="start">
|
|
<DropdownMenuItem onClick={() => setIsRenaming(true)}>
|
|
<span className="flex items-center">
|
|
<Pencil className={`mr-2 ${size}`} />
|
|
Rename
|
|
</span>
|
|
</DropdownMenuItem>
|
|
{props.sizing === "sm" && (
|
|
<DropdownMenuItem onClick={() => setIsSharing(true)}>
|
|
<span className="flex items-center">
|
|
<Share className={`mr-2 ${size}`} />
|
|
Share
|
|
</span>
|
|
</DropdownMenuItem>
|
|
)}
|
|
<DropdownMenuItem onClick={() => setIsDeleting(true)}>
|
|
<span className="flex items-center">
|
|
<Trash className={`mr-2 ${size}`} />
|
|
Delete
|
|
</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<SidebarMenuItem
|
|
onMouseEnter={() => 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" : ""}`}
|
|
>
|
|
<SidebarMenuButton asChild>
|
|
<Link
|
|
href={`/chat?conversationId=${props.conversation_id}`}
|
|
className="flex items-center gap-2 no-underline"
|
|
>
|
|
<p
|
|
className={`${styles.session} ${props.compressed ? styles.compressed : styles.expanded}`}
|
|
>
|
|
{title}
|
|
</p>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
<ChatSessionActionMenu
|
|
conversationId={props.conversation_id}
|
|
setTitle={setTitle}
|
|
sizing="sm"
|
|
/>
|
|
</SidebarMenuItem>
|
|
);
|
|
}
|
|
|
|
interface ChatSessionsModalProps {
|
|
data: GroupedChatHistory | null;
|
|
sideBarOpen: boolean;
|
|
}
|
|
|
|
interface AgentStyle {
|
|
color: string;
|
|
icon: string;
|
|
}
|
|
|
|
function ChatSessionsModal({ data, sideBarOpen }: ChatSessionsModalProps) {
|
|
const [agentsFilter, setAgentsFilter] = useState<string[]>([]);
|
|
const [agentOptions, setAgentOptions] = useState<string[]>([]);
|
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
|
|
|
const [agentNameToStyleMap, setAgentNameToStyleMap] = useState<Record<string, AgentStyle>>({});
|
|
|
|
useEffect(() => {
|
|
if (data) {
|
|
const agents: string[] = [];
|
|
let agentNameToStyleMapLocal: Record<string, AgentStyle> = {};
|
|
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 (
|
|
<Dialog>
|
|
<DialogTrigger
|
|
className={`flex text-left text-medium text-gray-500 hover:text-gray-300 cursor-pointer my-1 text-sm ${sideBarOpen ? "p-[0.5rem] " : "p-[0.1rem]"}`}
|
|
>
|
|
<span className="flex items-center gap-1">
|
|
<ChatsCircle className="inline h-4 w-4 mr-1" />
|
|
{sideBarOpen ? "All Conversations" : ""}
|
|
</span>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>All Conversations</DialogTitle>
|
|
<DialogDescription className="p-0">
|
|
<div className="flex flex-row justify-between mt-2 gap-2">
|
|
<Input
|
|
value={searchQuery}
|
|
onChange={(e) => {
|
|
setSearchQuery(e.target.value);
|
|
}}
|
|
placeholder="Search conversations"
|
|
/>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className={`p-0 px-1 ${agentsFilter.length > 0 ? "bg-muted text-muted-foreground" : "bg-inherit"} `}
|
|
>
|
|
<FunnelSimple />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuLabel>Agents</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{agentOptions.map((agent) => (
|
|
<DropdownMenuCheckboxItem
|
|
key={agent}
|
|
onSelect={(e) => e.preventDefault()}
|
|
checked={agentsFilter.includes(agent)}
|
|
onCheckedChange={(checked) => {
|
|
if (checked) {
|
|
setAgentsFilter([...agentsFilter, agent]);
|
|
} else {
|
|
setAgentsFilter(
|
|
agentsFilter.filter((a) => a !== agent),
|
|
);
|
|
}
|
|
}}
|
|
>
|
|
<div className="flex items-center justify-center px-1">
|
|
{getIconFromIconName(
|
|
agentNameToStyleMap[agent]?.icon,
|
|
agentNameToStyleMap[agent]?.color,
|
|
)}
|
|
<div className="break-words">{agent}</div>
|
|
</div>
|
|
</DropdownMenuCheckboxItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
<ScrollArea className="h-[500px] py-4">
|
|
{filteredData &&
|
|
Object.keys(filteredData).map((timeGrouping) => (
|
|
<div key={timeGrouping}>
|
|
<div
|
|
className={`text-muted-foreground text-sm font-bold p-[0.5rem] `}
|
|
>
|
|
{timeGrouping}
|
|
</div>
|
|
{filteredData[timeGrouping].map((chatHistory) => (
|
|
<ChatSession
|
|
updated={chatHistory.updated}
|
|
created={chatHistory.created}
|
|
compressed={false}
|
|
key={chatHistory.conversation_id}
|
|
conversation_id={chatHistory.conversation_id}
|
|
slug={chatHistory.slug}
|
|
agent_name={chatHistory.agent_name}
|
|
agent_color={chatHistory.agent_color}
|
|
agent_icon={chatHistory.agent_icon}
|
|
/>
|
|
))}
|
|
</div>
|
|
))}
|
|
</ScrollArea>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
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<ChatHistory[]>(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<ChatHistory[] | null>(null);
|
|
const [organizedData, setOrganizedData] = useState<GroupedChatHistory | null>(null);
|
|
const [subsetOrganizedData, setSubsetOrganizedData] = useState<GroupedChatHistory | null>(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 (
|
|
<SidebarGroup className="p-0 m-0">
|
|
<SidebarGroupLabel className="!p-0 m-0 px-0">Conversations</SidebarGroupLabel>
|
|
{Array.from({ length: 5 }).map((_, index) => (
|
|
<SidebarMenuItem key={index} className="p-0 list-none m-0">
|
|
<SidebarMenuSkeleton showIcon />
|
|
</SidebarMenuItem>
|
|
))}
|
|
</SidebarGroup>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SidebarGroup>
|
|
<div className={`flex justify-between flex-col`}>
|
|
{authenticatedData && (
|
|
<>
|
|
<SidebarGroupLabel className="!p-0 m-0 px-0">
|
|
Conversations
|
|
</SidebarGroupLabel>
|
|
<div
|
|
className={`${props.sideBarOpen ? "border-l-2 border-light-blue-500 border-opacity-25 " : ""}`}
|
|
>
|
|
<SessionsAndFiles
|
|
subsetOrganizedData={subsetOrganizedData}
|
|
organizedData={organizedData}
|
|
data={data}
|
|
uploadedFiles={props.uploadedFiles}
|
|
userProfile={authenticatedData}
|
|
conversationId={props.conversationId}
|
|
isMobileWidth={props.isMobileWidth}
|
|
sideBarOpen={props.sideBarOpen}
|
|
/>
|
|
</div>
|
|
{props.sideBarOpen && (
|
|
<FilesMenu
|
|
conversationId={props.conversationId}
|
|
uploadedFiles={props.uploadedFiles}
|
|
isMobileWidth={props.isMobileWidth}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</SidebarGroup>
|
|
);
|
|
}
|