mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
refactor(chat): consolidate chat handling by merging shared and private chat sections into a unified chat view, update related components and translations
This commit is contained in:
parent
9daaf12658
commit
c002f45c8e
15 changed files with 116 additions and 826 deletions
|
|
@ -3,7 +3,7 @@
|
|||
import { Inbox, LibraryBig } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useAnonymousMode } from "@/contexts/anonymous-mode";
|
||||
import { useLoginGate } from "@/contexts/login-gate";
|
||||
import { useAnnouncements } from "@/hooks/use-announcements";
|
||||
|
|
@ -110,15 +110,13 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
|
|||
navItems={navItems}
|
||||
onNavItemClick={handleNavItemClick}
|
||||
chats={[]}
|
||||
sharedChats={[]}
|
||||
activeChatId={null}
|
||||
onNewChat={resetChat}
|
||||
onChatSelect={handleChatSelect}
|
||||
onChatRename={gatedAction("rename chats")}
|
||||
onChatDelete={gatedAction("delete chats")}
|
||||
onChatArchive={gatedAction("archive chats")}
|
||||
onViewAllSharedChats={gatedAction("view shared chats")}
|
||||
onViewAllPrivateChats={gatedAction("view chat history")}
|
||||
onViewAllChats={gatedAction("view chat history")}
|
||||
user={{
|
||||
email: "Guest",
|
||||
name: "Guest",
|
||||
|
|
@ -137,7 +135,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
|
|||
onOpenChange: setIsDocsSidebarOpen,
|
||||
}}
|
||||
>
|
||||
<Fragment>{children}</Fragment>
|
||||
{children}
|
||||
</LayoutShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
});
|
||||
|
||||
// Unified slide-out panel state (only one can be open at a time)
|
||||
type SlideoutPanel = "inbox" | "shared" | "private" | null;
|
||||
type SlideoutPanel = "inbox" | "chats" | null;
|
||||
const [activeSlideoutPanel, setActiveSlideoutPanel] = useState<SlideoutPanel>(null);
|
||||
|
||||
const isInboxSidebarOpen = activeSlideoutPanel === "inbox";
|
||||
|
|
@ -304,34 +304,17 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
});
|
||||
}, [currentChatId, searchSpaceId, threadsData?.threads, syncChatTab]);
|
||||
|
||||
// Transform and split chats into private and shared based on visibility
|
||||
const { myChats, sharedChats } = useMemo(() => {
|
||||
if (!threadsData?.threads) return { myChats: [], sharedChats: [] };
|
||||
const chats = useMemo(() => {
|
||||
if (!threadsData?.threads) return [];
|
||||
|
||||
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,
|
||||
archived: thread.archived,
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
return { myChats: privateChats, sharedChats: sharedChatsList };
|
||||
return threadsData.threads.map<ChatItem>((thread) => ({
|
||||
id: thread.id,
|
||||
name: thread.title || `Chat ${thread.id}`,
|
||||
url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`,
|
||||
visibility: thread.visibility,
|
||||
isOwnThread: thread.is_own_thread,
|
||||
archived: thread.archived,
|
||||
}));
|
||||
}, [threadsData, searchSpaceId]);
|
||||
|
||||
// Navigation items
|
||||
|
|
@ -599,12 +582,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
}
|
||||
}, [router]);
|
||||
|
||||
const handleViewAllSharedChats = useCallback(() => {
|
||||
setActiveSlideoutPanel((prev) => (prev === "shared" ? null : "shared"));
|
||||
}, []);
|
||||
|
||||
const handleViewAllPrivateChats = useCallback(() => {
|
||||
setActiveSlideoutPanel((prev) => (prev === "private" ? null : "private"));
|
||||
const handleViewAllChats = useCallback(() => {
|
||||
setActiveSlideoutPanel((prev) => (prev === "chats" ? null : "chats"));
|
||||
}, []);
|
||||
|
||||
// Delete handlers
|
||||
|
|
@ -695,16 +674,14 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
searchSpace={activeSearchSpace}
|
||||
navItems={navItems}
|
||||
onNavItemClick={handleNavItemClick}
|
||||
chats={myChats}
|
||||
sharedChats={sharedChats}
|
||||
chats={chats}
|
||||
activeChatId={currentChatId}
|
||||
onNewChat={handleNewChat}
|
||||
onChatSelect={handleChatSelect}
|
||||
onChatRename={handleChatRename}
|
||||
onChatDelete={handleChatDelete}
|
||||
onChatArchive={handleChatArchive}
|
||||
onViewAllSharedChats={handleViewAllSharedChats}
|
||||
onViewAllPrivateChats={handleViewAllPrivateChats}
|
||||
onViewAllChats={handleViewAllChats}
|
||||
user={{
|
||||
email: user?.email || "",
|
||||
name: user?.display_name || user?.email?.split("@")[0],
|
||||
|
|
@ -759,10 +736,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
markAllAsRead: statusInbox.markAllAsRead,
|
||||
},
|
||||
}}
|
||||
allSharedChatsPanel={{
|
||||
searchSpaceId,
|
||||
}}
|
||||
allPrivateChatsPanel={{
|
||||
allChatsPanel={{
|
||||
searchSpaceId,
|
||||
}}
|
||||
documentsPanel={{
|
||||
|
|
|
|||
|
|
@ -70,8 +70,7 @@ export interface ChatsSectionProps {
|
|||
activeChatId?: number | null;
|
||||
onChatSelect: (chat: ChatItem) => void;
|
||||
onChatDelete?: (chat: ChatItem) => void;
|
||||
onViewAllSharedChats?: () => void;
|
||||
onViewAllPrivateChats?: () => void;
|
||||
onViewAllChats?: () => void;
|
||||
searchSpaceId?: string;
|
||||
}
|
||||
|
||||
|
|
@ -96,13 +95,11 @@ export interface SidebarProps {
|
|||
searchSpaceId?: string;
|
||||
navItems: NavItem[];
|
||||
chats: ChatItem[];
|
||||
sharedChats?: ChatItem[];
|
||||
activeChatId?: number | null;
|
||||
onNewChat: () => void;
|
||||
onChatSelect: (chat: ChatItem) => void;
|
||||
onChatDelete?: (chat: ChatItem) => void;
|
||||
onViewAllSharedChats?: () => void;
|
||||
onViewAllPrivateChats?: () => void;
|
||||
onViewAllChats?: () => void;
|
||||
user: User;
|
||||
theme?: string;
|
||||
onSettings?: () => void;
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ import {
|
|||
RightPanelToggleButton,
|
||||
} from "../right-panel/RightPanel";
|
||||
import {
|
||||
AllPrivateChatsSidebarContent,
|
||||
AllSharedChatsSidebarContent,
|
||||
AllChatsSidebarContent,
|
||||
DocumentsSidebar,
|
||||
InboxSidebarContent,
|
||||
MobileSidebar,
|
||||
|
|
@ -94,7 +93,7 @@ interface TabDataSource {
|
|||
markAllAsRead: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export type ActiveSlideoutPanel = "inbox" | "shared" | "private" | null;
|
||||
export type ActiveSlideoutPanel = "inbox" | "chats" | null;
|
||||
|
||||
// Inbox-related props — per-tab data sources with independent loading/pagination
|
||||
interface InboxProps {
|
||||
|
|
@ -115,15 +114,13 @@ interface LayoutShellProps {
|
|||
navItems: NavItem[];
|
||||
onNavItemClick?: (item: NavItem) => void;
|
||||
chats: ChatItem[];
|
||||
sharedChats?: ChatItem[];
|
||||
activeChatId?: number | null;
|
||||
onNewChat: () => void;
|
||||
onChatSelect: (chat: ChatItem) => void;
|
||||
onChatRename?: (chat: ChatItem) => void;
|
||||
onChatDelete?: (chat: ChatItem) => void;
|
||||
onChatArchive?: (chat: ChatItem) => void;
|
||||
onViewAllSharedChats?: () => void;
|
||||
onViewAllPrivateChats?: () => void;
|
||||
onViewAllChats?: () => void;
|
||||
user: User;
|
||||
onSettings?: () => void;
|
||||
onManageMembers?: () => void;
|
||||
|
|
@ -148,10 +145,7 @@ interface LayoutShellProps {
|
|||
inbox?: InboxProps;
|
||||
isLoadingChats?: boolean;
|
||||
// All chats panel props
|
||||
allSharedChatsPanel?: {
|
||||
searchSpaceId: string;
|
||||
};
|
||||
allPrivateChatsPanel?: {
|
||||
allChatsPanel?: {
|
||||
searchSpaceId: string;
|
||||
};
|
||||
documentsPanel?: {
|
||||
|
|
@ -226,15 +220,13 @@ export function LayoutShell({
|
|||
navItems,
|
||||
onNavItemClick,
|
||||
chats,
|
||||
sharedChats,
|
||||
activeChatId,
|
||||
onNewChat,
|
||||
onChatSelect,
|
||||
onChatRename,
|
||||
onChatDelete,
|
||||
onChatArchive,
|
||||
onViewAllSharedChats,
|
||||
onViewAllPrivateChats,
|
||||
onViewAllChats,
|
||||
user,
|
||||
onSettings,
|
||||
onManageMembers,
|
||||
|
|
@ -256,8 +248,7 @@ export function LayoutShell({
|
|||
onSlideoutPanelChange,
|
||||
inbox,
|
||||
isLoadingChats = false,
|
||||
allSharedChatsPanel,
|
||||
allPrivateChatsPanel,
|
||||
allChatsPanel,
|
||||
documentsPanel,
|
||||
onTabSwitch,
|
||||
}: LayoutShellProps) {
|
||||
|
|
@ -288,13 +279,7 @@ export function LayoutShell({
|
|||
const anySlideOutOpen = activeSlideoutPanel !== null;
|
||||
|
||||
const panelAriaLabel =
|
||||
activeSlideoutPanel === "inbox"
|
||||
? "Inbox"
|
||||
: activeSlideoutPanel === "shared"
|
||||
? "Shared Chats"
|
||||
: activeSlideoutPanel === "private"
|
||||
? "Private Chats"
|
||||
: "Panel";
|
||||
activeSlideoutPanel === "inbox" ? "Inbox" : activeSlideoutPanel === "chats" ? "Chats" : "Panel";
|
||||
|
||||
// Mobile layout
|
||||
if (isMobile) {
|
||||
|
|
@ -317,17 +302,14 @@ export function LayoutShell({
|
|||
navItems={navItems}
|
||||
onNavItemClick={onNavItemClick}
|
||||
chats={chats}
|
||||
sharedChats={sharedChats}
|
||||
activeChatId={activeChatId}
|
||||
onNewChat={onNewChat}
|
||||
onChatSelect={onChatSelect}
|
||||
onChatRename={onChatRename}
|
||||
onChatDelete={onChatDelete}
|
||||
onChatArchive={onChatArchive}
|
||||
onViewAllSharedChats={onViewAllSharedChats}
|
||||
onViewAllPrivateChats={onViewAllPrivateChats}
|
||||
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
|
||||
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
|
||||
onViewAllChats={onViewAllChats}
|
||||
isChatsPanelOpen={activeSlideoutPanel === "chats"}
|
||||
user={user}
|
||||
onSettings={onSettings}
|
||||
onManageMembers={onManageMembers}
|
||||
|
|
@ -379,34 +361,18 @@ export function LayoutShell({
|
|||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
|
||||
{activeSlideoutPanel === "chats" && allChatsPanel && (
|
||||
<motion.div
|
||||
key="shared"
|
||||
key="chats"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<AllSharedChatsSidebarContent
|
||||
<AllChatsSidebarContent
|
||||
onOpenChange={(open) => closeSlideout(open)}
|
||||
searchSpaceId={allSharedChatsPanel.searchSpaceId}
|
||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{activeSlideoutPanel === "private" && allPrivateChatsPanel && (
|
||||
<motion.div
|
||||
key="private"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<AllPrivateChatsSidebarContent
|
||||
onOpenChange={(open) => closeSlideout(open)}
|
||||
searchSpaceId={allPrivateChatsPanel.searchSpaceId}
|
||||
searchSpaceId={allChatsPanel.searchSpaceId}
|
||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
</motion.div>
|
||||
|
|
@ -478,17 +444,14 @@ export function LayoutShell({
|
|||
navItems={navItems}
|
||||
onNavItemClick={onNavItemClick}
|
||||
chats={chats}
|
||||
sharedChats={sharedChats}
|
||||
activeChatId={activeChatId}
|
||||
onNewChat={onNewChat}
|
||||
onChatSelect={onChatSelect}
|
||||
onChatRename={onChatRename}
|
||||
onChatDelete={onChatDelete}
|
||||
onChatArchive={onChatArchive}
|
||||
onViewAllSharedChats={onViewAllSharedChats}
|
||||
onViewAllPrivateChats={onViewAllPrivateChats}
|
||||
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
|
||||
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
|
||||
onViewAllChats={onViewAllChats}
|
||||
isChatsPanelOpen={activeSlideoutPanel === "chats"}
|
||||
user={user}
|
||||
onSettings={onSettings}
|
||||
onManageMembers={onManageMembers}
|
||||
|
|
@ -554,33 +517,18 @@ export function LayoutShell({
|
|||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
|
||||
{activeSlideoutPanel === "chats" && allChatsPanel && (
|
||||
<motion.div
|
||||
key="shared"
|
||||
key="chats"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<AllSharedChatsSidebarContent
|
||||
<AllChatsSidebarContent
|
||||
onOpenChange={(open) => closeSlideout(open)}
|
||||
searchSpaceId={allSharedChatsPanel.searchSpaceId}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{activeSlideoutPanel === "private" && allPrivateChatsPanel && (
|
||||
<motion.div
|
||||
key="private"
|
||||
className="h-full flex flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<AllPrivateChatsSidebarContent
|
||||
onOpenChange={(open) => closeSlideout(open)}
|
||||
searchSpaceId={allPrivateChatsPanel.searchSpaceId}
|
||||
searchSpaceId={allChatsPanel.searchSpaceId}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
Search,
|
||||
Trash2,
|
||||
User,
|
||||
Users,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
|
@ -52,21 +53,21 @@ import { formatThreadTimestamp } from "@/lib/format-date";
|
|||
import { cn } from "@/lib/utils";
|
||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||
|
||||
export interface AllPrivateChatsSidebarContentProps {
|
||||
export interface AllChatsSidebarContentProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
searchSpaceId: string;
|
||||
onCloseMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
interface AllPrivateChatsSidebarProps extends AllPrivateChatsSidebarContentProps {
|
||||
interface AllChatsSidebarProps extends AllChatsSidebarContentProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export function AllPrivateChatsSidebarContent({
|
||||
export function AllChatsSidebarContent({
|
||||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onCloseMobileSidebar,
|
||||
}: AllPrivateChatsSidebarContentProps) {
|
||||
}: AllChatsSidebarContentProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
|
@ -122,28 +123,20 @@ export function AllPrivateChatsSidebarContent({
|
|||
enabled: !!searchSpaceId && isSearchMode,
|
||||
});
|
||||
|
||||
// Filter to only private chats (PRIVATE visibility or no visibility set)
|
||||
const { activeChats, archivedChats } = useMemo(() => {
|
||||
if (isSearchMode) {
|
||||
const privateSearchResults = (searchData ?? []).filter(
|
||||
(thread) => thread.visibility !== "SEARCH_SPACE"
|
||||
);
|
||||
return {
|
||||
activeChats: privateSearchResults.filter((t) => !t.archived),
|
||||
archivedChats: privateSearchResults.filter((t) => t.archived),
|
||||
activeChats: (searchData ?? []).filter((t) => !t.archived),
|
||||
archivedChats: (searchData ?? []).filter((t) => t.archived),
|
||||
};
|
||||
}
|
||||
|
||||
if (!threadsData) return { activeChats: [], archivedChats: [] };
|
||||
|
||||
const activePrivate = threadsData.threads.filter(
|
||||
(thread) => thread.visibility !== "SEARCH_SPACE"
|
||||
);
|
||||
const archivedPrivate = threadsData.archived_threads.filter(
|
||||
(thread) => thread.visibility !== "SEARCH_SPACE"
|
||||
);
|
||||
|
||||
return { activeChats: activePrivate, archivedChats: archivedPrivate };
|
||||
return {
|
||||
activeChats: threadsData.threads,
|
||||
archivedChats: threadsData.archived_threads,
|
||||
};
|
||||
}, [threadsData, searchData, isSearchMode]);
|
||||
|
||||
const threads = showArchived ? archivedChats : activeChats;
|
||||
|
|
@ -265,7 +258,7 @@ export function AllPrivateChatsSidebarContent({
|
|||
<span className="sr-only">{t("close") || "Close"}</span>
|
||||
</Button>
|
||||
)}
|
||||
<h2 className="text-lg font-semibold">{t("chats") || "Private Chats"}</h2>
|
||||
<h2 className="text-lg font-semibold">{t("chats") || "Chats"}</h2>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
|
|
@ -370,7 +363,13 @@ export function AllPrivateChatsSidebarContent({
|
|||
isBusy && "opacity-50 pointer-events-none"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||
<span className="min-w-0 flex-1 truncate">{thread.title || "New Chat"}</span>
|
||||
{thread.visibility === "SEARCH_SPACE" ? (
|
||||
<Users
|
||||
aria-label={t("shared_chat") || "Shared chat"}
|
||||
className="h-3 w-3 shrink-0 text-muted-foreground/50"
|
||||
/>
|
||||
) : null}
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip delayDuration={600}>
|
||||
|
|
@ -388,7 +387,15 @@ export function AllPrivateChatsSidebarContent({
|
|||
isBusy && "opacity-50 pointer-events-none"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{thread.title || "New Chat"}
|
||||
</span>
|
||||
{thread.visibility === "SEARCH_SPACE" ? (
|
||||
<Users
|
||||
aria-label={t("shared_chat") || "Shared chat"}
|
||||
className="h-3 w-3 shrink-0 text-muted-foreground/50"
|
||||
/>
|
||||
) : null}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start">
|
||||
|
|
@ -486,7 +493,7 @@ export function AllPrivateChatsSidebarContent({
|
|||
<p className="text-xs text-muted-foreground">
|
||||
{showArchived
|
||||
? t("no_archived_chats") || "No archived chats"
|
||||
: t("no_chats") || "No private chats"}
|
||||
: t("no_chats") || "No chats"}
|
||||
</p>
|
||||
{!showArchived && (
|
||||
<p className="mt-1 text-[11px] text-muted-foreground/70">
|
||||
|
|
@ -545,21 +552,17 @@ export function AllPrivateChatsSidebarContent({
|
|||
);
|
||||
}
|
||||
|
||||
export function AllPrivateChatsSidebar({
|
||||
export function AllChatsSidebar({
|
||||
open,
|
||||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onCloseMobileSidebar,
|
||||
}: AllPrivateChatsSidebarProps) {
|
||||
}: AllChatsSidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
|
||||
return (
|
||||
<SidebarSlideOutPanel
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
ariaLabel={t("chats") || "Private Chats"}
|
||||
>
|
||||
<AllPrivateChatsSidebarContent
|
||||
<SidebarSlideOutPanel open={open} onOpenChange={onOpenChange} ariaLabel={t("chats") || "Chats"}>
|
||||
<AllChatsSidebarContent
|
||||
onOpenChange={onOpenChange}
|
||||
searchSpaceId={searchSpaceId}
|
||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
||||
|
|
@ -1,568 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSetAtom } from "jotai";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ChevronLeft,
|
||||
MessageCircleMore,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
RotateCcwIcon,
|
||||
Search,
|
||||
Trash2,
|
||||
Users,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { removeChatTabAtom } from "@/atoms/tabs/tabs.atom";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
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 { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||
import { useLongPress } from "@/hooks/use-long-press";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import {
|
||||
deleteThread,
|
||||
fetchThreads,
|
||||
searchThreads,
|
||||
updateThread,
|
||||
} from "@/lib/chat/thread-persistence";
|
||||
import { formatThreadTimestamp } from "@/lib/format-date";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||
|
||||
export interface AllSharedChatsSidebarContentProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
searchSpaceId: string;
|
||||
onCloseMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
interface AllSharedChatsSidebarProps extends AllSharedChatsSidebarContentProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export function AllSharedChatsSidebarContent({
|
||||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onCloseMobileSidebar,
|
||||
}: AllSharedChatsSidebarContentProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
const isMobile = useIsMobile();
|
||||
const removeChatTab = useSetAtom(removeChatTabAtom);
|
||||
|
||||
const currentChatId = Array.isArray(params.chat_id)
|
||||
? Number(params.chat_id[0])
|
||||
: params.chat_id
|
||||
? Number(params.chat_id)
|
||||
: null;
|
||||
const [deletingThreadId, setDeletingThreadId] = useState<number | null>(null);
|
||||
const [archivingThreadId, setArchivingThreadId] = useState<number | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
||||
const [showRenameDialog, setShowRenameDialog] = useState(false);
|
||||
const [renamingThread, setRenamingThread] = useState<{ id: number; title: string } | null>(null);
|
||||
const [newTitle, setNewTitle] = useState("");
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||
|
||||
const pendingThreadIdRef = useRef<number | null>(null);
|
||||
const { handlers: longPressHandlers, wasLongPress } = useLongPress(
|
||||
useCallback(() => {
|
||||
if (pendingThreadIdRef.current !== null) {
|
||||
setOpenDropdownId(pendingThreadIdRef.current);
|
||||
}
|
||||
}, [])
|
||||
);
|
||||
|
||||
const isSearchMode = !!debouncedSearchQuery.trim();
|
||||
|
||||
const {
|
||||
data: threadsData,
|
||||
error: threadsError,
|
||||
isLoading: isLoadingThreads,
|
||||
} = useQuery({
|
||||
queryKey: ["all-threads", searchSpaceId],
|
||||
queryFn: () => fetchThreads(Number(searchSpaceId)),
|
||||
enabled: !!searchSpaceId && !isSearchMode,
|
||||
});
|
||||
|
||||
const {
|
||||
data: searchData,
|
||||
error: searchError,
|
||||
isLoading: isLoadingSearch,
|
||||
} = useQuery({
|
||||
queryKey: ["search-threads", searchSpaceId, debouncedSearchQuery],
|
||||
queryFn: () => searchThreads(Number(searchSpaceId), debouncedSearchQuery.trim()),
|
||||
enabled: !!searchSpaceId && isSearchMode,
|
||||
});
|
||||
|
||||
// Filter to only shared chats (SEARCH_SPACE visibility)
|
||||
const { activeChats, archivedChats } = useMemo(() => {
|
||||
if (isSearchMode) {
|
||||
const sharedSearchResults = (searchData ?? []).filter(
|
||||
(thread) => thread.visibility === "SEARCH_SPACE"
|
||||
);
|
||||
return {
|
||||
activeChats: sharedSearchResults.filter((t) => !t.archived),
|
||||
archivedChats: sharedSearchResults.filter((t) => t.archived),
|
||||
};
|
||||
}
|
||||
|
||||
if (!threadsData) return { activeChats: [], archivedChats: [] };
|
||||
|
||||
const activeShared = threadsData.threads.filter(
|
||||
(thread) => thread.visibility === "SEARCH_SPACE"
|
||||
);
|
||||
const archivedShared = threadsData.archived_threads.filter(
|
||||
(thread) => thread.visibility === "SEARCH_SPACE"
|
||||
);
|
||||
|
||||
return { activeChats: activeShared, archivedChats: archivedShared };
|
||||
}, [threadsData, searchData, isSearchMode]);
|
||||
|
||||
const threads = showArchived ? archivedChats : activeChats;
|
||||
|
||||
const handleThreadClick = useCallback(
|
||||
(threadId: number) => {
|
||||
router.push(`/dashboard/${searchSpaceId}/new-chat/${threadId}`);
|
||||
onOpenChange(false);
|
||||
onCloseMobileSidebar?.();
|
||||
},
|
||||
[router, onOpenChange, searchSpaceId, onCloseMobileSidebar]
|
||||
);
|
||||
|
||||
const handleDeleteThread = useCallback(
|
||||
async (threadId: number) => {
|
||||
setDeletingThreadId(threadId);
|
||||
try {
|
||||
await deleteThread(threadId);
|
||||
const fallbackTab = removeChatTab(threadId);
|
||||
toast.success(t("chat_deleted") || "Chat deleted successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
|
||||
|
||||
if (currentChatId === threadId) {
|
||||
onOpenChange(false);
|
||||
setTimeout(() => {
|
||||
if (fallbackTab?.type === "chat" && fallbackTab.chatUrl) {
|
||||
router.push(fallbackTab.chatUrl);
|
||||
return;
|
||||
}
|
||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
||||
}, 250);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting thread:", error);
|
||||
toast.error(t("error_deleting_chat") || "Failed to delete chat");
|
||||
} finally {
|
||||
setDeletingThreadId(null);
|
||||
}
|
||||
},
|
||||
[queryClient, searchSpaceId, t, currentChatId, router, onOpenChange, removeChatTab]
|
||||
);
|
||||
|
||||
const handleToggleArchive = useCallback(
|
||||
async (threadId: number, currentlyArchived: boolean) => {
|
||||
setArchivingThreadId(threadId);
|
||||
try {
|
||||
await updateThread(threadId, { archived: !currentlyArchived });
|
||||
toast.success(
|
||||
currentlyArchived
|
||||
? t("chat_unarchived") || "Chat restored"
|
||||
: t("chat_archived") || "Chat archived"
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
|
||||
} catch (error) {
|
||||
console.error("Error archiving thread:", error);
|
||||
toast.error(t("error_archiving_chat") || "Failed to archive chat");
|
||||
} finally {
|
||||
setArchivingThreadId(null);
|
||||
}
|
||||
},
|
||||
[queryClient, searchSpaceId, t]
|
||||
);
|
||||
|
||||
const handleStartRename = useCallback((threadId: number, title: string) => {
|
||||
setRenamingThread({ id: threadId, title });
|
||||
setNewTitle(title);
|
||||
setShowRenameDialog(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmRename = useCallback(async () => {
|
||||
if (!renamingThread || !newTitle.trim()) return;
|
||||
setIsRenaming(true);
|
||||
try {
|
||||
await updateThread(renamingThread.id, { title: newTitle.trim() });
|
||||
toast.success(t("chat_renamed") || "Chat renamed");
|
||||
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["threads", searchSpaceId, "detail", String(renamingThread.id)],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error renaming thread:", error);
|
||||
toast.error(t("error_renaming_chat") || "Failed to rename chat");
|
||||
} finally {
|
||||
setIsRenaming(false);
|
||||
setShowRenameDialog(false);
|
||||
setRenamingThread(null);
|
||||
setNewTitle("");
|
||||
}
|
||||
}, [renamingThread, newTitle, queryClient, searchSpaceId, t]);
|
||||
|
||||
const handleClearSearch = useCallback(() => {
|
||||
setSearchQuery("");
|
||||
}, []);
|
||||
|
||||
const isLoading = isSearchMode ? isLoadingSearch : isLoadingThreads;
|
||||
const error = isSearchMode ? searchError : threadsError;
|
||||
|
||||
const activeCount = activeChats.length;
|
||||
const archivedCount = archivedChats.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="shrink-0 p-3 pb-1.5 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{isMobile && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full text-muted-foreground hover:text-accent-foreground"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span className="sr-only">{t("close") || "Close"}</span>
|
||||
</Button>
|
||||
)}
|
||||
<h2 className="text-lg font-semibold">{t("shared_chats") || "Shared Chats"}</h2>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("search_chats") || "Search chats..."}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="h-8 border-0 bg-muted pl-8 pr-7 text-sm shadow-none"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-1 top-1/2 h-5 w-5 -translate-y-1/2 rounded-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={handleClearSearch}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isSearchMode && (
|
||||
<Tabs
|
||||
value={showArchived ? "archived" : "active"}
|
||||
onValueChange={(value) => setShowArchived(value === "archived")}
|
||||
className="shrink-0 mx-3 mt-1.5"
|
||||
>
|
||||
<TabsList stretch showBottomBorder size="sm">
|
||||
<TabsTrigger value="active">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<MessageCircleMore className="h-3.5 w-3.5" />
|
||||
<span>Active</span>
|
||||
<span className="inline-flex h-4.5 min-w-4.5 items-center justify-center rounded-full bg-primary/20 px-1 text-[10px] font-medium text-muted-foreground">
|
||||
{activeCount}
|
||||
</span>
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="archived">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<ArchiveIcon className="h-3.5 w-3.5" />
|
||||
<span>Archived</span>
|
||||
<span className="inline-flex h-4.5 min-w-4.5 items-center justify-center rounded-full bg-primary/20 px-1 text-[10px] font-medium text-muted-foreground">
|
||||
{archivedCount}
|
||||
</span>
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-1.5">
|
||||
{isLoading ? (
|
||||
<div className="space-y-1">
|
||||
{[75, 90, 55, 80, 65, 85].map((titleWidth) => (
|
||||
<div
|
||||
key={`skeleton-${titleWidth}`}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5"
|
||||
>
|
||||
<Skeleton className="h-4 w-4 shrink-0 rounded" />
|
||||
<Skeleton className="h-4 rounded" style={{ width: `${titleWidth}%` }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-8 text-sm text-destructive">
|
||||
{t("error_loading_chats") || "Error loading chats"}
|
||||
</div>
|
||||
) : threads.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{threads.map((thread) => {
|
||||
const isDeleting = deletingThreadId === thread.id;
|
||||
const isArchiving = archivingThreadId === thread.id;
|
||||
const isBusy = isDeleting || isArchiving;
|
||||
const isActive = currentChatId === thread.id;
|
||||
|
||||
return (
|
||||
<div key={thread.id} className="group/item relative w-full">
|
||||
{isMobile ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
if (wasLongPress()) return;
|
||||
handleThreadClick(thread.id);
|
||||
}}
|
||||
onTouchStart={() => {
|
||||
pendingThreadIdRef.current = thread.id;
|
||||
longPressHandlers.onTouchStart();
|
||||
}}
|
||||
onTouchEnd={longPressHandlers.onTouchEnd}
|
||||
onTouchMove={longPressHandlers.onTouchMove}
|
||||
disabled={isBusy}
|
||||
className={cn(
|
||||
"h-auto w-full justify-start gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
|
||||
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||
isActive && "bg-accent text-accent-foreground",
|
||||
isBusy && "opacity-50 pointer-events-none"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip delayDuration={600}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => handleThreadClick(thread.id)}
|
||||
disabled={isBusy}
|
||||
className={cn(
|
||||
"h-auto w-full justify-start gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
|
||||
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||
isActive && "bg-accent text-accent-foreground",
|
||||
isBusy && "opacity-50 pointer-events-none"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start">
|
||||
<p>
|
||||
{t("updated") || "Updated"}: {formatThreadTimestamp(thread.updatedAt)}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute right-0 top-0 bottom-0 flex items-center rounded-r-md pl-6 pr-1",
|
||||
isActive
|
||||
? "bg-gradient-to-l from-accent from-60% to-transparent"
|
||||
: "bg-gradient-to-l from-sidebar from-60% to-transparent group-hover/item:from-accent",
|
||||
isMobile
|
||||
? "opacity-0"
|
||||
: openDropdownId === thread.id
|
||||
? "opacity-100"
|
||||
: "opacity-0 group-hover/item:opacity-100"
|
||||
)}
|
||||
>
|
||||
<DropdownMenu
|
||||
open={openDropdownId === thread.id}
|
||||
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"pointer-events-auto h-6 w-6 hover:bg-transparent",
|
||||
openDropdownId === thread.id && "bg-accent hover:bg-accent"
|
||||
)}
|
||||
disabled={isBusy}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Spinner size="xs" />
|
||||
) : (
|
||||
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||
{!thread.archived && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleStartRename(thread.id, thread.title || "New Chat")}
|
||||
>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
<span>{t("rename") || "Rename"}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||
disabled={isArchiving}
|
||||
>
|
||||
{thread.archived ? (
|
||||
<>
|
||||
<RotateCcwIcon className="mr-2 h-4 w-4" />
|
||||
<span>{t("unarchive") || "Restore"}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ArchiveIcon className="mr-2 h-4 w-4" />
|
||||
<span>{t("archive") || "Archive"}</span>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDeleteThread(thread.id)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<span>{t("delete") || "Delete"}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : isSearchMode ? (
|
||||
<div className="text-center py-8">
|
||||
<Search className="mx-auto mb-2.5 h-10 w-10 text-muted-foreground" />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("no_chats_found") || "No chats found"}
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground/70">
|
||||
{t("try_different_search") || "Try a different search term"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<Users className="mx-auto mb-2.5 h-10 w-10 text-muted-foreground" />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{showArchived
|
||||
? t("no_archived_chats") || "No archived chats"
|
||||
: t("no_shared_chats") || "No shared chats"}
|
||||
</p>
|
||||
{!showArchived && (
|
||||
<p className="mt-1 text-[11px] text-muted-foreground/70">
|
||||
Share a chat to collaborate with your team
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Dialog open={showRenameDialog} onOpenChange={setShowRenameDialog}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<span>{t("rename_chat") || "Rename Chat"}</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("rename_chat_description") || "Enter a new name for this conversation."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
value={newTitle}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
placeholder={t("chat_title_placeholder") || "Chat title"}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !isRenaming && newTitle.trim()) {
|
||||
handleConfirmRename();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DialogFooter className="flex sm:justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowRenameDialog(false)}
|
||||
disabled={isRenaming}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirmRename}
|
||||
disabled={isRenaming || !newTitle.trim()}
|
||||
className="gap-2"
|
||||
>
|
||||
{isRenaming ? (
|
||||
<>
|
||||
<Spinner size="xs" />
|
||||
<span>{t("renaming") || "Renaming"}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{t("rename") || "Rename"}</span>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function AllSharedChatsSidebar({
|
||||
open,
|
||||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onCloseMobileSidebar,
|
||||
}: AllSharedChatsSidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
|
||||
return (
|
||||
<SidebarSlideOutPanel
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
ariaLabel={t("shared_chats") || "Shared Chats"}
|
||||
>
|
||||
<AllSharedChatsSidebarContent
|
||||
onOpenChange={onOpenChange}
|
||||
searchSpaceId={searchSpaceId}
|
||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
||||
/>
|
||||
</SidebarSlideOutPanel>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { ArchiveIcon, MoreHorizontal, Pencil, RotateCcwIcon, Trash2 } from "lucide-react";
|
||||
import { ArchiveIcon, MoreHorizontal, Pencil, RotateCcwIcon, Trash2, Users } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -18,6 +18,7 @@ import { cn } from "@/lib/utils";
|
|||
interface ChatListItemProps {
|
||||
name: string;
|
||||
isActive?: boolean;
|
||||
isShared?: boolean;
|
||||
archived?: boolean;
|
||||
dropdownOpen?: boolean;
|
||||
onDropdownOpenChange?: (open: boolean) => void;
|
||||
|
|
@ -30,6 +31,7 @@ interface ChatListItemProps {
|
|||
export function ChatListItem({
|
||||
name,
|
||||
isActive,
|
||||
isShared,
|
||||
archived,
|
||||
dropdownOpen: controlledOpen,
|
||||
onDropdownOpenChange,
|
||||
|
|
@ -68,7 +70,13 @@ export function ChatListItem({
|
|||
isActive && "bg-accent text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{animatedName}</span>
|
||||
<span className="min-w-0 flex-1 truncate">{animatedName}</span>
|
||||
{isShared ? (
|
||||
<Users
|
||||
aria-label={t("shared_chat") || "Shared chat"}
|
||||
className="h-3 w-3 shrink-0 text-muted-foreground/50"
|
||||
/>
|
||||
) : null}
|
||||
</Button>
|
||||
|
||||
{/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */}
|
||||
|
|
|
|||
|
|
@ -19,17 +19,14 @@ interface MobileSidebarProps {
|
|||
navItems: NavItem[];
|
||||
onNavItemClick?: (item: NavItem) => void;
|
||||
chats: ChatItem[];
|
||||
sharedChats?: ChatItem[];
|
||||
activeChatId?: number | null;
|
||||
onNewChat: () => void;
|
||||
onChatSelect: (chat: ChatItem) => void;
|
||||
onChatRename?: (chat: ChatItem) => void;
|
||||
onChatDelete?: (chat: ChatItem) => void;
|
||||
onChatArchive?: (chat: ChatItem) => void;
|
||||
onViewAllSharedChats?: () => void;
|
||||
onViewAllPrivateChats?: () => void;
|
||||
isSharedChatsPanelOpen?: boolean;
|
||||
isPrivateChatsPanelOpen?: boolean;
|
||||
onViewAllChats?: () => void;
|
||||
isChatsPanelOpen?: boolean;
|
||||
user: User;
|
||||
onSettings?: () => void;
|
||||
onManageMembers?: () => void;
|
||||
|
|
@ -69,17 +66,14 @@ export function MobileSidebar({
|
|||
navItems,
|
||||
onNavItemClick,
|
||||
chats,
|
||||
sharedChats,
|
||||
activeChatId,
|
||||
onNewChat,
|
||||
onChatSelect,
|
||||
onChatRename,
|
||||
onChatDelete,
|
||||
onChatArchive,
|
||||
onViewAllSharedChats,
|
||||
onViewAllPrivateChats,
|
||||
isSharedChatsPanelOpen = false,
|
||||
isPrivateChatsPanelOpen = false,
|
||||
onViewAllChats,
|
||||
isChatsPanelOpen = false,
|
||||
user,
|
||||
onSettings,
|
||||
onManageMembers,
|
||||
|
|
@ -152,7 +146,6 @@ export function MobileSidebar({
|
|||
navItems={navItems}
|
||||
onNavItemClick={handleNavItemClick}
|
||||
chats={chats}
|
||||
sharedChats={sharedChats}
|
||||
activeChatId={activeChatId}
|
||||
onNewChat={() => {
|
||||
onNewChat();
|
||||
|
|
@ -162,24 +155,15 @@ export function MobileSidebar({
|
|||
onChatRename={onChatRename}
|
||||
onChatDelete={onChatDelete}
|
||||
onChatArchive={onChatArchive}
|
||||
onViewAllSharedChats={
|
||||
onViewAllSharedChats
|
||||
onViewAllChats={
|
||||
onViewAllChats
|
||||
? () => {
|
||||
onOpenChange(false);
|
||||
onViewAllSharedChats();
|
||||
onViewAllChats();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onViewAllPrivateChats={
|
||||
onViewAllPrivateChats
|
||||
? () => {
|
||||
onOpenChange(false);
|
||||
onViewAllPrivateChats();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
isSharedChatsPanelOpen={isSharedChatsPanelOpen}
|
||||
isPrivateChatsPanelOpen={isPrivateChatsPanelOpen}
|
||||
isChatsPanelOpen={isChatsPanelOpen}
|
||||
user={user}
|
||||
onSettings={
|
||||
onSettings
|
||||
|
|
|
|||
|
|
@ -67,17 +67,14 @@ interface SidebarProps {
|
|||
navItems: NavItem[];
|
||||
onNavItemClick?: (item: NavItem) => void;
|
||||
chats: ChatItem[];
|
||||
sharedChats?: ChatItem[];
|
||||
activeChatId?: number | null;
|
||||
onNewChat: () => void;
|
||||
onChatSelect: (chat: ChatItem) => void;
|
||||
onChatRename?: (chat: ChatItem) => void;
|
||||
onChatDelete?: (chat: ChatItem) => void;
|
||||
onChatArchive?: (chat: ChatItem) => void;
|
||||
onViewAllSharedChats?: () => void;
|
||||
onViewAllPrivateChats?: () => void;
|
||||
isSharedChatsPanelOpen?: boolean;
|
||||
isPrivateChatsPanelOpen?: boolean;
|
||||
onViewAllChats?: () => void;
|
||||
isChatsPanelOpen?: boolean;
|
||||
user: User;
|
||||
onSettings?: () => void;
|
||||
onManageMembers?: () => void;
|
||||
|
|
@ -106,17 +103,14 @@ export function Sidebar({
|
|||
navItems,
|
||||
onNavItemClick,
|
||||
chats,
|
||||
sharedChats = [],
|
||||
activeChatId,
|
||||
onNewChat,
|
||||
onChatSelect,
|
||||
onChatRename,
|
||||
onChatDelete,
|
||||
onChatArchive,
|
||||
onViewAllSharedChats,
|
||||
onViewAllPrivateChats,
|
||||
isSharedChatsPanelOpen = false,
|
||||
isPrivateChatsPanelOpen = false,
|
||||
onViewAllChats,
|
||||
isChatsPanelOpen = false,
|
||||
user,
|
||||
onSettings,
|
||||
onManageMembers,
|
||||
|
|
@ -264,73 +258,20 @@ export function Sidebar({
|
|||
<div className="flex-1 w-full" />
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col gap-1 pt-2 w-full min-h-0 overflow-hidden">
|
||||
{/* Shared Chats Section - takes only space needed, max 50% */}
|
||||
<SidebarSection
|
||||
title={t("shared_chats")}
|
||||
defaultOpen={true}
|
||||
fillHeight={false}
|
||||
className="shrink-0 max-h-[50%] flex flex-col"
|
||||
alwaysShowAction={!disableTooltips && isSharedChatsPanelOpen}
|
||||
action={
|
||||
onViewAllSharedChats ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={onViewAllSharedChats}
|
||||
className="h-auto cursor-pointer whitespace-nowrap bg-transparent p-0 text-xs font-medium text-muted-foreground/60 transition-colors hover:bg-transparent hover:text-muted-foreground"
|
||||
>
|
||||
{!disableTooltips && isSharedChatsPanelOpen ? t("hide") : t("show_all")}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{isLoadingChats ? (
|
||||
<ChatListSkeletonRows />
|
||||
) : sharedChats.length > 0 ? (
|
||||
<div className="relative min-h-0 flex-1">
|
||||
<div
|
||||
className={`flex flex-col gap-0.5 max-h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${sharedChats.length > 4 ? "pb-2" : ""}`}
|
||||
>
|
||||
{sharedChats.slice(0, 20).map((chat) => (
|
||||
<ChatListItem
|
||||
key={chat.id}
|
||||
name={chat.name}
|
||||
isActive={chat.id === activeChatId}
|
||||
archived={chat.archived}
|
||||
dropdownOpen={openDropdownChatId === chat.id}
|
||||
onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
|
||||
onClick={() => onChatSelect(chat)}
|
||||
onRename={() => onChatRename?.(chat)}
|
||||
onArchive={() => onChatArchive?.(chat)}
|
||||
onDelete={() => onChatDelete?.(chat)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Gradient fade indicator when more than 4 items */}
|
||||
{sharedChats.length > 4 && (
|
||||
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-5 bg-gradient-to-t from-sidebar/80 to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="px-2 py-1 text-sm text-muted-foreground/60">{t("no_shared_chats")}</p>
|
||||
)}
|
||||
</SidebarSection>
|
||||
|
||||
{/* Private Chats Section - fills remaining space */}
|
||||
<SidebarSection
|
||||
title={t("chats")}
|
||||
defaultOpen={true}
|
||||
fillHeight={true}
|
||||
alwaysShowAction={!disableTooltips && isPrivateChatsPanelOpen}
|
||||
alwaysShowAction={!disableTooltips && isChatsPanelOpen}
|
||||
action={
|
||||
onViewAllPrivateChats ? (
|
||||
onViewAllChats ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={onViewAllPrivateChats}
|
||||
onClick={onViewAllChats}
|
||||
className="h-auto cursor-pointer whitespace-nowrap bg-transparent p-0 text-xs font-medium text-muted-foreground/60 transition-colors hover:bg-transparent hover:text-muted-foreground"
|
||||
>
|
||||
{!disableTooltips && isPrivateChatsPanelOpen ? t("hide") : t("show_all")}
|
||||
{!disableTooltips && isChatsPanelOpen ? t("hide") : t("show_all")}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
|
|
@ -347,6 +288,7 @@ export function Sidebar({
|
|||
key={chat.id}
|
||||
name={chat.name}
|
||||
isActive={chat.id === activeChatId}
|
||||
isShared={chat.visibility === "SEARCH_SPACE"}
|
||||
archived={chat.archived}
|
||||
dropdownOpen={openDropdownChatId === chat.id}
|
||||
onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
export { AllPrivateChatsSidebar, AllPrivateChatsSidebarContent } from "./AllPrivateChatsSidebar";
|
||||
export { AllSharedChatsSidebar, AllSharedChatsSidebarContent } from "./AllSharedChatsSidebar";
|
||||
export { AllChatsSidebar, AllChatsSidebarContent } from "./AllChatsSidebar";
|
||||
export { ChatListItem } from "./ChatListItem";
|
||||
export { DocumentsSidebar } from "./DocumentsSidebar";
|
||||
export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue