diff --git a/apps/web/app/new/page.tsx b/apps/web/app/new/page.tsx index 7ddd3630..d03b9dc9 100644 --- a/apps/web/app/new/page.tsx +++ b/apps/web/app/new/page.tsx @@ -1,9 +1,27 @@ +"use client" + import { Header } from "@/components/new/header" +import { ChatSidebar } from "@/components/new/chat-sidebar" +import { AnimatePresence } from "motion/react" +import { MemoriesGrid } from "@/components/new/memories-grid" export default function NewPage() { return ( -
-
+
+
+
+
+
+
+ +
+ + + + +
+
+
) } \ No newline at end of file diff --git a/apps/web/components/new/chat-sidebar.tsx b/apps/web/components/new/chat-sidebar.tsx new file mode 100644 index 00000000..974469fa --- /dev/null +++ b/apps/web/components/new/chat-sidebar.tsx @@ -0,0 +1,204 @@ +"use client" + +import { useState } from "react" +import { motion, AnimatePresence } from "motion/react" +import NovaOrb from "@/components/nova/nova-orb" +import { Button } from "@ui/components/button" +import { HistoryIcon, PanelRightCloseIcon, SendIcon, SquarePenIcon } from "lucide-react" +import { cn } from "@lib/utils" +import { dmSansClassName } from "@/utils/fonts" + +export function ChatSidebar() { + const [message, setMessage] = useState("") + const [isChatOpen, setIsChatOpen] = useState(true) + + // Static placeholder messages for UI display + const messages = [ + //{ + // message: "Sample memory content", + // type: "memory" as const, + // memories: [ + // { + // url: "https://example.com", + // title: "Example Memory", + // description: "This is a sample memory description for UI display purposes.", + // fullContent: "This is a sample memory description for UI display purposes.", + // }, + // ], + //}, + ] + + const handleSend = () => { + setMessage("") + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + const toggleChat = () => { + setIsChatOpen(!isChatOpen) + } + + return ( + + {!isChatOpen ? ( + + + + Chat with Nova + + + ) : ( + +
+

Chat Title

+
+ + + + + + +
+
+
+ {messages.map((msg, i) => ( +
+ {msg.type === "waiting" ? ( +
+ + {msg.message} +
+ ) : ( + <> +
+ {i === 0 && ( +
+ )} +
+
+ {msg.type === "memory" && ( +
+ {msg.memories?.map((memory) => ( +
+ {memory.title && ( +

+ {memory.title} +

+ )} + {memory.url && ( + + {memory.url} + + )} + {memory.description && ( +

+ {memory.description} +

+ )} +
+ ))} +
+ )} + + )} +
+ ))} +
+ +
+
{ + e.preventDefault() + if (message.trim()) { + handleSend() + } + }} + > + setMessage(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Chat with your Supermemory" + className="w-full text-white placeholder:text-white/20 rounded-sm outline-none resize-none text-base leading-relaxed bg-transparent px-2 h-10" + /> +
+ +
+
+
+ + )} + + ) +} + diff --git a/apps/web/components/new/document-cards/file-preview.tsx b/apps/web/components/new/document-cards/file-preview.tsx new file mode 100644 index 00000000..d8943db4 --- /dev/null +++ b/apps/web/components/new/document-cards/file-preview.tsx @@ -0,0 +1,101 @@ +"use client" + +import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" +import type { z } from "zod" +import { dmSansClassName } from "@/utils/fonts" +import { cn } from "@lib/utils" +import { PDF } from "@ui/assets/icons" +import { FileText, Image, Video } from "lucide-react" + +type DocumentsResponse = z.infer +type DocumentWithMemories = DocumentsResponse["documents"][0] + +function getFileTypeInfo(document: DocumentWithMemories): { + icon: React.ReactNode + extension: string + color?: string +} { + const type = document.type?.toLowerCase() + const mimeType = document.metadata?.mimeType as string | undefined + + if (mimeType) { + if (mimeType === "application/pdf") { + return { + icon: , + extension: ".pdf", + color: "#DC2626", + } + } + if (mimeType.startsWith("image/")) { + const ext = mimeType.split("/")[1] || "jpg" + return { + icon: , + extension: `.${ext}`, + } + } + if (mimeType.startsWith("video/")) { + const ext = mimeType.split("/")[1] || "mp4" + return { + icon:
-
+

📁 My Space

+ + + Grid + Graph + +
+ {user && ( diff --git a/apps/web/components/new/memories-grid.tsx b/apps/web/components/new/memories-grid.tsx new file mode 100644 index 00000000..9b3e9c2c --- /dev/null +++ b/apps/web/components/new/memories-grid.tsx @@ -0,0 +1,372 @@ +"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 } from "react" +import type { z } from "zod" +import { Masonry, useInfiniteLoader } from "masonic" +import { Sparkles } from "lucide-react" +import { dmSansClassName } from "@/utils/fonts" +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" + +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() { + const { user } = useAuth() + const { selectedProject } = useProject() + const isMobile = useIsMobile() + + 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 hasMore = hasNextPage + const isLoadingMore = isFetchingNextPage + + const loadMoreDocuments = useCallback(async (): Promise => { + if (hasNextPage && !isFetchingNextPage) { + await fetchNextPage() + return + } + return + }, [hasNextPage, isFetchingNextPage, fetchNextPage]) + + const maybeLoadMore = useInfiniteLoader( + async (_startIndex, _stopIndex, _currentItems) => { + if (hasMore && !isLoadingMore) { + await loadMoreDocuments() + } + }, + { + isItemLoaded: (index, items) => !!items[index], + minimumBatchSize: 10, + threshold: 5, + }, + ) + + const renderDocumentCard = useCallback( + ({ + index, + data, + width, + }: { + index: number + data: DocumentWithMemories + width: number + }) => ( + + ), + [], + ) + + if (!user) { + return ( +
+
+

Please log in to view your memories

+
+
+ ) + } + + return ( +
+ + {error ? ( +
+
+ Error loading documents: {error.message} +
+
+ ) : isPending ? ( +
+
+ + Loading memory list... +
+
+ ) : documents.length === 0 && !isPending ? ( +
+
+ No memories found +
+
+ ) : ( +
+ d.id).join(",")}`} + items={documents} + render={renderDocumentCard} + columnGutter={16} + rowGutter={16} + columnWidth={216} + maxColumnCount={isMobile ? 1 : undefined} + itemHeightEstimate={200} + overscanBy={3} + onRender={maybeLoadMore} + /> + + {isLoadingMore && ( +
+
+ + Loading more memories... +
+
+ )} +
+ )} +
+ ) +} + +const DocumentCard = memo( + ({ + index: _index, + data: document, + width, + }: { + index: number + data: DocumentWithMemories + width: number + }) => { + return ( +
+ +
+
+

+ {document.title} +

+ {document.url && ( +

+ {getAbsoluteUrl(document.url)} +

+ )} +
+
+

+ + {document.memoryEntries.length} memories +

+

+ {new Date(document.createdAt).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} +

+
+
+
+ ) + }, +) + +DocumentCard.displayName = "DocumentCard" + +function getAbsoluteUrl(url: string): string { + try { + const urlObj = new URL(url) + return urlObj.host.replace(/^www\./, "") + } catch { + const match = url.match(/^https?:\/\/([^\/]+)/) + const host = match?.[1] ?? url.replace(/^https?:\/\//, "") + return host.replace(/^www\./, "") + } +} + +function SyncLogoIcon({ className }: { className?: string }) { + return ( + + + + + + + + + + + + + + + + + + + + + + ) +} + +function ContentPreview({ document }: { document: DocumentWithMemories }) { + // Check for Google Docs + if ( + document.url?.includes("https://docs.googleapis.com/v1/documents") || + document.url?.includes("docs.google.com/document") || + document.type === "google_doc" + ) { + return + } + + // Check for Twitter + if ( + document.url?.includes("x.com/") && + document.metadata?.sm_internal_twitter_metadata + ) { + return ( + + ) + } + + if ( + document.type === "pdf" || + document.type === "image" || + document.type === "video" || + document.metadata?.mimeType + ) { + return + } + + // Check for Website + if (document.url?.includes("https://")) { + return + } + + // Default to Note + return +} diff --git a/apps/web/globals.css b/apps/web/globals.css index 0fbf5557..f7f76fb6 100644 --- a/apps/web/globals.css +++ b/apps/web/globals.css @@ -1,3 +1,4 @@ +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap'); @import "tailwindcss"; @plugin "@tailwindcss/typography"; @@ -11,12 +12,30 @@ .sm-tweet-theme .react-tweet-theme { --tweet-container-margin: 0px; + font-size: inherit !important; +} + +.sm-tweet-theme .react-tweet-theme * { + min-width: 0 !important; } .sm-tweet-theme .tweet-header-module__A9EVQG__authorFollow { display: none; } +.tweet-container-module__CmFQMq__article { + padding: 0 !important; + font-family: "DM Sans", sans-serif !important; + font-size: 10px !important; + --tweet-header-font-size: 10px !important; + --tweet-header-line-height: 1.25rem !important; +} + +.tweet-body-module__ZNRZja__root > * { + font-size: 10px !important; + line-height: 0.25rem !important; +} + .scrollbar-thin { scrollbar-width: thin; scrollbar-color: #293952 transparent; diff --git a/packages/ui/assets/icons.tsx b/packages/ui/assets/icons.tsx index 5383f690..ef24567a 100644 --- a/packages/ui/assets/icons.tsx +++ b/packages/ui/assets/icons.tsx @@ -194,15 +194,15 @@ export const PDF = ({ className }: { className?: string }) => ( PDF );