diff --git a/apps/browser-extension/entrypoints/content/chatgpt.ts b/apps/browser-extension/entrypoints/content/chatgpt.ts index 51a04736..7c3d28aa 100644 --- a/apps/browser-extension/entrypoints/content/chatgpt.ts +++ b/apps/browser-extension/entrypoints/content/chatgpt.ts @@ -159,7 +159,7 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) { if (response?.success && response?.data) { const promptElement = document.getElementById("prompt-textarea") if (promptElement) { - promptElement.dataset.supermemories = `
Supermemories of user (only for the reference): ${response.data}
` + promptElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` console.log( "Prompt element dataset:", promptElement.dataset.supermemories, @@ -471,7 +471,7 @@ function updateChatGPTIconFeedback( const promptElement = document.getElementById("prompt-textarea") if (promptElement) { - promptElement.dataset.supermemories = `
Supermemories of user (only for the reference): ${updatedMemories}
` + promptElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${updatedMemories}` } content @@ -647,7 +647,7 @@ function setupChatGPTPromptCapture() { promptTextarea && !promptContent.includes("Supermemories of user") ) { - promptTextarea.innerHTML = `${promptTextarea.innerHTML} ${storedMemories}` + promptTextarea.appendChild(document.createTextNode(storedMemories)) promptContent = promptTextarea.textContent || "" } diff --git a/apps/browser-extension/entrypoints/content/claude.ts b/apps/browser-extension/entrypoints/content/claude.ts index 01016a40..d124c84a 100644 --- a/apps/browser-extension/entrypoints/content/claude.ts +++ b/apps/browser-extension/entrypoints/content/claude.ts @@ -230,7 +230,7 @@ async function getRelatedMemoriesForClaude(actionSource: string) { ) as HTMLElement if (textareaElement) { - textareaElement.dataset.supermemories = `
Supermemories of user (only for the reference): ${response.data}
` + textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` console.log( "Text element dataset:", textareaElement.dataset.supermemories, @@ -442,7 +442,7 @@ function updateClaudeIconFeedback( 'div[contenteditable="true"]', ) as HTMLElement if (textareaElement) { - textareaElement.dataset.supermemories = `
Supermemories of user (only for the reference): ${updatedMemories}
` + textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${updatedMemories}` } content @@ -520,7 +520,7 @@ function setupClaudePromptCapture() { contentEditableDiv && !promptContent.includes("Supermemories of user") ) { - contentEditableDiv.innerHTML = `${contentEditableDiv.innerHTML} ${storedMemories}` + contentEditableDiv.appendChild(document.createTextNode(storedMemories)) promptContent = contentEditableDiv.textContent || contentEditableDiv.innerText || "" } diff --git a/apps/browser-extension/entrypoints/content/t3.ts b/apps/browser-extension/entrypoints/content/t3.ts index 4d284a35..c7bdb09a 100644 --- a/apps/browser-extension/entrypoints/content/t3.ts +++ b/apps/browser-extension/entrypoints/content/t3.ts @@ -238,13 +238,7 @@ async function getRelatedMemoriesForT3(actionSource: string) { } if (textareaElement) { - if (textareaElement.tagName === "TEXTAREA") { - ;(textareaElement as HTMLTextAreaElement).dataset.supermemories = - `
Supermemories of user (only for the reference): ${response.data}
` - } else { - ;(textareaElement as HTMLElement).dataset.supermemories = - `
Supermemories of user (only for the reference): ${response.data}
` - } + textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` iconElement.dataset.memoriesData = response.data @@ -450,7 +444,7 @@ function updateT3IconFeedback( (document.querySelector("textarea") as HTMLTextAreaElement) || (document.querySelector('div[contenteditable="true"]') as HTMLElement) if (textareaElement) { - textareaElement.dataset.supermemories = `
Supermemories of user (only for the reference): ${updatedMemories}
` + textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${updatedMemories}` } content @@ -537,7 +531,7 @@ function setupT3PromptCapture() { `${promptContent} ${storedMemories}` promptContent = (textareaElement as HTMLTextAreaElement).value } else { - textareaElement.innerHTML = `${textareaElement.innerHTML} ${storedMemories}` + textareaElement.appendChild(document.createTextNode(storedMemories)) promptContent = textareaElement.textContent || textareaElement.innerText || "" } diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 0f7b54c0..1e29f968 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -184,6 +184,7 @@ "integrations/agent-framework", "integrations/mastra", "integrations/voltagent", + "integrations/convex", "integrations/langchain", "integrations/crewai", "integrations/agno", diff --git a/apps/docs/integrations/convex.mdx b/apps/docs/integrations/convex.mdx new file mode 100644 index 00000000..37130a84 --- /dev/null +++ b/apps/docs/integrations/convex.mdx @@ -0,0 +1,208 @@ +--- +title: "Convex" +sidebarTitle: "Convex" +description: "Add persistent memory to Convex apps with Supermemory" +icon: "database" +--- + +Convex apps don't have built-in memory for AI. Supermemory fixes that. You get a memory layer that stores conversations, builds user profiles, and gives your AI context about who it's talking to. + +## What you can do + +- Store user interactions and retrieve them in future sessions +- Build automatic user profiles from conversations +- Search memories to give your AI relevant context +- Keep everything in your Convex database for full visibility + +## Setup + +Install the packages: + +```bash +npm install supermemory convex +``` + +For the AI chat example, also install the AI SDK packages: + +```bash +npm install @supermemory/tools @ai-sdk/openai ai +``` + +Set up your environment variable in Convex: + +```bash +npx convex env set SUPERMEMORY_API_KEY your-supermemory-api-key +``` + +Get your Supermemory API key from [console.supermemory.ai](https://console.supermemory.ai). + +## Basic integration + +Create simple helper functions for each Supermemory operation: + +```typescript +// convex/memory.ts +import { action } from "./_generated/server"; +import { v } from "convex/values"; +import Supermemory from "supermemory"; + +const memory = new Supermemory({ apiKey: process.env.SUPERMEMORY_API_KEY }); + +// Get user profile and relevant memories +export const getProfile = action({ + args: { userId: v.string(), query: v.optional(v.string()) }, + handler: async (ctx, { userId, query }) => { + return await memory.profile({ + containerTag: userId, + q: query, + }); + }, +}); + +// Add a memory +export const addMemory = action({ + args: { userId: v.string(), content: v.string() }, + handler: async (ctx, { userId, content }) => { + return await memory.add({ + content, + containerTag: userId, + }); + }, +}); + +// Search memories +export const searchMemories = action({ + args: { userId: v.string(), query: v.string(), limit: v.optional(v.number()) }, + handler: async (ctx, { userId, query, limit }) => { + return await memory.search.memories({ + q: query, + containerTag: userId, + searchMode: "hybrid", + limit: limit ?? 10, + }); + }, +}); +``` + +--- + +## Example: AI chat with memory + +A chat endpoint using the Supermemory AI SDK middleware. It automatically injects context and saves memories. + +```typescript +// convex/chat.ts +import { action } from "./_generated/server"; +import { v } from "convex/values"; +import { generateText } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { withSupermemory } from "@supermemory/tools/ai-sdk"; + +export const chat = action({ + args: { userId: v.string(), message: v.string() }, + handler: async (ctx, { userId, message }) => { + // Wrap the model - automatically injects context and saves memories + const model = withSupermemory(openai("gpt-4o-mini"), { + containerTag: userId, + customId: `convex-chat-${userId}`, + mode: "full", + addMemory: "always", + }); + + const { text } = await generateText({ + model, + system: "You are a helpful assistant.", + prompt: message, + }); + + return text; + }, +}); +``` + +--- + +## Storing memories in Convex tables + +Keep a local copy of memories in your Convex database for full visibility: + +```typescript +// convex/schema.ts +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +export default defineSchema({ + memories: defineTable({ + userId: v.string(), + content: v.string(), + createdAt: v.number(), + }).index("by_user", ["userId"]), +}); +``` + +```typescript +// convex/memory.ts +import { action, mutation, query } from "./_generated/server"; +import { api } from "./_generated/api"; +import { v } from "convex/values"; +import Supermemory from "supermemory"; + +const memory = new Supermemory({ apiKey: process.env.SUPERMEMORY_API_KEY }); + +// Store in Convex +export const storeMemory = mutation({ + args: { userId: v.string(), content: v.string() }, + handler: async (ctx, { userId, content }) => { + return await ctx.db.insert("memories", { + userId, + content, + createdAt: Date.now(), + }); + }, +}); + +// Add memory to both Supermemory and Convex +export const addMemory = action({ + args: { userId: v.string(), content: v.string() }, + handler: async (ctx, { userId, content }) => { + // Add to Supermemory + await memory.add({ content, containerTag: userId }); + + // Store in Convex + // Note: in production, handle partial failures — if the Convex mutation + // fails after the Supermemory write succeeds, the two stores will be out of sync. + await ctx.runMutation(api.memory.storeMemory, { userId, content }); + }, +}); + +// List memories from Convex +export const listMemories = query({ + args: { userId: v.string() }, + handler: async (ctx, { userId }) => { + return await ctx.db + .query("memories") + .withIndex("by_user", q => q.eq("userId", userId)) + .order("desc") + .take(50); + }, +}); +``` + +--- + +## Related docs + + + + How automatic profiling works + + + Filtering and search modes + + + Memory middleware for Next.js + + + Memory for LangChain apps + + diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index ac554055..ad35867c 100644 --- a/apps/web/app/(app)/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -6,7 +6,10 @@ import { useCallback, useEffect, useMemo, + type Dispatch, + type RefObject, type ReactNode, + type SetStateAction, } from "react" import { useRouter } from "next/navigation" import { useAuth } from "@lib/auth-context" @@ -30,11 +33,25 @@ import { RaycastIcon, } from "@/components/integration-icons" import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons" -import { Sparkles, ChevronLeft, ChevronRight } from "lucide-react" +import { + Sparkles, + ChevronLeft, + ChevronRight, + AlertCircle, + CheckCircle2, + Loader2, +} from "lucide-react" import { analytics } from "@/lib/analytics" +import { consumePendingConnectUrl } from "@/lib/constants" type DetectedSource = "x" | "linkedin" | "resume" | null type Status = "idle" | "processing" | "done" | "error" +type AccountLookupStatus = "checking" | "found" | "not_found" | "error" +type AccountLookup = { + source: "x" | "linkedin" + status: AccountLookupStatus + message: string +} type DocStatus = | "unknown" | "queued" @@ -53,7 +70,7 @@ function XIcon({ className }: { className?: string }) { fill="currentColor" aria-hidden="true" > - + ) } @@ -66,7 +83,7 @@ function LinkedInIcon({ className }: { className?: string }) { fill="currentColor" aria-hidden="true" > - + ) } @@ -76,7 +93,7 @@ function SubmitArrow() { Submit @@ -121,8 +138,13 @@ const SOURCE_ICON: Record< } const SOURCE_LABEL: Record<"x" | "linkedin", string> = { - x: "X profile detected — press Enter to continue", - linkedin: "LinkedIn profile detected — press Enter to continue", + x: "X profile detected - checking account", + linkedin: "LinkedIn profile detected - checking account", +} + +const SOURCE_NAME: Record<"x" | "linkedin", string> = { + x: "X", + linkedin: "LinkedIn", } type SpotlightItem = { @@ -352,6 +374,167 @@ function buildSpotlightCatalog( } } +function isAccountSource(source: DetectedSource): source is "x" | "linkedin" { + return source === "x" || source === "linkedin" +} + +function useSpotlightAutoRotation( + status: Status, + pauseSpotlight: boolean, + setSpotlightCategory: Dispatch>, +) { + useEffect(() => { + if (status !== "processing") return + if (pauseSpotlight) return + const n = SPOTLIGHT_CATEGORY_ORDER.length + if (n <= 1) return + const t = setInterval(() => { + setSpotlightCategory((cur) => { + const i = SPOTLIGHT_CATEGORY_ORDER.indexOf(cur) + const from = i >= 0 ? i : 0 + const next = (from + 1) % n + return SPOTLIGHT_CATEGORY_ORDER[next] ?? cur + }) + }, 8000) + return () => clearInterval(t) + }, [status, pauseSpotlight, setSpotlightCategory]) +} + +function useInitialInputFocus(inputRef: RefObject) { + useEffect(() => { + const t = setTimeout(() => inputRef.current?.focus(), 500) + return () => clearTimeout(t) + }, [inputRef]) +} + +function useAccountLookup({ + detected, + status, + value, +}: { + detected: DetectedSource + status: Status + value: string +}) { + const [accountLookup, setAccountLookup] = useState(null) + + useEffect(() => { + if (status !== "idle") return + + const source = isAccountSource(detected) ? detected : null + const trimmedValue = value.trim() + + if (!source || !trimmedValue) { + setAccountLookup(null) + return + } + + const controller = new AbortController() + setAccountLookup({ + source, + status: "checking", + message: SOURCE_LABEL[source], + }) + + const timeout = setTimeout(async () => { + try { + const params = new URLSearchParams({ + source, + value: trimmedValue, + }) + const response = await fetch( + `/api/onboarding/account-status?${params.toString()}`, + { signal: controller.signal }, + ) + const data: { + found?: boolean + handle?: string + reason?: string + verified?: boolean + } = await response.json().catch(() => ({})) + + if (controller.signal.aborted) return + + if (response.ok && data.found === true) { + const account = + source === "x" && data.handle ? ` @${data.handle}` : "" + setAccountLookup({ + source, + status: "found", + message: `${SOURCE_NAME[source]} account${account} found - press Enter to continue`, + }) + return + } + + if ( + (response.ok && data.found === false) || + data.reason === "invalid" + ) { + setAccountLookup({ + source, + status: "not_found", + message: `${SOURCE_NAME[source]} account not found. Check the link and try again.`, + }) + return + } + + setAccountLookup({ + source, + status: "error", + message: `Could not verify ${SOURCE_NAME[source]} account. You can still continue.`, + }) + } catch (err) { + if (controller.signal.aborted) return + console.error(err) + setAccountLookup({ + source, + status: "error", + message: `Could not verify ${SOURCE_NAME[source]} account. You can still continue.`, + }) + } + }, 450) + + return () => { + clearTimeout(timeout) + controller.abort() + } + }, [detected, status, value]) + + return accountLookup +} + +function usePollingCleanup( + pollingRef: RefObject | null>, +) { + useEffect(() => { + return () => { + if (pollingRef.current) clearInterval(pollingRef.current) + } + }, [pollingRef]) +} + +function useDoneAnimation( + status: Status, + setStampLanded: Dispatch>, + setVisibleSnippets: Dispatch>, +) { + useEffect(() => { + if (status !== "done") return + setStampLanded(false) + setVisibleSnippets(0) + const t1 = setTimeout(() => setStampLanded(true), 400) + const t2 = setTimeout(() => setVisibleSnippets(1), 900) + const t3 = setTimeout(() => setVisibleSnippets(2), 1200) + const t4 = setTimeout(() => setVisibleSnippets(3), 1500) + return () => { + clearTimeout(t1) + clearTimeout(t2) + clearTimeout(t3) + clearTimeout(t4) + } + }, [status, setStampLanded, setVisibleSnippets]) +} + export default function OnboardingPage() { const router = useRouter() const { user, organizations, refetchOrganizations, setActiveOrg } = useAuth() @@ -374,6 +557,12 @@ export default function OnboardingPage() { const skippingRef = useRef(false) const [spotlightCategory, setSpotlightCategory] = useState("productivity") + + /** Navigate home, or back to the plugin connect page if one is pending. */ + const goHomeOrPendingConnect = useCallback(() => { + const pendingPath = consumePendingConnectUrl() + router.push(pendingPath ?? "/") + }, [router]) const [pauseSpotlight, setPauseSpotlight] = useState(false) const spotlightCatalog = useMemo( @@ -395,48 +584,11 @@ export default function OnboardingPage() { [spotlightCategory], ) - useEffect(() => { - if (status !== "processing") return - if (pauseSpotlight) return - const n = SPOTLIGHT_CATEGORY_ORDER.length - if (n <= 1) return - const t = setInterval(() => { - setSpotlightCategory((cur) => { - const i = SPOTLIGHT_CATEGORY_ORDER.indexOf(cur) - const from = i >= 0 ? i : 0 - const next = (from + 1) % n - return SPOTLIGHT_CATEGORY_ORDER[next] ?? cur - }) - }, 8000) - return () => clearInterval(t) - }, [status, pauseSpotlight]) - - useEffect(() => { - const t = setTimeout(() => inputRef.current?.focus(), 500) - return () => clearTimeout(t) - }, []) - - useEffect(() => { - return () => { - if (pollingRef.current) clearInterval(pollingRef.current) - } - }, []) - - useEffect(() => { - if (status !== "done") return - setStampLanded(false) - setVisibleSnippets(0) - const t1 = setTimeout(() => setStampLanded(true), 400) - const t2 = setTimeout(() => setVisibleSnippets(1), 900) - const t3 = setTimeout(() => setVisibleSnippets(2), 1200) - const t4 = setTimeout(() => setVisibleSnippets(3), 1500) - return () => { - clearTimeout(t1) - clearTimeout(t2) - clearTimeout(t3) - clearTimeout(t4) - } - }, [status]) + useSpotlightAutoRotation(status, pauseSpotlight, setSpotlightCategory) + useInitialInputFocus(inputRef) + const accountLookup = useAccountLookup({ detected, status, value }) + usePollingCleanup(pollingRef) + useDoneAnimation(status, setStampLanded, setVisibleSnippets) const handleChange = (v: string) => { setValue(v) @@ -467,7 +619,8 @@ export default function OnboardingPage() { skippingRef.current = true try { await ensureOrg() - router.push("/") + const pendingPath = consumePendingConnectUrl() + router.push(pendingPath ?? "/") } catch (err) { console.error(err) skippingRef.current = false @@ -597,7 +750,18 @@ export default function OnboardingPage() { } } - const canSubmit = detected && detected !== "resume" + const hasDetectedAccount = detected === "x" || detected === "linkedin" + const currentAccountLookup = + accountLookup?.source === detected ? accountLookup : null + const isCheckingAccount = + hasDetectedAccount && + (!currentAccountLookup || currentAccountLookup.status === "checking") + const canSubmit = Boolean( + hasDetectedAccount && + currentAccountLookup && + currentAccountLookup.status !== "checking" && + currentAccountLookup.status !== "not_found", + ) return ( // biome-ignore lint/a11y/noStaticElementInteractions: full-surface drag-and-drop for resume PDF @@ -705,6 +869,20 @@ export default function OnboardingPage() { )} /> + + {isCheckingAccount && ( + + + + )} + + {canSubmit && ( - {SOURCE_LABEL[detected as "x" | "linkedin"]} + {currentAccountLookup?.status === "found" && ( + + )} + {currentAccountLookup?.status === "not_found" && ( + + )} + {currentAccountLookup?.status === "error" && ( + + )} + {isCheckingAccount && ( + + )} + + {currentAccountLookup?.message ?? + SOURCE_LABEL[detected as "x" | "linkedin"]} + )} @@ -809,7 +1013,7 @@ export default function OnboardingPage() { Finishing your first save

- Most finish in under a minute. Below is optional — ways to add + Most finish in under a minute. Below is optional: ways to add more later.

@@ -948,7 +1152,7 @@ export default function OnboardingPage() {

Your first save is ready. When you want more, use Integrations - for browser, phone, editor, and AI tools — all in one place. + for browser, phone, editor, and AI tools, all in one place.

@@ -1077,7 +1281,7 @@ export default function OnboardingPage() { @@ -193,7 +193,7 @@ export default function ReferralPage() { variant="outline" className="w-full border-white/10 text-white hover:bg-white/5" > - + Share this link diff --git a/apps/web/app/ref/page.tsx b/apps/web/app/ref/page.tsx index 853ed900..9c97cdd6 100644 --- a/apps/web/app/ref/page.tsx +++ b/apps/web/app/ref/page.tsx @@ -16,8 +16,8 @@ export default function ReferralHomePage() {
-
- +
+
Missing Referral Code diff --git a/apps/web/app/upgrade-mcp/page.tsx b/apps/web/app/upgrade-mcp/page.tsx index 0b4fb05b..fbe52520 100644 --- a/apps/web/app/upgrade-mcp/page.tsx +++ b/apps/web/app/upgrade-mcp/page.tsx @@ -152,7 +152,7 @@ export default function MigrateMCPPage() {
- +
@@ -177,7 +177,7 @@ export default function MigrateMCPPage() { className="text-sm font-medium text-slate-200 flex items-center gap-2" htmlFor="mcpUrl" > - + MCP URL
@@ -231,13 +231,13 @@ export default function MigrateMCPPage() { > {migrateMutation.isPending ? ( <> - - Migrating documents... + + Migrating documents… ) : ( <> Start Upgrade - + )} @@ -260,7 +260,7 @@ export default function MigrateMCPPage() {
- +

Migration completed successfully!

diff --git a/apps/web/components/add-document/connections.tsx b/apps/web/components/add-document/connections.tsx index e9114276..4c455c63 100644 --- a/apps/web/components/add-document/connections.tsx +++ b/apps/web/components/add-document/connections.tsx @@ -9,9 +9,11 @@ import { useCustomer } from "autumn-js/react" import { Check, ChevronDown, - Clock, FolderOpen, + History, Loader, + Loader2, + Play, Trash2, Zap, } from "lucide-react" @@ -30,6 +32,11 @@ import { DropdownMenuTrigger, } from "@ui/components/dropdown-menu" import { RemoveConnectionDialog } from "@/components/remove-connection-dialog" +import { SyncStatusBadge } from "@/components/settings/sync-status-badge" +import { SyncHistoryPanel } from "@/components/settings/sync-history-panel" +import { useTriggerSync } from "@/hooks/use-trigger-sync" +import { formatRelativeTime } from "@/components/settings/sync-utils" +import type { ImportProvider } from "@/components/settings/sync-utils" type GDriveSyncScope = "scoped" | "full" @@ -71,17 +78,20 @@ const CONNECTORS: Record< }, } as const -function formatRelativeTime(date: string | null | undefined): string { - if (!date) return "Never" - const d = new Date(date) - const diffMs = Date.now() - d.getTime() - const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) - const diffDays = Math.floor(diffHours / 24) - if (diffHours < 1) return "Just now" - if (diffHours < 24) return `${diffHours}h ago` - if (diffDays === 1) return "Yesterday" - if (diffDays < 7) return `${diffDays} days ago` - return d.toLocaleDateString() +/** Extract typed metadata from a connection, with runtime validation. */ +function getConnectionMeta(connection: Connection) { + const m = connection.metadata as Record | undefined + return { + syncInProgress: m?.syncInProgress === true, + lastSyncedAt: + typeof m?.lastSyncedAt === "number" ? m.lastSyncedAt : undefined, + documentCount: typeof m?.documentCount === "number" ? m.documentCount : 0, + } +} + +/** Check if a connection's auth token has expired. */ +function isConnectionExpired(connection: Connection): boolean { + return !!connection.expiresAt && new Date(connection.expiresAt) <= new Date() } function ConnectionRow({ @@ -89,18 +99,23 @@ function ConnectionRow({ onDelete, isDeleting, projects, + onTriggerSync, + isSyncing, }: { connection: Connection onDelete: () => void isDeleting: boolean projects: Project[] + onTriggerSync: () => void + isSyncing: boolean }) { + const [historyOpen, setHistoryOpen] = useState(false) const config = CONNECTORS[connection.provider as ConnectorProvider] if (!config) return null const Icon = config.icon - const isConnected = - !connection.expiresAt || new Date(connection.expiresAt) > new Date() + const meta = getConnectionMeta(connection) + const expired = isConnectionExpired(connection) const getProjectName = (tag: string): string => { if (tag === DEFAULT_PROJECT_ID) return "Default" @@ -110,12 +125,8 @@ function ConnectionRow({ ) } - const documentCount = (connection.metadata?.documentCount as number) ?? 0 - const containerTags = ( - connection as Connection & { containerTags?: string[] } - ).containerTags - const projectName = containerTags?.[0] - ? getProjectName(containerTags[0]) + const projectName = connection.containerTags?.[0] + ? getProjectName(connection.containerTags[0]) : null return ( @@ -138,23 +149,11 @@ function ConnectionRow({ > {config.title} -
-
- - {isConnected ? "Connected" : "Disconnected"} - -
+
- +
+ + + +
@@ -186,17 +242,11 @@ function ConnectionRow({
)} -
- - - {formatRelativeTime(connection.createdAt)} - -
+ + Last synced: {formatRelativeTime(meta.lastSyncedAt)} +
- {documentCount} + {meta.documentCount}
+ + {historyOpen && ( +
+ +
+ )}
) @@ -226,7 +285,7 @@ interface ConnectContentProps { export function ConnectContent({ selectedProject }: ConnectContentProps) { const queryClient = useQueryClient() const autumn = useCustomer() - const isProUser = hasActivePlan(autumn.customer?.products, "api_pro") + const isProUser = hasActivePlan(autumn.data?.subscriptions, "api_pro") const [connectingProvider, setConnectingProvider] = useState(null) const [gdriveSyncScope, setGdriveSyncScope] = @@ -236,6 +295,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { open: boolean connection: Connection | null }>({ open: false, connection: null }) + const triggerSync = useTriggerSync() const projects = (queryClient.getQueryData(["projects"]) || []) as Project[] @@ -243,20 +303,26 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { const handleUpgrade = async () => { setIsUpgrading(true) try { - await autumn.attach({ - productId: "api_pro", + const result = await autumn.attach({ + planId: "api_pro", successUrl: window.location.href, }) + if (result?.paymentUrl) { + window.open(result.paymentUrl, "_self") + return + } + autumn.refetch?.() } catch (error) { console.error("Upgrade error:", error) toast.error("Failed to start upgrade process") + } finally { setIsUpgrading(false) } } - const connectionsFeature = autumn.customer?.features?.connections - const connectionsUsed = connectionsFeature?.usage ?? 0 - const connectionsLimit = connectionsFeature?.included_usage ?? 10 + const connectionsBalance = autumn.data?.balances?.connections + const connectionsUsed = connectionsBalance?.usage ?? 0 + const connectionsLimit = connectionsBalance?.granted ?? 10 const canAddConnection = connectionsUsed < connectionsLimit // Fetch connections @@ -276,7 +342,13 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { return response.data as Connection[] }, staleTime: 30 * 1000, - refetchInterval: 60 * 1000, + refetchInterval: (query) => { + const conns = query.state.data as Connection[] | undefined + if (conns?.some((c) => getConnectionMeta(c).syncInProgress)) { + return 5000 + } + return 60 * 1000 + }, refetchIntervalInBackground: true, }) @@ -409,7 +481,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { className="bg-[#14161A] rounded-[12px] px-4 py-3 flex items-center justify-between gap-3" >
- +

{config.title}

@@ -431,7 +503,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { className="bg-[#4BA0FA] text-black hover:bg-[#4BA0FA]/90 text-[14px] font-medium px-3 h-8 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > {isConnecting ? ( - + ) : ( "Connect" )} @@ -443,7 +515,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { type="button" className="bg-[#4BA0FA] text-black hover:bg-[#4BA0FA]/90 px-1.5 h-8 flex items-center transition-colors" > - + @@ -463,7 +535,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { > {label} {gdriveSyncScope === scope && ( - + )} ))} @@ -483,7 +555,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { className="bg-[#4BA0FA] text-black hover:bg-[#4BA0FA]/90 text-[14px] font-medium px-3 py-1.5 h-8" > {isConnecting ? ( - + ) : ( "Connect" )} @@ -524,7 +596,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { className="flex items-center gap-1.5 bg-[#4BA0FA] text-black hover:bg-[#4BA0FA]/90 disabled:opacity-50 disabled:cursor-not-allowed text-[13px] font-medium rounded-full h-8 px-3 transition-colors shrink-0" > {isAnyConnecting ? ( - + ) : ( <> + Add a connection @@ -638,6 +710,18 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { projects={projects} onDelete={() => setRemoveDialog({ open: true, connection })} isDeleting={deleteConnectionMutation.isPending} + onTriggerSync={() => + triggerSync.mutate({ + connectionId: connection.id, + provider: connection.provider as ImportProvider, + containerTags: connection.containerTags, + }) + } + isSyncing={ + (triggerSync.isPending && + triggerSync.variables?.connectionId === connection.id) || + getConnectionMeta(connection).syncInProgress + } /> ))}

@@ -650,14 +734,14 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { id="no-active-connections" className="bg-[#14161A] shadow-inside-out rounded-[12px] px-4 py-6 h-full mb-4 flex flex-col justify-center items-center" > - + {!isProUser ? ( <>

{isUpgrading || autumn.isLoading ? ( - - Upgrading... + + Upgrading… ) : ( <> @@ -676,19 +760,19 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {

- + Unlimited memories
- + 10 connections
- + Advanced search
- + Priority support
diff --git a/apps/web/components/add-document/file.tsx b/apps/web/components/add-document/file.tsx index 67e70d01..b9212bc3 100644 --- a/apps/web/components/add-document/file.tsx +++ b/apps/web/components/add-document/file.tsx @@ -210,10 +210,10 @@ export function FileContent({ multiple onChange={handleFileSelect} disabled={isSubmitting} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed" + className="absolute inset-0 size-full opacity-0 cursor-pointer disabled:cursor-not-allowed" accept={FILE_ACCEPT} /> -
+
{hasItems ? ( diff --git a/apps/web/components/add-document/index.tsx b/apps/web/components/add-document/index.tsx index 4c441ce1..6a4775ee 100644 --- a/apps/web/components/add-document/index.tsx +++ b/apps/web/components/add-document/index.tsx @@ -5,7 +5,7 @@ import { useQueryState } from "nuqs" import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog" import { cn } from "@lib/utils" import { dmSansClassName } from "@/lib/fonts" -import { FileTextIcon, GlobeIcon, ZapIcon, Loader2 } from "lucide-react" +import { FileTextIcon, GlobeIcon, ZapIcon, Loader2, XIcon } from "lucide-react" import { Button } from "@ui/components/button" import { ConnectContent } from "./connections" import { NoteContent } from "./note" @@ -16,7 +16,7 @@ import { toast } from "sonner" import { useDocumentMutations } from "../../hooks/use-document-mutations" import { useCustomer } from "autumn-js/react" import { useTokenUsage } from "@/hooks/use-token-usage" -import { tokensToCredits, formatUsageNumber } from "@/lib/billing-utils" +import { formatUsageNumber } from "@/lib/billing-utils" import { SpaceSelector } from "../space-selector" import { useIsMobile } from "@hooks/use-mobile" import { addDocumentParam } from "@/lib/search-params" @@ -35,10 +35,10 @@ export function AddDocumentModal({ isOpen, onClose }: AddDocumentModalProps) { !open && onClose()}> Add Document -
+
@@ -127,11 +127,8 @@ export function AddDocument({ const autumn = useCustomer() const { tokensUsed, - tokensLimit, - tokensPercent, searchesUsed, - searchesLimit, - searchesPercent, + planUsagePct, hasPaidPlan, isLoading: isLoadingUsage, } = useTokenUsage(autumn) @@ -259,19 +256,44 @@ export function AddDocument({ activeTab === "file" && (!fileTabHasPending || isSubmitting) return ( -
+
+ {isMobile && ( +
+
+

+ Add memory +

+

+ Save something to recall later +

+
+ +
+ )}
{tabs.map((tab) => ( @@ -288,6 +310,58 @@ export function AddDocument({ ))}
+ {isMobile && ( +
+
+ + Plan usage + + + {isLoadingUsage + ? "…" + : `${planUsagePct < 1 && planUsagePct > 0 ? "< 1" : Math.round(planUsagePct)}% used`} + +
+
+
80 + ? "#ef4444" + : hasPaidPlan + ? "linear-gradient(to right, #4BA0FA 80%, #002757 100%)" + : "#0054AD", + }} + title={`${formatUsageNumber(tokensUsed)} tokens · ${formatUsageNumber(searchesUsed)} queries`} + /> +
+ {!isLoadingUsage && ( +

+ {formatUsageNumber(tokensUsed)} tokens ·{" "} + {formatUsageNumber(searchesUsed)} queries +

+ )} +
+ )} + {!isMobile && (
@@ -298,72 +372,46 @@ export function AddDocument({ dmSansClassName(), )} > - Credits + Plan usage {isLoadingUsage ? "…" - : `${tokensToCredits(tokensUsed)} / ${tokensToCredits(tokensLimit)}`} + : `${planUsagePct < 1 && planUsagePct > 0 ? "< 1" : Math.round(planUsagePct)}% used`}
80 + planUsagePct > 80 ? "#ef4444" : hasPaidPlan ? "linear-gradient(to right, #4BA0FA 80%, #002757 100%)" : "#0054AD", }} + title={`${formatUsageNumber(tokensUsed)} tokens · ${formatUsageNumber(searchesUsed)} queries`} />
-
- -
-
- - Search Queries - - - {isLoadingUsage - ? "…" - : `${formatUsageNumber(searchesUsed)} / ${formatUsageNumber(searchesLimit)}`} - -
-
-
80 - ? "#ef4444" - : hasPaidPlan - ? "linear-gradient(to right, #4BA0FA 80%, #002757 100%)" - : "#0054AD", - }} - /> -
+ {formatUsageNumber(tokensUsed)} tokens ·{" "} + {formatUsageNumber(searchesUsed)} queries +

+ )}
{!hasPaidPlan && ( @@ -372,13 +420,19 @@ export function AddDocument({ onClick={async () => { setIsUpgrading(true) try { - await autumn.attach({ - productId: "api_pro", - successUrl: "https://app.supermemory.ai/settings#account", + const result = await autumn.attach({ + planId: "api_pro", + successUrl: `${window.location.origin}/settings#account`, }) - window.location.reload() + if (result?.paymentUrl) { + window.open(result.paymentUrl, "_self") + return + } + autumn.refetch?.() } catch (error) { console.error(error) + toast.error("Failed to start checkout. Please try again.") + } finally { setIsUpgrading(false) } }} @@ -400,7 +454,7 @@ export function AddDocument({ {isUpgrading ? ( <> - Upgrading... + Upgrading… ) : ( "Upgrade to Pro" @@ -414,11 +468,11 @@ export function AddDocument({
-
+
{activeTab === "note" && (
{!isMobile && ( @@ -467,13 +523,19 @@ export function AddDocument({ /> )}
@@ -484,11 +546,12 @@ export function AddDocument({ disabled={ activeTab === "file" ? fileTabSubmitDisabled : isSubmitting } + className={cn(isMobile && "h-11 min-w-[8rem] px-5")} > {isSubmitting ? ( <> - Adding... + Adding… ) : ( <> @@ -537,19 +600,24 @@ function TabButton({ type="button" onClick={onClick} className={cn( - "flex items-center gap-2 px-3 py-2 rounded-full text-left transition-colors whitespace-nowrap focus:outline-none focus:ring-0 shrink-0", - active ? "bg-[#14161A] shadow-inside-out" : "hover:bg-[#14161A]/50", + "relative flex h-14 min-w-0 flex-col items-center justify-center gap-1 rounded-xl px-1 text-center transition-colors focus:outline-none focus:ring-0", + active + ? "bg-[#0F141B] text-white shadow-inside-out ring-1 ring-[#2261CA33]" + : "text-[#8B8B8B] hover:bg-[#14161A]/50", dmSansClassName(), )} > - + {title.split(" ")[0]} {isPro && ( - + PRO )} diff --git a/apps/web/components/add-document/link.tsx b/apps/web/components/add-document/link.tsx index 2efb67dc..93e8821a 100644 --- a/apps/web/components/add-document/link.tsx +++ b/apps/web/components/add-document/link.tsx @@ -173,7 +173,7 @@ export function LinkContent({ {isPreviewLoading ? ( <> - Loading... + Loading… ) : ( "Preview Link" @@ -216,7 +216,7 @@ export function LinkContent({ {title { e.currentTarget.style.display = "none" e.currentTarget.parentElement?.classList.add("opacity-50") @@ -228,7 +228,7 @@ export function LinkContent({
) : (
- +
)}
diff --git a/apps/web/components/add-document/note.tsx b/apps/web/components/add-document/note.tsx index 4f833ca2..aa0fac97 100644 --- a/apps/web/components/add-document/note.tsx +++ b/apps/web/components/add-document/note.tsx @@ -40,11 +40,12 @@ export function NoteContent({ }, [isOpen, onContentChange]) return ( -
+
) diff --git a/apps/web/components/add-space-modal.tsx b/apps/web/components/add-space-modal.tsx index be666083..7c9f3cf3 100644 --- a/apps/web/components/add-space-modal.tsx +++ b/apps/web/components/add-space-modal.tsx @@ -150,7 +150,7 @@ export function AddSpaceModal({

- Creating... + Creating… ) : ( <> diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index 4a148929..f3885c3c 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -69,12 +69,12 @@ function ChatEmptyStatePlaceholder({ id="chat-empty-state" className="flex flex-col items-center justify-center h-full" > -
- - +
+ +
-

Ask me anything about your memories...

+

Ask me anything about your memories…

(null) + const isScrolledToBottomRef = useRef(true) const sentQueuedMessageRef = useRef(null) const { selectedProject } = useProject() const { allProjects } = useContainerTags() @@ -296,6 +297,7 @@ export function ChatSidebar({ const { scrollTop, scrollHeight, clientHeight } = container const distanceFromBottom = scrollHeight - scrollTop - clientHeight const isAtBottom = distanceFromBottom <= 20 + isScrolledToBottomRef.current = isAtBottom setIsScrolledToBottom(isAtBottom) }, []) @@ -303,6 +305,7 @@ export function ChatSidebar({ if (messagesContainerRef.current) { messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight + isScrolledToBottomRef.current = true setIsScrolledToBottom(true) } }, []) @@ -399,6 +402,12 @@ export function ChatSidebar({ } }, [selectedProject]) + useEffect(() => { + if (!isHistoryOpen) return + fetchThreads() + analytics.chatHistoryViewed?.() + }, [isHistoryOpen, fetchThreads]) + const loadThread = useCallback( async (id: string) => { try { @@ -441,12 +450,14 @@ export function ChatSidebar({ // Auto-restore thread from URL on mount (e.g. reload or direct link) const didAutoLoadRef = useRef(false) + const initialThreadIdRef = useRef(threadId) useEffect(() => { if (didAutoLoadRef.current) return - if (!threadId) return + const initialThreadId = initialThreadIdRef.current + if (!initialThreadId) return didAutoLoadRef.current = true - loadThread(threadId) - }, [threadId, loadThread]) + loadThread(initialThreadId) + }, [loadThread]) const deleteThread = useCallback( async (threadId: string) => { @@ -553,6 +564,46 @@ export function ChatSidebar({ checkIfScrolledToBottom() }, [messages, checkIfScrolledToBottom]) + useEffect(() => { + const isStreaming = status === "streaming" + const lastMessage = messages[messages.length - 1] + const isLastMessageFromAssistant = lastMessage?.role === "assistant" + + if ( + isStreaming && + isLastMessageFromAssistant && + isScrolledToBottomRef.current + ) { + scrollToBottom() + } + }, [status, messages, scrollToBottom]) + + useEffect(() => { + const container = messagesContainerRef.current + if (!container) return + + const isStreaming = status === "streaming" + if (!isStreaming) return + + const mutationObserver = new MutationObserver(() => { + if (isScrolledToBottomRef.current) { + requestAnimationFrame(() => { + scrollToBottom() + }) + } + }) + + mutationObserver.observe(container, { + childList: true, + subtree: true, + characterData: true, + }) + + return () => { + mutationObserver.disconnect() + } + }, [status, scrollToBottom]) + // Add scroll event listener to track scroll position useEffect(() => { const container = messagesContainerRef.current @@ -596,10 +647,7 @@ export function ChatSidebar({ open={isHistoryOpen} onOpenChange={(open) => { setIsHistoryOpen(open) - if (open) { - fetchThreads() - analytics.chatHistoryViewed?.() - } else { + if (!open) { setConfirmingDeleteId(null) } }} @@ -622,7 +670,7 @@ export function ChatSidebar({
{isLoadingThreads ? (
- +
) : threads.length === 0 ? (
@@ -659,7 +707,7 @@ export function ChatSidebar({ e.stopPropagation() deleteThread(thread.id) }} - className="h-7 w-7 bg-red-500 text-white hover:bg-red-600" + className="size-7 bg-red-500 text-white hover:bg-red-600" > @@ -671,7 +719,7 @@ export function ChatSidebar({ e.stopPropagation() setConfirmingDeleteId(null) }} - className="h-7 w-7" + className="size-7" > @@ -685,7 +733,7 @@ export function ChatSidebar({ e.stopPropagation() setConfirmingDeleteId(thread.id) }} - className="ml-2 h-7 w-7" + className="ml-2 size-7" > @@ -883,7 +931,7 @@ export function ChatSidebar({ ))} {(status === "submitted" || status === "streaming") && (
- +
)}
@@ -965,10 +1013,10 @@ export function ChatSidebar({ isResponding={status === "submitted" || status === "streaming"} activeStatus={ status === "submitted" - ? "Thinking..." + ? "Thinking…" : status === "streaming" - ? "Structuring response..." - : "Waiting for input..." + ? "Structuring response…" + : "Waiting for input…" } onExpandedChange={setIsInputExpanded} chainOfThoughtComponent={ diff --git a/apps/web/components/chat/input/actions.tsx b/apps/web/components/chat/input/actions.tsx index 44f8132f..f8122f8e 100644 --- a/apps/web/components/chat/input/actions.tsx +++ b/apps/web/components/chat/input/actions.tsx @@ -29,7 +29,7 @@ export function SendButton({ > Send Icon diff --git a/apps/web/components/chat/message/agent-message.tsx b/apps/web/components/chat/message/agent-message.tsx index 28f34919..63fb84f5 100644 --- a/apps/web/components/chat/message/agent-message.tsx +++ b/apps/web/components/chat/message/agent-message.tsx @@ -165,11 +165,9 @@ function BashToolDisplay({ part }: { part: ToolCallDisplayPart }) { : "text-white/70", )} > - {cmd ?? "..."} + {cmd ?? "…"} - {isLoading && ( - running... - )} + {isLoading && running…} {isDone && !hasOutput && ( done )} @@ -261,7 +259,7 @@ function ToolCallDisplay({ part }: { part: ToolCallDisplayPart }) { > {label} - {isLoading && running...} + {isLoading && running…} {isDone && done} {isError && error} {expanded ? ( diff --git a/apps/web/components/connect-ai-modal.tsx b/apps/web/components/connect-ai-modal.tsx index 70487cd6..b0691258 100644 --- a/apps/web/components/connect-ai-modal.tsx +++ b/apps/web/components/connect-ai-modal.tsx @@ -351,7 +351,7 @@ export function ConnectAIModal({ {/* Step 1: Client Selection */}
-
+
1

Select Your AI Client

@@ -373,7 +373,7 @@ export function ConnectAIModal({ type="button" >
-
+
{clientName} { analytics.mcpInstallCmdCopied() - toast.success("Opening Cursor installer...") + toast.success("Opening Cursor installer…") }} > {createMcpApiKeyMutation.isPending ? (
- +
) : ( <> @@ -684,7 +684,7 @@ export function ConnectAIModal({ @@ -919,8 +919,8 @@ export function ConnectAIModal({ > {migrateMCPMutation.isPending ? ( <> - - Migrating... + + Migrating… ) : ( "Migrate" diff --git a/apps/web/components/dashboard-view.tsx b/apps/web/components/dashboard-view.tsx index f0e19893..945f5b91 100644 --- a/apps/web/components/dashboard-view.tsx +++ b/apps/web/components/dashboard-view.tsx @@ -1055,7 +1055,7 @@ function MemoryOfDayCard({ data }: { data: MemoryOfDay }) { type="button" onClick={() => router.push(href)} className={cn( - "group w-full h-full text-left bg-surface-card/60 backdrop-blur-md rounded-[18px] p-3 flex flex-col justify-between transition-colors cursor-pointer shadow-[0_12px_40px_rgba(0,0,0,0.22)]", + "group size-full text-left bg-surface-card/60 backdrop-blur-md rounded-[18px] p-3 flex flex-col justify-between transition-colors cursor-pointer shadow-[0_12px_40px_rgba(0,0,0,0.22)]", dmSansClassName(), )} > @@ -1355,7 +1355,7 @@ export function DashboardView({ return (
@@ -1531,7 +1531,7 @@ export function DashboardView({ onClick={() => onOpenDocument(doc)} className="group flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-surface-hover" > -
+
{isLink ? ( ) : ( diff --git a/apps/web/components/document-cards/file-preview.tsx b/apps/web/components/document-cards/file-preview.tsx index 6013a203..2f0015fa 100644 --- a/apps/web/components/document-cards/file-preview.tsx +++ b/apps/web/components/document-cards/file-preview.tsx @@ -1,6 +1,6 @@ "use client" -import { memo, useState } from "react" +import { memo, useCallback, useState } from "react" import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" import type { z } from "zod" import { dmSansClassName } from "@/lib/fonts" @@ -49,6 +49,7 @@ export const FilePreview = memo(function FilePreview({ document: DocumentWithMemories }) { const [imageError, setImageError] = useState(false) + const [retryKey, setRetryKey] = useState(0) const { extension, color } = getFileTypeInfo(document) const type = document.type?.toLowerCase() @@ -58,6 +59,17 @@ export const FilePreview = memo(function FilePreview({ document.url && !imageError + // On first failure, wait briefly then force a re-render with a new key to + // retry the fetch (covers transient R2 timing issues). + // On second failure, give up and show the fallback file icon view. + const handleImageError = useCallback(() => { + if (retryKey === 0) { + setTimeout(() => setRetryKey(1), 500) + return + } + setImageError(true) + }, [retryKey]) + return (
{color && ( @@ -80,10 +92,11 @@ export const FilePreview = memo(function FilePreview({ />
{document.title setImageError(true)} + className="relative max-w-full max-h-full size-auto object-contain z-10" + onError={handleImageError} loading="lazy" />
@@ -93,7 +106,7 @@ export const FilePreview = memo(function FilePreview({

{label} diff --git a/apps/web/components/document-cards/note-preview.tsx b/apps/web/components/document-cards/note-preview.tsx index ef16948b..34bcadba 100644 --- a/apps/web/components/document-cards/note-preview.tsx +++ b/apps/web/components/document-cards/note-preview.tsx @@ -13,7 +13,7 @@ export function NotePreview({ document }: { document: DocumentWithMemories }) { return (

- +

Note

diff --git a/apps/web/components/document-cards/notion-preview.tsx b/apps/web/components/document-cards/notion-preview.tsx index f514e4aa..aebc4edc 100644 --- a/apps/web/components/document-cards/notion-preview.tsx +++ b/apps/web/components/document-cards/notion-preview.tsx @@ -73,9 +73,9 @@ export function NotionPreview({
{/* Decorative dots mimicking Notion's block handles */}
-
-
-
+
+
+
@@ -118,7 +118,7 @@ export function NotionPreview({
@@ -159,7 +159,7 @@ export function NotionPreview({ if (block.type === "bullet") { return (
-
+

{block.text}

diff --git a/apps/web/components/document-cards/tweet-preview.tsx b/apps/web/components/document-cards/tweet-preview.tsx index ba3b8d0f..807ba5b9 100644 --- a/apps/web/components/document-cards/tweet-preview.tsx +++ b/apps/web/components/document-cards/tweet-preview.tsx @@ -105,7 +105,7 @@ function CustomTweetMedia({ Tweet media {isVideo && (
diff --git a/apps/web/components/document-cards/website-preview.tsx b/apps/web/components/document-cards/website-preview.tsx index 067004fc..509b1e64 100644 --- a/apps/web/components/document-cards/website-preview.tsx +++ b/apps/web/components/document-cards/website-preview.tsx @@ -32,7 +32,7 @@ export const WebsitePreview = memo(function WebsitePreview({ {document.title setImageError(true)} loading="lazy" /> diff --git a/apps/web/components/document-cards/youtube-preview.tsx b/apps/web/components/document-cards/youtube-preview.tsx index 008ce35d..14089661 100644 --- a/apps/web/components/document-cards/youtube-preview.tsx +++ b/apps/web/components/document-cards/youtube-preview.tsx @@ -42,7 +42,7 @@ export const YoutubePreview = memo(function YoutubePreview({