From d67b6419b56b3006f7719935ee5b4e22022b40d8 Mon Sep 17 00:00:00 2001 From: Service Account Date: Thu, 26 Oct 2023 02:00:59 +0000 Subject: [PATCH] janhq/jan: Update tag build 1.0.14 for inference-plugin --- web/app/_components/HistoryItem/index.tsx | 40 ++++-- web/app/_components/HistoryList/index.tsx | 3 +- web/app/_components/InputToolbar/index.tsx | 89 ++++++------ .../SwitchingModelConfirmationModal/index.tsx | 129 ++++++++++++++++++ web/containers/Providers/index.tsx | 20 +-- web/helpers/EventHandler.tsx | 23 +++- web/helpers/ModalWrapper.tsx | 2 + web/helpers/atoms/Modal.atom.ts | 5 + web/hooks/useChatMessages.ts | 18 ++- web/hooks/useCreateConversation.ts | 7 +- web/hooks/useGetInputState.ts | 52 +++++++ web/hooks/useGetModelById.ts | 23 ++++ web/hooks/useGetUserConversations.ts | 7 + web/hooks/useStartStopModel.ts | 23 ++-- web/models/ChatMessage.ts | 15 +- 15 files changed, 357 insertions(+), 99 deletions(-) create mode 100644 web/app/_components/SwitchingModelConfirmationModal/index.tsx create mode 100644 web/hooks/useGetInputState.ts create mode 100644 web/hooks/useGetModelById.ts diff --git a/web/app/_components/HistoryItem/index.tsx b/web/app/_components/HistoryItem/index.tsx index ebf0a4f84..e1e959c88 100644 --- a/web/app/_components/HistoryItem/index.tsx +++ b/web/app/_components/HistoryItem/index.tsx @@ -1,6 +1,5 @@ import React from 'react' import { useAtomValue, useSetAtom } from 'jotai' -import { ModelManagementService } from '@janhq/core' import { getActiveConvoIdAtom, setActiveConvoIdAtom, @@ -12,11 +11,13 @@ import { } from '@helpers/atoms/MainView.atom' import { displayDate } from '@utils/datetime' import { twMerge } from 'tailwind-merge' -import { executeSerial } from '@services/pluginService' +import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' +import { switchingModelConfirmationModalPropsAtom } from '@helpers/atoms/Modal.atom' +import useStartStopModel from '@hooks/useStartStopModel' +import useGetModelById from '@hooks/useGetModelById' type Props = { conversation: Conversation - avatarUrl?: string name: string summary?: string updatedAt?: string @@ -24,22 +25,41 @@ type Props = { const HistoryItem: React.FC = ({ conversation, - avatarUrl, name, summary, updatedAt, }) => { - const setMainViewState = useSetAtom(setMainViewStateAtom) const activeConvoId = useAtomValue(getActiveConvoIdAtom) + const isSelected = activeConvoId === conversation._id + const activeModel = useAtomValue(activeAssistantModelAtom) + const { startModel } = useStartStopModel() + const { getModelById } = useGetModelById() + + const setMainViewState = useSetAtom(setMainViewStateAtom) const setActiveConvoId = useSetAtom(setActiveConvoIdAtom) const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom) - const isSelected = activeConvoId === conversation._id + const setConfirmationModalProps = useSetAtom( + switchingModelConfirmationModalPropsAtom + ) const onClick = async () => { - const model = await executeSerial( - ModelManagementService.GetModelById, - conversation.modelId - ) + if (conversation.modelId == null) { + console.debug('modelId is undefined') + return + } + + const model = await getModelById(conversation.modelId) + if (model != null) { + if (activeModel == null) { + // if there's no active model, we simply load conversation's model + startModel(model._id) + } else if (activeModel._id !== model._id) { + // display confirmation modal + setConfirmationModalProps({ + replacingModel: model, + }) + } + } if (conversation._id) updateConvWaiting(conversation._id, true) diff --git a/web/app/_components/HistoryList/index.tsx b/web/app/_components/HistoryList/index.tsx index 91847b03a..3cbb482f6 100644 --- a/web/app/_components/HistoryList/index.tsx +++ b/web/app/_components/HistoryList/index.tsx @@ -1,5 +1,5 @@ import HistoryItem from '../HistoryItem' -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import ExpandableHeader from '../ExpandableHeader' import { useAtomValue } from 'jotai' import { searchAtom } from '@helpers/JotaiWrapper' @@ -33,7 +33,6 @@ const HistoryList: React.FC = () => { key={convo._id} conversation={convo} summary={convo.summary} - avatarUrl={convo.image} name={convo.name || 'Jan'} updatedAt={convo.updatedAt ?? ''} /> diff --git a/web/app/_components/InputToolbar/index.tsx b/web/app/_components/InputToolbar/index.tsx index 8ef8d0392..16eccf920 100644 --- a/web/app/_components/InputToolbar/index.tsx +++ b/web/app/_components/InputToolbar/index.tsx @@ -3,66 +3,27 @@ import BasicPromptInput from '../BasicPromptInput' import BasicPromptAccessories from '../BasicPromptAccessories' -import { useAtomValue, useSetAtom } from 'jotai' +import { useAtomValue } from 'jotai' import SecondaryButton from '../SecondaryButton' -import { useEffect, useState } from 'react' import { PlusIcon } from '@heroicons/react/24/outline' import useCreateConversation from '@hooks/useCreateConversation' import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' import { - currentConversationAtom, currentConvoStateAtom, + getActiveConvoIdAtom, } from '@helpers/atoms/Conversation.atom' -import useGetBots from '@hooks/useGetBots' -import { activeBotAtom } from '@helpers/atoms/Bot.atom' -import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels' +import useGetInputState from '@hooks/useGetInputState' +import { Button } from '../../../uikit/button' +import useStartStopModel from '@hooks/useStartStopModel' const InputToolbar: React.FC = () => { const activeModel = useAtomValue(activeAssistantModelAtom) - const { requestCreateConvo } = useCreateConversation() const currentConvoState = useAtomValue(currentConvoStateAtom) - const currentConvo = useAtomValue(currentConversationAtom) + const { inputState, currentConvo } = useGetInputState() + const { requestCreateConvo } = useCreateConversation() + const { startModel } = useStartStopModel() - const setActiveBot = useSetAtom(activeBotAtom) - const { getBotById } = useGetBots() - const [inputState, setInputState] = useState< - 'available' | 'disabled' | 'loading' - >() - const [error, setError] = useState() - const { downloadedModels } = useGetDownloadedModels() - - useEffect(() => { - const getReplyState = async () => { - setInputState('loading') - if (currentConvo && currentConvo.botId && currentConvo.botId.length > 0) { - // if botId is set, check if bot is available - const bot = await getBotById(currentConvo.botId) - console.debug('Found bot', JSON.stringify(bot, null, 2)) - if (bot) { - setActiveBot(bot) - } - setInputState(bot ? 'available' : 'disabled') - setError( - bot - ? undefined - : `Bot ${currentConvo.botId} has been deleted by its creator. Your chat history is saved but you won't be able to send new messages.` - ) - } else { - const model = downloadedModels.find( - (model) => model._id === activeModel?._id - ) - - setInputState(model ? 'available' : 'disabled') - setError( - model - ? undefined - : `Model ${activeModel?._id} cannot be found. Your chat history is saved but you won't be able to send new messages.` - ) - } - } - getReplyState() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentConvo]) + const activeConvoId = useAtomValue(getActiveConvoIdAtom) const onNewConversationClick = () => { if (activeModel) { @@ -70,16 +31,42 @@ const InputToolbar: React.FC = () => { } } - if (inputState === 'loading') return
Loading..
+ const onStartModelClick = () => { + const modelId = currentConvo?.modelId + if (!modelId) return + startModel(modelId) + } - if (inputState === 'disabled') + if (!activeConvoId) { + return null + } + + if (inputState === 'model-mismatch' || inputState === 'loading') { + const message = inputState === 'loading' ? 'Loading..' : 'Model mismatch!' + return ( +
+
+

+ {message} +

+ +
+
+ ) + } + + if (inputState === 'model-not-found') { return (

- {error} + Model {currentConvo?.modelId} not found! Please re-download the model + first.

) + } return (
diff --git a/web/app/_components/SwitchingModelConfirmationModal/index.tsx b/web/app/_components/SwitchingModelConfirmationModal/index.tsx new file mode 100644 index 000000000..e8bbeac95 --- /dev/null +++ b/web/app/_components/SwitchingModelConfirmationModal/index.tsx @@ -0,0 +1,129 @@ +import React, { Fragment } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { ExclamationTriangleIcon, XMarkIcon } from '@heroicons/react/24/outline' +import { switchingModelConfirmationModalPropsAtom } from '@helpers/atoms/Modal.atom' +import { useAtom, useAtomValue } from 'jotai' +import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' +import useStartStopModel from '@hooks/useStartStopModel' + +export type SwitchingModelConfirmationModalProps = { + replacingModel: AssistantModel +} + +const SwitchingModelConfirmationModal: React.FC = () => { + const [props, setProps] = useAtom(switchingModelConfirmationModalPropsAtom) + const activeModel = useAtomValue(activeAssistantModelAtom) + const { startModel } = useStartStopModel() + + const onConfirmSwitchModelClick = () => { + const modelId = props?.replacingModel._id + if (modelId) { + startModel(modelId) + } + setProps(undefined) + } + + return ( + + setProps(undefined)} + > + +
+ + +
+
+ + +
+ +
+
+
+
+
+ + Switching model + +
+

+ Selected conversation is using model{' '} + + {props?.replacingModel._id} + + , but the active model is using{' '} + + {activeModel?._id} + + . +

+
+

+ Switch to + + {' '} + {props?.replacingModel._id}? + +

+
+
+
+
+ + +
+
+
+
+
+
+
+ ) +} + +export default SwitchingModelConfirmationModal diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index e0d28a8b8..623ef9a5e 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -69,17 +69,17 @@ const Providers = (props: PropsWithChildren) => { return ( {setupCore && ( - - - {activated ? ( + + {activated ? ( + {children} - ) : ( -
- -
- )} -
-
+ + ) : ( +
+ +
+ )} + )}
) diff --git a/web/helpers/EventHandler.tsx b/web/helpers/EventHandler.tsx index 50a1be598..50ff4e425 100644 --- a/web/helpers/EventHandler.tsx +++ b/web/helpers/EventHandler.tsx @@ -3,14 +3,29 @@ import { toChatMessage } from '@models/ChatMessage' import { events, EventName, NewMessageResponse } from '@janhq/core' import { useSetAtom } from 'jotai' import { ReactNode, useEffect } from 'react' +import useGetBots from '@hooks/useGetBots' +import useGetUserConversations from '@hooks/useGetUserConversations' export default function EventHandler({ children }: { children: ReactNode }) { const addNewMessage = useSetAtom(addNewMessageAtom) const updateMessage = useSetAtom(updateMessageAtom) + const { getBotById } = useGetBots() + const { getConversationById } = useGetUserConversations() - function handleNewMessageResponse(message: NewMessageResponse) { - const newResponse = toChatMessage(message) - addNewMessage(newResponse) + async function handleNewMessageResponse(message: NewMessageResponse) { + if (message.conversationId) { + const convo = await getConversationById(message.conversationId) + const botId = convo?.botId + console.debug('botId', botId) + if (botId) { + const bot = await getBotById(botId) + const newResponse = toChatMessage(message, bot) + addNewMessage(newResponse) + } else { + const newResponse = toChatMessage(message) + addNewMessage(newResponse) + } + } } async function handleMessageResponseUpdate( messageResponse: NewMessageResponse @@ -40,5 +55,5 @@ export default function EventHandler({ children }: { children: ReactNode }) { events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate) } }, []) - return <> {children} + return <>{children} } diff --git a/web/helpers/ModalWrapper.tsx b/web/helpers/ModalWrapper.tsx index 5e7846c05..1acc7e04a 100644 --- a/web/helpers/ModalWrapper.tsx +++ b/web/helpers/ModalWrapper.tsx @@ -5,6 +5,7 @@ import ConfirmDeleteConversationModal from '@/_components/ConfirmDeleteConversat import ConfirmDeleteModelModal from '@/_components/ConfirmDeleteModelModal' import ConfirmSignOutModal from '@/_components/ConfirmSignOutModal' import MobileMenuPane from '@/_components/MobileMenuPane' +import SwitchingModelConfirmationModal from '@/_components/SwitchingModelConfirmationModal' import { ReactNode } from 'react' type Props = { @@ -18,6 +19,7 @@ export const ModalWrapper: React.FC = ({ children }) => ( + {children} ) diff --git a/web/helpers/atoms/Modal.atom.ts b/web/helpers/atoms/Modal.atom.ts index d371801c6..a6484d8cd 100644 --- a/web/helpers/atoms/Modal.atom.ts +++ b/web/helpers/atoms/Modal.atom.ts @@ -1,3 +1,4 @@ +import { SwitchingModelConfirmationModalProps } from '@/_components/SwitchingModelConfirmationModal' import { atom } from 'jotai' export const showConfirmDeleteConversationModalAtom = atom(false) @@ -7,3 +8,7 @@ export const showingAdvancedPromptAtom = atom(false) export const showingProductDetailAtom = atom(false) export const showingMobilePaneAtom = atom(false) export const showingBotListModalAtom = atom(false) + +export const switchingModelConfirmationModalPropsAtom = atom< + SwitchingModelConfirmationModalProps | undefined +>(undefined) diff --git a/web/hooks/useChatMessages.ts b/web/hooks/useChatMessages.ts index a2174c61e..28dfb21f9 100644 --- a/web/hooks/useChatMessages.ts +++ b/web/hooks/useChatMessages.ts @@ -3,11 +3,15 @@ import { executeSerial } from '@services/pluginService' import { useAtomValue, useSetAtom } from 'jotai' import { useEffect } from 'react' import { DataService } from '@janhq/core' -import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom' +import { + getActiveConvoIdAtom, + userConversationsAtom, +} from '@helpers/atoms/Conversation.atom' import { getCurrentChatMessagesAtom, setCurrentChatMessagesAtom, } from '@helpers/atoms/ChatMessage.atom' +import useGetBots from './useGetBots' /** * Custom hooks to get chat messages for current(active) conversation @@ -16,6 +20,8 @@ const useChatMessages = () => { const setMessages = useSetAtom(setCurrentChatMessagesAtom) const messages = useAtomValue(getCurrentChatMessagesAtom) const activeConvoId = useAtomValue(getActiveConvoIdAtom) + const userConversations = useAtomValue(userConversationsAtom) + const { getBotById } = useGetBots() const getMessages = async (convoId: string) => { const data: any = await executeSerial( @@ -26,6 +32,12 @@ const useChatMessages = () => { return [] } + const convo = userConversations.find((c) => c._id === convoId) + if (convo && convo.botId) { + const bot = await getBotById(convo.botId) + return parseMessages(data, bot) + } + return parseMessages(data) } @@ -47,10 +59,10 @@ const useChatMessages = () => { return { messages } } -function parseMessages(messages: RawMessage[]): ChatMessage[] { +function parseMessages(messages: RawMessage[], bot?: Bot): ChatMessage[] { const newMessages: ChatMessage[] = [] for (const m of messages) { - const chatMessage = toChatMessage(m) + const chatMessage = toChatMessage(m, bot) newMessages.push(chatMessage) } return newMessages diff --git a/web/hooks/useCreateConversation.ts b/web/hooks/useCreateConversation.ts index 8dde6665e..54f233c83 100644 --- a/web/hooks/useCreateConversation.ts +++ b/web/hooks/useCreateConversation.ts @@ -6,19 +6,18 @@ import { setActiveConvoIdAtom, addNewConversationStateAtom, } from '@helpers/atoms/Conversation.atom' +import useGetModelById from './useGetModelById' const useCreateConversation = () => { const [userConversations, setUserConversations] = useAtom( userConversationsAtom ) + const { getModelById } = useGetModelById() const setActiveConvoId = useSetAtom(setActiveConvoIdAtom) const addNewConvoState = useSetAtom(addNewConversationStateAtom) const createConvoByBot = async (bot: Bot) => { - const model = await executeSerial( - ModelManagementService.GetModelById, - bot.modelId - ) + const model = await getModelById(bot.modelId) if (!model) { alert( diff --git a/web/hooks/useGetInputState.ts b/web/hooks/useGetInputState.ts new file mode 100644 index 000000000..d11225597 --- /dev/null +++ b/web/hooks/useGetInputState.ts @@ -0,0 +1,52 @@ +import { currentConversationAtom } from '@helpers/atoms/Conversation.atom' +import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' +import { useAtomValue } from 'jotai' +import { useEffect, useState } from 'react' +import { useGetDownloadedModels } from './useGetDownloadedModels' + +export default function useGetInputState() { + const [inputState, setInputState] = useState('loading') + const currentConvo = useAtomValue(currentConversationAtom) + const activeModel = useAtomValue(activeAssistantModelAtom) + const { downloadedModels } = useGetDownloadedModels() + + const handleInputState = ( + convo: Conversation | undefined, + currentModel: AssistantModel | undefined, + models: AssistantModel[] + ) => { + if (convo == null) return + if (currentModel == null) { + setInputState('loading') + return + } + + // check if convo model id is in downloaded models + const isModelAvailable = downloadedModels.some( + (model) => model._id === convo.modelId + ) + + if (!isModelAvailable) { + // can't find model in downloaded models + setInputState('model-not-found') + return + } + + if (convo.modelId !== currentModel._id) { + // in case convo model and active model is different, + // ask user to init the required model + setInputState('model-mismatch') + return + } + + setInputState('available') + } + + useEffect(() => { + handleInputState(currentConvo, activeModel, downloadedModels) + }, [currentConvo, activeModel, downloadedModels]) + + return { inputState, currentConvo } +} + +type InputType = 'available' | 'loading' | 'model-mismatch' | 'model-not-found' diff --git a/web/hooks/useGetModelById.ts b/web/hooks/useGetModelById.ts new file mode 100644 index 000000000..d97abdc18 --- /dev/null +++ b/web/hooks/useGetModelById.ts @@ -0,0 +1,23 @@ +import { ModelManagementService } from '@janhq/core' +import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager' + +export default function useGetModelById() { + const getModelById = async ( + modelId: string + ): Promise => { + return queryModelById(modelId) + } + + return { getModelById } +} + +const queryModelById = async ( + modelId: string +): Promise => { + const model = await executeSerial( + ModelManagementService.GetModelById, + modelId + ) + + return model +} diff --git a/web/hooks/useGetUserConversations.ts b/web/hooks/useGetUserConversations.ts index 88557cb6f..6055b6962 100644 --- a/web/hooks/useGetUserConversations.ts +++ b/web/hooks/useGetUserConversations.ts @@ -29,7 +29,14 @@ const useGetUserConversations = () => { } } + const getConversationById = async ( + id: string + ): Promise => { + return await executeSerial(DataService.GetConversationById, id) + } + return { + getConversationById, getUserConversations, } } diff --git a/web/hooks/useStartStopModel.ts b/web/hooks/useStartStopModel.ts index 743d8ca6d..574e000fc 100644 --- a/web/hooks/useStartStopModel.ts +++ b/web/hooks/useStartStopModel.ts @@ -1,11 +1,12 @@ import { executeSerial } from '@services/pluginService' -import { ModelManagementService, InferenceService } from '@janhq/core' +import { InferenceService } from '@janhq/core' import { useAtom, useSetAtom } from 'jotai' import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom' -import { useState } from 'react' +import useGetModelById from './useGetModelById' export default function useStartStopModel() { const [activeModel, setActiveModel] = useAtom(activeAssistantModelAtom) + const { getModelById } = useGetModelById() const setStateModel = useSetAtom(stateModel) const startModel = async (modelId: string) => { @@ -16,25 +17,27 @@ export default function useStartStopModel() { setStateModel({ state: 'start', loading: true, model: modelId }) - const model = await executeSerial( - ModelManagementService.GetModelById, - modelId - ) + const model = await getModelById(modelId) + if (!model) { alert(`Model ${modelId} not found! Please re-download the model first.`) setStateModel((prev) => ({ ...prev, loading: false })) + return } + const currentTime = Date.now() console.debug('Init model: ', model._id) - const res = await executeSerial(InferenceService.InitModel, model._id) + const res = await initModel(model._id) if (res?.error) { const errorMessage = `Failed to init model: ${res.error}` console.error(errorMessage) alert(errorMessage) } else { console.debug( - `Init model successfully!, take ${Date.now() - currentTime}ms` + `Init model ${modelId} successfully!, take ${ + Date.now() - currentTime + }ms` ) setActiveModel(model) } @@ -52,3 +55,7 @@ export default function useStartStopModel() { return { startModel, stopModel } } + +const initModel = async (modelId: string): Promise => { + return executeSerial(InferenceService.InitModel, modelId) +} diff --git a/web/models/ChatMessage.ts b/web/models/ChatMessage.ts index e074a011c..e5763c984 100644 --- a/web/models/ChatMessage.ts +++ b/web/models/ChatMessage.ts @@ -41,7 +41,8 @@ export interface RawMessage { } export const toChatMessage = ( - m: RawMessage | NewMessageResponse + m: RawMessage | NewMessageResponse, + bot?: Bot ): ChatMessage => { const createdAt = new Date(m.createdAt ?? '').getTime() const imageUrls: string[] = [] @@ -56,18 +57,18 @@ export const toChatMessage = ( const content = m.message ?? '' + let senderName = m.user === 'user' ? 'You' : 'Assistant' + if (senderName === 'Assistant' && bot) { + senderName = bot.name + } + return { id: (m._id ?? 0).toString(), conversationId: (m.conversationId ?? 0).toString(), messageType: messageType, messageSenderType: messageSenderType, senderUid: m.user?.toString() || '0', - senderName: - m.user === 'user' - ? 'You' - : m.user && m.user !== 'ai' && m.user !== 'assistant' - ? m.user - : 'Assistant', + senderName: senderName, senderAvatarUrl: m.avatar ? m.avatar : m.user === 'user'