2026-01-13 00:17:12 -08:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
import { format } from "date-fns";
|
|
|
|
|
import {
|
|
|
|
|
ArchiveIcon,
|
2026-02-22 00:24:49 +05:30
|
|
|
ChevronLeft,
|
2026-01-13 00:17:12 -08:00
|
|
|
MessageCircleMore,
|
|
|
|
|
MoreHorizontal,
|
2026-02-21 23:59:04 +05:30
|
|
|
PenLine,
|
2026-01-13 00:17:12 -08:00
|
|
|
RotateCcwIcon,
|
|
|
|
|
Search,
|
|
|
|
|
Trash2,
|
|
|
|
|
Users,
|
|
|
|
|
X,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
import { useParams, useRouter } from "next/navigation";
|
|
|
|
|
import { useTranslations } from "next-intl";
|
2026-03-22 00:01:31 +05:30
|
|
|
import { useCallback, useMemo, useRef, useState } from "react";
|
2026-01-13 00:17:12 -08:00
|
|
|
import { toast } from "sonner";
|
2026-03-21 13:20:13 +05:30
|
|
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
2026-01-13 00:17:12 -08:00
|
|
|
import { Button } from "@/components/ui/button";
|
2026-02-21 23:59:04 +05:30
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
} from "@/components/ui/dialog";
|
2026-01-13 00:17:12 -08:00
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
2026-02-06 16:58:38 +05:30
|
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
2026-01-26 23:32:30 -08:00
|
|
|
import { Spinner } from "@/components/ui/spinner";
|
2026-01-13 00:17:12 -08:00
|
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
|
|
|
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
2026-03-07 12:57:27 +05:30
|
|
|
import { useLongPress } from "@/hooks/use-long-press";
|
2026-02-06 23:34:35 +05:30
|
|
|
import { useIsMobile } from "@/hooks/use-mobile";
|
2026-01-13 00:17:12 -08:00
|
|
|
import {
|
|
|
|
|
deleteThread,
|
|
|
|
|
fetchThreads,
|
|
|
|
|
searchThreads,
|
|
|
|
|
updateThread,
|
|
|
|
|
} from "@/lib/chat/thread-persistence";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
2026-02-09 11:41:55 -05:00
|
|
|
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
2026-01-13 00:17:12 -08:00
|
|
|
|
2026-03-22 00:01:31 +05:30
|
|
|
export interface AllSharedChatsSidebarContentProps {
|
2026-01-13 00:17:12 -08:00
|
|
|
onOpenChange: (open: boolean) => void;
|
|
|
|
|
searchSpaceId: string;
|
|
|
|
|
onCloseMobileSidebar?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 00:01:31 +05:30
|
|
|
interface AllSharedChatsSidebarProps extends AllSharedChatsSidebarContentProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function AllSharedChatsSidebarContent({
|
2026-01-13 00:17:12 -08:00
|
|
|
onOpenChange,
|
|
|
|
|
searchSpaceId,
|
|
|
|
|
onCloseMobileSidebar,
|
2026-03-22 00:01:31 +05:30
|
|
|
}: AllSharedChatsSidebarContentProps) {
|
2026-01-13 00:17:12 -08:00
|
|
|
const t = useTranslations("sidebar");
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const params = useParams();
|
|
|
|
|
const queryClient = useQueryClient();
|
2026-02-06 23:34:35 +05:30
|
|
|
const isMobile = useIsMobile();
|
2026-01-13 00:17:12 -08:00
|
|
|
|
|
|
|
|
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);
|
2026-02-21 23:59:04 +05:30
|
|
|
const [showRenameDialog, setShowRenameDialog] = useState(false);
|
|
|
|
|
const [renamingThread, setRenamingThread] = useState<{ id: number; title: string } | null>(null);
|
|
|
|
|
const [newTitle, setNewTitle] = useState("");
|
|
|
|
|
const [isRenaming, setIsRenaming] = useState(false);
|
2026-01-13 00:17:12 -08:00
|
|
|
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
|
|
|
|
|
2026-03-07 04:44:24 +05:30
|
|
|
const pendingThreadIdRef = useRef<number | null>(null);
|
|
|
|
|
const { handlers: longPressHandlers, wasLongPress } = useLongPress(
|
|
|
|
|
useCallback(() => {
|
|
|
|
|
if (pendingThreadIdRef.current !== null) {
|
|
|
|
|
setOpenDropdownId(pendingThreadIdRef.current);
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
);
|
2026-03-07 03:50:45 +05:30
|
|
|
|
2026-01-13 00:17:12 -08:00
|
|
|
const isSearchMode = !!debouncedSearchQuery.trim();
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
data: threadsData,
|
|
|
|
|
error: threadsError,
|
|
|
|
|
isLoading: isLoadingThreads,
|
|
|
|
|
} = useQuery({
|
|
|
|
|
queryKey: ["all-threads", searchSpaceId],
|
|
|
|
|
queryFn: () => fetchThreads(Number(searchSpaceId)),
|
2026-03-22 00:01:31 +05:30
|
|
|
enabled: !!searchSpaceId && !isSearchMode,
|
2026-01-13 00:17:12 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
data: searchData,
|
|
|
|
|
error: searchError,
|
|
|
|
|
isLoading: isLoadingSearch,
|
|
|
|
|
} = useQuery({
|
|
|
|
|
queryKey: ["search-threads", searchSpaceId, debouncedSearchQuery],
|
|
|
|
|
queryFn: () => searchThreads(Number(searchSpaceId), debouncedSearchQuery.trim()),
|
2026-03-22 00:01:31 +05:30
|
|
|
enabled: !!searchSpaceId && isSearchMode,
|
2026-01-13 00:17:12 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
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(() => {
|
|
|
|
|
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]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-21 23:59:04 +05:30
|
|
|
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]);
|
|
|
|
|
|
2026-01-13 00:17:12 -08:00
|
|
|
const handleClearSearch = useCallback(() => {
|
|
|
|
|
setSearchQuery("");
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const isLoading = isSearchMode ? isLoadingSearch : isLoadingThreads;
|
|
|
|
|
const error = isSearchMode ? searchError : threadsError;
|
|
|
|
|
|
|
|
|
|
const activeCount = activeChats.length;
|
|
|
|
|
const archivedCount = archivedChats.length;
|
|
|
|
|
|
2026-02-09 11:41:55 -05:00
|
|
|
return (
|
2026-03-22 00:01:31 +05:30
|
|
|
<>
|
2026-02-09 11:41:55 -05:00
|
|
|
<div className="shrink-0 p-4 pb-2 space-y-3">
|
2026-02-09 16:49:11 -08:00
|
|
|
<div className="flex items-center gap-2">
|
2026-02-22 00:24:49 +05:30
|
|
|
{isMobile && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-8 w-8 rounded-full"
|
|
|
|
|
onClick={() => onOpenChange(false)}
|
|
|
|
|
>
|
|
|
|
|
<ChevronLeft className="h-4 w-4 text-muted-foreground" />
|
|
|
|
|
<span className="sr-only">{t("close") || "Close"}</span>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2026-02-09 16:49:11 -08:00
|
|
|
<Users className="h-5 w-5 text-primary" />
|
|
|
|
|
<h2 className="text-lg font-semibold">{t("shared_chats") || "Shared Chats"}</h2>
|
|
|
|
|
</div>
|
2026-01-13 00:17:12 -08:00
|
|
|
|
2026-02-09 16:49:11 -08:00
|
|
|
<div className="relative">
|
|
|
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
|
|
|
<Input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder={t("search_chats") || "Search chats..."}
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
className="pl-9 pr-8 h-9"
|
|
|
|
|
/>
|
|
|
|
|
{searchQuery && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
|
|
|
|
|
onClick={handleClearSearch}
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3.5 w-3.5" />
|
|
|
|
|
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-13 00:17:12 -08:00
|
|
|
|
2026-02-09 16:49:11 -08:00
|
|
|
{!isSearchMode && (
|
|
|
|
|
<Tabs
|
|
|
|
|
value={showArchived ? "archived" : "active"}
|
|
|
|
|
onValueChange={(value) => setShowArchived(value === "archived")}
|
2026-03-17 12:42:25 +05:30
|
|
|
className="shrink-0 mx-4 mt-2"
|
2026-02-09 16:49:11 -08:00
|
|
|
>
|
2026-03-17 12:42:25 +05:30
|
|
|
<TabsList stretch showBottomBorder size="sm">
|
|
|
|
|
<TabsTrigger value="active">
|
|
|
|
|
<span className="inline-flex items-center gap-1.5">
|
2026-02-09 16:49:11 -08:00
|
|
|
<MessageCircleMore className="h-4 w-4" />
|
|
|
|
|
<span>Active</span>
|
|
|
|
|
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
|
|
|
|
{activeCount}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
</TabsTrigger>
|
2026-03-17 12:42:25 +05:30
|
|
|
<TabsTrigger value="archived">
|
|
|
|
|
<span className="inline-flex items-center gap-1.5">
|
2026-02-09 16:49:11 -08:00
|
|
|
<ArchiveIcon className="h-4 w-4" />
|
|
|
|
|
<span>Archived</span>
|
|
|
|
|
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium">
|
|
|
|
|
{archivedCount}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
</TabsList>
|
|
|
|
|
</Tabs>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="space-y-1">
|
2026-03-17 03:55:49 +05:30
|
|
|
{[75, 90, 55, 80, 65, 85].map((titleWidth) => (
|
2026-03-17 04:40:46 +05:30
|
|
|
<div
|
|
|
|
|
key={`skeleton-${titleWidth}`}
|
|
|
|
|
className="flex items-center gap-2 rounded-md px-2 py-1.5"
|
|
|
|
|
>
|
2026-02-09 16:49:11 -08:00
|
|
|
<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;
|
2026-01-13 00:17:12 -08:00
|
|
|
|
2026-02-09 16:49:11 -08:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={thread.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
|
|
|
|
"hover:bg-accent hover:text-accent-foreground",
|
|
|
|
|
"transition-colors cursor-pointer",
|
|
|
|
|
isActive && "bg-accent text-accent-foreground",
|
|
|
|
|
isBusy && "opacity-50 pointer-events-none"
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-03-07 04:46:48 +05:30
|
|
|
{isMobile ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (wasLongPress()) return;
|
|
|
|
|
handleThreadClick(thread.id);
|
|
|
|
|
}}
|
|
|
|
|
onTouchStart={() => {
|
|
|
|
|
pendingThreadIdRef.current = thread.id;
|
|
|
|
|
longPressHandlers.onTouchStart();
|
|
|
|
|
}}
|
|
|
|
|
onTouchEnd={longPressHandlers.onTouchEnd}
|
|
|
|
|
onTouchMove={longPressHandlers.onTouchMove}
|
|
|
|
|
disabled={isBusy}
|
|
|
|
|
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
|
|
|
|
|
>
|
|
|
|
|
<span className="truncate">{thread.title || "New Chat"}</span>
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => handleThreadClick(thread.id)}
|
|
|
|
|
disabled={isBusy}
|
|
|
|
|
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
|
|
|
|
|
>
|
|
|
|
|
<span className="truncate">{thread.title || "New Chat"}</span>
|
|
|
|
|
</button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent side="bottom" align="start">
|
|
|
|
|
<p>
|
|
|
|
|
{t("updated") || "Updated"}:{" "}
|
|
|
|
|
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
|
|
|
|
|
</p>
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<DropdownMenu
|
|
|
|
|
open={openDropdownId === thread.id}
|
|
|
|
|
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
|
2026-02-09 16:49:11 -08:00
|
|
|
>
|
2026-03-07 04:46:48 +05:30
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className={cn(
|
|
|
|
|
"h-6 w-6 shrink-0",
|
|
|
|
|
isMobile
|
|
|
|
|
? "opacity-0 pointer-events-none absolute"
|
|
|
|
|
: "md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
|
|
|
|
|
"transition-opacity"
|
|
|
|
|
)}
|
2026-02-09 16:49:11 -08:00
|
|
|
disabled={isBusy}
|
2026-01-13 00:17:12 -08:00
|
|
|
>
|
2026-03-07 04:46:48 +05:30
|
|
|
{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>
|
2026-02-22 04:41:56 +05:30
|
|
|
<DropdownMenuContent align="end" className="w-40 z-80">
|
|
|
|
|
{!thread.archived && (
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => handleStartRename(thread.id, thread.title || "New Chat")}
|
|
|
|
|
>
|
|
|
|
|
<PenLine className="mr-2 h-4 w-4" />
|
|
|
|
|
<span>{t("rename") || "Rename"}</span>
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
2026-02-09 16:49:11 -08:00
|
|
|
<DropdownMenuItem
|
2026-02-22 04:41:56 +05:30
|
|
|
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
|
|
|
|
disabled={isArchiving}
|
2026-02-09 16:49:11 -08:00
|
|
|
>
|
2026-02-22 04:41:56 +05:30
|
|
|
{thread.archived ? (
|
2026-02-09 16:49:11 -08:00
|
|
|
<>
|
|
|
|
|
<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>
|
2026-03-07 04:46:48 +05:30
|
|
|
<DropdownMenuItem onClick={() => handleDeleteThread(thread.id)}>
|
2026-02-09 16:49:11 -08:00
|
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
|
|
|
<span>{t("delete") || "Delete"}</span>
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
2026-01-13 00:17:12 -08:00
|
|
|
</div>
|
2026-02-09 16:49:11 -08:00
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
) : isSearchMode ? (
|
|
|
|
|
<div className="text-center py-8">
|
|
|
|
|
<Search className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{t("no_chats_found") || "No chats found"}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground/70 mt-1">
|
|
|
|
|
{t("try_different_search") || "Try a different search term"}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="text-center py-8">
|
|
|
|
|
<Users className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{showArchived
|
|
|
|
|
? t("no_archived_chats") || "No archived chats"
|
|
|
|
|
: t("no_shared_chats") || "No shared chats"}
|
|
|
|
|
</p>
|
|
|
|
|
{!showArchived && (
|
|
|
|
|
<p className="text-xs text-muted-foreground/70 mt-1">
|
|
|
|
|
Share a chat to collaborate with your team
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-02-21 23:59:04 +05:30
|
|
|
<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 gap-2 sm:justify-end">
|
|
|
|
|
<Button
|
2026-03-07 04:15:40 +05:30
|
|
|
variant="secondary"
|
2026-02-21 23:59:04 +05:30
|
|
|
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>
|
2026-03-22 00:01:31 +05:30
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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}
|
|
|
|
|
/>
|
2026-02-09 11:41:55 -05:00
|
|
|
</SidebarSlideOutPanel>
|
2026-01-13 00:17:12 -08:00
|
|
|
);
|
|
|
|
|
}
|