Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/app/(auth)/login/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Title1Bold } from "@ui/text/title/title-1-bold"
import { InitialHeader } from "@/components/initial-header"
import { useRouter, useSearchParams } from "next/navigation"
import { useState, useEffect } from "react"
import { motion } from "framer-motion"
import { motion } from "motion/react"
import { dmSansClassName } from "@/utils/fonts"
import { cn } from "@lib/utils"
import { Logo } from "@ui/assets/Logo"
Expand Down
156 changes: 156 additions & 0 deletions apps/web/app/api/og/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import ogs from "open-graph-scraper"

export const runtime = "nodejs"

interface OGResponse {
title: string
description: string
image?: string
}

function isValidUrl(urlString: string): boolean {
try {
const url = new URL(urlString)
return url.protocol === "http:" || url.protocol === "https:"
} catch {
return false
}
}

function isPrivateHost(hostname: string): boolean {
const lowerHost = hostname.toLowerCase()

// Block localhost variants
if (
lowerHost === "localhost" ||
lowerHost === "127.0.0.1" ||
lowerHost === "::1" ||
lowerHost.startsWith("127.") ||
lowerHost.startsWith("0.0.0.0")
) {
return true
}

// Block RFC 1918 private IP ranges
const privateIpPatterns = [
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
]

return privateIpPatterns.some((pattern) => pattern.test(hostname))
}

function extractImageUrl(image: unknown): string | undefined {
if (!image) return undefined

if (typeof image === "string") {
return image
}

if (Array.isArray(image) && image.length > 0) {
const first = image[0]
if (first && typeof first === "object" && "url" in first) {
return String(first.url)
}
}

if (typeof image === "object" && image !== null && "url" in image) {
return String(image.url)
}

return undefined
}

function resolveImageUrl(
imageUrl: string | undefined,
baseUrl: string,
): string | undefined {
if (!imageUrl) return undefined

try {
const url = new URL(imageUrl)
return url.href
} catch {
try {
const base = new URL(baseUrl)
return new URL(imageUrl, base.href).href
} catch {
return undefined
}
}
}

export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const url = searchParams.get("url")

if (!url || !url.trim()) {
return Response.json(
{ error: "Missing or invalid url parameter" },
{ status: 400 },
)
}

const trimmedUrl = url.trim()

if (!isValidUrl(trimmedUrl)) {
return Response.json(
{ error: "Invalid URL. Must be http:// or https://" },
{ status: 400 },
)
}

const urlObj = new URL(trimmedUrl)
if (isPrivateHost(urlObj.hostname)) {
return Response.json(
{ error: "Private/localhost URLs are not allowed" },
{ status: 400 },
)
}

const { result, error } = await ogs({
url: trimmedUrl,
timeout: 8000,
fetchOptions: {
headers: {
"User-Agent":
"Mozilla/5.0 (compatible; SuperMemory/1.0; +https://supermemory.ai)",
},
},
})

if (error || !result) {
console.error("OG scraping error:", error)
return Response.json(
{ error: "Failed to fetch Open Graph data" },
{ status: 500 },
)
}

const ogTitle = result.ogTitle || result.twitterTitle || ""
const ogDescription =
result.ogDescription || result.twitterDescription || ""

const ogImageUrl =
extractImageUrl(result.ogImage) || extractImageUrl(result.twitterImage)

const resolvedImageUrl = resolveImageUrl(ogImageUrl, trimmedUrl)

const response: OGResponse = {
title: ogTitle,
description: ogDescription,
...(resolvedImageUrl && { image: resolvedImageUrl }),
}

return Response.json(response, {
headers: {
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
},
})
} catch (error) {
console.error("OG route error:", error)
return Response.json({ error: "Internal server error" }, { status: 500 })
}
}
2 changes: 1 addition & 1 deletion apps/web/app/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { AddDocumentModal } from "@/components/new/add-document"
import { MCPModal } from "@/components/new/mcp-modal"
import { HotkeysProvider } from "react-hotkeys-hook"
import { useHotkeys } from "react-hotkeys-hook"
import { AnimatePresence } from "framer-motion"
import { AnimatePresence } from "motion/react"

export default function NewPage() {
const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false)
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/onboarding/extension-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "lucide-react"
import { NavMenu } from "./nav-menu"
import { useOnboarding } from "./onboarding-context"
import { motion, AnimatePresence, type ResolvedValues } from "framer-motion"
import { motion, AnimatePresence, type ResolvedValues } from "motion/react"
import { useEffect, useMemo, useRef, useState, useLayoutEffect } from "react"
import React from "react"
import { cn } from "@lib/utils"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/onboarding/mcp-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { CheckIcon, CircleCheckIcon, CopyIcon, LoaderIcon } from "lucide-react"
import { TextMorph } from "@/components/text-morph"
import { NavMenu } from "./nav-menu"
import { cn } from "@lib/utils"
import { motion, AnimatePresence } from "framer-motion"
import { motion, AnimatePresence } from "motion/react"
import { useQuery } from "@tanstack/react-query"
import { $fetch } from "@lib/api"

Expand Down
2 changes: 1 addition & 1 deletion apps/web/components/chrome-extension-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
TwitterIcon,
} from "lucide-react"
import { useEffect, useState } from "react"
import { motion } from "framer-motion"
import { motion } from "motion/react"
import Image from "next/image"
import { analytics } from "@/lib/analytics"
import { useIsMobile } from "@hooks/use-mobile"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/components/connect-ai-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { z } from "zod/v4"
import { analytics } from "@/lib/analytics"
import { cn } from "@lib/utils"
import type { Project } from "@repo/lib/types"
import { motion, AnimatePresence } from "framer-motion"
import { motion, AnimatePresence } from "motion/react"

const clients = {
cursor: "Cursor",
Expand Down
43 changes: 34 additions & 9 deletions apps/web/components/new/add-document/connections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
const [isProUser, setIsProUser] = useState(false)
const [connectingProvider, setConnectingProvider] =
useState<ConnectorProvider | null>(null)
const [isUpgrading, setIsUpgrading] = useState(false)

// Check Pro status
useEffect(() => {
Expand All @@ -65,6 +66,20 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
}
}, [autumn.isLoading, autumn.customer])

const handleUpgrade = async () => {
setIsUpgrading(true)
try {
await autumn.attach({
productId: "consumer_pro",
successUrl: window.location.href,
})
} catch (error) {
console.error("Upgrade error:", error)
toast.error("Failed to start upgrade process")
setIsUpgrading(false)
}
}

// Check connections feature limits
const { data: connectionsCheck } = fetchConnectionsFeature(
autumn,
Expand Down Expand Up @@ -359,15 +374,25 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
{!isProUser ? (
<>
<p className="text-[14px] text-[#737373] mb-4 text-center">
<a
href="/pricing"
className="underline text-[#737373] hover:text-white"
>
Upgrade to Pro
</a>{" "}
to get
<br />
Supermemory Connections
{isUpgrading || autumn.isLoading ? (
<span className="inline-flex items-center gap-2">
<Loader className="h-4 w-4 animate-spin" />
Upgrading...
</span>
) : (
<>
<button
type="button"
onClick={handleUpgrade}
className="underline text-[#737373] hover:text-white transition-colors cursor-pointer"
>
Upgrade to Pro
</button>{" "}
to get
<br />
Supermemory Connections
</>
)}
</p>
<div className="space-y-2 text-[14px]">
<div className="flex items-center gap-2">
Expand Down
39 changes: 28 additions & 11 deletions apps/web/components/new/add-document/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import {
DropdownMenuTrigger,
} from "@repo/ui/components/dropdown-menu"
import { toast } from "sonner"
import { useDocumentMutations } from "./useDocumentMutations"
import { useDocumentMutations } from "../../../hooks/use-document-mutations"
import { useCustomer } from "autumn-js/react"
import { useMemoriesUsage } from "@/hooks/use-memories-usage"

type TabType = "note" | "link" | "file" | "connect"

Expand Down Expand Up @@ -132,6 +134,15 @@ export function AddDocument({
onClose,
})

const autumn = useCustomer()
const {
memoriesUsed,
memoriesLimit,
hasProProduct,
isLoading: isLoadingMemories,
usagePercent,
} = useMemoriesUsage(autumn)

useEffect(() => {
setLocalSelectedProject(globalSelectedProject)
}, [globalSelectedProject])
Expand Down Expand Up @@ -242,7 +253,7 @@ export function AddDocument({
noteMutation.isPending || linkMutation.isPending || fileMutation.isPending

return (
<div className="h-full flex flex-row text-white space-x-6">
<div className="h-full flex flex-row text-white space-x-5">
<div className="w-1/3 flex flex-col justify-between">
<div className="flex flex-col gap-1">
{tabs.map((tab) => (
Expand All @@ -266,7 +277,7 @@ export function AddDocument({
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}}
>
<div className="flex justify-between items-center mb-2">
<div className="flex justify-between items-center">
<span
className={cn(
"text-white text-[16px] font-medium",
Expand All @@ -276,19 +287,25 @@ export function AddDocument({
Memories
</span>
<span className={cn("text-[#737373] text-sm", dmSansClassName())}>
120/200
{isLoadingMemories
? "…"
: hasProProduct
? "Unlimited"
: `${memoriesUsed}/${memoriesLimit}`}
</span>
</div>
<div className="h-1.5 bg-[#0D121A] rounded-full overflow-hidden">
<div
className="h-full bg-[#2261CA] rounded-full"
style={{ width: "60%" }}
/>
</div>
{!hasProProduct && (
<div className="h-1.5 bg-[#0D121A] rounded-full overflow-hidden mt-2">
<div
className="h-full bg-[#2261CA] rounded-full"
style={{ width: `${usagePercent}%` }}
/>
</div>
)}
</div>
</div>

<div className="w-2/3 overflow-auto flex flex-col justify-between">
<div className="w-2/3 overflow-auto flex flex-col justify-between px-1 scrollbar-thin">
{activeTab === "note" && (
<NoteContent
onSubmit={handleNoteSubmit}
Expand Down
Loading
Loading