Add basic implementation for chat side panel components

This commit is contained in:
sabaimran 2024-07-02 21:56:43 +05:30
parent 0ee7cc8c47
commit 8a6722ba97
13 changed files with 476 additions and 84 deletions

View file

@ -121,6 +121,7 @@ div.agentIndicator {
div.chatLayout {
gap: 0;
grid-template-columns: 1fr;
}
}

View file

@ -141,22 +141,22 @@ export default function Chat() {
return (
<div className={styles.main + " " + styles.chatLayout}>
<title>
{title}
Khoj AI - {title}
</title>
<Suspense fallback={<Loading />}>
<div className={styles.sidePanel}>
<SidePanel webSocketConnected={chatWS !== null} />
</div>
<div className={styles.chatBox}>
<NavMenu selected="Chat" title={title} />
<div className={styles.chatBoxBody}>
<ChatBodyData
chatOptionsData={chatOptionsData}
setTitle={setTitle}
onConversationIdChange={handleConversationIdChange} />
<Suspense fallback={<Loading />}>
<ChatBodyData
chatOptionsData={chatOptionsData}
setTitle={setTitle}
onConversationIdChange={handleConversationIdChange} />
</Suspense>
</div>
</div>
</Suspense>
</div>
)
}

View file

@ -118,7 +118,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
setReferencePanelData={setReferencePanelData}
setShowReferencePanel={setShowReferencePanel}
customClassName='fullHistory'
borderLeftColor='orange-400'
borderLeftColor='orange-500'
/>
))}
{

View file

@ -14,7 +14,7 @@ div.chatMessageWrapper {
div.khojfullHistory {
border-color: var(--border-color);
border-width: 1px;
padding-left: 24px;
padding-left: 4px;
}
div.youfullHistory {
@ -105,6 +105,7 @@ button.codeCopyButton:hover {
}
div.feedbackButtons img,
button.codeCopyButton img,
button.copyButton img {
width: 24px;
}

View file

@ -154,6 +154,7 @@ export default function ChatMessage(props: ChatMessageProps) {
useEffect(() => {
if (messageRef.current) {
const preElements = messageRef.current.querySelectorAll('pre > .hljs');
console.log("make copy button");
preElements.forEach((preElement) => {
const copyButton = document.createElement('button');
const copyImage = document.createElement('img');
@ -212,7 +213,8 @@ export default function ChatMessage(props: ChatMessageProps) {
classes.push(styles[chatMessage.by]);
if (chatMessage.by === "khoj") {
classes.push(`border-l-4 border-opacity-50 border-l-orange-500 border-l-${props.borderLeftColor}`);
const dynamicBorderColor = `border-l-${props.borderLeftColor}`;
classes.push(`border-l-4 border-opacity-50 border-l-orange-400 ${dynamicBorderColor}`);
}
return classes.join(' ');

View file

@ -1,26 +1,17 @@
import styles from './loading.module.css';
import { CircleNotch } from '@phosphor-icons/react';
export default function Loading() {
// return (
// <div className={`${styles.loading} h-[100vh] flex items-center justify-center`}>
// <button type="button" className="bg-indigo-500" disabled>
// Loading...
// <span className="relative flex h-3 w-3">
// <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
// <span className="relative inline-flex rounded-full h-3 w-3 bg-sky-500"></span>
// </span>
// </button>
// </div>
// )
return (
<div className={`${styles.loader} h-[100vh] flex items-center justify-center`}></div>
);
}
export function InlineLoading() {
return (
<div className="h-[100vh] flex items-center justify-center">
<h2 className="text-4xl text-black animate-bounce">
Loading...
</h2>
</div>
<button>
<CircleNotch className='animate-spin h-5 w-5 mr-3' />
</button>
)
}

View file

@ -78,7 +78,7 @@ export default function NavMenu(props: NavMenuProps) {
</DropdownMenuContent>
</DropdownMenu>
:
<Menubar className='items-top'>
<Menubar className='items-top inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground'>
<MenubarMenu>
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>
<MenubarTrigger>Chat</MenubarTrigger>

View file

@ -9,10 +9,19 @@ import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import Link from "next/link";
import useSWR from "swr";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { InlineLoading } from "../loading/loading";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
@ -20,6 +29,8 @@ import {
import { ScrollArea } from "@/components/ui/scroll-area";
import { ArrowRight, ArrowLeft, ArrowDown, Spinner } from "@phosphor-icons/react";
interface ChatHistory {
conversation_id: string;
slug: string;
@ -35,9 +46,21 @@ import {
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 { 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";
// Define a fetcher function
const fetcher = (url: string) => fetch(url).then((res) => res.json());
interface GroupedChatHistory {
[key: string]: ChatHistory[];
@ -47,7 +70,7 @@ function renameConversation(conversationId: string, newTitle: string) {
const editUrl = `/api/chat/title?client=web&conversation_id=${conversationId}&title=${newTitle}`;
fetch(editUrl, {
method: 'POST',
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
@ -62,7 +85,7 @@ function renameConversation(conversationId: string, newTitle: string) {
});
}
function shareConversation(conversationId: string) {
function shareConversation(conversationId: string, setShareUrl: (url: string) => void) {
const shareUrl = `/api/chat/share?client=web&conversation_id=${conversationId}`;
fetch(shareUrl, {
@ -74,6 +97,7 @@ function shareConversation(conversationId: string) {
.then(response => response.json())
.then(data => {
console.log(data);
setShareUrl(data.url);
})
.catch(err => {
console.error(err);
@ -82,7 +106,7 @@ function shareConversation(conversationId: string) {
}
function deleteConversation(conversationId: string) {
const deleteUrl = `/api/chat/delete?client=web&conversation_id=${conversationId}`;
const deleteUrl = `/api/chat/history?client=web&conversation_id=${conversationId}`;
fetch(deleteUrl, {
method: 'DELETE',
@ -100,28 +124,354 @@ function deleteConversation(conversationId: string) {
});
}
interface FilesMenuProps {
conversationId: string;
}
function FilesMenu(props: FilesMenuProps) {
// Use SWR to fetch files
const { data: files, error } = useSWR('/api/config/data/computer', fetcher);
const { data: selectedFiles, error: selectedFilesError } = useSWR(`/api/chat/conversation/file-filters/${props.conversationId}`, fetcher);
const [isOpen, setIsOpen] = useState(false);
const [searchInput, setSearchInput] = useState('');
const [filteredFiles, setFilteredFiles] = useState<string[]>([]);
// Function to handle file click
const handleFileClick = (filename: string) => {
console.log(`File clicked: ${filename}`);
// Implement the logic you want to execute on file click
};
useEffect(() => {
if (!files) return;
if (searchInput === '') {
setFilteredFiles(files);
} else {
let filteredFiles = files.filter((filename: string) => filename.toLowerCase().includes(searchInput.toLowerCase()));
setFilteredFiles(filteredFiles);
}
}, [searchInput, files]);
if (error) return <div>Failed to load files</div>;
if (!files) return <InlineLoading />;
return (
<>
{/* <ScrollArea className="h-[40vh] w-[14rem]">
<ul className="indexed-files">
{files.length === 0 ? (
<div className="no-files-message">
<a className="inline-chat-link" href="https://docs.khoj.dev/category/clients/">How to upload files</a>
</div>
) : (
files.map((filename: string) => (
<li key={filename} className="fileName" id={filename} onClick={() => handleFileClick(filename)}>
{filename}
</li>
))
)}
</ul>
{files.length > 0 && <button className="file-toggle-button" style={{ display: "block" }}>Toggle Files</button>}
</ScrollArea> */}
<Popover
open={isOpen}
onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<div
className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl">
<div className="flex items-center justify-between space-x-4">
<h4 className="text-sm font-semibold">
Manage Files
<p>
<span className="text-muted-foreground text-xs">Using {files.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="w-80 mx-2">
<Input
placeholder="Find file"
className="rounded-md border-none py-2 text-sm text-wrap break-words my-2 bg-accent text-accent-foreground"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)} />
{
filteredFiles.length === 0 && (
<div className="rounded-md border-none py-2 text-sm text-wrap break-words">
No files found
</div>
)
}
{
filteredFiles.map((filename: string) => (
<div key={filename} className="rounded-md border-none py-2 text-sm text-wrap break-words">
{filename}
</div>
))
}
</PopoverContent>
</Popover>
{/* <Collapsible
open={isOpen}
onOpenChange={setIsOpen}
className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl"
>
<div className="flex items-center justify-between space-x-4">
<h4 className="text-sm font-semibold">
Manage Files
<p>
<span className="text-muted-foreground text-xs">Using {files.length} files</span>
</p>
</h4>
<CollapsibleTrigger asChild>
<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>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="space-y-2 w-min min-w-[14rem]">
<Input
placeholder="Find file"
className="rounded-md border-none py-2 text-sm text-wrap break-words my-2 bg-accent text-accent-foreground"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)} />
{
filteredFiles.length === 0 && (
<div className="rounded-md border-none py-2 text-sm text-wrap break-words">
No files found
</div>
)
}
{
filteredFiles.map((filename: string) => (
<div key={filename} className="rounded-md border-none py-2 text-sm text-wrap break-words">
{filename}
</div>
))
}
</CollapsibleContent>
</Collapsible> */}
</>
)
}
interface SessionsAndFilesProps {
webSocketConnected?: boolean;
setEnabled: (enabled: boolean) => void;
subsetOrganizedData: GroupedChatHistory | null;
organizedData: GroupedChatHistory | null;
data: ChatHistory[] | null;
userProfile: UserProfile | null;
}
function SessionsAndFiles(props: SessionsAndFilesProps) {
return (
<>
<div className={`${styles.expanded}`}>
<button className={styles.button} onClick={() => props.setEnabled(false)}>
<ArrowLeft />
</button>
</div>
<ScrollArea className="h-[40vh] w-[14rem]">
<div className={styles.sessionsList}>
{props.subsetOrganizedData != null && Object.keys(props.subsetOrganizedData).map((agentName) => (
<div key={agentName} className={`my-4`}>
<h3 className={`grid grid-flow-col auto-cols-max gap-2 my-4 font-bold text-sm`}>
{
props.subsetOrganizedData &&
<img src={props.subsetOrganizedData[agentName][0].agent_avatar} alt={agentName} width={24} height={24} />
}
{agentName}
</h3>
{props.subsetOrganizedData && props.subsetOrganizedData[agentName].map((chatHistory) => (
<ChatSession
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 />
{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);
}}
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);
console.log("shared");
}}
variant={'default'}>Copy</Button>
}
</DialogFooter>
</DialogContent>
</Dialog>
)
}
if (isDeleting) {
console.log("Deleting");
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>
<DropdownMenu
onOpenChange={(open) => setIsOpen(open)}
open={isOpen}>
<DropdownMenuTrigger>:</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => renameConversation(props.conversationId, 'New Title')}>
<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={() => shareConversation(props.conversationId)}>
<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={() => deleteConversation(props.conversationId)}>
<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>
@ -151,27 +501,24 @@ interface ChatSessionsModalProps {
data: GroupedChatHistory | null;
}
// function ConversationList()
function ChatSessionsModal({ data }: ChatSessionsModalProps) {
return (
<Dialog>
<DialogTrigger
className="flex text-left text-medium text-gray-500 hover:text-gray-900 cursor-pointer">
className="flex text-left text-medium text-gray-500 hover:text-gray-900 cursor-pointer my-4 text-sm">
Show All
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>All Conversations</DialogTitle>
<DialogDescription>
<ScrollArea className="h-[500px] w-[450px] rounded-md border p-4">
<ScrollArea className="h-[500px] w-[450px] p-4">
{data && Object.keys(data).map((agentName) => (
<div key={agentName}>
<h3 className={`grid grid-flow-col auto-cols-max gap-2`}>
<div className={`grid grid-flow-col auto-cols-max gap-2`}>
<img src={data[agentName][0].agent_avatar} alt={agentName} width={24} height={24} />
{agentName}
</h3>
</div>
{data[agentName].map((chatHistory) => (
<ChatSession
compressed={false}
@ -201,7 +548,7 @@ function UserProfileComponent(props: UserProfileProps) {
if (props.collapsed) {
return (
<div className={styles.profile}>
<Avatar>
<Avatar className="h-7 w-7">
<AvatarImage src={props.userProfile.photo} alt="user profile" />
<AvatarFallback>
{props.userProfile.username[0]}
@ -316,52 +663,24 @@ export default function SidePanel(props: SidePanelProps) {
{
enabled ?
<div className={`${styles.panelWrapper}`}>
<div className={`${styles.expanded}`}>
<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>
</div>
<ScrollArea className="h-[40vh] w-[14rem]">
<div className={styles.sessionsList}>
{subsetOrganizedData && Object.keys(subsetOrganizedData).map((agentName) => (
<div key={agentName} className={`my-4`}>
<h3 className={`grid grid-flow-col auto-cols-max gap-2 my-4 font-bold text-sm`}>
<img src={subsetOrganizedData[agentName][0].agent_avatar} alt={agentName} width={24} height={24} />
{agentName}
</h3>
{subsetOrganizedData[agentName].map((chatHistory) => (
<ChatSession
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>
{
(data && data.length > 5) && (
<ChatSessionsModal data={organizedData} />
)
}
{userProfile &&
<UserProfileComponent userProfile={userProfile} webSocketConnected={props.webSocketConnected} collapsed={false} />
}
<SessionsAndFiles
webSocketConnected={props.webSocketConnected}
setEnabled={setEnabled}
subsetOrganizedData={subsetOrganizedData}
organizedData={organizedData}
data={data}
userProfile={userProfile}
/>
</div>
:
<div>
<div className={`${styles.collapsed}`}>
{ userProfile &&
<button className={styles.button} onClick={() => setEnabled(true)}>
<ArrowRight />
</button>
{userProfile &&
<UserProfileComponent userProfile={userProfile} webSocketConnected={props.webSocketConnected} collapsed={true} />
}
<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>
}

View file

@ -79,7 +79,6 @@ div.profile {
grid-template-columns: auto 1fr;
gap: 1rem;
align-items: center;
align-self: flex-end;
margin-top: auto;
}

View file

@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View file

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View file

@ -20,11 +20,13 @@
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@types/katex": "^0.16.7",

View file

@ -535,6 +535,20 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-collapsible@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz#4d49ddcc7b7d38f6c82f1fd29674f6fab5353e77"
integrity sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-presence" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-collection@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed"
@ -692,6 +706,27 @@
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-visually-hidden" "1.1.0"
"@radix-ui/react-popover@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.1.tgz#604b783cdb3494ed4f16a58c17f0e81e61ab7775"
integrity sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-dismissable-layer" "1.1.0"
"@radix-ui/react-focus-guards" "1.1.0"
"@radix-ui/react-focus-scope" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-popper" "1.2.0"
"@radix-ui/react-portal" "1.1.1"
"@radix-ui/react-presence" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.7"
"@radix-ui/react-popper@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a"