mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-10-05 17:53:54 +02:00
Folder support
This commit is contained in:
@@ -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
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
]
|
||||
|
@@ -142,6 +142,7 @@ class ChatSessionDetails(BaseModel):
|
||||
persona_id: int
|
||||
time_created: str
|
||||
shared_status: ChatSessionSharedStatus
|
||||
folder_id: int | None
|
||||
|
||||
|
||||
class ChatSessionsResponse(BaseModel):
|
||||
|
@@ -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}>
|
||||
|
244
web/src/app/chat/folders/FolderList.tsx
Normal file
244
web/src/app/chat/folders/FolderList.tsx
Normal 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>
|
||||
);
|
||||
};
|
82
web/src/app/chat/folders/FolderManagement.tsx
Normal file
82
web/src/app/chat/folders/FolderManagement.tsx
Normal 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");
|
||||
}
|
||||
}
|
8
web/src/app/chat/folders/interfaces.ts
Normal file
8
web/src/app/chat/folders/interfaces.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ChatSession } from "../interfaces";
|
||||
|
||||
export interface Folder {
|
||||
folder_id: number;
|
||||
folder_name: string;
|
||||
display_priority: number;
|
||||
chat_sessions: ChatSession[];
|
||||
}
|
@@ -31,6 +31,7 @@ export interface ChatSession {
|
||||
persona_id: number;
|
||||
time_created: string;
|
||||
shared_status: ChatSessionSharedStatus;
|
||||
folder_id: number | null;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
|
@@ -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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@@ -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" />
|
||||
)}
|
||||
</>
|
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ export function BasicClickable({
|
||||
text-emphasis
|
||||
text-sm
|
||||
p-1
|
||||
h-full
|
||||
select-none
|
||||
hover:bg-hover
|
||||
${fullWidth ? "w-full" : ""}`}
|
||||
|
2
web/src/lib/drag/constants.ts
Normal file
2
web/src/lib/drag/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const CHAT_SESSION_ID_KEY = "chatSessionId";
|
||||
export const FOLDER_ID_KEY = "folderId";
|
Reference in New Issue
Block a user