Folder support

This commit is contained in:
Weves
2024-05-10 11:59:55 -07:00
committed by Chris Weaver
parent eb1b604b8c
commit 2e0be9f2da
15 changed files with 645 additions and 149 deletions

View File

@@ -16,13 +16,13 @@ from danswer.db.folder import rename_folder
from danswer.db.folder import update_folder_display_priority
from danswer.db.models import User
from danswer.server.features.folder.models import DeleteFolderOptions
from danswer.server.features.folder.models import FolderChatMinimalInfo
from danswer.server.features.folder.models import FolderChatSessionRequest
from danswer.server.features.folder.models import FolderCreationRequest
from danswer.server.features.folder.models import FolderResponse
from danswer.server.features.folder.models import FolderUpdateRequest
from danswer.server.features.folder.models import GetUserFoldersResponse
from danswer.server.models import DisplayPriorityRequest
from danswer.server.query_and_chat.models import ChatSessionDetails
router = APIRouter(prefix="/folder")
@@ -44,11 +44,16 @@ def get_folders(
folder_name=folder.name,
display_priority=folder.display_priority,
chat_sessions=[
FolderChatMinimalInfo(
chat_session_id=chat_session.id,
chat_session_name=chat_session.description,
ChatSessionDetails(
id=chat_session.id,
name=chat_session.description,
persona_id=chat_session.persona_id,
time_created=chat_session.time_created.isoformat(),
shared_status=chat_session.shared_status,
folder_id=folder.id,
)
for chat_session in folder.chat_sessions
if not chat_session.deleted
],
)
for folder in folders

View File

@@ -1,16 +1,13 @@
from pydantic import BaseModel
class FolderChatMinimalInfo(BaseModel):
chat_session_id: int
chat_session_name: str
from danswer.server.query_and_chat.models import ChatSessionDetails
class FolderResponse(BaseModel):
folder_id: int
folder_name: str | None
display_priority: int
chat_sessions: list[FolderChatMinimalInfo]
chat_sessions: list[ChatSessionDetails]
class GetUserFoldersResponse(BaseModel):

View File

@@ -81,6 +81,7 @@ def get_user_chat_sessions(
persona_id=chat.persona_id,
time_created=chat.time_created.isoformat(),
shared_status=chat.shared_status,
folder_id=chat.folder_id,
)
for chat in chat_sessions
]

View File

@@ -142,6 +142,7 @@ class ChatSessionDetails(BaseModel):
persona_id: int
time_created: str
shared_status: ChatSessionSharedStatus
folder_id: int | None
class ChatSessionsResponse(BaseModel):

View File

@@ -62,6 +62,7 @@ import Dropzone from "react-dropzone";
import { LLMProviderDescriptor } from "../admin/models/llm/interfaces";
import { checkLLMSupportsImageInput, getFinalLLM } from "@/lib/llm/utils";
import { InputBarPreviewImage } from "./images/InputBarPreviewImage";
import { Folder } from "./folders/interfaces";
const MAX_INPUT_HEIGHT = 200;
@@ -76,6 +77,8 @@ export function ChatPage({
defaultSelectedPersonaId,
documentSidebarInitialWidth,
defaultSidebarTab,
folders,
openedFolders,
}: {
user: User | null;
chatSessions: ChatSession[];
@@ -87,6 +90,8 @@ export function ChatPage({
defaultSelectedPersonaId?: number; // what persona to default to
documentSidebarInitialWidth?: number;
defaultSidebarTab?: Tabs;
folders: Folder[];
openedFolders: { [key: number]: boolean };
}) {
const router = useRouter();
const searchParams = useSearchParams();
@@ -642,6 +647,8 @@ export function ChatPage({
onPersonaChange={onPersonaChange}
user={user}
defaultTab={defaultSidebarTab}
folders={folders}
openedFolders={openedFolders}
/>
<div className="flex w-full overflow-x-hidden" ref={masterFlexboxRef}>

View File

@@ -0,0 +1,244 @@
"use client";
import React, { useState, useEffect } from "react";
import { Folder } from "./interfaces";
import { ChatSessionDisplay } from "../sessionSidebar/ChatSessionDisplay"; // Ensure this is correctly imported
import {
FiChevronDown,
FiChevronRight,
FiFolder,
FiEdit,
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";
const FolderItem = ({
folder,
currentChatId,
isInitiallyExpanded,
}: {
folder: Folder;
currentChatId?: number;
isInitiallyExpanded: boolean;
}) => {
const [isExpanded, setIsExpanded] = useState<boolean>(isInitiallyExpanded);
const [isEditing, setIsEditing] = useState<boolean>(false);
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 {
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 () => {
try {
await updateFolderName(folder.folder_id, editedFolderName);
setIsEditing(false);
router.refresh(); // Refresh values to update the sidebar
} catch (error) {
setPopup({ message: "Failed to save folder name", type: "error" });
}
};
const deleteFolderHandler = async (
event: React.MouseEvent<HTMLDivElement>
) => {
event.stopPropagation(); // Prevent the event from bubbling up to the toggle expansion
try {
await deleteFolder(folder.folder_id);
router.refresh(); // Refresh values to update the sidebar
} catch (error) {
setPopup({ message: "Failed to delete folder", type: "error" });
}
};
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(false);
const chatSessionId = parseInt(
event.dataTransfer.getData(CHAT_SESSION_ID_KEY),
10
);
try {
await addChatToFolder(folder.folder_id, chatSessionId);
router.refresh(); // Refresh to show the updated folder contents
} catch (error) {
setPopup({
message: "Failed to add chat session to folder",
type: "error",
});
}
};
const folders = folder.chat_sessions.sort((a, b) => {
return a.time_created.localeCompare(b.time_created);
});
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-hover" : ""
}`}
>
<BasicSelectable fullWidth selected={false}>
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<div onClick={toggleFolderExpansion} className="cursor-pointer">
<div className="text-sm text-medium 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
type="text"
value={editedFolderName}
onChange={handleFolderNameChange}
onKeyDown={handleKeyDown}
className="text-sm px-1 flex-1 min-w-0 -my-px mr-2"
/>
) : (
<div className="flex-1 min-w-0">
{editedFolderName || folder.folder_name}
</div>
)}
{isHovering && !isEditing && (
<div className="flex ml-auto my-auto">
<div
onClick={handleEditFolderName}
className="hover:bg-black/10 p-1 -m-1 rounded"
>
<FiEdit size={16} />
</div>
<div
onClick={deleteFolderHandler}
className="hover:bg-black/10 p-1 -m-1 rounded ml-2"
>
<FiTrash size={16} />
</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>
{isExpanded && folders && (
<div className={"ml-2 pl-2 border-l border-border"}>
{folders.map((chatSession) => (
<ChatSessionDisplay
key={chatSession.id}
chatSession={chatSession}
isSelected={chatSession.id === currentChatId}
skipGradient={isDragOver}
/>
))}
</div>
)}
</div>
);
};
export const FolderList = ({
folders,
currentChatId,
openedFolders,
}: {
folders: Folder[];
currentChatId?: number;
openedFolders: { [key: number]: boolean };
}) => {
if (folders.length === 0) {
return null;
}
return (
<div className="mt-1 pb-1 mb-1 overflow-y-auto">
{folders.map((folder) => (
<FolderItem
key={folder.folder_id}
folder={folder}
currentChatId={currentChatId}
isInitiallyExpanded={openedFolders[folder.folder_id] || false}
/>
))}
</div>
);
};

View File

@@ -0,0 +1,82 @@
import { useState, useEffect, FC } from "react";
// Function to create a new folder
export async function createFolder(folderName: string): Promise<number> {
const response = await fetch("/api/folder", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ folder_name: folderName }),
});
if (!response.ok) {
throw new Error("Failed to create folder");
}
const data = await response.json();
return data.folder_id;
}
// Function to add a chat session to a folder
export async function addChatToFolder(
folderId: number,
chatSessionId: number
): Promise<void> {
const response = await fetch(`/api/folder/${folderId}/add-chat-session`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ chat_session_id: chatSessionId }),
});
if (!response.ok) {
throw new Error("Failed to add chat to folder");
}
}
// Function to remove a chat session from a folder
export async function removeChatFromFolder(
folderId: number,
chatSessionId: number
): Promise<void> {
const response = await fetch(`/api/folder/${folderId}/remove-chat-session`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ chat_session_id: chatSessionId }),
});
if (!response.ok) {
throw new Error("Failed to remove chat from folder");
}
}
// Function to delete a folder
export async function deleteFolder(folderId: number): Promise<void> {
const response = await fetch(`/api/folder/${folderId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
if (!response.ok) {
throw new Error("Failed to delete folder");
}
}
// Function to update a folder's name
export async function updateFolderName(
folderId: number,
newName: string
): Promise<void> {
const response = await fetch(`/api/folder/${folderId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ folder_name: newName }),
});
if (!response.ok) {
throw new Error("Failed to update folder name");
}
}

View File

@@ -0,0 +1,8 @@
import { ChatSession } from "../interfaces";
export interface Folder {
folder_id: number;
folder_name: string;
display_priority: number;
chat_sessions: ChatSession[];
}

View File

@@ -31,6 +31,7 @@ export interface ChatSession {
persona_id: number;
time_created: string;
shared_status: ChatSessionSharedStatus;
folder_id: number | null;
}
export interface Message {

View File

@@ -31,6 +31,7 @@ import { Settings } from "../admin/settings/interfaces";
import { SIDEBAR_TAB_COOKIE, Tabs } from "./sessionSidebar/constants";
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
import { LLMProviderDescriptor } from "../admin/models/llm/interfaces";
import { Folder } from "./folders/interfaces";
export default async function Page({
searchParams,
@@ -48,6 +49,7 @@ export default async function Page({
fetchSS("/chat/get-user-chat-sessions"),
fetchSS("/query/valid-tags"),
fetchLLMProvidersSS(),
fetchSS("/folder"),
];
// catch cases where the backend is completely unreachable here
@@ -75,6 +77,7 @@ export default async function Page({
const chatSessionsResponse = results[5] as Response | null;
const tagsResponse = results[6] as Response | null;
const llmProviders = (results[7] || []) as LLMProviderDescriptor[];
const foldersResponse = results[8] as Response | null; // Handle folders result
const authDisabled = authTypeMetadata?.authType === "disabled";
if (!authDisabled && !user) {
@@ -170,6 +173,18 @@ export default async function Page({
personas = personas.filter((persona) => persona.num_chunks === 0);
}
let folders: Folder[] = [];
if (foldersResponse?.ok) {
folders = (await foldersResponse.json()).folders as Folder[];
} else {
console.log(`Failed to fetch folders - ${foldersResponse?.status}`);
}
const openedFoldersCookie = cookies().get("openedFolders");
const openedFolders = openedFoldersCookie
? JSON.parse(openedFoldersCookie.value)
: {};
return (
<>
<InstantSSRAutoRefresh />
@@ -193,6 +208,8 @@ export default async function Page({
defaultSelectedPersonaId={defaultPersonaId}
documentSidebarInitialWidth={finalDocumentSidebarInitialWidth}
defaultSidebarTab={defaultSidebarTab}
folders={folders} // Pass folders to ChatPage
openedFolders={openedFolders} // Pass opened folders state to ChatPage
/>
</>
);

View File

@@ -2,7 +2,7 @@
import { useRouter } from "next/navigation";
import { ChatSession } from "../interfaces";
import { useState } from "react";
import { useState, useEffect } from "react";
import { deleteChatSession, renameChatSession } from "../lib";
import { DeleteChatModal } from "../modal/DeleteChatModal";
import { BasicSelectable } from "@/components/BasicClickable";
@@ -19,16 +19,19 @@ import {
import { DefaultDropdownElement } from "@/components/Dropdown";
import { Popover } from "@/components/popover/Popover";
import { ShareChatSessionModal } from "../modal/ShareChatSessionModal";
interface ChatSessionDisplayProps {
chatSession: ChatSession;
isSelected: boolean;
}
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
export function ChatSessionDisplay({
chatSession,
isSelected,
}: ChatSessionDisplayProps) {
skipGradient,
}: {
chatSession: ChatSession;
isSelected: boolean;
// needed when the parent is trying to apply some background effect
// if not set, the gradient will still be applied and cause weirdness
skipGradient?: boolean;
}) {
const router = useRouter();
const [isDeletionModalVisible, setIsDeletionModalVisible] = useState(false);
const [isRenamingChat, setIsRenamingChat] = useState(false);
@@ -36,6 +39,18 @@ export function ChatSessionDisplay({
useState(false);
const [isShareModalVisible, setIsShareModalVisible] = useState(false);
const [chatName, setChatName] = useState(chatSession.name);
const [delayedSkipGradient, setDelayedSkipGradient] = useState(skipGradient);
useEffect(() => {
if (skipGradient) {
setDelayedSkipGradient(true);
} else {
const timer = setTimeout(() => {
setDelayedSkipGradient(skipGradient);
}, 300);
return () => clearTimeout(timer);
}
}, [skipGradient]);
const onRename = async () => {
const response = await renameChatSession(chatSession.id, chatName);
@@ -78,13 +93,24 @@ export function ChatSessionDisplay({
key={chatSession.id}
href={`/chat?chatId=${chatSession.id}`}
scroll={false}
draggable="true"
onDragStart={(event) => {
event.dataTransfer.setData(
CHAT_SESSION_ID_KEY,
chatSession.id.toString()
);
event.dataTransfer.setData(
FOLDER_ID_KEY,
chatSession.folder_id?.toString() || ""
);
}}
>
<BasicSelectable fullWidth selected={isSelected}>
<>
<div className="flex relative">
<div className="my-auto mr-2">
<FiMessageSquare size={16} />
</div>{" "}
</div>
{isRenamingChat ? (
<input
value={chatName}
@@ -157,6 +183,7 @@ export function ChatSessionDisplay({
</div>
}
requiresContentPadding
sideOffset={6}
/>
</div>
</div>
@@ -169,10 +196,10 @@ export function ChatSessionDisplay({
</div>
))}
</div>
{isSelected && !isRenamingChat && (
{isSelected && !isRenamingChat && !delayedSkipGradient && (
<div className="absolute bottom-0 right-0 top-0 bg-gradient-to-l to-transparent from-hover w-20 from-60% rounded" />
)}
{!isSelected && (
{!isSelected && !delayedSkipGradient && (
<div className="absolute bottom-0 right-0 top-0 bg-gradient-to-l to-transparent from-background w-8 from-0% rounded" />
)}
</>

View File

@@ -1,6 +1,7 @@
"use client";
import {
FiFolderPlus,
FiLogOut,
FiMessageSquare,
FiMoreHorizontal,
@@ -14,9 +15,7 @@ import { useRouter } from "next/navigation";
import { User } from "@/lib/types";
import { logout } from "@/lib/user";
import { BasicClickable, BasicSelectable } from "@/components/BasicClickable";
import { ChatSessionDisplay } from "./SessionDisplay";
import { ChatSession } from "../interfaces";
import { groupSessionsByDateRange } from "../lib";
import {
HEADER_PADDING,
NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA,
@@ -26,6 +25,9 @@ import { AssistantsTab } from "./AssistantsTab";
import { Persona } from "@/app/admin/assistants/interfaces";
import Cookies from "js-cookie";
import { SIDEBAR_TAB_COOKIE, Tabs } from "./constants";
import { Folder } from "../folders/interfaces";
import { createFolder } from "../folders/FolderManagement";
import { usePopup } from "@/components/admin/connectors/Popup";
export const ChatSidebar = ({
existingChats,
@@ -34,6 +36,8 @@ export const ChatSidebar = ({
onPersonaChange,
user,
defaultTab,
folders,
openedFolders,
}: {
existingChats: ChatSession[];
currentChatSession: ChatSession | null | undefined;
@@ -41,8 +45,11 @@ export const ChatSidebar = ({
onPersonaChange: (persona: Persona | null) => void;
user: User | null;
defaultTab?: Tabs;
folders: Folder[];
openedFolders: { [key: number]: boolean };
}) => {
const router = useRouter();
const { popup, setPopup } = usePopup();
const [openTab, _setOpenTab] = useState(defaultTab || Tabs.CHATS);
const setOpenTab = (tab: Tabs) => {
@@ -105,8 +112,10 @@ export const ChatSidebar = ({
}, [currentChatId]);
return (
<div
className={`
<>
{popup}
<div
className={`
flex-none
w-64
3xl:w-72
@@ -117,117 +126,148 @@ export const ChatSidebar = ({
flex-col
h-screen
transition-transform`}
id="chat-sidebar"
>
<div className="flex w-full mx-4 mt-4 text-sm gap-x-4 pb-2 border-b border-border">
<TabOption tab={Tabs.CHATS} />
<TabOption tab={Tabs.ASSISTANTS} />
</div>
{openTab == Tabs.CHATS && (
<>
<Link
href={
"/chat" +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA &&
currentChatSession
? `?assistantId=${currentChatSession.persona_id}`
: "")
}
className="mx-3 mt-5"
>
<BasicClickable fullWidth>
<div className="flex text-sm">
<FiPlusSquare className="my-auto mr-2" /> New Chat
</div>
</BasicClickable>
</Link>
<ChatTab
existingChats={existingChats}
currentChatId={currentChatId}
/>
</>
)}
{openTab == Tabs.ASSISTANTS && (
<>
<Link href="/assistants/new" className="mx-3 mt-5">
<BasicClickable fullWidth>
<div className="flex text-sm">
<FiPlusSquare className="my-auto mr-2" /> New Assistant
</div>
</BasicClickable>
</Link>
<AssistantsTab
personas={personas}
onPersonaChange={onPersonaChange}
user={user}
/>
</>
)}
<div
className="mt-auto py-2 border-t border-border px-3"
ref={userInfoRef}
id="chat-sidebar"
>
<div className="relative text-strong">
{userInfoVisible && (
<div
className={
(user ? "translate-y-[-110%]" : "translate-y-[-115%]") +
" absolute top-0 bg-background border border-border z-30 w-full rounded text-strong text-sm"
}
>
<div className="flex w-full px-3 mt-4 text-sm ">
<div className="flex w-full gap-x-4 pb-2 border-b border-border">
<TabOption tab={Tabs.CHATS} />
<TabOption tab={Tabs.ASSISTANTS} />
</div>
</div>
{openTab == Tabs.CHATS && (
<>
<div className="flex mt-5 items-center">
<Link
href="/search"
className="flex py-3 px-4 cursor-pointer hover:bg-hover"
href={
"/chat" +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA &&
currentChatSession
? `?assistantId=${currentChatSession.persona_id}`
: "")
}
className="ml-3 w-full"
>
<FiSearch className="my-auto mr-2" />
Danswer Search
<BasicClickable fullWidth>
<div className="flex items-center text-sm">
<FiPlusSquare className="mr-2" /> New Chat
</div>
</BasicClickable>
</Link>
<Link
href="/chat"
className="flex py-3 px-4 cursor-pointer hover:bg-hover"
>
<FiMessageSquare className="my-auto mr-2" />
Danswer Chat
</Link>
{(!user || user.role === "admin") && (
<Link
href="/admin/indexing/status"
className="flex py-3 px-4 cursor-pointer border-t border-border hover:bg-hover"
<div className="ml-1.5 mr-3 h-full">
<BasicClickable
onClick={() =>
createFolder("New Folder")
.then((folderId) => {
console.log(`Folder created with ID: ${folderId}`);
router.refresh();
})
.catch((error) => {
console.error("Failed to create folder:", error);
setPopup({
message: `Failed to create folder: ${error.message}`,
type: "error",
});
})
}
>
<FiTool className="my-auto mr-2" />
Admin Panel
</Link>
)}
{user && (
<div
onClick={handleLogout}
className="flex py-3 px-4 cursor-pointer border-t border-border rounded hover:bg-hover"
>
<FiLogOut className="my-auto mr-2" />
Log out
</div>
)}
</div>
)}
<BasicSelectable fullWidth selected={false}>
<div
onClick={() => setUserInfoVisible(!userInfoVisible)}
className="flex h-8"
>
<div className="my-auto mr-2 bg-user rounded-lg px-1.5">
{user && user.email ? user.email[0].toUpperCase() : "A"}
<div className="flex items-center text-sm h-full">
<FiFolderPlus className="mx-1 my-auto" />
</div>
</BasicClickable>
</div>
<p className="my-auto">
{user ? user.email : "Anonymous Possum"}
</p>
<FiMoreHorizontal className="my-auto ml-auto mr-2" size={20} />
</div>
</BasicSelectable>
<ChatTab
existingChats={existingChats}
currentChatId={currentChatId}
folders={folders}
openedFolders={openedFolders}
/>
</>
)}
{openTab == Tabs.ASSISTANTS && (
<>
<Link href="/assistants/new" className="mx-3 mt-5">
<BasicClickable fullWidth>
<div className="flex text-sm">
<FiPlusSquare className="my-auto mr-2" /> New Assistant
</div>
</BasicClickable>
</Link>
<AssistantsTab
personas={personas}
onPersonaChange={onPersonaChange}
user={user}
/>
</>
)}
<div
className="mt-auto py-2 border-t border-border px-3"
ref={userInfoRef}
>
<div className="relative text-strong">
{userInfoVisible && (
<div
className={
(user ? "translate-y-[-110%]" : "translate-y-[-115%]") +
" absolute top-0 bg-background border border-border z-30 w-full rounded text-strong text-sm"
}
>
<Link
href="/search"
className="flex py-3 px-4 cursor-pointer hover:bg-hover"
>
<FiSearch className="my-auto mr-2" />
Danswer Search
</Link>
<Link
href="/chat"
className="flex py-3 px-4 cursor-pointer hover:bg-hover"
>
<FiMessageSquare className="my-auto mr-2" />
Danswer Chat
</Link>
{(!user || user.role === "admin") && (
<Link
href="/admin/indexing/status"
className="flex py-3 px-4 cursor-pointer border-t border-border hover:bg-hover"
>
<FiTool className="my-auto mr-2" />
Admin Panel
</Link>
)}
{user && (
<div
onClick={handleLogout}
className="flex py-3 px-4 cursor-pointer border-t border-border rounded hover:bg-hover"
>
<FiLogOut className="my-auto mr-2" />
Log out
</div>
)}
</div>
)}
<BasicSelectable fullWidth selected={false}>
<div
onClick={() => setUserInfoVisible(!userInfoVisible)}
className="flex h-8"
>
<div className="my-auto mr-2 bg-user rounded-lg px-1.5">
{user && user.email ? user.email[0].toUpperCase() : "A"}
</div>
<p className="my-auto">
{user ? user.email : "Anonymous Possum"}
</p>
<FiMoreHorizontal className="my-auto ml-auto mr-2" size={20} />
</div>
</BasicSelectable>
</div>
</div>
</div>
</div>
</>
);
};

View File

@@ -1,40 +1,103 @@
import { ChatSession } from "../interfaces";
import { groupSessionsByDateRange } from "../lib";
import { ChatSessionDisplay } from "./SessionDisplay";
import { ChatSessionDisplay } from "./ChatSessionDisplay";
import { removeChatFromFolder } from "../folders/FolderManagement";
import { FolderList } from "../folders/FolderList";
import { Folder } from "../folders/interfaces";
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { useState } from "react";
export function ChatTab({
existingChats,
currentChatId,
folders,
openedFolders,
}: {
existingChats: ChatSession[];
currentChatId?: number;
folders: Folder[];
openedFolders: { [key: number]: boolean };
}) {
const groupedChatSessions = groupSessionsByDateRange(existingChats);
const { setPopup } = usePopup();
const router = useRouter();
const [isDragOver, setIsDragOver] = useState<boolean>(false);
const handleDropToRemoveFromFolder = async (
event: React.DragEvent<HTMLDivElement>
) => {
event.preventDefault();
setIsDragOver(false); // Reset drag over state on drop
const chatSessionId = parseInt(
event.dataTransfer.getData(CHAT_SESSION_ID_KEY),
10
);
const folderId = event.dataTransfer.getData(FOLDER_ID_KEY);
if (folderId) {
try {
await removeChatFromFolder(parseInt(folderId, 10), chatSessionId);
router.refresh(); // Refresh the page to reflect the changes
} catch (error) {
setPopup({
message: "Failed to remove chat from folder",
type: "error",
});
}
}
};
return (
<div className="mt-1 pb-1 mb-1 ml-3 overflow-y-auto h-full">
{Object.entries(groupedChatSessions).map(([dateRange, chatSessions]) => {
if (chatSessions.length > 0) {
return (
<div key={dateRange}>
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-5 font-bold">
{dateRange}
</div>
{chatSessions.map((chat) => {
const isSelected = currentChatId === chat.id;
return (
<div key={`${chat.id}-${chat.name}`} className="mr-3">
<ChatSessionDisplay
chatSession={chat}
isSelected={isSelected}
/>
<div className="mt-4 mb-1 ml-3 overflow-y-auto h-full">
<div className="border-b border-border pb-1 mr-3">
<FolderList
folders={folders}
currentChatId={currentChatId}
openedFolders={openedFolders}
/>
</div>
<div
onDragOver={(event) => {
event.preventDefault();
setIsDragOver(true);
}}
onDragLeave={() => setIsDragOver(false)}
onDrop={handleDropToRemoveFromFolder}
className={`transition duration-300 ease-in-out mr-3 ${
isDragOver ? "bg-hover" : ""
} rounded-md`}
>
{Object.entries(groupedChatSessions).map(
([dateRange, chatSessions]) => {
if (chatSessions.length > 0) {
return (
<div key={dateRange}>
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-5 font-bold">
{dateRange}
</div>
);
})}
</div>
);
}
})}
{chatSessions
.filter((chat) => chat.folder_id === null)
.map((chat) => {
const isSelected = currentChatId === chat.id;
return (
<div key={`${chat.id}-${chat.name}`}>
<ChatSessionDisplay
chatSession={chat}
isSelected={isSelected}
skipGradient={isDragOver}
/>
</div>
);
})}
</div>
);
}
}
)}
</div>
</div>
);
}

View File

@@ -19,6 +19,7 @@ export function BasicClickable({
text-emphasis
text-sm
p-1
h-full
select-none
hover:bg-hover
${fullWidth ? "w-full" : ""}`}

View File

@@ -0,0 +1,2 @@
export const CHAT_SESSION_ID_KEY = "chatSessionId";
export const FOLDER_ID_KEY = "folderId";