feat: chat presets (experimental)
This commit is contained in:
@@ -28,10 +28,10 @@ import { FC, useEffect, useState } from 'react';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router';
|
||||
import { pages } from './pages';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import commonStore from './stores/commonStore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomToastContainer } from './components/CustomToastContainer';
|
||||
|
||||
const App: FC = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
@@ -87,21 +87,7 @@ const App: FC = observer(() => {
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
<ToastContainer
|
||||
style={{
|
||||
width: '350px'
|
||||
}}
|
||||
position="top-center"
|
||||
autoClose={4000}
|
||||
pauseOnHover={true}
|
||||
hideProgressBar={true}
|
||||
newestOnTop={true}
|
||||
closeOnClick={false}
|
||||
rtl={false}
|
||||
pauseOnFocusLoss={false}
|
||||
draggable={false}
|
||||
theme={commonStore.settings.darkMode ? 'dark' : 'light'}
|
||||
/>
|
||||
<CustomToastContainer />
|
||||
</FluentProvider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -163,5 +163,30 @@
|
||||
"The model file is corrupted, please download again.": "模型文件损坏,请重新下载",
|
||||
"Found no NVIDIA driver, please install the latest driver.": "没有找到NVIDIA驱动,请安装最新驱动",
|
||||
"VRAM is not enough, please reduce stored layers or use a lower precision in Configs page.": "显存不足,请在配置页面减少载入显存层数,或使用更低的精度",
|
||||
"Failed to enable custom CUDA kernel, ninja is required to load C++ extensions. You may be using the CPU version of PyTorch, please reinstall PyTorch with CUDA. Or if you are using a custom Python interpreter, you must compile the CUDA kernel by yourself or disable Custom CUDA kernel acceleration.": "自定义CUDA算子开启失败,需要安装Ninja来读取C++扩展。你可能正在使用CPU版本的PyTorch,请重新安装CUDA版本的PyTorch。如果你正在使用自定义Python解释器,你必须自己编译CUDA算子或禁用自定义CUDA算子加速"
|
||||
"Failed to enable custom CUDA kernel, ninja is required to load C++ extensions. You may be using the CPU version of PyTorch, please reinstall PyTorch with CUDA. Or if you are using a custom Python interpreter, you must compile the CUDA kernel by yourself or disable Custom CUDA kernel acceleration.": "自定义CUDA算子开启失败,需要安装Ninja来读取C++扩展。你可能正在使用CPU版本的PyTorch,请重新安装CUDA版本的PyTorch。如果你正在使用自定义Python解释器,你必须自己编译CUDA算子或禁用自定义CUDA算子加速",
|
||||
"Presets": "预设",
|
||||
"Online": "在线",
|
||||
"english": "英文",
|
||||
"chinese": "中文",
|
||||
"default": "默认",
|
||||
"japanese": "日文",
|
||||
"New Preset": "新建预设",
|
||||
"Import": "导入",
|
||||
"Name": "名称",
|
||||
"Imported successfully": "导入成功",
|
||||
"Failed to import. Please copy a preset to the clipboard.": "导入失败。请复制一个预设到剪贴板",
|
||||
"Clipboard is empty.": "剪贴板没有内容",
|
||||
"Successfully copied to clipboard.": "成功复制到剪贴板",
|
||||
"Edit Messages": "编辑对话",
|
||||
"Go Back": "返回",
|
||||
"Description": "描述",
|
||||
"Avatar Url": "头像图片地址",
|
||||
"Welcome Message": "欢迎语",
|
||||
"Display Preset Messages": "显示预设中的对话",
|
||||
"Tag": "标签",
|
||||
"Activate": "激活",
|
||||
"New": "新建",
|
||||
"user": "用户",
|
||||
"assistant": "AI",
|
||||
"system": "系统"
|
||||
}
|
||||
17
frontend/src/components/CustomToastContainer.tsx
Normal file
17
frontend/src/components/CustomToastContainer.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import commonStore from '../stores/commonStore';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
|
||||
export const CustomToastContainer = () =>
|
||||
<ToastContainer
|
||||
style={{ width: '350px' }}
|
||||
position="top-center"
|
||||
autoClose={4000}
|
||||
pauseOnHover={true}
|
||||
hideProgressBar={true}
|
||||
newestOnTop={true}
|
||||
closeOnClick={false}
|
||||
rtl={false}
|
||||
pauseOnFocusLoss={false}
|
||||
draggable={false}
|
||||
theme={commonStore.settings.darkMode ? 'dark' : 'light'}
|
||||
/>;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, MouseEventHandler, ReactElement } from 'react';
|
||||
import React, { CSSProperties, FC, MouseEventHandler, ReactElement } from 'react';
|
||||
import { Button, Tooltip } from '@fluentui/react-components';
|
||||
|
||||
export const ToolTipButton: FC<{
|
||||
@@ -6,6 +6,7 @@ export const ToolTipButton: FC<{
|
||||
desc: string,
|
||||
icon?: ReactElement,
|
||||
className?: string,
|
||||
style?: CSSProperties,
|
||||
size?: 'small' | 'medium' | 'large',
|
||||
shape?: 'rounded' | 'circular' | 'square';
|
||||
appearance?: 'secondary' | 'primary' | 'outline' | 'subtle' | 'transparent';
|
||||
@@ -17,6 +18,7 @@ export const ToolTipButton: FC<{
|
||||
desc,
|
||||
icon,
|
||||
className,
|
||||
style,
|
||||
size,
|
||||
shape,
|
||||
appearance,
|
||||
@@ -26,8 +28,8 @@ export const ToolTipButton: FC<{
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip content={desc} showDelay={showDelay} hideDelay={0} relationship="label">
|
||||
<Button className={className} disabled={disabled} icon={icon} onClick={onClick} size={size} shape={shape}
|
||||
appearance={appearance}>{text}</Button>
|
||||
<Button style={style} className={className} disabled={disabled} icon={icon} onClick={onClick} size={size}
|
||||
shape={shape} appearance={appearance}>{text}</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ import { v4 as uuid } from 'uuid';
|
||||
import classnames from 'classnames';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
import { KebabHorizontalIcon, PencilIcon, SyncIcon, TrashIcon } from '@primer/octicons-react';
|
||||
import { ConversationPair } from '../utils/get-conversation-pairs';
|
||||
import logo from '../assets/images/logo.jpg';
|
||||
import MarkdownRender from '../components/MarkdownRender';
|
||||
import { ToolTipButton } from '../components/ToolTipButton';
|
||||
@@ -19,6 +18,8 @@ import { WorkHeader } from '../components/WorkHeader';
|
||||
import { DialogButton } from '../components/DialogButton';
|
||||
import { OpenFileFolder, OpenSaveFileDialog } from '../../wailsjs/go/backend_golang/App';
|
||||
import { toastWithButton } from '../utils';
|
||||
import { PresetsButton } from './PresetsManager/PresetsButton';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
|
||||
export const userName = 'M E';
|
||||
export const botName = 'A I';
|
||||
@@ -49,6 +50,13 @@ export type Conversation = {
|
||||
[uuid: string]: MessageItem
|
||||
}
|
||||
|
||||
export type Role = 'assistant' | 'user' | 'system';
|
||||
|
||||
export type ConversationMessage = {
|
||||
role: Role;
|
||||
content: string;
|
||||
}
|
||||
|
||||
let chatSseController: AbortController | null = null;
|
||||
|
||||
const MoreUtilsButton: FC<{ uuid: string, setEditing: (editing: boolean) => void }> = observer(({
|
||||
@@ -123,7 +131,7 @@ const ChatMessageItem: FC<{
|
||||
<Avatar
|
||||
color={messageItem.color}
|
||||
name={messageItem.sender}
|
||||
image={messageItem.avatarImg ? { src: messageItem.avatarImg } : undefined}
|
||||
image={(commonStore.activePreset && messageItem.sender === botName) ? { src: commonStore.activePreset.avatarImg } : messageItem.avatarImg ? { src: messageItem.avatarImg } : undefined}
|
||||
/>
|
||||
<div
|
||||
className={classnames(
|
||||
@@ -175,6 +183,7 @@ const ChatPanel: FC = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
const bodyRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const mq = useMediaQuery('(min-width: 640px)');
|
||||
const port = commonStore.getCurrentModelConfig().apiParameters.apiPort;
|
||||
|
||||
let lastMessageId: string;
|
||||
@@ -255,7 +264,7 @@ const ChatPanel: FC = observer(() => {
|
||||
let endIndex = endUuid ? (commonStore.conversationOrder.indexOf(endUuid) + (includeEndUuid ? 1 : 0)) : commonStore.conversationOrder.length;
|
||||
let targetRange = commonStore.conversationOrder.slice(startIndex, endIndex);
|
||||
|
||||
const messages: ConversationPair[] = [];
|
||||
const messages: ConversationMessage[] = [];
|
||||
targetRange.forEach((uuid, index) => {
|
||||
if (uuid === welcomeUuid)
|
||||
return;
|
||||
@@ -357,10 +366,11 @@ const ChatPanel: FC = observer(() => {
|
||||
<ChatMessageItem key={uuid} uuid={uuid} onSubmit={onSubmit} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<div className={classnames('flex items-end', mq ? 'gap-2' : '')}>
|
||||
<PresetsButton tab="Chat" size={mq ? 'large' : 'small'} shape="circular" appearance="subtle" />
|
||||
<DialogButton tooltip={t('Clear')}
|
||||
icon={<Delete28Regular />}
|
||||
size="large" shape="circular" appearance="subtle" title={t('Clear')}
|
||||
size={mq ? 'large' : 'small'} shape="circular" appearance="subtle" title={t('Clear')}
|
||||
contentText={t('Are you sure you want to clear the conversation? It cannot be undone.')}
|
||||
onConfirm={() => {
|
||||
if (generating)
|
||||
@@ -370,6 +380,7 @@ const ChatPanel: FC = observer(() => {
|
||||
}} />
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
style={{ minWidth: 0 }}
|
||||
className="grow"
|
||||
resize="vertical"
|
||||
placeholder={t('Type your message here')!}
|
||||
@@ -379,7 +390,7 @@ const ChatPanel: FC = observer(() => {
|
||||
/>
|
||||
<ToolTipButton desc={generating ? t('Stop') : t('Send')}
|
||||
icon={generating ? <RecordStop28Regular /> : <ArrowCircleUp28Regular />}
|
||||
size="large" shape="circular" appearance="subtle"
|
||||
size={mq ? 'large' : 'small'} shape="circular" appearance="subtle"
|
||||
onClick={(e) => {
|
||||
if (generating) {
|
||||
chatSseController?.abort();
|
||||
@@ -395,7 +406,7 @@ const ChatPanel: FC = observer(() => {
|
||||
}} />
|
||||
<ToolTipButton desc={t('Save')}
|
||||
icon={<Save28Regular />}
|
||||
size="large" shape="circular" appearance="subtle"
|
||||
size={mq ? 'large' : 'small'} shape="circular" appearance="subtle"
|
||||
onClick={() => {
|
||||
let savedContent: string = '';
|
||||
commonStore.conversationOrder.forEach((uuid) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import commonStore, { ModelStatus } from '../stores/commonStore';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
import { toast } from 'react-toastify';
|
||||
import { DialogButton } from '../components/DialogButton';
|
||||
import { PresetsButton } from './PresetsManager/PresetsButton';
|
||||
|
||||
export type CompletionParams = Omit<ApiParameters, 'apiPort'> & {
|
||||
stop: string,
|
||||
@@ -261,19 +262,23 @@ const CompletionPanel: FC = observer(() => {
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
/>
|
||||
<div className="flex flex-col gap-1 max-h-48 sm:max-w-sm sm:max-h-full">
|
||||
<Dropdown style={{ minWidth: 0 }}
|
||||
value={t(commonStore.completionPreset!.name)!}
|
||||
selectedOptions={[commonStore.completionPreset!.name]}
|
||||
onOptionSelect={(_, data) => {
|
||||
if (data.optionValue) {
|
||||
setPreset(defaultPresets.find((preset) => preset.name === data.optionValue)!);
|
||||
<div className="flex gap-2">
|
||||
<Dropdown style={{ minWidth: 0 }}
|
||||
className="grow"
|
||||
value={t(commonStore.completionPreset!.name)!}
|
||||
selectedOptions={[commonStore.completionPreset!.name]}
|
||||
onOptionSelect={(_, data) => {
|
||||
if (data.optionValue) {
|
||||
setPreset(defaultPresets.find((preset) => preset.name === data.optionValue)!);
|
||||
}
|
||||
}}>
|
||||
{
|
||||
defaultPresets.map((preset) =>
|
||||
<Option key={preset.name} value={preset.name}>{t(preset.name)!}</Option>)
|
||||
}
|
||||
}}>
|
||||
{
|
||||
defaultPresets.map((preset) =>
|
||||
<Option key={preset.name} value={preset.name}>{t(preset.name)!}</Option>)
|
||||
}
|
||||
</Dropdown>
|
||||
</Dropdown>
|
||||
<PresetsButton tab="Completion" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 overflow-x-hidden overflow-y-auto p-1">
|
||||
<Labeled flex breakline label={t('Max Response Token')}
|
||||
desc={t('By default, the maximum number of tokens that can be answered in a single response, it can be changed by the user by specifying API parameters.')}
|
||||
|
||||
154
frontend/src/pages/PresetsManager/MessagesEditor.tsx
Normal file
154
frontend/src/pages/PresetsManager/MessagesEditor.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||
import commonStore from '../../stores/commonStore';
|
||||
import { Preset } from './PresetsButton';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Button, Card, Dropdown, Option, Textarea } from '@fluentui/react-components';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolTipButton } from '../../components/ToolTipButton';
|
||||
import { Delete20Regular, ReOrderDotsVertical20Regular } from '@fluentui/react-icons';
|
||||
import { ConversationMessage, Role } from '../Chat';
|
||||
|
||||
type Item = {
|
||||
id: string;
|
||||
role: Role;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const getItems = (messages: ConversationMessage[]) =>
|
||||
messages.map((message, index) => ({
|
||||
id: uuid(),
|
||||
role: message.role,
|
||||
content: message.content
|
||||
})) as Item[];
|
||||
|
||||
const reorder = (list: Item[], startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const MessagesEditor: FC = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const editingPreset = commonStore.editingPreset!;
|
||||
const setEditingPreset = (newParams: Partial<Preset>) => {
|
||||
commonStore.setEditingPreset({
|
||||
...editingPreset,
|
||||
...newParams
|
||||
});
|
||||
};
|
||||
|
||||
const [items, setItems] = useState(getItems(editingPreset.messages));
|
||||
|
||||
const updateItems = (items: Item[]) => {
|
||||
setEditingPreset({
|
||||
messages: items.map(item => ({
|
||||
role: item.role,
|
||||
content: item.content
|
||||
}))
|
||||
});
|
||||
setItems(items);
|
||||
};
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newItems = reorder(
|
||||
items,
|
||||
result.source.index,
|
||||
result.destination.index
|
||||
);
|
||||
|
||||
updateItems(newItems);
|
||||
};
|
||||
|
||||
const createNewItem = () => {
|
||||
const newItems: Item[] = [...items, {
|
||||
id: uuid(),
|
||||
role: 'assistant',
|
||||
content: ''
|
||||
}];
|
||||
updateItems(newItems);
|
||||
};
|
||||
|
||||
const deleteItem = (id: string) => {
|
||||
const newItems: Item[] = items.filter(item => item.id !== id);
|
||||
updateItems(newItems);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-2 overflow-hidden">
|
||||
<Button style={{ width: '100%' }} onClick={createNewItem}>{t('New')}</Button>
|
||||
<div className="overflow-x-hidden overflow-y-auto p-2">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<Draggable key={item.id} draggableId={item.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={provided.draggableProps.style}
|
||||
className="select-none mb-2"
|
||||
>
|
||||
<div className="flex">
|
||||
<Card appearance="outline"
|
||||
style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}>
|
||||
<ReOrderDotsVertical20Regular />
|
||||
</Card>
|
||||
<Dropdown style={{ minWidth: 0, borderRadius: 0 }} listbox={{ style: { minWidth: 0 } }}
|
||||
value={t(item.role)!}
|
||||
selectedOptions={[item.role]}
|
||||
onOptionSelect={(_, data) => {
|
||||
if (data.optionValue) {
|
||||
items[index] = {
|
||||
...item,
|
||||
role: data.optionValue as Role
|
||||
};
|
||||
updateItems([...items]);
|
||||
}
|
||||
}}>
|
||||
<Option value="user">{t('user')!}</Option>
|
||||
<Option value="assistant">{t('assistant')!}</Option>
|
||||
<Option value="system">{t('system')!}</Option>
|
||||
</Dropdown>
|
||||
<Textarea resize="vertical" className="grow" value={item.content}
|
||||
style={{ minWidth: 0, borderRadius: 0 }}
|
||||
onChange={(e, data) => {
|
||||
items[index] = {
|
||||
...item,
|
||||
content: data.value
|
||||
};
|
||||
updateItems([...items]);
|
||||
}}></Textarea>
|
||||
<ToolTipButton
|
||||
style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }} desc={t('Delete')}
|
||||
icon={<Delete20Regular />} onClick={() => {
|
||||
deleteItem(item.id);
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
432
frontend/src/pages/PresetsManager/PresetsButton.tsx
Normal file
432
frontend/src/pages/PresetsManager/PresetsButton.tsx
Normal file
@@ -0,0 +1,432 @@
|
||||
// TODO refactor
|
||||
|
||||
import React, { FC, PropsWithChildren, ReactElement, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogSurface,
|
||||
DialogTrigger,
|
||||
Input,
|
||||
Switch,
|
||||
Tab,
|
||||
TabList,
|
||||
Text
|
||||
} from '@fluentui/react-components';
|
||||
import {
|
||||
Accessibility28Regular,
|
||||
Chat20Regular,
|
||||
ClipboardEdit20Regular,
|
||||
Delete20Regular,
|
||||
Dismiss20Regular,
|
||||
Edit20Regular,
|
||||
Globe20Regular
|
||||
} from '@fluentui/react-icons';
|
||||
import { ToolTipButton } from '../../components/ToolTipButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { botName, Conversation, ConversationMessage, MessageType, userName } from '../Chat';
|
||||
import { SelectTabEventHandler } from '@fluentui/react-tabs';
|
||||
import { Labeled } from '../../components/Labeled';
|
||||
import commonStore from '../../stores/commonStore';
|
||||
import logo from '../../assets/images/logo.jpg';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { MessagesEditor } from './MessagesEditor';
|
||||
import { ClipboardGetText, ClipboardSetText } from '../../../wailsjs/runtime';
|
||||
import { toast } from 'react-toastify';
|
||||
import { CustomToastContainer } from '../../components/CustomToastContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export type PresetType = 'chat' | 'completion' | 'chatInCompletion'
|
||||
|
||||
export type Preset = {
|
||||
name: string,
|
||||
tag: string,
|
||||
// if name and sourceUrl are same, it will be overridden when importing
|
||||
sourceUrl: string,
|
||||
desc: string,
|
||||
avatarImg: string,
|
||||
type: PresetType,
|
||||
// chat
|
||||
welcomeMessage: string,
|
||||
messages: ConversationMessage[],
|
||||
displayPresetMessages: boolean,
|
||||
// completion
|
||||
prompt: string,
|
||||
stop: string,
|
||||
injectStart: string,
|
||||
injectEnd: string,
|
||||
}
|
||||
|
||||
export const defaultPreset: Preset = {
|
||||
name: 'RWKV',
|
||||
tag: 'default',
|
||||
sourceUrl: '',
|
||||
desc: '',
|
||||
avatarImg: logo,
|
||||
type: 'chat',
|
||||
welcomeMessage: '',
|
||||
displayPresetMessages: true,
|
||||
messages: [],
|
||||
prompt: '',
|
||||
stop: '',
|
||||
injectStart: '',
|
||||
injectEnd: ''
|
||||
};
|
||||
|
||||
const setActivePreset = (preset: Preset) => {
|
||||
commonStore.setActivePreset(preset);
|
||||
//TODO if (preset.displayPresetMessages) {
|
||||
const conversation: Conversation = {};
|
||||
const conversationOrder: string[] = [];
|
||||
for (const message of preset.messages) {
|
||||
const newUuid = uuid();
|
||||
conversationOrder.push(newUuid);
|
||||
conversation[newUuid] = {
|
||||
sender: message.role === 'user' ? userName : botName,
|
||||
type: MessageType.Normal,
|
||||
color: message.role === 'user' ? 'brand' : 'colorful',
|
||||
time: new Date().toISOString(),
|
||||
content: message.content,
|
||||
side: message.role === 'user' ? 'right' : 'left',
|
||||
done: true
|
||||
};
|
||||
}
|
||||
commonStore.setConversation(conversation);
|
||||
commonStore.setConversationOrder(conversationOrder);
|
||||
//}
|
||||
};
|
||||
|
||||
export const PresetCardFrame: FC<PropsWithChildren & { onClick?: () => void }> = (props) => {
|
||||
return <Button
|
||||
className="flex flex-col gap-1 w-32 h-56 break-all"
|
||||
style={{ minWidth: 0, borderRadius: '0.75rem', justifyContent: 'unset' }}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</Button>;
|
||||
};
|
||||
|
||||
export const PresetCard: FC<{
|
||||
avatarImg: string,
|
||||
name: string,
|
||||
desc: string,
|
||||
tag: string,
|
||||
editable: boolean,
|
||||
presetIndex: number,
|
||||
onClick?: () => void
|
||||
}> = observer(({
|
||||
avatarImg, name, desc, tag, editable, presetIndex, onClick
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <PresetCardFrame onClick={onClick}>
|
||||
<img src={avatarImg} className="rounded-xl select-none ml-auto mr-auto h-28" />
|
||||
<Text size={400}>{name}</Text>
|
||||
<Text size={200} style={{
|
||||
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical'
|
||||
}}>{desc}</Text>
|
||||
<div className="grow" />
|
||||
<div className="flex justify-between w-full items-end">
|
||||
<div className="text-xs font-thin text-gray-500">{t(tag)}</div>
|
||||
{editable ?
|
||||
<ChatPresetEditor presetIndex={presetIndex} triggerButton={
|
||||
<ToolTipButton size="small" appearance="transparent" desc={t('Edit')} icon={<Edit20Regular />}
|
||||
onClick={() => {
|
||||
commonStore.setEditingPreset({ ...commonStore.presets[presetIndex] });
|
||||
}} />
|
||||
} />
|
||||
: <div />
|
||||
}
|
||||
</div>
|
||||
</PresetCardFrame>;
|
||||
});
|
||||
|
||||
export const ChatPresetEditor: FC<{
|
||||
triggerButton: ReactElement,
|
||||
presetIndex: number
|
||||
}> = observer(({ triggerButton, presetIndex }) => {
|
||||
const { t } = useTranslation();
|
||||
const [editingMessages, setEditingMessages] = useState(false);
|
||||
|
||||
if (!commonStore.editingPreset)
|
||||
commonStore.setEditingPreset({ ...defaultPreset });
|
||||
const editingPreset = commonStore.editingPreset!;
|
||||
|
||||
const setEditingPreset = (newParams: Partial<Preset>) => {
|
||||
commonStore.setEditingPreset({
|
||||
...editingPreset,
|
||||
...newParams
|
||||
});
|
||||
};
|
||||
|
||||
const importPreset = () => {
|
||||
ClipboardGetText().then((text) => {
|
||||
try {
|
||||
const preset = JSON.parse(text);
|
||||
setEditingPreset(preset);
|
||||
toast(t('Imported successfully'), {
|
||||
type: 'success',
|
||||
autoClose: 1000
|
||||
});
|
||||
} catch (e) {
|
||||
toast(t('Failed to import. Please copy a preset to the clipboard.'), {
|
||||
type: 'error',
|
||||
autoClose: 2500
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
toast(t('Clipboard is empty.'), {
|
||||
type: 'info',
|
||||
autoClose: 1000
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const copyPreset = () => {
|
||||
ClipboardSetText(JSON.stringify(editingPreset)).then((success) => {
|
||||
if (success)
|
||||
toast(t('Successfully copied to clipboard.'), {
|
||||
type: 'success',
|
||||
autoClose: 1000
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const savePreset = () => {
|
||||
if (presetIndex === -1) {
|
||||
commonStore.setPresets([...commonStore.presets, { ...editingPreset }]);
|
||||
setEditingPreset(defaultPreset);
|
||||
} else {
|
||||
commonStore.presets[presetIndex] = editingPreset;
|
||||
commonStore.setPresets(commonStore.presets);
|
||||
}
|
||||
};
|
||||
|
||||
const activatePreset = () => {
|
||||
savePreset();
|
||||
setActivePreset(editingPreset);
|
||||
};
|
||||
|
||||
const deletePreset = () => {
|
||||
commonStore.presets.splice(presetIndex, 1);
|
||||
commonStore.setPresets(commonStore.presets);
|
||||
};
|
||||
|
||||
return <Dialog>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
{triggerButton}
|
||||
</DialogTrigger>
|
||||
<DialogSurface style={{
|
||||
paddingTop: 0,
|
||||
maxWidth: '80vw',
|
||||
maxHeight: '80vh',
|
||||
width: '500px',
|
||||
height: '100%'
|
||||
}}>
|
||||
<DialogBody style={{ height: '100%', overflow: 'hidden' }}>
|
||||
<DialogContent className="flex flex-col gap-1 overflow-hidden">
|
||||
<CustomToastContainer />
|
||||
<div className="flex justify-between">{
|
||||
presetIndex === -1
|
||||
? <div />
|
||||
: <DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="subtle" icon={<Delete20Regular />} onClick={deletePreset} />
|
||||
</DialogTrigger>
|
||||
}
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="subtle" icon={<Dismiss20Regular />} />
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
<img src={editingPreset.avatarImg} className="rounded-xl select-none ml-auto mr-auto h-28" />
|
||||
<Labeled flex breakline label={t('Name')}
|
||||
content={
|
||||
<div className="flex gap-2">
|
||||
<Input className="grow" value={editingPreset.name} onChange={(e, data) => {
|
||||
setEditingPreset({
|
||||
name: data.value
|
||||
});
|
||||
}} />
|
||||
<ToolTipButton desc={!editingMessages ? t('Edit Messages') : t('Go Back')}
|
||||
icon={!editingMessages ? <Chat20Regular /> : <Dismiss20Regular />} onClick={() => {
|
||||
setEditingMessages(!editingMessages);
|
||||
}} />
|
||||
</div>
|
||||
} />
|
||||
{
|
||||
editingMessages ?
|
||||
<MessagesEditor /> :
|
||||
<div className="flex flex-col gap-1 p-2 overflow-x-hidden overflow-y-auto">
|
||||
<Labeled flex breakline label={t('Description')}
|
||||
content={
|
||||
<Input value={editingPreset.desc} onChange={(e, data) => {
|
||||
setEditingPreset({
|
||||
desc: data.value
|
||||
});
|
||||
}} />
|
||||
} />
|
||||
<Labeled flex breakline label={t('Avatar Url')}
|
||||
content={
|
||||
<Input value={editingPreset.avatarImg} onChange={(e, data) => {
|
||||
setEditingPreset({
|
||||
avatarImg: data.value
|
||||
});
|
||||
}} />
|
||||
} />
|
||||
<Labeled flex breakline label={t('Welcome Message')}
|
||||
content={
|
||||
<Input disabled value={editingPreset.welcomeMessage} onChange={(e, data) => {
|
||||
setEditingPreset({
|
||||
welcomeMessage: data.value
|
||||
});
|
||||
}} />
|
||||
} />
|
||||
<Labeled flex spaceBetween label={t('Display Preset Messages')}
|
||||
content={
|
||||
<Switch disabled checked={editingPreset.displayPresetMessages}
|
||||
onChange={(e, data) => {
|
||||
setEditingPreset({
|
||||
displayPresetMessages: data.checked
|
||||
});
|
||||
}} />
|
||||
} />
|
||||
<Labeled flex breakline label={t('Tag')}
|
||||
content={
|
||||
<Input value={editingPreset.tag} onChange={(e, data) => {
|
||||
setEditingPreset({
|
||||
tag: data.value
|
||||
});
|
||||
}} />
|
||||
} />
|
||||
</div>
|
||||
}
|
||||
<div className="grow" />
|
||||
<div className="flex justify-between">
|
||||
<Button onClick={importPreset}>{t('Import')}</Button>
|
||||
<Button onClick={copyPreset}>{t('Copy')}</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="primary" onClick={savePreset}>{t('Save')}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="primary" onClick={activatePreset}>{t('Activate')}</Button>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>;
|
||||
});
|
||||
|
||||
export const ChatPresets: FC = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div className="flex flex-wrap gap-2">
|
||||
<ChatPresetEditor presetIndex={-1} triggerButton={
|
||||
<PresetCardFrame>
|
||||
<div className="h-full flex items-center">
|
||||
{t('New Preset')}
|
||||
</div>
|
||||
</PresetCardFrame>}
|
||||
/>
|
||||
{/*TODO <PresetCardFrame>*/}
|
||||
{/* <div className="h-full flex items-center">*/}
|
||||
{/* {t('Import')}*/}
|
||||
{/* </div>*/}
|
||||
{/*</PresetCardFrame>*/}
|
||||
<PresetCard
|
||||
presetIndex={-1}
|
||||
editable={false}
|
||||
onClick={() => {
|
||||
setActivePreset(defaultPreset);
|
||||
}} avatarImg={defaultPreset.avatarImg} name={defaultPreset.name} desc={defaultPreset.desc} tag={defaultPreset.tag}
|
||||
/>
|
||||
{commonStore.presets.map((preset, index) => {
|
||||
return <PresetCard
|
||||
presetIndex={index}
|
||||
editable={true}
|
||||
onClick={() => {
|
||||
setActivePreset(preset);
|
||||
}}
|
||||
key={index} avatarImg={preset.avatarImg} name={preset.name} desc={preset.desc} tag={preset.tag}
|
||||
/>;
|
||||
})}
|
||||
</div>;
|
||||
});
|
||||
|
||||
type PresetsNavigationItem = {
|
||||
icon: ReactElement;
|
||||
element: ReactElement;
|
||||
};
|
||||
|
||||
const pages: { [label: string]: PresetsNavigationItem } = {
|
||||
Chat: {
|
||||
icon: <Chat20Regular />,
|
||||
element: <ChatPresets />
|
||||
},
|
||||
Completion: {
|
||||
icon: <ClipboardEdit20Regular />,
|
||||
element: <div>In Development</div>
|
||||
},
|
||||
Online: {
|
||||
icon: <Globe20Regular />,
|
||||
element: <div>In Development</div>
|
||||
}
|
||||
};
|
||||
|
||||
export const PresetsManager: FC<{ initTab: string }> = ({ initTab }) => {
|
||||
const { t } = useTranslation();
|
||||
const [tab, setTab] = useState(initTab);
|
||||
|
||||
const selectTab: SelectTabEventHandler = (e, data) =>
|
||||
typeof data.value === 'string' ? setTab(data.value) : null;
|
||||
|
||||
return <div className="flex flex-col gap-2 w-full h-full">
|
||||
<div className="flex justify-between">
|
||||
<TabList
|
||||
size="small"
|
||||
appearance="subtle"
|
||||
selectedValue={tab}
|
||||
onTabSelect={selectTab}
|
||||
>
|
||||
{Object.entries(pages).map(([label, { icon }]) => (
|
||||
<Tab icon={icon} key={label} value={label}>
|
||||
{t(label)}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button appearance="subtle" icon={<Dismiss20Regular />} />
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
<div className="grow overflow-x-hidden overflow-y-auto">
|
||||
{pages[tab].element}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const PresetsButton: FC<{
|
||||
tab: string,
|
||||
size?: 'small' | 'medium' | 'large',
|
||||
shape?: 'rounded' | 'circular' | 'square';
|
||||
appearance?: 'secondary' | 'primary' | 'outline' | 'subtle' | 'transparent';
|
||||
}> = ({ tab, size, shape, appearance }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Dialog>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<ToolTipButton desc={t('Presets')} size={size} shape={shape} appearance={appearance}
|
||||
icon={<Accessibility28Regular />} />
|
||||
</DialogTrigger>
|
||||
<DialogSurface style={{ paddingTop: 0, maxWidth: '90vw', width: 'fit-content' }}>
|
||||
<DialogBody>
|
||||
<DialogContent>
|
||||
<CustomToastContainer />
|
||||
<PresetsManager initTab={tab} />
|
||||
</DialogContent>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>;
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { getStatus } from './apis';
|
||||
import { EventsOn } from '../wailsjs/runtime';
|
||||
import manifest from '../../manifest.json';
|
||||
import { defaultModelConfigs, defaultModelConfigsMac } from './pages/defaultModelConfigs';
|
||||
import { Preset } from './pages/PresetsManager/PresetsButton';
|
||||
|
||||
export async function startup() {
|
||||
downloadProgramFiles();
|
||||
@@ -13,6 +14,8 @@ export async function startup() {
|
||||
commonStore.setDownloadList(data);
|
||||
});
|
||||
|
||||
initPresets();
|
||||
|
||||
await GetPlatform().then(p => commonStore.setPlatform(p as Platform));
|
||||
await initConfig();
|
||||
|
||||
@@ -65,4 +68,11 @@ async function initCache(initUnfinishedModels: boolean) {
|
||||
}).catch(() => {
|
||||
});
|
||||
await refreshModels(false, initUnfinishedModels);
|
||||
}
|
||||
}
|
||||
|
||||
async function initPresets() {
|
||||
await ReadJson('presets.json').then((presets: Preset[]) => {
|
||||
commonStore.setPresets(presets, false);
|
||||
}).catch(() => {
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import { getUserLanguage, isSystemLightMode, saveConfigs } from '../utils';
|
||||
import { getUserLanguage, isSystemLightMode, saveConfigs, savePresets } from '../utils';
|
||||
import { WindowSetDarkTheme, WindowSetLightTheme } from '../../wailsjs/runtime';
|
||||
import manifest from '../../../manifest.json';
|
||||
import { ModelConfig } from '../pages/Configs';
|
||||
@@ -13,6 +13,7 @@ import i18n from 'i18next';
|
||||
import { CompletionPreset } from '../pages/Completion';
|
||||
import { defaultModelConfigs, defaultModelConfigsMac } from '../pages/defaultModelConfigs';
|
||||
import commonStore from './commonStore';
|
||||
import { Preset } from '../pages/PresetsManager/PresetsButton';
|
||||
|
||||
export enum ModelStatus {
|
||||
Offline,
|
||||
@@ -38,12 +39,16 @@ class CommonStore {
|
||||
};
|
||||
depComplete: boolean = false;
|
||||
platform: Platform = 'windows';
|
||||
// presets manager
|
||||
editingPreset: Preset | null = null;
|
||||
presets: Preset[] = [];
|
||||
// home
|
||||
introduction: IntroductionContent = manifest.introduction;
|
||||
// chat
|
||||
currentInput: string = '';
|
||||
conversation: Conversation = {};
|
||||
conversationOrder: string[] = [];
|
||||
activePreset: Preset | null = null;
|
||||
// completion
|
||||
completionPreset: CompletionPreset | null = null;
|
||||
completionGenerating: boolean = false;
|
||||
@@ -202,6 +207,20 @@ class CommonStore {
|
||||
setLastUnfinishedModelDownloads(value: DownloadStatus[]) {
|
||||
this.lastUnfinishedModelDownloads = value;
|
||||
}
|
||||
|
||||
setEditingPreset(value: Preset) {
|
||||
this.editingPreset = value;
|
||||
}
|
||||
|
||||
setPresets(value: Preset[], savePreset: boolean = true) {
|
||||
this.presets = value;
|
||||
if (savePreset)
|
||||
savePresets();
|
||||
}
|
||||
|
||||
setActivePreset(value: Preset) {
|
||||
this.activePreset = value;
|
||||
}
|
||||
}
|
||||
|
||||
export default new CommonStore();
|
||||
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -206,6 +206,10 @@ export const saveCache = async () => {
|
||||
return SaveJson('cache.json', data);
|
||||
};
|
||||
|
||||
export const savePresets = async () => {
|
||||
return SaveJson('presets.json', commonStore.presets);
|
||||
};
|
||||
|
||||
export function getUserLanguage(): Language {
|
||||
// const l = navigator.language.toLowerCase();
|
||||
// if (['zh-hk', 'zh-mo', 'zh-tw', 'zh-cht', 'zh-hant'].includes(l)) return 'zhHant'
|
||||
|
||||
Reference in New Issue
Block a user