From c7ed4b07c29226182f64b6a4b02d4cb70bd58d3c Mon Sep 17 00:00:00 2001 From: josc146 Date: Wed, 24 May 2023 21:27:23 +0800 Subject: [PATCH] Completion Page --- frontend/src/_locales/zh-hans/main.json | 24 +- frontend/src/components/Labeled.tsx | 13 +- frontend/src/components/WorkHeader.tsx | 46 ++++ frontend/src/pages/Chat.tsx | 38 +-- frontend/src/pages/Completion.tsx | 302 ++++++++++++++++++++++++ frontend/src/pages/Configs.tsx | 10 +- frontend/src/pages/index.tsx | 9 + frontend/src/stores/commonStore.ts | 12 + 8 files changed, 405 insertions(+), 49 deletions(-) create mode 100644 frontend/src/components/WorkHeader.tsx create mode 100644 frontend/src/pages/Completion.tsx diff --git a/frontend/src/_locales/zh-hans/main.json b/frontend/src/_locales/zh-hans/main.json index 2af1d89..025324a 100644 --- a/frontend/src/_locales/zh-hans/main.json +++ b/frontend/src/_locales/zh-hans/main.json @@ -20,11 +20,11 @@ "Manage Models": "管理模型", "Model": "模型", "Model Parameters": "模型参数", - "Frequency Penalty *": "Frequency Penalty *", - "Presence Penalty *": "Presence Penalty *", - "Top_P *": "Top_P *", - "Temperature *": "Temperature *", - "Max Response Token *": "最大响应 Token *", + "Frequency Penalty": "Frequency Penalty", + "Presence Penalty": "Presence Penalty", + "Top_P": "Top_P", + "Temperature": "Temperature", + "Max Response Token": "最大响应 Token", "API Port": "API 端口", "Hover your mouse over the text to view a detailed description. Settings marked with * will take effect immediately after being saved.": "把鼠标悬停在文本上查看详细描述. 标记了星号 * 的设置在保存后会立即生效.", "Default API Parameters": "默认 API 参数", @@ -102,5 +102,17 @@ "Enabling this option can greatly improve inference speed, but there may be compatibility issues. If it fails to start, please turn off this option.": "开启这个选项能大大提升推理速度,但可能存在兼容性问题,如果启动失败,请关闭此选项", "Supported custom cuda file not found": "没有找到支持的自定义cuda文件", "Failed to copy custom cuda file": "自定义cuda文件复制失败", - "Downloading update, please wait. If it is not completed, please manually download the program from GitHub and replace the original program.": "正在下载更新,请等待。如果一直未完成,请从Github手动下载并覆盖原程序" + "Downloading update, please wait. If it is not completed, please manually download the program from GitHub and replace the original program.": "正在下载更新,请等待。如果一直未完成,请从Github手动下载并覆盖原程序", + "Completion": "补全", + "Parameters": "参数", + "Stop Sequences": "停止词", + "When this content appears in the response result, the generation will end.": "响应结果出现该内容时就结束生成", + "Reset": "重置", + "Generate": "生成", + "Writer": "写作", + "Translator": "翻译", + "Catgirl": "猫娘", + "Explain Code": "代码解释", + "Werewolf": "狼人杀", + "Blank": "空白" } \ No newline at end of file diff --git a/frontend/src/components/Labeled.tsx b/frontend/src/components/Labeled.tsx index 483bc6a..b13e327 100644 --- a/frontend/src/components/Labeled.tsx +++ b/frontend/src/components/Labeled.tsx @@ -3,18 +3,25 @@ import { Label, Tooltip } from '@fluentui/react-components'; import classnames from 'classnames'; export const Labeled: FC<{ - label: string; desc?: string | null, content: ReactElement, flex?: boolean, spaceBetween?: boolean + label: string; + desc?: string | null, + content: ReactElement, + flex?: boolean, + spaceBetween?: boolean, + breakline?: boolean }> = ({ label, desc, content, flex, - spaceBetween + spaceBetween, + breakline }) => { return (
{desc ? diff --git a/frontend/src/components/WorkHeader.tsx b/frontend/src/components/WorkHeader.tsx new file mode 100644 index 0000000..ec6100f --- /dev/null +++ b/frontend/src/components/WorkHeader.tsx @@ -0,0 +1,46 @@ +import React, { FC } from 'react'; +import { observer } from 'mobx-react-lite'; +import { Divider, PresenceBadge, Text } from '@fluentui/react-components'; +import commonStore, { ModelStatus } from '../stores/commonStore'; +import { ConfigSelector } from './ConfigSelector'; +import { RunButton } from './RunButton'; +import { PresenceBadgeStatus } from '@fluentui/react-badge'; +import { useTranslation } from 'react-i18next'; + +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 WorkHeader: FC = observer(() => { + const { t } = useTranslation(); + const port = commonStore.getCurrentModelConfig().apiParameters.apiPort; + + return ( +
+
+
+ + {t('Model Status') + ': ' + t(statusText[commonStore.status.modelStatus])} +
+
+ + +
+
+ + {t('This tool\'s API is compatible with OpenAI API. It can be used with any ChatGPT tool you like. Go to the settings of some ChatGPT tool, replace the \'https://api.openai.com\' part in the API address with \'') + `http://127.0.0.1:${port}` + '\'.'} + + +
+ ); +}); \ No newline at end of file diff --git a/frontend/src/pages/Chat.tsx b/frontend/src/pages/Chat.tsx index 682754d..44f4b62 100644 --- a/frontend/src/pages/Chat.tsx +++ b/frontend/src/pages/Chat.tsx @@ -1,11 +1,8 @@ import React, { FC, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { RunButton } from '../components/RunButton'; -import { Avatar, Divider, PresenceBadge, Text, Textarea } from '@fluentui/react-components'; +import { Avatar, PresenceBadge, Textarea } 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'; @@ -17,6 +14,7 @@ import { ArrowCircleUp28Regular, Delete28Regular, RecordStop28Regular } from '@f import { CopyButton } from '../components/CopyButton'; import { ReadButton } from '../components/ReadButton'; import { toast } from 'react-toastify'; +import { WorkHeader } from '../components/WorkHeader'; export const userName = 'M E'; export const botName = 'A I'; @@ -293,40 +291,10 @@ const ChatPanel: FC = observer(() => { ); }); -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(() => { - const { t } = useTranslation(); - const port = commonStore.getCurrentModelConfig().apiParameters.apiPort; - return (
-
-
- - {t('Model Status') + ': ' + t(statusText[commonStore.status.modelStatus])} -
-
- - -
-
- - {t('This tool\'s API is compatible with OpenAI API. It can be used with any ChatGPT tool you like. Go to the settings of some ChatGPT tool, replace the \'https://api.openai.com\' part in the API address with \'') + `http://127.0.0.1:${port}` + '\'.'} - - +
); diff --git a/frontend/src/pages/Completion.tsx b/frontend/src/pages/Completion.tsx new file mode 100644 index 0000000..4b74352 --- /dev/null +++ b/frontend/src/pages/Completion.tsx @@ -0,0 +1,302 @@ +import React, { FC, useEffect, useRef } from 'react'; +import { observer } from 'mobx-react-lite'; +import { WorkHeader } from '../components/WorkHeader'; +import { Button, Dropdown, Input, Option, Textarea } from '@fluentui/react-components'; +import { Labeled } from '../components/Labeled'; +import { ValuedSlider } from '../components/ValuedSlider'; +import { useTranslation } from 'react-i18next'; +import { ApiParameters } from './Configs'; +import commonStore, { ModelStatus } from '../stores/commonStore'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; +import { toast } from 'react-toastify'; + +export type CompletionParams = Omit & { stop: string } + +export type CompletionPreset = { + name: string, + prompt: string, + params: CompletionParams +} + +export const defaultPresets: CompletionPreset[] = [{ + name: 'Writer', + prompt: '以下是不朽的科幻史诗巨著,描写细腻,刻画了宏大的星际文明战争。\n第一章\n', + params: { + maxResponseToken: 4100, + temperature: 1, + topP: 0.5, + presencePenalty: 0.4, + frequencyPenalty: 0.4, + stop: '' + } +}, { + name: 'Translator', + prompt: '', + params: { + maxResponseToken: 4100, + temperature: 1, + topP: 0.5, + presencePenalty: 0.4, + frequencyPenalty: 0.4, + stop: '' + } +}, { + name: 'Catgirl', + prompt: '', + params: { + maxResponseToken: 4100, + temperature: 1, + topP: 0.5, + presencePenalty: 0.4, + frequencyPenalty: 0.4, + stop: '' + } +}, { + name: 'Explain Code', + prompt: '', + params: { + maxResponseToken: 4100, + temperature: 1, + topP: 0.5, + presencePenalty: 0.4, + frequencyPenalty: 0.4, + stop: '' + } +}, { + name: 'Werewolf', + prompt: '', + params: { + maxResponseToken: 4100, + temperature: 1, + topP: 0.5, + presencePenalty: 0.4, + frequencyPenalty: 0.4, + stop: '' + } +}, { + name: 'Blank', + prompt: '', + params: { + maxResponseToken: 4100, + temperature: 1, + topP: 0.5, + presencePenalty: 0.4, + frequencyPenalty: 0.4, + stop: '' + } +}]; + +const CompletionPanel: FC = observer(() => { + const { t } = useTranslation(); + const inputRef = useRef(null); + const port = commonStore.getCurrentModelConfig().apiParameters.apiPort; + const sseControllerRef = useRef(null); + + const scrollToBottom = () => { + if (inputRef.current) + inputRef.current.scrollTop = inputRef.current.scrollHeight; + }; + + useEffect(() => { + if (inputRef.current) + inputRef.current.style.height = '100%'; + scrollToBottom(); + }, []); + + if (!commonStore.completionPreset) + commonStore.setCompletionPreset(defaultPresets[0]); + + const name = commonStore.completionPreset!.name; + + const prompt = commonStore.completionPreset!.prompt; + const setPrompt = (prompt: string) => { + commonStore.setCompletionPreset({ + ...commonStore.completionPreset!, + prompt + }); + }; + + const params = commonStore.completionPreset!.params; + const setParams = (newParams: Partial) => { + commonStore.setCompletionPreset({ + ...commonStore.completionPreset!, + params: { + ...commonStore.completionPreset!.params, + ...newParams + } + }); + }; + + const onSubmit = (prompt: string) => { + if (commonStore.status.modelStatus === ModelStatus.Offline) { + toast(t('Please click the button in the top right corner to start the model'), { type: 'warning' }); + return; + } + + let answer = ''; + sseControllerRef.current = new AbortController(); + fetchEventSource(`http://127.0.0.1:${port}/completions`, // https://api.openai.com/v1/completions || http://127.0.0.1:${port}/completions + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer sk-` + }, + body: JSON.stringify({ + prompt, + stream: true, + model: 'text-davinci-003', + max_tokens: params.maxResponseToken, + temperature: params.temperature, + top_p: params.topP, + presence_penalty: params.presencePenalty, + frequency_penalty: params.frequencyPenalty, + stop: params.stop || undefined + }), + signal: sseControllerRef.current?.signal, + onmessage(e) { + console.log('sse message', e); + scrollToBottom(); + if (e.data === '[DONE]') { + commonStore.setCompletionGenerating(false); + 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].text; + setPrompt(prompt + answer); + } + }, + onclose() { + console.log('Connection closed'); + }, + onerror(err) { + commonStore.setCompletionGenerating(false); + throw err; + } + }); + }; + + return ( +
+