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:
MaheshtheDev 2025-10-04 21:38:13 +00:00
parent 4d6fd37c99
commit 583c2651b6
7 changed files with 165 additions and 101 deletions

View file

@ -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>

View file

@ -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")
}

View file

@ -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",
)}

View file

@ -89,6 +89,7 @@ const DocumentCard = memo(
url={document.url}
title={document.title || "Untitled Document"}
image={document.ogImage}
onOpenDetails={() => onOpenDetails(document)}
onDelete={() => onDelete(document)}
/>
)

View file

@ -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>

View file

@ -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">

View file

@ -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} />
}
}