diff --git a/frontend/.yarnrc.yml b/frontend/.yarnrc.yml index f3bd10e60..da3ec2ca7 100644 --- a/frontend/.yarnrc.yml +++ b/frontend/.yarnrc.yml @@ -2,6 +2,6 @@ compressionLevel: 1 enableGlobalCache: true -nodeLinker: pnpm +nodeLinker: node-modules yarnPath: .yarn/releases/yarn-4.7.0.cjs diff --git a/frontend/app/components/Kai/KaiStore.ts b/frontend/app/components/Kai/KaiStore.ts new file mode 100644 index 000000000..9d6b92fa8 --- /dev/null +++ b/frontend/app/components/Kai/KaiStore.ts @@ -0,0 +1,207 @@ +import { makeAutoObservable, runInAction } from 'mobx'; +import { BotChunk, ChatManager, Message } from './SocketManager'; +import { aiService } from 'App/services'; +import { toast } from 'react-toastify'; + +class KaiStore { + chatManager: ChatManager | null = null; + processingStage: BotChunk | null = null; + messages: Message[] = []; + queryText = ''; + loadingChat = false; + replacing = false; + + constructor() { + makeAutoObservable(this); + } + + get lastHumanMessage() { + let msg = null; + let index = null; + for (let i = this.messages.length - 1; i >= 0; i--) { + const message = this.messages[i]; + if (message.isUser) { + msg = message; + index = i; + break; + } + } + return { msg, index }; + } + + get lastKaiMessage() { + let msg = null; + let index = null; + for (let i = this.messages.length - 1; i >= 0; i--) { + const message = this.messages[i]; + if (!message.isUser) { + msg = message; + index = i; + break; + } + } + return { msg, index }; + } + + setQueryText = (text: string) => { + this.queryText = text; + }; + + setLoadingChat = (loading: boolean) => { + this.loadingChat = loading; + }; + + setChatManager = (chatManager: ChatManager) => { + this.chatManager = chatManager; + }; + + setProcessingStage = (stage: BotChunk | null) => { + this.processingStage = stage; + }; + + setMessages = (messages: Message[]) => { + this.messages = messages; + }; + + addMessage = (message: Message) => { + this.messages.push(message); + }; + + editMessage = (text: string) => { + this.setQueryText(text); + this.setReplacing(true); + }; + + replaceAtIndex = (message: Message, index: number) => { + const messages = [...this.messages]; + messages[index] = message; + this.setMessages(messages); + }; + + deleteAtIndex = (indexes: number[]) => { + if (!indexes.length) return; + const messages = this.messages.filter((_, i) => !indexes.includes(i)); + console.log(messages, indexes) + runInAction(() => { + this.messages = messages; + }) + } + + getChat = async (projectId: string, userId: string, threadId: string) => { + this.setLoadingChat(true); + try { + const res = await aiService.getKaiChat(projectId, userId, threadId); + if (res && res.length) { + this.setMessages( + res.map((m) => { + const isUser = m.role === 'human'; + return { + text: m.content, + isUser: isUser, + messageId: m.message_id, + }; + }), + ); + } + } catch (e) { + console.error(e); + toast.error("Couldn't load chat history. Please try again later."); + } finally { + this.setLoadingChat(false); + } + }; + + createChatManager = ( + settings: { projectId: string; userId: string; threadId: string }, + setTitle: (title: string) => void, + initialMsg: string | null, + ) => { + this.chatManager = new ChatManager(settings); + this.chatManager.setOnMsgHook({ + msgCallback: (msg) => { + if (msg.stage === 'start') { + this.setProcessingStage({ + ...msg, + content: 'Processing your request...', + }); + } + if (msg.stage === 'chart') { + this.setProcessingStage(msg); + } + if (msg.stage === 'final') { + const msgObj = { + text: msg.content, + isUser: false, + messageId: msg.messageId, + } + this.addMessage(msgObj); + this.setProcessingStage(null); + } + }, + titleCallback: setTitle, + }); + + if (initialMsg) { + this.sendMessage(initialMsg); + } + }; + + setReplacing = (replacing: boolean) => { + this.replacing = replacing; + }; + + sendMessage = (message: string) => { + console.log('send') + if (this.chatManager) { + this.chatManager.sendMessage(message, this.replacing); + } + if (this.replacing) { + console.log(this.lastHumanMessage, this.lastKaiMessage, 'replacing these two') + const deleting = [] + if (this.lastHumanMessage.index !== null) { + deleting.push(this.lastHumanMessage.index); + } + if (this.lastKaiMessage.index !== null) { + deleting.push(this.lastKaiMessage.index) + } + this.deleteAtIndex(deleting); + this.setReplacing(false) + } + this.addMessage({ + text: message, + isUser: true, + messageId: Date.now().toString(), + }); + }; + + sendMsgFeedback = (feedback: string, messageId: string) => { + const settings = { projectId: '2325', userId: '0' }; + aiService + .feedback( + feedback === 'like', + messageId, + settings.projectId, + settings.userId, + ) + .then(() => { + toast.success('Feedback saved.'); + }) + .catch((e) => { + console.error(e); + toast.error('Failed to send feedback. Please try again later.'); + }); + }; + + clearChat = () => { + this.setMessages([]); + this.setProcessingStage(null); + this.setLoadingChat(false); + this.setQueryText(''); + if (this.chatManager) { + this.chatManager.disconnect(); + this.chatManager = null; + } + }; +} + +export const kaiStore = new KaiStore(); diff --git a/frontend/app/components/Kai/SocketManager.ts b/frontend/app/components/Kai/SocketManager.ts index 020cf18c8..c2e755f55 100644 --- a/frontend/app/components/Kai/SocketManager.ts +++ b/frontend/app/components/Kai/SocketManager.ts @@ -36,12 +36,13 @@ export class ChatManager { this.socket = socket; } - sendMesage = (message: string) => { + sendMessage = (message: string, isReplace = false) => { this.socket.emit( 'message', JSON.stringify({ message, threadId: this.threadId, + replace: isReplace, }), ); }; @@ -78,3 +79,7 @@ export interface Message { isUser: boolean; messageId: string; } + +export interface SentMessage extends Message { + replace: boolean +} diff --git a/frontend/app/components/Kai/components/ChatInput.tsx b/frontend/app/components/Kai/components/ChatInput.tsx index 522228b5d..e3b671c23 100644 --- a/frontend/app/components/Kai/components/ChatInput.tsx +++ b/frontend/app/components/Kai/components/ChatInput.tsx @@ -1,25 +1,37 @@ import React from 'react' import { Button, Input } from "antd"; import { SendHorizonal } from "lucide-react"; +import { kaiStore } from "../KaiStore"; +import { observer } from "mobx-react-lite"; function ChatInput({ isLoading, onSubmit }: { isLoading?: boolean, onSubmit: (str: string) => void }) { - const [inputValue, setInputValue] = React.useState(''); + const inputRef = React.useRef(null); + const inputValue = kaiStore.queryText; + const setInputValue = (text: string) => { + kaiStore.setQueryText(text) + } const submit = () => { - onSubmit(inputValue) - setInputValue('') + if (inputValue.length > 0) { + onSubmit(inputValue) + setInputValue('') + } } + + React.useEffect(() => { + if (inputRef.current) { + inputRef.current.focus() + } + }, [inputValue]) + return ( setInputValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && inputValue) { - submit() - } - }} suffix={