fix: duplicated messages when user switch between conversations (#441)
Signed-off-by: James <james@jan.ai> Co-authored-by: James <james@jan.ai>
This commit is contained in:
parent
e05c08b95f
commit
1fd47ba453
@ -67,7 +67,6 @@
|
|||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^6.1.4",
|
"electron-updater": "^6.1.4",
|
||||||
"pacote": "^17.0.4",
|
"pacote": "^17.0.4",
|
||||||
"react-intersection-observer": "^9.5.2",
|
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-progress": "^3.0.0",
|
"request-progress": "^3.0.0",
|
||||||
"use-debounce": "^9.0.4"
|
"use-debounce": "^9.0.4"
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { currentPromptAtom } from '@helpers/JotaiWrapper'
|
|||||||
import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom'
|
import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom'
|
||||||
import { selectedModelAtom } from '@helpers/atoms/Model.atom'
|
import { selectedModelAtom } from '@helpers/atoms/Model.atom'
|
||||||
import useCreateConversation from '@hooks/useCreateConversation'
|
import useCreateConversation from '@hooks/useCreateConversation'
|
||||||
import useInitModel from '@hooks/useInitModel'
|
|
||||||
import useSendChatMessage from '@hooks/useSendChatMessage'
|
import useSendChatMessage from '@hooks/useSendChatMessage'
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
import { ChangeEvent, useEffect, useRef } from 'react'
|
import { ChangeEvent, useEffect, useRef } from 'react'
|
||||||
@ -16,8 +15,6 @@ const BasicPromptInput: React.FC = () => {
|
|||||||
const { sendChatMessage } = useSendChatMessage()
|
const { sendChatMessage } = useSendChatMessage()
|
||||||
const { requestCreateConvo } = useCreateConversation()
|
const { requestCreateConvo } = useCreateConversation()
|
||||||
|
|
||||||
const { initModel } = useInitModel()
|
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
const handleKeyDown = async (
|
const handleKeyDown = async (
|
||||||
@ -35,7 +32,6 @@ const BasicPromptInput: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await requestCreateConvo(selectedModel)
|
await requestCreateConvo(selectedModel)
|
||||||
await initModel(selectedModel)
|
|
||||||
sendChatMessage()
|
sendChatMessage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,60 +1,17 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useCallback, useRef, useState, useEffect } from 'react'
|
import React from 'react'
|
||||||
import ChatItem from '../ChatItem'
|
import ChatItem from '../ChatItem'
|
||||||
import useChatMessages from '@hooks/useChatMessages'
|
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 ChatBody: React.FC = () => {
|
||||||
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? ''
|
const { messages } = useChatMessages()
|
||||||
const messageList = useAtomValue(
|
|
||||||
selectAtom(
|
|
||||||
chatMessages,
|
|
||||||
useCallback((v) => v[activeConversationId], [activeConversationId])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
const [content, setContent] = useState<React.JSX.Element[]>([])
|
|
||||||
|
|
||||||
const [offset, setOffset] = useState(0)
|
|
||||||
const { loading, hasMore } = useChatMessages(offset)
|
|
||||||
const intersectObs = useRef<any>(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
|
|
||||||
<ChatItem ref={lastPostRef} message={message} key={message.id} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return <ChatItem message={message} key={message.id} />
|
|
||||||
})
|
|
||||||
setContent(list)
|
|
||||||
}, [messageList, lastPostRef])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="[&>*:nth-child(odd)]:bg-background flex h-full flex-1 flex-col-reverse overflow-y-auto">
|
<div className="flex h-full flex-1 flex-col-reverse overflow-y-auto [&>*:nth-child(odd)]:bg-background">
|
||||||
{content}
|
{messages.map((message) => (
|
||||||
|
<ChatItem message={message} key={message.id} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import Image from 'next/image'
|
|
||||||
import { ModelManagementService } from '@janhq/core'
|
import { ModelManagementService } from '@janhq/core'
|
||||||
import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager'
|
import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager'
|
||||||
import {
|
import {
|
||||||
getActiveConvoIdAtom,
|
getActiveConvoIdAtom,
|
||||||
setActiveConvoIdAtom,
|
setActiveConvoIdAtom,
|
||||||
updateConversationErrorAtom,
|
|
||||||
updateConversationWaitingForResponseAtom,
|
updateConversationWaitingForResponseAtom,
|
||||||
} from '@helpers/atoms/Conversation.atom'
|
} from '@helpers/atoms/Conversation.atom'
|
||||||
import {
|
import {
|
||||||
setMainViewStateAtom,
|
setMainViewStateAtom,
|
||||||
MainViewState,
|
MainViewState,
|
||||||
} from '@helpers/atoms/MainView.atom'
|
} from '@helpers/atoms/MainView.atom'
|
||||||
import useInitModel from '@hooks/useInitModel'
|
|
||||||
import { displayDate } from '@utils/datetime'
|
import { displayDate } from '@utils/datetime'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
@ -36,11 +33,8 @@ const HistoryItem: React.FC<Props> = ({
|
|||||||
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
||||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
||||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
||||||
const updateConvError = useSetAtom(updateConversationErrorAtom)
|
|
||||||
const isSelected = activeConvoId === conversation._id
|
const isSelected = activeConvoId === conversation._id
|
||||||
|
|
||||||
const { initModel } = useInitModel()
|
|
||||||
|
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
const model = await executeSerial(
|
const model = await executeSerial(
|
||||||
ModelManagementService.GetModelById,
|
ModelManagementService.GetModelById,
|
||||||
@ -48,13 +42,6 @@ const HistoryItem: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (conversation._id) updateConvWaiting(conversation._id, true)
|
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) {
|
if (activeConvoId !== conversation._id) {
|
||||||
setMainViewState(MainViewState.Conversation)
|
setMainViewState(MainViewState.Conversation)
|
||||||
|
|||||||
@ -4,9 +4,8 @@
|
|||||||
import BasicPromptInput from '../BasicPromptInput'
|
import BasicPromptInput from '../BasicPromptInput'
|
||||||
import BasicPromptAccessories from '../BasicPromptAccessories'
|
import BasicPromptAccessories from '../BasicPromptAccessories'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { showingAdvancedPromptAtom } from '@helpers/atoms/Modal.atom'
|
|
||||||
import SecondaryButton from '../SecondaryButton'
|
import SecondaryButton from '../SecondaryButton'
|
||||||
import { Fragment, useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { PlusIcon } from '@heroicons/react/24/outline'
|
import { PlusIcon } from '@heroicons/react/24/outline'
|
||||||
import useCreateConversation from '@hooks/useCreateConversation'
|
import useCreateConversation from '@hooks/useCreateConversation'
|
||||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
||||||
@ -19,7 +18,6 @@ import { activeBotAtom } from '@helpers/atoms/Bot.atom'
|
|||||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
||||||
|
|
||||||
const InputToolbar: React.FC = () => {
|
const InputToolbar: React.FC = () => {
|
||||||
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom)
|
|
||||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||||
const { requestCreateConvo } = useCreateConversation()
|
const { requestCreateConvo } = useCreateConversation()
|
||||||
const currentConvoState = useAtomValue(currentConvoStateAtom)
|
const currentConvoState = useAtomValue(currentConvoStateAtom)
|
||||||
@ -76,7 +74,7 @@ const InputToolbar: React.FC = () => {
|
|||||||
|
|
||||||
if (inputState === 'disabled')
|
if (inputState === 'disabled')
|
||||||
return (
|
return (
|
||||||
<div className="bg-background/90 sticky bottom-0 flex items-center justify-center">
|
<div className="sticky bottom-0 flex items-center justify-center bg-background/90">
|
||||||
<p className="mx-auto my-5 line-clamp-2 text-ellipsis text-center italic text-gray-600">
|
<p className="mx-auto my-5 line-clamp-2 text-ellipsis text-center italic text-gray-600">
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@ -84,7 +82,7 @@ const InputToolbar: React.FC = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background/90 sticky bottom-0 w-full px-5 py-0">
|
<div className="sticky bottom-0 w-full bg-background/90 px-5 py-0">
|
||||||
{currentConvoState?.error && (
|
{currentConvoState?.error && (
|
||||||
<div className="flex flex-row justify-center">
|
<div className="flex flex-row justify-center">
|
||||||
<span className="mx-5 my-2 text-sm text-red-500">
|
<span className="mx-5 my-2 text-sm text-red-500">
|
||||||
|
|||||||
@ -24,12 +24,14 @@ const modelActionMapper: Record<ModelActionType, ModelActionStyle> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
disabled?: boolean
|
||||||
type: ModelActionType
|
type: ModelActionType
|
||||||
onActionClick: (type: ModelActionType) => void
|
onActionClick: (type: ModelActionType) => void
|
||||||
onDeleteClick: () => void
|
onDeleteClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModelActionButton: React.FC<Props> = ({
|
const ModelActionButton: React.FC<Props> = ({
|
||||||
|
disabled = false,
|
||||||
type,
|
type,
|
||||||
onActionClick,
|
onActionClick,
|
||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
@ -48,6 +50,7 @@ const ModelActionButton: React.FC<Props> = ({
|
|||||||
<div className="flex items-center justify-end gap-x-4">
|
<div className="flex items-center justify-end gap-x-4">
|
||||||
<ModelActionMenu onDeleteClick={onDeleteClick} />
|
<ModelActionMenu onDeleteClick={onDeleteClick} />
|
||||||
<Button
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
size="sm"
|
size="sm"
|
||||||
themes={styles.title === 'Start' ? 'accent' : 'default'}
|
themes={styles.title === 'Start' ? 'accent' : 'default'}
|
||||||
onClick={() => onClick()}
|
onClick={() => onClick()}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ModelRow: React.FC<Props> = ({ model }) => {
|
const ModelRow: React.FC<Props> = ({ model }) => {
|
||||||
const { startModel, stopModel } = useStartStopModel()
|
const { loading, startModel, stopModel } = useStartStopModel()
|
||||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||||
const { deleteModel } = useDeleteModel()
|
const { deleteModel } = useDeleteModel()
|
||||||
|
|
||||||
@ -57,6 +57,7 @@ const ModelRow: React.FC<Props> = ({ model }) => {
|
|||||||
<ModelStatusComponent status={status} />
|
<ModelStatusComponent status={status} />
|
||||||
</td>
|
</td>
|
||||||
<ModelActionButton
|
<ModelActionButton
|
||||||
|
disabled={loading}
|
||||||
type={actionButtonType}
|
type={actionButtonType}
|
||||||
onActionClick={onModelActionClick}
|
onActionClick={onModelActionClick}
|
||||||
onDeleteClick={onDeleteClick}
|
onDeleteClick={onDeleteClick}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import useCreateConversation from '@hooks/useCreateConversation'
|
import useCreateConversation from '@hooks/useCreateConversation'
|
||||||
import PrimaryButton from '../PrimaryButton'
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
@ -7,11 +6,9 @@ import {
|
|||||||
setMainViewStateAtom,
|
setMainViewStateAtom,
|
||||||
} from '@helpers/atoms/MainView.atom'
|
} from '@helpers/atoms/MainView.atom'
|
||||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
||||||
import useInitModel from '@hooks/useInitModel'
|
|
||||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
||||||
import { Button } from '@uikit'
|
import { Button } from '@uikit'
|
||||||
|
import { MessageCircle } from 'lucide-react'
|
||||||
import {MessageCircle} from "lucide-react"
|
|
||||||
|
|
||||||
enum ActionButton {
|
enum ActionButton {
|
||||||
DownloadModel = 'Download a Model',
|
DownloadModel = 'Download a Model',
|
||||||
@ -25,8 +22,6 @@ const SidebarEmptyHistory: React.FC = () => {
|
|||||||
const { requestCreateConvo } = useCreateConversation()
|
const { requestCreateConvo } = useCreateConversation()
|
||||||
const [action, setAction] = useState(ActionButton.DownloadModel)
|
const [action, setAction] = useState(ActionButton.DownloadModel)
|
||||||
|
|
||||||
const { initModel } = useInitModel()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (downloadedModels.length > 0) {
|
if (downloadedModels.length > 0) {
|
||||||
setAction(ActionButton.StartChat)
|
setAction(ActionButton.StartChat)
|
||||||
@ -35,29 +30,24 @@ const SidebarEmptyHistory: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [downloadedModels])
|
}, [downloadedModels])
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = async () => {
|
||||||
if (action === ActionButton.DownloadModel) {
|
if (action === ActionButton.DownloadModel) {
|
||||||
setMainView(MainViewState.ExploreModel)
|
setMainView(MainViewState.ExploreModel)
|
||||||
} else {
|
} else {
|
||||||
if (!activeModel) {
|
if (!activeModel) {
|
||||||
setMainView(MainViewState.ConversationEmptyModel)
|
setMainView(MainViewState.ConversationEmptyModel)
|
||||||
} else {
|
} else {
|
||||||
createConversationAndInitModel(activeModel)
|
await requestCreateConvo(activeModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createConversationAndInitModel = async (model: AssistantModel) => {
|
|
||||||
await requestCreateConvo(model)
|
|
||||||
await initModel(model)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-3 py-10">
|
<div className="flex flex-col items-center gap-3 py-10">
|
||||||
<MessageCircle size={32} />
|
<MessageCircle size={32} />
|
||||||
<div className="flex flex-col items-center gap-y-2">
|
<div className="flex flex-col items-center gap-y-2">
|
||||||
<h6 className="text-center text-base">No Chat History</h6>
|
<h6 className="text-center text-base">No Chat History</h6>
|
||||||
<p className="text-center text-muted-foreground mb-6">
|
<p className="mb-6 text-center text-muted-foreground">
|
||||||
Get started by creating a new chat.
|
Get started by creating a new chat.
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={onClick} themes="accent">
|
<Button onClick={onClick} themes="accent">
|
||||||
|
|||||||
@ -4,14 +4,34 @@ import { getActiveConvoIdAtom } from './Conversation.atom'
|
|||||||
/**
|
/**
|
||||||
* Stores all chat messages for all conversations
|
* Stores all chat messages for all conversations
|
||||||
*/
|
*/
|
||||||
export const chatMessages = atom<Record<string, ChatMessage[]>>({})
|
const chatMessages = atom<Record<string, ChatMessage[]>>({})
|
||||||
|
|
||||||
export const currentChatMessagesAtom = atom<ChatMessage[]>((get) => {
|
/**
|
||||||
|
* Return the chat messages for the current active conversation
|
||||||
|
*/
|
||||||
|
export const getCurrentChatMessagesAtom = atom<ChatMessage[]>((get) => {
|
||||||
const activeConversationId = get(getActiveConvoIdAtom)
|
const activeConversationId = get(getActiveConvoIdAtom)
|
||||||
if (!activeConversationId) return []
|
if (!activeConversationId) return []
|
||||||
return get(chatMessages)[activeConversationId] ?? []
|
return get(chatMessages)[activeConversationId] ?? []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const setCurrentChatMessagesAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, messages: ChatMessage[]) => {
|
||||||
|
const currentConvoId = get(getActiveConvoIdAtom)
|
||||||
|
if (!currentConvoId) return
|
||||||
|
|
||||||
|
const newData: Record<string, ChatMessage[]> = {
|
||||||
|
...get(chatMessages),
|
||||||
|
}
|
||||||
|
newData[currentConvoId] = messages
|
||||||
|
set(chatMessages, newData)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for pagination. Add old messages to the current conversation
|
||||||
|
*/
|
||||||
export const addOldMessagesAtom = atom(
|
export const addOldMessagesAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, newMessages: ChatMessage[]) => {
|
(get, set, newMessages: ChatMessage[]) => {
|
||||||
|
|||||||
@ -1,62 +1,50 @@
|
|||||||
import { toChatMessage } from '@models/ChatMessage'
|
import { toChatMessage } from '@models/ChatMessage'
|
||||||
import { executeSerial } from '@services/pluginService'
|
import { executeSerial } from '@services/pluginService'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { DataService } from '@janhq/core'
|
import { DataService } from '@janhq/core'
|
||||||
import { addOldMessagesAtom } from '@helpers/atoms/ChatMessage.atom'
|
import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom'
|
||||||
import {
|
import {
|
||||||
currentConversationAtom,
|
getCurrentChatMessagesAtom,
|
||||||
conversationStatesAtom,
|
setCurrentChatMessagesAtom,
|
||||||
updateConversationHasMoreAtom,
|
} from '@helpers/atoms/ChatMessage.atom'
|
||||||
} from '@helpers/atoms/Conversation.atom'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hooks to get chat messages for current(active) conversation
|
* Custom hooks to get chat messages for current(active) conversation
|
||||||
*
|
|
||||||
* @param offset for pagination purpose
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
const useChatMessages = (offset = 0) => {
|
const useChatMessages = () => {
|
||||||
const [loading, setLoading] = useState(true)
|
const setMessages = useSetAtom(setCurrentChatMessagesAtom)
|
||||||
const addOldChatMessages = useSetAtom(addOldMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
const currentConvo = useAtomValue(currentConversationAtom)
|
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
||||||
const convoStates = useAtomValue(conversationStatesAtom)
|
|
||||||
const updateConvoHasMore = useSetAtom(updateConversationHasMoreAtom)
|
const getMessages = async (convoId: string) => {
|
||||||
|
const data: any = await executeSerial(
|
||||||
|
DataService.GetConversationMessages,
|
||||||
|
convoId
|
||||||
|
)
|
||||||
|
if (!data) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseMessages(data)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentConvo) {
|
if (!activeConvoId) {
|
||||||
|
console.error('active convo is undefined')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const hasMore = convoStates[currentConvo._id ?? '']?.hasMore ?? true
|
|
||||||
if (!hasMore) return
|
|
||||||
|
|
||||||
const getMessages = async () => {
|
getMessages(activeConvoId)
|
||||||
executeSerial(DataService.GetConversationMessages, currentConvo._id).then(
|
.then((messages) => {
|
||||||
(data: any) => {
|
setMessages(messages)
|
||||||
if (!data) {
|
})
|
||||||
return
|
.catch((err) => {
|
||||||
}
|
console.error(err)
|
||||||
const newMessages = parseMessages(data ?? [])
|
})
|
||||||
addOldChatMessages(newMessages)
|
}, [activeConvoId])
|
||||||
updateConvoHasMore(currentConvo._id ?? '', false)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
getMessages()
|
|
||||||
}, [
|
|
||||||
offset,
|
|
||||||
convoStates,
|
|
||||||
addOldChatMessages,
|
|
||||||
updateConvoHasMore,
|
|
||||||
currentConvo,
|
|
||||||
])
|
|
||||||
|
|
||||||
return {
|
return { messages }
|
||||||
loading: loading,
|
|
||||||
error: undefined,
|
|
||||||
hasMore: convoStates[currentConvo?._id ?? '']?.hasMore ?? true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMessages(messages: RawMessage[]): ChatMessage[] {
|
function parseMessages(messages: RawMessage[]): ChatMessage[] {
|
||||||
|
|||||||
@ -1,25 +1,18 @@
|
|||||||
import { useAtom, useSetAtom } from 'jotai'
|
import { useAtom, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { executeSerial } from '@services/pluginService'
|
import { executeSerial } from '@services/pluginService'
|
||||||
import { DataService, ModelManagementService } from '@janhq/core'
|
import { DataService, ModelManagementService } from '@janhq/core'
|
||||||
import {
|
import {
|
||||||
userConversationsAtom,
|
userConversationsAtom,
|
||||||
setActiveConvoIdAtom,
|
setActiveConvoIdAtom,
|
||||||
addNewConversationStateAtom,
|
addNewConversationStateAtom,
|
||||||
updateConversationWaitingForResponseAtom,
|
|
||||||
updateConversationErrorAtom,
|
|
||||||
} from '@helpers/atoms/Conversation.atom'
|
} from '@helpers/atoms/Conversation.atom'
|
||||||
import useInitModel from './useInitModel'
|
|
||||||
|
|
||||||
const useCreateConversation = () => {
|
const useCreateConversation = () => {
|
||||||
const { initModel } = useInitModel()
|
|
||||||
const [userConversations, setUserConversations] = useAtom(
|
const [userConversations, setUserConversations] = useAtom(
|
||||||
userConversationsAtom
|
userConversationsAtom
|
||||||
)
|
)
|
||||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
||||||
const addNewConvoState = useSetAtom(addNewConversationStateAtom)
|
const addNewConvoState = useSetAtom(addNewConversationStateAtom)
|
||||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
|
||||||
const updateConvError = useSetAtom(updateConversationErrorAtom)
|
|
||||||
|
|
||||||
const createConvoByBot = async (bot: Bot) => {
|
const createConvoByBot = async (bot: Bot) => {
|
||||||
const model = await executeSerial(
|
const model = await executeSerial(
|
||||||
@ -48,14 +41,6 @@ const useCreateConversation = () => {
|
|||||||
}
|
}
|
||||||
const id = await executeSerial(DataService.CreateConversation, conv)
|
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 = {
|
const mappedConvo: Conversation = {
|
||||||
_id: id,
|
_id: id,
|
||||||
modelId: model._id,
|
modelId: model._id,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { currentPromptAtom } from '@helpers/JotaiWrapper'
|
import { currentPromptAtom } from '@helpers/JotaiWrapper'
|
||||||
import { execute } from '@services/pluginService'
|
import { executeSerial } from '@services/pluginService'
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { DataService } from '@janhq/core'
|
import { DataService } from '@janhq/core'
|
||||||
import { deleteConversationMessage } from '@helpers/atoms/ChatMessage.atom'
|
import { deleteConversationMessage } from '@helpers/atoms/ChatMessage.atom'
|
||||||
@ -33,7 +33,7 @@ export default function useDeleteConversation() {
|
|||||||
const deleteConvo = async () => {
|
const deleteConvo = async () => {
|
||||||
if (activeConvoId) {
|
if (activeConvoId) {
|
||||||
try {
|
try {
|
||||||
await execute(DataService.DeleteConversation, activeConvoId)
|
await executeSerial(DataService.DeleteConversation, activeConvoId)
|
||||||
const currentConversations = userConversations.filter(
|
const currentConversations = userConversations.filter(
|
||||||
(c) => c._id !== activeConvoId
|
(c) => c._id !== activeConvoId
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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 }
|
|
||||||
}
|
|
||||||
@ -1,16 +1,22 @@
|
|||||||
import { executeSerial } from '@services/pluginService'
|
import { executeSerial } from '@services/pluginService'
|
||||||
import { ModelManagementService, InferenceService } from '@janhq/core'
|
import { ModelManagementService, InferenceService } from '@janhq/core'
|
||||||
import useInitModel from './useInitModel'
|
import { useAtom, useSetAtom } from 'jotai'
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
export default function useStartStopModel() {
|
export default function useStartStopModel() {
|
||||||
const { initModel } = useInitModel()
|
const [activeModel, setActiveModel] = useAtom(activeAssistantModelAtom)
|
||||||
const setActiveModel = useSetAtom(activeAssistantModelAtom)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const setStateModel = useSetAtom(stateModel)
|
const setStateModel = useSetAtom(stateModel)
|
||||||
|
|
||||||
const startModel = async (modelId: string) => {
|
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 })
|
setStateModel({ state: 'start', loading: true, model: modelId })
|
||||||
|
|
||||||
const model = await executeSerial(
|
const model = await executeSerial(
|
||||||
ModelManagementService.GetModelById,
|
ModelManagementService.GetModelById,
|
||||||
modelId
|
modelId
|
||||||
@ -18,10 +24,23 @@ export default function useStartStopModel() {
|
|||||||
if (!model) {
|
if (!model) {
|
||||||
alert(`Model ${modelId} not found! Please re-download the model first.`)
|
alert(`Model ${modelId} not found! Please re-download the model first.`)
|
||||||
setStateModel((prev) => ({ ...prev, loading: false }))
|
setStateModel((prev) => ({ ...prev, loading: false }))
|
||||||
} else {
|
|
||||||
await initModel(model)
|
|
||||||
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 {
|
||||||
|
console.debug(
|
||||||
|
`Init model successfully!, take ${Date.now() - currentTime}ms`
|
||||||
|
)
|
||||||
|
setActiveModel(model)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopModel = async (modelId: string) => {
|
const stopModel = async (modelId: string) => {
|
||||||
@ -33,5 +52,5 @@ export default function useStartStopModel() {
|
|||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { startModel, stopModel }
|
return { loading, startModel, stopModel }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,12 +24,11 @@
|
|||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.14",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"embla-carousel": "^8.0.0-rc11",
|
|
||||||
"embla-carousel-react": "^8.0.0-rc11",
|
|
||||||
"eslint": "8.45.0",
|
"eslint": "8.45.0",
|
||||||
"eslint-config-next": "13.4.10",
|
"eslint-config-next": "13.4.10",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
|
"react-intersection-observer": "^9.5.2",
|
||||||
"jotai": "^2.4.0",
|
"jotai": "^2.4.0",
|
||||||
"jotai-optics": "^0.3.1",
|
"jotai-optics": "^0.3.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
@ -39,7 +38,6 @@
|
|||||||
"next": "13.4.10",
|
"next": "13.4.10",
|
||||||
"next-auth": "^4.23.1",
|
"next-auth": "^4.23.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"optics-ts": "^2.4.1",
|
|
||||||
"postcss": "8.4.26",
|
"postcss": "8.4.26",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user