mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-19 07:42:43 +00:00
chore: quick bugs squash across the elements and added few more changes (#671)
This commit is contained in:
parent
34c58c37fd
commit
baf2848db4
19 changed files with 7174 additions and 96 deletions
|
|
@ -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"
|
||||
|
|
|
|||
156
apps/web/app/api/og/route.ts
Normal file
156
apps/web/app/api/og/route.ts
Normal file
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
@ -132,6 +134,15 @@ export function AddDocument({
|
|||
onClose,
|
||||
})
|
||||
|
||||
const autumn = useCustomer()
|
||||
const {
|
||||
memoriesUsed,
|
||||
memoriesLimit,
|
||||
hasProProduct,
|
||||
isLoading: isLoadingMemories,
|
||||
usagePercent,
|
||||
} = useMemoriesUsage(autumn)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSelectedProject(globalSelectedProject)
|
||||
}, [globalSelectedProject])
|
||||
|
|
@ -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) => (
|
||||
|
|
@ -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",
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,14 @@ import { cn } from "@lib/utils"
|
|||
import { Button } from "@ui/components/button"
|
||||
import { dmSansClassName } from "@/utils/fonts"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { Image as ImageIcon, Loader2 } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export interface LinkData {
|
||||
url: string
|
||||
title: string
|
||||
description: string
|
||||
image?: string
|
||||
}
|
||||
|
||||
interface LinkContentProps {
|
||||
|
|
@ -19,26 +22,50 @@ interface LinkContentProps {
|
|||
isOpen?: boolean
|
||||
}
|
||||
|
||||
export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: LinkContentProps) {
|
||||
export function LinkContent({
|
||||
onSubmit,
|
||||
onDataChange,
|
||||
isSubmitting,
|
||||
isOpen,
|
||||
}: LinkContentProps) {
|
||||
const [url, setUrl] = useState("")
|
||||
const [title, setTitle] = useState("")
|
||||
const [description, setDescription] = useState("")
|
||||
const [image, setImage] = useState<string | undefined>(undefined)
|
||||
const [isPreviewLoading, setIsPreviewLoading] = useState(false)
|
||||
|
||||
const canSubmit = url.trim().length > 0 && !isSubmitting
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (canSubmit && onSubmit) {
|
||||
onSubmit({ url, title, description })
|
||||
let normalizedUrl = url.trim()
|
||||
if (
|
||||
!normalizedUrl.startsWith("http://") &&
|
||||
!normalizedUrl.startsWith("https://")
|
||||
) {
|
||||
normalizedUrl = `https://${normalizedUrl}`
|
||||
}
|
||||
onSubmit({ url: normalizedUrl, title, description })
|
||||
}
|
||||
}
|
||||
|
||||
const updateData = (newUrl: string, newTitle: string, newDescription: string) => {
|
||||
onDataChange?.({ url: newUrl, title: newTitle, description: newDescription })
|
||||
const updateData = (
|
||||
newUrl: string,
|
||||
newTitle: string,
|
||||
newDescription: string,
|
||||
newImage?: string,
|
||||
) => {
|
||||
onDataChange?.({
|
||||
url: newUrl,
|
||||
title: newTitle,
|
||||
description: newDescription,
|
||||
...(newImage && { image: newImage }),
|
||||
})
|
||||
}
|
||||
|
||||
const handleUrlChange = (newUrl: string) => {
|
||||
setUrl(newUrl)
|
||||
updateData(newUrl, title, description)
|
||||
updateData(newUrl, title, description, image)
|
||||
}
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
|
|
@ -48,7 +75,60 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
|
|||
|
||||
const handleDescriptionChange = (newDescription: string) => {
|
||||
setDescription(newDescription)
|
||||
updateData(url, title, newDescription)
|
||||
updateData(url, title, newDescription, image)
|
||||
}
|
||||
|
||||
const handlePreviewLink = async () => {
|
||||
if (!url.trim()) {
|
||||
toast.error("Please enter a URL first")
|
||||
return
|
||||
}
|
||||
|
||||
let normalizedUrl = url.trim()
|
||||
if (
|
||||
!normalizedUrl.startsWith("http://") &&
|
||||
!normalizedUrl.startsWith("https://")
|
||||
) {
|
||||
normalizedUrl = `https://${normalizedUrl}`
|
||||
setUrl(normalizedUrl)
|
||||
updateData(normalizedUrl, title, description, image)
|
||||
}
|
||||
|
||||
setIsPreviewLoading(true)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/og?url=${encodeURIComponent(normalizedUrl)}`,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || "Failed to fetch preview")
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const newTitle = data.title || ""
|
||||
const newDescription = data.description || ""
|
||||
const newImage = data.image || undefined
|
||||
|
||||
setTitle(newTitle)
|
||||
setDescription(newDescription)
|
||||
setImage(newImage)
|
||||
updateData(url, newTitle, newDescription, newImage)
|
||||
|
||||
if (!newTitle && !newDescription && !newImage) {
|
||||
toast.info("No Open Graph data found for this URL")
|
||||
} else {
|
||||
toast.success("Preview loaded successfully")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Preview error:", error)
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to load preview",
|
||||
)
|
||||
} finally {
|
||||
setIsPreviewLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useHotkeys("mod+enter", handleSubmit, {
|
||||
|
|
@ -62,12 +142,13 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
|
|||
setUrl("")
|
||||
setTitle("")
|
||||
setDescription("")
|
||||
setImage(undefined)
|
||||
onDataChange?.({ url: "", title: "", description: "" })
|
||||
}
|
||||
}, [isOpen, onDataChange])
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col space-y-4 pt-4", dmSansClassName())}>
|
||||
<div className={cn("flex flex-col space-y-4 pt-4 mb-4", dmSansClassName())}>
|
||||
<div>
|
||||
<p
|
||||
className={cn("text-[16px] font-medium pl-2 pb-2", dmSansClassName())}
|
||||
|
|
@ -79,14 +160,24 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
|
|||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => handleUrlChange(e.target.value)}
|
||||
placeholder="https://maheshthedev.me"
|
||||
placeholder="https://example.com"
|
||||
disabled={isSubmitting}
|
||||
className={cn(
|
||||
"w-full p-4 rounded-xl bg-[#14161A] shadow-inside-out disabled:opacity-50",
|
||||
)}
|
||||
className="w-full p-4 rounded-xl bg-[#14161A] shadow-inside-out disabled:opacity-50 outline-1 outline-transparent focus:outline-[#525D6EB2]"
|
||||
/>
|
||||
<Button variant="linkPreview" className="absolute right-2 top-2" disabled={isSubmitting}>
|
||||
Preview Link
|
||||
<Button
|
||||
variant="linkPreview"
|
||||
className="absolute right-2 top-2"
|
||||
disabled={isSubmitting || isPreviewLoading || !url.trim()}
|
||||
onClick={handlePreviewLink}
|
||||
>
|
||||
{isPreviewLoading ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin mr-2" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
"Preview Link"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -100,8 +191,8 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
|
|||
value={title}
|
||||
onChange={(e) => handleTitleChange(e.target.value)}
|
||||
placeholder="Mahesh Sanikommu - Portfolio"
|
||||
disabled={isSubmitting}
|
||||
className="w-full px-4 py-3 bg-[#0F1217] rounded-xl disabled:opacity-50"
|
||||
disabled
|
||||
className="w-full px-4 py-3 bg-[#0F1217] rounded-xl disabled:opacity-50 outline-1 outline-transparent focus:outline-[#525D6EB2]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -112,17 +203,34 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
|
|||
value={description}
|
||||
onChange={(e) => handleDescriptionChange(e.target.value)}
|
||||
placeholder="Portfolio website of Mahesh Sanikommu"
|
||||
disabled={isSubmitting}
|
||||
className="w-full px-4 py-3 bg-[#0F1217] rounded-xl disabled:opacity-50"
|
||||
disabled
|
||||
className="w-full px-4 py-3 bg-[#0F1217] rounded-xl resize-none disabled:opacity-50 outline-1 outline-transparent focus:outline-[#525D6EB2]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="pl-2 pb-2 font-semibold text-[16px] text-[#737373]">
|
||||
Link Preview
|
||||
Link Preview Image
|
||||
</p>
|
||||
<div className="w-full px-4 py-3 bg-[#0F1217] rounded-xl">
|
||||
<p>{description || "Portfolio website of Mahesh Sanikommu"}</p>
|
||||
</div>
|
||||
{image ? (
|
||||
<div className="w-full max-w-md aspect-4/2 bg-[#0F1217] rounded-xl overflow-hidden">
|
||||
<img
|
||||
src={image}
|
||||
alt={title || "Link preview"}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = "none"
|
||||
e.currentTarget.parentElement?.classList.add("opacity-50")
|
||||
e.currentTarget.parentElement?.classList.add("flex")
|
||||
e.currentTarget.parentElement?.classList.add("items-center")
|
||||
e.currentTarget.parentElement?.classList.add("justify-center")
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full max-w-md aspect-4/2 bg-[#0F1217] opacity-50 rounded-xl flex items-center justify-center">
|
||||
<ImageIcon className="w-8 h-8 text-[#737373]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ interface NoteContentProps {
|
|||
isOpen?: boolean
|
||||
}
|
||||
|
||||
export function NoteContent({ onSubmit, onContentChange, isSubmitting, isOpen }: NoteContentProps) {
|
||||
export function NoteContent({
|
||||
onSubmit,
|
||||
onContentChange,
|
||||
isSubmitting,
|
||||
isOpen,
|
||||
}: NoteContentProps) {
|
||||
const [content, setContent] = useState("")
|
||||
|
||||
const canSubmit = content.trim().length > 0 && !isSubmitting
|
||||
|
|
@ -45,7 +50,7 @@ export function NoteContent({ onSubmit, onContentChange, isSubmitting, isOpen }:
|
|||
onChange={(e) => handleContentChange(e.target.value)}
|
||||
placeholder="Write your note here..."
|
||||
disabled={isSubmitting}
|
||||
className="w-full h-full p-4 mb-4! rounded-[14px] bg-[#14161A] shadow-inside-out resize-none disabled:opacity-50"
|
||||
className="w-full h-full p-4 mb-4! rounded-[14px] bg-[#14161A] shadow-inside-out resize-none disabled:opacity-50 outline-none"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import {
|
|||
FolderIcon,
|
||||
LogOut,
|
||||
Settings,
|
||||
Home,
|
||||
Code2,
|
||||
ExternalLink,
|
||||
} from "lucide-react"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { cn } from "@lib/utils"
|
||||
|
|
@ -31,6 +34,7 @@ import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
|
|||
import { useProjectMutations } from "@/hooks/use-project-mutations"
|
||||
import { useProject } from "@/stores"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import type { Project } from "@repo/lib/types"
|
||||
|
||||
interface HeaderProps {
|
||||
|
|
@ -69,19 +73,62 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
|
|||
return (
|
||||
<div className="flex p-4 justify-between items-center">
|
||||
<div className="flex items-center justify-center gap-4 z-10!">
|
||||
<div className="flex items-center">
|
||||
<Logo className="h-7" />
|
||||
{name && (
|
||||
<div className="flex flex-col items-start justify-center ml-2">
|
||||
<p className="text-[#8B8B8B] text-[11px] leading-tight">
|
||||
{userName}
|
||||
</p>
|
||||
<p className="text-white font-bold text-xl leading-none -mt-1">
|
||||
supermemory
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center rounded-lg px-2 py-1.5 -ml-2 cursor-pointer hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-colors"
|
||||
>
|
||||
<Logo className="h-7" />
|
||||
{name && (
|
||||
<div className="flex flex-col items-start justify-center ml-2">
|
||||
<p className="text-[#8B8B8B] text-[11px] leading-tight">
|
||||
{userName}
|
||||
</p>
|
||||
<p className="text-white font-bold text-xl leading-none -mt-1">
|
||||
supermemory
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="w-56 bg-[#0D121A] rounded-xl border-none p-1.5 ml-4 shadow-[0_0_20px_rgba(255,255,255,0.15)]"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
|
||||
>
|
||||
<Link href="/new">
|
||||
<Home className="h-4 w-4" />
|
||||
Home
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
|
||||
>
|
||||
<a
|
||||
href="https://console.supermemory.ai"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Code2 className="h-4 w-4" />
|
||||
Developer console
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
|
||||
>
|
||||
<a href="https://supermemory.ai" target="_blank" rel="noreferrer">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
supermemory.ai
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="self-stretch w-px bg-[#FFFFFF33]" />
|
||||
<div className="flex items-center gap-2">
|
||||
<p>📁 {projectName}</p>
|
||||
|
|
@ -212,7 +259,10 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
|
|||
<Settings className="h-4 w-4" />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => authClient.signOut()}>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
authClient.signOut()
|
||||
router.push("/login/new")
|
||||
}}>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Logout
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
import { dmSans125ClassName } from "@/utils/fonts"
|
||||
import { cn } from "@lib/utils"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { fetchMemoriesFeature, fetchSubscriptionStatus } from "@lib/queries"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
|
||||
import { useMemoriesUsage } from "@/hooks/use-memories-usage"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -82,26 +82,13 @@ export default function Account() {
|
|||
const [deleteConfirmText, setDeleteConfirmText] = useState("")
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
||||
|
||||
// Billing data
|
||||
const {
|
||||
data: status = {
|
||||
consumer_pro: { allowed: false, status: null },
|
||||
},
|
||||
memoriesUsed,
|
||||
memoriesLimit,
|
||||
hasProProduct,
|
||||
isLoading: isCheckingStatus,
|
||||
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
|
||||
|
||||
const proStatus = status.consumer_pro
|
||||
const hasProProduct = proStatus?.status !== null
|
||||
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(
|
||||
autumn,
|
||||
!autumn.isLoading && !isCheckingStatus,
|
||||
)
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 200
|
||||
|
||||
// Calculate progress percentage
|
||||
const usagePercent = Math.min((memoriesUsed / memoriesLimit) * 100, 100)
|
||||
usagePercent,
|
||||
} = useMemoriesUsage(autumn)
|
||||
|
||||
// Handlers
|
||||
const handleUpgrade = async () => {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function ActionButtons({
|
|||
className="flex-1 sm:flex-initial"
|
||||
>
|
||||
<Button
|
||||
className="w-full cursor-pointer"
|
||||
className="w-full cursor-pointer text-black dark:text-white"
|
||||
disabled={isSubmitting || isSubmitDisabled}
|
||||
onClick={submitType === "button" ? onSubmit : undefined}
|
||||
type={submitType}
|
||||
|
|
|
|||
|
|
@ -72,11 +72,11 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
|
|||
queryClient.setQueryData(
|
||||
["documents-with-memories", project],
|
||||
(old: DocumentsQueryData | undefined) => {
|
||||
if (!old) return { documents: [optimisticMemory], totalCount: 1 }
|
||||
const existingDocs = old?.documents ?? []
|
||||
return {
|
||||
...old,
|
||||
documents: [optimisticMemory, ...old.documents],
|
||||
totalCount: old.totalCount + 1,
|
||||
documents: [optimisticMemory, ...existingDocs],
|
||||
totalCount: (old?.totalCount ?? 0) + 1,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -155,11 +155,11 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
|
|||
queryClient.setQueryData(
|
||||
["documents-with-memories", project],
|
||||
(old: DocumentsQueryData | undefined) => {
|
||||
if (!old) return { documents: [optimisticMemory], totalCount: 1 }
|
||||
const existingDocs = old?.documents ?? []
|
||||
return {
|
||||
...old,
|
||||
documents: [optimisticMemory, ...old.documents],
|
||||
totalCount: old.totalCount + 1,
|
||||
documents: [optimisticMemory, ...existingDocs],
|
||||
totalCount: (old?.totalCount ?? 0) + 1,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -272,11 +272,11 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
|
|||
queryClient.setQueryData(
|
||||
["documents-with-memories", project],
|
||||
(old: DocumentsQueryData | undefined) => {
|
||||
if (!old) return { documents: [optimisticMemory], totalCount: 1 }
|
||||
const existingDocs = old?.documents ?? []
|
||||
return {
|
||||
...old,
|
||||
documents: [optimisticMemory, ...old.documents],
|
||||
totalCount: old.totalCount + 1,
|
||||
documents: [optimisticMemory, ...existingDocs],
|
||||
totalCount: (old?.totalCount ?? 0) + 1,
|
||||
}
|
||||
},
|
||||
)
|
||||
35
apps/web/hooks/use-memories-usage.ts
Normal file
35
apps/web/hooks/use-memories-usage.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { fetchMemoriesFeature, fetchSubscriptionStatus } from "@lib/queries"
|
||||
import type { useCustomer } from "autumn-js/react"
|
||||
|
||||
export function useMemoriesUsage(autumn: ReturnType<typeof useCustomer>) {
|
||||
const {
|
||||
data: status = {
|
||||
consumer_pro: { allowed: false, status: null },
|
||||
},
|
||||
isLoading: isCheckingStatus,
|
||||
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
|
||||
|
||||
const proStatus = status.consumer_pro
|
||||
const hasProProduct = proStatus?.status !== null
|
||||
|
||||
const { data: memoriesCheck, isLoading: isLoadingMemories } =
|
||||
fetchMemoriesFeature(autumn, !isCheckingStatus && !autumn.isLoading)
|
||||
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0
|
||||
|
||||
const isLoading = autumn.isLoading || isCheckingStatus || isLoadingMemories
|
||||
|
||||
const usagePercent =
|
||||
memoriesLimit <= 0
|
||||
? 0
|
||||
: Math.min(Math.max((memoriesUsed / memoriesLimit) * 100, 0), 100)
|
||||
|
||||
return {
|
||||
memoriesUsed,
|
||||
memoriesLimit,
|
||||
hasProProduct,
|
||||
isLoading,
|
||||
usagePercent,
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
"@react-router/fs-routes": "^7.6.2",
|
||||
"@react-router/node": "^7.6.2",
|
||||
"@react-router/serve": "^7.6.2",
|
||||
"@sentry/nextjs": "^10.22.0",
|
||||
"@sentry/nextjs": "^10.33.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-form": "^1.12.4",
|
||||
"@tanstack/react-query": "^5.90.14",
|
||||
|
|
@ -59,15 +59,15 @@
|
|||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"flubber": "^0.4.2",
|
||||
"framer-motion": "^12.23.12",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"is-hotkey": "^0.2.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"masonic": "^4.1.0",
|
||||
"motion": "^12.19.2",
|
||||
"motion": "^12.26.2",
|
||||
"next": "16.0.9",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.5.2",
|
||||
"open-graph-scraper": "^6.11.0",
|
||||
"pdfjs-dist": "5.4.296",
|
||||
"posthog-js": "^1.257.0",
|
||||
"random-word-slugs": "^0.1.7",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const buttonVariants = cva(
|
|||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
headers:
|
||||
"border-[#161F2C] border bg-gradient-to-b from-neutral-900 to-black !text-[14px]",
|
||||
"border-[#161F2C] border bg-gradient-to-b from-neutral-900 to-black !text-[14px] hover:from-neutral-800 hover:to-neutral-950 hover:border-[#2a3a4f] active:from-neutral-950 active:to-black transition-all",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue