From bb4305c194d32270d954df6b5cd0efc2d0dba04d Mon Sep 17 00:00:00 2001 From: Paul Ccari <46382556+paulclindo@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:33:44 -0500 Subject: [PATCH] feat: misc fixes, UI improvements, perf improvements (#598) * feat: tasks out of experimental * feat: remove registration code * fix: improve remove llm modal * refactor: button + unify * feat: set timeout request 10 min * clean up * fix: update prompt * unify buttons * unify buttons * fix types * fix types * refactor: chat message and improve performance * refacotr: tooltip provider * disable react scan * feat: implement enable/disable all tools, search files * fix UI * fixes * fixes * feat: enable search files * fix: enable more than 3 files upload * clean up * fix types * fix --- apps/shinkai-desktop/src/App.tsx | 30 +- .../src/components/agent/agent-form.tsx | 99 +- .../src/components/agent/agents.tsx | 6 +- .../ai-update-selection-action-bar.tsx | 15 +- .../chat-config-action-bar.tsx | 22 +- .../file-selection-action-bar.tsx | 6 +- .../prompt-selection-action-bar.tsx | 6 +- .../tools-switch-action-bar.tsx | 12 +- .../chat/components/message-list.tsx | 496 +++--- .../components/chat/components/message.tsx | 10 +- .../components/chat/conversation-footer.tsx | 668 +++----- .../components/chat/conversation-header.tsx | 4 +- .../components/tool-playground.tsx | 32 +- .../playground-tool/error-boundary.tsx | 4 +- .../context/prompt-selection-context.tsx | 36 +- .../src/components/sheet/table-chat.tsx | 2 +- .../vector-fs/components/all-files-tab.tsx | 88 +- apps/shinkai-desktop/src/main.tsx | 14 + apps/shinkai-desktop/src/pages/add-ai.tsx | 70 +- .../src/pages/ai-model-installation.tsx | 4 +- apps/shinkai-desktop/src/pages/ais.tsx | 86 +- .../src/pages/chat/chat-conversation.tsx | 55 +- .../src/pages/chat/empty-message.tsx | 173 +- .../src/pages/galxe-subscriptions.tsx | 5 +- .../src/pages/generate-code.tsx | 238 --- .../src/pages/layout/main-layout.tsx | 139 +- .../src/pages/layout/settings-layout.tsx | 6 - .../src/pages/prompt-library.tsx | 15 +- apps/shinkai-desktop/src/pages/tasks.tsx | 47 +- apps/shinkai-desktop/src/pages/tools.tsx | 432 ++--- apps/shinkai-desktop/src/routes/index.tsx | 18 +- .../spotlight/components/quick-ask.tsx | 2 +- .../shinkai-message-ts/src/api/tools/index.ts | 29 +- .../shinkai-message-ts/src/api/tools/types.ts | 18 +- .../src/api/vector-fs/index.ts | 18 + .../src/api/vector-fs/types.ts | 5 +- libs/shinkai-message-ts/src/http-client.ts | 2 +- .../FileUploaderUsingSymmetricKeyManager.ts | 214 --- .../src/forms/chat/chat-message.ts | 3 +- .../src/forms/chat/create-job.ts | 2 +- .../src/v2/mutations/addLLMProvider/index.ts | 2 +- .../src/v2/mutations/disableAllTools/index.ts | 10 + .../src/v2/mutations/disableAllTools/types.ts | 8 + .../disableAllTools/useDisableAllTools.ts | 33 + .../src/v2/mutations/enableAllTools/index.ts | 10 + .../src/v2/mutations/enableAllTools/types.ts | 8 + .../enableAllTools/useEnableAllTools.ts | 33 + .../src/v2/mutations/updatePrompt/index.ts | 2 + .../src/v2/mutations/updatePrompt/types.ts | 1 + .../getSearchDirectoryContents/index.ts | 14 + .../getSearchDirectoryContents/types.ts | 9 + .../useGetSearchDirectoryContents.ts | 33 + libs/shinkai-ui/src/components/button.tsx | 22 +- .../src/components/chat/message-list.tsx | 1 + .../src/components/markdown-preview.tsx | 8 +- .../rjsf/SubmitButton/SubmitButton.tsx | 5 +- package-lock.json | 1420 ++++++++++++++--- package.json | 2 + 58 files changed, 2654 insertions(+), 2098 deletions(-) delete mode 100644 apps/shinkai-desktop/src/pages/generate-code.tsx delete mode 100644 libs/shinkai-message-ts/src/wasm/FileUploaderUsingSymmetricKeyManager.ts create mode 100644 libs/shinkai-node-state/src/v2/mutations/disableAllTools/index.ts create mode 100644 libs/shinkai-node-state/src/v2/mutations/disableAllTools/types.ts create mode 100644 libs/shinkai-node-state/src/v2/mutations/disableAllTools/useDisableAllTools.ts create mode 100644 libs/shinkai-node-state/src/v2/mutations/enableAllTools/index.ts create mode 100644 libs/shinkai-node-state/src/v2/mutations/enableAllTools/types.ts create mode 100644 libs/shinkai-node-state/src/v2/mutations/enableAllTools/useEnableAllTools.ts create mode 100644 libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/index.ts create mode 100644 libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/types.ts create mode 100644 libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/useGetSearchDirectoryContents.ts diff --git a/apps/shinkai-desktop/src/App.tsx b/apps/shinkai-desktop/src/App.tsx index 13df264f2..178433e5e 100644 --- a/apps/shinkai-desktop/src/App.tsx +++ b/apps/shinkai-desktop/src/App.tsx @@ -1,6 +1,6 @@ import { I18nProvider } from '@shinkai_network/shinkai-i18n'; import { QueryProvider } from '@shinkai_network/shinkai-node-state'; -import { Toaster } from '@shinkai_network/shinkai-ui'; +import { Toaster, TooltipProvider } from '@shinkai_network/shinkai-ui'; import { info } from '@tauri-apps/plugin-log'; import { useEffect } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; @@ -18,19 +18,21 @@ function App() { }, []); useSyncStorageSecondary(); return ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); } diff --git a/apps/shinkai-desktop/src/components/agent/agent-form.tsx b/apps/shinkai-desktop/src/components/agent/agent-form.tsx index 27ff4ea55..a208c774b 100644 --- a/apps/shinkai-desktop/src/components/agent/agent-form.tsx +++ b/apps/shinkai-desktop/src/components/agent/agent-form.tsx @@ -1,9 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { PlusIcon } from '@radix-ui/react-icons'; import { useTranslation } from '@shinkai_network/shinkai-i18n'; -import { Agent } from '@shinkai_network/shinkai-message-ts/api/agents/types'; -import { JobConfig } from '@shinkai_network/shinkai-message-ts/api/jobs/types'; -import { ShinkaiPath } from '@shinkai_network/shinkai-message-ts/api/jobs/types'; import { DEFAULT_CHAT_CONFIG } from '@shinkai_network/shinkai-node-state/v2/constants'; import { useCreateAgent } from '@shinkai_network/shinkai-node-state/v2/mutations/createAgent/useCreateAgent'; import { useUpdateAgent } from '@shinkai_network/shinkai-node-state/v2/mutations/updateAgent/useUpdateAgent'; @@ -100,11 +97,19 @@ function AgentForm({ mode }: AgentFormProps) { const auth = useAuth((state) => state.auth); const navigate = useNavigate(); const { t } = useTranslation(); - const setSetJobScopeOpen = useSetJobScope((state) => state.setSetJobScopeOpen); + const setSetJobScopeOpen = useSetJobScope( + (state) => state.setSetJobScopeOpen, + ); const selectedKeys = useSetJobScope((state) => state.selectedKeys); - const selectedFileKeysRef = useSetJobScope((state) => state.selectedFileKeysRef); - const selectedFolderKeysRef = useSetJobScope((state) => state.selectedFolderKeysRef); - const onSelectedKeysChange = useSetJobScope((state) => state.onSelectedKeysChange); + const selectedFileKeysRef = useSetJobScope( + (state) => state.selectedFileKeysRef, + ); + const selectedFolderKeysRef = useSetJobScope( + (state) => state.selectedFolderKeysRef, + ); + const onSelectedKeysChange = useSetJobScope( + (state) => state.onSelectedKeysChange, + ); const { data: agent } = useGetAgent({ agentId: agentId ?? '', @@ -135,21 +140,25 @@ function AgentForm({ mode }: AgentFormProps) { scope: { vector_fs_items: [], vector_fs_folders: [], - vector_search_mode: "FillUpTo25k" - } + vector_search_mode: 'FillUpTo25k', + }, }, }); // Effect to update form when selected items change useEffect(() => { - if (selectedKeys || selectedFileKeysRef.size > 0 || selectedFolderKeysRef.size > 0) { + if ( + selectedKeys || + selectedFileKeysRef.size > 0 || + selectedFolderKeysRef.size > 0 + ) { const agentData = { ...form.getValues(), scope: { vector_fs_items: Array.from(selectedFileKeysRef.values()), vector_fs_folders: Array.from(selectedFolderKeysRef.values()), - vector_search_mode: "FillUpTo25k" - } + vector_search_mode: 'FillUpTo25k', + }, }; form.setValue('scope', agentData.scope); } @@ -161,7 +170,7 @@ function AgentForm({ mode }: AgentFormProps) { const scope = { vector_fs_items: Array.from(selectedFileKeysRef.values()), vector_fs_folders: Array.from(selectedFolderKeysRef.values()), - vector_search_mode: "FillUpTo25k" + vector_search_mode: 'FillUpTo25k', }; form.setValue('scope', scope); } @@ -179,7 +188,8 @@ function AgentForm({ mode }: AgentFormProps) { form.setValue('config', { custom_prompt: agent.config?.custom_prompt ?? '', custom_system_prompt: agent.config?.custom_system_prompt ?? '', - temperature: agent.config?.temperature ?? DEFAULT_CHAT_CONFIG.temperature, + temperature: + agent.config?.temperature ?? DEFAULT_CHAT_CONFIG.temperature, top_k: agent.config?.top_k ?? DEFAULT_CHAT_CONFIG.top_k, top_p: agent.config?.top_p ?? DEFAULT_CHAT_CONFIG.top_p, use_tools: agent.config?.use_tools ?? true, @@ -189,26 +199,27 @@ function AgentForm({ mode }: AgentFormProps) { form.setValue('llmProviderId', agent.llm_provider_id); // Set selected files and folders - if (agent.scope?.vector_fs_items?.length || agent.scope?.vector_fs_folders?.length) { - const selectedVRFilesPathMap = agent.scope.vector_fs_items.reduce>( - (acc: Record, filePath: string) => { - acc[filePath] = { - checked: true, - }; - return acc; - }, - {}, - ); - - const selectedVRFoldersPathMap = agent.scope.vector_fs_folders.reduce>( - (acc: Record, folderPath: string) => { - acc[folderPath] = { - checked: true, - }; - return acc; - }, - {}, - ); + if ( + agent.scope?.vector_fs_items?.length || + agent.scope?.vector_fs_folders?.length + ) { + const selectedVRFilesPathMap = agent.scope.vector_fs_items.reduce< + Record + >((acc: Record, filePath: string) => { + acc[filePath] = { + checked: true, + }; + return acc; + }, {}); + + const selectedVRFoldersPathMap = agent.scope.vector_fs_folders.reduce< + Record + >((acc: Record, folderPath: string) => { + acc[folderPath] = { + checked: true, + }; + return acc; + }, {}); onSelectedKeysChange({ ...selectedVRFilesPathMap, @@ -218,7 +229,7 @@ function AgentForm({ mode }: AgentFormProps) { form.setValue('scope', { vector_fs_items: agent.scope.vector_fs_items, vector_fs_folders: agent.scope.vector_fs_folders, - vector_search_mode: agent.scope.vector_search_mode || "FillUpTo25k" + vector_search_mode: agent.scope.vector_search_mode || 'FillUpTo25k', }); } } @@ -283,8 +294,8 @@ function AgentForm({ mode }: AgentFormProps) { scope: { vector_fs_items: Array.from(selectedFileKeysRef.values()), vector_fs_folders: Array.from(selectedFolderKeysRef.values()), - vector_search_mode: "FillUpTo25k" - } + vector_search_mode: 'FillUpTo25k', + }, }; if (mode === 'edit') { @@ -306,7 +317,10 @@ function AgentForm({ mode }: AgentFormProps) { return ( - +

Create and explore custom AI agents with tailored instructions and diverse skills. @@ -406,11 +420,10 @@ function AgentForm({ mode }: AgentFormProps) {

@@ -67,7 +67,7 @@ function Agents() { - diff --git a/apps/shinkai-desktop/src/components/chat/chat-action-bar/file-selection-action-bar.tsx b/apps/shinkai-desktop/src/components/chat/chat-action-bar/file-selection-action-bar.tsx index e44e90b41..d07add033 100644 --- a/apps/shinkai-desktop/src/components/chat/chat-action-bar/file-selection-action-bar.tsx +++ b/apps/shinkai-desktop/src/components/chat/chat-action-bar/file-selection-action-bar.tsx @@ -16,7 +16,7 @@ type FileUploadInputProps = { onClick: () => void; }; -export function FileSelectionActionBar({ +function FileSelectionActionBarBase({ onClick, inputProps, }: FileUploadInputProps) { @@ -45,3 +45,7 @@ export function FileSelectionActionBar({ ); } +export const FileSelectionActionBar = React.memo( + FileSelectionActionBarBase, + () => true, +); diff --git a/apps/shinkai-desktop/src/components/chat/chat-action-bar/prompt-selection-action-bar.tsx b/apps/shinkai-desktop/src/components/chat/chat-action-bar/prompt-selection-action-bar.tsx index 31ef6c8e3..c38ef2aed 100644 --- a/apps/shinkai-desktop/src/components/chat/chat-action-bar/prompt-selection-action-bar.tsx +++ b/apps/shinkai-desktop/src/components/chat/chat-action-bar/prompt-selection-action-bar.tsx @@ -7,11 +7,12 @@ import { } from '@shinkai_network/shinkai-ui'; import { PromptLibraryIcon } from '@shinkai_network/shinkai-ui/assets'; import { cn } from '@shinkai_network/shinkai-ui/utils'; +import { memo } from 'react'; import { usePromptSelectionStore } from '../../prompt/context/prompt-selection-context'; import { actionButtonClassnames } from '../conversation-footer'; -export default function PromptSelectionActionBar() { +function PromptSelectionActionBarBase() { const setPromptSelectionDrawerOpen = usePromptSelectionStore( (state) => state.setPromptSelectionDrawerOpen, ); @@ -38,3 +39,6 @@ export default function PromptSelectionActionBar() {
); } + +const PromptSelectionActionBar = memo(PromptSelectionActionBarBase); +export default PromptSelectionActionBar; diff --git a/apps/shinkai-desktop/src/components/chat/chat-action-bar/tools-switch-action-bar.tsx b/apps/shinkai-desktop/src/components/chat/chat-action-bar/tools-switch-action-bar.tsx index e8319a58b..c7ef8d732 100644 --- a/apps/shinkai-desktop/src/components/chat/chat-action-bar/tools-switch-action-bar.tsx +++ b/apps/shinkai-desktop/src/components/chat/chat-action-bar/tools-switch-action-bar.tsx @@ -12,7 +12,7 @@ import { } from '@shinkai_network/shinkai-ui'; import { ToolsIcon } from '@shinkai_network/shinkai-ui/assets'; import { cn } from '@shinkai_network/shinkai-ui/utils'; -import { useEffect } from 'react'; +import { memo, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { useParams } from 'react-router-dom'; import { toast } from 'sonner'; @@ -30,7 +30,7 @@ interface ToolsSwitchActionBarProps { onCheckedChange: (checked: boolean) => void; } -export function ToolsSwitchActionBar({ +function ToolsSwitchActionBarBase({ disabled, checked, onCheckedChange, @@ -64,7 +64,12 @@ export function ToolsSwitchActionBar({ ); } -export function UpdateToolsSwitchActionBar() { +export const ToolsSwitchActionBar = memo( + ToolsSwitchActionBarBase, + (prevProps, nextProps) => prevProps.checked === nextProps.checked, +); + +export function UpdateToolsSwitchActionBarBase() { const auth = useAuth((state) => state.auth); const { inboxId: encodedInboxId = '' } = useParams(); const inboxId = decodeURIComponent(encodedInboxId); @@ -127,3 +132,4 @@ export function UpdateToolsSwitchActionBar() { /> ); } +export const UpdateToolsSwitchActionBar = memo(UpdateToolsSwitchActionBarBase); diff --git a/apps/shinkai-desktop/src/components/chat/components/message-list.tsx b/apps/shinkai-desktop/src/components/chat/components/message-list.tsx index 4350d0621..0b6d3881b 100644 --- a/apps/shinkai-desktop/src/components/chat/components/message-list.tsx +++ b/apps/shinkai-desktop/src/components/chat/components/message-list.tsx @@ -11,6 +11,7 @@ import { } from '@tanstack/react-query'; import React, { Fragment, + memo, RefObject, useCallback, useEffect, @@ -51,277 +52,282 @@ function useScrollToBottom( }; } -export const MessageList = ({ - noMoreMessageLabel, - paginatedMessages, - isSuccess, - isLoading, - isFetchingPreviousPage, - hasPreviousPage, - fetchPreviousPage, - containerClassName, - lastMessageContent, - editAndRegenerateMessage, - regenerateMessage, - regenerateFirstMessage, - disabledRetryAndEdit, - messageExtra, - hidePythonExecution, -}: { - noMoreMessageLabel: string; - isSuccess: boolean; - isLoading: boolean; - isFetchingPreviousPage: boolean; - hasPreviousPage: boolean; - paginatedMessages: ChatConversationInfiniteData | undefined; - fetchPreviousPage: ( - options?: FetchPreviousPageOptions | undefined, - ) => Promise< - InfiniteQueryObserverResult - >; - regenerateMessage?: (messageId: string) => void; - regenerateFirstMessage?: (message: string) => void; - editAndRegenerateMessage?: (content: string, messageHash: string) => void; - containerClassName?: string; - lastMessageContent?: React.ReactNode; - disabledRetryAndEdit?: boolean; - messageExtra?: React.ReactNode; - hidePythonExecution?: boolean; -}) => { - const chatContainerRef = useRef(null); - const previousChatHeightRef = useRef(0); - const { ref, inView } = useInView(); - const messageList = paginatedMessages?.pages.flat() ?? []; +export const MessageList = memo( + ({ + noMoreMessageLabel, + paginatedMessages, + isSuccess, + isLoading, + isFetchingPreviousPage, + hasPreviousPage, + fetchPreviousPage, + containerClassName, + lastMessageContent, + editAndRegenerateMessage, + regenerateMessage, + regenerateFirstMessage, + disabledRetryAndEdit, + messageExtra, + hidePythonExecution, + }: { + noMoreMessageLabel: string; + isSuccess: boolean; + isLoading: boolean; + isFetchingPreviousPage: boolean; + hasPreviousPage: boolean; + paginatedMessages: ChatConversationInfiniteData | undefined; + fetchPreviousPage: ( + options?: FetchPreviousPageOptions | undefined, + ) => Promise< + InfiniteQueryObserverResult + >; + regenerateMessage?: (messageId: string) => void; + regenerateFirstMessage?: (message: string) => void; + editAndRegenerateMessage?: (content: string, messageHash: string) => void; + containerClassName?: string; + lastMessageContent?: React.ReactNode; + disabledRetryAndEdit?: boolean; + messageExtra?: React.ReactNode; + hidePythonExecution?: boolean; + }) => { + const chatContainerRef = useRef(null); + const previousChatHeightRef = useRef(0); + const { ref, inView } = useInView(); + const messageList = paginatedMessages?.pages.flat() ?? []; - const { autoScroll, setAutoScroll, scrollDomToBottom } = - useScrollToBottom(chatContainerRef); + const { autoScroll, setAutoScroll, scrollDomToBottom } = + useScrollToBottom(chatContainerRef); - const fetchPreviousMessages = useCallback(async () => { - setAutoScroll(false); - await fetchPreviousPage(); - }, [fetchPreviousPage]); + const fetchPreviousMessages = useCallback(async () => { + setAutoScroll(false); + await fetchPreviousPage(); + }, [fetchPreviousPage]); - useEffect(() => { - if (hasPreviousPage && inView) { - fetchPreviousMessages(); - } - }, [hasPreviousPage, inView]); + useEffect(() => { + if (hasPreviousPage && inView) { + fetchPreviousMessages(); + } + }, [hasPreviousPage, inView]); - // adjust the scroll position of a chat container after new messages are fetched - useLayoutEffect(() => { - if (!isFetchingPreviousPage && inView) { - const chatContainerElement = chatContainerRef.current; - if (!chatContainerElement) return; - const currentHeight = chatContainerElement.scrollHeight; - const previousHeight = previousChatHeightRef.current; + // adjust the scroll position of a chat container after new messages are fetched + useLayoutEffect(() => { + if (!isFetchingPreviousPage && inView) { + const chatContainerElement = chatContainerRef.current; + if (!chatContainerElement) return; + const currentHeight = chatContainerElement.scrollHeight; + const previousHeight = previousChatHeightRef.current; - if (!autoScroll) { - chatContainerElement.scrollTop = - currentHeight - previousHeight + chatContainerElement.scrollTop; - } else { - scrollDomToBottom(); - } + if (!autoScroll) { + chatContainerElement.scrollTop = + currentHeight - previousHeight + chatContainerElement.scrollTop; + } else { + scrollDomToBottom(); + } - chatContainerElement.scrollTop = currentHeight - previousHeight; - } - }, [paginatedMessages, isFetchingPreviousPage, inView]); + chatContainerElement.scrollTop = currentHeight - previousHeight; + } + }, [paginatedMessages, isFetchingPreviousPage, inView]); - useEffect(() => { - const chatContainerElement = chatContainerRef.current; - if (!chatContainerElement) return; - const handleScroll = async () => { - const currentHeight = chatContainerElement.scrollHeight; - const currentScrollTop = chatContainerElement.scrollTop; - previousChatHeightRef.current = currentHeight; - const scrollThreshold = 100; - const isNearBottom = - currentScrollTop + chatContainerElement.clientHeight >= - currentHeight - scrollThreshold; + useEffect(() => { + const chatContainerElement = chatContainerRef.current; + if (!chatContainerElement) return; + const handleScroll = async () => { + const currentHeight = chatContainerElement.scrollHeight; + const currentScrollTop = chatContainerElement.scrollTop; + previousChatHeightRef.current = currentHeight; + const scrollThreshold = 100; + const isNearBottom = + currentScrollTop + chatContainerElement.clientHeight >= + currentHeight - scrollThreshold; - setAutoScroll(isNearBottom); + setAutoScroll(isNearBottom); - if (inView && hasPreviousPage && !isFetchingPreviousPage) { - previousChatHeightRef.current = currentHeight - currentScrollTop; - } - }; + if (inView && hasPreviousPage && !isFetchingPreviousPage) { + previousChatHeightRef.current = currentHeight - currentScrollTop; + } + }; - chatContainerElement.addEventListener('scroll', handleScroll, { - passive: true, - }); - return () => { - chatContainerElement.removeEventListener('scroll', handleScroll); - }; - }, [ - fetchPreviousMessages, - hasPreviousPage, - inView, - isFetchingPreviousPage, - paginatedMessages?.pages?.length, - ]); + chatContainerElement.addEventListener('scroll', handleScroll, { + passive: true, + }); + return () => { + chatContainerElement.removeEventListener('scroll', handleScroll); + }; + }, [ + fetchPreviousMessages, + hasPreviousPage, + inView, + isFetchingPreviousPage, + paginatedMessages?.pages?.length, + ]); - useEffect(() => { - if (messageList?.length % 2 === 1) { - scrollDomToBottom(); - } - }, [messageList?.length]); + useEffect(() => { + if (messageList?.length % 2 === 1) { + scrollDomToBottom(); + } + }, [messageList?.length]); - useEffect(() => { - if (isSuccess) { - scrollDomToBottom(); - } - }, [isSuccess]); + useEffect(() => { + if (isSuccess) { + scrollDomToBottom(); + } + }, [isSuccess]); - return ( -
- {isSuccess && - !isFetchingPreviousPage && - !hasPreviousPage && - (paginatedMessages?.pages ?? [])?.length > 1 && ( -
- {noMoreMessageLabel} -
+ return ( +
- {isLoading && ( -
- {[...Array(10).keys()].map((index) => ( -
- - + {isSuccess && + !isFetchingPreviousPage && + !hasPreviousPage && + (paginatedMessages?.pages ?? [])?.length > 1 && ( +
+ {noMoreMessageLabel} +
+ )} +
+ {isLoading && ( +
+ {[...Array(10).keys()].map((index) => ( +
-
- ))} -
- )} - {(hasPreviousPage || isFetchingPreviousPage) && ( -
- {[...Array(4).keys()].map((index) => ( -
- - + + +
+ ))} +
+ )} + {(hasPreviousPage || isFetchingPreviousPage) && ( +
+ {[...Array(4).keys()].map((index) => ( +
-
- ))} -
- )} - {isSuccess && messageList?.length > 0 && ( - - {Object.entries(groupMessagesByDate(messageList)).map( - ([date, messages]) => { - return ( -
-
- - {getRelativeDateLabel( - new Date(messages[0].createdAt || ''), + key={`${index}`} + > + + +
+ ))} +
+ )} + {isSuccess && messageList?.length > 0 && ( + + {Object.entries(groupMessagesByDate(messageList)).map( + ([date, messages]) => { + return ( +
+
-
-
- {messages.map((message, messageIndex) => { - const previousMessage = messages[messageIndex - 1]; + > + + {getRelativeDateLabel( + new Date(messages[0].createdAt || ''), + )} + +
+
+ {messages.map((message, messageIndex) => { + const previousMessage = messages[messageIndex - 1]; - const grandparentHash = previousMessage - ? previousMessage.metadata.parentMessageId - : null; + const grandparentHash = previousMessage + ? previousMessage.metadata.parentMessageId + : null; - const firstMessageRetry = - disabledRetryAndEdit ?? - (grandparentHash === null || grandparentHash === ''); - const disabledRetryAndEditValue = - disabledRetryAndEdit ?? - (grandparentHash === null || grandparentHash === ''); + const firstMessageRetry = + disabledRetryAndEdit ?? + (grandparentHash === null || + grandparentHash === ''); + const disabledRetryAndEditValue = + disabledRetryAndEdit ?? + (grandparentHash === null || + grandparentHash === ''); - const handleRetryMessage = () => { - regenerateMessage?.(message?.messageId ?? ''); - }; + const handleRetryMessage = () => { + regenerateMessage?.(message?.messageId ?? ''); + }; - const handleEditMessage = (message: string) => { - editAndRegenerateMessage?.( - message, - previousMessage?.messageId ?? '', - ); - }; + const handleEditMessage = (message: string) => { + editAndRegenerateMessage?.( + message, + previousMessage?.messageId ?? '', + ); + }; - const handleFirstMessageRetry = () => { - regenerateFirstMessage?.(previousMessage.content); - }; + const handleFirstMessageRetry = () => { + regenerateFirstMessage?.(previousMessage.content); + }; - return ( - - ); - })} + return ( + + ); + })} +
-
- ); - }, - )} - {messageExtra} - {lastMessageContent} - - )} + ); + }, + )} + {messageExtra} + {lastMessageContent} + + )} +
-
- ); -}; + ); + }, +); +MessageList.displayName = 'MessageList'; diff --git a/apps/shinkai-desktop/src/components/chat/components/message.tsx b/apps/shinkai-desktop/src/components/chat/components/message.tsx index d68c45035..82487743a 100644 --- a/apps/shinkai-desktop/src/components/chat/components/message.tsx +++ b/apps/shinkai-desktop/src/components/chat/components/message.tsx @@ -271,18 +271,20 @@ export const MessageBase = ({
diff --git a/apps/shinkai-desktop/src/components/chat/conversation-footer.tsx b/apps/shinkai-desktop/src/components/chat/conversation-footer.tsx index e8160db6a..987698a43 100644 --- a/apps/shinkai-desktop/src/components/chat/conversation-footer.tsx +++ b/apps/shinkai-desktop/src/components/chat/conversation-footer.tsx @@ -11,10 +11,6 @@ import { ChatMessageFormSchema, chatMessageFormSchema, } from '@shinkai_network/shinkai-node-state/forms/chat/chat-message'; -import { - CreateJobFormSchema, - createJobFormSchema, -} from '@shinkai_network/shinkai-node-state/forms/chat/create-job'; import { useSendMessageToInbox } from '@shinkai_network/shinkai-node-state/lib/mutations/sendMesssageToInbox/useSendMessageToInbox'; import { Models } from '@shinkai_network/shinkai-node-state/lib/utils/models'; import { DEFAULT_CHAT_CONFIG } from '@shinkai_network/shinkai-node-state/v2/constants'; @@ -22,8 +18,6 @@ import { useCreateJob } from '@shinkai_network/shinkai-node-state/v2/mutations/c import { useSendMessageToJob } from '@shinkai_network/shinkai-node-state/v2/mutations/sendMessageToJob/useSendMessageToJob'; import { useStopGeneratingLLM } from '@shinkai_network/shinkai-node-state/v2/mutations/stopGeneratingLLM/useStopGeneratingLLM'; import { useGetChatConfig } from '@shinkai_network/shinkai-node-state/v2/queries/getChatConfig/useGetChatConfig'; -import { useGetChatConversationWithPagination } from '@shinkai_network/shinkai-node-state/v2/queries/getChatConversation/useGetChatConversationWithPagination'; -import { useGetLLMProviders } from '@shinkai_network/shinkai-node-state/v2/queries/getLLMProviders/useGetLLMProviders'; import { useGetSearchTools } from '@shinkai_network/shinkai-node-state/v2/queries/getToolsSearch/useGetToolsSearch'; import { Button, @@ -47,12 +41,13 @@ import { import { formatText, getFileExt } from '@shinkai_network/shinkai-ui/helpers'; import { useDebounce } from '@shinkai_network/shinkai-ui/hooks'; import { cn } from '@shinkai_network/shinkai-ui/utils'; +import equal from 'fast-deep-equal'; import { partial } from 'filesize'; import { AnimatePresence, motion } from 'framer-motion'; -import { Paperclip, X, XIcon } from 'lucide-react'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { Loader2, Paperclip, X, XIcon } from 'lucide-react'; +import { memo, useCallback, useEffect, useRef } from 'react'; import { useDropzone } from 'react-dropzone'; -import { useForm, useWatch } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { toast } from 'sonner'; @@ -91,88 +86,19 @@ export type ChatConversationLocationState = { llmProviderId: string; }; -function ConversationEmptyFooter() { - const { t } = useTranslation(); - const navigate = useNavigate(); - const { inboxId: encodedInboxId = '' } = useParams(); - const inboxId = decodeURIComponent(encodedInboxId); - const textareaRef = useRef(null); - - const onSelectedKeysChange = useSetJobScope( - (state) => state.onSelectedKeysChange, - ); - +const useSelectedFilesChat = ({ inboxId }: { inboxId?: string }) => { const selectedFileKeysRef = useSetJobScope( (state) => state.selectedFileKeysRef, ); const selectedFolderKeysRef = useSetJobScope( (state) => state.selectedFolderKeysRef, ); - const promptSelected = usePromptSelectionStore( - (state) => state.promptSelected, + const onSelectedKeysChange = useSetJobScope( + (state) => state.onSelectedKeysChange, ); - const auth = useAuth((state) => state.auth); - const { captureAnalyticEvent } = useAnalytics(); - const location = useLocation(); - const locationState = location.state as ChatConversationLocationState; - const isAgentInbox = locationState?.agentName; - const defaulAgentId = useSettings((state) => state.defaultAgentId); - - const chatForm = useForm({ - resolver: zodResolver(createJobFormSchema), - defaultValues: { - message: '', - files: [], - }, - }); - const selectedTool = chatForm.watch('tool'); - - const chatConfigForm = useForm({ - resolver: zodResolver(chatConfigFormSchema), - defaultValues: { - stream: DEFAULT_CHAT_CONFIG.stream, - customPrompt: '', - temperature: DEFAULT_CHAT_CONFIG.temperature, - topP: DEFAULT_CHAT_CONFIG.top_p, - topK: DEFAULT_CHAT_CONFIG.top_k, - useTools: DEFAULT_CHAT_CONFIG.use_tools, - }, - }); - - const { llmProviders } = useGetLLMProviders({ - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - }); - - useEffect(() => { - if (llmProviders?.length && !defaulAgentId) { - chatForm.setValue('agent', llmProviders?.[0].id); - } else { - chatForm.setValue('agent', defaulAgentId); - } - }, [llmProviders, chatForm, defaulAgentId]); - - useEffect(() => { - if (!locationState?.llmProviderId) { - return; - } - const llmProvider = llmProviders.find( - (agent) => agent.id === locationState.llmProviderId, - ); - if (llmProvider) { - chatForm.setValue('agent', llmProvider.id); - } - }, [chatForm, locationState, llmProviders]); - - useEffect(() => { - if (!locationState?.agentName) { - return; - } - chatForm.setValue('agent', locationState.agentName); - }, [chatForm, locationState, llmProviders]); useEffect(() => { if ( @@ -215,356 +141,73 @@ function ConversationEmptyFooter() { selectedFolderKeysRef, onSelectedKeysChange, ]); - - const currentMessage = useWatch({ - control: chatForm.control, - name: 'message', - }); - const debounceMessage = useDebounce(currentMessage, 500); - - const { data: searchToolList, isSuccess: isSearchToolListSuccess } = - useGetSearchTools( - { - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - search: debounceMessage, - }, - { - enabled: !!debounceMessage && !!currentMessage && !selectedTool, - select: (data) => data.slice(0, 3), - }, - ); - - const onDrop = useCallback( - (acceptedFiles: File[]) => { - const previousFiles = chatForm.getValues('files') ?? []; - const newFiles = [...previousFiles, ...acceptedFiles]; - chatForm.setValue('files', newFiles, { shouldValidate: true }); - }, - [chatForm], - ); - - const { - getRootProps: getRootFileProps, - getInputProps: getInputFileProps, - isDragActive, - open: openFilePicker, - } = useDropzone({ - noClick: true, - noKeyboard: true, - multiple: true, - onDrop, - }); - - const currentFiles = useWatch({ - control: chatForm.control, - name: 'files', - }); - const { mutateAsync: createJob, isPending } = useCreateJob({ - onError: (error) => { - toast.error('Failed to send message', { - description: error.response?.data?.message ?? error.message, - }); - }, - onSuccess: async (data, variables) => { - navigate( - `/inboxes/${encodeURIComponent(buildInboxIdFromJobId(data.jobId))}`, - ); - - const files = variables?.files ?? []; - const localFilesCount = (variables.selectedVRFiles ?? [])?.length; - const localFoldersCount = (variables.selectedVRFolders ?? [])?.length; - - if (localFilesCount > 0 || localFoldersCount > 0) { - captureAnalyticEvent('Ask Local Files', { - foldersCount: localFoldersCount, - filesCount: localFilesCount, - }); - } - if (files?.length > 0) { - captureAnalyticEvent('AI Chat with Files', { - filesCount: files.length, - }); - } else { - captureAnalyticEvent('AI Chat', undefined); - } + return { + selectedFileKeysRef, + selectedFolderKeysRef, + clearSelectedFiles: () => { + onSelectedKeysChange(null); + selectedFileKeysRef.clear(); + selectedFolderKeysRef.clear(); }, - }); - - useEffect(() => { - chatForm.setValue('message', promptSelected?.prompt ?? ''); - }, [chatForm, promptSelected]); - - useEffect(() => { - if (!textareaRef.current) return; - textareaRef.current.scrollTop = textareaRef.current.scrollHeight; - textareaRef.current.focus(); - }, [chatForm.watch('message')]); - - useEffect(() => { - chatConfigForm.setValue( - 'useTools', - promptSelected?.useTools ? true : DEFAULT_CHAT_CONFIG.use_tools, - ); - }, [chatConfigForm, chatForm, promptSelected]); - - const onSubmit = async (data: CreateJobFormSchema) => { - if (!auth || data.message.trim() === '') return; - const selectedVRFiles = - selectedFileKeysRef.size > 0 - ? Array.from(selectedFileKeysRef.values()) - : []; - const selectedVRFolders = - selectedFolderKeysRef.size > 0 - ? Array.from(selectedFolderKeysRef.values()) - : []; - - await createJob({ - nodeAddress: auth.node_address, - token: auth.api_v2_key, - llmProvider: data.agent, - content: data.message, - files: currentFiles, - isHidden: false, - toolKey: data.tool?.key, - selectedVRFiles, - selectedVRFolders, - ...(!isAgentInbox && { - chatConfig: { - stream: chatConfigForm.getValues('stream'), - custom_prompt: chatConfigForm.getValues('customPrompt') ?? '', - temperature: chatConfigForm.getValues('temperature'), - top_p: chatConfigForm.getValues('topP'), - top_k: chatConfigForm.getValues('topK'), - use_tools: chatConfigForm.getValues('useTools'), - }, - }), - }); - - chatForm.reset(); - selectedFileKeysRef.clear(); - selectedFolderKeysRef.clear(); - onSelectedKeysChange(null); }; +}; - return ( -
-
- ( - - - {t('chat.enterMessage')} - - -
-
-
- { - chatForm.setValue('agent', value); - }} - value={chatForm.watch('agent')} - /> - - - {!isAgentInbox && ( - { - chatConfigForm.setValue('useTools', checked); - }} - /> - )} -
- {!isAgentInbox && ( - - )} -
- - - {!debounceMessage && ( - - Enter to send - - )} - -
- } - disabled={isPending} - onChange={field.onChange} - onKeyDown={(e) => { - if ( - (e.ctrlKey || e.metaKey) && - e.key === 'z' && - promptSelected?.prompt === chatForm.watch('message') - ) { - chatForm.setValue('message', ''); - } - }} - onPaste={(event) => { - const items = event.clipboardData?.items; - if (items) { - for (let i = 0; i < items.length; i++) { - if (items[i].type.indexOf('image') !== -1) { - const file = items[i].getAsFile(); - if (file) { - onDrop([file]); - } - } - } - } - }} - onSubmit={chatForm.handleSubmit(onSubmit)} - ref={textareaRef} - topAddons={ - <> - {isDragActive && } - {selectedTool && ( - { - chatForm.setValue('tool', undefined); - }} - /> - )} - {!isDragActive && - currentFiles && - currentFiles.length > 0 && ( - { - const newFiles = [...currentFiles]; - newFiles.splice(index, 1); - chatForm.setValue('files', newFiles, { - shouldValidate: true, - }); - }} - /> - )} - - } - value={field.value} - /> - -
- {!!debounceMessage && - !selectedTool && - isSearchToolListSuccess && - searchToolList?.length > 0 && - searchToolList?.map((tool) => ( - - - { - chatForm.setValue('tool', { - key: tool.tool_router_key, - name: tool.name, - description: tool.description, - args: Object.keys( - tool.input_args.properties ?? {}, - ), - }); - chatConfigForm.setValue('useTools', true); - }} - type="button" - > - - {formatText(tool.name)} - - - - - {tool.description} - - - - ))} - {!debounceMessage && ( - - Shift + Enter for - a new line - - )} -
-
-
- - - )} - /> - -
- ); -} -function ConversationChatFooter({ inboxId }: { inboxId: string }) { +function ConversationChatFooter({ + inboxId, + isLoadingMessage, +}: { + inboxId: string; + isLoadingMessage: boolean; +}) { const { t } = useTranslation(); + const navigate = useNavigate(); const textareaRef = useRef(null); const auth = useAuth((state) => state.auth); const { captureAnalyticEvent } = useAnalytics(); + const { selectedFileKeysRef, selectedFolderKeysRef, clearSelectedFiles } = + useSelectedFilesChat({ + inboxId, + }); + + const promptSelected = usePromptSelectionStore( + (state) => state.promptSelected, + ); + + const location = useLocation(); + + const locationState = location.state as ChatConversationLocationState; + const chatForm = useForm({ resolver: zodResolver(chatMessageFormSchema), defaultValues: { message: '', files: [], + agent: '', }, }); + const defaulAgentId = useSettings((state) => state.defaultAgentId); + + useEffect(() => { + chatForm.reset(); + }, [chatForm, inboxId]); + + useEffect(() => { + if (!defaulAgentId || inboxId) return; + chatForm.setValue('agent', defaulAgentId); + }, [chatForm, defaulAgentId, inboxId]); + + const selectedTool = chatForm.watch('tool'); + const currentMessage = chatForm.watch('message'); + const currentFiles = chatForm.watch('files'); + const { data: chatConfig } = useGetChatConfig( { nodeAddress: auth?.node_address ?? '', token: auth?.api_v2_key ?? '', - jobId: extractJobIdFromInbox(inboxId), + jobId: inboxId ? extractJobIdFromInbox(inboxId) : '', }, { enabled: !!inboxId }, ); @@ -594,33 +237,29 @@ function ConversationChatFooter({ inboxId }: { inboxId: string }) { } }, [chatConfig, chatConfigForm]); - const selectedTool = chatForm.watch('tool'); + useEffect(() => { + if (!locationState?.llmProviderId) { + return; + } + chatForm.setValue('agent', locationState.llmProviderId); + }, [chatForm, locationState]); - const promptSelected = usePromptSelectionStore( - (state) => state.promptSelected, - ); + useEffect(() => { + if (!locationState?.agentName) { + return; + } + chatForm.setValue('agent', locationState.agentName); + }, [chatForm, locationState]); + + const currentInbox = useGetCurrentInbox(inboxId); - const currentInbox = useGetCurrentInbox(); - const isAgentInbox = currentInbox?.agent?.type === 'Agent'; + const isAgentInbox = + currentInbox?.agent?.type === 'Agent' || locationState?.agentName; const hasProviderEnableStreaming = streamingSupportedModels.includes( currentInbox?.agent?.model.split(':')?.[0] as Models, ); - const { data } = useGetChatConversationWithPagination({ - token: auth?.api_v2_key ?? '', - nodeAddress: auth?.node_address ?? '', - inboxId: inboxId as string, - shinkaiIdentity: auth?.shinkai_identity ?? '', - profile: auth?.profile ?? '', - refetchIntervalEnabled: - !hasProviderEnableStreaming || chatConfig?.stream === false, - }); - - const currentMessage = useWatch({ - control: chatForm.control, - name: 'message', - }); const debounceMessage = useDebounce(currentMessage, 500); const { data: searchToolList, isSuccess: isSearchToolListSuccess } = @@ -641,6 +280,7 @@ function ConversationChatFooter({ inboxId }: { inboxId: string }) { const previousFiles = chatForm.getValues('files') ?? []; const newFiles = [...previousFiles, ...acceptedFiles]; chatForm.setValue('files', newFiles, { shouldValidate: true }); + textareaRef.current?.focus(); }, [chatForm], ); @@ -657,11 +297,36 @@ function ConversationChatFooter({ inboxId }: { inboxId: string }) { onDrop, }); - const currentFiles = useWatch({ - control: chatForm.control, - name: 'files', - }); + const { mutateAsync: createJob, isPending } = useCreateJob({ + onError: (error) => { + toast.error('Failed to send message', { + description: error.response?.data?.message ?? error.message, + }); + }, + onSuccess: async (data, variables) => { + navigate( + `/inboxes/${encodeURIComponent(buildInboxIdFromJobId(data.jobId))}`, + ); + const files = variables?.files ?? []; + const localFilesCount = (variables.selectedVRFiles ?? [])?.length; + const localFoldersCount = (variables.selectedVRFolders ?? [])?.length; + + if (localFilesCount > 0 || localFoldersCount > 0) { + captureAnalyticEvent('Ask Local Files', { + foldersCount: localFoldersCount, + filesCount: localFilesCount, + }); + } + if (files?.length > 0) { + captureAnalyticEvent('AI Chat with Files', { + filesCount: files.length, + }); + } else { + captureAnalyticEvent('AI Chat', undefined); + } + }, + }); const { mutateAsync: sendMessageToInbox } = useSendMessageToInbox(); const { mutateAsync: sendMessageToJob } = useSendMessageToJob({ onSuccess: (_, variables) => { @@ -680,18 +345,45 @@ function ConversationChatFooter({ inboxId }: { inboxId: string }) { }, }); - const isLoadingMessage = useMemo(() => { - const lastMessage = data?.pages?.at(-1)?.at(-1); - return ( - !!inboxId && - lastMessage?.role === 'assistant' && - lastMessage?.status.type === 'running' - ); - }, [data?.pages, inboxId]); - const onSubmit = async (data: ChatMessageFormSchema) => { if (!auth || data.message.trim() === '') return; + if (!inboxId && data.agent) { + const selectedVRFiles = + selectedFileKeysRef.size > 0 + ? Array.from(selectedFileKeysRef.values()) + : []; + const selectedVRFolders = + selectedFolderKeysRef.size > 0 + ? Array.from(selectedFolderKeysRef.values()) + : []; + + await createJob({ + nodeAddress: auth.node_address, + token: auth.api_v2_key, + llmProvider: data.agent, + content: data.message, + files: currentFiles, + isHidden: false, + toolKey: data.tool?.key, + selectedVRFiles, + selectedVRFolders, + ...(!isAgentInbox && { + chatConfig: { + stream: chatConfigForm.getValues('stream'), + custom_prompt: chatConfigForm.getValues('customPrompt') ?? '', + temperature: chatConfigForm.getValues('temperature'), + top_p: chatConfigForm.getValues('topP'), + top_k: chatConfigForm.getValues('topK'), + use_tools: chatConfigForm.getValues('useTools'), + }, + }), + }); + + chatForm.reset(); + clearSelectedFiles(); + return; + } if (isJobInbox(inboxId)) { const jobId = extractJobIdFromInbox(inboxId); await sendMessageToJob({ @@ -723,6 +415,14 @@ function ConversationChatFooter({ inboxId }: { inboxId: string }) { chatForm.reset(); }; + useEffect(() => { + if (inboxId) return; + chatConfigForm.setValue( + 'useTools', + promptSelected?.useTools ? true : DEFAULT_CHAT_CONFIG.use_tools, + ); + }, [chatConfigForm, chatForm, promptSelected, inboxId]); + useEffect(() => { if (promptSelected) { chatForm.setValue('message', promptSelected.prompt); @@ -733,11 +433,7 @@ function ConversationChatFooter({ inboxId }: { inboxId: string }) { if (!textareaRef.current) return; textareaRef.current.scrollTop = textareaRef.current.scrollHeight; textareaRef.current.focus(); - }, [chatForm.watch('message')]); - - useEffect(() => { - chatForm.reset(); - }, [chatForm, inboxId]); + }, [currentMessage]); return (
- + {inboxId ? ( + + ) : ( + { + chatForm.setValue('agent', value); + }} + value={chatForm.watch('agent') ?? ''} + /> + )} - + {isAgentInbox ? null : inboxId ? ( + + ) : ( + { + chatConfigForm.setValue('useTools', checked); + }} + /> + )}
- {!isAgentInbox && } + {isAgentInbox ? null : inboxId ? ( + + ) : ( + + )}
} - disabled={isLoadingMessage} + disabled={isLoadingMessage || isPending} onChange={field.onChange} onKeyDown={(e) => { if ( @@ -848,6 +566,7 @@ function ConversationChatFooter({ inboxId }: { inboxId: string }) { currentFiles.length > 0 && ( { const newFiles = [...currentFiles]; newFiles.splice(index, 1); @@ -893,6 +612,7 @@ function ConversationChatFooter({ inboxId }: { inboxId: string }) { tool.input_args.properties ?? {}, ), }); + chatConfigForm.setValue('useTools', true); }} type="button" > @@ -929,16 +649,14 @@ function ConversationChatFooter({ inboxId }: { inboxId: string }) { ); } -export default function ConversationFooter() { - const { inboxId: encodedInboxId = '' } = useParams(); - const inboxId = decodeURIComponent(encodedInboxId); - if (!inboxId) { - return ; - } - return ; -} +export default memo( + ConversationChatFooter, + (prev, next) => + prev.inboxId === next.inboxId && + prev.isLoadingMessage === next.isLoadingMessage, +); -function StopGeneratingButton({ +function StopGeneratingButtonBase({ shouldStopGenerating, }: { shouldStopGenerating: boolean; @@ -977,13 +695,19 @@ function StopGeneratingButton({ ); } +const StopGeneratingButton = memo(StopGeneratingButtonBase); type FileListProps = { currentFiles: File[]; onRemoveFile: (index: number) => void; + isPending?: boolean; }; -const FileList = ({ currentFiles, onRemoveFile }: FileListProps) => { +const FileListBase = ({ + currentFiles, + onRemoveFile, + isPending, +}: FileListProps) => { const size = partial({ standard: 'jedec' }); return ( @@ -995,7 +719,10 @@ const FileList = ({ currentFiles, onRemoveFile }: FileListProps) => { key={index} >
- {getFileExt(file.name) && fileIconMap[getFileExt(file.name)] ? ( + {isPending ? ( + + ) : getFileExt(file.name) && + fileIconMap[getFileExt(file.name)] ? ( {
); }; +const FileList = memo(FileListBase, (prevProps, nextProps) => { + if (!equal(prevProps.currentFiles, nextProps.currentFiles)) return false; + if (prevProps.isPending !== nextProps.isPending) return false; + return true; +}); const DropFileActive = () => ( - wellelele + {description} diff --git a/apps/shinkai-desktop/src/components/chat/conversation-header.tsx b/apps/shinkai-desktop/src/components/chat/conversation-header.tsx index 186b5001a..e86a29f62 100644 --- a/apps/shinkai-desktop/src/components/chat/conversation-header.tsx +++ b/apps/shinkai-desktop/src/components/chat/conversation-header.tsx @@ -15,7 +15,7 @@ import { import { FilesIcon } from '@shinkai_network/shinkai-ui/assets'; import { cn } from '@shinkai_network/shinkai-ui/utils'; import { PanelRightClose, PanelRightOpen } from 'lucide-react'; -import { useEffect } from 'react'; +import { memo, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { useGetCurrentInbox } from '../../hooks/use-current-inbox'; @@ -319,4 +319,4 @@ const ConversationHeader = () => { return ; }; -export default ConversationHeader; +export default memo(ConversationHeader, () => true); diff --git a/apps/shinkai-desktop/src/components/playground-tool/components/tool-playground.tsx b/apps/shinkai-desktop/src/components/playground-tool/components/tool-playground.tsx index 8a2957a81..35fd006d7 100644 --- a/apps/shinkai-desktop/src/components/playground-tool/components/tool-playground.tsx +++ b/apps/shinkai-desktop/src/components/playground-tool/components/tool-playground.tsx @@ -474,7 +474,7 @@ function PlaygroundToolEditor({ xShinkaiToolId={xShinkaiToolId} /> @@ -578,14 +579,14 @@ function PlaygroundToolEditor({ @@ -803,9 +805,10 @@ function PlaygroundToolEditor({ {isMetadataGenerationSuccess && ( */} - {/*)}*/} + { + setSearchQuery(e.target.value); + }} + placeholder={t('common.searchPlaceholder')} + value={searchQuery} + /> + + {searchQuery && ( + + )}
@@ -414,37 +410,29 @@ const AllFiles = () => { {t('vectorFs.emptyState.noFiles')}
)} - {config.isDev && - searchQuery && + {searchQuery && isSearchVRItemsSuccess && searchVRItems?.length === 0 && (
{t('vectorFs.emptyState.noFiles')}
)} - {config.isDev && - searchQuery && + {searchQuery && isSearchVRItemsSuccess && searchQuery === debouncedSearchQuery && searchVRItems?.map((item) => { return ( ); })} diff --git a/apps/shinkai-desktop/src/main.tsx b/apps/shinkai-desktop/src/main.tsx index f9682ea9c..6735e93c6 100644 --- a/apps/shinkai-desktop/src/main.tsx +++ b/apps/shinkai-desktop/src/main.tsx @@ -1,3 +1,5 @@ +/* eslint-disable */ +// import { scan } from 'react-scan'; // import this BEFORE react import './globals.css'; import React from 'react'; @@ -5,6 +7,18 @@ import ReactDOM from 'react-dom/client'; import App from './App'; +/* + Enable react scan for performance monitoring + */ + +// if (typeof window !== 'undefined') { +// scan({ +// enabled: true, +// trackUnnecessaryRenders: true, +// // log: true, // logs render info to console (default: false) +// }); +// } + ReactDOM.createRoot(document.querySelector('#root') as HTMLElement).render( diff --git a/apps/shinkai-desktop/src/pages/add-ai.tsx b/apps/shinkai-desktop/src/pages/add-ai.tsx index e73609edd..1b52ce84d 100644 --- a/apps/shinkai-desktop/src/pages/add-ai.tsx +++ b/apps/shinkai-desktop/src/pages/add-ai.tsx @@ -29,7 +29,6 @@ import { TextField, } from '@shinkai_network/shinkai-ui'; import { cn } from '@shinkai_network/shinkai-ui/utils'; -import { Loader2 } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; @@ -242,32 +241,6 @@ const AddAIPage = () => { toolkit_permissions: [], model, }, - enableTest: false, - }); - }; - const handleTestAndSave = async (data: AddAgentFormSchema) => { - if (!auth) return; - let model = getModelObject(data.model, data.modelType); - if (isCustomModelMode && data.modelCustom && data.modelTypeCustom) { - model = getModelObject(data.modelCustom, data.modelTypeCustom); - } else if (isCustomModelType && data.modelTypeCustom) { - model = getModelObject(data.model, data.modelTypeCustom); - } - await addLLMProvider({ - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - agent: { - allowed_message_senders: [], - api_key: data.apikey, - external_url: data.externalUrl, - full_identity_name: `${auth.shinkai_identity}/${auth.profile}/agent/${data.agentName}`, - id: data.agentName, - perform_locally: false, - storage_bucket_permissions: [], - toolkit_permissions: [], - model, - }, - enableTest: true, }); }; @@ -458,38 +431,17 @@ const AddAIPage = () => { /> - {isPending ? ( -
- -
- ) : ( -
- - {currentModel !== Models.Ollama && ( - - )} -
- )} +
+ +
diff --git a/apps/shinkai-desktop/src/pages/ai-model-installation.tsx b/apps/shinkai-desktop/src/pages/ai-model-installation.tsx index 2ff73a46e..28724183c 100644 --- a/apps/shinkai-desktop/src/pages/ai-model-installation.tsx +++ b/apps/shinkai-desktop/src/pages/ai-model-installation.tsx @@ -417,7 +417,7 @@ const AIModelInstallation = ({

- - - + + + +
+ + + + +
+
+ + ); }; diff --git a/apps/shinkai-desktop/src/pages/chat/chat-conversation.tsx b/apps/shinkai-desktop/src/pages/chat/chat-conversation.tsx index 7a41c5dec..cb2a4e0f9 100644 --- a/apps/shinkai-desktop/src/pages/chat/chat-conversation.tsx +++ b/apps/shinkai-desktop/src/pages/chat/chat-conversation.tsx @@ -16,14 +16,14 @@ import { ChatConversationInfiniteData } from '@shinkai_network/shinkai-node-stat import { useGetChatConversationWithPagination } from '@shinkai_network/shinkai-node-state/v2/queries/getChatConversation/useGetChatConversationWithPagination'; import { useQueryClient } from '@tanstack/react-query'; import { produce } from 'immer'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { toast } from 'sonner'; import { MessageList } from '../../components/chat/components/message-list'; import { streamingSupportedModels } from '../../components/chat/constants'; import { useChatStore } from '../../components/chat/context/chat-context'; -import ConversationFooter from '../../components/chat/conversation-footer'; +import ConversationChatFooter from '../../components/chat/conversation-footer'; import ConversationHeader from '../../components/chat/conversation-header'; import MessageExtra from '../../components/chat/message-extra'; import { @@ -34,6 +34,7 @@ import { useGetCurrentInbox } from '../../hooks/use-current-inbox'; import { useAnalytics } from '../../lib/posthog-provider'; import { useAuth } from '../../store/auth'; import { useSettings } from '../../store/settings'; +import EmptyMessage from './empty-message'; export const useChatConversationWithOptimisticUpdates = ({ inboxId, @@ -143,8 +144,8 @@ const ChatConversation = () => { const { inboxId: encodedInboxId = '' } = useParams(); const inboxId = decodeURIComponent(encodedInboxId); - useWebSocketMessage({ inboxId, enabled: true }); - useWebSocketTools({ inboxId, enabled: true }); + useWebSocketMessage({ inboxId, enabled: !!inboxId }); + useWebSocketTools({ inboxId, enabled: !!inboxId }); const setSelectedArtifact = useChatStore( (state) => state.setSelectedArtifact, @@ -167,6 +168,16 @@ const ChatConversation = () => { inboxId, }); + const isLoadingMessage = useMemo(() => { + const lastMessage = data?.pages?.at(-1)?.at(-1); + return ( + !!inboxId && + lastMessage && + lastMessage.role === 'assistant' && + lastMessage.status.type === 'running' + ); + }, [data?.pages, inboxId]); + useEffect(() => { if (isError) { toast.error('Failed loading chat conversation', { @@ -250,22 +261,28 @@ const ChatConversation = () => { return (
- } - noMoreMessageLabel={t('chat.allMessagesLoaded')} - paginatedMessages={data} - regenerateFirstMessage={regenerateFirstMessage} - regenerateMessage={regenerateMessage} + {inboxId ? ( + } + noMoreMessageLabel={t('chat.allMessagesLoaded')} + paginatedMessages={data} + regenerateFirstMessage={regenerateFirstMessage} + regenerateMessage={regenerateMessage} + /> + ) : ( + + )} + - -
); }; diff --git a/apps/shinkai-desktop/src/pages/chat/empty-message.tsx b/apps/shinkai-desktop/src/pages/chat/empty-message.tsx index e5d893123..9fb2661f2 100644 --- a/apps/shinkai-desktop/src/pages/chat/empty-message.tsx +++ b/apps/shinkai-desktop/src/pages/chat/empty-message.tsx @@ -12,8 +12,6 @@ import { Link, useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import { useSetJobScope } from '../../components/chat/context/set-job-scope-context'; -import ConversationFooter from '../../components/chat/conversation-footer'; -import ConversationHeader from '../../components/chat/conversation-header'; import { usePromptSelectionStore } from '../../components/prompt/context/prompt-selection-context'; import { useAuth } from '../../store/auth'; import { useSettings } from '../../store/settings'; @@ -79,101 +77,98 @@ const EmptyMessage = () => { }; return ( -
- -
+ - -
- - 🤖 - +
+ + 🤖 + -

- {t('chat.emptyStateTitle')} -

-

- {t('chat.emptyStateDescription')} -

-
-
+

+ {t('chat.emptyStateTitle')} +

+

+ {t('chat.emptyStateDescription')} +

+
+
+ showSpotlightWindow()} + variant="outline" + > + Quick Ask Spotlight + + + {[ + { + text: 'Search in DuckDuckGo', + prompt: 'Search in DuckDuckGo for: ', + }, + { + text: 'Summarize a Youtube video', + prompt: 'Summarize a Youtube video: ', + }, + ].map((suggestion) => ( showSpotlightWindow()} + className="cursor-pointer justify-between bg-gray-300 py-2 text-left font-normal normal-case text-gray-50 transition-colors hover:bg-gray-200" + key={suggestion.text} + onClick={() => { + setPromptSelected({ + name: '', + prompt: suggestion.prompt, + is_enabled: true, + is_favorite: false, + is_system: true, + version: '1', + useTools: true, + rowid: 0, + }); + const element = document.querySelector( + '#chat-input', + ) as HTMLDivElement; + if (element) { + element?.focus?.(); + } + }} variant="outline" > - Quick Ask Spotlight + {suggestion.text} - {[ - { - text: 'Search in DuckDuckGo', - prompt: 'Search in DuckDuckGo for: ', - }, - { - text: 'Summarize a Youtube video', - prompt: 'Summarize a Youtube video: ', - }, - ].map((suggestion) => ( - { - setPromptSelected({ - name: '', - prompt: suggestion.prompt, - is_enabled: true, - is_favorite: false, - is_system: true, - version: '1', - useTools: true, - }); - const element = document.querySelector( - '#chat-input', - ) as HTMLDivElement; - if (element) { - element?.focus?.(); - } - }} - variant="outline" - > - {suggestion.text} - - - ))} - onCreateJob('Tell me about the Roman Empire')} - variant="outline" - > - Tell me about the Roman Empire - - -
+ ))} + onCreateJob('Tell me about the Roman Empire')} + variant="outline" + > + Tell me about the Roman Empire + + +
-
- {llmProviders.length === 0 ? ( - - {t('llmProviders.add')} - - ) : null} -
-
-
- +
+ {llmProviders.length === 0 ? ( + + {t('llmProviders.add')} + + ) : null} +
+
); }; diff --git a/apps/shinkai-desktop/src/pages/galxe-subscriptions.tsx b/apps/shinkai-desktop/src/pages/galxe-subscriptions.tsx index 134f0da18..ee3a5cf72 100644 --- a/apps/shinkai-desktop/src/pages/galxe-subscriptions.tsx +++ b/apps/shinkai-desktop/src/pages/galxe-subscriptions.tsx @@ -63,7 +63,7 @@ export const GalxeSusbcriptions = () => { const filteredSubscriptions = subscriptions ?.map((subscription) => { - const matchingFolder = subscriptionFolder?.child_folders?.find( + const matchingFolder = subscriptionFolder?.find( (folder) => folder.path.split('/')?.[2] === subscription.shared_folder.replace(/^\/+/, ''), @@ -78,8 +78,7 @@ export const GalxeSusbcriptions = () => { }) .filter((item) => !!item); - const isUserSubscribedToKnowledge = - (subscriptionFolder?.child_folders ?? [])?.length > 0; + const isUserSubscribedToKnowledge = (subscriptionFolder ?? [])?.length > 0; const inboxesWithSubscriptions: Inbox[] = inboxes.filter( (inbox) => diff --git a/apps/shinkai-desktop/src/pages/generate-code.tsx b/apps/shinkai-desktop/src/pages/generate-code.tsx deleted file mode 100644 index 0d8b2e43f..000000000 --- a/apps/shinkai-desktop/src/pages/generate-code.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { zodResolver } from '@hookform/resolvers/zod'; -import { QRSetupData } from '@shinkai_network/shinkai-message-ts/models'; -import { useCreateRegistrationCode } from '@shinkai_network/shinkai-node-state/lib/mutations/createRegistrationCode/useCreateRegistrationCode'; -import { - Button, - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - QrCodeModal, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - TextField, -} from '@shinkai_network/shinkai-ui'; -import { save } from '@tauri-apps/plugin-dialog'; -import { BaseDirectory, writeFile } from '@tauri-apps/plugin-fs'; -import { useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; - -import { useAuth } from '../store/auth'; -import { SubpageLayout } from './layout/simple-layout'; - -const saveImage = async (dataUrl: string) => { - const suggestedFilename = 'registration-code-shinkai.png'; - const filePath = await save({ - defaultPath: BaseDirectory.Download + '/' + suggestedFilename, - }); - const response = await fetch(dataUrl); - const arrayBuffer = await response.arrayBuffer(); - const uint8Array = new Uint8Array(arrayBuffer); - if (filePath) { - await writeFile(filePath, uint8Array); - } -}; - -enum IdentityType { - Profile = 'profile', - Device = 'device', -} - -enum PermissionType { - Admin = 'admin', - Standard = 'standard', - None = 'none', -} - -const generateCodeSchema = z.object({ - identityType: z.nativeEnum(IdentityType), - profile: z.string(), - permissionType: z.nativeEnum(PermissionType), -}); - -const identityTypeOptions = [IdentityType.Profile, IdentityType.Device]; -const permissionOptions = [ - PermissionType.Admin, - PermissionType.Standard, - PermissionType.None, -]; - -const GenerateCodePage = () => { - const auth = useAuth((state) => state.auth); - const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false); - const [generatedSetupData, setGeneratedSetupData] = useState< - QRSetupData | undefined - >(); - - const form = useForm>({ - resolver: zodResolver(generateCodeSchema), - defaultValues: { - profile: auth?.profile, - permissionType: PermissionType.Admin, - identityType: IdentityType.Device, - }, - }); - - const { mutateAsync: createRegistrationCode, isPending } = - useCreateRegistrationCode({ - onSuccess: (registrationCode) => { - const formValues = form.getValues(); - const setupData: QRSetupData = { - registration_code: registrationCode, - permission_type: formValues.permissionType, - identity_type: formValues.identityType, - profile: formValues.profile, - node_address: auth?.node_address ?? '', - shinkai_identity: auth?.shinkai_identity ?? '', - node_encryption_pk: auth?.node_encryption_pk ?? '', - node_signature_pk: auth?.node_signature_pk ?? '', - ...(formValues.identityType === IdentityType.Device && - formValues.profile === auth?.profile - ? { - profile_encryption_pk: auth.profile_encryption_pk, - profile_encryption_sk: auth.profile_encryption_sk, - profile_identity_pk: auth.profile_identity_pk, - profile_identity_sk: auth.profile_identity_sk, - } - : {}), - }; - setGeneratedSetupData(setupData); - setQrCodeModalOpen(true); - }, - }); - - const { identityType } = form.watch(); - - const onSubmit = async (data: z.infer) => { - await createRegistrationCode({ - nodeAddress: auth?.node_address ?? '', - permissionsType: data.permissionType, - identityType: data.identityType, - setupPayload: { - my_device_encryption_sk: auth?.my_device_encryption_sk ?? '', - my_device_identity_sk: auth?.my_device_identity_sk ?? '', - profile_encryption_sk: auth?.profile_encryption_sk ?? '', - profile_identity_sk: auth?.profile_identity_sk ?? '', - node_encryption_pk: auth?.node_encryption_pk ?? '', - permission_type: auth?.permission_type ?? '', - registration_name: auth?.registration_name ?? '', - profile: auth?.profile ?? '', - shinkai_identity: auth?.shinkai_identity ?? '', - node_address: auth?.node_address ?? '', - //TODO: remove from network components these unused params - registration_code: '', - identity_type: '', - }, - profileName: data.profile, - }); - }; - useEffect(() => { - if (form.getValues().profile) { - return; - } - form.setValue('profile', auth?.profile ?? ''); - }, [auth, form]); - return ( - -
- -
- {/* TODO: Re enable identity type selector later, Profiles probably won't be relevant to any frontend experiences for the next 6+ months @Rob */} - {false && ( - ( - - Select Identity Type - - - - )} - /> - )} - - {identityType === IdentityType.Device && ( - ( - - )} - /> - )} - - ( - - Select Permission Type - - - )} - /> -
- -
- - saveImage(dataUrl)} - open={qrCodeModalOpen} - title="Here's your QR Code" - value={JSON.stringify(generatedSetupData)} - /> -
- ); -}; - -export default GenerateCodePage; diff --git a/apps/shinkai-desktop/src/pages/layout/main-layout.tsx b/apps/shinkai-desktop/src/pages/layout/main-layout.tsx index 83b279a52..1242432e9 100644 --- a/apps/shinkai-desktop/src/pages/layout/main-layout.tsx +++ b/apps/shinkai-desktop/src/pages/layout/main-layout.tsx @@ -357,16 +357,16 @@ export function MainNav() { href: '/tools', icon: , }, + { + title: 'Scheduled Tasks', + href: '/tasks', + icon: , + }, optInExperimental && { title: 'Shinkai Sheet', href: '/sheets', icon: , }, - optInExperimental && { - title: 'Scheduled Tasks', - href: '/tasks', - icon: , - }, ].filter(Boolean) as NavigationLink[]; const footerNavigationLinks = [ @@ -403,82 +403,79 @@ export function MainNav() { )} - + + + + + + + {t('layout.sidebar.toggle')} + + + + + +
+
+ {!isGetStartedChecklistHidden && } + - + + + {sidebarExpanded && ( + + {t('chat.create')} + + )} + + - - {t('layout.sidebar.toggle')} + + {t('chat.create')} +
+ ⌘ + N +
- -
- -
-
- {!isGetStartedChecklistHidden && } - - - - navigate('/inboxes')} - transition={{ duration: 0.3 }} - whileHover={{ scale: !sidebarExpanded ? 1.05 : 1 }} - > - - - {sidebarExpanded && ( - - {t('chat.create')} - - )} - - - - - - {t('chat.create')} -
- ⌘ - N -
-
-
-
-
{navigationLinks.map((item) => { return ( diff --git a/apps/shinkai-desktop/src/pages/layout/settings-layout.tsx b/apps/shinkai-desktop/src/pages/layout/settings-layout.tsx index d57c658e7..8b5976430 100644 --- a/apps/shinkai-desktop/src/pages/layout/settings-layout.tsx +++ b/apps/shinkai-desktop/src/pages/layout/settings-layout.tsx @@ -10,7 +10,6 @@ import { import { ExportIcon, PromptLibraryIcon, - QrIcon, } from '@shinkai_network/shinkai-ui/assets'; import { cn } from '@shinkai_network/shinkai-ui/utils'; import { @@ -127,11 +126,6 @@ export function MainNav() { href: '/settings/export-connection', icon: , }, - { - title: t('settings.layout.createRegistrationCode'), - href: '/settings/generate-code', - icon: , - }, { title: t('settings.layout.publicKeys'), href: '/settings/public-keys', diff --git a/apps/shinkai-desktop/src/pages/prompt-library.tsx b/apps/shinkai-desktop/src/pages/prompt-library.tsx index 5c5b4ca32..86194d968 100644 --- a/apps/shinkai-desktop/src/pages/prompt-library.tsx +++ b/apps/shinkai-desktop/src/pages/prompt-library.tsx @@ -93,7 +93,7 @@ export const PromptLibrary = () => { ); }} > - @@ -348,6 +348,7 @@ function PromptPreview({ onCancel={() => setEditing(false)} onSave={async () => await updatePrompt({ + id: selectedPrompt.rowid, nodeAddress: auth?.node_address ?? '', token: auth?.api_v2_key ?? '', promptName: selectedPrompt.name, @@ -408,19 +409,21 @@ export const PromptEditor = ({ />
+ refetch()} + > + + Upcoming {

Upcoming Tasks

+ + + { + enableAllTools({ + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + }); + }} + > + + Enable All Tools + + { + disableAllTools({ + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + }); + }} + > + + Disable All Tools + + +
} title={t('tools.label')} > -
- { - setSearchQuery(e.target.value); - }} - placeholder="Search..." - spellCheck={false} - value={searchQuery} - /> - - {searchQuery && ( - - )} -
-
- {(isPending || !isSearchQuerySynced || isSearchToolListPending) && - Array.from({ length: 8 }).map((_, idx) => ( -
+ + {searchQuery && ( + + )} +
+
+ {(isPending || !isSearchQuerySynced || isSearchToolListPending) && + Array.from({ length: 8 }).map((_, idx) => ( +
+
+ +
+ + +
+ +
- - -
- ))} - {!searchQuery && - isSearchQuerySynced && - toolsList?.map((tool) => ( -
-
-
- - {formatText(tool.name)}{' '} - - - {getVersionFromTool(tool)} - - {tool.author !== '@@official.shinkai' && ( + ))} + {!searchQuery && + isSearchQuerySynced && + toolsList?.map((tool) => ( +
+
+
+ + {formatText(tool.name)}{' '} + - {tool.author} + {getVersionFromTool(tool)} - )} + {tool.author !== '@@official.shinkai' && ( + + {tool.author} + + )} +
+

+ {tool.description} +

-

- {tool.description} -

+ + + {t('common.configure')} + + + + { + await updateTool({ + toolKey: tool.tool_router_key, + toolType: tool.tool_type, + toolPayload: {} as ShinkaiTool, + isToolEnabled: !tool.enabled, + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + }); + }} + /> + + + + {t('common.enabled')} + + +
- ( +
- - {t('common.configure')} - - - - { - await updateTool({ - toolKey: tool.tool_router_key, - toolType: tool.tool_type, - toolPayload: {} as ShinkaiTool, - isToolEnabled: !tool.enabled, - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - }); - }} - /> - - - - {t('common.enabled')} - - - -
- ))} - {searchQuery && - isSearchQuerySynced && - searchToolList?.map((tool) => ( -
-
-
- - {formatText(tool.name)}{' '} - - - {getVersionFromTool(tool)} - - {tool.author !== '@@official.shinkai' && ( +
+
+ + {formatText(tool.name)}{' '} + - {tool.author} + {getVersionFromTool(tool)} - )} + {tool.author !== '@@official.shinkai' && ( + + {tool.author} + + )} +
+

+ {tool.description} +

-

- {tool.description} -

-
- - - {t('common.configure')} - + + + {t('common.configure')} + - - - { - await updateTool({ - toolKey: tool.tool_router_key, - toolType: tool.tool_type, - toolPayload: {} as ShinkaiTool, - isToolEnabled: !tool.enabled, - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - }); - }} - /> - - - - {t('common.enabled')} - - - -
- ))} - {searchQuery && isSearchQuerySynced && searchToolList?.length === 0 && ( -
-

- {t('tools.emptyState.search.text')} -

-
- )} + + + { + await updateTool({ + toolKey: tool.tool_router_key, + toolType: tool.tool_type, + toolPayload: {} as ShinkaiTool, + isToolEnabled: !tool.enabled, + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + }); + }} + /> + + + + {t('common.enabled')} + + + +
+ ))} + {searchQuery && + isSearchQuerySynced && + searchToolList?.length === 0 && ( +
+

+ {t('tools.emptyState.search.text')} +

+
+ )} +
); @@ -324,15 +398,17 @@ function ImportToolModal() { return ( - - + + + Import Tool Import Tool diff --git a/apps/shinkai-desktop/src/routes/index.tsx b/apps/shinkai-desktop/src/routes/index.tsx index b37f35959..c416fe4ad 100644 --- a/apps/shinkai-desktop/src/routes/index.tsx +++ b/apps/shinkai-desktop/src/routes/index.tsx @@ -33,7 +33,6 @@ import AIsPage from '../pages/ais'; import AnalyticsPage from '../pages/analytics'; import AnalyticsSettingsPage from '../pages/analytics-settings'; import ChatConversation from '../pages/chat/chat-conversation'; -import EmptyMessage from '../pages/chat/empty-message'; import ChatLayout from '../pages/chat/layout'; import { ConnectMethodQrCodePage } from '../pages/connect-method-qr-code'; import CreateChatPage from '../pages/create-chat'; @@ -45,7 +44,6 @@ import EditToolPage from '../pages/edit-tool'; import { ExportConnection } from '../pages/export-connection'; import FreeSubscriptionsPage from '../pages/free-subscription'; import { GalxeValidation } from '../pages/galxe-validation'; -import GenerateCodePage from '../pages/generate-code'; import GetStartedPage from '../pages/get-started'; import MainLayout from '../pages/layout/main-layout'; import OnboardingLayout from '../pages/layout/onboarding-layout'; @@ -212,7 +210,9 @@ const AppRoutes = () => { - + + + @@ -221,15 +221,8 @@ const AppRoutes = () => { } path="inboxes" > - } index /> - - - - } - path=":inboxId" - /> + } index /> + } path=":inboxId" /> { path={'settings'} > } index /> - } path={'generate-code'} /> } path={'export-connection'} /> } path={'public-keys'} /> { + const response = await httpClient.post( + urlJoin(nodeAddress, '/v2/enable_all_tools'), + {}, + { + headers: { Authorization: `Bearer ${bearerToken}` }, + responseType: 'json', + }, + ); + return response.data; +}; + +export const disableAllTools = async ( + nodeAddress: string, + bearerToken: string, +) => { + const response = await httpClient.post( + urlJoin(nodeAddress, '/v2/disable_all_tools'), + {}, + { headers: { Authorization: `Bearer ${bearerToken}` } }, + ); + return response.data; +}; diff --git a/libs/shinkai-message-ts/src/api/tools/types.ts b/libs/shinkai-message-ts/src/api/tools/types.ts index 9fe5fff70..145bda82e 100644 --- a/libs/shinkai-message-ts/src/api/tools/types.ts +++ b/libs/shinkai-message-ts/src/api/tools/types.ts @@ -163,18 +163,20 @@ export type PayInvoiceRequest = { export type PayInvoiceResponse = any; export type Prompt = { + rowid: number; name: string; prompt: string; is_system: boolean; is_enabled: boolean; version: string; is_favorite: boolean; - embedding?: string; - useTools?: boolean; + useTools?: boolean; // flag for prompt templates }; +export type CreatePrompt = Omit; + export type GetAllPromptsResponse = Prompt[]; export type SearchPromptsResponse = Prompt[]; -export type CreatePromptRequest = Prompt; +export type CreatePromptRequest = CreatePrompt; export type CreatePromptResponse = Prompt; export type UpdatePromptRequest = Prompt; export type DeletePromptRequest = { @@ -344,3 +346,13 @@ export type SetOAuthTokenResponse = { message: string; status: string; }; +export type EnableAllToolsResponse = { + [toolKey: string]: { + activated: boolean; + }; +}; +export type DisableAllToolsResponse = { + [toolKey: string]: { + activated: boolean; + }; +}; diff --git a/libs/shinkai-message-ts/src/api/vector-fs/index.ts b/libs/shinkai-message-ts/src/api/vector-fs/index.ts index 013e6d5c9..25d42ec5d 100644 --- a/libs/shinkai-message-ts/src/api/vector-fs/index.ts +++ b/libs/shinkai-message-ts/src/api/vector-fs/index.ts @@ -9,6 +9,8 @@ import { CreateFolderResponse, GetListDirectoryContentsRequest, GetListDirectoryContentsResponse, + GetSearchDirectoryContentsRequest, + GetSearchDirectoryContentsResponse, MoveFolderRequest, MoveFolderResponse, MoveFsItemRequest, @@ -35,6 +37,22 @@ export const getListDirectoryContents = async ( return response.data as GetListDirectoryContentsResponse; }; +export const getSearchDirectoryContents = async ( + nodeAddress: string, + bearerToken: string, + payload: GetSearchDirectoryContentsRequest, +) => { + const response = await httpClient.get( + urlJoin(nodeAddress, '/v2/search_files_by_name'), + { + params: payload, + headers: { Authorization: `Bearer ${bearerToken}` }, + responseType: 'json', + }, + ); + return response.data as GetSearchDirectoryContentsResponse; +}; + export const createFolder = async ( nodeAddress: string, bearerToken: string, diff --git a/libs/shinkai-message-ts/src/api/vector-fs/types.ts b/libs/shinkai-message-ts/src/api/vector-fs/types.ts index 5ded43ee7..b5c23a799 100644 --- a/libs/shinkai-message-ts/src/api/vector-fs/types.ts +++ b/libs/shinkai-message-ts/src/api/vector-fs/types.ts @@ -15,7 +15,10 @@ export type DirectoryContent = { }; export type GetListDirectoryContentsResponse = DirectoryContent[]; - +export type GetSearchDirectoryContentsRequest = { + name: string; +}; +export type GetSearchDirectoryContentsResponse = DirectoryContent[]; export type MoveFolderRequest = { origin_path: string; destination_path: string; diff --git a/libs/shinkai-message-ts/src/http-client.ts b/libs/shinkai-message-ts/src/http-client.ts index f788b5018..50e00aa57 100644 --- a/libs/shinkai-message-ts/src/http-client.ts +++ b/libs/shinkai-message-ts/src/http-client.ts @@ -1,5 +1,5 @@ import axios from 'axios'; export const httpClient = axios.create({ - timeout: 2 * 60 * 1000, // 2 minutes + timeout: 10 * 60 * 1000, // 10 minutes }); diff --git a/libs/shinkai-message-ts/src/wasm/FileUploaderUsingSymmetricKeyManager.ts b/libs/shinkai-message-ts/src/wasm/FileUploaderUsingSymmetricKeyManager.ts deleted file mode 100644 index 315b2067a..000000000 --- a/libs/shinkai-message-ts/src/wasm/FileUploaderUsingSymmetricKeyManager.ts +++ /dev/null @@ -1,214 +0,0 @@ -import axios from 'axios'; - -import { httpClient } from '../http-client'; -import { ShinkaiMessage } from '../models'; -import { calculate_blake3_hash } from '../pkg/shinkai_message_wasm'; -import { urlJoin } from '../utils/url-join'; -import { InboxNameWrapper } from './InboxNameWrapper'; -import { ShinkaiMessageBuilderWrapper } from './ShinkaiMessageBuilderWrapper'; - -export class FileUploader { - private base_url: string; - private my_encryption_secret_key: string; - private my_signature_secret_key: string; - private receiver_public_key: string; - private sender: string; - private sender_subidentity: string; - private receiver: string; - private job_id: string | undefined; - private job_inbox: string; - private symmetric_key: CryptoKey | null; - private folder_id: string | null; - - constructor( - base_url: string, - my_encryption_secret_key: string, - my_signature_secret_key: string, - receiver_public_key: string, - job_inbox: string, - sender: string, - sender_subidentity: string, - receiver: string, - ) { - this.base_url = base_url; - this.my_encryption_secret_key = my_encryption_secret_key; - this.my_signature_secret_key = my_signature_secret_key; - this.receiver_public_key = receiver_public_key; - if (job_inbox) { - const inbox = new InboxNameWrapper(job_inbox); - this.job_id = inbox.get_unique_id; - } - this.job_inbox = job_inbox; - this.sender = sender; - this.sender_subidentity = sender_subidentity; - this.receiver = receiver; - this.symmetric_key = null; - this.folder_id = null; - } - - async calculateHashFromSymmetricKey(): Promise { - if (!this.symmetric_key) { - throw new Error('Symmetric key is not set'); - } - - const rawKey = await window.crypto.subtle.exportKey( - 'raw', - this.symmetric_key, - ); - const rawKeyArray = new Uint8Array(rawKey); - const rawKeyString = Array.from(rawKeyArray) - .map((b) => b.toString(16).padStart(2, '0')) - .join(''); - const hash = calculate_blake3_hash(rawKeyString); - - return hash; - } - - async createFolder(): Promise { - const keyData = window.crypto.getRandomValues(new Uint8Array(32)); - this.symmetric_key = await window.crypto.subtle.importKey( - 'raw', - keyData, - 'AES-GCM', - true, - ['encrypt', 'decrypt'], - ); - - // Export symmetric key - const exportedKey = await window.crypto.subtle.exportKey( - 'raw', - this.symmetric_key, - ); - const exportedKeyArray = new Uint8Array(exportedKey); - const exportedKeyString = Array.from(exportedKeyArray) - .map((b) => b.toString(16).padStart(2, '0')) - .join(''); - - const message = - ShinkaiMessageBuilderWrapper.send_create_files_inbox_with_sym_key( - this.my_encryption_secret_key, - this.my_signature_secret_key, - this.receiver_public_key, - this.job_inbox, - exportedKeyString, - this.sender, - this.sender_subidentity, - this.receiver, - ); - - await httpClient.post( - urlJoin(this.base_url, '/v1/create_files_inbox_with_symmetric_key'), - message, - { - headers: { - 'Content-Type': 'application/json', - }, - responseType: 'json', - timeout: 5 * 60 * 1000, // 5 minutes - }, - ); - - this.folder_id = await this.calculateHashFromSymmetricKey(); - return this.folder_id; - } - - async uploadEncryptedFile(file: File, filename?: string): Promise { - if (!this.symmetric_key) { - throw new Error('Symmetric key is not set'); - } - - const iv = window.crypto.getRandomValues(new Uint8Array(12)); - const algorithm = { - name: 'AES-GCM', - iv, - }; - const fileData = await file.arrayBuffer(); - const encryptedFileData = await window.crypto.subtle.encrypt( - algorithm, - this.symmetric_key, - fileData, - ); - - const hash = await this.calculateHashFromSymmetricKey(); - const nonce = Array.from(iv) - .map((b) => b.toString(16).padStart(2, '0')) - .join(''); - - const formData = new FormData(); - formData.append( - 'file', - new Blob([encryptedFileData]), - filename || file.name, - ); - - await httpClient.post( - urlJoin( - this.base_url, - '/v1/add_file_to_inbox_with_symmetric_key', - hash, - nonce, - ), - formData, - ); - } - - async finalizeAndSend( - content: string, - parent: string | null, - ): Promise { - if (!this.job_id) { - throw new Error(`finalizeAndSend: job_id not found`); - } - const messageStr = ShinkaiMessageBuilderWrapper.job_message( - this.job_id, - content, - this.folder_id || '', - parent, - this.my_encryption_secret_key, - this.my_signature_secret_key, - this.receiver_public_key, - this.sender, - this.sender_subidentity, - this.receiver, - this.sender_subidentity, - ); - - const message = JSON.parse(messageStr); - - const response = await httpClient.post( - urlJoin(this.base_url, '/v1/job_message'), - message, - { - responseType: 'json', - }, - ); - return response.data; - } - - async finalizeAndAddItemsToDb( - destinationPath = '/', - ): Promise<{ status: string }> { - const messageStr = ShinkaiMessageBuilderWrapper.createItems( - this.my_encryption_secret_key, - this.my_signature_secret_key, - this.receiver_public_key, - destinationPath, - this.folder_id || '', - this.sender, - this.sender_subidentity, - this.receiver, - ); - - const message = JSON.parse(messageStr); - - const response = await axios.post( - urlJoin(this.base_url, '/v1/vec_fs/convert_files_and_save_to_folder'), - message, - { - timeout: Number.MAX_SAFE_INTEGER, - headers: { 'Content-Type': 'application/json' }, - }, - ); - return response.data; - } -} diff --git a/libs/shinkai-node-state/src/forms/chat/chat-message.ts b/libs/shinkai-node-state/src/forms/chat/chat-message.ts index 8f9adaf8f..ca2c0c060 100644 --- a/libs/shinkai-node-state/src/forms/chat/chat-message.ts +++ b/libs/shinkai-node-state/src/forms/chat/chat-message.ts @@ -1,8 +1,9 @@ import { z } from 'zod'; export const chatMessageFormSchema = z.object({ + agent: z.string().optional(), message: z.string().min(1), - files: z.array(z.any()).max(3).optional(), + files: z.array(z.any()).optional(), tool: z .object({ key: z.string().min(1), diff --git a/libs/shinkai-node-state/src/forms/chat/create-job.ts b/libs/shinkai-node-state/src/forms/chat/create-job.ts index 3da279946..8fca75ad0 100644 --- a/libs/shinkai-node-state/src/forms/chat/create-job.ts +++ b/libs/shinkai-node-state/src/forms/chat/create-job.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; export const createJobFormSchema = z.object({ agent: z.string().min(1), message: z.string().min(1), - files: z.array(z.any()).max(3), + files: z.array(z.any()), tool: z .object({ key: z.string().min(1), diff --git a/libs/shinkai-node-state/src/v2/mutations/addLLMProvider/index.ts b/libs/shinkai-node-state/src/v2/mutations/addLLMProvider/index.ts index 2d987c72f..e17e2b33f 100644 --- a/libs/shinkai-node-state/src/v2/mutations/addLLMProvider/index.ts +++ b/libs/shinkai-node-state/src/v2/mutations/addLLMProvider/index.ts @@ -11,7 +11,7 @@ export const addLLMProvider = async ({ agent, enableTest, }: AddLLMProviderInput) => { - if (!agent.model.Ollama && enableTest) { + if (!agent.model.Ollama || enableTest) { await testLLMProvider(nodeAddress, token, agent); } const data = await addLLMProviderAPI(nodeAddress, token, agent); diff --git a/libs/shinkai-node-state/src/v2/mutations/disableAllTools/index.ts b/libs/shinkai-node-state/src/v2/mutations/disableAllTools/index.ts new file mode 100644 index 000000000..0f47d31cb --- /dev/null +++ b/libs/shinkai-node-state/src/v2/mutations/disableAllTools/index.ts @@ -0,0 +1,10 @@ +import { disableAllTools as disableAllToolsApi } from '@shinkai_network/shinkai-message-ts/api/tools/index'; + +import { DisableAllToolsInput } from './types'; + +export const disableAllTools = async ({ + nodeAddress, + token, +}: DisableAllToolsInput) => { + return await disableAllToolsApi(nodeAddress, token); +}; diff --git a/libs/shinkai-node-state/src/v2/mutations/disableAllTools/types.ts b/libs/shinkai-node-state/src/v2/mutations/disableAllTools/types.ts new file mode 100644 index 000000000..d9e2e5f09 --- /dev/null +++ b/libs/shinkai-node-state/src/v2/mutations/disableAllTools/types.ts @@ -0,0 +1,8 @@ +import { Token } from '@shinkai_network/shinkai-message-ts/api/general/types'; +import { DisableAllToolsResponse } from '@shinkai_network/shinkai-message-ts/api/tools/types'; + +export type DisableAllToolsInput = Token & { + nodeAddress: string; +}; + +export type DisableAllToolsOutput = DisableAllToolsResponse; diff --git a/libs/shinkai-node-state/src/v2/mutations/disableAllTools/useDisableAllTools.ts b/libs/shinkai-node-state/src/v2/mutations/disableAllTools/useDisableAllTools.ts new file mode 100644 index 000000000..ae475058a --- /dev/null +++ b/libs/shinkai-node-state/src/v2/mutations/disableAllTools/useDisableAllTools.ts @@ -0,0 +1,33 @@ +import { + useMutation, + type UseMutationOptions, + useQueryClient, +} from '@tanstack/react-query'; + +import { FunctionKey } from '../../../lib/constants'; +import { APIError } from '../../types'; +import { disableAllTools } from '.'; +import { DisableAllToolsInput, DisableAllToolsOutput } from './types'; + +type Options = UseMutationOptions< + DisableAllToolsOutput, + APIError, + DisableAllToolsInput +>; + +export const useDisableAllTools = (options?: Options) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: disableAllTools, + ...options, + onSuccess: (response, variables, context) => { + queryClient.invalidateQueries({ + queryKey: [FunctionKey.GET_LIST_TOOLS], + }); + if (options?.onSuccess) { + options.onSuccess(response, variables, context); + } + }, + }); +}; diff --git a/libs/shinkai-node-state/src/v2/mutations/enableAllTools/index.ts b/libs/shinkai-node-state/src/v2/mutations/enableAllTools/index.ts new file mode 100644 index 000000000..3b118845d --- /dev/null +++ b/libs/shinkai-node-state/src/v2/mutations/enableAllTools/index.ts @@ -0,0 +1,10 @@ +import { enableAllTools as enableAllToolsApi } from '@shinkai_network/shinkai-message-ts/api/tools/index'; + +import { EnableAllToolsInput } from './types'; + +export const enableAllTools = async ({ + nodeAddress, + token, +}: EnableAllToolsInput) => { + return await enableAllToolsApi(nodeAddress, token); +}; diff --git a/libs/shinkai-node-state/src/v2/mutations/enableAllTools/types.ts b/libs/shinkai-node-state/src/v2/mutations/enableAllTools/types.ts new file mode 100644 index 000000000..f52194af0 --- /dev/null +++ b/libs/shinkai-node-state/src/v2/mutations/enableAllTools/types.ts @@ -0,0 +1,8 @@ +import { Token } from '@shinkai_network/shinkai-message-ts/api/general/types'; +import { EnableAllToolsResponse } from '@shinkai_network/shinkai-message-ts/api/tools/types'; + +export type EnableAllToolsInput = Token & { + nodeAddress: string; +}; + +export type EnableAllToolsOutput = EnableAllToolsResponse; diff --git a/libs/shinkai-node-state/src/v2/mutations/enableAllTools/useEnableAllTools.ts b/libs/shinkai-node-state/src/v2/mutations/enableAllTools/useEnableAllTools.ts new file mode 100644 index 000000000..e0589fb28 --- /dev/null +++ b/libs/shinkai-node-state/src/v2/mutations/enableAllTools/useEnableAllTools.ts @@ -0,0 +1,33 @@ +import { + useMutation, + type UseMutationOptions, + useQueryClient, +} from '@tanstack/react-query'; + +import { FunctionKey } from '../../../lib/constants'; +import { APIError } from '../../types'; +import { enableAllTools } from '.'; +import { EnableAllToolsInput, EnableAllToolsOutput } from './types'; + +type Options = UseMutationOptions< + EnableAllToolsOutput, + APIError, + EnableAllToolsInput +>; + +export const useEnableAllTools = (options?: Options) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: enableAllTools, + ...options, + onSuccess: (response, variables, context) => { + queryClient.invalidateQueries({ + queryKey: [FunctionKey.GET_LIST_TOOLS], + }); + if (options?.onSuccess) { + options.onSuccess(response, variables, context); + } + }, + }); +}; diff --git a/libs/shinkai-node-state/src/v2/mutations/updatePrompt/index.ts b/libs/shinkai-node-state/src/v2/mutations/updatePrompt/index.ts index 6b348daf3..274944382 100644 --- a/libs/shinkai-node-state/src/v2/mutations/updatePrompt/index.ts +++ b/libs/shinkai-node-state/src/v2/mutations/updatePrompt/index.ts @@ -11,6 +11,7 @@ export const updatePrompt = async ({ isPromptEnabled, isPromptSystem, promptVersion, + id, }: UpdatePromptInput) => { return await updatePromptApi(nodeAddress, token, { is_favorite: isPromptFavorite, @@ -19,5 +20,6 @@ export const updatePrompt = async ({ is_enabled: isPromptEnabled, is_system: isPromptSystem, version: promptVersion, + rowid: id, }); }; diff --git a/libs/shinkai-node-state/src/v2/mutations/updatePrompt/types.ts b/libs/shinkai-node-state/src/v2/mutations/updatePrompt/types.ts index fdbb80f93..dc0b87ae8 100644 --- a/libs/shinkai-node-state/src/v2/mutations/updatePrompt/types.ts +++ b/libs/shinkai-node-state/src/v2/mutations/updatePrompt/types.ts @@ -6,6 +6,7 @@ export type UpdatePromptOutput = { export type UpdatePromptInput = Token & { nodeAddress: string; + id: number; promptName: string; promptContent: string; isPromptFavorite: boolean; diff --git a/libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/index.ts b/libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/index.ts new file mode 100644 index 000000000..f423d5a23 --- /dev/null +++ b/libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/index.ts @@ -0,0 +1,14 @@ +import { getSearchDirectoryContents as getSearchDirectoryContentsApi } from '@shinkai_network/shinkai-message-ts/api/vector-fs/index'; + +import { GetSearchDirectoryContentsInput } from './types'; + +export const getSearchDirectoryContents = async ({ + nodeAddress, + token, + name, +}: GetSearchDirectoryContentsInput) => { + const response = await getSearchDirectoryContentsApi(nodeAddress, token, { + name, + }); + return response; +}; diff --git a/libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/types.ts b/libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/types.ts new file mode 100644 index 000000000..b8b51bce9 --- /dev/null +++ b/libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/types.ts @@ -0,0 +1,9 @@ +import { Token } from '@shinkai_network/shinkai-message-ts/api/general/types'; +import { GetSearchDirectoryContentsResponse } from '@shinkai_network/shinkai-message-ts/api/vector-fs/types'; + +export type GetSearchDirectoryContentsInput = Token & { + nodeAddress: string; + name: string; +}; +export type GetSearchDirectoryContentsOutput = + GetSearchDirectoryContentsResponse; diff --git a/libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/useGetSearchDirectoryContents.ts b/libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/useGetSearchDirectoryContents.ts new file mode 100644 index 000000000..bfe7efd01 --- /dev/null +++ b/libs/shinkai-node-state/src/v2/queries/getSearchDirectoryContents/useGetSearchDirectoryContents.ts @@ -0,0 +1,33 @@ +import { QueryObserverOptions, useQuery } from '@tanstack/react-query'; + +import { FunctionKeyV2 } from '../../constants'; +import { getSearchDirectoryContents } from './index'; +import { + GetSearchDirectoryContentsInput, + GetSearchDirectoryContentsOutput, +} from './types'; + +export type UseGetSearchDirectoryContents = [ + FunctionKeyV2.GET_VR_FILES_SEARCH, + GetSearchDirectoryContentsInput, +]; + +type Options = QueryObserverOptions< + GetSearchDirectoryContentsOutput, + Error, + GetSearchDirectoryContentsOutput, + GetSearchDirectoryContentsOutput, + UseGetSearchDirectoryContents +>; + +export const useGetSearchDirectoryContents = ( + input: GetSearchDirectoryContentsInput, + options?: Omit, +) => { + const response = useQuery({ + queryKey: [FunctionKeyV2.GET_VR_FILES_SEARCH, input], + queryFn: () => getSearchDirectoryContents(input), + ...options, + }); + return response; +}; diff --git a/libs/shinkai-ui/src/components/button.tsx b/libs/shinkai-ui/src/components/button.tsx index 51f27af3c..947f88b97 100644 --- a/libs/shinkai-ui/src/components/button.tsx +++ b/libs/shinkai-ui/src/components/button.tsx @@ -6,14 +6,14 @@ import * as React from 'react'; import { cn } from '../utils'; const buttonVariants = cva( - 'inline-flex items-center justify-center rounded-full text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-gray-50', + 'inline-flex items-center justify-center gap-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-gray-50', { variants: { variant: { default: 'bg-brand hover:bg-brand-500 text-white disabled:bg-gray-200 disabled:text-gray-100', destructive: - 'bg-red-500 text-gray-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/90 disabled:bg-gray-200 disabled:text-gray-100', + 'bg-red-500 text-gray-50 shadow-sm hover:bg-red-500/90 disabled:bg-gray-200 disabled:text-gray-100 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/90', outline: 'hover:bg-gray-350 border border-gray-200 bg-transparent shadow-sm hover:text-gray-50', secondary: @@ -26,15 +26,21 @@ const buttonVariants = cva( }, size: { default: 'h-[54px] px-8 py-4 text-base', + xs: 'h-[34px] px-3 py-1 text-xs', sm: 'h-[40px] px-3 py-3 text-xs', lg: 'h-[54px] px-2 py-4 text-base', icon: 'h-9 w-9', auto: 'h-auto p-4', }, + rounded: { + full: 'rounded-full', + lg: 'rounded-lg', + }, }, defaultVariants: { variant: 'default', size: 'default', + rounded: 'full', }, }, ); @@ -48,13 +54,21 @@ export interface ButtonProps const Button = React.forwardRef( ( - { className, variant, size, asChild = false, isLoading = false, ...props }, + { + className, + variant, + size, + rounded, + asChild = false, + isLoading = false, + ...props + }, ref, ) => { const Comp = asChild ? Slot : 'button'; return ( diff --git a/libs/shinkai-ui/src/components/chat/message-list.tsx b/libs/shinkai-ui/src/components/chat/message-list.tsx index b25c4040b..f7832df14 100644 --- a/libs/shinkai-ui/src/components/chat/message-list.tsx +++ b/libs/shinkai-ui/src/components/chat/message-list.tsx @@ -5,6 +5,7 @@ import { } from '@tanstack/react-query'; import React, { Fragment, + memo, RefObject, useCallback, useEffect, diff --git a/libs/shinkai-ui/src/components/markdown-preview.tsx b/libs/shinkai-ui/src/components/markdown-preview.tsx index c3f7ed88b..0d05fdc50 100644 --- a/libs/shinkai-ui/src/components/markdown-preview.tsx +++ b/libs/shinkai-ui/src/components/markdown-preview.tsx @@ -9,6 +9,7 @@ import React, { FC, forwardRef, ForwardRefExoticComponent, + memo, ReactNode, RefAttributes, useContext, @@ -508,7 +509,7 @@ export type MakeMarkdownTextProps = MarkdownTextPrimitiveProps & { isRunning?: boolean; }; -export const MarkdownText = ({ +export const MarkdownTextBase = ({ className, isRunning, components: userComponents, @@ -531,3 +532,8 @@ export const MarkdownText = ({ /> ); }; +export const MarkdownText = memo( + MarkdownTextBase, + (prev, next) => + prev.content === next.content && prev.isRunning === next.isRunning, +); diff --git a/libs/shinkai-ui/src/components/rjsf/SubmitButton/SubmitButton.tsx b/libs/shinkai-ui/src/components/rjsf/SubmitButton/SubmitButton.tsx index 1b57c0f4f..941c0d090 100644 --- a/libs/shinkai-ui/src/components/rjsf/SubmitButton/SubmitButton.tsx +++ b/libs/shinkai-ui/src/components/rjsf/SubmitButton/SubmitButton.tsx @@ -29,10 +29,11 @@ export default function SubmitButton<