"use client"
import { $fetch } from "@lib/api"
import { fetchMemoriesFeature } from "@repo/lib/queries"
import { Button } from "@repo/ui/components/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@repo/ui/components/dialog"
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@repo/ui/components/tabs"
import { Input } from "@repo/ui/components/input"
import { Label } from "@repo/ui/components/label"
import { Textarea } from "@repo/ui/components/textarea"
import { useForm } from "@tanstack/react-form"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
} from "@ui/components/shadcn-io/dropzone"
import { useCustomer } from "autumn-js/react"
import {
Brain,
FileIcon,
Link as LinkIcon,
Loader2,
PlugIcon,
Plus,
UploadIcon,
} from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import dynamic from "next/dynamic"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import { z } from "zod"
import { analytics } from "@/lib/analytics"
import { useProject } from "@/stores"
import { ConnectionsTabContent } from "../connections-tab-content"
import { ActionButtons } from "./action-buttons"
import { MemoryUsageRing } from "./memory-usage-ring"
import { ProjectSelection } from "./project-selection"
const TextEditor = dynamic(
() => import("./text-editor").then((mod) => ({ default: mod.TextEditor })),
{
loading: () => (
),
ssr: false,
},
)
export function AddMemoryView({
onClose,
initialTab = "note",
}: {
onClose?: () => void
initialTab?: "note" | "link" | "file" | "connect"
}) {
const queryClient = useQueryClient()
const { selectedProject, setSelectedProject } = useProject()
const [showAddDialog, setShowAddDialog] = useState(true)
const [selectedFiles, setSelectedFiles] = useState([])
const [activeTab, setActiveTab] = useState<
"note" | "link" | "file" | "connect"
>(initialTab)
const autumn = useCustomer()
const [showCreateProjectDialog, setShowCreateProjectDialog] = useState(false)
const [newProjectName, setNewProjectName] = useState("")
// Check memory limits
const { data: memoriesCheck } = fetchMemoriesFeature(
autumn,
!autumn.isLoading,
)
const memoriesUsed = memoriesCheck?.usage ?? 0
const memoriesLimit = memoriesCheck?.included_usage ?? 0
// Fetch projects for the dropdown
const { data: projects = [], isLoading: isLoadingProjects } = useQuery({
queryKey: ["projects"],
queryFn: async () => {
const response = await $fetch("@get/projects")
if (response.error) {
throw new Error(response.error?.message || "Failed to load projects")
}
return response.data?.projects || []
},
staleTime: 30 * 1000,
})
// Create project mutation
const createProjectMutation = useMutation({
mutationFn: async (name: string) => {
const response = await $fetch("@post/projects", {
body: { name },
})
if (response.error) {
throw new Error(response.error?.message || "Failed to create project")
}
return response.data
},
onSuccess: (data) => {
analytics.projectCreated()
toast.success("Project created successfully!")
setShowCreateProjectDialog(false)
setNewProjectName("")
queryClient.invalidateQueries({ queryKey: ["projects"] })
// Set the newly created project as selected
if (data?.containerTag) {
setSelectedProject(data.containerTag)
// Update form values
addContentForm.setFieldValue("project", data.containerTag)
fileUploadForm.setFieldValue("project", data.containerTag)
}
},
onError: (error) => {
toast.error("Failed to create project", {
description: error instanceof Error ? error.message : "Unknown error",
})
},
})
const addContentForm = useForm({
defaultValues: {
content: "",
project: selectedProject || "sm_project_default",
},
onSubmit: async ({ value, formApi }) => {
addContentMutation.mutate({
content: value.content,
project: value.project,
contentType: activeTab as "note" | "link",
})
formApi.reset()
},
validators: {
onChange: z.object({
content: z.string().min(1, "Content is required"),
project: z.string(),
}),
},
})
// Re-validate content field when tab changes between note/link
// biome-ignore lint/correctness/useExhaustiveDependencies: It is what it is
useEffect(() => {
// Trigger validation of the content field when switching between note/link
if (activeTab === "note" || activeTab === "link") {
const currentValue = addContentForm.getFieldValue("content")
if (currentValue) {
addContentForm.validateField("content", "change")
}
}
}, [activeTab])
// Form for file upload metadata
const fileUploadForm = useForm({
defaultValues: {
title: "",
description: "",
project: selectedProject || "sm_project_default",
},
onSubmit: async ({ value, formApi }) => {
if (selectedFiles.length === 0) {
toast.error("Please select a file to upload")
return
}
for (const file of selectedFiles) {
fileUploadMutation.mutate({
file,
title: value.title || undefined,
description: value.description || undefined,
project: value.project,
})
}
formApi.reset()
setSelectedFiles([])
},
})
const addContentMutation = useMutation({
mutationFn: async ({
content,
project,
contentType,
}: {
content: string
project: string
contentType: "note" | "link"
}) => {
// close the modal
onClose?.()
const processingPromise = (async () => {
// First, create the memory
const response = await $fetch("@post/documents", {
body: {
content: content,
containerTags: [project],
metadata: {
sm_source: "consumer", // Use "consumer" source to bypass limits
},
},
})
if (response.error) {
throw new Error(
response.error?.message || `Failed to add ${contentType}`,
)
}
const memoryId = response.data.id
// Polling function to check status
const pollForCompletion = async (): Promise => {
let attempts = 0
const maxAttempts = 60 // Maximum 5 minutes (60 attempts * 5 seconds)
while (attempts < maxAttempts) {
try {
const memory = await $fetch<{ status: string; content: string }>(
`@get/documents/${memoryId}`,
)
if (memory.error) {
throw new Error(
memory.error?.message || "Failed to fetch memory status",
)
}
// Check if processing is complete
// Adjust this condition based on your API response structure
if (
memory.data?.status === "done" ||
// Sometimes the memory might be ready when it has content and no processing status
memory.data?.content
) {
return memory.data
}
// If still processing, wait and try again
await new Promise((resolve) => setTimeout(resolve, 5000)) // Wait 5 seconds
attempts++
} catch (error) {
console.error("Error polling memory status:", error)
// Don't throw immediately, retry a few times
if (attempts >= 3) {
throw new Error("Failed to check processing status")
}
await new Promise((resolve) => setTimeout(resolve, 5000))
attempts++
}
}
// If we've exceeded max attempts, throw an error
throw new Error(
"Memory processing timed out. Please check back later.",
)
}
// Wait for completion
const completedMemory = await pollForCompletion()
return completedMemory
})()
toast.promise(processingPromise, {
loading: "Processing...",
success: `${contentType === "link" ? "Link" : "Note"} created successfully!`,
error: (err) =>
`Failed to add ${contentType}: ${err instanceof Error ? err.message : "Unknown error"}`,
})
return processingPromise
},
onMutate: async ({ content, project, contentType }) => {
console.log("🚀 onMutate starting...")
// Cancel any outgoing refetches
await queryClient.cancelQueries({
queryKey: ["documents-with-memories", project],
})
console.log("✅ Cancelled queries")
// Snapshot the previous value
const previousMemories = queryClient.getQueryData([
"documents-with-memories",
project,
])
console.log("📸 Previous memories:", previousMemories)
// Create optimistic memory
const optimisticMemory = {
id: `temp-${Date.now()}`,
content: contentType === "link" ? "" : content,
url: contentType === "link" ? content : null,
title:
contentType === "link" ? "Processing..." : content.substring(0, 100),
description:
contentType === "link"
? "Extracting content..."
: "Processing content...",
containerTags: [project],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
status: "queued",
type: contentType,
metadata: {
processingStage: "queued",
processingMessage: "Added to processing queue",
},
memoryEntries: [],
isOptimistic: true,
}
console.log("🎯 Created optimistic memory:", optimisticMemory)
// Optimistically update to include the new memory
queryClient.setQueryData(
["documents-with-memories", project],
(old: any) => {
console.log("🔄 Old data:", old)
const newData = old
? {
...old,
documents: [optimisticMemory, ...(old.documents || [])],
totalCount: (old.totalCount || 0) + 1,
}
: { documents: [optimisticMemory], totalCount: 1 }
console.log("✨ New data:", newData)
return newData
},
)
console.log("✅ onMutate completed")
return { previousMemories, optimisticId: optimisticMemory.id }
},
// If the mutation fails, roll back to the previous value
onError: (_error, variables, context) => {
if (context?.previousMemories) {
queryClient.setQueryData(
["documents-with-memories", variables.project],
context.previousMemories,
)
}
},
onSuccess: (_data, variables) => {
analytics.memoryAdded({
type: variables.contentType === "link" ? "link" : "note",
project_id: variables.project,
content_length: variables.content.length,
})
queryClient.invalidateQueries({
queryKey: ["documents-with-memories", variables.project],
})
setTimeout(() => {
queryClient.invalidateQueries({
queryKey: ["documents-with-memories", variables.project],
})
}, 30000) // 30 seconds
setTimeout(() => {
queryClient.invalidateQueries({
queryKey: ["documents-with-memories", variables.project],
})
}, 120000) // 2 minutes
setShowAddDialog(false)
onClose?.()
},
})
const fileUploadMutation = useMutation({
mutationFn: async ({
file,
title,
description,
project,
}: {
file: File
title?: string
description?: string
project: string
}) => {
// TEMPORARILY DISABLED: Limit check disabled
// Check if user can add more memories
// if (!canAddMemory && !isProUser) {
// throw new Error(
// `Free plan limit reached (${memoriesLimit} memories). Upgrade to Pro for up to 500 memories.`,
// );
// }
const formData = new FormData()
formData.append("file", file)
formData.append("containerTags", JSON.stringify([project]))
formData.append(
"metadata",
JSON.stringify({
sm_source: "consumer",
}),
)
const response = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/documents/file`,
{
method: "POST",
body: formData,
credentials: "include",
},
)
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || "Failed to upload file")
}
const data = await response.json()
// If we have metadata, we can update the document after creation
if (title || description) {
await $fetch(`@patch/documents/${data.id}`, {
body: {
metadata: {
...(title && { title }),
...(description && { description }),
sm_source: "consumer", // Use "consumer" source to bypass limits
},
},
})
}
return data
},
// Optimistic update
onMutate: async ({ file, title, description, project }) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({
queryKey: ["documents-with-memories", project],
})
// Snapshot the previous value
const previousMemories = queryClient.getQueryData([
"documents-with-memories",
project,
])
// Create optimistic memory for the file
const optimisticMemory = {
id: `temp-file-${Date.now()}`,
content: "",
url: null,
title: title || file.name,
description: description || `Uploading ${file.name}...`,
containerTags: [project],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
status: "processing",
type: "file",
metadata: {
fileName: file.name,
fileSize: file.size,
mimeType: file.type,
},
memoryEntries: [],
}
// Optimistically update to include the new memory
queryClient.setQueryData(
["documents-with-memories", project],
(old: any) => {
if (!old) return { documents: [optimisticMemory], totalCount: 1 }
return {
...old,
documents: [optimisticMemory, ...(old.documents || [])],
totalCount: (old.totalCount || 0) + 1,
}
},
)
// Return a context object with the snapshotted value
return { previousMemories }
},
// If the mutation fails, roll back to the previous value
onError: (error, variables, context) => {
if (context?.previousMemories) {
queryClient.setQueryData(
["documents-with-memories", variables.project],
context.previousMemories,
)
}
toast.error("Failed to upload file", {
description: error instanceof Error ? error.message : "Unknown error",
})
},
onSuccess: (_data, variables) => {
analytics.memoryAdded({
type: "file",
project_id: variables.project,
file_size: variables.file.size,
file_type: variables.file.type,
})
toast.success("File uploaded successfully!", {
description: "Your file is being processed",
})
setShowAddDialog(false)
onClose?.()
},
// Always refetch after error or success
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] })
},
})
return (
{showAddDialog && (
)}
{/* Create Project Dialog */}
{showCreateProjectDialog && (
)}
)
}
export function AddMemoryExpandedView() {
const [showDialog, setShowDialog] = useState(false)
const [selectedTab, setSelectedTab] = useState<
"note" | "link" | "file" | "connect"
>("note")
const handleOpenDialog = (tab: "note" | "link" | "file" | "connect") => {
setSelectedTab(tab)
setShowDialog(true)
}
return (
<>
Save any webpage, article, or file to your memory
{showDialog && (
setShowDialog(false)}
/>
)}
>
)
}