From 655ae794b62b02c427181a1d9d538832183257d7 Mon Sep 17 00:00:00 2001 From: Dev Khant Date: Fri, 7 Mar 2025 23:36:32 +0530 Subject: [PATCH] Examples: Add multimodal app (#2328) --- examples/multimodal-demo/.gitattributes | 2 + examples/multimodal-demo/.gitignore | 29 +++ examples/multimodal-demo/components.json | 20 ++ examples/multimodal-demo/eslint.config.js | 28 +++ examples/multimodal-demo/index.html | 13 + .../package.json | 7 +- examples/multimodal-demo/postcss.config.js | 6 + .../multimodal-demo/public/mem0_logo.jpeg | Bin 0 -> 8607 bytes examples/multimodal-demo/src/App.tsx | 13 + .../multimodal-demo/src/assets/mem0_logo.jpeg | Bin 0 -> 8607 bytes examples/multimodal-demo/src/assets/react.svg | 1 + examples/multimodal-demo/src/assets/user.jpg | Bin 0 -> 15547 bytes .../src/components/api-settings-popup.tsx | 91 +++++++ .../src/components/chevron-toggle.tsx | 35 +++ .../multimodal-demo/src/components/header.tsx | 81 +++++++ .../src/components/input-area.tsx | 107 +++++++++ .../src/components/memories.tsx | 84 +++++++ .../src/components/messages.tsx | 102 ++++++++ .../src/components/ui/avatar.tsx | 50 ++++ .../src/components/ui/badge.tsx | 36 +++ .../src/components/ui/button.tsx | 57 +++++ .../src/components/ui/card.tsx | 76 ++++++ .../src/components/ui/dialog.tsx | 120 ++++++++++ .../src/components/ui/input.tsx | 25 ++ .../src/components/ui/label.tsx | 24 ++ .../src/components/ui/scroll-area.tsx | 46 ++++ .../src/components/ui/select.tsx | 164 +++++++++++++ .../multimodal-demo/src/constants/messages.ts | 31 +++ .../src/contexts/GlobalContext.tsx | 110 +++++++++ examples/multimodal-demo/src/hooks/useAuth.ts | 73 ++++++ examples/multimodal-demo/src/hooks/useChat.ts | 220 +++++++++++++++++ .../src/hooks/useFileHandler.ts | 45 ++++ examples/multimodal-demo/src/index.css | 97 ++++++++ examples/multimodal-demo/src/libs/utils.ts | 6 + examples/multimodal-demo/src/main.tsx | 10 + examples/multimodal-demo/src/page.tsx | 14 ++ examples/multimodal-demo/src/pages/home.tsx | 41 ++++ examples/multimodal-demo/src/types.ts | 22 ++ .../multimodal-demo/src/utils/fileUtils.ts | 16 ++ examples/multimodal-demo/src/vite-env.d.ts | 1 + examples/multimodal-demo/tailwind.config.js | 62 +++++ examples/multimodal-demo/tsconfig.app.json | 32 +++ examples/multimodal-demo/tsconfig.json | 13 + examples/multimodal-demo/tsconfig.node.json | 24 ++ examples/multimodal-demo/useChat.ts | 223 ++++++++++++++++++ examples/multimodal-demo/vite.config.ts | 13 + 46 files changed, 2268 insertions(+), 2 deletions(-) create mode 100644 examples/multimodal-demo/.gitattributes create mode 100644 examples/multimodal-demo/.gitignore create mode 100644 examples/multimodal-demo/components.json create mode 100644 examples/multimodal-demo/eslint.config.js create mode 100644 examples/multimodal-demo/index.html rename examples/{vercel-ai-sdk-chat-app => multimodal-demo}/package.json (85%) create mode 100644 examples/multimodal-demo/postcss.config.js create mode 100644 examples/multimodal-demo/public/mem0_logo.jpeg create mode 100644 examples/multimodal-demo/src/App.tsx create mode 100644 examples/multimodal-demo/src/assets/mem0_logo.jpeg create mode 100644 examples/multimodal-demo/src/assets/react.svg create mode 100644 examples/multimodal-demo/src/assets/user.jpg create mode 100644 examples/multimodal-demo/src/components/api-settings-popup.tsx create mode 100644 examples/multimodal-demo/src/components/chevron-toggle.tsx create mode 100644 examples/multimodal-demo/src/components/header.tsx create mode 100644 examples/multimodal-demo/src/components/input-area.tsx create mode 100644 examples/multimodal-demo/src/components/memories.tsx create mode 100644 examples/multimodal-demo/src/components/messages.tsx create mode 100644 examples/multimodal-demo/src/components/ui/avatar.tsx create mode 100644 examples/multimodal-demo/src/components/ui/badge.tsx create mode 100644 examples/multimodal-demo/src/components/ui/button.tsx create mode 100644 examples/multimodal-demo/src/components/ui/card.tsx create mode 100644 examples/multimodal-demo/src/components/ui/dialog.tsx create mode 100644 examples/multimodal-demo/src/components/ui/input.tsx create mode 100644 examples/multimodal-demo/src/components/ui/label.tsx create mode 100644 examples/multimodal-demo/src/components/ui/scroll-area.tsx create mode 100644 examples/multimodal-demo/src/components/ui/select.tsx create mode 100644 examples/multimodal-demo/src/constants/messages.ts create mode 100644 examples/multimodal-demo/src/contexts/GlobalContext.tsx create mode 100644 examples/multimodal-demo/src/hooks/useAuth.ts create mode 100644 examples/multimodal-demo/src/hooks/useChat.ts create mode 100644 examples/multimodal-demo/src/hooks/useFileHandler.ts create mode 100644 examples/multimodal-demo/src/index.css create mode 100644 examples/multimodal-demo/src/libs/utils.ts create mode 100644 examples/multimodal-demo/src/main.tsx create mode 100644 examples/multimodal-demo/src/page.tsx create mode 100644 examples/multimodal-demo/src/pages/home.tsx create mode 100644 examples/multimodal-demo/src/types.ts create mode 100644 examples/multimodal-demo/src/utils/fileUtils.ts create mode 100644 examples/multimodal-demo/src/vite-env.d.ts create mode 100644 examples/multimodal-demo/tailwind.config.js create mode 100644 examples/multimodal-demo/tsconfig.app.json create mode 100644 examples/multimodal-demo/tsconfig.json create mode 100644 examples/multimodal-demo/tsconfig.node.json create mode 100644 examples/multimodal-demo/useChat.ts create mode 100644 examples/multimodal-demo/vite.config.ts diff --git a/examples/multimodal-demo/.gitattributes b/examples/multimodal-demo/.gitattributes new file mode 100644 index 00000000..dfe07704 --- /dev/null +++ b/examples/multimodal-demo/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/examples/multimodal-demo/.gitignore b/examples/multimodal-demo/.gitignore new file mode 100644 index 00000000..9767597e --- /dev/null +++ b/examples/multimodal-demo/.gitignore @@ -0,0 +1,29 @@ +**/.env +**/node_modules +**/dist +**/.DS_Store + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/multimodal-demo/components.json b/examples/multimodal-demo/components.json new file mode 100644 index 00000000..eaf9959b --- /dev/null +++ b/examples/multimodal-demo/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/libs/utils", + "ui": "@/components/ui", + "lib": "@/libs", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/examples/multimodal-demo/eslint.config.js b/examples/multimodal-demo/eslint.config.js new file mode 100644 index 00000000..092408a9 --- /dev/null +++ b/examples/multimodal-demo/eslint.config.js @@ -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 }, + ], + }, + }, +) diff --git a/examples/multimodal-demo/index.html b/examples/multimodal-demo/index.html new file mode 100644 index 00000000..e2135b1c --- /dev/null +++ b/examples/multimodal-demo/index.html @@ -0,0 +1,13 @@ + + + + + + + JustChat | Chat with AI + + +
+ + + diff --git a/examples/vercel-ai-sdk-chat-app/package.json b/examples/multimodal-demo/package.json similarity index 85% rename from examples/vercel-ai-sdk-chat-app/package.json rename to examples/multimodal-demo/package.json index 47a3e4c4..3a7a7434 100644 --- a/examples/vercel-ai-sdk-chat-app/package.json +++ b/examples/multimodal-demo/package.json @@ -24,9 +24,11 @@ "clsx": "^2.1.1", "framer-motion": "^11.11.11", "lucide-react": "^0.454.0", + "openai": "^4.86.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", + "mem0ai": "2.1.2", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" @@ -46,6 +48,7 @@ "tailwindcss": "^3.4.14", "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", - "vite": "^5.4.10" - } + "vite": "^6.2.1" + }, + "packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b" } diff --git a/examples/multimodal-demo/postcss.config.js b/examples/multimodal-demo/postcss.config.js new file mode 100644 index 00000000..2e7af2b7 --- /dev/null +++ b/examples/multimodal-demo/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/multimodal-demo/public/mem0_logo.jpeg b/examples/multimodal-demo/public/mem0_logo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..eb02b0ec9c908a940253f8e0092fb5023a36f25b GIT binary patch literal 8607 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 literal 0 HcmV?d00001 diff --git a/examples/multimodal-demo/src/App.tsx b/examples/multimodal-demo/src/App.tsx new file mode 100644 index 00000000..4564ce5d --- /dev/null +++ b/examples/multimodal-demo/src/App.tsx @@ -0,0 +1,13 @@ +import Home from "./page" + + +function App() { + + return ( + <> + + + ) +} + +export default App diff --git a/examples/multimodal-demo/src/assets/mem0_logo.jpeg b/examples/multimodal-demo/src/assets/mem0_logo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..eb02b0ec9c908a940253f8e0092fb5023a36f25b GIT binary patch literal 8607 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 literal 0 HcmV?d00001 diff --git a/examples/multimodal-demo/src/assets/react.svg b/examples/multimodal-demo/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/examples/multimodal-demo/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/multimodal-demo/src/assets/user.jpg b/examples/multimodal-demo/src/assets/user.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f2e7fc22d39645a57d6c2da8369874f2198a150a GIT binary patch literal 15547 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 literal 0 HcmV?d00001 diff --git a/examples/multimodal-demo/src/components/api-settings-popup.tsx b/examples/multimodal-demo/src/components/api-settings-popup.tsx new file mode 100644 index 00000000..8a4eac39 --- /dev/null +++ b/examples/multimodal-demo/src/components/api-settings-popup.tsx @@ -0,0 +1,91 @@ +import { Dispatch, SetStateAction, useContext, useEffect, useState } from 'react' +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" +import GlobalContext from '@/contexts/GlobalContext' +import { Provider } from '@/constants/messages' +export default function ApiSettingsPopup(props: { isOpen: boolean, setIsOpen: Dispatch> }) { + const {isOpen, setIsOpen} = props + const [mem0ApiKey, setMem0ApiKey] = useState('') + const [providerApiKey, setProviderApiKey] = useState('') + const [provider, setProvider] = useState('OpenAI') + const { selectorHandler, selectedOpenAIKey, selectedMem0Key, selectedProvider } = useContext(GlobalContext); + + const handleSave = () => { + // Here you would typically save the settings to your backend or local storage + selectorHandler(mem0ApiKey, providerApiKey, provider as Provider); + setIsOpen(false) + } + + useEffect(() => { + if (selectedOpenAIKey) { + setProviderApiKey(selectedOpenAIKey); + } + if (selectedMem0Key) { + setMem0ApiKey(selectedMem0Key); + } + if (selectedProvider) { + setProvider(selectedProvider); + } + }, [selectedOpenAIKey, selectedMem0Key, selectedProvider]); + + + + return ( + <> + + + + API Configuration Settings + +
+
+ + setMem0ApiKey(e.target.value)} + className="col-span-3 rounded-3xl" + /> +
+
+ + setProviderApiKey(e.target.value)} + className="col-span-3 rounded-3xl" + /> +
+
+ + +
+
+ + + + +
+
+ + ) +} \ No newline at end of file diff --git a/examples/multimodal-demo/src/components/chevron-toggle.tsx b/examples/multimodal-demo/src/components/chevron-toggle.tsx new file mode 100644 index 00000000..7b8b128e --- /dev/null +++ b/examples/multimodal-demo/src/components/chevron-toggle.tsx @@ -0,0 +1,35 @@ +import { Button } from "@/components/ui/button"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import React from "react"; + +const ChevronToggle = (props: { + isMemoriesExpanded: boolean; + setIsMemoriesExpanded: React.Dispatch>; +}) => { + const { isMemoriesExpanded, setIsMemoriesExpanded } = props; + return ( + <> +
+
+ +
+
+ + ); +}; + +export default ChevronToggle; diff --git a/examples/multimodal-demo/src/components/header.tsx b/examples/multimodal-demo/src/components/header.tsx new file mode 100644 index 00000000..7ddbd37d --- /dev/null +++ b/examples/multimodal-demo/src/components/header.tsx @@ -0,0 +1,81 @@ +import { Button } from "@/components/ui/button"; +import { ChevronRight, X, RefreshCcw, Settings } from "lucide-react"; +import { Dispatch, SetStateAction, useContext, useEffect, useState } from "react"; +import GlobalContext from "../contexts/GlobalContext"; +import { Input } from "./ui/input"; + +const Header = (props: { + setIsSettingsOpen: Dispatch>; +}) => { + const { setIsSettingsOpen } = props; + const { selectUserHandler, clearUserHandler, selectedUser, clearConfiguration } = useContext(GlobalContext); + const [userId, setUserId] = useState(""); + + const handleSelectUser = (e: React.ChangeEvent) => { + setUserId(e.target.value); + }; + + const handleClearUser = () => { + clearUserHandler(); + setUserId(""); + }; + + const handleSubmit = () => { + selectUserHandler(userId); + }; + + // New function to handle key down events + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); // Prevent form submission if it's in a form + handleSubmit(); + } + }; + + useEffect(() => { + if (selectedUser) { + setUserId(selectedUser); + } + }, [selectedUser]); + + return ( + <> +
+
+ Mem0 Assistant +
+
+
+ + + +
+
+ + +
+
+
+ + ); +}; + +export default Header; diff --git a/examples/multimodal-demo/src/components/input-area.tsx b/examples/multimodal-demo/src/components/input-area.tsx new file mode 100644 index 00000000..877e19a2 --- /dev/null +++ b/examples/multimodal-demo/src/components/input-area.tsx @@ -0,0 +1,107 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import GlobalContext from "@/contexts/GlobalContext"; +import { FileInfo } from "@/types"; +import { Images, Send, X } from "lucide-react"; +import { useContext, useRef, useState } from "react"; + +const InputArea = () => { + const [inputValue, setInputValue] = useState(""); + const { handleSend, selectedFile, setSelectedFile, setFile } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + + const ref = useRef(null); + const fileInputRef = useRef(null) + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (file) { + setSelectedFile({ + name: file.name, + type: file.type, + size: file.size + }) + setFile(file) + } + } + + const handleSendController = async () => { + setLoading(true); + setInputValue(""); + await handleSend(inputValue); + setLoading(false); + + // focus on input + setTimeout(() => { + ref.current?.focus(); + }, 0); + }; + + const handleClosePopup = () => { + setSelectedFile(null) + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + } + + return ( + <> +
+
+
+
+ + + {selectedFile && } +
+
+ setInputValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSendController()} + placeholder="Type a message..." + className="flex-1 pl-10 rounded-3xl" + disabled={loading} + ref={ref} + /> +
+ +
+
+
+ + ); +}; + +const FileInfoPopup = ({ file, onClose }: { file: FileInfo, onClose: () => void }) => { + return ( +
+
+
+

{file.name}

+ +
+

Type: {file.type}

+

Size: {(file.size / 1024).toFixed(2)} KB

+
+
+ ) +} + +export default InputArea; diff --git a/examples/multimodal-demo/src/components/memories.tsx b/examples/multimodal-demo/src/components/memories.tsx new file mode 100644 index 00000000..940fbe63 --- /dev/null +++ b/examples/multimodal-demo/src/components/memories.tsx @@ -0,0 +1,84 @@ +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { ScrollArea } from "@radix-ui/react-scroll-area"; +import { Memory } from "../types"; +import GlobalContext from "@/contexts/GlobalContext"; +import { useContext } from "react"; +import { motion } from "framer-motion"; + + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const MemoryItem = ({ memory }: { memory: Memory; index: number }) => { + return ( + +
+

{memory.content}

+
+
+ {new Date(memory.timestamp).toLocaleString()} +
+
+ {memory.tags.map((tag) => ( + + {tag} + + ))} +
+
+ ); +}; + +const Memories = (props: { isMemoriesExpanded: boolean }) => { + const { isMemoriesExpanded } = props; + const { memories } = useContext(GlobalContext); + + return ( + +
+ + Relevant Memories ({memories.length}) + +
+ {memories.length === 0 && ( + + No relevant memories found. +
+ Only the relevant memories will be displayed here. +
+ )} + + + {/* */} + {memories.map((memory: Memory, index: number) => ( + + ))} + {/* */} + + +
+ ); +}; + +export default Memories; \ No newline at end of file diff --git a/examples/multimodal-demo/src/components/messages.tsx b/examples/multimodal-demo/src/components/messages.tsx new file mode 100644 index 00000000..38e5a59e --- /dev/null +++ b/examples/multimodal-demo/src/components/messages.tsx @@ -0,0 +1,102 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Message } from "../types"; +import { useContext, useEffect, useRef } from "react"; +import GlobalContext from "@/contexts/GlobalContext"; +import Markdown from "react-markdown"; +import Mem00Logo from "../assets/mem0_logo.jpeg"; +import UserLogo from "../assets/user.jpg"; + +const Messages = () => { + const { messages, thinking } = useContext(GlobalContext); + const scrollAreaRef = useRef(null); + + // scroll to bottom + useEffect(() => { + if (scrollAreaRef.current) { + scrollAreaRef.current.scrollTop += 40; // Scroll down by 40 pixels + } + }, [messages, thinking]); + + return ( + <> + +
+ {messages.map((message: Message) => ( +
+
+
+ + + + {message.sender === "assistant" ? "AI" : "U"} + + +
+
+ {message.image && ( +
+ Message attachment +
+ )} + {message.content} + + {message.timestamp} + +
+
+
+ ))} + {thinking && ( +
+
+ + + {"AI"} + +
+
+
+
+
+
+
+
+
+ )} +
+
+ + ); +}; + +export default Messages; diff --git a/examples/multimodal-demo/src/components/ui/avatar.tsx b/examples/multimodal-demo/src/components/ui/avatar.tsx new file mode 100644 index 00000000..9065241a --- /dev/null +++ b/examples/multimodal-demo/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/libs/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/examples/multimodal-demo/src/components/ui/badge.tsx b/examples/multimodal-demo/src/components/ui/badge.tsx new file mode 100644 index 00000000..060b2f11 --- /dev/null +++ b/examples/multimodal-demo/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/libs/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, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/examples/multimodal-demo/src/components/ui/button.tsx b/examples/multimodal-demo/src/components/ui/button.tsx new file mode 100644 index 00000000..3e85ff7a --- /dev/null +++ b/examples/multimodal-demo/src/components/ui/button.tsx @@ -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 "@/libs/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, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/examples/multimodal-demo/src/components/ui/card.tsx b/examples/multimodal-demo/src/components/ui/card.tsx new file mode 100644 index 00000000..e90617d5 --- /dev/null +++ b/examples/multimodal-demo/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/libs/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/examples/multimodal-demo/src/components/ui/dialog.tsx b/examples/multimodal-demo/src/components/ui/dialog.tsx new file mode 100644 index 00000000..1796099a --- /dev/null +++ b/examples/multimodal-demo/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/libs/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/examples/multimodal-demo/src/components/ui/input.tsx b/examples/multimodal-demo/src/components/ui/input.tsx new file mode 100644 index 00000000..d2bdc607 --- /dev/null +++ b/examples/multimodal-demo/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/libs/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/examples/multimodal-demo/src/components/ui/label.tsx b/examples/multimodal-demo/src/components/ui/label.tsx new file mode 100644 index 00000000..4a31cf96 --- /dev/null +++ b/examples/multimodal-demo/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/libs/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/examples/multimodal-demo/src/components/ui/scroll-area.tsx b/examples/multimodal-demo/src/components/ui/scroll-area.tsx new file mode 100644 index 00000000..94e4b135 --- /dev/null +++ b/examples/multimodal-demo/src/components/ui/scroll-area.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/libs/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/examples/multimodal-demo/src/components/ui/select.tsx b/examples/multimodal-demo/src/components/ui/select.tsx new file mode 100644 index 00000000..cdf9257b --- /dev/null +++ b/examples/multimodal-demo/src/components/ui/select.tsx @@ -0,0 +1,164 @@ +"use client" + +import * as React from "react" +import { + CaretSortIcon, + CheckIcon, + ChevronDownIcon, + ChevronUpIcon, +} from "@radix-ui/react-icons" +import * as SelectPrimitive from "@radix-ui/react-select" + +import { cn } from "@/libs/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/examples/multimodal-demo/src/constants/messages.ts b/examples/multimodal-demo/src/constants/messages.ts new file mode 100644 index 00000000..af3280a0 --- /dev/null +++ b/examples/multimodal-demo/src/constants/messages.ts @@ -0,0 +1,31 @@ +import { Message } from "@/types"; + +export const WELCOME_MESSAGE: Message = { + id: "1", + content: "👋 Hi there! I'm your personal assistant. How can I help you today? 😊", + sender: "assistant", + timestamp: new Date().toLocaleTimeString(), +}; + +export const INVALID_CONFIG_MESSAGE: Message = { + id: "2", + content: "Invalid configuration. Please check your API keys, and add a user and try again.", + sender: "assistant", + timestamp: new Date().toLocaleTimeString(), +}; + +export const ERROR_MESSAGE: Message = { + id: "3", + content: "Something went wrong. Please try again.", + sender: "assistant", + timestamp: new Date().toLocaleTimeString(), +}; + +export const AI_MODELS = { + openai: "gpt-4o", + anthropic: "claude-3-haiku-20240307", + cohere: "command-r-plus", + groq: "gemma2-9b-it", +} as const; + +export type Provider = keyof typeof AI_MODELS; \ No newline at end of file diff --git a/examples/multimodal-demo/src/contexts/GlobalContext.tsx b/examples/multimodal-demo/src/contexts/GlobalContext.tsx new file mode 100644 index 00000000..755ea829 --- /dev/null +++ b/examples/multimodal-demo/src/contexts/GlobalContext.tsx @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { createContext } from 'react'; +import { Message, Memory, FileInfo } from '@/types'; +import { useAuth } from '@/hooks/useAuth'; +import { useChat } from '@/hooks/useChat'; +import { useFileHandler } from '@/hooks/useFileHandler'; +import { Provider } from '@/constants/messages'; + +interface GlobalContextType { + selectedUser: string; + selectUserHandler: (user: string) => void; + clearUserHandler: () => void; + messages: Message[]; + memories: Memory[]; + handleSend: (content: string) => Promise; + thinking: boolean; + selectedMem0Key: string; + selectedOpenAIKey: string; + selectedProvider: Provider; + selectorHandler: (mem0: string, openai: string, provider: Provider) => void; + clearConfiguration: () => void; + selectedFile: FileInfo | null; + setSelectedFile: (file: FileInfo | null) => void; + file: File | null; + setFile: (file: File | null) => void; +} + +const GlobalContext = createContext({} as GlobalContextType); + +const GlobalState = (props: { children: React.ReactNode }) => { + const { + mem0ApiKey: selectedMem0Key, + openaiApiKey: selectedOpenAIKey, + provider: selectedProvider, + user: selectedUser, + setAuth: selectorHandler, + setUser: selectUserHandler, + clearAuth: clearConfiguration, + clearUser: clearUserHandler, + } = useAuth(); + + const { + selectedFile, + file, + fileData, + setSelectedFile, + handleFile, + clearFile, + } = useFileHandler(); + + const { + messages, + memories, + thinking, + sendMessage, + } = useChat({ + user: selectedUser, + mem0ApiKey: selectedMem0Key, + openaiApiKey: selectedOpenAIKey, + provider: selectedProvider, + }); + + const handleSend = async (content: string) => { + if (file) { + await sendMessage(content, { + type: file.type, + data: fileData!, + }); + clearFile(); + } else { + await sendMessage(content); + } + }; + + const setFile = async (newFile: File | null) => { + if (newFile) { + await handleFile(newFile); + } else { + clearFile(); + } + }; + + return ( + + {props.children} + + ); +}; + +export default GlobalContext; +export { GlobalState }; \ No newline at end of file diff --git a/examples/multimodal-demo/src/hooks/useAuth.ts b/examples/multimodal-demo/src/hooks/useAuth.ts new file mode 100644 index 00000000..5687442c --- /dev/null +++ b/examples/multimodal-demo/src/hooks/useAuth.ts @@ -0,0 +1,73 @@ +import { useState, useEffect } from 'react'; +import { Provider } from '@/constants/messages'; + +interface UseAuthReturn { + mem0ApiKey: string; + openaiApiKey: string; + provider: Provider; + user: string; + setAuth: (mem0: string, openai: string, provider: Provider) => void; + setUser: (user: string) => void; + clearAuth: () => void; + clearUser: () => void; +} + +export const useAuth = (): UseAuthReturn => { + const [mem0ApiKey, setMem0ApiKey] = useState(''); + const [openaiApiKey, setOpenaiApiKey] = useState(''); + const [provider, setProvider] = useState('openai'); + const [user, setUser] = useState(''); + + useEffect(() => { + const mem0 = localStorage.getItem('mem0ApiKey'); + const openai = localStorage.getItem('openaiApiKey'); + const savedProvider = localStorage.getItem('provider') as Provider; + const savedUser = localStorage.getItem('user'); + + if (mem0 && openai && savedProvider) { + setAuth(mem0, openai, savedProvider); + } + if (savedUser) { + setUser(savedUser); + } + }, []); + + const setAuth = (mem0: string, openai: string, provider: Provider) => { + setMem0ApiKey(mem0); + setOpenaiApiKey(openai); + setProvider(provider); + localStorage.setItem('mem0ApiKey', mem0); + localStorage.setItem('openaiApiKey', openai); + localStorage.setItem('provider', provider); + }; + + const clearAuth = () => { + localStorage.removeItem('mem0ApiKey'); + localStorage.removeItem('openaiApiKey'); + localStorage.removeItem('provider'); + setMem0ApiKey(''); + setOpenaiApiKey(''); + setProvider('openai'); + }; + + const updateUser = (user: string) => { + setUser(user); + localStorage.setItem('user', user); + }; + + const clearUser = () => { + localStorage.removeItem('user'); + setUser(''); + }; + + return { + mem0ApiKey, + openaiApiKey, + provider, + user, + setAuth, + setUser: updateUser, + clearAuth, + clearUser, + }; +}; \ No newline at end of file diff --git a/examples/multimodal-demo/src/hooks/useChat.ts b/examples/multimodal-demo/src/hooks/useChat.ts new file mode 100644 index 00000000..b54826a9 --- /dev/null +++ b/examples/multimodal-demo/src/hooks/useChat.ts @@ -0,0 +1,220 @@ +import { useState } from 'react'; +import { MemoryClient } from 'saket-test'; +import { OpenAI } from 'openai'; +import { Message, Memory } from '@/types'; +import { WELCOME_MESSAGE, INVALID_CONFIG_MESSAGE, ERROR_MESSAGE, AI_MODELS, Provider } from '@/constants/messages'; + +interface UseChatProps { + user: string; + mem0ApiKey: string; + openaiApiKey: string; + provider: Provider; +} + +interface UseChatReturn { + messages: Message[]; + memories: Memory[]; + thinking: boolean; + sendMessage: (content: string, fileData?: { type: string; data: string | Buffer }) => Promise; +} + +type MessageContent = string | { + type: 'image_url'; + image_url: { + url: string; + }; +}; + +interface PromptMessage { + role: string; + content: MessageContent; +} + +export const useChat = ({ user, mem0ApiKey, openaiApiKey, provider }: UseChatProps): UseChatReturn => { + const [messages, setMessages] = useState([WELCOME_MESSAGE]); + const [memories, setMemories] = useState([]); + const [thinking, setThinking] = useState(false); + + const openai = new OpenAI({ apiKey: openaiApiKey}); + const memoryClient = new MemoryClient({ apiKey: mem0ApiKey }); + + const updateMemories = async (messages: PromptMessage[]) => { + console.log(messages); + try { + await memoryClient.add(messages, { + user_id: user, + output_format: "v1.1", + }); + + const response = await memoryClient.getAll({ + user_id: user, + page: 1, + page_size: 50, + }); + + const newMemories = response.results.map((memory: any) => ({ + id: memory.id, + content: memory.memory, + timestamp: memory.updated_at, + tags: memory.categories || [], + })); + setMemories(newMemories); + } catch (error) { + console.error('Error in updateMemories:', error); + } + }; + + const formatMessagesForPrompt = (messages: Message[]): PromptMessage[] => { + return messages.map((message) => { + if (message.image) { + return { + role: message.sender, + content: { + type: 'image_url', + image_url: { + url: message.image + } + }, + }; + } + + return { + role: message.sender, + content: message.content, + }; + }); + }; + + const sendMessage = async (content: string, fileData?: { type: string; data: string | Buffer }) => { + if (!content.trim() && !fileData) return; + + if (!user) { + const newMessage: Message = { + id: Date.now().toString(), + content, + sender: 'user', + timestamp: new Date().toLocaleTimeString(), + }; + setMessages((prev) => [...prev, newMessage, INVALID_CONFIG_MESSAGE]); + return; + } + + const userMessage: Message = { + id: Date.now().toString(), + content, + sender: 'user', + timestamp: new Date().toLocaleTimeString(), + ...(fileData?.type.startsWith('image/') && { image: fileData.data.toString() }), + }; + + setMessages((prev) => [...prev, userMessage]); + setThinking(true); + + // Get all messages for memory update + const allMessagesForMemory = formatMessagesForPrompt([...messages, userMessage]); + await updateMemories(allMessagesForMemory); + + try { + // Get only the last assistant message (if exists) and the current user message + const lastAssistantMessage = messages.filter(msg => msg.sender === 'assistant').slice(-1)[0]; + let messagesForLLM = lastAssistantMessage + ? [ + formatMessagesForPrompt([lastAssistantMessage])[0], + formatMessagesForPrompt([userMessage])[0] + ] + : [formatMessagesForPrompt([userMessage])[0]]; + + // Check if any message has image content + const hasImage = messagesForLLM.some(msg => { + if (typeof msg.content === 'object' && msg.content !== null) { + const content = msg.content as any; + return content.type === 'image_url'; + } + return false; + }); + + // For image messages, only use the text content + if (hasImage) { + messagesForLLM = [{ + role: 'user', + content: userMessage.content + }]; + } + + // Fetch relevant memories if there's an image + let relevantMemories = ''; + if (hasImage) { + try { + const searchResponse = await memoryClient.getAll({ + user_id: user, + page: 1, + page_size: 10, + }); + + relevantMemories = searchResponse.results + .map((memory: any) => `Previous context: ${memory.memory}`) + .join('\n'); + } catch (error) { + console.error('Error fetching memories:', error); + } + } + + // Add a system message with memories context if there are memories and image + if (relevantMemories.length > 0 && hasImage) { + messagesForLLM = [ + { + role: 'system', + content: `Here are some relevant details about the user:\n${relevantMemories}\n\nPlease use this context when responding to the user's message.` + }, + ...messagesForLLM + ]; + } + + console.log('Messages for LLM:', messagesForLLM); + const completion = await openai.chat.completions.create({ + model: "gpt-4", + messages: messagesForLLM.map(msg => ({ + role: msg.role, + content: msg.content + })), + stream: true, + }); + + const assistantMessageId = Date.now() + 1; + const assistantMessage: Message = { + id: assistantMessageId.toString(), + content: '', + sender: 'assistant', + timestamp: new Date().toLocaleTimeString(), + }; + + setMessages((prev) => [...prev, assistantMessage]); + + for await (const chunk of completion) { + const textPart = chunk.choices[0]?.delta?.content || ''; + assistantMessage.content += textPart; + setThinking(false); + + setMessages((prev) => + prev.map((msg) => + msg.id === assistantMessageId.toString() + ? { ...msg, content: assistantMessage.content } + : msg + ) + ); + } + } catch (error) { + console.error('Error in sendMessage:', error); + setMessages((prev) => [...prev, ERROR_MESSAGE]); + } finally { + setThinking(false); + } + }; + + return { + messages, + memories, + thinking, + sendMessage, + }; +}; \ No newline at end of file diff --git a/examples/multimodal-demo/src/hooks/useFileHandler.ts b/examples/multimodal-demo/src/hooks/useFileHandler.ts new file mode 100644 index 00000000..3353a8cf --- /dev/null +++ b/examples/multimodal-demo/src/hooks/useFileHandler.ts @@ -0,0 +1,45 @@ +import { useState } from 'react'; +import { FileInfo } from '@/types'; +import { convertToBase64, getFileBuffer } from '@/utils/fileUtils'; + +interface UseFileHandlerReturn { + selectedFile: FileInfo | null; + file: File | null; + fileData: string | Buffer | null; + setSelectedFile: (file: FileInfo | null) => void; + handleFile: (file: File) => Promise; + clearFile: () => void; +} + +export const useFileHandler = (): UseFileHandlerReturn => { + const [selectedFile, setSelectedFile] = useState(null); + const [file, setFile] = useState(null); + const [fileData, setFileData] = useState(null); + + const handleFile = async (file: File) => { + setFile(file); + + if (file.type.startsWith('image/')) { + const base64Data = await convertToBase64(file); + setFileData(base64Data); + } else if (file.type.startsWith('audio/')) { + const bufferData = await getFileBuffer(file); + setFileData(bufferData); + } + }; + + const clearFile = () => { + setSelectedFile(null); + setFile(null); + setFileData(null); + }; + + return { + selectedFile, + file, + fileData, + setSelectedFile, + handleFile, + clearFile, + }; +}; \ No newline at end of file diff --git a/examples/multimodal-demo/src/index.css b/examples/multimodal-demo/src/index.css new file mode 100644 index 00000000..405a75d5 --- /dev/null +++ b/examples/multimodal-demo/src/index.css @@ -0,0 +1,97 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem + } + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55% + } +} +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +.loader { + display: flex; + align-items: flex-end; + gap: 5px; +} + +.ball { + width: 6px; + height: 6px; + background-color: #4e4e4e; + border-radius: 50%; + animation: bounce 0.6s infinite alternate; +} + +.ball:nth-child(2) { + animation-delay: 0.2s; +} + +.ball:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes bounce { + from { + transform: translateY(0); + } + to { + transform: translateY(-4px); + } +} diff --git a/examples/multimodal-demo/src/libs/utils.ts b/examples/multimodal-demo/src/libs/utils.ts new file mode 100644 index 00000000..bd0c391d --- /dev/null +++ b/examples/multimodal-demo/src/libs/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/examples/multimodal-demo/src/main.tsx b/examples/multimodal-demo/src/main.tsx new file mode 100644 index 00000000..bef5202a --- /dev/null +++ b/examples/multimodal-demo/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/examples/multimodal-demo/src/page.tsx b/examples/multimodal-demo/src/page.tsx new file mode 100644 index 00000000..1f99e856 --- /dev/null +++ b/examples/multimodal-demo/src/page.tsx @@ -0,0 +1,14 @@ +"use client"; +import { GlobalState } from "./contexts/GlobalContext"; +import Component from "./pages/home"; + + +export default function Home() { + return ( +
+ + + +
+ ); +} diff --git a/examples/multimodal-demo/src/pages/home.tsx b/examples/multimodal-demo/src/pages/home.tsx new file mode 100644 index 00000000..f72b175e --- /dev/null +++ b/examples/multimodal-demo/src/pages/home.tsx @@ -0,0 +1,41 @@ +import { useState } from "react"; +import ApiSettingsPopup from "../components/api-settings-popup"; +import Memories from "../components/memories"; +import Header from "../components/header"; +import Messages from "../components/messages"; +import InputArea from "../components/input-area"; +import ChevronToggle from "../components/chevron-toggle"; + + +export default function Home() { + const [isMemoriesExpanded, setIsMemoriesExpanded] = useState(true); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + + return ( + <> + +
+ {/* Main Chat Area */} +
+ {/* Header */} +
+ + {/* Messages */} + + + {/* Input Area */} + +
+ + {/* Chevron Toggle */} + + + {/* Memories Sidebar */} + +
+ + ); +} diff --git a/examples/multimodal-demo/src/types.ts b/examples/multimodal-demo/src/types.ts new file mode 100644 index 00000000..770bc23f --- /dev/null +++ b/examples/multimodal-demo/src/types.ts @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export interface Memory { + id: string; + content: string; + timestamp: string; + tags: string[]; +} + +export interface Message { + id: string; + content: string; + sender: "user" | "assistant"; + timestamp: string; + image?: string; + audio?: any; +} + +export interface FileInfo { + name: string; + type: string; + size: number; +} \ No newline at end of file diff --git a/examples/multimodal-demo/src/utils/fileUtils.ts b/examples/multimodal-demo/src/utils/fileUtils.ts new file mode 100644 index 00000000..cd86f807 --- /dev/null +++ b/examples/multimodal-demo/src/utils/fileUtils.ts @@ -0,0 +1,16 @@ +import { Buffer } from 'buffer'; + +export const convertToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result as string); + reader.onerror = error => reject(error); + }); +}; + +export const getFileBuffer = async (file: File): Promise => { + const response = await fetch(URL.createObjectURL(file)); + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(arrayBuffer); +}; \ No newline at end of file diff --git a/examples/multimodal-demo/src/vite-env.d.ts b/examples/multimodal-demo/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/multimodal-demo/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/multimodal-demo/tailwind.config.js b/examples/multimodal-demo/tailwind.config.js new file mode 100644 index 00000000..15012851 --- /dev/null +++ b/examples/multimodal-demo/tailwind.config.js @@ -0,0 +1,62 @@ +// tailwind.config.js +/* eslint-env node */ + +/** @type {import('tailwindcss').Config} */ +import tailwindcssAnimate from 'tailwindcss-animate'; + +export default { + darkMode: ["class"], + content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], + theme: { + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))', + }, + }, + }, + }, + plugins: [tailwindcssAnimate], +}; diff --git a/examples/multimodal-demo/tsconfig.app.json b/examples/multimodal-demo/tsconfig.app.json new file mode 100644 index 00000000..6d0c89af --- /dev/null +++ b/examples/multimodal-demo/tsconfig.app.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/examples/multimodal-demo/tsconfig.json b/examples/multimodal-demo/tsconfig.json new file mode 100644 index 00000000..fec8c8e5 --- /dev/null +++ b/examples/multimodal-demo/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/examples/multimodal-demo/tsconfig.node.json b/examples/multimodal-demo/tsconfig.node.json new file mode 100644 index 00000000..abcd7f0d --- /dev/null +++ b/examples/multimodal-demo/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/multimodal-demo/useChat.ts b/examples/multimodal-demo/useChat.ts new file mode 100644 index 00000000..4f3f37c1 --- /dev/null +++ b/examples/multimodal-demo/useChat.ts @@ -0,0 +1,223 @@ +import { useState } from 'react'; +import { MemoryClient, Memory as Mem0Memory } from 'mem0ai'; +import { OpenAI } from 'openai'; +import { Message, Memory } from '@/types'; +import { WELCOME_MESSAGE, INVALID_CONFIG_MESSAGE, ERROR_MESSAGE, Provider } from '@/constants/messages'; + +interface UseChatProps { + user: string; + mem0ApiKey: string; + openaiApiKey: string; + provider: Provider; +} + +interface UseChatReturn { + messages: Message[]; + memories: Memory[]; + thinking: boolean; + sendMessage: (content: string, fileData?: { type: string; data: string | Buffer }) => Promise; +} + +type MessageContent = string | { + type: 'image_url'; + image_url: { + url: string; + }; +}; + +interface PromptMessage { + role: string; + content: MessageContent; +} + +export const useChat = ({ user, mem0ApiKey, openaiApiKey }: UseChatProps): UseChatReturn => { + const [messages, setMessages] = useState([WELCOME_MESSAGE]); + const [memories, setMemories] = useState(); + const [thinking, setThinking] = useState(false); + + const openai = new OpenAI({ apiKey: openaiApiKey, dangerouslyAllowBrowser: true}); + + const updateMemories = async (messages: PromptMessage[]) => { + const memoryClient = new MemoryClient({ apiKey: mem0ApiKey || '' }); + try { + await memoryClient.add(messages, { + user_id: user, + }); + + const response = await memoryClient.getAll({ + user_id: user, + }); + + const newMemories = response.map((memory: Mem0Memory) => ({ + id: memory.id || '', + content: memory.memory || '', + timestamp: String(memory.updated_at) || '', + tags: memory.categories || [], + })); + setMemories(newMemories); + } catch (error) { + console.error('Error in updateMemories:', error); + } + }; + + const formatMessagesForPrompt = (messages: Message[]): PromptMessage[] => { + return messages.map((message) => { + if (message.image) { + return { + role: message.sender, + content: { + type: 'image_url', + image_url: { + url: message.image + } + }, + }; + } + + return { + role: message.sender, + content: message.content, + }; + }); + }; + + const sendMessage = async (content: string, fileData?: { type: string; data: string | Buffer }) => { + if (!content.trim() && !fileData) return; + + const memoryClient = new MemoryClient({ apiKey: mem0ApiKey || '' }); + + if (!user) { + const newMessage: Message = { + id: Date.now().toString(), + content, + sender: 'user', + timestamp: new Date().toLocaleTimeString(), + }; + setMessages((prev) => [...prev, newMessage, INVALID_CONFIG_MESSAGE]); + return; + } + + const userMessage: Message = { + id: Date.now().toString(), + content, + sender: 'user', + timestamp: new Date().toLocaleTimeString(), + ...(fileData?.type.startsWith('image/') && { image: fileData.data.toString() }), + }; + + setMessages((prev) => [...prev, userMessage]); + setThinking(true); + + // Get all messages for memory update + const allMessagesForMemory = formatMessagesForPrompt([...messages, userMessage]); + await updateMemories(allMessagesForMemory); + + try { + // Get only the last assistant message (if exists) and the current user message + const lastAssistantMessage = messages.filter(msg => msg.sender === 'assistant').slice(-1)[0]; + let messagesForLLM = lastAssistantMessage + ? [ + formatMessagesForPrompt([lastAssistantMessage])[0], + formatMessagesForPrompt([userMessage])[0] + ] + : [formatMessagesForPrompt([userMessage])[0]]; + + // Check if any message has image content + const hasImage = messagesForLLM.some(msg => { + if (typeof msg.content === 'object' && msg.content !== null) { + const content = msg.content as MessageContent; + return typeof content === 'object' && content !== null && 'type' in content && content.type === 'image_url'; + } + return false; + }); + + // For image messages, only use the text content + if (hasImage) { + messagesForLLM = [ + ...messagesForLLM, + { + role: 'user', + content: userMessage.content + } + ]; + } + + // Fetch relevant memories if there's an image + let relevantMemories = ''; + try { + const searchResponse = await memoryClient.getAll({ + user_id: user + }); + + relevantMemories = searchResponse + .map((memory: Mem0Memory) => `Previous context: ${memory.memory}`) + .join('\n'); + } catch (error) { + console.error('Error fetching memories:', error); + } + + // Add a system message with memories context if there are memories and image + if (relevantMemories.length > 0 && hasImage) { + messagesForLLM = [ + { + role: 'system', + content: `Here are some relevant details about the user:\n${relevantMemories}\n\nPlease use this context when responding to the user's message.` + }, + ...messagesForLLM + ]; + } + + const generateRandomId = () => { + return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + } + + const completion = await openai.chat.completions.create({ + model: "gpt-4o-mini", + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + messages: messagesForLLM.map(msg => ({ + role: msg.role === 'user' ? 'user' : 'assistant', + content: typeof msg.content === 'object' && msg.content !== null ? [msg.content] : msg.content, + name: generateRandomId(), + })), + stream: true, + }); + + const assistantMessageId = Date.now() + 1; + const assistantMessage: Message = { + id: assistantMessageId.toString(), + content: '', + sender: 'assistant', + timestamp: new Date().toLocaleTimeString(), + }; + + setMessages((prev) => [...prev, assistantMessage]); + + for await (const chunk of completion) { + const textPart = chunk.choices[0]?.delta?.content || ''; + assistantMessage.content += textPart; + setThinking(false); + + setMessages((prev) => + prev.map((msg) => + msg.id === assistantMessageId.toString() + ? { ...msg, content: assistantMessage.content } + : msg + ) + ); + } + } catch (error) { + console.error('Error in sendMessage:', error); + setMessages((prev) => [...prev, ERROR_MESSAGE]); + } finally { + setThinking(false); + } + }; + + return { + messages, + memories: memories || [], + thinking, + sendMessage, + }; +}; \ No newline at end of file diff --git a/examples/multimodal-demo/vite.config.ts b/examples/multimodal-demo/vite.config.ts new file mode 100644 index 00000000..a761a870 --- /dev/null +++ b/examples/multimodal-demo/vite.config.ts @@ -0,0 +1,13 @@ +import path from "path" +import react from "@vitejs/plugin-react" +import { defineConfig } from "vite" + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + buffer: 'buffer' + }, + }, +})