"use client" import { useState, useEffect, useCallback, useRef } from "react" import { useQueryState } from "nuqs" import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog" import { Drawer, DrawerContent, DrawerTitle } from "@repo/ui/components/drawer" import { cn } from "@lib/utils" import { dmSansClassName } from "@/lib/fonts" import { FileTextIcon, GlobeIcon, ZapIcon, Loader2 } from "lucide-react" import { Button } from "@ui/components/button" import { ConnectContent } from "./connections" import { NoteContent } from "./note" import { LinkContent, type LinkData } from "./link" 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 { SpaceSelector } from "../space-selector" import { useIsMobile } from "@hooks/use-mobile" import { addDocumentParam } from "@/lib/search-params" import { usePromoCode } from "@/hooks/use-promo-code" type TabType = "note" | "link" | "file" | "connect" interface AddDocumentModalProps { isOpen: boolean onClose: () => void } export function AddDocumentModal({ isOpen, onClose }: AddDocumentModalProps) { const isMobile = useIsMobile() if (isMobile) { return ( !open && onClose()} shouldScaleBackground > div:first-child]:bg-[#3A4252] [&>div:first-child]:h-1 [&>div:first-child]:w-9 [&>div:first-child]:mt-2.5 [&>div:first-child]:mb-1", dmSansClassName(), )} > Add Document
) } return ( !open && onClose()}> Add Document
) } const tabs = [ { id: "note" as const, icon: FileTextIcon, title: "Write a note", compactLabel: "Note", description: "Save your thoughts, notes and summaries, as memories", }, { id: "link" as const, icon: GlobeIcon, title: "Save a link", compactLabel: "Links", description: "Add any webpage into your searchable knowledge base", }, { id: "file" as const, icon: FileTextIcon, title: "Upload files", compactLabel: "Files", description: "Turn images, PDFs, documents, and markdown into memories", }, { id: "connect" as const, icon: ZapIcon, title: "Connect knowledge bases", compactLabel: "Connect", description: "Sync with Google Drive, Notion and OneDrive and import data", isPro: true, }, ] export function AddDocument({ onClose, isOpen, }: { onClose: () => void isOpen?: boolean }) { const isMobile = useIsMobile() const [addParam, setAddParam] = useQueryState("add", addDocumentParam) const activeTab: TabType = addParam ?? "note" const setActiveTab = useCallback( (tab: TabType) => { setAddParam(tab) }, [setAddParam], ) const { selectedProject: globalSelectedProject } = useProject() const [localSelectedProject, setLocalSelectedProject] = useState( globalSelectedProject, ) // Form data state for button click handling const [noteContent, setNoteContent] = useState("") const [linkData, setLinkData] = useState({ url: "", title: "", description: "", }) const [fileData, setFileData] = useState({ items: [], title: "", description: "", }) const fileDataRef = useRef(fileData) fileDataRef.current = fileData const { noteMutation, linkMutation, bulkLinkMutation, fileMutation } = useDocumentMutations({ onClose, }) const autumn = useCustomer() const promoCode = usePromoCode() const { tokensUsed, searchesUsed, planUsagePct, hasPaidPlan, isLoading: isLoadingUsage, } = useTokenUsage(autumn) const [isUpgrading, setIsUpgrading] = useState(false) useEffect(() => { setLocalSelectedProject(globalSelectedProject) }, [globalSelectedProject]) useEffect(() => { if (!isOpen) { setFileData({ items: [], title: "", description: "" }) setNoteContent("") setLinkData({ url: "", title: "", description: "" }) } }, [isOpen]) // Submit handlers const handleNoteSubmit = useCallback( (content: string) => { if (!content.trim()) { toast.error("Please enter some content") return } noteMutation.mutate({ content, project: localSelectedProject }) }, [noteMutation, localSelectedProject], ) const handleLinkSubmit = useCallback( (data: LinkData) => { // In bulk mode the selection is the source of truth, not the raw textarea. if (data.bulkUrls) { if (data.bulkUrls.length >= 2) { bulkLinkMutation.mutate({ urls: data.bulkUrls, project: localSelectedProject, }) return } const [onlyUrl] = data.bulkUrls if (onlyUrl) { linkMutation.mutate({ url: onlyUrl, project: localSelectedProject }) return } toast.error("Select at least one link") return } if (!data.url.trim()) { toast.error("Please enter a URL") return } linkMutation.mutate({ url: data.url, project: localSelectedProject }) }, [linkMutation, bulkLinkMutation, localSelectedProject], ) const handleFileSubmit = useCallback( async (data: FileData) => { const pending = data.items.filter((i) => i.status === "pending") if (pending.length === 0) { toast.error("Please add at least one file") return } const applyMeta = pending.length === 1 setFileData((prev) => ({ ...prev, items: prev.items.map((i) => i.status === "pending" ? { ...i, status: "uploading" as const } : i, ), })) try { const result = await fileMutation.mutateAsync({ fileEntries: pending.map((i) => ({ id: i.id, file: i.file })), title: applyMeta ? data.title || undefined : undefined, description: applyMeta ? data.description || undefined : undefined, project: localSelectedProject, }) setFileData((prev) => ({ ...prev, items: prev.items.map((i) => { if (i.status !== "uploading") return i const fail = result.failures.find((f) => f.id === i.id) if (fail) { return { ...i, status: "error" as const, errorMessage: fail.message, } } return { ...i, status: "success" as const } }), })) } catch { setFileData((prev) => ({ ...prev, items: prev.items.map((i) => i.status === "uploading" ? { ...i, status: "error" as const, errorMessage: "Upload failed", } : i, ), })) } }, [fileMutation, localSelectedProject], ) // Data change handlers const handleNoteContentChange = useCallback((content: string) => { setNoteContent(content) }, []) const handleLinkDataChange = useCallback((data: LinkData) => { setLinkData(data) }, []) const handleFileDataChange = useCallback((data: FileData) => { setFileData(data) }, []) const handleNoteImportLinks = useCallback( (urls: string[]) => { setLinkData({ url: urls.join("\n"), title: "", description: "" }) setActiveTab("link") }, [setActiveTab], ) const handleButtonClick = () => { switch (activeTab) { case "note": handleNoteSubmit(noteContent) break case "link": handleLinkSubmit(linkData) break case "file": void handleFileSubmit(fileData) break } } const isSubmitting = noteMutation.isPending || linkMutation.isPending || bulkLinkMutation.isPending || fileMutation.isPending const fileTabHasPending = fileData.items.some((i) => i.status === "pending") const fileTabSubmitDisabled = activeTab === "file" && (!fileTabHasPending || isSubmitting) const linkBulkCount = linkData.bulkUrls?.length ?? 0 const linkTabSubmitDisabled = activeTab === "link" && linkData.bulkUrls !== undefined && linkBulkCount === 0 const spaceSelector = ( setLocalSelectedProject(projects[0] ?? localSelectedProject) } variant="insideOut" compact={isMobile} triggerClassName={cn(isMobile && "h-12 shrink-0")} /> ) return (
{isMobile && !hasPaidPlan && (
)} {!isMobile && (
{tabs.map((tab) => ( setActiveTab(tab.id)} icon={tab.icon} title={tab.title} compactLabel={tab.compactLabel} description={tab.description} isPro={tab.isPro} /> ))}
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

)}
{!hasPaidPlan && ( )}
)}
{isMobile && (
{tabs.map((tab) => ( setActiveTab(tab.id)} icon={tab.icon} title={tab.title} compactLabel={tab.compactLabel} description={tab.description} isPro={tab.isPro} compact /> ))}
)}
{activeTab === "note" && ( )} {activeTab === "link" && ( )} {activeTab === "file" && ( { void handleFileSubmit(fileDataRef.current) }} isSubmitting={fileMutation.isPending} isOpen={isOpen} /> )} {activeTab === "connect" && ( )}
{!isMobile && spaceSelector}
{isMobile && spaceSelector} {!isMobile && ( )} {activeTab !== "connect" && ( )}
) } function TabButton({ active, onClick, icon: Icon, title, compactLabel, description, isPro, compact, }: { active: boolean onClick: () => void icon: React.ComponentType<{ className?: string }> title: string compactLabel?: string description: string isPro?: boolean compact?: boolean }) { if (compact) { return ( ) } return ( ) }