mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
Enhanced Text Editor and format fixes with biome (#371)
This commit is contained in:
parent
890def8f89
commit
9e1e392a2b
149 changed files with 7812 additions and 6732 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { LoginPage } from "@repo/ui/pages/login"
|
||||
import { LoginPage } from "@repo/ui/pages/login";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
|
|
@ -8,5 +8,5 @@ export default function Page() {
|
|||
"Private, secure, and reliable.",
|
||||
]}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,39 @@
|
|||
import type { Metadata } from "next"
|
||||
import { JetBrains_Mono, Inter } from "next/font/google"
|
||||
import "../globals.css"
|
||||
import "@ui/globals.css"
|
||||
import { AuthProvider } from "@lib/auth-context"
|
||||
import { ErrorTrackingProvider } from "@lib/error-tracking"
|
||||
import { PostHogProvider } from "@lib/posthog"
|
||||
import { QueryProvider } from "@lib/query-client"
|
||||
import { AutumnProvider } from "autumn-js/react"
|
||||
import { Suspense } from "react"
|
||||
import { Toaster } from "sonner"
|
||||
import { TourProvider } from "@/components/tour"
|
||||
import { MobilePanelProvider } from "@/lib/mobile-panel-context"
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||
import "../globals.css";
|
||||
import "@ui/globals.css";
|
||||
import { AuthProvider } from "@lib/auth-context";
|
||||
import { ErrorTrackingProvider } from "@lib/error-tracking";
|
||||
import { PostHogProvider } from "@lib/posthog";
|
||||
import { QueryProvider } from "@lib/query-client";
|
||||
import { AutumnProvider } from "autumn-js/react";
|
||||
import { Suspense } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { TourProvider } from "@/components/tour";
|
||||
import { MobilePanelProvider } from "@/lib/mobile-panel-context";
|
||||
|
||||
import { ViewModeProvider } from "@/lib/view-mode-context"
|
||||
import { ViewModeProvider } from "@/lib/view-mode-context";
|
||||
|
||||
const sans = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
})
|
||||
});
|
||||
|
||||
const mono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-mono",
|
||||
})
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://app.supermemory.ai"),
|
||||
description: "Your memories, wherever you are",
|
||||
title: "supermemory app",
|
||||
}
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html className="dark bg-sm-black" lang="en">
|
||||
|
|
@ -65,5 +65,5 @@ export default function RootLayout({
|
|||
</AutumnProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import type { MetadataRoute } from 'next'
|
||||
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: 'Supermemory',
|
||||
short_name: 'supermemory',
|
||||
description: 'Your memories, wherever you are',
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#ffffff',
|
||||
theme_color: '#000000',
|
||||
icons: [
|
||||
{
|
||||
src: '/images/logo.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: "Supermemory",
|
||||
short_name: "supermemory",
|
||||
description: "Your memories, wherever you are",
|
||||
start_url: "/",
|
||||
display: "standalone",
|
||||
background_color: "#ffffff",
|
||||
theme_color: "#000000",
|
||||
icons: [
|
||||
{
|
||||
src: "/images/logo.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
"use client" // Error boundaries must be Client Components
|
||||
"use client"; // Error boundaries must be Client Components
|
||||
|
||||
import { Button } from "@ui/components/button"
|
||||
import { Title1Bold } from "@ui/text/title/title-1-bold"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
import { Button } from "@ui/components/button";
|
||||
import { Title1Bold } from "@ui/text/title/title-1-bold";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function NotFound({
|
||||
error,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
error: Error & { digest?: string };
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
console.error(error)
|
||||
}, [error])
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
|
|
@ -23,5 +23,5 @@ export default function NotFound({
|
|||
<Button onClick={() => router.back()}>Go back</Button>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
|
|
@ -7,32 +7,38 @@ import { MemoryGraph } from "@repo/ui/memory-graph";
|
|||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
import { Logo, LogoFull } from "@ui/assets/Logo";
|
||||
import { GlassMenuEffect } from "@ui/other/glass-effect";
|
||||
import { Button } from "@ui/components/button";
|
||||
import { Gift, LayoutGrid, List, LoaderIcon, MessageSquare, Unplug } from "lucide-react";
|
||||
import { GlassMenuEffect } from "@ui/other/glass-effect";
|
||||
import {
|
||||
Gift,
|
||||
LayoutGrid,
|
||||
List,
|
||||
LoaderIcon,
|
||||
MessageSquare,
|
||||
Unplug,
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { z } from "zod";
|
||||
import { MemoryListView } from "@/components/memory-list-view";
|
||||
import Menu from "@/components/menu";
|
||||
import type { TourStep } from "@/components/tour";
|
||||
import { TourAlertDialog, useTour } from "@/components/tour";
|
||||
import { useProject } from "@/stores";
|
||||
import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants";
|
||||
import { useViewMode } from "@/lib/view-mode-context";
|
||||
import { useChatOpen } from "@/stores";
|
||||
import { ChatRewrite } from "@/components/views/chat";
|
||||
import { useGraphHighlights } from "@/stores/highlights";
|
||||
import { ProjectSelector } from "@/components/project-selector";
|
||||
import { AddMemoryView } from "@/components/views/add-memory";
|
||||
import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal";
|
||||
import { ConnectAIModal } from "@/components/connect-ai-modal";
|
||||
import { InstallPrompt } from "@/components/install-prompt";
|
||||
import { MemoryListView } from "@/components/memory-list-view";
|
||||
import Menu from "@/components/menu";
|
||||
import { ProjectSelector } from "@/components/project-selector";
|
||||
import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal";
|
||||
import type { TourStep } from "@/components/tour";
|
||||
import { TourAlertDialog, useTour } from "@/components/tour";
|
||||
import { AddMemoryView } from "@/components/views/add-memory";
|
||||
import { ChatRewrite } from "@/components/views/chat";
|
||||
import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants";
|
||||
import { useViewMode } from "@/lib/view-mode-context";
|
||||
import { useChatOpen, useProject } from "@/stores";
|
||||
import { useGraphHighlights } from "@/stores/highlights";
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0];
|
||||
|
||||
const MemoryGraphPage = () => {
|
||||
const { documentIds: allHighlightDocumentIds } = useGraphHighlights();
|
||||
|
|
@ -41,26 +47,26 @@ const MemoryGraphPage = () => {
|
|||
const { selectedProject } = useProject();
|
||||
const { setSteps, isTourCompleted } = useTour();
|
||||
const { isOpen, setIsOpen } = useChatOpen();
|
||||
const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([])
|
||||
const [showAddMemoryView, setShowAddMemoryView] = useState(false)
|
||||
const [showReferralModal, setShowReferralModal] = useState(false)
|
||||
const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([]);
|
||||
const [showAddMemoryView, setShowAddMemoryView] = useState(false);
|
||||
const [showReferralModal, setShowReferralModal] = useState(false);
|
||||
|
||||
// Fetch projects meta to detect experimental flag
|
||||
const { data: projectsMeta = [] } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: async () => {
|
||||
const response = await $fetch("@get/projects")
|
||||
return response.data?.projects ?? []
|
||||
const response = await $fetch("@get/projects");
|
||||
return response.data?.projects ?? [];
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
});
|
||||
|
||||
const isCurrentProjectExperimental = !!projectsMeta.find(
|
||||
(p: any) => p.containerTag === selectedProject,
|
||||
)?.isExperimental
|
||||
)?.isExperimental;
|
||||
|
||||
// Tour state
|
||||
const [showTourDialog, setShowTourDialog] = useState(false)
|
||||
const [showTourDialog, setShowTourDialog] = useState(false);
|
||||
|
||||
// Define tour steps with useMemo to prevent recreation
|
||||
const tourSteps: TourStep[] = useMemo(() => {
|
||||
|
|
@ -193,36 +199,36 @@ const MemoryGraphPage = () => {
|
|||
selectorId: TOUR_STEP_IDS.FLOATING_CHAT,
|
||||
position: "left",
|
||||
},
|
||||
]
|
||||
}, [])
|
||||
];
|
||||
}, []);
|
||||
|
||||
// Check if tour has been completed before
|
||||
useEffect(() => {
|
||||
const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"
|
||||
const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true";
|
||||
if (!hasCompletedTour && !isTourCompleted) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowTourDialog(true)
|
||||
}, 1000) // Show after 1 second
|
||||
return () => clearTimeout(timer)
|
||||
setShowTourDialog(true);
|
||||
}, 1000); // Show after 1 second
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isTourCompleted])
|
||||
}, [isTourCompleted]);
|
||||
|
||||
// Set up tour steps
|
||||
useEffect(() => {
|
||||
setSteps(tourSteps)
|
||||
}, [setSteps, tourSteps])
|
||||
setSteps(tourSteps);
|
||||
}, [setSteps, tourSteps]);
|
||||
|
||||
// Save tour completion to localStorage
|
||||
useEffect(() => {
|
||||
if (isTourCompleted) {
|
||||
localStorage.setItem(TOUR_STORAGE_KEY, "true")
|
||||
localStorage.setItem(TOUR_STORAGE_KEY, "true");
|
||||
}
|
||||
}, [isTourCompleted])
|
||||
}, [isTourCompleted]);
|
||||
|
||||
// Progressive loading via useInfiniteQuery
|
||||
const IS_DEV = process.env.NODE_ENV === "development"
|
||||
const PAGE_SIZE = IS_DEV ? 3 : 100
|
||||
const MAX_TOTAL = 1000
|
||||
const IS_DEV = process.env.NODE_ENV === "development";
|
||||
const PAGE_SIZE = IS_DEV ? 3 : 100;
|
||||
const MAX_TOTAL = 1000;
|
||||
|
||||
const {
|
||||
data,
|
||||
|
|
@ -244,69 +250,76 @@ const MemoryGraphPage = () => {
|
|||
containerTags: selectedProject ? [selectedProject] : undefined,
|
||||
},
|
||||
disableValidation: true,
|
||||
})
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to fetch documents")
|
||||
throw new Error(response.error?.message || "Failed to fetch documents");
|
||||
}
|
||||
|
||||
return response.data
|
||||
return response.data;
|
||||
},
|
||||
getNextPageParam: (lastPage, allPages) => {
|
||||
const loaded = allPages.reduce(
|
||||
(acc, p) => acc + (p.documents?.length ?? 0),
|
||||
0,
|
||||
)
|
||||
if (loaded >= MAX_TOTAL) return undefined
|
||||
);
|
||||
if (loaded >= MAX_TOTAL) return undefined;
|
||||
|
||||
const { currentPage, totalPages } = lastPage.pagination
|
||||
const { currentPage, totalPages } = lastPage.pagination;
|
||||
if (currentPage < totalPages) {
|
||||
return currentPage + 1
|
||||
return currentPage + 1;
|
||||
}
|
||||
return undefined
|
||||
return undefined;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
});
|
||||
|
||||
const baseDocuments = useMemo(() => {
|
||||
return data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? []
|
||||
}, [data])
|
||||
return (
|
||||
data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? []
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
const allDocuments = useMemo(() => {
|
||||
if (injectedDocs.length === 0) return baseDocuments
|
||||
const byId = new Map<string, DocumentWithMemories>()
|
||||
for (const d of injectedDocs) byId.set(d.id, d)
|
||||
for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d)
|
||||
return Array.from(byId.values())
|
||||
}, [baseDocuments, injectedDocs])
|
||||
if (injectedDocs.length === 0) return baseDocuments;
|
||||
const byId = new Map<string, DocumentWithMemories>();
|
||||
for (const d of injectedDocs) byId.set(d.id, d);
|
||||
for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d);
|
||||
return Array.from(byId.values());
|
||||
}, [baseDocuments, injectedDocs]);
|
||||
|
||||
const totalLoaded = allDocuments.length
|
||||
const hasMore = hasNextPage
|
||||
const isLoadingMore = isFetchingNextPage
|
||||
const totalLoaded = allDocuments.length;
|
||||
const hasMore = hasNextPage;
|
||||
const isLoadingMore = isFetchingNextPage;
|
||||
|
||||
const loadMoreDocuments = useCallback(async (): Promise<void> => {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
await fetchNextPage()
|
||||
return
|
||||
await fetchNextPage();
|
||||
return;
|
||||
}
|
||||
return
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
|
||||
return;
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
// Reset injected docs when project changes
|
||||
useEffect(() => { setInjectedDocs([]) }, [selectedProject])
|
||||
useEffect(() => {
|
||||
setInjectedDocs([]);
|
||||
}, [selectedProject]);
|
||||
|
||||
// Surgical fetch of missing highlighted documents (customId-based IDs from search)
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) return
|
||||
const present = new Set<string>()
|
||||
if (!isOpen) return;
|
||||
if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0)
|
||||
return;
|
||||
const present = new Set<string>();
|
||||
for (const d of [...baseDocuments, ...injectedDocs]) {
|
||||
if (d.id) present.add(d.id)
|
||||
if ((d as any).customId) present.add((d as any).customId as string)
|
||||
if (d.id) present.add(d.id);
|
||||
if ((d as any).customId) present.add((d as any).customId as string);
|
||||
}
|
||||
const missing = allHighlightDocumentIds.filter((id: string) => !present.has(id))
|
||||
if (missing.length === 0) return
|
||||
let cancelled = false
|
||||
const missing = allHighlightDocumentIds.filter(
|
||||
(id: string) => !present.has(id),
|
||||
);
|
||||
if (missing.length === 0) return;
|
||||
let cancelled = false;
|
||||
const run = async () => {
|
||||
try {
|
||||
const resp = await $fetch("@post/memories/documents/by-ids", {
|
||||
|
|
@ -316,49 +329,63 @@ const MemoryGraphPage = () => {
|
|||
containerTags: selectedProject ? [selectedProject] : undefined,
|
||||
},
|
||||
disableValidation: true,
|
||||
})
|
||||
if (cancelled || (resp as any)?.error) return
|
||||
const extraDocs = (resp as any)?.data?.documents as DocumentWithMemories[] | undefined
|
||||
if (!extraDocs || extraDocs.length === 0) return
|
||||
});
|
||||
if (cancelled || (resp as any)?.error) return;
|
||||
const extraDocs = (resp as any)?.data?.documents as
|
||||
| DocumentWithMemories[]
|
||||
| undefined;
|
||||
if (!extraDocs || extraDocs.length === 0) return;
|
||||
setInjectedDocs((prev) => {
|
||||
const seen = new Set<string>([...prev.map((d) => d.id), ...baseDocuments.map((d) => d.id)])
|
||||
const merged = [...prev]
|
||||
const seen = new Set<string>([
|
||||
...prev.map((d) => d.id),
|
||||
...baseDocuments.map((d) => d.id),
|
||||
]);
|
||||
const merged = [...prev];
|
||||
for (const doc of extraDocs) {
|
||||
if (!seen.has(doc.id)) {
|
||||
merged.push(doc)
|
||||
seen.add(doc.id)
|
||||
merged.push(doc);
|
||||
seen.add(doc.id);
|
||||
}
|
||||
}
|
||||
return merged
|
||||
})
|
||||
} catch { }
|
||||
}
|
||||
void run()
|
||||
return () => { cancelled = true }
|
||||
}, [isOpen, allHighlightDocumentIds.join('|'), baseDocuments, injectedDocs, selectedProject, $fetch])
|
||||
return merged;
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
void run();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
isOpen,
|
||||
allHighlightDocumentIds.join("|"),
|
||||
baseDocuments,
|
||||
injectedDocs,
|
||||
selectedProject,
|
||||
$fetch,
|
||||
]);
|
||||
|
||||
// Handle view mode change
|
||||
const handleViewModeChange = useCallback(
|
||||
(mode: "graph" | "list") => {
|
||||
setViewMode(mode)
|
||||
setViewMode(mode);
|
||||
},
|
||||
[setViewMode],
|
||||
)
|
||||
);
|
||||
|
||||
// Prevent body scrolling
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = 'hidden'
|
||||
document.body.style.height = '100vh'
|
||||
document.documentElement.style.overflow = 'hidden'
|
||||
document.documentElement.style.height = '100vh'
|
||||
|
||||
document.body.style.overflow = "hidden";
|
||||
document.body.style.height = "100vh";
|
||||
document.documentElement.style.overflow = "hidden";
|
||||
document.documentElement.style.height = "100vh";
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = ''
|
||||
document.body.style.height = ''
|
||||
document.documentElement.style.overflow = ''
|
||||
document.documentElement.style.height = ''
|
||||
}
|
||||
}, [])
|
||||
document.body.style.overflow = "";
|
||||
document.body.style.height = "";
|
||||
document.documentElement.style.overflow = "";
|
||||
document.documentElement.style.height = "";
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative h-screen bg-[#0f1419] overflow-hidden touch-none">
|
||||
|
|
@ -409,52 +436,52 @@ const MemoryGraphPage = () => {
|
|||
</span>
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
animate={{
|
||||
color: viewMode === "list" ? "#93c5fd" : "#cbd5e1",
|
||||
}}
|
||||
className="relative h-8 px-3 flex items-center gap-2 text-sm font-medium rounded-md transition-colors"
|
||||
onClick={() => handleViewModeChange("list")}
|
||||
transition={{ duration: 0.2 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{viewMode === "list" && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-blue-500/20 rounded-md"
|
||||
layoutId="activeBackground"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<List className="w-4 h-4" />
|
||||
<span className="hidden md:inline">List</span>
|
||||
</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.button
|
||||
animate={{
|
||||
color: viewMode === "list" ? "#93c5fd" : "#cbd5e1",
|
||||
}}
|
||||
className="relative h-8 px-3 flex items-center gap-2 text-sm font-medium rounded-md transition-colors"
|
||||
onClick={() => handleViewModeChange("list")}
|
||||
transition={{ duration: 0.2 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{viewMode === "list" && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-blue-500/20 rounded-md"
|
||||
layoutId="activeBackground"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<List className="w-4 h-4" />
|
||||
<span className="hidden md:inline">List</span>
|
||||
</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Animated content switching */}
|
||||
<AnimatePresence mode="wait">
|
||||
{viewMode === "graph" ? (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="absolute inset-0"
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
id={TOUR_STEP_IDS.MEMORY_GRAPH}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
key="graph"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
}}
|
||||
>
|
||||
<MemoryGraph
|
||||
{/* Animated content switching */}
|
||||
<AnimatePresence mode="wait">
|
||||
{viewMode === "graph" ? (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="absolute inset-0"
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
id={TOUR_STEP_IDS.MEMORY_GRAPH}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
key="graph"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
}}
|
||||
>
|
||||
<MemoryGraph
|
||||
documents={allDocuments}
|
||||
error={error}
|
||||
hasMore={hasMore}
|
||||
|
|
@ -470,100 +497,109 @@ const MemoryGraphPage = () => {
|
|||
occludedRightPx={isOpen && !isMobile ? 600 : 0}
|
||||
autoLoadOnViewport={false}
|
||||
isExperimental={isCurrentProjectExperimental}
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="rounded-xl overflow-hidden">
|
||||
<div className="relative z-10 text-slate-200 px-6 py-4 text-center">
|
||||
<p className="text-lg font-medium mb-2">No Memories to Visualize</p>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition-colors cursor-pointer underline"
|
||||
onClick={() => setShowAddMemoryView(true)}
|
||||
>
|
||||
Create one?
|
||||
</button>
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="rounded-xl overflow-hidden">
|
||||
<div className="relative z-10 text-slate-200 px-6 py-4 text-center">
|
||||
<p className="text-lg font-medium mb-2">
|
||||
No Memories to Visualize
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition-colors cursor-pointer underline"
|
||||
onClick={() => setShowAddMemoryView(true)}
|
||||
>
|
||||
Create one?
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MemoryGraph>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="absolute inset-0 md:ml-18"
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
id={TOUR_STEP_IDS.MEMORY_LIST}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
key="list"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
}}
|
||||
>
|
||||
<MemoryListView
|
||||
documents={allDocuments}
|
||||
error={error}
|
||||
hasMore={hasMore}
|
||||
isLoading={isPending}
|
||||
isLoadingMore={isLoadingMore}
|
||||
loadMoreDocuments={loadMoreDocuments}
|
||||
totalLoaded={totalLoaded}
|
||||
</MemoryGraph>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="absolute inset-0 md:ml-18"
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
id={TOUR_STEP_IDS.MEMORY_LIST}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
key="list"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="rounded-xl overflow-hidden">
|
||||
<div className="relative z-10 text-slate-200 px-6 py-4 text-center">
|
||||
<p className="text-lg font-medium mb-2">No Memories to Visualize</p>
|
||||
<button
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition-colors cursor-pointer underline"
|
||||
onClick={() => setShowAddMemoryView(true)}
|
||||
type="button"
|
||||
>
|
||||
Create one?
|
||||
</button>
|
||||
<MemoryListView
|
||||
documents={allDocuments}
|
||||
error={error}
|
||||
hasMore={hasMore}
|
||||
isLoading={isPending}
|
||||
isLoadingMore={isLoadingMore}
|
||||
loadMoreDocuments={loadMoreDocuments}
|
||||
totalLoaded={totalLoaded}
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="rounded-xl overflow-hidden">
|
||||
<div className="relative z-10 text-slate-200 px-6 py-4 text-center">
|
||||
<p className="text-lg font-medium mb-2">
|
||||
No Memories to Visualize
|
||||
</p>
|
||||
<button
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition-colors cursor-pointer underline"
|
||||
onClick={() => setShowAddMemoryView(true)}
|
||||
type="button"
|
||||
>
|
||||
Create one?
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MemoryListView>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</MemoryListView>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Top Bar */}
|
||||
<div className="absolute top-2 left-0 right-0 z-10 p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 justify-between w-full md:w-fit md:justify-start">
|
||||
<Link
|
||||
className="pointer-events-auto"
|
||||
href="https://supermemory.ai"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<LogoFull className="h-8 hidden md:block" id={TOUR_STEP_IDS.LOGO} />
|
||||
<Logo className="h-8 md:hidden" id={TOUR_STEP_IDS.LOGO} />
|
||||
</Link>
|
||||
{/* Top Bar */}
|
||||
<div className="absolute top-2 left-0 right-0 z-10 p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 justify-between w-full md:w-fit md:justify-start">
|
||||
<Link
|
||||
className="pointer-events-auto"
|
||||
href="https://supermemory.ai"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<LogoFull
|
||||
className="h-8 hidden md:block"
|
||||
id={TOUR_STEP_IDS.LOGO}
|
||||
/>
|
||||
<Logo className="h-8 md:hidden" id={TOUR_STEP_IDS.LOGO} />
|
||||
</Link>
|
||||
|
||||
<div className="hidden sm:block">
|
||||
<ProjectSelector />
|
||||
<div className="hidden sm:block">
|
||||
<ProjectSelector />
|
||||
</div>
|
||||
|
||||
<ConnectAIModal>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-white/5 hover:bg-white/10 border-white/20 text-white hover:text-white px-2 sm:px-3"
|
||||
>
|
||||
<Unplug className="h-4 w-4" />
|
||||
<span className="hidden sm:inline ml-2">
|
||||
Connect to your AI
|
||||
</span>
|
||||
<span className="sm:hidden ml-1">Connect AI</span>
|
||||
</Button>
|
||||
</ConnectAIModal>
|
||||
</div>
|
||||
|
||||
<ConnectAIModal>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-white/5 hover:bg-white/10 border-white/20 text-white hover:text-white px-2 sm:px-3"
|
||||
>
|
||||
<Unplug className="h-4 w-4" />
|
||||
<span className="hidden sm:inline ml-2">Connect to your AI</span>
|
||||
<span className="sm:hidden ml-1">Connect AI</span>
|
||||
</Button>
|
||||
</ConnectAIModal>
|
||||
<div>
|
||||
<Menu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Menu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Open Chat Button */}
|
||||
{!isOpen && !isMobile && (
|
||||
<motion.div
|
||||
|
|
@ -598,7 +634,7 @@ const MemoryGraphPage = () => {
|
|||
id={TOUR_STEP_IDS.FLOATING_CHAT}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ x: isOpen ? 0 : (isMobile ? "100%" : 600) }}
|
||||
animate={{ x: isOpen ? 0 : isMobile ? "100%" : 600 }}
|
||||
className="absolute inset-0"
|
||||
exit={{ x: isMobile ? "100%" : 600 }}
|
||||
initial={{ x: isMobile ? "100%" : 600 }}
|
||||
|
|
@ -607,7 +643,8 @@ const MemoryGraphPage = () => {
|
|||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 40,
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<ChatRewrite />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
|
@ -623,18 +660,18 @@ const MemoryGraphPage = () => {
|
|||
<TourAlertDialog onOpenChange={setShowTourDialog} open={showTourDialog} />
|
||||
|
||||
{/* Referral/Upgrade Modal */}
|
||||
<ReferralUpgradeModal
|
||||
isOpen={showReferralModal}
|
||||
onClose={() => setShowReferralModal(false)}
|
||||
<ReferralUpgradeModal
|
||||
isOpen={showReferralModal}
|
||||
onClose={() => setShowReferralModal(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Wrapper component to handle auth and waitlist checks
|
||||
export default function Page() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
|
||||
// Check waitlist status
|
||||
const {
|
||||
|
|
@ -645,24 +682,24 @@ export default function Page() {
|
|||
queryKey: ["waitlist-status", user?.id],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await $fetch("@get/waitlist/status")
|
||||
return response.data
|
||||
const response = await $fetch("@get/waitlist/status");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error checking waitlist status:", error)
|
||||
console.error("Error checking waitlist status:", error);
|
||||
// Return null to indicate error, will handle in useEffect
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
},
|
||||
enabled: !!user && !user.isAnonymous,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
retry: 1, // Only retry once on failure
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (waitlistStatus && !waitlistStatus.accessGranted) {
|
||||
router.push("/waitlist")
|
||||
router.push("/waitlist");
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
// Show loading state while checking authentication and waitlist status
|
||||
if (!user || isCheckingWaitlist) {
|
||||
|
|
@ -673,9 +710,14 @@ export default function Page() {
|
|||
<p className="text-white/60">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// If we have a user and they have access, show the main component
|
||||
return <><MemoryGraphPage /><InstallPrompt /></>;
|
||||
return (
|
||||
<>
|
||||
<MemoryGraphPage />
|
||||
<InstallPrompt />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,41 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { $fetch } from "@lib/api"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { $fetch } from "@lib/api";
|
||||
import { Button } from "@ui/components/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/components/card"
|
||||
import { CheckIcon, CopyIcon, LoaderIcon, ShareIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useRouter, useParams } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
} from "@ui/components/card";
|
||||
import { CheckIcon, CopyIcon, LoaderIcon, ShareIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function ReferralPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const referralCode = params.code as string
|
||||
|
||||
const [isJoiningWaitlist, setIsJoiningWaitlist] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [referralData, setReferralData] = useState<{
|
||||
referrerName?: string
|
||||
valid: boolean
|
||||
} | null>(null)
|
||||
const [copiedLink, setCopiedLink] = useState(false)
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const referralCode = params.code as string;
|
||||
|
||||
const referralLink = `https://supermemory.ai/ref/${referralCode}`
|
||||
const [isJoiningWaitlist, setIsJoiningWaitlist] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [referralData, setReferralData] = useState<{
|
||||
referrerName?: string;
|
||||
valid: boolean;
|
||||
} | null>(null);
|
||||
const [copiedLink, setCopiedLink] = useState(false);
|
||||
|
||||
const referralLink = `https://supermemory.ai/ref/${referralCode}`;
|
||||
|
||||
// Verify referral code and get referrer info
|
||||
useEffect(() => {
|
||||
async function checkReferral() {
|
||||
if (!referralCode) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -43,42 +43,42 @@ export default function ReferralPage() {
|
|||
// For now, we'll assume it's valid - in the future this should call an API
|
||||
setReferralData({
|
||||
valid: true,
|
||||
referrerName: "A supermemory user" // Placeholder - should come from API
|
||||
})
|
||||
referrerName: "A supermemory user", // Placeholder - should come from API
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error checking referral:", error)
|
||||
setReferralData({ valid: false })
|
||||
console.error("Error checking referral:", error);
|
||||
setReferralData({ valid: false });
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
checkReferral()
|
||||
}, [referralCode])
|
||||
checkReferral();
|
||||
}, [referralCode]);
|
||||
|
||||
const handleJoinWaitlist = async () => {
|
||||
setIsJoiningWaitlist(true)
|
||||
setIsJoiningWaitlist(true);
|
||||
try {
|
||||
// Redirect to waitlist signup with referral code
|
||||
router.push(`/waitlist?ref=${referralCode}`)
|
||||
router.push(`/waitlist?ref=${referralCode}`);
|
||||
} catch (error) {
|
||||
console.error("Error joining waitlist:", error)
|
||||
toast.error("Failed to join waitlist. Please try again.")
|
||||
console.error("Error joining waitlist:", error);
|
||||
toast.error("Failed to join waitlist. Please try again.");
|
||||
} finally {
|
||||
setIsJoiningWaitlist(false)
|
||||
setIsJoiningWaitlist(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(referralLink)
|
||||
setCopiedLink(true)
|
||||
toast.success("Referral link copied!")
|
||||
setTimeout(() => setCopiedLink(false), 2000)
|
||||
await navigator.clipboard.writeText(referralLink);
|
||||
setCopiedLink(true);
|
||||
toast.success("Referral link copied!");
|
||||
setTimeout(() => setCopiedLink(false), 2000);
|
||||
} catch (error) {
|
||||
toast.error("Failed to copy link")
|
||||
toast.error("Failed to copy link");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
if (navigator.share) {
|
||||
|
|
@ -86,11 +86,11 @@ export default function ReferralPage() {
|
|||
title: "Join supermemory",
|
||||
text: "I'm excited about supermemory - it's going to change how we store and interact with our memories!",
|
||||
url: referralLink,
|
||||
})
|
||||
});
|
||||
} else {
|
||||
handleCopyLink()
|
||||
handleCopyLink();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
@ -100,7 +100,7 @@ export default function ReferralPage() {
|
|||
<p className="text-white/60">Checking invitation...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!referralData?.valid) {
|
||||
|
|
@ -118,15 +118,13 @@ export default function ReferralPage() {
|
|||
<CardContent>
|
||||
<div className="text-center">
|
||||
<Button asChild className="w-full">
|
||||
<Link href="https://supermemory.ai">
|
||||
Go to supermemory
|
||||
</Link>
|
||||
<Link href="https://supermemory.ai">Go to supermemory</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -142,21 +140,25 @@ export default function ReferralPage() {
|
|||
You're invited to supermemory!
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60 mt-2">
|
||||
{referralData.referrerName} invited you to join the future of memory management.
|
||||
{referralData.referrerName} invited you to join the future of
|
||||
memory management.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-[#0f1419] rounded-lg p-4 border border-white/10">
|
||||
<h3 className="text-white font-semibold mb-2">What is supermemory?</h3>
|
||||
<h3 className="text-white font-semibold mb-2">
|
||||
What is supermemory?
|
||||
</h3>
|
||||
<p className="text-white/70 text-sm leading-relaxed">
|
||||
supermemory is an AI-powered personal knowledge base that helps you store,
|
||||
organize, and interact with all your digital memories - from documents
|
||||
and links to conversations and ideas.
|
||||
supermemory is an AI-powered personal knowledge base that
|
||||
helps you store, organize, and interact with all your digital
|
||||
memories - from documents and links to conversations and
|
||||
ideas.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
||||
<Button
|
||||
onClick={handleJoinWaitlist}
|
||||
disabled={isJoiningWaitlist}
|
||||
className="w-full bg-orange-500 hover:bg-orange-600 text-white"
|
||||
|
|
@ -169,8 +171,8 @@ export default function ReferralPage() {
|
|||
</Button>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="https://supermemory.ai"
|
||||
<Link
|
||||
href="https://supermemory.ai"
|
||||
className="text-orange-500 hover:text-orange-400 text-sm underline"
|
||||
>
|
||||
Learn more about supermemory
|
||||
|
|
@ -183,7 +185,9 @@ export default function ReferralPage() {
|
|||
{/* Share Card */}
|
||||
<Card className="bg-[#1a1f2a] border-white/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg text-white">Share with friends</CardTitle>
|
||||
<CardTitle className="text-lg text-white">
|
||||
Share with friends
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60">
|
||||
Help others discover supermemory and earn priority access.
|
||||
</CardDescription>
|
||||
|
|
@ -223,5 +227,5 @@ export default function ReferralPage() {
|
|||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Button } from "@ui/components/button"
|
||||
import { Button } from "@ui/components/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/components/card"
|
||||
import { ShareIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
} from "@ui/components/card";
|
||||
import { ShareIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function ReferralHomePage() {
|
||||
return (
|
||||
|
|
@ -23,28 +23,33 @@ export default function ReferralHomePage() {
|
|||
Missing Referral Code
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60 mt-2">
|
||||
It looks like you're missing a referral code. Get one from a friend or join directly!
|
||||
It looks like you're missing a referral code. Get one from a friend
|
||||
or join directly!
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-[#0f1419] rounded-lg p-4 border border-white/10">
|
||||
<h3 className="text-white font-semibold mb-2">What is supermemory?</h3>
|
||||
<h3 className="text-white font-semibold mb-2">
|
||||
What is supermemory?
|
||||
</h3>
|
||||
<p className="text-white/70 text-sm leading-relaxed">
|
||||
supermemory is an AI-powered personal knowledge base that helps you store,
|
||||
organize, and interact with all your digital memories.
|
||||
supermemory is an AI-powered personal knowledge base that helps
|
||||
you store, organize, and interact with all your digital
|
||||
memories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button asChild className="w-full bg-orange-500 hover:bg-orange-600 text-white">
|
||||
<Link href="/waitlist">
|
||||
Join the Waitlist
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
className="w-full bg-orange-500 hover:bg-orange-600 text-white"
|
||||
>
|
||||
<Link href="/waitlist">Join the Waitlist</Link>
|
||||
</Button>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="https://supermemory.ai"
|
||||
<Link
|
||||
href="https://supermemory.ai"
|
||||
className="text-orange-500 hover:text-orange-400 text-sm underline"
|
||||
>
|
||||
Learn more about supermemory
|
||||
|
|
@ -54,5 +59,5 @@ export default function ReferralHomePage() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,115 +1,115 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { $fetch } from "@lib/api"
|
||||
import { authClient } from "@lib/auth"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { fetchConsumerProProduct } from "@lib/queries"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { $fetch } from "@lib/api";
|
||||
import { authClient } from "@lib/auth";
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
import { fetchConsumerProProduct } from "@lib/queries";
|
||||
import { Button } from "@ui/components/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/components/card"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { Clock, LoaderIcon, SkipForwardIcon, LogOut } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
} from "@ui/components/card";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
import { Clock, LoaderIcon, LogOut, SkipForwardIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function WaitlistPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const referralCode = searchParams.get('ref')
|
||||
const { user } = useAuth()
|
||||
const [isChecking, setIsChecking] = useState(true)
|
||||
const [isSkippingWaitlist, setIsSkippingWaitlist] = useState(false)
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const referralCode = searchParams.get("ref");
|
||||
const { user } = useAuth();
|
||||
const [isChecking, setIsChecking] = useState(true);
|
||||
const [isSkippingWaitlist, setIsSkippingWaitlist] = useState(false);
|
||||
const [waitlistStatus, setWaitlistStatus] = useState<{
|
||||
inWaitlist: boolean
|
||||
accessGranted: boolean
|
||||
createdAt: string
|
||||
} | null>(null)
|
||||
const autumn = useCustomer()
|
||||
inWaitlist: boolean;
|
||||
accessGranted: boolean;
|
||||
createdAt: string;
|
||||
} | null>(null);
|
||||
const autumn = useCustomer();
|
||||
|
||||
// @ts-ignore
|
||||
const { data: earlyAccess } = fetchConsumerProProduct(autumn)
|
||||
// @ts-expect-error
|
||||
const { data: earlyAccess } = fetchConsumerProProduct(autumn);
|
||||
|
||||
const handleSkipWaitlist = async () => {
|
||||
setIsSkippingWaitlist(true)
|
||||
setIsSkippingWaitlist(true);
|
||||
try {
|
||||
const res = await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
forceCheckout: true,
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
})
|
||||
});
|
||||
if (res.data && "checkout_url" in res.data && res.data.checkout_url) {
|
||||
router.push(res.data.checkout_url)
|
||||
router.push(res.data.checkout_url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error skipping waitlist:", error)
|
||||
console.error("Error skipping waitlist:", error);
|
||||
} finally {
|
||||
setIsSkippingWaitlist(false)
|
||||
setIsSkippingWaitlist(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await authClient.signOut()
|
||||
router.push("/")
|
||||
await authClient.signOut();
|
||||
router.push("/");
|
||||
} catch (error) {
|
||||
console.error("Error signing out:", error)
|
||||
toast.error("Failed to sign out")
|
||||
console.error("Error signing out:", error);
|
||||
toast.error("Failed to sign out");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function checkAccess() {
|
||||
if (!user) {
|
||||
router.push("/")
|
||||
return
|
||||
router.push("/");
|
||||
return;
|
||||
}
|
||||
|
||||
// Anonymous users should sign in first
|
||||
if (user.isAnonymous) {
|
||||
authClient.signOut()
|
||||
router.push("/")
|
||||
return
|
||||
authClient.signOut();
|
||||
router.push("/");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check waitlist status using the new endpoint
|
||||
const response = await $fetch("@get/waitlist/status")
|
||||
const response = await $fetch("@get/waitlist/status");
|
||||
|
||||
if (response.data) {
|
||||
setWaitlistStatus(response.data)
|
||||
setWaitlistStatus(response.data);
|
||||
|
||||
if (!response.data.inWaitlist) {
|
||||
authClient.signOut()
|
||||
router.push("/login")
|
||||
authClient.signOut();
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
// If user has access, redirect to home
|
||||
if (response.data.accessGranted) {
|
||||
router.push("/")
|
||||
router.push("/");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking waitlist status:", error)
|
||||
console.error("Error checking waitlist status:", error);
|
||||
// If there's an error, assume user is on waitlist
|
||||
setWaitlistStatus({
|
||||
inWaitlist: true,
|
||||
accessGranted: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
});
|
||||
} finally {
|
||||
setIsChecking(false)
|
||||
setIsChecking(false);
|
||||
}
|
||||
}
|
||||
|
||||
checkAccess()
|
||||
}, [user, router])
|
||||
checkAccess();
|
||||
}, [user, router]);
|
||||
|
||||
if (isChecking) {
|
||||
return (
|
||||
|
|
@ -119,7 +119,7 @@ export default function WaitlistPage() {
|
|||
<p className="text-white/60">Checking access...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -133,10 +133,9 @@ export default function WaitlistPage() {
|
|||
You're on the waitlist!
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60 mt-2">
|
||||
{referralCode
|
||||
{referralCode
|
||||
? "Thanks for joining through a friend's invitation! You've been added to the waitlist with priority access."
|
||||
: "Thanks for your interest in supermemory. We'll notify you as soon as we're ready for you."
|
||||
}
|
||||
: "Thanks for your interest in supermemory. We'll notify you as soon as we're ready for you."}
|
||||
</CardDescription>
|
||||
{referralCode && (
|
||||
<div className="mt-3 px-3 py-2 bg-orange-500/10 rounded-lg border border-orange-500/20">
|
||||
|
|
@ -199,8 +198,8 @@ export default function WaitlistPage() {
|
|||
<p className="text-white/50 text-xs">
|
||||
Signed in as {user.email}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleLogout}
|
||||
className="border-white/20 hover:bg-white/5"
|
||||
|
|
@ -214,5 +213,5 @@ export default function WaitlistPage() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
|
||||
"extends": "//",
|
||||
"root": false,
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
|
||||
"linter": {
|
||||
"domains": {
|
||||
"next": "recommended",
|
||||
"react": "recommended"
|
||||
"rules": {
|
||||
"nursery": {
|
||||
"recommended": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { cn } from "@lib/utils"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
|
|
@ -32,7 +32,7 @@ const buttonVariants = cva(
|
|||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
|
|
@ -42,9 +42,9 @@ function Button({
|
|||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
@ -52,7 +52,7 @@ function Button({
|
|||
data-slot="button"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import { Button } from "@ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -9,20 +9,20 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@ui/components/dialog"
|
||||
import { Input } from "@ui/components/input"
|
||||
} from "@ui/components/dialog";
|
||||
import { Input } from "@ui/components/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/select"
|
||||
import { CopyableCell } from "@ui/copyable-cell"
|
||||
import { CopyIcon, ExternalLink } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
} from "@ui/components/select";
|
||||
import { CopyableCell } from "@ui/copyable-cell";
|
||||
import { CopyIcon, ExternalLink } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const clients = {
|
||||
cursor: "Cursor",
|
||||
|
|
@ -34,23 +34,23 @@ const clients = {
|
|||
"roo-cline": "Roo Cline",
|
||||
witsy: "Witsy",
|
||||
enconvo: "Enconvo",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
interface ConnectAIModalProps {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ConnectAIModal({ children }: ConnectAIModalProps) {
|
||||
const [client, setClient] = useState<keyof typeof clients>("cursor")
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [showAllTools, setShowAllTools] = useState(false)
|
||||
const isMobile = useIsMobile()
|
||||
const installCommand = `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${client} --oauth=yes`
|
||||
const [client, setClient] = useState<keyof typeof clients>("cursor");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showAllTools, setShowAllTools] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
const installCommand = `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${client} --oauth=yes`;
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(installCommand)
|
||||
toast.success("Copied to clipboard!")
|
||||
}
|
||||
navigator.clipboard.writeText(installCommand);
|
||||
toast.success("Copied to clipboard!");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
|
|
@ -59,12 +59,13 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) {
|
|||
<DialogHeader>
|
||||
<DialogTitle>Connect Supermemory to Your AI</DialogTitle>
|
||||
<DialogDescription>
|
||||
Connect supermemory to your favorite AI tools using the Model Context Protocol (MCP).
|
||||
This allows your AI assistant to create, search, and access your memories directly.
|
||||
Connect supermemory to your favorite AI tools using the Model
|
||||
Context Protocol (MCP). This allows your AI assistant to create,
|
||||
search, and access your memories directly.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mb-6 block md:hidden">
|
||||
<div className="mb-6 block md:hidden">
|
||||
<label
|
||||
className="text-sm font-medium text-white/80 block mb-2"
|
||||
htmlFor="mcp-server-url"
|
||||
|
|
@ -78,10 +79,11 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) {
|
|||
/>
|
||||
</div>
|
||||
<p className="text-xs text-white/50 mt-2">
|
||||
Click URL to copy to clipboard. Use this URL to configure supermemory in your AI assistant.
|
||||
Click URL to copy to clipboard. Use this URL to configure
|
||||
supermemory in your AI assistant.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="hidden md:block">
|
||||
<h3 className="text-sm font-medium mb-3">Supported AI Tools</h3>
|
||||
|
|
@ -102,12 +104,18 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) {
|
|||
className={"rounded object-contain"}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
target.style.display = "none";
|
||||
const parent = target.parentElement;
|
||||
if (parent && !parent.querySelector('.fallback-text')) {
|
||||
const fallback = document.createElement('span');
|
||||
fallback.className = 'fallback-text text-xs font-bold text-muted-foreground';
|
||||
fallback.textContent = clientName.substring(0, 2).toUpperCase();
|
||||
if (
|
||||
parent &&
|
||||
!parent.querySelector(".fallback-text")
|
||||
) {
|
||||
const fallback = document.createElement("span");
|
||||
fallback.className =
|
||||
"fallback-text text-xs font-bold text-muted-foreground";
|
||||
fallback.textContent = clientName
|
||||
.substring(0, 2)
|
||||
.toUpperCase();
|
||||
parent.appendChild(fallback);
|
||||
}
|
||||
}}
|
||||
|
|
@ -136,7 +144,9 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) {
|
|||
<div className="space-y-3 flex gap-2 items-center justify-between">
|
||||
<Select
|
||||
value={client}
|
||||
onValueChange={(value) => setClient(value as keyof typeof clients)}
|
||||
onValueChange={(value) =>
|
||||
setClient(value as keyof typeof clients)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-48 mb-0">
|
||||
<SelectValue />
|
||||
|
|
@ -154,12 +164,18 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) {
|
|||
className="rounded object-contain"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
target.style.display = "none";
|
||||
const parent = target.parentElement;
|
||||
if (parent && !parent.querySelector('.fallback-text')) {
|
||||
const fallback = document.createElement('span');
|
||||
fallback.className = 'fallback-text text-xs font-bold text-muted-foreground';
|
||||
fallback.textContent = value.substring(0, 1).toUpperCase();
|
||||
if (
|
||||
parent &&
|
||||
!parent.querySelector(".fallback-text")
|
||||
) {
|
||||
const fallback = document.createElement("span");
|
||||
fallback.className =
|
||||
"fallback-text text-xs font-bold text-muted-foreground";
|
||||
fallback.textContent = value
|
||||
.substring(0, 1)
|
||||
.toUpperCase();
|
||||
parent.appendChild(fallback);
|
||||
}
|
||||
}}
|
||||
|
|
@ -171,22 +187,22 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) {
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="relative w-full flex items-center">
|
||||
<Input
|
||||
className="font-mono text-xs w-full pr-10"
|
||||
readOnly
|
||||
value={installCommand}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={copyToClipboard}
|
||||
className="absolute right-0 cursor-pointer"
|
||||
variant="ghost"
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full flex items-center">
|
||||
<Input
|
||||
className="font-mono text-xs w-full pr-10"
|
||||
readOnly
|
||||
value={installCommand}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={copyToClipboard}
|
||||
className="absolute right-0 cursor-pointer"
|
||||
variant="ghost"
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -203,17 +219,20 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) {
|
|||
<div className="flex justify-between items-center pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.open("https://docs.supermemory.ai/supermemory-mcp/introduction", "_blank")}
|
||||
onClick={() =>
|
||||
window.open(
|
||||
"https://docs.supermemory.ai/supermemory-mcp/introduction",
|
||||
"_blank",
|
||||
)
|
||||
}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
Learn More
|
||||
</Button>
|
||||
<Button onClick={() => setIsOpen(false)}>
|
||||
Done
|
||||
</Button>
|
||||
<Button onClick={() => setIsOpen(false)}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -8,37 +8,40 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@repo/ui/components/dialog"
|
||||
import { Input } from "@repo/ui/components/input"
|
||||
import { Label } from "@repo/ui/components/label"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { useState } from "react"
|
||||
import { useProjectMutations } from "@/hooks/use-project-mutations"
|
||||
} from "@repo/ui/components/dialog";
|
||||
import { Input } from "@repo/ui/components/input";
|
||||
import { Label } from "@repo/ui/components/label";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useState } from "react";
|
||||
import { useProjectMutations } from "@/hooks/use-project-mutations";
|
||||
|
||||
interface CreateProjectDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function CreateProjectDialog({ open, onOpenChange }: CreateProjectDialogProps) {
|
||||
const [projectName, setProjectName] = useState("")
|
||||
const { createProjectMutation } = useProjectMutations()
|
||||
export function CreateProjectDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: CreateProjectDialogProps) {
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const { createProjectMutation } = useProjectMutations();
|
||||
|
||||
const handleClose = () => {
|
||||
onOpenChange(false)
|
||||
setProjectName("")
|
||||
}
|
||||
onOpenChange(false);
|
||||
setProjectName("");
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
if (projectName.trim()) {
|
||||
createProjectMutation.mutate(projectName, {
|
||||
onSuccess: () => {
|
||||
handleClose()
|
||||
}
|
||||
})
|
||||
handleClose();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
|
|
@ -72,7 +75,7 @@ export function CreateProjectDialog({ open, onOpenChange }: CreateProjectDialogP
|
|||
onChange={(e) => setProjectName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && projectName.trim()) {
|
||||
handleCreate()
|
||||
handleCreate();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -82,7 +85,10 @@ export function CreateProjectDialog({ open, onOpenChange }: CreateProjectDialogP
|
|||
</motion.div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
@ -92,11 +98,16 @@ export function CreateProjectDialog({ open, onOpenChange }: CreateProjectDialogP
|
|||
Cancel
|
||||
</Button>
|
||||
</motion.div>
|
||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-white/10 hover:bg-white/20 text-white border-white/20"
|
||||
disabled={createProjectMutation.isPending || !projectName.trim()}
|
||||
disabled={
|
||||
createProjectMutation.isPending || !projectName.trim()
|
||||
}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
{createProjectMutation.isPending ? (
|
||||
|
|
@ -115,5 +126,5 @@ export function CreateProjectDialog({ open, onOpenChange }: CreateProjectDialogP
|
|||
</Dialog>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { motion } from "motion/react"
|
||||
import { motion } from "motion/react";
|
||||
|
||||
interface GlassMenuEffectProps {
|
||||
rounded?: string
|
||||
className?: string
|
||||
rounded?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function GlassMenuEffect({
|
||||
|
|
@ -33,5 +33,5 @@ export function GlassMenuEffect({
|
|||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,118 +1,132 @@
|
|||
import { useEffect, useState } from "react"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { X, Download, Share } from "lucide-react"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { Download, Share, X } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function InstallPrompt() {
|
||||
const [isIOS, setIsIOS] = useState(false)
|
||||
const [showPrompt, setShowPrompt] = useState(false)
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const isIOSDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
|
||||
const isInStandaloneMode = window.matchMedia('(display-mode: standalone)').matches
|
||||
const hasSeenPrompt = localStorage.getItem('install-prompt-dismissed') === 'true'
|
||||
|
||||
setIsIOS(isIOSDevice)
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
setShowPrompt(!hasSeenPrompt && (isDevelopment || (!isInStandaloneMode && (isIOSDevice || 'serviceWorker' in navigator))))
|
||||
|
||||
const handleBeforeInstallPrompt = (e: Event) => {
|
||||
e.preventDefault()
|
||||
setDeferredPrompt(e)
|
||||
if (!hasSeenPrompt) {
|
||||
setShowPrompt(true)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt()
|
||||
const { outcome } = await deferredPrompt.userChoice
|
||||
if (outcome === 'accepted') {
|
||||
localStorage.setItem('install-prompt-dismissed', 'true')
|
||||
setShowPrompt(false)
|
||||
}
|
||||
setDeferredPrompt(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDismiss = () => {
|
||||
localStorage.setItem('install-prompt-dismissed', 'true')
|
||||
setShowPrompt(false)
|
||||
}
|
||||
|
||||
if (!showPrompt) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 100, opacity: 0 }}
|
||||
initial={{ y: 100, opacity: 0 }}
|
||||
className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-sm md:hidden"
|
||||
>
|
||||
<div className="bg-black/90 backdrop-blur-md text-white rounded-2xl p-4 shadow-2xl border border-white/10">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-[#0f1419] rounded-lg flex items-center justify-center">
|
||||
<Download className="w-4 h-4" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm">Install Supermemory</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleDismiss}
|
||||
className="text-white/60 hover:text-white h-6 w-6 p-0"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-white/80 text-xs mb-4 leading-relaxed">
|
||||
Add Supermemory to your home screen for quick access and a better experience.
|
||||
</p>
|
||||
|
||||
{isIOS ? (
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/70 text-xs flex items-center gap-1">
|
||||
1. Tap the <Share className="w-3 h-3 inline" /> Share button in Safari
|
||||
</p>
|
||||
<p className="text-white/70 text-xs">
|
||||
2. Select "Add to Home Screen" ➕
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleDismiss}
|
||||
className="w-full text-xs"
|
||||
>
|
||||
Got it
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
size="sm"
|
||||
className="w-full bg-[#0f1419] hover:bg-[#1a1f2a] text-white text-xs"
|
||||
>
|
||||
<Download className="w-3 h-3 mr-1" />
|
||||
Add to Home Screen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
const [isIOS, setIsIOS] = useState(false);
|
||||
const [showPrompt, setShowPrompt] = useState(false);
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const isIOSDevice =
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
|
||||
const isInStandaloneMode = window.matchMedia(
|
||||
"(display-mode: standalone)",
|
||||
).matches;
|
||||
const hasSeenPrompt =
|
||||
localStorage.getItem("install-prompt-dismissed") === "true";
|
||||
|
||||
setIsIOS(isIOSDevice);
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
setShowPrompt(
|
||||
!hasSeenPrompt &&
|
||||
(isDevelopment ||
|
||||
(!isInStandaloneMode &&
|
||||
(isIOSDevice || "serviceWorker" in navigator))),
|
||||
);
|
||||
|
||||
const handleBeforeInstallPrompt = (e: Event) => {
|
||||
e.preventDefault();
|
||||
setDeferredPrompt(e);
|
||||
if (!hasSeenPrompt) {
|
||||
setShowPrompt(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"beforeinstallprompt",
|
||||
handleBeforeInstallPrompt,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
if (outcome === "accepted") {
|
||||
localStorage.setItem("install-prompt-dismissed", "true");
|
||||
setShowPrompt(false);
|
||||
}
|
||||
setDeferredPrompt(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
localStorage.setItem("install-prompt-dismissed", "true");
|
||||
setShowPrompt(false);
|
||||
};
|
||||
|
||||
if (!showPrompt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 100, opacity: 0 }}
|
||||
initial={{ y: 100, opacity: 0 }}
|
||||
className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-sm md:hidden"
|
||||
>
|
||||
<div className="bg-black/90 backdrop-blur-md text-white rounded-2xl p-4 shadow-2xl border border-white/10">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-[#0f1419] rounded-lg flex items-center justify-center">
|
||||
<Download className="w-4 h-4" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm">Install Supermemory</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleDismiss}
|
||||
className="text-white/60 hover:text-white h-6 w-6 p-0"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-white/80 text-xs mb-4 leading-relaxed">
|
||||
Add Supermemory to your home screen for quick access and a better
|
||||
experience.
|
||||
</p>
|
||||
|
||||
{isIOS ? (
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/70 text-xs flex items-center gap-1">
|
||||
1. Tap the <Share className="w-3 h-3 inline" /> Share button in
|
||||
Safari
|
||||
</p>
|
||||
<p className="text-white/70 text-xs">
|
||||
2. Select "Add to Home Screen" ➕
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleDismiss}
|
||||
className="w-full text-xs"
|
||||
>
|
||||
Got it
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
size="sm"
|
||||
className="w-full bg-[#0f1419] hover:bg-[#1a1f2a] text-white text-xs"
|
||||
>
|
||||
<Download className="w-3 h-3 mr-1" />
|
||||
Add to Home Screen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import { cn } from "@lib/utils";
|
||||
import {
|
||||
GoogleDocs,
|
||||
GoogleDrive,
|
||||
|
|
@ -12,165 +14,178 @@ import {
|
|||
NotionDoc,
|
||||
OneDrive,
|
||||
PDF,
|
||||
} from "@repo/ui/assets/icons"
|
||||
import { Badge } from "@repo/ui/components/badge"
|
||||
import { Card, CardContent, CardHeader } from "@repo/ui/components/card"
|
||||
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@repo/ui/components/sheet"
|
||||
} from "@repo/ui/assets/icons";
|
||||
import { Badge } from "@repo/ui/components/badge";
|
||||
import { Card, CardContent, CardHeader } from "@repo/ui/components/card";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "@repo/ui/components/drawer"
|
||||
import { colors } from "@repo/ui/memory-graph/constants"
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
|
||||
import { Label1Regular } from "@ui/text/label/label-1-regular"
|
||||
import { Brain, Calendar, ExternalLink, FileText, Sparkles } from "lucide-react"
|
||||
import { useVirtualizer } from "@tanstack/react-virtual"
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import type { z } from "zod"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import useResizeObserver from "@/hooks/use-resize-observer"
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import { cn } from "@lib/utils"
|
||||
} from "@repo/ui/components/drawer";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@repo/ui/components/sheet";
|
||||
import { colors } from "@repo/ui/memory-graph/constants";
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { Label1Regular } from "@ui/text/label/label-1-regular";
|
||||
import {
|
||||
Brain,
|
||||
Calendar,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
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";
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
type MemoryEntry = DocumentWithMemories["memoryEntries"][0]
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0];
|
||||
type MemoryEntry = DocumentWithMemories["memoryEntries"][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">
|
||||
<div>
|
||||
<h1
|
||||
<h1
|
||||
className="text-lg md:text-xl font-semibold"
|
||||
style={{ color: colors.text.primary }}
|
||||
>
|
||||
{getGreeting()}!
|
||||
</h1>
|
||||
<p
|
||||
className="text-xs md:text-sm"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
<p className="text-xs md:text-sm" style={{ color: colors.text.muted }}>
|
||||
Welcome back to your memory collection
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const formatDate = (date: string | Date) => {
|
||||
const dateObj = new Date(date)
|
||||
const now = new Date()
|
||||
const currentYear = now.getFullYear()
|
||||
const dateYear = dateObj.getFullYear()
|
||||
|
||||
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
||||
const month = monthNames[dateObj.getMonth()]
|
||||
const day = dateObj.getDate()
|
||||
|
||||
const dateObj = new Date(date);
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
const dateYear = dateObj.getFullYear();
|
||||
|
||||
const monthNames = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
const month = monthNames[dateObj.getMonth()];
|
||||
const day = dateObj.getDate();
|
||||
|
||||
const getOrdinalSuffix = (n: number) => {
|
||||
const s = ["th", "st", "nd", "rd"]
|
||||
const v = n % 100
|
||||
return n + (s[(v - 20) % 10] || s[v] || s[0]!)
|
||||
}
|
||||
|
||||
const formattedDay = getOrdinalSuffix(day)
|
||||
|
||||
const s = ["th", "st", "nd", "rd"];
|
||||
const v = n % 100;
|
||||
return n + (s[(v - 20) % 10] || s[v] || s[0]!);
|
||||
};
|
||||
|
||||
const formattedDay = getOrdinalSuffix(day);
|
||||
|
||||
if (dateYear !== currentYear) {
|
||||
return `${month} ${formattedDay}, ${dateYear}`
|
||||
return `${month} ${formattedDay}, ${dateYear}`;
|
||||
}
|
||||
|
||||
return `${month} ${formattedDay}`
|
||||
}
|
||||
|
||||
return `${month} ${formattedDay}`;
|
||||
};
|
||||
|
||||
const formatDocumentType = (type: string) => {
|
||||
// Special case for PDF
|
||||
if (type.toLowerCase() === "pdf") return "PDF"
|
||||
if (type.toLowerCase() === "pdf") return "PDF";
|
||||
|
||||
// Replace underscores with spaces and capitalize each word
|
||||
return type
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ")
|
||||
}
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
const getDocumentIcon = (type: string, className: string) => {
|
||||
const iconProps = {
|
||||
className,
|
||||
style: { color: colors.text.muted },
|
||||
}
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case "google_doc":
|
||||
return <GoogleDocs {...iconProps} />
|
||||
return <GoogleDocs {...iconProps} />;
|
||||
case "google_sheet":
|
||||
return <GoogleSheets {...iconProps} />
|
||||
return <GoogleSheets {...iconProps} />;
|
||||
case "google_slide":
|
||||
return <GoogleSlides {...iconProps} />
|
||||
return <GoogleSlides {...iconProps} />;
|
||||
case "google_drive":
|
||||
return <GoogleDrive {...iconProps} />
|
||||
return <GoogleDrive {...iconProps} />;
|
||||
case "notion":
|
||||
case "notion_doc":
|
||||
return <NotionDoc {...iconProps} />
|
||||
return <NotionDoc {...iconProps} />;
|
||||
case "word":
|
||||
case "microsoft_word":
|
||||
return <MicrosoftWord {...iconProps} />
|
||||
return <MicrosoftWord {...iconProps} />;
|
||||
case "excel":
|
||||
case "microsoft_excel":
|
||||
return <MicrosoftExcel {...iconProps} />
|
||||
return <MicrosoftExcel {...iconProps} />;
|
||||
case "powerpoint":
|
||||
case "microsoft_powerpoint":
|
||||
return <MicrosoftPowerpoint {...iconProps} />
|
||||
return <MicrosoftPowerpoint {...iconProps} />;
|
||||
case "onenote":
|
||||
case "microsoft_onenote":
|
||||
return <MicrosoftOneNote {...iconProps} />
|
||||
return <MicrosoftOneNote {...iconProps} />;
|
||||
case "onedrive":
|
||||
return <OneDrive {...iconProps} />
|
||||
return <OneDrive {...iconProps} />;
|
||||
case "pdf":
|
||||
return <PDF {...iconProps} />
|
||||
return <PDF {...iconProps} />;
|
||||
default:
|
||||
return <FileText {...iconProps} />
|
||||
return <FileText {...iconProps} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getSourceUrl = (document: DocumentWithMemories) => {
|
||||
if (document.type === "google_doc" && document.customId) {
|
||||
return `https://docs.google.com/document/d/${document.customId}`
|
||||
return `https://docs.google.com/document/d/${document.customId}`;
|
||||
}
|
||||
if (document.type === "google_sheet" && document.customId) {
|
||||
return `https://docs.google.com/spreadsheets/d/${document.customId}`
|
||||
return `https://docs.google.com/spreadsheets/d/${document.customId}`;
|
||||
}
|
||||
if (document.type === "google_slide" && document.customId) {
|
||||
return `https://docs.google.com/presentation/d/${document.customId}`
|
||||
return `https://docs.google.com/presentation/d/${document.customId}`;
|
||||
}
|
||||
// Fallback to existing URL for all other document types
|
||||
return document.url
|
||||
}
|
||||
return document.url;
|
||||
};
|
||||
|
||||
const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => {
|
||||
return (
|
||||
|
|
@ -199,8 +214,9 @@ const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => {
|
|||
}}
|
||||
>
|
||||
<Brain
|
||||
className={`w-4 h-4 flex-shrink-0 transition-all ${memory.isLatest ? "text-blue-400" : "text-blue-400/50"
|
||||
}`}
|
||||
className={`w-4 h-4 flex-shrink-0 transition-all ${
|
||||
memory.isLatest ? "text-blue-400" : "text-blue-400/50"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
|
|
@ -282,29 +298,29 @@ const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => {
|
|||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const DocumentCard = memo(
|
||||
({
|
||||
document,
|
||||
onOpenDetails,
|
||||
}: {
|
||||
document: DocumentWithMemories
|
||||
onOpenDetails: (document: DocumentWithMemories) => void
|
||||
document: DocumentWithMemories;
|
||||
onOpenDetails: (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,
|
||||
}}
|
||||
|
|
@ -313,15 +329,22 @@ const DocumentCard = memo(
|
|||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{getDocumentIcon(document.type, "w-4 h-4 flex-shrink-0")}
|
||||
<p className={cn("text-sm font-medium line-clamp-1", document.url ? "max-w-[190px]" : "max-w-[200px]")}>{document.title || "Untitled Document"}</p>
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm font-medium line-clamp-1",
|
||||
document.url ? "max-w-[190px]" : "max-w-[200px]",
|
||||
)}
|
||||
>
|
||||
{document.title || "Untitled Document"}
|
||||
</p>
|
||||
</div>
|
||||
{document.url && (
|
||||
<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)",
|
||||
|
|
@ -375,9 +398,9 @@ const DocumentCard = memo(
|
|||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
const DocumentDetailSheet = memo(
|
||||
({
|
||||
|
|
@ -386,20 +409,24 @@ const DocumentDetailSheet = memo(
|
|||
onClose,
|
||||
isMobile,
|
||||
}: {
|
||||
document: DocumentWithMemories | null
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
isMobile: boolean
|
||||
document: DocumentWithMemories | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
isMobile: boolean;
|
||||
}) => {
|
||||
if (!document) return null
|
||||
if (!document) return null;
|
||||
|
||||
const [isSummaryExpanded, setIsSummaryExpanded] = useState(false)
|
||||
const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten)
|
||||
const [isSummaryExpanded, setIsSummaryExpanded] = useState(false);
|
||||
const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten);
|
||||
const forgottenMemories = document.memoryEntries.filter(
|
||||
(m) => m.isForgotten,
|
||||
)
|
||||
);
|
||||
|
||||
const HeaderContent = ({ TitleComponent }: { TitleComponent: typeof SheetTitle | typeof DrawerTitle }) => (
|
||||
const HeaderContent = ({
|
||||
TitleComponent,
|
||||
}: {
|
||||
TitleComponent: typeof SheetTitle | typeof DrawerTitle;
|
||||
}) => (
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<div
|
||||
|
|
@ -421,17 +448,15 @@ const DocumentDetailSheet = memo(
|
|||
>
|
||||
<span>{formatDocumentType(document.type)}</span>
|
||||
<span>•</span>
|
||||
<span>
|
||||
{formatDate(document.createdAt)}
|
||||
</span>
|
||||
<span>{formatDate(document.createdAt)}</span>
|
||||
{document.url && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<button
|
||||
className="flex items-center gap-1 transition-all hover:gap-2"
|
||||
onClick={() => {
|
||||
const sourceUrl = getSourceUrl(document)
|
||||
window.open(sourceUrl ?? undefined, "_blank")
|
||||
const sourceUrl = getSourceUrl(document);
|
||||
window.open(sourceUrl ?? undefined, "_blank");
|
||||
}}
|
||||
style={{ color: colors.accent.primary }}
|
||||
type="button"
|
||||
|
|
@ -445,12 +470,12 @@ const DocumentDetailSheet = memo(
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
const SummarySection = () => {
|
||||
if (!document.summary) return null
|
||||
if (!document.summary) return null;
|
||||
|
||||
const shouldShowToggle = document.summary.length > 200 // Show toggle for longer summaries
|
||||
const shouldShowToggle = document.summary.length > 200; // Show toggle for longer summaries
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -460,8 +485,8 @@ const DocumentDetailSheet = memo(
|
|||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className={`text-sm ${!isSummaryExpanded ? 'line-clamp-3' : ''}`}
|
||||
<p
|
||||
className={`text-sm ${!isSummaryExpanded ? "line-clamp-3" : ""}`}
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
{document.summary}
|
||||
|
|
@ -473,12 +498,12 @@ const DocumentDetailSheet = memo(
|
|||
style={{ color: colors.accent.primary }}
|
||||
type="button"
|
||||
>
|
||||
{isSummaryExpanded ? 'Show less' : 'Show more'}
|
||||
{isSummaryExpanded ? "Show less" : "Show more"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const MemoryContent = () => (
|
||||
<div className="p-6 space-y-6">
|
||||
|
|
@ -529,26 +554,25 @@ const DocumentDetailSheet = memo(
|
|||
</div>
|
||||
)}
|
||||
|
||||
{activeMemories.length === 0 &&
|
||||
forgottenMemories.length === 0 && (
|
||||
<div
|
||||
className="text-center py-12 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.02)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
}}
|
||||
>
|
||||
<Brain
|
||||
className="w-12 h-12 mx-auto mb-4 opacity-30"
|
||||
style={{ color: colors.text.muted }}
|
||||
/>
|
||||
<p style={{ color: colors.text.muted }}>
|
||||
No memories found for this document
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{activeMemories.length === 0 && forgottenMemories.length === 0 && (
|
||||
<div
|
||||
className="text-center py-12 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.02)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
}}
|
||||
>
|
||||
<Brain
|
||||
className="w-12 h-12 mx-auto mb-4 opacity-30"
|
||||
style={{ color: colors.text.muted }}
|
||||
/>
|
||||
<p style={{ color: colors.text.muted }}>
|
||||
No memories found for this document
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
|
|
@ -582,7 +606,7 @@ const DocumentDetailSheet = memo(
|
|||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -616,9 +640,9 @@ const DocumentDetailSheet = memo(
|
|||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
export const MemoryListView = ({
|
||||
children,
|
||||
|
|
@ -629,26 +653,29 @@ 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 gap = 14;
|
||||
|
||||
const gap = 14
|
||||
|
||||
const { width: containerWidth } = useResizeObserver(containerRef);
|
||||
const columnWidth = isMobile ? containerWidth : 320
|
||||
const columns = Math.max(1, Math.floor((containerWidth + gap) / (columnWidth + gap)))
|
||||
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
|
||||
|
|
@ -659,46 +686,52 @@ 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, isLoadingMore, loadMoreDocuments, virtualizer.getVirtualItems(), virtualItems.length])
|
||||
}, [
|
||||
hasMore,
|
||||
isLoadingMore,
|
||||
loadMoreDocuments,
|
||||
virtualizer.getVirtualItems(),
|
||||
virtualItems.length,
|
||||
]);
|
||||
|
||||
// Always render with consistent structure
|
||||
return (
|
||||
|
|
@ -738,19 +771,21 @@ export const MemoryListView = ({
|
|||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="h-full overflow-auto mt-20 custom-scrollbar"
|
||||
>
|
||||
<GreetingMessage />
|
||||
|
||||
|
||||
<div
|
||||
className="w-full relative"
|
||||
style={{ height: `${virtualizer.getTotalSize() + (virtualItems.length * gap)}px` }}
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize() + virtualItems.length * gap}px`,
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const rowItems = virtualItems[virtualRow.index]
|
||||
if (!rowItems) return null
|
||||
const rowItems = virtualItems[virtualRow.index];
|
||||
if (!rowItems) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -758,25 +793,30 @@ export const MemoryListView = ({
|
|||
data-index={virtualRow.index}
|
||||
ref={virtualizer.measureElement}
|
||||
className="absolute top-0 left-0 w-full"
|
||||
style={{ transform: `translateY(${virtualRow.start + (virtualRow.index * gap)}px)` }}
|
||||
style={{
|
||||
transform: `translateY(${virtualRow.start + virtualRow.index * gap}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="grid justify-start"
|
||||
style={{ gridTemplateColumns: `repeat(${columns}, ${columnWidth}px)`, gap: `${gap}px` }}
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${columns}, ${columnWidth}px)`,
|
||||
gap: `${gap}px`,
|
||||
}}
|
||||
>
|
||||
{rowItems.map((document) => (
|
||||
{rowItems.map((document, columnIndex) => (
|
||||
<DocumentCard
|
||||
key={document.id}
|
||||
key={`${document.id}-${virtualRow.index}-${columnIndex}`}
|
||||
document={document}
|
||||
onOpenDetails={handleOpenDetails}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
{isLoadingMore && (
|
||||
<div className="py-8 flex items-center justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -798,5 +838,5 @@ export const MemoryListView = ({
|
|||
isMobile={isMobile}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,90 +1,85 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import {
|
||||
fetchConsumerProProduct,
|
||||
fetchMemoriesFeature,
|
||||
} from "@repo/lib/queries"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold"
|
||||
import { GlassMenuEffect } from "@ui/other/glass-effect"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import {
|
||||
MessageSquareMore,
|
||||
Plus,
|
||||
User,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import { AnimatePresence, LayoutGroup, motion } from "motion/react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useMobilePanel } from "@/lib/mobile-panel-context"
|
||||
import { TOUR_STEP_IDS } from "@/lib/tour-constants"
|
||||
import { ProjectSelector } from "./project-selector"
|
||||
import { useTour } from "./tour"
|
||||
import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory"
|
||||
import { MCPView } from "./views/mcp"
|
||||
import { ProfileView } from "./views/profile"
|
||||
import { useChatOpen } from "@/stores"
|
||||
import { Drawer } from "vaul"
|
||||
} from "@repo/lib/queries";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold";
|
||||
import { GlassMenuEffect } from "@ui/other/glass-effect";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
import { MessageSquareMore, Plus, User, X } from "lucide-react";
|
||||
import { AnimatePresence, LayoutGroup, motion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Drawer } from "vaul";
|
||||
import { useMobilePanel } from "@/lib/mobile-panel-context";
|
||||
import { TOUR_STEP_IDS } from "@/lib/tour-constants";
|
||||
import { useChatOpen } from "@/stores";
|
||||
import { ProjectSelector } from "./project-selector";
|
||||
import { useTour } from "./tour";
|
||||
import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory";
|
||||
import { MCPView } from "./views/mcp";
|
||||
import { ProfileView } from "./views/profile";
|
||||
|
||||
const MCPIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
<svg
|
||||
className={className}
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>ModelContextProtocol</title>
|
||||
<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
|
||||
<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function Menu({ id }: { id?: string }) {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [expandedView, setExpandedView] = useState<
|
||||
"addUrl" | "mcp" | "projects" | "profile" | null
|
||||
>(null)
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const [isCollapsing, setIsCollapsing] = useState(false)
|
||||
const [showAddMemoryView, setShowAddMemoryView] = useState(false)
|
||||
const isMobile = useIsMobile()
|
||||
const { activePanel, setActivePanel } = useMobilePanel()
|
||||
const { setMenuExpanded } = useTour()
|
||||
const autumn = useCustomer()
|
||||
const { setIsOpen } = useChatOpen()
|
||||
>(null);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [isCollapsing, setIsCollapsing] = useState(false);
|
||||
const [showAddMemoryView, setShowAddMemoryView] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
const { activePanel, setActivePanel } = useMobilePanel();
|
||||
const { setMenuExpanded } = useTour();
|
||||
const autumn = useCustomer();
|
||||
const { setIsOpen } = useChatOpen();
|
||||
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any)
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any);
|
||||
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0;
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0;
|
||||
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn as any)
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn as any);
|
||||
|
||||
useEffect(() => {
|
||||
if (memoriesCheck) {
|
||||
console.log({ memoriesCheck })
|
||||
console.log({ memoriesCheck });
|
||||
}
|
||||
|
||||
if (proCheck) {
|
||||
console.log({ proCheck })
|
||||
console.log({ proCheck });
|
||||
}
|
||||
}, [memoriesCheck, proCheck])
|
||||
}, [memoriesCheck, proCheck]);
|
||||
|
||||
const isProUser = proCheck?.allowed ?? false
|
||||
const isProUser = proCheck?.allowed ?? false;
|
||||
|
||||
const shouldShowLimitWarning =
|
||||
!isProUser && memoriesUsed >= memoriesLimit * 0.8
|
||||
!isProUser && memoriesUsed >= memoriesLimit * 0.8;
|
||||
|
||||
// Map menu item keys to tour IDs
|
||||
const menuItemTourIds: Record<string, string> = {
|
||||
addUrl: TOUR_STEP_IDS.MENU_ADD_MEMORY,
|
||||
projects: TOUR_STEP_IDS.MENU_PROJECTS,
|
||||
mcp: TOUR_STEP_IDS.MENU_MCP,
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
|
|
@ -111,55 +106,56 @@ function Menu({ id }: { id?: string }) {
|
|||
key: "profile" as const,
|
||||
disabled: false,
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const handleMenuItemClick = (
|
||||
key: "chat" | "addUrl" | "mcp" | "projects" | "profile",
|
||||
) => {
|
||||
if (key === "chat") {
|
||||
setIsOpen(true)
|
||||
setIsMobileMenuOpen(false)
|
||||
setIsOpen(true);
|
||||
setIsMobileMenuOpen(false);
|
||||
if (isMobile) {
|
||||
setActivePanel("chat")
|
||||
setActivePanel("chat");
|
||||
}
|
||||
} else {
|
||||
if (expandedView === key) {
|
||||
setIsCollapsing(true)
|
||||
setExpandedView(null)
|
||||
setIsCollapsing(true);
|
||||
setExpandedView(null);
|
||||
} else if (key === "addUrl") {
|
||||
setShowAddMemoryView(true)
|
||||
setExpandedView(null)
|
||||
setShowAddMemoryView(true);
|
||||
setExpandedView(null);
|
||||
} else {
|
||||
setExpandedView(key)
|
||||
setExpandedView(key);
|
||||
}
|
||||
if (isMobile) {
|
||||
setActivePanel("menu")
|
||||
setActivePanel("menu");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for active panel changes on mobile
|
||||
useEffect(() => {
|
||||
if (isMobile && activePanel !== "menu" && activePanel !== null) {
|
||||
// Another panel became active, close the menu
|
||||
setIsMobileMenuOpen(false)
|
||||
setExpandedView(null)
|
||||
setIsMobileMenuOpen(false);
|
||||
setExpandedView(null);
|
||||
}
|
||||
}, [isMobile, activePanel])
|
||||
}, [isMobile, activePanel]);
|
||||
|
||||
// Notify tour provider about expansion state changes
|
||||
useEffect(() => {
|
||||
const isExpanded = isMobile
|
||||
? isMobileMenuOpen || !!expandedView
|
||||
: isHovered || !!expandedView
|
||||
setMenuExpanded(isExpanded)
|
||||
}, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded])
|
||||
: isHovered || !!expandedView;
|
||||
setMenuExpanded(isExpanded);
|
||||
}, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded]);
|
||||
|
||||
// Calculate width based on state
|
||||
const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56
|
||||
const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56;
|
||||
|
||||
// Dynamic z-index for mobile based on active panel
|
||||
const mobileZIndex = isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]"
|
||||
const mobileZIndex =
|
||||
isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -293,11 +289,11 @@ function Menu({ id }: { id?: string }) {
|
|||
{item.text}
|
||||
{/* Show warning indicator for Add Memory when limits approached */}
|
||||
{shouldShowLimitWarning &&
|
||||
item.key === "addUrl" && (
|
||||
<span className="text-xs bg-yellow-500/20 text-yellow-400 px-1.5 py-0.5 rounded">
|
||||
{memoriesLimit - memoriesUsed} left
|
||||
</span>
|
||||
)}
|
||||
item.key === "addUrl" && (
|
||||
<span className="text-xs bg-yellow-500/20 text-yellow-400 px-1.5 py-0.5 rounded">
|
||||
{memoriesLimit - memoriesUsed} left
|
||||
</span>
|
||||
)}
|
||||
</motion.p>
|
||||
</motion.button>
|
||||
{index === 0 && (
|
||||
|
|
@ -374,8 +370,8 @@ function Menu({ id }: { id?: string }) {
|
|||
<Button
|
||||
className="text-white/70 hover:text-white transition-colors duration-200"
|
||||
onClick={() => {
|
||||
setIsCollapsing(true)
|
||||
setExpandedView(null)
|
||||
setIsCollapsing(true);
|
||||
setExpandedView(null);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
|
@ -408,13 +404,13 @@ function Menu({ id }: { id?: string }) {
|
|||
|
||||
{/* Mobile Menu with Vaul Drawer */}
|
||||
{isMobile && (
|
||||
<Drawer.Root
|
||||
<Drawer.Root
|
||||
open={isMobileMenuOpen || !!expandedView}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setIsMobileMenuOpen(false)
|
||||
setExpandedView(null)
|
||||
setActivePanel(null)
|
||||
setIsMobileMenuOpen(false);
|
||||
setExpandedView(null);
|
||||
setActivePanel(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -427,8 +423,8 @@ function Menu({ id }: { id?: string }) {
|
|||
className="w-14 h-14 flex items-center justify-center text-white rounded-full shadow-2xl"
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
onClick={() => {
|
||||
setIsMobileMenuOpen(true)
|
||||
setActivePanel("menu")
|
||||
setIsMobileMenuOpen(true);
|
||||
setActivePanel("menu");
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
|
|
@ -491,113 +487,120 @@ function Menu({ id }: { id?: string }) {
|
|||
key="menu-items-mobile"
|
||||
layout
|
||||
>
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
transition={{ delay: 0.08 }}
|
||||
>
|
||||
<ProjectSelector />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
transition={{ delay: 0.08 }}
|
||||
>
|
||||
<ProjectSelector />
|
||||
</motion.div>
|
||||
|
||||
{/* Menu Items */}
|
||||
<div className="flex flex-col gap-3">
|
||||
{menuItems.map((item, index) => (
|
||||
<div key={item.key}>
|
||||
<motion.button
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 0.1 + index * 0.05,
|
||||
duration: 0.3,
|
||||
ease: "easeOut",
|
||||
},
|
||||
}}
|
||||
className="flex w-full items-center gap-3 px-2 py-2 text-white/90 hover:text-white hover:bg-white/10 rounded-lg cursor-pointer relative"
|
||||
id={menuItemTourIds[item.key]}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
layout
|
||||
onClick={() => {
|
||||
handleMenuItemClick(item.key)
|
||||
if (item.key !== "mcp" && item.key !== "profile") {
|
||||
setIsMobileMenuOpen(false)
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<item.icon className="h-5 w-5 drop-shadow-lg flex-shrink-0" />
|
||||
<span className="drop-shadow-lg text-sm font-medium flex-1 text-left">
|
||||
{item.text}
|
||||
</span>
|
||||
{/* Show warning indicator for Add Memory when limits approached */}
|
||||
{shouldShowLimitWarning &&
|
||||
item.key === "addUrl" && (
|
||||
<span className="text-xs bg-yellow-500/20 text-yellow-400 px-1.5 py-0.5 rounded">
|
||||
{memoriesLimit - memoriesUsed} left
|
||||
</span>
|
||||
)}
|
||||
</motion.button>
|
||||
{/* Add horizontal line after first item */}
|
||||
{index === 0 && (
|
||||
<motion.div
|
||||
{/* Menu Items */}
|
||||
<div className="flex flex-col gap-3">
|
||||
{menuItems.map((item, index) => (
|
||||
<div key={item.key}>
|
||||
<motion.button
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scaleX: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 0.1 + index * 0.05,
|
||||
duration: 0.3,
|
||||
ease: "easeOut",
|
||||
},
|
||||
}}
|
||||
className="w-full h-px bg-white/20 mt-2 origin-left"
|
||||
initial={{ opacity: 0, scaleX: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay: 0.15 + index * 0.05,
|
||||
ease: [0.4, 0, 0.2, 1],
|
||||
className="flex w-full items-center gap-3 px-2 py-2 text-white/90 hover:text-white hover:bg-white/10 rounded-lg cursor-pointer relative"
|
||||
id={menuItemTourIds[item.key]}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
layout
|
||||
onClick={() => {
|
||||
handleMenuItemClick(item.key);
|
||||
if (
|
||||
item.key !== "mcp" &&
|
||||
item.key !== "profile"
|
||||
) {
|
||||
setIsMobileMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
animate={{ opacity: 1 }}
|
||||
className="w-full p-2 flex flex-col"
|
||||
exit={{ opacity: 0 }}
|
||||
initial={{ opacity: 0 }}
|
||||
key="expanded-view-mobile"
|
||||
layout
|
||||
>
|
||||
<div className="flex-1">
|
||||
<motion.div className="mb-4 flex items-center justify-between" layout>
|
||||
<HeadingH2Bold className="text-white">
|
||||
{expandedView === "addUrl" && "Add Memory"}
|
||||
{expandedView === "mcp" && "Model Context Protocol"}
|
||||
{expandedView === "profile" && "Profile"}
|
||||
</HeadingH2Bold>
|
||||
<Button
|
||||
className="text-white/70 hover:text-white transition-colors duration-200"
|
||||
onClick={() => {
|
||||
setIsCollapsing(true)
|
||||
setExpandedView(null)
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
<div className="max-h-[60vh] overflow-y-auto pr-1">
|
||||
{expandedView === "addUrl" && (
|
||||
<AddMemoryExpandedView />
|
||||
)}
|
||||
{expandedView === "mcp" && <MCPView />}
|
||||
{expandedView === "profile" && <ProfileView />}
|
||||
type="button"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<item.icon className="h-5 w-5 drop-shadow-lg flex-shrink-0" />
|
||||
<span className="drop-shadow-lg text-sm font-medium flex-1 text-left">
|
||||
{item.text}
|
||||
</span>
|
||||
{/* Show warning indicator for Add Memory when limits approached */}
|
||||
{shouldShowLimitWarning &&
|
||||
item.key === "addUrl" && (
|
||||
<span className="text-xs bg-yellow-500/20 text-yellow-400 px-1.5 py-0.5 rounded">
|
||||
{memoriesLimit - memoriesUsed} left
|
||||
</span>
|
||||
)}
|
||||
</motion.button>
|
||||
{/* Add horizontal line after first item */}
|
||||
{index === 0 && (
|
||||
<motion.div
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scaleX: 1,
|
||||
}}
|
||||
className="w-full h-px bg-white/20 mt-2 origin-left"
|
||||
initial={{ opacity: 0, scaleX: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay: 0.15 + index * 0.05,
|
||||
ease: [0.4, 0, 0.2, 1],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
animate={{ opacity: 1 }}
|
||||
className="w-full p-2 flex flex-col"
|
||||
exit={{ opacity: 0 }}
|
||||
initial={{ opacity: 0 }}
|
||||
key="expanded-view-mobile"
|
||||
layout
|
||||
>
|
||||
<div className="flex-1">
|
||||
<motion.div
|
||||
className="mb-4 flex items-center justify-between"
|
||||
layout
|
||||
>
|
||||
<HeadingH2Bold className="text-white">
|
||||
{expandedView === "addUrl" && "Add Memory"}
|
||||
{expandedView === "mcp" &&
|
||||
"Model Context Protocol"}
|
||||
{expandedView === "profile" && "Profile"}
|
||||
</HeadingH2Bold>
|
||||
<Button
|
||||
className="text-white/70 hover:text-white transition-colors duration-200"
|
||||
onClick={() => {
|
||||
setIsCollapsing(true);
|
||||
setExpandedView(null);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
<div className="max-h-[60vh] overflow-y-auto pr-1">
|
||||
{expandedView === "addUrl" && (
|
||||
<AddMemoryExpandedView />
|
||||
)}
|
||||
{expandedView === "mcp" && <MCPView />}
|
||||
{expandedView === "profile" && <ProfileView />}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer.Content>
|
||||
|
|
@ -612,7 +615,7 @@ function Menu({ id }: { id?: string }) {
|
|||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Menu
|
||||
export default Menu;
|
||||
|
|
|
|||
|
|
@ -1,21 +1,8 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { $fetch } from "@repo/lib/api"
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { ChevronDown, FolderIcon, Plus, Trash2, Loader2, MoreVertical, MoreHorizontal } from "lucide-react"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { CreateProjectDialog } from "./create-project-dialog"
|
||||
import { useProject } from "@/stores"
|
||||
import { useProjectName } from "@/hooks/use-project-name"
|
||||
import { useProjectMutations } from "@/hooks/use-project-mutations"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/ui/components/dropdown-menu"
|
||||
import { $fetch } from "@repo/lib/api";
|
||||
import { DEFAULT_PROJECT_ID } from "@repo/lib/constants";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -23,96 +10,121 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@repo/ui/components/dialog"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { Label } from "@repo/ui/components/label"
|
||||
} from "@repo/ui/components/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/ui/components/dropdown-menu";
|
||||
import { Label } from "@repo/ui/components/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@repo/ui/components/select"
|
||||
import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
|
||||
} from "@repo/ui/components/select";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ChevronDown,
|
||||
FolderIcon,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
MoreVertical,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useProjectMutations } from "@/hooks/use-project-mutations";
|
||||
import { useProjectName } from "@/hooks/use-project-name";
|
||||
import { useProject } from "@/stores";
|
||||
import { CreateProjectDialog } from "./create-project-dialog";
|
||||
|
||||
interface Project {
|
||||
id: string
|
||||
name: string
|
||||
containerTag: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
isExperimental?: boolean
|
||||
id: string;
|
||||
name: string;
|
||||
containerTag: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isExperimental?: boolean;
|
||||
}
|
||||
|
||||
export function ProjectSelector() {
|
||||
const queryClient = useQueryClient()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const { selectedProject } = useProject()
|
||||
const projectName = useProjectName()
|
||||
const { switchProject, deleteProjectMutation } = useProjectMutations()
|
||||
const queryClient = useQueryClient();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const { selectedProject } = useProject();
|
||||
const projectName = useProjectName();
|
||||
const { switchProject, deleteProjectMutation } = useProjectMutations();
|
||||
const [deleteDialog, setDeleteDialog] = useState<{
|
||||
open: boolean
|
||||
project: null | { id: string; name: string; containerTag: string }
|
||||
action: "move" | "delete"
|
||||
targetProjectId: string
|
||||
open: boolean;
|
||||
project: null | { id: string; name: string; containerTag: string };
|
||||
action: "move" | "delete";
|
||||
targetProjectId: string;
|
||||
}>({
|
||||
open: false,
|
||||
project: null,
|
||||
action: "move",
|
||||
targetProjectId: DEFAULT_PROJECT_ID,
|
||||
})
|
||||
});
|
||||
const [expDialog, setExpDialog] = useState<{
|
||||
open: boolean
|
||||
projectId: string
|
||||
open: boolean;
|
||||
projectId: string;
|
||||
}>({
|
||||
open: false,
|
||||
projectId: "",
|
||||
})
|
||||
});
|
||||
|
||||
const { data: projects = [], isLoading } = 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,
|
||||
})
|
||||
});
|
||||
|
||||
const enableExperimentalMutation = useMutation({
|
||||
mutationFn: async (projectId: string) => {
|
||||
const response = await $fetch(`@post/projects/${projectId}/enable-experimental`)
|
||||
const response = await $fetch(
|
||||
`@post/projects/${projectId}/enable-experimental`,
|
||||
);
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to enable experimental mode")
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to enable experimental mode",
|
||||
);
|
||||
}
|
||||
return response.data
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Experimental mode enabled for project")
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] })
|
||||
setExpDialog({ open: false, projectId: "" })
|
||||
toast.success("Experimental mode enabled for project");
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
setExpDialog({ open: false, projectId: "" });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to enable experimental mode", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const handleProjectSelect = (containerTag: string) => {
|
||||
switchProject(containerTag)
|
||||
setIsOpen(false)
|
||||
}
|
||||
switchProject(containerTag);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleCreateNewProject = () => {
|
||||
setIsOpen(false)
|
||||
setShowCreateDialog(true)
|
||||
}
|
||||
setIsOpen(false);
|
||||
setShowCreateDialog(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
|
|
@ -164,7 +176,9 @@ export function ProjectSelector() {
|
|||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderIcon className="h-3.5 w-3.5 text-white/70" />
|
||||
<span className="text-xs font-medium text-white">Default</span>
|
||||
<span className="text-xs font-medium text-white">
|
||||
Default
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
|
|
@ -183,9 +197,11 @@ export function ProjectSelector() {
|
|||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.03 }}
|
||||
>
|
||||
<div
|
||||
<div
|
||||
className="flex items-center gap-2 flex-1 cursor-pointer"
|
||||
onClick={() => handleProjectSelect(project.containerTag)}
|
||||
onClick={() =>
|
||||
handleProjectSelect(project.containerTag)
|
||||
}
|
||||
>
|
||||
<FolderIcon className="h-3.5 w-3.5 text-white/70" />
|
||||
<span className="text-xs font-medium text-white truncate max-w-32">
|
||||
|
|
@ -214,12 +230,12 @@ export function ProjectSelector() {
|
|||
<DropdownMenuItem
|
||||
className="text-blue-400 hover:text-blue-300 cursor-pointer text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.stopPropagation();
|
||||
setExpDialog({
|
||||
open: true,
|
||||
projectId: project.id,
|
||||
})
|
||||
setIsOpen(false)
|
||||
});
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="h-3 w-3 mr-2 rounded border border-blue-400" />
|
||||
|
|
@ -238,7 +254,7 @@ export function ProjectSelector() {
|
|||
<DropdownMenuItem
|
||||
className="text-red-400 hover:text-red-300 cursor-pointer text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.stopPropagation();
|
||||
setDeleteDialog({
|
||||
open: true,
|
||||
project: {
|
||||
|
|
@ -248,8 +264,8 @@ export function ProjectSelector() {
|
|||
},
|
||||
action: "move",
|
||||
targetProjectId: "",
|
||||
})
|
||||
setIsOpen(false)
|
||||
});
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-2" />
|
||||
|
|
@ -270,7 +286,9 @@ export function ProjectSelector() {
|
|||
transition={{ delay: (projects.length + 1) * 0.03 }}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 text-white/70" />
|
||||
<span className="text-xs font-medium text-white/80">New Project</span>
|
||||
<span className="text-xs font-medium text-white/80">
|
||||
New Project
|
||||
</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
@ -278,9 +296,9 @@ export function ProjectSelector() {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<CreateProjectDialog
|
||||
open={showCreateDialog}
|
||||
onOpenChange={setShowCreateDialog}
|
||||
<CreateProjectDialog
|
||||
open={showCreateDialog}
|
||||
onOpenChange={setShowCreateDialog}
|
||||
/>
|
||||
|
||||
{/* Delete Project Dialog */}
|
||||
|
|
@ -301,8 +319,8 @@ export function ProjectSelector() {
|
|||
<DialogHeader>
|
||||
<DialogTitle>Delete Project</DialogTitle>
|
||||
<DialogDescription className="text-white/60">
|
||||
Are you sure you want to delete "{deleteDialog.project.name}"?
|
||||
Choose what to do with the documents in this project.
|
||||
Are you sure you want to delete "{deleteDialog.project.name}
|
||||
"? Choose what to do with the documents in this project.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
|
|
@ -432,10 +450,11 @@ export function ProjectSelector() {
|
|||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
className={`${deleteDialog.action === "delete"
|
||||
className={`${
|
||||
deleteDialog.action === "delete"
|
||||
? "bg-red-600 hover:bg-red-700"
|
||||
: "bg-white/10 hover:bg-white/20"
|
||||
} text-white border-white/20`}
|
||||
} text-white border-white/20`}
|
||||
disabled={
|
||||
deleteProjectMutation.isPending ||
|
||||
(deleteDialog.action === "move" &&
|
||||
|
|
@ -443,23 +462,26 @@ export function ProjectSelector() {
|
|||
}
|
||||
onClick={() => {
|
||||
if (deleteDialog.project) {
|
||||
deleteProjectMutation.mutate({
|
||||
projectId: deleteDialog.project.id,
|
||||
action: deleteDialog.action,
|
||||
targetProjectId:
|
||||
deleteDialog.action === "move"
|
||||
? deleteDialog.targetProjectId
|
||||
: undefined,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setDeleteDialog({
|
||||
open: false,
|
||||
project: null,
|
||||
action: "move",
|
||||
targetProjectId: "",
|
||||
})
|
||||
}
|
||||
})
|
||||
deleteProjectMutation.mutate(
|
||||
{
|
||||
projectId: deleteDialog.project.id,
|
||||
action: deleteDialog.action,
|
||||
targetProjectId:
|
||||
deleteDialog.action === "move"
|
||||
? deleteDialog.targetProjectId
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setDeleteDialog({
|
||||
open: false,
|
||||
project: null,
|
||||
action: "move",
|
||||
targetProjectId: "",
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
|
|
@ -562,5 +584,5 @@ export function ProjectSelector() {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import {
|
||||
fetchMemoriesFeature,
|
||||
fetchSubscriptionStatus,
|
||||
} from "@lib/queries"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
import { fetchMemoriesFeature, fetchSubscriptionStatus } from "@lib/queries";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@repo/ui/components/dialog"
|
||||
import { Input } from "@repo/ui/components/input"
|
||||
import { Label } from "@repo/ui/components/label"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@repo/ui/components/tabs"
|
||||
import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
DialogTitle,
|
||||
} from "@repo/ui/components/dialog";
|
||||
import { Input } from "@repo/ui/components/input";
|
||||
import { Label } from "@repo/ui/components/label";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@repo/ui/components/tabs";
|
||||
import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
import {
|
||||
CheckCircle,
|
||||
Copy,
|
||||
|
|
@ -25,26 +27,29 @@ import {
|
|||
Gift,
|
||||
LoaderIcon,
|
||||
Share2,
|
||||
Users
|
||||
} from "lucide-react"
|
||||
import { motion } from "motion/react"
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
interface ReferralUpgradeModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalProps) {
|
||||
const { user } = useAuth()
|
||||
const autumn = useCustomer()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
export function ReferralUpgradeModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
}: ReferralUpgradeModalProps) {
|
||||
const { user } = useAuth();
|
||||
const autumn = useCustomer();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any)
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any);
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0;
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0;
|
||||
|
||||
// Fetch subscription status
|
||||
const {
|
||||
|
|
@ -52,53 +57,53 @@ export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalPr
|
|||
consumer_pro: null,
|
||||
},
|
||||
isLoading: isCheckingStatus,
|
||||
} = fetchSubscriptionStatus(autumn as any)
|
||||
} = fetchSubscriptionStatus(autumn as any);
|
||||
|
||||
const isPro = status.consumer_pro
|
||||
const isPro = status.consumer_pro;
|
||||
|
||||
// Handle upgrade
|
||||
const handleUpgrade = async () => {
|
||||
setIsLoading(true)
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
})
|
||||
window.location.reload()
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
setIsLoading(false)
|
||||
console.error(error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Generate referral link (you'll need to implement this based on your referral system)
|
||||
const referralLink = `https://app.supermemory.ai/ref/${user?.id || 'user'}`
|
||||
const referralLink = `https://app.supermemory.ai/ref/${user?.id || "user"}`;
|
||||
|
||||
const handleCopyReferralLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(referralLink)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
await navigator.clipboard.writeText(referralLink);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy:', error)
|
||||
console.error("Failed to copy:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: 'Join Supermemory',
|
||||
text: 'Check out Supermemory - the best way to organize and search your digital memories!',
|
||||
title: "Join Supermemory",
|
||||
text: "Check out Supermemory - the best way to organize and search your digital memories!",
|
||||
url: referralLink,
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sharing:', error)
|
||||
console.error("Error sharing:", error);
|
||||
}
|
||||
} else {
|
||||
handleCopyReferralLink()
|
||||
handleCopyReferralLink();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (user?.isAnonymous) {
|
||||
return (
|
||||
|
|
@ -127,7 +132,7 @@ export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalPr
|
|||
</motion.div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -149,14 +154,20 @@ export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalPr
|
|||
<div className="bg-white/5 rounded-lg p-4 mb-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm text-white/70">Current Usage</span>
|
||||
<span className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-400" : "text-white/90"}`}>
|
||||
<span
|
||||
className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-400" : "text-white/90"}`}
|
||||
>
|
||||
{memoriesUsed} / {memoriesLimit} memories
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-white/10 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
memoriesUsed >= memoriesLimit ? "bg-red-500" : isPro ? "bg-green-500" : "bg-blue-500"
|
||||
memoriesUsed >= memoriesLimit
|
||||
? "bg-red-500"
|
||||
: isPro
|
||||
? "bg-green-500"
|
||||
: "bg-blue-500"
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
|
||||
|
|
@ -173,7 +184,10 @@ export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalPr
|
|||
Refer Friends
|
||||
</TabsTrigger>
|
||||
{!isPro && (
|
||||
<TabsTrigger value="upgrade" className="flex items-center gap-2">
|
||||
<TabsTrigger
|
||||
value="upgrade"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
Upgrade Plan
|
||||
</TabsTrigger>
|
||||
|
|
@ -286,5 +300,5 @@ export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalPr
|
|||
</motion.div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,5 @@ import { cn } from "@lib/utils";
|
|||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export function Spinner({ className }: { className?: string }) {
|
||||
return (
|
||||
<Loader2 className={cn("size-4 animate-spin", className)} />
|
||||
)
|
||||
}
|
||||
return <Loader2 className={cn("size-4 animate-spin", className)} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,57 +1,57 @@
|
|||
'use client';
|
||||
import React, { useMemo, type JSX } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { cn } from '@lib/utils';
|
||||
"use client";
|
||||
import { cn } from "@lib/utils";
|
||||
import { motion } from "motion/react";
|
||||
import React, { type JSX, useMemo } from "react";
|
||||
|
||||
export type TextShimmerProps = {
|
||||
children: string;
|
||||
as?: React.ElementType;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
spread?: number;
|
||||
children: string;
|
||||
as?: React.ElementType;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
spread?: number;
|
||||
};
|
||||
|
||||
function TextShimmerComponent({
|
||||
children,
|
||||
as: Component = 'p',
|
||||
className,
|
||||
duration = 2,
|
||||
spread = 2,
|
||||
children,
|
||||
as: Component = "p",
|
||||
className,
|
||||
duration = 2,
|
||||
spread = 2,
|
||||
}: TextShimmerProps) {
|
||||
const MotionComponent = motion.create(
|
||||
Component as keyof JSX.IntrinsicElements
|
||||
);
|
||||
const MotionComponent = motion.create(
|
||||
Component as keyof JSX.IntrinsicElements,
|
||||
);
|
||||
|
||||
const dynamicSpread = useMemo(() => {
|
||||
return children.length * spread;
|
||||
}, [children, spread]);
|
||||
const dynamicSpread = useMemo(() => {
|
||||
return children.length * spread;
|
||||
}, [children, spread]);
|
||||
|
||||
return (
|
||||
<MotionComponent
|
||||
className={cn(
|
||||
'relative inline-block bg-[length:250%_100%,auto] bg-clip-text',
|
||||
'text-transparent [--base-color:#a1a1aa] [--base-gradient-color:#000]',
|
||||
'[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]',
|
||||
'dark:[--base-color:#71717a] dark:[--base-gradient-color:#ffffff] dark:[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]',
|
||||
className
|
||||
)}
|
||||
initial={{ backgroundPosition: '100% center' }}
|
||||
animate={{ backgroundPosition: '0% center' }}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
duration,
|
||||
ease: 'linear',
|
||||
}}
|
||||
style={
|
||||
{
|
||||
'--spread': `${dynamicSpread}px`,
|
||||
backgroundImage: `var(--bg), linear-gradient(var(--base-color), var(--base-color))`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</MotionComponent>
|
||||
);
|
||||
return (
|
||||
<MotionComponent
|
||||
className={cn(
|
||||
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text",
|
||||
"text-transparent [--base-color:#a1a1aa] [--base-gradient-color:#000]",
|
||||
"[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]",
|
||||
"dark:[--base-color:#71717a] dark:[--base-gradient-color:#ffffff] dark:[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]",
|
||||
className,
|
||||
)}
|
||||
initial={{ backgroundPosition: "100% center" }}
|
||||
animate={{ backgroundPosition: "0% center" }}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
duration,
|
||||
ease: "linear",
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--spread": `${dynamicSpread}px`,
|
||||
backgroundImage: `var(--bg), linear-gradient(var(--base-color), var(--base-color))`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</MotionComponent>
|
||||
);
|
||||
}
|
||||
|
||||
export const TextShimmer = React.memo(TextShimmerComponent);
|
||||
|
|
|
|||
|
|
@ -1,54 +1,54 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { GlassMenuEffect } from "@repo/ui/other/glass-effect"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import * as React from "react"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { GlassMenuEffect } from "@repo/ui/other/glass-effect";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import * as React from "react";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
|
||||
// Types
|
||||
export interface TourStep {
|
||||
content: React.ReactNode
|
||||
selectorId: string
|
||||
position?: "top" | "bottom" | "left" | "right" | "center"
|
||||
onClickWithinArea?: () => void
|
||||
content: React.ReactNode;
|
||||
selectorId: string;
|
||||
position?: "top" | "bottom" | "left" | "right" | "center";
|
||||
onClickWithinArea?: () => void;
|
||||
}
|
||||
|
||||
interface TourContextType {
|
||||
currentStep: number
|
||||
totalSteps: number
|
||||
nextStep: () => void
|
||||
previousStep: () => void
|
||||
endTour: () => void
|
||||
isActive: boolean
|
||||
isPaused: boolean
|
||||
startTour: () => void
|
||||
setSteps: (steps: TourStep[]) => void
|
||||
steps: TourStep[]
|
||||
isTourCompleted: boolean
|
||||
setIsTourCompleted: (completed: boolean) => void
|
||||
currentStep: number;
|
||||
totalSteps: number;
|
||||
nextStep: () => void;
|
||||
previousStep: () => void;
|
||||
endTour: () => void;
|
||||
isActive: boolean;
|
||||
isPaused: boolean;
|
||||
startTour: () => void;
|
||||
setSteps: (steps: TourStep[]) => void;
|
||||
steps: TourStep[];
|
||||
isTourCompleted: boolean;
|
||||
setIsTourCompleted: (completed: boolean) => void;
|
||||
// Expansion state tracking
|
||||
setMenuExpanded: (expanded: boolean) => void
|
||||
setChatExpanded: (expanded: boolean) => void
|
||||
setMenuExpanded: (expanded: boolean) => void;
|
||||
setChatExpanded: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
// Context
|
||||
const TourContext = React.createContext<TourContextType | undefined>(undefined)
|
||||
const TourContext = React.createContext<TourContextType | undefined>(undefined);
|
||||
|
||||
export function useTour() {
|
||||
const context = React.useContext(TourContext)
|
||||
const context = React.useContext(TourContext);
|
||||
if (!context) {
|
||||
throw new Error("useTour must be used within a TourProvider")
|
||||
throw new Error("useTour must be used within a TourProvider");
|
||||
}
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
// Provider
|
||||
interface TourProviderProps {
|
||||
children: React.ReactNode
|
||||
onComplete?: () => void
|
||||
className?: string
|
||||
isTourCompleted?: boolean
|
||||
children: React.ReactNode;
|
||||
onComplete?: () => void;
|
||||
className?: string;
|
||||
isTourCompleted?: boolean;
|
||||
}
|
||||
|
||||
export function TourProvider({
|
||||
|
|
@ -57,60 +57,61 @@ export function TourProvider({
|
|||
className,
|
||||
isTourCompleted: initialCompleted = false,
|
||||
}: TourProviderProps) {
|
||||
const [currentStep, setCurrentStep] = React.useState(-1)
|
||||
const [steps, setSteps] = React.useState<TourStep[]>([])
|
||||
const [isActive, setIsActive] = React.useState(false)
|
||||
const [isTourCompleted, setIsTourCompleted] = React.useState(initialCompleted)
|
||||
const [currentStep, setCurrentStep] = React.useState(-1);
|
||||
const [steps, setSteps] = React.useState<TourStep[]>([]);
|
||||
const [isActive, setIsActive] = React.useState(false);
|
||||
const [isTourCompleted, setIsTourCompleted] =
|
||||
React.useState(initialCompleted);
|
||||
|
||||
// Track expansion states
|
||||
const [isMenuExpanded, setIsMenuExpanded] = React.useState(false)
|
||||
const [isChatExpanded, setIsChatExpanded] = React.useState(false)
|
||||
const [isMenuExpanded, setIsMenuExpanded] = React.useState(false);
|
||||
const [isChatExpanded, setIsChatExpanded] = React.useState(false);
|
||||
|
||||
// Calculate if tour should be paused
|
||||
const isPaused = React.useMemo(() => {
|
||||
return isActive && (isMenuExpanded || isChatExpanded)
|
||||
}, [isActive, isMenuExpanded, isChatExpanded])
|
||||
return isActive && (isMenuExpanded || isChatExpanded);
|
||||
}, [isActive, isMenuExpanded, isChatExpanded]);
|
||||
|
||||
const startTour = React.useCallback(() => {
|
||||
console.debug("Starting tour with", steps.length, "steps")
|
||||
analytics.tourStarted()
|
||||
setCurrentStep(0)
|
||||
setIsActive(true)
|
||||
}, [steps])
|
||||
console.debug("Starting tour with", steps.length, "steps");
|
||||
analytics.tourStarted();
|
||||
setCurrentStep(0);
|
||||
setIsActive(true);
|
||||
}, [steps]);
|
||||
|
||||
const endTour = React.useCallback(() => {
|
||||
setCurrentStep(-1)
|
||||
setIsActive(false)
|
||||
setIsTourCompleted(true) // Mark tour as completed when ended/skipped
|
||||
analytics.tourSkipped()
|
||||
setCurrentStep(-1);
|
||||
setIsActive(false);
|
||||
setIsTourCompleted(true); // Mark tour as completed when ended/skipped
|
||||
analytics.tourSkipped();
|
||||
if (onComplete) {
|
||||
onComplete()
|
||||
onComplete();
|
||||
}
|
||||
}, [onComplete])
|
||||
}, [onComplete]);
|
||||
|
||||
const nextStep = React.useCallback(() => {
|
||||
if (currentStep < steps.length - 1) {
|
||||
setCurrentStep(currentStep + 1)
|
||||
setCurrentStep(currentStep + 1);
|
||||
} else {
|
||||
analytics.tourCompleted()
|
||||
endTour()
|
||||
setIsTourCompleted(true)
|
||||
analytics.tourCompleted();
|
||||
endTour();
|
||||
setIsTourCompleted(true);
|
||||
}
|
||||
}, [currentStep, steps.length, endTour])
|
||||
}, [currentStep, steps.length, endTour]);
|
||||
|
||||
const previousStep = React.useCallback(() => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1)
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
}, [currentStep])
|
||||
}, [currentStep]);
|
||||
|
||||
const setMenuExpanded = React.useCallback((expanded: boolean) => {
|
||||
setIsMenuExpanded(expanded)
|
||||
}, [])
|
||||
setIsMenuExpanded(expanded);
|
||||
}, []);
|
||||
|
||||
const setChatExpanded = React.useCallback((expanded: boolean) => {
|
||||
setIsChatExpanded(expanded)
|
||||
}, [])
|
||||
setIsChatExpanded(expanded);
|
||||
}, []);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
|
|
@ -142,7 +143,7 @@ export function TourProvider({
|
|||
setMenuExpanded,
|
||||
setChatExpanded,
|
||||
],
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<TourContext.Provider value={value}>
|
||||
|
|
@ -164,7 +165,7 @@ export function TourProvider({
|
|||
</>
|
||||
)}
|
||||
</TourContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Tour Highlight Component
|
||||
|
|
@ -173,33 +174,33 @@ function TourHighlight({
|
|||
steps,
|
||||
className,
|
||||
}: {
|
||||
currentStepIndex: number
|
||||
steps: TourStep[]
|
||||
className?: string
|
||||
currentStepIndex: number;
|
||||
steps: TourStep[];
|
||||
className?: string;
|
||||
}) {
|
||||
const { nextStep, previousStep, endTour } = useTour()
|
||||
const [elementRect, setElementRect] = React.useState<DOMRect | null>(null)
|
||||
const { nextStep, previousStep, endTour } = useTour();
|
||||
const [elementRect, setElementRect] = React.useState<DOMRect | null>(null);
|
||||
|
||||
// Get current step safely
|
||||
const step =
|
||||
currentStepIndex >= 0 && currentStepIndex < steps.length
|
||||
? steps[currentStepIndex]
|
||||
: null
|
||||
: null;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!step) return
|
||||
if (!step) return;
|
||||
|
||||
// Use requestAnimationFrame to ensure DOM is ready
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
const element = document.getElementById(step.selectorId)
|
||||
const element = document.getElementById(step.selectorId);
|
||||
console.debug(
|
||||
"Looking for element with ID:",
|
||||
step.selectorId,
|
||||
"Found:",
|
||||
!!element,
|
||||
)
|
||||
);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const rect = element.getBoundingClientRect();
|
||||
console.debug("Element rect:", {
|
||||
id: step.selectorId,
|
||||
top: rect.top,
|
||||
|
|
@ -208,34 +209,34 @@ function TourHighlight({
|
|||
height: rect.height,
|
||||
bottom: rect.bottom,
|
||||
right: rect.right,
|
||||
})
|
||||
setElementRect(rect)
|
||||
});
|
||||
setElementRect(rect);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Add click listener for onClickWithinArea
|
||||
let clickHandler: ((e: MouseEvent) => void) | null = null
|
||||
let clickHandler: ((e: MouseEvent) => void) | null = null;
|
||||
if (step.onClickWithinArea) {
|
||||
const element = document.getElementById(step.selectorId)
|
||||
const element = document.getElementById(step.selectorId);
|
||||
if (element) {
|
||||
clickHandler = (e: MouseEvent) => {
|
||||
if (element.contains(e.target as Node)) {
|
||||
step.onClickWithinArea?.()
|
||||
step.onClickWithinArea?.();
|
||||
}
|
||||
}
|
||||
document.addEventListener("click", clickHandler)
|
||||
};
|
||||
document.addEventListener("click", clickHandler);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId)
|
||||
cancelAnimationFrame(rafId);
|
||||
if (clickHandler) {
|
||||
document.removeEventListener("click", clickHandler)
|
||||
document.removeEventListener("click", clickHandler);
|
||||
}
|
||||
}
|
||||
}, [step])
|
||||
};
|
||||
}, [step]);
|
||||
|
||||
if (!step) return null
|
||||
if (!step) return null;
|
||||
|
||||
// Keep the wrapper mounted but animate the content
|
||||
return (
|
||||
|
|
@ -277,12 +278,12 @@ function TourHighlight({
|
|||
? elementRect.bottom + 8
|
||||
: step.position === "top"
|
||||
? elementRect.top - 200
|
||||
: elementRect.top + elementRect.height / 2 - 100
|
||||
: elementRect.top + elementRect.height / 2 - 100;
|
||||
|
||||
// Ensure tooltip stays within viewport
|
||||
const maxTop = window.innerHeight - 250 // Leave space for tooltip height
|
||||
const minTop = 10
|
||||
return Math.max(minTop, Math.min(baseTop, maxTop))
|
||||
const maxTop = window.innerHeight - 250; // Leave space for tooltip height
|
||||
const minTop = 10;
|
||||
return Math.max(minTop, Math.min(baseTop, maxTop));
|
||||
})(),
|
||||
left: (() => {
|
||||
const baseLeft =
|
||||
|
|
@ -290,12 +291,12 @@ function TourHighlight({
|
|||
? elementRect.right + 8
|
||||
: step.position === "left"
|
||||
? elementRect.left - 300
|
||||
: elementRect.left + elementRect.width / 2 - 150
|
||||
: elementRect.left + elementRect.width / 2 - 150;
|
||||
|
||||
// Ensure tooltip stays within viewport
|
||||
const maxLeft = window.innerWidth - 300 // Tooltip width
|
||||
const minLeft = 10
|
||||
return Math.max(minLeft, Math.min(baseLeft, maxLeft))
|
||||
const maxLeft = window.innerWidth - 300; // Tooltip width
|
||||
const minLeft = 10;
|
||||
return Math.max(minLeft, Math.min(baseLeft, maxLeft));
|
||||
})(),
|
||||
}}
|
||||
>
|
||||
|
|
@ -342,31 +343,31 @@ function TourHighlight({
|
|||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Tour Alert Dialog
|
||||
interface TourAlertDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function TourAlertDialog({ open, onOpenChange }: TourAlertDialogProps) {
|
||||
const { startTour, setIsTourCompleted } = useTour()
|
||||
const { startTour, setIsTourCompleted } = useTour();
|
||||
|
||||
const handleStart = () => {
|
||||
console.debug("TourAlertDialog: Starting tour")
|
||||
onOpenChange(false)
|
||||
startTour()
|
||||
}
|
||||
console.debug("TourAlertDialog: Starting tour");
|
||||
onOpenChange(false);
|
||||
startTour();
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
analytics.tourSkipped()
|
||||
setIsTourCompleted(true) // Mark tour as completed when skipped
|
||||
onOpenChange(false)
|
||||
}
|
||||
analytics.tourSkipped();
|
||||
setIsTourCompleted(true); // Mark tour as completed when skipped
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
if (!open) return null
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
|
|
@ -409,5 +410,5 @@ export function TourAlertDialog({ open, onOpenChange }: TourAlertDialogProps) {
|
|||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
67
apps/web/components/views/add-memory/action-buttons.tsx
Normal file
67
apps/web/components/views/add-memory/action-buttons.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Button } from '@repo/ui/components/button';
|
||||
import { Loader2, type LucideIcon } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
|
||||
interface ActionButtonsProps {
|
||||
onCancel: () => void;
|
||||
onSubmit?: () => void;
|
||||
submitText: string;
|
||||
submitIcon?: LucideIcon;
|
||||
isSubmitting?: boolean;
|
||||
isSubmitDisabled?: boolean;
|
||||
submitType?: 'button' | 'submit';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ActionButtons({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
submitText,
|
||||
submitIcon: SubmitIcon,
|
||||
isSubmitting = false,
|
||||
isSubmitDisabled = false,
|
||||
submitType = 'submit',
|
||||
className = '',
|
||||
}: ActionButtonsProps) {
|
||||
return (
|
||||
<div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
|
||||
<Button
|
||||
className="hover:bg-white/10 text-white border-none flex-1 sm:flex-initial"
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="flex-1 sm:flex-initial"
|
||||
>
|
||||
<Button
|
||||
className="bg-white/10 hover:bg-white/20 text-white border-white/20 w-full"
|
||||
disabled={isSubmitting || isSubmitDisabled}
|
||||
onClick={submitType === 'button' ? onSubmit : undefined}
|
||||
type={submitType}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
{submitText.includes('Add')
|
||||
? 'Adding...'
|
||||
: submitText.includes('Upload')
|
||||
? 'Uploading...'
|
||||
: 'Processing...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{SubmitIcon && <SubmitIcon className="h-4 w-4 mr-2" />}
|
||||
{submitText}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1205
apps/web/components/views/add-memory/index.tsx
Normal file
1205
apps/web/components/views/add-memory/index.tsx
Normal file
File diff suppressed because it is too large
Load diff
53
apps/web/components/views/add-memory/memory-usage-ring.tsx
Normal file
53
apps/web/components/views/add-memory/memory-usage-ring.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
interface MemoryUsageRingProps {
|
||||
memoriesUsed: number;
|
||||
memoriesLimit: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MemoryUsageRing({
|
||||
memoriesUsed,
|
||||
memoriesLimit,
|
||||
className = "",
|
||||
}: MemoryUsageRingProps) {
|
||||
const usagePercentage = memoriesUsed / memoriesLimit;
|
||||
const strokeColor =
|
||||
memoriesUsed >= memoriesLimit * 0.8 ? "rgb(251 191 36)" : "rgb(34 197 94)";
|
||||
const circumference = 2 * Math.PI * 10;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative group cursor-help self-center sm:self-end mb-1 hidden sm:block ${className}`}
|
||||
title={`${memoriesUsed} of ${memoriesLimit} memories used`}
|
||||
>
|
||||
<svg className="w-6 h-6 transform -rotate-90" viewBox="0 0 24 24">
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="rgb(255 255 255 / 0.1)"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
/>
|
||||
{/* Progress circle */}
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke={strokeColor}
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
strokeDasharray={`${circumference}`}
|
||||
strokeDashoffset={`${circumference * (1 - usagePercentage)}`}
|
||||
className="transition-all duration-300"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Tooltip on hover */}
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-black/90 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap">
|
||||
{memoriesUsed} / {memoriesLimit}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
94
apps/web/components/views/add-memory/project-selection.tsx
Normal file
94
apps/web/components/views/add-memory/project-selection.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@repo/ui/components/select';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
interface Project {
|
||||
id?: string;
|
||||
containerTag: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ProjectSelectionProps {
|
||||
projects: Project[];
|
||||
selectedProject: string;
|
||||
onProjectChange: (value: string) => void;
|
||||
onCreateProject: () => void;
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function ProjectSelection({
|
||||
projects,
|
||||
selectedProject,
|
||||
onProjectChange,
|
||||
onCreateProject,
|
||||
disabled = false,
|
||||
isLoading = false,
|
||||
className = '',
|
||||
id = 'project-select',
|
||||
}: ProjectSelectionProps) {
|
||||
const handleValueChange = (value: string) => {
|
||||
if (value === 'create-new-project') {
|
||||
onCreateProject();
|
||||
} else {
|
||||
onProjectChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
key={`${id}-${selectedProject}`}
|
||||
disabled={isLoading || disabled}
|
||||
onValueChange={handleValueChange}
|
||||
value={selectedProject}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`bg-white/5 border-white/10 text-white ${className}`}
|
||||
id={id}
|
||||
>
|
||||
<SelectValue placeholder="Select a project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
className="bg-black/90 backdrop-blur-xl border-white/10 z-[90]"
|
||||
position="popper"
|
||||
sideOffset={5}
|
||||
>
|
||||
<SelectItem
|
||||
className="text-white hover:bg-white/10"
|
||||
key="default"
|
||||
value="sm_project_default"
|
||||
>
|
||||
Default Project
|
||||
</SelectItem>
|
||||
{projects
|
||||
.filter((p) => p.containerTag !== 'sm_project_default' && p.id)
|
||||
.map((project) => (
|
||||
<SelectItem
|
||||
className="text-white hover:bg-white/10"
|
||||
key={project.id || project.containerTag}
|
||||
value={project.containerTag}
|
||||
>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem
|
||||
className="text-white hover:bg-white/10 border-t border-white/10 mt-1"
|
||||
key="create-new"
|
||||
value="create-new-project"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Create new project</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
28
apps/web/components/views/add-memory/tab-button.tsx
Normal file
28
apps/web/components/views/add-memory/tab-button.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
interface TabButtonProps {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function TabButton({
|
||||
icon: Icon,
|
||||
label,
|
||||
isActive,
|
||||
onClick,
|
||||
}: TabButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={`flex items-center gap-1.5 text-xs sm:text-xs px-4 sm:px-3 py-2 sm:py-1 h-8 sm:h-6 rounded-sm transition-colors whitespace-nowrap min-w-0 ${
|
||||
isActive ? 'bg-white/10' : 'hover:bg-white/5'
|
||||
}`}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
<Icon className="h-4 w-4 sm:h-3 sm:w-3" />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
545
apps/web/components/views/add-memory/text-editor.tsx
Normal file
545
apps/web/components/views/add-memory/text-editor.tsx
Normal file
|
|
@ -0,0 +1,545 @@
|
|||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import isHotkey from "is-hotkey";
|
||||
import {
|
||||
Bold,
|
||||
Code,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Italic,
|
||||
List,
|
||||
Quote,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
type BaseEditor,
|
||||
createEditor,
|
||||
type Descendant,
|
||||
Editor,
|
||||
Transforms,
|
||||
} from "slate";
|
||||
import type { ReactEditor as ReactEditorType } from "slate-react";
|
||||
import {
|
||||
Editable,
|
||||
ReactEditor,
|
||||
type RenderElementProps,
|
||||
type RenderLeafProps,
|
||||
Slate,
|
||||
withReact,
|
||||
} from "slate-react";
|
||||
|
||||
type CustomEditor = BaseEditor & ReactEditorType;
|
||||
|
||||
type ParagraphElement = {
|
||||
type: "paragraph";
|
||||
children: CustomText[];
|
||||
};
|
||||
|
||||
type HeadingElement = {
|
||||
type: "heading";
|
||||
level: number;
|
||||
children: CustomText[];
|
||||
};
|
||||
|
||||
type ListItemElement = {
|
||||
type: "list-item";
|
||||
children: CustomText[];
|
||||
};
|
||||
|
||||
type BlockQuoteElement = {
|
||||
type: "block-quote";
|
||||
children: CustomText[];
|
||||
};
|
||||
|
||||
type CustomElement =
|
||||
| ParagraphElement
|
||||
| HeadingElement
|
||||
| ListItemElement
|
||||
| BlockQuoteElement;
|
||||
|
||||
type FormattedText = {
|
||||
text: string;
|
||||
bold?: true;
|
||||
italic?: true;
|
||||
code?: true;
|
||||
};
|
||||
|
||||
type CustomText = FormattedText;
|
||||
|
||||
declare module "slate" {
|
||||
interface CustomTypes {
|
||||
Editor: CustomEditor;
|
||||
Element: CustomElement;
|
||||
Text: CustomText;
|
||||
}
|
||||
}
|
||||
|
||||
// Hotkey mappings
|
||||
const HOTKEYS: Record<string, keyof CustomText> = {
|
||||
"mod+b": "bold",
|
||||
"mod+i": "italic",
|
||||
"mod+`": "code",
|
||||
};
|
||||
|
||||
interface TextEditorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onBlur?: () => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const initialValue: Descendant[] = [
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [{ text: "" }],
|
||||
},
|
||||
];
|
||||
|
||||
const serialize = (nodes: Descendant[]): string => {
|
||||
return nodes.map((n) => serializeNode(n)).join("\n");
|
||||
};
|
||||
|
||||
const serializeNode = (node: CustomElement | CustomText): string => {
|
||||
if ("text" in node) {
|
||||
let text = node.text;
|
||||
if (node.bold) text = `**${text}**`;
|
||||
if (node.italic) text = `*${text}*`;
|
||||
if (node.code) text = `\`${text}\``;
|
||||
return text;
|
||||
}
|
||||
|
||||
const children = node.children
|
||||
? node.children.map(serializeNode).join("")
|
||||
: "";
|
||||
|
||||
switch (node.type) {
|
||||
case "paragraph":
|
||||
return children;
|
||||
case "heading":
|
||||
return `${"#".repeat(node.level || 1)} ${children}`;
|
||||
case "list-item":
|
||||
return `- ${children}`;
|
||||
case "block-quote":
|
||||
return `> ${children}`;
|
||||
default:
|
||||
return children;
|
||||
}
|
||||
};
|
||||
|
||||
const deserialize = (text: string): Descendant[] => {
|
||||
if (!text.trim()) {
|
||||
return initialValue;
|
||||
}
|
||||
|
||||
const lines = text.split("\n");
|
||||
const nodes: Descendant[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (trimmedLine.startsWith("# ")) {
|
||||
nodes.push({
|
||||
type: "heading",
|
||||
level: 1,
|
||||
children: [{ text: trimmedLine.slice(2) }],
|
||||
});
|
||||
} else if (trimmedLine.startsWith("## ")) {
|
||||
nodes.push({
|
||||
type: "heading",
|
||||
level: 2,
|
||||
children: [{ text: trimmedLine.slice(3) }],
|
||||
});
|
||||
} else if (trimmedLine.startsWith("### ")) {
|
||||
nodes.push({
|
||||
type: "heading",
|
||||
level: 3,
|
||||
children: [{ text: trimmedLine.slice(4) }],
|
||||
});
|
||||
} else if (trimmedLine.startsWith("- ")) {
|
||||
nodes.push({
|
||||
type: "list-item",
|
||||
children: [{ text: trimmedLine.slice(2) }],
|
||||
});
|
||||
} else if (trimmedLine.startsWith("> ")) {
|
||||
nodes.push({
|
||||
type: "block-quote",
|
||||
children: [{ text: trimmedLine.slice(2) }],
|
||||
});
|
||||
} else {
|
||||
nodes.push({
|
||||
type: "paragraph",
|
||||
children: [{ text: line }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return nodes.length > 0 ? nodes : initialValue;
|
||||
};
|
||||
|
||||
const isMarkActive = (editor: CustomEditor, format: keyof CustomText) => {
|
||||
const marks = Editor.marks(editor);
|
||||
return marks ? marks[format as keyof typeof marks] === true : false;
|
||||
};
|
||||
|
||||
const toggleMark = (editor: CustomEditor, format: keyof CustomText) => {
|
||||
const isActive = isMarkActive(editor, format);
|
||||
|
||||
if (isActive) {
|
||||
Editor.removeMark(editor, format);
|
||||
} else {
|
||||
Editor.addMark(editor, format, true);
|
||||
}
|
||||
|
||||
// Focus back to editor after toggling
|
||||
ReactEditor.focus(editor);
|
||||
};
|
||||
|
||||
const isBlockActive = (
|
||||
editor: CustomEditor,
|
||||
format: string,
|
||||
level?: number,
|
||||
) => {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const [match] = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
at: Editor.unhangRange(editor, selection),
|
||||
match: (n) =>
|
||||
!Editor.isEditor(n) &&
|
||||
(n as CustomElement).type === format &&
|
||||
(level === undefined || (n as HeadingElement).level === level),
|
||||
}),
|
||||
);
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
const toggleBlock = (editor: CustomEditor, format: string, level?: number) => {
|
||||
const isActive = isBlockActive(editor, format, level);
|
||||
const newProperties: any = {
|
||||
type: isActive ? "paragraph" : format,
|
||||
};
|
||||
|
||||
if (format === "heading" && level && !isActive) {
|
||||
newProperties.level = level;
|
||||
}
|
||||
|
||||
Transforms.setNodes(editor, newProperties);
|
||||
|
||||
// Focus back to editor after toggling
|
||||
ReactEditor.focus(editor);
|
||||
};
|
||||
|
||||
export function TextEditor({
|
||||
value = "",
|
||||
onChange,
|
||||
onBlur,
|
||||
placeholder = "Start writing...",
|
||||
disabled = false,
|
||||
className,
|
||||
}: TextEditorProps) {
|
||||
const editor = useMemo(() => withReact(createEditor()) as CustomEditor, []);
|
||||
const [editorValue, setEditorValue] = useState<Descendant[]>(() =>
|
||||
deserialize(value),
|
||||
);
|
||||
const [selection, setSelection] = useState(editor.selection);
|
||||
|
||||
const renderElement = useCallback((props: RenderElementProps) => {
|
||||
switch (props.element.type) {
|
||||
case "heading": {
|
||||
const element = props.element as HeadingElement;
|
||||
const HeadingTag = `h${element.level || 1}` as
|
||||
| "h1"
|
||||
| "h2"
|
||||
| "h3"
|
||||
| "h4"
|
||||
| "h5"
|
||||
| "h6";
|
||||
return (
|
||||
<HeadingTag
|
||||
{...props.attributes}
|
||||
className={cn(
|
||||
"font-bold",
|
||||
element.level === 1 && "text-2xl mb-4",
|
||||
element.level === 2 && "text-xl mb-3",
|
||||
element.level === 3 && "text-lg mb-2",
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</HeadingTag>
|
||||
);
|
||||
}
|
||||
case "list-item":
|
||||
return (
|
||||
<li {...props.attributes} className="ml-4 list-disc">
|
||||
{props.children}
|
||||
</li>
|
||||
);
|
||||
case "block-quote":
|
||||
return (
|
||||
<blockquote
|
||||
{...props.attributes}
|
||||
className="border-l-4 border-white/20 pl-4 italic text-white/80"
|
||||
>
|
||||
{props.children}
|
||||
</blockquote>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<p {...props.attributes} className="mb-2">
|
||||
{props.children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderLeaf = useCallback((props: RenderLeafProps) => {
|
||||
let { attributes, children, leaf } = props;
|
||||
|
||||
if (leaf.bold) {
|
||||
children = <strong>{children}</strong>;
|
||||
}
|
||||
|
||||
if (leaf.italic) {
|
||||
children = <em>{children}</em>;
|
||||
}
|
||||
|
||||
if (leaf.code) {
|
||||
children = (
|
||||
<code className="bg-white/10 px-1 rounded text-sm">{children}</code>
|
||||
);
|
||||
}
|
||||
|
||||
return <span {...attributes}>{children}</span>;
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
// Handle hotkeys for formatting
|
||||
for (const hotkey in HOTKEYS) {
|
||||
if (isHotkey(hotkey, event)) {
|
||||
event.preventDefault();
|
||||
const mark = HOTKEYS[hotkey];
|
||||
if (mark) {
|
||||
toggleMark(editor, mark);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle block formatting hotkeys
|
||||
if (isHotkey("mod+shift+1", event)) {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, "heading", 1);
|
||||
return;
|
||||
}
|
||||
if (isHotkey("mod+shift+2", event)) {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, "heading", 2);
|
||||
return;
|
||||
}
|
||||
if (isHotkey("mod+shift+3", event)) {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, "heading", 3);
|
||||
return;
|
||||
}
|
||||
if (isHotkey("mod+shift+8", event)) {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, "list-item");
|
||||
return;
|
||||
}
|
||||
if (isHotkey("mod+shift+.", event)) {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, "block-quote");
|
||||
return;
|
||||
}
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
const handleSlateChange = useCallback(
|
||||
(newValue: Descendant[]) => {
|
||||
setEditorValue(newValue);
|
||||
const serializedValue = serialize(newValue);
|
||||
onChange?.(serializedValue);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
// Memoized active states that update when selection changes
|
||||
const activeStates = useMemo(
|
||||
() => ({
|
||||
bold: isMarkActive(editor, "bold"),
|
||||
italic: isMarkActive(editor, "italic"),
|
||||
code: isMarkActive(editor, "code"),
|
||||
heading1: isBlockActive(editor, "heading", 1),
|
||||
heading2: isBlockActive(editor, "heading", 2),
|
||||
heading3: isBlockActive(editor, "heading", 3),
|
||||
listItem: isBlockActive(editor, "list-item"),
|
||||
blockQuote: isBlockActive(editor, "block-quote"),
|
||||
}),
|
||||
[editor, selection],
|
||||
);
|
||||
|
||||
const ToolbarButton = ({
|
||||
icon: Icon,
|
||||
isActive,
|
||||
onMouseDown,
|
||||
title,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
isActive: boolean;
|
||||
onMouseDown: (event: React.MouseEvent) => void;
|
||||
title: string;
|
||||
}) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-8 w-8 !p-0 text-white/70 transition-all duration-200 rounded-sm",
|
||||
"hover:bg-white/15 hover:text-white hover:scale-105",
|
||||
"active:scale-95",
|
||||
isActive && "bg-white/20 text-white",
|
||||
)}
|
||||
onMouseDown={onMouseDown}
|
||||
title={title}
|
||||
type="button"
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
"h-4 w-4 transition-transform duration-200",
|
||||
isActive && "scale-110",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col", className)}>
|
||||
<div className="flex-1 min-h-48 max-h-64 overflow-y-auto">
|
||||
<Slate
|
||||
editor={editor}
|
||||
initialValue={editorValue}
|
||||
onValueChange={handleSlateChange}
|
||||
onSelectionChange={() => setSelection(editor.selection)}
|
||||
>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
placeholder={placeholder}
|
||||
renderPlaceholder={({ children, attributes }) => {
|
||||
return (
|
||||
<div {...attributes} className="mt-2">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={onBlur}
|
||||
readOnly={disabled}
|
||||
className={cn(
|
||||
"outline-none w-full h-full text-white placeholder:text-white/50",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
)}
|
||||
style={{
|
||||
minHeight: "11rem",
|
||||
maxHeight: "15rem",
|
||||
padding: "12px",
|
||||
}}
|
||||
/>
|
||||
</Slate>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<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">
|
||||
{/* Text formatting */}
|
||||
<ToolbarButton
|
||||
icon={Bold}
|
||||
isActive={activeStates.bold}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
toggleMark(editor, "bold");
|
||||
}}
|
||||
title="Bold (Ctrl/Cmd+B)"
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={Italic}
|
||||
isActive={activeStates.italic}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
toggleMark(editor, "italic");
|
||||
}}
|
||||
title="Italic (Ctrl/Cmd+I)"
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={Code}
|
||||
isActive={activeStates.code}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
toggleMark(editor, "code");
|
||||
}}
|
||||
title="Code (Ctrl/Cmd+`)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-px h-6 bg-white/30 mx-2" />
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Block formatting */}
|
||||
<ToolbarButton
|
||||
icon={Heading1}
|
||||
isActive={activeStates.heading1}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, "heading", 1);
|
||||
}}
|
||||
title="Heading 1 (Ctrl/Cmd+Shift+1)"
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={Heading2}
|
||||
isActive={activeStates.heading2}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, "heading", 2);
|
||||
}}
|
||||
title="Heading 2 (Ctrl/Cmd+Shift+2)"
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={Heading3}
|
||||
isActive={activeStates.heading3}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, "heading", 3);
|
||||
}}
|
||||
title="Heading 3"
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={List}
|
||||
isActive={activeStates.listItem}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, "list-item");
|
||||
}}
|
||||
title="Bullet List"
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={Quote}
|
||||
isActive={activeStates.blockQuote}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, "block-quote");
|
||||
}}
|
||||
title="Quote"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,35 +1,35 @@
|
|||
import { useAuth } from "@lib/auth-context"
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
import {
|
||||
fetchConnectionsFeature,
|
||||
fetchMemoriesFeature,
|
||||
fetchSubscriptionStatus,
|
||||
} from "@lib/queries"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { HeadingH3Bold } from "@ui/text/heading/heading-h3-bold"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { CheckCircle, LoaderIcon, X } from "lucide-react"
|
||||
import { motion } from "motion/react"
|
||||
import Link from "next/link"
|
||||
import { useState, useEffect } from "react"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
} from "@lib/queries";
|
||||
import { Button } from "@ui/components/button";
|
||||
import { HeadingH3Bold } from "@ui/text/heading/heading-h3-bold";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
import { CheckCircle, LoaderIcon, X } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
|
||||
export function BillingView() {
|
||||
const autumn = useCustomer()
|
||||
const { user } = useAuth()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const autumn = useCustomer();
|
||||
const { user } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
analytics.billingViewed()
|
||||
}, [])
|
||||
analytics.billingViewed();
|
||||
}, []);
|
||||
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any)
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any);
|
||||
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0;
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0;
|
||||
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any)
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any);
|
||||
|
||||
const connectionsUsed = connectionsCheck?.usage ?? 0
|
||||
const connectionsUsed = connectionsCheck?.usage ?? 0;
|
||||
|
||||
// Fetch subscription status with React Query
|
||||
const {
|
||||
|
|
@ -37,34 +37,34 @@ export function BillingView() {
|
|||
consumer_pro: null,
|
||||
},
|
||||
isLoading: isCheckingStatus,
|
||||
} = fetchSubscriptionStatus(autumn as any)
|
||||
} = fetchSubscriptionStatus(autumn as any);
|
||||
|
||||
// Handle upgrade
|
||||
const handleUpgrade = async () => {
|
||||
analytics.upgradeInitiated()
|
||||
setIsLoading(true)
|
||||
analytics.upgradeInitiated();
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
})
|
||||
analytics.upgradeCompleted()
|
||||
window.location.reload()
|
||||
});
|
||||
analytics.upgradeCompleted();
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
setIsLoading(false)
|
||||
console.error(error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle manage billing
|
||||
const handleManageBilling = async () => {
|
||||
analytics.billingPortalOpened()
|
||||
analytics.billingPortalOpened();
|
||||
await autumn.openBillingPortal({
|
||||
returnUrl: "https://app.supermemory.ai",
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const isPro = status.consumer_pro
|
||||
const isPro = status.consumer_pro;
|
||||
|
||||
if (user?.isAnonymous) {
|
||||
return (
|
||||
|
|
@ -85,7 +85,7 @@ export function BillingView() {
|
|||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isPro) {
|
||||
|
|
@ -147,7 +147,7 @@ export function BillingView() {
|
|||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -257,5 +257,5 @@ export function BillingView() {
|
|||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,322 +1,446 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import { useChat, useCompletion } from '@ai-sdk/react'
|
||||
import { DefaultChatTransport } from 'ai'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useProject, usePersistentChat } from '@/stores'
|
||||
import { Button } from '@ui/components/button'
|
||||
import { Input } from '@ui/components/input'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Spinner } from '../../spinner'
|
||||
import { X, ArrowUp, Check, RotateCcw, Copy } from 'lucide-react'
|
||||
import { useGraphHighlights } from '@/stores/highlights'
|
||||
import { cn } from '@lib/utils'
|
||||
import { TextShimmer } from '@/components/text-shimmer'
|
||||
import { toast } from 'sonner'
|
||||
import { useChat, useCompletion } from "@ai-sdk/react";
|
||||
import { cn } from "@lib/utils";
|
||||
import { Button } from "@ui/components/button";
|
||||
import { Input } from "@ui/components/input";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { ArrowUp, Check, Copy, RotateCcw, X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { toast } from "sonner";
|
||||
import { TextShimmer } from "@/components/text-shimmer";
|
||||
import { usePersistentChat, useProject } from "@/stores";
|
||||
import { useGraphHighlights } from "@/stores/highlights";
|
||||
import { Spinner } from "../../spinner";
|
||||
|
||||
function useStickyAutoScroll(triggerKeys: ReadonlyArray<unknown>) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const bottomRef = useRef<HTMLDivElement>(null)
|
||||
const [isAutoScroll, setIsAutoScroll] = useState(true)
|
||||
const [isFarFromBottom, setIsFarFromBottom] = useState(false)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const [isAutoScroll, setIsAutoScroll] = useState(true);
|
||||
const [isFarFromBottom, setIsFarFromBottom] = useState(false);
|
||||
|
||||
function scrollToBottom(behavior: ScrollBehavior = 'auto') {
|
||||
const node = bottomRef.current
|
||||
if (node) node.scrollIntoView({ behavior, block: 'end' })
|
||||
}
|
||||
function scrollToBottom(behavior: ScrollBehavior = "auto") {
|
||||
const node = bottomRef.current;
|
||||
if (node) node.scrollIntoView({ behavior, block: "end" });
|
||||
}
|
||||
|
||||
useEffect(function observeBottomVisibility() {
|
||||
const container = scrollContainerRef.current
|
||||
const sentinel = bottomRef.current
|
||||
if (!container || !sentinel) return
|
||||
useEffect(function observeBottomVisibility() {
|
||||
const container = scrollContainerRef.current;
|
||||
const sentinel = bottomRef.current;
|
||||
if (!container || !sentinel) return;
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (!entries || entries.length === 0) return
|
||||
const isIntersecting = entries.some((e) => e.isIntersecting)
|
||||
setIsAutoScroll(isIntersecting)
|
||||
}, { root: container, rootMargin: '0px 0px 80px 0px', threshold: 0 })
|
||||
observer.observe(sentinel)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (!entries || entries.length === 0) return;
|
||||
const isIntersecting = entries.some((e) => e.isIntersecting);
|
||||
setIsAutoScroll(isIntersecting);
|
||||
},
|
||||
{ root: container, rootMargin: "0px 0px 80px 0px", threshold: 0 },
|
||||
);
|
||||
observer.observe(sentinel);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(function observeContentResize() {
|
||||
const container = scrollContainerRef.current
|
||||
if (!container) return
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (isAutoScroll) scrollToBottom('auto')
|
||||
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight
|
||||
setIsFarFromBottom(distanceFromBottom > 100)
|
||||
})
|
||||
resizeObserver.observe(container)
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [isAutoScroll])
|
||||
useEffect(
|
||||
function observeContentResize() {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (isAutoScroll) scrollToBottom("auto");
|
||||
const distanceFromBottom =
|
||||
container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
setIsFarFromBottom(distanceFromBottom > 100);
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
return () => resizeObserver.disconnect();
|
||||
},
|
||||
[isAutoScroll],
|
||||
);
|
||||
|
||||
function enableAutoScroll() {
|
||||
setIsAutoScroll(true)
|
||||
}
|
||||
function enableAutoScroll() {
|
||||
setIsAutoScroll(true);
|
||||
}
|
||||
|
||||
useEffect(function autoScrollOnNewContent() {
|
||||
if (isAutoScroll) scrollToBottom('auto')
|
||||
}, [isAutoScroll, ...triggerKeys])
|
||||
useEffect(
|
||||
function autoScrollOnNewContent() {
|
||||
if (isAutoScroll) scrollToBottom("auto");
|
||||
},
|
||||
[isAutoScroll, ...triggerKeys],
|
||||
);
|
||||
|
||||
function recomputeDistanceFromBottom() {
|
||||
const container = scrollContainerRef.current
|
||||
if (!container) return
|
||||
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight
|
||||
setIsFarFromBottom(distanceFromBottom > 100)
|
||||
}
|
||||
function recomputeDistanceFromBottom() {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
const distanceFromBottom =
|
||||
container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
setIsFarFromBottom(distanceFromBottom > 100);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
recomputeDistanceFromBottom()
|
||||
}, [...triggerKeys])
|
||||
useEffect(() => {
|
||||
recomputeDistanceFromBottom();
|
||||
}, [...triggerKeys]);
|
||||
|
||||
function onScroll() {
|
||||
recomputeDistanceFromBottom()
|
||||
}
|
||||
function onScroll() {
|
||||
recomputeDistanceFromBottom();
|
||||
}
|
||||
|
||||
return { scrollContainerRef, bottomRef, isAutoScroll, isFarFromBottom, onScroll, enableAutoScroll, scrollToBottom } as const
|
||||
return {
|
||||
scrollContainerRef,
|
||||
bottomRef,
|
||||
isAutoScroll,
|
||||
isFarFromBottom,
|
||||
onScroll,
|
||||
enableAutoScroll,
|
||||
scrollToBottom,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function ChatMessages() {
|
||||
const { selectedProject } = useProject()
|
||||
const { currentChatId, setCurrentChatId, setConversation, getCurrentConversation, setConversationTitle, getCurrentChat } = usePersistentChat()
|
||||
const { selectedProject } = useProject();
|
||||
const {
|
||||
currentChatId,
|
||||
setCurrentChatId,
|
||||
setConversation,
|
||||
getCurrentConversation,
|
||||
setConversationTitle,
|
||||
getCurrentChat,
|
||||
} = usePersistentChat();
|
||||
|
||||
const activeChatIdRef = useRef<string | null>(null)
|
||||
const shouldGenerateTitleRef = useRef<boolean>(false)
|
||||
const activeChatIdRef = useRef<string | null>(null);
|
||||
const shouldGenerateTitleRef = useRef<boolean>(false);
|
||||
|
||||
const { setDocumentIds } = useGraphHighlights()
|
||||
const { setDocumentIds } = useGraphHighlights();
|
||||
|
||||
const { messages, sendMessage, status, stop, setMessages, id, regenerate } = useChat({
|
||||
id: currentChatId ?? undefined,
|
||||
transport: new DefaultChatTransport({
|
||||
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat`,
|
||||
credentials: 'include',
|
||||
body: { metadata: { projectId: selectedProject } },
|
||||
}),
|
||||
maxSteps: 2,
|
||||
onFinish: (result) => {
|
||||
const activeId = activeChatIdRef.current
|
||||
if (!activeId) return
|
||||
if (result.message.role !== 'assistant') return
|
||||
const { messages, sendMessage, status, stop, setMessages, id, regenerate } =
|
||||
useChat({
|
||||
id: currentChatId ?? undefined,
|
||||
transport: new DefaultChatTransport({
|
||||
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat`,
|
||||
credentials: "include",
|
||||
body: { metadata: { projectId: selectedProject } },
|
||||
}),
|
||||
maxSteps: 2,
|
||||
onFinish: (result) => {
|
||||
const activeId = activeChatIdRef.current;
|
||||
if (!activeId) return;
|
||||
if (result.message.role !== "assistant") return;
|
||||
|
||||
if (shouldGenerateTitleRef.current) {
|
||||
const textPart = result.message.parts.find((p: any) => p?.type === 'text') as any
|
||||
const text = textPart?.text?.trim()
|
||||
if (text) {
|
||||
shouldGenerateTitleRef.current = false
|
||||
complete(text)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
if (shouldGenerateTitleRef.current) {
|
||||
const textPart = result.message.parts.find(
|
||||
(p: any) => p?.type === "text",
|
||||
) as any;
|
||||
const text = textPart?.text?.trim();
|
||||
if (text) {
|
||||
shouldGenerateTitleRef.current = false;
|
||||
complete(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
activeChatIdRef.current = (currentChatId ?? id) ?? null
|
||||
}, [currentChatId, id])
|
||||
useEffect(() => {
|
||||
activeChatIdRef.current = currentChatId ?? id ?? null;
|
||||
}, [currentChatId, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id && id !== currentChatId) {
|
||||
setCurrentChatId(id)
|
||||
}
|
||||
}, [id, currentChatId, setCurrentChatId])
|
||||
useEffect(() => {
|
||||
if (id && id !== currentChatId) {
|
||||
setCurrentChatId(id);
|
||||
}
|
||||
}, [id, currentChatId, setCurrentChatId]);
|
||||
|
||||
useEffect(() => {
|
||||
const msgs = getCurrentConversation()
|
||||
setMessages(msgs ?? [])
|
||||
setInput('')
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentChatId])
|
||||
useEffect(() => {
|
||||
const msgs = getCurrentConversation();
|
||||
setMessages(msgs ?? []);
|
||||
setInput("");
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentChatId]);
|
||||
|
||||
useEffect(() => {
|
||||
const activeId = currentChatId ?? id
|
||||
if (activeId && messages.length > 0) {
|
||||
setConversation(activeId, messages)
|
||||
}
|
||||
}, [messages, currentChatId, id, setConversation])
|
||||
useEffect(() => {
|
||||
const activeId = currentChatId ?? id;
|
||||
if (activeId && messages.length > 0) {
|
||||
setConversation(activeId, messages);
|
||||
}
|
||||
}, [messages, currentChatId, id, setConversation]);
|
||||
|
||||
const [input, setInput] = useState('')
|
||||
const { complete } = useCompletion({
|
||||
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/title`,
|
||||
credentials: 'include',
|
||||
onFinish: (_, completion) => {
|
||||
const activeId = activeChatIdRef.current
|
||||
if (!completion || !activeId) return
|
||||
setConversationTitle(activeId, completion.trim())
|
||||
}
|
||||
})
|
||||
const [input, setInput] = useState("");
|
||||
const { complete } = useCompletion({
|
||||
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/title`,
|
||||
credentials: "include",
|
||||
onFinish: (_, completion) => {
|
||||
const activeId = activeChatIdRef.current;
|
||||
if (!completion || !activeId) return;
|
||||
setConversationTitle(activeId, completion.trim());
|
||||
},
|
||||
});
|
||||
|
||||
// Update graph highlights from the most recent tool-searchMemories output
|
||||
useEffect(() => {
|
||||
try {
|
||||
const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant')
|
||||
if (!lastAssistant) return
|
||||
const lastSearchPart = [...(lastAssistant.parts as any[])]
|
||||
.reverse()
|
||||
.find((p) => p?.type === 'tool-searchMemories' && p?.state === 'output-available')
|
||||
if (!lastSearchPart) return
|
||||
const output = (lastSearchPart as any).output
|
||||
const ids = Array.isArray(output?.results)
|
||||
? (output.results as any[]).map((r) => r?.documentId).filter(Boolean) as string[]
|
||||
: []
|
||||
if (ids.length > 0) {
|
||||
setDocumentIds(ids)
|
||||
}
|
||||
} catch { }
|
||||
}, [messages, setDocumentIds])
|
||||
// Update graph highlights from the most recent tool-searchMemories output
|
||||
useEffect(() => {
|
||||
try {
|
||||
const lastAssistant = [...messages]
|
||||
.reverse()
|
||||
.find((m) => m.role === "assistant");
|
||||
if (!lastAssistant) return;
|
||||
const lastSearchPart = [...(lastAssistant.parts as any[])]
|
||||
.reverse()
|
||||
.find(
|
||||
(p) =>
|
||||
p?.type === "tool-searchMemories" &&
|
||||
p?.state === "output-available",
|
||||
);
|
||||
if (!lastSearchPart) return;
|
||||
const output = (lastSearchPart as any).output;
|
||||
const ids = Array.isArray(output?.results)
|
||||
? ((output.results as any[])
|
||||
.map((r) => r?.documentId)
|
||||
.filter(Boolean) as string[])
|
||||
: [];
|
||||
if (ids.length > 0) {
|
||||
setDocumentIds(ids);
|
||||
}
|
||||
} catch {}
|
||||
}, [messages, setDocumentIds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSummary = getCurrentChat()
|
||||
const hasTitle = Boolean(currentSummary?.title && currentSummary.title.trim().length > 0)
|
||||
shouldGenerateTitleRef.current = !hasTitle
|
||||
}, [currentChatId, id, getCurrentChat])
|
||||
const { scrollContainerRef, bottomRef, isFarFromBottom, onScroll, enableAutoScroll, scrollToBottom } = useStickyAutoScroll([messages, status])
|
||||
useEffect(() => {
|
||||
const currentSummary = getCurrentChat();
|
||||
const hasTitle = Boolean(
|
||||
currentSummary?.title && currentSummary.title.trim().length > 0,
|
||||
);
|
||||
shouldGenerateTitleRef.current = !hasTitle;
|
||||
}, [currentChatId, id, getCurrentChat]);
|
||||
const {
|
||||
scrollContainerRef,
|
||||
bottomRef,
|
||||
isFarFromBottom,
|
||||
onScroll,
|
||||
enableAutoScroll,
|
||||
scrollToBottom,
|
||||
} = useStickyAutoScroll([messages, status]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative grow">
|
||||
<div ref={scrollContainerRef} onScroll={onScroll} className="flex flex-col gap-2 absolute inset-0 overflow-y-auto px-4 pt-4 pb-7 scroll-pb-7">
|
||||
{messages.map((message) => (
|
||||
<div key={message.id} className={cn('flex flex-col', message.role === 'user' ? 'items-end' : 'items-start')}>
|
||||
<div className="flex flex-col gap-2 max-w-4/5 bg-white/10 py-3 px-4 rounded-lg">
|
||||
{message.parts
|
||||
.filter((part) => ['text', 'tool-searchMemories', 'tool-addMemory'].includes(part.type))
|
||||
.map((part, index) => {
|
||||
switch (part.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<div key={index} className="prose prose-sm prose-invert max-w-none">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{(part as any).text}</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
case 'tool-searchMemories':
|
||||
switch (part.state) {
|
||||
case 'input-available':
|
||||
case 'input-streaming':
|
||||
return (
|
||||
<div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
|
||||
<Spinner className="size-4" /> Searching memories...
|
||||
</div>
|
||||
)
|
||||
case 'output-error':
|
||||
return (
|
||||
<div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
|
||||
<X className="size-4" /> Error recalling memories
|
||||
</div>
|
||||
)
|
||||
case 'output-available':
|
||||
{
|
||||
const output = (part as any).output
|
||||
const foundCount = typeof output === 'object' && output !== null && 'count' in output ? Number(output.count) || 0 : 0
|
||||
const ids = Array.isArray(output?.results) ? (output.results as any[]).map((r) => r?.documentId).filter(Boolean) as string[] : []
|
||||
return (
|
||||
<div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
|
||||
<Check className="size-4" /> Found {foundCount} memories
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="relative grow">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
onScroll={onScroll}
|
||||
className="flex flex-col gap-2 absolute inset-0 overflow-y-auto px-4 pt-4 pb-7 scroll-pb-7"
|
||||
>
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={cn(
|
||||
"flex flex-col",
|
||||
message.role === "user" ? "items-end" : "items-start",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2 max-w-4/5 bg-white/10 py-3 px-4 rounded-lg">
|
||||
{message.parts
|
||||
.filter((part) =>
|
||||
["text", "tool-searchMemories", "tool-addMemory"].includes(
|
||||
part.type,
|
||||
),
|
||||
)
|
||||
.map((part, index) => {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="prose prose-sm prose-invert max-w-none"
|
||||
>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{(part as any).text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
case "tool-searchMemories":
|
||||
switch (part.state) {
|
||||
case "input-available":
|
||||
case "input-streaming":
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<Spinner className="size-4" /> Searching
|
||||
memories...
|
||||
</div>
|
||||
);
|
||||
case "output-error":
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<X className="size-4" /> Error recalling
|
||||
memories
|
||||
</div>
|
||||
);
|
||||
case "output-available": {
|
||||
const output = (part as any).output;
|
||||
const foundCount =
|
||||
typeof output === "object" &&
|
||||
output !== null &&
|
||||
"count" in output
|
||||
? Number(output.count) || 0
|
||||
: 0;
|
||||
const ids = Array.isArray(output?.results)
|
||||
? ((output.results as any[])
|
||||
.map((r) => r?.documentId)
|
||||
.filter(Boolean) as string[])
|
||||
: [];
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<Check className="size-4" /> Found {foundCount}{" "}
|
||||
memories
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
case "tool-addMemory":
|
||||
switch (part.state) {
|
||||
case "input-available":
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<Spinner className="size-4" /> Adding memory...
|
||||
</div>
|
||||
);
|
||||
case "output-error":
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<X className="size-4" /> Error adding memory
|
||||
</div>
|
||||
);
|
||||
case "output-available":
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<Check className="size-4" /> Memory added
|
||||
</div>
|
||||
);
|
||||
case "input-streaming":
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<Spinner className="size-4" /> Adding memory...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
case 'tool-addMemory':
|
||||
switch (part.state) {
|
||||
case 'input-available':
|
||||
return (
|
||||
<div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
|
||||
<Spinner className="size-4" /> Adding memory...
|
||||
</div>
|
||||
)
|
||||
case 'output-error':
|
||||
return (
|
||||
<div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
|
||||
<X className="size-4" /> Error adding memory
|
||||
</div>
|
||||
)
|
||||
case 'output-available':
|
||||
return (
|
||||
<div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
|
||||
<Check className="size-4" /> Memory added
|
||||
</div>
|
||||
)
|
||||
case 'input-streaming':
|
||||
return (
|
||||
<div key={index} className="text-sm flex items-center gap-2 text-muted-foreground">
|
||||
<Spinner className="size-4" /> Adding memory...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
{message.role === "assistant" && (
|
||||
<div className="flex items-center gap-0.5 mt-0.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
message.parts
|
||||
.filter((p) => p.type === "text")
|
||||
?.map((p) => (p as any).text)
|
||||
.join("\n") ?? "",
|
||||
);
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<Copy className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-6 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => regenerate({ messageId: message.id })}
|
||||
>
|
||||
<RotateCcw className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{status === "submitted" && (
|
||||
<div className="flex text-muted-foreground justify-start gap-2 px-4 py-3 items-center w-full">
|
||||
<Spinner className="size-4" />
|
||||
<TextShimmer className="text-sm" duration={1.5}>
|
||||
Thinking...
|
||||
</TextShimmer>
|
||||
</div>
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
{message.role === 'assistant' && (
|
||||
<div className='flex items-center gap-0.5 mt-0.5'>
|
||||
<Button variant="ghost" size="icon" className='size-7 text-muted-foreground hover:text-foreground' onClick={() => {
|
||||
navigator.clipboard.writeText(message.parts.filter((p) => p.type === 'text')?.map((p) => (p as any).text).join('\n') ?? '')
|
||||
toast.success('Copied to clipboard')
|
||||
}}>
|
||||
<Copy className="size-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className='size-6 text-muted-foreground hover:text-foreground' onClick={() => regenerate({ messageId: message.id })}>
|
||||
<RotateCcw className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
enableAutoScroll();
|
||||
scrollToBottom("smooth");
|
||||
}}
|
||||
className={cn(
|
||||
"rounded-full w-fit mx-auto shadow-md z-10 absolute inset-x-0 bottom-4 flex justify-center",
|
||||
"transition-all duration-200 ease-out",
|
||||
isFarFromBottom
|
||||
? "opacity-100 scale-100 pointer-events-auto"
|
||||
: "opacity-0 scale-95 pointer-events-none",
|
||||
)}
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
Scroll to bottom
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{status === 'submitted' && (
|
||||
<div className="flex text-muted-foreground justify-start gap-2 px-4 py-3 items-center w-full">
|
||||
<Spinner className="size-4" />
|
||||
<TextShimmer className='text-sm' duration={1.5}>Thinking...</TextShimmer>
|
||||
</div>
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
enableAutoScroll()
|
||||
scrollToBottom('smooth')
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-full w-fit mx-auto shadow-md z-10 absolute inset-x-0 bottom-4 flex justify-center',
|
||||
'transition-all duration-200 ease-out',
|
||||
isFarFromBottom ? 'opacity-100 scale-100 pointer-events-auto' : 'opacity-0 scale-95 pointer-events-none',
|
||||
)}
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
Scroll to bottom
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form
|
||||
className="flex gap-2 px-4 pb-4 pt-1 relative"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
if (status === 'submitted') return
|
||||
if (status === 'streaming') {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
if (input.trim()) {
|
||||
enableAutoScroll()
|
||||
scrollToBottom('auto')
|
||||
sendMessage({ text: input })
|
||||
setInput('')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-0 left-0 -mt-7 w-full h-7 bg-gradient-to-t from-background to-transparent" />
|
||||
<Input className="w-full" value={input} onChange={(e) => setInput(e.target.value)} disabled={status === 'submitted'} placeholder="Say something..." />
|
||||
<Button type="submit" disabled={status === 'submitted'}>
|
||||
{status === 'ready' ? <ArrowUp className="size-4" /> : status === 'submitted' ? <Spinner className="size-4" /> : <X className="size-4" />}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
<form
|
||||
className="flex gap-2 px-4 pb-4 pt-1 relative"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (status === "submitted") return;
|
||||
if (status === "streaming") {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
if (input.trim()) {
|
||||
enableAutoScroll();
|
||||
scrollToBottom("auto");
|
||||
sendMessage({ text: input });
|
||||
setInput("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-0 left-0 -mt-7 w-full h-7 bg-gradient-to-t from-background to-transparent" />
|
||||
<Input
|
||||
className="w-full"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
disabled={status === "submitted"}
|
||||
placeholder="Say something..."
|
||||
/>
|
||||
<Button type="submit" disabled={status === "submitted"}>
|
||||
{status === "ready" ? (
|
||||
<ArrowUp className="size-4" />
|
||||
) : status === "submitted" ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<X className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,124 +1,156 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useChatOpen, useProject } from '@/stores';
|
||||
import { usePersistentChat } from '@/stores';
|
||||
import { Button } from '@ui/components/button';
|
||||
import { Plus, X, Trash2, HistoryIcon } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@ui/components/dialog";
|
||||
import { ScrollArea } from '@ui/components/scroll-area';
|
||||
import { cn } from '@lib/utils';
|
||||
import { ChatMessages } from './chat-messages';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { analytics } from '@/lib/analytics';
|
||||
import { cn } from "@lib/utils";
|
||||
import { Button } from "@ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@ui/components/dialog";
|
||||
import { ScrollArea } from "@ui/components/scroll-area";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { HistoryIcon, Plus, Trash2, X } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
import { useChatOpen, usePersistentChat, useProject } from "@/stores";
|
||||
import { ChatMessages } from "./chat-messages";
|
||||
|
||||
export function ChatRewrite() {
|
||||
const { setIsOpen } = useChatOpen();
|
||||
const { selectedProject } = useProject();
|
||||
const { conversations, currentChatId, setCurrentChatId, getCurrentChat } = usePersistentChat();
|
||||
const { setIsOpen } = useChatOpen();
|
||||
const { selectedProject } = useProject();
|
||||
const { conversations, currentChatId, setCurrentChatId, getCurrentChat } =
|
||||
usePersistentChat();
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
return [...conversations].sort((a, b) => (a.lastUpdated < b.lastUpdated ? 1 : -1));
|
||||
}, [conversations]);
|
||||
const sorted = useMemo(() => {
|
||||
return [...conversations].sort((a, b) =>
|
||||
a.lastUpdated < b.lastUpdated ? 1 : -1,
|
||||
);
|
||||
}, [conversations]);
|
||||
|
||||
function handleNewChat() {
|
||||
analytics.newChatStarted();
|
||||
const newId = crypto.randomUUID();
|
||||
setCurrentChatId(newId);
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
function handleNewChat() {
|
||||
analytics.newChatStarted();
|
||||
const newId = crypto.randomUUID();
|
||||
setCurrentChatId(newId);
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
|
||||
function formatRelativeTime(isoString: string): string {
|
||||
return formatDistanceToNow(new Date(isoString), { addSuffix: true });
|
||||
}
|
||||
function formatRelativeTime(isoString: string): string {
|
||||
return formatDistanceToNow(new Date(isoString), { addSuffix: true });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col h-full overflow-y-hidden border-l bg-background'>
|
||||
<div className="border-b px-4 py-3 flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold line-clamp-1 text-ellipsis overflow-hidden">{getCurrentChat()?.title ?? "New Chat"}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" onClick={() => analytics.chatHistoryViewed()}>
|
||||
<HistoryIcon className='size-4 text-muted-foreground' />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader className="pb-4 border-b rounded-t-lg">
|
||||
<DialogTitle className="">
|
||||
Conversations
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Project <span className="font-mono font-medium">{selectedProject}</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-y-hidden border-l bg-background">
|
||||
<div className="border-b px-4 py-3 flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold line-clamp-1 text-ellipsis overflow-hidden">
|
||||
{getCurrentChat()?.title ?? "New Chat"}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => analytics.chatHistoryViewed()}
|
||||
>
|
||||
<HistoryIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader className="pb-4 border-b rounded-t-lg">
|
||||
<DialogTitle className="">Conversations</DialogTitle>
|
||||
<DialogDescription>
|
||||
Project{" "}
|
||||
<span className="font-mono font-medium">
|
||||
{selectedProject}
|
||||
</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="max-h-96">
|
||||
<div className="flex flex-col gap-1">
|
||||
{sorted.map((c) => {
|
||||
const isActive = c.id === currentChatId;
|
||||
return (
|
||||
<div
|
||||
key={c.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
setCurrentChatId(c.id);
|
||||
setIsDialogOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
'flex items-center justify-between rounded-md px-3 py-2 outline-none',
|
||||
'transition-colors',
|
||||
isActive ? 'bg-primary/10' : 'hover:bg-muted'
|
||||
)}
|
||||
aria-current={isActive ? 'true' : undefined}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('text-sm font-medium truncate', isActive ? 'text-foreground' : undefined)}>
|
||||
{c.title || 'Untitled Chat'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Last updated {formatRelativeTime(c.lastUpdated)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
analytics.chatDeleted();
|
||||
}}
|
||||
aria-label="Delete conversation"
|
||||
>
|
||||
<Trash2 className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{sorted.length === 0 && (
|
||||
<div className="text-xs text-muted-foreground px-3 py-2">No conversations yet</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<Button variant="outline" size="lg" className="w-full border-dashed" onClick={handleNewChat}>
|
||||
<Plus className='size-4 mr-1' /> New Conversation
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button variant="outline" size="icon" onClick={handleNewChat}>
|
||||
<Plus className='size-4 text-muted-foreground' />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={() => setIsOpen(false)}>
|
||||
<X className='size-4 text-muted-foreground' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ChatMessages />
|
||||
</div >
|
||||
);
|
||||
}
|
||||
<ScrollArea className="max-h-96">
|
||||
<div className="flex flex-col gap-1">
|
||||
{sorted.map((c) => {
|
||||
const isActive = c.id === currentChatId;
|
||||
return (
|
||||
<div
|
||||
key={c.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
setCurrentChatId(c.id);
|
||||
setIsDialogOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded-md px-3 py-2 outline-none",
|
||||
"transition-colors",
|
||||
isActive ? "bg-primary/10" : "hover:bg-muted",
|
||||
)}
|
||||
aria-current={isActive ? "true" : undefined}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-medium truncate",
|
||||
isActive ? "text-foreground" : undefined,
|
||||
)}
|
||||
>
|
||||
{c.title || "Untitled Chat"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Last updated {formatRelativeTime(c.lastUpdated)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
analytics.chatDeleted();
|
||||
}}
|
||||
aria-label="Delete conversation"
|
||||
>
|
||||
<Trash2 className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{sorted.length === 0 && (
|
||||
<div className="text-xs text-muted-foreground px-3 py-2">
|
||||
No conversations yet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="w-full border-dashed"
|
||||
onClick={handleNewChat}
|
||||
>
|
||||
<Plus className="size-4 mr-1" /> New Conversation
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button variant="outline" size="icon" onClick={handleNewChat}>
|
||||
<Plus className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<X className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ChatMessages />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,342 +1,342 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { $fetch } from '@lib/api';
|
||||
import { $fetch } from "@lib/api";
|
||||
import {
|
||||
fetchConnectionsFeature,
|
||||
fetchConsumerProProduct,
|
||||
} from '@repo/lib/queries';
|
||||
import { Button } from '@repo/ui/components/button';
|
||||
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 { useProject } from '@/stores';
|
||||
import { analytics } from '@/lib/analytics';
|
||||
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";
|
||||
|
||||
// Define types
|
||||
type Connection = z.infer<typeof ConnectionResponseSchema>;
|
||||
|
||||
// Connector configurations
|
||||
const CONNECTORS = {
|
||||
'google-drive': {
|
||||
title: 'Google Drive',
|
||||
description: 'Connect your Google Docs, Sheets, and Slides',
|
||||
icon: GoogleDrive,
|
||||
},
|
||||
notion: {
|
||||
title: 'Notion',
|
||||
description: 'Import your Notion pages and databases',
|
||||
icon: Notion,
|
||||
},
|
||||
onedrive: {
|
||||
title: 'OneDrive',
|
||||
description: 'Access your Microsoft Office documents',
|
||||
icon: OneDrive,
|
||||
},
|
||||
"google-drive": {
|
||||
title: "Google Drive",
|
||||
description: "Connect your Google Docs, Sheets, and Slides",
|
||||
icon: GoogleDrive,
|
||||
},
|
||||
notion: {
|
||||
title: "Notion",
|
||||
description: "Import your Notion pages and databases",
|
||||
icon: Notion,
|
||||
},
|
||||
onedrive: {
|
||||
title: "OneDrive",
|
||||
description: "Access your Microsoft Office documents",
|
||||
icon: OneDrive,
|
||||
},
|
||||
} as const;
|
||||
|
||||
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 handleUpgrade = async () => {
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: 'consumer_pro',
|
||||
successUrl: 'https://app.supermemory.ai/',
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
const handleUpgrade = async () => {
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any);
|
||||
const connectionsUsed = connectionsCheck?.balance ?? 0;
|
||||
const connectionsLimit = connectionsCheck?.included_usage ?? 0;
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any);
|
||||
const connectionsUsed = connectionsCheck?.balance ?? 0;
|
||||
const connectionsLimit = connectionsCheck?.included_usage ?? 0;
|
||||
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn as any);
|
||||
const isProUser = proCheck?.allowed ?? false;
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn as any);
|
||||
const isProUser = proCheck?.allowed ?? false;
|
||||
|
||||
const canAddConnection = connectionsUsed < connectionsLimit;
|
||||
const canAddConnection = connectionsUsed < connectionsLimit;
|
||||
|
||||
// Fetch connections
|
||||
const {
|
||||
data: connections = [],
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['connections'],
|
||||
queryFn: async () => {
|
||||
const response = await $fetch('@post/connections/list', {
|
||||
body: {
|
||||
containerTags: [],
|
||||
},
|
||||
});
|
||||
// Fetch connections
|
||||
const {
|
||||
data: connections = [],
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ["connections"],
|
||||
queryFn: async () => {
|
||||
const response = await $fetch("@post/connections/list", {
|
||||
body: {
|
||||
containerTags: [],
|
||||
},
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(
|
||||
response.error?.message || 'Failed to load connections'
|
||||
);
|
||||
}
|
||||
if (response.error) {
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to load connections",
|
||||
);
|
||||
}
|
||||
|
||||
return response.data as Connection[];
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
refetchInterval: 60 * 1000,
|
||||
});
|
||||
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]);
|
||||
// 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]);
|
||||
|
||||
// Add connection mutation
|
||||
const addConnectionMutation = useMutation({
|
||||
mutationFn: async (provider: ConnectorProvider) => {
|
||||
// Check if user can add connections
|
||||
if (!canAddConnection && !isProUser) {
|
||||
throw new Error(
|
||||
"Free plan doesn't include connections. Upgrade to Pro for unlimited connections."
|
||||
);
|
||||
}
|
||||
// Add connection mutation
|
||||
const addConnectionMutation = useMutation({
|
||||
mutationFn: async (provider: ConnectorProvider) => {
|
||||
// Check if user can add connections
|
||||
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', {
|
||||
params: { provider },
|
||||
body: {
|
||||
redirectUrl: window.location.href,
|
||||
containerTags: [selectedProject],
|
||||
},
|
||||
});
|
||||
const response = await $fetch("@post/connections/:provider", {
|
||||
params: { provider },
|
||||
body: {
|
||||
redirectUrl: window.location.href,
|
||||
containerTags: [selectedProject],
|
||||
},
|
||||
});
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: its fine
|
||||
if ('data' in response && !('error' in response.data!)) {
|
||||
return response.data;
|
||||
}
|
||||
// biome-ignore lint/style/noNonNullAssertion: its fine
|
||||
if ("data" in response && !("error" in response.data!)) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
throw new Error(response.error?.message || 'Failed to connect');
|
||||
},
|
||||
onSuccess: (data, provider) => {
|
||||
analytics.connectionAdded(provider);
|
||||
analytics.connectionAuthStarted();
|
||||
if (data?.authLink) {
|
||||
window.location.href = data.authLink;
|
||||
}
|
||||
},
|
||||
onError: (error, provider) => {
|
||||
analytics.connectionAuthFailed();
|
||||
toast.error(`Failed to connect ${provider}`, {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
},
|
||||
});
|
||||
throw new Error(response.error?.message || "Failed to connect");
|
||||
},
|
||||
onSuccess: (data, provider) => {
|
||||
analytics.connectionAdded(provider);
|
||||
analytics.connectionAuthStarted();
|
||||
if (data?.authLink) {
|
||||
window.location.href = data.authLink;
|
||||
}
|
||||
},
|
||||
onError: (error, provider) => {
|
||||
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}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
analytics.connectionDeleted();
|
||||
toast.success(
|
||||
'Connection removal has started. supermemory will permanently delete the documents in the next few minutes.'
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ['connections'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to remove connection', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
},
|
||||
});
|
||||
// Delete connection mutation
|
||||
const deleteConnectionMutation = useMutation({
|
||||
mutationFn: async (connectionId: string) => {
|
||||
await $fetch(`@delete/connections/${connectionId}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
analytics.connectionDeleted();
|
||||
toast.success(
|
||||
"Connection removal has started. supermemory will permanently delete the documents in the next few minutes.",
|
||||
);
|
||||
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];
|
||||
if (connector) {
|
||||
const Icon = connector.icon;
|
||||
return <Icon className="h-10 w-10" />;
|
||||
}
|
||||
return <span className="text-2xl">📎</span>;
|
||||
};
|
||||
const getProviderIcon = (provider: string) => {
|
||||
const connector = CONNECTORS[provider as ConnectorProvider];
|
||||
if (connector) {
|
||||
const Icon = connector.icon;
|
||||
return <Icon className="h-10 w-10" />;
|
||||
}
|
||||
return <span className="text-2xl">📎</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-white/70">
|
||||
Connect your favorite services to import documents
|
||||
</p>
|
||||
{!isProUser && (
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
Connections require a Pro subscription
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-white/70">
|
||||
Connect your favorite services to import documents
|
||||
</p>
|
||||
{!isProUser && (
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
Connections require a Pro subscription
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Show upgrade prompt for free users */}
|
||||
{!isProUser && (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
>
|
||||
<p className="text-sm text-yellow-400 mb-2">
|
||||
🔌 Connections are a Pro feature
|
||||
</p>
|
||||
<p className="text-xs text-white/60 mb-3">
|
||||
Connect Google Drive, Notion, OneDrive and more to automatically
|
||||
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"
|
||||
variant="secondary"
|
||||
>
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
{/* Show upgrade prompt for free users */}
|
||||
{!isProUser && (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
>
|
||||
<p className="text-sm text-yellow-400 mb-2">
|
||||
🔌 Connections are a Pro feature
|
||||
</p>
|
||||
<p className="text-xs text-white/60 mb-3">
|
||||
Connect Google Drive, Notion, OneDrive and more to automatically
|
||||
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"
|
||||
variant="secondary"
|
||||
>
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[...Array(2)].map((_, i) => (
|
||||
<motion.div
|
||||
animate={{ opacity: 1 }}
|
||||
className="p-4 bg-white/5 rounded-lg"
|
||||
initial={{ opacity: 0 }}
|
||||
key={`skeleton-${Date.now()}-${i}`}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
>
|
||||
<Skeleton className="h-12 w-full bg-white/10" />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
) : connections.length === 0 ? (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-center py-4"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ type: 'spring', damping: 20 }}
|
||||
>
|
||||
<p className="text-white/50 mb-2">No connections yet</p>
|
||||
<p className="text-xs text-white/40">
|
||||
Choose a service below to connect
|
||||
</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div className="space-y-2">
|
||||
<AnimatePresence>
|
||||
{connections.map((connection, index) => (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="flex items-center justify-between p-3 bg-white/5 rounded-lg hover:bg-white/10 transition-colors"
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
key={connection.id}
|
||||
layout
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<motion.div
|
||||
animate={{ rotate: 0, opacity: 1 }}
|
||||
initial={{ rotate: -180, opacity: 0 }}
|
||||
transition={{ delay: index * 0.05 + 0.2 }}
|
||||
>
|
||||
{getProviderIcon(connection.provider)}
|
||||
</motion.div>
|
||||
<div>
|
||||
<p className="font-medium text-white capitalize">
|
||||
{connection.provider.replace('-', ' ')}
|
||||
</p>
|
||||
{connection.email && (
|
||||
<p className="text-sm text-white/60">
|
||||
{connection.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
<Button
|
||||
className="text-white/50 hover:text-red-400"
|
||||
disabled={deleteConnectionMutation.isPending}
|
||||
onClick={() =>
|
||||
deleteConnectionMutation.mutate(connection.id)
|
||||
}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[...Array(2)].map((_, i) => (
|
||||
<motion.div
|
||||
animate={{ opacity: 1 }}
|
||||
className="p-4 bg-white/5 rounded-lg"
|
||||
initial={{ opacity: 0 }}
|
||||
key={`skeleton-${Date.now()}-${i}`}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
>
|
||||
<Skeleton className="h-12 w-full bg-white/10" />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
) : connections.length === 0 ? (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-center py-4"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ type: "spring", damping: 20 }}
|
||||
>
|
||||
<p className="text-white/50 mb-2">No connections yet</p>
|
||||
<p className="text-xs text-white/40">
|
||||
Choose a service below to connect
|
||||
</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div className="space-y-2">
|
||||
<AnimatePresence>
|
||||
{connections.map((connection, index) => (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="flex items-center justify-between p-3 bg-white/5 rounded-lg hover:bg-white/10 transition-colors"
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
key={connection.id}
|
||||
layout
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<motion.div
|
||||
animate={{ rotate: 0, opacity: 1 }}
|
||||
initial={{ rotate: -180, opacity: 0 }}
|
||||
transition={{ delay: index * 0.05 + 0.2 }}
|
||||
>
|
||||
{getProviderIcon(connection.provider)}
|
||||
</motion.div>
|
||||
<div>
|
||||
<p className="font-medium text-white capitalize">
|
||||
{connection.provider.replace("-", " ")}
|
||||
</p>
|
||||
{connection.email && (
|
||||
<p className="text-sm text-white/60">
|
||||
{connection.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
<Button
|
||||
className="text-white/50 hover:text-red-400"
|
||||
disabled={deleteConnectionMutation.isPending}
|
||||
onClick={() =>
|
||||
deleteConnectionMutation.mutate(connection.id)
|
||||
}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Available Connections Section */}
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium text-white mb-4">
|
||||
Available Connections
|
||||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{Object.entries(CONNECTORS).map(([provider, config], index) => {
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
key={provider}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Button
|
||||
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);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<Icon className="h-8 w-8 mr-3" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">{config.title}</div>
|
||||
<div className="text-sm text-white/60 mt-0.5">
|
||||
{config.description}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{/* Available Connections Section */}
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium text-white mb-4">
|
||||
Available Connections
|
||||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{Object.entries(CONNECTORS).map(([provider, config], index) => {
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
key={provider}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Button
|
||||
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);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<Icon className="h-8 w-8 mr-3" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">{config.title}</div>
|
||||
<div className="text-sm text-white/60 mt-0.5">
|
||||
{config.description}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { $fetch } from "@lib/api"
|
||||
import { authClient } from "@lib/auth"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { useForm } from "@tanstack/react-form"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { $fetch } from "@lib/api";
|
||||
import { authClient } from "@lib/auth";
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Button } from "@ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -11,18 +11,18 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@ui/components/dialog"
|
||||
import { Input } from "@ui/components/input"
|
||||
import { CopyableCell } from "@ui/copyable-cell"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import Image from "next/image"
|
||||
import { generateSlug } from "random-word-slugs"
|
||||
import { useState, useEffect } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod/v4"
|
||||
import { InstallationDialogContent } from "./installation-dialog-content"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
} from "@ui/components/dialog";
|
||||
import { Input } from "@ui/components/input";
|
||||
import { CopyableCell } from "@ui/copyable-cell";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import { generateSlug } from "random-word-slugs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod/v4";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
import { InstallationDialogContent } from "./installation-dialog-content";
|
||||
|
||||
// Validation schemas
|
||||
const mcpMigrationSchema = z.object({
|
||||
|
|
@ -33,56 +33,56 @@ const mcpMigrationSchema = z.object({
|
|||
/^https:\/\/mcp\.supermemory\.ai\/[^/]+\/sse$/,
|
||||
"Link must be in format: https://mcp.supermemory.ai/userId/sse",
|
||||
),
|
||||
})
|
||||
});
|
||||
|
||||
export function MCPView() {
|
||||
const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false)
|
||||
const projectId = localStorage.getItem("selectedProject") ?? "default"
|
||||
const { org } = useAuth()
|
||||
const [apiKey, setApiKey] = useState<string>()
|
||||
const [isInstallDialogOpen, setIsInstallDialogOpen] = useState(false)
|
||||
const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false);
|
||||
const projectId = localStorage.getItem("selectedProject") ?? "default";
|
||||
const { org } = useAuth();
|
||||
const [apiKey, setApiKey] = useState<string>();
|
||||
const [isInstallDialogOpen, setIsInstallDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
analytics.mcpViewOpened()
|
||||
}, [])
|
||||
analytics.mcpViewOpened();
|
||||
}, []);
|
||||
const apiKeyMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (apiKey) return apiKey
|
||||
if (apiKey) return apiKey;
|
||||
const res = await authClient.apiKey.create({
|
||||
metadata: {
|
||||
organizationId: org?.id,
|
||||
},
|
||||
name: generateSlug(),
|
||||
prefix: `sm_${org?.id}_`,
|
||||
})
|
||||
return res.key
|
||||
});
|
||||
return res.key;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setApiKey(data)
|
||||
setIsInstallDialogOpen(true)
|
||||
setApiKey(data);
|
||||
setIsInstallDialogOpen(true);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Form for MCP migration
|
||||
const mcpMigrationForm = useForm({
|
||||
defaultValues: { url: "" },
|
||||
onSubmit: async ({ value, formApi }) => {
|
||||
const userId = extractUserIdFromMCPUrl(value.url)
|
||||
const userId = extractUserIdFromMCPUrl(value.url);
|
||||
if (userId) {
|
||||
migrateMCPMutation.mutate({ userId, projectId })
|
||||
formApi.reset()
|
||||
migrateMCPMutation.mutate({ userId, projectId });
|
||||
formApi.reset();
|
||||
}
|
||||
},
|
||||
validators: {
|
||||
onChange: mcpMigrationSchema,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const extractUserIdFromMCPUrl = (url: string): string | null => {
|
||||
const regex = /^https:\/\/mcp\.supermemory\.ai\/([^/]+)\/sse$/
|
||||
const match = url.trim().match(regex)
|
||||
return match?.[1] || null
|
||||
}
|
||||
const regex = /^https:\/\/mcp\.supermemory\.ai\/([^/]+)\/sse$/;
|
||||
const match = url.trim().match(regex);
|
||||
return match?.[1] || null;
|
||||
};
|
||||
|
||||
// Migrate MCP mutation
|
||||
const migrateMCPMutation = useMutation({
|
||||
|
|
@ -90,33 +90,33 @@ export function MCPView() {
|
|||
userId,
|
||||
projectId,
|
||||
}: {
|
||||
userId: string
|
||||
projectId: string
|
||||
userId: string;
|
||||
projectId: string;
|
||||
}) => {
|
||||
const response = await $fetch("@post/memories/migrate-mcp", {
|
||||
body: { userId, projectId },
|
||||
})
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to migrate documents",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response.data
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast.success("Migration completed!", {
|
||||
description: `Successfully migrated ${data?.migratedCount} documents`,
|
||||
})
|
||||
setIsMigrateDialogOpen(false)
|
||||
});
|
||||
setIsMigrateDialogOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Migration failed", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
|
@ -155,9 +155,9 @@ export function MCPView() {
|
|||
<Button
|
||||
disabled={apiKeyMutation.isPending}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
apiKeyMutation.mutate()
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
apiKeyMutation.mutate();
|
||||
}}
|
||||
>
|
||||
Install Now
|
||||
|
|
@ -213,9 +213,9 @@ export function MCPView() {
|
|||
</DialogHeader>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
mcpMigrationForm.handleSubmit()
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
mcpMigrationForm.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
|
|
@ -268,8 +268,8 @@ export function MCPView() {
|
|||
<Button
|
||||
className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
|
||||
onClick={() => {
|
||||
setIsMigrateDialogOpen(false)
|
||||
mcpMigrationForm.reset()
|
||||
setIsMigrateDialogOpen(false);
|
||||
mcpMigrationForm.reset();
|
||||
}}
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
@ -307,5 +307,5 @@ export function MCPView() {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { Button } from "@ui/components/button"
|
||||
import { Button } from "@ui/components/button";
|
||||
import {
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@ui/components/dialog"
|
||||
import { Input } from "@ui/components/input"
|
||||
} from "@ui/components/dialog";
|
||||
import { Input } from "@ui/components/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/select"
|
||||
import { CopyIcon } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
} from "@ui/components/select";
|
||||
import { CopyIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
|
||||
const clients = {
|
||||
cursor: "Cursor",
|
||||
|
|
@ -28,10 +28,10 @@ const clients = {
|
|||
enconvo: "Enconvo",
|
||||
"gemini-cli": "Gemini CLI",
|
||||
"claude-code": "Claude Code",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
export function InstallationDialogContent() {
|
||||
const [client, setClient] = useState<keyof typeof clients>("cursor")
|
||||
const [client, setClient] = useState<keyof typeof clients>("cursor");
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
|
|
@ -67,13 +67,13 @@ export function InstallationDialogContent() {
|
|||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${client} --oauth=yes`,
|
||||
)
|
||||
analytics.mcpInstallCmdCopied()
|
||||
toast.success("Copied to clipboard!")
|
||||
);
|
||||
analytics.mcpInstallCmdCopied();
|
||||
toast.success("Copied to clipboard!");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="size-4" /> Copy Installation Command
|
||||
</Button>
|
||||
</DialogContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,43 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { authClient } from "@lib/auth"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { authClient } from "@lib/auth";
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
import {
|
||||
fetchConnectionsFeature,
|
||||
fetchMemoriesFeature,
|
||||
fetchSubscriptionStatus,
|
||||
} from "@lib/queries"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { CreditCard, LoaderIcon, LogOut, User, CheckCircle, X } from "lucide-react"
|
||||
import { motion } from "motion/react"
|
||||
import Link from "next/link"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
} from "@lib/queries";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
import {
|
||||
CheckCircle,
|
||||
CreditCard,
|
||||
LoaderIcon,
|
||||
LogOut,
|
||||
User,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
|
||||
export function ProfileView() {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { user: session, org } = useAuth()
|
||||
const organizations = org
|
||||
const autumn = useCustomer()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { user: session, org } = useAuth();
|
||||
const organizations = org;
|
||||
const autumn = useCustomer();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any)
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any);
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0;
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0;
|
||||
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any)
|
||||
const connectionsUsed = connectionsCheck?.usage ?? 0
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any);
|
||||
const connectionsUsed = connectionsCheck?.usage ?? 0;
|
||||
|
||||
// Fetch subscription status with React Query
|
||||
const {
|
||||
|
|
@ -38,36 +45,36 @@ export function ProfileView() {
|
|||
consumer_pro: null,
|
||||
},
|
||||
isLoading: isCheckingStatus,
|
||||
} = fetchSubscriptionStatus(autumn as any)
|
||||
} = fetchSubscriptionStatus(autumn as any);
|
||||
|
||||
const isPro = status.consumer_pro
|
||||
const isPro = status.consumer_pro;
|
||||
|
||||
const handleLogout = () => {
|
||||
analytics.userSignedOut()
|
||||
authClient.signOut()
|
||||
router.push("/login")
|
||||
}
|
||||
analytics.userSignedOut();
|
||||
authClient.signOut();
|
||||
router.push("/login");
|
||||
};
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
setIsLoading(true)
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
})
|
||||
window.location.reload()
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
setIsLoading(false)
|
||||
console.error(error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle manage billing
|
||||
const handleManageBilling = async () => {
|
||||
await autumn.openBillingPortal({
|
||||
returnUrl: "https://app.supermemory.ai",
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (session?.isAnonymous) {
|
||||
return (
|
||||
|
|
@ -78,7 +85,9 @@ export function ProfileView() {
|
|||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ type: "spring", damping: 20 }}
|
||||
>
|
||||
<p className="text-white/70 mb-4">Sign in to access your profile and billing</p>
|
||||
<p className="text-white/70 mb-4">
|
||||
Sign in to access your profile and billing
|
||||
</p>
|
||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
asChild
|
||||
|
|
@ -90,7 +99,7 @@ export function ProfileView() {
|
|||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -133,14 +142,20 @@ export function ProfileView() {
|
|||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-white/70">Memories</span>
|
||||
<span className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-400" : "text-white/90"}`}>
|
||||
<span
|
||||
className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-400" : "text-white/90"}`}
|
||||
>
|
||||
{memoriesUsed} / {memoriesLimit}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-white/10 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
memoriesUsed >= memoriesLimit ? "bg-red-500" : isPro ? "bg-green-500" : "bg-blue-500"
|
||||
memoriesUsed >= memoriesLimit
|
||||
? "bg-red-500"
|
||||
: isPro
|
||||
? "bg-green-500"
|
||||
: "bg-blue-500"
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
|
||||
|
|
@ -196,12 +211,16 @@ export function ProfileView() {
|
|||
{/* Plan Comparison - Only show for free users */}
|
||||
{!isPro && (
|
||||
<div className="bg-white/5 rounded-lg p-4 space-y-4">
|
||||
<HeadingH3Bold className="text-white text-sm">Upgrade to Pro</HeadingH3Bold>
|
||||
<HeadingH3Bold className="text-white text-sm">
|
||||
Upgrade to Pro
|
||||
</HeadingH3Bold>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Free Plan */}
|
||||
<div className="p-3 bg-white/5 rounded-lg border border-white/10">
|
||||
<h4 className="font-medium text-white/90 mb-3 text-sm">Free Plan</h4>
|
||||
<h4 className="font-medium text-white/90 mb-3 text-sm">
|
||||
Free Plan
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-center gap-2 text-sm text-white/70">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
|
|
@ -248,7 +267,8 @@ export function ProfileView() {
|
|||
</div>
|
||||
|
||||
<p className="text-xs text-white/50 text-center">
|
||||
$15/month (only for first 100 users) • Cancel anytime. No questions asked.
|
||||
$15/month (only for first 100 users) • Cancel anytime. No questions
|
||||
asked.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -262,5 +282,5 @@ export function ProfileView() {
|
|||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { $fetch } from "@lib/api"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { $fetch } from "@lib/api";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -10,57 +10,57 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@repo/ui/components/dialog"
|
||||
} from "@repo/ui/components/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/ui/components/dropdown-menu"
|
||||
import { Input } from "@repo/ui/components/input"
|
||||
import { Label } from "@repo/ui/components/label"
|
||||
} from "@repo/ui/components/dropdown-menu";
|
||||
import { Input } from "@repo/ui/components/input";
|
||||
import { Label } from "@repo/ui/components/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@repo/ui/components/select"
|
||||
import { Skeleton } from "@repo/ui/components/skeleton"
|
||||
} from "@repo/ui/components/select";
|
||||
import { Skeleton } from "@repo/ui/components/skeleton";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { FolderIcon, Loader2, MoreVertical, Plus, Trash2 } from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import { FolderIcon, Loader2, MoreVertical, Plus, Trash2 } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
import { useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { useProject } from "@/stores"
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useProject } from "@/stores";
|
||||
|
||||
// Projects View Component
|
||||
export function ProjectsView() {
|
||||
const queryClient = useQueryClient()
|
||||
const { selectedProject, setSelectedProject } = useProject()
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const [projectName, setProjectName] = useState("")
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedProject, setSelectedProject } = useProject();
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [deleteDialog, setDeleteDialog] = useState<{
|
||||
open: boolean
|
||||
project: null | { id: string; name: string; containerTag: string }
|
||||
action: "move" | "delete"
|
||||
targetProjectId: string
|
||||
open: boolean;
|
||||
project: null | { id: string; name: string; containerTag: string };
|
||||
action: "move" | "delete";
|
||||
targetProjectId: string;
|
||||
}>({
|
||||
open: false,
|
||||
project: null,
|
||||
action: "move",
|
||||
targetProjectId: "",
|
||||
})
|
||||
});
|
||||
const [expDialog, setExpDialog] = useState<{
|
||||
open: boolean
|
||||
projectId: string
|
||||
open: boolean;
|
||||
projectId: string;
|
||||
}>({
|
||||
open: false,
|
||||
projectId: "",
|
||||
})
|
||||
});
|
||||
|
||||
// Fetch projects
|
||||
const {
|
||||
|
|
@ -70,42 +70,42 @@ export function ProjectsView() {
|
|||
} = 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: () => {
|
||||
toast.success("Project created successfully!")
|
||||
setShowCreateDialog(false)
|
||||
setProjectName("")
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] })
|
||||
toast.success("Project created successfully!");
|
||||
setShowCreateDialog(false);
|
||||
setProjectName("");
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to create project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Delete project mutation
|
||||
const deleteProjectMutation = useMutation({
|
||||
|
|
@ -114,68 +114,72 @@ export function ProjectsView() {
|
|||
action,
|
||||
targetProjectId,
|
||||
}: {
|
||||
projectId: string
|
||||
action: "move" | "delete"
|
||||
targetProjectId?: string
|
||||
projectId: string;
|
||||
action: "move" | "delete";
|
||||
targetProjectId?: string;
|
||||
}) => {
|
||||
const response = await $fetch(`@delete/projects/${projectId}`, {
|
||||
body: { action, targetProjectId },
|
||||
})
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to delete project")
|
||||
throw new Error(response.error?.message || "Failed to delete project");
|
||||
}
|
||||
|
||||
return response.data
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Project deleted successfully")
|
||||
toast.success("Project deleted successfully");
|
||||
setDeleteDialog({
|
||||
open: false,
|
||||
project: null,
|
||||
action: "move",
|
||||
targetProjectId: "",
|
||||
})
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] })
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
|
||||
// If we deleted the selected project, switch to default
|
||||
if (deleteDialog.project?.containerTag === selectedProject) {
|
||||
setSelectedProject("sm_project_default")
|
||||
setSelectedProject("sm_project_default");
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to delete project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Enable experimental mode mutation
|
||||
const enableExperimentalMutation = useMutation({
|
||||
mutationFn: async (projectId: string) => {
|
||||
const response = await $fetch(`@post/projects/${projectId}/enable-experimental`)
|
||||
const response = await $fetch(
|
||||
`@post/projects/${projectId}/enable-experimental`,
|
||||
);
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to enable experimental mode")
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to enable experimental mode",
|
||||
);
|
||||
}
|
||||
return response.data
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Experimental mode enabled for project")
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] })
|
||||
setExpDialog({ open: false, projectId: "" })
|
||||
toast.success("Experimental mode enabled for project");
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
setExpDialog({ open: false, projectId: "" });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to enable experimental mode", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Handle project selection
|
||||
const handleProjectSelect = (containerTag: string) => {
|
||||
setSelectedProject(containerTag)
|
||||
toast.success("Project switched successfully")
|
||||
}
|
||||
setSelectedProject(containerTag);
|
||||
toast.success("Project switched successfully");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -238,10 +242,11 @@ export function ProjectsView() {
|
|||
{/* Default project */}
|
||||
<motion.div
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className={`flex items-center justify-between p-3 rounded-lg transition-colors cursor-pointer ${selectedProject === "sm_project_default"
|
||||
className={`flex items-center justify-between p-3 rounded-lg transition-colors cursor-pointer ${
|
||||
selectedProject === "sm_project_default"
|
||||
? "bg-white/20 border border-white/30"
|
||||
: "bg-white/5 hover:bg-white/10"
|
||||
}`}
|
||||
}`}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
key="default-project"
|
||||
|
|
@ -280,10 +285,11 @@ export function ProjectsView() {
|
|||
.map((project, index) => (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className={`flex items-center justify-between p-3 rounded-lg transition-colors cursor-pointer ${selectedProject === project.containerTag
|
||||
className={`flex items-center justify-between p-3 rounded-lg transition-colors cursor-pointer ${
|
||||
selectedProject === project.containerTag
|
||||
? "bg-white/20 border border-white/30"
|
||||
: "bg-white/5 hover:bg-white/10"
|
||||
}`}
|
||||
}`}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
key={project.id}
|
||||
|
|
@ -338,11 +344,11 @@ export function ProjectsView() {
|
|||
<DropdownMenuItem
|
||||
className="text-blue-400 hover:text-blue-300 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.stopPropagation();
|
||||
setExpDialog({
|
||||
open: true,
|
||||
projectId: project.id,
|
||||
})
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="h-4 w-4 mr-2 rounded border border-blue-400" />
|
||||
|
|
@ -361,7 +367,7 @@ export function ProjectsView() {
|
|||
<DropdownMenuItem
|
||||
className="text-red-400 hover:text-red-300 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.stopPropagation();
|
||||
setDeleteDialog({
|
||||
open: true,
|
||||
project: {
|
||||
|
|
@ -371,7 +377,7 @@ export function ProjectsView() {
|
|||
},
|
||||
action: "move",
|
||||
targetProjectId: "",
|
||||
})
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
|
|
@ -430,8 +436,8 @@ export function ProjectsView() {
|
|||
<Button
|
||||
className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
|
||||
onClick={() => {
|
||||
setShowCreateDialog(false)
|
||||
setProjectName("")
|
||||
setShowCreateDialog(false);
|
||||
setProjectName("");
|
||||
}}
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
@ -617,10 +623,11 @@ export function ProjectsView() {
|
|||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
className={`${deleteDialog.action === "delete"
|
||||
className={`${
|
||||
deleteDialog.action === "delete"
|
||||
? "bg-red-600 hover:bg-red-700"
|
||||
: "bg-white/10 hover:bg-white/20"
|
||||
} text-white border-white/20`}
|
||||
} text-white border-white/20`}
|
||||
disabled={
|
||||
deleteProjectMutation.isPending ||
|
||||
(deleteDialog.action === "move" &&
|
||||
|
|
@ -635,7 +642,7 @@ export function ProjectsView() {
|
|||
deleteDialog.action === "move"
|
||||
? deleteDialog.targetProjectId
|
||||
: undefined,
|
||||
})
|
||||
});
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
|
|
@ -738,5 +745,5 @@ export function ProjectsView() {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@import 'tailwindcss';
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
|
|
|||
|
|
@ -1,41 +1,41 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { $fetch } from "@lib/api"
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { useProject } from "@/stores"
|
||||
import { $fetch } from "@lib/api";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { useProject } from "@/stores";
|
||||
|
||||
export function useProjectMutations() {
|
||||
const queryClient = useQueryClient()
|
||||
const { selectedProject, setSelectedProject } = useProject()
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedProject, setSelectedProject } = useProject();
|
||||
|
||||
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) => {
|
||||
toast.success("Project created successfully!")
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] })
|
||||
toast.success("Project created successfully!");
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
|
||||
// Automatically switch to the newly created project
|
||||
if (data?.containerTag) {
|
||||
setSelectedProject(data.containerTag)
|
||||
setSelectedProject(data.containerTag);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to create project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const deleteProjectMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
|
|
@ -43,47 +43,47 @@ export function useProjectMutations() {
|
|||
action,
|
||||
targetProjectId,
|
||||
}: {
|
||||
projectId: string
|
||||
action: "move" | "delete"
|
||||
targetProjectId?: string
|
||||
projectId: string;
|
||||
action: "move" | "delete";
|
||||
targetProjectId?: string;
|
||||
}) => {
|
||||
const response = await $fetch(`@delete/projects/${projectId}`, {
|
||||
body: { action, targetProjectId },
|
||||
})
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to delete project")
|
||||
throw new Error(response.error?.message || "Failed to delete project");
|
||||
}
|
||||
|
||||
return response.data
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
toast.success("Project deleted successfully")
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] })
|
||||
toast.success("Project deleted successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
|
||||
// If we deleted the selected project, switch to default
|
||||
const deletedProject = queryClient.getQueryData<any[]>(["projects"])?.find(
|
||||
p => p.id === variables.projectId
|
||||
)
|
||||
const deletedProject = queryClient
|
||||
.getQueryData<any[]>(["projects"])
|
||||
?.find((p) => p.id === variables.projectId);
|
||||
if (deletedProject?.containerTag === selectedProject) {
|
||||
setSelectedProject("sm_project_default")
|
||||
setSelectedProject("sm_project_default");
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to delete project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const switchProject = (containerTag: string) => {
|
||||
setSelectedProject(containerTag)
|
||||
toast.success("Project switched successfully")
|
||||
}
|
||||
setSelectedProject(containerTag);
|
||||
toast.success("Project switched successfully");
|
||||
};
|
||||
|
||||
return {
|
||||
createProjectMutation,
|
||||
deleteProjectMutation,
|
||||
switchProject,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import { useMemo } from "react"
|
||||
import { useProject } from "@/stores"
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { useProject } from "@/stores";
|
||||
|
||||
/**
|
||||
* Returns the display name of the currently selected project.
|
||||
|
|
@ -10,17 +10,17 @@ import { useProject } from "@/stores"
|
|||
* hasn’t been fetched yet.
|
||||
*/
|
||||
export function useProjectName() {
|
||||
const { selectedProject } = useProject()
|
||||
const queryClient = useQueryClient()
|
||||
const { selectedProject } = useProject();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// This query is populated by ProjectsView – we just read from the cache.
|
||||
const projects = queryClient.getQueryData(["projects"]) as
|
||||
| Array<{ name: string; containerTag: string }>
|
||||
| undefined
|
||||
| undefined;
|
||||
|
||||
return useMemo(() => {
|
||||
if (selectedProject === "sm_project_default") return "Default Project"
|
||||
const found = projects?.find((p) => p.containerTag === selectedProject)
|
||||
return found?.name ?? selectedProject
|
||||
}, [projects, selectedProject])
|
||||
if (selectedProject === "sm_project_default") return "Default Project";
|
||||
const found = projects?.find((p) => p.containerTag === selectedProject);
|
||||
return found?.name ?? selectedProject;
|
||||
}, [projects, selectedProject]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function useResizeObserver<T extends HTMLElement>(
|
||||
ref: React.RefObject<T | null>
|
||||
ref: React.RefObject<T | null>,
|
||||
) {
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const observer = new ResizeObserver(([entry]) => {
|
||||
setSize({
|
||||
width: entry?.contentRect.width ?? 0,
|
||||
height: entry?.contentRect.height ?? 0,
|
||||
});
|
||||
});
|
||||
const observer = new ResizeObserver(([entry]) => {
|
||||
setSize({
|
||||
width: entry?.contentRect.width ?? 0,
|
||||
height: entry?.contentRect.height ?? 0,
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}, [ref]);
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}, [ref]);
|
||||
|
||||
return size;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// The added config here will be used whenever a users loads a page in their browser.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
_experiments: {
|
||||
|
|
@ -19,6 +19,6 @@ Sentry.init({
|
|||
|
||||
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||
tracesSampleRate: 1,
|
||||
})
|
||||
});
|
||||
|
||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart
|
||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import posthog from "posthog-js"
|
||||
import posthog from "posthog-js";
|
||||
|
||||
export const analytics = {
|
||||
userSignedOut: () => posthog.capture("user_signed_out"),
|
||||
|
|
@ -7,13 +7,13 @@ export const analytics = {
|
|||
tourSkipped: () => posthog.capture("tour_skipped"),
|
||||
|
||||
memoryAdded: (props: {
|
||||
type: "note" | "link" | "file"
|
||||
project_id?: string
|
||||
content_length?: number
|
||||
file_size?: number
|
||||
file_type?: string
|
||||
type: "note" | "link" | "file";
|
||||
project_id?: string;
|
||||
content_length?: number;
|
||||
file_size?: number;
|
||||
file_type?: string;
|
||||
}) => posthog.capture("memory_added", props),
|
||||
|
||||
|
||||
memoryDetailOpened: () => posthog.capture("memory_detail_opened"),
|
||||
|
||||
projectCreated: () => posthog.capture("project_created"),
|
||||
|
|
@ -22,9 +22,9 @@ export const analytics = {
|
|||
chatHistoryViewed: () => posthog.capture("chat_history_viewed"),
|
||||
chatDeleted: () => posthog.capture("chat_deleted"),
|
||||
|
||||
viewModeChanged: (mode: "graph" | "list") =>
|
||||
viewModeChanged: (mode: "graph" | "list") =>
|
||||
posthog.capture("view_mode_changed", { mode }),
|
||||
|
||||
|
||||
documentCardClicked: () => posthog.capture("document_card_clicked"),
|
||||
|
||||
billingViewed: () => posthog.capture("billing_viewed"),
|
||||
|
|
@ -32,13 +32,13 @@ export const analytics = {
|
|||
upgradeCompleted: () => posthog.capture("upgrade_completed"),
|
||||
billingPortalOpened: () => posthog.capture("billing_portal_opened"),
|
||||
|
||||
connectionAdded: (provider: string) =>
|
||||
connectionAdded: (provider: string) =>
|
||||
posthog.capture("connection_added", { provider }),
|
||||
connectionDeleted: () => posthog.capture("connection_deleted"),
|
||||
connectionAuthStarted: () => posthog.capture("connection_auth_started"),
|
||||
connectionAuthCompleted: () => posthog.capture("connection_auth_completed"),
|
||||
connectionAuthFailed: () => posthog.capture("connection_auth_failed"),
|
||||
|
||||
|
||||
mcpViewOpened: () => posthog.capture("mcp_view_opened"),
|
||||
mcpInstallCmdCopied: () => posthog.capture("mcp_install_cmd_copied"),
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { createContext, type ReactNode, useContext, useState } from "react"
|
||||
import { createContext, type ReactNode, useContext, useState } from "react";
|
||||
|
||||
type ActivePanel = "menu" | "chat" | null
|
||||
type ActivePanel = "menu" | "chat" | null;
|
||||
|
||||
interface MobilePanelContextType {
|
||||
activePanel: ActivePanel
|
||||
setActivePanel: (panel: ActivePanel) => void
|
||||
activePanel: ActivePanel;
|
||||
setActivePanel: (panel: ActivePanel) => void;
|
||||
}
|
||||
|
||||
const MobilePanelContext = createContext<MobilePanelContextType | undefined>(
|
||||
undefined,
|
||||
)
|
||||
);
|
||||
|
||||
export function MobilePanelProvider({ children }: { children: ReactNode }) {
|
||||
const [activePanel, setActivePanel] = useState<ActivePanel>(null)
|
||||
const [activePanel, setActivePanel] = useState<ActivePanel>(null);
|
||||
|
||||
return (
|
||||
<MobilePanelContext.Provider value={{ activePanel, setActivePanel }}>
|
||||
{children}
|
||||
</MobilePanelContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function useMobilePanel() {
|
||||
const context = useContext(MobilePanelContext)
|
||||
const context = useContext(MobilePanelContext);
|
||||
if (!context) {
|
||||
throw new Error("useMobilePanel must be used within a MobilePanelProvider")
|
||||
throw new Error("useMobilePanel must be used within a MobilePanelProvider");
|
||||
}
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const TOUR_STEP_IDS = {
|
|||
MENU_BILLING: "tour-menu-billing",
|
||||
// Legend
|
||||
LEGEND: "tour-legend",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
// Tour storage key for localStorage
|
||||
export const TOUR_STORAGE_KEY = "supermemory-tour-completed"
|
||||
export const TOUR_STORAGE_KEY = "supermemory-tour-completed";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
|
|
@ -6,73 +6,73 @@ import {
|
|||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
} from "react";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
|
||||
type ViewMode = "graph" | "list"
|
||||
type ViewMode = "graph" | "list";
|
||||
|
||||
interface ViewModeContextType {
|
||||
viewMode: ViewMode
|
||||
setViewMode: (mode: ViewMode) => void
|
||||
isInitialized: boolean
|
||||
viewMode: ViewMode;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
isInitialized: boolean;
|
||||
}
|
||||
|
||||
const ViewModeContext = createContext<ViewModeContextType | undefined>(
|
||||
undefined,
|
||||
)
|
||||
);
|
||||
|
||||
// Cookie utility functions
|
||||
const setCookie = (name: string, value: string, days = 365) => {
|
||||
if (typeof document === "undefined") return
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000)
|
||||
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`
|
||||
}
|
||||
if (typeof document === "undefined") return;
|
||||
const expires = new Date();
|
||||
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`;
|
||||
};
|
||||
|
||||
const getCookie = (name: string): string | null => {
|
||||
if (typeof document === "undefined") return null
|
||||
const nameEQ = `${name}=`
|
||||
const ca = document.cookie.split(";")
|
||||
if (typeof document === "undefined") return null;
|
||||
const nameEQ = `${name}=`;
|
||||
const ca = document.cookie.split(";");
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i]
|
||||
if (!c) continue
|
||||
while (c.charAt(0) === " ") c = c.substring(1, c.length)
|
||||
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length)
|
||||
let c = ca[i];
|
||||
if (!c) continue;
|
||||
while (c.charAt(0) === " ") c = c.substring(1, c.length);
|
||||
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const isMobileDevice = () => {
|
||||
if (typeof window === "undefined") return false
|
||||
return window.innerWidth < 768
|
||||
}
|
||||
if (typeof window === "undefined") return false;
|
||||
return window.innerWidth < 768;
|
||||
};
|
||||
|
||||
export function ViewModeProvider({ children }: { children: ReactNode }) {
|
||||
// Start with a default that works for SSR
|
||||
const [viewMode, setViewModeState] = useState<ViewMode>("graph")
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const [viewMode, setViewModeState] = useState<ViewMode>("graph");
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
// Load preferences on the client side
|
||||
useEffect(() => {
|
||||
if (!isInitialized) {
|
||||
// Check for saved preference first
|
||||
const savedMode = getCookie("memoryViewMode")
|
||||
const savedMode = getCookie("memoryViewMode");
|
||||
if (savedMode === "list" || savedMode === "graph") {
|
||||
setViewModeState(savedMode)
|
||||
setViewModeState(savedMode);
|
||||
} else {
|
||||
// If no saved preference, default to list on mobile, graph on desktop
|
||||
setViewModeState(isMobileDevice() ? "list" : "graph")
|
||||
setViewModeState(isMobileDevice() ? "list" : "graph");
|
||||
}
|
||||
setIsInitialized(true)
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [isInitialized])
|
||||
}, [isInitialized]);
|
||||
|
||||
// Save to cookie whenever view mode changes
|
||||
const handleSetViewMode = (mode: ViewMode) => {
|
||||
analytics.viewModeChanged(mode)
|
||||
setViewModeState(mode)
|
||||
setCookie("memoryViewMode", mode)
|
||||
}
|
||||
analytics.viewModeChanged(mode);
|
||||
setViewModeState(mode);
|
||||
setCookie("memoryViewMode", mode);
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewModeContext.Provider
|
||||
|
|
@ -84,13 +84,13 @@ export function ViewModeProvider({ children }: { children: ReactNode }) {
|
|||
>
|
||||
{children}
|
||||
</ViewModeContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function useViewMode() {
|
||||
const context = useContext(ViewModeContext)
|
||||
const context = useContext(ViewModeContext);
|
||||
if (!context) {
|
||||
throw new Error("useViewMode must be used within a ViewModeProvider")
|
||||
throw new Error("useViewMode must be used within a ViewModeProvider");
|
||||
}
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
import { $fetch } from "@lib/api"
|
||||
import { getSessionCookie } from "better-auth/cookies"
|
||||
import { NextResponse } from "next/server"
|
||||
import { $fetch } from "@lib/api";
|
||||
import { getSessionCookie } from "better-auth/cookies";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export default async function middleware(request: Request) {
|
||||
console.debug("[MIDDLEWARE] === MIDDLEWARE START ===")
|
||||
const url = new URL(request.url)
|
||||
console.debug("[MIDDLEWARE] Path:", url.pathname)
|
||||
console.debug("[MIDDLEWARE] Method:", request.method)
|
||||
console.debug("[MIDDLEWARE] === MIDDLEWARE START ===");
|
||||
const url = new URL(request.url);
|
||||
console.debug("[MIDDLEWARE] Path:", url.pathname);
|
||||
console.debug("[MIDDLEWARE] Method:", request.method);
|
||||
|
||||
const sessionCookie = getSessionCookie(request)
|
||||
console.debug("[MIDDLEWARE] Session cookie exists:", !!sessionCookie)
|
||||
const sessionCookie = getSessionCookie(request);
|
||||
console.debug("[MIDDLEWARE] Session cookie exists:", !!sessionCookie);
|
||||
|
||||
// Always allow access to login and waitlist pages
|
||||
const publicPaths = ["/login"]
|
||||
const publicPaths = ["/login"];
|
||||
if (publicPaths.includes(url.pathname)) {
|
||||
console.debug("[MIDDLEWARE] Public path, allowing access")
|
||||
return NextResponse.next()
|
||||
console.debug("[MIDDLEWARE] Public path, allowing access");
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// If no session cookie and not on a public path, redirect to login
|
||||
if (!sessionCookie) {
|
||||
console.debug(
|
||||
"[MIDDLEWARE] No session cookie and not on public path, redirecting to /login",
|
||||
)
|
||||
return NextResponse.redirect(new URL("/login", request.url))
|
||||
);
|
||||
return NextResponse.redirect(new URL("/login", request.url));
|
||||
}
|
||||
|
||||
if (url.pathname !== "/waitlist") {
|
||||
|
|
@ -31,20 +31,20 @@ export default async function middleware(request: Request) {
|
|||
headers: {
|
||||
Authorization: `Bearer ${sessionCookie}`,
|
||||
},
|
||||
})
|
||||
console.debug("[MIDDLEWARE] Waitlist status:", response.data)
|
||||
});
|
||||
console.debug("[MIDDLEWARE] Waitlist status:", response.data);
|
||||
if (response.data && !response.data.accessGranted) {
|
||||
return NextResponse.redirect(new URL("/waitlist", request.url))
|
||||
return NextResponse.redirect(new URL("/waitlist", request.url));
|
||||
}
|
||||
}
|
||||
|
||||
console.debug("[MIDDLEWARE] Passing through to next handler")
|
||||
console.debug("[MIDDLEWARE] === MIDDLEWARE END ===")
|
||||
return NextResponse.next()
|
||||
console.debug("[MIDDLEWARE] Passing through to next handler");
|
||||
console.debug("[MIDDLEWARE] === MIDDLEWARE END ===");
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/((?!_next/static|_next/image|images|icon.png|monitoring|opengraph-image.png|ingest|api|login|api/emails).*)",
|
||||
],
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { withSentryConfig } from "@sentry/nextjs"
|
||||
import type { NextConfig } from "next"
|
||||
import { withSentryConfig } from "@sentry/nextjs";
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
experimental: {
|
||||
|
|
@ -20,10 +20,10 @@ const nextConfig: NextConfig = {
|
|||
source: "/ingest/:path*",
|
||||
destination: "https://us.i.posthog.com/:path*",
|
||||
},
|
||||
]
|
||||
];
|
||||
},
|
||||
skipTrailingSlashRedirect: true,
|
||||
}
|
||||
};
|
||||
|
||||
export default withSentryConfig(nextConfig, {
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||
|
|
@ -54,7 +54,8 @@ export default withSentryConfig(nextConfig, {
|
|||
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
})
|
||||
});
|
||||
|
||||
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"
|
||||
initOpenNextCloudflareForDev()
|
||||
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
|
||||
|
||||
initOpenNextCloudflareForDev();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
||||
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
|
||||
|
||||
|
||||
export default defineCloudflareConfig({
|
||||
incrementalCache: r2IncrementalCache,
|
||||
incrementalCache: r2IncrementalCache,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,92 +1,96 @@
|
|||
{
|
||||
"name": "@repo/web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/google": "^2.0.0-beta.13",
|
||||
"@ai-sdk/react": "2.0.0-beta.24",
|
||||
"@better-fetch/fetch": "^1.1.18",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@opennextjs/cloudflare": "^1.6.1",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@react-router/fs-routes": "^7.6.2",
|
||||
"@react-router/node": "^7.6.2",
|
||||
"@react-router/serve": "^7.6.2",
|
||||
"@sentry/nextjs": "^9.33.0",
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-form": "^1.12.4",
|
||||
"@tanstack/react-query": "^5.81.2",
|
||||
"@tanstack/react-query-devtools": "^5.84.2",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"ai": "5.0.0-beta.24",
|
||||
"autumn-js": "0.0.116",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.6.0",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"isbot": "^5.1.28",
|
||||
"lucide-react": "^0.525.0",
|
||||
"motion": "^12.19.2",
|
||||
"next": "15.3.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.4.3",
|
||||
"posthog-js": "^1.257.0",
|
||||
"random-word-slugs": "^0.1.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "2",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shadcn-dropzone": "^0.2.1",
|
||||
"sonner": "^2.0.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"valibot": "^1.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
"zustand": "^5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@total-typescript/tsconfig": "^1.0.4",
|
||||
"@types/node": "^24.0.4",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3",
|
||||
"wrangler": "^4.26.0"
|
||||
}
|
||||
"name": "@repo/web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/google": "^2.0.0-beta.13",
|
||||
"@ai-sdk/react": "2.0.0-beta.24",
|
||||
"@better-fetch/fetch": "^1.1.18",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@opennextjs/cloudflare": "^1.6.1",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@react-router/fs-routes": "^7.6.2",
|
||||
"@react-router/node": "^7.6.2",
|
||||
"@react-router/serve": "^7.6.2",
|
||||
"@sentry/nextjs": "^9.33.0",
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-form": "^1.12.4",
|
||||
"@tanstack/react-query": "^5.81.2",
|
||||
"@tanstack/react-query-devtools": "^5.84.2",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"ai": "5.0.0-beta.24",
|
||||
"autumn-js": "0.0.116",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.6.0",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"is-hotkey": "^0.2.0",
|
||||
"isbot": "^5.1.28",
|
||||
"lucide-react": "^0.525.0",
|
||||
"motion": "^12.19.2",
|
||||
"next": "15.3.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.4.3",
|
||||
"posthog-js": "^1.257.0",
|
||||
"random-word-slugs": "^0.1.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "2",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shadcn-dropzone": "^0.2.1",
|
||||
"slate": "^0.118.0",
|
||||
"slate-react": "^0.117.4",
|
||||
"sonner": "^2.0.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"valibot": "^1.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
"zustand": "^5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@total-typescript/tsconfig": "^1.0.4",
|
||||
"@types/is-hotkey": "^0.1.10",
|
||||
"@types/node": "^24.0.4",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3",
|
||||
"wrangler": "^4.26.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
}
|
||||
};
|
||||
|
||||
export default config
|
||||
export default config;
|
||||
|
|
|
|||
|
|
@ -1,182 +1,214 @@
|
|||
import { create } from "zustand"
|
||||
import { persist } from "zustand/middleware"
|
||||
import type { UIMessage } from "@ai-sdk/react"
|
||||
import type { UIMessage } from "@ai-sdk/react";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
export interface ConversationSummary {
|
||||
id: string
|
||||
title?: string
|
||||
lastUpdated: string
|
||||
id: string;
|
||||
title?: string;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
interface ConversationRecord {
|
||||
messages: UIMessage[]
|
||||
title?: string
|
||||
lastUpdated: string
|
||||
messages: UIMessage[];
|
||||
title?: string;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
interface ProjectConversationsState {
|
||||
currentChatId: string | null
|
||||
conversations: Record<string, ConversationRecord>
|
||||
currentChatId: string | null;
|
||||
conversations: Record<string, ConversationRecord>;
|
||||
}
|
||||
|
||||
interface ConversationsStoreState {
|
||||
byProject: Record<string, ProjectConversationsState>
|
||||
setCurrentChatId: (projectId: string, chatId: string | null) => void
|
||||
setConversation: (projectId: string, chatId: string, messages: UIMessage[]) => void
|
||||
deleteConversation: (projectId: string, chatId: string) => void
|
||||
setConversationTitle: (projectId: string, chatId: string, title: string | undefined) => void
|
||||
byProject: Record<string, ProjectConversationsState>;
|
||||
setCurrentChatId: (projectId: string, chatId: string | null) => void;
|
||||
setConversation: (
|
||||
projectId: string,
|
||||
chatId: string,
|
||||
messages: UIMessage[],
|
||||
) => void;
|
||||
deleteConversation: (projectId: string, chatId: string) => void;
|
||||
setConversationTitle: (
|
||||
projectId: string,
|
||||
chatId: string,
|
||||
title: string | undefined,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const usePersistentChatStore = create<ConversationsStoreState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
byProject: {},
|
||||
persist(
|
||||
(set, get) => ({
|
||||
byProject: {},
|
||||
|
||||
setCurrentChatId(projectId, chatId) {
|
||||
set((state) => {
|
||||
const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {} }
|
||||
return {
|
||||
byProject: {
|
||||
...state.byProject,
|
||||
[projectId]: { ...project, currentChatId: chatId },
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
setCurrentChatId(projectId, chatId) {
|
||||
set((state) => {
|
||||
const project = state.byProject[projectId] ?? {
|
||||
currentChatId: null,
|
||||
conversations: {},
|
||||
};
|
||||
return {
|
||||
byProject: {
|
||||
...state.byProject,
|
||||
[projectId]: { ...project, currentChatId: chatId },
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setConversation(projectId, chatId, messages) {
|
||||
const now = new Date().toISOString()
|
||||
set((state) => {
|
||||
const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {} }
|
||||
const existing = project.conversations[chatId]
|
||||
const shouldTouchLastUpdated = (() => {
|
||||
if (!existing) return messages.length > 0
|
||||
const previousLength = existing.messages?.length ?? 0
|
||||
return messages.length > previousLength
|
||||
})()
|
||||
setConversation(projectId, chatId, messages) {
|
||||
const now = new Date().toISOString();
|
||||
set((state) => {
|
||||
const project = state.byProject[projectId] ?? {
|
||||
currentChatId: null,
|
||||
conversations: {},
|
||||
};
|
||||
const existing = project.conversations[chatId];
|
||||
const shouldTouchLastUpdated = (() => {
|
||||
if (!existing) return messages.length > 0;
|
||||
const previousLength = existing.messages?.length ?? 0;
|
||||
return messages.length > previousLength;
|
||||
})();
|
||||
|
||||
const record: ConversationRecord = {
|
||||
messages,
|
||||
title: existing?.title,
|
||||
lastUpdated: shouldTouchLastUpdated ? now : existing?.lastUpdated ?? now,
|
||||
}
|
||||
return {
|
||||
byProject: {
|
||||
...state.byProject,
|
||||
[projectId]: {
|
||||
currentChatId: project.currentChatId,
|
||||
conversations: {
|
||||
...project.conversations,
|
||||
[chatId]: record,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
const record: ConversationRecord = {
|
||||
messages,
|
||||
title: existing?.title,
|
||||
lastUpdated: shouldTouchLastUpdated
|
||||
? now
|
||||
: (existing?.lastUpdated ?? now),
|
||||
};
|
||||
return {
|
||||
byProject: {
|
||||
...state.byProject,
|
||||
[projectId]: {
|
||||
currentChatId: project.currentChatId,
|
||||
conversations: {
|
||||
...project.conversations,
|
||||
[chatId]: record,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
deleteConversation(projectId, chatId) {
|
||||
set((state) => {
|
||||
const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {} }
|
||||
const { [chatId]: _, ...rest } = project.conversations
|
||||
const nextCurrent = project.currentChatId === chatId ? null : project.currentChatId
|
||||
return {
|
||||
byProject: {
|
||||
...state.byProject,
|
||||
[projectId]: { currentChatId: nextCurrent, conversations: rest },
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteConversation(projectId, chatId) {
|
||||
set((state) => {
|
||||
const project = state.byProject[projectId] ?? {
|
||||
currentChatId: null,
|
||||
conversations: {},
|
||||
};
|
||||
const { [chatId]: _, ...rest } = project.conversations;
|
||||
const nextCurrent =
|
||||
project.currentChatId === chatId ? null : project.currentChatId;
|
||||
return {
|
||||
byProject: {
|
||||
...state.byProject,
|
||||
[projectId]: { currentChatId: nextCurrent, conversations: rest },
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setConversationTitle(projectId, chatId, title) {
|
||||
const now = new Date().toISOString()
|
||||
set((state) => {
|
||||
const project = state.byProject[projectId] ?? { currentChatId: null, conversations: {} }
|
||||
const existing = project.conversations[chatId]
|
||||
if (!existing) return { byProject: state.byProject }
|
||||
return {
|
||||
byProject: {
|
||||
...state.byProject,
|
||||
[projectId]: {
|
||||
currentChatId: project.currentChatId,
|
||||
conversations: {
|
||||
...project.conversations,
|
||||
[chatId]: { ...existing, title, lastUpdated: now },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "supermemory-chats",
|
||||
},
|
||||
),
|
||||
)
|
||||
setConversationTitle(projectId, chatId, title) {
|
||||
const now = new Date().toISOString();
|
||||
set((state) => {
|
||||
const project = state.byProject[projectId] ?? {
|
||||
currentChatId: null,
|
||||
conversations: {},
|
||||
};
|
||||
const existing = project.conversations[chatId];
|
||||
if (!existing) return { byProject: state.byProject };
|
||||
return {
|
||||
byProject: {
|
||||
...state.byProject,
|
||||
[projectId]: {
|
||||
currentChatId: project.currentChatId,
|
||||
conversations: {
|
||||
...project.conversations,
|
||||
[chatId]: { ...existing, title, lastUpdated: now },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "supermemory-chats",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Always scoped to the current project via useProject
|
||||
import { useProject } from ".";
|
||||
|
||||
export function usePersistentChat() {
|
||||
const { selectedProject } = useProject()
|
||||
const projectId = selectedProject
|
||||
const { selectedProject } = useProject();
|
||||
const projectId = selectedProject;
|
||||
|
||||
const projectState = usePersistentChatStore((s) => s.byProject[projectId])
|
||||
const setCurrentChatIdRaw = usePersistentChatStore((s) => s.setCurrentChatId)
|
||||
const setConversationRaw = usePersistentChatStore((s) => s.setConversation)
|
||||
const deleteConversationRaw = usePersistentChatStore((s) => s.deleteConversation)
|
||||
const setConversationTitleRaw = usePersistentChatStore((s) => s.setConversationTitle)
|
||||
const projectState = usePersistentChatStore((s) => s.byProject[projectId]);
|
||||
const setCurrentChatIdRaw = usePersistentChatStore((s) => s.setCurrentChatId);
|
||||
const setConversationRaw = usePersistentChatStore((s) => s.setConversation);
|
||||
const deleteConversationRaw = usePersistentChatStore(
|
||||
(s) => s.deleteConversation,
|
||||
);
|
||||
const setConversationTitleRaw = usePersistentChatStore(
|
||||
(s) => s.setConversationTitle,
|
||||
);
|
||||
|
||||
const conversations: ConversationSummary[] = (() => {
|
||||
const convs = projectState?.conversations ?? {}
|
||||
return Object.entries(convs).map(([id, rec]) => ({ id, title: rec.title, lastUpdated: rec.lastUpdated }))
|
||||
})()
|
||||
const conversations: ConversationSummary[] = (() => {
|
||||
const convs = projectState?.conversations ?? {};
|
||||
return Object.entries(convs).map(([id, rec]) => ({
|
||||
id,
|
||||
title: rec.title,
|
||||
lastUpdated: rec.lastUpdated,
|
||||
}));
|
||||
})();
|
||||
|
||||
const currentChatId = projectState?.currentChatId ?? null
|
||||
const currentChatId = projectState?.currentChatId ?? null;
|
||||
|
||||
function setCurrentChatId(chatId: string | null): void {
|
||||
setCurrentChatIdRaw(projectId, chatId)
|
||||
}
|
||||
function setCurrentChatId(chatId: string | null): void {
|
||||
setCurrentChatIdRaw(projectId, chatId);
|
||||
}
|
||||
|
||||
function setConversation(chatId: string, messages: UIMessage[]): void {
|
||||
setConversationRaw(projectId, chatId, messages)
|
||||
}
|
||||
function setConversation(chatId: string, messages: UIMessage[]): void {
|
||||
setConversationRaw(projectId, chatId, messages);
|
||||
}
|
||||
|
||||
function deleteConversation(chatId: string): void {
|
||||
deleteConversationRaw(projectId, chatId)
|
||||
}
|
||||
function deleteConversation(chatId: string): void {
|
||||
deleteConversationRaw(projectId, chatId);
|
||||
}
|
||||
|
||||
function setConversationTitle(chatId: string, title: string | undefined): void {
|
||||
setConversationTitleRaw(projectId, chatId, title)
|
||||
}
|
||||
function setConversationTitle(
|
||||
chatId: string,
|
||||
title: string | undefined,
|
||||
): void {
|
||||
setConversationTitleRaw(projectId, chatId, title);
|
||||
}
|
||||
|
||||
function getCurrentConversation(): UIMessage[] | undefined {
|
||||
const convs = projectState?.conversations ?? {}
|
||||
const id = currentChatId
|
||||
if (!id) return undefined
|
||||
return convs[id]?.messages
|
||||
}
|
||||
function getCurrentConversation(): UIMessage[] | undefined {
|
||||
const convs = projectState?.conversations ?? {};
|
||||
const id = currentChatId;
|
||||
if (!id) return undefined;
|
||||
return convs[id]?.messages;
|
||||
}
|
||||
|
||||
function getCurrentChat(): ConversationSummary | undefined {
|
||||
const id = currentChatId
|
||||
if (!id) return undefined
|
||||
const rec = projectState?.conversations?.[id]
|
||||
if (!rec) return undefined
|
||||
return { id, title: rec.title, lastUpdated: rec.lastUpdated }
|
||||
}
|
||||
function getCurrentChat(): ConversationSummary | undefined {
|
||||
const id = currentChatId;
|
||||
if (!id) return undefined;
|
||||
const rec = projectState?.conversations?.[id];
|
||||
if (!rec) return undefined;
|
||||
return { id, title: rec.title, lastUpdated: rec.lastUpdated };
|
||||
}
|
||||
|
||||
return {
|
||||
conversations,
|
||||
currentChatId,
|
||||
setCurrentChatId,
|
||||
setConversation,
|
||||
deleteConversation,
|
||||
setConversationTitle,
|
||||
getCurrentConversation,
|
||||
getCurrentChat,
|
||||
}
|
||||
return {
|
||||
conversations,
|
||||
currentChatId,
|
||||
setCurrentChatId,
|
||||
setConversation,
|
||||
deleteConversation,
|
||||
setConversationTitle,
|
||||
getCurrentConversation,
|
||||
getCurrentChat,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,35 @@
|
|||
import { create } from "zustand"
|
||||
import { create } from "zustand";
|
||||
|
||||
interface GraphHighlightsState {
|
||||
documentIds: string[]
|
||||
lastUpdated: number
|
||||
setDocumentIds: (ids: string[]) => void
|
||||
clear: () => void
|
||||
documentIds: string[];
|
||||
lastUpdated: number;
|
||||
setDocumentIds: (ids: string[]) => void;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export const useGraphHighlightsStore = create<GraphHighlightsState>()((set, get) => ({
|
||||
documentIds: [],
|
||||
lastUpdated: 0,
|
||||
setDocumentIds: (ids) => {
|
||||
const next = Array.from(new Set(ids))
|
||||
const prev = get().documentIds
|
||||
if (prev.length === next.length && prev.every((id) => next.includes(id))) {
|
||||
return
|
||||
}
|
||||
set({ documentIds: next, lastUpdated: Date.now() })
|
||||
},
|
||||
clear: () => set({ documentIds: [], lastUpdated: Date.now() }),
|
||||
}))
|
||||
export const useGraphHighlightsStore = create<GraphHighlightsState>()(
|
||||
(set, get) => ({
|
||||
documentIds: [],
|
||||
lastUpdated: 0,
|
||||
setDocumentIds: (ids) => {
|
||||
const next = Array.from(new Set(ids));
|
||||
const prev = get().documentIds;
|
||||
if (
|
||||
prev.length === next.length &&
|
||||
prev.every((id) => next.includes(id))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
set({ documentIds: next, lastUpdated: Date.now() });
|
||||
},
|
||||
clear: () => set({ documentIds: [], lastUpdated: Date.now() }),
|
||||
}),
|
||||
);
|
||||
|
||||
export function useGraphHighlights() {
|
||||
const documentIds = useGraphHighlightsStore((s) => s.documentIds)
|
||||
const lastUpdated = useGraphHighlightsStore((s) => s.lastUpdated)
|
||||
const setDocumentIds = useGraphHighlightsStore((s) => s.setDocumentIds)
|
||||
const clear = useGraphHighlightsStore((s) => s.clear)
|
||||
return { documentIds, lastUpdated, setDocumentIds, clear }
|
||||
const documentIds = useGraphHighlightsStore((s) => s.documentIds);
|
||||
const lastUpdated = useGraphHighlightsStore((s) => s.lastUpdated);
|
||||
const setDocumentIds = useGraphHighlightsStore((s) => s.setDocumentIds);
|
||||
const clear = useGraphHighlightsStore((s) => s.clear);
|
||||
return { documentIds, lastUpdated, setDocumentIds, clear };
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,78 +1,80 @@
|
|||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
interface ProjectState {
|
||||
selectedProject: string
|
||||
setSelectedProject: (projectId: string) => void
|
||||
selectedProject: string;
|
||||
setSelectedProject: (projectId: string) => void;
|
||||
}
|
||||
|
||||
export const useProjectStore = create<ProjectState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
selectedProject: 'sm_project_default',
|
||||
setSelectedProject: (projectId) => set({ selectedProject: projectId }),
|
||||
}),
|
||||
{
|
||||
name: 'selectedProject',
|
||||
}
|
||||
)
|
||||
)
|
||||
persist(
|
||||
(set) => ({
|
||||
selectedProject: "sm_project_default",
|
||||
setSelectedProject: (projectId) => set({ selectedProject: projectId }),
|
||||
}),
|
||||
{
|
||||
name: "selectedProject",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
interface MemoryGraphState {
|
||||
positionX: number
|
||||
positionY: number
|
||||
setPositionX: (x: number) => void
|
||||
setPositionY: (y: number) => void
|
||||
setPosition: (x: number, y: number) => void
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
setPositionX: (x: number) => void;
|
||||
setPositionY: (y: number) => void;
|
||||
setPosition: (x: number, y: number) => void;
|
||||
}
|
||||
|
||||
export const useMemoryGraphStore = create<MemoryGraphState>()((set) => ({
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
setPositionX: (x) => set({ positionX: x }),
|
||||
setPositionY: (y) => set({ positionY: y }),
|
||||
setPosition: (x, y) => set({ positionX: x, positionY: y }),
|
||||
}))
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
setPositionX: (x) => set({ positionX: x }),
|
||||
setPositionY: (y) => set({ positionY: y }),
|
||||
setPosition: (x, y) => set({ positionX: x, positionY: y }),
|
||||
}));
|
||||
|
||||
interface ChatState {
|
||||
isOpen: boolean
|
||||
setIsOpen: (isOpen: boolean) => void
|
||||
toggleChat: () => void
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
toggleChat: () => void;
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatState>()((set, get) => ({
|
||||
isOpen: false,
|
||||
setIsOpen: (isOpen) => set({ isOpen }),
|
||||
toggleChat: () => set({ isOpen: !get().isOpen }),
|
||||
}))
|
||||
isOpen: false,
|
||||
setIsOpen: (isOpen) => set({ isOpen }),
|
||||
toggleChat: () => set({ isOpen: !get().isOpen }),
|
||||
}));
|
||||
|
||||
export function useProject() {
|
||||
const selectedProject = useProjectStore(state => state.selectedProject)
|
||||
const setSelectedProject = useProjectStore(state => state.setSelectedProject)
|
||||
return { selectedProject, setSelectedProject }
|
||||
const selectedProject = useProjectStore((state) => state.selectedProject);
|
||||
const setSelectedProject = useProjectStore(
|
||||
(state) => state.setSelectedProject,
|
||||
);
|
||||
return { selectedProject, setSelectedProject };
|
||||
}
|
||||
|
||||
export function useMemoryGraphPosition() {
|
||||
const positionX = useMemoryGraphStore(state => state.positionX)
|
||||
const positionY = useMemoryGraphStore(state => state.positionY)
|
||||
const setPositionX = useMemoryGraphStore(state => state.setPositionX)
|
||||
const setPositionY = useMemoryGraphStore(state => state.setPositionY)
|
||||
const setPosition = useMemoryGraphStore(state => state.setPosition)
|
||||
|
||||
return {
|
||||
x: positionX,
|
||||
y: positionY,
|
||||
setX: setPositionX,
|
||||
setY: setPositionY,
|
||||
setPosition
|
||||
}
|
||||
const positionX = useMemoryGraphStore((state) => state.positionX);
|
||||
const positionY = useMemoryGraphStore((state) => state.positionY);
|
||||
const setPositionX = useMemoryGraphStore((state) => state.setPositionX);
|
||||
const setPositionY = useMemoryGraphStore((state) => state.setPositionY);
|
||||
const setPosition = useMemoryGraphStore((state) => state.setPosition);
|
||||
|
||||
return {
|
||||
x: positionX,
|
||||
y: positionY,
|
||||
setX: setPositionX,
|
||||
setY: setPositionY,
|
||||
setPosition,
|
||||
};
|
||||
}
|
||||
|
||||
export function useChatOpen() {
|
||||
const isOpen = useChatStore(state => state.isOpen)
|
||||
const setIsOpen = useChatStore(state => state.setIsOpen)
|
||||
const toggleChat = useChatStore(state => state.toggleChat)
|
||||
return { isOpen, setIsOpen, toggleChat }
|
||||
const isOpen = useChatStore((state) => state.isOpen);
|
||||
const setIsOpen = useChatStore((state) => state.setIsOpen);
|
||||
const toggleChat = useChatStore((state) => state.toggleChat);
|
||||
return { isOpen, setIsOpen, toggleChat };
|
||||
}
|
||||
|
||||
export { usePersistentChatStore, usePersistentChat } from "./chat"
|
||||
export { usePersistentChat, usePersistentChatStore } from "./chat";
|
||||
|
|
|
|||
75
biome.json
Normal file
75
biome.json
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
|
||||
"files": {
|
||||
"includes": [
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.json",
|
||||
"**/*.jsonc",
|
||||
"!**/node_modules",
|
||||
"!**/dist",
|
||||
"!**/build",
|
||||
"!**/.next",
|
||||
"!**/.turbo",
|
||||
"!**/*.d.ts"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 80,
|
||||
"attributePosition": "auto",
|
||||
"includes": ["**", "!**/*.md"]
|
||||
},
|
||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"a11y": {
|
||||
"recommended": true
|
||||
},
|
||||
"complexity": {
|
||||
"recommended": true
|
||||
},
|
||||
"correctness": {
|
||||
"recommended": true
|
||||
},
|
||||
"performance": {
|
||||
"recommended": true
|
||||
},
|
||||
"security": {
|
||||
"recommended": true
|
||||
},
|
||||
"style": {
|
||||
"recommended": true
|
||||
},
|
||||
"suspicious": {
|
||||
"recommended": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double",
|
||||
"jsxQuoteStyle": "double",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "asNeeded",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"quoteProperties": "asNeeded",
|
||||
"attributePosition": "auto"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"formatter": {
|
||||
"trailingCommas": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
126
package.json
126
package.json
|
|
@ -1,65 +1,65 @@
|
|||
{
|
||||
"name": "supermemory",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"format-lint": "bunx biome check --write",
|
||||
"check-types": "turbo run check-types"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"packageManager": "bun@1.2.17",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^1.2.12",
|
||||
"@ai-sdk/cerebras": "^0.2.16",
|
||||
"@ai-sdk/google": "^1.2.22",
|
||||
"@ai-sdk/openai": "^1.3.23",
|
||||
"@anthropic-ai/sdk": "^0.55.1",
|
||||
"@google/genai": "^1.10.0",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@hono/zod-validator": "^0.7.1",
|
||||
"@scalar/hono-api-reference": "^0.9.11",
|
||||
"ai": "^4.3.19",
|
||||
"alchemy": "^0.55.2",
|
||||
"atmn": "^0.0.16",
|
||||
"better-auth": "^1.3.3",
|
||||
"boxen": "^8.0.1",
|
||||
"cloudflare": "^4.5.0",
|
||||
"compromise": "^14.14.4",
|
||||
"dedent": "^1.6.0",
|
||||
"destr": "^2.0.5",
|
||||
"drizzle-orm": "^0.44.3",
|
||||
"drizzle-zod": "~0.7.1",
|
||||
"file-type": "^21.0.0",
|
||||
"hono-openapi": "^0.4.8",
|
||||
"llm-bridge": "1.0.8",
|
||||
"nanoid": "^5.1.5",
|
||||
"neverthrow": "^8.2.0",
|
||||
"pg": "^8.16.3",
|
||||
"pino": "^9.7.0",
|
||||
"postgres": "^3.4.7",
|
||||
"random-word-slugs": "^0.1.7",
|
||||
"resend": "^4.7.0",
|
||||
"zod": "^3.25.76",
|
||||
"zod-openapi": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.1.3",
|
||||
"@total-typescript/tsconfig": "^1.0.4",
|
||||
"@types/pg": "^8.15.4",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "5.8.3",
|
||||
"wrangler": "4.22.0"
|
||||
},
|
||||
"workerd": {
|
||||
"import": "./esm/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
"name": "supermemory",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"format-lint": "bunx biome check --write",
|
||||
"check-types": "turbo run check-types"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"packageManager": "bun@1.2.17",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^1.2.12",
|
||||
"@ai-sdk/cerebras": "^0.2.16",
|
||||
"@ai-sdk/google": "^1.2.22",
|
||||
"@ai-sdk/openai": "^1.3.23",
|
||||
"@anthropic-ai/sdk": "^0.55.1",
|
||||
"@google/genai": "^1.10.0",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@hono/zod-validator": "^0.7.1",
|
||||
"@scalar/hono-api-reference": "^0.9.11",
|
||||
"ai": "^4.3.19",
|
||||
"alchemy": "^0.55.2",
|
||||
"atmn": "^0.0.16",
|
||||
"better-auth": "^1.3.3",
|
||||
"boxen": "^8.0.1",
|
||||
"cloudflare": "^4.5.0",
|
||||
"compromise": "^14.14.4",
|
||||
"dedent": "^1.6.0",
|
||||
"destr": "^2.0.5",
|
||||
"drizzle-orm": "^0.44.3",
|
||||
"drizzle-zod": "~0.7.1",
|
||||
"file-type": "^21.0.0",
|
||||
"hono-openapi": "^0.4.8",
|
||||
"llm-bridge": "1.0.8",
|
||||
"nanoid": "^5.1.5",
|
||||
"neverthrow": "^8.2.0",
|
||||
"pg": "^8.16.3",
|
||||
"pino": "^9.7.0",
|
||||
"postgres": "^3.4.7",
|
||||
"random-word-slugs": "^0.1.7",
|
||||
"resend": "^4.7.0",
|
||||
"zod": "^3.25.76",
|
||||
"zod-openapi": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.1.3",
|
||||
"@total-typescript/tsconfig": "^1.0.4",
|
||||
"@types/pg": "^8.15.4",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "5.8.3",
|
||||
"wrangler": "4.22.0"
|
||||
},
|
||||
"workerd": {
|
||||
"import": "./esm/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import turboPlugin from "eslint-plugin-turbo";
|
||||
import tseslint from "typescript-eslint";
|
||||
import onlyWarn from "eslint-plugin-only-warn";
|
||||
import js from "@eslint/js"
|
||||
import eslintConfigPrettier from "eslint-config-prettier"
|
||||
import onlyWarn from "eslint-plugin-only-warn"
|
||||
import turboPlugin from "eslint-plugin-turbo"
|
||||
import tseslint from "typescript-eslint"
|
||||
|
||||
/**
|
||||
* A shared ESLint configuration for the repository.
|
||||
|
|
@ -10,23 +10,23 @@ import onlyWarn from "eslint-plugin-only-warn";
|
|||
* @type {import("eslint").Linter.Config[]}
|
||||
* */
|
||||
export const config = [
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
turbo: turboPlugin,
|
||||
},
|
||||
rules: {
|
||||
"turbo/no-undeclared-env-vars": "warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
onlyWarn,
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**"],
|
||||
},
|
||||
];
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
turbo: turboPlugin,
|
||||
},
|
||||
rules: {
|
||||
"turbo/no-undeclared-env-vars": "warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
onlyWarn,
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**"],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
import globals from "globals";
|
||||
import pluginNext from "@next/eslint-plugin-next";
|
||||
import { config as baseConfig } from "./base.js";
|
||||
import js from "@eslint/js"
|
||||
import pluginNext from "@next/eslint-plugin-next"
|
||||
import eslintConfigPrettier from "eslint-config-prettier"
|
||||
import pluginReact from "eslint-plugin-react"
|
||||
import pluginReactHooks from "eslint-plugin-react-hooks"
|
||||
import globals from "globals"
|
||||
import tseslint from "typescript-eslint"
|
||||
import { config as baseConfig } from "./base.js"
|
||||
|
||||
/**
|
||||
* A custom ESLint configuration for libraries that use Next.js.
|
||||
|
|
@ -13,37 +13,37 @@ import { config as baseConfig } from "./base.js";
|
|||
* @type {import("eslint").Linter.Config[]}
|
||||
* */
|
||||
export const nextJsConfig = [
|
||||
...baseConfig,
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
...pluginReact.configs.flat.recommended,
|
||||
languageOptions: {
|
||||
...pluginReact.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"@next/next": pluginNext,
|
||||
},
|
||||
rules: {
|
||||
...pluginNext.configs.recommended.rules,
|
||||
...pluginNext.configs["core-web-vitals"].rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"react-hooks": pluginReactHooks,
|
||||
},
|
||||
settings: { react: { version: "detect" } },
|
||||
rules: {
|
||||
...pluginReactHooks.configs.recommended.rules,
|
||||
// React scope no longer necessary with new JSX transform.
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
...baseConfig,
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
...pluginReact.configs.flat.recommended,
|
||||
languageOptions: {
|
||||
...pluginReact.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"@next/next": pluginNext,
|
||||
},
|
||||
rules: {
|
||||
...pluginNext.configs.recommended.rules,
|
||||
...pluginNext.configs["core-web-vitals"].rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"react-hooks": pluginReactHooks,
|
||||
},
|
||||
settings: { react: { version: "detect" } },
|
||||
rules: {
|
||||
...pluginReactHooks.configs.recommended.rules,
|
||||
// React scope no longer necessary with new JSX transform.
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
{
|
||||
"name": "@repo/eslint-config",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./base": "./base.js",
|
||||
"./next-js": "./next.js",
|
||||
"./react-internal": "./react-internal.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@next/eslint-plugin-next": "^15.4.2",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-turbo": "^2.5.0",
|
||||
"globals": "^16.3.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.39.0"
|
||||
}
|
||||
"name": "@repo/eslint-config",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./base": "./base.js",
|
||||
"./next-js": "./next.js",
|
||||
"./react-internal": "./react-internal.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@next/eslint-plugin-next": "^15.4.2",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-turbo": "^2.5.0",
|
||||
"globals": "^16.3.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.39.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
66
packages/eslint-config/react-internal.js
vendored
66
packages/eslint-config/react-internal.js
vendored
|
|
@ -1,39 +1,39 @@
|
|||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
import globals from "globals";
|
||||
import { config as baseConfig } from "./base.js";
|
||||
import js from "@eslint/js"
|
||||
import eslintConfigPrettier from "eslint-config-prettier"
|
||||
import pluginReact from "eslint-plugin-react"
|
||||
import pluginReactHooks from "eslint-plugin-react-hooks"
|
||||
import globals from "globals"
|
||||
import tseslint from "typescript-eslint"
|
||||
import { config as baseConfig } from "./base.js"
|
||||
|
||||
/**
|
||||
* A custom ESLint configuration for libraries that use React.
|
||||
*
|
||||
* @type {import("eslint").Linter.Config[]} */
|
||||
export const config = [
|
||||
...baseConfig,
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
...pluginReact.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"react-hooks": pluginReactHooks,
|
||||
},
|
||||
settings: { react: { version: "detect" } },
|
||||
rules: {
|
||||
...pluginReactHooks.configs.recommended.rules,
|
||||
// React scope no longer necessary with new JSX transform.
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
...baseConfig,
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
...pluginReact.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
"react-hooks": pluginReactHooks,
|
||||
},
|
||||
settings: { react: { version: "detect" } },
|
||||
rules: {
|
||||
...pluginReactHooks.configs.recommended.rules,
|
||||
// React scope no longer necessary with new JSX transform.
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@repo/hooks",
|
||||
"version": "0.0.0",
|
||||
"private": true
|
||||
}
|
||||
"name": "@repo/hooks",
|
||||
"version": "0.0.0",
|
||||
"private": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,14 +146,14 @@ export const apiSchema = createSchema({
|
|||
input: DocumentsWithMemoriesQuerySchema,
|
||||
output: DocumentsWithMemoriesResponseSchema,
|
||||
},
|
||||
"@post/memories/documents/by-ids": {
|
||||
input: z.object({
|
||||
ids: z.array(z.string()),
|
||||
by: z.enum(["id", "customId"]).optional(),
|
||||
containerTags: z.array(z.string()).optional(),
|
||||
}),
|
||||
output: DocumentsWithMemoriesResponseSchema,
|
||||
},
|
||||
"@post/memories/documents/by-ids": {
|
||||
input: z.object({
|
||||
ids: z.array(z.string()),
|
||||
by: z.enum(["id", "customId"]).optional(),
|
||||
containerTags: z.array(z.string()).optional(),
|
||||
}),
|
||||
output: DocumentsWithMemoriesResponseSchema,
|
||||
},
|
||||
"@post/memories/migrate-mcp": {
|
||||
input: MigrateMCPRequestSchema,
|
||||
output: MigrateMCPResponseSchema,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@repo/lib",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^1.2.12",
|
||||
"@ai-sdk/google": "^1.2.22",
|
||||
"@ai-sdk/groq": "^1.2.9",
|
||||
"ai-gateway-provider": "^0.0.11"
|
||||
}
|
||||
"name": "@repo/lib",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^1.2.12",
|
||||
"@ai-sdk/google": "^1.2.22",
|
||||
"@ai-sdk/groq": "^1.2.9",
|
||||
"ai-gateway-provider": "^0.0.11"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"incremental": false,
|
||||
"isolatedModules": true,
|
||||
"lib": ["es2022", "DOM", "DOM.Iterable"],
|
||||
"module": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"moduleResolution": "NodeNext",
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "ES2022"
|
||||
}
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"incremental": false,
|
||||
"isolatedModules": true,
|
||||
"lib": ["es2022", "DOM", "DOM.Iterable"],
|
||||
"module": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"moduleResolution": "NodeNext",
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "ES2022"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"plugins": [{ "name": "next" }],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowJs": true,
|
||||
"jsx": "preserve",
|
||||
"noEmit": true
|
||||
}
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"plugins": [{ "name": "next" }],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowJs": true,
|
||||
"jsx": "preserve",
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"name": "@repo/typescript-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
"name": "@repo/typescript-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ export const Logo = ({
|
|||
className,
|
||||
id,
|
||||
}: {
|
||||
className?: string
|
||||
id?: string
|
||||
className?: string;
|
||||
id?: string;
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
|
|
@ -19,15 +19,15 @@ export const Logo = ({
|
|||
fill="#EFEFEF"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const LogoFull = ({
|
||||
className,
|
||||
id,
|
||||
}: {
|
||||
className?: string
|
||||
id?: string
|
||||
className?: string;
|
||||
id?: string;
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
|
|
@ -47,5 +47,5 @@ export const LogoFull = ({
|
|||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const OneDrive = ({ className }: { className?: string }) => (
|
|||
fill="#28A8EA"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const GoogleDrive = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -56,7 +56,7 @@ export const GoogleDrive = ({ className }: { className?: string }) => (
|
|||
fill="#FFBA00"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const Notion = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -71,7 +71,7 @@ export const Notion = ({ className }: { className?: string }) => (
|
|||
/>
|
||||
<path d="M164.09.608L16.092 11.538C4.155 12.573 0 20.374 0 29.726v162.245c0 7.284 2.585 13.516 8.826 21.843l34.789 45.237c5.715 7.284 10.912 8.844 21.825 8.327l171.864-10.404c14.532-1.035 18.696-7.801 18.696-19.24V55.207c0-5.911-2.336-7.614-9.21-12.66l-1.185-.856L198.37 8.409C186.94.1 182.27-.952 164.09.608M69.327 52.22c-14.033.945-17.216 1.159-25.186-5.323L23.876 30.778c-2.06-2.086-1.026-4.69 4.163-5.207l142.274-10.395c11.947-1.043 18.17 3.12 22.842 6.758l24.401 17.68c1.043.525 3.638 3.637.517 3.637L71.146 52.095zm-16.36 183.954V81.222c0-6.767 2.077-9.887 8.3-10.413L230.02 60.93c5.724-.517 8.31 3.12 8.31 9.879v153.917c0 6.767-1.044 12.49-10.387 13.008l-161.487 9.361c-9.343.517-13.489-2.594-13.489-10.921M212.377 89.53c1.034 4.681 0 9.362-4.681 9.897l-7.783 1.542v114.404c-6.758 3.637-12.981 5.715-18.18 5.715c-8.308 0-10.386-2.604-16.609-10.396l-50.898-80.079v77.476l16.1 3.646s0 9.362-12.989 9.362l-35.814 2.077c-1.043-2.086 0-7.284 3.63-8.318l9.351-2.595V109.823l-12.98-1.052c-1.044-4.68 1.55-11.439 8.826-11.965l38.426-2.585l52.958 81.113v-71.76l-13.498-1.552c-1.043-5.733 3.111-9.896 8.3-10.404z" />
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const GoogleDocs = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -85,7 +85,7 @@ export const GoogleDocs = ({ className }: { className?: string }) => (
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const GoogleSheets = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -99,7 +99,7 @@ export const GoogleSheets = ({ className }: { className?: string }) => (
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const GoogleSlides = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -113,7 +113,7 @@ export const GoogleSlides = ({ className }: { className?: string }) => (
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const NotionDoc = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -127,7 +127,7 @@ export const NotionDoc = ({ className }: { className?: string }) => (
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const MicrosoftWord = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -141,7 +141,7 @@ export const MicrosoftWord = ({ className }: { className?: string }) => (
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const MicrosoftExcel = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -155,7 +155,7 @@ export const MicrosoftExcel = ({ className }: { className?: string }) => (
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const MicrosoftPowerpoint = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -169,7 +169,7 @@ export const MicrosoftPowerpoint = ({ className }: { className?: string }) => (
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const MicrosoftOneNote = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -183,7 +183,7 @@ export const MicrosoftOneNote = ({ className }: { className?: string }) => (
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const PDF = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
|
|
@ -205,4 +205,4 @@ export const PDF = ({ className }: { className?: string }) => (
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
|
||||
"extends": "//",
|
||||
"linter": {
|
||||
"domains": {
|
||||
"next": "recommended",
|
||||
"react": "recommended"
|
||||
}
|
||||
}
|
||||
"root": false,
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { cn } from "@lib/utils"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { cn } from "@lib/utils";
|
||||
import { Button } from "@ui/components/button";
|
||||
|
||||
interface ExternalAuthButtonProps extends React.ComponentProps<typeof Button> {
|
||||
authProvider: string
|
||||
authIcon: React.ReactNode
|
||||
authProvider: string;
|
||||
authIcon: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ExternalAuthButton({
|
||||
|
|
@ -25,5 +25,5 @@ export function ExternalAuthButton({
|
|||
Continue with {authProvider}
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
|
|
@ -21,7 +21,7 @@ function AccordionItem({
|
|||
data-slot="accordion-item"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
|
|
@ -43,7 +43,7 @@ function AccordionTrigger({
|
|||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
|
|
@ -59,7 +59,7 @@ function AccordionContent({
|
|||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
import { buttonVariants } from "@ui/components/button"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
import { buttonVariants } from "@ui/components/button";
|
||||
import type * as React from "react";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
|
|
@ -16,7 +16,7 @@ function AlertDialogTrigger({
|
|||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
|
|
@ -24,7 +24,7 @@ function AlertDialogPortal({
|
|||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
|
|
@ -40,7 +40,7 @@ function AlertDialogOverlay({
|
|||
data-slot="alert-dialog-overlay"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
|
|
@ -59,7 +59,7 @@ function AlertDialogContent({
|
|||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
|
|
@ -72,7 +72,7 @@ function AlertDialogHeader({
|
|||
data-slot="alert-dialog-header"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
|
|
@ -88,7 +88,7 @@ function AlertDialogFooter({
|
|||
data-slot="alert-dialog-footer"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
|
|
@ -101,7 +101,7 @@ function AlertDialogTitle({
|
|||
data-slot="alert-dialog-title"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
|
|
@ -114,7 +114,7 @@ function AlertDialogDescription({
|
|||
data-slot="alert-dialog-description"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
|
|
@ -126,7 +126,7 @@ function AlertDialogAction({
|
|||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
|
|
@ -138,7 +138,7 @@ function AlertDialogCancel({
|
|||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -153,4 +153,4 @@ export {
|
|||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import type * as React from "react";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
|
|
@ -17,7 +17,7 @@ function Avatar({
|
|||
data-slot="avatar"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
|
|
@ -30,7 +30,7 @@ function AvatarImage({
|
|||
data-slot="avatar-image"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
|
|
@ -46,7 +46,7 @@ function AvatarFallback({
|
|||
data-slot="avatar-fallback"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { cn } from "@lib/utils"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
|
|
@ -22,7 +22,7 @@ const badgeVariants = cva(
|
|||
variant: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
|
|
@ -31,7 +31,7 @@ function Badge({
|
|||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
@ -39,7 +39,7 @@ function Badge({
|
|||
data-slot="badge"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { cn } from "@lib/utils"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
|
|
@ -17,7 +17,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|||
data-slot="breadcrumb-list"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
|
|
@ -27,7 +27,7 @@ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
|||
data-slot="breadcrumb-item"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
|
|
@ -35,9 +35,9 @@ function BreadcrumbLink({
|
|||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
@ -45,7 +45,7 @@ function BreadcrumbLink({
|
|||
data-slot="breadcrumb-link"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
|
|
@ -59,7 +59,7 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|||
tabIndex={0}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
|
|
@ -77,7 +77,7 @@ function BreadcrumbSeparator({
|
|||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
|
|
@ -95,7 +95,7 @@ function BreadcrumbEllipsis({
|
|||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -106,4 +106,4 @@ export {
|
|||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { cn } from "@lib/utils"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
|
|
@ -32,7 +32,7 @@ const buttonVariants = cva(
|
|||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
|
|
@ -42,9 +42,9 @@ function Button({
|
|||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
@ -52,7 +52,7 @@ function Button({
|
|||
data-slot="button"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { cn } from "@lib/utils"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import type * as React from "react";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
|
|
@ -11,7 +11,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -24,7 +24,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card-header"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -34,7 +34,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card-title"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -44,7 +44,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card-description"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -57,7 +57,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card-action"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -67,7 +67,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -77,7 +77,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card-footer"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -88,4 +88,4 @@ export {
|
|||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,44 +1,44 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { cn } from "@lib/utils";
|
||||
import { Button } from "@ui/components/button";
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
import * as React from "react"
|
||||
} from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
}
|
||||
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
|
|
@ -56,52 +56,52 @@ function Carousel({
|
|||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins,
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
if (!api) return;
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
)
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
if (!api || !setApi) return;
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
if (!api) return;
|
||||
onSelect(api);
|
||||
api.on("reInit", onSelect);
|
||||
api.on("select", onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
api?.off("select", onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
|
|
@ -126,11 +126,11 @@ function Carousel({
|
|||
{children}
|
||||
</section>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -147,11 +147,11 @@ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
|||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel()
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -165,7 +165,7 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
|||
role="group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
|
|
@ -174,7 +174,7 @@ function CarouselPrevious({
|
|||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -195,7 +195,7 @@ function CarouselPrevious({
|
|||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
|
|
@ -204,7 +204,7 @@ function CarouselNext({
|
|||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -225,7 +225,7 @@ function CarouselNext({
|
|||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -235,4 +235,4 @@ export {
|
|||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,36 +1,36 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
}
|
||||
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
|
|
@ -40,13 +40,13 @@ function ChartContainer({
|
|||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"]
|
||||
>["children"];
|
||||
}) {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
|
|
@ -65,16 +65,16 @@ function ChartContainer({
|
|||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color,
|
||||
)
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -89,8 +89,8 @@ ${colorConfig
|
|||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
|
|
@ -99,10 +99,10 @@ ${colorConfig
|
|||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
|
|
@ -120,40 +120,40 @@ function ChartTooltipContent({
|
|||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
|
|
@ -162,13 +162,13 @@ function ChartTooltipContent({
|
|||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -180,9 +180,9 @@ function ChartTooltipContent({
|
|||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -241,14 +241,14 @@ function ChartTooltipContent({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
|
|
@ -258,13 +258,13 @@ function ChartLegendContent({
|
|||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -276,8 +276,8 @@ function ChartLegendContent({
|
|||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const key = `${nameKey || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -298,10 +298,10 @@ function ChartLegendContent({
|
|||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
|
|
@ -311,7 +311,7 @@ function getPayloadConfigFromPayload(
|
|||
key: string,
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
|
|
@ -319,15 +319,15 @@ function getPayloadConfigFromPayload(
|
|||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
|
|
@ -335,12 +335,12 @@ function getPayloadConfigFromPayload(
|
|||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config]
|
||||
: config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -350,4 +350,4 @@ export {
|
|||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
|
|
@ -25,7 +25,7 @@ function Checkbox({
|
|||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
export { Checkbox };
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
|
|
@ -16,7 +16,7 @@ function CollapsibleTrigger({
|
|||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
|
|
@ -27,7 +27,7 @@ function CollapsibleContent({
|
|||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { cn } from "@lib/utils";
|
||||
import { Button } from "@ui/components/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
|
|
@ -9,25 +9,29 @@ import {
|
|||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@ui/components/command"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
|
||||
import { Check, ChevronsUpDown, X } from "lucide-react"
|
||||
import * as React from "react"
|
||||
} from "@ui/components/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@ui/components/popover";
|
||||
import { Check, ChevronsUpDown, X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
interface Option {
|
||||
value: string
|
||||
label: string
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ComboboxProps {
|
||||
options: Option[]
|
||||
onSelect: (value: string) => void
|
||||
onSubmit: (newName: string) => void
|
||||
selectedValues: string[]
|
||||
setSelectedValues: React.Dispatch<React.SetStateAction<string[]>>
|
||||
className?: string
|
||||
placeholder?: string
|
||||
triggerClassName?: string
|
||||
options: Option[];
|
||||
onSelect: (value: string) => void;
|
||||
onSubmit: (newName: string) => void;
|
||||
selectedValues: string[];
|
||||
setSelectedValues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
triggerClassName?: string;
|
||||
}
|
||||
|
||||
export function Combobox({
|
||||
|
|
@ -40,36 +44,38 @@ export function Combobox({
|
|||
placeholder = "Select...",
|
||||
triggerClassName,
|
||||
}: ComboboxProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [inputValue, setInputValue] = React.useState("")
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [inputValue, setInputValue] = React.useState("");
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
onSelect(value)
|
||||
setOpen(false)
|
||||
setInputValue("")
|
||||
}
|
||||
onSelect(value);
|
||||
setOpen(false);
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
if (inputValue.trim()) {
|
||||
onSubmit(inputValue)
|
||||
setOpen(false)
|
||||
setInputValue("")
|
||||
onSubmit(inputValue);
|
||||
setOpen(false);
|
||||
setInputValue("");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (valueToRemove: string) => {
|
||||
setSelectedValues((prev) => prev.filter((value) => value !== valueToRemove))
|
||||
}
|
||||
setSelectedValues((prev) =>
|
||||
prev.filter((value) => value !== valueToRemove),
|
||||
);
|
||||
};
|
||||
|
||||
const filteredOptions = options.filter(
|
||||
(option) => !selectedValues.includes(option.value),
|
||||
)
|
||||
);
|
||||
|
||||
const isNewValue =
|
||||
inputValue.trim() &&
|
||||
!options.some(
|
||||
(option) => option.label.toLowerCase() === inputValue.toLowerCase(),
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover onOpenChange={setOpen} open={open}>
|
||||
|
|
@ -87,7 +93,7 @@ export function Combobox({
|
|||
<div className="flex flex-wrap gap-1 items-center w-full">
|
||||
{selectedValues.length > 0 ? (
|
||||
selectedValues.map((value) => {
|
||||
const option = options.find((opt) => opt.value === value)
|
||||
const option = options.find((opt) => opt.value === value);
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-secondary text-sm rounded-md"
|
||||
|
|
@ -97,15 +103,15 @@ export function Combobox({
|
|||
<button
|
||||
className="hover:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleRemove(value)
|
||||
e.stopPropagation();
|
||||
handleRemove(value);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<span className="text-muted-foreground">{placeholder}</span>
|
||||
|
|
@ -157,5 +163,5 @@ export function Combobox({
|
|||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import { cn } from "@lib/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@ui/components/dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
import type * as React from "react"
|
||||
} from "@ui/components/dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
|
|
@ -25,7 +25,7 @@ function Command({
|
|||
data-slot="command"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
|
|
@ -36,10 +36,10 @@ function CommandDialog({
|
|||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
title?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
|
|
@ -56,7 +56,7 @@ function CommandDialog({
|
|||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
|
|
@ -78,7 +78,7 @@ function CommandInput({
|
|||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
|
|
@ -94,7 +94,7 @@ function CommandList({
|
|||
data-slot="command-list"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
|
|
@ -106,7 +106,7 @@ function CommandEmpty({
|
|||
data-slot="command-empty"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
|
|
@ -122,7 +122,7 @@ function CommandGroup({
|
|||
data-slot="command-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
|
|
@ -135,7 +135,7 @@ function CommandSeparator({
|
|||
data-slot="command-separator"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
|
|
@ -151,7 +151,7 @@ function CommandItem({
|
|||
data-slot="command-item"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
|
|
@ -167,7 +167,7 @@ function CommandShortcut({
|
|||
data-slot="command-shortcut"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -180,4 +180,4 @@ export {
|
|||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
|
|
@ -42,7 +42,7 @@ function DialogOverlay({
|
|||
data-slot="dialog-overlay"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
|
|
@ -51,7 +51,7 @@ function DialogContent({
|
|||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
|
|
@ -76,7 +76,7 @@ function DialogContent({
|
|||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -86,7 +86,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="dialog-header"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -99,7 +99,7 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="dialog-footer"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
|
|
@ -112,7 +112,7 @@ function DialogTitle({
|
|||
data-slot="dialog-title"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
|
|
@ -125,7 +125,7 @@ function DialogDescription({
|
|||
data-slot="dialog-description"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -139,4 +139,4 @@ export {
|
|||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import type * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
import { cn } from "@lib/utils";
|
||||
import type * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
|
|
@ -41,7 +41,7 @@ function DrawerOverlay({
|
|||
data-slot="drawer-overlay"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
|
|
@ -68,7 +68,7 @@ function DrawerContent({
|
|||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -81,7 +81,7 @@ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="drawer-header"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -91,7 +91,7 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="drawer-footer"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
|
|
@ -104,7 +104,7 @@ function DrawerTitle({
|
|||
data-slot="drawer-title"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
|
|
@ -117,7 +117,7 @@ function DrawerDescription({
|
|||
data-slot="drawer-description"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -131,4 +131,4 @@ export {
|
|||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
|
|
@ -16,7 +16,7 @@ function DropdownMenuPortal({
|
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
|
|
@ -27,7 +27,7 @@ function DropdownMenuTrigger({
|
|||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
|
|
@ -47,7 +47,7 @@ function DropdownMenuContent({
|
|||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
|
|
@ -55,7 +55,7 @@ function DropdownMenuGroup({
|
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
|
|
@ -64,8 +64,8 @@ function DropdownMenuItem({
|
|||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
|
|
@ -78,7 +78,7 @@ function DropdownMenuItem({
|
|||
data-variant={variant}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
|
|
@ -104,7 +104,7 @@ function DropdownMenuCheckboxItem({
|
|||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
|
|
@ -115,7 +115,7 @@ function DropdownMenuRadioGroup({
|
|||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
|
|
@ -139,7 +139,7 @@ function DropdownMenuRadioItem({
|
|||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
|
|
@ -147,7 +147,7 @@ function DropdownMenuLabel({
|
|||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
|
|
@ -159,7 +159,7 @@ function DropdownMenuLabel({
|
|||
data-slot="dropdown-menu-label"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
|
|
@ -172,7 +172,7 @@ function DropdownMenuSeparator({
|
|||
data-slot="dropdown-menu-separator"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
|
|
@ -188,13 +188,13 @@ function DropdownMenuShortcut({
|
|||
data-slot="dropdown-menu-shortcut"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
|
|
@ -203,7 +203,7 @@ function DropdownMenuSubTrigger({
|
|||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
|
|
@ -218,7 +218,7 @@ function DropdownMenuSubTrigger({
|
|||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
|
|
@ -234,7 +234,7 @@ function DropdownMenuSubContent({
|
|||
data-slot="dropdown-menu-sub-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -253,4 +253,4 @@ export {
|
|||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { cn } from "@lib/utils"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import type * as React from "react";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
|
|
@ -14,7 +14,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||
type={type}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import type * as React from "react";
|
||||
|
||||
function Label({
|
||||
className,
|
||||
|
|
@ -17,7 +17,7 @@ function Label({
|
|||
data-slot="label"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import type * as React from "react";
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
|
|
@ -35,13 +35,13 @@ function PopoverContent({
|
|||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
import type * as React from "react";
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
|
|
@ -24,7 +24,7 @@ function Progress({
|
|||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress }
|
||||
export { Progress };
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
import type * as React from "react";
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
|
|
@ -24,7 +24,7 @@ function ScrollArea({
|
|||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
|
|
@ -51,7 +51,7 @@ function ScrollBar({
|
|||
data-slot="scroll-area-thumb"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
export { ScrollArea, ScrollBar };
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
|
|
@ -29,7 +29,7 @@ function SelectTrigger({
|
|||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
size?: "sm" | "default";
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
|
|
@ -46,7 +46,7 @@ function SelectTrigger({
|
|||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
|
|
@ -81,7 +81,7 @@ function SelectContent({
|
|||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
|
|
@ -94,7 +94,7 @@ function SelectLabel({
|
|||
data-slot="select-label"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
|
|
@ -118,7 +118,7 @@ function SelectItem({
|
|||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
|
|
@ -131,7 +131,7 @@ function SelectSeparator({
|
|||
data-slot="select-separator"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
|
|
@ -149,7 +149,7 @@ function SelectScrollUpButton({
|
|||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
|
|
@ -167,7 +167,7 @@ function SelectScrollDownButton({
|
|||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -181,4 +181,4 @@ export {
|
|||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import type * as React from "react";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
|
|
@ -21,7 +21,7 @@ function Separator({
|
|||
orientation={orientation}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
|
|
|
|||
|
|
@ -1,48 +1,48 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { UploadIcon } from "lucide-react"
|
||||
import type { ReactNode } from "react"
|
||||
import { createContext, useContext } from "react"
|
||||
import type { DropEvent, DropzoneOptions, FileRejection } from "react-dropzone"
|
||||
import { useDropzone } from "react-dropzone"
|
||||
import { cn } from "@lib/utils";
|
||||
import { Button } from "@ui/components/button";
|
||||
import { UploadIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import type { DropEvent, DropzoneOptions, FileRejection } from "react-dropzone";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
|
||||
type DropzoneContextType = {
|
||||
src?: File[]
|
||||
accept?: DropzoneOptions["accept"]
|
||||
maxSize?: DropzoneOptions["maxSize"]
|
||||
minSize?: DropzoneOptions["minSize"]
|
||||
maxFiles?: DropzoneOptions["maxFiles"]
|
||||
}
|
||||
src?: File[];
|
||||
accept?: DropzoneOptions["accept"];
|
||||
maxSize?: DropzoneOptions["maxSize"];
|
||||
minSize?: DropzoneOptions["minSize"];
|
||||
maxFiles?: DropzoneOptions["maxFiles"];
|
||||
};
|
||||
|
||||
const renderBytes = (bytes: number) => {
|
||||
const units = ["B", "KB", "MB", "GB", "TB", "PB"]
|
||||
let size = bytes
|
||||
let unitIndex = 0
|
||||
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)}${units[unitIndex]}`
|
||||
}
|
||||
return `${size.toFixed(2)}${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
const DropzoneContext = createContext<DropzoneContextType | undefined>(
|
||||
undefined,
|
||||
)
|
||||
);
|
||||
|
||||
export type DropzoneProps = Omit<DropzoneOptions, "onDrop"> & {
|
||||
src?: File[]
|
||||
className?: string
|
||||
src?: File[];
|
||||
className?: string;
|
||||
onDrop?: (
|
||||
acceptedFiles: File[],
|
||||
fileRejections: FileRejection[],
|
||||
event: DropEvent,
|
||||
) => void
|
||||
children?: ReactNode
|
||||
}
|
||||
) => void;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const Dropzone = ({
|
||||
accept,
|
||||
|
|
@ -66,15 +66,15 @@ export const Dropzone = ({
|
|||
disabled,
|
||||
onDrop: (acceptedFiles, fileRejections, event) => {
|
||||
if (fileRejections.length > 0) {
|
||||
const message = fileRejections.at(0)?.errors.at(0)?.message
|
||||
onError?.(new Error(message))
|
||||
return
|
||||
const message = fileRejections.at(0)?.errors.at(0)?.message;
|
||||
onError?.(new Error(message));
|
||||
return;
|
||||
}
|
||||
|
||||
onDrop?.(acceptedFiles, fileRejections, event)
|
||||
onDrop?.(acceptedFiles, fileRejections, event);
|
||||
},
|
||||
...props,
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<DropzoneContext.Provider
|
||||
|
|
@ -96,38 +96,38 @@ export const Dropzone = ({
|
|||
{children}
|
||||
</Button>
|
||||
</DropzoneContext.Provider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const useDropzoneContext = () => {
|
||||
const context = useContext(DropzoneContext)
|
||||
const context = useContext(DropzoneContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useDropzoneContext must be used within a Dropzone")
|
||||
throw new Error("useDropzoneContext must be used within a Dropzone");
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export type DropzoneContentProps = {
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const maxLabelItems = 1
|
||||
const maxLabelItems = 1;
|
||||
|
||||
export const DropzoneContent = ({
|
||||
children,
|
||||
className,
|
||||
}: DropzoneContentProps) => {
|
||||
const { src } = useDropzoneContext()
|
||||
const { src } = useDropzoneContext();
|
||||
|
||||
if (!src) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
if (children) {
|
||||
return children
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -146,41 +146,41 @@ export const DropzoneContent = ({
|
|||
Drag and drop or click to replace
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export type DropzoneEmptyStateProps = {
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const DropzoneEmptyState = ({
|
||||
children,
|
||||
className,
|
||||
}: DropzoneEmptyStateProps) => {
|
||||
const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext()
|
||||
const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext();
|
||||
|
||||
if (src) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
if (children) {
|
||||
return children
|
||||
return children;
|
||||
}
|
||||
|
||||
let caption = ""
|
||||
let caption = "";
|
||||
|
||||
if (accept) {
|
||||
caption += "Accepts "
|
||||
caption += new Intl.ListFormat("en").format(Object.keys(accept))
|
||||
caption += "Accepts ";
|
||||
caption += new Intl.ListFormat("en").format(Object.keys(accept));
|
||||
}
|
||||
|
||||
if (minSize && maxSize) {
|
||||
caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`
|
||||
caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`;
|
||||
} else if (minSize) {
|
||||
caption += ` at least ${renderBytes(minSize)}`
|
||||
caption += ` at least ${renderBytes(minSize)}`;
|
||||
} else if (maxSize) {
|
||||
caption += ` less than ${renderBytes(maxSize)}`
|
||||
caption += ` less than ${renderBytes(maxSize)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -198,5 +198,5 @@ export const DropzoneEmptyState = ({
|
|||
<p className="text-wrap text-muted-foreground text-xs">{caption}.</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
|
|
@ -40,7 +40,7 @@ function SheetOverlay({
|
|||
data-slot="sheet-overlay"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
|
|
@ -49,7 +49,7 @@ function SheetContent({
|
|||
side = "right",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
|
|
@ -77,7 +77,7 @@ function SheetContent({
|
|||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -87,7 +87,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="sheet-header"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -97,7 +97,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="sheet-footer"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
|
|
@ -110,7 +110,7 @@ function SheetTitle({
|
|||
data-slot="sheet-title"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
|
|
@ -123,7 +123,7 @@ function SheetDescription({
|
|||
data-slot="sheet-description"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -135,4 +135,4 @@ export {
|
|||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,55 +1,55 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import { cn } from "@lib/utils"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { Input } from "@ui/components/input"
|
||||
import { Separator } from "@ui/components/separator"
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import { cn } from "@lib/utils";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { Button } from "@ui/components/button";
|
||||
import { Input } from "@ui/components/input";
|
||||
import { Separator } from "@ui/components/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@ui/components/sheet"
|
||||
import { Skeleton } from "@ui/components/skeleton"
|
||||
} from "@ui/components/sheet";
|
||||
import { Skeleton } from "@ui/components/skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@ui/components/tooltip"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
import * as React from "react"
|
||||
} from "@ui/components/tooltip";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
state: "expanded" | "collapsed";
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||
}
|
||||
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
|
|
@ -61,36 +61,36 @@ function SidebarProvider({
|
|||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
const openState = typeof value === "function" ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState)
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open],
|
||||
)
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||
}, [isMobile, setOpen])
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
||||
}, [isMobile, setOpen]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
|
|
@ -99,18 +99,18 @@ function SidebarProvider({
|
|||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [toggleSidebar])
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
const state = open ? "expanded" : "collapsed";
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
|
|
@ -123,7 +123,7 @@ function SidebarProvider({
|
|||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, toggleSidebar],
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
|
|
@ -147,7 +147,7 @@ function SidebarProvider({
|
|||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
|
|
@ -158,11 +158,11 @@ function Sidebar({
|
|||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right"
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
|
|
@ -176,7 +176,7 @@ function Sidebar({
|
|||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
|
|
@ -201,7 +201,7 @@ function Sidebar({
|
|||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -249,7 +249,7 @@ function Sidebar({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
|
|
@ -257,7 +257,7 @@ function SidebarTrigger({
|
|||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -265,8 +265,8 @@ function SidebarTrigger({
|
|||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
|
@ -275,11 +275,11 @@ function SidebarTrigger({
|
|||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
|
|
@ -300,7 +300,7 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
|||
title="Toggle Sidebar"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
|
|
@ -314,7 +314,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
|||
data-slot="sidebar-inset"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
|
|
@ -328,7 +328,7 @@ function SidebarInput({
|
|||
data-slot="sidebar-input"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -339,7 +339,7 @@ function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="sidebar-header"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -350,7 +350,7 @@ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="sidebar-footer"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
|
|
@ -364,7 +364,7 @@ function SidebarSeparator({
|
|||
data-slot="sidebar-separator"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -378,7 +378,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="sidebar-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -389,7 +389,7 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="sidebar-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
|
|
@ -397,7 +397,7 @@ function SidebarGroupLabel({
|
|||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
@ -410,7 +410,7 @@ function SidebarGroupLabel({
|
|||
data-slot="sidebar-group-label"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
|
|
@ -418,7 +418,7 @@ function SidebarGroupAction({
|
|||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
@ -433,7 +433,7 @@ function SidebarGroupAction({
|
|||
data-slot="sidebar-group-action"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
|
|
@ -447,7 +447,7 @@ function SidebarGroupContent({
|
|||
data-slot="sidebar-group-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
|
|
@ -458,7 +458,7 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
|||
data-slot="sidebar-menu"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
|
|
@ -469,7 +469,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|||
data-slot="sidebar-menu-item"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
|
|
@ -492,7 +492,7 @@ const sidebarMenuButtonVariants = cva(
|
|||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
function SidebarMenuButton({
|
||||
asChild = false,
|
||||
|
|
@ -503,12 +503,12 @@ function SidebarMenuButton({
|
|||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
|
|
@ -519,16 +519,16 @@ function SidebarMenuButton({
|
|||
data-slot="sidebar-menu-button"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -541,7 +541,7 @@ function SidebarMenuButton({
|
|||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuAction({
|
||||
|
|
@ -550,10 +550,10 @@ function SidebarMenuAction({
|
|||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
@ -573,7 +573,7 @@ function SidebarMenuAction({
|
|||
data-slot="sidebar-menu-action"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
|
|
@ -595,7 +595,7 @@ function SidebarMenuBadge({
|
|||
data-slot="sidebar-menu-badge"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
|
|
@ -603,12 +603,12 @@ function SidebarMenuSkeleton({
|
|||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
showIcon?: boolean;
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
}, [])
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -633,7 +633,7 @@ function SidebarMenuSkeleton({
|
|||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
|
|
@ -648,7 +648,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
|||
data-slot="sidebar-menu-sub"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
|
|
@ -662,7 +662,7 @@ function SidebarMenuSubItem({
|
|||
data-slot="sidebar-menu-sub-item"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
|
|
@ -672,11 +672,11 @@ function SidebarMenuSubButton({
|
|||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
size?: "sm" | "md"
|
||||
isActive?: boolean
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
@ -694,7 +694,7 @@ function SidebarMenuSubButton({
|
|||
data-slot="sidebar-menu-sub-button"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -722,4 +722,4 @@ export {
|
|||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cn } from "@lib/utils"
|
||||
import { cn } from "@lib/utils";
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
|
|
@ -7,7 +7,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="skeleton"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
export { Skeleton };
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
|
|
@ -19,7 +19,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||
theme={theme as ToasterProps["theme"]}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster }
|
||||
export { Toaster };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@lib/utils";
|
||||
import type * as React from "react";
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
|
|
@ -15,7 +15,7 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
|
|
@ -25,7 +25,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|||
data-slot="table-header"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
|
|
@ -35,7 +35,7 @@ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|||
data-slot="table-body"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
|
|
@ -48,7 +48,7 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|||
data-slot="table-footer"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
|
|
@ -61,7 +61,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|||
data-slot="table-row"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
|
|
@ -74,7 +74,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|||
data-slot="table-head"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
|
|
@ -87,7 +87,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|||
data-slot="table-cell"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
|
|
@ -100,7 +100,7 @@ function TableCaption({
|
|||
data-slot="table-caption"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -112,4 +112,4 @@ export {
|
|||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue