diff --git a/apps/web/components/document-modal/graph-list-memories.tsx b/apps/web/components/document-modal/graph-list-memories.tsx index c8f4ae8d..313424e7 100644 --- a/apps/web/components/document-modal/graph-list-memories.tsx +++ b/apps/web/components/document-modal/graph-list-memories.tsx @@ -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>( @@ -193,7 +195,10 @@ export function GraphListMemories({ return (
!open && onClose()}> - + 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 = ( +
+ +
+ ) + + const documentInsights = ( +
+ {hasPluginInsights && } + {_document && (_document.summary || pluginDocument?.summary) && ( + + )} + {_document?.memoryEntries && _document.memoryEntries.length > 0 && ( + + )} +
+ ) + + const modalContent = ( + <> + {isMobile ? ( + + {_document?.title} - Document + + ) : ( {_document?.title} - Document -
-
- - </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> ) diff --git a/packages/memory-graph/src/components/legend.tsx b/packages/memory-graph/src/components/legend.tsx index 724efdd7..bd6b2f3a 100644 --- a/packages/memory-graph/src/components/legend.tsx +++ b/packages/memory-graph/src/components/legend.tsx @@ -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> diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx index ed5ec094..dd82f490 100644 --- a/packages/memory-graph/src/components/memory-graph.tsx +++ b/packages/memory-graph/src/components/memory-graph.tsx @@ -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>