430 lines
14 KiB
TypeScript
430 lines
14 KiB
TypeScript
// TODO refactor
|
|
|
|
import React, { FC, lazy, 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 { SelectTabEventHandler } from '@fluentui/react-tabs';
|
|
import { Labeled } from '../../components/Labeled';
|
|
import commonStore from '../../stores/commonStore';
|
|
import logo from '../../assets/images/logo.png';
|
|
import { observer } from 'mobx-react-lite';
|
|
import { ClipboardGetText, ClipboardSetText } from '../../../wailsjs/runtime';
|
|
import { toast } from 'react-toastify';
|
|
import { CustomToastContainer } from '../../components/CustomToastContainer';
|
|
import { absPathAsset, setActivePreset } from '../../utils';
|
|
import { Preset, PresetsNavigationItem } from '../../types/presets';
|
|
import { LazyImportComponent } from '../../components/LazyImportComponent';
|
|
|
|
const defaultPreset: Preset = {
|
|
name: 'RWKV',
|
|
tag: 'default',
|
|
sourceUrl: '',
|
|
desc: '',
|
|
avatarImg: logo,
|
|
type: 'chat',
|
|
welcomeMessage: '',
|
|
displayPresetMessages: true,
|
|
messages: [],
|
|
prompt: '',
|
|
stop: '',
|
|
injectStart: '',
|
|
injectEnd: '',
|
|
presystem: true,
|
|
userName: '',
|
|
assistantName: ''
|
|
};
|
|
|
|
const MessagesEditor = lazy(() => import('./MessagesEditor'));
|
|
|
|
const PresetCardFrame: FC<PropsWithChildren & {
|
|
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
|
}> = (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>;
|
|
};
|
|
|
|
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={(e) => {
|
|
if (onClick && e.currentTarget.contains(e.target as Node))
|
|
onClick();
|
|
}}>
|
|
<img src={absPathAsset(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={(e) => {
|
|
e.stopPropagation();
|
|
commonStore.setEditingPreset({ ...commonStore.presets[presetIndex] });
|
|
}} />
|
|
} />
|
|
: <div />
|
|
}
|
|
</div>
|
|
</PresetCardFrame>;
|
|
});
|
|
|
|
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 {
|
|
if (!text.trim().startsWith('{'))
|
|
text = new TextDecoder().decode(
|
|
new Uint8Array(atob(text)
|
|
.split('')
|
|
.map((c) => c.charCodeAt(0))));
|
|
const preset = JSON.parse(text);
|
|
setEditingPreset(preset);
|
|
setEditingMessages(false);
|
|
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={absPathAsset(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
|
|
});
|
|
}} />
|
|
<Button onClick={() => {
|
|
setEditingMessages(!editingMessages);
|
|
}}>{!editingMessages ? t('Edit Character Settings') : t('Go Back')}</Button>
|
|
</div>
|
|
} />
|
|
{
|
|
editingMessages ?
|
|
<div className="flex flex-col gap-1">
|
|
<Labeled flex spaceBetween label={t('Insert default system prompt at the beginning')}
|
|
content={
|
|
<Switch checked={editingPreset.presystem === undefined ? true : editingPreset.presystem}
|
|
onChange={(e, data) => {
|
|
setEditingPreset({
|
|
presystem: data.checked
|
|
});
|
|
}} />
|
|
} />
|
|
<Labeled flex breakline label={t('User Name')}
|
|
content={
|
|
<Input placeholder="User" value={editingPreset.userName} onChange={(e, data) => {
|
|
setEditingPreset({
|
|
userName: data.value
|
|
});
|
|
}} />
|
|
} />
|
|
<Labeled flex breakline label={t('Assistant Name')}
|
|
content={
|
|
<Input placeholder="Assistant" value={editingPreset.assistantName} onChange={(e, data) => {
|
|
setEditingPreset({
|
|
assistantName: data.value
|
|
});
|
|
}} />
|
|
} />
|
|
<LazyImportComponent lazyChildren={MessagesEditor} />
|
|
</div> :
|
|
<div className="flex flex-col gap-1 p-2 overflow-x-hidden overflow-y-auto">
|
|
<Labeled flex breakline label={`${t('Description')} (${t('Preview Only')})`}
|
|
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>;
|
|
});
|
|
|
|
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>;
|
|
});
|
|
|
|
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>
|
|
}
|
|
};
|
|
|
|
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>;
|
|
}; |