import React, { FC, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Avatar, PresenceBadge, Textarea } from '@fluentui/react-components'; import commonStore, { ModelStatus } from '../stores/commonStore'; import { observer } from 'mobx-react-lite'; import { v4 as uuid } from 'uuid'; import classnames from 'classnames'; import { fetchEventSource } from '@microsoft/fetch-event-source'; import { ConversationPair, getConversationPairs, Record } from '../utils/get-conversation-pairs'; import logo from '../assets/images/logo.jpg'; import MarkdownRender from '../components/MarkdownRender'; import { ToolTipButton } from '../components/ToolTipButton'; import { ArrowCircleUp28Regular, Delete28Regular, RecordStop28Regular, Save28Regular } from '@fluentui/react-icons'; import { CopyButton } from '../components/CopyButton'; import { ReadButton } from '../components/ReadButton'; import { toast } from 'react-toastify'; import { WorkHeader } from '../components/WorkHeader'; import { DialogButton } from '../components/DialogButton'; import { OpenFileFolder, OpenSaveFileDialog } from '../../wailsjs/go/backend_golang/App'; import { toastWithButton } from '../utils'; export const userName = 'M E'; export const botName = 'A I'; export enum MessageType { Normal, Error } export type Side = 'left' | 'right' export type Color = 'neutral' | 'brand' | 'colorful' export type MessageItem = { sender: string, type: MessageType, color: Color, avatarImg?: string, time: string, content: string, side: Side, done: boolean } export type Conversation = { [uuid: string]: MessageItem } let chatSseController: AbortController | null = null; const ChatPanel: FC = observer(() => { const { t } = useTranslation(); const bodyRef = useRef(null); const inputRef = useRef(null); const port = commonStore.getCurrentModelConfig().apiParameters.apiPort; let lastMessageId: string; let generating: boolean = false; if (commonStore.conversationOrder.length > 0) { lastMessageId = commonStore.conversationOrder[commonStore.conversationOrder.length - 1]; const lastMessage = commonStore.conversation[lastMessageId]; if (lastMessage.sender === botName) generating = !lastMessage.done; } useEffect(() => { if (inputRef.current) inputRef.current.style.maxHeight = '16rem'; scrollToBottom(); }, []); useEffect(() => { if (commonStore.conversationOrder.length === 0) { commonStore.setConversationOrder(['welcome']); commonStore.setConversation({ 'welcome': { sender: botName, type: MessageType.Normal, color: 'colorful', avatarImg: logo, time: new Date().toISOString(), content: t('Hello! I\'m RWKV, an open-source and commercially usable large language model.'), side: 'left', done: true } }); } }, []); const scrollToBottom = () => { if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight; }; const handleKeyDownOrClick = (e: any) => { e.stopPropagation(); if (e.type === 'click' || (e.keyCode === 13 && !e.shiftKey)) { e.preventDefault(); if (commonStore.status.status === ModelStatus.Offline) { toast(t('Please click the button in the top right corner to start the model'), { type: 'warning' }); return; } if (!commonStore.currentInput) return; onSubmit(commonStore.currentInput); commonStore.setCurrentInput(''); } }; const onSubmit = (message: string) => { const newId = uuid(); commonStore.conversation[newId] = { sender: userName, type: MessageType.Normal, color: 'brand', time: new Date().toISOString(), content: message, side: 'right', done: true }; commonStore.setConversation(commonStore.conversation); commonStore.conversationOrder.push(newId); commonStore.setConversationOrder(commonStore.conversationOrder); const records: Record[] = []; commonStore.conversationOrder.forEach((uuid, index) => { const messageItem = commonStore.conversation[uuid]; if (messageItem.done && messageItem.type === MessageType.Normal && messageItem.sender === botName) { if (index > 0) { const questionId = commonStore.conversationOrder[index - 1]; const question = commonStore.conversation[questionId]; if (question.done && question.type === MessageType.Normal && question.sender === userName) { records.push({ question: question.content, answer: messageItem.content }); } } } }); const messages = getConversationPairs(records, false); (messages as ConversationPair[]).push({ role: 'user', content: message }); const answerId = uuid(); commonStore.conversation[answerId] = { sender: botName, type: MessageType.Normal, color: 'colorful', avatarImg: logo, time: new Date().toISOString(), content: '', side: 'left', done: false }; commonStore.setConversation(commonStore.conversation); commonStore.conversationOrder.push(answerId); commonStore.setConversationOrder(commonStore.conversationOrder); setTimeout(scrollToBottom); let answer = ''; chatSseController = new AbortController(); fetchEventSource(`http://127.0.0.1:${port}/chat/completions`, // https://api.openai.com/v1/chat/completions || http://127.0.0.1:${port}/chat/completions { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer sk-` }, body: JSON.stringify({ messages, stream: true, model: 'gpt-3.5-turbo' }), signal: chatSseController?.signal, onmessage(e) { console.log('sse message', e); scrollToBottom(); if (e.data === '[DONE]') { commonStore.conversation[answerId].done = true; commonStore.conversation[answerId].content = commonStore.conversation[answerId].content.trim(); commonStore.setConversation(commonStore.conversation); commonStore.setConversationOrder([...commonStore.conversationOrder]); return; } let data; try { data = JSON.parse(e.data); } catch (error) { console.debug('json error', error); return; } if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) { answer += data.choices[0]?.delta?.content || ''; commonStore.conversation[answerId].content = answer; commonStore.setConversation(commonStore.conversation); commonStore.setConversationOrder([...commonStore.conversationOrder]); } }, onclose() { console.log('Connection closed'); }, onerror(err) { commonStore.conversation[answerId].type = MessageType.Error; commonStore.conversation[answerId].done = true; commonStore.setConversation(commonStore.conversation); commonStore.setConversationOrder([...commonStore.conversationOrder]); throw err; } }); }; return (
{commonStore.conversationOrder.map((uuid, index) => { const messageItem = commonStore.conversation[uuid]; return
{ const utils = document.getElementById('utils-' + uuid); if (utils) utils.classList.remove('invisible'); }} onMouseLeave={() => { const utils = document.getElementById('utils-' + uuid); if (utils) utils.classList.add('invisible'); }} >
{messageItem.content}
{(messageItem.type === MessageType.Error || !messageItem.done) && }
; })}
} size="large" shape="circular" appearance="subtle" title={t('Clear')} contentText={t('Are you sure you want to clear the conversation? It cannot be undone.')} onConfirm={() => { if (generating) chatSseController?.abort(); commonStore.setConversation({}); commonStore.setConversationOrder([]); }} />