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/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..b2d1e48ce 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ 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) / [QQ 群](https://user-images.githubusercontent.com/16968934/231095592-330adc52-0337-4c13-8452-938ec169e367.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) [![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,6 +20,7 @@ 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) - 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) @@ -45,6 +46,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. - 在 1 分钟内使用 Vercel **免费一键部署** - 精心设计的 UI,响应式设计,支持深色模式,支持 PWA - 极快的首屏加载速度(~100kb) +- 隐私安全,所有数据保存在用户浏览器本地 - 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) - 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话 - 一键导出聊天记录,完整的 Markdown 支持 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/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/button.module.scss b/app/components/button.module.scss index e7d5d8940..3a3393e7b 100644 --- a/app/components/button.module.scss +++ b/app/components/button.module.scss @@ -49,4 +49,7 @@ .icon-button-text { margin-left: 5px; font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index ab5d849f1..cab8812c3 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -96,7 +96,7 @@ export function ChatList() { index={i} selected={i === selectedIndex} onClick={() => selectSession(i)} - onDelete={chatStore.deleteSession} + onDelete={() => chatStore.deleteSession(i)} /> ))} {provided.placeholder} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 5c7cec3f1..f39a304d5 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -3,7 +3,7 @@ import { memo, 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 ExportIcon from "../icons/share.svg"; import MenuIcon from "../icons/menu.svg"; import CopyIcon from "../icons/copy.svg"; import DownloadIcon from "../icons/download.svg"; @@ -11,6 +11,8 @@ import LoadingIcon from "../icons/three-dots.svg"; import BotIcon from "../icons/bot.svg"; import AddIcon from "../icons/add.svg"; import DeleteIcon from "../icons/delete.svg"; +import MaxIcon from "../icons/max.svg"; +import MinIcon from "../icons/min.svg"; import { Message, @@ -19,6 +21,7 @@ import { BOT_HELLO, ROLES, createMessage, + useAccessStore, } from "../store"; import { @@ -351,6 +354,7 @@ export function Chat(props: { const [hitBottom, setHitBottom] = useState(false); const onChatBodyScroll = (e: HTMLElement) => { + setAutoScroll(false); const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20; setHitBottom(isTouchBottom); }; @@ -485,11 +489,17 @@ export function Chat(props: { const context: RenderMessage[] = session.context.slice(); + const accessStore = useAccessStore(); + if ( context.length === 0 && session.messages.at(0)?.content !== BOT_HELLO.content ) { - context.push(BOT_HELLO); + const copiedHello = Object.assign({}, BOT_HELLO); + if (!accessStore.isAuthorized()) { + copiedHello.content = Locale.Error.Unauthorized; + } + context.push(copiedHello); } // preview messages @@ -584,6 +594,19 @@ export function Chat(props: { }} /> + {!isMobileScreen() && ( +
+ : } + bordered + onClick={() => { + chatStore.updateConfig( + (config) => (config.tightBorder = !config.tightBorder), + ); + }} + /> +
+ )} Math.min(500, Math.max(220, x)); + + const chatStore = useChatStore(); + const startX = useRef(0); + const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300); + const lastUpdateTime = useRef(Date.now()); + + const handleMouseMove = useRef((e: MouseEvent) => { + if (Date.now() < lastUpdateTime.current + 100) { + return; + } + lastUpdateTime.current = Date.now(); + const d = e.clientX - startX.current; + const nextWidth = limit(startDragWidth.current + d); + chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth)); + }); + + const handleMouseUp = useRef(() => { + startDragWidth.current = chatStore.config.sidebarWidth ?? 300; + window.removeEventListener("mousemove", handleMouseMove.current); + window.removeEventListener("mouseup", handleMouseUp.current); + }); + + const onDragMouseDown = (e: MouseEvent) => { + startX.current = e.clientX; + + window.addEventListener("mousemove", handleMouseMove.current); + window.addEventListener("mouseup", handleMouseUp.current); + }; + + useEffect(() => { + if (isMobileScreen()) { + return; + } + + document.documentElement.style.setProperty( + "--sidebar-width", + `${limit(chatStore.config.sidebarWidth ?? 300)}px`, + ); + }, [chatStore.config.sidebarWidth]); + + return { + onDragMouseDown, + }; +} + const useHasHydrated = () => { const [hasHydrated, setHasHydrated] = useState(false); @@ -101,6 +148,9 @@ function _Home() { const [openSettings, setOpenSettings] = useState(false); const config = useChatStore((state) => state.config); + // drag side bar + const { onDragMouseDown } = useDragSideBar(); + useSwitchTheme(); if (loading) { @@ -174,6 +224,11 @@ function _Home() { /> + +
onDragMouseDown(e as any)} + >
diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss index 7d40d83b8..830e1baeb 100644 --- a/app/components/settings.module.scss +++ b/app/components/settings.module.scss @@ -19,11 +19,16 @@ cursor: pointer; } -.password-input { +.password-input-container { + max-width: 50%; display: flex; justify-content: flex-end; .password-eye { margin-right: 4px; } + + .password-input { + min-width: 80%; + } } diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 5800c2124..5bb435f20 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -26,7 +26,7 @@ import { import { Avatar } from "./chat"; import Locale, { AllLangs, changeLang, getLang } from "../locales"; -import { getCurrentVersion, getEmojiUrl } from "../utils"; +import { getEmojiUrl } from "../utils"; import Link from "next/link"; import { UPDATE_URL } from "../constant"; import { SearchService, usePromptStore } from "../store/prompt"; @@ -60,13 +60,17 @@ function PasswordInput(props: HTMLProps) { } return ( -
+
: } onClick={changeVisibility} className={styles["password-eye"]} /> - +
); } @@ -84,13 +88,13 @@ export function Settings(props: { closeSettings: () => void }) { const updateStore = useUpdateStore(); const [checkingUpdate, setCheckingUpdate] = useState(false); - const currentId = getCurrentVersion(); - const remoteId = updateStore.remoteId; - const hasNewVersion = currentId !== remoteId; + const currentVersion = updateStore.version; + const remoteId = updateStore.remoteVersion; + const hasNewVersion = currentVersion !== remoteId; function checkUpdate(force = false) { setCheckingUpdate(true); - updateStore.getLatestCommitId(force).then(() => { + updateStore.getLatestVersion(force).then(() => { setCheckingUpdate(false); }); } @@ -120,8 +124,7 @@ export function Settings(props: { closeSettings: () => void }) { const builtinCount = SearchService.count.builtin; const customCount = promptStore.prompts.size ?? 0; - const showUsage = !!accessStore.token || !!accessStore.accessCode; - + const showUsage = accessStore.isAuthorized(); useEffect(() => { checkUpdate(); showUsage && checkUsage(); @@ -221,7 +224,8 @@ export function Settings(props: { closeSettings: () => void }) { {/* void }) { > - - - - updateConfig( - (config) => - (config.disablePromptHint = e.currentTarget.checked), - ) - } - > - - - } - text={Locale.Settings.Prompt.Edit} - onClick={() => showToast(Locale.WIP)} - /> - - {enabledAccessControl ? ( void }) { + + + + updateConfig( + (config) => + (config.disablePromptHint = e.currentTarget.checked), + ) + } + > + + + + } + text={Locale.Settings.Prompt.Edit} + onClick={() => showToast(Locale.WIP)} + /> + + +