diff --git a/surfsense_web/components/assistant-ui/thread-list.tsx b/surfsense_web/components/assistant-ui/thread-list.tsx deleted file mode 100644 index 1753a6ccf..000000000 --- a/surfsense_web/components/assistant-ui/thread-list.tsx +++ /dev/null @@ -1,298 +0,0 @@ -"use client"; - -import { - ArchiveIcon, - MessageSquareIcon, - MoreVerticalIcon, - PlusIcon, - RotateCcwIcon, - TrashIcon, -} from "lucide-react"; -import { useRouter } from "next/navigation"; -import { memo, useCallback, useEffect, useMemo, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - createThreadListManager, - type ThreadListItem, - type ThreadListState, -} from "@/lib/chat/thread-persistence"; -import { cn } from "@/lib/utils"; - -interface ThreadListProps { - searchSpaceId: number; - currentThreadId?: number; - className?: string; -} - -export function ThreadList({ searchSpaceId, currentThreadId, className }: ThreadListProps) { - const router = useRouter(); - const [state, setState] = useState({ - threads: [], - archivedThreads: [], - isLoading: true, - error: null, - }); - const [showArchived, setShowArchived] = useState(false); - - // Create the thread list manager - const manager = useCallback( - () => - createThreadListManager({ - searchSpaceId, - currentThreadId: currentThreadId ?? null, - onThreadSwitch: (threadId) => { - router.push(`/dashboard/${searchSpaceId}/new-chat/${threadId}`); - }, - onNewThread: (threadId) => { - router.push(`/dashboard/${searchSpaceId}/new-chat/${threadId}`); - }, - }), - [searchSpaceId, currentThreadId, router] - ); - - // Load threads on mount and when searchSpaceId changes - const loadThreads = useCallback(async () => { - setState((prev) => ({ ...prev, isLoading: true })); - const newState = await manager().loadThreads(); - setState(newState); - }, [manager]); - - useEffect(() => { - loadThreads(); - }, [loadThreads]); - - // Handle new thread creation - const handleNewThread = async () => { - await manager().createNewThread(); - await loadThreads(); - }; - - // Handle thread actions - const handleArchive = async (threadId: number) => { - const success = await manager().archiveThread(threadId); - if (success) await loadThreads(); - }; - - const handleUnarchive = async (threadId: number) => { - const success = await manager().unarchiveThread(threadId); - if (success) await loadThreads(); - }; - - const handleDelete = async (threadId: number) => { - const success = await manager().deleteThread(threadId); - if (success) { - await loadThreads(); - // If we deleted the current thread, redirect to new chat - if (threadId === currentThreadId) { - router.push(`/dashboard/${searchSpaceId}/new-chat`); - } - } - }; - - const handleSwitchToThread = (threadId: number) => { - manager().switchToThread(threadId); - }; - - const displayedThreads = showArchived ? state.archivedThreads : state.threads; - - if (state.isLoading) { - return ( -
-
- Loading threads... -
-
- ); - } - - if (state.error) { - return ( -
-
- {state.error} - -
-
- ); - } - - return ( -
- {/* Header with New Chat button */} -
-

Conversations

- -
- - {/* Tab toggle for active/archived */} -
- - -
- - {/* Thread list */} -
- {displayedThreads.length === 0 ? ( -
- -

- {showArchived ? "No archived conversations" : "No conversations yet"} -

- {!showArchived && ( - - )} -
- ) : ( -
- {displayedThreads.map((thread) => ( - handleSwitchToThread(thread.id)} - onArchive={() => handleArchive(thread.id)} - onUnarchive={() => handleUnarchive(thread.id)} - onDelete={() => handleDelete(thread.id)} - /> - ))} -
- )} -
-
- ); -} - -interface ThreadListItemComponentProps { - thread: ThreadListItem; - isActive: boolean; - isArchived: boolean; - onClick: () => void; - onArchive: () => void; - onUnarchive: () => void; - onDelete: () => void; -} - -const ThreadListItemComponent = memo(function ThreadListItemComponent({ - thread, - isActive, - isArchived, - onClick, - onArchive, - onUnarchive, - onDelete, -}: ThreadListItemComponentProps) { - const relativeTime = useMemo( - () => formatRelativeTime(new Date(thread.updatedAt)), - [thread.updatedAt] - ); - - return ( - - - - {isArchived ? ( - - - Unarchive - - ) : ( - - - Archive - - )} - - - - Delete - - - - - ); -}); - -/** - * Format a date as relative time (e.g., "2 hours ago", "Yesterday") - */ -function formatRelativeTime(date: Date): string { - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffSecs = Math.floor(diffMs / 1000); - const diffMins = Math.floor(diffSecs / 60); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffSecs < 60) return "Just now"; - if (diffMins < 60) return `${diffMins} min${diffMins === 1 ? "" : "s"} ago`; - if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`; - if (diffDays === 1) return "Yesterday"; - if (diffDays < 7) return `${diffDays} days ago`; - - return date.toLocaleDateString(); -} diff --git a/surfsense_web/lib/chat/thread-persistence.ts b/surfsense_web/lib/chat/thread-persistence.ts index 2fb283f87..18ddb4ea0 100644 --- a/surfsense_web/lib/chat/thread-persistence.ts +++ b/surfsense_web/lib/chat/thread-persistence.ts @@ -232,103 +232,3 @@ export function getRegenerateUrl(threadId: number): string { return `${backendUrl}/api/v1/threads/${threadId}/regenerate`; } -// ============================================================================= -// Thread List Manager (for thread list sidebar) -// ============================================================================= - -export interface ThreadListAdapterConfig { - searchSpaceId: number; - currentThreadId: number | null; - onThreadSwitch: (threadId: number) => void; - onNewThread: (threadId: number) => void; -} - -export interface ThreadListState { - threads: ThreadListItem[]; - archivedThreads: ThreadListItem[]; - isLoading: boolean; - error: string | null; -} - -/** - * Creates a thread list management object. - * This provides methods to manage the thread list for the sidebar. - */ -export function createThreadListManager(config: ThreadListAdapterConfig) { - return { - async loadThreads(): Promise { - try { - const response = await fetchThreads(config.searchSpaceId); - return { - threads: response.threads, - archivedThreads: response.archived_threads, - isLoading: false, - error: null, - }; - } catch (error) { - console.error("[ThreadListManager] Failed to load threads:", error); - return { - threads: [], - archivedThreads: [], - isLoading: false, - error: error instanceof Error ? error.message : "Failed to load threads", - }; - } - }, - - async createNewThread(title = "New Chat"): Promise { - try { - const thread = await createThread(config.searchSpaceId, title); - config.onNewThread(thread.id); - return thread.id; - } catch (error) { - console.error("[ThreadListManager] Failed to create thread:", error); - return null; - } - }, - - switchToThread(threadId: number) { - config.onThreadSwitch(threadId); - }, - - async renameThread(threadId: number, newTitle: string): Promise { - try { - await updateThread(threadId, { title: newTitle }); - return true; - } catch (error) { - console.error("[ThreadListManager] Failed to rename thread:", error); - return false; - } - }, - - async archiveThread(threadId: number): Promise { - try { - await updateThread(threadId, { archived: true }); - return true; - } catch (error) { - console.error("[ThreadListManager] Failed to archive thread:", error); - return false; - } - }, - - async unarchiveThread(threadId: number): Promise { - try { - await updateThread(threadId, { archived: false }); - return true; - } catch (error) { - console.error("[ThreadListManager] Failed to unarchive thread:", error); - return false; - } - }, - - async deleteThread(threadId: number): Promise { - try { - await deleteThread(threadId); - return true; - } catch (error) { - console.error("[ThreadListManager] Failed to delete thread:", error); - return false; - } - }, - }; -}