From 789a77977525eb06be52c329a7a65ad47e6babfc Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 18 Apr 2023 01:34:12 +0800 Subject: [PATCH] feat: user prompts --- app/components/settings.module.scss | 49 ++++++++++++ app/components/settings.tsx | 119 +++++++++++++++++++++++++--- app/components/ui-lib.module.scss | 2 +- app/locales/cn.ts | 9 ++- app/locales/de.ts | 5 ++ app/locales/en.ts | 7 +- app/locales/es.ts | 5 ++ app/locales/it.ts | 5 ++ app/locales/jp.ts | 5 ++ app/locales/tr.ts | 5 ++ app/locales/tw.ts | 5 ++ app/store/prompt.ts | 79 +++++++++++++----- app/store/update.ts | 9 ++- app/styles/globals.scss | 2 + 14 files changed, 269 insertions(+), 37 deletions(-) diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss index 0628c6e3..b7f09558 100644 --- a/app/components/settings.module.scss +++ b/app/components/settings.module.scss @@ -34,10 +34,59 @@ } .user-prompt-modal { + min-height: 40vh; + .user-prompt-search { + width: 100%; + max-width: 100%; + margin-bottom: 10px; + background-color: var(--gray); } .user-prompt-list { + padding: 10px 0; + + .user-prompt-item { + margin-bottom: 10px; + widows: 100%; + + .user-prompt-header { + display: flex; + widows: 100%; + margin-bottom: 5px; + + .user-prompt-title { + flex-grow: 1; + max-width: 100%; + margin-right: 5px; + padding: 5px; + font-size: 12px; + text-align: left; + } + + .user-prompt-buttons { + display: flex; + align-items: center; + + .user-prompt-button { + height: 100%; + + &:not(:last-child) { + margin-right: 5px; + } + } + } + } + + .user-prompt-content { + width: 100%; + box-sizing: border-box; + padding: 5px; + margin-right: 10px; + font-size: 12px; + flex-grow: 1; + } + } } .user-prompt-actions { diff --git a/app/components/settings.tsx b/app/components/settings.tsx index d5f26c3e..d81b5b35 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -6,12 +6,13 @@ import styles from "./settings.module.scss"; import ResetIcon from "../icons/reload.svg"; import CloseIcon from "../icons/close.svg"; +import CopyIcon from "../icons/copy.svg"; import ClearIcon from "../icons/clear.svg"; import EditIcon from "../icons/edit.svg"; import EyeIcon from "../icons/eye.svg"; import EyeOffIcon from "../icons/eye-off.svg"; -import { List, ListItem, Popover, showToast } from "./ui-lib"; +import { Input, List, ListItem, Modal, Popover } from "./ui-lib"; import { IconButton } from "./button"; import { @@ -26,21 +27,112 @@ import { import { Avatar } from "./chat"; import Locale, { AllLangs, changeLang, getLang } from "../locales"; -import { getEmojiUrl } from "../utils"; +import { copyToClipboard, getEmojiUrl } from "../utils"; import Link from "next/link"; import { UPDATE_URL } from "../constant"; -import { SearchService, usePromptStore } from "../store/prompt"; -import { requestUsage } from "../requests"; +import { Prompt, SearchService, usePromptStore } from "../store/prompt"; import { ErrorBoundary } from "./error"; import { InputRange } from "./input-range"; -function UserPromptModal() { +function UserPromptModal(props: { onClose?: () => void }) { const promptStore = usePromptStore(); - const prompts = Array.from(promptStore.prompts.values()).sort( - (a, b) => a.id ?? 0 - (b.id ?? 0), - ); + const userPrompts = promptStore.getUserPrompts(); + const builtinPrompts = SearchService.builtinPrompts; + const allPrompts = userPrompts.concat(builtinPrompts); + const [searchInput, setSearchInput] = useState(""); + const [searchPrompts, setSearchPrompts] = useState([]); + const prompts = searchInput.length > 0 ? searchPrompts : allPrompts; - return <>; + useEffect(() => { + if (searchInput.length > 0) { + const searchResult = SearchService.search(searchInput); + setSearchPrompts(searchResult); + } else { + setSearchPrompts([]); + } + }, [searchInput]); + + return ( +
+ props.onClose?.()} + actions={[ + promptStore.add({ title: "", content: "" })} + icon={} + bordered + text={Locale.Settings.Prompt.Modal.Add} + />, + ]} + > +
+ setSearchInput(e.currentTarget.value)} + > + +
+ {prompts.map((v, _) => ( +
+
+ { + if (v.isUser) { + promptStore.updateUserPrompts( + v.id!, + (prompt) => (prompt.title = e.currentTarget.value), + ); + } + }} + > + +
+ {v.isUser && ( + } + bordered + className={styles["user-prompt-button"]} + onClick={() => promptStore.remove(v.id!)} + /> + )} + } + bordered + className={styles["user-prompt-button"]} + onClick={() => copyToClipboard(v.content)} + /> +
+
+ { + if (v.isUser) { + promptStore.updateUserPrompts( + v.id!, + (prompt) => (prompt.content = e.currentTarget.value), + ); + } + }} + /> +
+ ))} +
+
+
+
+ ); } function SettingItem(props: { @@ -129,7 +221,8 @@ export function Settings(props: { closeSettings: () => void }) { const promptStore = usePromptStore(); const builtinCount = SearchService.count.builtin; - const customCount = promptStore.prompts.size ?? 0; + const customCount = promptStore.getUserPrompts().length ?? 0; + const [shouldShowPromptModal, setShowPromptModal] = useState(false); const showUsage = accessStore.isAuthorized(); useEffect(() => { @@ -477,7 +570,7 @@ export function Settings(props: { closeSettings: () => void }) { } text={Locale.Settings.Prompt.Edit} - onClick={() => showToast(Locale.WIP)} + onClick={() => setShowPromptModal(true)} /> @@ -563,6 +656,10 @@ export function Settings(props: { closeSettings: () => void }) { > + + {shouldShowPromptModal && ( + setShowPromptModal(false)} /> + )} ); diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss index 457c5504..8965c06a 100644 --- a/app/components/ui-lib.module.scss +++ b/app/components/ui-lib.module.scss @@ -53,7 +53,7 @@ box-shadow: var(--card-shadow); background-color: var(--white); border-radius: 12px; - width: 50vw; + width: 60vw; animation: slide-in ease 0.3s; --modal-padding: 20px; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 391ade2a..d0ab27ca 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -59,10 +59,10 @@ const cn = { ResetAll: "重置所有选项", Close: "关闭", ConfirmResetAll: { - Confirm: "Are you sure you want to reset all configurations?", + Confirm: "确认清除所有配置?", }, ConfirmClearAll: { - Confirm: "Are you sure you want to reset all chat?", + Confirm: "确认清除所有聊天记录?", }, }, Lang: { @@ -105,6 +105,11 @@ const cn = { ListCount: (builtin: number, custom: number) => `内置 ${builtin} 条,用户定义 ${custom} 条`, Edit: "编辑", + Modal: { + Title: "提示词列表", + Add: "增加一条", + Search: "搜尋提示詞", + }, }, HistoryCount: { Title: "附带历史消息数", diff --git a/app/locales/de.ts b/app/locales/de.ts index d373cba5..e71abfaf 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -107,6 +107,11 @@ const de: LocaleType = { ListCount: (builtin: number, custom: number) => `${builtin} integriert, ${custom} benutzerdefiniert`, Edit: "Bearbeiten", + Modal: { + Title: "Prompt List", + Add: "Add One", + Search: "Search Prompts", + }, }, HistoryCount: { Title: "Anzahl der angehängten Nachrichten", diff --git a/app/locales/en.ts b/app/locales/en.ts index 213b02d3..20e56960 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -107,6 +107,11 @@ const en: LocaleType = { ListCount: (builtin: number, custom: number) => `${builtin} built-in, ${custom} user-defined`, Edit: "Edit", + Modal: { + Title: "Prompt List", + Add: "Add One", + Search: "Search Prompts", + }, }, HistoryCount: { Title: "Attached Messages Count", @@ -128,7 +133,7 @@ const en: LocaleType = { return `Used this month $${used}, subscription $${total}`; }, IsChecking: "Checking...", - Check: "Check Again", + Check: "Check", NoAccess: "Enter API Key to check balance", }, AccessCode: { diff --git a/app/locales/es.ts b/app/locales/es.ts index 5d6eca17..e2a9eb21 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -107,6 +107,11 @@ const es: LocaleType = { ListCount: (builtin: number, custom: number) => `${builtin} incorporado, ${custom} definido por el usuario`, Edit: "Editar", + Modal: { + Title: "Prompt List", + Add: "Add One", + Search: "Search Prompts", + }, }, HistoryCount: { Title: "Cantidad de mensajes adjuntos", diff --git a/app/locales/it.ts b/app/locales/it.ts index 22523888..f0453b5c 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -107,6 +107,11 @@ const it: LocaleType = { ListCount: (builtin: number, custom: number) => `${builtin} built-in, ${custom} user-defined`, Edit: "Modifica", + Modal: { + Title: "Prompt List", + Add: "Add One", + Search: "Search Prompts", + }, }, HistoryCount: { Title: "Conteggio dei messaggi allegati", diff --git a/app/locales/jp.ts b/app/locales/jp.ts index f4ad741c..a793b5fe 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -108,6 +108,11 @@ const jp = { ListCount: (builtin: number, custom: number) => `組み込み ${builtin} 件、ユーザー定義 ${custom} 件`, Edit: "編集", + Modal: { + Title: "提示词列表", + Add: "增加一条", + Search: "搜尋提示詞", + }, }, HistoryCount: { Title: "履歴メッセージ数を添付", diff --git a/app/locales/tr.ts b/app/locales/tr.ts index a7ede192..04a84624 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -107,6 +107,11 @@ const tr: LocaleType = { ListCount: (builtin: number, custom: number) => `${builtin} yerleşik, ${custom} kullanıcı tanımlı`, Edit: "Düzenle", + Modal: { + Title: "Prompt List", + Add: "Add One", + Search: "Search Prompts", + }, }, HistoryCount: { Title: "Ekli Mesaj Sayısı", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 1397bb09..2fbb2e47 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -105,6 +105,11 @@ const tw: LocaleType = { ListCount: (builtin: number, custom: number) => `內置 ${builtin} 條,用戶定義 ${custom} 條`, Edit: "編輯", + Modal: { + Title: "提示詞列表", + Add: "增加一條", + Search: "搜索提示词", + }, }, HistoryCount: { Title: "附帶歷史訊息數", diff --git a/app/store/prompt.ts b/app/store/prompt.ts index 648b3814..8d754ff5 100644 --- a/app/store/prompt.ts +++ b/app/store/prompt.ts @@ -5,62 +5,74 @@ import { getLang } from "../locales"; export interface Prompt { id?: number; + isUser?: boolean; title: string; content: string; } export interface PromptStore { + counter: number; latestId: number; - prompts: Map; + prompts: Record; add: (prompt: Prompt) => number; remove: (id: number) => void; search: (text: string) => Prompt[]; + + getUserPrompts: () => Prompt[]; + updateUserPrompts: (id: number, updater: (prompt: Prompt) => void) => void; } export const PROMPT_KEY = "prompt-store"; export const SearchService = { ready: false, - engine: new Fuse([], { keys: ["title"] }), + builtinEngine: new Fuse([], { keys: ["title"] }), + userEngine: new Fuse([], { keys: ["title"] }), count: { builtin: 0, }, - allBuiltInPrompts: [] as Prompt[], + allPrompts: [] as Prompt[], + builtinPrompts: [] as Prompt[], - init(prompts: Prompt[]) { + init(builtinPrompts: Prompt[], userPrompts: Prompt[]) { if (this.ready) { return; } - this.allBuiltInPrompts = prompts; - this.engine.setCollection(prompts); + this.allPrompts = userPrompts.concat(builtinPrompts); + this.builtinPrompts = builtinPrompts.slice(); + this.builtinEngine.setCollection(builtinPrompts); + this.userEngine.setCollection(userPrompts); this.ready = true; }, remove(id: number) { - this.engine.remove((doc) => doc.id === id); + this.userEngine.remove((doc) => doc.id === id); }, add(prompt: Prompt) { - this.engine.add(prompt); + this.userEngine.add(prompt); }, search(text: string) { - const results = this.engine.search(text); - return results.map((v) => v.item); + const userResults = this.userEngine.search(text); + const builtinResults = this.builtinEngine.search(text); + return userResults.concat(builtinResults).map((v) => v.item); }, }; export const usePromptStore = create()( persist( (set, get) => ({ + counter: 0, latestId: 0, - prompts: new Map(), + prompts: {}, add(prompt) { const prompts = get().prompts; prompt.id = get().latestId + 1; - prompts.set(prompt.id, prompt); + prompt.isUser = true; + prompts[prompt.id] = prompt; set(() => ({ latestId: prompt.id!, @@ -72,19 +84,40 @@ export const usePromptStore = create()( remove(id) { const prompts = get().prompts; - prompts.delete(id); + delete prompts[id]; SearchService.remove(id); set(() => ({ prompts, + counter: get().counter + 1, })); }, + getUserPrompts() { + const userPrompts = Object.values(get().prompts ?? {}); + userPrompts.sort((a, b) => (b.id && a.id ? b.id - a.id : 0)); + return userPrompts; + }, + + updateUserPrompts(id: number, updater) { + const prompt = get().prompts[id] ?? { + title: "", + content: "", + id, + }; + + SearchService.remove(id); + updater(prompt); + const prompts = get().prompts; + prompts[id] = prompt; + set(() => ({ prompts })); + SearchService.add(prompt); + }, + search(text) { if (text.length === 0) { - // return all prompts - const userPrompts = get().prompts?.values?.() ?? []; - return SearchService.allBuiltInPrompts.concat([...userPrompts]); + // return all rompts + return SearchService.allPrompts.concat([...get().getUserPrompts()]); } return SearchService.search(text) as Prompt[]; }, @@ -104,23 +137,27 @@ export const usePromptStore = create()( if (getLang() === "cn") { fetchPrompts = fetchPrompts.reverse(); } - const builtinPrompts = fetchPrompts - .map((promptList: PromptList) => { + const builtinPrompts = fetchPrompts.map( + (promptList: PromptList) => { return promptList.map( ([title, content]) => ({ + id: Math.random(), title, content, } as Prompt), ); - }) - .concat([...(state?.prompts?.values() ?? [])]); + }, + ); + + const userPrompts = + usePromptStore.getState().getUserPrompts() ?? []; const allPromptsForSearch = builtinPrompts .reduce((pre, cur) => pre.concat(cur), []) .filter((v) => !!v.title && !!v.content); SearchService.count.builtin = res.en.length + res.cn.length; - SearchService.init(allPromptsForSearch); + SearchService.init(allPromptsForSearch, userPrompts); }); }, }, diff --git a/app/store/update.ts b/app/store/update.ts index d49c246d..47b190b8 100644 --- a/app/store/update.ts +++ b/app/store/update.ts @@ -50,13 +50,16 @@ export const useUpdateStore = create()( const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE; if (!force && !overTenMins) return; + set(() => ({ + lastUpdate: Date.now(), + })); + try { // const data = await (await fetch(FETCH_TAG_URL)).json(); // const remoteId = data[0].name as string; const data = await (await fetch(FETCH_COMMIT_URL)).json(); const remoteId = (data[0].sha as string).substring(0, 7); set(() => ({ - lastUpdate: Date.now(), remoteVersion: remoteId, })); console.log("[Got Upstream] ", remoteId); @@ -69,6 +72,10 @@ export const useUpdateStore = create()( const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE; if (!overOneMinute && !force) return; + set(() => ({ + lastUpdateUsage: Date.now(), + })); + const usage = await requestUsage(); if (usage) { diff --git a/app/styles/globals.scss b/app/styles/globals.scss index cf36ee92..37c66228 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -140,6 +140,7 @@ label { input { text-align: center; + font-family: inherit; } input[type="checkbox"] { @@ -224,6 +225,7 @@ input[type="password"] { color: var(--black); padding: 0 10px; max-width: 50%; + font-family: inherit; } div.math {