Update AI SDK Example (#2271)
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mem0/vercel-ai-provider": "^0.0.7",
|
"@mem0/vercel-ai-provider": "0.0.12",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-icons": "^1.3.1",
|
"@radix-ui/react-icons": "^1.3.1",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"ai": "^3.4.31",
|
"ai": "4.1.42",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Label } from "@/components/ui/label"
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||||
import GlobalContext from '@/contexts/GlobalContext'
|
import GlobalContext from '@/contexts/GlobalContext'
|
||||||
|
import { Provider } from '@/constants/messages'
|
||||||
export default function ApiSettingsPopup(props: { isOpen: boolean, setIsOpen: Dispatch<SetStateAction<boolean>> }) {
|
export default function ApiSettingsPopup(props: { isOpen: boolean, setIsOpen: Dispatch<SetStateAction<boolean>> }) {
|
||||||
const {isOpen, setIsOpen} = props
|
const {isOpen, setIsOpen} = props
|
||||||
const [mem0ApiKey, setMem0ApiKey] = useState('')
|
const [mem0ApiKey, setMem0ApiKey] = useState('')
|
||||||
@@ -15,7 +15,7 @@ export default function ApiSettingsPopup(props: { isOpen: boolean, setIsOpen: Di
|
|||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
// Here you would typically save the settings to your backend or local storage
|
// Here you would typically save the settings to your backend or local storage
|
||||||
selectorHandler(mem0ApiKey, providerApiKey, provider);
|
selectorHandler(mem0ApiKey, providerApiKey, provider as Provider);
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
examples/vercel-ai-sdk-chat-app/src/constants/messages.ts
Normal file
31
examples/vercel-ai-sdk-chat-app/src/constants/messages.ts
Normal file
@@ -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;
|
||||||
@@ -1,281 +1,84 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { createContext, useEffect, useState } from "react";
|
import { createContext } from 'react';
|
||||||
import { createMem0, searchMemories } from "@mem0/vercel-ai-provider";
|
import { Message, Memory, FileInfo } from '@/types';
|
||||||
import { LanguageModelV1Prompt, streamText } from "ai";
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { Message, Memory, FileInfo } from "@/types";
|
import { useChat } from '@/hooks/useChat';
|
||||||
import { Buffer } from 'buffer';
|
import { useFileHandler } from '@/hooks/useFileHandler';
|
||||||
|
import { Provider } from '@/constants/messages';
|
||||||
|
|
||||||
const GlobalContext = createContext<any>({});
|
interface GlobalContextType {
|
||||||
|
selectedUser: string;
|
||||||
const WelcomeMessage: Message = {
|
selectUserHandler: (user: string) => void;
|
||||||
id: "1",
|
clearUserHandler: () => void;
|
||||||
content:
|
messages: Message[];
|
||||||
"👋 Hi there! I'm your personal assistant. How can I help you today? 😊",
|
memories: Memory[];
|
||||||
sender: "assistant",
|
handleSend: (content: string) => Promise<void>;
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
thinking: boolean;
|
||||||
};
|
selectedMem0Key: string;
|
||||||
|
selectedOpenAIKey: string;
|
||||||
const InvalidConfigMessage: Message = {
|
selectedProvider: Provider;
|
||||||
id: "2",
|
selectorHandler: (mem0: string, openai: string, provider: Provider) => void;
|
||||||
content:
|
clearConfiguration: () => void;
|
||||||
"Invalid configuration. Please check your API keys, and add a user and try again.",
|
selectedFile: FileInfo | null;
|
||||||
sender: "assistant",
|
setSelectedFile: (file: FileInfo | null) => void;
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
file: File | null;
|
||||||
};
|
setFile: (file: File | null) => void;
|
||||||
|
|
||||||
const SomethingWentWrongMessage: Message = {
|
|
||||||
id: "3",
|
|
||||||
content: "Something went wrong. Please try again.",
|
|
||||||
sender: "assistant",
|
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const models = {
|
|
||||||
"openai": "gpt-4o",
|
|
||||||
"anthropic": "claude-3-haiku-20240307",
|
|
||||||
"cohere": "command-r-plus",
|
|
||||||
"groq": "gemma2-9b-it"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getModel = (provider: string) => {
|
const GlobalContext = createContext<GlobalContextType>({} as GlobalContextType);
|
||||||
switch (provider) {
|
|
||||||
case "openai":
|
|
||||||
return models.openai;
|
|
||||||
case "anthropic":
|
|
||||||
return models.anthropic;
|
|
||||||
case "cohere":
|
|
||||||
return models.cohere;
|
|
||||||
case "groq":
|
|
||||||
return models.groq;
|
|
||||||
default:
|
|
||||||
return models.openai;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const GlobalState = (props: any) => {
|
const GlobalState = (props: { children: React.ReactNode }) => {
|
||||||
const [memories, setMemories] = useState<Memory[]>([]);
|
const {
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
|
||||||
const [selectedUser, setSelectedUser] = useState<string>("");
|
|
||||||
const [thinking, setThinking] = useState<boolean>(false);
|
|
||||||
const [selectedOpenAIKey, setSelectedOpenAIKey] = useState<string>("");
|
|
||||||
const [selectedMem0Key, setSelectedMem0Key] = useState<string>("");
|
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string>("openai");
|
|
||||||
const [selectedFile, setSelectedFile] = useState<FileInfo | null>(null)
|
|
||||||
const [file, setFile] = useState<any>(null)
|
|
||||||
|
|
||||||
const mem0 = createMem0({
|
|
||||||
provider: selectedProvider,
|
|
||||||
mem0ApiKey: selectedMem0Key,
|
mem0ApiKey: selectedMem0Key,
|
||||||
apiKey: selectedOpenAIKey,
|
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 clearConfiguration = () => {
|
const handleSend = async (content: string) => {
|
||||||
localStorage.removeItem("mem0ApiKey");
|
|
||||||
localStorage.removeItem("openaiApiKey");
|
|
||||||
localStorage.removeItem("provider");
|
|
||||||
setSelectedMem0Key("");
|
|
||||||
setSelectedOpenAIKey("");
|
|
||||||
setSelectedProvider("openai");
|
|
||||||
setSelectedUser("");
|
|
||||||
setMessages([WelcomeMessage]);
|
|
||||||
setMemories([]);
|
|
||||||
setFile(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectorHandler = (mem0: string, openai: string, provider: string) => {
|
|
||||||
setSelectedMem0Key(mem0);
|
|
||||||
setSelectedOpenAIKey(openai);
|
|
||||||
setSelectedProvider(provider);
|
|
||||||
localStorage.setItem("mem0ApiKey", mem0);
|
|
||||||
localStorage.setItem("openaiApiKey", openai);
|
|
||||||
localStorage.setItem("provider", provider);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mem0 = localStorage.getItem("mem0ApiKey");
|
|
||||||
const openai = localStorage.getItem("openaiApiKey");
|
|
||||||
const provider = localStorage.getItem("provider");
|
|
||||||
const user = localStorage.getItem("user");
|
|
||||||
if (mem0 && openai && provider) {
|
|
||||||
selectorHandler(mem0, openai, provider);
|
|
||||||
}
|
|
||||||
if (user) {
|
|
||||||
setSelectedUser(user);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selectUserHandler = (user: string) => {
|
|
||||||
setSelectedUser(user);
|
|
||||||
localStorage.setItem("user", user);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearUserHandler = () => {
|
|
||||||
setSelectedUser("");
|
|
||||||
setMemories([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMemories = async (messages: LanguageModelV1Prompt) => {
|
|
||||||
try {
|
|
||||||
const smemories = await searchMemories(messages, {
|
|
||||||
user_id: selectedUser || "",
|
|
||||||
mem0ApiKey: selectedMem0Key,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newMemories = smemories.map((memory: any) => ({
|
|
||||||
id: memory.id,
|
|
||||||
content: memory.memory,
|
|
||||||
timestamp: memory.updated_at,
|
|
||||||
tags: memory.categories,
|
|
||||||
}));
|
|
||||||
setMemories(newMemories);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in getMemories:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSend = async (inputValue: string) => {
|
|
||||||
if (!inputValue.trim() && !file) return;
|
|
||||||
if (!selectedUser) {
|
|
||||||
const newMessage: Message = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
content: inputValue,
|
|
||||||
sender: "user",
|
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
|
||||||
};
|
|
||||||
setMessages((prev) => [...prev, newMessage, InvalidConfigMessage]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userMessage: Message = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
content: inputValue,
|
|
||||||
sender: "user",
|
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let fileData;
|
|
||||||
if (file) {
|
if (file) {
|
||||||
if (file.type.startsWith("image/")) {
|
await sendMessage(content, {
|
||||||
// Convert image to Base64
|
type: file.type,
|
||||||
fileData = await convertToBase64(file);
|
data: fileData!,
|
||||||
userMessage.image = fileData;
|
|
||||||
} else if (file.type.startsWith("audio/")) {
|
|
||||||
// Convert audio to ArrayBuffer
|
|
||||||
fileData = await getFileBuffer(file);
|
|
||||||
userMessage.audio = fileData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the state with the new user message
|
|
||||||
setMessages((prev) => [...prev, userMessage]);
|
|
||||||
setThinking(true);
|
|
||||||
|
|
||||||
// Transform messages into the required format
|
|
||||||
const messagesForPrompt: LanguageModelV1Prompt = [];
|
|
||||||
messages.map((message) => {
|
|
||||||
const messageContent: any = {
|
|
||||||
role: message.sender,
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: message.content,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
if (message.image) {
|
|
||||||
messageContent.content.push({
|
|
||||||
type: "image",
|
|
||||||
image: message.image,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (message.audio) {
|
|
||||||
messageContent.content.push({
|
|
||||||
type: 'file',
|
|
||||||
mimeType: 'audio/mpeg',
|
|
||||||
data: message.audio,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if(!message.audio) messagesForPrompt.push(messageContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
const newMessage: any = {
|
|
||||||
role: "user",
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: inputValue,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
if (file) {
|
|
||||||
if (file.type.startsWith("image/")) {
|
|
||||||
newMessage.content.push({
|
|
||||||
type: "image",
|
|
||||||
image: userMessage.image,
|
|
||||||
});
|
|
||||||
} else if (file.type.startsWith("audio/")) {
|
|
||||||
newMessage.content.push({
|
|
||||||
type: 'file',
|
|
||||||
mimeType: 'audio/mpeg',
|
|
||||||
data: userMessage.audio,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesForPrompt.push(newMessage);
|
|
||||||
getMemories(messagesForPrompt);
|
|
||||||
|
|
||||||
setFile(null);
|
|
||||||
setSelectedFile(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { textStream } = await streamText({
|
|
||||||
model: mem0(getModel(selectedProvider), {
|
|
||||||
user_id: selectedUser || "",
|
|
||||||
}),
|
|
||||||
messages: messagesForPrompt,
|
|
||||||
});
|
});
|
||||||
|
clearFile();
|
||||||
const assistantMessageId = Date.now() + 1;
|
} else {
|
||||||
const assistantMessage: Message = {
|
await sendMessage(content);
|
||||||
id: assistantMessageId.toString(),
|
|
||||||
content: "",
|
|
||||||
sender: "assistant",
|
|
||||||
timestamp: new Date().toLocaleTimeString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
setMessages((prev) => [...prev, assistantMessage]);
|
|
||||||
|
|
||||||
// Stream the text part by part
|
|
||||||
for await (const textPart of textStream) {
|
|
||||||
assistantMessage.content += textPart;
|
|
||||||
setThinking(false);
|
|
||||||
setFile(null);
|
|
||||||
setSelectedFile(null);
|
|
||||||
|
|
||||||
setMessages((prev) =>
|
|
||||||
prev.map((msg) =>
|
|
||||||
msg.id === assistantMessageId.toString()
|
|
||||||
? { ...msg, content: assistantMessage.content }
|
|
||||||
: msg
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setThinking(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in handleSend:", error);
|
|
||||||
setMessages((prev) => [...prev, SomethingWentWrongMessage]);
|
|
||||||
setThinking(false);
|
|
||||||
setFile(null);
|
|
||||||
setSelectedFile(null);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const setFile = async (newFile: File | null) => {
|
||||||
setMessages([WelcomeMessage]);
|
if (newFile) {
|
||||||
}, []);
|
await handleFile(newFile);
|
||||||
|
} else {
|
||||||
|
clearFile();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlobalContext.Provider
|
<GlobalContext.Provider
|
||||||
@@ -295,7 +98,7 @@ const GlobalState = (props: any) => {
|
|||||||
selectedFile,
|
selectedFile,
|
||||||
setSelectedFile,
|
setSelectedFile,
|
||||||
file,
|
file,
|
||||||
setFile
|
setFile,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
@@ -304,21 +107,4 @@ const GlobalState = (props: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default GlobalContext;
|
export default GlobalContext;
|
||||||
export { GlobalState };
|
export { GlobalState };
|
||||||
|
|
||||||
|
|
||||||
const convertToBase64 = (file: File): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
reader.onload = () => resolve(reader.result as string); // Resolve with Base64 string
|
|
||||||
reader.onerror = error => reject(error); // Reject on error
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getFileBuffer(file: any) {
|
|
||||||
const response = await fetch(file);
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
73
examples/vercel-ai-sdk-chat-app/src/hooks/useAuth.ts
Normal file
73
examples/vercel-ai-sdk-chat-app/src/hooks/useAuth.ts
Normal file
@@ -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<string>('');
|
||||||
|
const [openaiApiKey, setOpenaiApiKey] = useState<string>('');
|
||||||
|
const [provider, setProvider] = useState<Provider>('openai');
|
||||||
|
const [user, setUser] = useState<string>('');
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
169
examples/vercel-ai-sdk-chat-app/src/hooks/useChat.ts
Normal file
169
examples/vercel-ai-sdk-chat-app/src/hooks/useChat.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { createMem0, getMemories } from '@mem0/vercel-ai-provider';
|
||||||
|
import { LanguageModelV1Prompt, streamText } from 'ai';
|
||||||
|
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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MemoryResponse {
|
||||||
|
id: string;
|
||||||
|
memory: string;
|
||||||
|
updated_at: string;
|
||||||
|
categories: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageContent =
|
||||||
|
| { type: 'text'; text: string }
|
||||||
|
| { type: 'image'; image: string }
|
||||||
|
| { type: 'file'; mimeType: string; data: Buffer };
|
||||||
|
|
||||||
|
interface PromptMessage {
|
||||||
|
role: string;
|
||||||
|
content: MessageContent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useChat = ({ user, mem0ApiKey, openaiApiKey, provider }: UseChatProps): UseChatReturn => {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([WELCOME_MESSAGE]);
|
||||||
|
const [memories, setMemories] = useState<Memory[]>([]);
|
||||||
|
const [thinking, setThinking] = useState(false);
|
||||||
|
|
||||||
|
const mem0 = createMem0({
|
||||||
|
provider,
|
||||||
|
mem0ApiKey,
|
||||||
|
apiKey: openaiApiKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMemories = async (messages: LanguageModelV1Prompt) => {
|
||||||
|
try {
|
||||||
|
const fetchedMemories = await getMemories(messages, {
|
||||||
|
user_id: user,
|
||||||
|
mem0ApiKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newMemories = fetchedMemories.map((memory: MemoryResponse) => ({
|
||||||
|
id: memory.id,
|
||||||
|
content: memory.memory,
|
||||||
|
timestamp: memory.updated_at,
|
||||||
|
tags: memory.categories,
|
||||||
|
}));
|
||||||
|
setMemories(newMemories);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getMemories:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatMessagesForPrompt = (messages: Message[]): PromptMessage[] => {
|
||||||
|
return messages.map((message) => {
|
||||||
|
const messageContent: MessageContent[] = [
|
||||||
|
{ type: 'text', text: message.content }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (message.image) {
|
||||||
|
messageContent.push({
|
||||||
|
type: 'image',
|
||||||
|
image: message.image,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.audio) {
|
||||||
|
messageContent.push({
|
||||||
|
type: 'file',
|
||||||
|
mimeType: 'audio/mpeg',
|
||||||
|
data: message.audio as Buffer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: message.sender,
|
||||||
|
content: messageContent,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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() }),
|
||||||
|
...(fileData?.type.startsWith('audio/') && { audio: fileData.data as Buffer }),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, userMessage]);
|
||||||
|
setThinking(true);
|
||||||
|
|
||||||
|
const messagesForPrompt = formatMessagesForPrompt([...messages, userMessage]);
|
||||||
|
await updateMemories(messagesForPrompt as LanguageModelV1Prompt);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { textStream } = await streamText({
|
||||||
|
model: mem0(AI_MODELS[provider], {
|
||||||
|
user_id: user,
|
||||||
|
}),
|
||||||
|
messages: messagesForPrompt as LanguageModelV1Prompt,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 textPart of textStream) {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
45
examples/vercel-ai-sdk-chat-app/src/hooks/useFileHandler.ts
Normal file
45
examples/vercel-ai-sdk-chat-app/src/hooks/useFileHandler.ts
Normal file
@@ -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<void>;
|
||||||
|
clearFile: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFileHandler = (): UseFileHandlerReturn => {
|
||||||
|
const [selectedFile, setSelectedFile] = useState<FileInfo | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [fileData, setFileData] = useState<string | Buffer | null>(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,
|
||||||
|
};
|
||||||
|
};
|
||||||
16
examples/vercel-ai-sdk-chat-app/src/utils/fileUtils.ts
Normal file
16
examples/vercel-ai-sdk-chat-app/src/utils/fileUtils.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Buffer } from 'buffer';
|
||||||
|
|
||||||
|
export const convertToBase64 = (file: File): Promise<string> => {
|
||||||
|
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<Buffer> => {
|
||||||
|
const response = await fetch(URL.createObjectURL(file));
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
return Buffer.from(arrayBuffer);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user