From 1db210097c431fa460aea5b8a1bb697fb0f2db6d Mon Sep 17 00:00:00 2001 From: RugerMc <550279039@qq.com> Date: Fri, 31 Mar 2023 13:16:12 +0800 Subject: [PATCH 01/42] feat: add switch of send preview bubble --- app/components/home.tsx | 27 ++++++++++++++------------- app/components/settings.tsx | 12 ++++++++++++ app/locales/cn.ts | 1 + app/locales/en.ts | 1 + app/locales/tw.ts | 1 + app/store/app.ts | 2 ++ 6 files changed, 31 insertions(+), 13 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 2f09aa27..2da10196 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -292,6 +292,8 @@ export function Chat(props: { const latestMessageRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); + const config = useChatStore((state) => state.config); + // preview messages const messages = (session.messages as RenderMessage[]) .concat( @@ -305,19 +307,18 @@ export function Chat(props: { }, ] : [], - ) - .concat( - userInput.length > 0 - ? [ - { - role: "user", - content: userInput, - date: new Date().toLocaleString(), - preview: true, - }, - ] - : [], - ); + ).concat( + userInput.length > 0 && config.sendPreviewBubble + ? [ + { + role: "user", + content: userInput, + date: new Date().toLocaleString(), + preview: false, + }, + ] + : [], + ); // auto scroll useLayoutEffect(() => { diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 711cb954..06ff76e1 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -278,6 +278,18 @@ export function Settings(props: { closeSettings: () => void }) { } > + + + + updateConfig( + (config) => (config.sendPreviewBubble = e.currentTarget.checked), + ) + } + > + Date: Fri, 31 Mar 2023 18:33:26 +0800 Subject: [PATCH 02/42] fix: #277 no cache for credit query --- app/requests.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/requests.ts b/app/requests.ts index d173eb0d..f4db7a1b 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -9,7 +9,7 @@ const makeRequestParam = ( options?: { filterBot?: boolean; stream?: boolean; - }, + } ): ChatRequest => { let sendMessages = messages.map((v) => ({ role: v.role, @@ -69,10 +69,9 @@ export async function requestChat(messages: Message[]) { } export async function requestUsage() { - const res = await requestOpenaiClient("dashboard/billing/credit_grants")( - null, - "GET", - ); + const res = await requestOpenaiClient( + "dashboard/billing/credit_grants?_vercel_no_cache=1" + )(null, "GET"); try { const response = (await res.json()) as { @@ -94,7 +93,7 @@ export async function requestChatStream( onMessage: (message: string, done: boolean) => void; onError: (error: Error) => void; onController?: (controller: AbortController) => void; - }, + } ) { const req = makeRequestParam(messages, { stream: true, @@ -189,7 +188,7 @@ export const ControllerPool = { addController( sessionIndex: number, messageIndex: number, - controller: AbortController, + controller: AbortController ) { const key = this.key(sessionIndex, messageIndex); this.controllers[key] = controller; From 5f7a264e52d8369df89842c3c362ff9e338216bf Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Fri, 31 Mar 2023 19:21:11 +0800 Subject: [PATCH 03/42] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=9C=A8?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E6=B5=8F=E8=A7=88=E5=99=A8=E9=AB=98=E5=BA=A6?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/home.module.scss | 4 ---- app/styles/globals.scss | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/components/home.module.scss b/app/components/home.module.scss index cef1662b..764805d8 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -218,7 +218,6 @@ flex: 1; overflow: auto; padding: 20px; - margin-bottom: 100px; } .chat-body-title { @@ -342,9 +341,6 @@ } .chat-input-panel { - position: absolute; - bottom: 0px; - display: flex; width: 100%; padding: 20px; box-sizing: border-box; diff --git a/app/styles/globals.scss b/app/styles/globals.scss index c514274a..6637016a 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -53,7 +53,7 @@ --sidebar-width: 300px; --window-content-width: calc(100% - var(--sidebar-width)); --message-max-width: 80%; - --full-height: 100vh; + --full-height: 100%; } @media only screen and (max-width: 600px) { @@ -75,6 +75,9 @@ @include dark; } } +html { + height: var(--full-height); +} body { background-color: var(--gray); From 407c9fc9c35392023d82e807792d085a68b71b20 Mon Sep 17 00:00:00 2001 From: hibobmaster <32976627+hibobmaster@users.noreply.github.com> Date: Fri, 31 Mar 2023 23:03:57 +0800 Subject: [PATCH 04/42] Update docker.yml --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 197e2f0d..a7a29644 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -28,7 +28,7 @@ jobs: images: yidadaa/chatgpt-next-web tags: | type=raw,value=latest - type=semver,pattern={{version}} + type=ref,event=tag - name: Set up QEMU From 4dc1e025e1eba7eb2dd9153897774ea7dd44eb8c Mon Sep 17 00:00:00 2001 From: leedom Date: Sat, 1 Apr 2023 10:24:06 +0800 Subject: [PATCH 05/42] feat: add confirm tips when deleting conversation on pc --- 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 2f09aa27..706156af 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -121,7 +121,7 @@ export function ChatList() { key={i} selected={i === selectedIndex} onClick={() => selectSession(i)} - onDelete={() => removeSession(i)} + onDelete={() => confirm(Locale.Home.DeleteChat) && removeSession(i)} /> ))} From 45bf2c3d2590b7c6ae43ebeaaffd13e4c489ca72 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sat, 1 Apr 2023 15:39:30 +0800 Subject: [PATCH 06/42] fix: remove scroll anchor height --- app/components/home.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 706156af..6f1d77cd 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -108,7 +108,7 @@ export function ChatList() { state.currentSessionIndex, state.selectSession, state.removeSession, - ], + ] ); return ( @@ -202,7 +202,7 @@ export function Chat(props: { setPromptHints(promptStore.search(text)); }, 100, - { leading: true, trailing: true }, + { leading: true, trailing: true } ); const onPromptSelect = (prompt: Prompt) => { @@ -216,7 +216,7 @@ export function Chat(props: { if (!dom) return; const paddingBottomNum: number = parseInt( window.getComputedStyle(dom).paddingBottom, - 10, + 10 ); dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum; }; @@ -304,7 +304,7 @@ export function Chat(props: { preview: true, }, ] - : [], + : [] ) .concat( userInput.length > 0 @@ -316,7 +316,7 @@ export function Chat(props: { preview: true, }, ] - : [], + : [] ); // auto scroll @@ -354,7 +354,7 @@ export function Chat(props: { const newTopic = prompt(Locale.Chat.Rename, session.topic); if (newTopic && newTopic !== session.topic) { chatStore.updateCurrentSession( - (session) => (session.topic = newTopic!), + (session) => (session.topic = newTopic!) ); } }} @@ -470,7 +470,7 @@ export function Chat(props: { ); })} -
+
-
@@ -600,7 +600,7 @@ export function Home() { state.newSession, state.currentSessionIndex, state.removeSession, - ], + ] ); const loading = !useHasHydrated(); const [showSideBar, setShowSideBar] = useState(true); From 0385f6ede919117e7278cd64fe01f7d688805059 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sat, 1 Apr 2023 15:46:34 +0800 Subject: [PATCH 07/42] fix: #305 disable double click to copy on pc --- app/components/home.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 6f1d77cd..de93510d 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -453,7 +453,10 @@ export function Chat(props: { className="markdown-body" style={{ fontSize: `${fontSize}px` }} onContextMenu={(e) => onRightClick(e, message)} - onDoubleClickCapture={() => setUserInput(message.content)} + onDoubleClickCapture={() => { + if (!isMobileScreen()) return; + setUserInput(message.content); + }} > From 83cea2adb842c23c3d817cfcc6d938795dfcd315 Mon Sep 17 00:00:00 2001 From: Jun Wu Date: Sat, 1 Apr 2023 02:34:33 -0700 Subject: [PATCH 08/42] api: set Content-Type to json This avoids issues in browsers like WeChat where the encoding is incorrect and the summary feature does not work if it contains zh-CN characters. --- app/api/openai/route.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/api/openai/route.ts b/app/api/openai/route.ts index 5bc317e5..5ddb0f4c 100644 --- a/app/api/openai/route.ts +++ b/app/api/openai/route.ts @@ -3,8 +3,10 @@ import { requestOpenai } from "../common"; async function makeRequest(req: NextRequest) { try { - const res = await requestOpenai(req); - return new Response(res.body); + const api = await requestOpenai(req); + const res = new NextResponse(api.body); + res.headers.set('Content-Type', 'application/json'); + return res; } catch (e) { console.error("[OpenAI] ", req.body, e); return NextResponse.json( From 327ac765df9413da68c1407e88050c1d2c4b351b Mon Sep 17 00:00:00 2001 From: Jun Wu Date: Sat, 1 Apr 2023 03:28:29 -0700 Subject: [PATCH 09/42] utils: simplify trimTopic Also avoid using Array.prototype.at, which does not seem to exist in the Wexin builtin webview (Android Wexin 8.0.30). --- app/utils.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index 64120df4..1fb3d316 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -2,15 +2,7 @@ import { showToast } from "./components/ui-lib"; import Locale from "./locales"; export function trimTopic(topic: string) { - const s = topic.split(""); - let lastChar = s.at(-1); // 获取 s 的最后一个字符 - let pattern = /[,。!?、,.!?]/; // 定义匹配中文和英文标点符号的正则表达式 - while (lastChar && pattern.test(lastChar!)) { - s.pop(); - lastChar = s.at(-1); - } - - return s.join(""); + return topic.replace(/[,。!?、,.!?]*$/, ""); } export function copyToClipboard(text: string) { From ad63b10aead9445dc864203505efb7620957d563 Mon Sep 17 00:00:00 2001 From: Yorun <547747006@qq.com> Date: Sat, 1 Apr 2023 11:52:10 +0000 Subject: [PATCH 10/42] ci: update sync action --- .github/workflows/sync.yml | 39 +++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 9914a603..1c9dc413 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -1,16 +1,29 @@ -# .github/workflows/sync.yml -name: Sync Fork +name: Upstream Sync on: - schedule: - - cron: "0 8 * * *" # 每天0点触发 + schedule: + - cron: '0 */12 * * *' # every 12 hours + workflow_dispatch: # on button click + jobs: - repo-sync: - runs-on: ubuntu-latest - steps: - - uses: TG908/fork-sync@v1.1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} # 这个 token action 会默认配置, 这里只需这样写就行 - owner: Yidadaa # fork 上游项目 owner - head: main # fork 上游项目需要同步的分支 - base: main # 需要同步到本项目的目标分支 + sync_latest_from_upstream: + name: Sync latest commits from upstream repo + runs-on: ubuntu-latest + + steps: + # Step 1: run a standard checkout action, provided by github + - name: Checkout target repo + uses: actions/checkout@v3 + + # Step 2: run the sync action + - name: Sync upstream changes + id: sync + uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 + with: + upstream_sync_repo: Yidadaa/ChatGPT-Next-Web + upstream_sync_branch: main + target_sync_branch: main + target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set + + # Set test_mode true to run tests instead of the true action!! + test_mode: false From 00a282214e60ad29a3041fc35fa2196b84751d7b Mon Sep 17 00:00:00 2001 From: linqirong <609413692@qq.com> Date: Sun, 2 Apr 2023 00:12:31 +0800 Subject: [PATCH 11/42] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E6=B3=95=E4=B8=8Benter=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=8F=91=E9=80=81=E6=B6=88=E6=81=AF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/home.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index de93510d..210e4d74 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -132,9 +132,9 @@ function useSubmitHandler() { const config = useChatStore((state) => state.config); const submitKey = config.submitKey; - const shouldSubmit = (e: KeyboardEvent) => { + const shouldSubmit = (e: React.KeyboardEvent) => { if (e.key !== "Enter") return false; - + if(e.key==='Enter' && e.nativeEvent.isComposing) return false return ( (config.submitKey === SubmitKey.AltEnter && e.altKey) || (config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) || @@ -256,7 +256,7 @@ export function Chat(props: { }; // check if should send message - const onInputKeyDown = (e: KeyboardEvent) => { + const onInputKeyDown = (e: React.KeyboardEvent) => { if (shouldSubmit(e)) { onUserSubmit(); e.preventDefault(); @@ -488,7 +488,7 @@ export function Chat(props: { rows={4} onInput={(e) => onInput(e.currentTarget.value)} value={userInput} - onKeyDown={(e) => onInputKeyDown(e as any)} + onKeyDown={onInputKeyDown} onFocus={() => setAutoScroll(true)} onBlur={() => { setAutoScroll(false); From cd5f8f74070e4e95b2eaff3f87051649f98d33d8 Mon Sep 17 00:00:00 2001 From: Jun Wu Date: Sat, 1 Apr 2023 11:38:52 -0700 Subject: [PATCH 12/42] app: polyfill Array.at This fixes compatibility issue with older browsers like WeChat webview. The summary feature now works as expected. --- app/requests.ts | 4 ++++ app/store/app.ts | 4 ++++ package.json | 3 ++- yarn.lock | 10 ++++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/requests.ts b/app/requests.ts index f4db7a1b..56fd6cb5 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -2,6 +2,10 @@ import type { ChatRequest, ChatReponse } from "./api/openai/typing"; import { filterConfig, Message, ModelConfig, useAccessStore } from "./store"; import Locale from "./locales"; +if (!Array.prototype.at) { + require('array.prototype.at/auto'); +} + const TIME_OUT_MS = 30000; const makeRequestParam = ( diff --git a/app/store/app.ts b/app/store/app.ts index 6ab3229a..cc52a3c7 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -11,6 +11,10 @@ import { trimTopic } from "../utils"; import Locale from "../locales"; +if (!Array.prototype.at) { + require('array.prototype.at/auto'); +} + export type Message = ChatCompletionResponseMessage & { date: string; streaming?: boolean; diff --git a/package.json b/package.json index eb17000e..7c6832ed 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^8.0.5", - "remark-breaks": "^3.0.2", "rehype-katex": "^6.0.2", "rehype-prism-plus": "^1.5.1", + "remark-breaks": "^3.0.2", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", "sass": "^1.59.2", @@ -39,6 +39,7 @@ "@types/react-dom": "^18.0.11", "@types/react-katex": "^3.0.0", "@types/spark-md5": "^3.0.2", + "array.prototype.at": "^1.1.1", "cross-env": "^7.0.3", "eslint": "^8.36.0", "eslint-config-next": "13.2.3", diff --git a/yarn.lock b/yarn.lock index 9f98a244..246b818b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1570,6 +1570,16 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +array.prototype.at@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array.prototype.at/-/array.prototype.at-1.1.1.tgz#6deda3cd3c704afa16361387ea344e0b8d8831b5" + integrity sha512-n/wYNLJy/fVEU9EGPt2ww920hy1XX3XB2yTREFy1QsxctBgQV/tZIwg1G8jVxELna4pLCzg/xvvS/DDXtI4NNg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + array.prototype.flat@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" From 506cdbc83c83feeabf6c427418ce04916bd3a8d6 Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Sun, 2 Apr 2023 13:42:47 +0800 Subject: [PATCH 13/42] feat: clear session only --- app/components/settings.tsx | 10 +++++----- app/store/app.ts | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index eb9bc6d4..c2bdb86e 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -46,14 +46,14 @@ function SettingItem(props: { export function Settings(props: { closeSettings: () => void }) { const [showEmojiPicker, setShowEmojiPicker] = useState(false); - const [config, updateConfig, resetConfig, clearAllData] = useChatStore( - (state) => [ + const [config, updateConfig, resetConfig, clearAllData, clearSessions] = + useChatStore((state) => [ state.config, state.updateConfig, state.resetConfig, state.clearAllData, - ] - ); + state.clearSessions, + ]); const updateStore = useUpdateStore(); const [checkingUpdate, setCheckingUpdate] = useState(false); @@ -93,7 +93,7 @@ export function Settings(props: { closeSettings: () => void }) {
} - onClick={clearAllData} + onClick={clearSessions} bordered title={Locale.Settings.Actions.ClearAll} /> diff --git a/app/store/app.ts b/app/store/app.ts index 703078ad..46e46d52 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -177,6 +177,7 @@ interface ChatStore { config: ChatConfig; sessions: ChatSession[]; currentSessionIndex: number; + clearSessions: () => void; removeSession: (index: number) => void; selectSession: (index: number) => void; newSession: () => void; @@ -211,6 +212,13 @@ export const useChatStore = create()( ...DEFAULT_CONFIG, }, + clearSessions(){ + set(() => ({ + sessions: [createEmptySession()], + currentSessionIndex: 0, + })); + }, + resetConfig() { set(() => ({ config: { ...DEFAULT_CONFIG } })); }, From ed5cd11d6ad984dcd78b1a65340b01dc7bc1fda4 Mon Sep 17 00:00:00 2001 From: Sad Pencil Date: Sun, 2 Apr 2023 14:23:12 +0800 Subject: [PATCH 14/42] Fix typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d026e61a..40f7fc74 100644 --- a/README.md +++ b/README.md @@ -43,14 +43,14 @@ One-Click to deploy your own ChatGPT web UI. - Plugins: support network search, caculator, any other apis etc. 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) ### 不会开发的功能 Not in Plan -- User login, accounts, cloud sync 用户登陆、账号管理、消息云同步 +- User login, accounts, cloud sync 用户登录、账号管理、消息云同步 - UI text customize 界面文字自定义 ## 开始使用 1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys); 2. 点击右侧按钮开始部署: - [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登陆即可,记得在环境变量页填入 API Key; + [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key; 3. 部署完毕后,即可开始使用; 4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。 From fea4f561b4c175c6f5c1fcc842e31a475132591b Mon Sep 17 00:00:00 2001 From: Cesaryuan <35998162+cesaryuan@users.noreply.github.com> Date: Sun, 2 Apr 2023 19:43:11 +0800 Subject: [PATCH 15/42] fix: fix history message count Bug: The length of `new Array(20).slice(20 - 24) ` is 4 which should be 24. --- app/store/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/app.ts b/app/store/app.ts index ec0c8c50..d6fd140f 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -384,7 +384,7 @@ export const useChatStore = create()( const config = get().config; const n = session.messages.length; const recentMessages = session.messages.slice( - n - config.historyMessageCount, + - config.historyMessageCount, ); const memoryPrompt = get().getMemoryPrompt(); From 12f342f01589a1a458d16601c47d617ebe124659 Mon Sep 17 00:00:00 2001 From: Cesaryuan Date: Sun, 2 Apr 2023 20:23:56 +0800 Subject: [PATCH 16/42] fix: historyMessageCount --- app/store/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/app.ts b/app/store/app.ts index d6fd140f..b0fcbe91 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -384,7 +384,7 @@ export const useChatStore = create()( const config = get().config; const n = session.messages.length; const recentMessages = session.messages.slice( - - config.historyMessageCount, + Math.max(0, n - config.historyMessageCount), ); const memoryPrompt = get().getMemoryPrompt(); From 16028795f91bb65c84362475b977271ac0df3243 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sun, 2 Apr 2023 12:28:18 +0000 Subject: [PATCH 17/42] fix: #203 pwa installation problem --- public/serviceWorker.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/serviceWorker.js b/public/serviceWorker.js index 028c79a8..f5a24b70 100644 --- a/public/serviceWorker.js +++ b/public/serviceWorker.js @@ -11,3 +11,5 @@ self.addEventListener("install", function (event) { }), ); }); + +self.addEventListener("fetch", (e) => {}); From a90e646381e91287ac355d952aaa3695317439ff Mon Sep 17 00:00:00 2001 From: MapleUncle Date: Sun, 2 Apr 2023 20:38:14 +0800 Subject: [PATCH 18/42] =?UTF-8?q?=F0=9F=90=9E=20fix(locales):=20Fix=20the?= =?UTF-8?q?=20missing=20SendPreviewBubble=20in=20ES=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/locales/es.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/locales/es.ts b/app/locales/es.ts index 1850e4cf..a78bf1aa 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -78,6 +78,7 @@ const es: LocaleType = { SendKey: "Tecla de envío", Theme: "Tema", TightBorder: "Borde ajustado", + SendPreviewBubble: "Send preview bubble", Prompt: { Disable: { Title: "Desactivar autocompletado", From 37587f6f717eb5092f1c5e5fb5eabedd40f12c94 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sun, 2 Apr 2023 13:56:34 +0000 Subject: [PATCH 19/42] fix: #244 optimize polyfill --- app/components/home.tsx | 33 ++++++++++++++++++++------------- app/page.tsx | 3 +++ app/requests.ts | 4 ---- app/store/app.ts | 17 ++++++----------- package.json | 2 +- 5 files changed, 30 insertions(+), 29 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 7ed35dfb..8e4013e2 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -21,7 +21,13 @@ import CloseIcon from "../icons/close.svg"; import CopyIcon from "../icons/copy.svg"; import DownloadIcon from "../icons/download.svg"; -import { Message, SubmitKey, useChatStore, ChatSession } from "../store"; +import { + Message, + SubmitKey, + useChatStore, + ChatSession, + BOT_HELLO, +} from "../store"; import { showModal, showToast } from "./ui-lib"; import { copyToClipboard, @@ -307,18 +313,19 @@ export function Chat(props: { }, ] : [], - ).concat( - userInput.length > 0 && config.sendPreviewBubble - ? [ - { - role: "user", - content: userInput, - date: new Date().toLocaleString(), - preview: false, - }, - ] - : [], - ); + ) + .concat( + userInput.length > 0 && config.sendPreviewBubble + ? [ + { + role: "user", + content: userInput, + date: new Date().toLocaleString(), + preview: false, + }, + ] + : [], + ); // auto scroll useLayoutEffect(() => { diff --git a/app/page.tsx b/app/page.tsx index 54300e71..2ad763ce 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,4 +1,7 @@ import { Analytics } from "@vercel/analytics/react"; + +import "array.prototype.at"; + import { Home } from "./components/home"; export default function App() { diff --git a/app/requests.ts b/app/requests.ts index a8ba4e9f..0be9dbf7 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -2,10 +2,6 @@ import type { ChatRequest, ChatReponse } from "./api/openai/typing"; import { filterConfig, Message, ModelConfig, useAccessStore } from "./store"; import Locale from "./locales"; -if (!Array.prototype.at) { - require("array.prototype.at/auto"); -} - const TIME_OUT_MS = 30000; const makeRequestParam = ( diff --git a/app/store/app.ts b/app/store/app.ts index e6327723..7c2b57f1 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -11,10 +11,6 @@ import { trimTopic } from "../utils"; import Locale from "../locales"; -if (!Array.prototype.at) { - require("array.prototype.at/auto"); -} - export type Message = ChatCompletionResponseMessage & { date: string; streaming?: boolean; @@ -162,6 +158,11 @@ export interface ChatSession { } const DEFAULT_TOPIC = Locale.Store.DefaultTopic; +export const BOT_HELLO = { + role: "assistant", + content: Locale.Store.BotHello, + date: "", +}; function createEmptySession(): ChatSession { const createDate = new Date().toLocaleString(); @@ -170,13 +171,7 @@ function createEmptySession(): ChatSession { id: Date.now(), topic: DEFAULT_TOPIC, memoryPrompt: "", - messages: [ - { - role: "assistant", - content: Locale.Store.BotHello, - date: createDate, - }, - ], + messages: [], stat: { tokenCount: 0, wordCount: 0, diff --git a/package.json b/package.json index 7c6832ed..b67d7b0d 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", + "array.prototype.at": "^1.1.1", "emoji-picker-react": "^4.4.7", "eventsource-parser": "^0.1.0", "fuse.js": "^6.6.2", @@ -39,7 +40,6 @@ "@types/react-dom": "^18.0.11", "@types/react-katex": "^3.0.0", "@types/spark-md5": "^3.0.2", - "array.prototype.at": "^1.1.1", "cross-env": "^7.0.3", "eslint": "^8.36.0", "eslint-config-next": "13.2.3", From 7b5af271d501b2c8d85f438dfa358913b8da81ac Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sun, 2 Apr 2023 14:22:06 +0000 Subject: [PATCH 20/42] fix: #367 failed to fetch account usage --- app/components/settings.tsx | 12 ++++-------- app/locales/cn.ts | 4 ++-- app/locales/en.ts | 4 ++-- app/locales/es.ts | 4 ++-- app/locales/tw.ts | 4 ++-- app/requests.ts | 19 ++++++++++++++----- 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 8f015006..43959698 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -72,7 +72,6 @@ export function Settings(props: { closeSettings: () => void }) { } const [usage, setUsage] = useState<{ - granted?: number; used?: number; }>(); const [loadingUsage, setLoadingUsage] = useState(false); @@ -81,8 +80,7 @@ export function Settings(props: { closeSettings: () => void }) { requestUsage() .then((res) => setUsage({ - granted: res?.total_granted, - used: res?.total_used, + used: res, }), ) .finally(() => { @@ -285,7 +283,8 @@ export function Settings(props: { closeSettings: () => void }) { checked={config.sendPreviewBubble} onChange={(e) => updateConfig( - (config) => (config.sendPreviewBubble = e.currentTarget.checked), + (config) => + (config.sendPreviewBubble = e.currentTarget.checked), ) } > @@ -360,10 +359,7 @@ export function Settings(props: { closeSettings: () => void }) { subTitle={ loadingUsage ? Locale.Settings.Usage.IsChecking - : Locale.Settings.Usage.SubTitle( - usage?.granted ?? "[?]", - usage?.used ?? "[?]", - ) + : Locale.Settings.Usage.SubTitle(usage?.used ?? "[?]") } > {loadingUsage ? ( diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 66436e12..62be467b 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -103,8 +103,8 @@ const cn = { }, Usage: { Title: "账户余额", - SubTitle(granted: any, used: any) { - return `总共 $${granted},已使用 $${used}`; + SubTitle(used: any) { + return `本月已使用 $${used}`; }, IsChecking: "正在检查…", Check: "重新检查", diff --git a/app/locales/en.ts b/app/locales/en.ts index 55884308..98fa7404 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -105,8 +105,8 @@ const en: LocaleType = { }, Usage: { Title: "Account Balance", - SubTitle(granted: any, used: any) { - return `Total $${granted}, Used $${used}`; + SubTitle(used: any) { + return `Used this month $${used}`; }, IsChecking: "Checking...", Check: "Check Again", diff --git a/app/locales/es.ts b/app/locales/es.ts index a78bf1aa..fca7202d 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -105,8 +105,8 @@ const es: LocaleType = { }, Usage: { Title: "Saldo de la cuenta", - SubTitle(granted: any, used: any) { - return `Total $${granted}, Usado $${used}`; + SubTitle(used: any) { + return `Usado $${used}`; }, IsChecking: "Comprobando...", Check: "Comprobar de nuevo", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 7137e884..27156283 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -103,8 +103,8 @@ const tw: LocaleType = { }, Usage: { Title: "帳戶餘額", - SubTitle(granted: any, used: any) { - return `總共 $${granted},已使用 $${used}`; + SubTitle(used: any) { + return `本月已使用 $${used}`; }, IsChecking: "正在檢查…", Check: "重新檢查", diff --git a/app/requests.ts b/app/requests.ts index 0be9dbf7..cf2ac7f7 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -48,6 +48,7 @@ export function requestOpenaiClient(path: string) { method, headers: { "Content-Type": "application/json", + "Cache-Control": "no-cache", path, ...getHeaders(), }, @@ -69,17 +70,25 @@ export async function requestChat(messages: Message[]) { } export async function requestUsage() { + const formatDate = (d: Date) => + `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d + .getDate() + .toString() + .padStart(2, "0")}`; + const ONE_DAY = 24 * 60 * 60 * 1000; + const now = new Date(Date.now() + ONE_DAY); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const startDate = formatDate(startOfMonth); + const endDate = formatDate(now); const res = await requestOpenaiClient( - "dashboard/billing/credit_grants?_vercel_no_cache=1", + `dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`, )(null, "GET"); try { const response = (await res.json()) as { - total_available: number; - total_granted: number; - total_used: number; + total_usage: number; }; - return response; + return Math.round(response.total_usage) / 100; } catch (error) { console.error("[Request usage] ", error, res.body); } From 4f0108b0eaa3fb1f06e3227c7f3ae9d22306621a Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sun, 2 Apr 2023 14:48:18 +0000 Subject: [PATCH 21/42] fix: #289 use highlight.js instead of prism --- app/components/markdown.tsx | 38 ++++++++++- app/layout.tsx | 2 +- app/styles/globals.scss | 1 + app/styles/highlight.scss | 114 +++++++++++++++++++++++++++++++++ app/styles/prism.scss | 122 ------------------------------------ package.json | 2 +- yarn.lock | 39 +++++++++++- 7 files changed, 190 insertions(+), 128 deletions(-) create mode 100644 app/styles/highlight.scss delete mode 100644 app/styles/prism.scss diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 6d3cd0bf..89492612 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -4,8 +4,8 @@ import RemarkMath from "remark-math"; import RemarkBreaks from "remark-breaks"; import RehypeKatex from "rehype-katex"; import RemarkGfm from "remark-gfm"; -import RehypePrsim from "rehype-prism-plus"; -import { useRef } from "react"; +import RehypeHighlight from "rehype-highlight"; +import { useRef, useState, RefObject, useEffect } from "react"; import { copyToClipboard } from "../utils"; export function PreCode(props: { children: any }) { @@ -27,11 +27,43 @@ export function PreCode(props: { children: any }) { ); } +const useLazyLoad = (ref: RefObject): boolean => { + const [isIntersecting, setIntersecting] = useState(false); + + useEffect(() => { + const observer = new IntersectionObserver(([entry]) => { + if (entry.isIntersecting) { + setIntersecting(true); + observer.disconnect(); + } + }); + + if (ref.current) { + observer.observe(ref.current); + } + + return () => { + observer.disconnect(); + }; + }, [ref]); + + return isIntersecting; +}; + export function Markdown(props: { content: string }) { return ( + License: see project LICENSE + Touched: 2022 +*/ + .hljs-comment, + .hljs-meta { + color: #565f89; + } + + .hljs-deletion, + .hljs-doctag, + .hljs-regexp, + .hljs-selector-attr, + .hljs-selector-class, + .hljs-selector-id, + .hljs-selector-pseudo, + .hljs-tag, + .hljs-template-tag, + .hljs-variable.language_ { + color: #f7768e; + } + + .hljs-link, + .hljs-literal, + .hljs-number, + .hljs-params, + .hljs-template-variable, + .hljs-type, + .hljs-variable { + color: #ff9e64; + } + + .hljs-attribute, + .hljs-built_in { + color: #e0af68; + } + + .hljs-keyword, + .hljs-property, + .hljs-subst, + .hljs-title, + .hljs-title.class_, + .hljs-title.class_.inherited__, + .hljs-title.function_ { + color: #7dcfff; + } + + .hljs-selector-tag { + color: #73daca; + } + + .hljs-addition, + .hljs-bullet, + .hljs-quote, + .hljs-string, + .hljs-symbol { + color: #9ece6a; + } + + .hljs-code, + .hljs-formula, + .hljs-section { + color: #7aa2f7; + } + + .hljs-attr, + .hljs-char.escape_, + .hljs-keyword, + .hljs-name, + .hljs-operator { + color: #bb9af7; + } + + .hljs-punctuation { + color: #c0caf5; + } + + .hljs { + background: #1a1b26; + color: #9aa5ce; + } + + .hljs-emphasis { + font-style: italic; + } + + .hljs-strong { + font-weight: 700; + } +} diff --git a/app/styles/prism.scss b/app/styles/prism.scss deleted file mode 100644 index 65ee8b5f..00000000 --- a/app/styles/prism.scss +++ /dev/null @@ -1,122 +0,0 @@ -.markdown-body { - pre { - background: #282a36; - color: #f8f8f2; - } - - code[class*="language-"], - pre[class*="language-"] { - color: #f8f8f2; - background: none; - text-shadow: 0 1px rgba(0, 0, 0, 0.3); - font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; - } - - /* Code blocks */ - pre[class*="language-"] { - padding: 1em; - margin: 0.5em 0; - overflow: auto; - border-radius: 0.3em; - } - - :not(pre) > code[class*="language-"], - pre[class*="language-"] { - background: #282a36; - } - - /* Inline code */ - :not(pre) > code[class*="language-"] { - padding: 0.1em; - border-radius: 0.3em; - white-space: normal; - } - - .token.comment, - .token.prolog, - .token.doctype, - .token.cdata { - color: #6272a4; - } - - .token.punctuation { - color: #f8f8f2; - } - - .namespace { - opacity: 0.7; - } - - .token.property, - .token.tag, - .token.constant, - .token.symbol, - .token.deleted { - color: #ff79c6; - } - - .token.boolean, - .token.number { - color: #bd93f9; - } - - .token.selector, - .token.attr-name, - .token.string, - .token.char, - .token.builtin, - .token.inserted { - color: #50fa7b; - } - - .token.operator, - .token.entity, - .token.url, - .language-css .token.string, - .style .token.string, - .token.variable { - color: #f8f8f2; - } - - .token.atrule, - .token.attr-value, - .token.function, - .token.class-name { - color: #f1fa8c; - } - - .token.keyword { - color: #8be9fd; - } - - .token.regex, - .token.important { - color: #ffb86c; - } - - .token.important, - .token.bold { - font-weight: bold; - } - - .token.italic { - font-style: italic; - } - - .token.entity { - cursor: help; - } -} diff --git a/package.json b/package.json index b67d7b0d..2e018647 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^8.0.5", + "rehype-highlight": "^6.0.0", "rehype-katex": "^6.0.2", - "rehype-prism-plus": "^1.5.1", "remark-breaks": "^3.0.2", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index 246b818b..fd26bb00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2548,6 +2548,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fault@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c" + integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== + dependencies: + format "^0.2.0" + 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" @@ -2612,6 +2619,11 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +format@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== + formdata-polyfill@^4.0.10: version "4.0.10" resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" @@ -2874,7 +2886,7 @@ hast-util-to-string@^2.0.0: dependencies: "@types/hast" "^2.0.0" -hast-util-to-text@^3.1.0: +hast-util-to-text@^3.0.0, hast-util-to-text@^3.1.0: version "3.1.2" resolved "https://registry.yarnpkg.com/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz#ecf30c47141f41e91a5d32d0b1e1859fd2ac04f2" integrity sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw== @@ -2900,6 +2912,11 @@ hastscript@^7.0.0: property-information "^6.0.0" space-separated-tokens "^2.0.0" +highlight.js@~11.7.0: + version "11.7.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e" + integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ== + human-signals@^4.3.0: version "4.3.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" @@ -3385,6 +3402,15 @@ loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lowlight@^2.0.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-2.8.1.tgz#5f54016ebd1b2f66b3d0b94d10ef6dd5df4f2e42" + integrity sha512-HCaGL61RKc1MYzEYn3rFoGkK0yslzCVDFJEanR19rc2L0mb8i58XM55jSRbzp9jcQrFzschPlwooC0vuNitk8Q== + dependencies: + "@types/hast" "^2.0.0" + fault "^2.0.0" + highlight.js "~11.7.0" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -4374,6 +4400,17 @@ regjsparser@^0.9.1: dependencies: jsesc "~0.5.0" +rehype-highlight@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/rehype-highlight/-/rehype-highlight-6.0.0.tgz#8097219d8813b51f4c2b6d92db27dac6cbc9a641" + integrity sha512-q7UtlFicLhetp7K48ZgZiJgchYscMma7XjzX7t23bqEJF8m6/s+viXQEe4oHjrATTIZpX7RG8CKD7BlNZoh9gw== + dependencies: + "@types/hast" "^2.0.0" + hast-util-to-text "^3.0.0" + lowlight "^2.0.0" + unified "^10.0.0" + unist-util-visit "^4.0.0" + rehype-katex@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/rehype-katex/-/rehype-katex-6.0.2.tgz#20197bbc10bdf79f6b999bffa6689d7f17226c35" From 6c1862797bb6d27c271d3cf0a3f80937e6f0c361 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sun, 2 Apr 2023 15:05:54 +0000 Subject: [PATCH 22/42] refactor: split homt.tsx components --- app/components/chat-list.tsx | 69 +++++ app/components/chat.tsx | 499 ++++++++++++++++++++++++++++++++ app/components/home.tsx | 533 +---------------------------------- app/components/settings.tsx | 2 +- 4 files changed, 572 insertions(+), 531 deletions(-) create mode 100644 app/components/chat-list.tsx create mode 100644 app/components/chat.tsx diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx new file mode 100644 index 00000000..5a74ff15 --- /dev/null +++ b/app/components/chat-list.tsx @@ -0,0 +1,69 @@ +import { useState, useRef, useEffect, useLayoutEffect } from "react"; +import DeleteIcon from "../icons/delete.svg"; +import styles from "./home.module.scss"; + +import { + Message, + SubmitKey, + useChatStore, + ChatSession, + BOT_HELLO, +} from "../store"; + +import Locale from "../locales"; + +export function ChatItem(props: { + onClick?: () => void; + onDelete?: () => void; + title: string; + count: number; + time: string; + selected: boolean; +}) { + return ( +
+
{props.title}
+
+
+ {Locale.ChatItem.ChatItemCount(props.count)} +
+
{props.time}
+
+
+ +
+
+ ); +} + +export function ChatList() { + const [sessions, selectedIndex, selectSession, removeSession] = useChatStore( + (state) => [ + state.sessions, + state.currentSessionIndex, + state.selectSession, + state.removeSession, + ], + ); + + return ( +
+ {sessions.map((item, i) => ( + selectSession(i)} + onDelete={() => confirm(Locale.Home.DeleteChat) && removeSession(i)} + /> + ))} +
+ ); +} diff --git a/app/components/chat.tsx b/app/components/chat.tsx new file mode 100644 index 00000000..348fe2ea --- /dev/null +++ b/app/components/chat.tsx @@ -0,0 +1,499 @@ +import { useDebouncedCallback } from "use-debounce"; +import { useState, useRef, useEffect, useLayoutEffect } from "react"; + +import SendWhiteIcon from "../icons/send-white.svg"; +import BrainIcon from "../icons/brain.svg"; +import ExportIcon from "../icons/export.svg"; +import MenuIcon from "../icons/menu.svg"; +import CopyIcon from "../icons/copy.svg"; +import DownloadIcon from "../icons/download.svg"; +import LoadingIcon from "../icons/three-dots.svg"; +import BotIcon from "../icons/bot.svg"; + +import { + Message, + SubmitKey, + useChatStore, + ChatSession, + BOT_HELLO, +} from "../store"; + +import { + copyToClipboard, + downloadAs, + isMobileScreen, + selectOrCopy, +} from "../utils"; + +import dynamic from "next/dynamic"; + +import { ControllerPool } from "../requests"; +import { Prompt, usePromptStore } from "../store/prompt"; +import Locale from "../locales"; + +import { IconButton } from "./button"; +import styles from "./home.module.scss"; + +import { showModal, showToast } from "./ui-lib"; + +const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { + loading: () => , +}); + +const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, { + loading: () => , +}); + +export function Avatar(props: { role: Message["role"] }) { + const config = useChatStore((state) => state.config); + + if (props.role === "assistant") { + return ; + } + + return ( +
+ +
+ ); +} + +function exportMessages(messages: Message[], topic: string) { + const mdText = + `# ${topic}\n\n` + + messages + .map((m) => { + return m.role === "user" ? `## ${m.content}` : m.content.trim(); + }) + .join("\n\n"); + const filename = `${topic}.md`; + + showModal({ + title: Locale.Export.Title, + children: ( +
+
{mdText}
+
+ ), + actions: [ + } + bordered + text={Locale.Export.Copy} + onClick={() => copyToClipboard(mdText)} + />, + } + bordered + text={Locale.Export.Download} + onClick={() => downloadAs(mdText, filename)} + />, + ], + }); +} + +function showMemoryPrompt(session: ChatSession) { + showModal({ + title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`, + children: ( +
+
+          {session.memoryPrompt || Locale.Memory.EmptyContent}
+        
+
+ ), + actions: [ + } + bordered + text={Locale.Memory.Copy} + onClick={() => copyToClipboard(session.memoryPrompt)} + />, + ], + }); +} + +function useSubmitHandler() { + const config = useChatStore((state) => state.config); + const submitKey = config.submitKey; + + const shouldSubmit = (e: React.KeyboardEvent) => { + if (e.key !== "Enter") return false; + if (e.key === "Enter" && e.nativeEvent.isComposing) return false; + return ( + (config.submitKey === SubmitKey.AltEnter && e.altKey) || + (config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) || + (config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) || + (config.submitKey === SubmitKey.MetaEnter && e.metaKey) || + (config.submitKey === SubmitKey.Enter && + !e.altKey && + !e.ctrlKey && + !e.shiftKey && + !e.metaKey) + ); + }; + + return { + submitKey, + shouldSubmit, + }; +} + +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; + sideBarShowing?: boolean; +}) { + type RenderMessage = Message & { preview?: boolean }; + + const chatStore = useChatStore(); + const [session, sessionIndex] = useChatStore((state) => [ + state.currentSession(), + state.currentSessionIndex, + ]); + const fontSize = useChatStore((state) => state.config.fontSize); + + const inputRef = useRef(null); + const [userInput, setUserInput] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const { submitKey, shouldSubmit } = useSubmitHandler(); + + // prompt hints + const promptStore = usePromptStore(); + const [promptHints, setPromptHints] = useState([]); + const onSearch = useDebouncedCallback( + (text: string) => { + setPromptHints(promptStore.search(text)); + }, + 100, + { leading: true, trailing: true }, + ); + + const onPromptSelect = (prompt: Prompt) => { + setUserInput(prompt.content); + setPromptHints([]); + inputRef.current?.focus(); + }; + + const scrollInput = () => { + const dom = inputRef.current; + if (!dom) return; + const paddingBottomNum: number = parseInt( + window.getComputedStyle(dom).paddingBottom, + 10, + ); + dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum; + }; + + // only search prompts when user input is short + const SEARCH_TEXT_LIMIT = 30; + const onInput = (text: string) => { + scrollInput(); + setUserInput(text); + const n = text.trim().length; + + // clear search results + if (n === 0) { + setPromptHints([]); + } else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) { + // check if need to trigger auto completion + if (text.startsWith("/") && text.length > 1) { + onSearch(text.slice(1)); + } + } + }; + + // submit user input + const onUserSubmit = () => { + if (userInput.length <= 0) return; + setIsLoading(true); + chatStore.onUserInput(userInput).then(() => setIsLoading(false)); + setUserInput(""); + setPromptHints([]); + inputRef.current?.focus(); + }; + + // stop response + const onUserStop = (messageIndex: number) => { + console.log(ControllerPool, sessionIndex, messageIndex); + ControllerPool.stop(sessionIndex, messageIndex); + }; + + // check if should send message + const onInputKeyDown = (e: React.KeyboardEvent) => { + if (shouldSubmit(e)) { + onUserSubmit(); + e.preventDefault(); + } + }; + const onRightClick = (e: any, message: Message) => { + // auto fill user input + if (message.role === "user") { + setUserInput(message.content); + } + + // copy to clipboard + if (selectOrCopy(e.currentTarget, message.content)) { + e.preventDefault(); + } + }; + + const onResend = (botIndex: number) => { + // find last user input message and resend + for (let i = botIndex; i >= 0; i -= 1) { + if (messages[i].role === "user") { + setIsLoading(true); + chatStore + .onUserInput(messages[i].content) + .then(() => setIsLoading(false)); + inputRef.current?.focus(); + return; + } + } + }; + + // for auto-scroll + const latestMessageRef = useRef(null); + const [autoScroll, setAutoScroll] = useState(true); + + const config = useChatStore((state) => state.config); + + // preview messages + const messages = (session.messages as RenderMessage[]) + .concat( + isLoading + ? [ + { + role: "assistant", + content: "……", + date: new Date().toLocaleString(), + preview: true, + }, + ] + : [], + ) + .concat( + userInput.length > 0 && config.sendPreviewBubble + ? [ + { + role: "user", + content: userInput, + date: new Date().toLocaleString(), + preview: false, + }, + ] + : [], + ); + + // auto scroll + useLayoutEffect(() => { + setTimeout(() => { + const dom = latestMessageRef.current; + const inputDom = inputRef.current; + + // only scroll when input overlaped message body + let shouldScroll = true; + if (dom && inputDom) { + const domRect = dom.getBoundingClientRect(); + const inputRect = inputDom.getBoundingClientRect(); + shouldScroll = domRect.top > inputRect.top; + } + + if (dom && autoScroll && shouldScroll) { + dom.scrollIntoView({ + block: "end", + }); + } + }, 500); + }); + + return ( +
+
+
+
{ + const newTopic = prompt(Locale.Chat.Rename, session.topic); + if (newTopic && newTopic !== session.topic) { + chatStore.updateCurrentSession( + (session) => (session.topic = newTopic!), + ); + } + }} + > + {session.topic} +
+
+ {Locale.Chat.SubTitle(session.messages.length)} +
+
+
+
+ } + bordered + title={Locale.Chat.Actions.ChatList} + onClick={props?.showSideBar} + /> +
+
+ } + bordered + title={Locale.Chat.Actions.CompressedHistory} + onClick={() => { + showMemoryPrompt(session); + }} + /> +
+
+ } + bordered + title={Locale.Chat.Actions.Export} + onClick={() => { + exportMessages(session.messages, session.topic); + }} + /> +
+
+
+ +
+ {messages.map((message, i) => { + const isUser = message.role === "user"; + + return ( +
+
+
+ +
+ {(message.preview || message.streaming) && ( +
+ {Locale.Chat.Typing} +
+ )} +
+ {!isUser && + !(message.preview || message.content.length === 0) && ( +
+ {message.streaming ? ( +
onUserStop(i)} + > + {Locale.Chat.Actions.Stop} +
+ ) : ( +
onResend(i)} + > + {Locale.Chat.Actions.Retry} +
+ )} + +
copyToClipboard(message.content)} + > + {Locale.Chat.Actions.Copy} +
+
+ )} + {(message.preview || message.content.length === 0) && + !isUser ? ( + + ) : ( +
onRightClick(e, message)} + onDoubleClickCapture={() => { + if (!isMobileScreen()) return; + setUserInput(message.content); + }} + > + +
+ )} +
+ {!isUser && !message.preview && ( +
+
+ {message.date.toLocaleString()} +
+
+ )} +
+
+ ); + })} +
+ - +
+
+ +
+ +
+