mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-10 22:41:17 +00:00
feat: url cards as content preview with rich details (#452)
Improved memory detail UI with better document icons and layout and Added dark mode support to the Chrome extension button ### What changed? - Enhanced the WebsiteCard component with an `onOpenDetails` prop - Improved the MemoryDetail component with: - Better layout for document headers and source links - Enhanced content scrolling with proper height constraints - Improved spacing and padding in various sections - Added favicon support to document icons, showing website favicons for webpage documents - Fixed document type detection for webpages and URLs - Added dark mode support to the Chrome extension button component with appropriate color classes [Screen Recording 2025-10-03 at 11.34.01 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/user-attachments/thumbnails/2f6ceef2-43a2-4e11-bafa-0f3ce614696b.mov" />](https://app.graphite.dev/user-attachments/video/2f6ceef2-43a2-4e11-bafa-0f3ce614696b.mov)
This commit is contained in:
parent
4d6fd37c99
commit
583c2651b6
7 changed files with 165 additions and 101 deletions
|
|
@ -130,42 +130,42 @@ export function ChromeExtensionButton() {
|
|||
|
||||
<div className="px-6 py-2 pb-4 space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 bg-blue-50 border border-blue-200 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<TwitterIcon className="fill-blue-500 text-blue-500" />
|
||||
<div className="w-10 h-10 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<TwitterIcon className="fill-blue-500 dark:fill-blue-400 text-blue-500 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm text-gray-800">
|
||||
<h3 className="font-semibold text-sm text-foreground">
|
||||
Twitter Imports
|
||||
</h3>
|
||||
<p className="text-xs text-gray-600">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Import your twitter timeline & save tweets.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 bg-orange-50 border border-orange-200 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Bookmark className="w-5 h-5 text-orange-600" />
|
||||
<div className="w-10 h-10 bg-orange-50 dark:bg-orange-950 border border-orange-200 dark:border-orange-800 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Bookmark className="w-5 h-5 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm text-gray-800">
|
||||
<h3 className="font-semibold text-sm text-foreground">
|
||||
Save All Bookmarks
|
||||
</h3>
|
||||
<p className="text-xs text-gray-600">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Instantly save any webpage to your memory.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 bg-green-50 border border-green-200 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Zap className="w-5 h-5 text-green-600" />
|
||||
<div className="w-10 h-10 bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Zap className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm text-gray-800">
|
||||
<h3 className="font-semibold text-sm text-foreground">
|
||||
Charge Empty Memory
|
||||
</h3>
|
||||
<p className="text-xs text-gray-600">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Automatically capture & organize your browsing history.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -175,7 +175,7 @@ export function ChromeExtensionButton() {
|
|||
<div className="px-6 pb-4">
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
className="w-full bg-white border border-[#686CFD] text-gray-800 hover:bg-gray-50 font-semibold rounded-lg h-10 flex items-center justify-center gap-3"
|
||||
className="w-full bg-background border border-primary text-foreground hover:bg-accent font-semibold rounded-lg h-10 flex items-center justify-center gap-3"
|
||||
>
|
||||
<div className="w-6 h-6 bg-[#686CFD] rounded-full flex items-center justify-center">
|
||||
<Image
|
||||
|
|
@ -189,7 +189,7 @@ export function ChromeExtensionButton() {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pb-4 flex items-center justify-center gap-6 text-xs text-gray-500">
|
||||
<div className="px-6 pb-4 flex items-center justify-center gap-6 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Users className="w-3 h-3" />
|
||||
<span>4K+ users</span>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ interface WebsiteCardProps {
|
|||
description?: string
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
onOpenDetails?: () => void
|
||||
onDelete?: () => void
|
||||
showExternalLink?: boolean
|
||||
}
|
||||
|
|
@ -36,6 +37,7 @@ export const WebsiteCard = ({
|
|||
description,
|
||||
className,
|
||||
onClick,
|
||||
onOpenDetails,
|
||||
onDelete,
|
||||
showExternalLink = true,
|
||||
}: WebsiteCardProps) => {
|
||||
|
|
@ -44,6 +46,8 @@ export const WebsiteCard = ({
|
|||
const handleCardClick = () => {
|
||||
if (onClick) {
|
||||
onClick()
|
||||
} else if (onOpenDetails) {
|
||||
onOpenDetails()
|
||||
} else {
|
||||
window.open(url, "_blank", "noopener,noreferrer")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
|
|||
<MonitorIcon
|
||||
className={cn(
|
||||
theme === "system"
|
||||
? "text-primay-foreground"
|
||||
? "text-primary-foreground"
|
||||
: "text-muted-foreground",
|
||||
"h-3 w-3 group-hover:text-foreground",
|
||||
)}
|
||||
|
|
@ -160,7 +160,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
|
|||
<SunIcon
|
||||
className={cn(
|
||||
theme === "light"
|
||||
? "text-primay-foreground"
|
||||
? "text-primary-foreground"
|
||||
: "text-muted-foreground",
|
||||
"h-3 w-3 group-hover:text-foreground",
|
||||
)}
|
||||
|
|
@ -178,7 +178,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
|
|||
<MoonIcon
|
||||
className={cn(
|
||||
theme === "dark"
|
||||
? "text-primay-foreground"
|
||||
? "text-primary-foreground"
|
||||
: "text-muted-foreground",
|
||||
"h-3 w-3 group-hover:text-foreground",
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ const DocumentCard = memo(
|
|||
url={document.url}
|
||||
title={document.title || "Untitled Document"}
|
||||
image={document.ogImage}
|
||||
onOpenDetails={() => onOpenDetails(document)}
|
||||
onDelete={() => onDelete(document)}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import type { z } from "zod"
|
|||
import { formatDate, getSourceUrl } from "."
|
||||
import { Label1Regular } from "@ui/text/label/label-1-regular"
|
||||
import { HTMLContentRenderer } from "./html-content-renderer"
|
||||
import { Button } from "@ui/components/button"
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
|
|
@ -141,11 +142,13 @@ export const MemoryDetail = memo(
|
|||
TitleComponent: typeof DialogTitle | typeof DrawerTitle
|
||||
}) => (
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-2 md:gap-3 flex-1 min-w-0">
|
||||
<div className="flex items-end gap-2 md:gap-3 flex-1 min-w-0">
|
||||
<div className="p-1.5 md:p-2 rounded-lg bg-muted/10 flex-shrink-0">
|
||||
{getDocumentIcon(
|
||||
document.type,
|
||||
"w-4 h-4 md:w-5 md:h-5 text-foreground",
|
||||
document.source ?? undefined,
|
||||
document.url ?? undefined,
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
|
|
@ -156,25 +159,24 @@ export const MemoryDetail = memo(
|
|||
<span>{formatDocumentType(document.type)}</span>
|
||||
<span>•</span>
|
||||
<span>{formatDate(document.createdAt)}</span>
|
||||
{document.url && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<button
|
||||
className="flex items-center gap-0.5 md:gap-1 transition-all hover:gap-1 md:hover:gap-2 text-primary hover:text-primary/80 whitespace-nowrap"
|
||||
onClick={() => {
|
||||
const sourceUrl = getSourceUrl(document)
|
||||
window.open(sourceUrl ?? undefined, "_blank")
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span className="hidden sm:inline">View source</span>
|
||||
<span className="sm:hidden">Source</span>
|
||||
<ExternalLink className="w-2.5 h-2.5 md:w-3 md:h-3" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{document.url && (
|
||||
<div className="flex items-end">
|
||||
<Button
|
||||
onClick={() => {
|
||||
const sourceUrl = getSourceUrl(document)
|
||||
window.open(sourceUrl ?? undefined, "_blank")
|
||||
}}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
<span className="hidden sm:inline">visit source</span>
|
||||
<span className="sm:hidden">Source</span>
|
||||
<ExternalLink className="w-2.5 h-2.5 md:w-3 md:h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -227,10 +229,10 @@ export const MemoryDetail = memo(
|
|||
<div className="space-y-6">
|
||||
{activeMemories.length > 0 && (
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-2 flex items-start gap-2 py-2 text-muted-foreground">
|
||||
<div className="text-sm font-medium flex items-start gap-2 pb-2 text-muted-foreground">
|
||||
Active Memories ({activeMemories.length})
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3 max-h-[80vh] overflow-y-auto custom-scrollbar">
|
||||
{activeMemories.map((memory) => (
|
||||
<div key={memory.id}>
|
||||
<MemoryDetailItem memory={memory} />
|
||||
|
|
@ -281,7 +283,7 @@ export const MemoryDetail = memo(
|
|||
Content
|
||||
</h4>
|
||||
</div>
|
||||
<div className="p-3 md:p-4">
|
||||
<div className="p-3 md:p-4 m-4">
|
||||
<ContentDisplaySection />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -324,8 +326,8 @@ export const MemoryDetail = memo(
|
|||
|
||||
<div className="flex-1 flex flex-col lg:flex-row overflow-hidden">
|
||||
<div className="flex-1 flex flex-col h-full justify-between min-w-0">
|
||||
<div className="p-2 px-3 md:px-4 overflow-y-auto custom-scrollbar transition-all duration-300">
|
||||
<h3 className="font-medium text-[10px] md:text-xs text-muted-foreground uppercase pb-1 px-1">
|
||||
<div className="p-2 px-3 md:pl-4 overflow-y-auto custom-scrollbar transition-all duration-300">
|
||||
<h3 className="font-medium text-[10px] md:text-sm text-muted-foreground pb-2 px-1">
|
||||
Content
|
||||
</h3>
|
||||
<ContentDisplaySection />
|
||||
|
|
@ -370,9 +372,9 @@ export const MemoryDetail = memo(
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-96 flex flex-col border-t lg:border-t-0 lg:border-l border-border">
|
||||
<div className="w-full lg:w-96 flex flex-col border-t lg:border-t-0 border-border">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex-1 memory-dialog-scroll overflow-y-auto p-3 md:p-4">
|
||||
<div className="flex-1 memory-dialog-scroll overflow-y-auto p-2 md:p-3">
|
||||
<MemoryContent />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -294,10 +294,7 @@ export const MemoryListView = ({
|
|||
// Always render with consistent structure
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="h-full overflow-hidden relative pb-20"
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="h-full overflow-hidden relative pb-20" ref={containerRef}>
|
||||
{error ? (
|
||||
<div className="h-full flex items-center justify-center p-4">
|
||||
<div className="rounded-xl overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -1,59 +1,119 @@
|
|||
import { MCPIcon } from '@/components/menu';
|
||||
import { colors } from '@repo/ui/memory-graph/constants';
|
||||
import { MCPIcon } from "@/components/menu"
|
||||
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';
|
||||
GoogleDocs,
|
||||
MicrosoftWord,
|
||||
NotionDoc,
|
||||
GoogleDrive,
|
||||
GoogleSheets,
|
||||
GoogleSlides,
|
||||
PDF,
|
||||
OneDrive,
|
||||
MicrosoftOneNote,
|
||||
MicrosoftPowerpoint,
|
||||
MicrosoftExcel,
|
||||
} from "@ui/assets/icons"
|
||||
import { FileText, Globe } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
export const getDocumentIcon = (type: string, className: string, source?: string) => {
|
||||
const iconProps = {
|
||||
className,
|
||||
style: { color: colors.text.muted },
|
||||
};
|
||||
const getFaviconUrl = (url: string): string => {
|
||||
try {
|
||||
const domain = new URL(url).hostname
|
||||
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
if(source === "mcp") {
|
||||
return <MCPIcon {...iconProps} />;
|
||||
}
|
||||
const FaviconIcon = ({
|
||||
url,
|
||||
className,
|
||||
iconProps,
|
||||
}: {
|
||||
url: string
|
||||
className: string
|
||||
iconProps: { className: string; style: { color: string } }
|
||||
}) => {
|
||||
const [hasError, setHasError] = useState(false)
|
||||
const faviconUrl = getFaviconUrl(url)
|
||||
|
||||
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} />;
|
||||
}
|
||||
};
|
||||
if (hasError || !faviconUrl) {
|
||||
return <Globe {...iconProps} />
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt="Website favicon"
|
||||
className={className}
|
||||
style={{
|
||||
width: "2em",
|
||||
height: "2em",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
onError={() => setHasError(true)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const getDocumentIcon = (
|
||||
type: string,
|
||||
className: string,
|
||||
source?: string,
|
||||
url?: string,
|
||||
) => {
|
||||
const iconProps = {
|
||||
className,
|
||||
style: { color: colors.text.muted },
|
||||
}
|
||||
|
||||
if (source === "mcp") {
|
||||
return <MCPIcon {...iconProps} />
|
||||
}
|
||||
|
||||
if (
|
||||
type === "webpage" ||
|
||||
type === "url" ||
|
||||
(url && (type === "unknown" || !type))
|
||||
) {
|
||||
if (url) {
|
||||
return (
|
||||
<FaviconIcon url={url} className={className} iconProps={iconProps} />
|
||||
)
|
||||
}
|
||||
|
||||
return <Globe {...iconProps} />
|
||||
}
|
||||
|
||||
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} />
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue