From 7a5c35baf3e5102c6cc9859589a10af6c911480c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=B3=E9=97=A8?= Date: Tue, 28 Mar 2023 18:48:03 +0800 Subject: [PATCH 01/21] fix: hide actions when loading --- app/components/home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 2526f2325..696061690 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -314,7 +314,7 @@ export function Chat(props: { showSideBar?: () => void }) { )}
- {!isUser && ( + {(!isUser && !message.preview) && (
{message.streaming ? (
Date: Tue, 28 Mar 2023 18:59:03 +0800 Subject: [PATCH 02/21] fix: hide actions when loading --- app/components/home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 696061690..7123d5d50 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -314,7 +314,7 @@ export function Chat(props: { showSideBar?: () => void }) {
)}
- {(!isUser && !message.preview) && ( + {(!isUser && !(message.preview || message.content.length === 0)) && (
{message.streaming ? (
Date: Tue, 28 Mar 2023 19:38:18 +0800 Subject: [PATCH 03/21] fix: minor fix --- app/components/home.module.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 730c05ef2..6fb44dea4 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -292,6 +292,7 @@ position: absolute; right: 20px; top: -26px; + left: 110px; transition: all ease 0.3s; opacity: 0; pointer-events: none; @@ -302,6 +303,7 @@ .chat-message-top-action { opacity: 0.5; color: var(--black); + white-space: nowrap; cursor: pointer; &:hover { From 1c017b8ee9119f0829fe59fc8ef0867d47ab21d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=B3=E9=97=A8?= Date: Tue, 28 Mar 2023 20:14:44 +0800 Subject: [PATCH 04/21] fix: minor fix --- app/components/home.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 6fb44dea4..8b26c6f74 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -292,7 +292,7 @@ position: absolute; right: 20px; top: -26px; - left: 110px; + left: 100px; transition: all ease 0.3s; opacity: 0; pointer-events: none; From 307be405ac2da05c3f0ac8da57d47d4107362a1e Mon Sep 17 00:00:00 2001 From: HimiCos Date: Tue, 28 Mar 2023 21:29:30 +0800 Subject: [PATCH 05/21] feat: language support traditional chinese --- app/components/settings.tsx | 4 ++ app/locales/cn.ts | 3 +- app/locales/en.ts | 3 +- app/locales/index.ts | 9 ++- app/locales/tw.ts | 120 ++++++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 app/locales/tw.ts diff --git a/app/components/settings.tsx b/app/components/settings.tsx index eb9bc6d49..c68d5aa4b 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -220,6 +220,10 @@ export function Settings(props: { closeSettings: () => void }) { + +
diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 32185f3b3..bed6d22c5 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -45,8 +45,9 @@ const cn = { Lang: { Name: "Language", Options: { - cn: "中文", + cn: "简体中文", en: "English", + tw: "繁體中文", }, }, Avatar: "头像", diff --git a/app/locales/en.ts b/app/locales/en.ts index 5401cda41..81f1396eb 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -49,8 +49,9 @@ const en: LocaleType = { Lang: { Name: "语言", Options: { - cn: "中文", + cn: "简体中文", en: "English", + tw: "繁體中文", }, }, Avatar: "Avatar", diff --git a/app/locales/index.ts b/app/locales/index.ts index e0a0921cf..bb6b0dec9 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -1,9 +1,10 @@ import CN from './cn' import EN from './en' +import TW from './tw' export type { LocaleType } from './cn' -type Lang = 'en' | 'cn' +type Lang = 'en' | 'cn' | 'tw' const LANG_KEY = 'lang' @@ -32,7 +33,7 @@ function getLanguage() { export function getLang(): Lang { const savedLang = getItem(LANG_KEY) - if (['en', 'cn'].includes(savedLang ?? '')) { + if (['en', 'cn', 'tw'].includes(savedLang ?? '')) { return savedLang as Lang } @@ -40,6 +41,8 @@ export function getLang(): Lang { if (lang.includes('zh') || lang.includes('cn')) { return 'cn' + } else if (lang.includes('tw')) { + return 'tw' } else { return 'en' } @@ -50,4 +53,4 @@ export function changeLang(lang: Lang) { location.reload() } -export default { en: EN, cn: CN }[getLang()] \ No newline at end of file +export default { en: EN, cn: CN, tw: TW }[getLang()] \ No newline at end of file diff --git a/app/locales/tw.ts b/app/locales/tw.ts new file mode 100644 index 000000000..514c31ad2 --- /dev/null +++ b/app/locales/tw.ts @@ -0,0 +1,120 @@ +import type { LocaleType } from "./index"; + +const tw: LocaleType = { + WIP: "該功能仍在開發中……", + Error: { + Unauthorized: "現在是未授權狀態,請在設置頁填寫授權碼。", + }, + ChatItem: { + ChatItemCount: (count: number) => `${count} 條對話`, + }, + Chat: { + SubTitle: (count: number) => `與 ChatGPT 的 ${count} 條對話`, + Actions: { + ChatList: "查看消息列表", + CompressedHistory: "查看壓縮後的歷史 Prompt", + Export: "導出聊天記錄", + Copy: "複製", + Stop: "停止", + Retry: "重試", + }, + Typing: "正在輸入…", + Input: (submitKey: string) => `輸入消息,${submitKey} 發送`, + Send: "發送", + }, + Export: { + Title: "導出聊天記錄為 Markdown", + Copy: "全部複製", + Download: "下載文件", + }, + Memory: { + Title: "上下文記憶 Prompt", + EmptyContent: "尚未記憶", + Copy: "全部複製", + }, + Home: { + NewChat: "新的聊天", + DeleteChat: "確認刪除選中的對話?", + }, + Settings: { + Title: "設置", + SubTitle: "設置選項", + Actions: { + ClearAll: "清除所有數據", + ResetAll: "重置所有選項", + Close: "關閉", + }, + Lang: { + Name: "語言", + Options: { + cn: "简体中文", + en: "English", + tw: "繁體中文", + }, + }, + Avatar: "頭像", + Update: { + Version: (x: string) => `當前版本:${x}`, + IsLatest: "已是最新版本", + CheckUpdate: "檢查更新", + IsChecking: "正在檢查更新...", + FoundUpdate: (x: string) => `發現新版本:${x}`, + GoToUpdate: "前往更新", + }, + SendKey: "發送鍵", + Theme: "主題", + TightBorder: "緊湊邊框", + HistoryCount: { + Title: "附帶歷史消息數", + SubTitle: "每次請求攜帶的歷史消息數", + }, + CompressThreshold: { + Title: "歷史消息長度壓縮閾值", + SubTitle: "當未壓縮的歷史消息超過該值時,將進行壓縮", + }, + Token: { + Title: "API Key", + SubTitle: "使用自己的 Key 可繞過受控訪問限制", + Placeholder: "OpenAI API Key", + }, + AccessCode: { + Title: "訪問碼", + SubTitle: "現在是受控訪問狀態", + Placeholder: "請輸入訪問碼", + }, + Model: "模型 (model)", + Temperature: { + Title: "隨機性 (temperature)", + SubTitle: "值越大,回復越隨機", + }, + MaxTokens: { + Title: "單次回復限制 (max_tokens)", + SubTitle: "單次交互所用的最大 Token 數", + }, + PresencePenlty: { + Title: "話題新鮮度 (presence_penalty)", + SubTitle: "值越大,越有可能擴展到新話題", + }, + }, + Store: { + DefaultTopic: "新的聊天", + BotHello: "有什麼可以幫你的嗎", + Error: "出錯了,稍後重試吧", + Prompt: { + History: (content: string) => + "這是 ai 和用戶的歷史聊天總結作為前情提要:" + content, + Topic: + "直接返回這句話的簡要主題,不要解釋,如果沒有主題,請直接返回「閒聊」", + Summarize: + "簡要總結一下你和用戶的對話,用作後續的上下文提示 prompt,控制在 50 字以內", + }, + ConfirmClearAll: "確認清除所有聊天、設置數據?", + }, + Copy: { + Success: "已寫入剪切板", + Failed: "複製失敗,請賦予剪切板權限", + }, +}; + +export default tw; + From 19b511e3f8ee677a604ae5c90ab3553a47f630e7 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Tue, 28 Mar 2023 23:13:30 +0800 Subject: [PATCH 06/21] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a0d4e68a0..c0f6d9dd2 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ If you would like to contribute your API key, you can email it to the author and [@mushan0x0](https://github.com/mushan0x0) [@ClarenceDan](https://github.com/ClarenceDan) [@zhangjia](https://github.com/zhangjia) +[@hoochanlon](https://github.com/hoochanlon) ### 贡献者 Contributor From 7d5e742ea61be97da4d4cff9ca69528ee171d216 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 27 Mar 2023 16:30:12 +0000 Subject: [PATCH 07/21] feat: #2 add prompt list --- .gitignore | 4 +- README.md | 2 + app/store/prompt.ts | 96 +++++++++++++++++++++++++++++++++++++++ package.json | 10 ++-- scripts/fetch-prompts.mjs | 49 ++++++++++++++++++++ yarn.lock | 49 ++++++++++++++++++++ 6 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 app/store/prompt.ts create mode 100644 scripts/fetch-prompts.mjs diff --git a/.gitignore b/.gitignore index 354be805d..0a3e52afa 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts -dev \ No newline at end of file +dev + +public/prompts.json \ No newline at end of file diff --git a/README.md b/README.md index c0f6d9dd2..dc1e7ca9a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ One-Click to deploy your own ChatGPT web UI. - 在 1 分钟内使用 Vercel **免费一键部署** - 精心设计的 UI,响应式设计,支持深色模式 - 极快的首屏加载速度(~85kb) +- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) - 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话 - 一键导出聊天记录,完整的 Markdown 支持 - 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问 @@ -31,6 +32,7 @@ One-Click to deploy your own ChatGPT web UI. - **Deploy for free with one-click** on Vercel in under 1 minute - Responsive design, and dark mode - Fast first screen loading speed (~85kb) +- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) - Automatically compresses chat history to support long conversations while also saving your tokens - One-click export all chat history with full Markdown support diff --git a/app/store/prompt.ts b/app/store/prompt.ts new file mode 100644 index 000000000..ffd6a74e3 --- /dev/null +++ b/app/store/prompt.ts @@ -0,0 +1,96 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import JsSearch from "js-search"; + +export interface Prompt { + id?: number; + shortcut: string; + title: string; + content: string; +} + +export interface PromptStore { + latestId: number; + prompts: Map; + + add: (prompt: Prompt) => number; + remove: (id: number) => void; + search: (text: string) => Prompt[]; +} + +export const PROMPT_KEY = "prompt-store"; + +export const SearchService = { + ready: false, + progress: 0, // 0 - 1, 1 means ready + engine: new JsSearch.Search("prompts"), + deleted: new Set(), + + async init(prompts: PromptStore["prompts"]) { + this.engine.addIndex("id"); + this.engine.addIndex("shortcut"); + this.engine.addIndex("title"); + + const n = prompts.size; + let count = 0; + for await (const prompt of prompts.values()) { + this.engine.addDocument(prompt); + count += 1; + this.progress = count / n; + } + this.ready = true; + }, + + remove(id: number) { + this.deleted.add(id); + }, + + add(prompt: Prompt) { + this.engine.addDocument(prompt); + }, + + search(text: string) { + const results = this.engine.search(text) as Prompt[]; + return results.filter((v) => !v.id || !this.deleted.has(v.id)); + }, +}; + +export const usePromptStore = create()( + persist( + (set, get) => ({ + latestId: 0, + prompts: new Map(), + + add(prompt) { + const prompts = get().prompts; + prompt.id = get().latestId + 1; + prompts.set(prompt.id, prompt); + + set(() => ({ + latestId: prompt.id!, + prompts: prompts, + })); + + return prompt.id!; + }, + + remove(id) { + const prompts = get().prompts; + prompts.delete(id); + SearchService.remove(id); + + set(() => ({ + prompts, + })); + }, + + search(text) { + return SearchService.search(text) as Prompt[]; + }, + }), + { + name: PROMPT_KEY, + version: 1, + } + ) +); diff --git a/package.json b/package.json index b7d1d6995..8cf529903 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,15 @@ "private": false, "license": "Anti 996", "scripts": { - "dev": "next dev", - "build": "next build", + "dev": "yarn fetch && next dev", + "build": "yarn fetch && next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "fetch": "node ./scripts/fetch-prompts.mjs" }, "dependencies": { "@svgr/webpack": "^6.5.1", + "@types/js-search": "^1.4.0", "@types/node": "^18.14.6", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", @@ -22,7 +24,9 @@ "eslint": "8.35.0", "eslint-config-next": "13.2.3", "eventsource-parser": "^0.1.0", + "js-search": "^2.0.0", "next": "^13.2.3", + "node-fetch": "^3.3.1", "openai": "^3.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/scripts/fetch-prompts.mjs b/scripts/fetch-prompts.mjs new file mode 100644 index 000000000..daebfe7a6 --- /dev/null +++ b/scripts/fetch-prompts.mjs @@ -0,0 +1,49 @@ +import fetch from "node-fetch"; +import fs from "fs/promises"; + +const CN_URL = + "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json"; +const EN_URL = + "https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv"; +const FILE = "./public/prompts.json"; + +async function fetchCN() { + console.log("[Fetch] fetching cn prompts..."); + try { + const raw = await (await fetch(CN_URL)).json(); + return raw.map((v) => [v.act, v.prompt]); + } catch (error) { + console.error("[Fetch] failed to fetch cn prompts", error); + return []; + } +} + +async function fetchEN() { + console.log("[Fetch] fetching en prompts..."); + try { + const raw = await (await fetch(EN_URL)).text(); + return raw + .split("\n") + .slice(1) + .map((v) => v.split('","').map((v) => v.replace('"', ""))); + } catch (error) { + console.error("[Fetch] failed to fetch cn prompts", error); + return []; + } +} + +async function main() { + Promise.all([fetchCN(), fetchEN()]) + .then(([cn, en]) => { + fs.writeFile(FILE, JSON.stringify({ cn, en })); + }) + .catch((e) => { + console.error("[Fetch] failed to fetch prompts"); + fs.writeFile(FILE, JSON.stringify({ cn: [], en: [] })); + }) + .finally(() => { + console.log("[Fetch] saved to " + FILE); + }); +} + +main(); diff --git a/yarn.lock b/yarn.lock index 7e69c5da5..7a8baf25a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1320,6 +1320,11 @@ dependencies: "@types/unist" "*" +"@types/js-search@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@types/js-search/-/js-search-1.4.0.tgz#f2d4afa176a4fc7b17fb46a1593847887fa1fb7b" + integrity sha512-OMDWvQP2AmxpQI9vFh7U/TzExNGB9Sj9WQCoxUR8VXZEv6jM4cyNzLODkh1gkBHJ9Er7kdasChzEpba4FxLGaA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -1879,6 +1884,11 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + debug@^3.2.7: version "3.2.7" resolved "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2413,6 +2423,14 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2469,6 +2487,13 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -3017,6 +3042,11 @@ js-sdsl@^4.1.4: resolved "https://registry.npmmirror.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== +js-search@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/js-search/-/js-search-2.0.0.tgz#84dc9d44e34ca0870d067e04b86d8038b77edc26" + integrity sha512-lJ8KzjlwcelIWuAdKyzsXv45W6OIwRpayzc7XmU8mzgWadg5UVOKVmnq/tXudddEB9Ceic3tVaGu6QOK/eebhg== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3693,6 +3723,20 @@ next@^13.2.3: "@next/swc-win32-ia32-msvc" "13.2.4" "@next/swc-win32-x64-msvc" "13.2.4" +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.1.tgz#b3eea7b54b3a48020e46f4f88b9c5a7430d20b2e" + integrity sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-releases@^2.0.8: version "2.0.10" resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" @@ -4662,6 +4706,11 @@ web-namespaces@^2.0.0: resolved "https://registry.npmmirror.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" From 6782e65fdf6ea7f79ead3c6907eacf110d097402 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Tue, 28 Mar 2023 17:30:11 +0000 Subject: [PATCH 08/21] feat: #2 add prompt hints --- app/components/home.module.scss | 58 ++++++++++++++++++++++- app/components/home.tsx | 82 +++++++++++++++++++++++++++++---- app/components/settings.tsx | 41 ++++++++++++++++- app/components/ui-lib.tsx | 2 +- app/icons/edit.svg | 1 + app/locales/cn.ts | 10 ++++ app/store/app.ts | 4 ++ app/store/prompt.ts | 63 +++++++++++++++++-------- package.json | 4 +- yarn.lock | 20 ++++---- 10 files changed, 238 insertions(+), 47 deletions(-) create mode 100644 app/icons/edit.svg diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 730c05ef2..462b19ce5 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -333,11 +333,65 @@ .chat-input-panel { position: absolute; - bottom: 20px; + bottom: 0px; display: flex; width: 100%; padding: 20px; box-sizing: border-box; + flex-direction: column; +} + +@mixin single-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.prompt-hints { + min-height: 20px; + width: 100%; + max-height: 50vh; + overflow: auto; + display: flex; + flex-direction: column-reverse; + + background-color: var(--white); + border: var(--border-in-light); + border-radius: 10px; + margin-bottom: 10px; + box-shadow: var(--shadow); + + .prompt-hint { + color: var(--black); + padding: 6px 10px; + animation: slide-in ease 0.3s; + cursor: pointer; + transition: all ease 0.3s; + border: transparent 1px solid; + margin: 4px; + border-radius: 8px; + + &:not(:last-child) { + margin-top: 0; + } + + .hint-title { + font-size: 12px; + font-weight: bolder; + + @include single-line(); + } + .hint-content { + font-size: 12px; + + @include single-line(); + } + + &-selected, + &:hover { + border-color: var(--primary); + } + } } .chat-input-panel-inner { @@ -375,7 +429,7 @@ position: absolute; right: 30px; - bottom: 10px; + bottom: 30px; } .export-content { diff --git a/app/components/home.tsx b/app/components/home.tsx index 850c51774..1a84d7274 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useRef, useEffect, useLayoutEffect } from "react"; +import { useDebouncedCallback } from "use-debounce"; import { IconButton } from "./button"; import styles from "./home.module.scss"; @@ -28,6 +29,7 @@ import Locale from "../locales"; import dynamic from "next/dynamic"; import { REPO_URL } from "../constant"; import { ControllerPool } from "../requests"; +import { Prompt, usePromptStore } from "../store/prompt"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -146,24 +148,77 @@ function useSubmitHandler() { }; } +export function PromptHints(props: { + prompts: Prompt[]; + onPromptSelect: (prompt: Prompt) => void; +}) { + if (props.prompts.length === 0) return null; + + return ( +
+ {props.prompts.map((prompt, i) => ( +
props.onPromptSelect(prompt)} + > +
{prompt.title}
+
{prompt.content}
+
+ ))} +
+ ); +} + export function Chat(props: { showSideBar?: () => void }) { type RenderMessage = Message & { preview?: boolean }; + const chatStore = useChatStore(); const [session, sessionIndex] = useChatStore((state) => [ state.currentSession(), state.currentSessionIndex, ]); + + const inputRef = useRef(null); const [userInput, setUserInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const { submitKey, shouldSubmit } = useSubmitHandler(); - const onUserInput = useChatStore((state) => state.onUserInput); + // prompt hints + const promptStore = usePromptStore(); + const [promptHints, setPromptHints] = useState([]); + const onSearch = useDebouncedCallback( + (text: string) => { + if (chatStore.config.disablePromptHint) return; + setPromptHints(promptStore.search(text)); + }, + 100, + { leading: true, trailing: true } + ); + + const onPromptSelect = (prompt: Prompt) => { + setUserInput(prompt.content); + setPromptHints([]); + inputRef.current?.focus(); + }; + + // only search prompts when user input is short + const SEARCH_TEXT_LIMIT = 10; + const onInput = (text: string) => { + setUserInput(text); + const n = text.trim().length; + if (n === 0 || n > SEARCH_TEXT_LIMIT) { + setPromptHints([]); + } else { + onSearch(text); + } + }; // submit user input const onUserSubmit = () => { if (userInput.length <= 0) return; setIsLoading(true); - onUserInput(userInput).then(() => setIsLoading(false)); + chatStore.onUserInput(userInput).then(() => setIsLoading(false)); setUserInput(""); inputRef.current?.focus(); }; @@ -198,7 +253,9 @@ export function Chat(props: { showSideBar?: () => void }) { for (let i = botIndex; i >= 0; i -= 1) { if (messages[i].role === "user") { setIsLoading(true); - onUserInput(messages[i].content).then(() => setIsLoading(false)); + chatStore + .onUserInput(messages[i].content) + .then(() => setIsLoading(false)); return; } } @@ -206,7 +263,6 @@ export function Chat(props: { showSideBar?: () => void }) { // for auto-scroll const latestMessageRef = useRef(null); - const inputRef = useRef(null); // wont scroll while hovering messages const [autoScroll, setAutoScroll] = useState(false); @@ -373,17 +429,21 @@ export function Chat(props: { showSideBar?: () => void }) {
+