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,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}
/>
);
}