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 { Inbox, LibraryBig } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import type { ReactNode } from "react";
|
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 { useAnonymousMode } from "@/contexts/anonymous-mode";
|
||||||
import { useLoginGate } from "@/contexts/login-gate";
|
import { useLoginGate } from "@/contexts/login-gate";
|
||||||
import { useAnnouncements } from "@/hooks/use-announcements";
|
import { useAnnouncements } from "@/hooks/use-announcements";
|
||||||
|
|
@ -110,15 +110,13 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
|
||||||
navItems={navItems}
|
navItems={navItems}
|
||||||
onNavItemClick={handleNavItemClick}
|
onNavItemClick={handleNavItemClick}
|
||||||
chats={[]}
|
chats={[]}
|
||||||
sharedChats={[]}
|
|
||||||
activeChatId={null}
|
activeChatId={null}
|
||||||
onNewChat={resetChat}
|
onNewChat={resetChat}
|
||||||
onChatSelect={handleChatSelect}
|
onChatSelect={handleChatSelect}
|
||||||
onChatRename={gatedAction("rename chats")}
|
onChatRename={gatedAction("rename chats")}
|
||||||
onChatDelete={gatedAction("delete chats")}
|
onChatDelete={gatedAction("delete chats")}
|
||||||
onChatArchive={gatedAction("archive chats")}
|
onChatArchive={gatedAction("archive chats")}
|
||||||
onViewAllSharedChats={gatedAction("view shared chats")}
|
onViewAllChats={gatedAction("view chat history")}
|
||||||
onViewAllPrivateChats={gatedAction("view chat history")}
|
|
||||||
user={{
|
user={{
|
||||||
email: "Guest",
|
email: "Guest",
|
||||||
name: "Guest",
|
name: "Guest",
|
||||||
|
|
@ -137,7 +135,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
|
||||||
onOpenChange: setIsDocsSidebarOpen,
|
onOpenChange: setIsDocsSidebarOpen,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Fragment>{children}</Fragment>
|
{children}
|
||||||
</LayoutShell>
|
</LayoutShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
});
|
});
|
||||||
|
|
||||||
// Unified slide-out panel state (only one can be open at a time)
|
// 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 [activeSlideoutPanel, setActiveSlideoutPanel] = useState<SlideoutPanel>(null);
|
||||||
|
|
||||||
const isInboxSidebarOpen = activeSlideoutPanel === "inbox";
|
const isInboxSidebarOpen = activeSlideoutPanel === "inbox";
|
||||||
|
|
@ -304,34 +304,17 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
});
|
});
|
||||||
}, [currentChatId, searchSpaceId, threadsData?.threads, syncChatTab]);
|
}, [currentChatId, searchSpaceId, threadsData?.threads, syncChatTab]);
|
||||||
|
|
||||||
// Transform and split chats into private and shared based on visibility
|
const chats = useMemo(() => {
|
||||||
const { myChats, sharedChats } = useMemo(() => {
|
if (!threadsData?.threads) return [];
|
||||||
if (!threadsData?.threads) return { myChats: [], sharedChats: [] };
|
|
||||||
|
|
||||||
const privateChats: ChatItem[] = [];
|
return threadsData.threads.map<ChatItem>((thread) => ({
|
||||||
const sharedChatsList: ChatItem[] = [];
|
id: thread.id,
|
||||||
|
name: thread.title || `Chat ${thread.id}`,
|
||||||
for (const thread of threadsData.threads) {
|
url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`,
|
||||||
const chatItem: ChatItem = {
|
visibility: thread.visibility,
|
||||||
id: thread.id,
|
isOwnThread: thread.is_own_thread,
|
||||||
name: thread.title || `Chat ${thread.id}`,
|
archived: thread.archived,
|
||||||
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 };
|
|
||||||
}, [threadsData, searchSpaceId]);
|
}, [threadsData, searchSpaceId]);
|
||||||
|
|
||||||
// Navigation items
|
// Navigation items
|
||||||
|
|
@ -599,12 +582,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
}
|
}
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const handleViewAllSharedChats = useCallback(() => {
|
const handleViewAllChats = useCallback(() => {
|
||||||
setActiveSlideoutPanel((prev) => (prev === "shared" ? null : "shared"));
|
setActiveSlideoutPanel((prev) => (prev === "chats" ? null : "chats"));
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleViewAllPrivateChats = useCallback(() => {
|
|
||||||
setActiveSlideoutPanel((prev) => (prev === "private" ? null : "private"));
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Delete handlers
|
// Delete handlers
|
||||||
|
|
@ -695,16 +674,14 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
searchSpace={activeSearchSpace}
|
searchSpace={activeSearchSpace}
|
||||||
navItems={navItems}
|
navItems={navItems}
|
||||||
onNavItemClick={handleNavItemClick}
|
onNavItemClick={handleNavItemClick}
|
||||||
chats={myChats}
|
chats={chats}
|
||||||
sharedChats={sharedChats}
|
|
||||||
activeChatId={currentChatId}
|
activeChatId={currentChatId}
|
||||||
onNewChat={handleNewChat}
|
onNewChat={handleNewChat}
|
||||||
onChatSelect={handleChatSelect}
|
onChatSelect={handleChatSelect}
|
||||||
onChatRename={handleChatRename}
|
onChatRename={handleChatRename}
|
||||||
onChatDelete={handleChatDelete}
|
onChatDelete={handleChatDelete}
|
||||||
onChatArchive={handleChatArchive}
|
onChatArchive={handleChatArchive}
|
||||||
onViewAllSharedChats={handleViewAllSharedChats}
|
onViewAllChats={handleViewAllChats}
|
||||||
onViewAllPrivateChats={handleViewAllPrivateChats}
|
|
||||||
user={{
|
user={{
|
||||||
email: user?.email || "",
|
email: user?.email || "",
|
||||||
name: user?.display_name || user?.email?.split("@")[0],
|
name: user?.display_name || user?.email?.split("@")[0],
|
||||||
|
|
@ -759,10 +736,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
markAllAsRead: statusInbox.markAllAsRead,
|
markAllAsRead: statusInbox.markAllAsRead,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
allSharedChatsPanel={{
|
allChatsPanel={{
|
||||||
searchSpaceId,
|
|
||||||
}}
|
|
||||||
allPrivateChatsPanel={{
|
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
}}
|
}}
|
||||||
documentsPanel={{
|
documentsPanel={{
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,7 @@ export interface ChatsSectionProps {
|
||||||
activeChatId?: number | null;
|
activeChatId?: number | null;
|
||||||
onChatSelect: (chat: ChatItem) => void;
|
onChatSelect: (chat: ChatItem) => void;
|
||||||
onChatDelete?: (chat: ChatItem) => void;
|
onChatDelete?: (chat: ChatItem) => void;
|
||||||
onViewAllSharedChats?: () => void;
|
onViewAllChats?: () => void;
|
||||||
onViewAllPrivateChats?: () => void;
|
|
||||||
searchSpaceId?: string;
|
searchSpaceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,13 +95,11 @@ export interface SidebarProps {
|
||||||
searchSpaceId?: string;
|
searchSpaceId?: string;
|
||||||
navItems: NavItem[];
|
navItems: NavItem[];
|
||||||
chats: ChatItem[];
|
chats: ChatItem[];
|
||||||
sharedChats?: ChatItem[];
|
|
||||||
activeChatId?: number | null;
|
activeChatId?: number | null;
|
||||||
onNewChat: () => void;
|
onNewChat: () => void;
|
||||||
onChatSelect: (chat: ChatItem) => void;
|
onChatSelect: (chat: ChatItem) => void;
|
||||||
onChatDelete?: (chat: ChatItem) => void;
|
onChatDelete?: (chat: ChatItem) => void;
|
||||||
onViewAllSharedChats?: () => void;
|
onViewAllChats?: () => void;
|
||||||
onViewAllPrivateChats?: () => void;
|
|
||||||
user: User;
|
user: User;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
onSettings?: () => void;
|
onSettings?: () => void;
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@ import {
|
||||||
RightPanelToggleButton,
|
RightPanelToggleButton,
|
||||||
} from "../right-panel/RightPanel";
|
} from "../right-panel/RightPanel";
|
||||||
import {
|
import {
|
||||||
AllPrivateChatsSidebarContent,
|
AllChatsSidebarContent,
|
||||||
AllSharedChatsSidebarContent,
|
|
||||||
DocumentsSidebar,
|
DocumentsSidebar,
|
||||||
InboxSidebarContent,
|
InboxSidebarContent,
|
||||||
MobileSidebar,
|
MobileSidebar,
|
||||||
|
|
@ -94,7 +93,7 @@ interface TabDataSource {
|
||||||
markAllAsRead: () => Promise<boolean>;
|
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
|
// Inbox-related props — per-tab data sources with independent loading/pagination
|
||||||
interface InboxProps {
|
interface InboxProps {
|
||||||
|
|
@ -115,15 +114,13 @@ interface LayoutShellProps {
|
||||||
navItems: NavItem[];
|
navItems: NavItem[];
|
||||||
onNavItemClick?: (item: NavItem) => void;
|
onNavItemClick?: (item: NavItem) => void;
|
||||||
chats: ChatItem[];
|
chats: ChatItem[];
|
||||||
sharedChats?: ChatItem[];
|
|
||||||
activeChatId?: number | null;
|
activeChatId?: number | null;
|
||||||
onNewChat: () => void;
|
onNewChat: () => void;
|
||||||
onChatSelect: (chat: ChatItem) => void;
|
onChatSelect: (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;
|
||||||
onViewAllSharedChats?: () => void;
|
onViewAllChats?: () => void;
|
||||||
onViewAllPrivateChats?: () => void;
|
|
||||||
user: User;
|
user: User;
|
||||||
onSettings?: () => void;
|
onSettings?: () => void;
|
||||||
onManageMembers?: () => void;
|
onManageMembers?: () => void;
|
||||||
|
|
@ -148,10 +145,7 @@ interface LayoutShellProps {
|
||||||
inbox?: InboxProps;
|
inbox?: InboxProps;
|
||||||
isLoadingChats?: boolean;
|
isLoadingChats?: boolean;
|
||||||
// All chats panel props
|
// All chats panel props
|
||||||
allSharedChatsPanel?: {
|
allChatsPanel?: {
|
||||||
searchSpaceId: string;
|
|
||||||
};
|
|
||||||
allPrivateChatsPanel?: {
|
|
||||||
searchSpaceId: string;
|
searchSpaceId: string;
|
||||||
};
|
};
|
||||||
documentsPanel?: {
|
documentsPanel?: {
|
||||||
|
|
@ -226,15 +220,13 @@ export function LayoutShell({
|
||||||
navItems,
|
navItems,
|
||||||
onNavItemClick,
|
onNavItemClick,
|
||||||
chats,
|
chats,
|
||||||
sharedChats,
|
|
||||||
activeChatId,
|
activeChatId,
|
||||||
onNewChat,
|
onNewChat,
|
||||||
onChatSelect,
|
onChatSelect,
|
||||||
onChatRename,
|
onChatRename,
|
||||||
onChatDelete,
|
onChatDelete,
|
||||||
onChatArchive,
|
onChatArchive,
|
||||||
onViewAllSharedChats,
|
onViewAllChats,
|
||||||
onViewAllPrivateChats,
|
|
||||||
user,
|
user,
|
||||||
onSettings,
|
onSettings,
|
||||||
onManageMembers,
|
onManageMembers,
|
||||||
|
|
@ -256,8 +248,7 @@ export function LayoutShell({
|
||||||
onSlideoutPanelChange,
|
onSlideoutPanelChange,
|
||||||
inbox,
|
inbox,
|
||||||
isLoadingChats = false,
|
isLoadingChats = false,
|
||||||
allSharedChatsPanel,
|
allChatsPanel,
|
||||||
allPrivateChatsPanel,
|
|
||||||
documentsPanel,
|
documentsPanel,
|
||||||
onTabSwitch,
|
onTabSwitch,
|
||||||
}: LayoutShellProps) {
|
}: LayoutShellProps) {
|
||||||
|
|
@ -288,13 +279,7 @@ export function LayoutShell({
|
||||||
const anySlideOutOpen = activeSlideoutPanel !== null;
|
const anySlideOutOpen = activeSlideoutPanel !== null;
|
||||||
|
|
||||||
const panelAriaLabel =
|
const panelAriaLabel =
|
||||||
activeSlideoutPanel === "inbox"
|
activeSlideoutPanel === "inbox" ? "Inbox" : activeSlideoutPanel === "chats" ? "Chats" : "Panel";
|
||||||
? "Inbox"
|
|
||||||
: activeSlideoutPanel === "shared"
|
|
||||||
? "Shared Chats"
|
|
||||||
: activeSlideoutPanel === "private"
|
|
||||||
? "Private Chats"
|
|
||||||
: "Panel";
|
|
||||||
|
|
||||||
// Mobile layout
|
// Mobile layout
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
|
|
@ -317,17 +302,14 @@ export function LayoutShell({
|
||||||
navItems={navItems}
|
navItems={navItems}
|
||||||
onNavItemClick={onNavItemClick}
|
onNavItemClick={onNavItemClick}
|
||||||
chats={chats}
|
chats={chats}
|
||||||
sharedChats={sharedChats}
|
|
||||||
activeChatId={activeChatId}
|
activeChatId={activeChatId}
|
||||||
onNewChat={onNewChat}
|
onNewChat={onNewChat}
|
||||||
onChatSelect={onChatSelect}
|
onChatSelect={onChatSelect}
|
||||||
onChatRename={onChatRename}
|
onChatRename={onChatRename}
|
||||||
onChatDelete={onChatDelete}
|
onChatDelete={onChatDelete}
|
||||||
onChatArchive={onChatArchive}
|
onChatArchive={onChatArchive}
|
||||||
onViewAllSharedChats={onViewAllSharedChats}
|
onViewAllChats={onViewAllChats}
|
||||||
onViewAllPrivateChats={onViewAllPrivateChats}
|
isChatsPanelOpen={activeSlideoutPanel === "chats"}
|
||||||
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
|
|
||||||
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
|
|
||||||
user={user}
|
user={user}
|
||||||
onSettings={onSettings}
|
onSettings={onSettings}
|
||||||
onManageMembers={onManageMembers}
|
onManageMembers={onManageMembers}
|
||||||
|
|
@ -379,34 +361,18 @@ export function LayoutShell({
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
|
{activeSlideoutPanel === "chats" && allChatsPanel && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="shared"
|
key="chats"
|
||||||
className="h-full flex flex-col"
|
className="h-full flex flex-col"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.15 }}
|
||||||
>
|
>
|
||||||
<AllSharedChatsSidebarContent
|
<AllChatsSidebarContent
|
||||||
onOpenChange={(open) => closeSlideout(open)}
|
onOpenChange={(open) => closeSlideout(open)}
|
||||||
searchSpaceId={allSharedChatsPanel.searchSpaceId}
|
searchSpaceId={allChatsPanel.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}
|
|
||||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -478,17 +444,14 @@ export function LayoutShell({
|
||||||
navItems={navItems}
|
navItems={navItems}
|
||||||
onNavItemClick={onNavItemClick}
|
onNavItemClick={onNavItemClick}
|
||||||
chats={chats}
|
chats={chats}
|
||||||
sharedChats={sharedChats}
|
|
||||||
activeChatId={activeChatId}
|
activeChatId={activeChatId}
|
||||||
onNewChat={onNewChat}
|
onNewChat={onNewChat}
|
||||||
onChatSelect={onChatSelect}
|
onChatSelect={onChatSelect}
|
||||||
onChatRename={onChatRename}
|
onChatRename={onChatRename}
|
||||||
onChatDelete={onChatDelete}
|
onChatDelete={onChatDelete}
|
||||||
onChatArchive={onChatArchive}
|
onChatArchive={onChatArchive}
|
||||||
onViewAllSharedChats={onViewAllSharedChats}
|
onViewAllChats={onViewAllChats}
|
||||||
onViewAllPrivateChats={onViewAllPrivateChats}
|
isChatsPanelOpen={activeSlideoutPanel === "chats"}
|
||||||
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
|
|
||||||
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
|
|
||||||
user={user}
|
user={user}
|
||||||
onSettings={onSettings}
|
onSettings={onSettings}
|
||||||
onManageMembers={onManageMembers}
|
onManageMembers={onManageMembers}
|
||||||
|
|
@ -554,33 +517,18 @@ export function LayoutShell({
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
|
{activeSlideoutPanel === "chats" && allChatsPanel && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="shared"
|
key="chats"
|
||||||
className="h-full flex flex-col"
|
className="h-full flex flex-col"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.15 }}
|
||||||
>
|
>
|
||||||
<AllSharedChatsSidebarContent
|
<AllChatsSidebarContent
|
||||||
onOpenChange={(open) => closeSlideout(open)}
|
onOpenChange={(open) => closeSlideout(open)}
|
||||||
searchSpaceId={allSharedChatsPanel.searchSpaceId}
|
searchSpaceId={allChatsPanel.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}
|
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
Search,
|
Search,
|
||||||
Trash2,
|
Trash2,
|
||||||
User,
|
User,
|
||||||
|
Users,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
|
@ -52,21 +53,21 @@ import { formatThreadTimestamp } from "@/lib/format-date";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||||
|
|
||||||
export interface AllPrivateChatsSidebarContentProps {
|
export interface AllChatsSidebarContentProps {
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
searchSpaceId: string;
|
searchSpaceId: string;
|
||||||
onCloseMobileSidebar?: () => void;
|
onCloseMobileSidebar?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AllPrivateChatsSidebarProps extends AllPrivateChatsSidebarContentProps {
|
interface AllChatsSidebarProps extends AllChatsSidebarContentProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AllPrivateChatsSidebarContent({
|
export function AllChatsSidebarContent({
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
onCloseMobileSidebar,
|
onCloseMobileSidebar,
|
||||||
}: AllPrivateChatsSidebarContentProps) {
|
}: AllChatsSidebarContentProps) {
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -122,28 +123,20 @@ export function AllPrivateChatsSidebarContent({
|
||||||
enabled: !!searchSpaceId && isSearchMode,
|
enabled: !!searchSpaceId && isSearchMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter to only private chats (PRIVATE visibility or no visibility set)
|
|
||||||
const { activeChats, archivedChats } = useMemo(() => {
|
const { activeChats, archivedChats } = useMemo(() => {
|
||||||
if (isSearchMode) {
|
if (isSearchMode) {
|
||||||
const privateSearchResults = (searchData ?? []).filter(
|
|
||||||
(thread) => thread.visibility !== "SEARCH_SPACE"
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
activeChats: privateSearchResults.filter((t) => !t.archived),
|
activeChats: (searchData ?? []).filter((t) => !t.archived),
|
||||||
archivedChats: privateSearchResults.filter((t) => t.archived),
|
archivedChats: (searchData ?? []).filter((t) => t.archived),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!threadsData) return { activeChats: [], archivedChats: [] };
|
if (!threadsData) return { activeChats: [], archivedChats: [] };
|
||||||
|
|
||||||
const activePrivate = threadsData.threads.filter(
|
return {
|
||||||
(thread) => thread.visibility !== "SEARCH_SPACE"
|
activeChats: threadsData.threads,
|
||||||
);
|
archivedChats: threadsData.archived_threads,
|
||||||
const archivedPrivate = threadsData.archived_threads.filter(
|
};
|
||||||
(thread) => thread.visibility !== "SEARCH_SPACE"
|
|
||||||
);
|
|
||||||
|
|
||||||
return { activeChats: activePrivate, archivedChats: archivedPrivate };
|
|
||||||
}, [threadsData, searchData, isSearchMode]);
|
}, [threadsData, searchData, isSearchMode]);
|
||||||
|
|
||||||
const threads = showArchived ? archivedChats : activeChats;
|
const threads = showArchived ? archivedChats : activeChats;
|
||||||
|
|
@ -265,7 +258,7 @@ export function AllPrivateChatsSidebarContent({
|
||||||
<span className="sr-only">{t("close") || "Close"}</span>
|
<span className="sr-only">{t("close") || "Close"}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<h2 className="text-lg font-semibold">{t("chats") || "Private Chats"}</h2>
|
<h2 className="text-lg font-semibold">{t("chats") || "Chats"}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -370,7 +363,13 @@ export function AllPrivateChatsSidebarContent({
|
||||||
isBusy && "opacity-50 pointer-events-none"
|
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>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip delayDuration={600}>
|
<Tooltip delayDuration={600}>
|
||||||
|
|
@ -388,7 +387,15 @@ export function AllPrivateChatsSidebarContent({
|
||||||
isBusy && "opacity-50 pointer-events-none"
|
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>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" align="start">
|
<TooltipContent side="bottom" align="start">
|
||||||
|
|
@ -486,7 +493,7 @@ export function AllPrivateChatsSidebarContent({
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{showArchived
|
{showArchived
|
||||||
? t("no_archived_chats") || "No archived chats"
|
? t("no_archived_chats") || "No archived chats"
|
||||||
: t("no_chats") || "No private chats"}
|
: t("no_chats") || "No chats"}
|
||||||
</p>
|
</p>
|
||||||
{!showArchived && (
|
{!showArchived && (
|
||||||
<p className="mt-1 text-[11px] text-muted-foreground/70">
|
<p className="mt-1 text-[11px] text-muted-foreground/70">
|
||||||
|
|
@ -545,21 +552,17 @@ export function AllPrivateChatsSidebarContent({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AllPrivateChatsSidebar({
|
export function AllChatsSidebar({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
onCloseMobileSidebar,
|
onCloseMobileSidebar,
|
||||||
}: AllPrivateChatsSidebarProps) {
|
}: AllChatsSidebarProps) {
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarSlideOutPanel
|
<SidebarSlideOutPanel open={open} onOpenChange={onOpenChange} ariaLabel={t("chats") || "Chats"}>
|
||||||
open={open}
|
<AllChatsSidebarContent
|
||||||
onOpenChange={onOpenChange}
|
|
||||||
ariaLabel={t("chats") || "Private Chats"}
|
|
||||||
>
|
|
||||||
<AllPrivateChatsSidebarContent
|
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
searchSpaceId={searchSpaceId}
|
searchSpaceId={searchSpaceId}
|
||||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
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";
|
"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 { useTranslations } from "next-intl";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -18,6 +18,7 @@ import { cn } from "@/lib/utils";
|
||||||
interface ChatListItemProps {
|
interface ChatListItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
isShared?: boolean;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
dropdownOpen?: boolean;
|
dropdownOpen?: boolean;
|
||||||
onDropdownOpenChange?: (open: boolean) => void;
|
onDropdownOpenChange?: (open: boolean) => void;
|
||||||
|
|
@ -30,6 +31,7 @@ interface ChatListItemProps {
|
||||||
export function ChatListItem({
|
export function ChatListItem({
|
||||||
name,
|
name,
|
||||||
isActive,
|
isActive,
|
||||||
|
isShared,
|
||||||
archived,
|
archived,
|
||||||
dropdownOpen: controlledOpen,
|
dropdownOpen: controlledOpen,
|
||||||
onDropdownOpenChange,
|
onDropdownOpenChange,
|
||||||
|
|
@ -68,7 +70,13 @@ export function ChatListItem({
|
||||||
isActive && "bg-accent text-accent-foreground"
|
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>
|
</Button>
|
||||||
|
|
||||||
{/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */}
|
{/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */}
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,14 @@ interface MobileSidebarProps {
|
||||||
navItems: NavItem[];
|
navItems: NavItem[];
|
||||||
onNavItemClick?: (item: NavItem) => void;
|
onNavItemClick?: (item: NavItem) => void;
|
||||||
chats: ChatItem[];
|
chats: ChatItem[];
|
||||||
sharedChats?: ChatItem[];
|
|
||||||
activeChatId?: number | null;
|
activeChatId?: number | null;
|
||||||
onNewChat: () => void;
|
onNewChat: () => void;
|
||||||
onChatSelect: (chat: ChatItem) => void;
|
onChatSelect: (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;
|
||||||
onViewAllSharedChats?: () => void;
|
onViewAllChats?: () => void;
|
||||||
onViewAllPrivateChats?: () => void;
|
isChatsPanelOpen?: boolean;
|
||||||
isSharedChatsPanelOpen?: boolean;
|
|
||||||
isPrivateChatsPanelOpen?: boolean;
|
|
||||||
user: User;
|
user: User;
|
||||||
onSettings?: () => void;
|
onSettings?: () => void;
|
||||||
onManageMembers?: () => void;
|
onManageMembers?: () => void;
|
||||||
|
|
@ -69,17 +66,14 @@ export function MobileSidebar({
|
||||||
navItems,
|
navItems,
|
||||||
onNavItemClick,
|
onNavItemClick,
|
||||||
chats,
|
chats,
|
||||||
sharedChats,
|
|
||||||
activeChatId,
|
activeChatId,
|
||||||
onNewChat,
|
onNewChat,
|
||||||
onChatSelect,
|
onChatSelect,
|
||||||
onChatRename,
|
onChatRename,
|
||||||
onChatDelete,
|
onChatDelete,
|
||||||
onChatArchive,
|
onChatArchive,
|
||||||
onViewAllSharedChats,
|
onViewAllChats,
|
||||||
onViewAllPrivateChats,
|
isChatsPanelOpen = false,
|
||||||
isSharedChatsPanelOpen = false,
|
|
||||||
isPrivateChatsPanelOpen = false,
|
|
||||||
user,
|
user,
|
||||||
onSettings,
|
onSettings,
|
||||||
onManageMembers,
|
onManageMembers,
|
||||||
|
|
@ -152,7 +146,6 @@ export function MobileSidebar({
|
||||||
navItems={navItems}
|
navItems={navItems}
|
||||||
onNavItemClick={handleNavItemClick}
|
onNavItemClick={handleNavItemClick}
|
||||||
chats={chats}
|
chats={chats}
|
||||||
sharedChats={sharedChats}
|
|
||||||
activeChatId={activeChatId}
|
activeChatId={activeChatId}
|
||||||
onNewChat={() => {
|
onNewChat={() => {
|
||||||
onNewChat();
|
onNewChat();
|
||||||
|
|
@ -162,24 +155,15 @@ export function MobileSidebar({
|
||||||
onChatRename={onChatRename}
|
onChatRename={onChatRename}
|
||||||
onChatDelete={onChatDelete}
|
onChatDelete={onChatDelete}
|
||||||
onChatArchive={onChatArchive}
|
onChatArchive={onChatArchive}
|
||||||
onViewAllSharedChats={
|
onViewAllChats={
|
||||||
onViewAllSharedChats
|
onViewAllChats
|
||||||
? () => {
|
? () => {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onViewAllSharedChats();
|
onViewAllChats();
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewAllPrivateChats={
|
isChatsPanelOpen={isChatsPanelOpen}
|
||||||
onViewAllPrivateChats
|
|
||||||
? () => {
|
|
||||||
onOpenChange(false);
|
|
||||||
onViewAllPrivateChats();
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isSharedChatsPanelOpen={isSharedChatsPanelOpen}
|
|
||||||
isPrivateChatsPanelOpen={isPrivateChatsPanelOpen}
|
|
||||||
user={user}
|
user={user}
|
||||||
onSettings={
|
onSettings={
|
||||||
onSettings
|
onSettings
|
||||||
|
|
|
||||||
|
|
@ -67,17 +67,14 @@ interface SidebarProps {
|
||||||
navItems: NavItem[];
|
navItems: NavItem[];
|
||||||
onNavItemClick?: (item: NavItem) => void;
|
onNavItemClick?: (item: NavItem) => void;
|
||||||
chats: ChatItem[];
|
chats: ChatItem[];
|
||||||
sharedChats?: ChatItem[];
|
|
||||||
activeChatId?: number | null;
|
activeChatId?: number | null;
|
||||||
onNewChat: () => void;
|
onNewChat: () => void;
|
||||||
onChatSelect: (chat: ChatItem) => void;
|
onChatSelect: (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;
|
||||||
onViewAllSharedChats?: () => void;
|
onViewAllChats?: () => void;
|
||||||
onViewAllPrivateChats?: () => void;
|
isChatsPanelOpen?: boolean;
|
||||||
isSharedChatsPanelOpen?: boolean;
|
|
||||||
isPrivateChatsPanelOpen?: boolean;
|
|
||||||
user: User;
|
user: User;
|
||||||
onSettings?: () => void;
|
onSettings?: () => void;
|
||||||
onManageMembers?: () => void;
|
onManageMembers?: () => void;
|
||||||
|
|
@ -106,17 +103,14 @@ export function Sidebar({
|
||||||
navItems,
|
navItems,
|
||||||
onNavItemClick,
|
onNavItemClick,
|
||||||
chats,
|
chats,
|
||||||
sharedChats = [],
|
|
||||||
activeChatId,
|
activeChatId,
|
||||||
onNewChat,
|
onNewChat,
|
||||||
onChatSelect,
|
onChatSelect,
|
||||||
onChatRename,
|
onChatRename,
|
||||||
onChatDelete,
|
onChatDelete,
|
||||||
onChatArchive,
|
onChatArchive,
|
||||||
onViewAllSharedChats,
|
onViewAllChats,
|
||||||
onViewAllPrivateChats,
|
isChatsPanelOpen = false,
|
||||||
isSharedChatsPanelOpen = false,
|
|
||||||
isPrivateChatsPanelOpen = false,
|
|
||||||
user,
|
user,
|
||||||
onSettings,
|
onSettings,
|
||||||
onManageMembers,
|
onManageMembers,
|
||||||
|
|
@ -264,73 +258,20 @@ export function Sidebar({
|
||||||
<div className="flex-1 w-full" />
|
<div className="flex-1 w-full" />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex flex-col gap-1 pt-2 w-full min-h-0 overflow-hidden">
|
<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
|
<SidebarSection
|
||||||
title={t("chats")}
|
title={t("chats")}
|
||||||
defaultOpen={true}
|
defaultOpen={true}
|
||||||
fillHeight={true}
|
fillHeight={true}
|
||||||
alwaysShowAction={!disableTooltips && isPrivateChatsPanelOpen}
|
alwaysShowAction={!disableTooltips && isChatsPanelOpen}
|
||||||
action={
|
action={
|
||||||
onViewAllPrivateChats ? (
|
onViewAllChats ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
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"
|
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>
|
</Button>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
|
|
@ -347,6 +288,7 @@ export function Sidebar({
|
||||||
key={chat.id}
|
key={chat.id}
|
||||||
name={chat.name}
|
name={chat.name}
|
||||||
isActive={chat.id === activeChatId}
|
isActive={chat.id === activeChatId}
|
||||||
|
isShared={chat.visibility === "SEARCH_SPACE"}
|
||||||
archived={chat.archived}
|
archived={chat.archived}
|
||||||
dropdownOpen={openDropdownChatId === chat.id}
|
dropdownOpen={openDropdownChatId === chat.id}
|
||||||
onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
|
onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
export { AllPrivateChatsSidebar, AllPrivateChatsSidebarContent } from "./AllPrivateChatsSidebar";
|
export { AllChatsSidebar, AllChatsSidebarContent } from "./AllChatsSidebar";
|
||||||
export { AllSharedChatsSidebar, AllSharedChatsSidebarContent } from "./AllSharedChatsSidebar";
|
|
||||||
export { ChatListItem } from "./ChatListItem";
|
export { ChatListItem } from "./ChatListItem";
|
||||||
export { DocumentsSidebar } from "./DocumentsSidebar";
|
export { DocumentsSidebar } from "./DocumentsSidebar";
|
||||||
export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar";
|
export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar";
|
||||||
|
|
|
||||||
|
|
@ -650,13 +650,14 @@
|
||||||
"created": "Created"
|
"created": "Created"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"chats": "Private Chats",
|
"chats": "Chats",
|
||||||
"shared_chats": "Shared Chats",
|
"shared_chats": "Shared Chats",
|
||||||
"search_chats": "Search chats",
|
"search_chats": "Search chats",
|
||||||
"no_chats_found": "No chats found",
|
"no_chats_found": "No chats found",
|
||||||
"no_shared_chats": "No shared chats",
|
"no_shared_chats": "No shared chats",
|
||||||
|
"shared_chat": "Shared chat",
|
||||||
"view_all_shared_chats": "View all shared chats",
|
"view_all_shared_chats": "View all shared chats",
|
||||||
"view_all_private_chats": "View all private chats",
|
"view_all_chats": "View all chats",
|
||||||
"show_all": "Show all",
|
"show_all": "Show all",
|
||||||
"hide": "Hide",
|
"hide": "Hide",
|
||||||
"no_chats": "No chats",
|
"no_chats": "No chats",
|
||||||
|
|
|
||||||
|
|
@ -650,13 +650,14 @@
|
||||||
"created": "Creado"
|
"created": "Creado"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"chats": "Chats privados",
|
"chats": "Chats",
|
||||||
"shared_chats": "Chats compartidos",
|
"shared_chats": "Chats compartidos",
|
||||||
"search_chats": "Buscar chats",
|
"search_chats": "Buscar chats",
|
||||||
"no_chats_found": "No se encontraron chats",
|
"no_chats_found": "No se encontraron chats",
|
||||||
"no_shared_chats": "No hay chats compartidos",
|
"no_shared_chats": "No hay chats compartidos",
|
||||||
|
"shared_chat": "Chat compartido",
|
||||||
"view_all_shared_chats": "Ver todos los chats compartidos",
|
"view_all_shared_chats": "Ver todos los chats compartidos",
|
||||||
"view_all_private_chats": "Ver todos los chats privados",
|
"view_all_chats": "Ver todos los chats",
|
||||||
"show_all": "Ver todo",
|
"show_all": "Ver todo",
|
||||||
"hide": "Ocultar",
|
"hide": "Ocultar",
|
||||||
"no_chats": "Sin chats",
|
"no_chats": "Sin chats",
|
||||||
|
|
|
||||||
|
|
@ -650,13 +650,14 @@
|
||||||
"created": "बनाया गया"
|
"created": "बनाया गया"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"chats": "निजी चैट",
|
"chats": "चैट",
|
||||||
"shared_chats": "साझा चैट",
|
"shared_chats": "साझा चैट",
|
||||||
"search_chats": "चैट खोजें",
|
"search_chats": "चैट खोजें",
|
||||||
"no_chats_found": "कोई चैट नहीं मिला",
|
"no_chats_found": "कोई चैट नहीं मिला",
|
||||||
"no_shared_chats": "कोई साझा चैट नहीं",
|
"no_shared_chats": "कोई साझा चैट नहीं",
|
||||||
|
"shared_chat": "साझा चैट",
|
||||||
"view_all_shared_chats": "सभी साझा चैट देखें",
|
"view_all_shared_chats": "सभी साझा चैट देखें",
|
||||||
"view_all_private_chats": "सभी निजी चैट देखें",
|
"view_all_chats": "सभी चैट देखें",
|
||||||
"show_all": "सभी देखें",
|
"show_all": "सभी देखें",
|
||||||
"hide": "छिपाएँ",
|
"hide": "छिपाएँ",
|
||||||
"no_chats": "कोई चैट नहीं",
|
"no_chats": "कोई चैट नहीं",
|
||||||
|
|
|
||||||
|
|
@ -650,13 +650,14 @@
|
||||||
"created": "Criado"
|
"created": "Criado"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"chats": "Chats privados",
|
"chats": "Chats",
|
||||||
"shared_chats": "Chats compartilhados",
|
"shared_chats": "Chats compartilhados",
|
||||||
"search_chats": "Pesquisar chats",
|
"search_chats": "Pesquisar chats",
|
||||||
"no_chats_found": "Nenhum chat encontrado",
|
"no_chats_found": "Nenhum chat encontrado",
|
||||||
"no_shared_chats": "Nenhum chat compartilhado",
|
"no_shared_chats": "Nenhum chat compartilhado",
|
||||||
|
"shared_chat": "Chat compartilhado",
|
||||||
"view_all_shared_chats": "Ver todos os chats compartilhados",
|
"view_all_shared_chats": "Ver todos os chats compartilhados",
|
||||||
"view_all_private_chats": "Ver todos os chats privados",
|
"view_all_chats": "Ver todos os chats",
|
||||||
"show_all": "Ver tudo",
|
"show_all": "Ver tudo",
|
||||||
"hide": "Ocultar",
|
"hide": "Ocultar",
|
||||||
"no_chats": "Nenhum chat",
|
"no_chats": "Nenhum chat",
|
||||||
|
|
|
||||||
|
|
@ -634,13 +634,14 @@
|
||||||
"created": "创建于"
|
"created": "创建于"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"chats": "私人对话",
|
"chats": "对话",
|
||||||
"shared_chats": "共享对话",
|
"shared_chats": "共享对话",
|
||||||
"search_chats": "搜索对话...",
|
"search_chats": "搜索对话...",
|
||||||
"no_chats_found": "未找到对话",
|
"no_chats_found": "未找到对话",
|
||||||
"no_shared_chats": "暂无共享对话",
|
"no_shared_chats": "暂无共享对话",
|
||||||
|
"shared_chat": "共享对话",
|
||||||
"view_all_shared_chats": "查看所有共享对话",
|
"view_all_shared_chats": "查看所有共享对话",
|
||||||
"view_all_private_chats": "查看所有私人对话",
|
"view_all_chats": "查看所有对话",
|
||||||
"show_all": "查看全部",
|
"show_all": "查看全部",
|
||||||
"hide": "隐藏",
|
"hide": "隐藏",
|
||||||
"no_chats": "无对话",
|
"no_chats": "无对话",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue