mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
refetching logic change
This commit is contained in:
parent
dd3122a483
commit
ea17bc4451
3 changed files with 372 additions and 335 deletions
|
|
@ -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,18 @@ 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 { 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 +31,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,102 +64,91 @@ const TextEditor = dynamic(
|
|||
),
|
||||
ssr: false,
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
// // Processing status component
|
||||
// function ProcessingStatus({ status }: { status: string }) {
|
||||
// const statusConfig = {
|
||||
// queued: { color: "text-yellow-400", label: "Queued", icon: "⏳" },
|
||||
// extracting: { color: "text-blue-400", label: "Extracting", icon: "📤" },
|
||||
// chunking: { color: "text-indigo-400", label: "Chunking", icon: "✂️" },
|
||||
// embedding: { color: "text-purple-400", label: "Embedding", icon: "🧠" },
|
||||
// indexing: { color: "text-pink-400", label: "Indexing", icon: "📝" },
|
||||
// unknown: { color: "text-gray-400", label: "Processing", icon: "⚙️" },
|
||||
// }
|
||||
|
||||
// const config =
|
||||
// statusConfig[status as keyof typeof statusConfig] || statusConfig.unknown
|
||||
|
||||
// return (
|
||||
// <div className={`flex items-center gap-1 text-xs ${config.color}`}>
|
||||
// <span>{config.icon}</span>
|
||||
// <span>{config.label}</span>
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
// 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();
|
||||
return firstLine.length > 0
|
||||
? firstLine.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: {
|
||||
|
|
@ -171,8 +160,8 @@ export function AddMemoryView({
|
|||
content: value.content,
|
||||
project: value.project,
|
||||
contentType: activeTab as "note" | "link",
|
||||
})
|
||||
formApi.reset()
|
||||
});
|
||||
formApi.reset();
|
||||
},
|
||||
validators: {
|
||||
onChange: z.object({
|
||||
|
|
@ -180,19 +169,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({
|
||||
|
|
@ -203,8 +192,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) {
|
||||
|
|
@ -213,13 +202,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 ({
|
||||
|
|
@ -227,191 +216,212 @@ export function AddMemoryView({
|
|||
project,
|
||||
contentType,
|
||||
}: {
|
||||
content: string
|
||||
project: string
|
||||
contentType: "note" | "link"
|
||||
content: string;
|
||||
project: string;
|
||||
contentType: "note" | "link";
|
||||
}) => {
|
||||
// close the modal
|
||||
onClose?.()
|
||||
console.log("📤 Creating memory...");
|
||||
|
||||
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
|
||||
},
|
||||
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}`,
|
||||
)
|
||||
}
|
||||
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<any> => {
|
||||
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
|
||||
console.log("✅ Memory created:", response.data);
|
||||
return response.data;
|
||||
},
|
||||
onMutate: async ({ content, project, contentType }) => {
|
||||
console.log("🚀 onMutate starting...")
|
||||
console.log("🚀 OPTIMISTIC UPDATE: Starting for", contentType);
|
||||
|
||||
// Cancel any outgoing refetches
|
||||
// Cancel queries to prevent conflicts
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["documents-with-memories", project],
|
||||
})
|
||||
console.log("✅ Cancelled queries")
|
||||
});
|
||||
console.log("📞 QUERIES CANCELLED for project:", project);
|
||||
|
||||
// Snapshot the previous value
|
||||
// Get previous data for rollback
|
||||
const previousMemories = queryClient.getQueryData([
|
||||
"documents-with-memories",
|
||||
project,
|
||||
])
|
||||
console.log("📸 Previous memories:", previousMemories)
|
||||
]);
|
||||
|
||||
// Create optimistic memory
|
||||
// Create optimistic memory with proper title
|
||||
const tempId = `temp-${Date.now()}`;
|
||||
const optimisticMemory = {
|
||||
id: `temp-${Date.now()}`,
|
||||
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...",
|
||||
contentType === "link" ? "Processing..." : getPlainTextTitle(content),
|
||||
description: "Processing...",
|
||||
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
|
||||
console.log("🎯 Adding optimistic memory:", optimisticMemory);
|
||||
|
||||
// Add to cache optimistically
|
||||
queryClient.setQueryData(
|
||||
["documents-with-memories", project],
|
||||
(old: any) => {
|
||||
console.log("🔄 Old data:", old)
|
||||
const newData = old
|
||||
if (old?.pages) {
|
||||
// Infinite query structure
|
||||
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 };
|
||||
}
|
||||
|
||||
// Regular query structure
|
||||
return old
|
||||
? {
|
||||
...old,
|
||||
documents: [optimisticMemory, ...(old.documents || [])],
|
||||
totalCount: (old.totalCount || 0) + 1,
|
||||
}
|
||||
: { documents: [optimisticMemory], totalCount: 1 }
|
||||
console.log("✨ New data:", newData)
|
||||
return newData
|
||||
: { documents: [optimisticMemory], totalCount: 1 };
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
console.log("✅ onMutate completed")
|
||||
return { previousMemories, optimisticId: optimisticMemory.id }
|
||||
return { previousMemories, optimisticId: tempId };
|
||||
},
|
||||
// If the mutation fails, roll back to the previous value
|
||||
onError: (error, variables, context) => {
|
||||
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}`);
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
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,
|
||||
})
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["documents-with-memories", variables.project],
|
||||
})
|
||||
toast.success(
|
||||
`${variables.contentType === "link" ? "Link" : "Note"} added successfully!`,
|
||||
);
|
||||
setShowAddDialog(false);
|
||||
onClose?.();
|
||||
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["documents-with-memories", variables.project],
|
||||
})
|
||||
}, 30000) // 30 seconds
|
||||
// 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);
|
||||
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["documents-with-memories", variables.project],
|
||||
})
|
||||
}, 120000) // 2 minutes
|
||||
if (memory.data && !memory.error) {
|
||||
// Check if memory has been processed (has memory entries, substantial content, and NOT untitled/processing)
|
||||
const hasRealTitle =
|
||||
memory.data.title &&
|
||||
!memory.data.title.toLowerCase().includes("untitled") &&
|
||||
!memory.data.title.toLowerCase().includes("processing") &&
|
||||
memory.data.title.length > 3;
|
||||
|
||||
setShowAddDialog(false)
|
||||
onClose?.()
|
||||
const isReady =
|
||||
memory.data.memoryEntries?.length > 0 ||
|
||||
(memory.data.content &&
|
||||
memory.data.content.length > 10 &&
|
||||
hasRealTitle);
|
||||
|
||||
console.log("📊 Memory ready check:", {
|
||||
isReady,
|
||||
hasMemoryEntries: memory.data.memoryEntries?.length,
|
||||
hasContent: memory.data.content?.length,
|
||||
title: memory.data.title,
|
||||
hasRealTitle,
|
||||
titleNotUntitled:
|
||||
memory.data.title &&
|
||||
!memory.data.title.toLowerCase().includes("untitled"),
|
||||
titleNotProcessing:
|
||||
memory.data.title &&
|
||||
!memory.data.title.toLowerCase().includes("processing"),
|
||||
});
|
||||
|
||||
if (isReady) {
|
||||
console.log("✅ Memory ready, replacing optimistic update");
|
||||
// Replace optimistic memory with real data
|
||||
queryClient.setQueryData(
|
||||
["documents-with-memories", variables.project],
|
||||
(old: any) => {
|
||||
if (old?.pages) {
|
||||
const newPages = old.pages.map((page: any) => ({
|
||||
...page,
|
||||
documents: page.documents.map((doc: any) => {
|
||||
if (doc.isOptimistic && doc.id.startsWith("temp-")) {
|
||||
return { ...memory.data, isOptimistic: false };
|
||||
}
|
||||
return doc;
|
||||
}),
|
||||
}));
|
||||
return { ...old, pages: newPages };
|
||||
}
|
||||
return old;
|
||||
},
|
||||
);
|
||||
return true; // Stop polling
|
||||
}
|
||||
}
|
||||
return false; // Continue polling
|
||||
} catch (error) {
|
||||
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;
|
||||
|
||||
const poll = async () => {
|
||||
if (attempts >= maxAttempts) {
|
||||
console.log("⚠️ Polling stopped after max attempts");
|
||||
return;
|
||||
}
|
||||
|
||||
const isDone = await pollMemory();
|
||||
attempts++;
|
||||
|
||||
if (!isDone && attempts < maxAttempts) {
|
||||
setTimeout(poll, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// Start polling after 2 seconds
|
||||
setTimeout(poll, 2000);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const fileUploadMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
|
|
@ -420,10 +430,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
|
||||
|
|
@ -433,9 +443,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`,
|
||||
|
|
@ -444,14 +454,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) {
|
||||
|
|
@ -463,23 +473,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 = {
|
||||
|
|
@ -499,23 +509,50 @@ export function AddMemoryView({
|
|||
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 }
|
||||
// Handle infinite query structure
|
||||
if (old?.pages) {
|
||||
// This is an infinite query - add to the first page
|
||||
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
|
||||
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) => {
|
||||
|
|
@ -523,11 +560,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({
|
||||
|
|
@ -535,18 +572,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?.();
|
||||
},
|
||||
// Always refetch after error or success
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] })
|
||||
},
|
||||
})
|
||||
// Don't invalidate queries immediately - let optimistic updates work
|
||||
// onSettled: () => {
|
||||
// queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] })
|
||||
// },
|
||||
});
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
|
|
@ -554,8 +591,8 @@ export function AddMemoryView({
|
|||
<Dialog
|
||||
key="add-memory-dialog"
|
||||
onOpenChange={(open) => {
|
||||
setShowAddDialog(open)
|
||||
if (!open) onClose?.()
|
||||
setShowAddDialog(open);
|
||||
if (!open) onClose?.();
|
||||
}}
|
||||
open={showAddDialog}
|
||||
>
|
||||
|
|
@ -614,9 +651,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">
|
||||
|
|
@ -632,9 +669,9 @@ export function AddMemoryView({
|
|||
validators={{
|
||||
onChange: ({ value }) => {
|
||||
if (!value || value.trim() === "") {
|
||||
return "Note is required"
|
||||
return "Note is required";
|
||||
}
|
||||
return undefined
|
||||
return undefined;
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
@ -716,9 +753,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"
|
||||
|
|
@ -732,9 +769,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">
|
||||
|
|
@ -756,13 +793,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";
|
||||
}
|
||||
},
|
||||
}}
|
||||
|
|
@ -842,9 +879,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"
|
||||
|
|
@ -858,9 +895,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">
|
||||
|
|
@ -994,10 +1031,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"
|
||||
|
|
@ -1065,8 +1102,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"
|
||||
|
|
@ -1103,19 +1140,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 (
|
||||
<>
|
||||
|
|
@ -1186,5 +1223,5 @@ export function AddMemoryExpandedView() {
|
|||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export * from "./tools"
|
||||
export * from "./tools";
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { z } from "zod"
|
||||
import { z } from "zod";
|
||||
|
||||
export const MetadataSchema = z.record(
|
||||
z.union([z.string(), z.number(), z.boolean()]),
|
||||
)
|
||||
export type Metadata = z.infer<typeof MetadataSchema>
|
||||
);
|
||||
export type Metadata = z.infer<typeof MetadataSchema>;
|
||||
|
||||
export const VisibilityEnum = z.enum(["public", "private", "unlisted"])
|
||||
export type Visibility = z.infer<typeof VisibilityEnum>
|
||||
export const VisibilityEnum = z.enum(["public", "private", "unlisted"]);
|
||||
export type Visibility = z.infer<typeof VisibilityEnum>;
|
||||
|
||||
export const DocumentTypeEnum = z.enum([
|
||||
"text",
|
||||
|
|
@ -20,8 +20,8 @@ export const DocumentTypeEnum = z.enum([
|
|||
"notion_doc",
|
||||
"webpage",
|
||||
"onedrive",
|
||||
])
|
||||
export type DocumentType = z.infer<typeof DocumentTypeEnum>
|
||||
]);
|
||||
export type DocumentType = z.infer<typeof DocumentTypeEnum>;
|
||||
|
||||
export const DocumentStatusEnum = z.enum([
|
||||
"unknown",
|
||||
|
|
@ -32,8 +32,8 @@ export const DocumentStatusEnum = z.enum([
|
|||
"indexing",
|
||||
"done",
|
||||
"failed",
|
||||
])
|
||||
export type DocumentStatus = z.infer<typeof DocumentStatusEnum>
|
||||
]);
|
||||
export type DocumentStatus = z.infer<typeof DocumentStatusEnum>;
|
||||
|
||||
export const ProcessingStepSchema = z.object({
|
||||
name: z.string(),
|
||||
|
|
@ -43,8 +43,8 @@ export const ProcessingStepSchema = z.object({
|
|||
error: z.string().optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
finalStatus: z.enum(["done", "failed"]).optional(),
|
||||
})
|
||||
export type ProcessingStep = z.infer<typeof ProcessingStepSchema>
|
||||
});
|
||||
export type ProcessingStep = z.infer<typeof ProcessingStepSchema>;
|
||||
|
||||
export const ProcessingMetadataSchema = z.object({
|
||||
startTime: z.number(),
|
||||
|
|
@ -55,8 +55,8 @@ export const ProcessingMetadataSchema = z.object({
|
|||
chunkingStrategy: z.string().optional(),
|
||||
tokenCount: z.number().optional(),
|
||||
steps: z.array(ProcessingStepSchema),
|
||||
})
|
||||
export type ProcessingMetadata = z.infer<typeof ProcessingMetadataSchema>
|
||||
});
|
||||
export type ProcessingMetadata = z.infer<typeof ProcessingMetadataSchema>;
|
||||
|
||||
export const DocumentSchema = z.object({
|
||||
id: z.string(),
|
||||
|
|
@ -97,11 +97,11 @@ export const DocumentSchema = z.object({
|
|||
// Timestamps
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
export type Document = z.infer<typeof DocumentSchema>
|
||||
});
|
||||
export type Document = z.infer<typeof DocumentSchema>;
|
||||
|
||||
export const ChunkTypeEnum = z.enum(["text", "image"])
|
||||
export type ChunkType = z.infer<typeof ChunkTypeEnum>
|
||||
export const ChunkTypeEnum = z.enum(["text", "image"]);
|
||||
export type ChunkType = z.infer<typeof ChunkTypeEnum>;
|
||||
|
||||
export const ChunkSchema = z.object({
|
||||
id: z.string(),
|
||||
|
|
@ -120,15 +120,15 @@ export const ChunkSchema = z.object({
|
|||
matryokshaEmbeddingModel: z.string().nullable().optional(),
|
||||
|
||||
createdAt: z.coerce.date(),
|
||||
})
|
||||
export type Chunk = z.infer<typeof ChunkSchema>
|
||||
});
|
||||
export type Chunk = z.infer<typeof ChunkSchema>;
|
||||
|
||||
export const ConnectionProviderEnum = z.enum([
|
||||
"notion",
|
||||
"google-drive",
|
||||
"onedrive",
|
||||
])
|
||||
export type ConnectionProvider = z.infer<typeof ConnectionProviderEnum>
|
||||
]);
|
||||
export type ConnectionProvider = z.infer<typeof ConnectionProviderEnum>;
|
||||
|
||||
export const ConnectionStateSchema = z.object({
|
||||
stateToken: z.string(),
|
||||
|
|
@ -142,8 +142,8 @@ export const ConnectionStateSchema = z.object({
|
|||
containerTags: z.array(z.string()).nullable().optional(),
|
||||
createdAt: z.coerce.date(),
|
||||
expiresAt: z.coerce.date().nullable().optional(),
|
||||
})
|
||||
export type ConnectionState = z.infer<typeof ConnectionStateSchema>
|
||||
});
|
||||
export type ConnectionState = z.infer<typeof ConnectionStateSchema>;
|
||||
|
||||
export const ConnectionSchema = z.object({
|
||||
id: z.string(),
|
||||
|
|
@ -163,8 +163,8 @@ export const ConnectionSchema = z.object({
|
|||
metadata: z.record(z.unknown()),
|
||||
|
||||
createdAt: z.coerce.date(),
|
||||
})
|
||||
export type Connection = z.infer<typeof ConnectionSchema>
|
||||
});
|
||||
export type Connection = z.infer<typeof ConnectionSchema>;
|
||||
|
||||
export const RequestTypeEnum = z.enum([
|
||||
"add",
|
||||
|
|
@ -175,8 +175,8 @@ export const RequestTypeEnum = z.enum([
|
|||
"delete",
|
||||
"chat",
|
||||
"search_v4",
|
||||
])
|
||||
export type RequestType = z.infer<typeof RequestTypeEnum>
|
||||
]);
|
||||
export type RequestType = z.infer<typeof RequestTypeEnum>;
|
||||
|
||||
export const ApiRequestSchema = z.object({
|
||||
id: z.string(),
|
||||
|
|
@ -212,8 +212,8 @@ export const ApiRequestSchema = z.object({
|
|||
origin: z.string().default("api"),
|
||||
|
||||
createdAt: z.coerce.date(),
|
||||
})
|
||||
export type ApiRequest = z.infer<typeof ApiRequestSchema>
|
||||
});
|
||||
export type ApiRequest = z.infer<typeof ApiRequestSchema>;
|
||||
|
||||
export const SpaceSchema = z.object({
|
||||
id: z.string(),
|
||||
|
|
@ -233,11 +233,11 @@ export const SpaceSchema = z.object({
|
|||
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
export type Space = z.infer<typeof SpaceSchema>
|
||||
});
|
||||
export type Space = z.infer<typeof SpaceSchema>;
|
||||
|
||||
export const MemoryRelationEnum = z.enum(["updates", "extends", "derives"])
|
||||
export type MemoryRelation = z.infer<typeof MemoryRelationEnum>
|
||||
export const MemoryRelationEnum = z.enum(["updates", "extends", "derives"]);
|
||||
export type MemoryRelation = z.infer<typeof MemoryRelationEnum>;
|
||||
|
||||
export const MemoryEntrySchema = z.object({
|
||||
id: z.string(),
|
||||
|
|
@ -274,14 +274,14 @@ export const MemoryEntrySchema = z.object({
|
|||
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
export type MemoryEntry = z.infer<typeof MemoryEntrySchema>
|
||||
});
|
||||
export type MemoryEntry = z.infer<typeof MemoryEntrySchema>;
|
||||
|
||||
export const DocumentsToSpacesSchema = z.object({
|
||||
documentId: z.string(),
|
||||
spaceId: z.string(),
|
||||
})
|
||||
export type DocumentsToSpaces = z.infer<typeof DocumentsToSpacesSchema>
|
||||
});
|
||||
export type DocumentsToSpaces = z.infer<typeof DocumentsToSpacesSchema>;
|
||||
|
||||
export const MemoryDocumentSourceSchema = z.object({
|
||||
memoryEntryId: z.string(),
|
||||
|
|
@ -289,11 +289,11 @@ export const MemoryDocumentSourceSchema = z.object({
|
|||
relevanceScore: z.number().default(100),
|
||||
metadata: z.record(z.unknown()).nullable().optional(),
|
||||
addedAt: z.coerce.date(),
|
||||
})
|
||||
export type MemoryDocumentSource = z.infer<typeof MemoryDocumentSourceSchema>
|
||||
});
|
||||
export type MemoryDocumentSource = z.infer<typeof MemoryDocumentSourceSchema>;
|
||||
|
||||
export const SpaceRoleEnum = z.enum(["owner", "admin", "editor", "viewer"])
|
||||
export type SpaceRole = z.infer<typeof SpaceRoleEnum>
|
||||
export const SpaceRoleEnum = z.enum(["owner", "admin", "editor", "viewer"]);
|
||||
export type SpaceRole = z.infer<typeof SpaceRoleEnum>;
|
||||
|
||||
export const SpacesToMembersSchema = z.object({
|
||||
spaceId: z.string(),
|
||||
|
|
@ -302,8 +302,8 @@ export const SpacesToMembersSchema = z.object({
|
|||
metadata: MetadataSchema.nullable().optional(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
export type SpacesToMembers = z.infer<typeof SpacesToMembersSchema>
|
||||
});
|
||||
export type SpacesToMembers = z.infer<typeof SpacesToMembersSchema>;
|
||||
|
||||
export const OrganizationSettingsSchema = z.object({
|
||||
id: z.string(),
|
||||
|
|
@ -331,8 +331,8 @@ export const OrganizationSettingsSchema = z.object({
|
|||
onedriveClientSecret: z.string().nullable().optional(),
|
||||
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
export type OrganizationSettings = z.infer<typeof OrganizationSettingsSchema>
|
||||
});
|
||||
export type OrganizationSettings = z.infer<typeof OrganizationSettingsSchema>;
|
||||
|
||||
export const schemas = {
|
||||
// Base types
|
||||
|
|
@ -368,4 +368,4 @@ export const schemas = {
|
|||
|
||||
// Auth
|
||||
OrganizationSettingsSchema,
|
||||
} as const
|
||||
} as const;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue