+ {trigger}
{isOpen && (
<>
@@ -64,7 +86,7 @@ export default function ChatModelSelector({
aria-label="Close model selector"
/>
-
+
{models.map((model) => {
const modelData = modelNames[model.id]
diff --git a/apps/web/components/dashboard-view.tsx b/apps/web/components/dashboard-view.tsx
new file mode 100644
index 000000000..be611c3fa
--- /dev/null
+++ b/apps/web/components/dashboard-view.tsx
@@ -0,0 +1,869 @@
+"use client"
+
+import type { ReactNode } from "react"
+import { useMemo, useState, useEffect } from "react"
+import { useAuth } from "@lib/auth-context"
+import { $fetch } from "@lib/api"
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
+import { useQuery } from "@tanstack/react-query"
+import { useRouter } from "next/navigation"
+import {
+ ArrowRight,
+ ExternalLink,
+ FileText,
+ Lightbulb,
+ Link2,
+ RotateCcw,
+ SearchIcon,
+ Terminal,
+} from "lucide-react"
+import type { z } from "zod"
+import { CHROME_EXTENSION_URL, RAYCAST_EXTENSION_URL } from "@lib/constants"
+import { cn } from "@lib/utils"
+import { dmSansClassName } from "@/lib/fonts"
+import { useProject } from "@/stores"
+import {
+ HighlightsCard,
+ type HighlightItem,
+} from "@/components/highlights-card"
+import { StaticGraphPreview } from "@/components/memory-graph/graph-card"
+import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip"
+import { ChromeIcon, RaycastIcon } from "@/components/integration-icons"
+import { GoogleDrive, Notion, MCPIcon } from "@ui/assets/icons"
+import { analytics } from "@/lib/analytics"
+import type { IntegrationParamValue } from "@/lib/search-params"
+import { motion, AnimatePresence } from "motion/react"
+import {
+ usePersonalization,
+ type Profession,
+} from "@/hooks/use-personalization"
+
+type DocumentsResponse = z.infer
+type DocumentWithMemories = DocumentsResponse["documents"][0]
+
+const fadeUp = {
+ initial: { opacity: 0, y: 8 },
+ animate: { opacity: 1, y: 0 },
+ transition: {
+ duration: 0.3,
+ ease: [0.4, 0, 0.2, 1] as [number, number, number, number],
+ },
+}
+
+const CYCLE_INTERVAL_MS = 8_000
+
+const PLUGIN_TAGLINES: Record>> = {
+ developer: {
+ mcp: "Ask Claude about your saved docs and specs from any IDE",
+ chrome: "Save Stack Overflow answers, docs and repos in one click",
+ raycast: "Search your tech docs and snippets without context switching",
+ notion: "Make your engineering specs and RFCs instantly findable",
+ "google-drive": "Query your design docs, code specs and shared files",
+ },
+ research: {
+ mcp: "Ask Claude across your entire reading list and notes",
+ chrome: "Clip papers and articles directly while you read",
+ raycast: "Pull up citations and notes without breaking your focus",
+ notion: "Keep your literature review alongside your saved papers",
+ "google-drive": "Index datasets, papers and research docs in one place",
+ },
+ finance: {
+ mcp: "Ask Claude about your saved thesis notes and research",
+ chrome: "Save earnings calls, market reports and articles instantly",
+ raycast: "Surface your research and models without breaking flow",
+ notion: "Make your investment thesis and portfolio notes searchable",
+ "google-drive": "Query your financial models, decks and reports instantly",
+ },
+ design: {
+ mcp: "Ask Claude about your saved briefs and design research",
+ chrome: "Save inspiration and references as you browse",
+ raycast: "Find your saved references and briefs from anywhere",
+ notion: "Make your design system docs and briefs searchable",
+ "google-drive": "Index your briefs, feedback docs and creative assets",
+ },
+ legal: {
+ mcp: "Ask Claude across your saved contracts and case notes",
+ chrome: "Clip case law, statutes and legal articles in one click",
+ raycast: "Surface contracts and precedents without leaving your workflow",
+ notion: "Keep memos, briefs and case notes instantly searchable",
+ "google-drive": "Index contracts, filings and legal research docs",
+ },
+ marketing: {
+ mcp: "Ask Claude across your saved campaigns and research",
+ chrome: "Save competitor pages and inspiration as you browse",
+ raycast: "Pull up campaign briefs and notes without context switching",
+ notion: "Make your content calendar and campaign briefs searchable",
+ "google-drive": "Query campaign reports, briefs and creative assets",
+ },
+ medical: {
+ mcp: "Ask Claude across your medical literature and clinical notes",
+ chrome: "Save studies and clinical resources while you read",
+ raycast: "Surface guidelines and notes without breaking your flow",
+ notion: "Keep clinical notes and research in one searchable place",
+ "google-drive": "Index guidelines, studies and patient education docs",
+ },
+ default: {
+ mcp: "Ask Claude using your own saved knowledge",
+ chrome: "Save any page in one click while you browse",
+ raycast: "Search your memory without leaving the keyboard",
+ notion: "Make every note and doc instantly searchable",
+ "google-drive": "Ask questions across your docs, slides and sheets",
+ },
+}
+
+export type MemoryOfDay = {
+ memories: string[]
+ timeLabel: string
+ sourceDocumentId: string | null
+}
+
+const TIPS: Record = {
+ developer: [
+ "Use ⌘K to search code snippets and docs by intent, not just keywords",
+ "Connect Claude MCP to query your saved knowledge from any IDE",
+ "Save GitHub repos and READMEs — ask questions across all of them",
+ "Use 'Related' on highlights to find connected technical concepts",
+ ],
+ research: [
+ "Save papers and ask questions across your entire reading list",
+ "Use 'Related' on highlights to surface connected research",
+ "Connect Notion to index your notes alongside your papers",
+ "Semantic search means you can ask questions, not just search titles",
+ ],
+ finance: [
+ "Save articles and ask follow-up questions across your research",
+ "Connect Notion to keep your investment thesis searchable",
+ "Use ⌘K to find specific data points across all your saves",
+ "Daily Brief surfaces connections you may have missed",
+ ],
+ design: [
+ "Save inspiration and search by concept — 'minimalist UI' finds the right ones",
+ "Use ⌘K to rediscover references by meaning, not filename",
+ "Connect Notion to make your briefs and moodboards searchable",
+ "Chrome extension saves any page in one click while you browse",
+ ],
+ legal: [
+ "Save documents and search across them semantically in seconds",
+ "Connect Notion to index your memos and case notes together",
+ "Use Daily Brief to resurface relevant precedents automatically",
+ "Google Drive sync keeps your contracts indexed and queryable",
+ ],
+ marketing: [
+ "Save campaigns and resources — ask what worked across all of them",
+ "Chrome extension captures competitor pages in one click",
+ "Use 'Related' to find similar campaigns in your archive",
+ "Connect Notion to make your campaign briefs instantly searchable",
+ ],
+ medical: [
+ "Save studies and query across your entire reading list",
+ "Connect Notion to keep clinical notes alongside research",
+ "Use ⌘K to find specific findings across hundreds of papers",
+ "Daily Brief surfaces relevant research from your saves automatically",
+ ],
+ default: [
+ "Use ⌘K to search by meaning — ask questions, not just keywords",
+ "Daily Brief surfaces insights from your saves each morning",
+ "Chrome extension saves any page in one click while you browse",
+ "Connect integrations to make all your knowledge searchable here",
+ ],
+}
+
+const PROFESSION_PLUGIN_ORDER: Record = {
+ developer: ["mcp", "chrome", "raycast", "notion", "google-drive"],
+ research: ["notion", "chrome", "google-drive", "mcp", "raycast"],
+ finance: ["notion", "google-drive", "chrome", "mcp", "raycast"],
+ design: ["chrome", "notion", "raycast", "mcp", "google-drive"],
+ legal: ["notion", "google-drive", "chrome", "mcp", "raycast"],
+ marketing: ["chrome", "notion", "raycast", "google-drive", "mcp"],
+ medical: ["notion", "chrome", "google-drive", "mcp", "raycast"],
+ default: ["mcp", "chrome", "notion", "raycast", "google-drive"],
+}
+
+const PROFESSION_LABELS: {
+ value: Exclude
+ label: string
+}[] = [
+ { value: "developer", label: "Developer" },
+ { value: "research", label: "Researcher" },
+ { value: "finance", label: "Finance" },
+ { value: "design", label: "Designer" },
+ { value: "legal", label: "Legal" },
+ { value: "marketing", label: "Marketing" },
+ { value: "medical", label: "Medical" },
+]
+
+// Static plugin metadata — shared between PluginPromoCard and RecommendedPluginsCard
+const PLUGIN_STATIC = [
+ {
+ id: "mcp",
+ name: "Claude MCP",
+ Icon: MCPIcon,
+ accentColor: "#D4A853",
+ tagline: "Ask Claude from your own saved knowledge, not just training data",
+ cta: "Set up",
+ },
+ {
+ id: "chrome",
+ name: "Chrome Extension",
+ Icon: ChromeIcon,
+ accentColor: "#4BA0FA",
+ tagline: "Save any page in one click — findable by meaning, forever",
+ cta: "Install",
+ },
+ {
+ id: "raycast",
+ name: "Raycast",
+ Icon: RaycastIcon,
+ accentColor: "#FF6363",
+ tagline: "Search your entire memory without leaving your keyboard",
+ cta: "Install",
+ },
+ {
+ id: "notion",
+ name: "Notion",
+ Icon: Notion,
+ accentColor: "#FAFAFA",
+ tagline: "Sync your workspace and make every note searchable everywhere",
+ cta: "Connect",
+ },
+ {
+ id: "google-drive",
+ name: "Google Drive",
+ Icon: GoogleDrive,
+ accentColor: "#4BA0FA",
+ tagline:
+ "Index your Drive files — ask questions across docs, slides, sheets",
+ cta: "Connect",
+ },
+] as const
+
+function RecommendedPluginsCard({
+ profession,
+ setProfession,
+ connectedProviders,
+ hasMcp,
+ onOpenPlugins,
+ onOpenIntegrations,
+}: {
+ profession: Profession
+ setProfession: (p: Profession) => void
+ connectedProviders: Set
+ hasMcp: boolean
+ onOpenPlugins: () => void
+ onOpenIntegrations: (integration?: IntegrationParamValue) => void
+}) {
+ const [isEditing, setIsEditing] = useState(false)
+ useEffect(() => {
+ setIsEditing(false)
+ }, [])
+ const showPicker = profession === "default" || isEditing
+ const allPlugins = useMemo(() => {
+ const onClicks: Record void> = {
+ mcp: onOpenPlugins,
+ chrome: () =>
+ window.open(CHROME_EXTENSION_URL, "_blank", "noopener,noreferrer"),
+ raycast: () =>
+ window.open(RAYCAST_EXTENSION_URL, "_blank", "noopener,noreferrer"),
+ notion: () => onOpenIntegrations("notion"),
+ "google-drive": () => onOpenIntegrations("google-drive"),
+ }
+ const connected: Record = {
+ mcp: hasMcp,
+ chrome: false,
+ raycast: false,
+ notion: connectedProviders.has("notion"),
+ "google-drive": connectedProviders.has("google-drive"),
+ }
+ return PLUGIN_STATIC.map((p) => ({
+ ...p,
+ connected: connected[p.id] ?? false,
+ onClick: onClicks[p.id]!,
+ }))
+ }, [hasMcp, connectedProviders, onOpenPlugins, onOpenIntegrations])
+
+ const order = PROFESSION_PLUGIN_ORDER[profession]
+ const suggestions = useMemo(
+ () =>
+ order
+ .map((id) => allPlugins.find((p) => p.id === id))
+ .filter((p): p is NonNullable => !!p && !p.connected)
+ .slice(0, 3),
+ [order, allPlugins],
+ )
+
+ return (
+
+ {showPicker ? (
+
+
+ {isEditing ? "Change your field:" : "What's your field?"}
+
+
+ {PROFESSION_LABELS.map(({ value, label }) => (
+ {
+ setProfession(value)
+ setIsEditing(false)
+ }}
+ className={cn(
+ "rounded-full border px-2.5 py-1 text-[11px] font-medium transition-all cursor-pointer",
+ profession === value
+ ? "border-[#3374FF]/40 bg-[#3374FF]/10 text-[#6BB0FF]"
+ : "border-[#161F2C] text-[#525D6E] hover:border-[#3374FF]/25 hover:text-[#4BA0FA]",
+ )}
+ >
+ {label}
+
+ ))}
+
+ {isEditing && (
+
setIsEditing(false)}
+ className="text-[10px] text-[#3A4455] hover:text-[#525D6E] transition-colors text-left cursor-pointer"
+ >
+ Cancel
+
+ )}
+
+ ) : suggestions.length === 0 ? (
+
+ ) : (
+ <>
+
+ {suggestions.map((plugin) => (
+
+
+
+
+
+ {plugin.name}
+
+
+ {PLUGIN_TAGLINES[profession][plugin.id] ?? plugin.tagline}
+
+
+
+ {plugin.cta} →
+
+
+
+ ))}
+
+
setIsEditing(true)}
+ className="text-left px-2 pb-1 text-[10px] text-[#3A4455] hover:text-[#525D6E] transition-colors cursor-pointer"
+ >
+ Not a{" "}
+ {PROFESSION_LABELS.find(
+ (p) => p.value === profession,
+ )?.label.toLowerCase()}
+ ? Change →
+
+ >
+ )}
+
+ )
+}
+
+function MemoryOfDayCard({ data }: { data: MemoryOfDay }) {
+ const router = useRouter()
+
+ const memory = data.memories[0]
+
+ if (!memory) return null
+
+ return (
+ router.push("/?view=list")}
+ className={cn(
+ "group w-full h-full text-left bg-[#0B1017] border border-[rgba(255,255,255,0.05)] rounded-[18px] p-3 flex flex-col justify-between hover:border-[rgba(255,255,255,0.10)] transition-colors cursor-pointer",
+ dmSansClassName(),
+ )}
+ >
+
+
+ {data.timeLabel}
+
+
+ {memory}
+
+
+
+
+ View memories →
+
+
+ )
+}
+
+function PluginPromoCard({
+ hasMcp,
+ connectedProviders,
+ onOpenPlugins,
+ onOpenIntegrations,
+}: {
+ hasMcp: boolean
+ connectedProviders: Set
+ onOpenPlugins: () => void
+ onOpenIntegrations: (integration?: IntegrationParamValue) => void
+}) {
+ const plugins = useMemo(() => {
+ const onClicks: Record void> = {
+ mcp: onOpenPlugins,
+ chrome: () =>
+ window.open(CHROME_EXTENSION_URL, "_blank", "noopener,noreferrer"),
+ raycast: () =>
+ window.open(RAYCAST_EXTENSION_URL, "_blank", "noopener,noreferrer"),
+ notion: () => onOpenIntegrations("notion"),
+ "google-drive": () => onOpenIntegrations("google-drive"),
+ }
+ const connected: Record = {
+ mcp: hasMcp,
+ chrome: false,
+ raycast: false,
+ notion: connectedProviders.has("notion"),
+ "google-drive": connectedProviders.has("google-drive"),
+ }
+ return PLUGIN_STATIC.map((p) => ({
+ ...p,
+ connected: connected[p.id] ?? false,
+ onClick: onClicks[p.id]!,
+ })).filter((p) => !p.connected)
+ }, [hasMcp, connectedProviders, onOpenPlugins, onOpenIntegrations])
+
+ const [index, setIndex] = useState(0)
+
+ // Reset when the plugins list changes length (e.g., user connects one)
+ useEffect(() => {
+ setIndex(0)
+ }, [])
+
+ useEffect(() => {
+ if (plugins.length <= 1) return
+ const id = setInterval(
+ () => setIndex((i) => (i + 1) % plugins.length),
+ CYCLE_INTERVAL_MS,
+ )
+ return () => clearInterval(id)
+ }, [plugins.length])
+
+ const safeIndex = Math.min(index, plugins.length - 1)
+ const plugin = plugins[safeIndex]
+
+ return (
+
+ {plugin ? (
+ <>
+
+
+
+
+ {plugins.length > 1 && (
+
+ {plugins.map((_, i) => (
+ setIndex(i)}
+ className={cn(
+ "rounded-full transition-all cursor-pointer",
+ i === safeIndex
+ ? "w-3 h-1 bg-[#4BA0FA]"
+ : "size-1 bg-[#2A3040] hover:bg-[#3A4455]",
+ )}
+ />
+ ))}
+
+ )}
+
+
+
+ {plugin.name}
+
+
+ {plugin.tagline}
+
+
+
+
+
+
+ {plugin.cta}
+
+
+ >
+ ) : (
+
+
+
+ All integrations connected
+
+
+ )}
+
+ )
+}
+
+export function DashboardView({
+ spaceLabel,
+ headerNotice,
+ highlights,
+ isLoadingHighlights,
+ onAddMemory,
+ onOpenSearch,
+ onOpenIntegrations,
+ onOpenPlugins,
+ onNavigateToMemories,
+ onNavigateToGraph,
+ onOpenDocument,
+ onHighlightsChat,
+ onHighlightsShowRelated,
+ onResetHighlights,
+ memoryOfDay,
+}: {
+ spaceLabel: string
+ headerNotice?: ReactNode
+ highlights: HighlightItem[]
+ isLoadingHighlights: boolean
+ onAddMemory: (tab: "note" | "link") => void
+ onOpenSearch: () => void
+ onOpenIntegrations: (integration?: IntegrationParamValue) => void
+ onOpenPlugins: () => void
+ onNavigateToMemories: () => void
+ onNavigateToGraph: () => void
+ onOpenDocument: (document: DocumentWithMemories) => void
+ onHighlightsChat: (seed: string) => void
+ onHighlightsShowRelated: (query: string) => void
+ onResetHighlights: () => void
+ memoryOfDay: MemoryOfDay | null
+}) {
+ const { user } = useAuth()
+ const { effectiveContainerTags } = useProject()
+ const _router = useRouter()
+ const { data: recentsData } = useQuery({
+ queryKey: ["dashboard-recents", effectiveContainerTags],
+ queryFn: async (): Promise => {
+ const response = await $fetch("@post/documents/documents", {
+ body: {
+ page: 1,
+ limit: 5,
+ sort: "createdAt",
+ order: "desc",
+ containerTags: effectiveContainerTags,
+ },
+ disableValidation: true,
+ })
+ if (response.error) throw new Error(response.error?.message)
+ return response.data as DocumentsResponse
+ },
+ staleTime: 60 * 1000,
+ enabled: !!user,
+ })
+
+ const { data: connections = [] } = useQuery({
+ queryKey: ["connections-list", effectiveContainerTags],
+ queryFn: async () => {
+ const response = await $fetch("@post/connections/list", {
+ body: { containerTags: effectiveContainerTags },
+ })
+ if (response.error) return []
+ return response.data ?? []
+ },
+ staleTime: 5 * 60 * 1000,
+ enabled: !!user,
+ })
+
+ const { data: mcpData } = useQuery({
+ queryKey: ["mcp-status"],
+ queryFn: async () => {
+ const response = await $fetch("@get/mcp/has-login")
+ return response.data ?? { previousLogin: false }
+ },
+ staleTime: 5 * 60 * 1000,
+ enabled: !!user,
+ })
+
+ const {
+ copy: personalizedCopy,
+ profession,
+ setProfession,
+ } = usePersonalization()
+
+ const recents = recentsData?.documents ?? []
+ const totalMemories = recentsData?.pagination?.totalItems ?? 0
+ const hasMcp = mcpData?.previousLogin ?? false
+ const connectedProviders = new Set(connections.map((c) => c.provider))
+
+ const dayOfYear = Math.round(
+ (Date.now() - new Date(new Date().getFullYear(), 0, 1).getTime()) /
+ 86_400_000,
+ )
+ const tips = TIPS[profession]
+ const tip = tips[dayOfYear % tips.length]
+
+ return (
+
+
+ {headerNotice ?
{headerNotice}
: null}
+
+ {/* Header */}
+
+
+
+ Home
+
+
+ {spaceLabel}
+
+
+ {totalMemories > 0 && (
+
+
+
+
+
+
+
+
+ View graph
+
+
+ )}
+
+
+ {/* Daily Brief — hero */}
+
+
+
+ Daily brief
+
+
+
+
+
+
+
+
+ Refresh daily brief
+
+
+
+
+
+
+
+
+ {memoryOfDay ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Actions + connection status — single unified row */}
+
+ {/* Quick actions */}
+
+ onAddMemory("link")}
+ className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm text-[#5A6478] hover:bg-[#0D121A] hover:text-white transition-colors cursor-pointer"
+ >
+
+ {personalizedCopy.saveLink}
+
+ ·
+ onAddMemory("note")}
+ className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm text-[#5A6478] hover:bg-[#0D121A] hover:text-white transition-colors cursor-pointer"
+ >
+
+ {personalizedCopy.writeNote}
+
+ ·
+ {
+ analytics.searchOpened({ source: "header" })
+ onOpenSearch()
+ }}
+ className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm text-[#5A6478] hover:bg-[#0D121A] hover:text-white transition-colors cursor-pointer"
+ >
+
+ Search
+
+
+
+ {/* Tip of the day */}
+
+
+ {tip}
+
+
+
+ {/* Recently saved + Suggested for you */}
+
+ {recents.length > 0 ? (
+ <>
+ {/* Shared header row — both labels aligned */}
+
+
+
+
+ Suggested for you
+
+
+
+
+ {/* Content row */}
+
+
+ {recents.map((doc) => {
+ const isLink = !!doc.url
+ return (
+
+ onOpenDocument(doc)}
+ className="group flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-[#0D121A]"
+ >
+
+ {isLink ? (
+
+ ) : (
+
+ )}
+
+
+ {doc.title?.trim() || "Untitled"}
+
+
+
+
+ )
+ })}
+
+
+
+
+
+
+ >
+ ) : (
+ /* No recents yet — show suggestions full-width */
+ <>
+
+ Suggested for you
+
+
+
+
+ >
+ )}
+
+
+
+ )
+}
diff --git a/apps/web/components/document-cards/file-preview.tsx b/apps/web/components/document-cards/file-preview.tsx
index cd10bcd83..6013a2030 100644
--- a/apps/web/components/document-cards/file-preview.tsx
+++ b/apps/web/components/document-cards/file-preview.tsx
@@ -96,14 +96,14 @@ export const FilePreview = memo(function FilePreview({
className="w-4 h-4"
/>
{extension}
{document.content && (
-
+
{document.content}
)}
diff --git a/apps/web/components/document-cards/google-docs-preview.tsx b/apps/web/components/document-cards/google-docs-preview.tsx
index 2516a3660..393bc2ec8 100644
--- a/apps/web/components/document-cards/google-docs-preview.tsx
+++ b/apps/web/components/document-cards/google-docs-preview.tsx
@@ -24,20 +24,20 @@ export function GoogleDocsPreview({
url={document.url}
className="w-4 h-4"
/>
-
+
{label}
{document.summary ? (
-
+
{document.summary}
) : document.content ? (
-
+
{document.content}
) : (
-
+
No summary available
)}
diff --git a/apps/web/components/document-cards/mcp-preview.tsx b/apps/web/components/document-cards/mcp-preview.tsx
index a8a792fbf..f241353d1 100644
--- a/apps/web/components/document-cards/mcp-preview.tsx
+++ b/apps/web/components/document-cards/mcp-preview.tsx
@@ -16,7 +16,7 @@ export function McpPreview({ document }: { document: DocumentWithMemories }) {
@@ -26,12 +26,12 @@ export function McpPreview({ document }: { document: DocumentWithMemories }) {
{document.title && (
-
+
{document.title}
)}
{document.content && (
-
+
{document.content}
)}
diff --git a/apps/web/components/document-cards/note-preview.tsx b/apps/web/components/document-cards/note-preview.tsx
index c05645f3a..ef16948b9 100644
--- a/apps/web/components/document-cards/note-preview.tsx
+++ b/apps/web/components/document-cards/note-preview.tsx
@@ -14,7 +14,7 @@ export function NotePreview({ document }: { document: DocumentWithMemories }) {
@@ -23,14 +23,14 @@ export function NotePreview({ document }: { document: DocumentWithMemories }) {
{document.title}
)}
{document.summary && (
-
+
{document.summary}
)}
diff --git a/apps/web/components/document-cards/notion-preview.tsx b/apps/web/components/document-cards/notion-preview.tsx
index d3f4425b9..f514e4aa6 100644
--- a/apps/web/components/document-cards/notion-preview.tsx
+++ b/apps/web/components/document-cards/notion-preview.tsx
@@ -65,7 +65,7 @@ export function NotionPreview({
Notion
@@ -85,7 +85,7 @@ export function NotionPreview({
{document.title}
@@ -128,6 +128,8 @@ export function NotionPreview({
-
+
{block.text}
@@ -168,7 +170,7 @@ export function NotionPreview({
return (
{block.text}
@@ -176,7 +178,7 @@ export function NotionPreview({
})}
) : document.summary ? (
-
+
{document.summary}
) : null}
@@ -189,7 +191,7 @@ export function NotionPreview({
)}
-
+
{new Date(document.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
diff --git a/apps/web/components/document-cards/tweet-preview.tsx b/apps/web/components/document-cards/tweet-preview.tsx
index d4a0f4a37..ba3b8d0f2 100644
--- a/apps/web/components/document-cards/tweet-preview.tsx
+++ b/apps/web/components/document-cards/tweet-preview.tsx
@@ -63,7 +63,7 @@ function CustomTweetHeader({
@@ -73,7 +73,7 @@ function CustomTweetHeader({
diff --git a/apps/web/components/document-cards/youtube-preview.tsx b/apps/web/components/document-cards/youtube-preview.tsx
index 5f53d490b..008ce35d5 100644
--- a/apps/web/components/document-cards/youtube-preview.tsx
+++ b/apps/web/components/document-cards/youtube-preview.tsx
@@ -21,12 +21,12 @@ export const YoutubePreview = memo(function YoutubePreview({
return (
{document.title && (
-
+
{document.title}
)}
{document.content && (
-
+
{document.content}
)}
diff --git a/apps/web/components/document-icon.tsx b/apps/web/components/document-icon.tsx
index 286db1c3d..244744b82 100644
--- a/apps/web/components/document-icon.tsx
+++ b/apps/web/components/document-icon.tsx
@@ -128,9 +128,15 @@ function TextDocumentIcon({ className }: { className?: string }) {
function XIcon({ className }: { className?: string }) {
return (
-
- 𝕏
-
+
+ X (Twitter)
+
+
)
}
diff --git a/apps/web/components/document-modal/content/index.tsx b/apps/web/components/document-modal/content/index.tsx
index 39c5a2f0f..4cfa79503 100644
--- a/apps/web/components/document-modal/content/index.tsx
+++ b/apps/web/components/document-modal/content/index.tsx
@@ -87,6 +87,7 @@ export function DocumentContent({
)
diff --git a/apps/web/components/document-modal/content/tweet.tsx b/apps/web/components/document-modal/content/tweet.tsx
index 61867af99..87d170c3d 100644
--- a/apps/web/components/document-modal/content/tweet.tsx
+++ b/apps/web/components/document-modal/content/tweet.tsx
@@ -7,9 +7,14 @@ import { ExternalLinkIcon } from "lucide-react"
interface TweetContentProps {
url?: string | null
tweetMetadata?: unknown
+ content?: string | null
}
-export function TweetContent({ url, tweetMetadata }: TweetContentProps) {
+export function TweetContent({
+ url,
+ tweetMetadata,
+ content,
+}: TweetContentProps) {
if (tweetMetadata) {
return (
@@ -18,6 +23,27 @@ export function TweetContent({ url, tweetMetadata }: TweetContentProps) {
)
}
+ if (content) {
+ return (
+
+ )
+ }
+
return (
Tweet preview unavailable
diff --git a/apps/web/components/document-modal/summary.tsx b/apps/web/components/document-modal/summary.tsx
index f0618fa18..4421d33d4 100644
--- a/apps/web/components/document-modal/summary.tsx
+++ b/apps/web/components/document-modal/summary.tsx
@@ -37,7 +37,8 @@ export function Summary({
Summary
-
+
+
powered by supermemory
diff --git a/apps/web/components/documents-command-palette.tsx b/apps/web/components/documents-command-palette.tsx
index ec001aaeb..6750a0328 100644
--- a/apps/web/components/documents-command-palette.tsx
+++ b/apps/web/components/documents-command-palette.tsx
@@ -35,7 +35,6 @@ interface DocumentsCommandPaletteProps {
open: boolean
onOpenChange: (open: boolean) => void
projectId: string
- novaContainerTags?: string[]
onOpenDocument: (document: DocumentWithMemories) => void
onAddMemory?: () => void
onOpenIntegrations?: () => void
@@ -46,7 +45,6 @@ export function DocumentsCommandPalette({
open,
onOpenChange,
projectId,
- novaContainerTags,
onOpenDocument,
onAddMemory,
onOpenIntegrations,
@@ -159,8 +157,7 @@ export function DocumentsCommandPalette({
body: {
q: search.trim(),
limit: 10,
- containerTags:
- novaContainerTags ?? (projectId ? [projectId] : undefined),
+ containerTags: projectId ? [projectId] : undefined,
includeSummary: true,
},
signal: controller.signal,
@@ -178,7 +175,7 @@ export function DocumentsCommandPalette({
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current)
}
- }, [search, projectId, novaContainerTags])
+ }, [search, projectId])
// Build the item list
const hasQuery = search.trim().length > 0
@@ -202,7 +199,7 @@ export function DocumentsCommandPalette({
// Reset selection on items change
useEffect(() => {
setSelectedIndex(0)
- }, [search, searchResults.length])
+ }, [])
// Scroll selected into view
useEffect(() => {
diff --git a/apps/web/components/ensure-workspace.tsx b/apps/web/components/ensure-workspace.tsx
index 370d5ca0a..6ac37b957 100644
--- a/apps/web/components/ensure-workspace.tsx
+++ b/apps/web/components/ensure-workspace.tsx
@@ -20,7 +20,7 @@ export function EnsureWorkspace({ children }: { children: React.ReactNode }) {
if (organizations === null) return
if (organizations.length > 0) return
if (pathname.startsWith("/onboarding")) return
- router.replace("/onboarding/welcome?step=input")
+ router.replace("/onboarding")
}, [session, organizations, isRestoring, pathname, router])
return children
diff --git a/apps/web/components/graph-layout-view.tsx b/apps/web/components/graph-layout-view.tsx
index b74eb4810..0a21c357d 100644
--- a/apps/web/components/graph-layout-view.tsx
+++ b/apps/web/components/graph-layout-view.tsx
@@ -12,11 +12,7 @@ import { dmSansClassName } from "@/lib/fonts"
import { ShareModal } from "./share-modal"
import { shareParam } from "@/lib/search-params"
-interface GraphLayoutViewProps {
- isChatOpen: boolean
-}
-
-export const GraphLayoutView = memo
(({ isChatOpen }) => {
+export const GraphLayoutView = memo(function GraphLayoutView() {
const { effectiveContainerTags } = useProject()
const { documentIds: allHighlightDocumentIds } = useGraphHighlights()
const [isShareModalOpen, setIsShareModalOpen] = useQueryState(
@@ -34,14 +30,14 @@ export const GraphLayoutView = memo(({ isChatOpen }) => {
}, [setIsShareModalOpen])
return (
-
+
{/* Full-width graph */}
diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx
index a66626b07..2e45cac1f 100644
--- a/apps/web/components/header.tsx
+++ b/apps/web/components/header.tsx
@@ -3,7 +3,6 @@
import { Logo } from "@ui/assets/Logo"
import { useAuth } from "@lib/auth-context"
import {
- LayoutGridIcon,
Plus,
SearchIcon,
Settings,
@@ -13,11 +12,12 @@ import {
ExternalLink,
MenuIcon,
MessageCircleIcon,
+ LifeBuoy,
+ LayoutGrid,
} from "lucide-react"
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
-import { Tabs, TabsList, TabsTrigger } from "@ui/components/tabs"
import { GraphIcon, IntegrationsIcon } from "@/components/integration-icons"
import {
DropdownMenu,
@@ -26,6 +26,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@ui/components/dropdown-menu"
+import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip"
import { useProject } from "@/stores"
import { useRouter } from "next/navigation"
import Link from "next/link"
@@ -34,17 +35,16 @@ import { useIsMobile } from "@hooks/use-mobile"
import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
import { UserProfileMenu } from "@/components/user-profile-menu"
import { FeedbackModal } from "./feedback-modal"
-import { useViewMode, type ViewMode } from "@/lib/view-mode-context"
+import { useViewMode } from "@/lib/view-mode-context"
import { useQueryState } from "nuqs"
import { feedbackParam } from "@/lib/search-params"
interface HeaderProps {
onAddMemory?: () => void
- onOpenChat?: () => void
onOpenSearch?: () => void
}
-export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
+export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
const { user, isRestoring } = useAuth()
const { selectedProjects, setSelectedProjects } = useProject()
const router = useRouter()
@@ -65,21 +65,21 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
""
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
return (
-
-
+
+
-
+
{!isMobile && userName && (
-
-
+
+
{userName}
-
+
supermemory
@@ -129,7 +129,6 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
-
{!isMobile && (
{!isMobile && (
-
- setViewMode(v === "grid" ? "list" : (v as ViewMode))
- }
- >
-
-
-
- Grid
-
-
-
- Graph
-
-
-
- Integrations
-
-
-
+
+
+
+ void setViewMode("dashboard")}
+ className={cn(
+ "flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-full border transition-colors",
+ viewMode === "dashboard"
+ ? "border-[#2261CA33] bg-[#00173C] text-white"
+ : "border-[#161F2C] bg-muted text-muted-foreground hover:bg-white/5",
+ dmSansClassName(),
+ )}
+ >
+
+
+
+
+ Home
+
+
+
+ {(
+ [
+ {
+ mode: "integrations" as const,
+ label: "Integrations",
+ icon: IntegrationsIcon,
+ },
+ { mode: "graph" as const, label: "Graph", icon: GraphIcon },
+ {
+ mode: "list" as const,
+ label: "Memories",
+ icon: LayoutGrid,
+ },
+ ] as const
+ ).map(({ mode, label, icon: Icon }) => (
+ void setViewMode(mode)}
+ className={cn(
+ "inline-flex h-[calc(100%-1px)] min-h-0 cursor-pointer items-center justify-center gap-1 rounded-full border border-transparent px-2.5 text-xs font-medium whitespace-nowrap transition-colors sm:gap-1.5 sm:px-3 sm:text-sm",
+ viewMode === mode
+ ? "border-[#2261CA33] bg-[#00173C] text-white"
+ : "text-foreground hover:bg-white/5",
+ dmSansClassName(),
+ )}
+ >
+
+ {label}
+
+ ))}
+
+
+
+ void setViewMode("chat")}
+ className={cn(
+ "flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-full border transition-colors",
+ viewMode === "chat"
+ ? "border-[#2261CA33] bg-[#00173C] text-white"
+ : "border-[#161F2C] bg-muted text-muted-foreground hover:bg-white/5",
+ dmSansClassName(),
+ )}
+ >
+
+
+
+
+ Chat
+
+
+
)}
-
+
{isMobile ? (
<>
Add memory
+
void setViewMode("dashboard")}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+
+ Home
+
setViewMode("integrations")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
@@ -225,7 +278,28 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
Integrations
void setViewMode("graph")}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+
+ Graph
+
+
onOpenSearch?.()}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+
+ Search
+
+
void setViewMode("list")}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+
+ Memories
+
+
void setViewMode("chat")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
>
@@ -236,7 +310,7 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
onClick={handleFeedback}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
>
-
+
Feedback
) : (
<>
-
-
-
- C
-
-
-
-
-
-
+
+
- Command Key
-
-
- K
-
-
-
-
-
- Feedback
-
-
+
+
Add memory
+
+
+
+ Add memory (C)
+
+
+
+
+
+
+
+
+
+ Search (⌘K)
+
+
>
)}
-
+
-
-
- Loading highlights...
-
+
+
+
)
}
diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx
index a23040237..4ccc45602 100644
--- a/apps/web/components/integrations-view.tsx
+++ b/apps/web/components/integrations-view.tsx
@@ -25,6 +25,7 @@ import {
type IntegrationParamValue,
} from "@/lib/search-params"
import Image from "next/image"
+import { IntegrationGridCard } from "@/components/integrations/integration-grid-card"
type CardId =
| "mcp"
@@ -238,39 +239,14 @@ export function IntegrationsView() {
{cards.map((card) => (
-
setSelectedCard(card.id)}
- className={cn(
- "bg-[#080B0F] relative rounded-xl p-4 pt-14",
- "border border-[#0D121A]",
- "hover:border-[#3374FF]/50",
- "transition-all duration-300 cursor-pointer text-left w-full",
- "hover:bg-[url('/onboarding/bg-gradient-1.png')] hover:bg-[length:200%_auto] hover:bg-[center_top_1rem] hover:bg-no-repeat",
- "group",
- )}
- >
- {card.pro && (
-
- PRO
-
- )}
-
- {card.icon}
-
-
-
{card.title}
-
- {card.description}
-
-
-
+ />
))}
diff --git a/apps/web/components/integrations/integration-grid-card.tsx b/apps/web/components/integrations/integration-grid-card.tsx
new file mode 100644
index 000000000..cfaf7da0f
--- /dev/null
+++ b/apps/web/components/integrations/integration-grid-card.tsx
@@ -0,0 +1,54 @@
+"use client"
+
+import type { ReactNode } from "react"
+import { cn } from "@lib/utils"
+import { dmSansClassName } from "@/lib/fonts"
+
+export function IntegrationGridCard({
+ title,
+ description,
+ icon,
+ pro,
+ onClick,
+}: {
+ title: string
+ description: string
+ icon: ReactNode
+ pro?: boolean
+ onClick: () => void
+}) {
+ return (
+
+ {pro ? (
+
+ PRO
+
+ ) : null}
+
+ {icon}
+
+
+
{title}
+
+ {description}
+
+
+
+ )
+}
diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx
index 66ad36e37..0c8a2be39 100644
--- a/apps/web/components/memories-grid.tsx
+++ b/apps/web/components/memories-grid.tsx
@@ -26,8 +26,7 @@ import { McpPreview } from "./document-cards/mcp-preview"
import { NotionPreview } from "./document-cards/notion-preview"
import { getFaviconUrl } from "@/lib/url-helpers"
import { QuickNoteCard } from "./quick-note-card"
-import { HighlightsCard, type HighlightItem } from "./highlights-card"
-import { GraphCard } from "./memory-graph"
+import type { HighlightItem } from "./highlights-card"
import { Button } from "@ui/components/button"
import {
categoriesParam,
@@ -44,7 +43,16 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@ui/components/alert-dialog"
-import { CheckIcon, Loader, Trash2Icon, XIcon } from "lucide-react"
+import {
+ AlignLeft,
+ CheckIcon,
+ LayoutGrid,
+ Loader,
+ Trash2Icon,
+ XIcon,
+} from "lucide-react"
+import { useProcessingDocuments } from "@/hooks/use-processing-documents"
+import { TimelineView } from "./timeline-view"
// Document category type
type DocumentCategory =
@@ -171,7 +179,9 @@ function MemoriesGridLoading() {
}
// Discriminated union for masonry items
-type MasonryItem = { type: "document"; id: string; data: DocumentWithMemories }
+type MasonryItem =
+ | { type: "document"; id: string; data: DocumentWithMemories }
+ | { type: "quick-note"; id: "quick-note" }
interface QuickNoteProps {
onSave: (content: string) => void
@@ -226,8 +236,18 @@ export function MemoriesGrid({
emptyStateProps,
}: MemoriesGridProps) {
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
+ const [localViewMode, setLocalViewMode] = useState<"grid" | "timeline">(
+ () => {
+ if (typeof window === "undefined") return "grid"
+ return (
+ (localStorage.getItem("memories-view-mode") as "grid" | "timeline") ??
+ "grid"
+ )
+ },
+ )
const { user, isSessionPending } = useAuth()
const { effectiveContainerTags } = useProject()
+ const processingStatusMap = useProcessingDocuments()
const isMobile = useIsMobile()
const [selectedCategories, setSelectedCategories] = useQueryState(
"categories",
@@ -309,6 +329,11 @@ export function MemoriesGrid({
enabled: !!user,
})
+ const handleSetViewMode = useCallback((mode: "grid" | "timeline") => {
+ setLocalViewMode(mode)
+ localStorage.setItem("memories-view-mode", mode)
+ }, [])
+
const handleCategoryToggle = useCallback(
(category: DocumentCategory) => {
setSelectedCategories((prev) => {
@@ -334,23 +359,27 @@ export function MemoriesGrid({
}, [data])
const hasQuickNote = !!quickNoteProps
- const hasHighlights = !!highlightsProps
+ const _hasHighlights = !!highlightsProps
const masonryItems: MasonryItem[] = useMemo(() => {
const items: MasonryItem[] = []
+ if (!isMobile && hasQuickNote) {
+ items.push({ type: "quick-note", id: "quick-note" })
+ }
+
for (const doc of documents) {
items.push({ type: "document", id: doc.id, data: doc })
}
return items
- }, [documents])
+ }, [documents, isMobile, hasQuickNote])
// Stable key for Masonry based on document IDs, not item values
const masonryKey = useMemo(() => {
const docIds = documents.map((d) => d.id).join(",")
- return `masonry-${documents.length}-${docIds}-${isChatOpen}`
- }, [documents, isChatOpen])
+ return `masonry-${documents.length}-${docIds}-${isChatOpen}-${hasQuickNote}`
+ }, [documents, isChatOpen, hasQuickNote])
const isLoadingMore = isFetchingNextPage
@@ -402,6 +431,26 @@ export function MemoriesGrid({
onBulkDelete?.()
}, [onBulkDelete])
+ // All mutable values the render function needs — kept in a ref so the
+ // function identity never changes (masonic uses render as a React component
+ // type, so a new reference unmounts every item and kills textarea focus).
+ const renderRef = useRef({
+ quickNoteProps,
+ handleCardClick,
+ isSelectionMode,
+ selectedDocumentIds,
+ onToggleSelection,
+ processingStatusMap,
+ })
+ renderRef.current = {
+ quickNoteProps,
+ handleCardClick,
+ isSelectionMode,
+ selectedDocumentIds,
+ onToggleSelection,
+ processingStatusMap,
+ }
+
const renderMasonryItem = useCallback(
({
index,
@@ -412,6 +461,16 @@ export function MemoriesGrid({
data: MasonryItem
width: number
}) => {
+ const r = renderRef.current
+
+ if (data.type === "quick-note") {
+ return r.quickNoteProps ? (
+
+
+
+ ) : null
+ }
+
if (data.type === "document") {
const doc = data.data
return (
@@ -420,14 +479,17 @@ export function MemoriesGrid({
index={index}
data={doc}
width={width}
- onClick={handleCardClick}
- isSelectionMode={isSelectionMode}
- isSelected={doc.id ? selectedDocumentIds.has(doc.id) : false}
+ onClick={r.handleCardClick}
+ isSelectionMode={r.isSelectionMode}
+ isSelected={doc.id ? r.selectedDocumentIds.has(doc.id) : false}
onToggleSelection={
- doc.id && onToggleSelection
- ? () => onToggleSelection(doc.id as string)
+ doc.id && r.onToggleSelection
+ ? () => r.onToggleSelection?.(doc.id as string)
: undefined
}
+ processingStatus={
+ doc.id ? r.processingStatusMap.get(doc.id) : undefined
+ }
/>
)
@@ -435,7 +497,8 @@ export function MemoriesGrid({
return null
},
- [handleCardClick, isSelectionMode, selectedDocumentIds, onToggleSelection],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
)
if (isSessionPending) {
@@ -496,6 +559,35 @@ export function MemoriesGrid({
))}
+ {/* View mode toggle */}
+
+
handleSetViewMode("grid")}
+ >
+
+
+
handleSetViewMode("timeline")}
+ >
+
+
+
{isSelectionMode && (
<>
) : (
- {!isMobile && (hasQuickNote || hasHighlights) && (
-
- {hasQuickNote && quickNoteProps && (
-
-
-
- )}
- {hasHighlights && highlightsProps && (
-
-
-
- )}
-
-
-
-
+ {localViewMode === "timeline" ? (
+
+ ) : (
+
)}
-
-
- {isLoadingMore && (
+
+ {isLoadingMore && localViewMode === "grid" && (
@@ -676,7 +757,7 @@ function DocumentUrlDisplay({ url }: { url: string }) {
{isLoading ? "YouTube" : channelName || "YouTube"}
@@ -688,7 +769,7 @@ function DocumentUrlDisplay({ url }: { url: string }) {
{getAbsoluteUrl(url)}
@@ -701,6 +782,74 @@ function isTemporaryId(id: string | null | undefined): boolean {
return id.startsWith("temp-") || id.startsWith("temp-file-")
}
+const PROCESSING_WORDS = [
+ "Reading",
+ "Absorbing",
+ "Scanning",
+ "Thinking",
+ "Connecting",
+ "Pondering",
+ "Synthesizing",
+ "Reflecting",
+ "Understanding",
+ "Organizing",
+ "Memorizing",
+ "Filing",
+ "Saving",
+ "Learning",
+ "Cataloguing",
+ "Weaving",
+]
+
+function ProcessingBadge() {
+ const [wordIndex, setWordIndex] = useState(() =>
+ Math.floor(Math.random() * PROCESSING_WORDS.length),
+ )
+
+ useEffect(() => {
+ const id = setInterval(() => {
+ setWordIndex((i) => (i + 1) % PROCESSING_WORDS.length)
+ }, 1800)
+ return () => clearInterval(id)
+ }, [])
+
+ return (
+
+
+
+
+
+
+ {PROCESSING_WORDS[wordIndex]}
+
+
+ )
+}
+
+function DoneBadge() {
+ return (
+
+
+
+ Done
+
+
+ )
+}
+
const DocumentCard = memo(
({
index: _index,
@@ -710,6 +859,7 @@ const DocumentCard = memo(
isSelectionMode = false,
isSelected = false,
onToggleSelection,
+ processingStatus,
}: {
index: number
data: DocumentWithMemories
@@ -718,12 +868,26 @@ const DocumentCard = memo(
isSelectionMode?: boolean
isSelected?: boolean
onToggleSelection?: () => void
+ processingStatus?: string
}) => {
const canSelect =
!isTemporaryId(document.id) && !isTemporaryId(document.customId)
const [rotation, setRotation] = useState({ rotateX: 0, rotateY: 0 })
const cardRef = useRef
(null)
const [ogData, setOgData] = useState(null)
+ const [showDone, setShowDone] = useState(false)
+ const prevStatusRef = useRef(processingStatus)
+
+ useEffect(() => {
+ const prev = prevStatusRef.current
+ prevStatusRef.current = processingStatus
+ // Show the "done" checkmark briefly when the card leaves the processing map
+ if (prev && !processingStatus) {
+ setShowDone(true)
+ const id = setTimeout(() => setShowDone(false), 2000)
+ return () => clearTimeout(id)
+ }
+ }, [processingStatus])
const ogImage = (document as DocumentWithMemories & { ogImage?: string })
.ogImage
@@ -847,15 +1011,16 @@ const DocumentCard = memo(
) && (
{document.url &&
- !document.url.includes("x.com") &&
- !document.url.includes("twitter.com") &&
- !document.url.includes("files.supermemory.ai") && (
+ !document.url.includes("files.supermemory.ai") &&
+ (document.title ||
+ (!document.url.includes("x.com") &&
+ !document.url.includes("twitter.com"))) && (
{document.title || ogData?.title || "Untitled Document"}
@@ -878,16 +1043,22 @@ const DocumentCard = memo(
0
+ processingStatus ||
+ showDone ||
+ document.memoryEntries.length > 0
? "justify-between"
: "justify-end",
)}
>
- {document.memoryEntries.length > 0 && (
+ {processingStatus ? (
+
+ ) : showDone ? (
+
+ ) : document.memoryEntries.length > 0 ? (
{document.memoryEntries.length}
- )}
+ ) : null}
{new Date(document.createdAt).toLocaleDateString("en-US", {
@@ -939,10 +1110,7 @@ function ContentPreview({
return
}
- if (
- document.url?.includes("x.com/") &&
- document.metadata?.sm_internal_twitter_metadata
- ) {
+ if (document.metadata?.sm_internal_twitter_metadata) {
return (
+ }
+
if (document.source === "mcp") {
return
}
diff --git a/apps/web/components/memory-graph/graph-card.tsx b/apps/web/components/memory-graph/graph-card.tsx
index d6837521b..807f594b1 100644
--- a/apps/web/components/memory-graph/graph-card.tsx
+++ b/apps/web/components/memory-graph/graph-card.tsx
@@ -23,7 +23,7 @@ function seededRandom(seed: number) {
}
}
-function StaticGraphPreview({
+export function StaticGraphPreview({
documentCount,
memoryCount,
width,
@@ -70,10 +70,10 @@ function StaticGraphPreview({
let b = Math.floor(rand() * nodes.length)
if (b === a) b = (a + 1) % nodes.length
result.push({
- x1: nodes[a]!.x,
- y1: nodes[a]!.y,
- x2: nodes[b]!.x,
- y2: nodes[b]!.y,
+ x1: nodes[a]?.x,
+ y1: nodes[a]?.y,
+ x2: nodes[b]?.x,
+ y2: nodes[b]?.y,
})
}
return result
diff --git a/apps/web/components/onboarding/setup/integrations-step.tsx b/apps/web/components/onboarding/setup/integrations-step.tsx
index d7f16e517..40a856a23 100644
--- a/apps/web/components/onboarding/setup/integrations-step.tsx
+++ b/apps/web/components/onboarding/setup/integrations-step.tsx
@@ -171,14 +171,7 @@ export function IntegrationsStep() {
})}
-
-
router.push("/onboarding/setup?step=relatable")}
- >
- ← Back
-
+
([])
-
- const handleContinueOrSkip = () => {
- const selectedTexts = selectedOptions.map(
- (idx) => relatableOptions[idx]?.text || "",
- )
- analytics.onboardingRelatableSelected({ options: selectedTexts })
- router.push("/onboarding/setup?step=integrations")
- }
-
- return (
-
-
- Which of these sound most relatable?
-
-
-
- {relatableOptions.map((option, index) => (
-
-
{
- setSelectedOptions((prev) =>
- prev.includes(index)
- ? prev.filter((i) => i !== index)
- : [...prev, index],
- )
- }}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault()
- setSelectedOptions((prev) =>
- prev.includes(index)
- ? prev.filter((i) => i !== index)
- : [...prev, index],
- )
- }
- }}
- type="button"
- >
-
- {selectedOptions.includes(index) && (
-
- )}
-
-
-
- {option.emoji}
-
-
- {option.text}
-
-
-
-
- ))}
-
-
-
-
- {selectedOptions.length === 0
- ? "Skip for now →"
- : "Remember this →"}
-
-
-
-
- )
-}
diff --git a/apps/web/components/onboarding/welcome/continue-step.tsx b/apps/web/components/onboarding/welcome/continue-step.tsx
index 15bddeb50..1b8fe91b1 100644
--- a/apps/web/components/onboarding/welcome/continue-step.tsx
+++ b/apps/web/components/onboarding/welcome/continue-step.tsx
@@ -42,11 +42,11 @@ export function OnboardingContentStep({
const router = useRouter()
const handleContinue = () => {
- router.push("/onboarding/welcome?step=features")
+ router.push("/old/onboarding/welcome?step=features")
}
const handleAddMemories = () => {
- router.push("/onboarding/welcome?step=memories")
+ router.push("/old/onboarding/welcome?step=memories")
}
const isContinue = currentView === "continue"
diff --git a/apps/web/components/onboarding/welcome/input-step.tsx b/apps/web/components/onboarding/welcome/input-step.tsx
index 7cb7fdb33..f89156c85 100644
--- a/apps/web/components/onboarding/welcome/input-step.tsx
+++ b/apps/web/components/onboarding/welcome/input-step.tsx
@@ -23,28 +23,10 @@ export function InputStep({
isSubmitting && "pointer-events-none",
)}
style={{ gap: "24px" }}
- initial={{
- opacity: 0,
- y: 10,
- }}
- animate={{
- opacity: 1,
- y: 0,
- }}
- exit={{
- opacity: 0,
- y: -10,
- transition: {
- duration: 0.5,
- ease: "easeOut",
- bounce: 0,
- },
- }}
- transition={{
- duration: 0.8,
- ease: "easeOut",
- delay: 1,
- }}
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ exit={{ opacity: 0, transition: { duration: 0.3, ease: "easeOut" } }}
+ transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
layout
>