2023-05-19 07:40:17 +00:00
|
|
|
import React, {FC, useEffect, 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 07:40:17 +00:00
|
|
|
import {Avatar, Divider, PresenceBadge, Text, Textarea} 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';
|
2023-05-19 07:40:17 +00:00
|
|
|
import MarkdownRender from '../components/MarkdownRender';
|
2023-05-19 12:10:30 +00:00
|
|
|
import {ToolTipButton} from '../components/ToolTipButton';
|
|
|
|
import {ArrowCircleUp28Regular, Delete28Regular, RecordStop28Regular} from '@fluentui/react-icons';
|
2023-05-19 12:55:12 +00:00
|
|
|
import {CopyButton} from '../components/CopyButton';
|
|
|
|
import {ReadButton} from '../components/ReadButton';
|
2023-05-19 06:22:37 +00:00
|
|
|
|
|
|
|
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);
|
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;
|
|
|
|
if (conversationsOrder.length > 0) {
|
|
|
|
lastMessageId = conversationsOrder[conversationsOrder.length - 1];
|
|
|
|
const lastMessage = conversations[lastMessageId];
|
|
|
|
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-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();
|
|
|
|
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();
|
|
|
|
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});
|
2023-05-19 06:22:37 +00:00
|
|
|
}
|
|
|
|
}
|
2023-05-19 12:10:30 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
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]') {
|
2023-05-19 06:22:37 +00:00
|
|
|
conversations[answerId].done = true;
|
|
|
|
setConversations(conversations);
|
|
|
|
setConversationsOrder([...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 || '';
|
|
|
|
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-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">
|
|
|
|
{conversationsOrder.map((uuid, index) => {
|
|
|
|
const conversation = conversations[uuid];
|
|
|
|
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}
|
|
|
|
image={conversation.avatarImg ? {src: conversation.avatarImg} : undefined}
|
|
|
|
/>
|
|
|
|
<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">
|
|
|
|
<div className="grow"/>
|
|
|
|
{(conversation.type === MessageType.Error || !conversation.done) &&
|
|
|
|
<PresenceBadge size="extra-small" status={
|
|
|
|
conversation.type === MessageType.Error ? 'busy' : 'away'
|
|
|
|
}/>
|
|
|
|
}
|
|
|
|
<div className="flex invisible" id={'utils-' + uuid}>
|
|
|
|
<ReadButton content={conversation.content}/>
|
|
|
|
<CopyButton content={conversation.content}/>
|
|
|
|
</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')}
|
|
|
|
icon={<Delete28Regular/>}
|
|
|
|
size="large" shape="circular" appearance="subtle"
|
|
|
|
onClick={(e) => {
|
|
|
|
setConversations({});
|
|
|
|
setConversationsOrder([]);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<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')}
|
|
|
|
icon={generating ? <RecordStop28Regular/> : <ArrowCircleUp28Regular/>}
|
|
|
|
size="large" shape="circular" appearance="subtle"
|
|
|
|
onClick={(e) => {
|
|
|
|
if (generating) {
|
|
|
|
sseControllerRef.current?.abort();
|
|
|
|
if (lastMessageId) {
|
|
|
|
conversations[lastMessageId].type = MessageType.Error;
|
|
|
|
conversations[lastMessageId].done = true;
|
|
|
|
setConversations(conversations);
|
|
|
|
setConversationsOrder([...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
|
|
|
|
|
|
|
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
|
|
|
});
|