import React, {FC, useEffect, useRef, useState} from 'react'; import {useTranslation} from 'react-i18next'; import {RunButton} from '../components/RunButton'; import {Avatar, Divider, PresenceBadge, Text, Textarea} from '@fluentui/react-components'; import commonStore, {ModelStatus} from '../stores/commonStore'; import {observer} from 'mobx-react-lite'; import {PresenceBadgeStatus} from '@fluentui/react-badge'; import {ConfigSelector} from '../components/ConfigSelector'; 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 '../../../build/appicon.png'; import MarkdownRender from '../components/MarkdownRender'; import {ToolTipButton} from '../components/ToolTipButton'; import {ArrowCircleUp28Regular, Delete28Regular, RecordStop28Regular} from '@fluentui/react-icons'; const userName = 'M E'; const botName = 'A I'; enum MessageType { Normal, Error } type Side = 'left' | 'right' type Color = 'neutral' | 'brand' | 'colorful' type MessageItem = { sender: string, type: MessageType, color: Color, avatarImg?: string, time: string, content: string, side: Side, done: boolean } type Conversations = { [uuid: string]: MessageItem } const ChatPanel: FC = observer(() => { const {t} = useTranslation(); const [message, setMessage] = useState(''); const [conversations, setConversations] = useState({}); const [conversationsOrder, setConversationsOrder] = useState([]); const bodyRef = useRef(null); const inputRef = useRef(null); const port = commonStore.getCurrentModelConfig().apiParameters.apiPort; const sseControllerRef = useRef(null); let lastMessageId: string; let generating: boolean = false; if (conversationsOrder.length > 0) { lastMessageId = conversationsOrder[conversationsOrder.length - 1]; const lastMessage = conversations[lastMessageId]; if (lastMessage.sender === botName) generating = !lastMessage.done; } useEffect(() => { if (inputRef.current) inputRef.current.style.maxHeight = '16rem'; }, []); 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 (!message) return; onSubmit(message); setMessage(''); } }; const onSubmit = (message: string) => { const newId = uuid(); conversations[newId] = { sender: userName, type: MessageType.Normal, color: 'brand', time: new Date().toISOString(), content: message, side: 'right', done: true }; setConversations(conversations); conversationsOrder.push(newId); setConversationsOrder(conversationsOrder); const records: Record[] = []; conversationsOrder.forEach((uuid, index) => { const conversation = conversations[uuid]; if (conversation.done && conversation.type === MessageType.Normal && conversation.sender === botName) { if (index > 0) { const questionId = conversationsOrder[index - 1]; const question = conversations[questionId]; if (question.done && question.type === MessageType.Normal && question.sender === userName) { records.push({question: question.content, answer: conversation.content}); } } } }); const messages = getConversationPairs(records, false); (messages as ConversationPair[]).push({role: 'user', content: message}); const answerId = uuid(); conversations[answerId] = { sender: botName, type: MessageType.Normal, color: 'colorful', avatarImg: logo, time: new Date().toISOString(), content: '', side: 'left', done: false }; setConversations(conversations); conversationsOrder.push(answerId); setConversationsOrder(conversationsOrder); setTimeout(scrollToBottom); let answer = ''; sseControllerRef.current = 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: sseControllerRef.current?.signal, onmessage(e) { console.log('sse message', e); scrollToBottom(); if (e.data === '[DONE]') { conversations[answerId].done = true; setConversations(conversations); setConversationsOrder([...conversationsOrder]); 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 || ''; conversations[answerId].content = answer; setConversations(conversations); setConversationsOrder([...conversationsOrder]); } }, onclose() { console.log('Connection closed'); }, onerror(err) { conversations[answerId].type = MessageType.Error; conversations[answerId].done = true; setConversations(conversations); setConversationsOrder([...conversationsOrder]); throw err; } }); }; return (
{conversationsOrder.map((uuid, index) => { const conversation = conversations[uuid]; return
{conversation.content}
{(conversation.type === MessageType.Error || !conversation.done) && }
; })}
} size="large" shape="circular" appearance="subtle" onClick={(e) => { setConversations({}); setConversationsOrder([]); }} />