2026-01-08 19:10:40 +02:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
2026-03-06 14:40:10 +05:30
|
|
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
2026-03-07 04:46:48 +05:30
|
|
|
import { AlertTriangle, Inbox, Megaphone, SquareLibrary } from "lucide-react";
|
2026-01-08 19:10:40 +02:00
|
|
|
import { useParams, usePathname, useRouter } from "next/navigation";
|
|
|
|
|
import { useTranslations } from "next-intl";
|
|
|
|
|
import { useTheme } from "next-themes";
|
2026-03-09 02:22:01 +05:30
|
|
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
2026-01-24 15:17:35 +05:30
|
|
|
import { toast } from "sonner";
|
2026-01-23 13:27:14 -05:00
|
|
|
import { currentThreadAtom, resetCurrentThreadAtom } from "@/atoms/chat/current-thread.atom";
|
2026-03-06 14:40:10 +05:30
|
|
|
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
2026-03-10 01:26:37 -07:00
|
|
|
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
|
2026-03-11 02:18:33 +05:30
|
|
|
import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom";
|
2026-01-12 16:38:40 +02:00
|
|
|
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
|
2026-01-08 19:10:40 +02:00
|
|
|
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
2026-03-16 21:10:46 +05:30
|
|
|
import {
|
2026-03-17 01:50:15 +05:30
|
|
|
morePagesDialogAtom,
|
2026-03-16 21:10:46 +05:30
|
|
|
searchSpaceSettingsDialogAtom,
|
2026-03-16 21:51:15 +05:30
|
|
|
teamDialogAtom,
|
2026-03-16 21:10:46 +05:30
|
|
|
userSettingsDialogAtom,
|
|
|
|
|
} from "@/atoms/settings/settings-dialog.atoms";
|
2026-01-08 19:10:40 +02:00
|
|
|
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
2026-03-21 13:20:13 +05:30
|
|
|
import { MorePagesDialog } from "@/components/settings/more-pages-dialog";
|
|
|
|
|
import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";
|
|
|
|
|
import { TeamDialog } from "@/components/settings/team-dialog";
|
|
|
|
|
import { UserSettingsDialog } from "@/components/settings/user-settings-dialog";
|
2026-03-07 04:15:40 +05:30
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
} from "@/components/ui/alert-dialog";
|
2026-01-08 19:10:40 +02:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
} from "@/components/ui/dialog";
|
2026-02-03 20:47:18 -05:00
|
|
|
import { Input } from "@/components/ui/input";
|
2026-03-21 13:20:13 +05:30
|
|
|
import { Spinner } from "@/components/ui/spinner";
|
2026-02-12 16:12:45 -08:00
|
|
|
import { useAnnouncements } from "@/hooks/use-announcements";
|
2026-03-07 02:34:23 +05:30
|
|
|
import { useDocumentsProcessing } from "@/hooks/use-documents-processing";
|
2026-03-07 12:57:27 +05:30
|
|
|
import { useInbox } from "@/hooks/use-inbox";
|
2026-03-11 02:18:59 +05:30
|
|
|
import { useIsMobile } from "@/hooks/use-mobile";
|
2026-03-10 01:26:37 -07:00
|
|
|
import { notificationsApiService } from "@/lib/apis/notifications-api.service";
|
2026-01-08 19:10:40 +02:00
|
|
|
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
2026-02-05 18:56:38 +02:00
|
|
|
import { logout } from "@/lib/auth-utils";
|
2026-02-09 16:49:11 -08:00
|
|
|
import { deleteThread, fetchThreads, updateThread } from "@/lib/chat/thread-persistence";
|
2026-01-16 11:32:06 -08:00
|
|
|
import { cleanupElectric } from "@/lib/electric/client";
|
2026-01-08 19:10:40 +02:00
|
|
|
import { resetUser, trackLogout } from "@/lib/posthog/events";
|
|
|
|
|
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
2026-01-13 00:17:12 -08:00
|
|
|
import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types";
|
2026-01-12 16:38:40 +02:00
|
|
|
import { CreateSearchSpaceDialog } from "../ui/dialogs";
|
2026-01-12 14:17:15 -08:00
|
|
|
import { LayoutShell } from "../ui/shell";
|
2026-02-09 16:49:11 -08:00
|
|
|
|
2026-01-08 19:10:40 +02:00
|
|
|
interface LayoutDataProviderProps {
|
|
|
|
|
searchSpaceId: string;
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 01:20:51 +05:30
|
|
|
/**
|
|
|
|
|
* Format count for display: shows numbers up to 999, then "1k+", "2k+", etc.
|
|
|
|
|
*/
|
|
|
|
|
function formatInboxCount(count: number): string {
|
|
|
|
|
if (count <= 999) {
|
|
|
|
|
return count.toString();
|
|
|
|
|
}
|
|
|
|
|
const thousands = Math.floor(count / 1000);
|
|
|
|
|
return `${thousands}k+`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 04:46:48 +05:30
|
|
|
export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProviderProps) {
|
2026-01-08 19:10:40 +02:00
|
|
|
const t = useTranslations("dashboard");
|
|
|
|
|
const tCommon = useTranslations("common");
|
2026-01-24 15:17:35 +05:30
|
|
|
const tSidebar = useTranslations("sidebar");
|
2026-01-08 19:10:40 +02:00
|
|
|
const router = useRouter();
|
|
|
|
|
const params = useParams();
|
|
|
|
|
const pathname = usePathname();
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const { theme, setTheme } = useTheme();
|
2026-03-11 02:18:33 +05:30
|
|
|
const isMobile = useIsMobile();
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-02-12 16:12:45 -08:00
|
|
|
// Announcements
|
|
|
|
|
const { unreadCount: announcementUnreadCount } = useAnnouncements();
|
|
|
|
|
|
2026-01-08 19:10:40 +02:00
|
|
|
// Atoms
|
|
|
|
|
const { data: user } = useAtomValue(currentUserAtom);
|
2026-03-11 15:10:53 -07:00
|
|
|
const {
|
|
|
|
|
data: searchSpacesData,
|
|
|
|
|
refetch: refetchSearchSpaces,
|
|
|
|
|
isSuccess: searchSpacesLoaded,
|
|
|
|
|
} = useAtomValue(searchSpacesAtom);
|
2026-01-12 16:38:40 +02:00
|
|
|
const { mutateAsync: deleteSearchSpace } = useAtomValue(deleteSearchSpaceMutationAtom);
|
2026-01-23 13:27:14 -05:00
|
|
|
const currentThreadState = useAtomValue(currentThreadAtom);
|
|
|
|
|
const resetCurrentThread = useSetAtom(resetCurrentThreadAtom);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-01-23 13:27:14 -05:00
|
|
|
// State for handling new chat navigation when router is out of sync
|
|
|
|
|
const [pendingNewChat, setPendingNewChat] = useState(false);
|
|
|
|
|
|
2026-03-09 02:22:01 +05:30
|
|
|
// Key used to force-remount the page component (e.g. after deleting the active chat
|
|
|
|
|
// when the router is out of sync due to replaceState)
|
|
|
|
|
const [chatResetKey, setChatResetKey] = useState(0);
|
|
|
|
|
|
2026-01-23 13:27:14 -05:00
|
|
|
// Current IDs from URL, with fallback to atom for replaceState updates
|
2026-01-08 19:10:40 +02:00
|
|
|
const currentChatId = params?.chat_id
|
|
|
|
|
? Number(Array.isArray(params.chat_id) ? params.chat_id[0] : params.chat_id)
|
2026-01-23 13:27:14 -05:00
|
|
|
: currentThreadState.id;
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-01-21 19:43:20 +05:30
|
|
|
// Fetch current search space (for caching purposes)
|
|
|
|
|
useQuery({
|
2026-01-08 19:10:40 +02:00
|
|
|
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId),
|
|
|
|
|
queryFn: () => searchSpacesApiService.getSearchSpace({ id: Number(searchSpaceId) }),
|
|
|
|
|
enabled: !!searchSpaceId,
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-27 16:32:05 +05:30
|
|
|
// Fetch threads (40 total to allow up to 20 per section - shared/private)
|
2026-02-04 20:13:33 +05:30
|
|
|
const { data: threadsData, isPending: isLoadingThreads } = useQuery({
|
2026-01-27 16:32:05 +05:30
|
|
|
queryKey: ["threads", searchSpaceId, { limit: 40 }],
|
|
|
|
|
queryFn: () => fetchThreads(Number(searchSpaceId), 40),
|
2026-01-08 19:10:40 +02:00
|
|
|
enabled: !!searchSpaceId,
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-13 00:17:12 -08:00
|
|
|
// Separate sidebar states for shared and private chats
|
|
|
|
|
const [isAllSharedChatsSidebarOpen, setIsAllSharedChatsSidebarOpen] = useState(false);
|
|
|
|
|
const [isAllPrivateChatsSidebarOpen, setIsAllPrivateChatsSidebarOpen] = useState(false);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-01-21 19:43:20 +05:30
|
|
|
// Inbox sidebar state
|
|
|
|
|
const [isInboxSidebarOpen, setIsInboxSidebarOpen] = useState(false);
|
|
|
|
|
|
2026-03-06 14:40:10 +05:30
|
|
|
// Documents sidebar state (shared atom so Composer can toggle it)
|
|
|
|
|
const [isDocumentsSidebarOpen, setIsDocumentsSidebarOpen] = useAtom(documentsSidebarOpenAtom);
|
2026-03-10 12:26:45 +05:30
|
|
|
const [isDocumentsDocked, setIsDocumentsDocked] = useState(true);
|
2026-03-11 02:18:33 +05:30
|
|
|
const [isRightPanelCollapsed, setIsRightPanelCollapsed] = useAtom(rightPanelCollapsedAtom);
|
2026-03-10 12:26:45 +05:30
|
|
|
|
|
|
|
|
// Open documents sidebar by default on desktop (docked mode)
|
|
|
|
|
const documentsInitialized = useRef(false);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!documentsInitialized.current) {
|
|
|
|
|
documentsInitialized.current = true;
|
|
|
|
|
const isDesktop = typeof window !== "undefined" && window.innerWidth >= 768;
|
|
|
|
|
if (isDesktop) {
|
|
|
|
|
setIsDocumentsSidebarOpen(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [setIsDocumentsSidebarOpen]);
|
2026-03-05 20:34:02 +05:30
|
|
|
|
2026-03-03 13:09:29 -05:00
|
|
|
// Announcements sidebar state
|
|
|
|
|
const [isAnnouncementsSidebarOpen, setIsAnnouncementsSidebarOpen] = useState(false);
|
|
|
|
|
|
2026-01-13 00:49:27 +02:00
|
|
|
// Search space dialog state
|
2026-01-12 16:38:40 +02:00
|
|
|
const [isCreateSearchSpaceDialogOpen, setIsCreateSearchSpaceDialogOpen] = useState(false);
|
|
|
|
|
|
2026-03-06 19:35:35 +05:30
|
|
|
// Per-tab inbox hooks — each has independent API loading, pagination,
|
|
|
|
|
// and Electric live queries. The Electric sync shape is shared (client-level cache).
|
2026-01-21 19:43:20 +05:30
|
|
|
const userId = user?.id ? String(user.id) : null;
|
2026-03-06 19:35:35 +05:30
|
|
|
const numericSpaceId = Number(searchSpaceId) || null;
|
2026-01-28 02:14:36 +05:30
|
|
|
|
2026-03-10 01:26:37 -07:00
|
|
|
// Batch-fetch unread counts for all categories in a single request
|
|
|
|
|
// instead of 2 separate /unread-count calls.
|
|
|
|
|
const { data: batchUnread, isLoading: isBatchUnreadLoading } = useQuery({
|
|
|
|
|
queryKey: cacheKeys.notifications.batchUnreadCounts(numericSpaceId),
|
|
|
|
|
queryFn: () => notificationsApiService.getBatchUnreadCounts(numericSpaceId ?? undefined),
|
|
|
|
|
enabled: !!userId && !!numericSpaceId,
|
|
|
|
|
staleTime: 30_000,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const commentsInbox = useInbox(
|
|
|
|
|
userId,
|
|
|
|
|
numericSpaceId,
|
|
|
|
|
"comments",
|
|
|
|
|
batchUnread?.comments,
|
|
|
|
|
!isBatchUnreadLoading
|
|
|
|
|
);
|
|
|
|
|
const statusInbox = useInbox(
|
|
|
|
|
userId,
|
|
|
|
|
numericSpaceId,
|
|
|
|
|
"status",
|
|
|
|
|
batchUnread?.status,
|
|
|
|
|
!isBatchUnreadLoading
|
|
|
|
|
);
|
2026-03-06 19:35:35 +05:30
|
|
|
|
|
|
|
|
const totalUnreadCount = commentsInbox.unreadCount + statusInbox.unreadCount;
|
2026-01-28 02:14:36 +05:30
|
|
|
|
2026-03-10 01:26:37 -07:00
|
|
|
// Sync status inbox items to a shared atom so child components
|
|
|
|
|
// (e.g. ConnectorPopup) can read them without creating duplicate useInbox hooks.
|
|
|
|
|
const setStatusInboxItems = useSetAtom(statusInboxItemsAtom);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setStatusInboxItems(statusInbox.inboxItems);
|
|
|
|
|
}, [statusInbox.inboxItems, setStatusInboxItems]);
|
|
|
|
|
|
2026-03-08 21:16:52 +05:30
|
|
|
// Document processing status — drives sidebar status indicator (spinner / check / error)
|
|
|
|
|
const documentsProcessingStatus = useDocumentsProcessing(numericSpaceId);
|
2026-03-07 02:34:23 +05:30
|
|
|
|
2026-02-01 18:02:17 -08:00
|
|
|
// Track seen notification IDs to detect new page_limit_exceeded notifications
|
|
|
|
|
const seenPageLimitNotifications = useRef<Set<number>>(new Set());
|
|
|
|
|
const isInitialLoad = useRef(true);
|
|
|
|
|
|
2026-03-17 01:50:15 +05:30
|
|
|
const setMorePagesOpen = useSetAtom(morePagesDialogAtom);
|
|
|
|
|
|
2026-02-01 18:02:17 -08:00
|
|
|
// Effect to show toast for new page_limit_exceeded notifications
|
|
|
|
|
useEffect(() => {
|
2026-03-06 19:35:35 +05:30
|
|
|
if (statusInbox.loading) return;
|
2026-02-01 18:02:17 -08:00
|
|
|
|
2026-03-06 19:35:35 +05:30
|
|
|
const pageLimitNotifications = statusInbox.inboxItems.filter(
|
2026-02-01 18:02:17 -08:00
|
|
|
(item) => item.type === "page_limit_exceeded"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (isInitialLoad.current) {
|
|
|
|
|
for (const notification of pageLimitNotifications) {
|
|
|
|
|
seenPageLimitNotifications.current.add(notification.id);
|
|
|
|
|
}
|
|
|
|
|
isInitialLoad.current = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newNotifications = pageLimitNotifications.filter(
|
|
|
|
|
(notification) => !seenPageLimitNotifications.current.has(notification.id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for (const notification of newNotifications) {
|
|
|
|
|
seenPageLimitNotifications.current.add(notification.id);
|
|
|
|
|
|
|
|
|
|
toast.error(notification.title, {
|
|
|
|
|
description: notification.message,
|
|
|
|
|
duration: 8000,
|
|
|
|
|
icon: <AlertTriangle className="h-5 w-5 text-amber-500" />,
|
|
|
|
|
action: {
|
|
|
|
|
label: "View Plans",
|
2026-03-17 01:50:15 +05:30
|
|
|
onClick: () => setMorePagesOpen(true),
|
2026-02-01 18:02:17 -08:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-17 01:50:15 +05:30
|
|
|
}, [statusInbox.inboxItems, statusInbox.loading, searchSpaceId, setMorePagesOpen]);
|
2026-01-28 02:14:36 +05:30
|
|
|
|
2026-01-08 19:10:40 +02:00
|
|
|
// Delete dialogs state
|
|
|
|
|
const [showDeleteChatDialog, setShowDeleteChatDialog] = useState(false);
|
|
|
|
|
const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null);
|
|
|
|
|
const [isDeletingChat, setIsDeletingChat] = useState(false);
|
|
|
|
|
|
2026-02-03 20:47:18 -05:00
|
|
|
// Rename dialog state
|
|
|
|
|
const [showRenameChatDialog, setShowRenameChatDialog] = useState(false);
|
|
|
|
|
const [chatToRename, setChatToRename] = useState<{ id: number; name: string } | null>(null);
|
|
|
|
|
const [newChatTitle, setNewChatTitle] = useState("");
|
|
|
|
|
const [isRenamingChat, setIsRenamingChat] = useState(false);
|
|
|
|
|
|
2026-01-13 02:06:35 -08:00
|
|
|
// Delete/Leave search space dialog state
|
2026-01-13 01:45:58 -08:00
|
|
|
const [showDeleteSearchSpaceDialog, setShowDeleteSearchSpaceDialog] = useState(false);
|
2026-01-13 02:06:35 -08:00
|
|
|
const [showLeaveSearchSpaceDialog, setShowLeaveSearchSpaceDialog] = useState(false);
|
2026-01-13 01:45:58 -08:00
|
|
|
const [searchSpaceToDelete, setSearchSpaceToDelete] = useState<SearchSpace | null>(null);
|
2026-01-13 02:06:35 -08:00
|
|
|
const [searchSpaceToLeave, setSearchSpaceToLeave] = useState<SearchSpace | null>(null);
|
2026-01-13 01:45:58 -08:00
|
|
|
const [isDeletingSearchSpace, setIsDeletingSearchSpace] = useState(false);
|
2026-01-13 02:06:35 -08:00
|
|
|
const [isLeavingSearchSpace, setIsLeavingSearchSpace] = useState(false);
|
2026-01-13 01:45:58 -08:00
|
|
|
|
2026-01-23 13:27:14 -05:00
|
|
|
// Effect to complete new chat navigation after router syncs
|
|
|
|
|
// This runs when handleNewChat detected an out-of-sync state and triggered a sync
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (pendingNewChat && params?.chat_id) {
|
|
|
|
|
// Router is now synced (chat_id is in params), complete navigation to new-chat
|
|
|
|
|
resetCurrentThread();
|
|
|
|
|
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
|
|
|
|
setPendingNewChat(false);
|
|
|
|
|
}
|
|
|
|
|
}, [pendingNewChat, params?.chat_id, router, searchSpaceId, resetCurrentThread]);
|
|
|
|
|
|
2026-03-11 16:37:56 -07:00
|
|
|
// Reset transient slide-out panels when switching search spaces.
|
|
|
|
|
// Some browsers can retain overlay/backdrop state across route transitions.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setIsInboxSidebarOpen(false);
|
|
|
|
|
setIsAllSharedChatsSidebarOpen(false);
|
|
|
|
|
setIsAllPrivateChatsSidebarOpen(false);
|
|
|
|
|
setIsAnnouncementsSidebarOpen(false);
|
|
|
|
|
}, [searchSpaceId]);
|
|
|
|
|
|
2026-01-12 15:47:56 +02:00
|
|
|
const searchSpaces: SearchSpace[] = useMemo(() => {
|
2026-01-08 19:10:40 +02:00
|
|
|
if (!searchSpacesData || !Array.isArray(searchSpacesData)) return [];
|
|
|
|
|
return searchSpacesData.map((space) => ({
|
|
|
|
|
id: space.id,
|
|
|
|
|
name: space.name,
|
|
|
|
|
description: space.description,
|
|
|
|
|
isOwner: space.is_owner,
|
|
|
|
|
memberCount: space.member_count || 0,
|
2026-01-12 16:38:40 +02:00
|
|
|
createdAt: space.created_at,
|
2026-01-08 19:10:40 +02:00
|
|
|
}));
|
|
|
|
|
}, [searchSpacesData]);
|
|
|
|
|
|
2026-01-12 16:47:15 +02:00
|
|
|
// Find active search space from list (has is_owner and member_count)
|
|
|
|
|
const activeSearchSpace: SearchSpace | null = useMemo(() => {
|
|
|
|
|
if (!searchSpaceId || !searchSpaces.length) return null;
|
|
|
|
|
return searchSpaces.find((s) => s.id === Number(searchSpaceId)) ?? null;
|
|
|
|
|
}, [searchSpaceId, searchSpaces]);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-03-11 15:09:10 -07:00
|
|
|
// Safety redirect: if the current search space is no longer in the user's list
|
|
|
|
|
// (e.g. deleted by background task, membership revoked), redirect to a valid space.
|
|
|
|
|
useEffect(() => {
|
2026-03-11 15:10:53 -07:00
|
|
|
if (!searchSpacesLoaded || !searchSpaceId || isDeletingSearchSpace || isLeavingSearchSpace)
|
|
|
|
|
return;
|
2026-03-11 15:09:10 -07:00
|
|
|
if (searchSpaces.length > 0 && !activeSearchSpace) {
|
|
|
|
|
router.replace(`/dashboard/${searchSpaces[0].id}/new-chat`);
|
|
|
|
|
} else if (searchSpaces.length === 0 && searchSpacesLoaded) {
|
|
|
|
|
router.replace("/dashboard");
|
|
|
|
|
}
|
2026-03-11 15:10:53 -07:00
|
|
|
}, [
|
|
|
|
|
searchSpacesLoaded,
|
|
|
|
|
searchSpaceId,
|
|
|
|
|
searchSpaces,
|
|
|
|
|
activeSearchSpace,
|
|
|
|
|
isDeletingSearchSpace,
|
|
|
|
|
isLeavingSearchSpace,
|
|
|
|
|
router,
|
|
|
|
|
]);
|
2026-03-11 15:09:10 -07:00
|
|
|
|
2026-01-13 00:17:12 -08:00
|
|
|
// Transform and split chats into private and shared based on visibility
|
|
|
|
|
const { myChats, sharedChats } = useMemo(() => {
|
|
|
|
|
if (!threadsData?.threads) return { myChats: [], sharedChats: [] };
|
|
|
|
|
|
|
|
|
|
const privateChats: ChatItem[] = [];
|
|
|
|
|
const sharedChatsList: ChatItem[] = [];
|
|
|
|
|
|
|
|
|
|
for (const thread of threadsData.threads) {
|
|
|
|
|
const chatItem: ChatItem = {
|
|
|
|
|
id: thread.id,
|
|
|
|
|
name: thread.title || `Chat ${thread.id}`,
|
|
|
|
|
url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`,
|
|
|
|
|
visibility: thread.visibility,
|
|
|
|
|
isOwnThread: thread.is_own_thread,
|
2026-01-24 15:17:35 +05:30
|
|
|
archived: thread.archived,
|
2026-01-13 00:17:12 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Split based on visibility, not ownership:
|
|
|
|
|
// - PRIVATE chats go to "Private Chats" section
|
|
|
|
|
// - SEARCH_SPACE chats go to "Shared Chats" section
|
|
|
|
|
if (thread.visibility === "SEARCH_SPACE") {
|
|
|
|
|
sharedChatsList.push(chatItem);
|
|
|
|
|
} else {
|
|
|
|
|
privateChats.push(chatItem);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-01-13 00:17:12 -08:00
|
|
|
return { myChats: privateChats, sharedChats: sharedChatsList };
|
|
|
|
|
}, [threadsData, searchSpaceId]);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
|
|
|
|
// Navigation items
|
|
|
|
|
const navItems: NavItem[] = useMemo(
|
|
|
|
|
() => [
|
|
|
|
|
{
|
2026-01-21 19:43:20 +05:30
|
|
|
title: "Inbox",
|
2026-03-07 04:37:37 +05:30
|
|
|
url: "#inbox",
|
2026-01-21 19:43:20 +05:30
|
|
|
icon: Inbox,
|
|
|
|
|
isActive: isInboxSidebarOpen,
|
2026-01-28 02:14:36 +05:30
|
|
|
badge: totalUnreadCount > 0 ? formatInboxCount(totalUnreadCount) : undefined,
|
2026-01-08 19:10:40 +02:00
|
|
|
},
|
2026-03-11 02:18:59 +05:30
|
|
|
{
|
|
|
|
|
title: "Documents",
|
|
|
|
|
url: "#documents",
|
|
|
|
|
icon: SquareLibrary,
|
|
|
|
|
isActive: isMobile
|
|
|
|
|
? isDocumentsSidebarOpen
|
|
|
|
|
: isDocumentsSidebarOpen && !isRightPanelCollapsed,
|
|
|
|
|
statusIndicator: documentsProcessingStatus,
|
|
|
|
|
},
|
2026-02-12 16:12:45 -08:00
|
|
|
{
|
|
|
|
|
title: "Announcements",
|
2026-03-07 04:37:37 +05:30
|
|
|
url: "#announcements",
|
2026-02-12 16:12:45 -08:00
|
|
|
icon: Megaphone,
|
2026-03-03 13:09:29 -05:00
|
|
|
isActive: isAnnouncementsSidebarOpen,
|
2026-02-12 16:12:45 -08:00
|
|
|
badge: announcementUnreadCount > 0 ? formatInboxCount(announcementUnreadCount) : undefined,
|
|
|
|
|
},
|
2026-01-08 19:10:40 +02:00
|
|
|
],
|
2026-03-11 02:18:59 +05:30
|
|
|
[
|
|
|
|
|
isMobile,
|
|
|
|
|
isInboxSidebarOpen,
|
|
|
|
|
isDocumentsSidebarOpen,
|
|
|
|
|
isRightPanelCollapsed,
|
|
|
|
|
totalUnreadCount,
|
|
|
|
|
isAnnouncementsSidebarOpen,
|
|
|
|
|
announcementUnreadCount,
|
|
|
|
|
documentsProcessingStatus,
|
|
|
|
|
]
|
2026-01-08 19:10:40 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Handlers
|
2026-01-12 15:47:56 +02:00
|
|
|
const handleSearchSpaceSelect = useCallback(
|
2026-01-08 19:10:40 +02:00
|
|
|
(id: number) => {
|
|
|
|
|
router.push(`/dashboard/${id}/new-chat`);
|
|
|
|
|
},
|
|
|
|
|
[router]
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-12 15:47:56 +02:00
|
|
|
const handleAddSearchSpace = useCallback(() => {
|
2026-01-12 16:38:40 +02:00
|
|
|
setIsCreateSearchSpaceDialogOpen(true);
|
|
|
|
|
}, []);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-03-16 21:10:46 +05:30
|
|
|
const setUserSettingsDialog = useSetAtom(userSettingsDialogAtom);
|
|
|
|
|
const setSearchSpaceSettingsDialog = useSetAtom(searchSpaceSettingsDialogAtom);
|
2026-03-16 21:51:15 +05:30
|
|
|
const setTeamDialogOpen = useSetAtom(teamDialogAtom);
|
2026-03-16 21:10:46 +05:30
|
|
|
|
2026-01-12 17:06:05 +02:00
|
|
|
const handleUserSettings = useCallback(() => {
|
2026-03-16 21:10:46 +05:30
|
|
|
setUserSettingsDialog({ open: true, initialTab: "profile" });
|
|
|
|
|
}, [setUserSettingsDialog]);
|
2026-01-12 17:06:05 +02:00
|
|
|
|
2026-01-12 16:38:40 +02:00
|
|
|
const handleSearchSpaceSettings = useCallback(
|
2026-03-16 21:10:46 +05:30
|
|
|
(_space: SearchSpace) => {
|
|
|
|
|
setSearchSpaceSettingsDialog({ open: true, initialTab: "general" });
|
2026-01-12 16:38:40 +02:00
|
|
|
},
|
2026-03-16 21:10:46 +05:30
|
|
|
[setSearchSpaceSettingsDialog]
|
2026-01-12 16:38:40 +02:00
|
|
|
);
|
|
|
|
|
|
2026-01-13 01:45:58 -08:00
|
|
|
const handleSearchSpaceDeleteClick = useCallback((space: SearchSpace) => {
|
2026-01-13 02:06:35 -08:00
|
|
|
// If user is owner, show delete dialog; otherwise show leave dialog
|
|
|
|
|
if (space.isOwner) {
|
|
|
|
|
setSearchSpaceToDelete(space);
|
|
|
|
|
setShowDeleteSearchSpaceDialog(true);
|
|
|
|
|
} else {
|
|
|
|
|
setSearchSpaceToLeave(space);
|
|
|
|
|
setShowLeaveSearchSpaceDialog(true);
|
|
|
|
|
}
|
2026-01-13 01:45:58 -08:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const confirmDeleteSearchSpace = useCallback(async () => {
|
|
|
|
|
if (!searchSpaceToDelete) return;
|
|
|
|
|
setIsDeletingSearchSpace(true);
|
|
|
|
|
try {
|
|
|
|
|
await deleteSearchSpace({ id: searchSpaceToDelete.id });
|
2026-03-11 15:09:10 -07:00
|
|
|
|
|
|
|
|
const isCurrentSpace = Number(searchSpaceId) === searchSpaceToDelete.id;
|
|
|
|
|
|
|
|
|
|
// Await refetch so we have the freshest list (backend now hides [DELETING] spaces)
|
|
|
|
|
const result = await refetchSearchSpaces();
|
2026-03-11 15:10:53 -07:00
|
|
|
const updatedSpaces = (result.data ?? []).filter((s) => s.id !== searchSpaceToDelete.id);
|
2026-03-11 15:09:10 -07:00
|
|
|
|
|
|
|
|
if (isCurrentSpace) {
|
|
|
|
|
if (updatedSpaces.length > 0) {
|
|
|
|
|
router.push(`/dashboard/${updatedSpaces[0].id}/new-chat`);
|
|
|
|
|
} else {
|
|
|
|
|
router.push("/dashboard");
|
2026-01-12 16:38:40 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-13 01:45:58 -08:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error deleting search space:", error);
|
2026-03-11 15:09:10 -07:00
|
|
|
toast.error(
|
|
|
|
|
t.has("delete_space_error") ? t("delete_space_error") : "Failed to delete search space"
|
|
|
|
|
);
|
2026-01-13 01:45:58 -08:00
|
|
|
} finally {
|
|
|
|
|
setIsDeletingSearchSpace(false);
|
|
|
|
|
setShowDeleteSearchSpaceDialog(false);
|
|
|
|
|
setSearchSpaceToDelete(null);
|
|
|
|
|
}
|
2026-03-11 15:10:53 -07:00
|
|
|
}, [searchSpaceToDelete, deleteSearchSpace, refetchSearchSpaces, searchSpaceId, router, t]);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-01-13 02:06:35 -08:00
|
|
|
const confirmLeaveSearchSpace = useCallback(async () => {
|
|
|
|
|
if (!searchSpaceToLeave) return;
|
|
|
|
|
setIsLeavingSearchSpace(true);
|
|
|
|
|
try {
|
|
|
|
|
await searchSpacesApiService.leaveSearchSpace(searchSpaceToLeave.id);
|
2026-03-11 15:09:10 -07:00
|
|
|
|
|
|
|
|
const isCurrentSpace = Number(searchSpaceId) === searchSpaceToLeave.id;
|
|
|
|
|
|
|
|
|
|
const result = await refetchSearchSpaces();
|
2026-03-11 15:10:53 -07:00
|
|
|
const updatedSpaces = (result.data ?? []).filter((s) => s.id !== searchSpaceToLeave.id);
|
2026-03-11 15:09:10 -07:00
|
|
|
|
|
|
|
|
if (isCurrentSpace) {
|
|
|
|
|
if (updatedSpaces.length > 0) {
|
|
|
|
|
router.push(`/dashboard/${updatedSpaces[0].id}/new-chat`);
|
|
|
|
|
} else {
|
|
|
|
|
router.push("/dashboard");
|
2026-01-13 02:06:35 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error leaving search space:", error);
|
2026-03-11 15:09:10 -07:00
|
|
|
toast.error(t.has("leave_error") ? t("leave_error") : "Failed to leave search space");
|
2026-01-13 02:06:35 -08:00
|
|
|
} finally {
|
|
|
|
|
setIsLeavingSearchSpace(false);
|
|
|
|
|
setShowLeaveSearchSpaceDialog(false);
|
|
|
|
|
setSearchSpaceToLeave(null);
|
|
|
|
|
}
|
2026-03-11 15:09:10 -07:00
|
|
|
}, [searchSpaceToLeave, refetchSearchSpaces, searchSpaceId, router, t]);
|
2026-01-13 02:06:35 -08:00
|
|
|
|
2026-01-08 19:10:40 +02:00
|
|
|
const handleNavItemClick = useCallback(
|
|
|
|
|
(item: NavItem) => {
|
2026-01-21 19:43:20 +05:30
|
|
|
if (item.url === "#inbox") {
|
2026-02-09 11:41:55 -05:00
|
|
|
setIsInboxSidebarOpen((prev) => {
|
|
|
|
|
if (!prev) {
|
|
|
|
|
setIsAllSharedChatsSidebarOpen(false);
|
|
|
|
|
setIsAllPrivateChatsSidebarOpen(false);
|
2026-03-07 04:37:37 +05:30
|
|
|
setIsAnnouncementsSidebarOpen(false);
|
2026-03-05 20:34:02 +05:30
|
|
|
}
|
|
|
|
|
return !prev;
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (item.url === "#documents") {
|
2026-03-11 02:18:33 +05:30
|
|
|
if (!isMobile) {
|
|
|
|
|
if (!isDocumentsSidebarOpen) {
|
|
|
|
|
setIsDocumentsSidebarOpen(true);
|
|
|
|
|
setIsRightPanelCollapsed(false);
|
2026-03-05 20:34:02 +05:30
|
|
|
setIsInboxSidebarOpen(false);
|
|
|
|
|
setIsAllSharedChatsSidebarOpen(false);
|
|
|
|
|
setIsAllPrivateChatsSidebarOpen(false);
|
2026-03-03 13:09:29 -05:00
|
|
|
setIsAnnouncementsSidebarOpen(false);
|
2026-03-11 02:18:33 +05:30
|
|
|
} else {
|
|
|
|
|
setIsRightPanelCollapsed((prev) => !prev);
|
2026-03-03 13:09:29 -05:00
|
|
|
}
|
2026-03-11 02:18:33 +05:30
|
|
|
} else {
|
|
|
|
|
setIsDocumentsSidebarOpen((prev) => {
|
|
|
|
|
if (!prev) {
|
|
|
|
|
setIsInboxSidebarOpen(false);
|
|
|
|
|
setIsAllSharedChatsSidebarOpen(false);
|
|
|
|
|
setIsAllPrivateChatsSidebarOpen(false);
|
|
|
|
|
setIsAnnouncementsSidebarOpen(false);
|
|
|
|
|
}
|
|
|
|
|
return !prev;
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-03 13:09:29 -05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (item.url === "#announcements") {
|
|
|
|
|
setIsAnnouncementsSidebarOpen((prev) => {
|
|
|
|
|
if (!prev) {
|
|
|
|
|
setIsInboxSidebarOpen(false);
|
|
|
|
|
setIsAllSharedChatsSidebarOpen(false);
|
|
|
|
|
setIsAllPrivateChatsSidebarOpen(false);
|
2026-02-09 11:41:55 -05:00
|
|
|
}
|
|
|
|
|
return !prev;
|
|
|
|
|
});
|
2026-01-21 19:43:20 +05:30
|
|
|
return;
|
|
|
|
|
}
|
2026-01-08 19:10:40 +02:00
|
|
|
router.push(item.url);
|
|
|
|
|
},
|
2026-03-11 02:18:33 +05:30
|
|
|
[router, isMobile, isDocumentsSidebarOpen, setIsDocumentsSidebarOpen, setIsRightPanelCollapsed]
|
2026-01-08 19:10:40 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleNewChat = useCallback(() => {
|
2026-01-23 13:27:14 -05:00
|
|
|
// Check if router is out of sync (thread created via replaceState but params don't have chat_id)
|
|
|
|
|
const isOutOfSync = currentThreadState.id !== null && !params?.chat_id;
|
|
|
|
|
|
|
|
|
|
if (isOutOfSync) {
|
|
|
|
|
// First sync Next.js router by navigating to the current chat's actual URL
|
|
|
|
|
// This updates the router's internal state to match the browser URL
|
|
|
|
|
router.replace(`/dashboard/${searchSpaceId}/new-chat/${currentThreadState.id}`);
|
|
|
|
|
// Set flag to trigger navigation to new-chat after params update
|
|
|
|
|
setPendingNewChat(true);
|
|
|
|
|
} else {
|
|
|
|
|
// Normal navigation - router is in sync
|
|
|
|
|
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
|
|
|
|
}
|
|
|
|
|
}, [router, searchSpaceId, currentThreadState.id, params?.chat_id]);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
|
|
|
|
const handleChatSelect = useCallback(
|
|
|
|
|
(chat: ChatItem) => {
|
|
|
|
|
router.push(chat.url);
|
|
|
|
|
},
|
|
|
|
|
[router]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleChatDelete = useCallback((chat: ChatItem) => {
|
|
|
|
|
setChatToDelete({ id: chat.id, name: chat.name });
|
|
|
|
|
setShowDeleteChatDialog(true);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-02-03 20:47:18 -05:00
|
|
|
const handleChatRename = useCallback((chat: ChatItem) => {
|
|
|
|
|
setChatToRename({ id: chat.id, name: chat.name });
|
|
|
|
|
setNewChatTitle(chat.name);
|
|
|
|
|
setShowRenameChatDialog(true);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-01-24 15:17:35 +05:30
|
|
|
const handleChatArchive = useCallback(
|
|
|
|
|
async (chat: ChatItem) => {
|
|
|
|
|
const newArchivedState = !chat.archived;
|
|
|
|
|
const successMessage = newArchivedState
|
|
|
|
|
? tSidebar("chat_archived") || "Chat archived"
|
|
|
|
|
: tSidebar("chat_unarchived") || "Chat restored";
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await updateThread(chat.id, { archived: newArchivedState });
|
|
|
|
|
toast.success(successMessage);
|
|
|
|
|
// Invalidate queries to refresh UI (React Query will only refetch active queries)
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error archiving thread:", error);
|
|
|
|
|
toast.error(tSidebar("error_archiving_chat") || "Failed to archive chat");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[queryClient, searchSpaceId, tSidebar]
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-08 19:10:40 +02:00
|
|
|
const handleSettings = useCallback(() => {
|
2026-03-16 21:10:46 +05:30
|
|
|
setSearchSpaceSettingsDialog({ open: true, initialTab: "general" });
|
|
|
|
|
}, [setSearchSpaceSettingsDialog]);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-01-12 15:47:56 +02:00
|
|
|
const handleManageMembers = useCallback(() => {
|
2026-03-16 21:51:15 +05:30
|
|
|
setTeamDialogOpen(true);
|
|
|
|
|
}, [setTeamDialogOpen]);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-01-16 11:32:06 -08:00
|
|
|
const handleLogout = useCallback(async () => {
|
2026-01-08 19:10:40 +02:00
|
|
|
try {
|
|
|
|
|
trackLogout();
|
|
|
|
|
resetUser();
|
2026-01-16 11:32:06 -08:00
|
|
|
|
|
|
|
|
// Best-effort cleanup of Electric SQL / PGlite
|
|
|
|
|
// Even if this fails, login-time cleanup will handle it
|
|
|
|
|
try {
|
|
|
|
|
await cleanupElectric();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn("[Logout] Electric cleanup failed (will be handled on next login):", err);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 18:56:38 +02:00
|
|
|
// Revoke refresh token on server and clear all tokens from localStorage
|
|
|
|
|
await logout();
|
|
|
|
|
|
2026-01-08 19:10:40 +02:00
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
router.push("/");
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error during logout:", error);
|
2026-02-05 18:56:38 +02:00
|
|
|
await logout();
|
2026-01-08 19:10:40 +02:00
|
|
|
router.push("/");
|
|
|
|
|
}
|
|
|
|
|
}, [router]);
|
|
|
|
|
|
2026-01-13 00:17:12 -08:00
|
|
|
const handleViewAllSharedChats = useCallback(() => {
|
2026-03-17 03:55:49 +05:30
|
|
|
setIsAllSharedChatsSidebarOpen((prev) => {
|
|
|
|
|
if (!prev) {
|
|
|
|
|
setIsAllPrivateChatsSidebarOpen(false);
|
|
|
|
|
setIsInboxSidebarOpen(false);
|
|
|
|
|
setIsAnnouncementsSidebarOpen(false);
|
|
|
|
|
}
|
|
|
|
|
return !prev;
|
|
|
|
|
});
|
2026-03-10 15:20:51 +05:30
|
|
|
}, []);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-01-13 00:17:12 -08:00
|
|
|
const handleViewAllPrivateChats = useCallback(() => {
|
2026-03-17 03:55:49 +05:30
|
|
|
setIsAllPrivateChatsSidebarOpen((prev) => {
|
|
|
|
|
if (!prev) {
|
|
|
|
|
setIsAllSharedChatsSidebarOpen(false);
|
|
|
|
|
setIsInboxSidebarOpen(false);
|
|
|
|
|
setIsAnnouncementsSidebarOpen(false);
|
|
|
|
|
}
|
|
|
|
|
return !prev;
|
|
|
|
|
});
|
2026-03-10 15:20:51 +05:30
|
|
|
}, []);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
|
|
|
|
// Delete handlers
|
|
|
|
|
const confirmDeleteChat = useCallback(async () => {
|
|
|
|
|
if (!chatToDelete) return;
|
|
|
|
|
setIsDeletingChat(true);
|
|
|
|
|
try {
|
|
|
|
|
await deleteThread(chatToDelete.id);
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
|
|
|
|
|
if (currentChatId === chatToDelete.id) {
|
2026-03-09 02:22:01 +05:30
|
|
|
resetCurrentThread();
|
|
|
|
|
const isOutOfSync = currentThreadState.id !== null && !params?.chat_id;
|
|
|
|
|
if (isOutOfSync) {
|
|
|
|
|
window.history.replaceState(null, "", `/dashboard/${searchSpaceId}/new-chat`);
|
|
|
|
|
setChatResetKey((k) => k + 1);
|
|
|
|
|
} else {
|
|
|
|
|
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
|
|
|
|
}
|
2026-01-08 19:10:40 +02:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error deleting thread:", error);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsDeletingChat(false);
|
|
|
|
|
setShowDeleteChatDialog(false);
|
|
|
|
|
setChatToDelete(null);
|
|
|
|
|
}
|
2026-03-09 02:22:01 +05:30
|
|
|
}, [
|
|
|
|
|
chatToDelete,
|
|
|
|
|
queryClient,
|
|
|
|
|
searchSpaceId,
|
|
|
|
|
resetCurrentThread,
|
|
|
|
|
currentChatId,
|
|
|
|
|
currentThreadState.id,
|
|
|
|
|
params?.chat_id,
|
|
|
|
|
router,
|
|
|
|
|
]);
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-02-03 20:47:18 -05:00
|
|
|
// Rename handler
|
|
|
|
|
const confirmRenameChat = useCallback(async () => {
|
|
|
|
|
if (!chatToRename || !newChatTitle.trim()) return;
|
|
|
|
|
setIsRenamingChat(true);
|
|
|
|
|
try {
|
|
|
|
|
await updateThread(chatToRename.id, { title: newChatTitle.trim() });
|
|
|
|
|
toast.success(tSidebar("chat_renamed") || "Chat renamed");
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error renaming thread:", error);
|
|
|
|
|
toast.error(tSidebar("error_renaming_chat") || "Failed to rename chat");
|
|
|
|
|
} finally {
|
|
|
|
|
setIsRenamingChat(false);
|
|
|
|
|
setShowRenameChatDialog(false);
|
|
|
|
|
setChatToRename(null);
|
|
|
|
|
setNewChatTitle("");
|
|
|
|
|
}
|
|
|
|
|
}, [chatToRename, newChatTitle, queryClient, searchSpaceId, tSidebar]);
|
|
|
|
|
|
2026-01-08 19:10:40 +02:00
|
|
|
// Page usage
|
|
|
|
|
const pageUsage = user
|
|
|
|
|
? {
|
|
|
|
|
pagesUsed: user.pages_used,
|
|
|
|
|
pagesLimit: user.pages_limit,
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
// Detect if we're on the chat page (needs overflow-hidden for chat's own scroll)
|
|
|
|
|
const isChatPage = pathname?.includes("/new-chat") ?? false;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<LayoutShell
|
2026-01-12 15:47:56 +02:00
|
|
|
searchSpaces={searchSpaces}
|
|
|
|
|
activeSearchSpaceId={Number(searchSpaceId)}
|
|
|
|
|
onSearchSpaceSelect={handleSearchSpaceSelect}
|
2026-01-13 01:45:58 -08:00
|
|
|
onSearchSpaceDelete={handleSearchSpaceDeleteClick}
|
|
|
|
|
onSearchSpaceSettings={handleSearchSpaceSettings}
|
2026-01-12 15:47:56 +02:00
|
|
|
onAddSearchSpace={handleAddSearchSpace}
|
|
|
|
|
searchSpace={activeSearchSpace}
|
2026-01-08 19:10:40 +02:00
|
|
|
navItems={navItems}
|
|
|
|
|
onNavItemClick={handleNavItemClick}
|
2026-01-13 00:17:12 -08:00
|
|
|
chats={myChats}
|
|
|
|
|
sharedChats={sharedChats}
|
2026-01-08 19:10:40 +02:00
|
|
|
activeChatId={currentChatId}
|
|
|
|
|
onNewChat={handleNewChat}
|
|
|
|
|
onChatSelect={handleChatSelect}
|
2026-02-03 20:47:18 -05:00
|
|
|
onChatRename={handleChatRename}
|
2026-01-08 19:10:40 +02:00
|
|
|
onChatDelete={handleChatDelete}
|
2026-01-24 15:17:35 +05:30
|
|
|
onChatArchive={handleChatArchive}
|
2026-01-13 00:17:12 -08:00
|
|
|
onViewAllSharedChats={handleViewAllSharedChats}
|
|
|
|
|
onViewAllPrivateChats={handleViewAllPrivateChats}
|
2026-01-14 15:22:24 +02:00
|
|
|
user={{
|
|
|
|
|
email: user?.email || "",
|
|
|
|
|
name: user?.display_name || user?.email?.split("@")[0],
|
|
|
|
|
avatarUrl: user?.avatar_url || undefined,
|
|
|
|
|
}}
|
2026-01-13 01:45:58 -08:00
|
|
|
onSettings={handleSettings}
|
|
|
|
|
onManageMembers={handleManageMembers}
|
|
|
|
|
onUserSettings={handleUserSettings}
|
2026-01-08 19:10:40 +02:00
|
|
|
onLogout={handleLogout}
|
|
|
|
|
pageUsage={pageUsage}
|
|
|
|
|
theme={theme}
|
2026-01-20 16:04:56 +05:30
|
|
|
setTheme={setTheme}
|
2026-01-08 19:10:40 +02:00
|
|
|
isChatPage={isChatPage}
|
2026-02-04 20:13:33 +05:30
|
|
|
isLoadingChats={isLoadingThreads}
|
2026-01-27 19:46:43 +05:30
|
|
|
inbox={{
|
|
|
|
|
isOpen: isInboxSidebarOpen,
|
|
|
|
|
onOpenChange: setIsInboxSidebarOpen,
|
2026-01-28 02:14:36 +05:30
|
|
|
totalUnreadCount,
|
2026-03-06 19:35:35 +05:30
|
|
|
comments: {
|
|
|
|
|
items: commentsInbox.inboxItems,
|
|
|
|
|
unreadCount: commentsInbox.unreadCount,
|
|
|
|
|
loading: commentsInbox.loading,
|
|
|
|
|
loadingMore: commentsInbox.loadingMore,
|
|
|
|
|
hasMore: commentsInbox.hasMore,
|
|
|
|
|
loadMore: commentsInbox.loadMore,
|
|
|
|
|
markAsRead: commentsInbox.markAsRead,
|
|
|
|
|
markAllAsRead: commentsInbox.markAllAsRead,
|
|
|
|
|
},
|
|
|
|
|
status: {
|
|
|
|
|
items: statusInbox.inboxItems,
|
|
|
|
|
unreadCount: statusInbox.unreadCount,
|
|
|
|
|
loading: statusInbox.loading,
|
|
|
|
|
loadingMore: statusInbox.loadingMore,
|
|
|
|
|
hasMore: statusInbox.hasMore,
|
|
|
|
|
loadMore: statusInbox.loadMore,
|
|
|
|
|
markAsRead: statusInbox.markAsRead,
|
|
|
|
|
markAllAsRead: statusInbox.markAllAsRead,
|
|
|
|
|
},
|
2026-03-10 16:17:12 +05:30
|
|
|
}}
|
2026-03-03 13:09:29 -05:00
|
|
|
announcementsPanel={{
|
|
|
|
|
open: isAnnouncementsSidebarOpen,
|
|
|
|
|
onOpenChange: setIsAnnouncementsSidebarOpen,
|
|
|
|
|
}}
|
2026-02-09 11:41:55 -05:00
|
|
|
allSharedChatsPanel={{
|
|
|
|
|
open: isAllSharedChatsSidebarOpen,
|
|
|
|
|
onOpenChange: setIsAllSharedChatsSidebarOpen,
|
|
|
|
|
searchSpaceId,
|
|
|
|
|
}}
|
|
|
|
|
allPrivateChatsPanel={{
|
|
|
|
|
open: isAllPrivateChatsSidebarOpen,
|
|
|
|
|
onOpenChange: setIsAllPrivateChatsSidebarOpen,
|
|
|
|
|
searchSpaceId,
|
|
|
|
|
}}
|
2026-03-05 20:34:02 +05:30
|
|
|
documentsPanel={{
|
|
|
|
|
open: isDocumentsSidebarOpen,
|
|
|
|
|
onOpenChange: setIsDocumentsSidebarOpen,
|
2026-03-10 12:26:45 +05:30
|
|
|
isDocked: isDocumentsDocked,
|
|
|
|
|
onDockedChange: setIsDocumentsDocked,
|
2026-03-05 20:34:02 +05:30
|
|
|
}}
|
2026-01-08 19:10:40 +02:00
|
|
|
>
|
2026-03-09 02:22:01 +05:30
|
|
|
<Fragment key={chatResetKey}>{children}</Fragment>
|
2026-01-08 19:10:40 +02:00
|
|
|
</LayoutShell>
|
|
|
|
|
|
|
|
|
|
{/* Delete Chat Dialog */}
|
2026-03-07 04:15:40 +05:30
|
|
|
<AlertDialog open={showDeleteChatDialog} onOpenChange={setShowDeleteChatDialog}>
|
|
|
|
|
<AlertDialogContent className="sm:max-w-md">
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>{t("delete_chat")}</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
2026-01-08 19:10:40 +02:00
|
|
|
{t("delete_chat_confirm")} <span className="font-medium">{chatToDelete?.name}</span>?{" "}
|
|
|
|
|
{t("action_cannot_undone")}
|
2026-03-07 04:15:40 +05:30
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
2026-03-07 04:46:48 +05:30
|
|
|
<AlertDialogCancel disabled={isDeletingChat}>{tCommon("cancel")}</AlertDialogCancel>
|
2026-03-07 04:15:40 +05:30
|
|
|
<AlertDialogAction
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
confirmDeleteChat();
|
|
|
|
|
}}
|
2026-01-08 19:10:40 +02:00
|
|
|
disabled={isDeletingChat}
|
2026-03-07 04:15:40 +05:30
|
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 gap-2"
|
2026-01-08 19:10:40 +02:00
|
|
|
>
|
2026-03-21 13:20:13 +05:30
|
|
|
{isDeletingChat ? <Spinner size="sm" /> : tCommon("delete")}
|
2026-03-07 04:15:40 +05:30
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
2026-01-08 19:10:40 +02:00
|
|
|
|
2026-02-03 20:47:18 -05:00
|
|
|
{/* Rename Chat Dialog */}
|
|
|
|
|
<Dialog open={showRenameChatDialog} onOpenChange={setShowRenameChatDialog}>
|
|
|
|
|
<DialogContent className="sm:max-w-md">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle className="flex items-center gap-2">
|
|
|
|
|
<span>{tSidebar("rename_chat") || "Rename Chat"}</span>
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
{tSidebar("rename_chat_description") || "Enter a new name for this conversation."}
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<Input
|
|
|
|
|
value={newChatTitle}
|
|
|
|
|
onChange={(e) => setNewChatTitle(e.target.value)}
|
|
|
|
|
placeholder={tSidebar("chat_title_placeholder") || "Chat title"}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === "Enter" && !isRenamingChat && newChatTitle.trim()) {
|
|
|
|
|
confirmRenameChat();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<DialogFooter className="flex gap-2 sm:justify-end">
|
|
|
|
|
<Button
|
2026-03-07 04:15:40 +05:30
|
|
|
variant="secondary"
|
2026-02-03 20:47:18 -05:00
|
|
|
onClick={() => setShowRenameChatDialog(false)}
|
|
|
|
|
disabled={isRenamingChat}
|
|
|
|
|
>
|
|
|
|
|
{tCommon("cancel")}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={confirmRenameChat}
|
|
|
|
|
disabled={isRenamingChat || !newChatTitle.trim()}
|
|
|
|
|
className="gap-2"
|
|
|
|
|
>
|
|
|
|
|
{isRenamingChat ? (
|
|
|
|
|
<>
|
|
|
|
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
2026-02-21 23:59:04 +05:30
|
|
|
{tSidebar("renaming") || "Renaming"}
|
2026-02-03 20:47:18 -05:00
|
|
|
</>
|
|
|
|
|
) : (
|
2026-03-07 04:15:40 +05:30
|
|
|
tSidebar("rename") || "Rename"
|
2026-02-03 20:47:18 -05:00
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
2026-01-13 01:45:58 -08:00
|
|
|
{/* Delete Search Space Dialog */}
|
2026-03-07 04:15:40 +05:30
|
|
|
<AlertDialog open={showDeleteSearchSpaceDialog} onOpenChange={setShowDeleteSearchSpaceDialog}>
|
|
|
|
|
<AlertDialogContent className="sm:max-w-md">
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>{t("delete_search_space")}</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
2026-01-13 01:45:58 -08:00
|
|
|
{t("delete_space_confirm", { name: searchSpaceToDelete?.name || "" })}
|
2026-03-07 04:15:40 +05:30
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel disabled={isDeletingSearchSpace}>
|
2026-01-13 01:45:58 -08:00
|
|
|
{tCommon("cancel")}
|
2026-03-07 04:15:40 +05:30
|
|
|
</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
confirmDeleteSearchSpace();
|
|
|
|
|
}}
|
2026-01-13 01:45:58 -08:00
|
|
|
disabled={isDeletingSearchSpace}
|
2026-03-07 04:15:40 +05:30
|
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 gap-2"
|
2026-01-13 01:45:58 -08:00
|
|
|
>
|
|
|
|
|
{isDeletingSearchSpace ? (
|
|
|
|
|
<>
|
|
|
|
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
|
|
|
{t("deleting")}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
2026-03-07 04:15:40 +05:30
|
|
|
tCommon("delete")
|
2026-01-13 01:45:58 -08:00
|
|
|
)}
|
2026-03-07 04:15:40 +05:30
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
2026-01-13 01:45:58 -08:00
|
|
|
|
2026-01-13 02:06:35 -08:00
|
|
|
{/* Leave Search Space Dialog */}
|
2026-03-07 04:15:40 +05:30
|
|
|
<AlertDialog open={showLeaveSearchSpaceDialog} onOpenChange={setShowLeaveSearchSpaceDialog}>
|
|
|
|
|
<AlertDialogContent className="sm:max-w-md">
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>{t("leave_title")}</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
2026-01-13 02:06:35 -08:00
|
|
|
{t("leave_confirm", { name: searchSpaceToLeave?.name || "" })}
|
2026-03-07 04:15:40 +05:30
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel disabled={isLeavingSearchSpace}>
|
2026-01-13 02:06:35 -08:00
|
|
|
{tCommon("cancel")}
|
2026-03-07 04:15:40 +05:30
|
|
|
</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
confirmLeaveSearchSpace();
|
|
|
|
|
}}
|
2026-01-13 02:06:35 -08:00
|
|
|
disabled={isLeavingSearchSpace}
|
2026-03-07 04:15:40 +05:30
|
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 gap-2"
|
2026-01-13 02:06:35 -08:00
|
|
|
>
|
|
|
|
|
{isLeavingSearchSpace ? (
|
|
|
|
|
<>
|
|
|
|
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
|
|
|
{t("leaving")}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
2026-03-07 04:15:40 +05:30
|
|
|
t("leave")
|
2026-01-13 02:06:35 -08:00
|
|
|
)}
|
2026-03-07 04:15:40 +05:30
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
2026-01-13 02:06:35 -08:00
|
|
|
|
2026-01-12 16:38:40 +02:00
|
|
|
{/* Create Search Space Dialog */}
|
|
|
|
|
<CreateSearchSpaceDialog
|
|
|
|
|
open={isCreateSearchSpaceDialogOpen}
|
|
|
|
|
onOpenChange={setIsCreateSearchSpaceDialogOpen}
|
|
|
|
|
/>
|
2026-03-16 21:10:46 +05:30
|
|
|
|
|
|
|
|
{/* Settings Dialogs */}
|
|
|
|
|
<SearchSpaceSettingsDialog searchSpaceId={Number(searchSpaceId)} />
|
|
|
|
|
<UserSettingsDialog />
|
2026-03-16 21:51:15 +05:30
|
|
|
<TeamDialog searchSpaceId={Number(searchSpaceId)} />
|
2026-03-17 01:50:15 +05:30
|
|
|
<MorePagesDialog />
|
2026-01-08 19:10:40 +02:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|