diff --git a/backend/alembic/versions/35e518e0ddf4_properly_cascade.py b/backend/alembic/versions/35e518e0ddf4_properly_cascade.py new file mode 100644 index 00000000000..c18c988fddd --- /dev/null +++ b/backend/alembic/versions/35e518e0ddf4_properly_cascade.py @@ -0,0 +1,121 @@ +"""properly_cascade + +Revision ID: 35e518e0ddf4 +Revises: 91a0a4d62b14 +Create Date: 2024-09-20 21:24:04.891018 + +""" +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "35e518e0ddf4" +down_revision = "91a0a4d62b14" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Update chat_message foreign key constraint + op.drop_constraint( + "chat_message_chat_session_id_fkey", "chat_message", type_="foreignkey" + ) + op.create_foreign_key( + "chat_message_chat_session_id_fkey", + "chat_message", + "chat_session", + ["chat_session_id"], + ["id"], + ondelete="CASCADE", + ) + + # Update chat_message__search_doc foreign key constraints + op.drop_constraint( + "chat_message__search_doc_chat_message_id_fkey", + "chat_message__search_doc", + type_="foreignkey", + ) + op.drop_constraint( + "chat_message__search_doc_search_doc_id_fkey", + "chat_message__search_doc", + type_="foreignkey", + ) + + op.create_foreign_key( + "chat_message__search_doc_chat_message_id_fkey", + "chat_message__search_doc", + "chat_message", + ["chat_message_id"], + ["id"], + ondelete="CASCADE", + ) + op.create_foreign_key( + "chat_message__search_doc_search_doc_id_fkey", + "chat_message__search_doc", + "search_doc", + ["search_doc_id"], + ["id"], + ondelete="CASCADE", + ) + + # Add CASCADE delete for tool_call foreign key + op.drop_constraint("tool_call_message_id_fkey", "tool_call", type_="foreignkey") + op.create_foreign_key( + "tool_call_message_id_fkey", + "tool_call", + "chat_message", + ["message_id"], + ["id"], + ondelete="CASCADE", + ) + + +def downgrade() -> None: + # Revert chat_message foreign key constraint + op.drop_constraint( + "chat_message_chat_session_id_fkey", "chat_message", type_="foreignkey" + ) + op.create_foreign_key( + "chat_message_chat_session_id_fkey", + "chat_message", + "chat_session", + ["chat_session_id"], + ["id"], + ) + + # Revert chat_message__search_doc foreign key constraints + op.drop_constraint( + "chat_message__search_doc_chat_message_id_fkey", + "chat_message__search_doc", + type_="foreignkey", + ) + op.drop_constraint( + "chat_message__search_doc_search_doc_id_fkey", + "chat_message__search_doc", + type_="foreignkey", + ) + + op.create_foreign_key( + "chat_message__search_doc_chat_message_id_fkey", + "chat_message__search_doc", + "chat_message", + ["chat_message_id"], + ["id"], + ) + op.create_foreign_key( + "chat_message__search_doc_search_doc_id_fkey", + "chat_message__search_doc", + "search_doc", + ["search_doc_id"], + ["id"], + ) + + # Revert tool_call foreign key constraint + op.drop_constraint("tool_call_message_id_fkey", "tool_call", type_="foreignkey") + op.create_foreign_key( + "tool_call_message_id_fkey", + "tool_call", + "chat_message", + ["message_id"], + ["id"], + ) diff --git a/backend/onyx/db/chat.py b/backend/onyx/db/chat.py index a2cda0a30c7..3601fdc67cf 100644 --- a/backend/onyx/db/chat.py +++ b/backend/onyx/db/chat.py @@ -316,6 +316,23 @@ def update_chat_session( return chat_session +def delete_all_chat_sessions_for_user( + user: User | None, db_session: Session, hard_delete: bool = HARD_DELETE_CHATS +) -> None: + user_id = user.id if user is not None else None + + query = db_session.query(ChatSession).filter( + ChatSession.user_id == user_id, ChatSession.onyxbot_flow.is_(False) + ) + + if hard_delete: + query.delete(synchronize_session=False) + else: + query.update({ChatSession.deleted: True}, synchronize_session=False) + + db_session.commit() + + def delete_chat_session( user_id: UUID | None, chat_session_id: UUID, diff --git a/backend/onyx/db/models.py b/backend/onyx/db/models.py index e5644c98320..a356e397a94 100644 --- a/backend/onyx/db/models.py +++ b/backend/onyx/db/models.py @@ -1010,7 +1010,7 @@ class ChatSession(Base): "ChatFolder", back_populates="chat_sessions" ) messages: Mapped[list["ChatMessage"]] = relationship( - "ChatMessage", back_populates="chat_session" + "ChatMessage", back_populates="chat_session", cascade="all, delete-orphan" ) persona: Mapped["Persona"] = relationship("Persona") @@ -1078,6 +1078,8 @@ class ChatMessage(Base): "SearchDoc", secondary=ChatMessage__SearchDoc.__table__, back_populates="chat_messages", + cascade="all, delete-orphan", + single_parent=True, ) tool_call: Mapped["ToolCall"] = relationship( diff --git a/backend/onyx/server/query_and_chat/chat_backend.py b/backend/onyx/server/query_and_chat/chat_backend.py index 21499032980..b9e3dc06686 100644 --- a/backend/onyx/server/query_and_chat/chat_backend.py +++ b/backend/onyx/server/query_and_chat/chat_backend.py @@ -35,6 +35,7 @@ from onyx.db.chat import add_chats_to_session_from_slack_thread from onyx.db.chat import create_chat_session from onyx.db.chat import create_new_chat_message +from onyx.db.chat import delete_all_chat_sessions_for_user from onyx.db.chat import delete_chat_session from onyx.db.chat import duplicate_chat_session_for_user_from_slack from onyx.db.chat import get_chat_message @@ -280,6 +281,17 @@ def patch_chat_session( return None +@router.delete("/delete-all-chat-sessions") +def delete_all_chat_sessions( + user: User | None = Depends(current_user), + db_session: Session = Depends(get_session), +) -> None: + try: + delete_all_chat_sessions_for_user(user=user, db_session=db_session) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + @router.delete("/delete-chat-session/{session_id}") def delete_chat_session_by_id( session_id: UUID, diff --git a/backend/onyx/server/query_and_chat/models.py b/backend/onyx/server/query_and_chat/models.py index 16b7d1b061f..068591c0a52 100644 --- a/backend/onyx/server/query_and_chat/models.py +++ b/backend/onyx/server/query_and_chat/models.py @@ -11,6 +11,7 @@ from onyx.configs.constants import DocumentSource from onyx.configs.constants import MessageType from onyx.configs.constants import SearchFeedbackType +from onyx.configs.constants import SessionType from onyx.context.search.models import BaseFilters from onyx.context.search.models import ChunkContext from onyx.context.search.models import RerankingDetails @@ -151,6 +152,10 @@ class ChatSessionUpdateRequest(BaseModel): sharing_status: ChatSessionSharedStatus +class DeleteAllSessionsRequest(BaseModel): + session_type: SessionType + + class RenameChatSessionResponse(BaseModel): new_name: str # This is only really useful if the name is generated diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index bcc790e56fe..89ba2960f0d 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -27,6 +27,7 @@ import { buildLatestMessageChain, checkAnyAssistantHasSearch, createChatSession, + deleteAllChatSessions, deleteChatSession, getCitedDocumentsFromMessage, getHumanAndAIMessageFromMessageNumber, @@ -1837,6 +1838,7 @@ export function ChatPage({ const innerSidebarElementRef = useRef(null); const [settingsToggled, setSettingsToggled] = useState(false); + const [showDeleteAllModal, setShowDeleteAllModal] = useState(false); const currentPersona = alternativeAssistant || liveAssistant; useEffect(() => { @@ -1903,11 +1905,6 @@ export function ChatPage({ const showShareModal = (chatSession: ChatSession) => { setSharedChatSession(chatSession); }; - const [documentSelection, setDocumentSelection] = useState(false); - // const toggleDocumentSelectionAspects = () => { - // setDocumentSelection((documentSelection) => !documentSelection); - // setShowDocSidebar(false); - // }; const toggleDocumentSidebar = () => { if (!documentSidebarToggled) { @@ -1972,6 +1969,32 @@ export function ChatPage({ + {showDeleteAllModal && ( + setShowDeleteAllModal(false)} + additionalDetails="This action cannot be undone. All your chat sessions will be deleted." + onSubmit={async () => { + const response = await deleteAllChatSessions("Chat"); + if (response.ok) { + setShowDeleteAllModal(false); + setPopup({ + message: "All your chat sessions have been deleted.", + type: "success", + }); + refreshChatSessions(); + router.push("/chat"); + } else { + setPopup({ + message: "Failed to delete all chat sessions.", + type: "error", + }); + } + }} + /> + )} + {currentFeedback && ( setShowDeleteAllModal(true)} /> @@ -2739,6 +2763,10 @@ export function ChatPage({ removeDocs={() => { clearSelectedDocuments(); }} + showDocs={() => { + setFiltersToggled(false); + setDocumentSidebarToggled(true); + }} removeFilters={() => { filterManager.setSelectedSources([]); filterManager.setSelectedTags([]); @@ -2751,7 +2779,6 @@ export function ChatPage({ chatState={currentSessionChatState} stopGenerating={stopGenerating} openModelSettings={() => setSettingsToggled(true)} - showDocs={() => setDocumentSelection(true)} selectedDocuments={selectedDocuments} // assistant stuff selectedAssistant={liveAssistant} diff --git a/web/src/app/chat/input/ChatInputBar.tsx b/web/src/app/chat/input/ChatInputBar.tsx index ed828ad9a5f..e7517f25e40 100644 --- a/web/src/app/chat/input/ChatInputBar.tsx +++ b/web/src/app/chat/input/ChatInputBar.tsx @@ -31,14 +31,7 @@ import { SettingsContext } from "@/components/settings/SettingsProvider"; import { ChatState } from "../types"; import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText"; import { useAssistants } from "@/components/context/AssistantsContext"; -import AnimatedToggle from "@/components/search/SearchBar"; -import { Popup } from "@/components/admin/connectors/Popup"; -import { AssistantsTab } from "../modal/configuration/AssistantsTab"; -import { IconType } from "react-icons"; -import { LlmTab } from "../modal/configuration/LlmTab"; import { XIcon } from "lucide-react"; -import { FilterPills } from "./FilterPills"; -import { Tag } from "@/lib/types"; import FiltersDisplay from "./FilterDisplay"; const MAX_INPUT_HEIGHT = 200; @@ -47,7 +40,6 @@ interface ChatInputBarProps { removeFilters: () => void; removeDocs: () => void; openModelSettings: () => void; - showDocs: () => void; showConfigureAPIKey: () => void; selectedDocuments: OnyxDocument[]; message: string; @@ -57,6 +49,7 @@ interface ChatInputBarProps { filterManager: FilterManager; llmOverrideManager: LlmOverrideManager; chatState: ChatState; + showDocs: () => void; alternativeAssistant: Persona | null; // assistants selectedAssistant: Persona; @@ -75,8 +68,8 @@ export function ChatInputBar({ removeFilters, removeDocs, openModelSettings, - showDocs, showConfigureAPIKey, + showDocs, selectedDocuments, message, setMessage, @@ -284,10 +277,6 @@ export function ChatInputBar({ )} - {/*
- -
*/} -
void; stopGenerating?: () => void; explicitlyUntoggle: () => void; + showDeleteAllModal?: () => void; backgroundToggled?: boolean; } @@ -49,6 +50,7 @@ export const HistorySidebar = forwardRef( stopGenerating = () => null, showShareModal, showDeleteModal, + showDeleteAllModal, backgroundToggled, }, ref: ForwardedRef @@ -176,6 +178,7 @@ export const HistorySidebar = forwardRef( currentChatId={currentChatId} folders={folders} openedFolders={openedFolders} + showDeleteAllModal={showDeleteAllModal} />
diff --git a/web/src/app/chat/sessionSidebar/PagesTab.tsx b/web/src/app/chat/sessionSidebar/PagesTab.tsx index 8879506cb60..7bab1a18961 100644 --- a/web/src/app/chat/sessionSidebar/PagesTab.tsx +++ b/web/src/app/chat/sessionSidebar/PagesTab.tsx @@ -9,6 +9,8 @@ import { usePopup } from "@/components/admin/connectors/Popup"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { pageType } from "./types"; +import { FiTrash2 } from "react-icons/fi"; +import { NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED } from "@/lib/constants"; export function PagesTab({ page, @@ -20,6 +22,7 @@ export function PagesTab({ newFolderId, showShareModal, showDeleteModal, + showDeleteAllModal, }: { page: pageType; existingChats?: ChatSession[]; @@ -30,6 +33,7 @@ export function PagesTab({ newFolderId: number | null; showShareModal?: (chatSession: ChatSession) => void; showDeleteModal?: (chatSession: ChatSession) => void; + showDeleteAllModal?: () => void; }) { const groupedChatSessions = existingChats ? groupSessionsByDateRange(existingChats) @@ -63,82 +67,98 @@ export function PagesTab({ const isHistoryEmpty = !existingChats || existingChats.length === 0; return ( -
- {folders && folders.length > 0 && ( -
-
- Chat Folders -
- -
- )} +
{ - event.preventDefault(); - setIsDragOver(true); - }} - onDragLeave={() => setIsDragOver(false)} - onDrop={handleDropToRemoveFromFolder} - className={`pt-1 transition duration-300 ease-in-out mr-3 ${ - isDragOver ? "bg-hover" : "" - } rounded-md`} + className={` flex-grow overflow-y-auto ${ + NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED && "pb-20 " + }`} > - {(page == "chat" || page == "search") && ( -

- {page == "chat" && "Chat "} - {page == "search" && "Search "} - History -

+ {folders && folders.length > 0 && ( +
+
+ Chat Folders +
+ +
)} - {isHistoryEmpty ? ( -

- {page === "search" - ? "Try running a search! Your search history will appear here." - : "Try sending a message! Your chat history will appear here."} -

- ) : ( - Object.entries(groupedChatSessions).map( - ([dateRange, chatSessions], ind) => { - if (chatSessions.length > 0) { - return ( -
-
- {dateRange} + +
{ + event.preventDefault(); + setIsDragOver(true); + }} + onDragLeave={() => setIsDragOver(false)} + onDrop={handleDropToRemoveFromFolder} + className={`pt-1 transition duration-300 ease-in-out mr-3 ${ + isDragOver ? "bg-hover" : "" + } rounded-md`} + > + {(page == "chat" || page == "search") && ( +

+ {page == "chat" && "Chat "} + {page == "search" && "Search "} + History +

+ )} + {isHistoryEmpty ? ( +

+ Try sending a message! Your chat history will appear here. +

+ ) : ( + Object.entries(groupedChatSessions).map( + ([dateRange, chatSessions], ind) => { + if (chatSessions.length > 0) { + return ( +
+
+ {dateRange} +
+ {chatSessions + .filter((chat) => chat.folder_id === null) + .map((chat) => { + const isSelected = currentChatId === chat.id; + return ( +
+ +
+ ); + })}
- {chatSessions - .filter((chat) => chat.folder_id === null) - .map((chat) => { - const isSelected = currentChatId === chat.id; - return ( -
- -
- ); - })} -
- ); + ); + } } - } - ) + ) + )} +
+ {showDeleteAllModal && NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED && ( +
+ +
)}
diff --git a/web/src/components/modals/DeleteEntityModal.tsx b/web/src/components/modals/DeleteEntityModal.tsx index 85cda2fd4d5..0670416f4b9 100644 --- a/web/src/components/modals/DeleteEntityModal.tsx +++ b/web/src/components/modals/DeleteEntityModal.tsx @@ -22,8 +22,7 @@ export const DeleteEntityModal = ({

Delete {entityType}?

- Click below to confirm that you want to delete{" "} - "{entityName}" + Click below to confirm that you want to delete {entityName}

{additionalDetails &&

{additionalDetails}

}
diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index c84bc2eeeff..83fbb1174c7 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -75,3 +75,6 @@ export const REGISTRATION_URL = process.env.INTERNAL_URL || "http://127.0.0.1:3001"; export const TEST_ENV = process.env.TEST_ENV?.toLowerCase() === "true"; + +export const NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED = + process.env.NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED?.toLowerCase() === "true";