mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 21:34:00 +01:00
feat: prompt library
This commit is contained in:
parent
5d9ca85cf6
commit
b9657c6325
|
@ -14,6 +14,7 @@
|
||||||
"i18next-browser-languagedetector": "^7.0.1",
|
"i18next-browser-languagedetector": "^7.0.1",
|
||||||
"i18next-http-backend": "^2.1.1",
|
"i18next-http-backend": "^2.1.1",
|
||||||
"jspdf": "^2.5.1",
|
"jspdf": "^2.5.1",
|
||||||
|
"match-sorter": "^6.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^12.2.0",
|
"react-i18next": "^12.2.0",
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
"rehype-sanitize": "^5.0.1",
|
"rehype-sanitize": "^5.0.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"zustand": "^4.3.6"
|
"zustand": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -32,6 +34,7 @@
|
||||||
"@types/react": "^18.0.27",
|
"@types/react": "^18.0.27",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
"@types/react-scroll-to-bottom": "^4.2.0",
|
"@types/react-scroll-to-bottom": "^4.2.0",
|
||||||
|
"@types/uuid": "^9.0.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
|
|
|
@ -23,5 +23,10 @@
|
||||||
"darkMode": "Dark Mode",
|
"darkMode": "Dark Mode",
|
||||||
"setting": "Settings",
|
"setting": "Settings",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"autoTitle": "Auto generate title"
|
"autoTitle": "Auto generate title",
|
||||||
|
"prompt": "Prompt",
|
||||||
|
"promptLibrary": "Prompt Library",
|
||||||
|
"name": "Name",
|
||||||
|
"search": "Search",
|
||||||
|
"morePrompts": "You can find more prompts here: "
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,5 +23,10 @@
|
||||||
"darkMode": "黑暗模式",
|
"darkMode": "黑暗模式",
|
||||||
"setting": "设置",
|
"setting": "设置",
|
||||||
"image": "图片",
|
"image": "图片",
|
||||||
"autoTitle": "自动生成标题"
|
"autoTitle": "自动生成标题",
|
||||||
|
"prompt": "提示词",
|
||||||
|
"promptLibrary": "提示词资料库",
|
||||||
|
"name": "名称",
|
||||||
|
"search": "搜索",
|
||||||
|
"morePrompts": "更多提示词请点击:"
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,5 +23,10 @@
|
||||||
"darkMode": "黑暗模式",
|
"darkMode": "黑暗模式",
|
||||||
"setting": "設定",
|
"setting": "設定",
|
||||||
"image": "圖片",
|
"image": "圖片",
|
||||||
"autoTitle": "自動生成標題"
|
"autoTitle": "自動生成標題",
|
||||||
|
"prompt": "Prompt",
|
||||||
|
"promptLibrary": "Prompt 資料庫",
|
||||||
|
"name": "名",
|
||||||
|
"search": "檢索",
|
||||||
|
"morePrompts": "如果你想揾更多 prompt,撳呢度:"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import useStore from '@store/store';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { matchSorter } from 'match-sorter';
|
||||||
|
import { Prompt } from '@type/prompt';
|
||||||
|
|
||||||
|
const CommandPrompt = ({
|
||||||
|
_setContent,
|
||||||
|
}: {
|
||||||
|
_setContent: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const prompts = useStore((state) => state.prompts);
|
||||||
|
const [dropDown, setDropDown] = useState<boolean>(false);
|
||||||
|
const [_prompts, _setPrompts] = useState<Prompt[]>(prompts);
|
||||||
|
const [input, setInput] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filteredPrompts = matchSorter(useStore.getState().prompts, input, {
|
||||||
|
keys: ['name'],
|
||||||
|
});
|
||||||
|
_setPrompts(filteredPrompts);
|
||||||
|
}, [input]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
_setPrompts(prompts);
|
||||||
|
setInput('');
|
||||||
|
}, [prompts]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative max-wd-sm'>
|
||||||
|
<button
|
||||||
|
className='btn btn-neutral btn-small'
|
||||||
|
onClick={() => setDropDown(!dropDown)}
|
||||||
|
>
|
||||||
|
/
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
dropDown ? '' : 'hidden'
|
||||||
|
} absolute top-100 bottom-100 right-0 z-10 bg-white rounded-lg shadow-xl border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800 opacity-90`}
|
||||||
|
>
|
||||||
|
<div className='text-sm px-4 py-2 w-max'>{t('promptLibrary')}</div>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='text-gray-800 dark:text-white p-3 text-sm border-none bg-gray-200 dark:bg-gray-600 m-0 w-full mr-0 h-8 focus:outline-none'
|
||||||
|
value={input}
|
||||||
|
placeholder={t('search') as string}
|
||||||
|
onChange={(e) => {
|
||||||
|
setInput(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ul className='text-sm text-gray-700 dark:text-gray-200 p-0 m-0 w-max max-w-sm max-md:max-w-[90vw] max-h-32 overflow-auto'>
|
||||||
|
{_prompts.map((cp) => (
|
||||||
|
<li
|
||||||
|
className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer text-start w-full'
|
||||||
|
onClick={() => {
|
||||||
|
_setContent((prev) => prev + cp.prompt);
|
||||||
|
setDropDown(false);
|
||||||
|
}}
|
||||||
|
key={cp.id}
|
||||||
|
>
|
||||||
|
{cp.name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommandPrompt;
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './CommandPrompt';
|
|
@ -26,6 +26,7 @@ import useSubmit from '@hooks/useSubmit';
|
||||||
import { ChatInterface } from '@type/chat';
|
import { ChatInterface } from '@type/chat';
|
||||||
|
|
||||||
import PopupModal from '@components/PopupModal';
|
import PopupModal from '@components/PopupModal';
|
||||||
|
import CommandPrompt from './CommandPrompt';
|
||||||
import CodeBlock from './CodeBlock';
|
import CodeBlock from './CodeBlock';
|
||||||
import { codeLanguageSubset } from '@constants/chat';
|
import { codeLanguageSubset } from '@constants/chat';
|
||||||
|
|
||||||
|
@ -310,13 +311,6 @@ const EditView = ({
|
||||||
if (textareaRef.current) textareaRef.current.style.height = 'auto';
|
if (textareaRef.current) textareaRef.current.style.height = 'auto';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (textareaRef.current) {
|
|
||||||
textareaRef.current.style.height = 'auto';
|
|
||||||
textareaRef.current.style.height = `${e.target.scrollHeight}px`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if ((e.ctrlKey || e.shiftKey) && e.key === 'Enter') {
|
if ((e.ctrlKey || e.shiftKey) && e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -368,6 +362,13 @@ const EditView = ({
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.style.height = 'auto';
|
||||||
|
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
}, [_content]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textareaRef.current) {
|
if (textareaRef.current) {
|
||||||
textareaRef.current.style.height = 'auto';
|
textareaRef.current.style.height = 'auto';
|
||||||
|
@ -391,7 +392,6 @@ const EditView = ({
|
||||||
_setContent(e.target.value);
|
_setContent(e.target.value);
|
||||||
}}
|
}}
|
||||||
value={_content}
|
value={_content}
|
||||||
onInput={handleInput}
|
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
rows={1}
|
rows={1}
|
||||||
></textarea>
|
></textarea>
|
||||||
|
@ -402,6 +402,7 @@ const EditView = ({
|
||||||
handleSave={handleSave}
|
handleSave={handleSave}
|
||||||
setIsModalOpen={setIsModalOpen}
|
setIsModalOpen={setIsModalOpen}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
_setContent={_setContent}
|
||||||
/>
|
/>
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<PopupModal
|
<PopupModal
|
||||||
|
@ -422,17 +423,20 @@ const EditViewButtons = React.memo(
|
||||||
handleSave,
|
handleSave,
|
||||||
setIsModalOpen,
|
setIsModalOpen,
|
||||||
setIsEdit,
|
setIsEdit,
|
||||||
|
_setContent,
|
||||||
}: {
|
}: {
|
||||||
sticky?: boolean;
|
sticky?: boolean;
|
||||||
handleSaveAndSubmit: () => void;
|
handleSaveAndSubmit: () => void;
|
||||||
handleSave: () => void;
|
handleSave: () => void;
|
||||||
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
_setContent: React.Dispatch<React.SetStateAction<string>>;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='text-center mt-2 flex justify-center'>
|
<div className='flex'>
|
||||||
|
<div className='flex-1 text-center mt-2 flex justify-center'>
|
||||||
{sticky && (
|
{sticky && (
|
||||||
<button
|
<button
|
||||||
className='btn relative mr-2 btn-primary'
|
className='btn relative mr-2 btn-primary'
|
||||||
|
@ -479,6 +483,8 @@ const EditViewButtons = React.memo(
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<CommandPrompt _setContent={_setContent} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
146
src/components/PromptLibraryMenu/PromptLibraryMenu.tsx
Normal file
146
src/components/PromptLibraryMenu/PromptLibraryMenu.tsx
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import useStore from '@store/store';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import PopupModal from '@components/PopupModal';
|
||||||
|
import { Prompt } from '@type/prompt';
|
||||||
|
import PlusIcon from '@icon/PlusIcon';
|
||||||
|
import CrossIcon from '@icon/CrossIcon';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const PromptLibraryMenu = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button className='btn btn-neutral' onClick={() => setIsModalOpen(true)}>
|
||||||
|
{t('promptLibrary')}
|
||||||
|
</button>
|
||||||
|
{isModalOpen && (
|
||||||
|
<PromptLibraryMenuPopUp setIsModalOpen={setIsModalOpen} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PromptLibraryMenuPopUp = ({
|
||||||
|
setIsModalOpen,
|
||||||
|
}: {
|
||||||
|
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const setPrompts = useStore((state) => state.setPrompts);
|
||||||
|
const [_prompts, _setPrompts] = useState<Prompt[]>(
|
||||||
|
JSON.parse(JSON.stringify(useStore.getState().prompts))
|
||||||
|
);
|
||||||
|
const container = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
e.target.style.height = 'auto';
|
||||||
|
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
setPrompts(_prompts);
|
||||||
|
setIsModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPrompt = () => {
|
||||||
|
const updatedPrompts: Prompt[] = JSON.parse(JSON.stringify(_prompts));
|
||||||
|
updatedPrompts.push({
|
||||||
|
id: uuidv4(),
|
||||||
|
name: '',
|
||||||
|
prompt: '',
|
||||||
|
});
|
||||||
|
_setPrompts(updatedPrompts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePrompt = (index: number) => {
|
||||||
|
const updatedPrompts: Prompt[] = JSON.parse(JSON.stringify(_prompts));
|
||||||
|
updatedPrompts.splice(index, 1);
|
||||||
|
_setPrompts(updatedPrompts);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (container && container.current) {
|
||||||
|
container.current.querySelectorAll('textarea').forEach((elem) => {
|
||||||
|
elem.style.height = `${elem.scrollHeight}px`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopupModal
|
||||||
|
title={t('promptLibrary') as string}
|
||||||
|
setIsModalOpen={setIsModalOpen}
|
||||||
|
handleConfirm={handleSave}
|
||||||
|
>
|
||||||
|
<div className='p-6 border-b border-gray-200 dark:border-gray-600 w-[90vw] max-w-full text-sm text-gray-900 dark:text-gray-300'>
|
||||||
|
<div className='flex flex-col p-2 max-w-full' ref={container}>
|
||||||
|
<div className='flex font-bold border-b border-gray-500/50 mb-1 p-1'>
|
||||||
|
<div className='flex-1'>{t('name')}</div>
|
||||||
|
<div className='flex-1'>{t('prompt')}</div>
|
||||||
|
</div>
|
||||||
|
{_prompts.map((prompt, index) => (
|
||||||
|
<div
|
||||||
|
key={prompt.id}
|
||||||
|
className='flex items-center border-b border-gray-500/50 mb-1 p-1'
|
||||||
|
>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<textarea
|
||||||
|
className='m-0 resize-none rounded-lg bg-transparent overflow-y-hidden leading-7 p-1 focus:ring-1 focus:ring-blue w-full'
|
||||||
|
onChange={(e) => {
|
||||||
|
_setPrompts((prev) => {
|
||||||
|
const newPrompts = [...prev];
|
||||||
|
newPrompts[index].name = e.target.value;
|
||||||
|
return newPrompts;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onInput={handleInput}
|
||||||
|
value={prompt.name}
|
||||||
|
rows={1}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<textarea
|
||||||
|
className='m-0 resize-none rounded-lg bg-transparent overflow-y-hidden leading-7 p-1 focus:ring-1 focus:ring-blue w-full'
|
||||||
|
onChange={(e) => {
|
||||||
|
_setPrompts((prev) => {
|
||||||
|
const newPrompts = [...prev];
|
||||||
|
newPrompts[index].prompt = e.target.value;
|
||||||
|
return newPrompts;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onInput={handleInput}
|
||||||
|
value={prompt.prompt}
|
||||||
|
rows={1}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='cursor-pointer'
|
||||||
|
onClick={() => deletePrompt(index)}
|
||||||
|
>
|
||||||
|
<CrossIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-center' onClick={addPrompt}>
|
||||||
|
<PlusIcon className='cursor-pointer' />
|
||||||
|
</div>
|
||||||
|
<div className='mt-6 px-2'>
|
||||||
|
{t('morePrompts')}
|
||||||
|
<a
|
||||||
|
href='https://github.com/f/awesome-chatgpt-prompts'
|
||||||
|
className='link'
|
||||||
|
>
|
||||||
|
awesome-chatgpt-prompts
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopupModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PromptLibraryMenu;
|
1
src/components/PromptLibraryMenu/index.ts
Normal file
1
src/components/PromptLibraryMenu/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './PromptLibraryMenu';
|
|
@ -7,6 +7,7 @@ import SettingIcon from '@icon/SettingIcon';
|
||||||
import ThemeSwitcher from '@components/Menu/MenuOptions/ThemeSwitcher';
|
import ThemeSwitcher from '@components/Menu/MenuOptions/ThemeSwitcher';
|
||||||
import LanguageSelector from '@components/LanguageSelector';
|
import LanguageSelector from '@components/LanguageSelector';
|
||||||
import AutoTitleToggle from './AutoTitleToggle';
|
import AutoTitleToggle from './AutoTitleToggle';
|
||||||
|
import PromptLibraryMenu from '@components/PromptLibraryMenu';
|
||||||
|
|
||||||
const SettingsMenu = () => {
|
const SettingsMenu = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -37,6 +38,7 @@ const SettingsMenu = () => {
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
<AutoTitleToggle />
|
<AutoTitleToggle />
|
||||||
|
<PromptLibraryMenu />
|
||||||
</div>
|
</div>
|
||||||
</PopupModal>
|
</PopupModal>
|
||||||
)}
|
)}
|
||||||
|
|
19
src/constants/prompt.ts
Normal file
19
src/constants/prompt.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Prompt } from '@type/prompt';
|
||||||
|
|
||||||
|
// prompts from https://github.com/f/awesome-chatgpt-prompts
|
||||||
|
const defaultPrompts: Prompt[] = [
|
||||||
|
{
|
||||||
|
id: '0d3e9cb7-b585-43fa-acc3-840c189f6b93',
|
||||||
|
name: 'English Translator',
|
||||||
|
prompt:
|
||||||
|
'I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. My first sentence is "落霞與孤鶩齊飛,秋水共長天一色".',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'daaf35d9-56fd-4bad-95da-5acfc51a5120',
|
||||||
|
name: 'Interviewer',
|
||||||
|
prompt:
|
||||||
|
'I want you to act as an interviewer. I will be the candidate and you will ask me the interview questions for the Frontend Developer position. I want you to only reply as the interviewer. Do not write all the conservation at once. I want you to only do the interview with me. Ask me the questions and wait for my answers. Do not write explanations. Ask me the questions one by one like an interviewer does and wait for my answers. My first sentence is "Hi".',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default defaultPrompts;
|
|
@ -2,9 +2,11 @@ import {
|
||||||
LocalStorageInterfaceV0ToV1,
|
LocalStorageInterfaceV0ToV1,
|
||||||
LocalStorageInterfaceV1ToV2,
|
LocalStorageInterfaceV1ToV2,
|
||||||
LocalStorageInterfaceV2ToV3,
|
LocalStorageInterfaceV2ToV3,
|
||||||
|
LocalStorageInterfaceV3ToV4,
|
||||||
} from '@type/chat';
|
} from '@type/chat';
|
||||||
import { defaultChatConfig } from '@constants/chat';
|
import { defaultChatConfig } from '@constants/chat';
|
||||||
import { officialAPIEndpoint } from '@constants/auth';
|
import { officialAPIEndpoint } from '@constants/auth';
|
||||||
|
import defaultPrompts from '@constants/prompt';
|
||||||
|
|
||||||
export const migrateV0 = (persistedState: LocalStorageInterfaceV0ToV1) => {
|
export const migrateV0 = (persistedState: LocalStorageInterfaceV0ToV1) => {
|
||||||
persistedState.chats.forEach((chat) => {
|
persistedState.chats.forEach((chat) => {
|
||||||
|
@ -31,3 +33,7 @@ export const migrateV2 = (persistedState: LocalStorageInterfaceV2ToV3) => {
|
||||||
});
|
});
|
||||||
persistedState.autoTitle = false;
|
persistedState.autoTitle = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const migrateV3 = (persistedState: LocalStorageInterfaceV3ToV4) => {
|
||||||
|
persistedState.prompts = defaultPrompts;
|
||||||
|
};
|
||||||
|
|
18
src/store/prompt-slice.ts
Normal file
18
src/store/prompt-slice.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { StoreSlice } from './store';
|
||||||
|
import { Prompt } from '@type/prompt';
|
||||||
|
import defaultPrompts from '@constants/prompt';
|
||||||
|
|
||||||
|
export interface PromptSlice {
|
||||||
|
prompts: Prompt[];
|
||||||
|
setPrompts: (commandPrompt: Prompt[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPromptSlice: StoreSlice<PromptSlice> = (set, get) => ({
|
||||||
|
prompts: defaultPrompts,
|
||||||
|
setPrompts: (prompts: Prompt[]) => {
|
||||||
|
set((prev: PromptSlice) => ({
|
||||||
|
...prev,
|
||||||
|
prompts: prompts,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
|
@ -4,14 +4,20 @@ import { ChatSlice, createChatSlice } from './chat-slice';
|
||||||
import { InputSlice, createInputSlice } from './input-slice';
|
import { InputSlice, createInputSlice } from './input-slice';
|
||||||
import { AuthSlice, createAuthSlice } from './auth-slice';
|
import { AuthSlice, createAuthSlice } from './auth-slice';
|
||||||
import { ConfigSlice, createConfigSlice } from './config-slice';
|
import { ConfigSlice, createConfigSlice } from './config-slice';
|
||||||
|
import { PromptSlice, createPromptSlice } from './prompt-slice';
|
||||||
import {
|
import {
|
||||||
LocalStorageInterfaceV0ToV1,
|
LocalStorageInterfaceV0ToV1,
|
||||||
LocalStorageInterfaceV1ToV2,
|
LocalStorageInterfaceV1ToV2,
|
||||||
LocalStorageInterfaceV2ToV3,
|
LocalStorageInterfaceV2ToV3,
|
||||||
|
LocalStorageInterfaceV3ToV4,
|
||||||
} from '@type/chat';
|
} from '@type/chat';
|
||||||
import { migrateV0, migrateV1, migrateV2 } from './migrate';
|
import { migrateV0, migrateV1, migrateV2, migrateV3 } from './migrate';
|
||||||
|
|
||||||
export type StoreState = ChatSlice & InputSlice & AuthSlice & ConfigSlice;
|
export type StoreState = ChatSlice &
|
||||||
|
InputSlice &
|
||||||
|
AuthSlice &
|
||||||
|
ConfigSlice &
|
||||||
|
PromptSlice;
|
||||||
|
|
||||||
export type StoreSlice<T> = (
|
export type StoreSlice<T> = (
|
||||||
set: StoreApi<StoreState>['setState'],
|
set: StoreApi<StoreState>['setState'],
|
||||||
|
@ -25,6 +31,7 @@ const useStore = create<StoreState>()(
|
||||||
...createInputSlice(set, get),
|
...createInputSlice(set, get),
|
||||||
...createAuthSlice(set, get),
|
...createAuthSlice(set, get),
|
||||||
...createConfigSlice(set, get),
|
...createConfigSlice(set, get),
|
||||||
|
...createPromptSlice(set, get),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'free-chat-gpt',
|
name: 'free-chat-gpt',
|
||||||
|
@ -36,8 +43,9 @@ const useStore = create<StoreState>()(
|
||||||
apiEndpoint: state.apiEndpoint,
|
apiEndpoint: state.apiEndpoint,
|
||||||
theme: state.theme,
|
theme: state.theme,
|
||||||
autoTitle: state.autoTitle,
|
autoTitle: state.autoTitle,
|
||||||
|
prompts: state.prompts,
|
||||||
}),
|
}),
|
||||||
version: 3,
|
version: 4,
|
||||||
migrate: (persistedState, version) => {
|
migrate: (persistedState, version) => {
|
||||||
switch (version) {
|
switch (version) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -46,6 +54,8 @@ const useStore = create<StoreState>()(
|
||||||
migrateV1(persistedState as LocalStorageInterfaceV1ToV2);
|
migrateV1(persistedState as LocalStorageInterfaceV1ToV2);
|
||||||
case 2:
|
case 2:
|
||||||
migrateV2(persistedState as LocalStorageInterfaceV2ToV3);
|
migrateV2(persistedState as LocalStorageInterfaceV2ToV3);
|
||||||
|
case 3:
|
||||||
|
migrateV3(persistedState as LocalStorageInterfaceV3ToV4);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return persistedState as StoreState;
|
return persistedState as StoreState;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Prompt } from './prompt';
|
||||||
import { Theme } from './theme';
|
import { Theme } from './theme';
|
||||||
|
|
||||||
export type Role = 'user' | 'assistant' | 'system';
|
export type Role = 'user' | 'assistant' | 'system';
|
||||||
|
@ -51,3 +52,14 @@ export interface LocalStorageInterfaceV2ToV3 {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
autoTitle: boolean;
|
autoTitle: boolean;
|
||||||
}
|
}
|
||||||
|
export interface LocalStorageInterfaceV3ToV4 {
|
||||||
|
chats: ChatInterface[];
|
||||||
|
currentChatIndex: number;
|
||||||
|
apiKey: string;
|
||||||
|
apiFree: boolean;
|
||||||
|
apiFreeEndpoint: string;
|
||||||
|
apiEndpoint?: string;
|
||||||
|
theme: Theme;
|
||||||
|
autoTitle: boolean;
|
||||||
|
prompts: Prompt[];
|
||||||
|
}
|
||||||
|
|
5
src/types/prompt.ts
Normal file
5
src/types/prompt.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export interface Prompt {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
prompt: string;
|
||||||
|
}
|
23
yarn.lock
23
yarn.lock
|
@ -437,6 +437,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
||||||
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
||||||
|
|
||||||
|
"@types/uuid@^9.0.1":
|
||||||
|
version "9.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.1.tgz#98586dc36aee8dacc98cc396dbca8d0429647aa6"
|
||||||
|
integrity sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==
|
||||||
|
|
||||||
"@vitejs/plugin-react-swc@^3.0.0":
|
"@vitejs/plugin-react-swc@^3.0.0":
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-3.2.0.tgz#7c4f6e116a296c27f680d05750f9dbf798cf7709"
|
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-3.2.0.tgz#7c4f6e116a296c27f680d05750f9dbf798cf7709"
|
||||||
|
@ -1158,6 +1163,14 @@ markdown-table@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd"
|
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd"
|
||||||
integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==
|
integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==
|
||||||
|
|
||||||
|
match-sorter@^6.3.1:
|
||||||
|
version "6.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda"
|
||||||
|
integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.12.5"
|
||||||
|
remove-accents "0.4.2"
|
||||||
|
|
||||||
math-random@2.0.1:
|
math-random@2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/math-random/-/math-random-2.0.1.tgz#5604b16c6a9a4aee63aff13937fb909b27e46b3a"
|
resolved "https://registry.yarnpkg.com/math-random/-/math-random-2.0.1.tgz#5604b16c6a9a4aee63aff13937fb909b27e46b3a"
|
||||||
|
@ -1985,6 +1998,11 @@ remark-rehype@^10.0.0:
|
||||||
mdast-util-to-hast "^12.1.0"
|
mdast-util-to-hast "^12.1.0"
|
||||||
unified "^10.0.0"
|
unified "^10.0.0"
|
||||||
|
|
||||||
|
remove-accents@0.4.2:
|
||||||
|
version "0.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
|
||||||
|
integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
|
||||||
|
|
||||||
resolve-from@^4.0.0:
|
resolve-from@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||||
|
@ -2256,6 +2274,11 @@ utrie@^1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-arraybuffer "^1.0.2"
|
base64-arraybuffer "^1.0.2"
|
||||||
|
|
||||||
|
uuid@^9.0.0:
|
||||||
|
version "9.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
|
||||||
|
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
|
||||||
|
|
||||||
uvu@^0.5.0:
|
uvu@^0.5.0:
|
||||||
version "0.5.6"
|
version "0.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df"
|
resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df"
|
||||||
|
|
Loading…
Reference in a new issue