aboutsummaryrefslogtreecommitdiff
path: root/apps/web/components/views/add-memory
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/components/views/add-memory')
-rw-r--r--apps/web/components/views/add-memory/action-buttons.tsx118
-rw-r--r--apps/web/components/views/add-memory/fixed-mutation.tsx244
-rw-r--r--apps/web/components/views/add-memory/index.tsx417
-rw-r--r--apps/web/components/views/add-memory/memory-usage-ring.tsx23
-rw-r--r--apps/web/components/views/add-memory/project-selection.tsx164
-rw-r--r--apps/web/components/views/add-memory/tab-button.tsx42
-rw-r--r--apps/web/components/views/add-memory/text-editor.tsx22
7 files changed, 399 insertions, 631 deletions
diff --git a/apps/web/components/views/add-memory/action-buttons.tsx b/apps/web/components/views/add-memory/action-buttons.tsx
index 6dc49304..596531b3 100644
--- a/apps/web/components/views/add-memory/action-buttons.tsx
+++ b/apps/web/components/views/add-memory/action-buttons.tsx
@@ -1,67 +1,67 @@
-import { Button } from '@repo/ui/components/button';
-import { Loader2, type LucideIcon } from 'lucide-react';
-import { motion } from 'motion/react';
+import { Button } from "@repo/ui/components/button";
+import { Loader2, type LucideIcon } from "lucide-react";
+import { motion } from "motion/react";
interface ActionButtonsProps {
- onCancel: () => void;
- onSubmit?: () => void;
- submitText: string;
- submitIcon?: LucideIcon;
- isSubmitting?: boolean;
- isSubmitDisabled?: boolean;
- submitType?: 'button' | 'submit';
- className?: string;
+ onCancel: () => void;
+ onSubmit?: () => void;
+ submitText: string;
+ submitIcon?: LucideIcon;
+ isSubmitting?: boolean;
+ isSubmitDisabled?: boolean;
+ submitType?: "button" | "submit";
+ className?: string;
}
export function ActionButtons({
- onCancel,
- onSubmit,
- submitText,
- submitIcon: SubmitIcon,
- isSubmitting = false,
- isSubmitDisabled = false,
- submitType = 'submit',
- className = '',
+ onCancel,
+ onSubmit,
+ submitText,
+ submitIcon: SubmitIcon,
+ isSubmitting = false,
+ isSubmitDisabled = false,
+ submitType = "submit",
+ className = "",
}: ActionButtonsProps) {
- return (
- <div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
- <Button
- className="hover:bg-white/10 text-white border-none flex-1 sm:flex-initial"
- onClick={onCancel}
- type="button"
- variant="ghost"
- >
- Cancel
- </Button>
+ return (
+ <div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
+ <Button
+ className="hover:bg-white/10 text-white border-none flex-1 sm:flex-initial"
+ onClick={onCancel}
+ type="button"
+ variant="ghost"
+ >
+ Cancel
+ </Button>
- <motion.div
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- className="flex-1 sm:flex-initial"
- >
- <Button
- className="bg-white/10 hover:bg-white/20 text-white border-white/20 w-full"
- disabled={isSubmitting || isSubmitDisabled}
- onClick={submitType === 'button' ? onSubmit : undefined}
- type={submitType}
- >
- {isSubmitting ? (
- <>
- <Loader2 className="h-4 w-4 animate-spin mr-2" />
- {submitText.includes('Add')
- ? 'Adding...'
- : submitText.includes('Upload')
- ? 'Uploading...'
- : 'Processing...'}
- </>
- ) : (
- <>
- {SubmitIcon && <SubmitIcon className="h-4 w-4 mr-2" />}
- {submitText}
- </>
- )}
- </Button>
- </motion.div>
- </div>
- );
+ <motion.div
+ className="flex-1 sm:flex-initial"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20 w-full"
+ disabled={isSubmitting || isSubmitDisabled}
+ onClick={submitType === "button" ? onSubmit : undefined}
+ type={submitType}
+ >
+ {isSubmitting ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ {submitText.includes("Add")
+ ? "Adding..."
+ : submitText.includes("Upload")
+ ? "Uploading..."
+ : "Processing..."}
+ </>
+ ) : (
+ <>
+ {SubmitIcon && <SubmitIcon className="h-4 w-4 mr-2" />}
+ {submitText}
+ </>
+ )}
+ </Button>
+ </motion.div>
+ </div>
+ );
}
diff --git a/apps/web/components/views/add-memory/fixed-mutation.tsx b/apps/web/components/views/add-memory/fixed-mutation.tsx
deleted file mode 100644
index c71793b5..00000000
--- a/apps/web/components/views/add-memory/fixed-mutation.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-import { $fetch } from "@lib/api"
-import { useMutation } from "@tanstack/react-query"
-import { toast } from "sonner"
-
-// Simplified mutation that doesn't block UI with polling
-export const createSimpleAddContentMutation = (
- queryClient: any,
- onClose?: () => void,
-) => {
- return useMutation({
- mutationFn: async ({
- content,
- project,
- contentType,
- }: {
- content: string
- project: string
- contentType: "note" | "link"
- }) => {
- // Just create the memory, don't wait for processing
- const response = await $fetch("@post/documents", {
- body: {
- content: content,
- containerTags: [project],
- metadata: {
- sm_source: "consumer",
- },
- },
- })
-
- if (response.error) {
- throw new Error(
- response.error?.message || `Failed to add ${contentType}`,
- )
- }
-
- return { id: response.data.id, contentType }
- },
- onMutate: async ({ content, project, contentType }) => {
- // 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
- const tempId = `temp-${Date.now()}`
- const optimisticMemory = {
- id: tempId,
- 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: "processing",
- type: contentType,
- metadata: {
- processingStage: "queued",
- processingMessage: "Added to processing queue",
- },
- memoryEntries: [],
- isOptimistic: true,
- }
-
- // Optimistically update to include the new memory
- queryClient.setQueryData(
- ["documents-with-memories", project],
- (old: any) => {
- // Handle infinite query structure
- if (old?.pages) {
- const newPages = [...old.pages]
- if (newPages.length > 0) {
- // Add to the first page
- const firstPage = { ...newPages[0] }
- firstPage.documents = [
- optimisticMemory,
- ...(firstPage.documents || []),
- ]
- newPages[0] = firstPage
- } else {
- // No pages yet, create the first page
- newPages.push({
- documents: [optimisticMemory],
- pagination: { currentPage: 1, totalPages: 1, totalCount: 1 },
- totalCount: 1,
- })
- }
-
- return {
- ...old,
- pages: newPages,
- }
- }
- // Fallback for regular query structure
- const newData = old
- ? {
- ...old,
- documents: [optimisticMemory, ...(old.documents || [])],
- totalCount: (old.totalCount || 0) + 1,
- }
- : { documents: [optimisticMemory], totalCount: 1 }
- return newData
- },
- )
-
- return { previousMemories, optimisticId: tempId }
- },
- onError: (error, variables, context) => {
- // Roll back on error
- if (context?.previousMemories) {
- queryClient.setQueryData(
- ["documents-with-memories", variables.project],
- context.previousMemories,
- )
- }
- toast.error(`Failed to add ${variables.contentType}`, {
- description: error instanceof Error ? error.message : "Unknown error",
- })
- },
- onSuccess: (data, variables, context) => {
- // Show success message
- toast.success(
- `${variables.contentType === "link" ? "Link" : "Note"} created successfully!`,
- )
-
- // Close modal
- onClose?.()
-
- // Start polling for this specific memory ID
- // The polling will happen in the background and update the optimistic memory when done
- startMemoryPolling(
- data.id,
- variables.project,
- context?.optimisticId,
- queryClient,
- )
- },
- })
-}
-
-// Background polling function
-const startMemoryPolling = (
- memoryId: string,
- project: string,
- optimisticId: string | undefined,
- queryClient: any,
-) => {
- const pollMemory = async () => {
- try {
- const memory = await $fetch(`@get/documents/${memoryId}`)
-
- if (memory.error) {
- console.error("Failed to fetch memory status:", memory.error)
- return false
- }
-
- const isComplete =
- memory.data?.status === "done" ||
- memory.data?.content ||
- memory.data?.memoryEntries?.length > 0
-
- if (isComplete) {
- // Replace optimistic memory with real data
- queryClient.setQueryData(
- ["documents-with-memories", project],
- (old: any) => {
- if (old?.pages) {
- // Handle infinite query structure
- const newPages = old.pages.map((page: any) => ({
- ...page,
- documents: page.documents.map((doc: any) => {
- if (doc.isOptimistic || doc.id === optimisticId) {
- // Replace with real memory
- return {
- ...memory.data,
- isOptimistic: false,
- }
- }
- return doc
- }),
- }))
-
- return {
- ...old,
- pages: newPages,
- }
- }
- // Handle regular query structure
- return {
- ...old,
- documents: old.documents.map((doc: any) => {
- if (doc.isOptimistic || doc.id === optimisticId) {
- return {
- ...memory.data,
- isOptimistic: false,
- }
- }
- return doc
- }),
- }
- },
- )
- return true // Stop polling
- }
-
- return false // Continue polling
- } catch (error) {
- console.error("Error polling memory:", error)
- return false // Continue polling
- }
- }
-
- // Poll every 3 seconds, max 60 attempts (3 minutes)
- let attempts = 0
- const maxAttempts = 60
-
- const poll = async () => {
- if (attempts >= maxAttempts) {
- console.log("Memory polling timed out")
- return
- }
-
- const isComplete = await pollMemory()
- attempts++
-
- if (!isComplete && attempts < maxAttempts) {
- setTimeout(poll, 3000) // Poll again in 3 seconds
- }
- }
-
- // Start polling after a short delay
- setTimeout(poll, 2000)
-}
diff --git a/apps/web/components/views/add-memory/index.tsx b/apps/web/components/views/add-memory/index.tsx
index 5116e952..96bb0a8b 100644
--- a/apps/web/components/views/add-memory/index.tsx
+++ b/apps/web/components/views/add-memory/index.tsx
@@ -1,9 +1,9 @@
-import { $fetch } from "@lib/api";
+import { $fetch } from "@lib/api"
import {
fetchConsumerProProduct,
fetchMemoriesFeature,
-} from "@repo/lib/queries";
-import { Button } from "@repo/ui/components/button";
+} from "@repo/lib/queries"
+import { Button } from "@repo/ui/components/button"
import {
Dialog,
DialogContent,
@@ -11,18 +11,19 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from "@repo/ui/components/dialog";
-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";
+} from "@repo/ui/components/dialog"
+import { Input } from "@repo/ui/components/input"
+import { Label } from "@repo/ui/components/label"
+import { Textarea } from "@repo/ui/components/textarea"
+import type { GetMemoryResponseSchema } from "@repo/validation/api"
+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";
+} from "@ui/components/shadcn-io/dropzone"
+import { useCustomer } from "autumn-js/react"
import {
Brain,
FileIcon,
@@ -31,19 +32,19 @@ import {
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";
-import { TabButton } from "./tab-button";
+} 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"
+import { TabButton } from "./tab-button"
const TextEditor = dynamic(
() => import("./text-editor").then((mod) => ({ default: mod.TextEditor })),
@@ -64,91 +65,100 @@ const TextEditor = dynamic(
),
ssr: false,
},
-);
+)
// Simple function to extract plain text title from HTML content
const getPlainTextTitle = (htmlContent: string) => {
- const temp = document.createElement("div");
- temp.innerHTML = htmlContent;
- const plainText = temp.textContent || temp.innerText || htmlContent;
- const firstLine = plainText.split("\n")[0].trim();
+ const temp = document.createElement("div")
+ temp.innerHTML = htmlContent
+ const plainText = temp.textContent || temp.innerText || htmlContent
+
+ if (!plainText) {
+ return "Untitled"
+ }
+ const firstLine = plainText.split("\n")[0]?.trim()
+
+ if (!firstLine) {
+ return plainText.substring(0, 100)
+ }
+
return firstLine.length > 0
? firstLine.substring(0, 100)
- : plainText.substring(0, 100);
-};
+ : plainText.substring(0, 100)
+}
export function AddMemoryView({
onClose,
initialTab = "note",
}: {
- onClose?: () => void;
- initialTab?: "note" | "link" | "file" | "connect";
+ onClose?: () => void
+ initialTab?: "note" | "link" | "file" | "connect"
}) {
- const queryClient = useQueryClient();
- const { selectedProject, setSelectedProject } = useProject();
- const [showAddDialog, setShowAddDialog] = useState(true);
- const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
+ const queryClient = useQueryClient()
+ const { selectedProject, setSelectedProject } = useProject()
+ const [showAddDialog, setShowAddDialog] = useState(true)
+ const [selectedFiles, setSelectedFiles] = useState<File[]>([])
const [activeTab, setActiveTab] = useState<
"note" | "link" | "file" | "connect"
- >(initialTab);
- const autumn = useCustomer();
- const [showCreateProjectDialog, setShowCreateProjectDialog] = useState(false);
- const [newProjectName, setNewProjectName] = useState("");
+ >(initialTab)
+ const autumn = useCustomer()
+ const [showCreateProjectDialog, setShowCreateProjectDialog] = useState(false)
+ const [newProjectName, setNewProjectName] = useState("")
// Check memory limits
- const { data: memoriesCheck } = fetchMemoriesFeature(autumn);
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn)
- const memoriesUsed = memoriesCheck?.usage ?? 0;
- const memoriesLimit = memoriesCheck?.included_usage ?? 0;
+ 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");
+ const response = await $fetch("@get/projects")
if (response.error) {
- throw new Error(response.error?.message || "Failed to load projects");
+ throw new Error(response.error?.message || "Failed to load projects")
}
- return response.data?.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");
+ throw new Error(response.error?.message || "Failed to create project")
}
- return response.data;
+ return response.data
},
onSuccess: (data) => {
- analytics.projectCreated();
- toast.success("Project created successfully!");
- setShowCreateProjectDialog(false);
- setNewProjectName("");
- queryClient.invalidateQueries({ queryKey: ["projects"] });
+ 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);
+ setSelectedProject(data.containerTag)
// Update form values
- addContentForm.setFieldValue("project", data.containerTag);
- fileUploadForm.setFieldValue("project", data.containerTag);
+ 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: {
@@ -160,8 +170,8 @@ export function AddMemoryView({
content: value.content,
project: value.project,
contentType: activeTab as "note" | "link",
- });
- formApi.reset();
+ })
+ formApi.reset()
},
validators: {
onChange: z.object({
@@ -169,19 +179,19 @@ export function AddMemoryView({
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");
+ const currentValue = addContentForm.getFieldValue("content")
if (currentValue) {
- addContentForm.validateField("content", "change");
+ addContentForm.validateField("content", "change")
}
}
- }, [activeTab]);
+ }, [activeTab])
// Form for file upload metadata
const fileUploadForm = useForm({
@@ -192,8 +202,8 @@ export function AddMemoryView({
},
onSubmit: async ({ value, formApi }) => {
if (selectedFiles.length === 0) {
- toast.error("Please select a file to upload");
- return;
+ toast.error("Please select a file to upload")
+ return
}
for (const file of selectedFiles) {
@@ -202,13 +212,13 @@ export function AddMemoryView({
title: value.title || undefined,
description: value.description || undefined,
project: value.project,
- });
+ })
}
- formApi.reset();
- setSelectedFiles([]);
+ formApi.reset()
+ setSelectedFiles([])
},
- });
+ })
const addContentMutation = useMutation({
mutationFn: async ({
@@ -216,11 +226,11 @@ export function AddMemoryView({
project,
contentType,
}: {
- content: string;
- project: string;
- contentType: "note" | "link";
+ content: string
+ project: string
+ contentType: "note" | "link"
}) => {
- console.log("📤 Creating memory...");
+ console.log("📤 Creating memory...")
const response = await $fetch("@post/documents", {
body: {
@@ -230,34 +240,34 @@ export function AddMemoryView({
sm_source: "consumer",
},
},
- });
+ })
if (response.error) {
throw new Error(
response.error?.message || `Failed to add ${contentType}`,
- );
+ )
}
- console.log("✅ Memory created:", response.data);
- return response.data;
+ console.log("✅ Memory created:", response.data)
+ return response.data
},
onMutate: async ({ content, project, contentType }) => {
- console.log("🚀 OPTIMISTIC UPDATE: Starting for", contentType);
+ console.log("🚀 OPTIMISTIC UPDATE: Starting for", contentType)
// Cancel queries to prevent conflicts
await queryClient.cancelQueries({
queryKey: ["documents-with-memories", project],
- });
- console.log("📞 QUERIES CANCELLED for project:", project);
+ })
+ console.log("📞 QUERIES CANCELLED for project:", project)
// Get previous data for rollback
const previousMemories = queryClient.getQueryData([
"documents-with-memories",
project,
- ]);
+ ])
// Create optimistic memory with proper title
- const tempId = `temp-${Date.now()}`;
+ const tempId = `temp-${Date.now()}`
const optimisticMemory = {
id: tempId,
content: contentType === "link" ? "" : content,
@@ -270,9 +280,9 @@ export function AddMemoryView({
updatedAt: new Date().toISOString(),
memoryEntries: [],
isOptimistic: true,
- };
+ }
- console.log("🎯 Adding optimistic memory:", optimisticMemory);
+ console.log("🎯 Adding optimistic memory:", optimisticMemory)
// Add to cache optimistically
queryClient.setQueryData(
@@ -280,19 +290,19 @@ export function AddMemoryView({
(old: any) => {
if (old?.pages) {
// Infinite query structure
- const newPages = [...old.pages];
+ const newPages = [...old.pages]
if (newPages.length > 0) {
newPages[0] = {
...newPages[0],
documents: [optimisticMemory, ...(newPages[0].documents || [])],
- };
+ }
} else {
newPages.push({
documents: [optimisticMemory],
pagination: { currentPage: 1, totalPages: 1, totalCount: 1 },
- });
+ })
}
- return { ...old, pages: newPages };
+ return { ...old, pages: newPages }
}
// Regular query structure
@@ -302,44 +312,46 @@ export function AddMemoryView({
documents: [optimisticMemory, ...(old.documents || [])],
totalCount: (old.totalCount || 0) + 1,
}
- : { documents: [optimisticMemory], totalCount: 1 };
+ : { documents: [optimisticMemory], totalCount: 1 }
},
- );
+ )
- return { previousMemories, optimisticId: tempId };
+ return { previousMemories, optimisticId: tempId }
},
onError: (error, variables, context) => {
- console.log("❌ Mutation failed, rolling back");
+ console.log("❌ Mutation failed, rolling back")
if (context?.previousMemories) {
queryClient.setQueryData(
["documents-with-memories", variables.project],
context.previousMemories,
- );
+ )
}
- toast.error(`Failed to add ${variables.contentType}`);
+ toast.error(`Failed to add ${variables.contentType}`)
},
onSuccess: (data, variables, context) => {
console.log(
"✅ Mutation succeeded, starting simple polling for memory:",
data.id,
- );
+ )
analytics.memoryAdded({
type: variables.contentType === "link" ? "link" : "note",
project_id: variables.project,
content_length: variables.content.length,
- });
+ })
toast.success(
`${variables.contentType === "link" ? "Link" : "Note"} added successfully!`,
- );
- setShowAddDialog(false);
- onClose?.();
+ )
+ setShowAddDialog(false)
+ onClose?.()
// Simple polling to replace optimistic update when ready
const pollMemory = async () => {
try {
- const memory = await $fetch(`@get/documents/${data.id}`);
- console.log("🔍 Polling memory:", memory.data);
+ const memory = await $fetch<z.infer<typeof GetMemoryResponseSchema>>(
+ `@get/documents/${data.id}`,
+ )
+ console.log("🔍 Polling memory:", memory.data)
if (memory.data && !memory.error) {
// Check if memory has been processed (has memory entries, substantial content, and NOT untitled/processing)
@@ -347,17 +359,16 @@ export function AddMemoryView({
memory.data.title &&
!memory.data.title.toLowerCase().includes("untitled") &&
!memory.data.title.toLowerCase().includes("processing") &&
- memory.data.title.length > 3;
+ memory.data.title.length > 3
const isReady =
- memory.data.memoryEntries?.length > 0 ||
- (memory.data.content &&
- memory.data.content.length > 10 &&
- hasRealTitle);
+ memory.data.content &&
+ memory.data.content.length > 10 &&
+ hasRealTitle
console.log("📊 Memory ready check:", {
isReady,
- hasMemoryEntries: memory.data.memoryEntries?.length,
+ hasMemoryEntries: 0,
hasContent: memory.data.content?.length,
title: memory.data.title,
hasRealTitle,
@@ -367,10 +378,10 @@ export function AddMemoryView({
titleNotProcessing:
memory.data.title &&
!memory.data.title.toLowerCase().includes("processing"),
- });
+ })
if (isReady) {
- console.log("✅ Memory ready, replacing optimistic update");
+ console.log("✅ Memory ready, replacing optimistic update")
// Replace optimistic memory with real data
queryClient.setQueryData(
["documents-with-memories", variables.project],
@@ -380,48 +391,48 @@ export function AddMemoryView({
...page,
documents: page.documents.map((doc: any) => {
if (doc.isOptimistic && doc.id.startsWith("temp-")) {
- return { ...memory.data, isOptimistic: false };
+ return { ...memory.data, isOptimistic: false }
}
- return doc;
+ return doc
}),
- }));
- return { ...old, pages: newPages };
+ }))
+ return { ...old, pages: newPages }
}
- return old;
+ return old
},
- );
- return true; // Stop polling
+ )
+ return true // Stop polling
}
}
- return false; // Continue polling
+ return false // Continue polling
} catch (error) {
- console.error("❌ Error polling memory:", error);
- return false;
+ console.error("❌ Error polling memory:", error)
+ return false
}
- };
+ }
// Poll every 3 seconds for up to 30 attempts (90 seconds)
- let attempts = 0;
- const maxAttempts = 30;
+ let attempts = 0
+ const maxAttempts = 30
const poll = async () => {
if (attempts >= maxAttempts) {
- console.log("⚠️ Polling stopped after max attempts");
- return;
+ console.log("⚠️ Polling stopped after max attempts")
+ return
}
- const isDone = await pollMemory();
- attempts++;
+ const isDone = await pollMemory()
+ attempts++
if (!isDone && attempts < maxAttempts) {
- setTimeout(poll, 3000);
+ setTimeout(poll, 3000)
}
- };
+ }
// Start polling after 2 seconds
- setTimeout(poll, 2000);
+ setTimeout(poll, 2000)
},
- });
+ })
const fileUploadMutation = useMutation({
mutationFn: async ({
@@ -430,10 +441,10 @@ export function AddMemoryView({
description,
project,
}: {
- file: File;
- title?: string;
- description?: string;
- project: string;
+ file: File
+ title?: string
+ description?: string
+ project: string
}) => {
// TEMPORARILY DISABLED: Limit check disabled
// Check if user can add more memories
@@ -443,9 +454,9 @@ export function AddMemoryView({
// );
// }
- const formData = new FormData();
- formData.append("file", file);
- formData.append("containerTags", JSON.stringify([project]));
+ const formData = new FormData()
+ formData.append("file", file)
+ formData.append("containerTags", JSON.stringify([project]))
const response = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/documents/file`,
@@ -454,14 +465,14 @@ export function AddMemoryView({
body: formData,
credentials: "include",
},
- );
+ )
if (!response.ok) {
- const error = await response.json();
- throw new Error(error.error || "Failed to upload file");
+ const error = await response.json()
+ throw new Error(error.error || "Failed to upload file")
}
- const data = await response.json();
+ const data = await response.json()
// If we have metadata, we can update the document after creation
if (title || description) {
@@ -473,23 +484,23 @@ export function AddMemoryView({
sm_source: "consumer", // Use "consumer" source to bypass limits
},
},
- });
+ })
}
- return data;
+ 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 = {
@@ -509,7 +520,7 @@ export function AddMemoryView({
mimeType: file.type,
},
memoryEntries: [],
- };
+ }
// Optimistically update to include the new memory
queryClient.setQueryData(
@@ -518,41 +529,41 @@ export function AddMemoryView({
// Handle infinite query structure
if (old?.pages) {
// This is an infinite query - add to the first page
- const newPages = [...old.pages];
+ const newPages = [...old.pages]
if (newPages.length > 0) {
// Add to the first page
- const firstPage = { ...newPages[0] };
+ const firstPage = { ...newPages[0] }
firstPage.documents = [
optimisticMemory,
...(firstPage.documents || []),
- ];
- newPages[0] = firstPage;
+ ]
+ newPages[0] = firstPage
} else {
// No pages yet, create the first page
newPages.push({
documents: [optimisticMemory],
pagination: { currentPage: 1, totalPages: 1, totalCount: 1 },
totalCount: 1,
- });
+ })
}
return {
...old,
pages: newPages,
- };
+ }
}
// Fallback for regular query structure
- if (!old) return { documents: [optimisticMemory], totalCount: 1 };
+ 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 };
+ return { previousMemories }
},
// If the mutation fails, roll back to the previous value
onError: (error, variables, context) => {
@@ -560,11 +571,11 @@ export function AddMemoryView({
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({
@@ -572,18 +583,18 @@ export function AddMemoryView({
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?.();
+ })
+ setShowAddDialog(false)
+ onClose?.()
},
// Don't invalidate queries immediately - let optimistic updates work
// onSettled: () => {
// queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] })
// },
- });
+ })
return (
<AnimatePresence mode="wait">
@@ -591,8 +602,8 @@ export function AddMemoryView({
<Dialog
key="add-memory-dialog"
onOpenChange={(open) => {
- setShowAddDialog(open);
- if (!open) onClose?.();
+ setShowAddDialog(open)
+ if (!open) onClose?.()
}}
open={showAddDialog}
>
@@ -651,9 +662,9 @@ export function AddMemoryView({
<div className="space-y-4">
<form
onSubmit={(e) => {
- e.preventDefault();
- e.stopPropagation();
- addContentForm.handleSubmit();
+ e.preventDefault()
+ e.stopPropagation()
+ addContentForm.handleSubmit()
}}
>
<div className="grid gap-4">
@@ -669,9 +680,9 @@ export function AddMemoryView({
validators={{
onChange: ({ value }) => {
if (!value || value.trim() === "") {
- return "Note is required";
+ return "Note is required"
}
- return undefined;
+ return undefined
},
}}
>
@@ -753,9 +764,9 @@ export function AddMemoryView({
isSubmitDisabled={!addContentForm.state.canSubmit}
isSubmitting={addContentMutation.isPending}
onCancel={() => {
- setShowAddDialog(false);
- onClose?.();
- addContentForm.reset();
+ setShowAddDialog(false)
+ onClose?.()
+ addContentForm.reset()
}}
submitIcon={Plus}
submitText="Add Note"
@@ -769,9 +780,9 @@ export function AddMemoryView({
<div className="space-y-4">
<form
onSubmit={(e) => {
- e.preventDefault();
- e.stopPropagation();
- addContentForm.handleSubmit();
+ e.preventDefault()
+ e.stopPropagation()
+ addContentForm.handleSubmit()
}}
>
<div className="grid gap-4">
@@ -793,13 +804,13 @@ export function AddMemoryView({
validators={{
onChange: ({ value }) => {
if (!value || value.trim() === "") {
- return "Link is required";
+ return "Link is required"
}
try {
- new URL(value);
- return undefined;
+ new URL(value)
+ return undefined
} catch {
- return "Please enter a valid link";
+ return "Please enter a valid link"
}
},
}}
@@ -879,9 +890,9 @@ export function AddMemoryView({
isSubmitDisabled={!addContentForm.state.canSubmit}
isSubmitting={addContentMutation.isPending}
onCancel={() => {
- setShowAddDialog(false);
- onClose?.();
- addContentForm.reset();
+ setShowAddDialog(false)
+ onClose?.()
+ addContentForm.reset()
}}
submitIcon={Plus}
submitText="Add Link"
@@ -895,9 +906,9 @@ export function AddMemoryView({
<div className="space-y-4">
<form
onSubmit={(e) => {
- e.preventDefault();
- e.stopPropagation();
- fileUploadForm.handleSubmit();
+ e.preventDefault()
+ e.stopPropagation()
+ fileUploadForm.handleSubmit()
}}
>
<div className="grid gap-4">
@@ -1031,10 +1042,10 @@ export function AddMemoryView({
isSubmitDisabled={selectedFiles.length === 0}
isSubmitting={fileUploadMutation.isPending}
onCancel={() => {
- setShowAddDialog(false);
- onClose?.();
- fileUploadForm.reset();
- setSelectedFiles([]);
+ setShowAddDialog(false)
+ onClose?.()
+ fileUploadForm.reset()
+ setSelectedFiles([])
}}
submitIcon={UploadIcon}
submitText="Upload File"
@@ -1102,8 +1113,8 @@ export function AddMemoryView({
<Button
className="bg-white/5 hover:bg-white/10 border-white/10 text-white w-full sm:w-auto"
onClick={() => {
- setShowCreateProjectDialog(false);
- setNewProjectName("");
+ setShowCreateProjectDialog(false)
+ setNewProjectName("")
}}
type="button"
variant="outline"
@@ -1140,19 +1151,19 @@ export function AddMemoryView({
</Dialog>
)}
</AnimatePresence>
- );
+ )
}
export function AddMemoryExpandedView() {
- const [showDialog, setShowDialog] = useState(false);
+ const [showDialog, setShowDialog] = useState(false)
const [selectedTab, setSelectedTab] = useState<
"note" | "link" | "file" | "connect"
- >("note");
+ >("note")
const handleOpenDialog = (tab: "note" | "link" | "file" | "connect") => {
- setSelectedTab(tab);
- setShowDialog(true);
- };
+ setSelectedTab(tab)
+ setShowDialog(true)
+ }
return (
<>
@@ -1223,5 +1234,5 @@ export function AddMemoryExpandedView() {
/>
)}
</>
- );
+ )
}
diff --git a/apps/web/components/views/add-memory/memory-usage-ring.tsx b/apps/web/components/views/add-memory/memory-usage-ring.tsx
index f6fb9836..7dbf8563 100644
--- a/apps/web/components/views/add-memory/memory-usage-ring.tsx
+++ b/apps/web/components/views/add-memory/memory-usage-ring.tsx
@@ -1,7 +1,7 @@
interface MemoryUsageRingProps {
- memoriesUsed: number;
- memoriesLimit: number;
- className?: string;
+ memoriesUsed: number
+ memoriesLimit: number
+ className?: string
}
export function MemoryUsageRing({
@@ -9,10 +9,10 @@ export function MemoryUsageRing({
memoriesLimit,
className = "",
}: MemoryUsageRingProps) {
- const usagePercentage = memoriesUsed / memoriesLimit;
+ const usagePercentage = memoriesUsed / memoriesLimit
const strokeColor =
- memoriesUsed >= memoriesLimit * 0.8 ? "rgb(251 191 36)" : "rgb(34 197 94)";
- const circumference = 2 * Math.PI * 10;
+ memoriesUsed >= memoriesLimit * 0.8 ? "rgb(251 191 36)" : "rgb(34 197 94)"
+ const circumference = 2 * Math.PI * 10
return (
<div
@@ -20,27 +20,28 @@ export function MemoryUsageRing({
title={`${memoriesUsed} of ${memoriesLimit} memories used`}
>
<svg className="w-6 h-6 transform -rotate-90" viewBox="0 0 24 24">
+ <title>Memory Usage</title>
{/* Background circle */}
<circle
cx="12"
cy="12"
+ fill="none"
r="10"
stroke="rgb(255 255 255 / 0.1)"
strokeWidth="2"
- fill="none"
/>
{/* Progress circle */}
<circle
+ className="transition-all duration-300"
cx="12"
cy="12"
+ fill="none"
r="10"
stroke={strokeColor}
- strokeWidth="2"
- fill="none"
strokeDasharray={`${circumference}`}
strokeDashoffset={`${circumference * (1 - usagePercentage)}`}
- className="transition-all duration-300"
strokeLinecap="round"
+ strokeWidth="2"
/>
</svg>
@@ -49,5 +50,5 @@ export function MemoryUsageRing({
{memoriesUsed} / {memoriesLimit}
</div>
</div>
- );
+ )
}
diff --git a/apps/web/components/views/add-memory/project-selection.tsx b/apps/web/components/views/add-memory/project-selection.tsx
index f23768a3..b0f79792 100644
--- a/apps/web/components/views/add-memory/project-selection.tsx
+++ b/apps/web/components/views/add-memory/project-selection.tsx
@@ -1,94 +1,94 @@
import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@repo/ui/components/select';
-import { Plus } from 'lucide-react';
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@repo/ui/components/select";
+import { Plus } from "lucide-react";
interface Project {
- id?: string;
- containerTag: string;
- name: string;
+ id?: string;
+ containerTag: string;
+ name: string;
}
interface ProjectSelectionProps {
- projects: Project[];
- selectedProject: string;
- onProjectChange: (value: string) => void;
- onCreateProject: () => void;
- disabled?: boolean;
- isLoading?: boolean;
- className?: string;
- id?: string;
+ projects: Project[];
+ selectedProject: string;
+ onProjectChange: (value: string) => void;
+ onCreateProject: () => void;
+ disabled?: boolean;
+ isLoading?: boolean;
+ className?: string;
+ id?: string;
}
export function ProjectSelection({
- projects,
- selectedProject,
- onProjectChange,
- onCreateProject,
- disabled = false,
- isLoading = false,
- className = '',
- id = 'project-select',
+ projects,
+ selectedProject,
+ onProjectChange,
+ onCreateProject,
+ disabled = false,
+ isLoading = false,
+ className = "",
+ id = "project-select",
}: ProjectSelectionProps) {
- const handleValueChange = (value: string) => {
- if (value === 'create-new-project') {
- onCreateProject();
- } else {
- onProjectChange(value);
- }
- };
+ const handleValueChange = (value: string) => {
+ if (value === "create-new-project") {
+ onCreateProject();
+ } else {
+ onProjectChange(value);
+ }
+ };
- return (
- <Select
- key={`${id}-${selectedProject}`}
- disabled={isLoading || disabled}
- onValueChange={handleValueChange}
- value={selectedProject}
- >
- <SelectTrigger
- className={`bg-white/5 border-white/10 text-white ${className}`}
- id={id}
- >
- <SelectValue placeholder="Select a project" />
- </SelectTrigger>
- <SelectContent
- className="bg-black/90 backdrop-blur-xl border-white/10 z-[90]"
- position="popper"
- sideOffset={5}
- >
- <SelectItem
- className="text-white hover:bg-white/10"
- key="default"
- value="sm_project_default"
- >
- Default Project
- </SelectItem>
- {projects
- .filter((p) => p.containerTag !== 'sm_project_default' && p.id)
- .map((project) => (
- <SelectItem
- className="text-white hover:bg-white/10"
- key={project.id || project.containerTag}
- value={project.containerTag}
- >
- {project.name}
- </SelectItem>
- ))}
- <SelectItem
- className="text-white hover:bg-white/10 border-t border-white/10 mt-1"
- key="create-new"
- value="create-new-project"
- >
- <div className="flex items-center gap-2">
- <Plus className="h-4 w-4" />
- <span>Create new project</span>
- </div>
- </SelectItem>
- </SelectContent>
- </Select>
- );
+ return (
+ <Select
+ disabled={isLoading || disabled}
+ key={`${id}-${selectedProject}`}
+ onValueChange={handleValueChange}
+ value={selectedProject}
+ >
+ <SelectTrigger
+ className={`bg-white/5 border-white/10 text-white ${className}`}
+ id={id}
+ >
+ <SelectValue placeholder="Select a project" />
+ </SelectTrigger>
+ <SelectContent
+ className="bg-black/90 backdrop-blur-xl border-white/10 z-[90]"
+ position="popper"
+ sideOffset={5}
+ >
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key="default"
+ value="sm_project_default"
+ >
+ Default Project
+ </SelectItem>
+ {projects
+ .filter((p) => p.containerTag !== "sm_project_default" && p.id)
+ .map((project) => (
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key={project.id || project.containerTag}
+ value={project.containerTag}
+ >
+ {project.name}
+ </SelectItem>
+ ))}
+ <SelectItem
+ className="text-white hover:bg-white/10 border-t border-white/10 mt-1"
+ key="create-new"
+ value="create-new-project"
+ >
+ <div className="flex items-center gap-2">
+ <Plus className="h-4 w-4" />
+ <span>Create new project</span>
+ </div>
+ </SelectItem>
+ </SelectContent>
+ </Select>
+ );
}
diff --git a/apps/web/components/views/add-memory/tab-button.tsx b/apps/web/components/views/add-memory/tab-button.tsx
index 72dfbbd7..3afbdedf 100644
--- a/apps/web/components/views/add-memory/tab-button.tsx
+++ b/apps/web/components/views/add-memory/tab-button.tsx
@@ -1,28 +1,28 @@
-import type { LucideIcon } from 'lucide-react';
+import type { LucideIcon } from "lucide-react";
interface TabButtonProps {
- icon: LucideIcon;
- label: string;
- isActive: boolean;
- onClick: () => void;
+ icon: LucideIcon;
+ label: string;
+ isActive: boolean;
+ onClick: () => void;
}
export function TabButton({
- icon: Icon,
- label,
- isActive,
- onClick,
+ icon: Icon,
+ label,
+ isActive,
+ onClick,
}: TabButtonProps) {
- return (
- <button
- className={`flex items-center gap-1.5 text-xs sm:text-xs px-4 sm:px-3 py-2 sm:py-1 h-8 sm:h-6 rounded-sm transition-colors whitespace-nowrap min-w-0 ${
- isActive ? 'bg-white/10' : 'hover:bg-white/5'
- }`}
- onClick={onClick}
- type="button"
- >
- <Icon className="h-4 w-4 sm:h-3 sm:w-3" />
- {label}
- </button>
- );
+ return (
+ <button
+ className={`flex items-center gap-1.5 text-xs sm:text-xs px-4 sm:px-3 py-2 sm:py-1 h-8 sm:h-6 rounded-sm transition-colors whitespace-nowrap min-w-0 ${
+ isActive ? "bg-white/10" : "hover:bg-white/5"
+ }`}
+ onClick={onClick}
+ type="button"
+ >
+ <Icon className="h-4 w-4 sm:h-3 sm:w-3" />
+ {label}
+ </button>
+ );
}
diff --git a/apps/web/components/views/add-memory/text-editor.tsx b/apps/web/components/views/add-memory/text-editor.tsx
index 5a07b8f7..79b42aa8 100644
--- a/apps/web/components/views/add-memory/text-editor.tsx
+++ b/apps/web/components/views/add-memory/text-editor.tsx
@@ -399,8 +399,6 @@ export function TextEditor({
title: string;
}) => (
<Button
- variant="ghost"
- size="sm"
className={cn(
"h-8 w-8 !p-0 text-white/70 transition-all duration-200 rounded-sm",
"hover:bg-white/15 hover:text-white hover:scale-105",
@@ -408,8 +406,10 @@ export function TextEditor({
isActive && "bg-white/20 text-white",
)}
onMouseDown={onMouseDown}
+ size="sm"
title={title}
type="button"
+ variant="ghost"
>
<Icon
className={cn(
@@ -426,13 +426,20 @@ export function TextEditor({
<Slate
editor={editor}
initialValue={editorValue}
- onValueChange={handleSlateChange}
onSelectionChange={() => setSelection(editor.selection)}
+ onValueChange={handleSlateChange}
>
<Editable
+ className={cn(
+ "outline-none w-full h-full text-white placeholder:text-white/50",
+ disabled && "opacity-50 cursor-not-allowed",
+ )}
+ onBlur={onBlur}
+ onKeyDown={handleKeyDown}
+ placeholder={placeholder}
+ readOnly={disabled}
renderElement={renderElement}
renderLeaf={renderLeaf}
- placeholder={placeholder}
renderPlaceholder={({ children, attributes }) => {
return (
<div {...attributes} className="mt-2">
@@ -440,13 +447,6 @@ export function TextEditor({
</div>
);
}}
- onKeyDown={handleKeyDown}
- onBlur={onBlur}
- readOnly={disabled}
- className={cn(
- "outline-none w-full h-full text-white placeholder:text-white/50",
- disabled && "opacity-50 cursor-not-allowed",
- )}
style={{
minHeight: "11rem",
maxHeight: "15rem",