RWKV-Runner/frontend/src/pages/Chat.tsx

302 lines
11 KiB
TypeScript
Raw Normal View History

2023-05-21 05:48:11 +00:00
import React, { FC, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
2023-05-24 13:27:23 +00:00
import { Avatar, PresenceBadge, Textarea } from '@fluentui/react-components';
2023-05-21 05:48:11 +00:00
import commonStore, { ModelStatus } from '../stores/commonStore';
import { observer } from 'mobx-react-lite';
import { v4 as uuid } from 'uuid';
2023-05-19 06:22:37 +00:00
import classnames from 'classnames';
2023-05-21 05:48:11 +00:00
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { ConversationPair, getConversationPairs, Record } from '../utils/get-conversation-pairs';
2023-05-29 12:13:06 +00:00
import logo from '../../../build/appicon.jpg';
2023-05-19 07:40:17 +00:00
import MarkdownRender from '../components/MarkdownRender';
2023-05-21 05:48:11 +00:00
import { ToolTipButton } from '../components/ToolTipButton';
import { ArrowCircleUp28Regular, Delete28Regular, RecordStop28Regular } from '@fluentui/react-icons';
import { CopyButton } from '../components/CopyButton';
import { ReadButton } from '../components/ReadButton';
import { toast } from 'react-toastify';
2023-05-24 13:27:23 +00:00
import { WorkHeader } from '../components/WorkHeader';
2023-05-19 06:22:37 +00:00
2023-05-20 08:07:08 +00:00
export const userName = 'M E';
export const botName = 'A I';
2023-05-19 06:22:37 +00:00
2023-05-20 08:07:08 +00:00
export enum MessageType {
2023-05-19 06:22:37 +00:00
Normal,
Error
}
2023-05-20 08:07:08 +00:00
export type Side = 'left' | 'right'
2023-05-19 06:22:37 +00:00
2023-05-20 08:07:08 +00:00
export type Color = 'neutral' | 'brand' | 'colorful'
2023-05-19 06:22:37 +00:00
2023-05-20 08:07:08 +00:00
export type MessageItem = {
2023-05-19 06:22:37 +00:00
sender: string,
type: MessageType,
color: Color,
avatarImg?: string,
time: string,
content: string,
side: Side,
done: boolean
}
2023-05-20 08:07:08 +00:00
export type Conversations = {
2023-05-19 06:22:37 +00:00
[uuid: string]: MessageItem
}
const ChatPanel: FC = observer(() => {
2023-05-21 05:48:11 +00:00
const { t } = useTranslation();
2023-05-19 06:22:37 +00:00
const [message, setMessage] = useState('');
const bodyRef = useRef<HTMLDivElement>(null);
2023-05-19 07:40:17 +00:00
const inputRef = useRef<HTMLTextAreaElement>(null);
2023-05-19 06:22:37 +00:00
const port = commonStore.getCurrentModelConfig().apiParameters.apiPort;
2023-05-19 12:10:30 +00:00
const sseControllerRef = useRef<AbortController | null>(null);
let lastMessageId: string;
let generating: boolean = false;
2023-05-20 08:07:08 +00:00
if (commonStore.conversationsOrder.length > 0) {
lastMessageId = commonStore.conversationsOrder[commonStore.conversationsOrder.length - 1];
const lastMessage = commonStore.conversations[lastMessageId];
2023-05-19 12:10:30 +00:00
if (lastMessage.sender === botName)
generating = !lastMessage.done;
}
2023-05-19 06:22:37 +00:00
2023-05-19 07:40:17 +00:00
useEffect(() => {
if (inputRef.current)
inputRef.current.style.maxHeight = '16rem';
2023-05-23 04:24:39 +00:00
scrollToBottom();
2023-05-19 07:40:17 +00:00
}, []);
2023-05-20 08:07:08 +00:00
useEffect(() => {
if (commonStore.conversationsOrder.length === 0) {
commonStore.setConversationsOrder(['welcome']);
commonStore.setConversations({
'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 available large language model.'),
side: 'left',
done: true
}
});
}
}, []);
2023-05-19 06:22:37 +00:00
const scrollToBottom = () => {
if (bodyRef.current)
bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
};
2023-05-19 12:10:30 +00:00
const handleKeyDownOrClick = (e: any) => {
e.stopPropagation();
if (e.type === 'click' || (e.keyCode === 13 && !e.shiftKey)) {
e.preventDefault();
2023-05-24 16:51:45 +00:00
if (commonStore.status.status === ModelStatus.Offline) {
2023-05-21 05:48:11 +00:00
toast(t('Please click the button in the top right corner to start the model'), { type: 'warning' });
2023-05-20 02:38:35 +00:00
return;
}
2023-05-19 12:10:30 +00:00
if (!message) return;
onSubmit(message);
2023-05-19 06:22:37 +00:00
setMessage('');
2023-05-19 12:10:30 +00:00
}
};
const onSubmit = (message: string) => {
const newId = uuid();
2023-05-20 08:07:08 +00:00
commonStore.conversations[newId] = {
2023-05-19 12:10:30 +00:00
sender: userName,
type: MessageType.Normal,
color: 'brand',
time: new Date().toISOString(),
content: message,
side: 'right',
done: true
};
2023-05-20 08:07:08 +00:00
commonStore.setConversations(commonStore.conversations);
commonStore.conversationsOrder.push(newId);
commonStore.setConversationsOrder(commonStore.conversationsOrder);
2023-05-19 12:10:30 +00:00
const records: Record[] = [];
2023-05-20 08:07:08 +00:00
commonStore.conversationsOrder.forEach((uuid, index) => {
const conversation = commonStore.conversations[uuid];
2023-05-19 12:10:30 +00:00
if (conversation.done && conversation.type === MessageType.Normal && conversation.sender === botName) {
if (index > 0) {
2023-05-20 08:07:08 +00:00
const questionId = commonStore.conversationsOrder[index - 1];
const question = commonStore.conversations[questionId];
2023-05-19 12:10:30 +00:00
if (question.done && question.type === MessageType.Normal && question.sender === userName) {
2023-05-21 05:48:11 +00:00
records.push({ question: question.content, answer: conversation.content });
2023-05-19 06:22:37 +00:00
}
}
2023-05-19 12:10:30 +00:00
}
});
const messages = getConversationPairs(records, false);
2023-05-21 05:48:11 +00:00
(messages as ConversationPair[]).push({ role: 'user', content: message });
2023-05-19 12:10:30 +00:00
const answerId = uuid();
2023-05-20 08:07:08 +00:00
commonStore.conversations[answerId] = {
2023-05-19 12:10:30 +00:00
sender: botName,
type: MessageType.Normal,
color: 'colorful',
avatarImg: logo,
time: new Date().toISOString(),
content: '',
side: 'left',
done: false
};
2023-05-20 08:07:08 +00:00
commonStore.setConversations(commonStore.conversations);
commonStore.conversationsOrder.push(answerId);
commonStore.setConversationsOrder(commonStore.conversationsOrder);
2023-05-19 12:10:30 +00:00
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]') {
2023-05-20 08:07:08 +00:00
commonStore.conversations[answerId].done = true;
2023-05-21 15:44:56 +00:00
commonStore.conversations[answerId].content = commonStore.conversations[answerId].content.trim();
2023-05-20 08:07:08 +00:00
commonStore.setConversations(commonStore.conversations);
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
2023-05-19 12:10:30 +00:00
return;
2023-05-19 06:22:37 +00:00
}
2023-05-19 12:10:30 +00:00
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 || '';
2023-05-20 08:07:08 +00:00
commonStore.conversations[answerId].content = answer;
commonStore.setConversations(commonStore.conversations);
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
2023-05-19 12:10:30 +00:00
}
},
onclose() {
console.log('Connection closed');
},
onerror(err) {
2023-05-20 08:07:08 +00:00
commonStore.conversations[answerId].type = MessageType.Error;
commonStore.conversations[answerId].done = true;
commonStore.setConversations(commonStore.conversations);
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
2023-05-19 12:10:30 +00:00
throw err;
}
});
2023-05-19 06:22:37 +00:00
};
2023-05-05 15:23:34 +00:00
2023-05-19 02:08:28 +00:00
return (
2023-05-19 06:22:37 +00:00
<div className="flex flex-col w-full grow gap-4 pt-4 overflow-hidden">
<div ref={bodyRef} className="grow overflow-y-scroll overflow-x-hidden pr-2">
2023-05-20 08:07:08 +00:00
{commonStore.conversationsOrder.map((uuid, index) => {
const conversation = commonStore.conversations[uuid];
2023-05-19 06:22:37 +00:00
return <div
key={uuid}
className={classnames(
2023-05-19 07:40:17 +00:00
'flex gap-2 mb-2 overflow-hidden',
2023-05-19 06:22:37 +00:00
conversation.side === 'left' ? 'flex-row' : 'flex-row-reverse'
)}
2023-05-19 12:55:12 +00:00
onMouseEnter={() => {
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');
}}
2023-05-19 06:22:37 +00:00
>
<Avatar
color={conversation.color}
name={conversation.sender}
2023-05-21 05:48:11 +00:00
image={conversation.avatarImg ? { src: conversation.avatarImg } : undefined}
2023-05-19 06:22:37 +00:00
/>
<div
className={classnames(
2023-05-19 07:40:17 +00:00
'p-2 rounded-lg overflow-hidden',
2023-05-19 06:22:37 +00:00
conversation.side === 'left' ? 'bg-gray-200' : 'bg-blue-500',
conversation.side === 'left' ? 'text-gray-600' : 'text-white'
)}
>
2023-05-19 07:40:17 +00:00
<MarkdownRender>{conversation.content}</MarkdownRender>
2023-05-19 06:22:37 +00:00
</div>
2023-05-19 12:55:12 +00:00
<div className="flex flex-col gap-1 items-start">
2023-05-21 05:48:11 +00:00
<div className="grow" />
2023-05-19 12:55:12 +00:00
{(conversation.type === MessageType.Error || !conversation.done) &&
<PresenceBadge size="extra-small" status={
conversation.type === MessageType.Error ? 'busy' : 'away'
2023-05-21 05:48:11 +00:00
} />
2023-05-19 12:55:12 +00:00
}
<div className="flex invisible" id={'utils-' + uuid}>
2023-05-21 05:48:11 +00:00
<ReadButton content={conversation.content} />
<CopyButton content={conversation.content} />
2023-05-19 12:55:12 +00:00
</div>
</div>
2023-05-19 06:22:37 +00:00
</div>;
})}
</div>
2023-05-19 12:10:30 +00:00
<div className="flex items-end gap-2">
<ToolTipButton desc={t('Clear')}
2023-05-21 05:48:11 +00:00
icon={<Delete28Regular />}
size="large" shape="circular" appearance="subtle"
onClick={(e) => {
if (generating)
sseControllerRef.current?.abort();
commonStore.setConversations({});
commonStore.setConversationsOrder([]);
}}
2023-05-19 12:10:30 +00:00
/>
<Textarea
ref={inputRef}
className="grow"
resize="vertical"
placeholder={t('Type your message here')!}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={handleKeyDownOrClick}
/>
<ToolTipButton desc={generating ? t('Stop') : t('Send')}
2023-05-21 05:48:11 +00:00
icon={generating ? <RecordStop28Regular /> : <ArrowCircleUp28Regular />}
size="large" shape="circular" appearance="subtle"
onClick={(e) => {
if (generating) {
sseControllerRef.current?.abort();
if (lastMessageId) {
commonStore.conversations[lastMessageId].type = MessageType.Error;
commonStore.conversations[lastMessageId].done = true;
commonStore.setConversations(commonStore.conversations);
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
}
} else {
handleKeyDownOrClick(e);
}
}} />
2023-05-19 06:22:37 +00:00
</div>
</div>
2023-05-19 02:08:28 +00:00
);
2023-05-19 06:22:37 +00:00
});
2023-05-19 02:08:28 +00:00
export const Chat: FC = observer(() => {
2023-05-05 15:23:34 +00:00
return (
2023-05-19 02:08:28 +00:00
<div className="flex flex-col gap-1 p-2 h-full overflow-hidden">
2023-05-24 13:27:23 +00:00
<WorkHeader />
2023-05-21 05:48:11 +00:00
<ChatPanel />
2023-05-19 02:08:28 +00:00
</div>
2023-05-05 15:23:34 +00:00
);
2023-05-19 02:08:28 +00:00
});