mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
feat(chat): implement chat tab synchronization and enhance thread activation with new hooks for improved navigation and metadata management
This commit is contained in:
parent
168c0d2f89
commit
08801fe3e8
13 changed files with 276 additions and 85 deletions
|
|
@ -8,11 +8,7 @@ import { useTranslations } from "next-intl";
|
|||
import { useTheme } from "next-themes";
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
currentThreadAtom,
|
||||
resetCurrentThreadAtom,
|
||||
setCurrentThreadMetadataAtom,
|
||||
} from "@/atoms/chat/current-thread.atom";
|
||||
import { currentThreadAtom, resetCurrentThreadAtom } from "@/atoms/chat/current-thread.atom";
|
||||
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
||||
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
|
||||
import { announcementsDialogAtom } from "@/atoms/layout/dialogs.atom";
|
||||
|
|
@ -45,6 +41,7 @@ import {
|
|||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useActivateChatThread } from "@/hooks/use-activate-chat-thread";
|
||||
import { useAnnouncements } from "@/hooks/use-announcements";
|
||||
import { useInbox } from "@/hooks/use-inbox";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
|
|
@ -98,9 +95,9 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
const { mutateAsync: deleteSearchSpace } = useAtomValue(deleteSearchSpaceMutationAtom);
|
||||
const currentThreadState = useAtomValue(currentThreadAtom);
|
||||
const resetCurrentThread = useSetAtom(resetCurrentThreadAtom);
|
||||
const setCurrentThreadMetadata = useSetAtom(setCurrentThreadMetadataAtom);
|
||||
const syncChatTab = useSetAtom(syncChatTabAtom);
|
||||
const removeChatTab = useSetAtom(removeChatTabAtom);
|
||||
const { activateChatThread, prefetchChatThread } = useActivateChatThread();
|
||||
const { mutateAsync: archiveThread } = useArchiveThread(searchSpaceId);
|
||||
const { mutateAsync: deleteThread } = useDeleteThread(searchSpaceId);
|
||||
const { mutateAsync: renameThread } = useRenameThread(searchSpaceId);
|
||||
|
|
@ -309,6 +306,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
title: chatId ? (thread?.title ?? undefined) : "New Chat",
|
||||
chatUrl,
|
||||
searchSpaceId: Number(searchSpaceId),
|
||||
...(thread?.visibility !== undefined ? { visibility: thread.visibility } : {}),
|
||||
});
|
||||
}, [currentChatId, searchSpaceId, threadsData?.threads, syncChatTab]);
|
||||
|
||||
|
|
@ -469,12 +467,34 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
const handleTabSwitch = useCallback(
|
||||
(tab: Tab) => {
|
||||
if (tab.type === "chat") {
|
||||
const url = tab.chatUrl || `/dashboard/${searchSpaceId}/new-chat`;
|
||||
router.push(url);
|
||||
activateChatThread({
|
||||
id: tab.chatId ?? null,
|
||||
title: tab.title,
|
||||
url: tab.chatUrl,
|
||||
searchSpaceId: tab.searchSpaceId ?? searchSpaceId,
|
||||
...(tab.visibility !== undefined ? { visibility: tab.visibility } : {}),
|
||||
...(tab.hasComments !== undefined ? { hasComments: tab.hasComments } : {}),
|
||||
});
|
||||
}
|
||||
// Document tabs are handled in-place by LayoutShell — no navigation needed
|
||||
},
|
||||
[router, searchSpaceId]
|
||||
[activateChatThread, searchSpaceId]
|
||||
);
|
||||
|
||||
const handleTabPrefetch = useCallback(
|
||||
(tab: Tab) => {
|
||||
if (tab.type === "chat") {
|
||||
prefetchChatThread(tab.chatId);
|
||||
}
|
||||
},
|
||||
[prefetchChatThread]
|
||||
);
|
||||
|
||||
const handleChatPrefetch = useCallback(
|
||||
(chat: ChatItem) => {
|
||||
prefetchChatThread(chat.id);
|
||||
},
|
||||
[prefetchChatThread]
|
||||
);
|
||||
|
||||
const handleNavItemClick = useCallback(
|
||||
|
|
@ -526,20 +546,15 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
|
||||
const handleChatSelect = useCallback(
|
||||
(chat: ChatItem) => {
|
||||
syncChatTab({
|
||||
chatId: chat.id,
|
||||
title: chat.name,
|
||||
chatUrl: chat.url,
|
||||
searchSpaceId: Number(searchSpaceId),
|
||||
});
|
||||
setCurrentThreadMetadata({
|
||||
activateChatThread({
|
||||
id: chat.id,
|
||||
visibility: chat.visibility ?? "PRIVATE",
|
||||
hasComments: false,
|
||||
title: chat.name,
|
||||
url: chat.url,
|
||||
searchSpaceId,
|
||||
...(chat.visibility !== undefined ? { visibility: chat.visibility } : {}),
|
||||
});
|
||||
router.push(chat.url);
|
||||
},
|
||||
[router, searchSpaceId, setCurrentThreadMetadata, syncChatTab]
|
||||
[activateChatThread, searchSpaceId]
|
||||
);
|
||||
|
||||
const handleChatDelete = useCallback((chat: ChatItem) => {
|
||||
|
|
@ -611,7 +626,16 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
if (currentChatId === chatToDelete.id) {
|
||||
resetCurrentThread();
|
||||
if (fallbackTab?.type === "chat" && fallbackTab.chatUrl) {
|
||||
router.push(fallbackTab.chatUrl);
|
||||
activateChatThread({
|
||||
id: fallbackTab.chatId ?? null,
|
||||
title: fallbackTab.title,
|
||||
url: fallbackTab.chatUrl,
|
||||
searchSpaceId: fallbackTab.searchSpaceId ?? searchSpaceId,
|
||||
...(fallbackTab.visibility !== undefined ? { visibility: fallbackTab.visibility } : {}),
|
||||
...(fallbackTab.hasComments !== undefined
|
||||
? { hasComments: fallbackTab.hasComments }
|
||||
: {}),
|
||||
});
|
||||
} else {
|
||||
const isOutOfSync = currentThreadState.id !== null && !params?.chat_id;
|
||||
if (isOutOfSync) {
|
||||
|
|
@ -639,6 +663,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
params?.chat_id,
|
||||
router,
|
||||
removeChatTab,
|
||||
activateChatThread,
|
||||
]);
|
||||
|
||||
// Rename handler
|
||||
|
|
@ -693,6 +718,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
activeChatId={currentChatId}
|
||||
onNewChat={handleNewChat}
|
||||
onChatSelect={handleChatSelect}
|
||||
onChatPrefetch={handleChatPrefetch}
|
||||
onChatRename={handleChatRename}
|
||||
onChatDelete={handleChatDelete}
|
||||
onChatArchive={handleChatArchive}
|
||||
|
|
@ -759,6 +785,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
onOpenChange: setIsDocumentsSidebarOpen,
|
||||
}}
|
||||
onTabSwitch={handleTabSwitch}
|
||||
onTabPrefetch={handleTabPrefetch}
|
||||
>
|
||||
<Fragment key={chatResetKey}>{children}</Fragment>
|
||||
</LayoutShell>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
|
|||
const currentThreadState = useAtomValue(currentThreadAtom);
|
||||
|
||||
const hasThread = isChatPage && !isDocumentTab && currentThreadState.id !== null;
|
||||
const activeSearchSpaceId = searchSpaceId ? Number(searchSpaceId) : null;
|
||||
const canRenderShareButton =
|
||||
hasThread &&
|
||||
currentThreadState.id !== null &&
|
||||
currentThreadState.visibility !== null &&
|
||||
currentThreadState.searchSpaceId !== null &&
|
||||
activeSearchSpaceId !== null &&
|
||||
currentThreadState.searchSpaceId === activeSearchSpaceId;
|
||||
|
||||
// Free chat pages have their own header with model selector; only render mobile trigger
|
||||
if (isFreePage) {
|
||||
|
|
@ -37,19 +45,24 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
|
|||
);
|
||||
}
|
||||
|
||||
const threadForButton: ThreadRecord | null =
|
||||
hasThread && currentThreadState.id !== null && searchSpaceId
|
||||
? {
|
||||
id: currentThreadState.id,
|
||||
visibility: currentThreadState.visibility ?? "PRIVATE",
|
||||
created_by_id: null,
|
||||
search_space_id: Number(searchSpaceId),
|
||||
title: "",
|
||||
archived: false,
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
}
|
||||
: null;
|
||||
let threadForButton: ThreadRecord | null = null;
|
||||
if (
|
||||
canRenderShareButton &&
|
||||
currentThreadState.id !== null &&
|
||||
currentThreadState.visibility !== null &&
|
||||
currentThreadState.searchSpaceId !== null
|
||||
) {
|
||||
threadForButton = {
|
||||
id: currentThreadState.id,
|
||||
visibility: currentThreadState.visibility,
|
||||
created_by_id: null,
|
||||
search_space_id: currentThreadState.searchSpaceId,
|
||||
title: "",
|
||||
archived: false,
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 bg-main-panel/95 backdrop-blur supports-backdrop-filter:bg-main-panel/60 px-4">
|
||||
|
|
@ -64,7 +77,7 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
|
|||
{/* Right side - Actions */}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{hasThread && <ActionLogButton threadId={currentThreadState.id} />}
|
||||
{hasThread && <ChatShareButton thread={threadForButton} />}
|
||||
{threadForButton && <ChatShareButton thread={threadForButton} />}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ interface LayoutShellProps {
|
|||
activeChatId?: number | null;
|
||||
onNewChat: () => void;
|
||||
onChatSelect: (chat: ChatItem) => void;
|
||||
onChatPrefetch?: (chat: ChatItem) => void;
|
||||
onChatRename?: (chat: ChatItem) => void;
|
||||
onChatDelete?: (chat: ChatItem) => void;
|
||||
onChatArchive?: (chat: ChatItem) => void;
|
||||
|
|
@ -153,11 +154,13 @@ interface LayoutShellProps {
|
|||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
onTabSwitch?: (tab: Tab) => void;
|
||||
onTabPrefetch?: (tab: Tab) => void;
|
||||
}
|
||||
|
||||
function MainContentPanel({
|
||||
isChatPage,
|
||||
onTabSwitch,
|
||||
onTabPrefetch,
|
||||
onNewChat,
|
||||
showRightPanelExpandButton = true,
|
||||
showTopBorder = false,
|
||||
|
|
@ -165,6 +168,7 @@ function MainContentPanel({
|
|||
}: {
|
||||
isChatPage: boolean;
|
||||
onTabSwitch?: (tab: Tab) => void;
|
||||
onTabPrefetch?: (tab: Tab) => void;
|
||||
onNewChat?: () => void;
|
||||
showRightPanelExpandButton?: boolean;
|
||||
showTopBorder?: boolean;
|
||||
|
|
@ -179,6 +183,7 @@ function MainContentPanel({
|
|||
>
|
||||
<TabBar
|
||||
onTabSwitch={onTabSwitch}
|
||||
onTabPrefetch={onTabPrefetch}
|
||||
onNewChat={onNewChat}
|
||||
rightActions={showRightPanelExpandButton ? <RightPanelExpandButton /> : null}
|
||||
className="min-w-0"
|
||||
|
|
@ -223,6 +228,7 @@ export function LayoutShell({
|
|||
activeChatId,
|
||||
onNewChat,
|
||||
onChatSelect,
|
||||
onChatPrefetch,
|
||||
onChatRename,
|
||||
onChatDelete,
|
||||
onChatArchive,
|
||||
|
|
@ -251,6 +257,7 @@ export function LayoutShell({
|
|||
allChatsPanel,
|
||||
documentsPanel,
|
||||
onTabSwitch,
|
||||
onTabPrefetch,
|
||||
}: LayoutShellProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const electronAPI = useElectronAPI();
|
||||
|
|
@ -305,6 +312,7 @@ export function LayoutShell({
|
|||
activeChatId={activeChatId}
|
||||
onNewChat={onNewChat}
|
||||
onChatSelect={onChatSelect}
|
||||
onChatPrefetch={onChatPrefetch}
|
||||
onChatRename={onChatRename}
|
||||
onChatDelete={onChatDelete}
|
||||
onChatArchive={onChatArchive}
|
||||
|
|
@ -447,6 +455,7 @@ export function LayoutShell({
|
|||
activeChatId={activeChatId}
|
||||
onNewChat={onNewChat}
|
||||
onChatSelect={onChatSelect}
|
||||
onChatPrefetch={onChatPrefetch}
|
||||
onChatRename={onChatRename}
|
||||
onChatDelete={onChatDelete}
|
||||
onChatArchive={onChatArchive}
|
||||
|
|
@ -551,6 +560,7 @@ export function LayoutShell({
|
|||
<MainContentPanel
|
||||
isChatPage={isChatPage}
|
||||
onTabSwitch={onTabSwitch}
|
||||
onTabPrefetch={onTabPrefetch}
|
||||
onNewChat={onNewChat}
|
||||
showRightPanelExpandButton={!isMacDesktop}
|
||||
showTopBorder={isMacDesktop}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ import { useParams, useRouter } from "next/navigation";
|
|||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { setCurrentThreadMetadataAtom } from "@/atoms/chat/current-thread.atom";
|
||||
import { removeChatTabAtom, syncChatTabAtom } from "@/atoms/tabs/tabs.atom";
|
||||
import { removeChatTabAtom } from "@/atoms/tabs/tabs.atom";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -41,11 +40,11 @@ import { Input } from "@/components/ui/input";
|
|||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useActivateChatThread } from "@/hooks/use-activate-chat-thread";
|
||||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||
import { useLongPress } from "@/hooks/use-long-press";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { useArchiveThread, useDeleteThread, useRenameThread } from "@/hooks/use-thread-mutations";
|
||||
import { prefetchThreadData } from "@/hooks/use-thread-queries";
|
||||
import { fetchThreads, searchThreads, type ThreadListItem } from "@/lib/chat/thread-persistence";
|
||||
import { formatThreadTimestamp } from "@/lib/format-date";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -72,8 +71,7 @@ export function AllChatsSidebarContent({
|
|||
const queryClient = useQueryClient();
|
||||
const isMobile = useIsMobile();
|
||||
const removeChatTab = useSetAtom(removeChatTabAtom);
|
||||
const syncChatTab = useSetAtom(syncChatTabAtom);
|
||||
const setCurrentThreadMetadata = useSetAtom(setCurrentThreadMetadataAtom);
|
||||
const { activateChatThread, prefetchChatThread } = useActivateChatThread();
|
||||
const { mutateAsync: deleteThread } = useDeleteThread(searchSpaceId);
|
||||
const { mutateAsync: archiveThread } = useArchiveThread(searchSpaceId);
|
||||
const { mutateAsync: renameThread } = useRenameThread(searchSpaceId);
|
||||
|
|
@ -146,30 +144,16 @@ export function AllChatsSidebarContent({
|
|||
|
||||
const handleThreadClick = useCallback(
|
||||
(thread: ThreadListItem) => {
|
||||
const chatUrl = `/dashboard/${searchSpaceId}/new-chat/${thread.id}`;
|
||||
syncChatTab({
|
||||
chatId: thread.id,
|
||||
title: thread.title || "New Chat",
|
||||
chatUrl,
|
||||
searchSpaceId: Number(searchSpaceId),
|
||||
});
|
||||
setCurrentThreadMetadata({
|
||||
activateChatThread({
|
||||
id: thread.id,
|
||||
title: thread.title || "New Chat",
|
||||
searchSpaceId,
|
||||
visibility: thread.visibility,
|
||||
hasComments: false,
|
||||
});
|
||||
router.push(chatUrl);
|
||||
onOpenChange(false);
|
||||
onCloseMobileSidebar?.();
|
||||
},
|
||||
[
|
||||
router,
|
||||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onCloseMobileSidebar,
|
||||
setCurrentThreadMetadata,
|
||||
syncChatTab,
|
||||
]
|
||||
[activateChatThread, onOpenChange, searchSpaceId, onCloseMobileSidebar]
|
||||
);
|
||||
|
||||
const handleDeleteThread = useCallback(
|
||||
|
|
@ -183,8 +167,23 @@ export function AllChatsSidebarContent({
|
|||
if (currentChatId === threadId) {
|
||||
onOpenChange(false);
|
||||
setTimeout(() => {
|
||||
if (fallbackTab?.type === "chat" && fallbackTab.chatUrl) {
|
||||
router.push(fallbackTab.chatUrl);
|
||||
if (
|
||||
fallbackTab?.type === "chat" &&
|
||||
fallbackTab.chatUrl &&
|
||||
fallbackTab.chatId !== undefined
|
||||
) {
|
||||
activateChatThread({
|
||||
id: fallbackTab.chatId ?? null,
|
||||
title: fallbackTab.title,
|
||||
url: fallbackTab.chatUrl,
|
||||
searchSpaceId: fallbackTab.searchSpaceId ?? searchSpaceId,
|
||||
...(fallbackTab.visibility !== undefined
|
||||
? { visibility: fallbackTab.visibility }
|
||||
: {}),
|
||||
...(fallbackTab.hasComments !== undefined
|
||||
? { hasComments: fallbackTab.hasComments }
|
||||
: {}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
||||
|
|
@ -197,7 +196,16 @@ export function AllChatsSidebarContent({
|
|||
setDeletingThreadId(null);
|
||||
}
|
||||
},
|
||||
[deleteThread, t, currentChatId, router, onOpenChange, removeChatTab, searchSpaceId]
|
||||
[
|
||||
activateChatThread,
|
||||
deleteThread,
|
||||
t,
|
||||
currentChatId,
|
||||
router,
|
||||
onOpenChange,
|
||||
removeChatTab,
|
||||
searchSpaceId,
|
||||
]
|
||||
);
|
||||
|
||||
const handleToggleArchive = useCallback(
|
||||
|
|
@ -362,8 +370,8 @@ export function AllChatsSidebarContent({
|
|||
if (wasLongPress()) return;
|
||||
handleThreadClick(thread);
|
||||
}}
|
||||
onMouseEnter={() => prefetchThreadData(queryClient, thread.id)}
|
||||
onFocus={() => prefetchThreadData(queryClient, thread.id)}
|
||||
onMouseEnter={() => prefetchChatThread(thread.id)}
|
||||
onFocus={() => prefetchChatThread(thread.id)}
|
||||
onTouchStart={() => {
|
||||
pendingThreadIdRef.current = thread.id;
|
||||
longPressHandlers.onTouchStart();
|
||||
|
|
@ -389,8 +397,8 @@ export function AllChatsSidebarContent({
|
|||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => handleThreadClick(thread)}
|
||||
onMouseEnter={() => prefetchThreadData(queryClient, thread.id)}
|
||||
onFocus={() => prefetchThreadData(queryClient, thread.id)}
|
||||
onMouseEnter={() => prefetchChatThread(thread.id)}
|
||||
onFocus={() => prefetchChatThread(thread.id)}
|
||||
disabled={isBusy}
|
||||
className={cn(
|
||||
"h-auto w-full justify-start gap-2 overflow-hidden px-2 py-1.5 text-left font-normal",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface MobileSidebarProps {
|
|||
activeChatId?: number | null;
|
||||
onNewChat: () => void;
|
||||
onChatSelect: (chat: ChatItem) => void;
|
||||
onChatPrefetch?: (chat: ChatItem) => void;
|
||||
onChatRename?: (chat: ChatItem) => void;
|
||||
onChatDelete?: (chat: ChatItem) => void;
|
||||
onChatArchive?: (chat: ChatItem) => void;
|
||||
|
|
@ -69,6 +70,7 @@ export function MobileSidebar({
|
|||
activeChatId,
|
||||
onNewChat,
|
||||
onChatSelect,
|
||||
onChatPrefetch,
|
||||
onChatRename,
|
||||
onChatDelete,
|
||||
onChatArchive,
|
||||
|
|
@ -152,6 +154,7 @@ export function MobileSidebar({
|
|||
onOpenChange(false);
|
||||
}}
|
||||
onChatSelect={handleChatSelect}
|
||||
onChatPrefetch={onChatPrefetch}
|
||||
onChatRename={onChatRename}
|
||||
onChatDelete={onChatDelete}
|
||||
onChatArchive={onChatArchive}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { CreditCard, Dot, SquarePen, Zap } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
|
|
@ -11,7 +10,6 @@ import { Button } from "@/components/ui/button";
|
|||
import { Progress } from "@/components/ui/progress";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useIsAnonymous } from "@/contexts/anonymous-mode";
|
||||
import { prefetchThreadData } from "@/hooks/use-thread-queries";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
|
||||
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
||||
|
|
@ -72,6 +70,7 @@ interface SidebarProps {
|
|||
activeChatId?: number | null;
|
||||
onNewChat: () => void;
|
||||
onChatSelect: (chat: ChatItem) => void;
|
||||
onChatPrefetch?: (chat: ChatItem) => void;
|
||||
onChatRename?: (chat: ChatItem) => void;
|
||||
onChatDelete?: (chat: ChatItem) => void;
|
||||
onChatArchive?: (chat: ChatItem) => void;
|
||||
|
|
@ -108,6 +107,7 @@ export function Sidebar({
|
|||
activeChatId,
|
||||
onNewChat,
|
||||
onChatSelect,
|
||||
onChatPrefetch,
|
||||
onChatRename,
|
||||
onChatDelete,
|
||||
onChatArchive,
|
||||
|
|
@ -134,7 +134,6 @@ export function Sidebar({
|
|||
collapsedHeaderContent,
|
||||
}: SidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const queryClient = useQueryClient();
|
||||
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
|
||||
|
||||
// Inbox, Automations, and Documents are rendered explicitly right below
|
||||
|
|
@ -296,7 +295,7 @@ export function Sidebar({
|
|||
dropdownOpen={openDropdownChatId === chat.id}
|
||||
onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
|
||||
onClick={() => onChatSelect(chat)}
|
||||
onPrefetch={() => prefetchThreadData(queryClient, chat.id)}
|
||||
onPrefetch={() => onChatPrefetch?.(chat)}
|
||||
onRename={() => onChatRename?.(chat)}
|
||||
onArchive={() => onChatArchive?.(chat)}
|
||||
onDelete={() => onChatDelete?.(chat)}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { cn } from "@/lib/utils";
|
|||
|
||||
interface TabBarProps {
|
||||
onTabSwitch?: (tab: Tab) => void;
|
||||
onTabPrefetch?: (tab: Tab) => void;
|
||||
onNewChat?: () => void;
|
||||
leftActions?: React.ReactNode;
|
||||
rightActions?: React.ReactNode;
|
||||
|
|
@ -36,6 +37,7 @@ function nextTabListScrollLeft(input: {
|
|||
|
||||
export function TabBar({
|
||||
onTabSwitch,
|
||||
onTabPrefetch,
|
||||
onNewChat,
|
||||
leftActions,
|
||||
rightActions,
|
||||
|
|
@ -71,6 +73,15 @@ export function TabBar({
|
|||
[activeTabId, switchTab, onTabSwitch]
|
||||
);
|
||||
|
||||
const handleTabPrefetch = useCallback(
|
||||
(tab: Tab) => {
|
||||
if (tab.type === "chat") {
|
||||
onTabPrefetch?.(tab);
|
||||
}
|
||||
},
|
||||
[onTabPrefetch]
|
||||
);
|
||||
|
||||
const handleTabClose = useCallback(
|
||||
(e: React.MouseEvent, tabId: string) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -195,7 +206,11 @@ export function TabBar({
|
|||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => handleTabClick(tab)}
|
||||
onMouseEnter={() => setHoveredTabIndex(index)}
|
||||
onMouseEnter={() => {
|
||||
setHoveredTabIndex(index);
|
||||
handleTabPrefetch(tab);
|
||||
}}
|
||||
onFocus={() => handleTabPrefetch(tab)}
|
||||
onMouseLeave={() => setHoveredTabIndex(null)}
|
||||
className={cn(
|
||||
"h-full w-full justify-start overflow-hidden px-3 text-left text-[13px] font-medium transition-colors duration-150",
|
||||
|
|
|
|||
|
|
@ -77,8 +77,9 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
|||
});
|
||||
const hasPublicSnapshots = (snapshotsData?.snapshots?.length ?? 0) > 0;
|
||||
|
||||
// Use Jotai visibility if available (synced from chat page), otherwise fall back to thread prop
|
||||
const currentVisibility = currentThreadState.visibility ?? thread?.visibility ?? "PRIVATE";
|
||||
// Use Jotai visibility if available (synced from chat page), otherwise fall back to thread prop.
|
||||
// Unknown visibility should not be presented as private while thread detail is still resolving.
|
||||
const currentVisibility = currentThreadState.visibility ?? thread?.visibility;
|
||||
|
||||
const handleVisibilityChange = useCallback(
|
||||
async (newVisibility: ChatVisibility) => {
|
||||
|
|
@ -120,7 +121,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
|||
}, [thread, createSnapshot, queryClient]);
|
||||
|
||||
// Don't show if no thread (new chat that hasn't been created yet)
|
||||
if (!thread) {
|
||||
if (!thread || currentVisibility === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue