diff --git a/frontend/src/app/(dashboard)/layout.tsx b/frontend/src/app/(dashboard)/layout.tsx index 74be50d8..19f180b1 100644 --- a/frontend/src/app/(dashboard)/layout.tsx +++ b/frontend/src/app/(dashboard)/layout.tsx @@ -4,15 +4,15 @@ import { useEffect, useState } from 'react'; import { SidebarLeft } from '@/components/sidebar/sidebar-left'; import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; // import { PricingAlert } from "@/components/billing/pricing-alert" -import { MaintenanceAlert } from "@/components/maintenance-alert" -import { useAccounts } from "@/hooks/use-accounts" -import { useAuth } from "@/components/AuthProvider" -import { useRouter } from "next/navigation" -import { Loader2 } from "lucide-react" -import { checkApiHealth } from "@/lib/api" -import { MaintenancePage } from "@/components/maintenance/maintenance-page" -import { DeleteOperationProvider } from "@/contexts/DeleteOperationContext" -import { StatusOverlay } from "@/components/ui/status-overlay" +import { MaintenanceAlert } from '@/components/maintenance-alert'; +import { useAccounts } from '@/hooks/use-accounts'; +import { useAuth } from '@/components/AuthProvider'; +import { useRouter } from 'next/navigation'; +import { Loader2 } from 'lucide-react'; +import { checkApiHealth } from '@/lib/api'; +import { MaintenancePage } from '@/components/maintenance/maintenance-page'; +import { DeleteOperationProvider } from '@/contexts/DeleteOperationContext'; +import { StatusOverlay } from '@/components/ui/status-overlay'; interface DashboardLayoutProps { children: React.ReactNode; @@ -84,27 +84,25 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) { -
- {children} -
+
{children}
- + {/* */} - + - + {/* Status overlay for deletion operations */}
- ) + ); } diff --git a/frontend/src/components/home/sections/pricing-section.tsx b/frontend/src/components/home/sections/pricing-section.tsx index 01b7dfdf..383aa02f 100644 --- a/frontend/src/components/home/sections/pricing-section.tsx +++ b/frontend/src/components/home/sections/pricing-section.tsx @@ -666,15 +666,15 @@ export function PricingSection({ } }; - // if (isLocalMode()) { - // return ( - //
- //

- // Running in local development mode - billing features are disabled - //

- //
- // ); - // } + if (isLocalMode()) { + return ( +
+

+ Running in local development mode - billing features are disabled +

+
+ ); + } return (
([]) - const [isLoading, setIsLoading] = useState(true) - const [loadingThreadId, setLoadingThreadId] = useState(null) - const pathname = usePathname() - const router = useRouter() - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) - const [threadToDelete, setThreadToDelete] = useState<{ id: string; name: string } | null>(null) - const [isDeleting, setIsDeleting] = useState(false) - const isNavigatingRef = useRef(false) + const { isMobile, state } = useSidebar(); + const [threads, setThreads] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [loadingThreadId, setLoadingThreadId] = useState(null); + const pathname = usePathname(); + const router = useRouter(); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [threadToDelete, setThreadToDelete] = useState<{ + id: string; + name: string; + } | null>(null); + const [isDeleting, setIsDeleting] = useState(false); + const isNavigatingRef = useRef(false); const { performDelete, isOperationInProgress } = useDeleteOperation(); const isPerformingActionRef = useRef(false); @@ -209,33 +212,37 @@ export function NavAgents() { // Add event handler for completed navigation useEffect(() => { const handleNavigationComplete = () => { - console.log("NAVIGATION - Navigation event completed"); - document.body.style.pointerEvents = "auto"; + console.log('NAVIGATION - Navigation event completed'); + document.body.style.pointerEvents = 'auto'; isNavigatingRef.current = false; }; - - window.addEventListener("popstate", handleNavigationComplete); - + + window.addEventListener('popstate', handleNavigationComplete); + return () => { - window.removeEventListener("popstate", handleNavigationComplete); + window.removeEventListener('popstate', handleNavigationComplete); // Ensure we clean up any leftover styles - document.body.style.pointerEvents = "auto"; + document.body.style.pointerEvents = 'auto'; }; }, []); - + // Reset isNavigatingRef when pathname changes useEffect(() => { isNavigatingRef.current = false; - document.body.style.pointerEvents = "auto"; + document.body.style.pointerEvents = 'auto'; }, [pathname]); // Function to handle thread click with loading state - const handleThreadClick = (e: React.MouseEvent, threadId: string, url: string) => { - e.preventDefault() - setLoadingThreadId(threadId) - router.push(url) - } - + const handleThreadClick = ( + e: React.MouseEvent, + threadId: string, + url: string, + ) => { + e.preventDefault(); + setLoadingThreadId(threadId); + router.push(url); + }; + // Function to handle thread deletion const handleDeleteThread = async (threadId: string, threadName: string) => { setThreadToDelete({ id: threadId, name: threadName }); @@ -244,25 +251,25 @@ export function NavAgents() { const confirmDelete = async () => { if (!threadToDelete || isPerformingActionRef.current) return; - + // Mark action in progress isPerformingActionRef.current = true; - + // Close dialog first for immediate feedback setIsDeleteDialogOpen(false); - + const threadId = threadToDelete.id; const isActive = pathname?.includes(threadId); - + // Store threadToDelete in a local variable since it might be cleared const deletedThread = { ...threadToDelete }; - + // Log operation start - console.log("DELETION - Starting thread deletion process", { + console.log('DELETION - Starting thread deletion process', { threadId: deletedThread.id, - isCurrentThread: isActive + isCurrentThread: isActive, }); - + // Use the centralized deletion system with completion callback await performDelete( threadId, @@ -270,19 +277,19 @@ export function NavAgents() { async () => { // Delete the thread await deleteThread(threadId); - + // Update the thread list - setThreads(prev => prev.filter(t => t.threadId !== threadId)); - + setThreads((prev) => prev.filter((t) => t.threadId !== threadId)); + // Show success message - toast.success("Conversation deleted successfully"); + toast.success('Conversation deleted successfully'); }, // Completion callback to reset local state () => { setThreadToDelete(null); setIsDeleting(false); isPerformingActionRef.current = false; - } + }, ); }; @@ -428,7 +435,14 @@ export function NavAgents() { - handleDeleteThread(thread.threadId, thread.projectName)}> + + handleDeleteThread( + thread.threadId, + thread.projectName, + ) + } + > Delete diff --git a/frontend/src/components/thread/DeleteConfirmationDialog.tsx b/frontend/src/components/thread/DeleteConfirmationDialog.tsx index 8118edbf..ea2c595d 100644 --- a/frontend/src/components/thread/DeleteConfirmationDialog.tsx +++ b/frontend/src/components/thread/DeleteConfirmationDialog.tsx @@ -1,7 +1,7 @@ -"use client" +'use client'; -import React from "react" -import { Loader2 } from "lucide-react" +import React from 'react'; +import { Loader2 } from 'lucide-react'; import { AlertDialog, @@ -12,14 +12,14 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, -} from "@/components/ui/alert-dialog" +} from '@/components/ui/alert-dialog'; interface DeleteConfirmationDialogProps { - isOpen: boolean - onClose: () => void - onConfirm: () => void - threadName: string - isDeleting: boolean + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + threadName: string; + isDeleting: boolean; } /** @@ -38,7 +38,7 @@ export function DeleteConfirmationDialog({ Delete conversation - Are you sure you want to delete the conversation{" "} + Are you sure you want to delete the conversation{' '} "{threadName}"?
This action cannot be undone. @@ -48,8 +48,8 @@ export function DeleteConfirmationDialog({ Cancel { - e.preventDefault() - onConfirm() + e.preventDefault(); + onConfirm(); }} disabled={isDeleting} className="bg-destructive text-white hover:bg-destructive/90" @@ -60,11 +60,11 @@ export function DeleteConfirmationDialog({ Deleting... ) : ( - "Delete" + 'Delete' )} - ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/components/thread/chat-input/model-selector.tsx b/frontend/src/components/thread/chat-input/model-selector.tsx index e25c6610..57c66a1a 100644 --- a/frontend/src/components/thread/chat-input/model-selector.tsx +++ b/frontend/src/components/thread/chat-input/model-selector.tsx @@ -46,7 +46,7 @@ export const ModelSelector: React.FC = ({ case 'base-only': return { icon: , - tooltip: 'Requires Base plan or higher', + tooltip: 'Requires Pro plan or higher', }; case 'extra-only': return { diff --git a/frontend/src/components/ui/status-overlay.tsx b/frontend/src/components/ui/status-overlay.tsx index defa9ef4..30840a6e 100644 --- a/frontend/src/components/ui/status-overlay.tsx +++ b/frontend/src/components/ui/status-overlay.tsx @@ -4,9 +4,9 @@ import { useDeleteOperation } from '@/contexts/DeleteOperationContext'; export function StatusOverlay() { const { state } = useDeleteOperation(); - + if (state.operation === 'none' || !state.isDeleting) return null; - + return (
{state.operation === 'pending' && ( @@ -15,14 +15,14 @@ export function StatusOverlay() { Processing... )} - + {state.operation === 'success' && ( <> Completed )} - + {state.operation === 'error' && ( <> @@ -31,4 +31,4 @@ export function StatusOverlay() { )}
); -} \ No newline at end of file +} diff --git a/frontend/src/contexts/DeleteOperationContext.tsx b/frontend/src/contexts/DeleteOperationContext.tsx index cde3146e..b3e5579e 100644 --- a/frontend/src/contexts/DeleteOperationContext.tsx +++ b/frontend/src/contexts/DeleteOperationContext.tsx @@ -1,4 +1,10 @@ -import React, { createContext, useContext, useReducer, useEffect, useRef } from 'react'; +import React, { + createContext, + useContext, + useReducer, + useEffect, + useRef, +} from 'react'; type DeleteState = { isDeleting: boolean; @@ -7,7 +13,7 @@ type DeleteState = { operation: 'none' | 'pending' | 'success' | 'error'; }; -type DeleteAction = +type DeleteAction = | { type: 'START_DELETE'; id: string; isActive: boolean } | { type: 'DELETE_SUCCESS' } | { type: 'DELETE_ERROR' } @@ -17,7 +23,7 @@ const initialState: DeleteState = { isDeleting: false, targetId: null, isActive: false, - operation: 'none' + operation: 'none', }; function deleteReducer(state: DeleteState, action: DeleteAction): DeleteState { @@ -28,18 +34,18 @@ function deleteReducer(state: DeleteState, action: DeleteAction): DeleteState { isDeleting: true, targetId: action.id, isActive: action.isActive, - operation: 'pending' + operation: 'pending', }; case 'DELETE_SUCCESS': return { ...state, - operation: 'success' + operation: 'success', }; case 'DELETE_ERROR': return { ...state, isDeleting: false, - operation: 'error' + operation: 'error', }; case 'RESET': return initialState; @@ -52,20 +58,26 @@ type DeleteOperationContextType = { state: DeleteState; dispatch: React.Dispatch; performDelete: ( - id: string, - isActive: boolean, + id: string, + isActive: boolean, deleteFunction: () => Promise, - onComplete?: () => void + onComplete?: () => void, ) => Promise; isOperationInProgress: React.MutableRefObject; }; -const DeleteOperationContext = createContext(undefined); +const DeleteOperationContext = createContext< + DeleteOperationContextType | undefined +>(undefined); -export function DeleteOperationProvider({ children }: { children: React.ReactNode }) { +export function DeleteOperationProvider({ + children, +}: { + children: React.ReactNode; +}) { const [state, dispatch] = useReducer(deleteReducer, initialState); const isOperationInProgress = useRef(false); - + // Listen for state changes to handle navigation useEffect(() => { if (state.operation === 'success' && state.isActive) { @@ -75,112 +87,114 @@ export function DeleteOperationProvider({ children }: { children: React.ReactNod // Use window.location for reliable navigation window.location.pathname = '/dashboard'; } catch (error) { - console.error("Navigation error:", error); + console.error('Navigation error:', error); } }, 500); return () => clearTimeout(timer); } }, [state.operation, state.isActive]); - + // Auto-reset after operations complete useEffect(() => { if (state.operation === 'success' && !state.isActive) { const timer = setTimeout(() => { dispatch({ type: 'RESET' }); // Ensure pointer events are restored - document.body.style.pointerEvents = "auto"; + document.body.style.pointerEvents = 'auto'; isOperationInProgress.current = false; - + // Restore sidebar menu interactivity - const sidebarMenu = document.querySelector(".sidebar-menu"); + const sidebarMenu = document.querySelector('.sidebar-menu'); if (sidebarMenu) { - sidebarMenu.classList.remove("pointer-events-none"); + sidebarMenu.classList.remove('pointer-events-none'); } }, 1000); return () => clearTimeout(timer); } - + if (state.operation === 'error') { // Reset on error immediately - document.body.style.pointerEvents = "auto"; + document.body.style.pointerEvents = 'auto'; isOperationInProgress.current = false; - + // Restore sidebar menu interactivity - const sidebarMenu = document.querySelector(".sidebar-menu"); + const sidebarMenu = document.querySelector('.sidebar-menu'); if (sidebarMenu) { - sidebarMenu.classList.remove("pointer-events-none"); + sidebarMenu.classList.remove('pointer-events-none'); } } }, [state.operation, state.isActive]); - + const performDelete = async ( - id: string, - isActive: boolean, + id: string, + isActive: boolean, deleteFunction: () => Promise, - onComplete?: () => void + onComplete?: () => void, ) => { // Prevent multiple operations if (isOperationInProgress.current) return; isOperationInProgress.current = true; - + // Disable pointer events during operation - document.body.style.pointerEvents = "none"; - + document.body.style.pointerEvents = 'none'; + // Disable sidebar menu interactions - const sidebarMenu = document.querySelector(".sidebar-menu"); + const sidebarMenu = document.querySelector('.sidebar-menu'); if (sidebarMenu) { - sidebarMenu.classList.add("pointer-events-none"); + sidebarMenu.classList.add('pointer-events-none'); } - + dispatch({ type: 'START_DELETE', id, isActive }); - + try { // Execute the delete operation await deleteFunction(); - + // Use precise timing for UI updates setTimeout(() => { dispatch({ type: 'DELETE_SUCCESS' }); - + // For non-active threads, restore interaction with delay if (!isActive) { setTimeout(() => { - document.body.style.pointerEvents = "auto"; - + document.body.style.pointerEvents = 'auto'; + if (sidebarMenu) { - sidebarMenu.classList.remove("pointer-events-none"); + sidebarMenu.classList.remove('pointer-events-none'); } - + // Call the completion callback if (onComplete) onComplete(); }, 100); } }, 50); } catch (error) { - console.error("Delete operation failed:", error); - + console.error('Delete operation failed:', error); + // Reset states on error - document.body.style.pointerEvents = "auto"; + document.body.style.pointerEvents = 'auto'; isOperationInProgress.current = false; - + if (sidebarMenu) { - sidebarMenu.classList.remove("pointer-events-none"); + sidebarMenu.classList.remove('pointer-events-none'); } - + dispatch({ type: 'DELETE_ERROR' }); - + // Call the completion callback if (onComplete) onComplete(); } }; - + return ( - + {children} ); @@ -189,7 +203,9 @@ export function DeleteOperationProvider({ children }: { children: React.ReactNod export function useDeleteOperation() { const context = useContext(DeleteOperationContext); if (context === undefined) { - throw new Error('useDeleteOperation must be used within a DeleteOperationProvider'); + throw new Error( + 'useDeleteOperation must be used within a DeleteOperationProvider', + ); } return context; -} \ No newline at end of file +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 766e26d9..db6cd8d4 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1247,44 +1247,46 @@ export const toggleThreadPublicStatus = async ( export const deleteThread = async (threadId: string): Promise => { try { const supabase = createClient(); - + // First delete all agent runs associated with this thread console.log(`Deleting all agent runs for thread ${threadId}`); const { error: agentRunsError } = await supabase .from('agent_runs') .delete() .eq('thread_id', threadId); - + if (agentRunsError) { console.error('Error deleting agent runs:', agentRunsError); throw new Error(`Error deleting agent runs: ${agentRunsError.message}`); } - + // Then delete all messages associated with the thread console.log(`Deleting all messages for thread ${threadId}`); const { error: messagesError } = await supabase .from('messages') .delete() .eq('thread_id', threadId); - + if (messagesError) { console.error('Error deleting messages:', messagesError); throw new Error(`Error deleting messages: ${messagesError.message}`); } - + // Finally, delete the thread itself console.log(`Deleting thread ${threadId}`); const { error: threadError } = await supabase .from('threads') .delete() .eq('thread_id', threadId); - + if (threadError) { console.error('Error deleting thread:', threadError); throw new Error(`Error deleting thread: ${threadError.message}`); } - - console.log(`Thread ${threadId} successfully deleted with all related items`); + + console.log( + `Thread ${threadId} successfully deleted with all related items`, + ); } catch (error) { console.error('Error deleting thread and related items:', error); throw error;