mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +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
|
|
@ -37,7 +37,7 @@ import { closeReportPanelAtom } from "@/atoms/chat/report-panel.atom";
|
||||||
import { type AgentCreatedDocument, agentCreatedDocumentsAtom } from "@/atoms/documents/ui.atoms";
|
import { type AgentCreatedDocument, agentCreatedDocumentsAtom } from "@/atoms/documents/ui.atoms";
|
||||||
import { closeEditorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
import { closeEditorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||||
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
||||||
import { removeChatTabAtom, updateChatTabTitleAtom } from "@/atoms/tabs/tabs.atom";
|
import { removeChatTabAtom, syncChatTabAtom, updateChatTabTitleAtom } from "@/atoms/tabs/tabs.atom";
|
||||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||||
import {
|
import {
|
||||||
EditMessageDialog,
|
EditMessageDialog,
|
||||||
|
|
@ -450,6 +450,7 @@ export default function NewChatPage() {
|
||||||
const clearTargetCommentId = useSetAtom(clearTargetCommentIdAtom);
|
const clearTargetCommentId = useSetAtom(clearTargetCommentIdAtom);
|
||||||
const closeReportPanel = useSetAtom(closeReportPanelAtom);
|
const closeReportPanel = useSetAtom(closeReportPanelAtom);
|
||||||
const closeEditorPanel = useSetAtom(closeEditorPanelAtom);
|
const closeEditorPanel = useSetAtom(closeEditorPanelAtom);
|
||||||
|
const syncChatTab = useSetAtom(syncChatTabAtom);
|
||||||
const updateChatTabTitle = useSetAtom(updateChatTabTitleAtom);
|
const updateChatTabTitle = useSetAtom(updateChatTabTitleAtom);
|
||||||
const removeChatTab = useSetAtom(removeChatTabAtom);
|
const removeChatTab = useSetAtom(removeChatTabAtom);
|
||||||
const setAgentCreatedDocuments = useSetAtom(agentCreatedDocumentsAtom);
|
const setAgentCreatedDocuments = useSetAtom(agentCreatedDocumentsAtom);
|
||||||
|
|
@ -726,9 +727,18 @@ export default function NewChatPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (threadDetailQuery.data?.id === activeThreadId) {
|
if (threadDetailQuery.data?.id === activeThreadId) {
|
||||||
setCurrentThread(threadDetailQuery.data);
|
const thread = threadDetailQuery.data;
|
||||||
|
setCurrentThread(thread);
|
||||||
|
syncChatTab({
|
||||||
|
chatId: thread.id,
|
||||||
|
title: thread.title,
|
||||||
|
chatUrl: `/dashboard/${thread.search_space_id ?? searchSpaceId}/new-chat/${thread.id}`,
|
||||||
|
searchSpaceId: thread.search_space_id ?? searchSpaceId,
|
||||||
|
visibility: thread.visibility,
|
||||||
|
hasComments: thread.has_comments ?? false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [activeThreadId, threadDetailQuery.data]);
|
}, [activeThreadId, searchSpaceId, syncChatTab, threadDetailQuery.data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const messagesResponse = threadMessagesQuery.data;
|
const messagesResponse = threadMessagesQuery.data;
|
||||||
|
|
@ -856,6 +866,7 @@ export default function NewChatPage() {
|
||||||
}
|
}
|
||||||
setCurrentThreadMetadata({
|
setCurrentThreadMetadata({
|
||||||
id: null,
|
id: null,
|
||||||
|
searchSpaceId: null,
|
||||||
visibility: null,
|
visibility: null,
|
||||||
hasComments: false,
|
hasComments: false,
|
||||||
});
|
});
|
||||||
|
|
@ -869,6 +880,7 @@ export default function NewChatPage() {
|
||||||
|
|
||||||
setCurrentThreadMetadata({
|
setCurrentThreadMetadata({
|
||||||
id: currentThread.id,
|
id: currentThread.id,
|
||||||
|
searchSpaceId: currentThread.search_space_id ?? searchSpaceId,
|
||||||
visibility,
|
visibility,
|
||||||
hasComments: currentThread.has_comments ?? false,
|
hasComments: currentThread.has_comments ?? false,
|
||||||
});
|
});
|
||||||
|
|
@ -877,6 +889,7 @@ export default function NewChatPage() {
|
||||||
currentThread,
|
currentThread,
|
||||||
currentThreadState.id,
|
currentThreadState.id,
|
||||||
currentThreadState.visibility,
|
currentThreadState.visibility,
|
||||||
|
searchSpaceId,
|
||||||
setCurrentThreadMetadata,
|
setCurrentThreadMetadata,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,14 @@ import { reportPanelAtom } from "./report-panel.atom";
|
||||||
|
|
||||||
interface CurrentThreadState {
|
interface CurrentThreadState {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
|
searchSpaceId: number | null;
|
||||||
visibility: ChatVisibility | null;
|
visibility: ChatVisibility | null;
|
||||||
hasComments: boolean;
|
hasComments: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CurrentThreadMetadataPatch {
|
interface CurrentThreadMetadataPatch {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
|
searchSpaceId?: number | null;
|
||||||
visibility?: ChatVisibility | null;
|
visibility?: ChatVisibility | null;
|
||||||
hasComments?: boolean;
|
hasComments?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +24,7 @@ interface CurrentThreadMetadataUpdate {
|
||||||
|
|
||||||
const initialState: CurrentThreadState = {
|
const initialState: CurrentThreadState = {
|
||||||
id: null,
|
id: null,
|
||||||
|
searchSpaceId: null,
|
||||||
visibility: null,
|
visibility: null,
|
||||||
hasComments: false,
|
hasComments: false,
|
||||||
};
|
};
|
||||||
|
|
@ -32,21 +35,33 @@ export const commentsEnabledAtom = atom(
|
||||||
(get) => get(currentThreadAtom).visibility === "SEARCH_SPACE"
|
(get) => get(currentThreadAtom).visibility === "SEARCH_SPACE"
|
||||||
);
|
);
|
||||||
|
|
||||||
export const setThreadVisibilityAtom = atom(null, (get, set, newVisibility: ChatVisibility) => {
|
|
||||||
set(currentThreadAtom, { ...get(currentThreadAtom), visibility: newVisibility });
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setCurrentThreadMetadataAtom = atom(
|
export const setCurrentThreadMetadataAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, metadata: CurrentThreadMetadataPatch) => {
|
(get, set, metadata: CurrentThreadMetadataPatch) => {
|
||||||
const current = get(currentThreadAtom);
|
const current = get(currentThreadAtom);
|
||||||
|
const isSameThread = current.id === metadata.id;
|
||||||
|
|
||||||
set(currentThreadAtom, {
|
set(currentThreadAtom, {
|
||||||
...current,
|
...current,
|
||||||
id: metadata.id,
|
id: metadata.id,
|
||||||
visibility: "visibility" in metadata ? (metadata.visibility ?? null) : current.visibility,
|
searchSpaceId:
|
||||||
|
"searchSpaceId" in metadata
|
||||||
|
? (metadata.searchSpaceId ?? null)
|
||||||
|
: isSameThread
|
||||||
|
? current.searchSpaceId
|
||||||
|
: null,
|
||||||
|
visibility:
|
||||||
|
"visibility" in metadata
|
||||||
|
? (metadata.visibility ?? null)
|
||||||
|
: isSameThread
|
||||||
|
? current.visibility
|
||||||
|
: null,
|
||||||
hasComments:
|
hasComments:
|
||||||
"hasComments" in metadata ? (metadata.hasComments ?? false) : current.hasComments,
|
"hasComments" in metadata
|
||||||
|
? (metadata.hasComments ?? false)
|
||||||
|
: isSameThread
|
||||||
|
? current.hasComments
|
||||||
|
: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { atomWithStorage, createJSONStorage } from "jotai/utils";
|
import { atomWithStorage, createJSONStorage } from "jotai/utils";
|
||||||
|
import type { ChatVisibility } from "@/lib/chat/thread-persistence";
|
||||||
|
|
||||||
export type TabType = "chat" | "document";
|
export type TabType = "chat" | "document";
|
||||||
|
|
||||||
|
|
@ -10,6 +11,8 @@ export interface Tab {
|
||||||
/** For chat tabs */
|
/** For chat tabs */
|
||||||
chatId?: number | null;
|
chatId?: number | null;
|
||||||
chatUrl?: string;
|
chatUrl?: string;
|
||||||
|
visibility?: ChatVisibility;
|
||||||
|
hasComments?: boolean;
|
||||||
/** For document tabs */
|
/** For document tabs */
|
||||||
documentId?: number;
|
documentId?: number;
|
||||||
searchSpaceId?: number;
|
searchSpaceId?: number;
|
||||||
|
|
@ -79,11 +82,15 @@ export const syncChatTabAtom = atom(
|
||||||
title,
|
title,
|
||||||
chatUrl,
|
chatUrl,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
|
visibility,
|
||||||
|
hasComments,
|
||||||
}: {
|
}: {
|
||||||
chatId: number | null;
|
chatId: number | null;
|
||||||
title?: string;
|
title?: string;
|
||||||
chatUrl?: string;
|
chatUrl?: string;
|
||||||
searchSpaceId: number;
|
searchSpaceId: number;
|
||||||
|
visibility?: ChatVisibility;
|
||||||
|
hasComments?: boolean;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
if (chatId && get(deletedChatIdsAtom).has(chatId)) {
|
if (chatId && get(deletedChatIdsAtom).has(chatId)) {
|
||||||
|
|
@ -105,6 +112,8 @@ export const syncChatTabAtom = atom(
|
||||||
title: title || t.title,
|
title: title || t.title,
|
||||||
chatUrl: chatUrl || t.chatUrl,
|
chatUrl: chatUrl || t.chatUrl,
|
||||||
searchSpaceId: searchSpaceId ?? t.searchSpaceId,
|
searchSpaceId: searchSpaceId ?? t.searchSpaceId,
|
||||||
|
...(visibility !== undefined ? { visibility } : {}),
|
||||||
|
...(hasComments !== undefined ? { hasComments } : {}),
|
||||||
}
|
}
|
||||||
: t
|
: t
|
||||||
),
|
),
|
||||||
|
|
@ -140,6 +149,8 @@ export const syncChatTabAtom = atom(
|
||||||
chatId,
|
chatId,
|
||||||
chatUrl,
|
chatUrl,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
|
...(visibility !== undefined ? { visibility } : {}),
|
||||||
|
...(hasComments !== undefined ? { hasComments } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let updatedTabs: Tab[];
|
let updatedTabs: Tab[];
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,7 @@ import { useTranslations } from "next-intl";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import { currentThreadAtom, resetCurrentThreadAtom } from "@/atoms/chat/current-thread.atom";
|
||||||
currentThreadAtom,
|
|
||||||
resetCurrentThreadAtom,
|
|
||||||
setCurrentThreadMetadataAtom,
|
|
||||||
} from "@/atoms/chat/current-thread.atom";
|
|
||||||
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
||||||
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
|
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
|
||||||
import { announcementsDialogAtom } from "@/atoms/layout/dialogs.atom";
|
import { announcementsDialogAtom } from "@/atoms/layout/dialogs.atom";
|
||||||
|
|
@ -45,6 +41,7 @@ import {
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { useActivateChatThread } from "@/hooks/use-activate-chat-thread";
|
||||||
import { useAnnouncements } from "@/hooks/use-announcements";
|
import { useAnnouncements } from "@/hooks/use-announcements";
|
||||||
import { useInbox } from "@/hooks/use-inbox";
|
import { useInbox } from "@/hooks/use-inbox";
|
||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
|
|
@ -98,9 +95,9 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
const { mutateAsync: deleteSearchSpace } = useAtomValue(deleteSearchSpaceMutationAtom);
|
const { mutateAsync: deleteSearchSpace } = useAtomValue(deleteSearchSpaceMutationAtom);
|
||||||
const currentThreadState = useAtomValue(currentThreadAtom);
|
const currentThreadState = useAtomValue(currentThreadAtom);
|
||||||
const resetCurrentThread = useSetAtom(resetCurrentThreadAtom);
|
const resetCurrentThread = useSetAtom(resetCurrentThreadAtom);
|
||||||
const setCurrentThreadMetadata = useSetAtom(setCurrentThreadMetadataAtom);
|
|
||||||
const syncChatTab = useSetAtom(syncChatTabAtom);
|
const syncChatTab = useSetAtom(syncChatTabAtom);
|
||||||
const removeChatTab = useSetAtom(removeChatTabAtom);
|
const removeChatTab = useSetAtom(removeChatTabAtom);
|
||||||
|
const { activateChatThread, prefetchChatThread } = useActivateChatThread();
|
||||||
const { mutateAsync: archiveThread } = useArchiveThread(searchSpaceId);
|
const { mutateAsync: archiveThread } = useArchiveThread(searchSpaceId);
|
||||||
const { mutateAsync: deleteThread } = useDeleteThread(searchSpaceId);
|
const { mutateAsync: deleteThread } = useDeleteThread(searchSpaceId);
|
||||||
const { mutateAsync: renameThread } = useRenameThread(searchSpaceId);
|
const { mutateAsync: renameThread } = useRenameThread(searchSpaceId);
|
||||||
|
|
@ -309,6 +306,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
title: chatId ? (thread?.title ?? undefined) : "New Chat",
|
title: chatId ? (thread?.title ?? undefined) : "New Chat",
|
||||||
chatUrl,
|
chatUrl,
|
||||||
searchSpaceId: Number(searchSpaceId),
|
searchSpaceId: Number(searchSpaceId),
|
||||||
|
...(thread?.visibility !== undefined ? { visibility: thread.visibility } : {}),
|
||||||
});
|
});
|
||||||
}, [currentChatId, searchSpaceId, threadsData?.threads, syncChatTab]);
|
}, [currentChatId, searchSpaceId, threadsData?.threads, syncChatTab]);
|
||||||
|
|
||||||
|
|
@ -469,12 +467,34 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
const handleTabSwitch = useCallback(
|
const handleTabSwitch = useCallback(
|
||||||
(tab: Tab) => {
|
(tab: Tab) => {
|
||||||
if (tab.type === "chat") {
|
if (tab.type === "chat") {
|
||||||
const url = tab.chatUrl || `/dashboard/${searchSpaceId}/new-chat`;
|
activateChatThread({
|
||||||
router.push(url);
|
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
|
// 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(
|
const handleNavItemClick = useCallback(
|
||||||
|
|
@ -526,20 +546,15 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
|
|
||||||
const handleChatSelect = useCallback(
|
const handleChatSelect = useCallback(
|
||||||
(chat: ChatItem) => {
|
(chat: ChatItem) => {
|
||||||
syncChatTab({
|
activateChatThread({
|
||||||
chatId: chat.id,
|
|
||||||
title: chat.name,
|
|
||||||
chatUrl: chat.url,
|
|
||||||
searchSpaceId: Number(searchSpaceId),
|
|
||||||
});
|
|
||||||
setCurrentThreadMetadata({
|
|
||||||
id: chat.id,
|
id: chat.id,
|
||||||
visibility: chat.visibility ?? "PRIVATE",
|
title: chat.name,
|
||||||
hasComments: false,
|
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) => {
|
const handleChatDelete = useCallback((chat: ChatItem) => {
|
||||||
|
|
@ -611,7 +626,16 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
if (currentChatId === chatToDelete.id) {
|
if (currentChatId === chatToDelete.id) {
|
||||||
resetCurrentThread();
|
resetCurrentThread();
|
||||||
if (fallbackTab?.type === "chat" && fallbackTab.chatUrl) {
|
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 {
|
} else {
|
||||||
const isOutOfSync = currentThreadState.id !== null && !params?.chat_id;
|
const isOutOfSync = currentThreadState.id !== null && !params?.chat_id;
|
||||||
if (isOutOfSync) {
|
if (isOutOfSync) {
|
||||||
|
|
@ -639,6 +663,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
params?.chat_id,
|
params?.chat_id,
|
||||||
router,
|
router,
|
||||||
removeChatTab,
|
removeChatTab,
|
||||||
|
activateChatThread,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Rename handler
|
// Rename handler
|
||||||
|
|
@ -693,6 +718,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
activeChatId={currentChatId}
|
activeChatId={currentChatId}
|
||||||
onNewChat={handleNewChat}
|
onNewChat={handleNewChat}
|
||||||
onChatSelect={handleChatSelect}
|
onChatSelect={handleChatSelect}
|
||||||
|
onChatPrefetch={handleChatPrefetch}
|
||||||
onChatRename={handleChatRename}
|
onChatRename={handleChatRename}
|
||||||
onChatDelete={handleChatDelete}
|
onChatDelete={handleChatDelete}
|
||||||
onChatArchive={handleChatArchive}
|
onChatArchive={handleChatArchive}
|
||||||
|
|
@ -759,6 +785,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
onOpenChange: setIsDocumentsSidebarOpen,
|
onOpenChange: setIsDocumentsSidebarOpen,
|
||||||
}}
|
}}
|
||||||
onTabSwitch={handleTabSwitch}
|
onTabSwitch={handleTabSwitch}
|
||||||
|
onTabPrefetch={handleTabPrefetch}
|
||||||
>
|
>
|
||||||
<Fragment key={chatResetKey}>{children}</Fragment>
|
<Fragment key={chatResetKey}>{children}</Fragment>
|
||||||
</LayoutShell>
|
</LayoutShell>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,14 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
|
||||||
const currentThreadState = useAtomValue(currentThreadAtom);
|
const currentThreadState = useAtomValue(currentThreadAtom);
|
||||||
|
|
||||||
const hasThread = isChatPage && !isDocumentTab && currentThreadState.id !== null;
|
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
|
// Free chat pages have their own header with model selector; only render mobile trigger
|
||||||
if (isFreePage) {
|
if (isFreePage) {
|
||||||
|
|
@ -37,19 +45,24 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const threadForButton: ThreadRecord | null =
|
let threadForButton: ThreadRecord | null = null;
|
||||||
hasThread && currentThreadState.id !== null && searchSpaceId
|
if (
|
||||||
? {
|
canRenderShareButton &&
|
||||||
id: currentThreadState.id,
|
currentThreadState.id !== null &&
|
||||||
visibility: currentThreadState.visibility ?? "PRIVATE",
|
currentThreadState.visibility !== null &&
|
||||||
created_by_id: null,
|
currentThreadState.searchSpaceId !== null
|
||||||
search_space_id: Number(searchSpaceId),
|
) {
|
||||||
title: "",
|
threadForButton = {
|
||||||
archived: false,
|
id: currentThreadState.id,
|
||||||
created_at: "",
|
visibility: currentThreadState.visibility,
|
||||||
updated_at: "",
|
created_by_id: null,
|
||||||
}
|
search_space_id: currentThreadState.searchSpaceId,
|
||||||
: null;
|
title: "",
|
||||||
|
archived: false,
|
||||||
|
created_at: "",
|
||||||
|
updated_at: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<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 */}
|
{/* Right side - Actions */}
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
{hasThread && <ActionLogButton threadId={currentThreadState.id} />}
|
{hasThread && <ActionLogButton threadId={currentThreadState.id} />}
|
||||||
{hasThread && <ChatShareButton thread={threadForButton} />}
|
{threadForButton && <ChatShareButton thread={threadForButton} />}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ interface LayoutShellProps {
|
||||||
activeChatId?: number | null;
|
activeChatId?: number | null;
|
||||||
onNewChat: () => void;
|
onNewChat: () => void;
|
||||||
onChatSelect: (chat: ChatItem) => void;
|
onChatSelect: (chat: ChatItem) => void;
|
||||||
|
onChatPrefetch?: (chat: ChatItem) => void;
|
||||||
onChatRename?: (chat: ChatItem) => void;
|
onChatRename?: (chat: ChatItem) => void;
|
||||||
onChatDelete?: (chat: ChatItem) => void;
|
onChatDelete?: (chat: ChatItem) => void;
|
||||||
onChatArchive?: (chat: ChatItem) => void;
|
onChatArchive?: (chat: ChatItem) => void;
|
||||||
|
|
@ -153,11 +154,13 @@ interface LayoutShellProps {
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
};
|
};
|
||||||
onTabSwitch?: (tab: Tab) => void;
|
onTabSwitch?: (tab: Tab) => void;
|
||||||
|
onTabPrefetch?: (tab: Tab) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MainContentPanel({
|
function MainContentPanel({
|
||||||
isChatPage,
|
isChatPage,
|
||||||
onTabSwitch,
|
onTabSwitch,
|
||||||
|
onTabPrefetch,
|
||||||
onNewChat,
|
onNewChat,
|
||||||
showRightPanelExpandButton = true,
|
showRightPanelExpandButton = true,
|
||||||
showTopBorder = false,
|
showTopBorder = false,
|
||||||
|
|
@ -165,6 +168,7 @@ function MainContentPanel({
|
||||||
}: {
|
}: {
|
||||||
isChatPage: boolean;
|
isChatPage: boolean;
|
||||||
onTabSwitch?: (tab: Tab) => void;
|
onTabSwitch?: (tab: Tab) => void;
|
||||||
|
onTabPrefetch?: (tab: Tab) => void;
|
||||||
onNewChat?: () => void;
|
onNewChat?: () => void;
|
||||||
showRightPanelExpandButton?: boolean;
|
showRightPanelExpandButton?: boolean;
|
||||||
showTopBorder?: boolean;
|
showTopBorder?: boolean;
|
||||||
|
|
@ -179,6 +183,7 @@ function MainContentPanel({
|
||||||
>
|
>
|
||||||
<TabBar
|
<TabBar
|
||||||
onTabSwitch={onTabSwitch}
|
onTabSwitch={onTabSwitch}
|
||||||
|
onTabPrefetch={onTabPrefetch}
|
||||||
onNewChat={onNewChat}
|
onNewChat={onNewChat}
|
||||||
rightActions={showRightPanelExpandButton ? <RightPanelExpandButton /> : null}
|
rightActions={showRightPanelExpandButton ? <RightPanelExpandButton /> : null}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
|
|
@ -223,6 +228,7 @@ export function LayoutShell({
|
||||||
activeChatId,
|
activeChatId,
|
||||||
onNewChat,
|
onNewChat,
|
||||||
onChatSelect,
|
onChatSelect,
|
||||||
|
onChatPrefetch,
|
||||||
onChatRename,
|
onChatRename,
|
||||||
onChatDelete,
|
onChatDelete,
|
||||||
onChatArchive,
|
onChatArchive,
|
||||||
|
|
@ -251,6 +257,7 @@ export function LayoutShell({
|
||||||
allChatsPanel,
|
allChatsPanel,
|
||||||
documentsPanel,
|
documentsPanel,
|
||||||
onTabSwitch,
|
onTabSwitch,
|
||||||
|
onTabPrefetch,
|
||||||
}: LayoutShellProps) {
|
}: LayoutShellProps) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const electronAPI = useElectronAPI();
|
const electronAPI = useElectronAPI();
|
||||||
|
|
@ -305,6 +312,7 @@ export function LayoutShell({
|
||||||
activeChatId={activeChatId}
|
activeChatId={activeChatId}
|
||||||
onNewChat={onNewChat}
|
onNewChat={onNewChat}
|
||||||
onChatSelect={onChatSelect}
|
onChatSelect={onChatSelect}
|
||||||
|
onChatPrefetch={onChatPrefetch}
|
||||||
onChatRename={onChatRename}
|
onChatRename={onChatRename}
|
||||||
onChatDelete={onChatDelete}
|
onChatDelete={onChatDelete}
|
||||||
onChatArchive={onChatArchive}
|
onChatArchive={onChatArchive}
|
||||||
|
|
@ -447,6 +455,7 @@ export function LayoutShell({
|
||||||
activeChatId={activeChatId}
|
activeChatId={activeChatId}
|
||||||
onNewChat={onNewChat}
|
onNewChat={onNewChat}
|
||||||
onChatSelect={onChatSelect}
|
onChatSelect={onChatSelect}
|
||||||
|
onChatPrefetch={onChatPrefetch}
|
||||||
onChatRename={onChatRename}
|
onChatRename={onChatRename}
|
||||||
onChatDelete={onChatDelete}
|
onChatDelete={onChatDelete}
|
||||||
onChatArchive={onChatArchive}
|
onChatArchive={onChatArchive}
|
||||||
|
|
@ -551,6 +560,7 @@ export function LayoutShell({
|
||||||
<MainContentPanel
|
<MainContentPanel
|
||||||
isChatPage={isChatPage}
|
isChatPage={isChatPage}
|
||||||
onTabSwitch={onTabSwitch}
|
onTabSwitch={onTabSwitch}
|
||||||
|
onTabPrefetch={onTabPrefetch}
|
||||||
onNewChat={onNewChat}
|
onNewChat={onNewChat}
|
||||||
showRightPanelExpandButton={!isMacDesktop}
|
showRightPanelExpandButton={!isMacDesktop}
|
||||||
showTopBorder={isMacDesktop}
|
showTopBorder={isMacDesktop}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ import { useParams, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { setCurrentThreadMetadataAtom } from "@/atoms/chat/current-thread.atom";
|
import { removeChatTabAtom } from "@/atoms/tabs/tabs.atom";
|
||||||
import { removeChatTabAtom, syncChatTabAtom } from "@/atoms/tabs/tabs.atom";
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -41,11 +40,11 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { useActivateChatThread } from "@/hooks/use-activate-chat-thread";
|
||||||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||||
import { useLongPress } from "@/hooks/use-long-press";
|
import { useLongPress } from "@/hooks/use-long-press";
|
||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
import { useArchiveThread, useDeleteThread, useRenameThread } from "@/hooks/use-thread-mutations";
|
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 { fetchThreads, searchThreads, type ThreadListItem } from "@/lib/chat/thread-persistence";
|
||||||
import { formatThreadTimestamp } from "@/lib/format-date";
|
import { formatThreadTimestamp } from "@/lib/format-date";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -72,8 +71,7 @@ export function AllChatsSidebarContent({
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const removeChatTab = useSetAtom(removeChatTabAtom);
|
const removeChatTab = useSetAtom(removeChatTabAtom);
|
||||||
const syncChatTab = useSetAtom(syncChatTabAtom);
|
const { activateChatThread, prefetchChatThread } = useActivateChatThread();
|
||||||
const setCurrentThreadMetadata = useSetAtom(setCurrentThreadMetadataAtom);
|
|
||||||
const { mutateAsync: deleteThread } = useDeleteThread(searchSpaceId);
|
const { mutateAsync: deleteThread } = useDeleteThread(searchSpaceId);
|
||||||
const { mutateAsync: archiveThread } = useArchiveThread(searchSpaceId);
|
const { mutateAsync: archiveThread } = useArchiveThread(searchSpaceId);
|
||||||
const { mutateAsync: renameThread } = useRenameThread(searchSpaceId);
|
const { mutateAsync: renameThread } = useRenameThread(searchSpaceId);
|
||||||
|
|
@ -146,30 +144,16 @@ export function AllChatsSidebarContent({
|
||||||
|
|
||||||
const handleThreadClick = useCallback(
|
const handleThreadClick = useCallback(
|
||||||
(thread: ThreadListItem) => {
|
(thread: ThreadListItem) => {
|
||||||
const chatUrl = `/dashboard/${searchSpaceId}/new-chat/${thread.id}`;
|
activateChatThread({
|
||||||
syncChatTab({
|
|
||||||
chatId: thread.id,
|
|
||||||
title: thread.title || "New Chat",
|
|
||||||
chatUrl,
|
|
||||||
searchSpaceId: Number(searchSpaceId),
|
|
||||||
});
|
|
||||||
setCurrentThreadMetadata({
|
|
||||||
id: thread.id,
|
id: thread.id,
|
||||||
|
title: thread.title || "New Chat",
|
||||||
|
searchSpaceId,
|
||||||
visibility: thread.visibility,
|
visibility: thread.visibility,
|
||||||
hasComments: false,
|
|
||||||
});
|
});
|
||||||
router.push(chatUrl);
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onCloseMobileSidebar?.();
|
onCloseMobileSidebar?.();
|
||||||
},
|
},
|
||||||
[
|
[activateChatThread, onOpenChange, searchSpaceId, onCloseMobileSidebar]
|
||||||
router,
|
|
||||||
onOpenChange,
|
|
||||||
searchSpaceId,
|
|
||||||
onCloseMobileSidebar,
|
|
||||||
setCurrentThreadMetadata,
|
|
||||||
syncChatTab,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteThread = useCallback(
|
const handleDeleteThread = useCallback(
|
||||||
|
|
@ -183,8 +167,23 @@ export function AllChatsSidebarContent({
|
||||||
if (currentChatId === threadId) {
|
if (currentChatId === threadId) {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (fallbackTab?.type === "chat" && fallbackTab.chatUrl) {
|
if (
|
||||||
router.push(fallbackTab.chatUrl);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
||||||
|
|
@ -197,7 +196,16 @@ export function AllChatsSidebarContent({
|
||||||
setDeletingThreadId(null);
|
setDeletingThreadId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[deleteThread, t, currentChatId, router, onOpenChange, removeChatTab, searchSpaceId]
|
[
|
||||||
|
activateChatThread,
|
||||||
|
deleteThread,
|
||||||
|
t,
|
||||||
|
currentChatId,
|
||||||
|
router,
|
||||||
|
onOpenChange,
|
||||||
|
removeChatTab,
|
||||||
|
searchSpaceId,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleToggleArchive = useCallback(
|
const handleToggleArchive = useCallback(
|
||||||
|
|
@ -362,8 +370,8 @@ export function AllChatsSidebarContent({
|
||||||
if (wasLongPress()) return;
|
if (wasLongPress()) return;
|
||||||
handleThreadClick(thread);
|
handleThreadClick(thread);
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => prefetchThreadData(queryClient, thread.id)}
|
onMouseEnter={() => prefetchChatThread(thread.id)}
|
||||||
onFocus={() => prefetchThreadData(queryClient, thread.id)}
|
onFocus={() => prefetchChatThread(thread.id)}
|
||||||
onTouchStart={() => {
|
onTouchStart={() => {
|
||||||
pendingThreadIdRef.current = thread.id;
|
pendingThreadIdRef.current = thread.id;
|
||||||
longPressHandlers.onTouchStart();
|
longPressHandlers.onTouchStart();
|
||||||
|
|
@ -389,8 +397,8 @@ export function AllChatsSidebarContent({
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => handleThreadClick(thread)}
|
onClick={() => handleThreadClick(thread)}
|
||||||
onMouseEnter={() => prefetchThreadData(queryClient, thread.id)}
|
onMouseEnter={() => prefetchChatThread(thread.id)}
|
||||||
onFocus={() => prefetchThreadData(queryClient, thread.id)}
|
onFocus={() => prefetchChatThread(thread.id)}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-auto w-full justify-start gap-2 overflow-hidden px-2 py-1.5 text-left font-normal",
|
"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;
|
activeChatId?: number | null;
|
||||||
onNewChat: () => void;
|
onNewChat: () => void;
|
||||||
onChatSelect: (chat: ChatItem) => void;
|
onChatSelect: (chat: ChatItem) => void;
|
||||||
|
onChatPrefetch?: (chat: ChatItem) => void;
|
||||||
onChatRename?: (chat: ChatItem) => void;
|
onChatRename?: (chat: ChatItem) => void;
|
||||||
onChatDelete?: (chat: ChatItem) => void;
|
onChatDelete?: (chat: ChatItem) => void;
|
||||||
onChatArchive?: (chat: ChatItem) => void;
|
onChatArchive?: (chat: ChatItem) => void;
|
||||||
|
|
@ -69,6 +70,7 @@ export function MobileSidebar({
|
||||||
activeChatId,
|
activeChatId,
|
||||||
onNewChat,
|
onNewChat,
|
||||||
onChatSelect,
|
onChatSelect,
|
||||||
|
onChatPrefetch,
|
||||||
onChatRename,
|
onChatRename,
|
||||||
onChatDelete,
|
onChatDelete,
|
||||||
onChatArchive,
|
onChatArchive,
|
||||||
|
|
@ -152,6 +154,7 @@ export function MobileSidebar({
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}}
|
}}
|
||||||
onChatSelect={handleChatSelect}
|
onChatSelect={handleChatSelect}
|
||||||
|
onChatPrefetch={onChatPrefetch}
|
||||||
onChatRename={onChatRename}
|
onChatRename={onChatRename}
|
||||||
onChatDelete={onChatDelete}
|
onChatDelete={onChatDelete}
|
||||||
onChatArchive={onChatArchive}
|
onChatArchive={onChatArchive}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { CreditCard, Dot, SquarePen, Zap } from "lucide-react";
|
import { CreditCard, Dot, SquarePen, Zap } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
|
@ -11,7 +10,6 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { useIsAnonymous } from "@/contexts/anonymous-mode";
|
import { useIsAnonymous } from "@/contexts/anonymous-mode";
|
||||||
import { prefetchThreadData } from "@/hooks/use-thread-queries";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
|
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
|
||||||
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
||||||
|
|
@ -72,6 +70,7 @@ interface SidebarProps {
|
||||||
activeChatId?: number | null;
|
activeChatId?: number | null;
|
||||||
onNewChat: () => void;
|
onNewChat: () => void;
|
||||||
onChatSelect: (chat: ChatItem) => void;
|
onChatSelect: (chat: ChatItem) => void;
|
||||||
|
onChatPrefetch?: (chat: ChatItem) => void;
|
||||||
onChatRename?: (chat: ChatItem) => void;
|
onChatRename?: (chat: ChatItem) => void;
|
||||||
onChatDelete?: (chat: ChatItem) => void;
|
onChatDelete?: (chat: ChatItem) => void;
|
||||||
onChatArchive?: (chat: ChatItem) => void;
|
onChatArchive?: (chat: ChatItem) => void;
|
||||||
|
|
@ -108,6 +107,7 @@ export function Sidebar({
|
||||||
activeChatId,
|
activeChatId,
|
||||||
onNewChat,
|
onNewChat,
|
||||||
onChatSelect,
|
onChatSelect,
|
||||||
|
onChatPrefetch,
|
||||||
onChatRename,
|
onChatRename,
|
||||||
onChatDelete,
|
onChatDelete,
|
||||||
onChatArchive,
|
onChatArchive,
|
||||||
|
|
@ -134,7 +134,6 @@ export function Sidebar({
|
||||||
collapsedHeaderContent,
|
collapsedHeaderContent,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
|
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
|
||||||
|
|
||||||
// Inbox, Automations, and Documents are rendered explicitly right below
|
// Inbox, Automations, and Documents are rendered explicitly right below
|
||||||
|
|
@ -296,7 +295,7 @@ export function Sidebar({
|
||||||
dropdownOpen={openDropdownChatId === chat.id}
|
dropdownOpen={openDropdownChatId === chat.id}
|
||||||
onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
|
onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
|
||||||
onClick={() => onChatSelect(chat)}
|
onClick={() => onChatSelect(chat)}
|
||||||
onPrefetch={() => prefetchThreadData(queryClient, chat.id)}
|
onPrefetch={() => onChatPrefetch?.(chat)}
|
||||||
onRename={() => onChatRename?.(chat)}
|
onRename={() => onChatRename?.(chat)}
|
||||||
onArchive={() => onChatArchive?.(chat)}
|
onArchive={() => onChatArchive?.(chat)}
|
||||||
onDelete={() => onChatDelete?.(chat)}
|
onDelete={() => onChatDelete?.(chat)}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface TabBarProps {
|
interface TabBarProps {
|
||||||
onTabSwitch?: (tab: Tab) => void;
|
onTabSwitch?: (tab: Tab) => void;
|
||||||
|
onTabPrefetch?: (tab: Tab) => void;
|
||||||
onNewChat?: () => void;
|
onNewChat?: () => void;
|
||||||
leftActions?: React.ReactNode;
|
leftActions?: React.ReactNode;
|
||||||
rightActions?: React.ReactNode;
|
rightActions?: React.ReactNode;
|
||||||
|
|
@ -36,6 +37,7 @@ function nextTabListScrollLeft(input: {
|
||||||
|
|
||||||
export function TabBar({
|
export function TabBar({
|
||||||
onTabSwitch,
|
onTabSwitch,
|
||||||
|
onTabPrefetch,
|
||||||
onNewChat,
|
onNewChat,
|
||||||
leftActions,
|
leftActions,
|
||||||
rightActions,
|
rightActions,
|
||||||
|
|
@ -71,6 +73,15 @@ export function TabBar({
|
||||||
[activeTabId, switchTab, onTabSwitch]
|
[activeTabId, switchTab, onTabSwitch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleTabPrefetch = useCallback(
|
||||||
|
(tab: Tab) => {
|
||||||
|
if (tab.type === "chat") {
|
||||||
|
onTabPrefetch?.(tab);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onTabPrefetch]
|
||||||
|
);
|
||||||
|
|
||||||
const handleTabClose = useCallback(
|
const handleTabClose = useCallback(
|
||||||
(e: React.MouseEvent, tabId: string) => {
|
(e: React.MouseEvent, tabId: string) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -195,7 +206,11 @@ export function TabBar({
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => handleTabClick(tab)}
|
onClick={() => handleTabClick(tab)}
|
||||||
onMouseEnter={() => setHoveredTabIndex(index)}
|
onMouseEnter={() => {
|
||||||
|
setHoveredTabIndex(index);
|
||||||
|
handleTabPrefetch(tab);
|
||||||
|
}}
|
||||||
|
onFocus={() => handleTabPrefetch(tab)}
|
||||||
onMouseLeave={() => setHoveredTabIndex(null)}
|
onMouseLeave={() => setHoveredTabIndex(null)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full w-full justify-start overflow-hidden px-3 text-left text-[13px] font-medium transition-colors duration-150",
|
"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;
|
const hasPublicSnapshots = (snapshotsData?.snapshots?.length ?? 0) > 0;
|
||||||
|
|
||||||
// Use Jotai visibility if available (synced from chat page), otherwise fall back to thread prop
|
// Use Jotai visibility if available (synced from chat page), otherwise fall back to thread prop.
|
||||||
const currentVisibility = currentThreadState.visibility ?? thread?.visibility ?? "PRIVATE";
|
// Unknown visibility should not be presented as private while thread detail is still resolving.
|
||||||
|
const currentVisibility = currentThreadState.visibility ?? thread?.visibility;
|
||||||
|
|
||||||
const handleVisibilityChange = useCallback(
|
const handleVisibilityChange = useCallback(
|
||||||
async (newVisibility: ChatVisibility) => {
|
async (newVisibility: ChatVisibility) => {
|
||||||
|
|
@ -120,7 +121,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
}, [thread, createSnapshot, queryClient]);
|
}, [thread, createSnapshot, queryClient]);
|
||||||
|
|
||||||
// Don't show if no thread (new chat that hasn't been created yet)
|
// Don't show if no thread (new chat that hasn't been created yet)
|
||||||
if (!thread) {
|
if (!thread || currentVisibility === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
79
surfsense_web/hooks/use-activate-chat-thread.ts
Normal file
79
surfsense_web/hooks/use-activate-chat-thread.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { setCurrentThreadMetadataAtom } from "@/atoms/chat/current-thread.atom";
|
||||||
|
import { syncChatTabAtom } from "@/atoms/tabs/tabs.atom";
|
||||||
|
import type { ChatVisibility } from "@/lib/chat/thread-persistence";
|
||||||
|
import { prefetchThreadData } from "./use-thread-queries";
|
||||||
|
|
||||||
|
interface ActivateChatThreadInput {
|
||||||
|
id: number | null;
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
searchSpaceId: number | string;
|
||||||
|
visibility?: ChatVisibility;
|
||||||
|
hasComments?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchSpaceId(searchSpaceId: number | string): number {
|
||||||
|
const parsed =
|
||||||
|
typeof searchSpaceId === "number" ? searchSpaceId : Number.parseInt(searchSpaceId, 10);
|
||||||
|
return Number.isNaN(parsed) ? 0 : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChatUrl(searchSpaceId: number | string, threadId: number | null): string {
|
||||||
|
return threadId
|
||||||
|
? `/dashboard/${searchSpaceId}/new-chat/${threadId}`
|
||||||
|
: `/dashboard/${searchSpaceId}/new-chat`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useActivateChatThread() {
|
||||||
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const syncChatTab = useSetAtom(syncChatTabAtom);
|
||||||
|
const setCurrentThreadMetadata = useSetAtom(setCurrentThreadMetadataAtom);
|
||||||
|
|
||||||
|
const prefetchChatThread = useCallback(
|
||||||
|
(threadId: number | null | undefined) => {
|
||||||
|
if (typeof threadId === "number" && threadId > 0) {
|
||||||
|
prefetchThreadData(queryClient, threadId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[queryClient]
|
||||||
|
);
|
||||||
|
|
||||||
|
const activateChatThread = useCallback(
|
||||||
|
({ id, title, url, searchSpaceId, visibility, hasComments }: ActivateChatThreadInput) => {
|
||||||
|
const numericSearchSpaceId = getSearchSpaceId(searchSpaceId);
|
||||||
|
const chatUrl = url ?? getChatUrl(searchSpaceId, id);
|
||||||
|
|
||||||
|
syncChatTab({
|
||||||
|
chatId: id,
|
||||||
|
title: id ? title : (title ?? "New Chat"),
|
||||||
|
chatUrl,
|
||||||
|
searchSpaceId: numericSearchSpaceId,
|
||||||
|
...(visibility !== undefined ? { visibility } : {}),
|
||||||
|
...(hasComments !== undefined ? { hasComments } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
setCurrentThreadMetadata({
|
||||||
|
id,
|
||||||
|
searchSpaceId: numericSearchSpaceId,
|
||||||
|
...(visibility !== undefined ? { visibility } : {}),
|
||||||
|
...(hasComments !== undefined ? { hasComments } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
prefetchThreadData(queryClient, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push(chatUrl);
|
||||||
|
},
|
||||||
|
[queryClient, router, setCurrentThreadMetadata, syncChatTab]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { activateChatThread, prefetchChatThread };
|
||||||
|
}
|
||||||
|
|
@ -19,11 +19,8 @@ function stableEntries(obj: Record<string, unknown> | null | undefined): unknown
|
||||||
export const cacheKeys = {
|
export const cacheKeys = {
|
||||||
// New chat threads (assistant-ui)
|
// New chat threads (assistant-ui)
|
||||||
threads: {
|
threads: {
|
||||||
list: (searchSpaceId: number) => ["threads", searchSpaceId] as const,
|
|
||||||
detail: (threadId: number) => ["threads", "detail", threadId] as const,
|
detail: (threadId: number) => ["threads", "detail", threadId] as const,
|
||||||
messages: (threadId: number) => ["threads", "messages", threadId] as const,
|
messages: (threadId: number) => ["threads", "messages", threadId] as const,
|
||||||
search: (searchSpaceId: number, query: string) =>
|
|
||||||
["threads", "search", searchSpaceId, query] as const,
|
|
||||||
},
|
},
|
||||||
documents: {
|
documents: {
|
||||||
globalQueryParams: (queries: GetDocumentsRequest["queryParams"]) =>
|
globalQueryParams: (queries: GetDocumentsRequest["queryParams"]) =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue