added deletion of sessions and added fetch sessions with query instead of with useEffect
This commit is contained in:
parent
76c967ce17
commit
f41ffc8df2
6 changed files with 436 additions and 140 deletions
58
frontend/components/delete-session-modal.tsx
Normal file
58
frontend/components/delete-session-modal.tsx
Normal file
|
|
@ -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 (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||
Delete Conversation
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete "{sessionTitle}"? This
|
||||
action cannot be undone and will permanently remove the conversation
|
||||
and all its messages.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={isDeleting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={onConfirm}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="h-full relative">
|
||||
<div className="hidden h-full md:flex md:w-72 md:flex-col md:fixed md:inset-y-0 z-[80] border-r border-border/40">
|
||||
<Navigation />
|
||||
<Navigation
|
||||
conversations={conversations}
|
||||
isConversationsLoading={isConversationsLoading}
|
||||
onNewConversation={handleNewConversation}
|
||||
/>
|
||||
</div>
|
||||
<main className="md:pl-72">
|
||||
<div className="flex flex-col min-h-screen">
|
||||
|
|
@ -31,7 +59,7 @@ export function NavigationLayout({ children }: NavigationLayoutProps) {
|
|||
{/* Search component could go here */}
|
||||
</div>
|
||||
<nav className="flex items-center space-x-2">
|
||||
<KnowledgeFilterDropdown
|
||||
<KnowledgeFilterDropdown
|
||||
selectedFilter={selectedFilter}
|
||||
onFilterSelect={setSelectedFilter}
|
||||
/>
|
||||
|
|
@ -41,12 +69,10 @@ export function NavigationLayout({ children }: NavigationLayoutProps) {
|
|||
</div>
|
||||
</header>
|
||||
<div className="flex-1">
|
||||
<div className="container py-6 lg:py-8">
|
||||
{children}
|
||||
</div>
|
||||
<div className="container py-6 lg:py-8">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ChatConversation[]>([]);
|
||||
const [loadingConversations, setLoadingConversations] = useState(false);
|
||||
const [loadingNewConversation, setLoadingNewConversation] = useState(false);
|
||||
const [previousConversationCount, setPreviousConversationCount] = useState(0);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const [conversationToDelete, setConversationToDelete] =
|
||||
useState<ChatConversation | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(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",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center flex-1">
|
||||
|
|
@ -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
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 hover:bg-accent rounded"
|
||||
onClick={handleNewConversation}
|
||||
title="Start new conversation"
|
||||
|
|
@ -379,7 +372,7 @@ export function Navigation() {
|
|||
<div className="px-3 flex-1 min-h-0 flex flex-col">
|
||||
{/* Conversations List - grows naturally, doesn't fill all space */}
|
||||
<div className="flex-shrink-0 overflow-y-auto scrollbar-hide space-y-1 max-h-full">
|
||||
{loadingNewConversation ? (
|
||||
{loadingNewConversation || isConversationsLoading ? (
|
||||
<div className="text-sm text-muted-foreground p-2">
|
||||
Loading...
|
||||
</div>
|
||||
|
|
@ -387,8 +380,9 @@ export function Navigation() {
|
|||
<>
|
||||
{/* Show placeholder conversation if it exists */}
|
||||
{placeholderConversation && (
|
||||
<div
|
||||
className="p-2 rounded-lg bg-accent/50 border border-dashed border-accent cursor-pointer group"
|
||||
<button
|
||||
type="button"
|
||||
className="w-full p-2 rounded-lg bg-accent/50 border border-dashed border-accent cursor-pointer group text-left"
|
||||
onClick={() => {
|
||||
// Don't load placeholder as a real conversation, just focus the input
|
||||
if (typeof window !== "undefined") {
|
||||
|
|
@ -402,7 +396,7 @@ export function Navigation() {
|
|||
<div className="text-xs text-muted-foreground">
|
||||
Start typing to begin...
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Show regular conversations */}
|
||||
|
|
@ -412,9 +406,10 @@ export function Navigation() {
|
|||
</div>
|
||||
) : (
|
||||
conversations.map((conversation) => (
|
||||
<div
|
||||
<button
|
||||
key={conversation.response_id}
|
||||
className={`p-2 rounded-lg group ${
|
||||
type="button"
|
||||
className={`w-full p-2 rounded-lg group relative text-left ${
|
||||
loading
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "hover:bg-accent cursor-pointer"
|
||||
|
|
@ -428,21 +423,39 @@ export function Navigation() {
|
|||
loadConversation(conversation);
|
||||
refreshConversations();
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className="text-sm font-medium text-foreground mb-1 truncate">
|
||||
{conversation.title}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{conversation.total_messages} messages
|
||||
</div>
|
||||
{conversation.last_activity && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{new Date(
|
||||
conversation.last_activity
|
||||
).toLocaleDateString()}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-foreground mb-1 truncate">
|
||||
{conversation.title}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{conversation.total_messages} messages
|
||||
</div>
|
||||
{conversation.last_activity && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{new Date(
|
||||
conversation.last_activity,
|
||||
).toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) =>
|
||||
handleDeleteConversation(conversation, e)
|
||||
}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-destructive/10 rounded text-muted-foreground hover:text-destructive ml-2 flex-shrink-0"
|
||||
title="Delete conversation"
|
||||
disabled={
|
||||
loading || deleteSessionMutation.isPending
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
|
|
@ -456,6 +469,7 @@ export function Navigation() {
|
|||
Conversation knowledge
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFilePickerClick}
|
||||
className="p-1 hover:bg-accent rounded"
|
||||
disabled={loading}
|
||||
|
|
@ -476,9 +490,9 @@ export function Navigation() {
|
|||
No documents yet
|
||||
</div>
|
||||
) : (
|
||||
conversationDocs.map((doc, index) => (
|
||||
conversationDocs.map((doc) => (
|
||||
<div
|
||||
key={index}
|
||||
key={`${doc.filename}-${doc.uploadTime.getTime()}`}
|
||||
className="p-2 rounded-lg hover:bg-accent cursor-pointer group flex items-center"
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2 text-muted-foreground flex-shrink-0" />
|
||||
|
|
@ -495,6 +509,18 @@ export function Navigation() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Session Modal */}
|
||||
<DeleteSessionModal
|
||||
isOpen={deleteModalOpen}
|
||||
onClose={() => {
|
||||
setDeleteModalOpen(false);
|
||||
setConversationToDelete(null);
|
||||
}}
|
||||
onConfirm={confirmDeleteConversation}
|
||||
sessionTitle={conversationToDelete?.title || ""}
|
||||
isDeleting={deleteSessionMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
57
frontend/src/app/api/queries/useDeleteSessionMutation.ts
Normal file
57
frontend/src/app/api/queries/useDeleteSessionMutation.ts
Normal file
|
|
@ -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<DeleteSessionResponse, Error, DeleteSessionParams>,
|
||||
"mutationFn"
|
||||
>,
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<DeleteSessionResponse, Error, DeleteSessionParams>({
|
||||
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,
|
||||
});
|
||||
};
|
||||
105
frontend/src/app/api/queries/useGetConversationsQuery.ts
Normal file
105
frontend/src/app/api/queries/useGetConversationsQuery.ts
Normal file
|
|
@ -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<UseQueryOptions, "queryKey" | "queryFn">,
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
async function getConversations(): Promise<ChatConversation[]> {
|
||||
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;
|
||||
};
|
||||
|
|
@ -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 }) {
|
|||
</div>
|
||||
</header>
|
||||
<div className="side-bar-arrangement bg-background fixed left-0 top-[53px] bottom-0 md:flex hidden">
|
||||
<Navigation />
|
||||
<Navigation
|
||||
conversations={conversations}
|
||||
isConversationsLoading={isConversationsLoading}
|
||||
onNewConversation={handleNewConversation}
|
||||
/>
|
||||
</div>
|
||||
<main
|
||||
className={`md:pl-72 transition-all duration-300 overflow-y-auto h-[calc(100vh-53px)] ${
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue