This commit is contained in:
josc146 2023-05-20 16:07:08 +08:00
parent 5883686003
commit ca33d75f19
8 changed files with 188 additions and 149 deletions

View File

@ -18,19 +18,19 @@ import {CopyButton} from '../components/CopyButton';
import {ReadButton} from '../components/ReadButton'; import {ReadButton} from '../components/ReadButton';
import {toast} from 'react-toastify'; import {toast} from 'react-toastify';
const userName = 'M E'; export const userName = 'M E';
const botName = 'A I'; export const botName = 'A I';
enum MessageType { export enum MessageType {
Normal, Normal,
Error Error
} }
type Side = 'left' | 'right' export type Side = 'left' | 'right'
type Color = 'neutral' | 'brand' | 'colorful' export type Color = 'neutral' | 'brand' | 'colorful'
type MessageItem = { export type MessageItem = {
sender: string, sender: string,
type: MessageType, type: MessageType,
color: Color, color: Color,
@ -41,26 +41,13 @@ type MessageItem = {
done: boolean done: boolean
} }
type Conversations = { export type Conversations = {
[uuid: string]: MessageItem [uuid: string]: MessageItem
} }
const ChatPanel: FC = observer(() => { const ChatPanel: FC = observer(() => {
const {t} = useTranslation(); const {t} = useTranslation();
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [conversations, setConversations] = useState<Conversations>({
'welcome': {
sender: botName,
type: MessageType.Normal,
color: 'colorful',
avatarImg: logo,
time: new Date().toISOString(),
content: t('Hello! I\'m RWKV, an open-source and commercially available large language model.'),
side: 'left',
done: true
}
});
const [conversationsOrder, setConversationsOrder] = useState<string[]>(['welcome']);
const bodyRef = useRef<HTMLDivElement>(null); const bodyRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
const port = commonStore.getCurrentModelConfig().apiParameters.apiPort; const port = commonStore.getCurrentModelConfig().apiParameters.apiPort;
@ -68,9 +55,9 @@ const ChatPanel: FC = observer(() => {
let lastMessageId: string; let lastMessageId: string;
let generating: boolean = false; let generating: boolean = false;
if (conversationsOrder.length > 0) { if (commonStore.conversationsOrder.length > 0) {
lastMessageId = conversationsOrder[conversationsOrder.length - 1]; lastMessageId = commonStore.conversationsOrder[commonStore.conversationsOrder.length - 1];
const lastMessage = conversations[lastMessageId]; const lastMessage = commonStore.conversations[lastMessageId];
if (lastMessage.sender === botName) if (lastMessage.sender === botName)
generating = !lastMessage.done; generating = !lastMessage.done;
} }
@ -80,6 +67,24 @@ const ChatPanel: FC = observer(() => {
inputRef.current.style.maxHeight = '16rem'; inputRef.current.style.maxHeight = '16rem';
}, []); }, []);
useEffect(() => {
if (commonStore.conversationsOrder.length === 0) {
commonStore.setConversationsOrder(['welcome']);
commonStore.setConversations({
'welcome': {
sender: botName,
type: MessageType.Normal,
color: 'colorful',
avatarImg: logo,
time: new Date().toISOString(),
content: t('Hello! I\'m RWKV, an open-source and commercially available large language model.'),
side: 'left',
done: true
}
});
}
}, []);
const scrollToBottom = () => { const scrollToBottom = () => {
if (bodyRef.current) if (bodyRef.current)
bodyRef.current.scrollTop = bodyRef.current.scrollHeight; bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
@ -101,7 +106,7 @@ const ChatPanel: FC = observer(() => {
const onSubmit = (message: string) => { const onSubmit = (message: string) => {
const newId = uuid(); const newId = uuid();
conversations[newId] = { commonStore.conversations[newId] = {
sender: userName, sender: userName,
type: MessageType.Normal, type: MessageType.Normal,
color: 'brand', color: 'brand',
@ -110,17 +115,17 @@ const ChatPanel: FC = observer(() => {
side: 'right', side: 'right',
done: true done: true
}; };
setConversations(conversations); commonStore.setConversations(commonStore.conversations);
conversationsOrder.push(newId); commonStore.conversationsOrder.push(newId);
setConversationsOrder(conversationsOrder); commonStore.setConversationsOrder(commonStore.conversationsOrder);
const records: Record[] = []; const records: Record[] = [];
conversationsOrder.forEach((uuid, index) => { commonStore.conversationsOrder.forEach((uuid, index) => {
const conversation = conversations[uuid]; const conversation = commonStore.conversations[uuid];
if (conversation.done && conversation.type === MessageType.Normal && conversation.sender === botName) { if (conversation.done && conversation.type === MessageType.Normal && conversation.sender === botName) {
if (index > 0) { if (index > 0) {
const questionId = conversationsOrder[index - 1]; const questionId = commonStore.conversationsOrder[index - 1];
const question = conversations[questionId]; const question = commonStore.conversations[questionId];
if (question.done && question.type === MessageType.Normal && question.sender === userName) { if (question.done && question.type === MessageType.Normal && question.sender === userName) {
records.push({question: question.content, answer: conversation.content}); records.push({question: question.content, answer: conversation.content});
} }
@ -131,7 +136,7 @@ const ChatPanel: FC = observer(() => {
(messages as ConversationPair[]).push({role: 'user', content: message}); (messages as ConversationPair[]).push({role: 'user', content: message});
const answerId = uuid(); const answerId = uuid();
conversations[answerId] = { commonStore.conversations[answerId] = {
sender: botName, sender: botName,
type: MessageType.Normal, type: MessageType.Normal,
color: 'colorful', color: 'colorful',
@ -141,9 +146,9 @@ const ChatPanel: FC = observer(() => {
side: 'left', side: 'left',
done: false done: false
}; };
setConversations(conversations); commonStore.setConversations(commonStore.conversations);
conversationsOrder.push(answerId); commonStore.conversationsOrder.push(answerId);
setConversationsOrder(conversationsOrder); commonStore.setConversationsOrder(commonStore.conversationsOrder);
setTimeout(scrollToBottom); setTimeout(scrollToBottom);
let answer = ''; let answer = '';
sseControllerRef.current = new AbortController(); sseControllerRef.current = new AbortController();
@ -164,9 +169,9 @@ const ChatPanel: FC = observer(() => {
console.log('sse message', e); console.log('sse message', e);
scrollToBottom(); scrollToBottom();
if (e.data === '[DONE]') { if (e.data === '[DONE]') {
conversations[answerId].done = true; commonStore.conversations[answerId].done = true;
setConversations(conversations); commonStore.setConversations(commonStore.conversations);
setConversationsOrder([...conversationsOrder]); commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
return; return;
} }
let data; let data;
@ -178,19 +183,19 @@ const ChatPanel: FC = observer(() => {
} }
if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) { if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) {
answer += data.choices[0]?.delta?.content || ''; answer += data.choices[0]?.delta?.content || '';
conversations[answerId].content = answer; commonStore.conversations[answerId].content = answer;
setConversations(conversations); commonStore.setConversations(commonStore.conversations);
setConversationsOrder([...conversationsOrder]); commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
} }
}, },
onclose() { onclose() {
console.log('Connection closed'); console.log('Connection closed');
}, },
onerror(err) { onerror(err) {
conversations[answerId].type = MessageType.Error; commonStore.conversations[answerId].type = MessageType.Error;
conversations[answerId].done = true; commonStore.conversations[answerId].done = true;
setConversations(conversations); commonStore.setConversations(commonStore.conversations);
setConversationsOrder([...conversationsOrder]); commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
throw err; throw err;
} }
}); });
@ -199,8 +204,8 @@ const ChatPanel: FC = observer(() => {
return ( return (
<div className="flex flex-col w-full grow gap-4 pt-4 overflow-hidden"> <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"> <div ref={bodyRef} className="grow overflow-y-scroll overflow-x-hidden pr-2">
{conversationsOrder.map((uuid, index) => { {commonStore.conversationsOrder.map((uuid, index) => {
const conversation = conversations[uuid]; const conversation = commonStore.conversations[uuid];
return <div return <div
key={uuid} key={uuid}
className={classnames( className={classnames(
@ -250,8 +255,8 @@ const ChatPanel: FC = observer(() => {
icon={<Delete28Regular/>} icon={<Delete28Regular/>}
size="large" shape="circular" appearance="subtle" size="large" shape="circular" appearance="subtle"
onClick={(e) => { onClick={(e) => {
setConversations({}); commonStore.setConversations({});
setConversationsOrder([]); commonStore.setConversationsOrder([]);
}} }}
/> />
<Textarea <Textarea
@ -270,10 +275,10 @@ const ChatPanel: FC = observer(() => {
if (generating) { if (generating) {
sseControllerRef.current?.abort(); sseControllerRef.current?.abort();
if (lastMessageId) { if (lastMessageId) {
conversations[lastMessageId].type = MessageType.Error; commonStore.conversations[lastMessageId].type = MessageType.Error;
conversations[lastMessageId].done = true; commonStore.conversations[lastMessageId].done = true;
setConversations(conversations); commonStore.setConversations(commonStore.conversations);
setConversationsOrder([...conversationsOrder]); commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
} }
} else { } else {
handleKeyDownOrClick(e); handleKeyDownOrClick(e);

View File

@ -4,7 +4,7 @@ import React, {FC} from 'react';
import {Section} from '../components/Section'; import {Section} from '../components/Section';
import {Labeled} from '../components/Labeled'; import {Labeled} from '../components/Labeled';
import {ToolTipButton} from '../components/ToolTipButton'; import {ToolTipButton} from '../components/ToolTipButton';
import commonStore, {ApiParameters, Device, ModelParameters, Precision} from '../stores/commonStore'; import commonStore from '../stores/commonStore';
import {observer} from 'mobx-react-lite'; import {observer} from 'mobx-react-lite';
import {toast} from 'react-toastify'; import {toast} from 'react-toastify';
import {ValuedSlider} from '../components/ValuedSlider'; import {ValuedSlider} from '../components/ValuedSlider';
@ -18,6 +18,57 @@ import manifest from '../../../manifest.json';
import {getStrategy, refreshLocalModels} from '../utils'; import {getStrategy, refreshLocalModels} from '../utils';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
export type ApiParameters = {
apiPort: number
maxResponseToken: number;
temperature: number;
topP: number;
presencePenalty: number;
frequencyPenalty: number;
}
export type Device = 'CPU' | 'CUDA';
export type Precision = 'fp16' | 'int8' | 'fp32';
export type ModelParameters = {
// different models can not have the same name
modelName: string;
device: Device;
precision: Precision;
storedLayers: number;
maxStoredLayers: number;
enableHighPrecisionForLastLayer: boolean;
}
export type ModelConfig = {
// different configs can have the same name
name: string;
apiParameters: ApiParameters
modelParameters: ModelParameters
}
export const defaultModelConfigs: ModelConfig[] = [
{
name: 'Default',
apiParameters: {
apiPort: 8000,
maxResponseToken: 4100,
temperature: 1,
topP: 1,
presencePenalty: 0,
frequencyPenalty: 0
},
modelParameters: {
modelName: 'RWKV-4-Raven-1B5-v11-Eng99%-Other1%-20230425-ctx4096.pth',
device: 'CUDA',
precision: 'fp16',
storedLayers: 25,
maxStoredLayers: 25,
enableHighPrecisionForLastLayer: false
}
}
];
export const Configs: FC = observer(() => { export const Configs: FC = observer(() => {
const {t} = useTranslation(); const {t} = useTranslation();
const [selectedIndex, setSelectedIndex] = React.useState(commonStore.currentModelConfigIndex); const [selectedIndex, setSelectedIndex] = React.useState(commonStore.currentModelConfigIndex);

View File

@ -9,6 +9,18 @@ import {ToolTipButton} from '../components/ToolTipButton';
import {Folder20Regular, Pause20Regular, Play20Regular} from '@fluentui/react-icons'; import {Folder20Regular, Pause20Regular, Play20Regular} from '@fluentui/react-icons';
import {ContinueDownload, OpenFileFolder, PauseDownload} from '../../wailsjs/go/backend_golang/App'; import {ContinueDownload, OpenFileFolder, PauseDownload} from '../../wailsjs/go/backend_golang/App';
export type DownloadStatus = {
name: string;
path: string;
url: string;
transferred: number;
size: number;
speed: number;
progress: number;
downloading: boolean;
done: boolean;
}
export const Downloads: FC = observer(() => { export const Downloads: FC = observer(() => {
const {t} = useTranslation(); const {t} = useTranslation();
const finishedDownloads = commonStore.downloadList.filter((status) => status.done).length; const finishedDownloads = commonStore.downloadList.filter((status) => status.done).length;

View File

@ -15,7 +15,7 @@ import {
import {ToolTipButton} from '../components/ToolTipButton'; import {ToolTipButton} from '../components/ToolTipButton';
import {ArrowClockwise20Regular, ArrowDownload20Regular, Folder20Regular, Open20Regular} from '@fluentui/react-icons'; import {ArrowClockwise20Regular, ArrowDownload20Regular, Folder20Regular, Open20Regular} from '@fluentui/react-icons';
import {observer} from 'mobx-react-lite'; import {observer} from 'mobx-react-lite';
import commonStore, {ModelSourceItem} from '../stores/commonStore'; import commonStore from '../stores/commonStore';
import {BrowserOpenURL} from '../../wailsjs/runtime'; import {BrowserOpenURL} from '../../wailsjs/runtime';
import {AddToDownloadList, OpenFileFolder} from '../../wailsjs/go/backend_golang/App'; import {AddToDownloadList, OpenFileFolder} from '../../wailsjs/go/backend_golang/App';
import manifest from '../../../manifest.json'; import manifest from '../../../manifest.json';
@ -24,6 +24,18 @@ import {bytesToGb, refreshModels, saveConfigs, toastWithButton} from '../utils';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
import {useNavigate} from 'react-router'; import {useNavigate} from 'react-router';
export type ModelSourceItem = {
name: string;
size: number;
lastUpdated: string;
desc?: { [lang: string]: string; };
SHA256?: string;
url?: string;
downloadUrl?: string;
isLocal?: boolean;
lastUpdatedMs?: number;
};
const columns: TableColumnDefinition<ModelSourceItem>[] = [ const columns: TableColumnDefinition<ModelSourceItem>[] = [
createTableColumn<ModelSourceItem>({ createTableColumn<ModelSourceItem>({
columnId: 'file', columnId: 'file',

View File

@ -5,7 +5,20 @@ import {Labeled} from '../components/Labeled';
import commonStore from '../stores/commonStore'; import commonStore from '../stores/commonStore';
import {observer} from 'mobx-react-lite'; import {observer} from 'mobx-react-lite';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
import {checkUpdate, Language, Languages} from '../utils'; import {checkUpdate} from '../utils';
export const Languages = {
dev: 'English', // i18n default
zh: '简体中文'
};
export type Language = keyof typeof Languages;
export type Settings = {
language: Language,
darkMode: boolean
autoUpdatesCheck: boolean
}
export const Settings: FC = observer(() => { export const Settings: FC = observer(() => {
const {t, i18n} = useTranslation(); const {t, i18n} = useTranslation();

View File

@ -1,8 +1,9 @@
import commonStore, {defaultModelConfigs} from './stores/commonStore'; import commonStore from './stores/commonStore';
import {ReadJson} from '../wailsjs/go/backend_golang/App'; import {ReadJson} from '../wailsjs/go/backend_golang/App';
import {checkUpdate, downloadProgramFiles, LocalConfig, refreshModels} from './utils'; import {checkUpdate, downloadProgramFiles, LocalConfig, refreshModels} from './utils';
import {getStatus} from './apis'; import {getStatus} from './apis';
import {EventsOn} from '../wailsjs/runtime'; import {EventsOn} from '../wailsjs/runtime';
import {defaultModelConfigs} from './pages/Configs';
export async function startup() { export async function startup() {
downloadProgramFiles(); downloadProgramFiles();

View File

@ -1,7 +1,12 @@
import {makeAutoObservable} from 'mobx'; import {makeAutoObservable} from 'mobx';
import {getUserLanguage, isSystemLightMode, saveConfigs, Settings} from '../utils'; import {getUserLanguage, isSystemLightMode, saveConfigs} from '../utils';
import {WindowSetDarkTheme, WindowSetLightTheme} from '../../wailsjs/runtime'; import {WindowSetDarkTheme, WindowSetLightTheme} from '../../wailsjs/runtime';
import manifest from '../../../manifest.json'; import manifest from '../../../manifest.json';
import {defaultModelConfigs, ModelConfig} from '../pages/Configs';
import {Conversations} from '../pages/Chat';
import {ModelSourceItem} from '../pages/Models';
import {DownloadStatus} from '../pages/Downloads';
import {Settings} from '../pages/Settings';
export enum ModelStatus { export enum ModelStatus {
Offline, Offline,
@ -10,99 +15,41 @@ export enum ModelStatus {
Working, Working,
} }
export type ModelSourceItem = {
name: string;
size: number;
lastUpdated: string;
desc?: { [lang: string]: string; };
SHA256?: string;
url?: string;
downloadUrl?: string;
isLocal?: boolean;
lastUpdatedMs?: number;
};
export type ApiParameters = {
apiPort: number
maxResponseToken: number;
temperature: number;
topP: number;
presencePenalty: number;
frequencyPenalty: number;
}
export type Device = 'CPU' | 'CUDA';
export type Precision = 'fp16' | 'int8' | 'fp32';
export type ModelParameters = {
// different models can not have the same name
modelName: string;
device: Device;
precision: Precision;
storedLayers: number;
maxStoredLayers: number;
enableHighPrecisionForLastLayer: boolean;
}
export type ModelConfig = {
// different configs can have the same name
name: string;
apiParameters: ApiParameters
modelParameters: ModelParameters
}
export type DownloadStatus = {
name: string;
path: string;
url: string;
transferred: number;
size: number;
speed: number;
progress: number;
downloading: boolean;
done: boolean;
}
export const defaultModelConfigs: ModelConfig[] = [
{
name: 'Default',
apiParameters: {
apiPort: 8000,
maxResponseToken: 4100,
temperature: 1,
topP: 1,
presencePenalty: 0,
frequencyPenalty: 0
},
modelParameters: {
modelName: 'RWKV-4-Raven-1B5-v11-Eng99%-Other1%-20230425-ctx4096.pth',
device: 'CUDA',
precision: 'fp16',
storedLayers: 25,
maxStoredLayers: 25,
enableHighPrecisionForLastLayer: false
}
}
];
class CommonStore { class CommonStore {
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
} }
// global
modelStatus: ModelStatus = ModelStatus.Offline; modelStatus: ModelStatus = ModelStatus.Offline;
// home
introduction: { [lang: string]: string } = manifest.introduction;
// chat
conversations: Conversations = {};
conversationsOrder: string[] = [];
// configs
currentModelConfigIndex: number = 0; currentModelConfigIndex: number = 0;
modelConfigs: ModelConfig[] = []; modelConfigs: ModelConfig[] = [];
// models
modelSourceManifestList: string = 'https://cdn.jsdelivr.net/gh/josstorer/RWKV-Runner/manifest.json;'; modelSourceManifestList: string = 'https://cdn.jsdelivr.net/gh/josstorer/RWKV-Runner/manifest.json;';
modelSourceList: ModelSourceItem[] = []; modelSourceList: ModelSourceItem[] = [];
// downloads
downloadList: DownloadStatus[] = [];
// settings
settings: Settings = { settings: Settings = {
language: getUserLanguage(), language: getUserLanguage(),
darkMode: !isSystemLightMode(), darkMode: !isSystemLightMode(),
autoUpdatesCheck: true autoUpdatesCheck: true
}; };
introduction: { [lang: string]: string } = manifest.introduction;
// about
about: { [lang: string]: string } = manifest.about; about: { [lang: string]: string } = manifest.about;
downloadList: DownloadStatus[] = [];
getCurrentModelConfig = () => { getCurrentModelConfig = () => {
return this.modelConfigs[this.currentModelConfigIndex]; return this.modelConfigs[this.currentModelConfigIndex];
@ -184,6 +131,14 @@ class CommonStore {
setDownloadList = (value: DownloadStatus[]) => { setDownloadList = (value: DownloadStatus[]) => {
this.downloadList = value; this.downloadList = value;
}; };
setConversations = (value: Conversations) => {
this.conversations = value;
};
setConversationsOrder = (value: string[]) => {
this.conversationsOrder = value;
};
} }
export default new CommonStore(); export default new CommonStore();

View File

@ -8,29 +8,19 @@ import {
UpdateApp UpdateApp
} from '../../wailsjs/go/backend_golang/App'; } from '../../wailsjs/go/backend_golang/App';
import manifest from '../../../manifest.json'; import manifest from '../../../manifest.json';
import commonStore, {ModelConfig, ModelParameters, ModelSourceItem} from '../stores/commonStore'; import commonStore from '../stores/commonStore';
import {toast} from 'react-toastify'; import {toast} from 'react-toastify';
import {t} from 'i18next'; import {t} from 'i18next';
import {ToastOptions} from 'react-toastify/dist/types'; import {ToastOptions} from 'react-toastify/dist/types';
import {Button} from '@fluentui/react-components'; import {Button} from '@fluentui/react-components';
import {Language, Languages, Settings} from '../pages/Settings';
export const Languages = { import {ModelSourceItem} from '../pages/Models';
dev: 'English', // i18n default import {ModelConfig, ModelParameters} from '../pages/Configs';
zh: '简体中文'
};
export type Language = keyof typeof Languages;
export type Cache = { export type Cache = {
models: ModelSourceItem[] models: ModelSourceItem[]
} }
export type Settings = {
language: Language,
darkMode: boolean
autoUpdatesCheck: boolean
}
export type LocalConfig = { export type LocalConfig = {
modelSourceManifestList: string modelSourceManifestList: string
currentModelConfigIndex: number currentModelConfigIndex: number