diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 38c272e85..52feae50e 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -5,7 +5,7 @@ permissions: on: schedule: - - cron: "0 */6 * * *" # every 6 hours + - cron: "0 * * * *" # every hour workflow_dispatch: jobs: diff --git a/README_CN.md b/README_CN.md index cba9df9c5..efd5d56a1 100644 --- a/README_CN.md +++ b/README_CN.md @@ -11,7 +11,7 @@ [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) -![主界面](./static/cover.png) +![主界面](./docs/images/cover.png) @@ -29,7 +29,7 @@ 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 和[页面访问密码](#配置页面访问密码) CODE; 3. 部署完毕后,即可开始使用; 4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。 @@ -53,6 +53,8 @@ > 配置密码后,用户需要在设置页手动填写访问码才可以正常聊天,否则会通过消息提示未授权状态。 +> **警告**:请务必将密码的位数设置得足够长,最好 7 位以上,否则[会被爆破](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。 + 本项目提供有限的权限控制功能,请在 Vercel 项目控制面板的环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义密码: ``` diff --git a/app/components/chat.tsx b/app/components/chat.tsx index de9854a16..a81c5772d 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1,4 +1,4 @@ -import { useDebouncedCallback } from "use-debounce"; +import { useDebounce, useDebouncedCallback } from "use-debounce"; import { memo, useState, useRef, useEffect, useLayoutEffect } from "react"; import SendWhiteIcon from "../icons/send-white.svg"; @@ -27,6 +27,7 @@ import { getEmojiUrl, isMobileScreen, selectOrCopy, + autoGrowTextArea, } from "../utils"; import dynamic from "next/dynamic"; @@ -39,7 +40,7 @@ import { IconButton } from "./button"; import styles from "./home.module.scss"; import chatStyle from "./chat.module.scss"; -import { Input, Modal, showModal, showToast } from "./ui-lib"; +import { Input, Modal, showModal } from "./ui-lib"; const Markdown = dynamic( async () => memo((await import("./markdown")).Markdown), @@ -382,6 +383,27 @@ export function Chat(props: {}) { dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum; }; + // auto grow input + const [inputRows, setInputRows] = useState(2); + const measure = useDebouncedCallback( + () => { + const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1; + const inputRows = Math.min( + 5, + Math.max(2 + Number(!isMobileScreen()), rows), + ); + setInputRows(inputRows); + }, + 100, + { + leading: true, + trailing: true, + }, + ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(measure, [userInput]); + // only search prompts when user input is short const SEARCH_TEXT_LIMIT = 30; const onInput = (text: string) => { @@ -396,9 +418,6 @@ export function Chat(props: {}) { // check if need to trigger auto completion if (text.startsWith("/")) { let searchText = text.slice(1); - if (searchText.length === 0) { - searchText = " "; - } onSearch(searchText); } } @@ -669,7 +688,6 @@ export function Chat(props: {}) { ref={inputRef} className={styles["chat-input"]} placeholder={Locale.Chat.Input(submitKey)} - rows={2} onInput={(e) => onInput(e.currentTarget.value)} value={userInput} onKeyDown={onInputKeyDown} @@ -679,6 +697,7 @@ export function Chat(props: {}) { setTimeout(() => setPromptHints([]), 500); }} autoFocus={sidebarCollapse} + rows={inputRows} /> } diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 8281debb3..0efeb1b17 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -522,17 +522,11 @@ background-color: var(--white); color: var(--black); font-family: inherit; - padding: 10px 14px 50px; + padding: 10px 90px 10px 14px; resize: none; outline: none; } -@media only screen and (max-width: 600px) { - .chat-input { - font-size: 16px; - } -} - .chat-input:focus { border: 1px solid var(--primary); } @@ -543,7 +537,17 @@ position: absolute; right: 30px; - bottom: 30px; + bottom: 32px; +} + +@media only screen and (max-width: 600px) { + .chat-input { + font-size: 16px; + } + + .chat-input-send { + bottom: 30px; + } } .export-content { diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 25991d742..c8076640c 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -67,7 +67,7 @@ export function Markdown(props: { content: string }) { components={{ pre: PreCode, }} - linkTarget={'_blank'} + linkTarget={"_blank"} > {props.content} diff --git a/app/components/settings.tsx b/app/components/settings.tsx index d1d217ead..0d1c1d05c 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -156,7 +156,14 @@ export function Settings(props: { closeSettings: () => void }) {
} - onClick={clearSessions} + onClick={() => { + const confirmed = window.confirm( + `${Locale.Settings.Actions.ConfirmClearAll.Confirm}`, + ); + if (confirmed) { + clearSessions(); + } + }} bordered title={Locale.Settings.Actions.ClearAll} /> @@ -164,7 +171,14 @@ export function Settings(props: { closeSettings: () => void }) {
} - onClick={resetConfig} + onClick={() => { + const confirmed = window.confirm( + `${Locale.Settings.Actions.ConfirmResetAll.Confirm}`, + ); + if (confirmed) { + resetConfig(); + } + }} bordered title={Locale.Settings.Actions.ResetAll} /> diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss index 83eb614f7..67d13d3e8 100644 --- a/app/components/ui-lib.module.scss +++ b/app/components/ui-lib.module.scss @@ -9,6 +9,7 @@ .popover { position: relative; + z-index: 2; } .popover-content { diff --git a/app/locales/cn.ts b/app/locales/cn.ts index e21272a12..01b236d7f 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -21,11 +21,11 @@ const cn = { Rename: "重命名对话", Typing: "正在输入…", Input: (submitKey: string) => { - var inputHints = `输入消息,${submitKey} 发送`; + var inputHints = `${submitKey} 发送`; if (submitKey === String(SubmitKey.Enter)) { inputHints += ",Shift + Enter 换行"; } - return inputHints; + return inputHints + ",/ 触发补全"; }, Send: "发送", }, @@ -57,6 +57,12 @@ const cn = { ClearAll: "清除所有数据", ResetAll: "重置所有选项", Close: "关闭", + ConfirmResetAll: { + Confirm: "Are you sure you want to reset all configurations?", + }, + ConfirmClearAll: { + Confirm: "Are you sure you want to reset all chat?", + }, }, Lang: { Name: "Language", @@ -126,7 +132,7 @@ const cn = { Model: "模型 (model)", Temperature: { Title: "随机性 (temperature)", - SubTitle: "值越大,回复越随机", + SubTitle: "值越大,回复越随机,大于 1 的值可能会导致乱码", }, MaxTokens: { Title: "单次回复限制 (max_tokens)", diff --git a/app/locales/en.ts b/app/locales/en.ts index 61d20b60f..731309034 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -23,11 +23,11 @@ const en: LocaleType = { Rename: "Rename Chat", Typing: "Typing…", Input: (submitKey: string) => { - var inputHints = `Type something and press ${submitKey} to send`; + var inputHints = `${submitKey} to send`; if (submitKey === String(SubmitKey.Enter)) { - inputHints += ", press Shift + Enter to newline"; + inputHints += ", Shift + Enter to wrap"; } - return inputHints; + return inputHints + ", / to search prompts"; }, Send: "Send", }, @@ -60,6 +60,12 @@ const en: LocaleType = { ClearAll: "Clear All Data", ResetAll: "Reset All Settings", Close: "Close", + ConfirmResetAll: { + Confirm: "Are you sure you want to reset all configurations?", + }, + ConfirmClearAll: { + Confirm: "Are you sure you want to reset all chat?", + }, }, Lang: { Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` diff --git a/app/locales/es.ts b/app/locales/es.ts index 5a83cb55c..06277f6b5 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -60,6 +60,12 @@ const es: LocaleType = { ClearAll: "Borrar todos los datos", ResetAll: "Restablecer todas las configuraciones", Close: "Cerrar", + ConfirmResetAll: { + Confirm: "Are you sure you want to reset all configurations?", + }, + ConfirmClearAll: { + Confirm: "Are you sure you want to reset all chat?", + }, }, Lang: { Name: "Language", diff --git a/app/locales/it.ts b/app/locales/it.ts index 7108090eb..70967d966 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -60,6 +60,12 @@ const it: LocaleType = { ClearAll: "Cancella tutti i dati", ResetAll: "Resetta tutte le impostazioni", Close: "Chiudi", + ConfirmResetAll: { + Confirm: "Sei sicuro vuoi cancellare tutte le impostazioni?", + }, + ConfirmClearAll: { + Confirm: "Sei sicuro vuoi cancellare tutte le chat?", + }, }, Lang: { Name: "Lingue", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index ff1794b55..3861a671c 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -58,6 +58,12 @@ const tw: LocaleType = { ClearAll: "清除所有數據", ResetAll: "重置所有設定", Close: "關閉", + ConfirmResetAll: { + Confirm: "Are you sure you want to reset all configurations?", + }, + ConfirmClearAll: { + Confirm: "Are you sure you want to reset all chat?", + }, }, Lang: { Name: "Language", diff --git a/app/requests.ts b/app/requests.ts index 5ca5e217a..d7d8e4184 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -24,7 +24,11 @@ const makeRequestParam = ( sendMessages = sendMessages.filter((m) => m.role !== "assistant"); } - const modelConfig = useChatStore.getState().config.modelConfig; + const modelConfig = { ...useChatStore.getState().config.modelConfig }; + + // @yidadaa: wont send max_tokens, because it is nonsense for Muggles + // @ts-expect-error + delete modelConfig.max_tokens; return { messages: sendMessages, diff --git a/app/store/app.ts b/app/store/app.ts index e6e391162..bf302eca9 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -337,18 +337,19 @@ export const useChatStore = create()( const isLastSession = get().sessions.length === 1; if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) { get().removeSession(index); + + showToast(Locale.Home.DeleteToast, { + text: Locale.Home.Revert, + onClick() { + set((state) => ({ + sessions: state.sessions + .slice(0, index) + .concat([deletedSession]) + .concat(state.sessions.slice(index + Number(isLastSession))), + })); + }, + }); } - showToast(Locale.Home.DeleteToast, { - text: Locale.Home.Revert, - onClick() { - set((state) => ({ - sessions: state.sessions - .slice(0, index) - .concat([deletedSession]) - .concat(state.sessions.slice(index + Number(isLastSession))), - })); - }, - }); }, currentSession() { diff --git a/app/store/prompt.ts b/app/store/prompt.ts index b91a38d0b..d0dd454ac 100644 --- a/app/store/prompt.ts +++ b/app/store/prompt.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import Fuse from "fuse.js"; +import { getLang } from "../locales"; export interface Prompt { id?: number; @@ -25,11 +26,13 @@ export const SearchService = { count: { builtin: 0, }, + allBuiltInPrompts: [] as Prompt[], init(prompts: Prompt[]) { if (this.ready) { return; } + this.allBuiltInPrompts = prompts; this.engine.setCollection(prompts); this.ready = true; }, @@ -78,6 +81,11 @@ export const usePromptStore = create()( }, search(text) { + if (text.length === 0) { + // return all prompts + const userPrompts = get().prompts?.values?.() ?? []; + return SearchService.allBuiltInPrompts.concat([...userPrompts]); + } return SearchService.search(text) as Prompt[]; }, }), @@ -92,7 +100,11 @@ export const usePromptStore = create()( fetch(PROMPT_URL) .then((res) => res.json()) .then((res) => { - const builtinPrompts = [res.en, res.cn] + let fetchPrompts = [res.en, res.cn]; + if (getLang() === "cn") { + fetchPrompts = fetchPrompts.reverse(); + } + const builtinPrompts = fetchPrompts .map((promptList: PromptList) => { return promptList.map( ([title, content]) => diff --git a/app/utils.ts b/app/utils.ts index 333866c7b..9a792fd52 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -51,6 +51,12 @@ export function isMobileScreen() { return window.innerWidth <= 600; } +export function isFirefox() { + return ( + typeof navigator !== "undefined" && /firefox/i.test(navigator.userAgent) + ); +} + export function selectOrCopy(el: HTMLElement, content: string) { const currentSelection = window.getSelection(); @@ -91,3 +97,51 @@ export function getCurrentVersion() { export function getEmojiUrl(unified: string, style: EmojiStyle) { return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`; } + +function getDomContentWidth(dom: HTMLElement) { + const style = window.getComputedStyle(dom); + const paddingWidth = + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight); + const width = dom.clientWidth - paddingWidth; + return width; +} + +function getOrCreateMeasureDom(id: string, init?: (dom: HTMLElement) => void) { + let dom = document.getElementById(id); + + if (!dom) { + dom = document.createElement("span"); + dom.style.position = "absolute"; + dom.style.wordBreak = "break-word"; + dom.style.fontSize = "14px"; + dom.style.transform = "translateY(-200vh)"; + dom.style.pointerEvents = "none"; + dom.style.opacity = "0"; + dom.id = id; + document.body.appendChild(dom); + init?.(dom); + } + + return dom!; +} + +export function autoGrowTextArea(dom: HTMLTextAreaElement) { + const measureDom = getOrCreateMeasureDom("__measure"); + const singleLineDom = getOrCreateMeasureDom("__single_measure", (dom) => { + dom.innerText = "TEXT_FOR_MEASURE"; + }); + + const width = getDomContentWidth(dom); + measureDom.style.width = width + "px"; + measureDom.innerHTML = dom.value.trim().length > 0 ? dom.value : "1"; + + const lineWrapCount = Math.max(0, dom.value.split("\n").length - 1); + const height = parseFloat(window.getComputedStyle(measureDom).height); + const singleLineHeight = parseFloat( + window.getComputedStyle(singleLineDom).height, + ); + + const rows = Math.round(height / singleLineHeight) + lineWrapCount; + + return rows; +} diff --git a/tsconfig.json b/tsconfig.json index 14d189328..c73eef3e8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,6 @@ "@/*": ["./*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/calcTextareaHeight.ts"], "exclude": ["node_modules"] } diff --git a/vercel.json b/vercel.json new file mode 100644 index 000000000..0cae358a1 --- /dev/null +++ b/vercel.json @@ -0,0 +1,5 @@ +{ + "github": { + "silent": true + } +}