mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
feat: simplify link tab and remove usage bar from add-memory modal
- Remove broken preview section from link tab (disabled title/description/image fields that were never sent to API) - Remove plan usage bar and upgrade CTA from modal sidebar (desktop + mobile) - Normalize URLs in parent handleLinkSubmit for consistent behavior
This commit is contained in:
parent
085c3dd8b3
commit
5051a2ad5b
2 changed files with 14 additions and 336 deletions
|
|
@ -14,9 +14,7 @@ import { FileContent, type FileData } from "./file"
|
|||
import { useProject } from "@/stores"
|
||||
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 { formatUsageNumber } from "@/lib/billing-utils"
|
||||
import { normalizeUrl } from "@/lib/url-helpers"
|
||||
import { SpaceSelector } from "../space-selector"
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import { addDocumentParam } from "@/lib/search-params"
|
||||
|
|
@ -124,16 +122,6 @@ export function AddDocument({
|
|||
onClose,
|
||||
})
|
||||
|
||||
const autumn = useCustomer()
|
||||
const {
|
||||
tokensUsed,
|
||||
searchesUsed,
|
||||
planUsagePct,
|
||||
hasPaidPlan,
|
||||
isLoading: isLoadingUsage,
|
||||
} = useTokenUsage(autumn)
|
||||
const [isUpgrading, setIsUpgrading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSelectedProject(globalSelectedProject)
|
||||
}, [globalSelectedProject])
|
||||
|
|
@ -158,11 +146,12 @@ export function AddDocument({
|
|||
|
||||
const handleLinkSubmit = useCallback(
|
||||
(data: LinkData) => {
|
||||
if (!data.url.trim()) {
|
||||
const normalizedUrl = normalizeUrl(data.url.trim())
|
||||
if (!normalizedUrl || normalizedUrl === "https://") {
|
||||
toast.error("Please enter a URL")
|
||||
return
|
||||
}
|
||||
linkMutation.mutate({ url: data.url, project: localSelectedProject })
|
||||
linkMutation.mutate({ url: normalizedUrl, project: localSelectedProject })
|
||||
},
|
||||
[linkMutation, localSelectedProject],
|
||||
)
|
||||
|
|
@ -309,161 +298,6 @@ export function AddDocument({
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<div className="mt-3 flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span
|
||||
className={cn(
|
||||
"text-[#FAFAFA] text-sm font-medium",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
Plan usage
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-medium tabular-nums",
|
||||
hasPaidPlan ? "text-[#4BA0FA]" : "text-[#737373]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
{isLoadingUsage
|
||||
? "…"
|
||||
: `${planUsagePct < 1 && planUsagePct > 0 ? "< 1" : Math.round(planUsagePct)}% used`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 w-full rounded-[40px] bg-[#2E353D] p-px overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-[40px]"
|
||||
style={{
|
||||
width: `${planUsagePct}%`,
|
||||
background:
|
||||
planUsagePct > 80
|
||||
? "#ef4444"
|
||||
: hasPaidPlan
|
||||
? "linear-gradient(to right, #4BA0FA 80%, #002757 100%)"
|
||||
: "#0054AD",
|
||||
}}
|
||||
title={`${formatUsageNumber(tokensUsed)} tokens · ${formatUsageNumber(searchesUsed)} queries`}
|
||||
/>
|
||||
</div>
|
||||
{!isLoadingUsage && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-xs text-[#737373] tabular-nums",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
{formatUsageNumber(tokensUsed)} tokens ·{" "}
|
||||
{formatUsageNumber(searchesUsed)} queries
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
<div data-testid="usage-counter" className="flex flex-col gap-3 mr-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span
|
||||
className={cn(
|
||||
"text-[#FAFAFA] text-sm font-medium",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
Plan usage
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-medium tabular-nums",
|
||||
hasPaidPlan ? "text-[#4BA0FA]" : "text-[#737373]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
{isLoadingUsage
|
||||
? "…"
|
||||
: `${planUsagePct < 1 && planUsagePct > 0 ? "< 1" : Math.round(planUsagePct)}% used`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 w-full rounded-[40px] bg-[#2E353D] p-px overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-[40px]"
|
||||
style={{
|
||||
width: `${planUsagePct}%`,
|
||||
background:
|
||||
planUsagePct > 80
|
||||
? "#ef4444"
|
||||
: hasPaidPlan
|
||||
? "linear-gradient(to right, #4BA0FA 80%, #002757 100%)"
|
||||
: "#0054AD",
|
||||
}}
|
||||
title={`${formatUsageNumber(tokensUsed)} tokens · ${formatUsageNumber(searchesUsed)} queries`}
|
||||
/>
|
||||
</div>
|
||||
{!isLoadingUsage && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-xs text-[#737373] tabular-nums",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
{formatUsageNumber(tokensUsed)} tokens ·{" "}
|
||||
{formatUsageNumber(searchesUsed)} queries
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hasPaidPlan && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setIsUpgrading(true)
|
||||
try {
|
||||
const result = await autumn.attach({
|
||||
planId: "api_pro",
|
||||
successUrl: `${window.location.origin}/settings#account`,
|
||||
})
|
||||
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)
|
||||
}
|
||||
}}
|
||||
disabled={isUpgrading}
|
||||
className={cn(
|
||||
"relative w-full h-9 rounded-[10px] flex items-center justify-center",
|
||||
"text-[#FAFAFA] font-medium text-[13px]",
|
||||
"disabled:opacity-60 disabled:cursor-not-allowed",
|
||||
"cursor-pointer transition-opacity hover:opacity-90",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%)",
|
||||
boxShadow:
|
||||
"1px 1px 2px 0px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20)",
|
||||
}}
|
||||
>
|
||||
{isUpgrading ? (
|
||||
<>
|
||||
<Loader2 className="size-3 animate-spin mr-1.5" />
|
||||
Upgrading…
|
||||
</>
|
||||
) : (
|
||||
"Upgrade to Pro"
|
||||
)}
|
||||
<div className="absolute inset-0 pointer-events-none rounded-[inherit] shadow-[inset_1px_1px_2px_1px_#1A88FF]" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -2,11 +2,8 @@
|
|||
|
||||
import { useState, useEffect } from "react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { Image as ImageIcon, Loader2 } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export interface LinkData {
|
||||
url: string
|
||||
|
|
@ -29,10 +26,6 @@ export function LinkContent({
|
|||
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
|
||||
|
||||
|
|
@ -45,90 +38,13 @@ export function LinkContent({
|
|||
) {
|
||||
normalizedUrl = `https://${normalizedUrl}`
|
||||
}
|
||||
onSubmit({ url: normalizedUrl, title, description })
|
||||
onSubmit({ url: normalizedUrl, title: "", description: "" })
|
||||
}
|
||||
}
|
||||
|
||||
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, image)
|
||||
}
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setTitle(newTitle)
|
||||
updateData(url, newTitle, description)
|
||||
}
|
||||
|
||||
const handleDescriptionChange = (newDescription: string) => {
|
||||
setDescription(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)
|
||||
}
|
||||
onDataChange?.({ url: newUrl, title: "", description: "" })
|
||||
}
|
||||
|
||||
useHotkeys("mod+enter", handleSubmit, {
|
||||
|
|
@ -140,9 +56,6 @@ export function LinkContent({
|
|||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setUrl("")
|
||||
setTitle("")
|
||||
setDescription("")
|
||||
setImage(undefined)
|
||||
onDataChange?.({ url: "", title: "", description: "" })
|
||||
}
|
||||
}, [isOpen, onDataChange])
|
||||
|
|
@ -155,83 +68,14 @@ export function LinkContent({
|
|||
>
|
||||
Paste a link to turn it into a memory
|
||||
</p>
|
||||
<div className="flex relative">
|
||||
<input
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => handleUrlChange(e.target.value)}
|
||||
placeholder="https://example.com"
|
||||
disabled={isSubmitting}
|
||||
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 || isPreviewLoading || !url.trim()}
|
||||
onClick={handlePreviewLink}
|
||||
>
|
||||
{isPreviewLoading ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin mr-2" />
|
||||
Loading…
|
||||
</>
|
||||
) : (
|
||||
"Preview Link"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#14161A] rounded-[14px] py-6 px-4 space-y-4 shadow-inside-out">
|
||||
<div>
|
||||
<p className="pl-2 pb-2 font-semibold text-[16px] text-[#737373]">
|
||||
Link title
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => handleTitleChange(e.target.value)}
|
||||
placeholder="Mahesh Sanikommu - Portfolio"
|
||||
disabled
|
||||
className="w-full px-4 py-3 bg-[#0F1217] rounded-xl 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 description
|
||||
</p>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => handleDescriptionChange(e.target.value)}
|
||||
placeholder="Portfolio website of Mahesh Sanikommu"
|
||||
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 Image
|
||||
</p>
|
||||
{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="size-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="size-8 text-[#737373]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => handleUrlChange(e.target.value)}
|
||||
placeholder="https://example.com/article"
|
||||
disabled={isSubmitting}
|
||||
className="w-full p-4 rounded-xl bg-[#14161A] shadow-inside-out disabled:opacity-50 outline-1 outline-transparent focus:outline-[#525D6EB2]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue