mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
Merge branch 'main' of https://github.com/supermemoryai/supermemory
This commit is contained in:
commit
8de2099e14
5 changed files with 650 additions and 517 deletions
|
|
@ -1,47 +1,56 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import { cn } from "@lib/utils";
|
||||
import { Badge } from "@repo/ui/components/badge";
|
||||
import { Card, CardContent, CardHeader } from "@repo/ui/components/card";
|
||||
import { colors } from "@repo/ui/memory-graph/constants";
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import { cn } from "@lib/utils"
|
||||
import { Badge } from "@repo/ui/components/badge"
|
||||
import { Card, CardContent, CardHeader } from "@repo/ui/components/card"
|
||||
import {
|
||||
Brain,
|
||||
ExternalLink,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { z } from "zod";
|
||||
import useResizeObserver from "@/hooks/use-resize-observer";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@repo/ui/components/alert-dialog"
|
||||
import { colors } from "@repo/ui/memory-graph/constants"
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
|
||||
import { useVirtualizer } from "@tanstack/react-virtual"
|
||||
import { Brain, ExternalLink, Sparkles, Trash2 } from "lucide-react"
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import type { z } from "zod"
|
||||
import useResizeObserver from "@/hooks/use-resize-observer"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import { useDeleteDocument } from "@lib/queries"
|
||||
import { useProject } from "@/stores"
|
||||
|
||||
import { MemoryDetail } from "./memories/memory-detail";
|
||||
import { getDocumentIcon } from "@/lib/document-icon";
|
||||
import { formatDate, getSourceUrl } from "./memories";
|
||||
import { MemoryDetail } from "./memories/memory-detail"
|
||||
import { getDocumentIcon } from "@/lib/document-icon"
|
||||
import { formatDate, getSourceUrl } from "./memories"
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0];
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
|
||||
interface MemoryListViewProps {
|
||||
children?: React.ReactNode;
|
||||
documents: DocumentWithMemories[];
|
||||
isLoading: boolean;
|
||||
isLoadingMore: boolean;
|
||||
error: Error | null;
|
||||
totalLoaded: number;
|
||||
hasMore: boolean;
|
||||
loadMoreDocuments: () => Promise<void>;
|
||||
children?: React.ReactNode
|
||||
documents: DocumentWithMemories[]
|
||||
isLoading: boolean
|
||||
isLoadingMore: boolean
|
||||
error: Error | null
|
||||
totalLoaded: number
|
||||
hasMore: boolean
|
||||
loadMoreDocuments: () => Promise<void>
|
||||
}
|
||||
|
||||
const GreetingMessage = memo(() => {
|
||||
const getGreeting = () => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) return "Good morning";
|
||||
if (hour < 17) return "Good afternoon";
|
||||
return "Good evening";
|
||||
};
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 12) return "Good morning"
|
||||
if (hour < 17) return "Good afternoon"
|
||||
return "Good evening"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 mb-3 px-4 md:mb-6 md:mt-3">
|
||||
|
|
@ -57,28 +66,30 @@ const GreetingMessage = memo(() => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const DocumentCard = memo(
|
||||
({
|
||||
document,
|
||||
onOpenDetails,
|
||||
onDelete,
|
||||
}: {
|
||||
document: DocumentWithMemories;
|
||||
onOpenDetails: (document: DocumentWithMemories) => void;
|
||||
document: DocumentWithMemories
|
||||
onOpenDetails: (document: DocumentWithMemories) => void
|
||||
onDelete: (document: DocumentWithMemories) => void
|
||||
}) => {
|
||||
const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten);
|
||||
const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten)
|
||||
const forgottenMemories = document.memoryEntries.filter(
|
||||
(m) => m.isForgotten,
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="h-full mx-4 p-4 transition-all cursor-pointer group relative overflow-hidden border-0 gap-2 md:w-full"
|
||||
onClick={() => {
|
||||
analytics.documentCardClicked();
|
||||
onOpenDetails(document);
|
||||
analytics.documentCardClicked()
|
||||
onOpenDetails(document)
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: colors.document.primary,
|
||||
|
|
@ -101,9 +112,9 @@ const DocumentCard = memo(
|
|||
<button
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const sourceUrl = getSourceUrl(document);
|
||||
window.open(sourceUrl ?? undefined, "_blank");
|
||||
e.stopPropagation()
|
||||
const sourceUrl = getSourceUrl(document)
|
||||
window.open(sourceUrl ?? undefined, "_blank")
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
||||
|
|
@ -128,38 +139,84 @@ const DocumentCard = memo(
|
|||
{document.content}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{activeMemories.length > 0 && (
|
||||
<Badge
|
||||
className="text-xs text-accent-foreground"
|
||||
style={{
|
||||
backgroundColor: colors.memory.secondary,
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
<Brain className="w-3 h-3 mr-1" />
|
||||
{activeMemories.length}{" "}
|
||||
{activeMemories.length === 1 ? "memory" : "memories"}
|
||||
</Badge>
|
||||
)}
|
||||
{forgottenMemories.length > 0 && (
|
||||
<Badge
|
||||
className="text-xs"
|
||||
style={{
|
||||
borderColor: "rgba(255, 255, 255, 0.2)",
|
||||
color: colors.text.muted,
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
{forgottenMemories.length} forgotten
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{activeMemories.length > 0 && (
|
||||
<Badge
|
||||
className="text-xs text-accent-foreground"
|
||||
style={{
|
||||
backgroundColor: colors.memory.secondary,
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
<Brain className="w-3 h-3 mr-1" />
|
||||
{activeMemories.length}{" "}
|
||||
{activeMemories.length === 1 ? "memory" : "memories"}
|
||||
</Badge>
|
||||
)}
|
||||
{forgottenMemories.length > 0 && (
|
||||
<Badge
|
||||
className="text-xs"
|
||||
style={{
|
||||
borderColor: "rgba(255, 255, 255, 0.2)",
|
||||
color: colors.text.muted,
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
{forgottenMemories.length} forgotten
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-red-500/20"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
style={{
|
||||
color: colors.text.muted,
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Document</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete this document and all its
|
||||
related memories? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete(document)
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export const MemoryListView = ({
|
||||
children,
|
||||
|
|
@ -170,29 +227,38 @@ export const MemoryListView = ({
|
|||
hasMore,
|
||||
loadMoreDocuments,
|
||||
}: MemoryListViewProps) => {
|
||||
const [selectedSpace, _] = useState<string>("all");
|
||||
const [selectedSpace, _] = useState<string>("all")
|
||||
const [selectedDocument, setSelectedDocument] =
|
||||
useState<DocumentWithMemories | null>(null);
|
||||
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isMobile = useIsMobile();
|
||||
useState<DocumentWithMemories | null>(null)
|
||||
const [isDetailOpen, setIsDetailOpen] = useState(false)
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const isMobile = useIsMobile()
|
||||
const { selectedProject } = useProject()
|
||||
const deleteDocumentMutation = useDeleteDocument(selectedProject)
|
||||
|
||||
const gap = 14;
|
||||
const gap = 14
|
||||
|
||||
const { width: containerWidth } = useResizeObserver(containerRef);
|
||||
const columnWidth = isMobile ? containerWidth : 320;
|
||||
const handleDeleteDocument = useCallback(
|
||||
(document: DocumentWithMemories) => {
|
||||
deleteDocumentMutation.mutate(document.id)
|
||||
},
|
||||
[deleteDocumentMutation],
|
||||
)
|
||||
|
||||
const { width: containerWidth } = useResizeObserver(containerRef)
|
||||
const columnWidth = isMobile ? containerWidth : 320
|
||||
const columns = Math.max(
|
||||
1,
|
||||
Math.floor((containerWidth + gap) / (columnWidth + gap)),
|
||||
);
|
||||
)
|
||||
|
||||
// Filter documents based on selected space
|
||||
const filteredDocuments = useMemo(() => {
|
||||
if (!documents) return [];
|
||||
if (!documents) return []
|
||||
|
||||
if (selectedSpace === "all") {
|
||||
return documents;
|
||||
return documents
|
||||
}
|
||||
|
||||
return documents
|
||||
|
|
@ -203,44 +269,44 @@ export const MemoryListView = ({
|
|||
(memory.spaceContainerTag ?? memory.spaceId) === selectedSpace,
|
||||
),
|
||||
}))
|
||||
.filter((doc) => doc.memoryEntries.length > 0);
|
||||
}, [documents, selectedSpace]);
|
||||
.filter((doc) => doc.memoryEntries.length > 0)
|
||||
}, [documents, selectedSpace])
|
||||
|
||||
const handleOpenDetails = useCallback((document: DocumentWithMemories) => {
|
||||
analytics.memoryDetailOpened();
|
||||
setSelectedDocument(document);
|
||||
setIsDetailOpen(true);
|
||||
}, []);
|
||||
analytics.memoryDetailOpened()
|
||||
setSelectedDocument(document)
|
||||
setIsDetailOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseDetails = useCallback(() => {
|
||||
setIsDetailOpen(false);
|
||||
setTimeout(() => setSelectedDocument(null), 300);
|
||||
}, []);
|
||||
setIsDetailOpen(false)
|
||||
setTimeout(() => setSelectedDocument(null), 300)
|
||||
}, [])
|
||||
|
||||
const virtualItems = useMemo(() => {
|
||||
const items = [];
|
||||
const items = []
|
||||
for (let i = 0; i < filteredDocuments.length; i += columns) {
|
||||
items.push(filteredDocuments.slice(i, i + columns));
|
||||
items.push(filteredDocuments.slice(i, i + columns))
|
||||
}
|
||||
return items;
|
||||
}, [filteredDocuments, columns]);
|
||||
return items
|
||||
}, [filteredDocuments, columns])
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: virtualItems.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
overscan: 5,
|
||||
estimateSize: () => 200,
|
||||
});
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const [lastItem] = [...virtualizer.getVirtualItems()].reverse();
|
||||
const [lastItem] = [...virtualizer.getVirtualItems()].reverse()
|
||||
|
||||
if (!lastItem || !hasMore || isLoadingMore) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (lastItem.index >= virtualItems.length - 1) {
|
||||
loadMoreDocuments();
|
||||
loadMoreDocuments()
|
||||
}
|
||||
}, [
|
||||
hasMore,
|
||||
|
|
@ -248,15 +314,15 @@ export const MemoryListView = ({
|
|||
loadMoreDocuments,
|
||||
virtualizer.getVirtualItems(),
|
||||
virtualItems.length,
|
||||
]);
|
||||
])
|
||||
|
||||
// Always render with consistent structure
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="h-full overflow-hidden relative pb-20"
|
||||
style={{ backgroundColor: colors.background.primary }}
|
||||
ref={containerRef}
|
||||
style={{ backgroundColor: colors.background.primary }}
|
||||
>
|
||||
{error ? (
|
||||
<div className="h-full flex items-center justify-center p-4">
|
||||
|
|
@ -301,8 +367,8 @@ export const MemoryListView = ({
|
|||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const rowItems = virtualItems[virtualRow.index];
|
||||
if (!rowItems) return null;
|
||||
const rowItems = virtualItems[virtualRow.index]
|
||||
if (!rowItems) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -326,11 +392,12 @@ export const MemoryListView = ({
|
|||
key={`${document.id}-${virtualRow.index}-${columnIndex}`}
|
||||
document={document}
|
||||
onOpenDetails={handleOpenDetails}
|
||||
onDelete={handleDeleteDocument}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
|
@ -355,5 +422,5 @@ export const MemoryListView = ({
|
|||
isMobile={isMobile}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,37 +31,40 @@ import {
|
|||
PlugIcon,
|
||||
Plus,
|
||||
UploadIcon,
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
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";
|
||||
import dynamic from "next/dynamic";
|
||||
} from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
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"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
const TextEditor = dynamic(() => import("./text-editor").then(mod => ({ default: mod.TextEditor })), {
|
||||
loading: () => (
|
||||
<div className="bg-white/5 border border-white/10 rounded-md">
|
||||
<div className="flex-1 min-h-48 max-h-64 overflow-y-auto flex items-center justify-center text-white/70">
|
||||
Loading editor...
|
||||
</div>
|
||||
<div className="p-1 flex items-center gap-2 bg-white/5 backdrop-blur-sm rounded-b-md">
|
||||
<div className="flex items-center gap-1 opacity-50">
|
||||
<div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" />
|
||||
<div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" />
|
||||
<div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" />
|
||||
const TextEditor = dynamic(
|
||||
() => import("./text-editor").then((mod) => ({ default: mod.TextEditor })),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="bg-white/5 border border-white/10 rounded-md">
|
||||
<div className="flex-1 min-h-48 max-h-64 overflow-y-auto flex items-center justify-center text-white/70">
|
||||
Loading editor...
|
||||
</div>
|
||||
<div className="p-1 flex items-center gap-2 bg-white/5 backdrop-blur-sm rounded-b-md">
|
||||
<div className="flex items-center gap-1 opacity-50">
|
||||
<div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" />
|
||||
<div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" />
|
||||
<div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
ssr: false,
|
||||
});
|
||||
),
|
||||
ssr: false,
|
||||
},
|
||||
)
|
||||
|
||||
// // Processing status component
|
||||
// function ProcessingStatus({ status }: { status: string }) {
|
||||
|
|
@ -89,80 +92,74 @@ 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 as any);
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn)
|
||||
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0;
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0;
|
||||
|
||||
// Check if user is pro
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn as any);
|
||||
const isProUser = proCheck?.allowed ?? false;
|
||||
|
||||
const canAddMemory = memoriesUsed < memoriesLimit;
|
||||
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: {
|
||||
|
|
@ -174,8 +171,8 @@ export function AddMemoryView({
|
|||
content: value.content,
|
||||
project: value.project,
|
||||
contentType: activeTab as "note" | "link",
|
||||
});
|
||||
formApi.reset();
|
||||
})
|
||||
formApi.reset()
|
||||
},
|
||||
validators: {
|
||||
onChange: z.object({
|
||||
|
|
@ -183,19 +180,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({
|
||||
|
|
@ -206,8 +203,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) {
|
||||
|
|
@ -216,25 +213,13 @@ export function AddMemoryView({
|
|||
title: value.title || undefined,
|
||||
description: value.description || undefined,
|
||||
project: value.project,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
formApi.reset();
|
||||
setSelectedFiles([]);
|
||||
formApi.reset()
|
||||
setSelectedFiles([])
|
||||
},
|
||||
});
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
const addContentMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
|
|
@ -242,12 +227,12 @@ export function AddMemoryView({
|
|||
project,
|
||||
contentType,
|
||||
}: {
|
||||
content: string;
|
||||
project: string;
|
||||
contentType: "note" | "link";
|
||||
content: string
|
||||
project: string
|
||||
contentType: "note" | "link"
|
||||
}) => {
|
||||
// close the modal
|
||||
onClose?.();
|
||||
onClose?.()
|
||||
|
||||
const processingPromise = (async () => {
|
||||
// First, create the memory
|
||||
|
|
@ -259,31 +244,31 @@ export function AddMemoryView({
|
|||
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;
|
||||
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)
|
||||
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/memories/" + memoryId,
|
||||
);
|
||||
)
|
||||
|
||||
if (memory.error) {
|
||||
throw new Error(
|
||||
memory.error?.message || "Failed to fetch memory status",
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Check if processing is complete
|
||||
|
|
@ -293,58 +278,58 @@ export function AddMemoryView({
|
|||
// Sometimes the memory might be ready when it has content and no processing status
|
||||
memory.data?.content
|
||||
) {
|
||||
return memory.data;
|
||||
return memory.data
|
||||
}
|
||||
|
||||
// If still processing, wait and try again
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait 5 seconds
|
||||
attempts++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000)) // Wait 5 seconds
|
||||
attempts++
|
||||
} catch (error) {
|
||||
console.error("Error polling memory status:", 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");
|
||||
throw new Error("Failed to check processing status")
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
attempts++;
|
||||
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;
|
||||
})();
|
||||
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;
|
||||
return processingPromise
|
||||
},
|
||||
onMutate: async ({ content, project, contentType }) => {
|
||||
console.log("🚀 onMutate starting...");
|
||||
console.log("🚀 onMutate starting...")
|
||||
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["documents-with-memories", project],
|
||||
});
|
||||
console.log("✅ Cancelled queries");
|
||||
})
|
||||
console.log("✅ Cancelled queries")
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousMemories = queryClient.getQueryData([
|
||||
"documents-with-memories",
|
||||
project,
|
||||
]);
|
||||
console.log("📸 Previous memories:", previousMemories);
|
||||
])
|
||||
console.log("📸 Previous memories:", previousMemories)
|
||||
|
||||
// Create optimistic memory
|
||||
const optimisticMemory = {
|
||||
|
|
@ -368,28 +353,28 @@ export function AddMemoryView({
|
|||
},
|
||||
memoryEntries: [],
|
||||
isOptimistic: true,
|
||||
};
|
||||
console.log("🎯 Created optimistic memory:", optimisticMemory);
|
||||
}
|
||||
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);
|
||||
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;
|
||||
: { documents: [optimisticMemory], totalCount: 1 }
|
||||
console.log("✨ New data:", newData)
|
||||
return newData
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
console.log("✅ onMutate completed");
|
||||
return { previousMemories, optimisticId: optimisticMemory.id };
|
||||
console.log("✅ onMutate completed")
|
||||
return { previousMemories, optimisticId: optimisticMemory.id }
|
||||
},
|
||||
// If the mutation fails, roll back to the previous value
|
||||
onError: (error, variables, context) => {
|
||||
|
|
@ -397,7 +382,7 @@ export function AddMemoryView({
|
|||
queryClient.setQueryData(
|
||||
["documents-with-memories", variables.project],
|
||||
context.previousMemories,
|
||||
);
|
||||
)
|
||||
}
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
|
|
@ -405,28 +390,28 @@ export function AddMemoryView({
|
|||
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
|
||||
})
|
||||
}, 30000) // 30 seconds
|
||||
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["documents-with-memories", variables.project],
|
||||
});
|
||||
}, 120000); // 2 minutes
|
||||
})
|
||||
}, 120000) // 2 minutes
|
||||
|
||||
setShowAddDialog(false);
|
||||
onClose?.();
|
||||
setShowAddDialog(false)
|
||||
onClose?.()
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const fileUploadMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
|
|
@ -435,10 +420,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
|
||||
|
|
@ -448,9 +433,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/memories/file`,
|
||||
|
|
@ -459,14 +444,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) {
|
||||
|
|
@ -478,23 +463,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 = {
|
||||
|
|
@ -514,23 +499,23 @@ 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 };
|
||||
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) => {
|
||||
|
|
@ -538,11 +523,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({
|
||||
|
|
@ -550,18 +535,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"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] })
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
|
|
@ -569,8 +554,8 @@ export function AddMemoryView({
|
|||
<Dialog
|
||||
key="add-memory-dialog"
|
||||
onOpenChange={(open) => {
|
||||
setShowAddDialog(open);
|
||||
if (!open) onClose?.();
|
||||
setShowAddDialog(open)
|
||||
if (!open) onClose?.()
|
||||
}}
|
||||
open={showAddDialog}
|
||||
>
|
||||
|
|
@ -597,26 +582,26 @@ export function AddMemoryView({
|
|||
<div className="bg-white/5 p-1 h-10 sm:h-8 rounded-md flex overflow-x-auto">
|
||||
<TabButton
|
||||
icon={Brain}
|
||||
label="Note"
|
||||
isActive={activeTab === "note"}
|
||||
label="Note"
|
||||
onClick={() => setActiveTab("note")}
|
||||
/>
|
||||
<TabButton
|
||||
icon={LinkIcon}
|
||||
label="Link"
|
||||
isActive={activeTab === "link"}
|
||||
label="Link"
|
||||
onClick={() => setActiveTab("link")}
|
||||
/>
|
||||
<TabButton
|
||||
icon={FileIcon}
|
||||
label="File"
|
||||
isActive={activeTab === "file"}
|
||||
label="File"
|
||||
onClick={() => setActiveTab("file")}
|
||||
/>
|
||||
<TabButton
|
||||
icon={PlugIcon}
|
||||
label="Connect"
|
||||
isActive={activeTab === "connect"}
|
||||
label="Connect"
|
||||
onClick={() => setActiveTab("connect")}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -629,9 +614,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">
|
||||
|
|
@ -647,9 +632,9 @@ export function AddMemoryView({
|
|||
validators={{
|
||||
onChange: ({ value }) => {
|
||||
if (!value || value.trim() === "") {
|
||||
return "Note is required";
|
||||
return "Note is required"
|
||||
}
|
||||
return undefined;
|
||||
return undefined
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
@ -729,9 +714,9 @@ export function AddMemoryView({
|
|||
|
||||
<ActionButtons
|
||||
onCancel={() => {
|
||||
setShowAddDialog(false);
|
||||
onClose?.();
|
||||
addContentForm.reset();
|
||||
setShowAddDialog(false)
|
||||
onClose?.()
|
||||
addContentForm.reset()
|
||||
}}
|
||||
submitText="Add Note"
|
||||
submitIcon={Plus}
|
||||
|
|
@ -747,9 +732,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">
|
||||
|
|
@ -771,13 +756,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"
|
||||
}
|
||||
},
|
||||
}}
|
||||
|
|
@ -855,9 +840,9 @@ export function AddMemoryView({
|
|||
|
||||
<ActionButtons
|
||||
onCancel={() => {
|
||||
setShowAddDialog(false);
|
||||
onClose?.();
|
||||
addContentForm.reset();
|
||||
setShowAddDialog(false)
|
||||
onClose?.()
|
||||
addContentForm.reset()
|
||||
}}
|
||||
submitText="Add Link"
|
||||
submitIcon={Plus}
|
||||
|
|
@ -873,9 +858,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">
|
||||
|
|
@ -1007,10 +992,10 @@ export function AddMemoryView({
|
|||
|
||||
<ActionButtons
|
||||
onCancel={() => {
|
||||
setShowAddDialog(false);
|
||||
onClose?.();
|
||||
fileUploadForm.reset();
|
||||
setSelectedFiles([]);
|
||||
setShowAddDialog(false)
|
||||
onClose?.()
|
||||
fileUploadForm.reset()
|
||||
setSelectedFiles([])
|
||||
}}
|
||||
submitText="Upload File"
|
||||
submitIcon={UploadIcon}
|
||||
|
|
@ -1080,8 +1065,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"
|
||||
|
|
@ -1118,19 +1103,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 (
|
||||
<>
|
||||
|
|
@ -1201,5 +1186,5 @@ export function AddMemoryExpandedView() {
|
|||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,22 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import { $fetch } from "@lib/api";
|
||||
import {
|
||||
fetchConnectionsFeature,
|
||||
fetchConsumerProProduct,
|
||||
} from "@repo/lib/queries";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@repo/ui/components/dialog";
|
||||
import { Skeleton } from "@repo/ui/components/skeleton";
|
||||
import type { ConnectionResponseSchema } from "@repo/validation/api";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { z } from "zod";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
import { useProject } from "@/stores";
|
||||
import { $fetch } from "@lib/api"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { Skeleton } from "@repo/ui/components/skeleton"
|
||||
import type { ConnectionResponseSchema } from "@repo/validation/api"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { Trash2 } from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import type { z } from "zod"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import { useProject } from "@/stores"
|
||||
|
||||
// Define types
|
||||
type Connection = z.infer<typeof ConnectionResponseSchema>;
|
||||
type Connection = z.infer<typeof ConnectionResponseSchema>
|
||||
|
||||
// Connector configurations
|
||||
const CONNECTORS = {
|
||||
|
|
@ -47,35 +35,45 @@ const CONNECTORS = {
|
|||
description: "Access your Microsoft Office documents",
|
||||
icon: OneDrive,
|
||||
},
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
type ConnectorProvider = keyof typeof CONNECTORS;
|
||||
type ConnectorProvider = keyof typeof CONNECTORS
|
||||
|
||||
export function ConnectionsTabContent() {
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedProject } = useProject();
|
||||
const autumn = useCustomer();
|
||||
const queryClient = useQueryClient()
|
||||
const { selectedProject } = useProject()
|
||||
const autumn = useCustomer()
|
||||
const [isProUser, setIsProUser] = useState(false)
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
});
|
||||
window.location.reload();
|
||||
})
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error(error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any);
|
||||
const connectionsUsed = connectionsCheck?.balance ?? 0;
|
||||
const connectionsLimit = connectionsCheck?.included_usage ?? 0;
|
||||
// Set pro user status when autumn data loads
|
||||
useEffect(() => {
|
||||
if (!autumn.isLoading) {
|
||||
setIsProUser(
|
||||
autumn.customer?.products.some(
|
||||
(product) => product.id === "consumer_pro",
|
||||
) ?? false,
|
||||
)
|
||||
}
|
||||
}, [autumn.isLoading, autumn.customer])
|
||||
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn as any);
|
||||
const isProUser = proCheck?.allowed ?? false;
|
||||
// Get connections data directly from autumn customer
|
||||
const connectionsFeature = autumn.customer?.features?.connections
|
||||
const connectionsUsed = connectionsFeature?.usage ?? 0
|
||||
const connectionsLimit = connectionsFeature?.included_usage ?? 0
|
||||
|
||||
const canAddConnection = connectionsUsed < connectionsLimit;
|
||||
const canAddConnection = connectionsUsed < connectionsLimit
|
||||
|
||||
// Fetch connections
|
||||
const {
|
||||
|
|
@ -89,28 +87,26 @@ export function ConnectionsTabContent() {
|
|||
body: {
|
||||
containerTags: [],
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to load connections",
|
||||
);
|
||||
throw new Error(response.error?.message || "Failed to load connections")
|
||||
}
|
||||
|
||||
return response.data as Connection[];
|
||||
return response.data as Connection[]
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
refetchInterval: 60 * 1000,
|
||||
});
|
||||
})
|
||||
|
||||
// Show error toast if connections fail to load
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toast.error("Failed to load connections", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
})
|
||||
}
|
||||
}, [error]);
|
||||
}, [error])
|
||||
|
||||
// Add connection mutation
|
||||
const addConnectionMutation = useMutation({
|
||||
|
|
@ -119,7 +115,7 @@ export function ConnectionsTabContent() {
|
|||
if (!canAddConnection && !isProUser) {
|
||||
throw new Error(
|
||||
"Free plan doesn't include connections. Upgrade to Pro for unlimited connections.",
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const response = await $fetch("@post/connections/:provider", {
|
||||
|
|
@ -128,57 +124,61 @@ export function ConnectionsTabContent() {
|
|||
redirectUrl: window.location.href,
|
||||
containerTags: [selectedProject],
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: its fine
|
||||
if ("data" in response && !("error" in response.data!)) {
|
||||
return response.data;
|
||||
return response.data
|
||||
}
|
||||
|
||||
throw new Error(response.error?.message || "Failed to connect");
|
||||
throw new Error(response.error?.message || "Failed to connect")
|
||||
},
|
||||
onSuccess: (data, provider) => {
|
||||
analytics.connectionAdded(provider);
|
||||
analytics.connectionAuthStarted();
|
||||
analytics.connectionAdded(provider)
|
||||
analytics.connectionAuthStarted()
|
||||
autumn.track({
|
||||
featureId: "connections",
|
||||
value: 1,
|
||||
})
|
||||
if (data?.authLink) {
|
||||
window.location.href = data.authLink;
|
||||
window.location.href = data.authLink
|
||||
}
|
||||
},
|
||||
onError: (error, provider) => {
|
||||
analytics.connectionAuthFailed();
|
||||
analytics.connectionAuthFailed()
|
||||
toast.error(`Failed to connect ${provider}`, {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// Delete connection mutation
|
||||
const deleteConnectionMutation = useMutation({
|
||||
mutationFn: async (connectionId: string) => {
|
||||
await $fetch(`@delete/connections/${connectionId}`);
|
||||
await $fetch(`@delete/connections/${connectionId}`)
|
||||
},
|
||||
onSuccess: () => {
|
||||
analytics.connectionDeleted();
|
||||
analytics.connectionDeleted()
|
||||
toast.success(
|
||||
"Connection removal has started. supermemory will permanently delete the documents in the next few minutes.",
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["connections"] });
|
||||
)
|
||||
queryClient.invalidateQueries({ queryKey: ["connections"] })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to remove connection", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const getProviderIcon = (provider: string) => {
|
||||
const connector = CONNECTORS[provider as ConnectorProvider];
|
||||
const connector = CONNECTORS[provider as ConnectorProvider]
|
||||
if (connector) {
|
||||
const Icon = connector.icon;
|
||||
return <Icon className="h-10 w-10" />;
|
||||
const Icon = connector.icon
|
||||
return <Icon className="h-10 w-10" />
|
||||
}
|
||||
return <span className="text-2xl">📎</span>;
|
||||
};
|
||||
return <span className="text-2xl">📎</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -186,7 +186,12 @@ export function ConnectionsTabContent() {
|
|||
<p className="text-sm text-white/70">
|
||||
Connect your favorite services to import documents
|
||||
</p>
|
||||
{!isProUser && (
|
||||
{isProUser && !autumn.isLoading && (
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
{connectionsUsed} of {connectionsLimit} connections used
|
||||
</p>
|
||||
)}
|
||||
{!isProUser && !autumn.isLoading && (
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
Connections require a Pro subscription
|
||||
</p>
|
||||
|
|
@ -194,7 +199,7 @@ export function ConnectionsTabContent() {
|
|||
</div>
|
||||
|
||||
{/* Show upgrade prompt for free users */}
|
||||
{!isProUser && (
|
||||
{!autumn.isLoading && !isProUser && (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
|
||||
|
|
@ -208,7 +213,6 @@ export function ConnectionsTabContent() {
|
|||
sync your documents.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-400 border-yellow-500/30"
|
||||
onClick={handleUpgrade}
|
||||
size="sm"
|
||||
|
|
@ -306,7 +310,7 @@ export function ConnectionsTabContent() {
|
|||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{Object.entries(CONNECTORS).map(([provider, config], index) => {
|
||||
const Icon = config.icon;
|
||||
const Icon = config.icon
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -320,7 +324,7 @@ export function ConnectionsTabContent() {
|
|||
className="justify-start h-auto p-4 bg-white/5 hover:bg-white/10 border-white/10 text-white w-full"
|
||||
disabled={addConnectionMutation.isPending}
|
||||
onClick={() => {
|
||||
addConnectionMutation.mutate(provider as ConnectorProvider);
|
||||
addConnectionMutation.mutate(provider as ConnectorProvider)
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
|
|
@ -333,10 +337,10 @@ export function ConnectionsTabContent() {
|
|||
</div>
|
||||
</Button>
|
||||
</motion.div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,35 @@
|
|||
import { $fetch } from "@lib/api";
|
||||
import { authClient } from "@lib/auth";
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
import { generateId } from "@lib/generate-id";
|
||||
import { $fetch } from "@lib/api"
|
||||
import { authClient } from "@lib/auth"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { generateId } from "@lib/generate-id"
|
||||
import {
|
||||
ADD_MEMORY_SHORTCUT_URL,
|
||||
SEARCH_MEMORY_SHORTCUT_URL,
|
||||
} from "@repo/lib/constants";
|
||||
import {
|
||||
fetchConnectionsFeature,
|
||||
fetchConsumerProProduct,
|
||||
} from "@repo/lib/queries";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
} from "@repo/lib/constants"
|
||||
import { fetchConnectionsFeature } from "@repo/lib/queries"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
} from "@repo/ui/components/dialog";
|
||||
import { Skeleton } from "@repo/ui/components/skeleton";
|
||||
import type { ConnectionResponseSchema } from "@repo/validation/api";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
import { Check, Copy, Smartphone, Trash2 } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { z } from "zod";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
import { useProject } from "@/stores";
|
||||
} from "@repo/ui/components/dialog"
|
||||
import { Skeleton } from "@repo/ui/components/skeleton"
|
||||
import type { ConnectionResponseSchema } from "@repo/validation/api"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { Check, Copy, Smartphone, Trash2 } from "lucide-react"
|
||||
import { motion } from "motion/react"
|
||||
import Image from "next/image"
|
||||
import { useEffect, useId, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import type { z } from "zod"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import { useProject } from "@/stores"
|
||||
|
||||
type Connection = z.infer<typeof ConnectionResponseSchema>;
|
||||
type Connection = z.infer<typeof ConnectionResponseSchema>
|
||||
|
||||
const CONNECTORS = {
|
||||
"google-drive": {
|
||||
|
|
@ -50,9 +47,9 @@ const CONNECTORS = {
|
|||
description: "Access your Microsoft Office documents",
|
||||
icon: OneDrive,
|
||||
},
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
type ConnectorProvider = keyof typeof CONNECTORS;
|
||||
type ConnectorProvider = keyof typeof CONNECTORS
|
||||
|
||||
const ChromeIcon = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -83,41 +80,49 @@ const ChromeIcon = ({ className }: { className?: string }) => (
|
|||
d="M95.252 47.628h82.479A95.237 95.237 0 0 0 142.87 12.76 95.23 95.23 0 0 0 95.245 0a95.222 95.222 0 0 0-47.623 12.767 95.23 95.23 0 0 0-34.856 34.872l41.24 71.43.011.006a47.62 47.62 0 0 1-.015-47.633 47.61 47.61 0 0 1 41.252-23.815z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
)
|
||||
|
||||
export function IntegrationsView() {
|
||||
const { org } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedProject } = useProject();
|
||||
const autumn = useCustomer();
|
||||
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
|
||||
const [apiKey, setApiKey] = useState<string>("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { org } = useAuth()
|
||||
const queryClient = useQueryClient()
|
||||
const { selectedProject } = useProject()
|
||||
const autumn = useCustomer()
|
||||
const [showApiKeyModal, setShowApiKeyModal] = useState(false)
|
||||
const [apiKey, setApiKey] = useState<string>("")
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [isProUser, setIsProUser] = useState(false)
|
||||
const [selectedShortcutType, setSelectedShortcutType] = useState<
|
||||
"add" | "search" | null
|
||||
>(null);
|
||||
const apiKeyId = useId();
|
||||
>(null)
|
||||
const apiKeyId = useId()
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
});
|
||||
window.location.reload();
|
||||
})
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error(error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any);
|
||||
const connectionsUsed = connectionsCheck?.balance ?? 0;
|
||||
const connectionsLimit = connectionsCheck?.included_usage ?? 0;
|
||||
useEffect(() => {
|
||||
if (!autumn.isLoading) {
|
||||
setIsProUser(
|
||||
autumn.customer?.products.some(
|
||||
(product) => product.id === "consumer_pro",
|
||||
) ?? false,
|
||||
)
|
||||
}
|
||||
}, [autumn.isLoading, autumn.customer])
|
||||
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn as any);
|
||||
const isProUser = proCheck?.allowed ?? false;
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn)
|
||||
const connectionsUsed = connectionsCheck?.balance ?? 0
|
||||
const connectionsLimit = connectionsCheck?.included_usage ?? 0
|
||||
|
||||
const canAddConnection = connectionsUsed < connectionsLimit;
|
||||
const canAddConnection = connectionsUsed < connectionsLimit
|
||||
|
||||
const {
|
||||
data: connections = [],
|
||||
|
|
@ -130,19 +135,17 @@ export function IntegrationsView() {
|
|||
body: {
|
||||
containerTags: [],
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to load connections",
|
||||
);
|
||||
throw new Error(response.error?.message || "Failed to load connections")
|
||||
}
|
||||
|
||||
return response.data as Connection[];
|
||||
return response.data as Connection[]
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
refetchInterval: 60 * 1000,
|
||||
});
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionsError) {
|
||||
|
|
@ -151,16 +154,16 @@ export function IntegrationsView() {
|
|||
connectionsError instanceof Error
|
||||
? connectionsError.message
|
||||
: "Unknown error",
|
||||
});
|
||||
})
|
||||
}
|
||||
}, [connectionsError]);
|
||||
}, [connectionsError])
|
||||
|
||||
const addConnectionMutation = useMutation({
|
||||
mutationFn: async (provider: ConnectorProvider) => {
|
||||
if (!canAddConnection && !isProUser) {
|
||||
throw new Error(
|
||||
"Free plan doesn't include connections. Upgrade to Pro for unlimited connections.",
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const response = await $fetch("@post/connections/:provider", {
|
||||
|
|
@ -169,47 +172,47 @@ export function IntegrationsView() {
|
|||
redirectUrl: window.location.href,
|
||||
containerTags: [selectedProject],
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: its fine
|
||||
if ("data" in response && !("error" in response.data!)) {
|
||||
return response.data;
|
||||
return response.data
|
||||
}
|
||||
|
||||
throw new Error(response.error?.message || "Failed to connect");
|
||||
throw new Error(response.error?.message || "Failed to connect")
|
||||
},
|
||||
onSuccess: (data, provider) => {
|
||||
analytics.connectionAdded(provider);
|
||||
analytics.connectionAuthStarted();
|
||||
analytics.connectionAdded(provider)
|
||||
analytics.connectionAuthStarted()
|
||||
if (data?.authLink) {
|
||||
window.location.href = data.authLink;
|
||||
window.location.href = data.authLink
|
||||
}
|
||||
},
|
||||
onError: (error, provider) => {
|
||||
analytics.connectionAuthFailed();
|
||||
analytics.connectionAuthFailed()
|
||||
toast.error(`Failed to connect ${provider}`, {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const deleteConnectionMutation = useMutation({
|
||||
mutationFn: async (connectionId: string) => {
|
||||
await $fetch(`@delete/connections/${connectionId}`);
|
||||
await $fetch(`@delete/connections/${connectionId}`)
|
||||
},
|
||||
onSuccess: () => {
|
||||
analytics.connectionDeleted();
|
||||
analytics.connectionDeleted()
|
||||
toast.success(
|
||||
"Connection removal has started. supermemory will permanently delete all documents related to the connection in the next few minutes.",
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["connections"] });
|
||||
)
|
||||
queryClient.invalidateQueries({ queryKey: ["connections"] })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to remove connection", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const createApiKeyMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
|
|
@ -220,60 +223,60 @@ export function IntegrationsView() {
|
|||
},
|
||||
name: `ios-${generateId().slice(0, 8)}`,
|
||||
prefix: `sm_${org?.id}_`,
|
||||
});
|
||||
return res.key;
|
||||
})
|
||||
return res.key
|
||||
},
|
||||
onSuccess: (apiKey) => {
|
||||
setApiKey(apiKey);
|
||||
setShowApiKeyModal(true);
|
||||
setCopied(false);
|
||||
handleCopyApiKey();
|
||||
setApiKey(apiKey)
|
||||
setShowApiKeyModal(true)
|
||||
setCopied(false)
|
||||
handleCopyApiKey()
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to create API key", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const handleShortcutClick = (shortcutType: "add" | "search") => {
|
||||
setSelectedShortcutType(shortcutType);
|
||||
createApiKeyMutation.mutate();
|
||||
};
|
||||
setSelectedShortcutType(shortcutType)
|
||||
createApiKeyMutation.mutate()
|
||||
}
|
||||
|
||||
const handleCopyApiKey = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(apiKey);
|
||||
setCopied(true);
|
||||
toast.success("API key copied to clipboard!");
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
await navigator.clipboard.writeText(apiKey)
|
||||
setCopied(true)
|
||||
toast.success("API key copied to clipboard!")
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
toast.error("Failed to copy API key");
|
||||
toast.error("Failed to copy API key")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleOpenShortcut = () => {
|
||||
if (!selectedShortcutType) {
|
||||
toast.error("No shortcut type selected");
|
||||
return;
|
||||
toast.error("No shortcut type selected")
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedShortcutType === "add") {
|
||||
window.open(ADD_MEMORY_SHORTCUT_URL, "_blank");
|
||||
window.open(ADD_MEMORY_SHORTCUT_URL, "_blank")
|
||||
} else if (selectedShortcutType === "search") {
|
||||
window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank");
|
||||
window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDialogClose = (open: boolean) => {
|
||||
setShowApiKeyModal(open);
|
||||
setShowApiKeyModal(open)
|
||||
if (!open) {
|
||||
// Reset state when dialog closes
|
||||
setSelectedShortcutType(null);
|
||||
setApiKey("");
|
||||
setCopied(false);
|
||||
setSelectedShortcutType(null)
|
||||
setApiKey("")
|
||||
setCopied(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 sm:space-y-4 custom-scrollbar">
|
||||
|
|
@ -362,15 +365,15 @@ export function IntegrationsView() {
|
|||
<svg
|
||||
className="h-5 w-5 text-green-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<title>Connection Link Icon</title>
|
||||
<path
|
||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
@ -390,7 +393,7 @@ export function IntegrationsView() {
|
|||
</div>
|
||||
|
||||
{/* Show upgrade prompt for free users */}
|
||||
{!isProUser && (
|
||||
{!autumn.isLoading && !isProUser && (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg mb-3"
|
||||
|
|
@ -432,11 +435,11 @@ export function IntegrationsView() {
|
|||
) : (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(CONNECTORS).map(([provider, config], index) => {
|
||||
const Icon = config.icon;
|
||||
const Icon = config.icon
|
||||
const connection = connections.find(
|
||||
(conn) => conn.provider === provider,
|
||||
);
|
||||
const isConnected = !!connection;
|
||||
)
|
||||
const isConnected = !!connection
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
|
@ -462,14 +465,14 @@ export function IntegrationsView() {
|
|||
</p>
|
||||
{isConnected ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full" />
|
||||
<span className="text-xs text-green-400 font-medium">
|
||||
Connected
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden sm:flex items-center gap-1">
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full" />
|
||||
<span className="text-xs text-gray-400 font-medium">
|
||||
Disconnected
|
||||
</span>
|
||||
|
|
@ -509,7 +512,7 @@ export function IntegrationsView() {
|
|||
) : (
|
||||
<div className="flex items-center justify-between gap-2 w-full sm:w-auto">
|
||||
<div className="sm:hidden flex items-center gap-1">
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full" />
|
||||
<span className="text-xs text-gray-400 font-medium">
|
||||
Disconnected
|
||||
</span>
|
||||
|
|
@ -520,12 +523,14 @@ export function IntegrationsView() {
|
|||
className="flex-shrink-0"
|
||||
>
|
||||
<Button
|
||||
className="bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border-blue-600/30 min-w-[80px]"
|
||||
disabled={addConnectionMutation.isPending}
|
||||
className="bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border-blue-600/30 min-w-[80px] disabled:cursor-not-allowed"
|
||||
disabled={
|
||||
addConnectionMutation.isPending || !isProUser
|
||||
}
|
||||
onClick={() => {
|
||||
addConnectionMutation.mutate(
|
||||
provider as ConnectorProvider,
|
||||
);
|
||||
)
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
|
|
@ -540,7 +545,7 @@ export function IntegrationsView() {
|
|||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -665,5 +670,5 @@ export function IntegrationsView() {
|
|||
</DialogPortal>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import type { useCustomer } from "autumn-js/react"
|
||||
import { $fetch } from "./api"
|
||||
import { toast } from "sonner"
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "../validation/api"
|
||||
import type { z } from "zod"
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
|
||||
export const fetchSubscriptionStatus = (
|
||||
autumn: ReturnType<typeof useCustomer>,
|
||||
|
|
@ -84,3 +91,68 @@ export const fetchProProduct = (autumn: ReturnType<typeof useCustomer>) =>
|
|||
staleTime: 30 * 1000, // 30 seconds
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes
|
||||
})
|
||||
|
||||
export const useDeleteDocument = (selectedProject: string) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (documentId: string) => {
|
||||
// context for LLM: delete/memories/:documentId is documents delete endpoint not memories delete endpoint
|
||||
const response = await $fetch(`@delete/memories/${documentId}`)
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to delete document")
|
||||
}
|
||||
return response.data
|
||||
},
|
||||
onMutate: async (documentId: string) => {
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["documents-with-memories", selectedProject],
|
||||
})
|
||||
|
||||
const previousData = queryClient.getQueryData([
|
||||
"documents-with-memories",
|
||||
selectedProject,
|
||||
])
|
||||
|
||||
queryClient.setQueryData(
|
||||
["documents-with-memories", selectedProject],
|
||||
(old: unknown) => {
|
||||
if (!old || typeof old !== "object") return old
|
||||
const typedOld = old as {
|
||||
pages?: Array<{ documents?: DocumentWithMemories[] }>
|
||||
}
|
||||
return {
|
||||
...typedOld,
|
||||
pages: typedOld.pages?.map((page) => ({
|
||||
...page,
|
||||
documents: page.documents?.filter(
|
||||
(doc: DocumentWithMemories) => doc.id !== documentId,
|
||||
),
|
||||
})),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return { previousData }
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Memory deleted successfully")
|
||||
},
|
||||
onError: (error, _documentId, context) => {
|
||||
if (context?.previousData) {
|
||||
queryClient.setQueryData(
|
||||
["documents-with-memories", selectedProject],
|
||||
context.previousData,
|
||||
)
|
||||
}
|
||||
toast.error("Failed to delete memory", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["documents-with-memories", selectedProject],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue