mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
Merge pull request #377 from supermemoryai/mahesh/update-memory-detail-view
ui (memory detail): improved memory detail view and open chat
This commit is contained in:
commit
d48d2bc07c
6 changed files with 797 additions and 758 deletions
|
|
@ -388,284 +388,284 @@ const MemoryGraphPage = () => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative h-screen bg-[#0f1419] overflow-hidden touch-none">
|
||||
{/* Main content area */}
|
||||
<motion.div
|
||||
animate={{
|
||||
marginRight: isOpen && !isMobile ? 600 : 0,
|
||||
}}
|
||||
className="h-full relative"
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: [0.4, 0, 0.2, 1], // Material Design easing - snappy but smooth
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="absolute md:top-4 md:right-4 md:bottom-auto md:left-auto bottom-8 left-6 z-20 rounded-xl overflow-hidden"
|
||||
id={TOUR_STEP_IDS.VIEW_TOGGLE}
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
||||
>
|
||||
<GlassMenuEffect rounded="rounded-xl" />
|
||||
<div className="relative z-10 p-2 flex gap-1">
|
||||
<motion.button
|
||||
animate={{
|
||||
color: viewMode === "graph" ? "#93c5fd" : "#cbd5e1",
|
||||
}}
|
||||
className="relative h-8 px-3 flex items-center gap-2 text-sm font-medium rounded-md transition-colors"
|
||||
onClick={() => handleViewModeChange("graph")}
|
||||
transition={{ duration: 0.2 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{viewMode === "graph" && (
|
||||
<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">
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
<span className="hidden md:inline">Graph</span>
|
||||
</span>
|
||||
</motion.button>
|
||||
<div className="relative h-screen bg-[#0f1419] overflow-hidden touch-none">
|
||||
{/* Main content area */}
|
||||
<motion.div
|
||||
animate={{
|
||||
marginRight: isOpen && !isMobile ? 600 : 0,
|
||||
}}
|
||||
className="h-full relative"
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: [0.4, 0, 0.2, 1], // Material Design easing - snappy but smooth
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="absolute md:top-4 md:right-4 md:bottom-auto md:left-auto bottom-8 left-6 z-20 rounded-xl overflow-hidden"
|
||||
id={TOUR_STEP_IDS.VIEW_TOGGLE}
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||
>
|
||||
<GlassMenuEffect rounded="rounded-xl" />
|
||||
<div className="relative z-10 p-2 flex gap-1">
|
||||
<motion.button
|
||||
animate={{
|
||||
color: viewMode === 'graph' ? '#93c5fd' : '#cbd5e1',
|
||||
}}
|
||||
className="relative h-8 px-3 flex items-center gap-2 text-sm font-medium rounded-md transition-colors"
|
||||
onClick={() => handleViewModeChange('graph')}
|
||||
transition={{ duration: 0.2 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{viewMode === 'graph' && (
|
||||
<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">
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
<span className="hidden md:inline">Graph</span>
|
||||
</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
|
||||
documents={allDocuments}
|
||||
error={error}
|
||||
hasMore={hasMore}
|
||||
isLoading={isPending}
|
||||
isLoadingMore={isLoadingMore}
|
||||
legendId={TOUR_STEP_IDS.LEGEND}
|
||||
loadMoreDocuments={loadMoreDocuments}
|
||||
showSpacesSelector={false}
|
||||
totalLoaded={totalLoaded}
|
||||
variant="consumer"
|
||||
highlightDocumentIds={allHighlightDocumentIds}
|
||||
highlightsVisible={isOpen}
|
||||
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>
|
||||
</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}
|
||||
>
|
||||
<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>
|
||||
</MemoryListView>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* 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}
|
||||
isLoading={isPending}
|
||||
isLoadingMore={isLoadingMore}
|
||||
legendId={TOUR_STEP_IDS.LEGEND}
|
||||
loadMoreDocuments={loadMoreDocuments}
|
||||
showSpacesSelector={false}
|
||||
totalLoaded={totalLoaded}
|
||||
variant="consumer"
|
||||
highlightDocumentIds={allHighlightDocumentIds}
|
||||
highlightsVisible={isOpen}
|
||||
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>
|
||||
</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}
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<div>
|
||||
<Menu />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Menu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Open Chat Button */}
|
||||
{!isOpen && !isMobile && (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="fixed bottom-6 right-6 z-50"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 25,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className="h-14 px-4 bg-blue-600 hover:bg-blue-700 text-white shadow-lg hover:shadow-xl transition-all duration-200 rounded-full flex items-center gap-2"
|
||||
onClick={() => setIsOpen(true)}
|
||||
size="lg"
|
||||
>
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
<span className="font-medium">Open Chat</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
{/* Floating Open Chat Button */}
|
||||
{!isOpen && !isMobile && (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="fixed bottom-6 right-6 z-50"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 25,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className="px-4 bg-white hover:bg-white/80 text-[#001A39] shadow-lg hover:shadow-xl transition-all duration-200 rounded-full flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => setIsOpen(true)}
|
||||
size="lg"
|
||||
>
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
<span className="font-medium">Open Chat</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Chat panel - positioned absolutely */}
|
||||
<motion.div
|
||||
className="fixed top-0 right-0 h-full z-50 md:z-auto"
|
||||
style={{
|
||||
width: isOpen ? (isMobile ? "100vw" : "600px") : 0,
|
||||
pointerEvents: isOpen ? "auto" : "none",
|
||||
}}
|
||||
id={TOUR_STEP_IDS.FLOATING_CHAT}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ x: isOpen ? 0 : isMobile ? "100%" : 600 }}
|
||||
className="absolute inset-0"
|
||||
exit={{ x: isMobile ? "100%" : 600 }}
|
||||
initial={{ x: isMobile ? "100%" : 600 }}
|
||||
key="chat"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 40,
|
||||
}}
|
||||
>
|
||||
<ChatRewrite />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
{/* Chat panel - positioned absolutely */}
|
||||
<motion.div
|
||||
className="fixed top-0 right-0 h-full z-50 md:z-auto"
|
||||
style={{
|
||||
width: isOpen ? (isMobile ? '100vw' : '600px') : 0,
|
||||
pointerEvents: isOpen ? 'auto' : 'none',
|
||||
}}
|
||||
id={TOUR_STEP_IDS.FLOATING_CHAT}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ x: isOpen ? 0 : isMobile ? '100%' : 600 }}
|
||||
className="absolute inset-0"
|
||||
exit={{ x: isMobile ? '100%' : 600 }}
|
||||
initial={{ x: isMobile ? '100%' : 600 }}
|
||||
key="chat"
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 500,
|
||||
damping: 40,
|
||||
}}
|
||||
>
|
||||
<ChatRewrite />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{showAddMemoryView && (
|
||||
<AddMemoryView
|
||||
initialTab="note"
|
||||
onClose={() => setShowAddMemoryView(false)}
|
||||
/>
|
||||
)}
|
||||
{showAddMemoryView && (
|
||||
<AddMemoryView
|
||||
initialTab="note"
|
||||
onClose={() => setShowAddMemoryView(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tour Alert Dialog */}
|
||||
<TourAlertDialog onOpenChange={setShowTourDialog} open={showTourDialog} />
|
||||
{/* Tour Alert Dialog */}
|
||||
<TourAlertDialog onOpenChange={setShowTourDialog} open={showTourDialog} />
|
||||
|
||||
{/* Referral/Upgrade Modal */}
|
||||
<ReferralUpgradeModal
|
||||
isOpen={showReferralModal}
|
||||
onClose={() => setShowReferralModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
{/* Referral/Upgrade Modal */}
|
||||
<ReferralUpgradeModal
|
||||
isOpen={showReferralModal}
|
||||
onClose={() => setShowReferralModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Wrapper component to handle auth and waitlist checks
|
||||
|
|
|
|||
53
apps/web/components/memories/index.tsx
Normal file
53
apps/web/components/memories/index.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { DocumentWithMemories } from "@ui/memory-graph/types";
|
||||
|
||||
export 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 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);
|
||||
|
||||
if (dateYear !== currentYear) {
|
||||
return `${month} ${formattedDay}, ${dateYear}`;
|
||||
}
|
||||
|
||||
return `${month} ${formattedDay}`;
|
||||
};
|
||||
|
||||
export const getSourceUrl = (document: DocumentWithMemories) => {
|
||||
if (document.type === "google_doc" && 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}`;
|
||||
}
|
||||
if (document.type === "google_slide" && document.customId) {
|
||||
return `https://docs.google.com/presentation/d/${document.customId}`;
|
||||
}
|
||||
// Fallback to existing URL for all other document types
|
||||
return document.url;
|
||||
};
|
||||
415
apps/web/components/memories/memory-detail.tsx
Normal file
415
apps/web/components/memories/memory-detail.tsx
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
import { getDocumentIcon } from '@/lib/document-icon';
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from '@repo/ui/components/drawer';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@repo/ui/components/sheet';
|
||||
import {
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from '@repo/ui/components/tabs';
|
||||
import { colors } from '@repo/ui/memory-graph/constants';
|
||||
import type { DocumentsWithMemoriesResponseSchema } from '@repo/validation/api';
|
||||
import { Badge } from '@ui/components/badge';
|
||||
import { Brain, Calendar, CircleUserRound, ExternalLink, List, Sparkles } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import type { z } from 'zod';
|
||||
import { formatDate, getSourceUrl } from '.';
|
||||
import { Label1Regular } from '@ui/text/label/label-1-regular';
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
|
||||
type DocumentWithMemories = DocumentsResponse['documents'][0];
|
||||
type MemoryEntry = DocumentWithMemories['memoryEntries'][0];
|
||||
|
||||
const formatDocumentType = (type: string) => {
|
||||
// Special case for 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(' ');
|
||||
};
|
||||
|
||||
const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => {
|
||||
return (
|
||||
<button
|
||||
className="p-4 rounded-lg transition-all relative overflow-hidden cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: memory.isLatest
|
||||
? colors.memory.primary
|
||||
: 'rgba(255, 255, 255, 0.02)',
|
||||
}}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-start gap-2 relative z-10">
|
||||
<div
|
||||
className="p-1 rounded"
|
||||
style={{
|
||||
backgroundColor: memory.isLatest
|
||||
? colors.memory.secondary
|
||||
: 'transparent',
|
||||
}}
|
||||
>
|
||||
<Brain
|
||||
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">
|
||||
<Label1Regular
|
||||
className="text-sm leading-relaxed text-left"
|
||||
style={{ color: colors.text.primary }}
|
||||
>
|
||||
{memory.memory}
|
||||
</Label1Regular>
|
||||
<div className="flex gap-2 justify-between">
|
||||
<div
|
||||
className="flex items-center gap-4 text-xs"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{formatDate(memory.createdAt)}
|
||||
</span>
|
||||
<span className="font-mono">v{memory.version}</span>
|
||||
{memory.sourceRelevanceScore && (
|
||||
<span
|
||||
className="flex items-center gap-1"
|
||||
style={{
|
||||
color:
|
||||
memory.sourceRelevanceScore > 70
|
||||
? colors.accent.emerald
|
||||
: colors.text.muted,
|
||||
}}
|
||||
>
|
||||
<Sparkles className="w-3 h-3" />
|
||||
{memory.sourceRelevanceScore}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{memory.isForgotten && (
|
||||
<Badge
|
||||
className="text-xs border-red-500/30 backdrop-blur-sm"
|
||||
style={{
|
||||
backgroundColor: colors.status.forgotten,
|
||||
color: '#dc2626',
|
||||
backdropFilter: 'blur(4px)',
|
||||
WebkitBackdropFilter: 'blur(4px)',
|
||||
}}
|
||||
variant="destructive"
|
||||
>
|
||||
Forgotten
|
||||
</Badge>
|
||||
)}
|
||||
{memory.isLatest && (
|
||||
<Badge
|
||||
className="text-xs"
|
||||
style={{
|
||||
backgroundColor: colors.memory.secondary,
|
||||
color: colors.text.primary,
|
||||
backdropFilter: 'blur(4px)',
|
||||
WebkitBackdropFilter: 'blur(4px)',
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
Latest
|
||||
</Badge>
|
||||
)}
|
||||
{memory.forgetAfter && (
|
||||
<Badge
|
||||
className="text-xs backdrop-blur-sm"
|
||||
style={{
|
||||
color: colors.status.expiring,
|
||||
backgroundColor: 'rgba(251, 165, 36, 0.1)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
WebkitBackdropFilter: 'blur(4px)',
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Expires: {formatDate(memory.forgetAfter)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
export const MemoryDetail = memo(
|
||||
({
|
||||
document,
|
||||
isOpen,
|
||||
onClose,
|
||||
isMobile,
|
||||
}: {
|
||||
document: DocumentWithMemories | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
isMobile: boolean;
|
||||
}) => {
|
||||
if (!document) return null;
|
||||
|
||||
const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten);
|
||||
const forgottenMemories = document.memoryEntries.filter(
|
||||
(m) => m.isForgotten
|
||||
);
|
||||
|
||||
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
|
||||
className="p-2 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: colors.background.secondary,
|
||||
}}
|
||||
>
|
||||
{getDocumentIcon(document.type, 'w-5 h-5')}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TitleComponent style={{ color: colors.text.primary }}>
|
||||
{document.title || 'Untitled Document'}
|
||||
</TitleComponent>
|
||||
<div
|
||||
className="flex items-center gap-2 mt-1 text-xs"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
<span>{formatDocumentType(document.type)}</span>
|
||||
<span>•</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');
|
||||
}}
|
||||
style={{ color: colors.accent.primary }}
|
||||
type="button"
|
||||
>
|
||||
View source
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ContentAndSummarySection = () => {
|
||||
const hasContent = document.content && document.content.trim().length > 0;
|
||||
const hasSummary = document.summary && document.summary.trim().length > 0;
|
||||
|
||||
if (!hasContent && !hasSummary) return null;
|
||||
|
||||
const defaultTab = hasContent ? 'content' : 'summary';
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Tabs defaultValue={defaultTab} className="w-full">
|
||||
<TabsList
|
||||
className={`grid w-full bg-white/5 border border-white/10 h-11 ${
|
||||
hasContent && hasSummary ? 'grid-cols-2' : 'grid-cols-1'
|
||||
}`}
|
||||
>
|
||||
{hasContent && (
|
||||
<TabsTrigger
|
||||
value="content"
|
||||
className="text-xs bg-transparent h-8"
|
||||
style={{ color: colors.text.secondary }}
|
||||
>
|
||||
<CircleUserRound className="w-3 h-3" />
|
||||
Original Content
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{hasSummary && (
|
||||
<TabsTrigger
|
||||
value="summary"
|
||||
className="text-xs flex items-center gap-1 bg-transparent h-8"
|
||||
style={{ color: colors.text.secondary }}
|
||||
>
|
||||
<List className="w-3 h-3" />
|
||||
Summary
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
{hasContent && (
|
||||
<TabsContent value="content" className="mt-3">
|
||||
<div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-white/[0.03] border border-white/[0.08]">
|
||||
<p
|
||||
className="text-sm leading-relaxed whitespace-pre-wrap"
|
||||
style={{ color: colors.text.primary }}
|
||||
>
|
||||
{document.content}
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{hasSummary && (
|
||||
<TabsContent value="summary" className="mt-3">
|
||||
<div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-indigo-500/5 border border-indigo-500/15">
|
||||
<p
|
||||
className="text-sm leading-relaxed whitespace-pre-wrap"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
{document.summary}
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoryContent = () => (
|
||||
<div className="space-y-6 px-6">
|
||||
{activeMemories.length > 0 && (
|
||||
<div>
|
||||
<div
|
||||
className="text-sm font-medium mb-2 flex items-start gap-2 py-2"
|
||||
style={{
|
||||
color: colors.text.secondary,
|
||||
}}
|
||||
>
|
||||
Active Memories ({activeMemories.length})
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{activeMemories.map((memory) => (
|
||||
<div
|
||||
key={memory.id}
|
||||
>
|
||||
<MemoryDetailItem memory={memory} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{forgottenMemories.length > 0 && (
|
||||
<div>
|
||||
<div
|
||||
className="text-sm font-medium mb-4 px-3 py-2 rounded-lg opacity-60"
|
||||
style={{
|
||||
color: colors.text.muted,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
}}
|
||||
>
|
||||
Forgotten Memories ({forgottenMemories.length})
|
||||
</div>
|
||||
<div className="space-y-3 opacity-40">
|
||||
{forgottenMemories.map((memory) => (
|
||||
<MemoryDetailItem key={memory.id} memory={memory} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeMemories.length === 0 && forgottenMemories.length === 0 && (
|
||||
<div
|
||||
className="text-center py-12 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
}}
|
||||
>
|
||||
<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 (
|
||||
<Drawer onOpenChange={onClose} open={isOpen}>
|
||||
<DrawerContent
|
||||
className="border-0 p-0 overflow-hidden max-h-[90vh]"
|
||||
style={{
|
||||
backgroundColor: colors.background.secondary,
|
||||
borderTop: `1px solid ${colors.document.border}`,
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
}}
|
||||
>
|
||||
{/* Header section with glass effect */}
|
||||
<div
|
||||
className="p-4 relative border-b"
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
borderBottom: `1px solid ${colors.document.border}`,
|
||||
}}
|
||||
>
|
||||
<DrawerHeader className="pb-0 px-0 text-left">
|
||||
<HeaderContent TitleComponent={DrawerTitle} />
|
||||
</DrawerHeader>
|
||||
|
||||
<ContentAndSummarySection />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 memory-drawer-scroll overflow-y-auto">
|
||||
<MemoryContent />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet onOpenChange={onClose} open={isOpen}>
|
||||
<SheetContent
|
||||
className="w-full sm:max-w-2xl border-0 p-0 overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: colors.background.secondary,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="p-6 relative"
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
}}
|
||||
>
|
||||
<SheetHeader className="pb-0">
|
||||
<HeaderContent TitleComponent={SheetTitle} />
|
||||
</SheetHeader>
|
||||
|
||||
<ContentAndSummarySection />
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100vh-200px)] memory-sheet-scroll overflow-y-auto">
|
||||
<MemoryContent />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -2,42 +2,14 @@
|
|||
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import { cn } from "@lib/utils";
|
||||
import {
|
||||
GoogleDocs,
|
||||
GoogleDrive,
|
||||
GoogleSheets,
|
||||
GoogleSlides,
|
||||
MicrosoftExcel,
|
||||
MicrosoftOneNote,
|
||||
MicrosoftPowerpoint,
|
||||
MicrosoftWord,
|
||||
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 {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} 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";
|
||||
|
|
@ -45,9 +17,12 @@ import type { z } from "zod";
|
|||
import useResizeObserver from "@/hooks/use-resize-observer";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
|
||||
import { MemoryDetail } from "./memories/memory-detail";
|
||||
import { getDocumentIcon } from "@/lib/document-icon";
|
||||
import { formatDate, getSourceUrl } from "./memories";
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0];
|
||||
type MemoryEntry = DocumentWithMemories["memoryEntries"][0];
|
||||
|
||||
interface MemoryListViewProps {
|
||||
children?: React.ReactNode;
|
||||
|
|
@ -85,222 +60,6 @@ const GreetingMessage = memo(() => {
|
|||
);
|
||||
});
|
||||
|
||||
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 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);
|
||||
|
||||
if (dateYear !== currentYear) {
|
||||
return `${month} ${formattedDay}, ${dateYear}`;
|
||||
}
|
||||
|
||||
return `${month} ${formattedDay}`;
|
||||
};
|
||||
|
||||
const formatDocumentType = (type: string) => {
|
||||
// Special case for 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(" ");
|
||||
};
|
||||
|
||||
const getDocumentIcon = (type: string, className: string) => {
|
||||
const iconProps = {
|
||||
className,
|
||||
style: { color: colors.text.muted },
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case "google_doc":
|
||||
return <GoogleDocs {...iconProps} />;
|
||||
case "google_sheet":
|
||||
return <GoogleSheets {...iconProps} />;
|
||||
case "google_slide":
|
||||
return <GoogleSlides {...iconProps} />;
|
||||
case "google_drive":
|
||||
return <GoogleDrive {...iconProps} />;
|
||||
case "notion":
|
||||
case "notion_doc":
|
||||
return <NotionDoc {...iconProps} />;
|
||||
case "word":
|
||||
case "microsoft_word":
|
||||
return <MicrosoftWord {...iconProps} />;
|
||||
case "excel":
|
||||
case "microsoft_excel":
|
||||
return <MicrosoftExcel {...iconProps} />;
|
||||
case "powerpoint":
|
||||
case "microsoft_powerpoint":
|
||||
return <MicrosoftPowerpoint {...iconProps} />;
|
||||
case "onenote":
|
||||
case "microsoft_onenote":
|
||||
return <MicrosoftOneNote {...iconProps} />;
|
||||
case "onedrive":
|
||||
return <OneDrive {...iconProps} />;
|
||||
case "pdf":
|
||||
return <PDF {...iconProps} />;
|
||||
default:
|
||||
return <FileText {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getSourceUrl = (document: DocumentWithMemories) => {
|
||||
if (document.type === "google_doc" && 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}`;
|
||||
}
|
||||
if (document.type === "google_slide" && document.customId) {
|
||||
return `https://docs.google.com/presentation/d/${document.customId}`;
|
||||
}
|
||||
// Fallback to existing URL for all other document types
|
||||
return document.url;
|
||||
};
|
||||
|
||||
const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => {
|
||||
return (
|
||||
<button
|
||||
className="p-4 rounded-lg border transition-all relative overflow-hidden cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: memory.isLatest
|
||||
? colors.memory.primary
|
||||
: "rgba(255, 255, 255, 0.02)",
|
||||
borderColor: memory.isLatest
|
||||
? colors.memory.border
|
||||
: "rgba(255, 255, 255, 0.1)",
|
||||
backdropFilter: "blur(8px)",
|
||||
WebkitBackdropFilter: "blur(8px)",
|
||||
}}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-start gap-2 relative z-10">
|
||||
<div
|
||||
className="p-1 rounded"
|
||||
style={{
|
||||
backgroundColor: memory.isLatest
|
||||
? colors.memory.secondary
|
||||
: "transparent",
|
||||
}}
|
||||
>
|
||||
<Brain
|
||||
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">
|
||||
<Label1Regular
|
||||
className="text-sm leading-relaxed text-left"
|
||||
style={{ color: colors.text.primary }}
|
||||
>
|
||||
{memory.memory}
|
||||
</Label1Regular>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{memory.isForgotten && (
|
||||
<Badge
|
||||
className="text-xs border-red-500/30 backdrop-blur-sm"
|
||||
style={{
|
||||
backgroundColor: colors.status.forgotten,
|
||||
color: "#dc2626",
|
||||
backdropFilter: "blur(4px)",
|
||||
WebkitBackdropFilter: "blur(4px)",
|
||||
}}
|
||||
variant="destructive"
|
||||
>
|
||||
Forgotten
|
||||
</Badge>
|
||||
)}
|
||||
{memory.isLatest && (
|
||||
<Badge
|
||||
className="text-xs border-blue-400/30 backdrop-blur-sm"
|
||||
style={{
|
||||
backgroundColor: colors.memory.secondary,
|
||||
color: colors.accent.primary,
|
||||
backdropFilter: "blur(4px)",
|
||||
WebkitBackdropFilter: "blur(4px)",
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
Latest
|
||||
</Badge>
|
||||
)}
|
||||
{memory.forgetAfter && (
|
||||
<Badge
|
||||
className="text-xs backdrop-blur-sm"
|
||||
style={{
|
||||
borderColor: colors.status.expiring,
|
||||
color: colors.status.expiring,
|
||||
backgroundColor: "rgba(251, 165, 36, 0.1)",
|
||||
backdropFilter: "blur(4px)",
|
||||
WebkitBackdropFilter: "blur(4px)",
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Expires: {formatDate(memory.forgetAfter)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-4 text-xs"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{formatDate(memory.createdAt)}
|
||||
</span>
|
||||
<span className="font-mono">v{memory.version}</span>
|
||||
{memory.sourceRelevanceScore && (
|
||||
<span
|
||||
className="flex items-center gap-1"
|
||||
style={{
|
||||
color:
|
||||
memory.sourceRelevanceScore > 70
|
||||
? colors.accent.emerald
|
||||
: colors.text.muted,
|
||||
}}
|
||||
>
|
||||
<Sparkles className="w-3 h-3" />
|
||||
{memory.sourceRelevanceScore}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
const DocumentCard = memo(
|
||||
({
|
||||
document,
|
||||
|
|
@ -361,12 +120,12 @@ const DocumentCard = memo(
|
|||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="relative z-10 px-0">
|
||||
{document.summary && (
|
||||
{document.content && (
|
||||
<p
|
||||
className="text-xs line-clamp-2 mb-3"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
{document.summary}
|
||||
{document.content}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
|
|
@ -402,248 +161,6 @@ const DocumentCard = memo(
|
|||
},
|
||||
);
|
||||
|
||||
const DocumentDetailSheet = memo(
|
||||
({
|
||||
document,
|
||||
isOpen,
|
||||
onClose,
|
||||
isMobile,
|
||||
}: {
|
||||
document: DocumentWithMemories | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
isMobile: boolean;
|
||||
}) => {
|
||||
if (!document) return null;
|
||||
|
||||
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;
|
||||
}) => (
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<div
|
||||
className="p-2 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: colors.document.secondary,
|
||||
border: `1px solid ${colors.document.border}`,
|
||||
}}
|
||||
>
|
||||
{getDocumentIcon(document.type, "w-5 h-5")}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TitleComponent style={{ color: colors.text.primary }}>
|
||||
{document.title || "Untitled Document"}
|
||||
</TitleComponent>
|
||||
<div
|
||||
className="flex items-center gap-2 mt-1 text-xs"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
<span>{formatDocumentType(document.type)}</span>
|
||||
<span>•</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");
|
||||
}}
|
||||
style={{ color: colors.accent.primary }}
|
||||
type="button"
|
||||
>
|
||||
View source
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SummarySection = () => {
|
||||
if (!document.summary) return null;
|
||||
|
||||
const shouldShowToggle = document.summary.length > 200; // Show toggle for longer summaries
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mt-4 p-3 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.03)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className={`text-sm ${!isSummaryExpanded ? "line-clamp-3" : ""}`}
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
{document.summary}
|
||||
</p>
|
||||
{shouldShowToggle && (
|
||||
<button
|
||||
onClick={() => setIsSummaryExpanded(!isSummaryExpanded)}
|
||||
className="mt-2 text-xs hover:underline transition-all"
|
||||
style={{ color: colors.accent.primary }}
|
||||
type="button"
|
||||
>
|
||||
{isSummaryExpanded ? "Show less" : "Show more"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoryContent = () => (
|
||||
<div className="p-6 space-y-6">
|
||||
{activeMemories.length > 0 && (
|
||||
<div>
|
||||
<div
|
||||
className="text-sm font-medium mb-4 flex items-start gap-2 px-3 py-2 rounded-lg"
|
||||
style={{
|
||||
color: colors.text.secondary,
|
||||
backgroundColor: colors.memory.primary,
|
||||
border: `1px solid ${colors.memory.border}`,
|
||||
}}
|
||||
>
|
||||
<Brain className="w-4 h-4 text-blue-400" />
|
||||
Active Memories ({activeMemories.length})
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{activeMemories.map((memory, index) => (
|
||||
<div
|
||||
className="animate-in fade-in slide-in-from-right-2"
|
||||
key={memory.id}
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
<MemoryDetailItem memory={memory} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{forgottenMemories.length > 0 && (
|
||||
<div>
|
||||
<div
|
||||
className="text-sm font-medium mb-4 px-3 py-2 rounded-lg opacity-60"
|
||||
style={{
|
||||
color: colors.text.muted,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.02)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
}}
|
||||
>
|
||||
Forgotten Memories ({forgottenMemories.length})
|
||||
</div>
|
||||
<div className="space-y-3 opacity-40">
|
||||
{forgottenMemories.map((memory) => (
|
||||
<MemoryDetailItem key={memory.id} memory={memory} />
|
||||
))}
|
||||
</div>
|
||||
</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 (
|
||||
<Drawer onOpenChange={onClose} open={isOpen}>
|
||||
<DrawerContent
|
||||
className="border-0 p-0 overflow-hidden max-h-[90vh]"
|
||||
style={{
|
||||
backgroundColor: colors.background.secondary,
|
||||
borderTop: `1px solid ${colors.document.border}`,
|
||||
backdropFilter: "blur(20px)",
|
||||
WebkitBackdropFilter: "blur(20px)",
|
||||
}}
|
||||
>
|
||||
{/* Header section with glass effect */}
|
||||
<div
|
||||
className="p-4 relative border-b"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.02)",
|
||||
borderBottom: `1px solid ${colors.document.border}`,
|
||||
}}
|
||||
>
|
||||
<DrawerHeader className="pb-0 px-0 text-left">
|
||||
<HeaderContent TitleComponent={DrawerTitle} />
|
||||
</DrawerHeader>
|
||||
|
||||
<SummarySection />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 memory-drawer-scroll overflow-y-auto">
|
||||
<MemoryContent />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet onOpenChange={onClose} open={isOpen}>
|
||||
<SheetContent
|
||||
className="w-full sm:max-w-2xl border-0 p-0 overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: colors.background.secondary,
|
||||
borderLeft: `1px solid ${colors.document.border}`,
|
||||
backdropFilter: "blur(20px)",
|
||||
WebkitBackdropFilter: "blur(20px)",
|
||||
}}
|
||||
>
|
||||
{/* Header section with glass effect */}
|
||||
<div
|
||||
className="p-6 relative"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.02)",
|
||||
borderBottom: `1px solid ${colors.document.border}`,
|
||||
}}
|
||||
>
|
||||
<SheetHeader className="pb-0">
|
||||
<HeaderContent TitleComponent={SheetTitle} />
|
||||
</SheetHeader>
|
||||
|
||||
<SummarySection />
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100vh-200px)] memory-sheet-scroll overflow-y-auto">
|
||||
<MemoryContent />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const MemoryListView = ({
|
||||
children,
|
||||
documents,
|
||||
|
|
@ -831,7 +348,7 @@ export const MemoryListView = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<DocumentDetailSheet
|
||||
<MemoryDetail
|
||||
document={selectedDocument}
|
||||
isOpen={isDetailOpen}
|
||||
onClose={handleCloseDetails}
|
||||
|
|
|
|||
54
apps/web/lib/document-icon.tsx
Normal file
54
apps/web/lib/document-icon.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { colors } from '@repo/ui/memory-graph/constants';
|
||||
import {
|
||||
GoogleDocs,
|
||||
MicrosoftWord,
|
||||
NotionDoc,
|
||||
GoogleDrive,
|
||||
GoogleSheets,
|
||||
GoogleSlides,
|
||||
PDF,
|
||||
OneDrive,
|
||||
MicrosoftOneNote,
|
||||
MicrosoftPowerpoint,
|
||||
MicrosoftExcel,
|
||||
} from '@ui/assets/icons';
|
||||
import { FileText } from 'lucide-react';
|
||||
|
||||
export const getDocumentIcon = (type: string, className: string) => {
|
||||
const iconProps = {
|
||||
className,
|
||||
style: { color: colors.text.muted },
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'google_doc':
|
||||
return <GoogleDocs {...iconProps} />;
|
||||
case 'google_sheet':
|
||||
return <GoogleSheets {...iconProps} />;
|
||||
case 'google_slide':
|
||||
return <GoogleSlides {...iconProps} />;
|
||||
case 'google_drive':
|
||||
return <GoogleDrive {...iconProps} />;
|
||||
case 'notion':
|
||||
case 'notion_doc':
|
||||
return <NotionDoc {...iconProps} />;
|
||||
case 'word':
|
||||
case 'microsoft_word':
|
||||
return <MicrosoftWord {...iconProps} />;
|
||||
case 'excel':
|
||||
case 'microsoft_excel':
|
||||
return <MicrosoftExcel {...iconProps} />;
|
||||
case 'powerpoint':
|
||||
case 'microsoft_powerpoint':
|
||||
return <MicrosoftPowerpoint {...iconProps} />;
|
||||
case 'onenote':
|
||||
case 'microsoft_onenote':
|
||||
return <MicrosoftOneNote {...iconProps} />;
|
||||
case 'onedrive':
|
||||
return <OneDrive {...iconProps} />;
|
||||
case 'pdf':
|
||||
return <PDF {...iconProps} />;
|
||||
default:
|
||||
return <FileText {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
|
@ -83,7 +83,7 @@ function SheetContent({
|
|||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
className={cn("flex flex-col gap-1.5 py-4", className)}
|
||||
data-slot="sheet-header"
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue