diff --git a/README_CN.md b/README_CN.md index cba9df9c..efd5d56a 100644 --- a/README_CN.md +++ b/README_CN.md @@ -11,7 +11,7 @@ [](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) - + @@ -29,7 +29,7 @@ 1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys); 2. 点击右侧按钮开始部署: - [](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; + [](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/calcTextareaHeight.ts b/app/calcTextareaHeight.ts deleted file mode 100644 index 555c8ceb..00000000 --- a/app/calcTextareaHeight.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * fork from element-plus - * https://github.com/element-plus/element-plus/blob/dev/packages/components/input/src/utils.ts - */ - -import { isFirefox } from "./utils"; - -let hiddenTextarea: HTMLTextAreaElement | undefined = undefined; - -const HIDDEN_STYLE = ` - height:0 !important; - visibility:hidden !important; - ${isFirefox() ? "" : "overflow:hidden !important;"} - position:absolute !important; - z-index:-1000 !important; - top:0 !important; - right:0 !important; -`; - -const CONTEXT_STYLE = [ - "letter-spacing", - "line-height", - "padding-top", - "padding-bottom", - "font-family", - "font-weight", - "font-size", - "text-rendering", - "text-transform", - "width", - "text-indent", - "padding-left", - "padding-right", - "border-width", - "box-sizing", -]; - -type NodeStyle = { - contextStyle: string; - boxSizing: string; - paddingSize: number; - borderSize: number; -}; - -type TextAreaHeight = { - height: string; - minHeight?: string; -}; - -function calculateNodeStyling(targetElement: Element): NodeStyle { - const style = window.getComputedStyle(targetElement); - - const boxSizing = style.getPropertyValue("box-sizing"); - - const paddingSize = - Number.parseFloat(style.getPropertyValue("padding-bottom")) + - Number.parseFloat(style.getPropertyValue("padding-top")); - - const borderSize = - Number.parseFloat(style.getPropertyValue("border-bottom-width")) + - Number.parseFloat(style.getPropertyValue("border-top-width")); - - const contextStyle = CONTEXT_STYLE.map( - (name) => `${name}:${style.getPropertyValue(name)}`, - ).join(";"); - - return { contextStyle, paddingSize, borderSize, boxSizing }; -} - -export default function calcTextareaHeight( - targetElement: HTMLTextAreaElement, - minRows: number = 2, - maxRows?: number, -): TextAreaHeight { - if (!hiddenTextarea) { - hiddenTextarea = document.createElement("textarea"); - document.body.appendChild(hiddenTextarea); - } - - const { paddingSize, borderSize, boxSizing, contextStyle } = - calculateNodeStyling(targetElement); - - hiddenTextarea.setAttribute("style", `${contextStyle};${HIDDEN_STYLE}`); - hiddenTextarea.value = targetElement.value || targetElement.placeholder || ""; - - let height = hiddenTextarea.scrollHeight; - const result = {} as TextAreaHeight; - - if (boxSizing === "border-box") { - height = height + borderSize; - } else if (boxSizing === "content-box") { - height = height - paddingSize; - } - - hiddenTextarea.value = ""; - const singleRowHeight = hiddenTextarea.scrollHeight - paddingSize; - - if (minRows) { - let minHeight = singleRowHeight * minRows; - if (boxSizing === "border-box") { - minHeight = minHeight + paddingSize + borderSize; - } - height = Math.max(minHeight, height); - result.minHeight = `${minHeight}px`; - } - if (maxRows) { - let maxHeight = singleRowHeight * maxRows; - if (boxSizing === "border-box") { - maxHeight = maxHeight + paddingSize + borderSize; - } - height = Math.min(maxHeight, height); - } - result.height = `${height}px`; - hiddenTextarea.parentNode?.removeChild(hiddenTextarea); - hiddenTextarea = undefined; - - return result; -} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 0e719d84..c5c257e5 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,9 +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 calcTextareaHeight from "../calcTextareaHeight"; +import { Input, Modal, showModal } from "./ui-lib"; const Markdown = dynamic( async () => memo((await import("./markdown")).Markdown), @@ -333,10 +332,6 @@ function useScrollToBottom() { export function Chat(props: { showSideBar?: () => void; sideBarShowing?: boolean; - autoSize: { - minRows: number; - maxRows?: number; - }; }) { type RenderMessage = Message & { preview?: boolean }; @@ -354,7 +349,6 @@ export function Chat(props: { const { submitKey, shouldSubmit } = useSubmitHandler(); const { scrollRef, setAutoScroll } = useScrollToBottom(); const [hitBottom, setHitBottom] = useState(false); - const [textareaStyle, setTextareaStyle] = useState({}); const onChatBodyScroll = (e: HTMLElement) => { const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20; @@ -388,13 +382,26 @@ export function Chat(props: { dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum; }; - // textarea has an adaptive height - const resizeTextarea = () => { - const dom = inputRef.current; - if (!dom) return; - const { minRows, maxRows } = props.autoSize; - setTextareaStyle(calcTextareaHeight(dom, minRows, maxRows)); - }; + // 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; @@ -410,9 +417,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); } } @@ -527,12 +531,6 @@ export function Chat(props: { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Textarea Adaptive height - useEffect(() => { - resizeTextarea(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userInput]); - return (