Add Mem0 Demo (#2291)

This commit is contained in:
Saket Aryan
2025-03-04 23:34:59 +05:30
committed by GitHub
parent f7500c925e
commit aa7ab9736d
45 changed files with 2464 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
"use client";
import "@assistant-ui/react-markdown/styles/dot.css";
import {
CodeHeaderProps,
MarkdownTextPrimitive,
unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
useIsMarkdownCodeBlock,
} from "@assistant-ui/react-markdown";
import remarkGfm from "remark-gfm";
import { FC, memo, useState } from "react";
import { CheckIcon, CopyIcon } from "lucide-react";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { cn } from "@/lib/utils";
const MarkdownTextImpl = () => {
return (
<MarkdownTextPrimitive
remarkPlugins={[remarkGfm]}
className="aui-md"
components={defaultComponents}
/>
);
};
export const MarkdownText = memo(MarkdownTextImpl);
const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
const { isCopied, copyToClipboard } = useCopyToClipboard();
const onCopy = () => {
if (!code || isCopied) return;
copyToClipboard(code);
};
return (
<div className="flex items-center justify-between gap-4 rounded-t-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white">
<span className="lowercase [&>span]:text-xs">{language}</span>
<TooltipIconButton tooltip="Copy" onClick={onCopy}>
{!isCopied && <CopyIcon />}
{isCopied && <CheckIcon />}
</TooltipIconButton>
</div>
);
};
const useCopyToClipboard = ({
copiedDuration = 3000,
}: {
copiedDuration?: number;
} = {}) => {
const [isCopied, setIsCopied] = useState<boolean>(false);
const copyToClipboard = (value: string) => {
if (!value) return;
navigator.clipboard.writeText(value).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), copiedDuration);
});
};
return { isCopied, copyToClipboard };
};
const defaultComponents = memoizeMarkdownComponents({
h1: ({ className, ...props }) => (
<h1 className={cn("mb-8 scroll-m-20 text-4xl font-extrabold tracking-tight last:mb-0", className)} {...props} />
),
h2: ({ className, ...props }) => (
<h2 className={cn("mb-4 mt-8 scroll-m-20 text-3xl font-semibold tracking-tight first:mt-0 last:mb-0", className)} {...props} />
),
h3: ({ className, ...props }) => (
<h3 className={cn("mb-4 mt-6 scroll-m-20 text-2xl font-semibold tracking-tight first:mt-0 last:mb-0", className)} {...props} />
),
h4: ({ className, ...props }) => (
<h4 className={cn("mb-4 mt-6 scroll-m-20 text-xl font-semibold tracking-tight first:mt-0 last:mb-0", className)} {...props} />
),
h5: ({ className, ...props }) => (
<h5 className={cn("my-4 text-lg font-semibold first:mt-0 last:mb-0", className)} {...props} />
),
h6: ({ className, ...props }) => (
<h6 className={cn("my-4 font-semibold first:mt-0 last:mb-0", className)} {...props} />
),
p: ({ className, ...props }) => (
<p className={cn("mb-5 mt-5 leading-7 first:mt-0 last:mb-0", className)} {...props} />
),
a: ({ className, ...props }) => (
<a className={cn("text-primary font-medium underline underline-offset-4", className)} {...props} />
),
blockquote: ({ className, ...props }) => (
<blockquote className={cn("border-l-2 pl-6 italic", className)} {...props} />
),
ul: ({ className, ...props }) => (
<ul className={cn("my-5 ml-6 list-disc [&>li]:mt-2", className)} {...props} />
),
ol: ({ className, ...props }) => (
<ol className={cn("my-5 ml-6 list-decimal [&>li]:mt-2", className)} {...props} />
),
hr: ({ className, ...props }) => (
<hr className={cn("my-5 border-b", className)} {...props} />
),
table: ({ className, ...props }) => (
<table className={cn("my-5 w-full border-separate border-spacing-0 overflow-y-auto", className)} {...props} />
),
th: ({ className, ...props }) => (
<th className={cn("bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [&[align=center]]:text-center [&[align=right]]:text-right", className)} {...props} />
),
td: ({ className, ...props }) => (
<td className={cn("border-b border-l px-4 py-2 text-left last:border-r [&[align=center]]:text-center [&[align=right]]:text-right", className)} {...props} />
),
tr: ({ className, ...props }) => (
<tr className={cn("m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg", className)} {...props} />
),
sup: ({ className, ...props }) => (
<sup className={cn("[&>a]:text-xs [&>a]:no-underline", className)} {...props} />
),
pre: ({ className, ...props }) => (
<pre className={cn("overflow-x-auto rounded-b-lg bg-black p-4 text-white", className)} {...props} />
),
code: function Code({ className, ...props }) {
const isCodeBlock = useIsMarkdownCodeBlock();
return (
<code
className={cn(!isCodeBlock && "bg-muted rounded border font-semibold", className)}
{...props}
/>
);
},
CodeHeader,
});

View File

@@ -0,0 +1,106 @@
"use client";
import * as React from "react";
import { Book } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "../ui/scroll-area";
export type Memory = {
event: "ADD" | "UPDATE" | "DELETE" | "GET";
id: string;
memory: string;
score: number;
};
interface MemoryIndicatorProps {
memories: Memory[];
}
export default function MemoryIndicator({ memories }: MemoryIndicatorProps) {
const [isOpen, setIsOpen] = React.useState(false);
// Determine the memory state
const hasAccessed = memories.some((memory) => memory.event === "GET");
const hasUpdated = memories.some((memory) => memory.event !== "GET");
let statusText = "";
let variant: "default" | "secondary" | "outline" = "default";
if (hasAccessed && hasUpdated) {
statusText = "Memory accessed and updated";
variant = "default";
} else if (hasAccessed) {
statusText = "Memory accessed";
variant = "secondary";
} else if (hasUpdated) {
statusText = "Memory updated";
variant = "default";
}
if (!statusText) return null;
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Badge
variant={variant}
className="flex items-center gap-1 cursor-pointer hover:opacity-90 transition-opacity rounded-full bg-zinc-800 hover:bg-zinc-700 dark:bg-[#6366f1] text-white"
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
>
<Book className="h-3.5 w-3.5" />
<span>{statusText}</span>
</Badge>
</PopoverTrigger>
<PopoverContent
className="w-80 p-4 rounded-xl border-[#e2e8f0] dark:border-zinc-700"
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
>
<div className="space-y-3">
<h4 className="text-sm font-semibold">Memories</h4>
<ScrollArea className="h-[200px]">
<ul className="text-sm space-y-2 pr-4">
{memories.map((memory) => (
<li
key={memory.id + memory.event}
className="flex items-start gap-2 pb-2 border-b border-[#e2e8f0] dark:border-zinc-700 last:border-0 last:pb-0"
>
<Badge
variant={
memory.event === "GET"
? "secondary"
: memory.event === "ADD"
? "outline"
: memory.event === "UPDATE"
? "default"
: "destructive"
}
className="mt-0.5 text-xs shrink-0 rounded-full"
>
{memory.event === "GET" && "Accessed"}
{memory.event === "ADD" && "Created"}
{memory.event === "UPDATE" && "Updated"}
{memory.event === "DELETE" && "Deleted"}
</Badge>
<span className="flex-1">{memory.memory}</span>
{memory.event === "GET" && (
<span className="shrink-0">
{Math.round(memory.score * 100)}%
</span>
)}
</li>
))}
</ul>
</ScrollArea>
</div>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,80 @@
import { useMessage } from "@assistant-ui/react";
import { FC, useMemo } from "react";
import MemoryIndicator, { Memory } from "./memory-indicator";
type RetrievedMemory = {
isNew: boolean;
id: string;
memory: string;
user_id: string;
categories: readonly string[];
immutable: boolean;
created_at: string;
updated_at: string;
score: number;
};
type NewMemory = {
id: string;
data: {
memory: string;
};
event: "ADD" | "DELETE";
};
type NewMemoryAnnotation = {
readonly type: "mem0-update";
readonly memories: readonly NewMemory[];
};
type GetMemoryAnnotation = {
readonly type: "mem0-get";
readonly memories: readonly RetrievedMemory[];
};
type MemoryAnnotation = NewMemoryAnnotation | GetMemoryAnnotation;
const isMemoryAnnotation = (a: unknown): a is MemoryAnnotation =>
typeof a === "object" &&
a != null &&
"type" in a &&
(a.type === "mem0-update" || a.type === "mem0-get");
const useMemories = (): Memory[] => {
const annotations = useMessage((m) => m.metadata.unstable_annotations);
console.log("annotations", annotations);
return useMemo(
() =>
annotations?.filter(isMemoryAnnotation).flatMap((a) => {
if (a.type === "mem0-update") {
return a.memories.map(
(m): Memory => ({
event: m.event,
id: m.id,
memory: m.data.memory,
score: 1,
})
);
} else if (a.type === "mem0-get") {
return a.memories.map((m) => ({
event: "GET",
id: m.id,
memory: m.memory,
score: m.score,
}));
}
throw new Error("Unexpected annotation: " + JSON.stringify(a));
}) ?? [],
[annotations]
);
};
export const MemoryUI: FC = () => {
const memories = useMemories();
return (
<div className="flex mb-1">
<MemoryIndicator memories={memories} />
</div>
);
};

View File

@@ -0,0 +1,40 @@
"use client";
import React from "react";
import Image from "next/image";
export default function ThemeAwareLogo({
width = 40,
height = 40,
variant = "default",
isDarkMode = false,
}: {
width?: number;
height?: number;
variant?: "default" | "collapsed";
isDarkMode?: boolean;
}) {
// For collapsed variant, always use the icon
if (variant === "collapsed") {
return (
<div
className={`flex items-center justify-center rounded-full ${isDarkMode ? 'bg-[#6366f1]' : 'bg-[#4f46e5]'}`}
style={{ width, height }}
>
<span className="text-white font-bold text-lg">M</span>
</div>
);
}
// For default variant, use the full logo image
const logoSrc = isDarkMode ? "/images/assistant-ui-dark.svg" : "/images/assistant-ui.svg";
return (
<Image
src={logoSrc}
alt="Mem0.ai"
width={width}
height={height}
/>
);
}

View File

@@ -0,0 +1,125 @@
import type { FC } from "react";
import {
ThreadListItemPrimitive,
ThreadListPrimitive,
} from "@assistant-ui/react";
import { ArchiveIcon, PlusIcon, RefreshCwIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
interface ThreadListProps {
onResetUserId?: () => void;
}
export const ThreadList: FC<ThreadListProps> = ({ onResetUserId }) => {
const [open, setOpen] = useState(false);
return (
<div className="flex-col h-full border-r border-[#e2e8f0] bg-white dark:bg-zinc-900 dark:border-zinc-800 p-3 overflow-y-auto hidden md:flex">
<ThreadListPrimitive.Root className="flex flex-col items-stretch gap-1.5">
<ThreadListNew />
<div className="mt-4 mb-2 flex justify-between items-center px-2.5">
<h2 className="text-sm font-medium text-[#475569] dark:text-zinc-300">Recent Chats</h2>
{onResetUserId && (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>
<TooltipIconButton
tooltip="Reset Memory"
className="hover:text-[#4f46e5] text-[#475569] dark:text-zinc-300 dark:hover:text-[#6366f1] size-4 p-0"
variant="ghost"
>
<RefreshCwIcon className="w-4 h-4" />
</TooltipIconButton>
</AlertDialogTrigger>
<AlertDialogContent className="bg-white dark:bg-zinc-900 border-[#e2e8f0] dark:border-zinc-800">
<AlertDialogHeader>
<AlertDialogTitle className="text-[#1e293b] dark:text-white">Reset Memory</AlertDialogTitle>
<AlertDialogDescription className="text-[#475569] dark:text-zinc-300">
This will permanently delete all your chat history and memories. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="text-[#475569] dark:text-zinc-300 hover:bg-[#eef2ff] dark:hover:bg-zinc-800">Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
onResetUserId();
setOpen(false);
}}
className="bg-[#4f46e5] hover:bg-[#4338ca] dark:bg-[#6366f1] dark:hover:bg-[#4f46e5] text-white"
>
Reset
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
<ThreadListItems />
</ThreadListPrimitive.Root>
</div>
);
};
const ThreadListNew: FC = () => {
return (
<ThreadListPrimitive.New asChild>
<Button
className="hover:bg-[#8ea4e8] dark:hover:bg-zinc-800 dark:data-[active]:bg-zinc-800 flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start bg-[#4f46e5] text-white dark:bg-[#6366f1]"
variant="default"
>
<PlusIcon className="w-4 h-4" />
New Thread
</Button>
</ThreadListPrimitive.New>
);
};
const ThreadListItems: FC = () => {
return <ThreadListPrimitive.Items components={{ ThreadListItem }} />;
};
const ThreadListItem: FC = () => {
return (
<ThreadListItemPrimitive.Root className="data-[active]:bg-[#eef2ff] hover:bg-[#eef2ff] dark:hover:bg-zinc-800 dark:data-[active]:bg-zinc-800 dark:text-white focus-visible:bg-[#eef2ff] dark:focus-visible:bg-zinc-800 focus-visible:ring-[#4f46e5] flex items-center gap-2 rounded-lg transition-all focus-visible:outline-none focus-visible:ring-2">
<ThreadListItemPrimitive.Trigger className="flex-grow px-3 py-2 text-start">
<ThreadListItemTitle />
</ThreadListItemPrimitive.Trigger>
<ThreadListItemArchive />
</ThreadListItemPrimitive.Root>
);
};
const ThreadListItemTitle: FC = () => {
return (
<p className="text-sm">
<ThreadListItemPrimitive.Title fallback="New Chat" />
</p>
);
};
const ThreadListItemArchive: FC = () => {
return (
<ThreadListItemPrimitive.Archive asChild>
<TooltipIconButton
className="hover:text-[#4f46e5] text-[#475569] dark:text-zinc-300 dark:hover:text-[#6366f1] ml-auto mr-3 size-4 p-0"
variant="ghost"
tooltip="Archive thread"
>
<ArchiveIcon />
</TooltipIconButton>
</ThreadListItemPrimitive.Archive>
);
};

View File

@@ -0,0 +1,457 @@
"use client"
import {
ActionBarPrimitive,
BranchPickerPrimitive,
ComposerPrimitive,
MessagePrimitive,
ThreadPrimitive,
ThreadListItemPrimitive,
ThreadListPrimitive,
useMessage,
} from "@assistant-ui/react";
import type { FC } from "react";
import {
ArrowDownIcon,
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon,
CopyIcon,
PencilIcon,
RefreshCwIcon,
SendHorizontalIcon,
ArchiveIcon,
PlusIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { Dispatch, SetStateAction, useState } from "react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { MemoryUI } from "./memory-ui";
import MarkdownRenderer from "../mem0/markdown";
import React from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
interface ThreadProps {
sidebarOpen: boolean;
setSidebarOpen: Dispatch<SetStateAction<boolean>>;
onResetUserId?: () => void;
}
export const Thread: FC<ThreadProps> = ({ sidebarOpen, setSidebarOpen, onResetUserId }) => {
const [resetDialogOpen, setResetDialogOpen] = useState(false);
return (
<ThreadPrimitive.Root
className="bg-[#f8fafc] dark:bg-zinc-900 box-border h-full flex flex-col overflow-hidden relative"
style={{
["--thread-max-width" as string]: "42rem",
}}
>
{/* Mobile sidebar overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/40 z-30 md:hidden"
onClick={() => setSidebarOpen(false)}
></div>
)}
{/* Mobile sidebar drawer */}
<div className={cn(
"fixed inset-y-0 left-0 z-40 w-[85%] bg-white dark:bg-zinc-900 transform transition-transform duration-300 ease-in-out md:hidden",
sidebarOpen ? "translate-x-0" : "-translate-x-full"
)}>
<div className="h-full flex flex-col">
<div className="flex items-center justify-between border-b dark:text-white border-[#e2e8f0] dark:border-zinc-800 p-4">
<h2 className="font-medium">Recent Chats</h2>
<div className="flex items-center gap-2">
{onResetUserId && (
<AlertDialog open={resetDialogOpen} onOpenChange={setResetDialogOpen}>
<AlertDialogTrigger asChild>
<TooltipIconButton
tooltip="Reset Memory"
className="hover:text-[#4f46e5] text-[#475569] dark:text-zinc-300 dark:hover:text-[#6366f1] size-8 p-0"
variant="ghost"
>
<RefreshCwIcon className="w-4 h-4" />
</TooltipIconButton>
</AlertDialogTrigger>
<AlertDialogContent className="bg-white dark:bg-zinc-900 border-[#e2e8f0] dark:border-zinc-800">
<AlertDialogHeader>
<AlertDialogTitle className="text-[#1e293b] dark:text-white">Reset Memory</AlertDialogTitle>
<AlertDialogDescription className="text-[#475569] dark:text-zinc-300">
This will permanently delete all your chat history and memories. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="text-[#475569] dark:text-zinc-300 hover:bg-[#eef2ff] dark:hover:bg-zinc-800">Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
onResetUserId();
setResetDialogOpen(false);
}}
className="bg-[#4f46e5] hover:bg-[#4338ca] dark:bg-[#6366f1] dark:hover:bg-[#4f46e5] text-white"
>
Reset
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarOpen(false)}
className="text-[#475569] dark:text-zinc-300 hover:bg-[#eef2ff] dark:hover:bg-zinc-800 h-8 w-8 p-0"
>
</Button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-3">
<ThreadListPrimitive.Root className="flex flex-col items-stretch gap-1.5 h-full dark:text-white">
<ThreadListPrimitive.New asChild>
<Button
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 dark:data-[active]:bg-zinc-800 flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start bg-[#4f46e5] text-white dark:bg-[#6366f1]"
variant="default"
>
<PlusIcon className="w-4 h-4" />
New Thread
</Button>
</ThreadListPrimitive.New>
<div className="mt-4 mb-2">
<h2 className="text-sm font-medium text-[#475569] dark:text-zinc-300 px-2.5">Recent Chats</h2>
</div>
<ThreadListPrimitive.Items components={{ ThreadListItem }} />
</ThreadListPrimitive.Root>
</div>
</div>
</div>
<ScrollArea className="flex-1">
<div className="flex h-full flex-col items-center px-4 pt-8 justify-end">
<ThreadWelcome />
<ThreadPrimitive.Messages
components={{
UserMessage: UserMessage,
EditComposer: EditComposer,
AssistantMessage: AssistantMessage,
}}
/>
<ThreadPrimitive.If empty={false}>
<div className="min-h-8 flex-grow" />
</ThreadPrimitive.If>
</div>
</ScrollArea>
<div className="sticky bottom-0 mt-3 flex w-full max-w-[var(--thread-max-width)] flex-col items-center justify-end rounded-t-lg bg-inherit px-4 pb-4 mx-auto">
<ThreadScrollToBottom />
<Composer />
</div>
</ThreadPrimitive.Root>
);
};
const ThreadScrollToBottom: FC = () => {
return (
<ThreadPrimitive.ScrollToBottom asChild>
<TooltipIconButton
tooltip="Scroll to bottom"
variant="outline"
className="absolute -top-8 rounded-full disabled:invisible bg-white dark:bg-zinc-800 border-[#e2e8f0] dark:border-zinc-700 hover:bg-[#eef2ff] dark:hover:bg-zinc-700"
>
<ArrowDownIcon className="text-[#475569] dark:text-zinc-300" />
</TooltipIconButton>
</ThreadPrimitive.ScrollToBottom>
);
};
const ThreadWelcome: FC = () => {
return (
<ThreadPrimitive.Empty>
<div className="flex w-full max-w-[var(--thread-max-width)] flex-grow flex-col">
<div className="flex w-full flex-grow flex-col items-center justify-start h-[calc(100vh-23rem)] md:h-[calc(100vh-18rem)]">
<div className="flex flex-col items-center justify-center h-full">
<div className="text-5xl font-bold text-[#1e293b] dark:text-white mb-2">
Mem0 Demo
</div>
<p className="text-center text-sm text-[#1e293b] dark:text-white mb-2 w-3/4">
A personalized AI chat app powered by Mem0 that remembers your preferences, facts, and memories.
</p>
</div>
</div>
<div className="flex flex-col items-center justify-center">
<p className="mt-4 font-medium text-[#1e293b] dark:text-white">
How can I help you today?
</p>
<ThreadWelcomeSuggestions />
</div>
</div>
</ThreadPrimitive.Empty>
);
};
const ThreadWelcomeSuggestions: FC = () => {
return (
<div className="mt-3 flex w-full items-stretch justify-center gap-4 dark:text-white">
<ThreadPrimitive.Suggestion
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-[2rem] border border-[#e2e8f0] dark:border-zinc-700 p-3 transition-colors ease-in"
prompt="I like to travel to "
method="replace"
>
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">Travel</span>
</ThreadPrimitive.Suggestion>
<ThreadPrimitive.Suggestion
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-[2rem] border border-[#e2e8f0] dark:border-zinc-700 p-3 transition-colors ease-in"
prompt="I like to eat "
method="replace"
>
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">Food</span>
</ThreadPrimitive.Suggestion>
<ThreadPrimitive.Suggestion
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-[2rem] border border-[#e2e8f0] dark:border-zinc-700 p-3 transition-colors ease-in"
prompt="I am working on "
method="replace"
>
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">Project details</span>
</ThreadPrimitive.Suggestion>
</div>
);
};
const Composer: FC = () => {
return (
<ComposerPrimitive.Root className="focus-within:border-[#4f46e5]/20 dark:focus-within:border-[#6366f1]/20 flex w-full flex-wrap items-end rounded-full border border-[#e2e8f0] dark:border-zinc-700 bg-white dark:bg-zinc-800 px-2.5 shadow-sm transition-colors ease-in">
<ComposerPrimitive.Input
rows={1}
autoFocus
placeholder="Message to Mem0..."
className="placeholder:text-zinc-400 dark:placeholder:text-zinc-500 max-h-40 flex-grow resize-none border-none bg-transparent px-2 py-4 text-sm outline-none focus:ring-0 disabled:cursor-not-allowed text-[#1e293b] dark:text-zinc-200"
/>
<ComposerAction />
</ComposerPrimitive.Root>
);
};
const ComposerAction: FC = () => {
return (
<>
<ThreadPrimitive.If running={false}>
<ComposerPrimitive.Send asChild>
<TooltipIconButton
tooltip="Send"
variant="default"
className="my-2.5 size-8 p-2 transition-opacity ease-in bg-[#4f46e5] dark:bg-[#6366f1] hover:bg-[#4338ca] dark:hover:bg-[#4f46e5] text-white rounded-full"
>
<SendHorizontalIcon />
</TooltipIconButton>
</ComposerPrimitive.Send>
</ThreadPrimitive.If>
<ThreadPrimitive.If running>
<ComposerPrimitive.Cancel asChild>
<TooltipIconButton
tooltip="Cancel"
variant="default"
className="my-2.5 size-8 p-2 transition-opacity ease-in bg-[#4f46e5] dark:bg-[#6366f1] hover:bg-[#4338ca] dark:hover:bg-[#4f46e5] text-white rounded-full"
>
<CircleStopIcon />
</TooltipIconButton>
</ComposerPrimitive.Cancel>
</ThreadPrimitive.If>
</>
);
};
const UserMessage: FC = () => {
return (
<MessagePrimitive.Root className="grid auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] gap-y-2 [&:where(>*)]:col-start-2 w-full max-w-[var(--thread-max-width)] py-4">
<UserActionBar />
<div className="bg-[#4f46e5] text-sm dark:bg-[#6366f1] text-white max-w-[calc(var(--thread-max-width)*0.8)] break-words rounded-3xl px-5 py-2.5 col-start-2 row-start-2">
<MessagePrimitive.Content />
</div>
<BranchPicker className="col-span-full col-start-1 row-start-3 -mr-1 justify-end" />
</MessagePrimitive.Root>
);
};
const UserActionBar: FC = () => {
return (
<ActionBarPrimitive.Root
hideWhenRunning
autohide="not-last"
className="flex flex-col items-end col-start-1 row-start-2 mr-3 mt-2.5"
>
<ActionBarPrimitive.Edit asChild>
<TooltipIconButton
tooltip="Edit"
className="text-[#475569] dark:text-zinc-300 hover:text-[#4f46e5] dark:hover:text-[#6366f1] hover:bg-[#eef2ff] dark:hover:bg-zinc-800"
>
<PencilIcon />
</TooltipIconButton>
</ActionBarPrimitive.Edit>
</ActionBarPrimitive.Root>
);
};
const EditComposer: FC = () => {
return (
<ComposerPrimitive.Root className="bg-[#eef2ff] dark:bg-zinc-800 my-4 flex w-full max-w-[var(--thread-max-width)] flex-col gap-2 rounded-xl">
<ComposerPrimitive.Input className="text-[#1e293b] dark:text-zinc-200 flex h-8 w-full resize-none bg-transparent p-4 pb-0 outline-none" />
<div className="mx-3 mb-3 flex items-center justify-center gap-2 self-end">
<ComposerPrimitive.Cancel asChild>
<Button
variant="ghost"
className="text-[#475569] dark:text-zinc-300 hover:bg-[#eef2ff]/50 dark:hover:bg-zinc-700/50"
>
Cancel
</Button>
</ComposerPrimitive.Cancel>
<ComposerPrimitive.Send asChild>
<Button className="bg-[#4f46e5] dark:bg-[#6366f1] hover:bg-[#4338ca] dark:hover:bg-[#4f46e5] text-white rounded-[2rem]">
Send
</Button>
</ComposerPrimitive.Send>
</div>
</ComposerPrimitive.Root>
);
};
const AssistantMessage: FC = () => {
const content = useMessage((m) => m.content);
const markdownText = React.useMemo(() => {
if (!content) return '';
if (typeof content === 'string') return content;
if (Array.isArray(content) && content.length > 0 && 'text' in content[0]) {
return content[0].text || '';
}
return '';
}, [content]);
return (
<MessagePrimitive.Root className="grid grid-cols-[auto_auto_1fr] grid-rows-[auto_1fr] relative w-full max-w-[var(--thread-max-width)] py-4">
<div className="text-[#1e293b] dark:text-zinc-200 max-w-[calc(var(--thread-max-width)*0.8)] break-words leading-7 col-span-2 col-start-2 row-start-1 my-1.5 bg-white dark:bg-zinc-800 rounded-3xl px-5 py-2.5 border border-[#e2e8f0] dark:border-zinc-700 shadow-sm">
<MemoryUI />
<MarkdownRenderer
markdownText={markdownText}
showCopyButton={true}
isDarkMode={document.documentElement.classList.contains('dark')}
/>
</div>
<AssistantActionBar />
<BranchPicker className="col-start-2 row-start-2 -ml-2 mr-2" />
</MessagePrimitive.Root>
);
};
const AssistantActionBar: FC = () => {
return (
<ActionBarPrimitive.Root
hideWhenRunning
autohideFloat="single-branch"
className="text-[#475569] dark:text-zinc-300 flex gap-1 col-start-3 row-start-2 ml-1 data-[floating]:bg-white data-[floating]:dark:bg-zinc-800 data-[floating]:absolute data-[floating]:rounded-md data-[floating]:border data-[floating]:border-[#e2e8f0] data-[floating]:dark:border-zinc-700 data-[floating]:p-1 data-[floating]:shadow-sm"
>
<ActionBarPrimitive.Copy asChild>
<TooltipIconButton
tooltip="Copy"
className="hover:text-[#4f46e5] dark:hover:text-[#6366f1] hover:bg-[#eef2ff] dark:hover:bg-zinc-700"
>
<MessagePrimitive.If copied>
<CheckIcon />
</MessagePrimitive.If>
<MessagePrimitive.If copied={false}>
<CopyIcon />
</MessagePrimitive.If>
</TooltipIconButton>
</ActionBarPrimitive.Copy>
<ActionBarPrimitive.Reload asChild>
<TooltipIconButton
tooltip="Refresh"
className="hover:text-[#4f46e5] dark:hover:text-[#6366f1] hover:bg-[#eef2ff] dark:hover:bg-zinc-700"
>
<RefreshCwIcon />
</TooltipIconButton>
</ActionBarPrimitive.Reload>
</ActionBarPrimitive.Root>
);
};
const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
className,
...rest
}) => {
return (
<BranchPickerPrimitive.Root
hideWhenSingleBranch
className={cn("text-[#475569] dark:text-zinc-300 inline-flex items-center text-xs", className)}
{...rest}
>
<BranchPickerPrimitive.Previous asChild>
<TooltipIconButton
tooltip="Previous"
className="hover:text-[#4f46e5] dark:hover:text-[#6366f1] hover:bg-[#eef2ff] dark:hover:bg-zinc-700"
>
<ChevronLeftIcon />
</TooltipIconButton>
</BranchPickerPrimitive.Previous>
<span className="font-medium">
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
</span>
<BranchPickerPrimitive.Next asChild>
<TooltipIconButton
tooltip="Next"
className="hover:text-[#4f46e5] dark:hover:text-[#6366f1] hover:bg-[#eef2ff] dark:hover:bg-zinc-700"
>
<ChevronRightIcon />
</TooltipIconButton>
</BranchPickerPrimitive.Next>
</BranchPickerPrimitive.Root>
);
};
const CircleStopIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="16" height="16">
<rect width="10" height="10" x="3" y="3" rx="2" />
</svg>
);
};
// Component for reuse in mobile drawer
const ThreadListItem: FC = () => {
return (
<ThreadListItemPrimitive.Root className="data-[active]:bg-[#eef2ff] hover:bg-[#eef2ff] dark:hover:bg-zinc-800 dark:data-[active]:bg-zinc-800 focus-visible:bg-[#eef2ff] dark:focus-visible:bg-zinc-800 focus-visible:ring-[#4f46e5] flex items-center gap-2 rounded-lg transition-all focus-visible:outline-none focus-visible:ring-2">
<ThreadListItemPrimitive.Trigger className="flex-grow px-3 py-2 text-start">
<p className="text-sm">
<ThreadListItemPrimitive.Title fallback="New Chat" />
</p>
</ThreadListItemPrimitive.Trigger>
<ThreadListItemPrimitive.Archive asChild>
<TooltipIconButton
className="hover:text-[#4f46e5] text-[#475569] dark:text-zinc-300 dark:hover:text-[#6366f1] ml-auto mr-3 size-4 p-0"
variant="ghost"
tooltip="Archive thread"
>
<ArchiveIcon />
</TooltipIconButton>
</ThreadListItemPrimitive.Archive>
</ThreadListItemPrimitive.Root>
);
};

View File

@@ -0,0 +1,44 @@
"use client";
import { forwardRef } from "react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button, ButtonProps } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export type TooltipIconButtonProps = ButtonProps & {
tooltip: string;
side?: "top" | "bottom" | "left" | "right";
};
export const TooltipIconButton = forwardRef<
HTMLButtonElement,
TooltipIconButtonProps
>(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
{...rest}
className={cn("size-6 p-1", className)}
ref={ref}
>
{children}
<span className="sr-only">{tooltip}</span>
</Button>
</TooltipTrigger>
<TooltipContent side={side}>{tooltip}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
});
TooltipIconButton.displayName = "TooltipIconButton";

View File

@@ -0,0 +1,27 @@
const GithubButton = ({ url }: { url: string }) => {
return (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center bg-black text-white rounded-full shadow-lg hover:bg-gray-800 transition border border-gray-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="white"
className="w-6 h-6"
>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.167 6.839 9.49.5.09.682-.217.682-.482 0-.237-.009-.868-.014-1.703-2.782.603-3.369-1.34-3.369-1.34-.455-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.004.07 1.532 1.032 1.532 1.032.892 1.528 2.341 1.087 2.91.832.091-.647.35-1.086.636-1.337-2.22-.253-4.555-1.11-4.555-4.943 0-1.092.39-1.984 1.03-2.682-.103-.253-.447-1.273.098-2.654 0 0 .84-.269 2.75 1.025A9.564 9.564 0 0112 6.8c.85.004 1.705.114 2.504.334 1.91-1.294 2.75-1.025 2.75-1.025.546 1.381.202 2.401.099 2.654.641.698 1.03 1.59 1.03 2.682 0 3.842-2.337 4.687-4.564 4.936.36.31.679.919.679 1.852 0 1.337-.012 2.416-.012 2.743 0 .267.18.576.688.477C19.138 20.163 22 16.414 22 12c0-5.523-4.477-10-10-10z"
clipRule="evenodd"
/>
</svg>
</a>
);
};
export default GithubButton;

View File

@@ -0,0 +1,108 @@
.token {
word-break: break-word; /* Break long words */
overflow-wrap: break-word; /* Wrap text if it's too long */
width: 100%;
white-space: pre-wrap;
}
.prose li p {
margin-top: -19px;
}
@keyframes highlightSweep {
0% {
transform: scaleX(0);
opacity: 0;
}
100% {
transform: scaleX(1);
opacity: 1;
}
}
.highlight-text {
display: inline-block;
position: relative;
font-weight: normal;
padding: 0;
border-radius: 4px;
}
.highlight-text::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgb(233 213 255 / 0.7);
transform-origin: left;
transform: scaleX(0);
opacity: 0;
z-index: -1;
border-radius: inherit;
}
@keyframes fontWeightAnimation {
0% {
font-weight: normal;
padding: 0;
}
100% {
font-weight: 600;
padding: 0 4px;
}
}
@keyframes backgroundColorAnimation {
0% {
background-color: transparent;
}
100% {
background-color: rgba(180, 231, 255, 0.7);
}
}
.highlight-text.animate {
animation:
fontWeightAnimation 0.1s ease-out forwards,
backgroundColorAnimation 0.1s ease-out forwards;
animation-delay: 0.88s, 1.1s;
}
.highlight-text.dark {
background-color: rgba(213, 242, 255, 0.7);
color: #000;
}
.highlight-text.animate::before {
animation: highlightSweep 0.5s ease-out forwards;
animation-delay: 0.6s;
animation-fill-mode: forwards;
animation-iteration-count: 1;
}
:root[class~="dark"] .highlight-text::before {
background: rgb(88 28 135 / 0.5);
}
@keyframes blink {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
.markdown-cursor {
display: inline-block;
animation: blink 0.8s ease-in-out infinite;
color: rgba(213, 242, 255, 0.7);
margin-left: 1px;
font-size: 1.2em;
line-height: 1;
vertical-align: baseline;
position: relative;
top: 2px;
}
:root[class~="dark"] .markdown-cursor {
color: #6366f1;
}

View File

@@ -0,0 +1,226 @@
"use client"
import { CSSProperties, useState, ReactNode, useRef } from "react"
import React from "react"
import Markdown, { Components } from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { coldarkCold, coldarkDark } from "react-syntax-highlighter/dist/esm/styles/prism"
import remarkGfm from "remark-gfm"
import remarkMath from "remark-math"
import { Button } from "@/components/ui/button"
import { Check, Copy } from "lucide-react"
import { cn } from "@/lib/utils"
import "./markdown.css"
interface MarkdownRendererProps {
markdownText: string
actualCode?: string
className?: string
style?: { prism?: { [key: string]: CSSProperties } }
messageId?: string
showCopyButton?: boolean
isDarkMode?: boolean
}
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
markdownText = '',
className,
style,
actualCode,
messageId = '',
showCopyButton = true,
isDarkMode = false
}) => {
const [copied, setCopied] = useState(false);
const [isStreaming, setIsStreaming] = useState(true);
const highlightBuffer = useRef<string[]>([]);
const isCollecting = useRef(false);
const processedTextRef = useRef<string>('');
const safeMarkdownText = React.useMemo(() => {
return typeof markdownText === 'string' ? markdownText : '';
}, [markdownText]);
const preProcessText = React.useCallback((text: unknown): string => {
if (typeof text !== 'string' || !text) return '';
// Remove highlight tags initially for clean rendering
return text.replace(/<highlight>.*?<\/highlight>/g, (match) => {
// Extract the content between tags
const content = match.replace(/<highlight>|<\/highlight>/g, '');
return content;
});
}, []);
// Reset streaming state when markdownText changes
React.useEffect(() => {
// Preprocess the text first
processedTextRef.current = preProcessText(safeMarkdownText);
setIsStreaming(true);
const timer = setTimeout(() => {
setIsStreaming(false);
}, 500);
return () => clearTimeout(timer);
}, [safeMarkdownText, preProcessText]);
const copyToClipboard = async (code: string) => {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 1000);
};
const processText = React.useCallback((text: string) => {
if (typeof text !== 'string') return text;
// Only process highlights after streaming is complete
if (!isStreaming) {
if (text === '<highlight>') {
isCollecting.current = true;
return null;
}
if (text === '</highlight>') {
isCollecting.current = false;
const content = highlightBuffer.current.join('');
highlightBuffer.current = [];
return (
<span
key={`highlight-${messageId}-${content}`}
className={cn("highlight-text animate text-black", {
"dark": isDarkMode
})}
>
{content}
</span>
);
}
if (isCollecting.current) {
highlightBuffer.current.push(text);
return null;
}
}
return text;
}, [isStreaming, messageId, isDarkMode]);
const processChildren = React.useCallback((children: ReactNode): ReactNode => {
if (typeof children === 'string') {
return processText(children);
}
if (Array.isArray(children)) {
return children.map(child => {
const processed = processChildren(child);
return processed === null ? null : processed;
}).filter(Boolean);
}
return children;
}, [processText]);
const CodeBlock = React.useCallback(({
language,
code,
actualCode,
showCopyButton = true,
}: {
language: string;
code: string;
actualCode?: string;
showCopyButton?: boolean;
}) => (
<div className="relative my-4 rounded-xl overflow-hidden bg-neutral-100 w-full max-w-full border border-neutral-200">
{showCopyButton && (
<div className="flex items-center justify-between px-4 py-2 rounded-t-md shadow-md">
<span className="text-xs text-neutral-700 dark:text-white font-inter-display">
{language}
</span>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-neutral-700 dark:text-white"
onClick={() => copyToClipboard(actualCode || code)}
>
{copied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4 text-muted-foreground" />
)}
</Button>
</div>
)}
<div className="max-w-full w-full overflow-hidden">
<SyntaxHighlighter
language={language}
style={style?.prism || (isDarkMode ? coldarkDark : coldarkCold)}
customStyle={{
margin: 0,
borderTopLeftRadius: "0",
borderTopRightRadius: "0",
padding: "16px",
fontSize: "0.9rem",
lineHeight: "1.3",
backgroundColor: isDarkMode ? "#262626" : "#fff",
wordBreak: "break-word",
overflowWrap: "break-word",
}}
>
{code}
</SyntaxHighlighter>
</div>
</div>
), [copied, isDarkMode, style]);
const components = {
p: ({ children, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
<p className="m-0 p-0" {...props}>{processChildren(children)}</p>
),
span: ({ children, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
<span {...props}>{processChildren(children)}</span>
),
li: ({ children, ...props }: React.HTMLAttributes<HTMLLIElement>) => (
<li {...props}>{processChildren(children)}</li>
),
strong: ({ children, ...props }: React.HTMLAttributes<HTMLElement>) => (
<strong {...props}>{processChildren(children)}</strong>
),
em: ({ children, ...props }: React.HTMLAttributes<HTMLElement>) => (
<em {...props}>{processChildren(children)}</em>
),
code: ({ className, children, ...props }: React.HTMLAttributes<HTMLElement>) => {
const match = /language-(\w+)/.exec(className || "");
if (match) {
return (
<CodeBlock
language={match[1]}
code={String(children)}
actualCode={actualCode}
showCopyButton={showCopyButton}
/>
);
}
return (
<code className={className} {...props}>
{processChildren(children)}
</code>
);
}
} satisfies Components;
return (
<div className={cn(
"min-w-[100%] max-w-[100%] my-2 prose-hr:my-0 prose-h4:my-1 text-sm prose-ul:-my-2 prose-ol:-my-2 prose-li:-my-2 prose break-words prose-pre:bg-transparent prose-pre:-my-2 dark:prose-invert prose-p:leading-snug prose-pre:p-0 prose-h3:-my-2 prose-p:-my-2",
className
)}>
<Markdown
remarkPlugins={[remarkGfm, remarkMath]}
components={components}
>
{(isStreaming ? processedTextRef.current : safeMarkdownText)}
</Markdown>
{(isStreaming || (!isStreaming && !processedTextRef.current)) && <span className="markdown-cursor"></span>}
</div>
);
};
export default MarkdownRenderer;

View File

@@ -0,0 +1,40 @@
"use client";
import React from "react";
import Image from "next/image";
export default function ThemeAwareLogo({
width = 120,
height = 40,
variant = "default",
isDarkMode = false,
}: {
width?: number;
height?: number;
variant?: "default" | "collapsed";
isDarkMode?: boolean;
}) {
// For collapsed variant, always use the icon
if (variant === "collapsed") {
return (
<div
className={`flex items-center justify-center rounded-full ${isDarkMode ? 'bg-[#6366f1]' : 'bg-[#4f46e5]'}`}
style={{ width, height }}
>
<span className="text-white font-bold text-lg">M</span>
</div>
);
}
// For default variant, use the full logo image
const logoSrc = isDarkMode ? "/images/dark.svg" : "/images/light.svg";
return (
<Image
src={logoSrc}
alt="Mem0.ai"
width={width}
height={height}
/>
);
}

View File

@@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-zinc-200 dark:bg-zinc-700" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }