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.folder import update_folder_display_priority
|
||||||
from danswer.db.models import User
|
from danswer.db.models import User
|
||||||
from danswer.server.features.folder.models import DeleteFolderOptions
|
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 FolderChatSessionRequest
|
||||||
from danswer.server.features.folder.models import FolderCreationRequest
|
from danswer.server.features.folder.models import FolderCreationRequest
|
||||||
from danswer.server.features.folder.models import FolderResponse
|
from danswer.server.features.folder.models import FolderResponse
|
||||||
from danswer.server.features.folder.models import FolderUpdateRequest
|
from danswer.server.features.folder.models import FolderUpdateRequest
|
||||||
from danswer.server.features.folder.models import GetUserFoldersResponse
|
from danswer.server.features.folder.models import GetUserFoldersResponse
|
||||||
from danswer.server.models import DisplayPriorityRequest
|
from danswer.server.models import DisplayPriorityRequest
|
||||||
|
from danswer.server.query_and_chat.models import ChatSessionDetails
|
||||||
|
|
||||||
router = APIRouter(prefix="/folder")
|
router = APIRouter(prefix="/folder")
|
||||||
|
|
||||||
@@ -44,11 +44,16 @@ def get_folders(
|
|||||||
folder_name=folder.name,
|
folder_name=folder.name,
|
||||||
display_priority=folder.display_priority,
|
display_priority=folder.display_priority,
|
||||||
chat_sessions=[
|
chat_sessions=[
|
||||||
FolderChatMinimalInfo(
|
ChatSessionDetails(
|
||||||
chat_session_id=chat_session.id,
|
id=chat_session.id,
|
||||||
chat_session_name=chat_session.description,
|
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
|
for chat_session in folder.chat_sessions
|
||||||
|
if not chat_session.deleted
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
for folder in folders
|
for folder in folders
|
||||||
|
@@ -1,16 +1,13 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from danswer.server.query_and_chat.models import ChatSessionDetails
|
||||||
class FolderChatMinimalInfo(BaseModel):
|
|
||||||
chat_session_id: int
|
|
||||||
chat_session_name: str
|
|
||||||
|
|
||||||
|
|
||||||
class FolderResponse(BaseModel):
|
class FolderResponse(BaseModel):
|
||||||
folder_id: int
|
folder_id: int
|
||||||
folder_name: str | None
|
folder_name: str | None
|
||||||
display_priority: int
|
display_priority: int
|
||||||
chat_sessions: list[FolderChatMinimalInfo]
|
chat_sessions: list[ChatSessionDetails]
|
||||||
|
|
||||||
|
|
||||||
class GetUserFoldersResponse(BaseModel):
|
class GetUserFoldersResponse(BaseModel):
|
||||||
|
@@ -81,6 +81,7 @@ def get_user_chat_sessions(
|
|||||||
persona_id=chat.persona_id,
|
persona_id=chat.persona_id,
|
||||||
time_created=chat.time_created.isoformat(),
|
time_created=chat.time_created.isoformat(),
|
||||||
shared_status=chat.shared_status,
|
shared_status=chat.shared_status,
|
||||||
|
folder_id=chat.folder_id,
|
||||||
)
|
)
|
||||||
for chat in chat_sessions
|
for chat in chat_sessions
|
||||||
]
|
]
|
||||||
|
@@ -142,6 +142,7 @@ class ChatSessionDetails(BaseModel):
|
|||||||
persona_id: int
|
persona_id: int
|
||||||
time_created: str
|
time_created: str
|
||||||
shared_status: ChatSessionSharedStatus
|
shared_status: ChatSessionSharedStatus
|
||||||
|
folder_id: int | None
|
||||||
|
|
||||||
|
|
||||||
class ChatSessionsResponse(BaseModel):
|
class ChatSessionsResponse(BaseModel):
|
||||||
|
@@ -62,6 +62,7 @@ import Dropzone from "react-dropzone";
|
|||||||
import { LLMProviderDescriptor } from "../admin/models/llm/interfaces";
|
import { LLMProviderDescriptor } from "../admin/models/llm/interfaces";
|
||||||
import { checkLLMSupportsImageInput, getFinalLLM } from "@/lib/llm/utils";
|
import { checkLLMSupportsImageInput, getFinalLLM } from "@/lib/llm/utils";
|
||||||
import { InputBarPreviewImage } from "./images/InputBarPreviewImage";
|
import { InputBarPreviewImage } from "./images/InputBarPreviewImage";
|
||||||
|
import { Folder } from "./folders/interfaces";
|
||||||
|
|
||||||
const MAX_INPUT_HEIGHT = 200;
|
const MAX_INPUT_HEIGHT = 200;
|
||||||
|
|
||||||
@@ -76,6 +77,8 @@ export function ChatPage({
|
|||||||
defaultSelectedPersonaId,
|
defaultSelectedPersonaId,
|
||||||
documentSidebarInitialWidth,
|
documentSidebarInitialWidth,
|
||||||
defaultSidebarTab,
|
defaultSidebarTab,
|
||||||
|
folders,
|
||||||
|
openedFolders,
|
||||||
}: {
|
}: {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
chatSessions: ChatSession[];
|
chatSessions: ChatSession[];
|
||||||
@@ -87,6 +90,8 @@ export function ChatPage({
|
|||||||
defaultSelectedPersonaId?: number; // what persona to default to
|
defaultSelectedPersonaId?: number; // what persona to default to
|
||||||
documentSidebarInitialWidth?: number;
|
documentSidebarInitialWidth?: number;
|
||||||
defaultSidebarTab?: Tabs;
|
defaultSidebarTab?: Tabs;
|
||||||
|
folders: Folder[];
|
||||||
|
openedFolders: { [key: number]: boolean };
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -642,6 +647,8 @@ export function ChatPage({
|
|||||||
onPersonaChange={onPersonaChange}
|
onPersonaChange={onPersonaChange}
|
||||||
user={user}
|
user={user}
|
||||||
defaultTab={defaultSidebarTab}
|
defaultTab={defaultSidebarTab}
|
||||||
|
folders={folders}
|
||||||
|
openedFolders={openedFolders}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex w-full overflow-x-hidden" ref={masterFlexboxRef}>
|
<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;
|
persona_id: number;
|
||||||
time_created: string;
|
time_created: string;
|
||||||
shared_status: ChatSessionSharedStatus;
|
shared_status: ChatSessionSharedStatus;
|
||||||
|
folder_id: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
|
@@ -31,6 +31,7 @@ import { Settings } from "../admin/settings/interfaces";
|
|||||||
import { SIDEBAR_TAB_COOKIE, Tabs } from "./sessionSidebar/constants";
|
import { SIDEBAR_TAB_COOKIE, Tabs } from "./sessionSidebar/constants";
|
||||||
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||||
import { LLMProviderDescriptor } from "../admin/models/llm/interfaces";
|
import { LLMProviderDescriptor } from "../admin/models/llm/interfaces";
|
||||||
|
import { Folder } from "./folders/interfaces";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page({
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -48,6 +49,7 @@ export default async function Page({
|
|||||||
fetchSS("/chat/get-user-chat-sessions"),
|
fetchSS("/chat/get-user-chat-sessions"),
|
||||||
fetchSS("/query/valid-tags"),
|
fetchSS("/query/valid-tags"),
|
||||||
fetchLLMProvidersSS(),
|
fetchLLMProvidersSS(),
|
||||||
|
fetchSS("/folder"),
|
||||||
];
|
];
|
||||||
|
|
||||||
// catch cases where the backend is completely unreachable here
|
// 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 chatSessionsResponse = results[5] as Response | null;
|
||||||
const tagsResponse = results[6] as Response | null;
|
const tagsResponse = results[6] as Response | null;
|
||||||
const llmProviders = (results[7] || []) as LLMProviderDescriptor[];
|
const llmProviders = (results[7] || []) as LLMProviderDescriptor[];
|
||||||
|
const foldersResponse = results[8] as Response | null; // Handle folders result
|
||||||
|
|
||||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||||
if (!authDisabled && !user) {
|
if (!authDisabled && !user) {
|
||||||
@@ -170,6 +173,18 @@ export default async function Page({
|
|||||||
personas = personas.filter((persona) => persona.num_chunks === 0);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<InstantSSRAutoRefresh />
|
<InstantSSRAutoRefresh />
|
||||||
@@ -193,6 +208,8 @@ export default async function Page({
|
|||||||
defaultSelectedPersonaId={defaultPersonaId}
|
defaultSelectedPersonaId={defaultPersonaId}
|
||||||
documentSidebarInitialWidth={finalDocumentSidebarInitialWidth}
|
documentSidebarInitialWidth={finalDocumentSidebarInitialWidth}
|
||||||
defaultSidebarTab={defaultSidebarTab}
|
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 { useRouter } from "next/navigation";
|
||||||
import { ChatSession } from "../interfaces";
|
import { ChatSession } from "../interfaces";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { deleteChatSession, renameChatSession } from "../lib";
|
import { deleteChatSession, renameChatSession } from "../lib";
|
||||||
import { DeleteChatModal } from "../modal/DeleteChatModal";
|
import { DeleteChatModal } from "../modal/DeleteChatModal";
|
||||||
import { BasicSelectable } from "@/components/BasicClickable";
|
import { BasicSelectable } from "@/components/BasicClickable";
|
||||||
@@ -19,16 +19,19 @@ import {
|
|||||||
import { DefaultDropdownElement } from "@/components/Dropdown";
|
import { DefaultDropdownElement } from "@/components/Dropdown";
|
||||||
import { Popover } from "@/components/popover/Popover";
|
import { Popover } from "@/components/popover/Popover";
|
||||||
import { ShareChatSessionModal } from "../modal/ShareChatSessionModal";
|
import { ShareChatSessionModal } from "../modal/ShareChatSessionModal";
|
||||||
|
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
|
||||||
interface ChatSessionDisplayProps {
|
|
||||||
chatSession: ChatSession;
|
|
||||||
isSelected: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChatSessionDisplay({
|
export function ChatSessionDisplay({
|
||||||
chatSession,
|
chatSession,
|
||||||
isSelected,
|
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 router = useRouter();
|
||||||
const [isDeletionModalVisible, setIsDeletionModalVisible] = useState(false);
|
const [isDeletionModalVisible, setIsDeletionModalVisible] = useState(false);
|
||||||
const [isRenamingChat, setIsRenamingChat] = useState(false);
|
const [isRenamingChat, setIsRenamingChat] = useState(false);
|
||||||
@@ -36,6 +39,18 @@ export function ChatSessionDisplay({
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [isShareModalVisible, setIsShareModalVisible] = useState(false);
|
const [isShareModalVisible, setIsShareModalVisible] = useState(false);
|
||||||
const [chatName, setChatName] = useState(chatSession.name);
|
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 onRename = async () => {
|
||||||
const response = await renameChatSession(chatSession.id, chatName);
|
const response = await renameChatSession(chatSession.id, chatName);
|
||||||
@@ -78,13 +93,24 @@ export function ChatSessionDisplay({
|
|||||||
key={chatSession.id}
|
key={chatSession.id}
|
||||||
href={`/chat?chatId=${chatSession.id}`}
|
href={`/chat?chatId=${chatSession.id}`}
|
||||||
scroll={false}
|
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}>
|
<BasicSelectable fullWidth selected={isSelected}>
|
||||||
<>
|
<>
|
||||||
<div className="flex relative">
|
<div className="flex relative">
|
||||||
<div className="my-auto mr-2">
|
<div className="my-auto mr-2">
|
||||||
<FiMessageSquare size={16} />
|
<FiMessageSquare size={16} />
|
||||||
</div>{" "}
|
</div>
|
||||||
{isRenamingChat ? (
|
{isRenamingChat ? (
|
||||||
<input
|
<input
|
||||||
value={chatName}
|
value={chatName}
|
||||||
@@ -157,6 +183,7 @@ export function ChatSessionDisplay({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
requiresContentPadding
|
requiresContentPadding
|
||||||
|
sideOffset={6}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,10 +196,10 @@ export function ChatSessionDisplay({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</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" />
|
<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" />
|
<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";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
FiFolderPlus,
|
||||||
FiLogOut,
|
FiLogOut,
|
||||||
FiMessageSquare,
|
FiMessageSquare,
|
||||||
FiMoreHorizontal,
|
FiMoreHorizontal,
|
||||||
@@ -14,9 +15,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { User } from "@/lib/types";
|
import { User } from "@/lib/types";
|
||||||
import { logout } from "@/lib/user";
|
import { logout } from "@/lib/user";
|
||||||
import { BasicClickable, BasicSelectable } from "@/components/BasicClickable";
|
import { BasicClickable, BasicSelectable } from "@/components/BasicClickable";
|
||||||
import { ChatSessionDisplay } from "./SessionDisplay";
|
|
||||||
import { ChatSession } from "../interfaces";
|
import { ChatSession } from "../interfaces";
|
||||||
import { groupSessionsByDateRange } from "../lib";
|
|
||||||
import {
|
import {
|
||||||
HEADER_PADDING,
|
HEADER_PADDING,
|
||||||
NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA,
|
NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA,
|
||||||
@@ -26,6 +25,9 @@ import { AssistantsTab } from "./AssistantsTab";
|
|||||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { SIDEBAR_TAB_COOKIE, Tabs } from "./constants";
|
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 = ({
|
export const ChatSidebar = ({
|
||||||
existingChats,
|
existingChats,
|
||||||
@@ -34,6 +36,8 @@ export const ChatSidebar = ({
|
|||||||
onPersonaChange,
|
onPersonaChange,
|
||||||
user,
|
user,
|
||||||
defaultTab,
|
defaultTab,
|
||||||
|
folders,
|
||||||
|
openedFolders,
|
||||||
}: {
|
}: {
|
||||||
existingChats: ChatSession[];
|
existingChats: ChatSession[];
|
||||||
currentChatSession: ChatSession | null | undefined;
|
currentChatSession: ChatSession | null | undefined;
|
||||||
@@ -41,8 +45,11 @@ export const ChatSidebar = ({
|
|||||||
onPersonaChange: (persona: Persona | null) => void;
|
onPersonaChange: (persona: Persona | null) => void;
|
||||||
user: User | null;
|
user: User | null;
|
||||||
defaultTab?: Tabs;
|
defaultTab?: Tabs;
|
||||||
|
folders: Folder[];
|
||||||
|
openedFolders: { [key: number]: boolean };
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { popup, setPopup } = usePopup();
|
||||||
|
|
||||||
const [openTab, _setOpenTab] = useState(defaultTab || Tabs.CHATS);
|
const [openTab, _setOpenTab] = useState(defaultTab || Tabs.CHATS);
|
||||||
const setOpenTab = (tab: Tabs) => {
|
const setOpenTab = (tab: Tabs) => {
|
||||||
@@ -105,8 +112,10 @@ export const ChatSidebar = ({
|
|||||||
}, [currentChatId]);
|
}, [currentChatId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className={`
|
{popup}
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
flex-none
|
flex-none
|
||||||
w-64
|
w-64
|
||||||
3xl:w-72
|
3xl:w-72
|
||||||
@@ -117,117 +126,148 @@ export const ChatSidebar = ({
|
|||||||
flex-col
|
flex-col
|
||||||
h-screen
|
h-screen
|
||||||
transition-transform`}
|
transition-transform`}
|
||||||
id="chat-sidebar"
|
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}
|
|
||||||
>
|
>
|
||||||
<div className="relative text-strong">
|
<div className="flex w-full px-3 mt-4 text-sm ">
|
||||||
{userInfoVisible && (
|
<div className="flex w-full gap-x-4 pb-2 border-b border-border">
|
||||||
<div
|
<TabOption tab={Tabs.CHATS} />
|
||||||
className={
|
<TabOption tab={Tabs.ASSISTANTS} />
|
||||||
(user ? "translate-y-[-110%]" : "translate-y-[-115%]") +
|
</div>
|
||||||
" absolute top-0 bg-background border border-border z-30 w-full rounded text-strong text-sm"
|
</div>
|
||||||
}
|
|
||||||
>
|
{openTab == Tabs.CHATS && (
|
||||||
|
<>
|
||||||
|
<div className="flex mt-5 items-center">
|
||||||
<Link
|
<Link
|
||||||
href="/search"
|
href={
|
||||||
className="flex py-3 px-4 cursor-pointer hover:bg-hover"
|
"/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" />
|
<BasicClickable fullWidth>
|
||||||
Danswer Search
|
<div className="flex items-center text-sm">
|
||||||
|
<FiPlusSquare className="mr-2" /> New Chat
|
||||||
|
</div>
|
||||||
|
</BasicClickable>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
|
||||||
href="/chat"
|
<div className="ml-1.5 mr-3 h-full">
|
||||||
className="flex py-3 px-4 cursor-pointer hover:bg-hover"
|
<BasicClickable
|
||||||
>
|
onClick={() =>
|
||||||
<FiMessageSquare className="my-auto mr-2" />
|
createFolder("New Folder")
|
||||||
Danswer Chat
|
.then((folderId) => {
|
||||||
</Link>
|
console.log(`Folder created with ID: ${folderId}`);
|
||||||
{(!user || user.role === "admin") && (
|
router.refresh();
|
||||||
<Link
|
})
|
||||||
href="/admin/indexing/status"
|
.catch((error) => {
|
||||||
className="flex py-3 px-4 cursor-pointer border-t border-border hover:bg-hover"
|
console.error("Failed to create folder:", error);
|
||||||
|
setPopup({
|
||||||
|
message: `Failed to create folder: ${error.message}`,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FiTool className="my-auto mr-2" />
|
<div className="flex items-center text-sm h-full">
|
||||||
Admin Panel
|
<FiFolderPlus className="mx-1 my-auto" />
|
||||||
</Link>
|
</div>
|
||||||
)}
|
</BasicClickable>
|
||||||
{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>
|
</div>
|
||||||
<p className="my-auto">
|
|
||||||
{user ? user.email : "Anonymous Possum"}
|
|
||||||
</p>
|
|
||||||
<FiMoreHorizontal className="my-auto ml-auto mr-2" size={20} />
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,40 +1,103 @@
|
|||||||
import { ChatSession } from "../interfaces";
|
import { ChatSession } from "../interfaces";
|
||||||
import { groupSessionsByDateRange } from "../lib";
|
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({
|
export function ChatTab({
|
||||||
existingChats,
|
existingChats,
|
||||||
currentChatId,
|
currentChatId,
|
||||||
|
folders,
|
||||||
|
openedFolders,
|
||||||
}: {
|
}: {
|
||||||
existingChats: ChatSession[];
|
existingChats: ChatSession[];
|
||||||
currentChatId?: number;
|
currentChatId?: number;
|
||||||
|
folders: Folder[];
|
||||||
|
openedFolders: { [key: number]: boolean };
|
||||||
}) {
|
}) {
|
||||||
const groupedChatSessions = groupSessionsByDateRange(existingChats);
|
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 (
|
return (
|
||||||
<div className="mt-1 pb-1 mb-1 ml-3 overflow-y-auto h-full">
|
<div className="mt-4 mb-1 ml-3 overflow-y-auto h-full">
|
||||||
{Object.entries(groupedChatSessions).map(([dateRange, chatSessions]) => {
|
<div className="border-b border-border pb-1 mr-3">
|
||||||
if (chatSessions.length > 0) {
|
<FolderList
|
||||||
return (
|
folders={folders}
|
||||||
<div key={dateRange}>
|
currentChatId={currentChatId}
|
||||||
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-5 font-bold">
|
openedFolders={openedFolders}
|
||||||
{dateRange}
|
/>
|
||||||
</div>
|
</div>
|
||||||
{chatSessions.map((chat) => {
|
|
||||||
const isSelected = currentChatId === chat.id;
|
<div
|
||||||
return (
|
onDragOver={(event) => {
|
||||||
<div key={`${chat.id}-${chat.name}`} className="mr-3">
|
event.preventDefault();
|
||||||
<ChatSessionDisplay
|
setIsDragOver(true);
|
||||||
chatSession={chat}
|
}}
|
||||||
isSelected={isSelected}
|
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)
|
||||||
</div>
|
.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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,7 @@ export function BasicClickable({
|
|||||||
text-emphasis
|
text-emphasis
|
||||||
text-sm
|
text-sm
|
||||||
p-1
|
p-1
|
||||||
|
h-full
|
||||||
select-none
|
select-none
|
||||||
hover:bg-hover
|
hover:bg-hover
|
||||||
${fullWidth ? "w-full" : ""}`}
|
${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