Add memory drawer (#1015)

Co-authored-by: Ishaan Gupta <ishaankone@gmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Vedant Mahajan 2026-05-29 23:47:19 +05:30 • committed by GitHub
parent 6fb42ad0e0
commit fc08bd891f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 226 additions and 103 deletions

View file

@ -169,9 +169,11 @@ function VersionStatus({
export function GraphListMemories({
memoryEntries,
documentId,
className,
}: {
memoryEntries: MemoryEntry[]
documentId?: string
className?: string
}) {
const { effectiveContainerTags } = useProject()
const [expandedMemories, setExpandedMemories] = useState<Set<string>>(
@ -193,7 +195,10 @@ export function GraphListMemories({
return (
<div
id="document-memories"
className="relative flex-1 px-3 pt-3 rounded-[14px] flex flex-col overflow-hidden shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
className={cn(
"relative h-[360px] shrink-0 px-3 pt-3 rounded-[14px] flex flex-col overflow-hidden shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)] md:h-auto md:min-h-0 md:flex-1 md:shrink",
className,
)}
style={{
backgroundImage: "url('/dot-pattern.svg')",
backgroundRepeat: "repeat",

View file

@ -1,7 +1,9 @@
"use client"
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
import { Drawer, DrawerContent, DrawerTitle } from "@repo/ui/components/drawer"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/components/tabs"
import {
ArrowUpRightIcon,
XIcon,
@ -268,60 +270,114 @@ export function DocumentModal({
],
)
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent
className={cn(
"p-0 border-none bg-[#1B1F24] flex flex-col px-3 md:px-4 pt-3 pb-4 gap-3",
isMobile
? "w-[calc(100vw-1rem)]! h-[calc(100dvh-1rem)]! max-w-none! max-h-none! rounded-xl"
: "w-[80%]! max-w-[1158px]! h-[86%]! max-h-[684px]! rounded-[22px]",
dmSansClassName(),
)}
style={{
boxShadow:
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}}
showCloseButton={false}
>
const hasPluginInsights =
pluginDocument &&
pluginDocument.kind !== "claude-code-doc" &&
pluginDocument.kind !== "openclaw-session"
const hasDocumentInsights = Boolean(
hasPluginInsights ||
_document?.summary ||
pluginDocument?.summary ||
(_document?.memoryEntries && _document.memoryEntries.length > 0),
)
const documentPreview = (
<div
id="document-preview"
className="bg-[#14161A] rounded-[14px] overflow-hidden flex min-h-0 flex-1 flex-col shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)] relative"
>
<DocumentContent
document={_document}
textEditorProps={textEditorProps}
pluginDocument={pluginDocument}
/>
</div>
)
const documentInsights = (
<div
id="document-memories-summary"
className={cn(
"gap-3 flex flex-col overflow-hidden",
isMobile && "min-h-0 flex-1",
dmSansClassName(),
)}
>
{hasPluginInsights && <PluginDetails parsed={pluginDocument} />}
{_document && (_document.summary || pluginDocument?.summary) && (
<DocumentSummary
memoryEntries={_document.memoryEntries}
summary={(pluginDocument?.summary ?? _document.summary) as string}
createdAt={_document.createdAt}
/>
)}
{_document?.memoryEntries && _document.memoryEntries.length > 0 && (
<GraphListMemories
memoryEntries={_document.memoryEntries as MemoryEntry[]}
documentId={_document.id}
className={
isMobile ? "h-auto min-h-[420px] flex-1 shrink" : undefined
}
/>
)}
</div>
)
const modalContent = (
<>
{isMobile ? (
<DrawerTitle className="sr-only">
{_document?.title} - Document
</DrawerTitle>
) : (
<DialogTitle className="sr-only">
{_document?.title} - Document
</DialogTitle>
<div className="flex items-center justify-between h-fit gap-2 md:gap-4">
<div className="flex-1 min-w-0">
<Title
title={_document?.title}
documentType={_document?.type ?? "text"}
url={_document?.url}
pluginIconSrc={pluginDocument?.pluginIconSrc}
/>
</div>
<div className="flex items-center gap-1.5 md:gap-2 shrink-0">
{pluginDocument?.kind === "claude-code-doc" &&
_document?.customId && (
<CopySessionIdButton sessionId={_document.customId} />
)}
<DeleteButton
documentId={_document?.id}
customId={_document?.customId}
deleteMutation={deleteMutation}
/>
{_document?.url && (
<a
href={getDocumentSourceUrl(_document)}
target="_blank"
rel="noopener noreferrer"
className={cn(
"flex items-center gap-1 bg-[#0D121A] rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]",
isMobile ? "size-7 justify-center" : "px-3 py-2",
)}
>
{!isMobile && (
<span className="line-clamp-1">Visit source</span>
)}
<ArrowUpRightIcon className="size-4 text-[#737373]" />
</a>
)}
<div className="flex items-center justify-between h-fit gap-2 md:gap-4">
<div className="flex-1 min-w-0">
<Title
title={_document?.title}
documentType={_document?.type ?? "text"}
url={_document?.url}
pluginIconSrc={pluginDocument?.pluginIconSrc}
/>
</div>
<div className="flex items-center gap-1.5 md:gap-2 shrink-0">
{pluginDocument?.kind === "claude-code-doc" &&
_document?.customId && (
<CopySessionIdButton sessionId={_document.customId} />
)}
<DeleteButton
documentId={_document?.id}
customId={_document?.customId}
deleteMutation={deleteMutation}
/>
{_document?.url && (
<a
href={getDocumentSourceUrl(_document)}
target="_blank"
rel="noopener noreferrer"
className={cn(
"flex items-center gap-1 bg-[#0D121A] rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]",
isMobile ? "size-7 justify-center" : "px-3 py-2",
)}
>
{!isMobile && <span className="line-clamp-1">Visit source</span>}
<ArrowUpRightIcon className="size-4 text-[#737373]" />
</a>
)}
{isMobile ? (
<button
className="bg-[#0D121A] size-7 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus:outline-none disabled:pointer-events-none cursor-pointer [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
type="button"
tabIndex={-1}
onClick={onClose}
>
<XIcon stroke="#737373" />
<span className="sr-only">Close</span>
</button>
) : (
<DialogPrimitive.Close
className="bg-[#0D121A] size-7 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus:outline-none disabled:pointer-events-none cursor-pointer [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
data-slot="dialog-close"
@ -331,50 +387,91 @@ export function DocumentModal({
<XIcon stroke="#737373" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</div>
)}
</div>
<div className="flex-1 grid grid-cols-1 md:grid-cols-[2fr_1fr] gap-3 overflow-hidden min-h-0">
<div
id="document-preview"
className={cn(
"bg-[#14161A] rounded-[14px] overflow-hidden flex flex-col shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)] relative",
)}
</div>
{isMobile && hasDocumentInsights ? (
<Tabs
defaultValue="content"
className="flex min-h-0 flex-1 flex-col pt-1.5"
>
<TabsList className="grid h-11 w-full grid-cols-2 rounded-full border border-[#263142] bg-[#0A1019] p-1 shadow-[inset_0_1px_2px_rgba(255,255,255,0.04),0_1px_3px_rgba(0,0,0,0.35)]">
<TabsTrigger
value="content"
className="rounded-full text-[15px] font-medium text-[#8E99AA] transition-colors data-[state=active]:bg-[#0B2B60]! data-[state=active]:text-[#F8FAFC] data-[state=active]:shadow-[inset_0_1px_1px_rgba(255,255,255,0.08),0_1px_4px_rgba(54,155,253,0.18)]"
>
Content
</TabsTrigger>
<TabsTrigger
value="insights"
className="rounded-full text-[15px] font-medium text-[#8E99AA] transition-colors data-[state=active]:bg-[#0B2B60]! data-[state=active]:text-[#F8FAFC] data-[state=active]:shadow-[inset_0_1px_1px_rgba(255,255,255,0.08),0_1px_4px_rgba(54,155,253,0.18)]"
>
Insights
</TabsTrigger>
</TabsList>
<TabsContent value="content" className="mt-4 flex min-h-0 flex-1">
{documentPreview}
</TabsContent>
<TabsContent
value="insights"
className="mt-4 flex min-h-0 flex-1 flex-col overflow-y-auto pb-1 scrollbar-thin"
>
<DocumentContent
document={_document}
textEditorProps={textEditorProps}
pluginDocument={pluginDocument}
/>
</div>
<div
id="document-memories-summary"
className={cn(
"gap-3 flex flex-col overflow-hidden",
dmSansClassName(),
)}
>
{pluginDocument &&
pluginDocument.kind !== "claude-code-doc" &&
pluginDocument.kind !== "openclaw-session" && (
<PluginDetails parsed={pluginDocument} />
)}
{_document && (_document.summary || pluginDocument?.summary) && (
<DocumentSummary
memoryEntries={_document.memoryEntries}
summary={
(pluginDocument?.summary ?? _document.summary) as string
}
createdAt={_document.createdAt}
/>
)}
{_document?.memoryEntries && _document.memoryEntries.length > 0 && (
<GraphListMemories
memoryEntries={_document.memoryEntries as MemoryEntry[]}
documentId={_document.id}
/>
)}
</div>
{documentInsights}
</TabsContent>
</Tabs>
) : isMobile ? (
<div className="flex min-h-0 flex-1 pt-1.5">{documentPreview}</div>
) : (
<div className="flex-1 grid grid-cols-1 md:grid-cols-[2fr_1fr] gap-3 min-h-0 overflow-hidden">
{documentPreview}
{documentInsights}
</div>
)}
</>
)
if (isMobile) {
return (
<Drawer
open={isOpen}
onOpenChange={(open: boolean) => !open && onClose()}
shouldScaleBackground
>
<DrawerContent
className={cn(
"flex flex-col gap-0 border-none bg-[#1B1F24] p-0",
"h-[88svh] max-h-[88svh] overflow-hidden rounded-t-[22px]",
"[&>div:first-child]:bg-[#3A4252] [&>div:first-child]:h-1 [&>div:first-child]:w-9 [&>div:first-child]:mt-2.5 [&>div:first-child]:mb-1",
dmSansClassName(),
)}
style={{
boxShadow:
"0 -12px 40px rgba(0, 0, 0, 0.45), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}}
>
<div className="flex min-h-0 flex-1 flex-col gap-3 overflow-hidden px-3 pt-2 pb-4">
{modalContent}
</div>
</DrawerContent>
</Drawer>
)
}
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent
className={cn(
"p-0 border-none bg-[#1B1F24] flex flex-col px-3 md:px-4 pt-3 pb-4 gap-3",
"w-[80%]! max-w-[1158px]! h-[86%]! max-h-[684px]! rounded-[22px]",
dmSansClassName(),
)}
style={{
boxShadow:
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}}
showCloseButton={false}
>
{modalContent}
</DialogContent>
</Dialog>
)

View file

@ -6,6 +6,8 @@ interface LegendProps {
edges?: GraphEdge[]
isLoading?: boolean
colors: GraphThemeColors
compact?: boolean
maxHeight?: number
}
function HexagonIcon({
@ -191,6 +193,8 @@ export const Legend = memo(function Legend({
edges = [],
isLoading: _isLoading = false,
colors,
compact = false,
maxHeight,
}: LegendProps) {
const [isExpanded, setIsExpanded] = useState(false)
const [connectionsExpanded, setConnectionsExpanded] = useState(true)
@ -201,7 +205,8 @@ export const Legend = memo(function Legend({
const outerStyle: React.CSSProperties = {
overflow: "hidden",
width: 214,
width: compact ? "min(214px, calc(100vw - 32px))" : 214,
maxWidth: "100%",
}
const cardStyle: React.CSSProperties = {
@ -209,6 +214,7 @@ export const Legend = memo(function Legend({
backgroundColor: colors.controlBg,
border: `1px solid ${colors.controlBorder}`,
boxShadow: "0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)",
maxHeight,
}
const headerBtnStyle: React.CSSProperties = {
@ -217,6 +223,7 @@ export const Legend = memo(function Legend({
alignItems: "center",
gap: 6,
width: "100%",
justifyContent: "flex-start",
cursor: "pointer",
outline: "none",
background: "none",
@ -263,6 +270,21 @@ export const Legend = memo(function Legend({
gap: 8,
}
const expandedContentStyle: React.CSSProperties = {
marginTop: 16,
display: "flex",
flexDirection: "column",
gap: 16,
...(compact
? {
maxHeight: maxHeight ? Math.max(maxHeight - 56, 112) : 220,
overflowY: "auto",
overscrollBehavior: "contain",
paddingRight: 2,
}
: {}),
}
return (
<div style={outerStyle}>
<div style={cardStyle}>
@ -281,14 +303,7 @@ export const Legend = memo(function Legend({
</button>
{isExpanded && (
<div
style={{
marginTop: 16,
display: "flex",
flexDirection: "column",
gap: 16,
}}
>
<div style={expandedContentStyle}>
{/* Statistics section */}
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<span style={sectionLabelStyle}>Statistics</span>

View file

@ -83,6 +83,9 @@ export function MemoryGraph({
const graphFitHeight = isCompactViewport
? Math.max(containerSize.height - 170, 240)
: containerSize.height
const compactLegendMaxHeight = isCompactViewport
? Math.max(containerSize.height - 104, 160)
: undefined
// Rebuild version chain index during render (not in an effect) so that
// the chain data is up-to-date when getChain() is called in useMemo below.
@ -644,13 +647,14 @@ export function MemoryGraph({
const bottomLeftStackStyle: React.CSSProperties = {
position: "absolute",
bottom: 16,
left: 16,
bottom: isCompactViewport ? 12 : 16,
left: isCompactViewport ? 12 : 16,
zIndex: 20,
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: 8,
maxWidth: isCompactViewport ? "calc(100% - 24px)" : undefined,
}
return (
@ -722,7 +726,9 @@ export function MemoryGraph({
<Legend
colors={colors}
edges={edges}
compact={isCompactViewport}
isLoading={isLoading}
maxHeight={compactLegendMaxHeight}
nodes={nodes}
/>
</div>