mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-08 03:08:21 +00:00
added file previews and mcp card
This commit is contained in:
parent
5d48097b6f
commit
2a3d3cde58
14 changed files with 550 additions and 253 deletions
|
|
@ -24,4 +24,4 @@ export default function NewPage() {
|
|||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ import { useState } from "react"
|
|||
import { motion, AnimatePresence } from "motion/react"
|
||||
import NovaOrb from "@/components/nova/nova-orb"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { HistoryIcon, PanelRightCloseIcon, SendIcon, SquarePenIcon } from "lucide-react"
|
||||
import {
|
||||
HistoryIcon,
|
||||
PanelRightCloseIcon,
|
||||
SendIcon,
|
||||
SquarePenIcon,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/utils/fonts"
|
||||
|
||||
|
|
@ -77,26 +82,38 @@ export function ChatSidebar() {
|
|||
exit={{ x: "100px", opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut", bounce: 0 }}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 px-6">
|
||||
<div className="flex items-center justify-between p-4 px-6">
|
||||
<p>Chat Title</p>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<Button variant="headers" className="rounded-full text-base gap-2 !h-10 border-[#73737333] bg-[#0D121A]">
|
||||
<HistoryIcon className="size-4 text-[#737373]" />
|
||||
</Button>
|
||||
<Button variant="headers" className="rounded-full text-base gap-2 !h-10 border-[#73737333] bg-[#0D121A]">
|
||||
<SquarePenIcon className="size-4 text-[#737373]" />
|
||||
<span className={cn("bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center", dmSansClassName())}>T</span>
|
||||
</Button>
|
||||
<motion.button
|
||||
onClick={toggleChat}
|
||||
className="flex items-center gap-2 rounded-full p-2 text-xs text-white cursor-pointer"
|
||||
layoutId="chat-toggle-button"
|
||||
>
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
</motion.button>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 !h-10 border-[#73737333] bg-[#0D121A]"
|
||||
>
|
||||
<HistoryIcon className="size-4 text-[#737373]" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 !h-10 border-[#73737333] bg-[#0D121A]"
|
||||
>
|
||||
<SquarePenIcon className="size-4 text-[#737373]" />
|
||||
<span
|
||||
className={cn(
|
||||
"bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
T
|
||||
</span>
|
||||
</Button>
|
||||
<motion.button
|
||||
onClick={toggleChat}
|
||||
className="flex items-center gap-2 rounded-full p-2 text-xs text-white cursor-pointer"
|
||||
layoutId="chat-toggle-button"
|
||||
>
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col px-4 space-y-3 pb-4 justify-end">
|
||||
{messages.map((msg, i) => (
|
||||
<div
|
||||
|
|
@ -201,4 +218,3 @@ export function ChatSidebar() {
|
|||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ function getFileTypeInfo(document: DocumentWithMemories): {
|
|||
return {
|
||||
icon: <PDF className="w-4 h-4 text-[#DC2626]" />,
|
||||
extension: ".pdf",
|
||||
color: "#DC2626",
|
||||
color: "#DC2626",
|
||||
}
|
||||
}
|
||||
if (mimeType.startsWith("image/")) {
|
||||
|
|
@ -49,7 +49,7 @@ function getFileTypeInfo(document: DocumentWithMemories): {
|
|||
return {
|
||||
icon: <PDF className="w-4 h-4 text-[#DC2626]" />,
|
||||
extension: ".pdf",
|
||||
color: "#DC2626",
|
||||
color: "#DC2626",
|
||||
}
|
||||
case "image":
|
||||
return {
|
||||
|
|
@ -72,10 +72,13 @@ function getFileTypeInfo(document: DocumentWithMemories): {
|
|||
export function FilePreview({ document }: { document: DocumentWithMemories }) {
|
||||
const [imageError, setImageError] = useState(false)
|
||||
const { icon, extension, color } = getFileTypeInfo(document)
|
||||
|
||||
|
||||
const type = document.type?.toLowerCase()
|
||||
const mimeType = document.metadata?.mimeType as string | undefined
|
||||
const isImage = (mimeType?.startsWith("image/") || type === "image") && document.url && !imageError
|
||||
const isImage =
|
||||
(mimeType?.startsWith("image/") || type === "image") &&
|
||||
document.url &&
|
||||
!imageError
|
||||
|
||||
return (
|
||||
<div className="bg-[#0B1017] rounded-[18px] gap-3 relative overflow-hidden">
|
||||
|
|
@ -87,25 +90,25 @@ export function FilePreview({ document }: { document: DocumentWithMemories }) {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{isImage ? (
|
||||
<>
|
||||
<div className="relative w-full aspect-video bg-gray-100 overflow-hidden">
|
||||
<img
|
||||
src={document.url!}
|
||||
alt={document.title || "Image preview"}
|
||||
className="w-full h-full object-cover"
|
||||
onError={() => setImageError(true)}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
{document.content && (
|
||||
<div className="p-3 pt-2">
|
||||
<p className="text-[10px] text-[#737373] line-clamp-4">
|
||||
{document.content}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
{isImage && document.url ? (
|
||||
<div className="relative w-full overflow-hidden flex items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: `url(${document.url})`,
|
||||
filter: "blur(10px)",
|
||||
transform: "scale(1.1)",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
<img
|
||||
src={document.url}
|
||||
alt={document.title || "Image preview"}
|
||||
className="relative max-w-full max-h-full w-auto h-auto object-contain z-10"
|
||||
onError={() => setImageError(true)}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-3">
|
||||
<div className="flex items-center gap-1 mb-2">
|
||||
|
|
@ -127,4 +130,3 @@ export function FilePreview({ document }: { document: DocumentWithMemories }) {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ import { cn } from "@lib/utils"
|
|||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
|
||||
export function GoogleDocsPreview({ document }: { document: DocumentWithMemories }) {
|
||||
export function GoogleDocsPreview({
|
||||
document,
|
||||
}: {
|
||||
document: DocumentWithMemories
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-[#0B1017] p-3 rounded-[18px] gap-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
|
|
@ -56,4 +60,3 @@ export function GoogleDocsPreview({ document }: { document: DocumentWithMemories
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
36
apps/web/components/new/document-cards/mcp-preview.tsx
Normal file
36
apps/web/components/new/document-cards/mcp-preview.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"use client"
|
||||
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
|
||||
import type { z } from "zod"
|
||||
import { dmSansClassName } from "@/utils/fonts"
|
||||
import { cn } from "@lib/utils"
|
||||
import { ClaudeDesktopIcon, MCPIcon } from "@ui/assets/icons"
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
|
||||
export function McpPreview({ document }: { document: DocumentWithMemories }) {
|
||||
return (
|
||||
<div className="bg-[#0B1017] p-3 rounded-[18px] space-y-2">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<p className={cn(dmSansClassName(), "text-[12px] font-semibold flex items-center gap-1")}>
|
||||
<ClaudeDesktopIcon className="size-3" />
|
||||
Claude Desktop
|
||||
</p>
|
||||
<MCPIcon className="size-6" />
|
||||
</div>
|
||||
<div>
|
||||
{document.title && (
|
||||
<p className={cn(dmSansClassName(), "text-[12px] font-semibold")}>
|
||||
{document.title}
|
||||
</p>
|
||||
)}
|
||||
{document.content && (
|
||||
<p className="text-[10px] text-[#737373] line-clamp-4">
|
||||
{document.content}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ function NoteIcon() {
|
|||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Note Icon</title>
|
||||
<mask
|
||||
id="mask0_344_4970"
|
||||
style={{ maskType: "alpha" }}
|
||||
|
|
@ -87,21 +88,17 @@ export function NotePreview({ document }: { document: DocumentWithMemories }) {
|
|||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
{
|
||||
document.title && (
|
||||
{document.title && (
|
||||
<p className={cn(dmSansClassName(), "text-[12px] font-semibold")}>
|
||||
{document.title}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
{document.content && (
|
||||
<p className="text-[10px] text-[#737373] line-clamp-4">
|
||||
{document.content}
|
||||
</p>
|
||||
)}
|
||||
)}
|
||||
{document.content && (
|
||||
<p className="text-[10px] text-[#737373] line-clamp-4">
|
||||
{document.content}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,4 +36,3 @@ export function TweetPreview({ data }: { data: Tweet }) {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,14 @@ import { cn } from "@lib/utils"
|
|||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
|
||||
export function WebsitePreview({ document }: { document: DocumentWithMemories }) {
|
||||
export function WebsitePreview({
|
||||
document,
|
||||
}: {
|
||||
document: DocumentWithMemories
|
||||
}) {
|
||||
const [imageError, setImageError] = useState(false)
|
||||
const ogImage = (document as DocumentWithMemories & { ogImage?: string }).ogImage
|
||||
const ogImage = (document as DocumentWithMemories & { ogImage?: string })
|
||||
.ogImage
|
||||
|
||||
return (
|
||||
<div className="bg-[#0B1017] rounded-[18px] overflow-hidden">
|
||||
|
|
@ -45,4 +50,3 @@ export function WebsitePreview({ document }: { document: DocumentWithMemories })
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,16 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
|
|||
import type { z } from "zod"
|
||||
import { dmSansClassName } from "@/utils/fonts"
|
||||
import { cn } from "@lib/utils"
|
||||
import { extractYouTubeVideoId } from "./youtube-utils"
|
||||
import { extractYouTubeVideoId } from "../utils"
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
|
||||
export function YoutubePreview({ document }: { document: DocumentWithMemories }) {
|
||||
export function YoutubePreview({
|
||||
document,
|
||||
}: {
|
||||
document: DocumentWithMemories
|
||||
}) {
|
||||
const videoId = extractYouTubeVideoId(document.url)
|
||||
|
||||
if (!videoId) {
|
||||
|
|
@ -46,4 +50,3 @@ export function YoutubePreview({ document }: { document: DocumentWithMemories })
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,71 +4,195 @@ import { Logo } from "@ui/assets/Logo"
|
|||
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { useEffect, useState } from "react"
|
||||
import { ChevronsLeftRight, LayoutGridIcon, Plus, SearchIcon } from "lucide-react"
|
||||
import {
|
||||
ChevronsLeftRight,
|
||||
LayoutGridIcon,
|
||||
Plus,
|
||||
SearchIcon,
|
||||
FolderIcon,
|
||||
} from "lucide-react"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/utils/fonts"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@ui/components/tabs"
|
||||
import { useProjectName } from "@/hooks/use-project-name"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/components/dropdown-menu"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { $fetch } from "@repo/lib/api"
|
||||
import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
|
||||
import { useProjectMutations } from "@/hooks/use-project-mutations"
|
||||
import { useProject } from "@/stores"
|
||||
|
||||
interface Project {
|
||||
id: string
|
||||
name: string
|
||||
containerTag: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
isExperimental?: boolean
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
const { user } = useAuth()
|
||||
const [name, setName] = useState<string>("")
|
||||
const { user } = useAuth()
|
||||
const [name, setName] = useState<string>("")
|
||||
const projectName = useProjectName()
|
||||
const { selectedProject } = useProject()
|
||||
const { switchProject } = useProjectMutations()
|
||||
|
||||
useEffect(() => {
|
||||
const storedName =
|
||||
localStorage.getItem("username") || localStorage.getItem("userName") || ""
|
||||
setName(storedName)
|
||||
}, [])
|
||||
const { data: projects = [] } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: async () => {
|
||||
const response = await $fetch("@get/projects")
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to load projects")
|
||||
}
|
||||
|
||||
return response.data?.projects || []
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const storedName =
|
||||
localStorage.getItem("username") || localStorage.getItem("userName") || ""
|
||||
setName(storedName)
|
||||
}, [])
|
||||
return (
|
||||
<div className="flex p-4 justify-between items-center">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="flex items-center">
|
||||
<Logo className="h-7" />
|
||||
{name && (
|
||||
<div className="flex flex-col items-start justify-center ml-2">
|
||||
<p className="text-[#8B8B8B] text-sm leading-tight">{name}'s</p>
|
||||
<p className="text-white font-bold text-xl leading-none -mt-1">
|
||||
supermemory
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="self-stretch w-[1px] bg-[#FFFFFF33]" />
|
||||
<div className="flex items-center gap-2">
|
||||
<p>📁 My Space</p>
|
||||
<ChevronsLeftRight className="size-4 rotate-90" />
|
||||
</div>
|
||||
</div>
|
||||
<Tabs defaultValue="grid">
|
||||
<TabsList className="rounded-full border border-[#161F2C] !h-11">
|
||||
<TabsTrigger value="grid" className={cn("rounded-full data-[state=active]:!bg-[#00173C] dark:data-[state=active]:!border-[#2261CA33] px-4 py-4", dmSansClassName())}><LayoutGridIcon className="size-4" />Grid</TabsTrigger>
|
||||
<TabsTrigger value="graph" className={cn("rounded-full dark:data-[state=active]:!bg-[#00173C] dark:data-[state=active]:!border-[#2261CA33] px-4 py-4", dmSansClassName())}><LayoutGridIcon className="size-4" />Graph</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="headers" className="rounded-full text-base gap-2 !h-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="size-4" />
|
||||
Add memory
|
||||
</div>
|
||||
<span className={cn("bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center", dmSansClassName())}>C</span>
|
||||
</Button>
|
||||
<Button variant="headers" className="rounded-full text-base gap-2 !h-10">
|
||||
<SearchIcon className="size-4" />
|
||||
<span className="bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm text-[10px] flex items-center justify-center gap-0.5 px-1">
|
||||
<svg className="size-[7.5px]" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.66663 0.416626C6.33511 0.416626 6.01716 0.548322 5.78274 0.782743C5.54832 1.01716 5.41663 1.33511 5.41663 1.66663V6.66663C5.41663 6.99815 5.54832 7.31609 5.78274 7.55051C6.01716 7.78493 6.33511 7.91663 6.66663 7.91663C6.99815 7.91663 7.31609 7.78493 7.55051 7.55051C7.78493 7.31609 7.91663 6.99815 7.91663 6.66663C7.91663 6.33511 7.78493 6.01716 7.55051 5.78274C7.31609 5.54832 6.99815 5.41663 6.66663 5.41663H1.66663C1.33511 5.41663 1.01716 5.54832 0.782743 5.78274C0.548322 6.01716 0.416626 6.33511 0.416626 6.66663C0.416626 6.99815 0.548322 7.31609 0.782743 7.55051C1.01716 7.78493 1.33511 7.91663 1.66663 7.91663C1.99815 7.91663 2.31609 7.78493 2.55051 7.55051C2.78493 7.31609 2.91663 6.99815 2.91663 6.66663V1.66663C2.91663 1.33511 2.78493 1.01716 2.55051 0.782743C2.31609 0.548322 1.99815 0.416626 1.66663 0.416626C1.33511 0.416626 1.01716 0.548322 0.782743 0.782743C0.548322 1.01716 0.416626 1.33511 0.416626 1.66663C0.416626 1.99815 0.548322 2.31609 0.782743 2.55051C1.01716 2.78493 1.33511 2.91663 1.66663 2.91663H6.66663C6.99815 2.91663 7.31609 2.78493 7.55051 2.55051C7.78493 2.31609 7.91663 1.99815 7.91663 1.66663C7.91663 1.33511 7.78493 1.01716 7.55051 0.782743C7.31609 0.548322 6.99815 0.416626 6.66663 0.416626Z" stroke="#737373" strokeWidth="0.833333" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span className={cn(dmSansClassName())}>K</span>
|
||||
</span>
|
||||
</Button>
|
||||
{user && (
|
||||
<Avatar className="border border-border h-8 w-8 md:h-10 md:w-10">
|
||||
<AvatarImage src={user?.image ?? ""} />
|
||||
<AvatarFallback>{user?.name?.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="flex items-center">
|
||||
<Logo className="h-7" />
|
||||
{name && (
|
||||
<div className="flex flex-col items-start justify-center ml-2">
|
||||
<p className="text-[#8B8B8B] text-sm leading-tight">{name}'s</p>
|
||||
<p className="text-white font-bold text-xl leading-none -mt-1">
|
||||
supermemory
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="self-stretch w-[1px] bg-[#FFFFFF33]" />
|
||||
<div className="flex items-center gap-2">
|
||||
<p>📁 {projectName}</p>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer hover:opacity-70 transition-opacity"
|
||||
aria-label="Change project"
|
||||
>
|
||||
<ChevronsLeftRight className="size-4 rotate-90" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuItem
|
||||
onClick={() => switchProject(DEFAULT_PROJECT_ID)}
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
selectedProject === DEFAULT_PROJECT_ID && "bg-accent",
|
||||
)}
|
||||
>
|
||||
<FolderIcon className="h-3.5 w-3.5 mr-2" />
|
||||
<span className="text-sm">Default Project</span>
|
||||
</DropdownMenuItem>
|
||||
{projects
|
||||
.filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID)
|
||||
.map((project: Project) => (
|
||||
<DropdownMenuItem
|
||||
key={project.id}
|
||||
onClick={() => switchProject(project.containerTag)}
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
selectedProject === project.containerTag && "bg-accent",
|
||||
)}
|
||||
>
|
||||
<FolderIcon className="h-3.5 w-3.5 mr-2 opacity-70" />
|
||||
<span className="text-sm truncate">{project.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs defaultValue="grid">
|
||||
<TabsList className="rounded-full border border-[#161F2C] !h-11">
|
||||
<TabsTrigger
|
||||
value="grid"
|
||||
className={cn(
|
||||
"rounded-full data-[state=active]:!bg-[#00173C] dark:data-[state=active]:!border-[#2261CA33] px-4 py-4",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<LayoutGridIcon className="size-4" />
|
||||
Grid
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="graph"
|
||||
className={cn(
|
||||
"rounded-full dark:data-[state=active]:!bg-[#00173C] dark:data-[state=active]:!border-[#2261CA33] px-4 py-4",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<LayoutGridIcon className="size-4" />
|
||||
Graph
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 !h-10"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="size-4" />
|
||||
Add memory
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
C
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 !h-10"
|
||||
>
|
||||
<SearchIcon className="size-4" />
|
||||
<span className="bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm text-[10px] flex items-center justify-center gap-0.5 px-1">
|
||||
<svg
|
||||
className="size-[7.5px]"
|
||||
viewBox="0 0 9 9"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Search Icon</title>
|
||||
<path
|
||||
d="M6.66663 0.416626C6.33511 0.416626 6.01716 0.548322 5.78274 0.782743C5.54832 1.01716 5.41663 1.33511 5.41663 1.66663V6.66663C5.41663 6.99815 5.54832 7.31609 5.78274 7.55051C6.01716 7.78493 6.33511 7.91663 6.66663 7.91663C6.99815 7.91663 7.31609 7.78493 7.55051 7.55051C7.78493 7.31609 7.91663 6.99815 7.91663 6.66663C7.91663 6.33511 7.78493 6.01716 7.55051 5.78274C7.31609 5.54832 6.99815 5.41663 6.66663 5.41663H1.66663C1.33511 5.41663 1.01716 5.54832 0.782743 5.78274C0.548322 6.01716 0.416626 6.33511 0.416626 6.66663C0.416626 6.99815 0.548322 7.31609 0.782743 7.55051C1.01716 7.78493 1.33511 7.91663 1.66663 7.91663C1.99815 7.91663 2.31609 7.78493 2.55051 7.55051C2.78493 7.31609 2.91663 6.99815 2.91663 6.66663V1.66663C2.91663 1.33511 2.78493 1.01716 2.55051 0.782743C2.31609 0.548322 1.99815 0.416626 1.66663 0.416626C1.33511 0.416626 1.01716 0.548322 0.782743 0.782743C0.548322 1.01716 0.416626 1.33511 0.416626 1.66663C0.416626 1.99815 0.548322 2.31609 0.782743 2.55051C1.01716 2.78493 1.33511 2.91663 1.66663 2.91663H6.66663C6.99815 2.91663 7.31609 2.78493 7.55051 2.55051C7.78493 2.31609 7.91663 1.99815 7.91663 1.66663C7.91663 1.33511 7.78493 1.01716 7.55051 0.782743C7.31609 0.548322 6.99815 0.416626 6.66663 0.416626Z"
|
||||
stroke="#737373"
|
||||
strokeWidth="0.833333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className={cn(dmSansClassName())}>K</span>
|
||||
</span>
|
||||
</Button>
|
||||
{user && (
|
||||
<Avatar className="border border-border h-8 w-8 md:h-10 md:w-10">
|
||||
<AvatarImage src={user?.image ?? ""} />
|
||||
<AvatarFallback>{user?.name?.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,9 @@ import { GoogleDocsPreview } from "./document-cards/google-docs-preview"
|
|||
import { FilePreview } from "./document-cards/file-preview"
|
||||
import { NotePreview } from "./document-cards/note-preview"
|
||||
import { YoutubePreview } from "./document-cards/youtube-preview"
|
||||
import {
|
||||
isYouTubeUrl,
|
||||
useYouTubeChannelName,
|
||||
} from "./document-cards/youtube-utils"
|
||||
import { getAbsoluteUrl, isYouTubeUrl, useYouTubeChannelName } from "./utils"
|
||||
import { SyncLogoIcon } from "@ui/assets/icons"
|
||||
import { McpPreview } from "./document-cards/mcp-preview"
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
|
|
@ -83,7 +82,9 @@ export function MemoriesGrid() {
|
|||
})
|
||||
|
||||
const documents = useMemo(() => {
|
||||
return data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? []
|
||||
return (
|
||||
data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? []
|
||||
)
|
||||
}, [data])
|
||||
|
||||
const hasMore = hasNextPage
|
||||
|
|
@ -119,13 +120,7 @@ export function MemoriesGrid() {
|
|||
index: number
|
||||
data: DocumentWithMemories
|
||||
width: number
|
||||
}) => (
|
||||
<DocumentCard
|
||||
index={index}
|
||||
data={data}
|
||||
width={width}
|
||||
/>
|
||||
),
|
||||
}) => <DocumentCard index={index} data={data} width={width} />,
|
||||
[],
|
||||
)
|
||||
|
||||
|
|
@ -234,54 +229,71 @@ const DocumentCard = memo(
|
|||
width: number
|
||||
}) => {
|
||||
return (
|
||||
<div className="rounded-[22px] bg-[#1B1F24] px-1 space-y-2 pt-1" style={{ width }}>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-[22px] bg-[#1B1F24] px-1 space-y-2 pt-1",
|
||||
document.type === "image" ||
|
||||
document.metadata?.mimeType?.toString().startsWith("image/")
|
||||
? "pb-1"
|
||||
: "",
|
||||
)}
|
||||
style={{ width }}
|
||||
>
|
||||
<ContentPreview document={document} />
|
||||
<div className="pb-[10px] space-y-1">
|
||||
{document.url && !document.url.includes("x.com") && !document.url.includes("twitter.com") && !document.url.includes("files.supermemory.ai") && (
|
||||
<div className="px-3">
|
||||
<p
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-[12px] text-[#E5E5E5] line-clamp-1 font-semibold",
|
||||
{!(
|
||||
document.type === "image" ||
|
||||
document.metadata?.mimeType?.toString().startsWith("image/")
|
||||
) && (
|
||||
<div className="pb-[10px] space-y-1">
|
||||
{document.url &&
|
||||
!document.url.includes("x.com") &&
|
||||
!document.url.includes("twitter.com") &&
|
||||
!document.url.includes("files.supermemory.ai") && (
|
||||
<div className="px-3">
|
||||
<p
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-[12px] text-[#E5E5E5] line-clamp-1 font-semibold",
|
||||
)}
|
||||
>
|
||||
{document.title}
|
||||
</p>
|
||||
|
||||
<DocumentUrlDisplay url={document.url} />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{document.title}
|
||||
</p>
|
||||
|
||||
<DocumentUrlDisplay url={document.url} />
|
||||
<div className="flex items-center justify-between px-3">
|
||||
<p
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-[10px] text-[#369BFD] line-clamp-1 font-semibold flex items-center gap-1",
|
||||
)}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
|
||||
backgroundClip: "text",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
}}
|
||||
>
|
||||
<SyncLogoIcon className="size-2" />
|
||||
{document.memoryEntries.length} memories
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-[10px] text-[#737373] line-clamp-1",
|
||||
)}
|
||||
>
|
||||
{new Date(document.createdAt).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between px-3">
|
||||
<p
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-[10px] text-[#369BFD] line-clamp-1 font-semibold flex items-center gap-1",
|
||||
)}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
|
||||
backgroundClip: "text",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
}}
|
||||
>
|
||||
<SyncLogoIcon className="size-2" />
|
||||
{document.memoryEntries.length} memories
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-[10px] text-[#737373] line-clamp-1",
|
||||
)}
|
||||
>
|
||||
{new Date(document.createdAt).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
@ -289,72 +301,7 @@ const DocumentCard = memo(
|
|||
|
||||
DocumentCard.displayName = "DocumentCard"
|
||||
|
||||
function getAbsoluteUrl(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
return urlObj.host.replace(/^www\./, "")
|
||||
} catch {
|
||||
const match = url.match(/^https?:\/\/([^\/]+)/)
|
||||
const host = match?.[1] ?? url.replace(/^https?:\/\//, "")
|
||||
return host.replace(/^www\./, "")
|
||||
}
|
||||
}
|
||||
|
||||
function SyncLogoIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width="11"
|
||||
height="9"
|
||||
viewBox="0 0 11 9"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g clipPath="url(#clip0_344_4856)">
|
||||
<path
|
||||
d="M10.596 3.41884H6.66329V0.000488281H5.39264V3.70946C5.39264 4.10339 5.54815 4.48172 5.82456 4.76047L9.03576 7.99894L9.9342 7.09287L7.56245 4.70102H10.5968V3.41959L10.596 3.41884Z"
|
||||
fill="url(#paint0_linear_344_4856)"
|
||||
/>
|
||||
<path
|
||||
d="M0.662587 1.57476L3.03434 3.96665H0V5.24807H3.93276V8.66641H5.20341V4.95745C5.20341 4.56349 5.0479 4.18516 4.77149 3.90644L1.56102 0.668701L0.662587 1.57476Z"
|
||||
fill="url(#paint1_linear_344_4856)"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_344_4856"
|
||||
x1="5.65905"
|
||||
y1="-0.00784643"
|
||||
x2="15.4099"
|
||||
y2="0.406611"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#369BFD" />
|
||||
<stop offset="0.41" stopColor="#36FDFD" />
|
||||
<stop offset="0.79" stopColor="#36FDB5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_344_4856"
|
||||
x1="0.266373"
|
||||
y1="0.660367"
|
||||
x2="10.0159"
|
||||
y2="1.07475"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#369BFD" />
|
||||
<stop offset="0.41" stopColor="#36FDFD" />
|
||||
<stop offset="0.79" stopColor="#36FDB5" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_344_4856">
|
||||
<rect width="10.6889" height="8.66667" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ContentPreview({ document }: { document: DocumentWithMemories }) {
|
||||
// Check for Google Docs
|
||||
if (
|
||||
document.url?.includes("https://docs.googleapis.com/v1/documents") ||
|
||||
document.url?.includes("docs.google.com/document") ||
|
||||
|
|
@ -363,7 +310,6 @@ function ContentPreview({ document }: { document: DocumentWithMemories }) {
|
|||
return <GoogleDocsPreview document={document} />
|
||||
}
|
||||
|
||||
// Check for Twitter
|
||||
if (
|
||||
document.url?.includes("x.com/") &&
|
||||
document.metadata?.sm_internal_twitter_metadata
|
||||
|
|
@ -377,6 +323,10 @@ function ContentPreview({ document }: { document: DocumentWithMemories }) {
|
|||
)
|
||||
}
|
||||
|
||||
if (document.source === "mcp") {
|
||||
return <McpPreview document={document} />
|
||||
}
|
||||
|
||||
if (isYouTubeUrl(document.url)) {
|
||||
return <YoutubePreview document={document} />
|
||||
}
|
||||
|
|
@ -390,7 +340,6 @@ function ContentPreview({ document }: { document: DocumentWithMemories }) {
|
|||
return <FilePreview document={document} />
|
||||
}
|
||||
|
||||
// Check for Website
|
||||
if (document.url?.includes("https://")) {
|
||||
return <WebsitePreview document={document} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ export function isYouTubeUrl(url: string | undefined | null): boolean {
|
|||
)
|
||||
}
|
||||
|
||||
export function extractYouTubeVideoId(url: string | undefined | null): string | null {
|
||||
export function extractYouTubeVideoId(
|
||||
url: string | undefined | null,
|
||||
): string | null {
|
||||
if (!url) return null
|
||||
|
||||
// Handle youtu.be format
|
||||
|
|
@ -27,7 +29,9 @@ export function extractYouTubeVideoId(url: string | undefined | null): string |
|
|||
if (embedMatch?.[1]) return embedMatch[1]
|
||||
|
||||
// Handle m.youtube.com format
|
||||
const mobileMatch = url.match(/(?:m\.youtube\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/)
|
||||
const mobileMatch = url.match(
|
||||
/(?:m\.youtube\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/,
|
||||
)
|
||||
if (mobileMatch?.[1]) return mobileMatch[1]
|
||||
|
||||
return null
|
||||
|
|
@ -61,3 +65,14 @@ export function useYouTubeChannelName(url: string | undefined | null) {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
export function getAbsoluteUrl(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
return urlObj.host.replace(/^www\./, "")
|
||||
} catch {
|
||||
const match = url.match(/^https?:\/\/([^\/]+)/)
|
||||
const host = match?.[1] ?? url.replace(/^https?:\/\//, "")
|
||||
return host.replace(/^www\./, "")
|
||||
}
|
||||
}
|
||||
|
|
@ -81,13 +81,6 @@
|
|||
"useNumberNamespace": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useSingleVarDeclarator": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noUnknownAtRules": {
|
||||
"options": {
|
||||
"ignore": ["source", "theme", "apply"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue