feat: added shared chats

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-01-13 00:17:12 -08:00
parent 764dd05582
commit f22d649239
22 changed files with 1881 additions and 506 deletions

View file

@ -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>
)}