mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-10-05 17:53:54 +02:00
359 lines
11 KiB
TypeScript
359 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useRef } from "react";
|
|
import { Folder } from "./interfaces";
|
|
import { ChatSessionDisplay } from "../sessionSidebar/ChatSessionDisplay"; // Ensure this is correctly imported
|
|
import {
|
|
FiChevronDown,
|
|
FiChevronRight,
|
|
FiFolder,
|
|
FiEdit2,
|
|
FiCheck,
|
|
FiX,
|
|
FiTrash, // Import the trash icon
|
|
} from "react-icons/fi";
|
|
import { BasicSelectable } from "@/components/BasicClickable";
|
|
import {
|
|
addChatToFolder,
|
|
deleteFolder,
|
|
updateFolderName,
|
|
} from "./FolderManagement";
|
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
|
import { useRouter } from "next/navigation";
|
|
import { CHAT_SESSION_ID_KEY } from "@/lib/drag/constants";
|
|
import Cookies from "js-cookie";
|
|
import { Popover } from "@/components/popover/Popover";
|
|
import { ChatSession } from "../interfaces";
|
|
import { useChatContext } from "@/components/context/ChatContext";
|
|
|
|
const FolderItem = ({
|
|
folder,
|
|
currentChatId,
|
|
isInitiallyExpanded,
|
|
initiallySelected,
|
|
showShareModal,
|
|
showDeleteModal,
|
|
}: {
|
|
folder: Folder;
|
|
currentChatId?: string;
|
|
isInitiallyExpanded: boolean;
|
|
initiallySelected: boolean;
|
|
|
|
showShareModal: ((chatSession: ChatSession) => void) | undefined;
|
|
showDeleteModal: ((chatSession: ChatSession) => void) | undefined;
|
|
}) => {
|
|
const { refreshChatSessions } = useChatContext();
|
|
const [isExpanded, setIsExpanded] = useState<boolean>(isInitiallyExpanded);
|
|
const [isEditing, setIsEditing] = useState<boolean>(initiallySelected);
|
|
const [editedFolderName, setEditedFolderName] = useState<string>(
|
|
folder.folder_name
|
|
);
|
|
const [isHovering, setIsHovering] = useState<boolean>(false);
|
|
const [isDragOver, setIsDragOver] = useState<boolean>(false);
|
|
const { setPopup } = usePopup();
|
|
const router = useRouter();
|
|
|
|
const toggleFolderExpansion = () => {
|
|
if (!isEditing) {
|
|
const newIsExpanded = !isExpanded;
|
|
setIsExpanded(newIsExpanded);
|
|
// Update the cookie with the new state
|
|
const openedFoldersCookieVal = Cookies.get("openedFolders");
|
|
const openedFolders = openedFoldersCookieVal
|
|
? JSON.parse(openedFoldersCookieVal)
|
|
: {};
|
|
if (newIsExpanded) {
|
|
openedFolders[folder.folder_id!] = true;
|
|
} else {
|
|
setShowDeleteConfirm(false);
|
|
|
|
delete openedFolders[folder.folder_id!];
|
|
}
|
|
Cookies.set("openedFolders", JSON.stringify(openedFolders));
|
|
}
|
|
};
|
|
|
|
const handleEditFolderName = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
event.stopPropagation(); // Prevent the event from bubbling up to the toggle expansion
|
|
setIsEditing(true);
|
|
};
|
|
|
|
const handleFolderNameChange = (
|
|
event: React.ChangeEvent<HTMLInputElement>
|
|
) => {
|
|
setEditedFolderName(event.target.value);
|
|
};
|
|
|
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
if (event.key === "Enter") {
|
|
saveFolderName();
|
|
}
|
|
};
|
|
|
|
const saveFolderName = async (continueEditing?: boolean) => {
|
|
try {
|
|
await updateFolderName(folder.folder_id!, editedFolderName);
|
|
if (!continueEditing) {
|
|
setIsEditing(false);
|
|
}
|
|
router.refresh(); // Refresh values to update the sidebar
|
|
} catch (error) {
|
|
setPopup({ message: "Failed to save folder name", type: "error" });
|
|
}
|
|
};
|
|
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
|
|
const deleteConfirmRef = useRef<HTMLDivElement>(null);
|
|
|
|
const handleDeleteClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
event.stopPropagation();
|
|
setShowDeleteConfirm(true);
|
|
};
|
|
|
|
const confirmDelete = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
event.stopPropagation();
|
|
try {
|
|
await deleteFolder(folder.folder_id!);
|
|
router.refresh();
|
|
} catch (error) {
|
|
setPopup({ message: "Failed to delete folder", type: "error" });
|
|
} finally {
|
|
setShowDeleteConfirm(false);
|
|
}
|
|
};
|
|
|
|
const cancelDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
event.stopPropagation();
|
|
setShowDeleteConfirm(false);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
deleteConfirmRef.current &&
|
|
!deleteConfirmRef.current.contains(event.target as Node)
|
|
) {
|
|
setShowDeleteConfirm(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
};
|
|
}, []);
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (initiallySelected && inputRef.current) {
|
|
inputRef.current.focus();
|
|
}
|
|
}, [initiallySelected]);
|
|
|
|
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
|
|
event.preventDefault();
|
|
setIsDragOver(false);
|
|
const chatSessionId = event.dataTransfer.getData(CHAT_SESSION_ID_KEY);
|
|
try {
|
|
await addChatToFolder(folder.folder_id!, chatSessionId);
|
|
await refreshChatSessions();
|
|
router.refresh();
|
|
} catch (error) {
|
|
setPopup({
|
|
message: "Failed to add chat session to folder",
|
|
type: "error",
|
|
});
|
|
}
|
|
};
|
|
|
|
const folders = folder.chat_sessions.sort((a, b) => {
|
|
return a.time_updated.localeCompare(b.time_updated);
|
|
});
|
|
|
|
// Determine whether to show the trash can icon
|
|
const showTrashIcon = (isHovering && !isEditing) || showDeleteConfirm;
|
|
|
|
return (
|
|
<div
|
|
key={folder.folder_id}
|
|
onDragOver={(event) => {
|
|
event.preventDefault();
|
|
setIsDragOver(true);
|
|
}}
|
|
onDragLeave={() => setIsDragOver(false)}
|
|
onDrop={handleDrop}
|
|
className={`transition duration-300 ease-in-out rounded-md ${
|
|
isDragOver ? "bg-accent-background-hovered" : ""
|
|
}`}
|
|
>
|
|
<BasicSelectable fullWidth selected={false}>
|
|
<div
|
|
onMouseEnter={() => setIsHovering(true)}
|
|
onMouseLeave={() => setIsHovering(false)}
|
|
>
|
|
<div onClick={toggleFolderExpansion} className="cursor-pointer">
|
|
<div className="text-sm text-text-600 flex items-center justify-start w-full">
|
|
<div className="mr-2">
|
|
{isExpanded ? (
|
|
<FiChevronDown size={16} />
|
|
) : (
|
|
<FiChevronRight size={16} />
|
|
)}
|
|
</div>
|
|
<div>
|
|
<FiFolder size={16} className="mr-2" />
|
|
</div>
|
|
{isEditing ? (
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
value={editedFolderName}
|
|
onChange={handleFolderNameChange}
|
|
onKeyDown={handleKeyDown}
|
|
onBlur={() => saveFolderName(true)}
|
|
className="text-sm px-1 flex-1 min-w-0 -my-px mr-2"
|
|
/>
|
|
) : (
|
|
<div className="flex-1 break-all min-w-0">
|
|
{editedFolderName || folder.folder_name}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex ml-auto my-auto">
|
|
<div
|
|
onClick={handleEditFolderName}
|
|
className={`hover:bg-black/10 p-1 -m-1 rounded ${
|
|
isHovering && !isEditing
|
|
? ""
|
|
: "opacity-0 pointer-events-none"
|
|
}`}
|
|
>
|
|
<FiEdit2 size={16} />
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<Popover
|
|
open={showDeleteConfirm}
|
|
onOpenChange={setShowDeleteConfirm}
|
|
content={
|
|
<div
|
|
onClick={handleDeleteClick}
|
|
className={`hover:bg-black/10 p-1 -m-1 rounded ml-2 ${
|
|
showTrashIcon ? "" : "opacity-0 pointer-events-none"
|
|
}`}
|
|
>
|
|
<FiTrash size={16} />
|
|
</div>
|
|
}
|
|
popover={
|
|
<div className="p-2 w-[225px] bg-background-100 rounded shadow-lg">
|
|
<p className="text-sm mb-2">
|
|
Are you sure you want to delete folder{" "}
|
|
<i>{folder.folder_name}</i>?
|
|
</p>
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={confirmDelete}
|
|
className="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-xs mr-2"
|
|
>
|
|
Yes
|
|
</button>
|
|
<button
|
|
onClick={cancelDelete}
|
|
className="bg-background-300 hover:bg-background-200 px-2 py-1 rounded text-xs"
|
|
>
|
|
No
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
side="top"
|
|
align="center"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{isEditing && (
|
|
<div className="flex ml-auto my-auto">
|
|
<div
|
|
onClick={() => saveFolderName()}
|
|
className="hover:bg-black/10 p-1 -m-1 rounded"
|
|
>
|
|
<FiCheck size={16} />
|
|
</div>
|
|
<div
|
|
onClick={() => setIsEditing(false)}
|
|
className="hover:bg-black/10 p-1 -m-1 rounded ml-2"
|
|
>
|
|
<FiX size={16} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</BasicSelectable>
|
|
|
|
{/* Expanded Folder Content */}
|
|
{isExpanded && folders && (
|
|
<div className={"mr-4 pl-2 w-full border-l border-border"}>
|
|
{folders.map((chatSession) => (
|
|
<ChatSessionDisplay
|
|
key={chatSession.id}
|
|
chatSession={chatSession}
|
|
isSelected={chatSession.id === currentChatId}
|
|
showShareModal={showShareModal}
|
|
showDeleteModal={showDeleteModal}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const FolderList = ({
|
|
folders,
|
|
currentChatId,
|
|
openedFolders,
|
|
newFolderId,
|
|
showShareModal,
|
|
showDeleteModal,
|
|
}: {
|
|
folders: Folder[];
|
|
currentChatId?: string;
|
|
openedFolders?: { [key: number]: boolean };
|
|
newFolderId: number | null;
|
|
showShareModal: ((chatSession: ChatSession) => void) | undefined;
|
|
showDeleteModal: ((chatSession: ChatSession) => void) | undefined;
|
|
}) => {
|
|
if (folders.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="mt-1 mb-1 overflow-visible">
|
|
{folders.map((folder) => (
|
|
<FolderItem
|
|
key={folder.folder_id}
|
|
folder={folder}
|
|
currentChatId={currentChatId}
|
|
initiallySelected={newFolderId == folder.folder_id}
|
|
isInitiallyExpanded={
|
|
openedFolders ? openedFolders[folder.folder_id!] || false : false
|
|
}
|
|
showShareModal={showShareModal}
|
|
showDeleteModal={showDeleteModal}
|
|
/>
|
|
))}
|
|
{folders.length == 1 && folders[0].chat_sessions.length == 0 && (
|
|
<p className="text-sm font-normal text-subtle mt-2">
|
|
{" "}
|
|
Drag a chat into a folder to save for later{" "}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|