chat page
This commit is contained in:
parent
bb8d121991
commit
1105fbf6ec
@ -56,7 +56,7 @@ async def completions(body: CompletionBody, request: Request):
|
||||
set_rwkv_config(model, body)
|
||||
if body.stream:
|
||||
for response, delta in rwkv_generate(
|
||||
model, completion_text, stop="Bob:"
|
||||
model, completion_text, stop="\n\nBob"
|
||||
):
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
@ -90,7 +90,7 @@ async def completions(body: CompletionBody, request: Request):
|
||||
else:
|
||||
response = None
|
||||
for response, delta in rwkv_generate(
|
||||
model, completion_text, stop="Bob:"
|
||||
model, completion_text, stop="\n\nBob"
|
||||
):
|
||||
pass
|
||||
yield {
|
||||
|
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
@ -10,6 +10,8 @@
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.20.0",
|
||||
"@fluentui/react-icons": "^2.0.201",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"classnames": "^2.3.2",
|
||||
"i18next": "^22.4.15",
|
||||
"mobx": "^6.9.0",
|
||||
"mobx-react-lite": "^3.4.3",
|
||||
@ -19,11 +21,13 @@
|
||||
"react-router": "^6.11.1",
|
||||
"react-router-dom": "^6.11.1",
|
||||
"react-toastify": "^9.1.3",
|
||||
"usehooks-ts": "^2.9.1"
|
||||
"usehooks-ts": "^2.9.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
@ -1885,6 +1889,11 @@
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
|
||||
"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": {
|
||||
"version": "2.1.5",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.0.0.tgz",
|
||||
@ -2168,6 +2183,11 @@
|
||||
"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": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
|
||||
@ -3505,6 +3525,14 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"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": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.3.6.tgz",
|
||||
|
@ -11,6 +11,8 @@
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.20.0",
|
||||
"@fluentui/react-icons": "^2.0.201",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"classnames": "^2.3.2",
|
||||
"i18next": "^22.4.15",
|
||||
"mobx": "^6.9.0",
|
||||
"mobx-react-lite": "^3.4.3",
|
||||
@ -20,11 +22,13 @@
|
||||
"react-router": "^6.11.1",
|
||||
"react-router-dom": "^6.11.1",
|
||||
"react-toastify": "^9.1.3",
|
||||
"usehooks-ts": "^2.9.1"
|
||||
"usehooks-ts": "^2.9.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
|
@ -63,5 +63,8 @@
|
||||
"Convert Success": "转换成功",
|
||||
"Convert Failed": "转换失败",
|
||||
"Model Not Found": "模型不存在",
|
||||
"Model Status": "模型状态"
|
||||
"Model Status": "模型状态",
|
||||
"Clear": "清除",
|
||||
"Send": "发送",
|
||||
"Type your message here": "在此输入消息"
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import {FC, ReactElement} from 'react';
|
||||
import {Label, Tooltip} from '@fluentui/react-components';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export const Labeled: FC<{
|
||||
label: string; desc?: string, content: ReactElement, flex?: boolean, spaceBetween?: boolean
|
||||
@ -11,10 +12,10 @@ export const Labeled: FC<{
|
||||
spaceBetween
|
||||
}) => {
|
||||
return (
|
||||
<div className={
|
||||
(flex ? 'flex' : 'grid grid-cols-2') + ' ' +
|
||||
(spaceBetween ? 'justify-between' : '') + ' ' +
|
||||
'items-center'
|
||||
<div className={classnames(
|
||||
'items-center',
|
||||
flex ? 'flex' : 'grid grid-cols-2',
|
||||
spaceBetween && 'justify-between')
|
||||
}>
|
||||
{desc ?
|
||||
<Tooltip content={desc} showDelay={0} hideDelay={0} relationship="description">
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, * as React_2 from 'react';
|
||||
import {CSSProperties, FC} from 'react';
|
||||
import React, {CSSProperties, FC} from 'react';
|
||||
import {Input} from '@fluentui/react-components';
|
||||
import {SliderOnChangeData} from '@fluentui/react-slider';
|
||||
|
||||
@ -8,7 +7,7 @@ export const NumberInput: FC<{
|
||||
min: number,
|
||||
max: number,
|
||||
step?: number,
|
||||
onChange?: (ev: React_2.ChangeEvent<HTMLInputElement>, data: SliderOnChangeData) => void
|
||||
onChange?: (ev: React.ChangeEvent<HTMLInputElement>, data: SliderOnChangeData) => void
|
||||
style?: CSSProperties
|
||||
}> = ({value, min, max, step, onChange, style}) => {
|
||||
return (
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, * as React_2 from 'react';
|
||||
import {FC, useEffect, useRef} from 'react';
|
||||
import React, {FC, useEffect, useRef} from 'react';
|
||||
import {Slider, Text} from '@fluentui/react-components';
|
||||
import {SliderOnChangeData} from '@fluentui/react-slider';
|
||||
import {NumberInput} from './NumberInput';
|
||||
@ -10,7 +9,7 @@ export const ValuedSlider: FC<{
|
||||
max: number,
|
||||
step?: number,
|
||||
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}) => {
|
||||
const sliderRef = useRef<HTMLInputElement>(null);
|
||||
useEffect(() => {
|
||||
|
@ -1,17 +1,213 @@
|
||||
import React, {FC} from 'react';
|
||||
import React, {FC, useRef, useState} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
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 {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';
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const ChatPanel: FC = () => {
|
||||
return (
|
||||
<div></div>
|
||||
<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 = {
|
||||
[ModelStatus.Offline]: 'Offline',
|
||||
|
27
frontend/src/utils/get-conversation-pairs.ts
Normal file
27
frontend/src/utils/get-conversation-pairs.ts
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user