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

246 lines
8.0 KiB
TypeScript
Raw Normal View History

2023-05-19 06:22:37 +00:00
import React, {FC, useRef, useState} from 'react';
2023-05-18 12:48:53 +00:00
import {useTranslation} from 'react-i18next';
2023-05-19 02:08:28 +00:00
import {RunButton} from '../components/RunButton';
2023-05-19 06:22:37 +00:00
import {Avatar, Divider, Input, PresenceBadge, Text} from '@fluentui/react-components';
2023-05-19 02:08:28 +00:00
import commonStore, {ModelStatus} from '../stores/commonStore';
import {observer} from 'mobx-react-lite';
import {PresenceBadgeStatus} from '@fluentui/react-badge';
import {ConfigSelector} from '../components/ConfigSelector';
2023-05-19 06:22:37 +00:00
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';
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<Conversations>({});
const [conversationsOrder, setConversationsOrder] = useState<string[]>([]);
const bodyRef = useRef<HTMLDivElement>(null);
const port = commonStore.getCurrentModelConfig().apiParameters.apiPort;
const scrollToBottom = () => {
if (bodyRef.current)
bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
};
const handleSubmit = (e: React.ChangeEvent<HTMLFormElement>) => {
e.preventDefault();
if (message !== '') {
setMessage('');
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 = '';
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'
}),
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;
}
});
}
};
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">
{conversationsOrder.map((uuid, index) => {
const conversation = conversations[uuid];
return <div
key={uuid}
className={classnames(
'flex gap-2 mb-2',
conversation.side === 'left' ? 'flex-row' : 'flex-row-reverse'
)}
>
<Avatar
color={conversation.color}
name={conversation.sender}
image={conversation.avatarImg ? {src: conversation.avatarImg} : undefined}
/>
<div
className={classnames(
'p-2 rounded-lg',
conversation.side === 'left' ? 'bg-gray-200' : 'bg-blue-500',
conversation.side === 'left' ? 'text-gray-600' : 'text-white'
)}
>
{conversation.content}
</div>
{(conversation.type === MessageType.Error || !conversation.done) &&
<PresenceBadge status={
conversation.type === MessageType.Error ? 'busy' : 'away'
}/>
}
</div>;
})}
</div>
<div className="flex">
<button className="bg-blue-500 text-white rounded-lg py-2 px-4 mr-2" onClick={() => {
setConversations({});
setConversationsOrder([]);
}}>
{t('Clear')}
</button>
<form onSubmit={handleSubmit} className="flex grow">
<Input
type="text"
className="grow mr-2"
placeholder={t('Type your message here')!}
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button type="submit" className="bg-blue-500 text-white rounded-lg py-2 px-4">
{t('Send')}
</button>
</form>
</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
const statusText = {
[ModelStatus.Offline]: 'Offline',
[ModelStatus.Starting]: 'Starting',
[ModelStatus.Loading]: 'Loading',
[ModelStatus.Working]: 'Working'
};
const badgeStatus: { [modelStatus: number]: PresenceBadgeStatus } = {
[ModelStatus.Offline]: 'unknown',
[ModelStatus.Starting]: 'away',
[ModelStatus.Loading]: 'away',
[ModelStatus.Working]: 'available'
};
export const Chat: FC = observer(() => {
2023-05-18 12:48:53 +00:00
const {t} = useTranslation();
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">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<PresenceBadge status={badgeStatus[commonStore.modelStatus]}/>
<Text size={100}>{t('Model Status') + ': ' + t(statusText[commonStore.modelStatus])}</Text>
</div>
<div className="flex items-center gap-2">
<ConfigSelector size="small"/>
<RunButton iconMode/>
</div>
2023-05-17 15:27:52 +00:00
</div>
2023-05-19 02:08:28 +00:00
<Divider style={{flexGrow: 0}}/>
<ChatPanel/>
</div>
2023-05-05 15:23:34 +00:00
);
2023-05-19 02:08:28 +00:00
});