diff --git a/.github/ISSUE_TEMPLATE/反馈问题.md b/.github/ISSUE_TEMPLATE/反馈问题.md index 4545721e6..ea56aa6fa 100644 --- a/.github/ISSUE_TEMPLATE/反馈问题.md +++ b/.github/ISSUE_TEMPLATE/反馈问题.md @@ -7,6 +7,11 @@ assignees: '' --- +**反馈须知** +> 请在下方中括号内输入 x 来表示你已经知晓相关内容。 +- [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答; +- [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。 + **描述问题** 请在此描述你遇到了什么问题。 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a7a29644d..8ac96f193 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -43,7 +43,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 52feae50e..15d324074 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -31,3 +31,10 @@ jobs: # Set test_mode true to run tests instead of the true action!! test_mode: false + + - name: Sync check + if: failure() + run: | + echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。" + echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]." + exit 1 diff --git a/Dockerfile b/Dockerfile index 018396ef3..7ed7bc155 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,6 @@ RUN apk update && apk add --no-cache git ENV OPENAI_API_KEY="" ENV CODE="" -ARG DOCKER=true WORKDIR /app COPY --from=deps /app/node_modules ./node_modules @@ -46,7 +45,7 @@ CMD if [ -n "$PROXY_URL" ]; then \ host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ port=$(echo $PROXY_URL | cut -d: -f3); \ conf=/etc/proxychains.conf; \ - echo "strict_chain" >> $conf; \ + echo "strict_chain" > $conf; \ echo "proxy_dns" >> $conf; \ echo "remote_dns_subnet 224" >> $conf; \ echo "tcp_read_time_out 15000" >> $conf; \ diff --git a/README.md b/README.md index ba4a6dc49..a6288798a 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,15 @@

ChatGPT Next Web

+English / [简体中文](./README_CN.md) + One-Click to deploy well-designed ChatGPT web UI on Vercel. 一键免费部署你的私人 ChatGPT 网页应用。 -[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join Discord](https://discord.gg/zrhvHCr79N) / [QQ 群](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) +[Demo](https://chat-gpt-next-web.vercel.app/) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join Discord](https://discord.gg/zrhvHCr79N) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) + +[演示](https://chat-gpt-next-web.vercel.app/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://user-images.githubusercontent.com/16968934/231789746-41f34d05-6ef9-43f3-a1d1-ff109d4c3c14.jpg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) [![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) @@ -20,8 +24,9 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. ## Features - **Deploy for free with one-click** on Vercel in under 1 minute +- Privacy first, all data stored locally in the browser - Responsive design, dark mode and PWA -- Fast first screen loading speed (~100kb) +- Fast first screen loading speed (~100kb), support streaming response - 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 @@ -44,7 +49,8 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - 在 1 分钟内使用 Vercel **免费一键部署** - 精心设计的 UI,响应式设计,支持深色模式,支持 PWA -- 极快的首屏加载速度(~100kb) +- 极快的首屏加载速度(~100kb),支持流式响应 +- 隐私安全,所有数据保存在用户浏览器本地 - 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) - 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话 - 一键导出聊天记录,完整的 Markdown 支持 @@ -74,7 +80,9 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. ## FAQ -[简体中文 > 常见问题](./docs/faq-cn.md) | [English > FAQ](./docs/faq.en.md) +[简体中文 > 常见问题](./docs/faq-cn.md) + +[English > FAQ](./docs/faq-en.md) ## Keep Updated @@ -84,12 +92,19 @@ If you have deployed your own project with just one click following the steps ab We recommend that you follow the steps below to re-deploy: -- Delete the original repo; -- Fork this project; -- Go to the Vercel dashboard, delete the original project, then create a new project and select the project you just forked to redeploy; -- Please manually add an environment variable named `OPENAI_API_KEY` and enter your API key as the value during the redeploy process. +- Delete the original repository; +- Use the fork button in the upper right corner of the page to fork this project; +- Choose and deploy in Vercel again, [please see the detailed tutorial](./docs/vercel-cn.md). -This project will be continuously updated, and after forking the project, the upstream code will be automatically synchronized every day without additional operations. +### Enable Automatic Updates + +After forking the project, due to the limitations imposed by Github, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour: + +![Automatic Updates](./docs/images/enable-actions.jpg) + +![Enable Automatic Updates](./docs/images/enable-actions-sync.jpg) + +### Manually Updating Code If you want to update instantly, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code. @@ -121,18 +136,12 @@ Access passsword, separated by comma. ### `BASE_URL` (optional) -> Default: `api.openai.com` +> Default: `https://api.openai.com` + +> Examples: `http://your-openai-proxy.com` Override openai api request base url. -### `PROTOCOL` (optional) - -> Default: `https` - -> Values: `http` | `https` - -Override openai api request protocol. - ## Development > [简体中文 > 如何进行二次开发](./README_CN.md#开发) @@ -200,6 +209,8 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ### Sponsor +> 仅列出捐赠金额 >= 100RMB 的用户。 + [@mushan0x0](https://github.com/mushan0x0) [@ClarenceDan](https://github.com/ClarenceDan) [@zhangjia](https://github.com/zhangjia) @@ -209,6 +220,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s [@webees](https://github.com/webees) [@chazzhou](https://github.com/chazzhou) [@hauy](https://github.com/hauy) +[@Corwin006](https://github.com/Corwin006) ### Contributor diff --git a/README_CN.md b/README_CN.md index 0833ae1ac..d2d64aa00 100644 --- a/README_CN.md +++ b/README_CN.md @@ -38,12 +38,19 @@ 如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。 推荐你按照下列步骤重新部署: -- 删除掉原先的 repo; -- fork 本项目; -- 前往 vercel 控制台,删除掉原先的 project,然后新建 project,选择你刚刚 fork 出来的项目重新进行部署即可; -- 在重新部署的过程中,请手动添加名为 `OPENAI_API_KEY` 的环境变量,并填入你的 api key 作为值。 +- 删除掉原先的仓库; +- 使用页面右上角的 fork 按钮,fork 本项目; +- 在 Vercel 重新选择并部署,[请查看详细教程](./docs/vercel-cn.md#如何新建项目)。 -本项目会持续更新,当你 Fork 项目之后,默认会每天自动同步上游代码,无需额外操作。 +### 打开自动更新 + +当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新: + +![自动更新](./docs/images/enable-actions.jpg) + +![启用自动更新](./docs/images/enable-actions-sync.jpg) + +### 手动更新代码 如果你想让手动立即更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步。 @@ -79,17 +86,13 @@ OpanAI 密钥,你在 openai 账户页面申请的 api key。 ### `BASE_URL` (可选) -> Default: `api.openai.com` +> Default: `https://api.openai.com` + +> Examples: `http://your-openai-proxy.com` OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填写此选项。 -### `PROTOCOL` (可选) - -> Default: `https` - -> Values: `http` | `https` - -OpenAI 代理接口协议,如果遇到 ssl 证书问题,请尝试通过此选项设置为 http。 +> 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。 ## 开发 @@ -131,7 +134,8 @@ docker run -d -p 3000:3000 \ docker run -d -p 3000:3000 \ -e OPENAI_API_KEY="sk-xxxx" \ -e CODE="页面访问密码" \ - -e PROXY_URL="http://localhost:7890" \ + --net=host \ + -e PROXY_URL="http://127.0.0.1:7890" \ yidadaa/chatgpt-next-web ``` @@ -147,12 +151,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ### 捐赠者 -> 仅列出了部分大额打赏,小额打赏(< 100RMB)人数太多,在此不再列出,敬请谅解。 - -[@mushan0x0](https://github.com/mushan0x0) -[@ClarenceDan](https://github.com/ClarenceDan) -[@zhangjia](https://github.com/zhangjia) -[@hoochanlon](https://github.com/hoochanlon) +> 见英文版。 ### 贡献者 diff --git a/app/api/access.ts b/app/api/access.ts deleted file mode 100644 index d3e4c9cf9..000000000 --- a/app/api/access.ts +++ /dev/null @@ -1,17 +0,0 @@ -import md5 from "spark-md5"; - -export function getAccessCodes(): Set { - const code = process.env.CODE; - - try { - const codes = (code?.split(",") ?? []) - .filter((v) => !!v) - .map((v) => md5.hash(v.trim())); - return new Set(codes); - } catch (e) { - return new Set(); - } -} - -export const ACCESS_CODES = getAccessCodes(); -export const IS_IN_DOCKER = process.env.DOCKER; diff --git a/app/api/chat-stream/route.ts b/app/api/chat-stream/route.ts index 526623ce1..41f135495 100644 --- a/app/api/chat-stream/route.ts +++ b/app/api/chat-stream/route.ts @@ -40,7 +40,7 @@ async function createStream(req: NextRequest) { const parser = createParser(onParse); for await (const chunk of res.body as any) { - parser.feed(decoder.decode(chunk)); + parser.feed(decoder.decode(chunk, { stream: true })); } }, }); diff --git a/app/api/common.ts b/app/api/common.ts index 842eeacaf..53ab18ed6 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -9,9 +9,16 @@ export async function requestOpenai(req: NextRequest) { const apiKey = req.headers.get("token"); const openaiPath = req.headers.get("path"); - console.log("[Proxy] ", openaiPath); + let baseUrl = BASE_URL; - return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, { + if (!baseUrl.startsWith("http")) { + baseUrl = `${PROTOCOL}://${baseUrl}`; + } + + console.log("[Proxy] ", openaiPath); + console.log("[Base Url]", baseUrl); + + return fetch(`${baseUrl}/${openaiPath}`, { headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, diff --git a/app/api/config/route.ts b/app/api/config/route.ts new file mode 100644 index 000000000..e04e22a0c --- /dev/null +++ b/app/api/config/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { getServerSideConfig } from "../../config/server"; + +const serverConfig = getServerSideConfig(); + +// Danger! Don not write any secret value here! +// 警告!不要在这里写入任何敏感信息! +const DANGER_CONFIG = { + needCode: serverConfig.needCode, +}; + +declare global { + type DangerConfig = typeof DANGER_CONFIG; +} + +export async function POST(req: NextRequest) { + return NextResponse.json({ + needCode: serverConfig.needCode, + }); +} diff --git a/app/api/openai/typing.ts b/app/api/openai/typing.ts index 8c4218467..b936530c3 100644 --- a/app/api/openai/typing.ts +++ b/app/api/openai/typing.ts @@ -4,4 +4,4 @@ import type { } from "openai"; export type ChatRequest = CreateChatCompletionRequest; -export type ChatReponse = CreateChatCompletionResponse; +export type ChatResponse = CreateChatCompletionResponse; diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index f57e6c100..7cd2889f7 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -1,5 +1,29 @@ @import "../styles/animation.scss"; +.chat-input-actions { + display: flex; + flex-wrap: wrap; + + .chat-input-action { + display: inline-flex; + border-radius: 20px; + font-size: 12px; + background-color: var(--white); + color: var(--black); + border: var(--border-in-light); + padding: 4px 10px; + animation: slide-in ease 0.3s; + box-shadow: var(--card-shadow); + transition: all ease 0.3s; + margin-bottom: 10px; + align-items: center; + + &:not(:last-child) { + margin-right: 5px; + } + } +} + .prompt-toast { position: absolute; bottom: -50px; diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 33ac3ac57..f7b67b293 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -3,6 +3,7 @@ import { memo, useState, useRef, useEffect, useLayoutEffect } from "react"; import SendWhiteIcon from "../icons/send-white.svg"; import BrainIcon from "../icons/brain.svg"; +import RenameIcon from "../icons/rename.svg"; import ExportIcon from "../icons/share.svg"; import ReturnIcon from "../icons/return.svg"; import CopyIcon from "../icons/copy.svg"; @@ -14,6 +15,11 @@ import DeleteIcon from "../icons/delete.svg"; import MaxIcon from "../icons/max.svg"; import MinIcon from "../icons/min.svg"; +import LightIcon from "../icons/light.svg"; +import DarkIcon from "../icons/dark.svg"; +import AutoIcon from "../icons/auto.svg"; +import BottomIcon from "../icons/bottom.svg"; + import { Message, SubmitKey, @@ -22,6 +28,7 @@ import { ROLES, createMessage, useAccessStore, + Theme, } from "../store"; import { @@ -31,6 +38,7 @@ import { isMobileScreen, selectOrCopy, autoGrowTextArea, + getCSSVar, } from "../utils"; import dynamic from "next/dynamic"; @@ -60,7 +68,11 @@ export function Avatar(props: { role: Message["role"] }) { const config = useChatStore((state) => state.config); if (props.role !== "user") { - return ; + return ( +
+ +
+ ); } return ( @@ -316,22 +328,78 @@ function useScrollToBottom() { // for auto-scroll const scrollRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); + const scrollToBottom = () => { + const dom = scrollRef.current; + if (dom) { + setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1); + } + }; // auto scroll useLayoutEffect(() => { - const dom = scrollRef.current; - if (dom && autoScroll) { - setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1); - } + autoScroll && scrollToBottom(); }); return { scrollRef, autoScroll, setAutoScroll, + scrollToBottom, }; } +export function ChatActions(props: { + showPromptModal: () => void; + scrollToBottom: () => void; + hitBottom: boolean; +}) { + const chatStore = useChatStore(); + + const theme = chatStore.config.theme; + + function nextTheme() { + const themes = [Theme.Auto, Theme.Light, Theme.Dark]; + const themeIndex = themes.indexOf(theme); + const nextIndex = (themeIndex + 1) % themes.length; + const nextTheme = themes[nextIndex]; + chatStore.updateConfig((config) => (config.theme = nextTheme)); + } + + return ( +
+ {!props.hitBottom && ( +
+ +
+ )} + {props.hitBottom && ( +
+ +
+ )} + +
+ {theme === Theme.Auto ? ( + + ) : theme === Theme.Light ? ( + + ) : theme === Theme.Dark ? ( + + ) : null} +
+
+ ); +} + export function Chat(props: { showSideBar?: () => void; sideBarShowing?: boolean; @@ -350,7 +418,7 @@ export function Chat(props: { const [beforeInput, setBeforeInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const { submitKey, shouldSubmit } = useSubmitHandler(); - const { scrollRef, setAutoScroll } = useScrollToBottom(); + const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom(); const [hitBottom, setHitBottom] = useState(false); const onChatBodyScroll = (e: HTMLElement) => { @@ -375,16 +443,6 @@ export function Chat(props: { 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; - }; - // auto grow input const [inputRows, setInputRows] = useState(2); const measure = useDebouncedCallback( @@ -409,7 +467,6 @@ export function Chat(props: { // 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; @@ -533,6 +590,13 @@ export function Chat(props: { const [showPromptModal, setShowPromptModal] = useState(false); + const renameSession = () => { + const newTopic = prompt(Locale.Chat.Rename, session.topic); + if (newTopic && newTopic !== session.topic) { + chatStore.updateCurrentSession((session) => (session.topic = newTopic!)); + } + }; + // Auto focus useEffect(() => { if (props.sideBarShowing && isMobileScreen()) return; @@ -546,14 +610,7 @@ export function Chat(props: {
{ - const newTopic = prompt(Locale.Chat.Rename, session.topic); - if (newTopic && newTopic !== session.topic) { - chatStore.updateCurrentSession( - (session) => (session.topic = newTopic!), - ); - } - }} + onClickCapture={renameSession} > {session.topic}
@@ -572,12 +629,9 @@ export function Chat(props: {
} + icon={} bordered - title={Locale.Chat.Actions.CompressedHistory} - onClick={() => { - setShowPromptModal(true); - }} + onClick={renameSession} />
@@ -672,22 +726,20 @@ export function Chat(props: {
)} - {(message.preview || message.content.length === 0) && - !isUser ? ( - - ) : ( -
onRightClick(e, message)} - onDoubleClickCapture={() => { - if (!isMobileScreen()) return; - setUserInput(message.content); - }} - > - -
- )} + onRightClick(e, message)} + onDoubleClickCapture={() => { + if (!isMobileScreen()) return; + setUserInput(message.content); + }} + fontSize={fontSize} + parentRef={scrollRef} + /> {!isUser && !message.preview && (
@@ -704,6 +756,12 @@ export function Chat(props: {
+ + setShowPromptModal(true)} + scrollToBottom={scrollToBottom} + hitBottom={hitBottom} + />