chat page

This commit is contained in:
josc146 2023-05-19 14:22:37 +08:00
parent bb8d121991
commit 1105fbf6ec
9 changed files with 277 additions and 20 deletions

View File

@ -56,7 +56,7 @@ async def completions(body: CompletionBody, request: Request):
set_rwkv_config(model, body) set_rwkv_config(model, body)
if body.stream: if body.stream:
for response, delta in rwkv_generate( for response, delta in rwkv_generate(
model, completion_text, stop="Bob:" model, completion_text, stop="\n\nBob"
): ):
if await request.is_disconnected(): if await request.is_disconnected():
break break
@ -90,7 +90,7 @@ async def completions(body: CompletionBody, request: Request):
else: else:
response = None response = None
for response, delta in rwkv_generate( for response, delta in rwkv_generate(
model, completion_text, stop="Bob:" model, completion_text, stop="\n\nBob"
): ):
pass pass
yield { yield {

View File

@ -10,6 +10,8 @@
"dependencies": { "dependencies": {
"@fluentui/react-components": "^9.20.0", "@fluentui/react-components": "^9.20.0",
"@fluentui/react-icons": "^2.0.201", "@fluentui/react-icons": "^2.0.201",
"@microsoft/fetch-event-source": "^2.0.1",
"classnames": "^2.3.2",
"i18next": "^22.4.15", "i18next": "^22.4.15",
"mobx": "^6.9.0", "mobx": "^6.9.0",
"mobx-react-lite": "^3.4.3", "mobx-react-lite": "^3.4.3",
@ -19,11 +21,13 @@
"react-router": "^6.11.1", "react-router": "^6.11.1",
"react-router-dom": "^6.11.1", "react-router-dom": "^6.11.1",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"usehooks-ts": "^2.9.1" "usehooks-ts": "^2.9.1",
"uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.6", "@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.4",
"@types/uuid": "^9.0.1",
"@vitejs/plugin-react": "^4.0.0", "@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"postcss": "^8.4.23", "postcss": "^8.4.23",
@ -1885,6 +1889,11 @@
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true "dev": true
}, },
"node_modules/@microsoft/fetch-event-source": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1964,6 +1973,12 @@
"resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.3.tgz", "resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.3.tgz",
"integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
}, },
"node_modules/@types/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmmirror.com/@types/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
"dev": true
},
"node_modules/@vitejs/plugin-react": { "node_modules/@vitejs/plugin-react": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.0.0.tgz",
@ -2168,6 +2183,11 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
@ -3505,6 +3525,14 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true "dev": true
}, },
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "4.3.6", "version": "4.3.6",
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.3.6.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-4.3.6.tgz",

View File

@ -11,6 +11,8 @@
"dependencies": { "dependencies": {
"@fluentui/react-components": "^9.20.0", "@fluentui/react-components": "^9.20.0",
"@fluentui/react-icons": "^2.0.201", "@fluentui/react-icons": "^2.0.201",
"@microsoft/fetch-event-source": "^2.0.1",
"classnames": "^2.3.2",
"i18next": "^22.4.15", "i18next": "^22.4.15",
"mobx": "^6.9.0", "mobx": "^6.9.0",
"mobx-react-lite": "^3.4.3", "mobx-react-lite": "^3.4.3",
@ -20,11 +22,13 @@
"react-router": "^6.11.1", "react-router": "^6.11.1",
"react-router-dom": "^6.11.1", "react-router-dom": "^6.11.1",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"usehooks-ts": "^2.9.1" "usehooks-ts": "^2.9.1",
"uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.6", "@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.4",
"@types/uuid": "^9.0.1",
"@vitejs/plugin-react": "^4.0.0", "@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"postcss": "^8.4.23", "postcss": "^8.4.23",

View File

@ -63,5 +63,8 @@
"Convert Success": "转换成功", "Convert Success": "转换成功",
"Convert Failed": "转换失败", "Convert Failed": "转换失败",
"Model Not Found": "模型不存在", "Model Not Found": "模型不存在",
"Model Status": "模型状态" "Model Status": "模型状态",
"Clear": "清除",
"Send": "发送",
"Type your message here": "在此输入消息"
} }

View File

@ -1,5 +1,6 @@
import {FC, ReactElement} from 'react'; import {FC, ReactElement} from 'react';
import {Label, Tooltip} from '@fluentui/react-components'; import {Label, Tooltip} from '@fluentui/react-components';
import classnames from 'classnames';
export const Labeled: FC<{ export const Labeled: FC<{
label: string; desc?: string, content: ReactElement, flex?: boolean, spaceBetween?: boolean label: string; desc?: string, content: ReactElement, flex?: boolean, spaceBetween?: boolean
@ -11,10 +12,10 @@ export const Labeled: FC<{
spaceBetween spaceBetween
}) => { }) => {
return ( return (
<div className={ <div className={classnames(
(flex ? 'flex' : 'grid grid-cols-2') + ' ' + 'items-center',
(spaceBetween ? 'justify-between' : '') + ' ' + flex ? 'flex' : 'grid grid-cols-2',
'items-center' spaceBetween && 'justify-between')
}> }>
{desc ? {desc ?
<Tooltip content={desc} showDelay={0} hideDelay={0} relationship="description"> <Tooltip content={desc} showDelay={0} hideDelay={0} relationship="description">

View File

@ -1,5 +1,4 @@
import React, * as React_2 from 'react'; import React, {CSSProperties, FC} from 'react';
import {CSSProperties, FC} from 'react';
import {Input} from '@fluentui/react-components'; import {Input} from '@fluentui/react-components';
import {SliderOnChangeData} from '@fluentui/react-slider'; import {SliderOnChangeData} from '@fluentui/react-slider';
@ -8,7 +7,7 @@ export const NumberInput: FC<{
min: number, min: number,
max: number, max: number,
step?: number, step?: number,
onChange?: (ev: React_2.ChangeEvent<HTMLInputElement>, data: SliderOnChangeData) => void onChange?: (ev: React.ChangeEvent<HTMLInputElement>, data: SliderOnChangeData) => void
style?: CSSProperties style?: CSSProperties
}> = ({value, min, max, step, onChange, style}) => { }> = ({value, min, max, step, onChange, style}) => {
return ( return (

View File

@ -1,5 +1,4 @@
import React, * as React_2 from 'react'; import React, {FC, useEffect, useRef} from 'react';
import {FC, useEffect, useRef} from 'react';
import {Slider, Text} from '@fluentui/react-components'; import {Slider, Text} from '@fluentui/react-components';
import {SliderOnChangeData} from '@fluentui/react-slider'; import {SliderOnChangeData} from '@fluentui/react-slider';
import {NumberInput} from './NumberInput'; import {NumberInput} from './NumberInput';
@ -10,7 +9,7 @@ export const ValuedSlider: FC<{
max: number, max: number,
step?: number, step?: number,
input?: boolean input?: boolean
onChange?: (ev: React_2.ChangeEvent<HTMLInputElement>, data: SliderOnChangeData) => void onChange?: (ev: React.ChangeEvent<HTMLInputElement>, data: SliderOnChangeData) => void
}> = ({value, min, max, step, input, onChange}) => { }> = ({value, min, max, step, input, onChange}) => {
const sliderRef = useRef<HTMLInputElement>(null); const sliderRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {

View File

@ -1,18 +1,214 @@
import React, {FC} from 'react'; import React, {FC, useRef, useState} from 'react';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
import {RunButton} from '../components/RunButton'; import {RunButton} from '../components/RunButton';
import {Divider, PresenceBadge, Text} from '@fluentui/react-components'; import {Avatar, Divider, Input, PresenceBadge, Text} from '@fluentui/react-components';
import commonStore, {ModelStatus} from '../stores/commonStore'; import commonStore, {ModelStatus} from '../stores/commonStore';
import {observer} from 'mobx-react-lite'; import {observer} from 'mobx-react-lite';
import {PresenceBadgeStatus} from '@fluentui/react-badge'; import {PresenceBadgeStatus} from '@fluentui/react-badge';
import {ConfigSelector} from '../components/ConfigSelector'; 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';
const ChatPanel: FC = () => { const userName = 'M E';
return ( const botName = 'A I';
<div></div>
); 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;
}
});
}
};
return (
<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>
);
});
const statusText = { const statusText = {
[ModelStatus.Offline]: 'Offline', [ModelStatus.Offline]: 'Offline',
[ModelStatus.Starting]: 'Starting', [ModelStatus.Starting]: 'Starting',

View File

@ -0,0 +1,27 @@
export type Record = {
question: string;
answer: string;
}
export type ConversationPair = {
role: string;
content: string;
}
export function getConversationPairs(records: Record[], isCompletion: boolean): string | ConversationPair[] {
let pairs;
if (isCompletion) {
pairs = '';
for (const record of records) {
pairs += 'Human: ' + record.question + '\nAI: ' + record.answer + '\n';
}
} else {
pairs = [];
for (const record of records) {
pairs.push({role: 'user', content: record.question});
pairs.push({role: 'assistant', content: record.answer});
}
}
return pairs;
}