"use client" import { useAuth } from "@lib/auth-context" import { $fetch } from "@repo/lib/api" import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" import { useInfiniteQuery } from "@tanstack/react-query" import { useCallback, memo, useMemo, useState, useRef } from "react" import type { z } from "zod" import { Masonry, useInfiniteLoader } from "masonic" import { dmSansClassName } from "@/lib/fonts" import { SuperLoader } from "@/components/superloader" import { cn } from "@lib/utils" import { Button } from "@ui/components/button" import { useProject } from "@/stores" import { useIsMobile } from "@hooks/use-mobile" import type { Tweet } from "react-tweet/api" import { TweetPreview } from "./document-cards/tweet-preview" import { WebsitePreview } from "./document-cards/website-preview" import { GoogleDocsPreview } from "./document-cards/google-docs-preview" import { FilePreview } from "./document-cards/file-preview" import { NotePreview } from "./document-cards/note-preview" import { YoutubePreview } from "./document-cards/youtube-preview" import { getAbsoluteUrl, isYouTubeUrl, useYouTubeChannelName } from "./utils" import { SyncLogoIcon } from "@ui/assets/icons" import { McpPreview } from "./document-cards/mcp-preview" import { DocumentModal } from "./document-modal" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] const IS_DEV = process.env.NODE_ENV === "development" const PAGE_SIZE = IS_DEV ? 100 : 100 const MAX_TOTAL = 1000 export function MemoriesGrid({ isChatOpen }: { isChatOpen: boolean }) { const { user } = useAuth() const { selectedProject } = useProject() const isMobile = useIsMobile() const [selectedDocument, setSelectedDocument] = useState(null) const [isModalOpen, setIsModalOpen] = useState(false) const { data, error, isPending, isFetchingNextPage, hasNextPage, fetchNextPage, } = useInfiniteQuery({ queryKey: ["documents-with-memories", selectedProject], initialPageParam: 1, queryFn: async ({ pageParam }) => { const response = await $fetch("@post/documents/documents", { body: { page: pageParam as number, limit: (pageParam as number) === 1 ? (IS_DEV ? 500 : 500) : PAGE_SIZE, sort: "createdAt", order: "desc", containerTags: selectedProject ? [selectedProject] : undefined, }, disableValidation: true, }) if (response.error) { throw new Error(response.error?.message || "Failed to fetch documents") } return response.data }, getNextPageParam: (lastPage, allPages) => { const loaded = allPages.reduce( (acc, p) => acc + (p.documents?.length ?? 0), 0, ) if (loaded >= MAX_TOTAL) return undefined const { currentPage, totalPages } = lastPage.pagination if (currentPage < totalPages) { return currentPage + 1 } return undefined }, staleTime: 5 * 60 * 1000, enabled: !!user, }) const documents = useMemo(() => { return ( data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? [] ) }, [data]) const loadMoreDocuments = useCallback(async (): Promise => { if (hasNextPage && !isFetchingNextPage) { await fetchNextPage() return } return }, [hasNextPage, isFetchingNextPage, fetchNextPage]) const maybeLoadMore = useInfiniteLoader( async (_startIndex, _stopIndex, _currentItems) => { if (hasNextPage && !isFetchingNextPage) { await loadMoreDocuments() } }, { isItemLoaded: (index, items) => !!items[index], minimumBatchSize: 10, threshold: 5, }, ) const handleCardClick = useCallback((document: DocumentWithMemories) => { setSelectedDocument(document) setIsModalOpen(true) }, []) const renderDocumentCard = useCallback( ({ index, data, width, }: { index: number data: DocumentWithMemories width: number }) => ( ), [handleCardClick], ) if (!user) { return (

Please log in to view your memories

) } return (
{error ? (
Error loading documents: {error.message}
) : isPending ? (
) : documents.length === 0 && !isPending ? (
No memories found
) : (
d.id).join(",")}-${isChatOpen}`} items={documents} render={renderDocumentCard} columnGutter={0} rowGutter={0} columnWidth={216} maxColumnCount={isMobile ? 1 : undefined} itemHeightEstimate={200} overscanBy={3} onRender={maybeLoadMore} /> {isFetchingNextPage && (
)}
)} setIsModalOpen(false)} />
) } function DocumentUrlDisplay({ url }: { url: string }) { const isYouTube = isYouTubeUrl(url) const { data: channelName, isLoading } = useYouTubeChannelName( isYouTube ? url : null, ) if (isYouTube) { return (

{isLoading ? "YouTube" : channelName || "YouTube"}

) } return (

{getAbsoluteUrl(url)}

) } const DocumentCard = memo( ({ index: _index, data: document, width, onClick, }: { index: number data: DocumentWithMemories width: number onClick: (document: DocumentWithMemories) => void }) => { const [rotation, setRotation] = useState({ rotateX: 0, rotateY: 0 }) const cardRef = useRef(null) const handleMouseMove = (e: React.MouseEvent) => { if (!cardRef.current) return const rect = cardRef.current.getBoundingClientRect() const centerX = rect.left + rect.width / 2 const centerY = rect.top + rect.height / 2 const mouseX = e.clientX - centerX const mouseY = e.clientY - centerY // Calculate rotation angles (max 15 degrees) const rotateY = (mouseX / (rect.width / 2)) * 15 const rotateX = -(mouseY / (rect.height / 2)) * 15 setRotation({ rotateX, rotateY }) } const handleMouseLeave = () => { setRotation({ rotateX: 0, rotateY: 0 }) } return (
) }, ) DocumentCard.displayName = "DocumentCard" function ContentPreview({ document }: { document: DocumentWithMemories }) { if ( document.url?.includes("https://docs.googleapis.com/v1/documents") || document.url?.includes("docs.google.com/document") || document.type === "google_doc" ) { return } if ( document.url?.includes("x.com/") && document.metadata?.sm_internal_twitter_metadata ) { return ( ) } if (document.source === "mcp") { return } if (isYouTubeUrl(document.url)) { return } if ( document.type === "pdf" || document.type === "image" || document.type === "video" || document.metadata?.mimeType ) { return } if (document.url?.includes("https://")) { return } // Default to Note return }