diff --git a/apps/shinkai-app/src/components/ChatMessages.tsx b/apps/shinkai-app/src/components/ChatMessages.tsx new file mode 100644 index 000000000..b4da08957 --- /dev/null +++ b/apps/shinkai-app/src/components/ChatMessages.tsx @@ -0,0 +1,191 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { + getLastMessagesFromInbox, + getLastUnreadMessagesFromInbox, +} from '@shinkai/shinkai-message-ts/api/methods'; +import { ShinkaiMessage } from '@shinkai/shinkai-message-ts/models'; +import { IonList, IonItem, IonButton } from '@ionic/react'; +import Avatar from '../components/ui/Avatar'; +import { cn } from '../theme/lib/utils'; +import { IonContentCustom } from './ui/Layout'; +import { calculateMessageHash } from '@shinkai/shinkai-message-ts/utils/shinkai_message_handler'; +import { RootState } from '../store'; +import { receiveLastMessagesFromInbox } from '../store/actions'; + +interface ChatMessagesProps { + deserializedId: string; +} + +const ChatMessages: React.FC = ({ deserializedId }) => { + console.log('Loading ChatMessages.tsx'); + const dispatch = useDispatch(); + const setupDetailsState = useSelector( + (state: RootState) => state.setupDetails + ); + const reduxMessages = useSelector( + (state: RootState) => state.messages.inboxes[deserializedId] + ); + + const [lastKey, setLastKey] = useState(undefined); + const [mostRecentKey, setMostRecentKey] = useState( + undefined + ); + const [prevMessagesLength, setPrevMessagesLength] = useState(0); + const [hasMoreMessages, setHasMoreMessages] = useState(true); + const [messages, setMessages] = useState([]); + + useEffect(() => { + console.log('deserializedId:', deserializedId); + getLastMessagesFromInbox( + deserializedId, + 10, + lastKey, + setupDetailsState + ).then((messages) => { + dispatch(receiveLastMessagesFromInbox(deserializedId, messages)); + }); + }, [dispatch, setupDetailsState, deserializedId, lastKey]); + + useEffect(() => { + const interval = setInterval(() => { + getLastUnreadMessagesFromInbox( + deserializedId, + 10, + mostRecentKey, + setupDetailsState + ).then((messages) => { + dispatch(receiveLastMessagesFromInbox(deserializedId, messages)); + }); + }, 5000); // 2000 milliseconds = 2 seconds + return () => clearInterval(interval); + }, [ + dispatch, + deserializedId, + mostRecentKey, + setupDetailsState, + ]); + + useEffect(() => { + if (reduxMessages && reduxMessages.length > 0) { + // console.log("Redux Messages:", reduxMessages); + const lastMessage = reduxMessages[reduxMessages.length - 1]; + console.log('Last Message:', lastMessage); + const timeKey = lastMessage.external_metadata.scheduled_time; + const hashKey = calculateMessageHash(lastMessage); + const lastMessageKey = `${timeKey}:::${hashKey}`; + setLastKey(lastMessageKey); + + const mostRecentMessage = reduxMessages[0]; + const mostRecentTimeKey = + mostRecentMessage.external_metadata.scheduled_time; + const mostRecentHashKey = calculateMessageHash(mostRecentMessage); + const mostRecentMessageKey = `${mostRecentTimeKey}:::${mostRecentHashKey}`; + setMostRecentKey(mostRecentMessageKey); + + setMessages(reduxMessages); + + if (reduxMessages.length - prevMessagesLength < 10) { + setHasMoreMessages(false); + } + setPrevMessagesLength(reduxMessages.length); + } + }, [reduxMessages, prevMessagesLength]); + + const extractContent = (messageBody: any) => { + // TODO: extend it so it can be re-used by JobChat or normal Chat + if (messageBody && 'unencrypted' in messageBody) { + if ('unencrypted' in messageBody.unencrypted.message_data) { + return JSON.parse( + messageBody.unencrypted.message_data.unencrypted.message_raw_content + ).content; + } else { + return JSON.parse( + messageBody.unencrypted.message_data.encrypted.content + ).content; + } + } else if (messageBody?.encrypted) { + return JSON.parse(messageBody.encrypted.content).content; + } + return ''; + }; + + return ( + +
+ {hasMoreMessages && ( + + dispatch( + getLastMessagesFromInbox( + deserializedId, + 10, + lastKey, + setupDetailsState + // true + ) + ) + } + > + Load More + + )} + + {messages && + messages.slice().map((message, index) => { + const { shinkai_identity, profile, registration_name } = + setupDetailsState; + + const localIdentity = `${profile}/device/${registration_name}`; + // console.log("Message:", message); + let isLocalMessage = false; + if (message.body && 'unencrypted' in message.body) { + isLocalMessage = + message.body.unencrypted.internal_metadata + .sender_subidentity === localIdentity; + } + + return ( + +
+ + +

{extractContent(message.body)}

+ {message?.external_metadata?.scheduled_time && ( + + {new Date( + message.external_metadata.scheduled_time + ).toLocaleString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} + + )} +
+
+ ); + })} +
+
+
+ ); +}; + +export default ChatMessages; diff --git a/apps/shinkai-app/src/features/chat/chatSlice.ts b/apps/shinkai-app/src/features/chat/chatSlice.ts deleted file mode 100644 index 5f860122e..000000000 --- a/apps/shinkai-app/src/features/chat/chatSlice.ts +++ /dev/null @@ -1,27 +0,0 @@ -// src/features/chat/chatSlice.ts -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { RootState } from '../../store/main'; - -interface ChatState { - messages: any[]; // Replace any with the type of your messages -} - -const initialState: ChatState = { - messages: [], -}; - -export const chatSlice = createSlice({ - name: 'chat', - initialState, - reducers: { - addMessage: (state, action: PayloadAction) => { // Replace any with the type of your messages - state.messages.push(action.payload); - }, - }, -}); - -export const { addMessage } = chatSlice.actions; - -export const selectMessages = (state: RootState) => state.chat.messages; - -export default chatSlice.reducer; diff --git a/apps/shinkai-app/src/hooks/usetSetup.ts b/apps/shinkai-app/src/hooks/usetSetup.ts index c09fa511f..26f6ff449 100644 --- a/apps/shinkai-app/src/hooks/usetSetup.ts +++ b/apps/shinkai-app/src/hooks/usetSetup.ts @@ -1,14 +1,17 @@ // hooks/useSetup.ts import { useEffect } from "react"; -import { useSelector } from "react-redux"; +import { shallowEqual, useSelector } from "react-redux"; import { RootState } from "../store"; import { ApiConfig } from "@shinkai/shinkai-message-ts/api"; export const useSetup = () => { - const { setupDetailsState } = useSelector((state: RootState) => state); + const setupDetails = useSelector( + (state: RootState) => state.setupDetails, + shallowEqual + ); useEffect(() => { - console.log("Redux State:", setupDetailsState); - ApiConfig.getInstance().setEndpoint(setupDetailsState.node_address); - }, [setupDetailsState]); -}; \ No newline at end of file + console.log("Redux State:", setupDetails); + ApiConfig.getInstance().setEndpoint(setupDetails.node_address); + }, [setupDetails]); +}; diff --git a/apps/shinkai-app/src/pages/AddAgent.tsx b/apps/shinkai-app/src/pages/AddAgent.tsx index c3aaf652f..bc1ba7a2e 100644 --- a/apps/shinkai-app/src/pages/AddAgent.tsx +++ b/apps/shinkai-app/src/pages/AddAgent.tsx @@ -23,18 +23,18 @@ import { import { useEffect, useState } from "react"; import { IonContentCustom, IonHeaderCustom } from "../components/ui/Layout"; import Button from "../components/ui/Button"; -import Input from "../components/ui/Input"; import { useDispatch, useSelector } from "react-redux"; import { RootState } from "../store"; import { SerializedAgent, AgentAPIModel } from "@shinkai/shinkai-message-ts/models"; import { addAgent } from "@shinkai/shinkai-message-ts/api"; import { useSetup } from "../hooks/usetSetup"; +import { useHistory } from 'react-router-dom'; const AddAgent: React.FC = () => { useSetup(); const dispatch = useDispatch(); const setupDetailsState = useSelector( - (state: RootState) => state.setupDetailsState + (state: RootState) => state.setupDetails ); const [agent, setAgent] = useState>({ perform_locally: false, @@ -70,7 +70,7 @@ const AddAgent: React.FC = () => { }); }; - const handleSubmit = () => { + const handleSubmit = async () => { const { shinkai_identity, profile } = setupDetailsState; const node_name = shinkai_identity; @@ -82,13 +82,18 @@ const AddAgent: React.FC = () => { } console.log("Submitting agent:", agent); - - addAgent( + + const resp = await addAgent( profile, node_name, agent as SerializedAgent, setupDetailsState ); + if (resp) { + // TODO: show a success toast + // eslint-disable-next-line no-restricted-globals + history.back(); + } }; return ( diff --git a/apps/shinkai-app/src/pages/AdminCommands.tsx b/apps/shinkai-app/src/pages/AdminCommands.tsx index 361d9c350..c232891a0 100644 --- a/apps/shinkai-app/src/pages/AdminCommands.tsx +++ b/apps/shinkai-app/src/pages/AdminCommands.tsx @@ -24,7 +24,7 @@ import { useSetup } from "../hooks/usetSetup"; const AdminCommands: React.FC = () => { useSetup(); const setupDetailsState = useSelector( - (state: RootState) => state.setupDetailsState + (state: RootState) => state.setupDetails ); const [showCodeRegistrationActionSheet, setShowCodeRegistrationActionSheet] = useState(false); @@ -36,7 +36,7 @@ const AdminCommands: React.FC = () => { const [profileName, setProfileName] = useState(""); const dispatch = useDispatch(); const registrationCode = useSelector( - (state: RootState) => state.registrationCode + (state: RootState) => state.other.registrationCode ); const commands = [ "Get Peers", diff --git a/apps/shinkai-app/src/pages/Chat.tsx b/apps/shinkai-app/src/pages/Chat.tsx index e1c8410f8..6dae6e402 100644 --- a/apps/shinkai-app/src/pages/Chat.tsx +++ b/apps/shinkai-app/src/pages/Chat.tsx @@ -21,7 +21,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { getLastMessagesFromInbox, - createChatWithMessage, sendTextMessageWithInbox, } from '@shinkai/shinkai-message-ts/api'; import { RootState } from '../store'; @@ -42,6 +41,7 @@ import { IonHeaderCustom, } from '../components/ui/Layout'; import { addMessageToInbox, receiveLastMessagesFromInbox, receiveLoadMoreMessagesFromInbox } from '../store/actions'; +import { RECEIVE_LAST_MESSAGES_FROM_INBOX } from '../store/types'; const parseDate = (dateString: string) => { return new Date(dateString); @@ -53,7 +53,7 @@ const Chat: React.FC = () => { const dispatch = useDispatch(); const setupDetailsState = useSelector( - (state: RootState) => state.setupDetailsState + (state: RootState) => state.setupDetails ); const { id } = useParams<{ id: string }>(); @@ -64,7 +64,7 @@ const Chat: React.FC = () => { const [prevMessagesLength, setPrevMessagesLength] = useState(0); const reduxMessages = useSelector( - (state: RootState) => state.inboxes[deserializedId] + (state: RootState) => state.messages.inboxes[deserializedId] ); const [messages, setMessages] = useState([]); @@ -78,9 +78,9 @@ const Chat: React.FC = () => { console.log('deserializedId:', deserializedId); getLastMessagesFromInbox(deserializedId, 10, lastKey, setupDetailsState).then((messages) => { console.log("receiveLastMessagesFromInbox Response:", messages); - dispatch(receiveLastMessagesFromInbox(deserializedId, messages)); + dispatch({ type: RECEIVE_LAST_MESSAGES_FROM_INBOX, payload: messages }); }); - }, [id, dispatch, setupDetailsState]); + }, [id, dispatch, setupDetailsState, deserializedId, lastKey]); useEffect(() => { if (reduxMessages && reduxMessages.length > 0) { diff --git a/apps/shinkai-app/src/pages/Connect.tsx b/apps/shinkai-app/src/pages/Connect.tsx index f6547b787..5b3ea4cd0 100644 --- a/apps/shinkai-app/src/pages/Connect.tsx +++ b/apps/shinkai-app/src/pages/Connect.tsx @@ -21,14 +21,11 @@ import { generateSignatureKeys, } from "@shinkai/shinkai-message-ts/utils"; import { QRSetupData } from "../models/QRSetupData"; -import { SetupDetailsState } from "../store/reducers"; -import { InputCustomEvent } from "@ionic/core/dist/types/components/input/input-interface"; -import { cn } from "../theme/lib/utils"; import Button from "../components/ui/Button"; -import { IonHeaderCustom } from "../components/ui/Layout"; import Input from "../components/ui/Input"; import { scan, cloudUpload, checkmarkSharp } from "ionicons/icons"; import { useRegistrationCode } from "../store/actions"; +import { SetupDetailsState } from "../store/reducers/setupDetailsReducer"; export type MergedSetupType = SetupDetailsState & QRSetupData; @@ -58,7 +55,7 @@ const Connect: React.FC = () => { const [error, setError] = useState(null); const dispatch = useDispatch(); const history = useHistory(); - const errorFromState = useSelector((state: RootState) => state.error); + const errorFromState = useSelector((state: RootState) => state.other.error); // Generate keys when the component mounts useEffect(() => { diff --git a/apps/shinkai-app/src/pages/CreateChat.tsx b/apps/shinkai-app/src/pages/CreateChat.tsx index dfb41e4d5..89335effd 100644 --- a/apps/shinkai-app/src/pages/CreateChat.tsx +++ b/apps/shinkai-app/src/pages/CreateChat.tsx @@ -20,18 +20,18 @@ import { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createChatWithMessage } from '@shinkai/shinkai-message-ts/api'; import { useSetup } from '../hooks/usetSetup'; -import { RootState } from '../store/reducers'; import { useHistory } from 'react-router-dom'; import { History } from 'history'; import { IonContentCustom, IonHeaderCustom } from '../components/ui/Layout'; import Input from '../components/ui/Input'; import Button from '../components/ui/Button'; import { addMessageToInbox } from '../store/actions'; +import { RootState } from '../store'; const CreateChat: React.FC = () => { useSetup(); const setupDetailsState = useSelector( - (state: RootState) => state.setupDetailsState + (state: RootState) => state.setupDetails ); const [shinkaiIdentity, setShinkaiIdentity] = useState(''); const [messageText, setMessageText] = useState(''); diff --git a/apps/shinkai-app/src/pages/CreateJob.tsx b/apps/shinkai-app/src/pages/CreateJob.tsx index 589c79b46..fbfba0f14 100644 --- a/apps/shinkai-app/src/pages/CreateJob.tsx +++ b/apps/shinkai-app/src/pages/CreateJob.tsx @@ -32,13 +32,13 @@ const CreateJob: React.FC = () => { useSetup(); const dispatch = useDispatch(); const setupDetailsState = useSelector( - (state: RootState) => state.setupDetailsState + (state: RootState) => state.setupDetails ); const [jobContent, setJobContent] = useState(""); const [selectedAgent, setSelectedAgent] = useState( null ); - const agents = useSelector((state: RootState) => state.agents); + const agents = useSelector((state: RootState) => state.other.agents); const history: History = useHistory(); useEffect(() => { @@ -100,8 +100,7 @@ const CreateJob: React.FC = () => { receiver_subidentity, setupDetailsState ); - // TODO: Review this, we are using api answer and dispatching it as an action - dispatch(result); + dispatch({ type: "SEND_MESSAGE_SUCCESS", payload: result }); // Hacky solution because react-router can't handle dots in the URL const jobInboxName = InboxNameWrapper.get_job_inbox_name_from_params( diff --git a/apps/shinkai-app/src/pages/Home.tsx b/apps/shinkai-app/src/pages/Home.tsx index 23c1a7ff3..ec69a5909 100644 --- a/apps/shinkai-app/src/pages/Home.tsx +++ b/apps/shinkai-app/src/pages/Home.tsx @@ -17,24 +17,25 @@ import { import { addOutline, arrowForward, cloudUpload } from 'ionicons/icons'; import './Home.css'; import { useHistory } from 'react-router-dom'; -import { RootState } from '../store'; import { useDispatch, useSelector } from 'react-redux'; import React, { useEffect, useState } from 'react'; import { ApiConfig, getAllInboxesForProfile, } from '@shinkai/shinkai-message-ts/api'; + import { clearStore, receiveAllInboxesForProfile } from '../store/actions'; import Avatar from '../components/ui/Avatar'; import { IonContentCustom, IonHeaderCustom } from '../components/ui/Layout'; +import { RootState } from '../store'; const Home: React.FC = () => { - const { setupDetailsState } = useSelector((state: RootState) => state); + const setupDetails = useSelector((state: RootState) => state.setupDetails); const history = useHistory(); const dispatch = useDispatch(); const { shinkai_identity, profile, registration_name, permission_type } = - setupDetailsState; + setupDetails; const displayString = ( <> {`${shinkai_identity}/${profile}/device/${registration_name}`}{' '} @@ -43,20 +44,20 @@ const Home: React.FC = () => { ); const [showActionSheet, setShowActionSheet] = useState(false); const [showLogoutAlert, setShowLogoutAlert] = useState(false); - const inboxes = useSelector((state: RootState) => state.inboxes); + const inboxes = useSelector((state: RootState) => state.other.just_inboxes); console.log('Inboxes:', inboxes); useEffect(() => { - console.log('Redux State:', setupDetailsState); - ApiConfig.getInstance().setEndpoint(setupDetailsState.node_address); + console.log('Redux State:', setupDetails); + ApiConfig.getInstance().setEndpoint(setupDetails.node_address); }, []); useEffect(() => { - console.log('Redux State:', setupDetailsState); - ApiConfig.getInstance().setEndpoint(setupDetailsState.node_address); + console.log('Redux State:', setupDetails); + ApiConfig.getInstance().setEndpoint(setupDetails.node_address); // Local Identity - const { shinkai_identity, profile, registration_name } = setupDetailsState; + const { shinkai_identity, profile, registration_name } = setupDetails; const sender = shinkai_identity; const sender_subidentity = `${profile}/device/${registration_name}`; @@ -70,9 +71,8 @@ const Home: React.FC = () => { sender_subidentity, receiver, target_shinkai_name_profile, - setupDetailsState + setupDetails ).then((inboxes) => dispatch(receiveAllInboxesForProfile(inboxes))); - }, []); return ( @@ -100,39 +100,40 @@ const Home: React.FC = () => {
- {Object.entries(inboxes).map(([position, inboxId]) => ( - { - const encodedInboxId = position - .toString() - .replace(/\./g, '~'); - if (encodedInboxId.startsWith('inbox')) { - history.push( - `/chat/${encodeURIComponent(encodedInboxId)}` - ); - } else if (encodedInboxId.startsWith('job_inbox')) { - history.push( - `/job-chat/${encodeURIComponent(encodedInboxId)}` - ); - } - }} - > - - - {JSON.stringify(position)} - - - - ))} + {inboxes && + inboxes.map((inbox_name) => ( + { + const encodedInboxId = inbox_name + .toString() + .replace(/\./g, '~'); + if (encodedInboxId.startsWith('inbox')) { + history.push( + `/chat/${encodeURIComponent(encodedInboxId)}` + ); + } else if (encodedInboxId.startsWith('job_inbox')) { + history.push( + `/job-chat/${encodeURIComponent(encodedInboxId)}` + ); + } + }} + > + + + {JSON.stringify(inbox_name)} + + + + ))}
diff --git a/apps/shinkai-app/src/pages/JobChat.tsx b/apps/shinkai-app/src/pages/JobChat.tsx index 461b19dbc..cab5808f7 100644 --- a/apps/shinkai-app/src/pages/JobChat.tsx +++ b/apps/shinkai-app/src/pages/JobChat.tsx @@ -1,155 +1,71 @@ import { IonBackButton, - IonButton, IonButtons, - IonContent, - IonFooter, - IonHeader, IonIcon, - IonInput, - IonItem, - IonLabel, - IonList, IonPage, - IonText, IonTextarea, IonTitle, - IonToolbar, -} from "@ionic/react"; -import { useParams } from "react-router-dom"; -import React, { useEffect, useRef, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { - getLastMessagesFromInbox, - sendMessageToJob, -} from "@shinkai/shinkai-message-ts/api"; -import { RootState } from "../store"; -import { useSetup } from "../hooks/usetSetup"; +} from '@ionic/react'; +import { useParams } from 'react-router-dom'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { sendMessageToJob } from '@shinkai/shinkai-message-ts/api'; +import { useSetup } from '../hooks/usetSetup'; import { extractJobIdFromInbox, - extractReceiverShinkaiName, getOtherPersonIdentity, -} from "../utils/inbox_name_handler"; -import { ShinkaiMessage } from "@shinkai/shinkai-message-ts/models"; -import { calculateMessageHash } from "@shinkai/shinkai-message-ts/utils"; -import Avatar from "../components/ui/Avatar"; -import { cn } from "../theme/lib/utils"; -import { send } from "ionicons/icons"; -import "./Chat.css"; -import { - IonContentCustom, - IonFooterCustom, - IonHeaderCustom, -} from "../components/ui/Layout"; -import { receiveLoadMoreMessagesFromInbox } from "../store/actions"; - -const parseDate = (dateString: string) => { - return new Date(dateString); -}; +} from '../utils/inbox_name_handler'; +import { cn } from '../theme/lib/utils'; +import { send } from 'ionicons/icons'; +import './Chat.css'; +import { IonFooterCustom, IonHeaderCustom } from '../components/ui/Layout'; +import ChatMessages from '../components/ChatMessages'; +import { RootState } from '../store'; const JobChat: React.FC = () => { - console.log("Loading Chat.tsx"); + console.log('Loading JobChat.tsx'); useSetup(); const dispatch = useDispatch(); const setupDetailsState = useSelector( - (state: RootState) => state.setupDetailsState + (state: RootState) => state.setupDetails, + shallowEqual ); + const { shinkai_identity, profile } = setupDetailsState; const { id } = useParams<{ id: string }>(); - const bottomChatRef = useRef(null); - const deserializedId = decodeURIComponent(id).replace(/~/g, "."); - const [lastKey, setLastKey] = useState(undefined); - const [hasMoreMessages, setHasMoreMessages] = useState(true); - const [prevMessagesLength, setPrevMessagesLength] = useState(0); - - const reduxMessages = useSelector( - (state: RootState) => state.inboxes[deserializedId] - ); - - const [messages, setMessages] = useState([]); - const [inputMessage, setInputMessage] = useState(""); + const deserializedId = decodeURIComponent(id).replace(/~/g, '.'); + const [inputMessage, setInputMessage] = useState(''); const otherPersonIdentity = getOtherPersonIdentity( deserializedId, setupDetailsState.shinkai_identity ); - const extractContent = (messageBody: any) => { - if (messageBody && "unencrypted" in messageBody) { - if ("unencrypted" in messageBody.unencrypted.message_data) { - return JSON.parse( - messageBody.unencrypted.message_data.unencrypted.message_raw_content - ).content; - } else { - return JSON.parse( - messageBody.unencrypted.message_data.encrypted.content - ).content; - } - } else if (messageBody?.encrypted) { - return JSON.parse(messageBody.encrypted.content).content; - } - return ""; - }; - - useEffect(() => { - console.log("deserializedId:", deserializedId); - dispatch( - getLastMessagesFromInbox(deserializedId, 10, lastKey, setupDetailsState) - ); - }, [id, dispatch, setupDetailsState]); - - useEffect(() => { - if (reduxMessages && reduxMessages.length > 0) { - console.log("Redux Messages:", reduxMessages); - const lastMessage = reduxMessages[reduxMessages.length - 1]; - console.log("Last Message:", lastMessage); - const timeKey = lastMessage.external_metadata.scheduled_time; - const hashKey = calculateMessageHash(lastMessage); - const lastMessageKey = `${timeKey}:${hashKey}`; - setLastKey(lastMessageKey); - setMessages(reduxMessages); - - if (reduxMessages.length - prevMessagesLength < 10) { - setHasMoreMessages(false); - } - setPrevMessagesLength(reduxMessages.length); - } - }, [reduxMessages]); - - useEffect(() => { - // Check if the user is at the bottom of the chat - const isUserAtBottom = - bottomChatRef.current && - bottomChatRef.current.getBoundingClientRect().bottom <= - window.innerHeight; - - // If the user is at the bottom, scroll to the bottom - if (isUserAtBottom) { - bottomChatRef.current?.scrollIntoView({ behavior: "smooth" }); - } - }, [messages]); - - const sendMessage = async () => { - console.log("Sending message: ", inputMessage); - - // Local Identity - const { shinkai_identity, profile, registration_name } = setupDetailsState; - // let sender = shinkai_identity; + const sendMessage = useCallback(async () => { const sender = `${shinkai_identity}/${profile}`; + console.log('Sending message: ', inputMessage); + console.log('Sender:', sender); - console.log("Sender:", sender); - - const result = await sendMessageToJob( + const message_to_send = inputMessage; + setInputMessage(''); + sendMessageToJob( extractJobIdFromInbox(deserializedId.toString()), - inputMessage, + message_to_send, sender, shinkai_identity, - "", + '', setupDetailsState - ); - dispatch(result); - setInputMessage(""); - }; + ).then((message) => { + dispatch({ type: 'SEND_MESSAGE_SUCCESS', payload: message }); + }); + }, [ + inputMessage, + dispatch, + setupDetailsState, + shinkai_identity, + deserializedId, + profile + ]); return ( @@ -164,87 +80,15 @@ const JobChat: React.FC = () => { {/**/} - - -
- {hasMoreMessages && ( - { - getLastMessagesFromInbox( - deserializedId, - 10, - lastKey, - setupDetailsState - ).then((messages) => { - console.log("receiveLoadMoreMessagesFromInbox Response:", messages); - dispatch(receiveLoadMoreMessagesFromInbox(deserializedId, messages)); - }) - }} - > - Load More - - )} - - {messages && - messages - .slice() - .reverse() - .map((message, index) => { - const { shinkai_identity, profile, registration_name } = - setupDetailsState; - - const localIdentity = `${profile}/device/${registration_name}`; - // console.log("Message:", message); - let isLocalMessage = false; - if (message.body && "unencrypted" in message.body) { - isLocalMessage = - message.body.unencrypted.internal_metadata - .sender_subidentity === localIdentity; - } - - return ( - -
- - -

{extractContent(message.body)}

- {message?.external_metadata?.scheduled_time && ( - - {parseDate( - message.external_metadata.scheduled_time - ).toLocaleTimeString()} - - )} -
-
- ); - })} -
-
-
- +
{ e.preventDefault(); - if (inputMessage.trim() !== "") { + if (inputMessage.trim() !== '') { sendMessage(); } }} @@ -257,7 +101,10 @@ const JobChat: React.FC = () => { fill="outline" className="m-0 w-full bg-transparent p-0 pl-2 pr-12 md:pl-0" value={inputMessage} - onIonChange={(e) => setInputMessage(e.detail.value!)} + onIonChange={(e) => { + const newMessage = e.detail.value!; + setInputMessage(newMessage); + }} placeholder="Type a message" > @@ -265,10 +112,10 @@ const JobChat: React.FC = () => { onClick={sendMessage} aria-label="Send Message" className={cn( - "absolute z-10 p-3 rounded-md text-gray-500 bottom-[1px] right-1", - "md:bottom-2.5 md:right-2", - "hover:bg-gray-100 disabled:hover:bg-transparent", - "dark:text-white dark:hover:text-gray-100 dark:hover:bg-gray-700 dark:disabled:hover:bg-transparent" + 'absolute z-10 p-3 rounded-md text-gray-500 bottom-[1px] right-1', + 'md:bottom-2.5 md:right-2', + 'hover:bg-gray-100 disabled:hover:bg-transparent', + 'dark:text-white dark:hover:text-gray-100 dark:hover:bg-gray-700 dark:disabled:hover:bg-transparent' )} > diff --git a/apps/shinkai-app/src/store/actions.ts b/apps/shinkai-app/src/store/actions.ts index 1f0136f95..9273cd727 100644 --- a/apps/shinkai-app/src/store/actions.ts +++ b/apps/shinkai-app/src/store/actions.ts @@ -1,5 +1,5 @@ import { SerializedAgent } from "@shinkai/shinkai-message-ts/models"; -import { SetupDetailsState } from "./reducers"; +import { SetupDetailsState } from "./reducers/setupDetailsReducer"; import { GET_PUBLIC_KEY, USE_REGISTRATION_CODE, @@ -15,6 +15,7 @@ import { GET_AVAILABLE_AGENTS, CLEAR_MESSAGES, ADD_AGENTS, + RECEIVE_UNREAD_MESSAGES_FROM_INBOX, } from "./types"; export const getPublicKey = (publicKey: string) => ({ @@ -40,6 +41,14 @@ export const receiveLastMessagesFromInbox = ( payload: { inboxId, messages }, }); +export const receiveUnreadMessagesFromInbox = ( + inboxId: string, + messages: any[] +) => ({ + type: RECEIVE_UNREAD_MESSAGES_FROM_INBOX, + payload: { inboxId, messages }, +}); + export const receiveLoadMoreMessagesFromInbox = ( inboxId: string, messages: any[] @@ -93,4 +102,4 @@ export function addAgents(agents: SerializedAgent[]): AddAgentsAction { type: ADD_AGENTS, payload: agents, }; -} \ No newline at end of file +} diff --git a/apps/shinkai-app/src/store/index.ts b/apps/shinkai-app/src/store/index.ts index 5859f738b..ea0afbf22 100644 --- a/apps/shinkai-app/src/store/index.ts +++ b/apps/shinkai-app/src/store/index.ts @@ -1,12 +1,11 @@ -import { createStore, applyMiddleware, Store } from 'redux'; +import { createStore, applyMiddleware, Store, compose } from 'redux'; import thunk, { ThunkAction } from 'redux-thunk'; import storage from 'redux-persist/lib/storage'; import { persistStore, persistReducer } from 'redux-persist'; -import rootReducer, { RootState as RootStateFromReducer } from './reducers'; +import rootReducer from './reducers'; import { Action } from 'redux'; -export type RootState = RootStateFromReducer; - +export type RootState = ReturnType; export type AppDispatch = Store['dispatch']; export type AppThunk = ThunkAction< @@ -19,10 +18,17 @@ export type AppThunk = ThunkAction< const persistConfig = { key: 'root', storage, - whitelist: ['registrationStatus', 'setupDetailsState'] + whitelist: ['other', 'setupDetails'], + debug: true, }; const persistedReducer = persistReducer(persistConfig, rootReducer); -export const store = createStore(persistedReducer, applyMiddleware(thunk)); +// Use Redux DevTools extension if it's installed in the user's browser +const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose; + +export const store = createStore( + persistedReducer, + composeEnhancers(applyMiddleware(thunk)) +); export const persistor = persistStore(store); diff --git a/apps/shinkai-app/src/store/main.ts b/apps/shinkai-app/src/store/main.ts deleted file mode 100644 index 1bb10ff48..000000000 --- a/apps/shinkai-app/src/store/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -// src/store/main.ts -import { configureStore } from '@reduxjs/toolkit'; -import chatReducer from '../features/chat/chatSlice'; - -export const store = configureStore({ - reducer: { - chat: chatReducer, - }, -}); - -export type RootState = ReturnType; -export type AppDispatch = typeof store.dispatch; diff --git a/apps/shinkai-app/src/store/reducers.ts b/apps/shinkai-app/src/store/reducers.ts index 30ea201be..140eb9327 100644 --- a/apps/shinkai-app/src/store/reducers.ts +++ b/apps/shinkai-app/src/store/reducers.ts @@ -1,228 +1,12 @@ -import { Base58String } from "../models/QRSetupData"; -import { SerializedAgent, ShinkaiMessage } from "@shinkai/shinkai-message-ts/models"; -import { calculateMessageHash } from "@shinkai/shinkai-message-ts/utils"; -import { - Action, - GET_PUBLIC_KEY, - USE_REGISTRATION_CODE, - PING_ALL, - REGISTRATION_ERROR, - CREATE_REGISTRATION_CODE, - CLEAR_REGISTRATION_CODE, - RECEIVE_LAST_MESSAGES_FROM_INBOX, - CLEAR_STORE, - ADD_MESSAGE_TO_INBOX, - RECEIVE_ALL_INBOXES_FOR_PROFILE, - RECEIVE_LOAD_MORE_MESSAGES_FROM_INBOX, - ADD_AGENTS, -} from "./types"; - -export type SetupDetailsState = { - profile: string; - permission_type: string; - registration_name: string; - node_address: string; - shinkai_identity: string; - node_encryption_pk: Base58String; - node_signature_pk: Base58String; - profile_encryption_sk: Base58String; - profile_encryption_pk: Base58String; - profile_identity_sk: Base58String; - profile_identity_pk: Base58String; - my_device_encryption_sk: Base58String; - my_device_encryption_pk: Base58String; - my_device_identity_sk: Base58String; - my_device_identity_pk: Base58String; -}; - -const setupInitialState: SetupDetailsState = { - profile: "", - permission_type: "", - registration_name: "", - node_address: "", - shinkai_identity: "", - node_encryption_pk: "", - node_signature_pk: "", - profile_encryption_sk: "", - profile_encryption_pk: "", - profile_identity_sk: "", - profile_identity_pk: "", - my_device_encryption_sk: "", - my_device_encryption_pk: "", - my_device_identity_sk: "", - my_device_identity_pk: "", -}; - -export interface RootState { - registrationCode: string; - publicKey: string; - registrationStatus: boolean; - pingResult: string; - setupDetailsState: SetupDetailsState; - error: string | null; - inboxes: { - [inboxId: string]: any[]; - }; - messageHashes: { - [inboxId: string]: Set; - }; - agents: { - [agentId: string]: SerializedAgent; - }; -} - -const initialState: RootState = { - publicKey: "", - registrationStatus: false, - pingResult: "", - setupDetailsState: setupInitialState, - registrationCode: "", - error: null, - inboxes: {}, - messageHashes: {}, - agents: {}, -}; - -const rootReducer = (state = initialState, action: Action): RootState => { - switch (action.type) { - case GET_PUBLIC_KEY: - return { ...state, publicKey: action.payload }; - case USE_REGISTRATION_CODE: - return { - ...state, - registrationStatus: true, - setupDetailsState: action.payload, - }; - case RECEIVE_LOAD_MORE_MESSAGES_FROM_INBOX: { - const { inboxId, messages } = action.payload; - const currentMessages = state.inboxes[inboxId] || []; - const currentMessageHashes = state.messageHashes[inboxId] || new Set(); - - const uniqueNewMessages = messages.filter((msg: ShinkaiMessage) => { - const hash = calculateMessageHash(msg); - if (currentMessageHashes.has(hash)) { - return false; - } else { - currentMessageHashes.add(hash); - return true; - } - }); - - return { - ...state, - inboxes: { - ...state.inboxes, - [inboxId]: [...currentMessages, ...uniqueNewMessages], - }, - messageHashes: { - ...state.messageHashes, - [inboxId]: currentMessageHashes, - }, - }; - } - case RECEIVE_LAST_MESSAGES_FROM_INBOX: { - const { inboxId, messages } = action.payload; - const currentMessages = state.inboxes[inboxId] || []; - const currentMessageHashes = state.messageHashes[inboxId] || new Set(); - - const uniqueNewMessages = messages.filter((msg: ShinkaiMessage) => { - const hash = calculateMessageHash(msg); - if (currentMessageHashes.has(hash)) { - return false; - } else { - currentMessageHashes.add(hash); - return true; - } - }); - - return { - ...state, - inboxes: { - ...state.inboxes, - [inboxId]: [...currentMessages, ...uniqueNewMessages], - }, - messageHashes: { - ...state.messageHashes, - [inboxId]: currentMessageHashes, - }, - }; - } - case ADD_MESSAGE_TO_INBOX: { - const { inboxId, message } = action.payload; - const currentMessages = state.inboxes[inboxId] || []; - const currentMessageHashes = state.messageHashes[inboxId] || new Set(); - - const hash = calculateMessageHash(message); - if (currentMessageHashes.has(hash)) { - // If the message is a duplicate, don't add it - return state; - } else { - // If the message is unique, add it to the inbox and the hash to the set - currentMessageHashes.add(hash); - return { - ...state, - inboxes: { - ...state.inboxes, - [inboxId]: [message, ...currentMessages], - }, - messageHashes: { - ...state.messageHashes, - [inboxId]: currentMessageHashes, - }, - }; - } - } - case RECEIVE_ALL_INBOXES_FOR_PROFILE: { - const newInboxes = action.payload; - if (typeof newInboxes !== "object") { - console.error( - "Invalid payload for RECEIVE_ALL_INBOXES_FOR_PROFILE: ", - newInboxes - ); - return state; - } - return { - ...state, - inboxes: { - ...state.inboxes, - ...Object.keys(newInboxes).reduce( - (result: { [key: string]: any[] }, key) => { - if (!state.inboxes[key]) { - console.log("value for key: ", newInboxes[key]); - result[newInboxes[key]] = []; - } - return result; - }, - {} - ), - }, - }; - } - case ADD_AGENTS: { - const newAgents = action.payload; - const updatedAgents = { ...state.agents }; - newAgents.forEach((agent: SerializedAgent) => { - updatedAgents[agent.id] = agent; - }); - return { - ...state, - agents: updatedAgents, - }; - } - case CREATE_REGISTRATION_CODE: - return { ...state, registrationCode: action.payload }; - case REGISTRATION_ERROR: - return { ...state, error: action.payload }; - case CLEAR_REGISTRATION_CODE: - return { ...state, registrationCode: "" }; - case PING_ALL: - return { ...state, pingResult: action.payload }; - case CLEAR_STORE: - state = initialState; - return state; - default: - return state; - } -}; +import { combineReducers } from "redux"; +import { setupDetailsReducer } from "./reducers/setupDetailsReducer"; +import { messagesReducer } from "./reducers/messagesReducer"; +import otherReducer from "./reducers/otherReducer"; + +const rootReducer = combineReducers({ + setupDetails: setupDetailsReducer, + messages: messagesReducer, + other: otherReducer, +}); export default rootReducer; diff --git a/apps/shinkai-app/src/store/reducers/messagesReducer.ts b/apps/shinkai-app/src/store/reducers/messagesReducer.ts new file mode 100644 index 000000000..4d83fb214 --- /dev/null +++ b/apps/shinkai-app/src/store/reducers/messagesReducer.ts @@ -0,0 +1,218 @@ +import { ShinkaiMessage } from "@shinkai/shinkai-message-ts/models"; +import { calculateMessageHash } from "@shinkai/shinkai-message-ts/utils/shinkai_message_handler"; +import { + ADD_MESSAGE_TO_INBOX, + Action, + RECEIVE_ALL_INBOXES_FOR_PROFILE, + RECEIVE_LAST_MESSAGES_FROM_INBOX, + RECEIVE_LOAD_MORE_MESSAGES_FROM_INBOX, + RECEIVE_UNREAD_MESSAGES_FROM_INBOX, +} from "../types"; + +export interface MessagesState { + inboxes: { + [inboxId: string]: any[]; + }; + messageHashes: { + [inboxId: string]: { [hash: string]: boolean }; + }; +} + +const messagesState: MessagesState = { + inboxes: {}, + messageHashes: {}, +}; + +interface InboxMessagesAction { + type: + | typeof RECEIVE_LOAD_MORE_MESSAGES_FROM_INBOX + | typeof RECEIVE_LAST_MESSAGES_FROM_INBOX + | typeof RECEIVE_UNREAD_MESSAGES_FROM_INBOX; + payload?: { + inboxId: string; + messages: ShinkaiMessage[]; + }; +} + +interface AddMessageAction { + type: typeof ADD_MESSAGE_TO_INBOX; + payload?: { + inboxId: string; + message: ShinkaiMessage; + }; +} + +interface ReceiveAllInboxesAction { + type: typeof RECEIVE_ALL_INBOXES_FOR_PROFILE; + payload?: string[]; +} + +type MessagesAction = + | InboxMessagesAction + | AddMessageAction + | ReceiveAllInboxesAction; + +export const messagesReducer = ( + state = messagesState, + action: MessagesAction +): MessagesState => { + switch (action.type) { + case RECEIVE_UNREAD_MESSAGES_FROM_INBOX: { + if (!action.payload) { + return state; + } + const { inboxId, messages } = action.payload; + const currentMessages = state.inboxes[inboxId] || []; + const currentMessageHashes = state.messageHashes[inboxId] || {}; + + const uniqueNewMessages = messages.filter((msg: ShinkaiMessage) => { + const hash = calculateMessageHash(msg); + if (currentMessageHashes[hash]) { + return false; + } else { + currentMessageHashes[hash] = true; + return true; + } + }); + + return { + ...state, + inboxes: { + ...state.inboxes, + [inboxId]: [...currentMessages, ...uniqueNewMessages], + }, + messageHashes: { + ...state.messageHashes, + [inboxId]: currentMessageHashes, + }, + }; + } + case RECEIVE_LOAD_MORE_MESSAGES_FROM_INBOX: { + if (!action.payload) { + return state; + } + const { inboxId, messages } = action.payload; + const currentMessages = state.inboxes[inboxId] || []; + const currentMessageHashes = state.messageHashes[inboxId] || {}; + + const uniqueNewMessages = messages.filter((msg: ShinkaiMessage) => { + const hash = calculateMessageHash(msg); + if (currentMessageHashes[hash]) { + return false; + } else { + currentMessageHashes[hash] = true; + return true; + } + }); + + return { + ...state, + inboxes: { + ...state.inboxes, + [inboxId]: [...currentMessages, ...uniqueNewMessages], + }, + messageHashes: { + ...state.messageHashes, + [inboxId]: currentMessageHashes, + }, + }; + } + case RECEIVE_LAST_MESSAGES_FROM_INBOX: { + if (!action.payload) { + return state; + } + const { inboxId, messages } = action.payload; + const currentMessages = state.inboxes[inboxId] || []; + const currentMessageHashes = state.messageHashes[inboxId] || {}; + + console.log("RECEIVE_LAST_MESSAGES_FROM_INBOX> currentMessageHashes: ", currentMessageHashes); + console.log("RECEIVE_LAST_MESSAGES_FROM_INBOX> new messages: ", messages); + const uniqueNewMessages = messages.filter((msg: ShinkaiMessage) => { + const hash = calculateMessageHash(msg); + if (currentMessageHashes[hash]) { + return false; + } else { + currentMessageHashes[hash] = true; + return true; + } + }); + + return { + ...state, + inboxes: { + ...state.inboxes, + [inboxId]: [...currentMessages, ...uniqueNewMessages], + }, + messageHashes: { + ...state.messageHashes, + [inboxId]: currentMessageHashes, + }, + }; + } + case ADD_MESSAGE_TO_INBOX: { + console.log("ADD_MESSAGE_TO_INBOX"); + console.log("action.payload: ", action.payload); + if (!action.payload) { + return state; + } + const { inboxId, message } = action.payload; + const currentMessages = state.inboxes[inboxId] || []; + const currentMessageHashes = state.messageHashes[inboxId] || new Set(); + + const hash = calculateMessageHash(message); + if (currentMessageHashes[hash]) { + // If the message is a duplicate, don't add it + return state; + } else { + // If the message is unique, add it to the inbox and the hash to the set + currentMessageHashes[hash] = true; + return { + ...state, + inboxes: { + ...state.inboxes, + [inboxId]: [message, ...currentMessages], + }, + messageHashes: { + ...state.messageHashes, + [inboxId]: currentMessageHashes, + }, + }; + } + } + case RECEIVE_ALL_INBOXES_FOR_PROFILE: { + if (!action.payload) { + return state; + } + const newInboxes: { [key: string]: any } = action.payload; + if (typeof newInboxes !== "object") { + console.error( + "Invalid payload for RECEIVE_ALL_INBOXES_FOR_PROFILE: ", + newInboxes + ); + return state; + } + return { + ...state, + inboxes: { + ...state.inboxes, + ...Object.keys(newInboxes).reduce( + (result: { [key: string]: any[] }, key) => { + // Only initialize the inbox if it doesn't already exist in the state + if (!state.inboxes[key]) { + console.log("value for key: ", newInboxes[key]); + result[newInboxes[key]] = []; + } else { + // If the inbox already exists, keep the current messages + result[newInboxes[key]] = state.inboxes[key]; + } + return result; + }, + {} + ), + }, + }; + } + default: + return state; + } +}; diff --git a/apps/shinkai-app/src/store/reducers/otherReducer.ts b/apps/shinkai-app/src/store/reducers/otherReducer.ts new file mode 100644 index 000000000..f1540710f --- /dev/null +++ b/apps/shinkai-app/src/store/reducers/otherReducer.ts @@ -0,0 +1,87 @@ +import { SerializedAgent } from "@shinkai/shinkai-message-ts/models"; +import { + ADD_AGENTS, + Action, + CLEAR_REGISTRATION_CODE, + CLEAR_STORE, + CREATE_REGISTRATION_CODE, + GET_PUBLIC_KEY, + PING_ALL, + RECEIVE_ALL_INBOXES_FOR_PROFILE, + REGISTRATION_ERROR, + USE_REGISTRATION_CODE, +} from "../types"; + +export interface OtherState { + registrationCode: string; + publicKey: string; + registrationStatus: boolean; + pingResult: string; + error: string | null; + agents: { + [agentId: string]: SerializedAgent; + }; + just_inboxes: string[]; +} + +const initialState: OtherState = { + publicKey: "", + registrationStatus: false, + pingResult: "", + registrationCode: "", + error: null, + agents: {}, + just_inboxes: [], +}; + +const otherReducer = (state = initialState, action: Action): OtherState => { + switch (action.type) { + case USE_REGISTRATION_CODE: + return { + ...state, + registrationStatus: true, + }; + case RECEIVE_ALL_INBOXES_FOR_PROFILE: { + const newInboxes = action.payload; + if (!Array.isArray(newInboxes)) { + console.error( + "Invalid payload for RECEIVE_ALL_INBOXES_FOR_PROFILE: ", + newInboxes + ); + return state; + } + return { + ...state, + just_inboxes: newInboxes, + }; + } + case GET_PUBLIC_KEY: + return { ...state, publicKey: action.payload }; + case ADD_AGENTS: { + const newAgents = action.payload; + const updatedAgents = { ...state.agents }; + newAgents.forEach((agent: SerializedAgent) => { + updatedAgents[agent.id] = agent; + }); + return { + ...state, + agents: updatedAgents, + }; + } + case CREATE_REGISTRATION_CODE: + return { ...state, registrationCode: action.payload }; + case REGISTRATION_ERROR: + return { ...state, error: action.payload }; + case CLEAR_REGISTRATION_CODE: + return { ...state, registrationCode: "" }; + case PING_ALL: + return { ...state, pingResult: action.payload }; + case CLEAR_STORE: + state = initialState; + return state; + default: + return state; + } +}; + +export default otherReducer; diff --git a/apps/shinkai-app/src/store/reducers/setupDetailsReducer.ts b/apps/shinkai-app/src/store/reducers/setupDetailsReducer.ts new file mode 100644 index 000000000..4ab3f4fb0 --- /dev/null +++ b/apps/shinkai-app/src/store/reducers/setupDetailsReducer.ts @@ -0,0 +1,58 @@ +import { USE_REGISTRATION_CODE } from "../types"; +import { Base58String } from "../../models/QRSetupData"; + +export type SetupDetailsState = { + profile: string; + permission_type: string; + registration_name: string; + node_address: string; + shinkai_identity: string; + node_encryption_pk: Base58String; + node_signature_pk: Base58String; + profile_encryption_sk: Base58String; + profile_encryption_pk: Base58String; + profile_identity_sk: Base58String; + profile_identity_pk: Base58String; + my_device_encryption_sk: Base58String; + my_device_encryption_pk: Base58String; + my_device_identity_sk: Base58String; + my_device_identity_pk: Base58String; +}; + +const setupInitialState: SetupDetailsState = { + profile: "", + permission_type: "", + registration_name: "", + node_address: "", + shinkai_identity: "", + node_encryption_pk: "", + node_signature_pk: "", + profile_encryption_sk: "", + profile_encryption_pk: "", + profile_identity_sk: "", + profile_identity_pk: "", + my_device_encryption_sk: "", + my_device_encryption_pk: "", + my_device_identity_sk: "", + my_device_identity_pk: "", +}; + +interface SetupDetailsAction { + type: typeof USE_REGISTRATION_CODE; + payload?: SetupDetailsState; +} + +export const setupDetailsReducer = ( + state = setupInitialState, + action: SetupDetailsAction +): SetupDetailsState => { + switch (action.type) { + case USE_REGISTRATION_CODE: { + const newState = action.payload ? action.payload : state; + console.log("New state: ", newState); + return newState; + } + default: + return state; + } +}; diff --git a/apps/shinkai-app/src/store/types.ts b/apps/shinkai-app/src/store/types.ts index ea7fa28b2..1228c0937 100644 --- a/apps/shinkai-app/src/store/types.ts +++ b/apps/shinkai-app/src/store/types.ts @@ -5,6 +5,7 @@ export const REGISTRATION_ERROR = 'REGISTRATION_ERROR'; export const PING_ALL = 'PING_ALL'; export const CLEAR_REGISTRATION_CODE = 'CLEAR_REGISTRATION_CODE'; export const RECEIVE_LAST_MESSAGES_FROM_INBOX = "RECEIVE_LAST_MESSAGES_FROM_INBOX"; +export const RECEIVE_UNREAD_MESSAGES_FROM_INBOX = "RECEIVE_UNREAD_MESSAGES_FROM_INBOX"; export const RECEIVE_LOAD_MORE_MESSAGES_FROM_INBOX = "RECEIVE_LOAD_MORE_MESSAGES_FROM_INBOX"; export const CLEAR_STORE = 'CLEAR_STORE'; export const ADD_MESSAGE_TO_INBOX = 'ADD_MESSAGE_TO_INBOX'; @@ -17,4 +18,3 @@ export interface Action { type: string; payload?: any; } - \ No newline at end of file diff --git a/apps/shinkai-app/src/types.d.ts b/apps/shinkai-app/src/types.d.ts index e8e550d94..6ffbaa30c 100644 --- a/apps/shinkai-app/src/types.d.ts +++ b/apps/shinkai-app/src/types.d.ts @@ -5,3 +5,8 @@ export type AppThunk = ThunkAction< Action >; +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; + } +} diff --git a/libs/shinkai-message-ts/src/api/methods.ts b/libs/shinkai-message-ts/src/api/methods.ts index c7bd09046..56537d5be 100644 --- a/libs/shinkai-message-ts/src/api/methods.ts +++ b/libs/shinkai-message-ts/src/api/methods.ts @@ -1,11 +1,18 @@ -import axios from "axios"; -import { ShinkaiMessageBuilderWrapper } from "../wasm/ShinkaiMessageBuilderWrapper"; -import { ApiConfig } from "./api_config"; -import { AgentCredentialsPayload, CredentialsPayload, JobCredentialsPayload, LastMessagesFromInboxCredentialsPayload, SetupPayload, ShinkaiMessage } from "../models"; -import { ShinkaiNameWrapper } from "../wasm/ShinkaiNameWrapper"; -import { InboxNameWrapper } from "../pkg/shinkai_message_wasm"; -import { SerializedAgent } from "../models/SchemaTypes"; -import { SerializedAgentWrapper } from "../wasm/SerializedAgentWrapper"; +import axios from 'axios'; +import { ShinkaiMessageBuilderWrapper } from '../wasm/ShinkaiMessageBuilderWrapper'; +import { ApiConfig } from './api_config'; +import { + AgentCredentialsPayload, + CredentialsPayload, + JobCredentialsPayload, + LastMessagesFromInboxCredentialsPayload, + SetupPayload, + ShinkaiMessage, +} from '../models'; +import { ShinkaiNameWrapper } from '../wasm/ShinkaiNameWrapper'; +import { InboxNameWrapper } from '../pkg/shinkai_message_wasm'; +import { SerializedAgent } from '../models/SchemaTypes'; +import { SerializedAgentWrapper } from '../wasm/SerializedAgentWrapper'; // Helper function to handle HTTP errors export const handleHttpError = (response: any) => { @@ -23,286 +30,324 @@ export const fetchPublicKey = () => async (): Promise => { const response = await axios.get(`${apiEndpoint}/get_public_key`); return response.data; } catch (error) { - console.error("Error fetching public key:", error); - throw(error); + console.error('Error fetching public key:', error); + throw error; } }; -export const createChatWithMessage = - async ( - sender: string, - sender_subidentity: string, - receiver: string, - receiver_subidentity: string, - text_message: string, - setupDetailsState: { - my_device_encryption_sk: string; - my_device_identity_sk: string; - node_encryption_pk: string; - } - ): Promise<{ inboxId: string, message: ShinkaiMessage }> => { - const senderShinkaiName = new ShinkaiNameWrapper( - sender + "/" + sender_subidentity - ); - const receiverShinkaiName = new ShinkaiNameWrapper( - receiver + "/" + receiver_subidentity +export const createChatWithMessage = async ( + sender: string, + sender_subidentity: string, + receiver: string, + receiver_subidentity: string, + text_message: string, + setupDetailsState: { + my_device_encryption_sk: string; + my_device_identity_sk: string; + node_encryption_pk: string; + } +): Promise<{ inboxId: string; message: ShinkaiMessage }> => { + const senderShinkaiName = new ShinkaiNameWrapper( + sender + '/' + sender_subidentity + ); + const receiverShinkaiName = new ShinkaiNameWrapper( + receiver + '/' + receiver_subidentity + ); + + const senderProfile = senderShinkaiName.extract_profile(); + const receiverProfile = receiverShinkaiName.extract_profile(); + + const inbox = InboxNameWrapper.get_regular_inbox_name_from_params( + senderProfile.get_node_name, + senderProfile.get_profile_name, + receiverProfile.get_node_name, + receiverProfile.get_profile_name, + true + ); + + try { + const messageStr = ShinkaiMessageBuilderWrapper.create_chat_with_message( + setupDetailsState.my_device_encryption_sk, + setupDetailsState.my_device_identity_sk, + setupDetailsState.node_encryption_pk, + sender, + sender_subidentity, + receiver, + receiver_subidentity, + text_message, + inbox.get_value ); - const senderProfile = senderShinkaiName.extract_profile(); - const receiverProfile = receiverShinkaiName.extract_profile(); + const message: ShinkaiMessage = JSON.parse(messageStr); - const inbox = InboxNameWrapper.get_regular_inbox_name_from_params( - senderProfile.get_node_name, - senderProfile.get_profile_name, - receiverProfile.get_node_name, - receiverProfile.get_profile_name, - true - ); + const apiEndpoint = ApiConfig.getInstance().getEndpoint(); + const response = await axios.post(`${apiEndpoint}/v1/send`, message); - try { - const messageStr = ShinkaiMessageBuilderWrapper.create_chat_with_message( + handleHttpError(response); + if (message.body && 'unencrypted' in message.body) { + const inboxId = message.body.unencrypted.internal_metadata.inbox; + return { inboxId, message }; + } else { + throw new Error('message body is null or encrypted'); + } + } catch (error) { + console.error('Error sending text message:', error); + throw error; + } +}; + +export const sendTextMessageWithInbox = async ( + sender: string, + sender_subidentity: string, + receiver: string, + text_message: string, + inbox_name: string, + setupDetailsState: CredentialsPayload +): Promise<{ inboxId: string; message: ShinkaiMessage }> => { + try { + const messageStr = + ShinkaiMessageBuilderWrapper.send_text_message_with_inbox( setupDetailsState.my_device_encryption_sk, setupDetailsState.my_device_identity_sk, setupDetailsState.node_encryption_pk, sender, sender_subidentity, receiver, - receiver_subidentity, - text_message, - inbox.get_value + '', + inbox_name, + text_message ); - const message: ShinkaiMessage = JSON.parse(messageStr); - - const apiEndpoint = ApiConfig.getInstance().getEndpoint(); - const response = await axios.post(`${apiEndpoint}/v1/send`, message); - - handleHttpError(response); - if (message.body && "unencrypted" in message.body) { - const inboxId = message.body.unencrypted.internal_metadata.inbox; - return { inboxId, message }; - } else { - throw new Error("message body is null or encrypted") - } - } catch (error) { - console.error("Error sending text message:", error); - throw(error) - } - }; - -export const sendTextMessageWithInbox = - async ( - sender: string, - sender_subidentity: string, - receiver: string, - text_message: string, - inbox_name: string, - setupDetailsState: CredentialsPayload - ): Promise<{ inboxId: string, message: ShinkaiMessage }> => { - try { - const messageStr = - ShinkaiMessageBuilderWrapper.send_text_message_with_inbox( - setupDetailsState.my_device_encryption_sk, - setupDetailsState.my_device_identity_sk, - setupDetailsState.node_encryption_pk, - sender, - sender_subidentity, - receiver, - "", - inbox_name, - text_message - ); - - const message: ShinkaiMessage = JSON.parse(messageStr); - console.log("Message:", message); - - const apiEndpoint = ApiConfig.getInstance().getEndpoint(); - const response = await axios.post(`${apiEndpoint}/v1/send`, message); - handleHttpError(response); - if (message.body && "unencrypted" in message.body) { - const inboxId = message.body.unencrypted.internal_metadata.inbox; - return { inboxId, message }; - } else { - throw new Error("message body is null or encrypted") - } - } catch (error) { - console.error("Error sending text message:", error); - throw(error) - } - }; - -export const getAllInboxesForProfile = - async ( - sender: string, - sender_subidentity: string, - receiver: string, - target_shinkai_name_profile: string, - setupDetailsState: CredentialsPayload - ): Promise => { - try { - const messageStr = - ShinkaiMessageBuilderWrapper.get_all_inboxes_for_profile( - setupDetailsState.my_device_encryption_sk, - setupDetailsState.my_device_identity_sk, - setupDetailsState.node_encryption_pk, - sender, - sender_subidentity, - receiver, - target_shinkai_name_profile - ); - - const message = JSON.parse(messageStr); - console.log("Message:", message); - - const apiEndpoint = ApiConfig.getInstance().getEndpoint(); - const response = await axios.post( - `${apiEndpoint}/v1/get_all_inboxes_for_profile`, - message - ); - handleHttpError(response); - console.log("GetAllInboxesForProfile Response:", response.data); - return response.data; - } catch (error) { - console.error("Error getting all inboxes for profile:", error); - throw(error) - } - }; - -export const getLastMessagesFromInbox = - async ( - inbox: string, - count: number, - lastKey: string | undefined, - setupDetailsState: LastMessagesFromInboxCredentialsPayload, - ): Promise => { - try { - console.log("lastKey: ", lastKey); - const sender = - setupDetailsState.shinkai_identity + "/" + setupDetailsState.profile; - - const messageStr = - ShinkaiMessageBuilderWrapper.get_last_messages_from_inbox( - setupDetailsState.profile_encryption_sk, - setupDetailsState.profile_identity_sk, - setupDetailsState.node_encryption_pk, - inbox, - count, - lastKey, - sender, - "", - setupDetailsState.shinkai_identity - ); - - const message = JSON.parse(messageStr); - console.log("Message:", message); - - const apiEndpoint = ApiConfig.getInstance().getEndpoint(); - const response = await axios.post( - `${apiEndpoint}/v1/last_messages_from_inbox`, - message - ); + const message: ShinkaiMessage = JSON.parse(messageStr); + console.log('Message:', message); - handleHttpError(response); - return response.data; - } catch (error) { - console.error("Error getting last messages from inbox:", error); - throw(error); + const apiEndpoint = ApiConfig.getInstance().getEndpoint(); + const response = await axios.post(`${apiEndpoint}/v1/send`, message); + handleHttpError(response); + if (message.body && 'unencrypted' in message.body) { + const inboxId = message.body.unencrypted.internal_metadata.inbox; + return { inboxId, message }; + } else { + throw new Error('message body is null or encrypted'); } - }; - -export const submitRequestRegistrationCode = - async ( - identity_permissions: string, - code_type = "profile", - setupDetailsState: SetupPayload, - ): Promise => { - try { - // TODO: refactor the profile name to be a constant - // maybe we should add ShinkaiName and InboxName to the wasm library (just ADDED them this needs refactor) - const sender_profile_name = - setupDetailsState.profile + - "/device/" + - setupDetailsState.registration_name; - console.log("sender_profile_name:", sender_profile_name); - console.log("identity_permissions:", identity_permissions); - console.log("code_type:", code_type); - - const messageStr = ShinkaiMessageBuilderWrapper.request_code_registration( - setupDetailsState.my_device_encryption_sk, - setupDetailsState.my_device_identity_sk, + } catch (error) { + console.error('Error sending text message:', error); + throw error; + } +}; + +export const getAllInboxesForProfile = async ( + sender: string, + sender_subidentity: string, + receiver: string, + target_shinkai_name_profile: string, + setupDetailsState: CredentialsPayload +): Promise => { + try { + const messageStr = ShinkaiMessageBuilderWrapper.get_all_inboxes_for_profile( + setupDetailsState.my_device_encryption_sk, + setupDetailsState.my_device_identity_sk, + setupDetailsState.node_encryption_pk, + sender, + sender_subidentity, + receiver, + target_shinkai_name_profile + ); + + const message = JSON.parse(messageStr); + console.log('Message:', message); + + const apiEndpoint = ApiConfig.getInstance().getEndpoint(); + const response = await axios.post( + `${apiEndpoint}/v1/get_all_inboxes_for_profile`, + message + ); + handleHttpError(response); + console.log('GetAllInboxesForProfile Response:', response.data); + return response.data; + } catch (error) { + console.error('Error getting all inboxes for profile:', error); + throw error; + } +}; + +export const getLastMessagesFromInbox = async ( + inbox: string, + count: number, + lastKey: string | undefined, + setupDetailsState: LastMessagesFromInboxCredentialsPayload +): Promise => { + try { + console.log('lastKey: ', lastKey); + const sender = + setupDetailsState.shinkai_identity + '/' + setupDetailsState.profile; + + const messageStr = + ShinkaiMessageBuilderWrapper.get_last_messages_from_inbox( + setupDetailsState.profile_encryption_sk, + setupDetailsState.profile_identity_sk, setupDetailsState.node_encryption_pk, - identity_permissions, - code_type, - sender_profile_name, + inbox, + count, + lastKey, + sender, + '', setupDetailsState.shinkai_identity ); - const message = JSON.parse(messageStr); - console.log("Message:", message); + const message = JSON.parse(messageStr); + console.log('Message:', message); - const apiEndpoint = ApiConfig.getInstance().getEndpoint(); - const response = await axios.post( - `${apiEndpoint}/v1/create_registration_code`, - message - ); + const apiEndpoint = ApiConfig.getInstance().getEndpoint(); + const response = await axios.post( + `${apiEndpoint}/v1/last_messages_from_inbox`, + message + ); - handleHttpError(response); - return response.data.code; - } catch (error) { - console.error("Error creating registration code:", error); - throw(error); - } - }; - -export const submitRegistrationCode = - async (setupData: SetupPayload): Promise => { - try { - const messageStr = - ShinkaiMessageBuilderWrapper.use_code_registration_for_device( - setupData.my_device_encryption_sk, - setupData.my_device_identity_sk, - setupData.profile_encryption_sk, - setupData.profile_identity_sk, - setupData.node_encryption_pk, - setupData.registration_code, - setupData.identity_type, - setupData.permission_type, - setupData.registration_name, - setupData.profile || "", // sender_profile_name: it doesn't exist yet in the Node - setupData.shinkai_identity - ); - - const message = JSON.parse(messageStr); - console.log( - "submitRegistrationCode registration_name: ", - setupData.registration_name - ); - console.log( - "submitRegistrationCode identity_type: ", - setupData.identity_type - ); - console.log( - "submitRegistrationCode permission_type: ", - setupData.permission_type + handleHttpError(response); + return response.data; + } catch (error) { + console.error('Error getting last messages from inbox:', error); + throw error; + } +}; + +export const getLastUnreadMessagesFromInbox = async ( + inbox: string, + count: number, + fromKey: string | undefined, + setupDetailsState: LastMessagesFromInboxCredentialsPayload +): Promise => { + try { + console.log('fromKey: ', fromKey); + const sender = + setupDetailsState.shinkai_identity + '/' + setupDetailsState.profile; + + const messageStr = + ShinkaiMessageBuilderWrapper.get_last_messages_from_inbox( + setupDetailsState.profile_encryption_sk, + setupDetailsState.profile_identity_sk, + setupDetailsState.node_encryption_pk, + inbox, + count, + fromKey, + sender, + '', + setupDetailsState.shinkai_identity ); - console.log("submitRegistrationCode Message:", message); - // Use node_address from setupData for API endpoint - const response = await axios.post( - `${setupData.node_address}/v1/use_registration_code`, - message + const message = JSON.parse(messageStr); + console.log('Message:', message); + + const apiEndpoint = ApiConfig.getInstance().getEndpoint(); + const response = await axios.post( + `${apiEndpoint}/v1/last_unread_messages_from_inbox`, + message + ); + + handleHttpError(response); + console.log('getLastUnreadMessagesFromInbox Response:', response.data); + return response.data; + // dispatch(receiveUnreadMessagesFromInbox(inbox, results)); + } catch (error) { + console.error('Error getting last messages from inbox:', error); + throw error; + } +}; + +export const submitRequestRegistrationCode = async ( + identity_permissions: string, + code_type = 'profile', + setupDetailsState: SetupPayload +): Promise => { + try { + // TODO: refactor the profile name to be a constant + // maybe we should add ShinkaiName and InboxName to the wasm library (just ADDED them this needs refactor) + const sender_profile_name = + setupDetailsState.profile + + '/device/' + + setupDetailsState.registration_name; + console.log('sender_profile_name:', sender_profile_name); + console.log('identity_permissions:', identity_permissions); + console.log('code_type:', code_type); + + const messageStr = ShinkaiMessageBuilderWrapper.request_code_registration( + setupDetailsState.my_device_encryption_sk, + setupDetailsState.my_device_identity_sk, + setupDetailsState.node_encryption_pk, + identity_permissions, + code_type, + sender_profile_name, + setupDetailsState.shinkai_identity + ); + + const message = JSON.parse(messageStr); + console.log('Message:', message); + + const apiEndpoint = ApiConfig.getInstance().getEndpoint(); + const response = await axios.post( + `${apiEndpoint}/v1/create_registration_code`, + message + ); + + handleHttpError(response); + return response.data.code; + } catch (error) { + console.error('Error creating registration code:', error); + throw error; + } +}; + +export const submitRegistrationCode = async ( + setupData: SetupPayload +): Promise => { + try { + const messageStr = + ShinkaiMessageBuilderWrapper.use_code_registration_for_device( + setupData.my_device_encryption_sk, + setupData.my_device_identity_sk, + setupData.profile_encryption_sk, + setupData.profile_identity_sk, + setupData.node_encryption_pk, + setupData.registration_code, + setupData.identity_type, + setupData.permission_type, + setupData.registration_name, + setupData.profile || '', // sender_profile_name: it doesn't exist yet in the Node + setupData.shinkai_identity ); - handleHttpError(response); + const message = JSON.parse(messageStr); + console.log( + 'submitRegistrationCode registration_name: ', + setupData.registration_name + ); + console.log( + 'submitRegistrationCode identity_type: ', + setupData.identity_type + ); + console.log( + 'submitRegistrationCode permission_type: ', + setupData.permission_type + ); + console.log('submitRegistrationCode Message:', message); + + // Use node_address from setupData for API endpoint + const response = await axios.post( + `${setupData.node_address}/v1/use_registration_code`, + message + ); - // Update the API_ENDPOINT after successful registration - ApiConfig.getInstance().setEndpoint(setupData.node_address); - return true; - } catch (error) { - console.log("Error using registration code:", error); - return false; - } - }; + handleHttpError(response); + + // Update the API_ENDPOINT after successful registration + ApiConfig.getInstance().setEndpoint(setupData.node_address); + return true; + } catch (error) { + console.log('Error using registration code:', error); + return false; + } +}; export const pingAllNodes = async (): Promise => { const apiEndpoint = ApiConfig.getInstance().getEndpoint(); @@ -311,8 +356,8 @@ export const pingAllNodes = async (): Promise => { handleHttpError(response); return response.data.result; } catch (error) { - console.error("Error pinging all nodes:", error); - throw(error); + console.error('Error pinging all nodes:', error); + throw error; } }; @@ -339,86 +384,81 @@ export const createJob = async ( const apiEndpoint = ApiConfig.getInstance().getEndpoint(); const response = await axios.post(`${apiEndpoint}/v1/create_job`, message); handleHttpError(response); - console.log("createJob Response:", response.data); + console.log('createJob Response:', response.data); const jobId = response.data; return jobId; } catch (error) { - console.error("Error creating job:", error); - throw(error); + console.error('Error creating job:', error); + throw error; } }; -export const sendMessageToJob = - async ( - jobId: string, - content: string, - sender: string, - receiver: string, - receiver_subidentity: string, - setupDetailsState: JobCredentialsPayload - ): Promise => { - try { - const messageStr = ShinkaiMessageBuilderWrapper.job_message( - jobId, - content, - setupDetailsState.profile_encryption_sk, - setupDetailsState.profile_identity_sk, - setupDetailsState.node_encryption_pk, - sender, - receiver, - receiver_subidentity - ); +export const sendMessageToJob = async ( + jobId: string, + content: string, + sender: string, + receiver: string, + receiver_subidentity: string, + setupDetailsState: JobCredentialsPayload +): Promise => { + try { + const messageStr = ShinkaiMessageBuilderWrapper.job_message( + jobId, + content, + setupDetailsState.profile_encryption_sk, + setupDetailsState.profile_identity_sk, + setupDetailsState.node_encryption_pk, + sender, + receiver, + receiver_subidentity + ); - const message = JSON.parse(messageStr); - // console.log("Message:", message); + const message = JSON.parse(messageStr); + // console.log("Message:", message); - const apiEndpoint = ApiConfig.getInstance().getEndpoint(); - const response = await axios.post( - `${apiEndpoint}/v1/job_message`, - message - ); - handleHttpError(response); - return response.data; - } catch (error) { - console.error("Error sending message to job:", error); - throw(error); - } - }; - -export const getProfileAgents = - async ( - sender: string, - sender_subidentity: string, - receiver: string, - setupDetailsState: CredentialsPayload - ): Promise => { - try { - const messageStr = ShinkaiMessageBuilderWrapper.get_profile_agents( - setupDetailsState.my_device_encryption_sk, - setupDetailsState.my_device_identity_sk, - setupDetailsState.node_encryption_pk, - sender, - sender_subidentity, - receiver - ); + const apiEndpoint = ApiConfig.getInstance().getEndpoint(); + const response = await axios.post(`${apiEndpoint}/v1/job_message`, message); + handleHttpError(response); + return response.data; + } catch (error) { + console.error('Error sending message to job:', error); + throw error; + } +}; - const message = JSON.parse(messageStr); - // console.log("Message:", message); +export const getProfileAgents = async ( + sender: string, + sender_subidentity: string, + receiver: string, + setupDetailsState: CredentialsPayload +): Promise => { + try { + const messageStr = ShinkaiMessageBuilderWrapper.get_profile_agents( + setupDetailsState.my_device_encryption_sk, + setupDetailsState.my_device_identity_sk, + setupDetailsState.node_encryption_pk, + sender, + sender_subidentity, + receiver + ); - const apiEndpoint = ApiConfig.getInstance().getEndpoint(); - const response = await axios.post( - `${apiEndpoint}/v1/available_agents`, - message - ); + const message = JSON.parse(messageStr); + // console.log("Message:", message); - console.log("getProfileAgents Response:", response.data); - handleHttpError(response); - return response.data; - } catch (error) { - console.error("Error sending message to job:", error); - throw(error); - } - }; + const apiEndpoint = ApiConfig.getInstance().getEndpoint(); + const response = await axios.post( + `${apiEndpoint}/v1/available_agents`, + message + ); + + console.log('getProfileAgents Response:', response.data); + handleHttpError(response); + return response.data; + } catch (error) { + console.error('Error sending message to job:', error); + throw error; + } +}; export const addAgent = async ( sender_subidentity: string, @@ -432,8 +472,8 @@ export const addAgent = async ( setupDetailsState.profile_encryption_sk, setupDetailsState.profile_identity_sk, setupDetailsState.node_encryption_pk, - node_name + "/" + sender_subidentity, - "", + node_name + '/' + sender_subidentity, + '', node_name, agent_wrapped ); @@ -444,10 +484,10 @@ export const addAgent = async ( const apiEndpoint = ApiConfig.getInstance().getEndpoint(); const response = await axios.post(`${apiEndpoint}/v1/add_agent`, message); - console.log("addAgent Response:", response.data); + console.log('addAgent Response:', response.data); handleHttpError(response); return response.data; } catch (error) { - console.error("Error sending message to add agent:", error); + console.error('Error sending message to add agent:', error); } }; diff --git a/package-lock.json b/package-lock.json index aa553dcdb..780aaf065 100644 --- a/package-lock.json +++ b/package-lock.json @@ -132,6 +132,10 @@ "vite-plugin-top-level-await": "^1.3.1", "vite-plugin-wasm": "^3.2.2", "vitest": "^0.34.3" + }, + "engines": { + "node": "18", + "npm": "9" } }, "../shinkai-message-ts": {