mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-11 22:51:05 +00:00
chore(nova): loader on initial data load (#817)

This commit is contained in:
parent
1dbd45427a
commit
9473d64c1d
5 changed files with 102 additions and 33 deletions
|
|
@ -845,11 +845,11 @@ function App() {
|
|||
<div className="text-center py-2">
|
||||
{authInvalidated ? (
|
||||
<div className="mb-8">
|
||||
<div className="p-3 mb-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<h2 className="m-0 mb-2 text-sm font-semibold text-red-800 leading-tight">
|
||||
<div className="p-3 mb-4 rounded-lg border border-red-900/60 bg-red-950/40">
|
||||
<h2 className="m-0 mb-2 text-sm font-semibold text-red-200 leading-tight">
|
||||
Session Expired
|
||||
</h2>
|
||||
<p className="m-0 text-xs text-red-600 leading-tight">
|
||||
<p className="m-0 text-xs text-red-300/90 leading-tight">
|
||||
Logged out since authentication was invalidated. Please
|
||||
login again.
|
||||
</p>
|
||||
|
|
@ -857,18 +857,18 @@ function App() {
|
|||
</div>
|
||||
) : (
|
||||
<div className="mb-8">
|
||||
<h2 className="m-0 mb-4 text-sm font-normal leading-tight">
|
||||
Login to unlock all chrome extension features
|
||||
<h2 className="m-0 mb-4 text-sm font-medium leading-snug text-neutral-100">
|
||||
Login to unlock all Chrome extension features
|
||||
</h2>
|
||||
|
||||
<ul className="list-none p-0 m-0 text-left">
|
||||
<li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold">
|
||||
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
|
||||
Save any page to your supermemory
|
||||
</li>
|
||||
<li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold">
|
||||
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
|
||||
Import all your Twitter / X Bookmarks
|
||||
</li>
|
||||
<li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold">
|
||||
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
|
||||
Import your ChatGPT Memories
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -876,10 +876,10 @@ function App() {
|
|||
)}
|
||||
|
||||
<div className="mt-8">
|
||||
<p className="m-0 mb-4 text-sm text-[#737373]">
|
||||
<p className="m-0 mb-4 text-sm text-neutral-400">
|
||||
Having trouble logging in?{" "}
|
||||
<button
|
||||
className="bg-transparent border-none text-blue-500 cursor-pointer underline text-sm p-0 hover:text-blue-700"
|
||||
className="bg-transparent border-none text-sky-400 cursor-pointer underline text-sm p-0 hover:text-sky-300"
|
||||
onClick={() => {
|
||||
window.open("mailto:support@supermemory.ai", "_blank")
|
||||
}}
|
||||
|
|
@ -890,7 +890,7 @@ function App() {
|
|||
</p>
|
||||
|
||||
<button
|
||||
className="w-full py-3 px-6 bg-gray-700 text-white border-none rounded-3xl text-base font-medium cursor-pointer transition-colors duration-200 hover:bg-gray-800 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
className="w-full py-3 px-6 bg-[#2d3f5c] text-white border-none rounded-3xl text-base font-medium cursor-pointer transition-colors duration-200 hover:bg-[#3d5270] disabled:bg-neutral-600 disabled:cursor-not-allowed"
|
||||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: import.meta.env.PROD
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
border: 1px solid #242424;
|
||||
color-scheme: dark;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
background-color: #0a0e14;
|
||||
border: 1px solid #0a0e14;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
|
|
@ -16,11 +16,6 @@
|
|||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
a:hover {
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,35 @@
|
|||
import path from "node:path"
|
||||
import { createRequire } from "node:module"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { defineConfig, type WxtViteConfig } from "wxt"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
function reactPackageRoot(pkg: "react" | "react-dom"): string {
|
||||
return path.dirname(require.resolve(`${pkg}/package.json`))
|
||||
}
|
||||
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
modules: ["@wxt-dev/module-react"],
|
||||
vite: () =>
|
||||
({
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
dedupe: ["react", "react-dom"],
|
||||
alias: {
|
||||
react: reactPackageRoot("react"),
|
||||
"react-dom": reactPackageRoot("react-dom"),
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ["react", "react-dom", "@tanstack/react-query"],
|
||||
},
|
||||
}) as WxtViteConfig,
|
||||
manifest: {
|
||||
name: "supermemory",
|
||||
homepage_url: "https://supermemory.ai",
|
||||
version: "6.1.1",
|
||||
version: "6.1.2",
|
||||
permissions: ["storage", "activeTab", "webRequest", "tabs"],
|
||||
host_permissions: [
|
||||
"*://x.com/*",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { useQueryState } from "nuqs"
|
|||
import type { z } from "zod"
|
||||
import { Masonry, useInfiniteLoader } from "masonic"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { SuperLoader } from "@/components/superloader"
|
||||
import { ErrorBoundary } from "@/components/error-boundary"
|
||||
import { cn } from "@lib/utils"
|
||||
import { useProject } from "@/stores"
|
||||
|
|
@ -42,7 +41,7 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@ui/components/alert-dialog"
|
||||
import { CheckIcon, Trash2Icon, XIcon } from "lucide-react"
|
||||
import { CheckIcon, Loader, Trash2Icon, XIcon } from "lucide-react"
|
||||
|
||||
// Document category type
|
||||
type DocumentCategory =
|
||||
|
|
@ -77,6 +76,55 @@ type OgData = {
|
|||
const PAGE_SIZE = 100
|
||||
const MAX_TOTAL = 1000
|
||||
|
||||
const MEMORIES_LOADING_LABELS = [
|
||||
"Getting your supermemories…",
|
||||
"Fetching your documents…",
|
||||
"Warming up Nova…",
|
||||
"Almost there…",
|
||||
] as const
|
||||
|
||||
function useRotatingLoadingLabel(
|
||||
labels: readonly string[],
|
||||
intervalMs = 2400,
|
||||
): string {
|
||||
const [index, setIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const id = window.setInterval(() => {
|
||||
setIndex((i) => (i + 1) % labels.length)
|
||||
}, intervalMs)
|
||||
return () => window.clearInterval(id)
|
||||
}, [labels.length, intervalMs])
|
||||
|
||||
const label = labels.at(index) ?? labels.at(0)
|
||||
return label ?? "Loading…"
|
||||
}
|
||||
|
||||
function MemoriesGridLoading() {
|
||||
const label = useRotatingLoadingLabel(MEMORIES_LOADING_LABELS)
|
||||
return (
|
||||
<output
|
||||
aria-label={label}
|
||||
className="h-full min-h-[min(50dvh,360px)] w-full flex items-center justify-center gap-4 px-8 py-20"
|
||||
aria-live="polite"
|
||||
>
|
||||
<Loader
|
||||
className={cn("shrink-0 animate-spin text-sky-400")}
|
||||
aria-hidden
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<p
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-center text-base text-[#A3A3A3] max-w-md leading-relaxed",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
</output>
|
||||
)
|
||||
}
|
||||
|
||||
// Discriminated union for masonry items
|
||||
type MasonryItem = { type: "document"; id: string; data: DocumentWithMemories }
|
||||
|
||||
|
|
@ -135,7 +183,7 @@ export function MemoriesGrid({
|
|||
emptyStateProps,
|
||||
}: MemoriesGridProps) {
|
||||
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
|
||||
const { user } = useAuth()
|
||||
const { user, isSessionPending } = useAuth()
|
||||
const { effectiveContainerTags } = useProject()
|
||||
const isMobile = useIsMobile()
|
||||
const [selectedCategories, setSelectedCategories] = useQueryState(
|
||||
|
|
@ -343,6 +391,10 @@ export function MemoriesGrid({
|
|||
[handleCardClick, isSelectionMode, selectedDocumentIds, onToggleSelection],
|
||||
)
|
||||
|
||||
if (isSessionPending) {
|
||||
return <MemoriesGridLoading />
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
|
|
@ -504,9 +556,7 @@ export function MemoriesGrid({
|
|||
</div>
|
||||
</div>
|
||||
) : isPending ? (
|
||||
<div className="h-full flex items-center justify-center p-4">
|
||||
<SuperLoader />
|
||||
</div>
|
||||
<MemoriesGridLoading />
|
||||
) : showNovaEmptyState ? (
|
||||
<NovaEmptyState
|
||||
onAddMemory={emptyStateProps.onAddMemory}
|
||||
|
|
@ -558,8 +608,8 @@ export function MemoriesGrid({
|
|||
/>
|
||||
|
||||
{isLoadingMore && (
|
||||
<div className="py-8 flex items-center justify-center">
|
||||
<SuperLoader />
|
||||
<div className="py-10 flex items-center justify-center">
|
||||
<Loader className="size-10 animate-spin text-sky-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ interface AuthContextType {
|
|||
org: Organization | null
|
||||
organizations: OrganizationListItem[] | null
|
||||
isRestoring: boolean
|
||||
isSessionPending: boolean
|
||||
setActiveOrg: (orgSlug: string) => Promise<void>
|
||||
updateOrgMetadata: (partial: Record<string, unknown>) => void
|
||||
refetchOrganizations: () => Promise<unknown>
|
||||
|
|
@ -32,7 +33,7 @@ interface AuthContextType {
|
|||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const { data: session } = useSession()
|
||||
const { data: session, isPending: isSessionPending } = useSession()
|
||||
const [org, setOrg] = useState<Organization | null>(null)
|
||||
const [isRestoring, setIsRestoring] = useState(true)
|
||||
const {
|
||||
|
|
@ -73,6 +74,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isSessionPending) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!session?.session) {
|
||||
setIsRestoring(false)
|
||||
setOrg(null)
|
||||
|
|
@ -145,7 +150,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [session, orgsData, orgsPending, setActiveOrg])
|
||||
}, [session, isSessionPending, orgsData, orgsPending, setActiveOrg])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
|
|
@ -181,6 +186,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
org,
|
||||
organizations,
|
||||
isRestoring,
|
||||
isSessionPending,
|
||||
session: session?.session ?? null,
|
||||
user: session?.user ?? null,
|
||||
setActiveOrg,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue