import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { ActionIcon, Center, Text, TextInput, Loader, Tooltip, } from "@mantine/core"; import { modals } from "@mantine/modals"; import { useDebouncedValue } from "@mantine/hooks"; import { IconPlus, IconSearch, IconMessageCircle2 } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { useChatsQuery, useDeleteChatMutation, useUpdateChatTitleMutation, useSearchChatsQuery, } from "../queries/ai-chat-query"; import AiChatSidebarItem from "./ai-chat-sidebar-item"; import { groupChatsByAge } from "../utils/group-chats-by-age"; import classes from "../styles/chat-sidebar.module.css"; export default function AiChatSidebar() { const { t } = useTranslation(); const navigate = useNavigate(); const { chatId } = useParams<{ chatId: string }>(); const [search, setSearch] = useState(""); const [debouncedSearch] = useDebouncedValue(search, 300); const chatsQuery = useChatsQuery(); const searchQuery = useSearchChatsQuery(debouncedSearch); const deleteMutation = useDeleteChatMutation(); const renameMutation = useUpdateChatTitleMutation(); const chats = useMemo(() => { if (debouncedSearch) { return searchQuery.data || []; } return chatsQuery.data?.pages.flatMap((p) => p.items) || []; }, [debouncedSearch, searchQuery.data, chatsQuery.data]); const groupedChats = useMemo(() => groupChatsByAge(chats, t), [chats, t]); const sentinelRef = useRef(null); const { hasNextPage, fetchNextPage, isFetchingNextPage } = chatsQuery; const isSearching = Boolean(debouncedSearch); useEffect(() => { if (isSearching) return; const sentinel = sentinelRef.current; if (!sentinel) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, { threshold: 0.1 }, ); observer.observe(sentinel); return () => observer.disconnect(); }, [isSearching, hasNextPage, isFetchingNextPage, fetchNextPage]); const handleNewChat = useCallback( (event: React.MouseEvent) => { if ( event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey ) { return; } event.preventDefault(); navigate("/ai"); }, [navigate], ); const handleDelete = useCallback( (id: string, title: string | null) => { modals.openConfirmModal({ title: t("Delete chat"), centered: true, children: ( {t("Are you sure you want to delete '{{title}}'? This action cannot be undone.", { title: title || t("Untitled"), })} ), labels: { confirm: t("Delete"), cancel: t("Cancel") }, confirmProps: { color: "red" }, onConfirm: () => { deleteMutation.mutate(id, { onSuccess: () => { if (chatId === id) { navigate("/ai"); } }, }); }, }); }, [deleteMutation, chatId, navigate, t], ); const handleRename = useCallback( (chatId: string, title: string) => { renameMutation.mutate({ chatId, title }); }, [renameMutation], ); const isLoading = chatsQuery.isLoading || searchQuery.isLoading; return (
{t("AI Chat")}
} size="xs" value={search} onChange={(e) => setSearch(e.currentTarget.value)} />
{isLoading && } {!isLoading && chats.length === 0 && (
{isSearching ? t("No chats found") : t("No conversations yet")}
{isSearching ? t("Try a different search term.") : t("Start a new chat to see it here.")}
)} {isSearching ? chats.map((chat) => ( )) : groupedChats.map((group) => (
{group.label}
{group.chats.map((chat) => ( ))}
))} {!isSearching && ( <>
{isFetchingNextPage && (
)} )}
); }