diff --git a/backend-python/routes/completion.py b/backend-python/routes/completion.py
index fa1ebd1..2d6d587 100644
--- a/backend-python/routes/completion.py
+++ b/backend-python/routes/completion.py
@@ -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 {
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index df8c3c6..c68c01e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -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",
diff --git a/frontend/package.json b/frontend/package.json
index ca00b44..42173a9 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
diff --git a/frontend/src/_locales/zh-hans/main.json b/frontend/src/_locales/zh-hans/main.json
index b20c438..f59006c 100644
--- a/frontend/src/_locales/zh-hans/main.json
+++ b/frontend/src/_locales/zh-hans/main.json
@@ -63,5 +63,8 @@
"Convert Success": "转换成功",
"Convert Failed": "转换失败",
"Model Not Found": "模型不存在",
- "Model Status": "模型状态"
+ "Model Status": "模型状态",
+ "Clear": "清除",
+ "Send": "发送",
+ "Type your message here": "在此输入消息"
}
\ No newline at end of file
diff --git a/frontend/src/components/Labeled.tsx b/frontend/src/components/Labeled.tsx
index 6bb164e..3548e7f 100644
--- a/frontend/src/components/Labeled.tsx
+++ b/frontend/src/components/Labeled.tsx
@@ -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 (
-
{desc ?
diff --git a/frontend/src/components/NumberInput.tsx b/frontend/src/components/NumberInput.tsx
index 3a76772..c5d99da 100644
--- a/frontend/src/components/NumberInput.tsx
+++ b/frontend/src/components/NumberInput.tsx
@@ -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, data: SliderOnChangeData) => void
+ onChange?: (ev: React.ChangeEvent, data: SliderOnChangeData) => void
style?: CSSProperties
}> = ({value, min, max, step, onChange, style}) => {
return (
diff --git a/frontend/src/components/ValuedSlider.tsx b/frontend/src/components/ValuedSlider.tsx
index 71c0654..2d0f337 100644
--- a/frontend/src/components/ValuedSlider.tsx
+++ b/frontend/src/components/ValuedSlider.tsx
@@ -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, data: SliderOnChangeData) => void
+ onChange?: (ev: React.ChangeEvent, data: SliderOnChangeData) => void
}> = ({value, min, max, step, input, onChange}) => {
const sliderRef = useRef(null);
useEffect(() => {
diff --git a/frontend/src/pages/Chat.tsx b/frontend/src/pages/Chat.tsx
index 4e85c9d..5a0f736 100644
--- a/frontend/src/pages/Chat.tsx
+++ b/frontend/src/pages/Chat.tsx
@@ -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({});
+ const [conversationsOrder, setConversationsOrder] = useState([]);
+ const bodyRef = useRef(null);
+ const port = commonStore.getCurrentModelConfig().apiParameters.apiPort;
+
+ const scrollToBottom = () => {
+ if (bodyRef.current)
+ bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
+ };
+
+ const handleSubmit = (e: React.ChangeEvent) => {
+ 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 (
-
+
+
+ {conversationsOrder.map((uuid, index) => {
+ const conversation = conversations[uuid];
+ return
+
+
+ {conversation.content}
+
+ {(conversation.type === MessageType.Error || !conversation.done) &&
+
+ }
+
;
+ })}
+
+
+
+
+
+
);
-};
+});
const statusText = {
[ModelStatus.Offline]: 'Offline',
diff --git a/frontend/src/utils/get-conversation-pairs.ts b/frontend/src/utils/get-conversation-pairs.ts
new file mode 100644
index 0000000..05cf570
--- /dev/null
+++ b/frontend/src/utils/get-conversation-pairs.ts
@@ -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;
+}