diff --git a/electron/package.json b/electron/package.json index 67d39f917f..452c69ab8a 100644 --- a/electron/package.json +++ b/electron/package.json @@ -67,7 +67,6 @@ "electron-store": "^8.1.0", "electron-updater": "^6.1.4", "pacote": "^17.0.4", - "react-intersection-observer": "^9.5.2", "request": "^2.88.2", "request-progress": "^3.0.0", "use-debounce": "^9.0.4" diff --git a/web/app/_components/BasicPromptInput/index.tsx b/web/app/_components/BasicPromptInput/index.tsx index bb2eeb8e02..b73c16570d 100644 --- a/web/app/_components/BasicPromptInput/index.tsx +++ b/web/app/_components/BasicPromptInput/index.tsx @@ -4,7 +4,6 @@ import { currentPromptAtom } from '@helpers/JotaiWrapper' import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom' import { selectedModelAtom } from '@helpers/atoms/Model.atom' import useCreateConversation from '@hooks/useCreateConversation' -import useInitModel from '@hooks/useInitModel' import useSendChatMessage from '@hooks/useSendChatMessage' import { useAtom, useAtomValue } from 'jotai' import { ChangeEvent, useEffect, useRef } from 'react' @@ -16,8 +15,6 @@ const BasicPromptInput: React.FC = () => { const { sendChatMessage } = useSendChatMessage() const { requestCreateConvo } = useCreateConversation() - const { initModel } = useInitModel() - const textareaRef = useRef(null) const handleKeyDown = async ( @@ -35,7 +32,6 @@ const BasicPromptInput: React.FC = () => { } await requestCreateConvo(selectedModel) - await initModel(selectedModel) sendChatMessage() } } diff --git a/web/app/_components/ChatBody/index.tsx b/web/app/_components/ChatBody/index.tsx index 523c0d4c72..3d759d388b 100644 --- a/web/app/_components/ChatBody/index.tsx +++ b/web/app/_components/ChatBody/index.tsx @@ -1,60 +1,17 @@ 'use client' -import React, { useCallback, useRef, useState, useEffect } from 'react' +import React from 'react' import ChatItem from '../ChatItem' import useChatMessages from '@hooks/useChatMessages' -import { useAtomValue } from 'jotai' -import { selectAtom } from 'jotai/utils' -import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom' -import { chatMessages } from '@helpers/atoms/ChatMessage.atom' const ChatBody: React.FC = () => { - const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? '' - const messageList = useAtomValue( - selectAtom( - chatMessages, - useCallback((v) => v[activeConversationId], [activeConversationId]) - ) - ) - const [content, setContent] = useState([]) - - const [offset, setOffset] = useState(0) - const { loading, hasMore } = useChatMessages(offset) - const intersectObs = useRef(null) - - const lastPostRef = useCallback( - (message: ChatMessage) => { - if (loading) return - - if (intersectObs.current) intersectObs.current.disconnect() - - intersectObs.current = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && hasMore) { - setOffset((prevOffset) => prevOffset + 5) - } - }) - - if (message) intersectObs.current.observe(message) - }, - [loading, hasMore] - ) - - useEffect(() => { - const list = messageList?.map((message, index) => { - if (messageList?.length === index + 1) { - return ( - // @ts-ignore - - ) - } - return - }) - setContent(list) - }, [messageList, lastPostRef]) + const { messages } = useChatMessages() return ( -
- {content} +
+ {messages.map((message) => ( + + ))}
) } diff --git a/web/app/_components/HistoryItem/index.tsx b/web/app/_components/HistoryItem/index.tsx index 9f084f2bc0..118ebf5011 100644 --- a/web/app/_components/HistoryItem/index.tsx +++ b/web/app/_components/HistoryItem/index.tsx @@ -1,19 +1,16 @@ import React from 'react' import { useAtomValue, useSetAtom } from 'jotai' -import Image from 'next/image' import { ModelManagementService } from '@janhq/core' import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager' import { getActiveConvoIdAtom, setActiveConvoIdAtom, - updateConversationErrorAtom, updateConversationWaitingForResponseAtom, } from '@helpers/atoms/Conversation.atom' import { setMainViewStateAtom, MainViewState, } from '@helpers/atoms/MainView.atom' -import useInitModel from '@hooks/useInitModel' import { displayDate } from '@utils/datetime' import { twMerge } from 'tailwind-merge' @@ -36,11 +33,8 @@ const HistoryItem: React.FC = ({ const activeConvoId = useAtomValue(getActiveConvoIdAtom) const setActiveConvoId = useSetAtom(setActiveConvoIdAtom) const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom) - const updateConvError = useSetAtom(updateConversationErrorAtom) const isSelected = activeConvoId === conversation._id - const { initModel } = useInitModel() - const onClick = async () => { const model = await executeSerial( ModelManagementService.GetModelById, @@ -48,13 +42,6 @@ const HistoryItem: React.FC = ({ ) if (conversation._id) updateConvWaiting(conversation._id, true) - initModel(model).then((res: any) => { - if (conversation._id) updateConvWaiting(conversation._id, false) - - if (res?.error && conversation._id) { - updateConvError(conversation._id, res.error) - } - }) if (activeConvoId !== conversation._id) { setMainViewState(MainViewState.Conversation) diff --git a/web/app/_components/InputToolbar/index.tsx b/web/app/_components/InputToolbar/index.tsx index ebb644bf56..8ef8d03925 100644 --- a/web/app/_components/InputToolbar/index.tsx +++ b/web/app/_components/InputToolbar/index.tsx @@ -4,9 +4,8 @@ import BasicPromptInput from '../BasicPromptInput' import BasicPromptAccessories from '../BasicPromptAccessories' import { useAtomValue, useSetAtom } from 'jotai' -import { showingAdvancedPromptAtom } from '@helpers/atoms/Modal.atom' import SecondaryButton from '../SecondaryButton' -import { Fragment, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { PlusIcon } from '@heroicons/react/24/outline' import useCreateConversation from '@hooks/useCreateConversation' import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' @@ -19,7 +18,6 @@ import { activeBotAtom } from '@helpers/atoms/Bot.atom' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels' const InputToolbar: React.FC = () => { - const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom) const activeModel = useAtomValue(activeAssistantModelAtom) const { requestCreateConvo } = useCreateConversation() const currentConvoState = useAtomValue(currentConvoStateAtom) @@ -76,7 +74,7 @@ const InputToolbar: React.FC = () => { if (inputState === 'disabled') return ( -
+

{error}

@@ -84,7 +82,7 @@ const InputToolbar: React.FC = () => { ) return ( -
+
{currentConvoState?.error && (
diff --git a/web/app/_components/ModelActionButton/index.tsx b/web/app/_components/ModelActionButton/index.tsx index 4f97072cc8..c1a7c64e0d 100644 --- a/web/app/_components/ModelActionButton/index.tsx +++ b/web/app/_components/ModelActionButton/index.tsx @@ -24,12 +24,14 @@ const modelActionMapper: Record = { } type Props = { + disabled?: boolean type: ModelActionType onActionClick: (type: ModelActionType) => void onDeleteClick: () => void } const ModelActionButton: React.FC = ({ + disabled = false, type, onActionClick, onDeleteClick, @@ -48,6 +50,7 @@ const ModelActionButton: React.FC = ({
diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts index 2230ccca4b..b61103479c 100644 --- a/web/helpers/atoms/ChatMessage.atom.ts +++ b/web/helpers/atoms/ChatMessage.atom.ts @@ -4,14 +4,34 @@ import { getActiveConvoIdAtom } from './Conversation.atom' /** * Stores all chat messages for all conversations */ -export const chatMessages = atom>({}) +const chatMessages = atom>({}) -export const currentChatMessagesAtom = atom((get) => { +/** + * Return the chat messages for the current active conversation + */ +export const getCurrentChatMessagesAtom = atom((get) => { const activeConversationId = get(getActiveConvoIdAtom) if (!activeConversationId) return [] return get(chatMessages)[activeConversationId] ?? [] }) +export const setCurrentChatMessagesAtom = atom( + null, + (get, set, messages: ChatMessage[]) => { + const currentConvoId = get(getActiveConvoIdAtom) + if (!currentConvoId) return + + const newData: Record = { + ...get(chatMessages), + } + newData[currentConvoId] = messages + set(chatMessages, newData) + } +) + +/** + * Used for pagination. Add old messages to the current conversation + */ export const addOldMessagesAtom = atom( null, (get, set, newMessages: ChatMessage[]) => { diff --git a/web/hooks/useChatMessages.ts b/web/hooks/useChatMessages.ts index e8ab48945e..a2174c61e2 100644 --- a/web/hooks/useChatMessages.ts +++ b/web/hooks/useChatMessages.ts @@ -1,62 +1,50 @@ import { toChatMessage } from '@models/ChatMessage' import { executeSerial } from '@services/pluginService' import { useAtomValue, useSetAtom } from 'jotai' -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { DataService } from '@janhq/core' -import { addOldMessagesAtom } from '@helpers/atoms/ChatMessage.atom' +import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom' import { - currentConversationAtom, - conversationStatesAtom, - updateConversationHasMoreAtom, -} from '@helpers/atoms/Conversation.atom' + getCurrentChatMessagesAtom, + setCurrentChatMessagesAtom, +} from '@helpers/atoms/ChatMessage.atom' /** * Custom hooks to get chat messages for current(active) conversation - * - * @param offset for pagination purpose - * @returns */ -const useChatMessages = (offset = 0) => { - const [loading, setLoading] = useState(true) - const addOldChatMessages = useSetAtom(addOldMessagesAtom) - const currentConvo = useAtomValue(currentConversationAtom) - const convoStates = useAtomValue(conversationStatesAtom) - const updateConvoHasMore = useSetAtom(updateConversationHasMoreAtom) +const useChatMessages = () => { + const setMessages = useSetAtom(setCurrentChatMessagesAtom) + const messages = useAtomValue(getCurrentChatMessagesAtom) + const activeConvoId = useAtomValue(getActiveConvoIdAtom) + + const getMessages = async (convoId: string) => { + const data: any = await executeSerial( + DataService.GetConversationMessages, + convoId + ) + if (!data) { + return [] + } + + return parseMessages(data) + } useEffect(() => { - if (!currentConvo) { + if (!activeConvoId) { + console.error('active convo is undefined') return } - const hasMore = convoStates[currentConvo._id ?? '']?.hasMore ?? true - if (!hasMore) return - const getMessages = async () => { - executeSerial(DataService.GetConversationMessages, currentConvo._id).then( - (data: any) => { - if (!data) { - return - } - const newMessages = parseMessages(data ?? []) - addOldChatMessages(newMessages) - updateConvoHasMore(currentConvo._id ?? '', false) - setLoading(false) - } - ) - } - getMessages() - }, [ - offset, - convoStates, - addOldChatMessages, - updateConvoHasMore, - currentConvo, - ]) + getMessages(activeConvoId) + .then((messages) => { + setMessages(messages) + }) + .catch((err) => { + console.error(err) + }) + }, [activeConvoId]) - return { - loading: loading, - error: undefined, - hasMore: convoStates[currentConvo?._id ?? '']?.hasMore ?? true, - } + return { messages } } function parseMessages(messages: RawMessage[]): ChatMessage[] { diff --git a/web/hooks/useCreateConversation.ts b/web/hooks/useCreateConversation.ts index 4c1e810d9e..8dde6665ec 100644 --- a/web/hooks/useCreateConversation.ts +++ b/web/hooks/useCreateConversation.ts @@ -1,25 +1,18 @@ import { useAtom, useSetAtom } from 'jotai' - import { executeSerial } from '@services/pluginService' import { DataService, ModelManagementService } from '@janhq/core' import { userConversationsAtom, setActiveConvoIdAtom, addNewConversationStateAtom, - updateConversationWaitingForResponseAtom, - updateConversationErrorAtom, } from '@helpers/atoms/Conversation.atom' -import useInitModel from './useInitModel' const useCreateConversation = () => { - const { initModel } = useInitModel() const [userConversations, setUserConversations] = useAtom( userConversationsAtom ) const setActiveConvoId = useSetAtom(setActiveConvoIdAtom) const addNewConvoState = useSetAtom(addNewConversationStateAtom) - const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom) - const updateConvError = useSetAtom(updateConversationErrorAtom) const createConvoByBot = async (bot: Bot) => { const model = await executeSerial( @@ -48,14 +41,6 @@ const useCreateConversation = () => { } const id = await executeSerial(DataService.CreateConversation, conv) - if (id) updateConvWaiting(id, true) - initModel(model).then((res: any) => { - if (id) updateConvWaiting(id, false) - if (res?.error) { - updateConvError(id, res.error) - } - }) - const mappedConvo: Conversation = { _id: id, modelId: model._id, diff --git a/web/hooks/useDeleteConversation.ts b/web/hooks/useDeleteConversation.ts index a3876b03bc..d78acb48ad 100644 --- a/web/hooks/useDeleteConversation.ts +++ b/web/hooks/useDeleteConversation.ts @@ -1,5 +1,5 @@ import { currentPromptAtom } from '@helpers/JotaiWrapper' -import { execute } from '@services/pluginService' +import { executeSerial } from '@services/pluginService' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { DataService } from '@janhq/core' import { deleteConversationMessage } from '@helpers/atoms/ChatMessage.atom' @@ -33,7 +33,7 @@ export default function useDeleteConversation() { const deleteConvo = async () => { if (activeConvoId) { try { - await execute(DataService.DeleteConversation, activeConvoId) + await executeSerial(DataService.DeleteConversation, activeConvoId) const currentConversations = userConversations.filter( (c) => c._id !== activeConvoId ) diff --git a/web/hooks/useInitModel.ts b/web/hooks/useInitModel.ts deleted file mode 100644 index 33121845b9..0000000000 --- a/web/hooks/useInitModel.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { executeSerial } from '@services/pluginService' -import { InferenceService } from '@janhq/core' -import { useAtom } from 'jotai' -import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' - -export default function useInitModel() { - const [activeModel, setActiveModel] = useAtom(activeAssistantModelAtom) - - const initModel = async (model: AssistantModel) => { - if (activeModel && activeModel._id === model._id) { - console.debug(`Model ${model._id} is already init. Ignore..`) - return - } - - const currentTime = Date.now() - console.debug('Init model: ', model._id) - - const res = await executeSerial(InferenceService.InitModel, model._id) - if (res?.error) { - console.error('Failed to init model: ', res.error) - return res - } else { - console.debug( - `Init model successfully!, take ${Date.now() - currentTime}ms` - ) - setActiveModel(model) - return {} - } - } - - return { initModel } -} diff --git a/web/hooks/useStartStopModel.ts b/web/hooks/useStartStopModel.ts index 829dc0906a..b6aca77261 100644 --- a/web/hooks/useStartStopModel.ts +++ b/web/hooks/useStartStopModel.ts @@ -1,16 +1,22 @@ import { executeSerial } from '@services/pluginService' import { ModelManagementService, InferenceService } from '@janhq/core' -import useInitModel from './useInitModel' -import { useSetAtom } from 'jotai' +import { useAtom, useSetAtom } from 'jotai' import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom' +import { useState } from 'react' export default function useStartStopModel() { - const { initModel } = useInitModel() - const setActiveModel = useSetAtom(activeAssistantModelAtom) + const [activeModel, setActiveModel] = useAtom(activeAssistantModelAtom) + const [loading, setLoading] = useState(false) const setStateModel = useSetAtom(stateModel) const startModel = async (modelId: string) => { + if (activeModel && activeModel._id === modelId) { + console.debug(`Model ${modelId} is already init. Ignore..`) + return + } + setStateModel({ state: 'start', loading: true, model: modelId }) + const model = await executeSerial( ModelManagementService.GetModelById, modelId @@ -18,10 +24,23 @@ export default function useStartStopModel() { if (!model) { alert(`Model ${modelId} not found! Please re-download the model first.`) setStateModel((prev) => ({ ...prev, loading: false })) + } + const currentTime = Date.now() + console.debug('Init model: ', model._id) + + const res = await executeSerial(InferenceService.InitModel, model._id) + + if (res?.error) { + const errorMessage = `Failed to init model: ${res.error}` + console.error(errorMessage) + alert(errorMessage) } else { - await initModel(model) - setStateModel((prev) => ({ ...prev, loading: false })) + console.debug( + `Init model successfully!, take ${Date.now() - currentTime}ms` + ) + setActiveModel(model) } + setLoading(false) } const stopModel = async (modelId: string) => { @@ -33,5 +52,5 @@ export default function useStartStopModel() { }, 500) } - return { startModel, stopModel } + return { loading, startModel, stopModel } } diff --git a/web/package.json b/web/package.json index 2925b11562..6f83b327ae 100644 --- a/web/package.json +++ b/web/package.json @@ -24,12 +24,11 @@ "autoprefixer": "10.4.14", "class-variance-authority": "^0.7.0", "classnames": "^2.3.2", - "embla-carousel": "^8.0.0-rc11", - "embla-carousel-react": "^8.0.0-rc11", "eslint": "8.45.0", "eslint-config-next": "13.4.10", "framer-motion": "^10.16.4", "highlight.js": "^11.9.0", + "react-intersection-observer": "^9.5.2", "jotai": "^2.4.0", "jotai-optics": "^0.3.1", "jwt-decode": "^3.1.2", @@ -39,7 +38,6 @@ "next": "13.4.10", "next-auth": "^4.23.1", "next-themes": "^0.2.1", - "optics-ts": "^2.4.1", "postcss": "8.4.26", "react": "18.2.0", "react-dom": "18.2.0",