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:
MaheshtheDev 2026-05-12 00:23:49 +00:00
parent 085c3dd8b3
commit 5051a2ad5b
2 changed files with 14 additions and 336 deletions

View file

@ -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

View file

@ -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>
)