From b9657c6325ed6be87f20b6fa5ed0f35da51b1a61 Mon Sep 17 00:00:00 2001 From: Jing Hua Date: Mon, 13 Mar 2023 21:54:05 +0800 Subject: [PATCH] feat: prompt library --- package.json | 3 + public/locales/en/main.json | 7 +- public/locales/zh-CN/main.json | 7 +- public/locales/zh-HK/main.json | 7 +- .../Message/CommandPrompt/CommandPrompt.tsx | 72 +++++++++ .../Message/CommandPrompt/index.ts | 1 + .../ChatContent/Message/MessageContent.tsx | 100 ++++++------ .../PromptLibraryMenu/PromptLibraryMenu.tsx | 146 ++++++++++++++++++ src/components/PromptLibraryMenu/index.ts | 1 + src/components/SettingsMenu/SettingsMenu.tsx | 2 + src/constants/prompt.ts | 19 +++ src/store/migrate.ts | 6 + src/store/prompt-slice.ts | 18 +++ src/store/store.ts | 16 +- src/types/chat.ts | 12 ++ src/types/prompt.ts | 5 + yarn.lock | 23 +++ 17 files changed, 392 insertions(+), 53 deletions(-) create mode 100644 src/components/Chat/ChatContent/Message/CommandPrompt/CommandPrompt.tsx create mode 100644 src/components/Chat/ChatContent/Message/CommandPrompt/index.ts create mode 100644 src/components/PromptLibraryMenu/PromptLibraryMenu.tsx create mode 100644 src/components/PromptLibraryMenu/index.ts create mode 100644 src/constants/prompt.ts create mode 100644 src/store/prompt-slice.ts create mode 100644 src/types/prompt.ts diff --git a/package.json b/package.json index bc99f34..bfd11a1 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "i18next-browser-languagedetector": "^7.0.1", "i18next-http-backend": "^2.1.1", "jspdf": "^2.5.1", + "match-sorter": "^6.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^12.2.0", @@ -24,6 +25,7 @@ "rehype-sanitize": "^5.0.1", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", + "uuid": "^9.0.0", "zustand": "^4.3.6" }, "devDependencies": { @@ -32,6 +34,7 @@ "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "@types/react-scroll-to-bottom": "^4.2.0", + "@types/uuid": "^9.0.1", "@vitejs/plugin-react-swc": "^3.0.0", "autoprefixer": "^10.4.13", "postcss": "^8.4.21", diff --git a/public/locales/en/main.json b/public/locales/en/main.json index 42dc454..8a9b494 100644 --- a/public/locales/en/main.json +++ b/public/locales/en/main.json @@ -23,5 +23,10 @@ "darkMode": "Dark Mode", "setting": "Settings", "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: " } diff --git a/public/locales/zh-CN/main.json b/public/locales/zh-CN/main.json index cfa7a42..d94c36c 100644 --- a/public/locales/zh-CN/main.json +++ b/public/locales/zh-CN/main.json @@ -23,5 +23,10 @@ "darkMode": "黑暗模式", "setting": "设置", "image": "图片", - "autoTitle": "自动生成标题" + "autoTitle": "自动生成标题", + "prompt": "提示词", + "promptLibrary": "提示词资料库", + "name": "名称", + "search": "搜索", + "morePrompts": "更多提示词请点击:" } diff --git a/public/locales/zh-HK/main.json b/public/locales/zh-HK/main.json index 785e384..9178a24 100644 --- a/public/locales/zh-HK/main.json +++ b/public/locales/zh-HK/main.json @@ -23,5 +23,10 @@ "darkMode": "黑暗模式", "setting": "設定", "image": "圖片", - "autoTitle": "自動生成標題" + "autoTitle": "自動生成標題", + "prompt": "Prompt", + "promptLibrary": "Prompt 資料庫", + "name": "名", + "search": "檢索", + "morePrompts": "如果你想揾更多 prompt,撳呢度:" } diff --git a/src/components/Chat/ChatContent/Message/CommandPrompt/CommandPrompt.tsx b/src/components/Chat/ChatContent/Message/CommandPrompt/CommandPrompt.tsx new file mode 100644 index 0000000..f8af930 --- /dev/null +++ b/src/components/Chat/ChatContent/Message/CommandPrompt/CommandPrompt.tsx @@ -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>; +}) => { + const { t } = useTranslation(); + const prompts = useStore((state) => state.prompts); + const [dropDown, setDropDown] = useState(false); + const [_prompts, _setPrompts] = useState(prompts); + const [input, setInput] = useState(''); + + useEffect(() => { + const filteredPrompts = matchSorter(useStore.getState().prompts, input, { + keys: ['name'], + }); + _setPrompts(filteredPrompts); + }, [input]); + + useEffect(() => { + _setPrompts(prompts); + setInput(''); + }, [prompts]); + + return ( +
+ +
+
{t('promptLibrary')}
+ { + setInput(e.target.value); + }} + /> +
    + {_prompts.map((cp) => ( +
  • { + _setContent((prev) => prev + cp.prompt); + setDropDown(false); + }} + key={cp.id} + > + {cp.name} +
  • + ))} +
+
+
+ ); +}; + +export default CommandPrompt; diff --git a/src/components/Chat/ChatContent/Message/CommandPrompt/index.ts b/src/components/Chat/ChatContent/Message/CommandPrompt/index.ts new file mode 100644 index 0000000..68d34d0 --- /dev/null +++ b/src/components/Chat/ChatContent/Message/CommandPrompt/index.ts @@ -0,0 +1 @@ +export { default } from './CommandPrompt'; diff --git a/src/components/Chat/ChatContent/Message/MessageContent.tsx b/src/components/Chat/ChatContent/Message/MessageContent.tsx index 809b537..cba75cc 100644 --- a/src/components/Chat/ChatContent/Message/MessageContent.tsx +++ b/src/components/Chat/ChatContent/Message/MessageContent.tsx @@ -26,6 +26,7 @@ import useSubmit from '@hooks/useSubmit'; import { ChatInterface } from '@type/chat'; import PopupModal from '@components/PopupModal'; +import CommandPrompt from './CommandPrompt'; import CodeBlock from './CodeBlock'; import { codeLanguageSubset } from '@constants/chat'; @@ -310,13 +311,6 @@ const EditView = ({ if (textareaRef.current) textareaRef.current.style.height = 'auto'; }; - const handleInput = (e: React.ChangeEvent) => { - if (textareaRef.current) { - textareaRef.current.style.height = 'auto'; - textareaRef.current.style.height = `${e.target.scrollHeight}px`; - } - }; - const handleKeyDown = (e: React.KeyboardEvent) => { if ((e.ctrlKey || e.shiftKey) && e.key === 'Enter') { e.preventDefault(); @@ -368,6 +362,13 @@ const EditView = ({ handleSubmit(); }; + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; + } + }, [_content]); + useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; @@ -391,7 +392,6 @@ const EditView = ({ _setContent(e.target.value); }} value={_content} - onInput={handleInput} onKeyDown={handleKeyDown} rows={1} > @@ -402,6 +402,7 @@ const EditView = ({ handleSave={handleSave} setIsModalOpen={setIsModalOpen} setIsEdit={setIsEdit} + _setContent={_setContent} /> {isModalOpen && ( void; handleSave: () => void; setIsModalOpen: React.Dispatch>; setIsEdit: React.Dispatch>; + _setContent: React.Dispatch>; }) => { const { t } = useTranslation(); return ( -
- {sticky && ( +
+
+ {sticky && ( + + )} + - )} - + {sticky || ( + + )} - {sticky || ( - - )} - - {sticky || ( - - )} + {sticky || ( + + )} +
+
); } diff --git a/src/components/PromptLibraryMenu/PromptLibraryMenu.tsx b/src/components/PromptLibraryMenu/PromptLibraryMenu.tsx new file mode 100644 index 0000000..b1c8836 --- /dev/null +++ b/src/components/PromptLibraryMenu/PromptLibraryMenu.tsx @@ -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(false); + return ( +
+ + {isModalOpen && ( + + )} +
+ ); +}; + +const PromptLibraryMenuPopUp = ({ + setIsModalOpen, +}: { + setIsModalOpen: React.Dispatch>; +}) => { + const { t } = useTranslation(); + + const setPrompts = useStore((state) => state.setPrompts); + const [_prompts, _setPrompts] = useState( + JSON.parse(JSON.stringify(useStore.getState().prompts)) + ); + const container = useRef(null); + + const handleInput = (e: React.ChangeEvent) => { + 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 ( + +
+
+
+
{t('name')}
+
{t('prompt')}
+
+ {_prompts.map((prompt, index) => ( +
+
+ +
+
+ +
+
deletePrompt(index)} + > + +
+
+ ))} +
+
+ +
+
+ {t('morePrompts')} + + awesome-chatgpt-prompts + +
+
+
+ ); +}; + +export default PromptLibraryMenu; diff --git a/src/components/PromptLibraryMenu/index.ts b/src/components/PromptLibraryMenu/index.ts new file mode 100644 index 0000000..20f1652 --- /dev/null +++ b/src/components/PromptLibraryMenu/index.ts @@ -0,0 +1 @@ +export { default } from './PromptLibraryMenu'; diff --git a/src/components/SettingsMenu/SettingsMenu.tsx b/src/components/SettingsMenu/SettingsMenu.tsx index 642bddb..188e4cb 100644 --- a/src/components/SettingsMenu/SettingsMenu.tsx +++ b/src/components/SettingsMenu/SettingsMenu.tsx @@ -7,6 +7,7 @@ import SettingIcon from '@icon/SettingIcon'; import ThemeSwitcher from '@components/Menu/MenuOptions/ThemeSwitcher'; import LanguageSelector from '@components/LanguageSelector'; import AutoTitleToggle from './AutoTitleToggle'; +import PromptLibraryMenu from '@components/PromptLibraryMenu'; const SettingsMenu = () => { const { t } = useTranslation(); @@ -37,6 +38,7 @@ const SettingsMenu = () => { +
)} diff --git a/src/constants/prompt.ts b/src/constants/prompt.ts new file mode 100644 index 0000000..63141b3 --- /dev/null +++ b/src/constants/prompt.ts @@ -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; diff --git a/src/store/migrate.ts b/src/store/migrate.ts index 0b9abb1..c3576d6 100644 --- a/src/store/migrate.ts +++ b/src/store/migrate.ts @@ -2,9 +2,11 @@ import { LocalStorageInterfaceV0ToV1, LocalStorageInterfaceV1ToV2, LocalStorageInterfaceV2ToV3, + LocalStorageInterfaceV3ToV4, } from '@type/chat'; import { defaultChatConfig } from '@constants/chat'; import { officialAPIEndpoint } from '@constants/auth'; +import defaultPrompts from '@constants/prompt'; export const migrateV0 = (persistedState: LocalStorageInterfaceV0ToV1) => { persistedState.chats.forEach((chat) => { @@ -31,3 +33,7 @@ export const migrateV2 = (persistedState: LocalStorageInterfaceV2ToV3) => { }); persistedState.autoTitle = false; }; + +export const migrateV3 = (persistedState: LocalStorageInterfaceV3ToV4) => { + persistedState.prompts = defaultPrompts; +}; diff --git a/src/store/prompt-slice.ts b/src/store/prompt-slice.ts new file mode 100644 index 0000000..5073156 --- /dev/null +++ b/src/store/prompt-slice.ts @@ -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 = (set, get) => ({ + prompts: defaultPrompts, + setPrompts: (prompts: Prompt[]) => { + set((prev: PromptSlice) => ({ + ...prev, + prompts: prompts, + })); + }, +}); diff --git a/src/store/store.ts b/src/store/store.ts index d38a61e..a337e5b 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -4,14 +4,20 @@ import { ChatSlice, createChatSlice } from './chat-slice'; import { InputSlice, createInputSlice } from './input-slice'; import { AuthSlice, createAuthSlice } from './auth-slice'; import { ConfigSlice, createConfigSlice } from './config-slice'; +import { PromptSlice, createPromptSlice } from './prompt-slice'; import { LocalStorageInterfaceV0ToV1, LocalStorageInterfaceV1ToV2, LocalStorageInterfaceV2ToV3, + LocalStorageInterfaceV3ToV4, } 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 = ( set: StoreApi['setState'], @@ -25,6 +31,7 @@ const useStore = create()( ...createInputSlice(set, get), ...createAuthSlice(set, get), ...createConfigSlice(set, get), + ...createPromptSlice(set, get), }), { name: 'free-chat-gpt', @@ -36,8 +43,9 @@ const useStore = create()( apiEndpoint: state.apiEndpoint, theme: state.theme, autoTitle: state.autoTitle, + prompts: state.prompts, }), - version: 3, + version: 4, migrate: (persistedState, version) => { switch (version) { case 0: @@ -46,6 +54,8 @@ const useStore = create()( migrateV1(persistedState as LocalStorageInterfaceV1ToV2); case 2: migrateV2(persistedState as LocalStorageInterfaceV2ToV3); + case 3: + migrateV3(persistedState as LocalStorageInterfaceV3ToV4); break; } return persistedState as StoreState; diff --git a/src/types/chat.ts b/src/types/chat.ts index 157d8b7..e227d9e 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -1,3 +1,4 @@ +import { Prompt } from './prompt'; import { Theme } from './theme'; export type Role = 'user' | 'assistant' | 'system'; @@ -51,3 +52,14 @@ export interface LocalStorageInterfaceV2ToV3 { theme: Theme; autoTitle: boolean; } +export interface LocalStorageInterfaceV3ToV4 { + chats: ChatInterface[]; + currentChatIndex: number; + apiKey: string; + apiFree: boolean; + apiFreeEndpoint: string; + apiEndpoint?: string; + theme: Theme; + autoTitle: boolean; + prompts: Prompt[]; +} diff --git a/src/types/prompt.ts b/src/types/prompt.ts new file mode 100644 index 0000000..afafa7e --- /dev/null +++ b/src/types/prompt.ts @@ -0,0 +1,5 @@ +export interface Prompt { + id: string; + name: string; + prompt: string; +} diff --git a/yarn.lock b/yarn.lock index 0526b56..c852251 100644 --- a/yarn.lock +++ b/yarn.lock @@ -437,6 +437,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" 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": version "3.2.0" 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" 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: version "2.0.1" 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" 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: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -2256,6 +2274,11 @@ utrie@^1.0.2: dependencies: 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: version "0.5.6" resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df"