diff --git a/frontend/components/delete-session-modal.tsx b/frontend/components/delete-session-modal.tsx
new file mode 100644
index 00000000..7b57a44f
--- /dev/null
+++ b/frontend/components/delete-session-modal.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import { AlertTriangle } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+
+interface DeleteSessionModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ sessionTitle: string;
+ isDeleting?: boolean;
+}
+
+export function DeleteSessionModal({
+ isOpen,
+ onClose,
+ onConfirm,
+ sessionTitle,
+ isDeleting = false,
+}: DeleteSessionModalProps) {
+ return (
+
+ );
+}
diff --git a/frontend/components/navigation-layout.tsx b/frontend/components/navigation-layout.tsx
index fae8da62..d7a564a7 100644
--- a/frontend/components/navigation-layout.tsx
+++ b/frontend/components/navigation-layout.tsx
@@ -1,8 +1,12 @@
-"use client"
+"use client";
-import { Navigation } from "@/components/navigation";
-import { ModeToggle } from "@/components/mode-toggle";
+import { usePathname } from "next/navigation";
+import { useGetConversationsQuery } from "@/app/api/queries/useGetConversationsQuery";
import { KnowledgeFilterDropdown } from "@/components/knowledge-filter-dropdown";
+import { ModeToggle } from "@/components/mode-toggle";
+import { Navigation } from "@/components/navigation";
+import { useAuth } from "@/contexts/auth-context";
+import { useChat } from "@/contexts/chat-context";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
interface NavigationLayoutProps {
@@ -11,11 +15,35 @@ interface NavigationLayoutProps {
export function NavigationLayout({ children }: NavigationLayoutProps) {
const { selectedFilter, setSelectedFilter } = useKnowledgeFilter();
-
+ const pathname = usePathname();
+ const { isAuthenticated, isNoAuthMode } = useAuth();
+ const {
+ endpoint,
+ refreshTrigger,
+ refreshConversations,
+ startNewConversation,
+ } = useChat();
+
+ // Only fetch conversations on chat page
+ const isOnChatPage = pathname === "/" || pathname === "/chat";
+ const { data: conversations = [], isLoading: isConversationsLoading } =
+ useGetConversationsQuery(endpoint, refreshTrigger, {
+ enabled: isOnChatPage && (isAuthenticated || isNoAuthMode),
+ });
+
+ const handleNewConversation = () => {
+ refreshConversations();
+ startNewConversation();
+ };
+
return (
-
+
@@ -31,7 +59,7 @@ export function NavigationLayout({ children }: NavigationLayoutProps) {
{/* Search component could go here */}
-
- {children}
-
+
{children}
);
-}
\ No newline at end of file
+}
diff --git a/frontend/components/navigation.tsx b/frontend/components/navigation.tsx
index b651ef6a..11c2db5c 100644
--- a/frontend/components/navigation.tsx
+++ b/frontend/components/navigation.tsx
@@ -1,24 +1,27 @@
"use client";
-import { useChat } from "@/contexts/chat-context";
-import { cn } from "@/lib/utils";
import {
FileText,
Library,
MessageSquare,
Plus,
Settings2,
+ Trash2,
} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
-import { useCallback, useEffect, useRef, useState } from "react";
-
-import { EndpointType } from "@/contexts/chat-context";
-import { useLoadingStore } from "@/stores/loadingStore";
-import { KnowledgeFilterList } from "./knowledge-filter-list";
+import { useEffect, useRef, useState } from "react";
+import { toast } from "sonner";
+import { useDeleteSessionMutation } from "@/app/api/queries/useDeleteSessionMutation";
+import { type EndpointType, useChat } from "@/contexts/chat-context";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
+import { cn } from "@/lib/utils";
+import { useLoadingStore } from "@/stores/loadingStore";
+import { DeleteSessionModal } from "./delete-session-modal";
+import { KnowledgeFilterList } from "./knowledge-filter-list";
-interface RawConversation {
+// Re-export the types for backward compatibility
+export interface RawConversation {
response_id: string;
title: string;
endpoint: string;
@@ -35,7 +38,7 @@ interface RawConversation {
[key: string]: unknown;
}
-interface ChatConversation {
+export interface ChatConversation {
response_id: string;
title: string;
endpoint: EndpointType;
@@ -52,11 +55,20 @@ interface ChatConversation {
[key: string]: unknown;
}
-export function Navigation() {
+interface NavigationProps {
+ conversations?: ChatConversation[];
+ isConversationsLoading?: boolean;
+ onNewConversation?: () => void;
+}
+
+export function Navigation({
+ conversations = [],
+ isConversationsLoading = false,
+ onNewConversation,
+}: NavigationProps = {}) {
const pathname = usePathname();
const {
endpoint,
- refreshTrigger,
loadConversation,
currentConversationId,
setCurrentConversationId,
@@ -70,18 +82,64 @@ export function Navigation() {
const { loading } = useLoadingStore();
- const [conversations, setConversations] = useState([]);
- const [loadingConversations, setLoadingConversations] = useState(false);
const [loadingNewConversation, setLoadingNewConversation] = useState(false);
const [previousConversationCount, setPreviousConversationCount] = useState(0);
+ const [deleteModalOpen, setDeleteModalOpen] = useState(false);
+ const [conversationToDelete, setConversationToDelete] =
+ useState(null);
const fileInputRef = useRef(null);
const { selectedFilter, setSelectedFilter } = useKnowledgeFilter();
+ // Delete session mutation
+ const deleteSessionMutation = useDeleteSessionMutation({
+ onSuccess: () => {
+ toast.success("Conversation deleted successfully");
+
+ // If we deleted the current conversation, select another one
+ if (
+ conversationToDelete &&
+ currentConversationId === conversationToDelete.response_id
+ ) {
+ // Filter out the deleted conversation and find the next one
+ const remainingConversations = conversations.filter(
+ (conv) => conv.response_id !== conversationToDelete.response_id,
+ );
+
+ if (remainingConversations.length > 0) {
+ // Load the first available conversation (most recent)
+ loadConversation(remainingConversations[0]);
+ } else {
+ // No conversations left, start a new one
+ setCurrentConversationId(null);
+ if (onNewConversation) {
+ onNewConversation();
+ } else {
+ refreshConversations();
+ startNewConversation();
+ }
+ }
+ }
+
+ setDeleteModalOpen(false);
+ setConversationToDelete(null);
+ },
+ onError: (error) => {
+ toast.error(`Failed to delete conversation: ${error.message}`);
+ },
+ });
+
const handleNewConversation = () => {
setLoadingNewConversation(true);
- refreshConversations();
- startNewConversation();
+
+ // Use the prop callback if provided, otherwise use the context method
+ if (onNewConversation) {
+ onNewConversation();
+ } else {
+ refreshConversations();
+ startNewConversation();
+ }
+
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("newConversation"));
}
@@ -98,7 +156,7 @@ export function Navigation() {
window.dispatchEvent(
new CustomEvent("fileUploadStart", {
detail: { filename: file.name },
- })
+ }),
);
try {
@@ -122,7 +180,7 @@ export function Navigation() {
filename: file.name,
error: "Failed to process document",
},
- })
+ }),
);
// Trigger loading end event
@@ -142,7 +200,7 @@ export function Navigation() {
window.dispatchEvent(
new CustomEvent("fileUploaded", {
detail: { file, result },
- })
+ }),
);
// Trigger loading end event
@@ -156,7 +214,7 @@ export function Navigation() {
window.dispatchEvent(
new CustomEvent("fileUploadError", {
detail: { filename: file.name, error: "Failed to process document" },
- })
+ }),
);
}
};
@@ -176,6 +234,25 @@ export function Navigation() {
}
};
+ const handleDeleteConversation = (
+ conversation: ChatConversation,
+ event: React.MouseEvent,
+ ) => {
+ event.preventDefault();
+ event.stopPropagation();
+ setConversationToDelete(conversation);
+ setDeleteModalOpen(true);
+ };
+
+ const confirmDeleteConversation = () => {
+ if (conversationToDelete) {
+ deleteSessionMutation.mutate({
+ sessionId: conversationToDelete.response_id,
+ endpoint: endpoint,
+ });
+ }
+ };
+
const routes = [
{
label: "Chat",
@@ -200,91 +277,6 @@ export function Navigation() {
const isOnChatPage = pathname === "/" || pathname === "/chat";
const isOnKnowledgePage = pathname.startsWith("/knowledge");
- const createDefaultPlaceholder = useCallback(() => {
- return {
- response_id: "new-conversation-" + Date.now(),
- title: "New conversation",
- endpoint: endpoint,
- messages: [
- {
- role: "assistant",
- content: "How can I assist?",
- timestamp: new Date().toISOString(),
- },
- ],
- created_at: new Date().toISOString(),
- last_activity: new Date().toISOString(),
- total_messages: 1,
- } as ChatConversation;
- }, [endpoint]);
-
- const fetchConversations = useCallback(async () => {
- setLoadingConversations(true);
- try {
- // Fetch from the selected endpoint only
- const apiEndpoint =
- endpoint === "chat" ? "/api/chat/history" : "/api/langflow/history";
-
- const response = await fetch(apiEndpoint);
- if (response.ok) {
- const history = await response.json();
- const rawConversations = history.conversations || [];
-
- // Cast conversations to proper type and ensure endpoint is correct
- const conversations: ChatConversation[] = rawConversations.map(
- (conv: RawConversation) => ({
- ...conv,
- endpoint: conv.endpoint as EndpointType,
- })
- );
-
- // Sort conversations by last activity (most recent first)
- conversations.sort((a: ChatConversation, b: ChatConversation) => {
- const aTime = new Date(
- a.last_activity || a.created_at || 0
- ).getTime();
- const bTime = new Date(
- b.last_activity || b.created_at || 0
- ).getTime();
- return bTime - aTime;
- });
-
- setConversations(conversations);
-
- // If no conversations exist and no placeholder is shown, create a default placeholder
- if (conversations.length === 0 && !placeholderConversation) {
- setPlaceholderConversation(createDefaultPlaceholder());
- }
- } else {
- setConversations([]);
-
- // Also create placeholder when request fails and no conversations exist
- if (!placeholderConversation) {
- setPlaceholderConversation(createDefaultPlaceholder());
- }
- }
-
- // Conversation documents are now managed in chat context
- } catch (error) {
- console.error(`Failed to fetch ${endpoint} conversations:`, error);
- setConversations([]);
- } finally {
- setLoadingConversations(false);
- }
- }, [
- endpoint,
- placeholderConversation,
- setPlaceholderConversation,
- createDefaultPlaceholder,
- ]);
-
- // Fetch chat conversations when on chat page, endpoint changes, or refresh is triggered
- useEffect(() => {
- if (isOnChatPage) {
- fetchConversations();
- }
- }, [isOnChatPage, endpoint, refreshTrigger, fetchConversations]);
-
// Clear placeholder when conversation count increases (new conversation was created)
useEffect(() => {
const currentCount = conversations.length;
@@ -326,7 +318,7 @@ export function Navigation() {
"text-sm group flex p-3 w-full justify-start font-medium cursor-pointer hover:bg-accent hover:text-accent-foreground rounded-lg transition-all",
route.active
? "bg-accent text-accent-foreground shadow-sm"
- : "text-foreground hover:text-accent-foreground"
+ : "text-foreground hover:text-accent-foreground",
)}
>
@@ -335,7 +327,7 @@ export function Navigation() {
"h-4 w-4 mr-3 shrink-0",
route.active
? "text-accent-foreground"
- : "text-muted-foreground group-hover:text-foreground"
+ : "text-muted-foreground group-hover:text-foreground",
)}
/>
{route.label}
@@ -366,6 +358,7 @@ export function Navigation() {
Conversations
);
}
diff --git a/frontend/src/app/api/queries/useDeleteSessionMutation.ts b/frontend/src/app/api/queries/useDeleteSessionMutation.ts
new file mode 100644
index 00000000..996e8a44
--- /dev/null
+++ b/frontend/src/app/api/queries/useDeleteSessionMutation.ts
@@ -0,0 +1,57 @@
+import {
+ type MutationOptions,
+ useMutation,
+ useQueryClient,
+} from "@tanstack/react-query";
+import type { EndpointType } from "@/contexts/chat-context";
+
+interface DeleteSessionParams {
+ sessionId: string;
+ endpoint: EndpointType;
+}
+
+interface DeleteSessionResponse {
+ success: boolean;
+ message: string;
+}
+
+export const useDeleteSessionMutation = (
+ options?: Omit<
+ MutationOptions,
+ "mutationFn"
+ >,
+) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ sessionId }: DeleteSessionParams) => {
+ const response = await fetch(`/api/sessions/${sessionId}`, {
+ method: "DELETE",
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.error || `Failed to delete session: ${response.status}`,
+ );
+ }
+
+ return response.json();
+ },
+ onSettled: (_data, _error, variables) => {
+ // Invalidate conversations query to refresh the list
+ // Use a slight delay to ensure the success callback completes first
+ setTimeout(() => {
+ queryClient.invalidateQueries({
+ queryKey: ["conversations", variables.endpoint],
+ });
+
+ // Also invalidate any specific conversation queries
+ queryClient.invalidateQueries({
+ queryKey: ["conversations"],
+ });
+ }, 0);
+ },
+ ...options,
+ });
+};
diff --git a/frontend/src/app/api/queries/useGetConversationsQuery.ts b/frontend/src/app/api/queries/useGetConversationsQuery.ts
new file mode 100644
index 00000000..f7e579b3
--- /dev/null
+++ b/frontend/src/app/api/queries/useGetConversationsQuery.ts
@@ -0,0 +1,105 @@
+import {
+ type UseQueryOptions,
+ useQuery,
+ useQueryClient,
+} from "@tanstack/react-query";
+import type { EndpointType } from "@/contexts/chat-context";
+
+export interface RawConversation {
+ response_id: string;
+ title: string;
+ endpoint: string;
+ messages: Array<{
+ role: string;
+ content: string;
+ timestamp?: string;
+ response_id?: string;
+ }>;
+ created_at?: string;
+ last_activity?: string;
+ previous_response_id?: string;
+ total_messages: number;
+ [key: string]: unknown;
+}
+
+export interface ChatConversation {
+ response_id: string;
+ title: string;
+ endpoint: EndpointType;
+ messages: Array<{
+ role: string;
+ content: string;
+ timestamp?: string;
+ response_id?: string;
+ }>;
+ created_at?: string;
+ last_activity?: string;
+ previous_response_id?: string;
+ total_messages: number;
+ [key: string]: unknown;
+}
+
+export interface ConversationHistoryResponse {
+ conversations: RawConversation[];
+ [key: string]: unknown;
+}
+
+export const useGetConversationsQuery = (
+ endpoint: EndpointType,
+ refreshTrigger?: number,
+ options?: Omit,
+) => {
+ const queryClient = useQueryClient();
+
+ async function getConversations(): Promise {
+ try {
+ // Fetch from the selected endpoint only
+ const apiEndpoint =
+ endpoint === "chat" ? "/api/chat/history" : "/api/langflow/history";
+
+ const response = await fetch(apiEndpoint);
+
+ if (!response.ok) {
+ console.error(`Failed to fetch conversations: ${response.status}`);
+ return [];
+ }
+
+ const history: ConversationHistoryResponse = await response.json();
+ const rawConversations = history.conversations || [];
+
+ // Cast conversations to proper type and ensure endpoint is correct
+ const conversations: ChatConversation[] = rawConversations.map(
+ (conv: RawConversation) => ({
+ ...conv,
+ endpoint: conv.endpoint as EndpointType,
+ }),
+ );
+
+ // Sort conversations by last activity (most recent first)
+ conversations.sort((a: ChatConversation, b: ChatConversation) => {
+ const aTime = new Date(a.last_activity || a.created_at || 0).getTime();
+ const bTime = new Date(b.last_activity || b.created_at || 0).getTime();
+ return bTime - aTime;
+ });
+
+ return conversations;
+ } catch (error) {
+ console.error(`Failed to fetch ${endpoint} conversations:`, error);
+ return [];
+ }
+ }
+
+ const queryResult = useQuery(
+ {
+ queryKey: ["conversations", endpoint, refreshTrigger],
+ placeholderData: (prev) => prev,
+ queryFn: getConversations,
+ staleTime: 0, // Always consider data stale to ensure fresh data on trigger changes
+ gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes
+ ...options,
+ },
+ queryClient,
+ );
+
+ return queryResult;
+};
diff --git a/frontend/src/components/layout-wrapper.tsx b/frontend/src/components/layout-wrapper.tsx
index 9be42730..79d4b095 100644
--- a/frontend/src/components/layout-wrapper.tsx
+++ b/frontend/src/components/layout-wrapper.tsx
@@ -2,28 +2,48 @@
import { Bell, Loader2 } from "lucide-react";
import { usePathname } from "next/navigation";
+import { useGetConversationsQuery } from "@/app/api/queries/useGetConversationsQuery";
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel";
+import Logo from "@/components/logo/logo";
import { Navigation } from "@/components/navigation";
import { TaskNotificationMenu } from "@/components/task-notification-menu";
import { Button } from "@/components/ui/button";
import { UserNav } from "@/components/user-nav";
import { useAuth } from "@/contexts/auth-context";
+import { useChat } from "@/contexts/chat-context";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
// import { GitHubStarButton } from "@/components/github-star-button"
// import { DiscordLink } from "@/components/discord-link"
import { useTask } from "@/contexts/task-context";
-import Logo from "@/components/logo/logo";
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const { tasks, isMenuOpen, toggleMenu } = useTask();
const { isPanelOpen } = useKnowledgeFilter();
const { isLoading, isAuthenticated, isNoAuthMode } = useAuth();
+ const {
+ endpoint,
+ refreshTrigger,
+ refreshConversations,
+ startNewConversation,
+ } = useChat();
const { isLoading: isSettingsLoading, data: settings } = useGetSettingsQuery({
enabled: isAuthenticated || isNoAuthMode,
});
+ // Only fetch conversations on chat page
+ const isOnChatPage = pathname === "/" || pathname === "/chat";
+ const { data: conversations = [], isLoading: isConversationsLoading } =
+ useGetConversationsQuery(endpoint, refreshTrigger, {
+ enabled: isOnChatPage && (isAuthenticated || isNoAuthMode),
+ });
+
+ const handleNewConversation = () => {
+ refreshConversations();
+ startNewConversation();
+ };
+
// List of paths that should not show navigation
const authPaths = ["/login", "/auth/callback", "/onboarding"];
const isAuthPage = authPaths.includes(pathname);
@@ -33,7 +53,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
(task) =>
task.status === "pending" ||
task.status === "running" ||
- task.status === "processing"
+ task.status === "processing",
);
// Show loading state when backend isn't ready
@@ -99,7 +119,11 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
-
+