mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 08:46:22 +02:00
feat: added shared chats
This commit is contained in:
parent
764dd05582
commit
f22d649239
22 changed files with 1881 additions and 506 deletions
|
|
@ -4,18 +4,21 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import { format } from "date-fns";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
Globe,
|
||||
Loader2,
|
||||
Lock,
|
||||
MessageCircleMore,
|
||||
MoreHorizontal,
|
||||
RotateCcwIcon,
|
||||
Search,
|
||||
Trash2,
|
||||
Users,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -38,6 +41,8 @@ import {
|
|||
} from "@/lib/chat/thread-persistence";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type TabType = "shared" | "private";
|
||||
|
||||
interface AllChatsSidebarProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
|
|
@ -65,7 +70,7 @@ export function AllChatsSidebar({
|
|||
const [deletingThreadId, setDeletingThreadId] = useState<number | null>(null);
|
||||
const [archivingThreadId, setArchivingThreadId] = useState<number | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<TabType>("shared");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||
|
|
@ -122,6 +127,34 @@ export function AllChatsSidebar({
|
|||
enabled: !!searchSpaceId && open && isSearchMode,
|
||||
});
|
||||
|
||||
// Split threads into shared and private based on visibility
|
||||
const { sharedChats, privateChats } = useMemo(() => {
|
||||
let allThreads: ThreadListItem[] = [];
|
||||
|
||||
if (isSearchMode) {
|
||||
allThreads = searchData ?? [];
|
||||
} else if (threadsData) {
|
||||
// Combine active and archived threads for filtering
|
||||
allThreads = [...threadsData.threads, ...threadsData.archived_threads];
|
||||
}
|
||||
|
||||
const shared: ThreadListItem[] = [];
|
||||
const privateChatsList: ThreadListItem[] = [];
|
||||
|
||||
for (const thread of allThreads) {
|
||||
if (thread.visibility === "SEARCH_SPACE") {
|
||||
shared.push(thread);
|
||||
} else {
|
||||
privateChatsList.push(thread);
|
||||
}
|
||||
}
|
||||
|
||||
return { sharedChats: shared, privateChats: privateChatsList };
|
||||
}, [threadsData, searchData, isSearchMode]);
|
||||
|
||||
// Get threads for current tab
|
||||
const threads = activeTab === "shared" ? sharedChats : privateChats;
|
||||
|
||||
// Handle thread navigation
|
||||
const handleThreadClick = useCallback(
|
||||
(threadId: number) => {
|
||||
|
|
@ -191,20 +224,12 @@ export function AllChatsSidebar({
|
|||
setSearchQuery("");
|
||||
}, []);
|
||||
|
||||
// Determine which data source to use
|
||||
let threads: ThreadListItem[] = [];
|
||||
if (isSearchMode) {
|
||||
threads = searchData ?? [];
|
||||
} else if (threadsData) {
|
||||
threads = showArchived ? threadsData.archived_threads : threadsData.threads;
|
||||
}
|
||||
|
||||
const isLoading = isSearchMode ? isLoadingSearch : isLoadingThreads;
|
||||
const error = isSearchMode ? searchError : threadsError;
|
||||
|
||||
// Get counts for tabs
|
||||
const activeCount = threadsData?.threads.length ?? 0;
|
||||
const archivedCount = threadsData?.archived_threads.length ?? 0;
|
||||
const sharedCount = sharedChats.length;
|
||||
const privateCount = privateChats.length;
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
|
|
@ -218,7 +243,7 @@ export function AllChatsSidebar({
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-[70] bg-black/50"
|
||||
className="fixed inset-0 z-70 bg-black/50"
|
||||
onClick={() => onOpenChange(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
|
@ -229,13 +254,13 @@ export function AllChatsSidebar({
|
|||
animate={{ x: 0 }}
|
||||
exit={{ x: "-100%" }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||
className="fixed inset-y-0 left-0 z-[70] w-80 bg-background shadow-xl flex flex-col pointer-events-auto isolate"
|
||||
className="fixed inset-y-0 left-0 z-70 w-80 bg-background shadow-xl flex flex-col pointer-events-auto isolate"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("all_chats") || "All Chats"}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0 p-4 pb-2 space-y-3">
|
||||
<div className="shrink-0 p-4 pb-2 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">{t("all_chats") || "All Chats"}</h2>
|
||||
<Button
|
||||
|
|
@ -273,35 +298,35 @@ export function AllChatsSidebar({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab toggle for active/archived (only show when not searching) */}
|
||||
{!isSearchMode && (
|
||||
<div className="flex-shrink-0 flex border-b mx-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowArchived(false)}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-2 text-center text-xs font-medium transition-colors",
|
||||
!showArchived
|
||||
? "border-b-2 border-primary text-primary"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
Active ({activeCount})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowArchived(true)}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-2 text-center text-xs font-medium transition-colors",
|
||||
showArchived
|
||||
? "border-b-2 border-primary text-primary"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
Archived ({archivedCount})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Tab toggle for shared/private chats */}
|
||||
<div className="shrink-0 flex border-b mx-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("shared")}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-2 text-center text-xs font-medium transition-colors flex items-center justify-center gap-1.5",
|
||||
activeTab === "shared"
|
||||
? "border-b-2 border-primary text-primary"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Users className="h-3.5 w-3.5" />
|
||||
{t("shared_chats") || "Shared"} ({sharedCount})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("private")}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-2 text-center text-xs font-medium transition-colors flex items-center justify-center gap-1.5",
|
||||
activeTab === "private"
|
||||
? "border-b-2 border-primary text-primary"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Lock className="h-3.5 w-3.5" />
|
||||
{t("chats") || "Private"} ({privateCount})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
|
||||
|
|
@ -320,6 +345,7 @@ export function AllChatsSidebar({
|
|||
const isArchiving = archivingThreadId === thread.id;
|
||||
const isBusy = isDeleting || isArchiving;
|
||||
const isActive = currentChatId === thread.id;
|
||||
const isShared = thread.visibility === "SEARCH_SPACE";
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -329,7 +355,8 @@ export function AllChatsSidebar({
|
|||
"hover:bg-accent hover:text-accent-foreground",
|
||||
"transition-colors cursor-pointer",
|
||||
isActive && "bg-accent text-accent-foreground",
|
||||
isBusy && "opacity-50 pointer-events-none"
|
||||
isBusy && "opacity-50 pointer-events-none",
|
||||
thread.archived && "opacity-60"
|
||||
)}
|
||||
>
|
||||
{/* Main clickable area for navigation */}
|
||||
|
|
@ -343,13 +370,21 @@ export function AllChatsSidebar({
|
|||
>
|
||||
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||
{thread.archived && (
|
||||
<ArchiveIcon className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start">
|
||||
<p>
|
||||
{t("updated") || "Updated"}:{" "}
|
||||
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
<p>
|
||||
{t("updated") || "Updated"}:{" "}
|
||||
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
|
||||
</p>
|
||||
{thread.archived && (
|
||||
<p className="text-muted-foreground text-xs">Archived</p>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -377,7 +412,7 @@ export function AllChatsSidebar({
|
|||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40 z-[80]">
|
||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||
disabled={isArchiving}
|
||||
|
|
@ -420,16 +455,26 @@ export function AllChatsSidebar({
|
|||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<MessageCircleMore className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{showArchived
|
||||
? t("no_archived_chats") || "No archived chats"
|
||||
: t("no_chats") || "No chats yet"}
|
||||
</p>
|
||||
{!showArchived && (
|
||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||
{t("start_new_chat_hint") || "Start a new chat from the chat page"}
|
||||
</p>
|
||||
{activeTab === "shared" ? (
|
||||
<>
|
||||
<Users className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("no_shared_chats") || "No shared chats"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||
Share a chat to collaborate with your team
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Lock className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("no_chats") || "No private chats"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||
{t("start_new_chat_hint") || "Start a new chat from the chat page"}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue