Add mem0 as a service integration

This commit is contained in:
Deshraj Yadav
2025-03-18 01:02:49 -07:00
parent f3b50bc3c4
commit 7ad36eeaf4
44 changed files with 7041 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
!**/lib
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,27 @@
# React Implementation
Basic implementation using the [Pipecat React SDK](https://docs.pipecat.ai/client/react/introduction).
## Setup
1. Run the bot server; see [README](../../README).
2. Navigate to the `client/react` directory:
```bash
cd client/react
```
3. Install dependencies:
```bash
npm install
```
4. Run the client app:
```
npm run dev
```
5. Visit http://localhost:5173 in your browser.

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/App.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mem0 - Pipecat React Client</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
{
"name": "react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.7",
"@pipecat-ai/client-js": "^0.3.2",
"@pipecat-ai/client-react": "^0.3.2",
"@pipecat-ai/daily-transport": "^0.3.4",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@tailwindcss/vite": "^4.0.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.4.10",
"lucide-react": "^0.479.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@types/node": "^22.13.9",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.15.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.12.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.12",
"typescript": "~5.6.2",
"typescript-eslint": "^8.15.0",
"vite": "^6.0.9"
},
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b"
}

View File

@@ -0,0 +1,267 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background-color: #f0f0f0;
}
@keyframes message-pop {
0% {
opacity: 0;
transform: scale(0.8);
}
50% {
transform: scale(1.05);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes pulse-shadow {
0% {
box-shadow: 0 0 0 0 rgba(161, 161, 170, 0.7);
}
70% {
box-shadow: 0 0 0 20px rgba(161, 161, 170, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(161, 161, 170, 0);
}
}
@keyframes wave {
0% { transform: scaleY(0.2); }
50% { transform: scaleY(1); }
100% { transform: scaleY(0.2); }
}
@keyframes wave-listening {
0% { transform: scaleY(0.3); }
50% { transform: scaleY(0.5); }
100% { transform: scaleY(0.3); }
}
@keyframes wave-user {
0% { transform: scaleY(0.4); }
50% { transform: scaleY(1.2); }
100% { transform: scaleY(0.4); }
}
@keyframes wave-assistant {
0% { transform: scaleY(0.3); }
50% { transform: scaleY(0.9); }
100% { transform: scaleY(0.3); }
}
@keyframes memory-indicator-pop {
0% {
opacity: 0;
transform: translateY(-10px);
height: 0;
margin-bottom: 0;
}
30% {
height: 24px;
margin-bottom: 0.25rem;
}
60% {
opacity: 0.7;
}
100% {
opacity: 1;
transform: translateY(0);
height: 24px;
margin-bottom: 0.25rem;
}
}
.animate-message-pop {
animation: message-pop 0.3s ease-out forwards;
}
.animate-pulse-shadow {
animation: pulse-shadow 2s infinite;
}
.waveform-bar {
animation: wave-user 0.8s ease-in-out infinite;
transform-origin: bottom;
}
.waveform-bar-listening {
animation: wave-listening 1.5s ease-in-out infinite;
transform-origin: bottom;
}
.waveform-bar.bg-zinc-600 {
animation: wave-assistant 1s ease-in-out infinite;
}
.waveform-bar.bg-zinc-400 {
animation: wave-listening 1.5s ease-in-out infinite;
}
.waveform-bar:nth-child(2) { animation-delay: 0.1s; }
.waveform-bar:nth-child(3) { animation-delay: 0.2s; }
.waveform-bar:nth-child(4) { animation-delay: 0.3s; }
.waveform-bar:nth-child(5) { animation-delay: 0.4s; }
.slide-up-enter {
opacity: 0;
transform: translateY(20px);
}
.slide-up-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms, transform 300ms;
}
.slide-up-exit {
opacity: 1;
transform: translateY(0);
}
.slide-up-exit-active {
opacity: 0;
transform: translateY(-20px);
transition: opacity 300ms, transform 300ms;
}
/* Smooth scrolling for the entire page */
html {
scroll-behavior: smooth;
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.141 0.005 285.823);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.141 0.005 285.823);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.274 0.006 286.033);
--input: oklch(0.274 0.006 286.033);
--ring: oklch(0.442 0.017 285.786);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.274 0.006 286.033);
--sidebar-ring: oklch(0.442 0.017 285.786);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
.animate-memory-indicator {
animation: memory-indicator-pop 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
transform-origin: top;
}

View File

@@ -0,0 +1,168 @@
import {
RTVIClientAudio,
useRTVIClient,
useRTVIClientTransportState,
} from '@pipecat-ai/client-react';
import { RTVIProvider } from './providers/RTVIProvider';
import { DebugDisplay } from './components/DebugDisplay';
import { Waveform } from './components/Waveform';
import StaticMemoryPanel from './components/StaticMemoryPanel';
import './App.css';
import Navbar from './components/Navbar';
import { ScrollArea } from './components/ui/scroll-area';
import { InteractiveHoverButton } from "@/components/magicui/interactive-hover-button";
import { useState, useEffect } from 'react';
import { CaretRight, Memory } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
import { Button } from "./components/ui/button";
function AppContent() {
const client = useRTVIClient();
const transportState = useRTVIClientTransportState();
const isConnected = ['connected', 'ready'].includes(transportState);
const isConnecting = ['connecting', 'disconnecting'].includes(transportState);
const [speakingState, setSpeakingState] = useState<'user' | 'assistant' | 'system' | 'idle'>('idle');
const [leftPanelCollapsed, setLeftPanelCollapsed] = useState(true);
const [rightPanelCollapsed, setRightPanelCollapsed] = useState(false);
const [isFirstConnectionConnected, setIsFirstConnectionConnected] = useState(false);
const handleReset = () => {
setIsFirstConnectionConnected(false);
}
// Simulated memory counts - replace with actual counts from your components
const leftMemoryCount = 3;
const rightMemoryCount = 3;
const handleConnect = async () => {
if (!client) {
console.error('RTVI client is not initialized');
return;
}
try {
if (isConnected) {
await client.disconnect();
} else {
await client.connect();
}
setIsFirstConnectionConnected(true);
} catch (error) {
console.error('Connection error:', error);
}
};
// Listen for message events from DebugDisplay
useEffect(() => {
const handleMessage = (event: CustomEvent<{ type: 'user' | 'assistant' | 'system' }>) => {
setSpeakingState(event.detail.type);
// Reset to idle after animation
setTimeout(() => setSpeakingState('idle'), 2000);
};
window.addEventListener('newMessage', handleMessage as EventListener);
return () => window.removeEventListener('newMessage', handleMessage as EventListener);
}, []);
return (
<div className="app">
<div className="mb-24">
<Navbar onReset={handleReset} />
</div>
{!isConnected && !isFirstConnectionConnected ? (
<div className="h-[calc(100vh-12rem)] flex items-center justify-center">
<InteractiveHoverButton
onClick={handleConnect}
disabled={isConnecting}
className="px-8 py-4 text-xl animate-pulse-shadow"
>
{isConnecting ? 'Connecting...' : 'Connect'}
</InteractiveHoverButton>
</div>
) : (
<div className="flex px-4 relative">
{/* Main Content */}
<div className={cn(
"transition-all duration-300 ease-in-out flex-1 relative",
leftPanelCollapsed && rightPanelCollapsed ? "mx-4" : "",
!leftPanelCollapsed && !rightPanelCollapsed ? "mx-4" : "",
!leftPanelCollapsed && rightPanelCollapsed ? "ml-4 mr-4" : "",
leftPanelCollapsed && !rightPanelCollapsed ? "ml-4 mr-4" : ""
)}>
<ScrollArea className="w-full h-[calc(100vh-10rem)] rounded-xl bg-zinc-50 dark:bg-neutral-950">
<DebugDisplay onNewMessage={(type) => setSpeakingState(type)} />
</ScrollArea>
<div className="absolute bottom-6 left-1/2 -translate-x-1/2">
<div className="p-4 rounded-full shadow-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 transition-all duration-300 hover:shadow-xl">
<Waveform speakingState={speakingState} />
</div>
</div>
</div>
{/* Right Panel */}
<div className={cn(
"transition-all duration-300 ease-in-out group",
rightPanelCollapsed ? "w-12" : "w-[25rem]"
)}>
{/* Collapsed State with Memory Icons */}
<div className={cn(
"absolute top-0 right-0 h-full w-12 flex flex-col items-center pt-4 gap-2",
rightPanelCollapsed ? "opacity-100" : "opacity-0 pointer-events-none"
)}>
<Button
variant="ghost"
size="icon"
onClick={() => setRightPanelCollapsed(false)}
className="h-10 w-10 rounded-xl bg-violet-100 dark:bg-violet-900/30 hover:bg-violet-200 dark:hover:bg-violet-900/50"
>
<CaretRight
className="h-5 w-5 text-violet-600 dark:text-violet-400 rotate-180"
/>
</Button>
<div className="flex flex-col items-center gap-1 mt-2">
<Memory className="h-5 w-5 text-violet-500" weight="duotone" />
<span className="text-xs font-medium text-violet-600 dark:text-violet-400">{rightMemoryCount}</span>
</div>
</div>
{/* Expanded State */}
<div className={cn(
"transition-all duration-300 w-[25rem]",
rightPanelCollapsed ? "opacity-0 pointer-events-none" : "opacity-100"
)}>
<div className="relative">
<Button
variant="ghost"
size="icon"
onClick={() => setRightPanelCollapsed(true)}
className="absolute left-2 top-2 z-10 h-10 w-10 rounded-xl bg-violet-100 dark:bg-violet-900/30 hover:bg-violet-200 dark:hover:bg-violet-900/50"
>
<CaretRight
className="h-5 w-5 text-violet-600 dark:text-violet-400"
/>
</Button>
<ScrollArea>
<StaticMemoryPanel />
</ScrollArea>
</div>
</div>
</div>
</div>
)}
<RTVIClientAudio />
</div>
);
}
function App() {
return (
<RTVIProvider>
<AppContent />
</RTVIProvider>
);
}
export default App;

View File

@@ -0,0 +1,39 @@
import {
useRTVIClient,
useRTVIClientTransportState,
} from '@pipecat-ai/client-react';
import { InteractiveHoverButton } from "@/components/magicui/interactive-hover-button";
export function ConnectButton() {
const client = useRTVIClient();
const transportState = useRTVIClientTransportState();
const isConnected = ['connected', 'ready'].includes(transportState);
const handleClick = async () => {
if (!client) {
console.error('RTVI client is not initialized');
return;
}
try {
if (isConnected) {
await client.disconnect();
} else {
await client.connect();
}
} catch (error) {
console.error('Connection error:', error);
}
};
return (
<InteractiveHoverButton
onClick={handleClick}
disabled={
!client || ['connecting', 'disconnecting'].includes(transportState)
}>
{!client || ['connecting', 'disconnecting'].includes(transportState) ? 'Connecting...' : isConnected ? 'Disconnect' : 'Connect'}
</InteractiveHoverButton>
);
}

View File

@@ -0,0 +1,244 @@
import { useRef, useCallback, useState, useEffect } from 'react';
import {
Participant,
RTVIEvent,
TransportState,
TranscriptData,
BotLLMTextData,
} from '@pipecat-ai/client-js';
import { useRTVIClient, useRTVIClientEvent } from '@pipecat-ai/client-react';
interface Message {
id: string;
text: string;
type: 'user' | 'assistant' | 'system';
timestamp: Date;
showMemoryRefresh?: boolean;
memoryRefreshTime?: number;
showMemoryIndicator?: boolean;
}
interface DebugDisplayProps {
onNewMessage: (type: 'user' | 'assistant' | 'system') => void;
}
// Create a shared context for memory refresh
export const memoryRefreshTrigger = {
value: 0,
subscribers: new Set<(value: number) => void>(),
increment() {
this.value++;
this.subscribers.forEach(callback => callback(this.value));
}
};
export function DebugDisplay({ onNewMessage }: DebugDisplayProps) {
const [messages, setMessages] = useState<Message[]>([]);
const messagesEndRef = useRef<HTMLDivElement>(null);
const client = useRTVIClient();
// Add effect to handle delayed appearance of memory refresh indicators
useEffect(() => {
const timeouts: NodeJS.Timeout[] = [];
messages.forEach(message => {
if (message.showMemoryRefresh && !message.showMemoryIndicator) {
const timeout = setTimeout(() => {
setMessages(prev => prev.map(msg =>
msg.id === message.id
? { ...msg, showMemoryIndicator: true }
: msg
));
}, 800);
timeouts.push(timeout);
}
});
return () => {
timeouts.forEach(timeout => clearTimeout(timeout));
};
}, [messages]);
const generateRandomRefreshTime = () => {
return Number((Math.random() * (100 - 50) + 50).toFixed(2));
};
const scrollToBottom = useCallback(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, []);
const addMessage = useCallback((text: string, type: 'user' | 'assistant' | 'system') => {
const newMessage: Message = {
id: `${Date.now()}-${Math.random()}`,
text,
type,
timestamp: new Date(),
showMemoryRefresh: type === 'user',
memoryRefreshTime: type === 'user' ? generateRandomRefreshTime() : undefined
};
setMessages(prev => [...prev, newMessage]);
onNewMessage(type);
if (type === 'assistant') {
memoryRefreshTrigger.increment();
}
setTimeout(scrollToBottom, 100);
}, [scrollToBottom, onNewMessage]);
// Log transport state changes
useRTVIClientEvent(
RTVIEvent.TransportStateChanged,
useCallback(
(state: TransportState) => {
addMessage(`Transport state changed: ${state}`, 'system');
},
[addMessage]
)
);
// Log bot connection events
useRTVIClientEvent(
RTVIEvent.BotConnected,
useCallback(
(participant?: Participant) => {
addMessage(`Bot connected: ${JSON.stringify(participant)}`, 'system');
},
[addMessage]
)
);
useRTVIClientEvent(
RTVIEvent.BotDisconnected,
useCallback(
(participant?: Participant) => {
addMessage(`Bot disconnected: ${JSON.stringify(participant)}`, 'system');
},
[addMessage]
)
);
// Log track events
useRTVIClientEvent(
RTVIEvent.TrackStarted,
useCallback(
(track: MediaStreamTrack, participant?: Participant) => {
addMessage(
`Track started: ${track.kind} from ${participant?.name || 'unknown'}`,
'system'
);
},
[addMessage]
)
);
useRTVIClientEvent(
RTVIEvent.TrackStopped,
useCallback(
(track: MediaStreamTrack, participant?: Participant) => {
addMessage(
`Track stopped: ${track.kind} from ${participant?.name || 'unknown'}`,
'system'
);
},
[addMessage]
)
);
// Log bot ready state and check tracks
useRTVIClientEvent(
RTVIEvent.BotReady,
useCallback(() => {
addMessage('Bot ready', 'system');
if (!client) return;
const tracks = client.tracks();
addMessage(
`Available tracks: ${JSON.stringify({
local: {
audio: !!tracks.local.audio,
video: !!tracks.local.video,
},
bot: {
audio: !!tracks.bot?.audio,
video: !!tracks.bot?.video,
},
})}`,
'system'
);
}, [client, addMessage])
);
// Log transcripts
useRTVIClientEvent(
RTVIEvent.UserTranscript,
useCallback(
(data: TranscriptData) => {
// Only log final transcripts
if (data.final) {
addMessage(data.text, 'user');
}
},
[addMessage]
)
);
useRTVIClientEvent(
RTVIEvent.BotTranscript,
useCallback(
(data: BotLLMTextData) => {
addMessage(data.text, 'assistant');
},
[addMessage]
)
);
return (
<div className="w-full h-full mx-auto p-6 bg-zinc-50">
<div className="flex flex-col gap-4 pb-16">
{messages.map((message) => (
<div
key={message.id}
className={`
flex animate-message-pop
${message.type === 'system' ? 'justify-center mx-auto w-full' : 'max-w-[80%]'}
${message.type === 'user' ? 'justify-end ml-auto' : ''}
${message.type === 'assistant' ? 'justify-start mr-auto' : ''}
`}
>
{message.type === 'system' ? (
<div className="text-sm text-zinc-500 font-medium text-center">
{message.text}
</div>
) : (
<div className="flex flex-col">
{!message.showMemoryRefresh && message.showMemoryIndicator && (
<div className="text-xs bg-blue-100 text-blue-400 px-2 py-1 rounded-md self-end animate-memory-indicator overflow-hidden">
Memory refresh: {message.memoryRefreshTime}ms
</div>
)}
<div
className={`
px-4 py-3 rounded-2xl shadow-sm transition-all duration-300 hover:shadow-md
${message.type === 'user' ? 'bg-zinc-800 text-white' : ''}
${message.type === 'assistant' ? 'bg-white text-zinc-800' : ''}
`}
>
<div className="break-words">{message.text}</div>
<div className="text-xs mt-1 opacity-70">
{message.timestamp.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</div>
</div>
</div>
)}
</div>
))}
<div ref={messagesEndRef} />
</div>
</div>
);
}

View File

@@ -0,0 +1,78 @@
import type React from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Tag, Clock } from "lucide-react"
import { Memory } from "@phosphor-icons/react"
import { formatDistanceToNow } from "date-fns"
import { cn } from "@/lib/utils"
interface MemoryCardProps {
memory: {
id: string
content: string
createdAt: string
tags?: string[]
categories?: string[]
}
}
export const MemoryCard: React.FC<MemoryCardProps> = ({ memory }) => {
return (
<Card className="w-full overflow-hidden border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
<CardContent className="p-4">
<div className="flex items-center gap-3 mb-4">
<div
className={cn(
"h-8 w-8 rounded-full flex items-center justify-center",
"bg-violet-100 dark:bg-violet-900/30",
)}
>
<Memory className="h-4 w-4 text-violet-500" weight="duotone" />
</div>
<div className="flex items-center gap-2 text-xs text-neutral-500 dark:text-neutral-400">
<Clock className="h-3 w-3" />
<span>{formatDistanceToNow(new Date(memory.createdAt), { addSuffix: true })}</span>
</div>
</div>
<div className="relative pl-4">
<div className="absolute -left-0 top-0 bottom-0 w-[2px] rounded-full bg-violet-100 dark:bg-violet-900" />
<div className="prose prose-sm dark:prose-invert max-w-none">
<p className="text-sm text-neutral-800 dark:text-neutral-200 leading-relaxed">{memory.content}</p>
</div>
{memory.tags && memory.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-3">
{memory.tags.map((tag, tagIndex) => (
<Badge
key={tagIndex}
variant="secondary"
className="px-1.5 py-0 h-4 text-[10px] bg-violet-50 dark:bg-violet-900/20 text-violet-600 dark:text-violet-400 hover:bg-violet-100 dark:hover:bg-violet-900/40 transition-colors"
>
<Tag className="h-2.5 w-2.5 mr-1" />
{tag}
</Badge>
))}
</div>
)}
{memory.categories && memory.categories.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-3">
{memory.categories.map((category, categoryIndex) => (
<Badge
key={categoryIndex}
variant="secondary"
className="px-1.5 py-0 h-4 text-[10px] bg-violet-50 dark:bg-violet-900/20 text-violet-600 dark:text-violet-400 hover:bg-violet-100 dark:hover:bg-violet-900/40 transition-colors"
>
<Tag className="h-2.5 w-2.5 mr-1" />
{category}
</Badge>
))}
</div>
)}
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,17 @@
import { ConnectButton } from './ConnectButton'
import ThemeAwareLogo from './ThemeAwareLogo'
const Navbar = ({ onReset }: { onReset: () => void }) => {
return (
<nav className="fixed top-4 left-1/2 -translate-x-1/2 w-full max-w-[95%] bg-white rounded-3xl shadow-lg px-6 py-2 flex items-center justify-between z-50">
<div onClick={onReset} className="cursor-pointer">
<ThemeAwareLogo />
</div>
<button className="p-2 hover:bg-zinc-100 rounded-full transition-colors">
<ConnectButton />
</button>
</nav>
)
}
export default Navbar

View File

@@ -0,0 +1,134 @@
import React, { useState, useEffect, useCallback } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ScrollArea } from "./ui/scroll-area"
import { Memory, CaretRight } from "@phosphor-icons/react"
import { cn } from "@/lib/utils"
import { Button } from "./ui/button"
import { MemoryCard } from "./MemoryCard"
import { memoryRefreshTrigger } from "./DebugDisplay"
interface MemoryItem {
id: string // Kept for React key prop
content: string
createdAt: string
categories: string[]
}
const fetchMemories = async () => {
const response = await fetch("http://localhost:7860/memories")
const data = await response.json()
return data
}
const MemorySkeleton = () => (
<div className="animate-pulse space-y-4">
<div className="flex items-start space-x-4">
<div className="h-10 w-10 rounded-full bg-neutral-200 dark:bg-neutral-800" />
<div className="space-y-3 flex-1">
<div className="h-4 w-1/4 bg-neutral-200 dark:bg-neutral-800 rounded" />
<div className="space-y-2">
<div className="h-4 w-full bg-neutral-200 dark:bg-neutral-800 rounded" />
<div className="h-4 w-5/6 bg-neutral-200 dark:bg-neutral-800 rounded" />
</div>
</div>
</div>
</div>
)
export default function StaticMemoryPanel() {
const [isCollapsed, setIsCollapsed] = useState(false)
const [memories, setMemories] = useState<MemoryItem[]>([])
const [isInitialLoading, setIsInitialLoading] = useState(true)
const [localRefreshTrigger, setLocalRefreshTrigger] = useState(0)
const fetchEffectMemories = useCallback(async () => {
// Only set loading if this is the initial fetch (no memories)
if (memories.length === 0) {
setIsInitialLoading(true)
}
try {
const memories = await fetchMemories()
if (memories) {
setMemories(memories as unknown as MemoryItem[])
} else {
setMemories([])
console.log("No memories found")
}
} catch (error) {
console.error("Error fetching memories:", error)
setMemories([])
} finally {
setIsInitialLoading(false)
}
}, [memories.length])
useEffect(() => {
fetchEffectMemories()
}, [fetchEffectMemories, localRefreshTrigger])
useEffect(() => {
const handleRefresh = (value: number) => {
setLocalRefreshTrigger(value);
};
memoryRefreshTrigger.subscribers.add(handleRefresh);
return () => {
memoryRefreshTrigger.subscribers.delete(handleRefresh);
};
}, []);
return (
<Card className="w-full overflow-hidden border border-neutral-200 dark:border-neutral-800 bg-zinc-50 dark:bg-neutral-900">
<CardHeader className="p-4 pb-2 flex flex-row items-center justify-between space-y-0 border-b border-neutral-200 dark:border-neutral-800">
<div className="flex items-center gap-3">
<div className={cn("h-8 w-8 rounded-full flex items-center justify-center bg-violet-100 dark:bg-violet-900/30")}>
<Memory className="h-4 w-4 text-violet-500" weight="duotone" />
</div>
<div className="flex flex-col">
<CardTitle className="text-base font-medium text-neutral-900 dark:text-neutral-100">
Memories ({memories.length})
</CardTitle>
</div>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setIsCollapsed(!isCollapsed)}
className="h-8 w-8"
>
<CaretRight
className={cn(
"h-4 w-4 text-neutral-500 transition-transform",
isCollapsed ? "" : "rotate-90"
)}
/>
</Button>
</CardHeader>
<div className={cn(
"transition-all duration-300 ease-in-out",
isCollapsed ? "h-0" : "h-[calc(100vh-218px)]"
)}>
{!isCollapsed && (
<ScrollArea className="h-full">
<CardContent className="py-4">
<div className="space-y-6">
{isInitialLoading && memories.length === 0 ? (
<>
<MemorySkeleton />
<MemorySkeleton />
<MemorySkeleton />
</>
) : (
memories.map((memory) => (
<MemoryCard key={memory.id} memory={memory} />
))
)}
</div>
</CardContent>
</ScrollArea>
)}
</div>
</Card>
)
}

View File

@@ -0,0 +1,11 @@
import { useRTVIClientTransportState } from '@pipecat-ai/client-react';
export function StatusDisplay() {
const transportState = useRTVIClientTransportState();
return (
<div className="status">
Status: <span>{transportState}</span>
</div>
);
}

View File

@@ -0,0 +1,12 @@
import mem0Logo from "./light.svg";
export default function ThemeAwareLogo({
width = 100,
height = 35,
}: {
width?: number;
height?: number;
}) {
return <img src={mem0Logo} alt="Mem0.ai" width={width} height={height} />;
}

View File

@@ -0,0 +1,33 @@
interface WaveformProps {
speakingState: 'user' | 'assistant' | 'system' | 'idle';
}
export function Waveform({ speakingState }: WaveformProps) {
const getAnimationClass = () => {
switch (speakingState) {
case 'user':
return 'waveform-bar bg-zinc-800';
case 'assistant':
return 'waveform-bar bg-zinc-600';
case 'system':
return 'waveform-bar-listening bg-zinc-400';
default:
return 'waveform-bar-listening bg-zinc-300';
}
};
return (
<div className="flex items-end gap-1 h-8">
{[...Array(5)].map((_, i) => (
<div
key={i}
className={`w-1 rounded-full ${getAnimationClass()}`}
style={{
height: speakingState === 'idle' ? '40%' : `${Math.random() * 60 + 40}%`,
animationDelay: `${i * 0.1}s`
}}
/>
))}
</div>
);
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,35 @@
import React from "react";
import { ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
interface InteractiveHoverButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
export const InteractiveHoverButton = React.forwardRef<
HTMLButtonElement,
InteractiveHoverButtonProps
>(({ children, className, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
"group relative w-auto cursor-pointer overflow-hidden rounded-full border bg-background p-2 px-6 text-center font-semibold hover:text-white",
className,
)}
{...props}
>
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-black transition-all duration-300 group-hover:scale-[100.8]"></div>
<span className="inline-block transition-all duration-300 group-hover:translate-x-12 group-hover:opacity-0">
{children}
</span>
</div>
<div className="absolute top-0 z-10 flex h-full w-full translate-x-12 items-center justify-center gap-2 text-primary-foreground opacity-0 transition-all duration-300 group-hover:-translate-x-5 group-hover:opacity-100">
<span>{children}</span>
<ArrowRight />
</div>
</button>
);
});
InteractiveHoverButton.displayName = "InteractiveHoverButton";

View File

@@ -0,0 +1,56 @@
import { useEffect, useRef } from "react"
import { cn } from "@/lib/utils"
interface BorderTrailProps extends React.HTMLAttributes<HTMLDivElement> {
size?: number
duration?: number
}
export const BorderTrail: React.FC<BorderTrailProps> = ({ className, size = 100, duration = 5, ...props }) => {
const borderRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const borderElement = borderRef.current
if (!borderElement) return
const animateBorder = () => {
const startTime = performance.now()
let animationFrameId: number
const animate = (currentTime: number) => {
const elapsed = (currentTime - startTime) / 1000
const progress = (elapsed % duration) / duration
const rotation = progress * 360
if (borderElement) {
borderElement.style.transform = `rotate(${rotation}deg)`
}
animationFrameId = requestAnimationFrame(animate)
}
animationFrameId = requestAnimationFrame(animate)
return () => {
cancelAnimationFrame(animationFrameId)
}
}
const cleanup = animateBorder()
return cleanup
}, [duration])
return (
<div
ref={borderRef}
className={cn("absolute inset-0 rounded-full opacity-70", className)}
style={{
width: `${size}%`,
height: `${size}%`,
left: `${(100 - size) / 2}%`,
top: `${(100 - size) / 2}%`,
}}
{...props}
/>
)
}

View File

@@ -0,0 +1,55 @@
import { useEffect, useRef } from "react"
import { cn } from "@/lib/utils"
interface TextShimmerProps extends React.HTMLAttributes<HTMLDivElement> {
duration?: number
children: React.ReactNode
}
export const TextShimmer: React.FC<TextShimmerProps> = ({ className, duration = 3, children, ...props }) => {
const shimmerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const shimmerElement = shimmerRef.current
if (!shimmerElement) return
const animateShimmer = () => {
const startTime = performance.now()
let animationFrameId: number
const animate = (currentTime: number) => {
const elapsed = (currentTime - startTime) / 1000
const progress = (elapsed % duration) / duration
const position = progress * 200 - 100 // -100% to 100%
if (shimmerElement) {
shimmerElement.style.backgroundPosition = `${position}% 0`
}
animationFrameId = requestAnimationFrame(animate)
}
animationFrameId = requestAnimationFrame(animate)
return () => {
cancelAnimationFrame(animationFrameId)
}
}
const cleanup = animateShimmer()
return cleanup
}, [duration])
return (
<div
ref={shimmerRef}
className={cn(
"inline-block bg-gradient-to-r from-transparent via-violet-400/20 to-transparent bg-[length:200%_100%]",
className
)}
{...props}
>
{children}
</div>
)
}

View File

@@ -0,0 +1,31 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: "default" | "secondary" | "destructive" | "outline"
}
export function Badge({
className,
variant = "default",
...props
}: BadgeProps) {
return (
<div
className={cn(
"inline-flex items-center rounded-full 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",
{
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80":
variant === "default",
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80":
variant === "secondary",
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80":
variant === "destructive",
"text-foreground": variant === "outline",
},
className
)}
{...props}
/>
)
}

View File

@@ -0,0 +1,58 @@
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-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
outline:
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-xs 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 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1,54 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border border-neutral-200 bg-white text-neutral-950 shadow-sm dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
export { Card, CardHeader, CardTitle, CardContent }

View File

@@ -0,0 +1,56 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,22 @@
import { type PropsWithChildren } from 'react';
import { RTVIClient } from '@pipecat-ai/client-js';
import { DailyTransport } from '@pipecat-ai/daily-transport';
import { RTVIClientProvider } from '@pipecat-ai/client-react';
const transport = new DailyTransport();
const client = new RTVIClient({
transport,
params: {
baseUrl: 'http://localhost:7860',
endpoints: {
connect: '/connect',
},
},
enableMic: true,
enableCam: false,
});
export function RTVIProvider({ children }: PropsWithChildren) {
return <RTVIClientProvider client={client}>{children}</RTVIClientProvider>;
}

View File

@@ -0,0 +1,53 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
keyframes: {
'message-pop': {
'0%': {
opacity: '0',
transform: 'scale(0.95) translateY(10px)'
},
'100%': {
opacity: '1',
transform: 'scale(1) translateY(0)'
}
},
'memory-pop': {
'0%': {
opacity: '0',
transform: 'translateY(10px)'
},
'100%': {
opacity: '1',
transform: 'translateY(0)'
}
},
'loading-bar': {
'0%': {
transform: 'translateX(-100%)'
},
'100%': {
transform: 'translateX(100%)'
}
},
'shimmer': {
'100%': {
transform: 'translateX(100%)'
}
}
},
animation: {
'message-pop': 'message-pop 0.3s ease-out',
'memory-pop': 'memory-pop 0.3s ease-out forwards',
'loading-bar': 'loading-bar 2s infinite',
'shimmer': 'shimmer 1.5s infinite'
}
},
},
plugins: [],
}

View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,14 @@
import path from 'path'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})