Make most major changes for an updated chat UI (#843)

- Updated references panel
- Use subtle coloring for chat cards
- Chat streaming with train of thought
- Side panel with limited sessions, expandable
- Manage conversation file filters easily from the side panel
- Updated nav menu, easily go to agents/automations/profile
- Upload data from the chat UI (on click attachment icon)
- Slash command pop-up menu, scrollable and selectable
- Dark mode-enabled
- Mostly mobile friendly
This commit is contained in:
sabaimran
2024-07-14 10:48:06 -07:00
committed by GitHub
parent 02658ad4fd
commit 06dce4729b
46 changed files with 5652 additions and 954 deletions

View File

@@ -2,150 +2,787 @@
import styles from "./sidePanel.module.css";
import { useEffect, useState } from "react";
import { Suspense, useEffect, useState } from "react";
import { UserProfile } from "@/app/common/auth";
import { UserProfile, useAuthenticatedData } from "@/app/common/auth";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import Link from "next/link";
import useSWR from "swr";
import Image from "next/image";
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 } from "@phosphor-icons/react";
interface ChatHistory {
conversation_id: string;
slug: string;
agent_name: string;
agent_avatar: string;
compressed: boolean;
created: string;
}
function ChatSession(prop: ChatHistory) {
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, buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
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";
// 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<string[]>(props.conversationId ? '/api/config/data/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<string[]>([]);
const [addedFiles, setAddedFiles] = useState<string[]>([]);
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 <div>Failed to load files</div>;
if (selectedFilesError) return <div>Failed to load selected files</div>;
if (!files) return <InlineLoading />;
if (!selectedFiles) return <InlineLoading />;
const FilesMenuCommandBox = () => {
return (
<Command>
<CommandInput placeholder="Find file" />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<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="Configure files">
{unfilteredFiles.map((filename: string) => (
addedFiles && addedFiles.includes(filename) ?
<CommandItem
key={filename}
value={filename}
className="bg-accent text-accent-foreground mb-1"
onSelect={(value) => {
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) => {
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>Manage files for this conversation</DrawerDescription>
</DrawerHeader>
<div className={`${styles.panelWrapper}`}>
<FilesMenuCommandBox />
</div>
<DrawerFooter>
<DrawerClose>
<Button variant="outline">Done</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
</>
);
}
return (
<div key={prop.conversation_id} className={styles.session}>
<Link href={`/chat?conversationId=${prop.conversation_id}`}>
<p className={styles.session}>{prop.slug || "New Conversation 🌱"}</p>
<>
<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">
Manage Context
<p>
<span className="text-muted-foreground text-xs">Using {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 {
webSocketConnected?: boolean;
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 (
<>
<ScrollArea className="h-[40vh]">
<ScrollAreaScrollbar orientation="vertical" className="h-full w-2.5 border-l border-l-transparent p-[1px]" />
<div className={styles.sessionsList}>
{props.subsetOrganizedData != null && Object.keys(props.subsetOrganizedData).map((timeGrouping) => (
<div key={timeGrouping} className={`my-4`}>
<div className={`text-muted-foreground text-sm font-bold p-[0.5rem] `}>
{timeGrouping}
</div>
{props.subsetOrganizedData && props.subsetOrganizedData[timeGrouping].map((chatHistory) => (
<ChatSession
created={chatHistory.created}
compressed={true}
key={chatHistory.conversation_id}
conversation_id={chatHistory.conversation_id}
slug={chatHistory.slug}
agent_avatar={chatHistory.agent_avatar}
agent_name={chatHistory.agent_name} />
))}
</div>
))}
</div>
</ScrollArea>
{
(props.data && props.data.length > 5) && (
<ChatSessionsModal data={props.organizedData} />
)
}
<FilesMenu conversationId={props.conversationId} uploadedFiles={props.uploadedFiles} isMobileWidth={props.isMobileWidth} />
{props.userProfile &&
<UserProfileComponent userProfile={props.userProfile} webSocketConnected={props.webSocketConnected} collapsed={false} />
}</>
)
}
interface ChatSessionActionMenuProps {
conversationId: string;
}
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]);
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);
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>
)
}
return (
<DropdownMenu
onOpenChange={(open) => setIsOpen(open)}
open={isOpen}>
<DropdownMenuTrigger><DotsThreeVertical className="h-4 w-4" /></DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => setIsRenaming(true)}>
<Pencil className="mr-2 h-4 w-4" />Rename
</Button>
</DropdownMenuItem>
<DropdownMenuItem>
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => setIsSharing(true)}>
<Share className="mr-2 h-4 w-4" />Share
</Button>
</DropdownMenuItem>
<DropdownMenuItem>
<Button className="p-0 text-sm h-auto text-rose-300 hover:text-rose-400" variant={'ghost'} onClick={() => setIsDeleting(true)}>
<Trash className="mr-2 h-4 w-4" />Delete
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
function ChatSession(props: ChatHistory) {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
key={props.conversation_id}
className={`${styles.session} ${props.compressed ? styles.compressed : '!max-w-full'} ${isHovered ? `${styles.sessionHover}` : ''}`}>
<Link href={`/chat?conversationId=${props.conversation_id}`}>
<p className={styles.session}>{props.slug || "New Conversation 🌱"}</p>
</Link>
<ChatSessionActionMenu conversationId={props.conversation_id} />
</div>
);
}
interface ChatSessionsModalProps {
data: ChatHistory[];
setIsExpanded: React.Dispatch<React.SetStateAction<boolean>>;
data: GroupedChatHistory | null;
}
function ChatSessionsModal({data, setIsExpanded}: ChatSessionsModalProps) {
function ChatSessionsModal({ data }: ChatSessionsModalProps) {
return (
<div className={styles.modalSessionsList}>
<div className={styles.content}>
{data.map((chatHistory) => (
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} />
))}
<button className={styles.showMoreButton} onClick={() => setIsExpanded(false)}>
Close
</button>
</div>
</div>
<Dialog>
<DialogTrigger
className="flex text-left text-medium text-gray-500 hover:text-gray-300 cursor-pointer my-4 text-sm p-[0.5rem]">
<span className="mr-2">See All <ArrowRight className="h-4 w-4" /></span>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>All Conversations</DialogTitle>
<DialogDescription
className="p-0">
<ScrollArea className="h-[500px] p-4">
{data && Object.keys(data).map((timeGrouping) => (
<div key={timeGrouping}>
<div className={`text-muted-foreground text-sm font-bold p-[0.5rem] `}>
{timeGrouping}
</div>
{data[timeGrouping].map((chatHistory) => (
<ChatSession
created={chatHistory.created}
compressed={false}
key={chatHistory.conversation_id}
conversation_id={chatHistory.conversation_id}
slug={chatHistory.slug}
agent_avatar={chatHistory.agent_avatar}
agent_name={chatHistory.agent_name} />
))}
</div>
))}
</ScrollArea>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
);
}
export default function SidePanel() {
interface UserProfileProps {
userProfile: UserProfile;
webSocketConnected?: boolean;
collapsed: boolean;
}
function UserProfileComponent(props: UserProfileProps) {
if (props.collapsed) {
return (
<div className={styles.profile}>
<Avatar className="h-7 w-7">
<AvatarImage src={props.userProfile.photo} alt="user profile" />
<AvatarFallback>
{props.userProfile.username[0]}
</AvatarFallback>
</Avatar>
</div>
);
}
return (
<div className={styles.profile}>
<Link href="/config" target="_blank" rel="noopener noreferrer">
<Avatar>
<AvatarImage src={props.userProfile.photo} alt="user profile" />
<AvatarFallback>
{props.userProfile.username[0]}
</AvatarFallback>
</Avatar>
</Link>
<div className={styles.profileDetails}>
<p>{props.userProfile?.username}</p>
{/* Connected Indicator */}
<div className="flex gap-2 items-center">
<div className={`inline-flex h-4 w-4 rounded-full opacity-75 ${props.webSocketConnected ? 'bg-green-500' : 'bg-rose-500'}`}></div>
<p className="text-muted-foreground text-sm">
{props.webSocketConnected ? "Connected" : "Disconnected"}
</p>
</div>
</div>
</div>
);
}
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<ChatHistory[]>(url, fetchChatHistory);
return {
data,
isLoading: !error && !data,
isError: error,
};
};
interface SidePanelProps {
webSocketConnected?: boolean;
conversationId: string | null;
uploadedFiles: string[];
}
export default function SidePanel(props: SidePanelProps) {
const [data, setData] = useState<ChatHistory[] | null>(null);
const [dataToShow, setDataToShow] = useState<ChatHistory[] | null>(null);
const [isLoading, setLoading] = useState(true)
const [organizedData, setOrganizedData] = useState<GroupedChatHistory | null>(null);
const [subsetOrganizedData, setSubsetOrganizedData] = useState<GroupedChatHistory | null>(null);
const [enabled, setEnabled] = useState(false);
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
const authenticatedData = useAuthenticatedData();
const { data: chatSessions } = useChatSessionsFetchRequest(authenticatedData ? `/api/chat/sessions` : '');
useEffect(() => {
fetch('/api/chat/sessions', { method: 'GET' })
.then(response => response.json())
.then((data: ChatHistory[]) => {
setLoading(false);
// Render chat options, if any
if (data) {
setData(data);
setDataToShow(data.slice(0, 5));
const [isMobileWidth, setIsMobileWidth] = useState(false);
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++;
}
})
.catch(err => {
console.error(err);
return;
});
fetch('/api/v1/user', { method: 'GET' })
.then(response => response.json())
.then((data: UserProfile) => {
setUserProfile(data);
})
.catch(err => {
console.error(err);
return;
});
}, []);
return (
<div className={`${styles.panel}`}>
setSubsetOrganizedData(subsetOrganizedData);
setOrganizedData(groupedData);
}
}, [chatSessions]);
useEffect(() => {
if (window.innerWidth < 768) {
setIsMobileWidth(true);
}
window.addEventListener('resize', () => {
setIsMobileWidth(window.innerWidth < 768);
});
}, []);
return (
<div className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed}`}>
<div className="flex items-start justify-between">
<Image src="/khoj-logo.svg"
alt="logo"
width={40}
height={40}
/>
{
authenticatedData &&
isMobileWidth ?
<Drawer>
<DrawerTrigger><ArrowRight className="h-4 w-4 mx-2" /></DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Sessions and Files</DrawerTitle>
<DrawerDescription>View all conversation sessions and manage conversation file filters</DrawerDescription>
</DrawerHeader>
<div className={`${styles.panelWrapper}`}>
<SessionsAndFiles
webSocketConnected={props.webSocketConnected}
setEnabled={setEnabled}
subsetOrganizedData={subsetOrganizedData}
organizedData={organizedData}
data={data}
uploadedFiles={props.uploadedFiles}
userProfile={authenticatedData}
conversationId={props.conversationId}
isMobileWidth={isMobileWidth}
/>
</div>
<DrawerFooter>
<DrawerClose>
<Button variant="outline">Done</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
:
<button className={styles.button} onClick={() => setEnabled(!enabled)}>
{enabled ? <ArrowLeft className="h-4 w-4" /> : <ArrowRight className="h-4 w-4 mx-2" />}
</button>
}
</div>
{
enabled ?
<div>
<div className={`${styles.expanded}`}>
<div className={`${styles.profile}`}>
{ userProfile &&
<div className={styles.profile}>
<img
className={styles.profile}
src={userProfile.photo}
alt="profile"
width={24}
height={24}
/>
<p>{userProfile?.username}</p>
</div>
}
</div>
<button className={styles.button} onClick={() => setEnabled(false)}>
{/* Push Close Icon */}
<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M8.70710678,12 L19.5,12 C19.7761424,12 20,12.2238576 20,12.5 C20,12.7761424 19.7761424,13 19.5,13 L8.70710678,13 L11.8535534,16.1464466 C12.0488155,16.3417088 12.0488155,16.6582912 11.8535534,16.8535534 C11.6582912,17.0488155 11.3417088,17.0488155 11.1464466,16.8535534 L7.14644661,12.8535534 C6.95118446,12.6582912 6.95118446,12.3417088 7.14644661,12.1464466 L11.1464466,8.14644661 C11.3417088,7.95118446 11.6582912,7.95118446 11.8535534,8.14644661 C12.0488155,8.34170876 12.0488155,8.65829124 11.8535534,8.85355339 L8.70710678,12 L8.70710678,12 Z M4,5.5 C4,5.22385763 4.22385763,5 4.5,5 C4.77614237,5 5,5.22385763 5,5.5 L5,19.5 C5,19.7761424 4.77614237,20 4.5,20 C4.22385763,20 4,19.7761424 4,19.5 L4,5.5 Z"></path> </g></svg>
</button>
<h3>Recent Conversations</h3>
</div>
<div className={styles.sessionsList}>
{dataToShow && dataToShow.map((chatHistory) => (
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} />
))}
</div>
{
(data && data.length > 5) && (
(isExpanded) ?
<ChatSessionsModal data={data} setIsExpanded={setIsExpanded} />
:
<button className={styles.showMoreButton} onClick={() => {
setIsExpanded(true);
}}>
Show All
</button>
)
}
</div>
:
<div>
<div className={`${styles.collapsed}`}>
{ userProfile &&
<div className={`${styles.profile}`}>
<img
className={styles.profile}
src={userProfile.photo}
alt="profile"
width={24}
height={24}
/>
</div>
}
<button className={styles.button} onClick={() => setEnabled(true)}>
{/* Pull Open Icon */}
<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M15.2928932,12 L12.1464466,8.85355339 C11.9511845,8.65829124 11.9511845,8.34170876 12.1464466,8.14644661 C12.3417088,7.95118446 12.6582912,7.95118446 12.8535534,8.14644661 L16.8535534,12.1464466 C17.0488155,12.3417088 17.0488155,12.6582912 16.8535534,12.8535534 L12.8535534,16.8535534 C12.6582912,17.0488155 12.3417088,17.0488155 12.1464466,16.8535534 C11.9511845,16.6582912 11.9511845,16.3417088 12.1464466,16.1464466 L15.2928932,13 L4.5,13 C4.22385763,13 4,12.7761424 4,12.5 C4,12.2238576 4.22385763,12 4.5,12 L15.2928932,12 Z M19,5.5 C19,5.22385763 19.2238576,5 19.5,5 C19.7761424,5 20,5.22385763 20,5.5 L20,19.5 C20,19.7761424 19.7761424,20 19.5,20 C19.2238576,20 19,19.7761424 19,19.5 L19,5.5 Z"></path> </g></svg>
</button>
</div>
</div>
authenticatedData && enabled &&
<div className={`${styles.panelWrapper}`}>
<SessionsAndFiles
webSocketConnected={props.webSocketConnected}
setEnabled={setEnabled}
subsetOrganizedData={subsetOrganizedData}
organizedData={organizedData}
data={data}
uploadedFiles={props.uploadedFiles}
userProfile={authenticatedData}
conversationId={props.conversationId}
isMobileWidth={isMobileWidth}
/>
</div>
}
</div>
);
{
!authenticatedData && enabled &&
<div className={`${styles.panelWrapper}`}>
<Link href="/">
<Button variant="ghost"><House className="h-4 w-4 mr-1" />Home</Button>
</Link>
<Link href="/">
<Button variant="ghost"><StackPlus className="h-4 w-4 mr-1" />New Conversation</Button>
</Link>
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`}> {/* Redirect to login page */}
<Button variant="default"><UserCirclePlus className="h-4 w-4 mr-1"/>Sign Up</Button>
</Link>
</div>
}
</div>
);
}

View File

@@ -2,9 +2,28 @@ div.session {
padding: 0.5rem;
margin-bottom: 0.25rem;
border-radius: 0.5rem;
color: var(--main-text-color);
cursor: pointer;
max-width: 14rem;
font-size: medium;
display: grid;
grid-template-columns: minmax(auto, 350px) 1fr;
}
div.compressed {
grid-template-columns: minmax(auto, 12rem) 1fr 1fr;
}
div.sessionHover {
background-color: hsla(var(--popover));
}
div.session:hover {
background-color: hsla(var(--popover));
color: hsla(var(--popover-foreground));
}
div.session a {
text-decoration: none;
}
button.button {
@@ -17,29 +36,23 @@ button.button {
}
button.showMoreButton {
background: var(--intense-green);
border: none;
color: var(--frosted-background-color);
border-radius: 0.5rem;
padding: 8px;
}
div.panel {
display: grid;
grid-auto-flow: row;
display: flex;
flex-direction: column;
padding: 1rem;
border-radius: 1rem;
background-color: var(--calm-blue);
color: var(--main-text-color);
height: 100%;
overflow-y: auto;
max-width: auto;
transition: background-color 0.5s;
}
div.expanded {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
background-color: hsla(var(--muted));
height: 100%;
}
div.collapsed {
@@ -47,14 +60,12 @@ div.collapsed {
grid-template-columns: 1fr;
}
div.session:hover {
background-color: var(--calmer-blue);
}
p.session {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
text-align: left;
font-size: small;
}
div.header {
@@ -62,10 +73,18 @@ div.header {
grid-template-columns: 1fr auto;
}
img.profile {
width: 24px;
height: 24px;
border-radius: 50%;
div.profile {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
align-items: center;
margin-top: auto;
}
div.panelWrapper {
display: grid;
grid-template-rows: auto 1fr auto auto;
height: 100%;
}
@@ -75,7 +94,7 @@ div.modalSessionsList {
left: 0;
width: 100%;
height: 100%;
background-color: var(--frosted-background-color);
background-color: hsla(var(--frosted-background-color));
z-index: 1;
display: flex;
justify-content: center;
@@ -86,7 +105,7 @@ div.modalSessionsList {
div.modalSessionsList div.content {
max-width: 80%;
max-height: 80%;
background-color: var(--frosted-background-color);
background-color: hsla(var(--frosted-background-color));
overflow: auto;
padding: 20px;
border-radius: 10px;
@@ -96,3 +115,33 @@ div.modalSessionsList div.session {
max-width: 100%;
text-overflow: ellipsis;
}
@media screen and (max-width: 768px) {
div.panel {
padding: 0.5rem;
position: absolute;
width: 100%;
}
div.expanded {
z-index: 1;
}
div.singleReference {
padding: 4px;
}
div.panelWrapper {
width: 100%;
}
div.session.compressed {
max-width: 100%;
grid-template-columns: minmax(auto, 350px) 1fr;
}
div.session {
max-width: 100%;
grid-template-columns: 200px 1fr;
}
}