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 6f7547b21..7ed7bc155 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,6 +8,7 @@ WORKDIR /app
COPY package.json yarn.lock ./
+RUN yarn config set registry 'https://registry.npm.taobao.org'
RUN yarn install
FROM base AS builder
@@ -16,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
@@ -27,6 +27,9 @@ RUN yarn build
FROM base AS runner
WORKDIR /app
+RUN apk add proxychains-ng
+
+ENV PROXY_URL=""
ENV OPENAI_API_KEY=""
ENV CODE=""
@@ -37,4 +40,20 @@ COPY --from=builder /app/.next/server ./.next/server
EXPOSE 3000
-CMD ["node","server.js"]
+CMD if [ -n "$PROXY_URL" ]; then \
+ protocol=$(echo $PROXY_URL | cut -d: -f1); \
+ 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 "proxy_dns" >> $conf; \
+ echo "remote_dns_subnet 224" >> $conf; \
+ echo "tcp_read_time_out 15000" >> $conf; \
+ echo "tcp_connect_time_out 8000" >> $conf; \
+ echo "[ProxyList]" >> $conf; \
+ echo "$protocol $host $port" >> $conf; \
+ cat /etc/proxychains.conf; \
+ proxychains -f $conf node server.js; \
+ else \
+ node server.js; \
+ fi
diff --git a/README.md b/README.md
index 55a6da404..8a09a0657 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)
[](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,13 +49,15 @@ 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 支持
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
-## 开发计划
+## 开发计划
+
- [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
- [ ] 允许用户自行编辑内置 Prompt 列表
- [ ] 使用 tauri 打包桌面应用
@@ -58,11 +65,12 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
### 不会开发的功能
+
- 界面文字自定义
- 用户登录、账号管理、消息云同步
-
## Get Started
+
> [简体中文 > 如何开始使用](./README_CN.md#开始使用)
1. Get [OpenAI API Key](https://platform.openai.com/account/api-keys);
@@ -71,27 +79,37 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
3. Enjoy :)
## FAQ
-[简体中文 > 常见问题](./docs/faq-cn.md) | [English > FAQ](./docs/faq.en.md)
+
+[简体中文 > 常见问题](./docs/faq-cn.md)
+
+[English > FAQ](./docs/faq-en.md)
## Keep Updated
+
> [简体中文 > 如何保持代码更新](./README_CN.md#保持更新)
If you have deployed your own project with just one click following the steps above, you may encounter the issue of "Updates Available" constantly showing up. This is because Vercel will create a new project for you by default instead of forking this project, resulting in the inability to detect updates correctly.
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:
-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.
+
+
+
+
+### 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.
You can star or watch this project or follow author to get release notifictions in time.
## Access Password
+
> [简体中文 > 如何增加访问密码](./README_CN.md#配置页面访问密码)
This project provides limited access control. Please add an environment variable named `CODE` on the vercel environment variables page. The value should be passwords separated by comma like this:
@@ -103,6 +121,7 @@ code1,code2,code3
After adding or modifying this environment variable, please redeploy the project for the changes to take effect.
## Environment Variables
+
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
### `OPENAI_API_KEY` (required)
@@ -128,6 +147,7 @@ Override openai api request base url.
Override openai api request protocol.
## Development
+
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
@@ -149,13 +169,28 @@ yarn dev
```
## Deployment
+
> [简体中文 > 如何部署到私人服务器](./README_CN.md#部署)
+
### Docker (Recommended)
```shell
docker pull yidadaa/chatgpt-next-web
-docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-web
+docker run -d -p 3000:3000 \
+ -e OPENAI_API_KEY="sk-xxxx" \
+ -e CODE="your-password" \
+ yidadaa/chatgpt-next-web
+```
+
+You can start service behind a proxy:
+
+```shell
+docker run -d -p 3000:3000 \
+ -e OPENAI_API_KEY="sk-xxxx" \
+ -e CODE="your-password" \
+ -e PROXY_URL="http://localhost:7890" \
+ yidadaa/chatgpt-next-web
```
### Shell
@@ -163,6 +198,7 @@ docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
+
## Screenshots

@@ -170,11 +206,15 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s

## Donation
+
[Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
## Special Thanks
+
### Sponsor
+> 仅列出捐赠金额 >= 100RMB 的用户。
+
[@mushan0x0](https://github.com/mushan0x0)
[@ClarenceDan](https://github.com/ClarenceDan)
[@zhangjia](https://github.com/zhangjia)
@@ -184,6 +224,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 efd5d56a1..7d1a835c4 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -38,12 +38,18 @@
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 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,启用之后即可开启每小时定时自动更新:
+
+
+
+
+
+### 手动更新代码
如果你想让手动立即更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步。
@@ -64,6 +70,7 @@ code1,code2,code3
增加或修改该环境变量后,请**重新部署**项目使改动生效。
## 环境变量
+
> 本项目大多数配置项都通过环境变量来设置。
### `OPENAI_API_KEY` (必填项)
@@ -110,37 +117,51 @@ OPENAI_API_KEY=
2. 执行 `yarn install && yarn dev` 即可。
## 部署
+
### 容器部署 (推荐)
+
> 注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。
```shell
docker pull yidadaa/chatgpt-next-web
-docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-web
+docker run -d -p 3000:3000 \
+ -e OPENAI_API_KEY="sk-xxxx" \
+ -e CODE="页面访问密码" \
+ yidadaa/chatgpt-next-web
+```
+
+你也可以指定 proxy:
+
+```shell
+docker run -d -p 3000:3000 \
+ -e OPENAI_API_KEY="sk-xxxx" \
+ -e CODE="页面访问密码" \
+ --net=host \
+ -e PROXY_URL="http://127.0.0.1:7890" \
+ yidadaa/chatgpt-next-web
```
### 本地部署
+
在控制台运行下方命令:
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
-
## 鸣谢
-### 捐赠者
-> 仅列出了部分大额打赏,小额打赏(< 100RMB)人数太多,在此不再列出,敬请谅解。
-[@mushan0x0](https://github.com/mushan0x0)
-[@ClarenceDan](https://github.com/ClarenceDan)
-[@zhangjia](https://github.com/zhangjia)
-[@hoochanlon](https://github.com/hoochanlon)
+### 捐赠者
+
+> 见英文版。
### 贡献者
[见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
## 开源协议
+
> 反对 996,从我开始。
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
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.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 c5c257e54..b9ae13926 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 ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
@@ -11,6 +11,13 @@ 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 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,
@@ -19,6 +26,8 @@ import {
BOT_HELLO,
ROLES,
createMessage,
+ useAccessStore,
+ Theme,
} from "../store";
import {
@@ -28,6 +37,7 @@ import {
isMobileScreen,
selectOrCopy,
autoGrowTextArea,
+ getCSSVar,
} from "../utils";
import dynamic from "next/dynamic";
@@ -57,7 +67,11 @@ export function Avatar(props: { role: Message["role"] }) {
const config = useChatStore((state) => state.config);
if (props.role !== "user") {
- return ;
+ return (
+
+
+
+ );
}
return (
@@ -313,22 +327,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;
@@ -347,7 +417,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) => {
@@ -485,11 +555,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 +660,19 @@ export function Chat(props: {
}}
/>
+ {!isMobileScreen() && (
+
+ : }
+ bordered
+ onClick={() => {
+ chatStore.updateConfig(
+ (config) => (config.tightBorder = !config.tightBorder),
+ );
+ }}
+ />
+
+ )}
)}
- {(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 && (
@@ -682,6 +769,12 @@ export function Chat(props: {
+
+
setShowPromptModal(true)}
+ scrollToBottom={scrollToBottom}
+ hitBottom={hitBottom}
+ />
+
+
onDragMouseDown(e as any)}
+ >
diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx
index c8076640c..25d0584f6 100644
--- a/app/components/markdown.tsx
+++ b/app/components/markdown.tsx
@@ -8,6 +8,8 @@ import RehypeHighlight from "rehype-highlight";
import { useRef, useState, RefObject, useEffect } from "react";
import { copyToClipboard } from "../utils";
+import LoadingIcon from "../icons/three-dots.svg";
+
export function PreCode(props: { children: any }) {
const ref = useRef(null);
@@ -27,49 +29,78 @@ export function PreCode(props: { children: any }) {
);
}
-const useLazyLoad = (ref: RefObject): boolean => {
- const [isIntersecting, setIntersecting] = useState(false);
+export function Markdown(
+ props: {
+ content: string;
+ loading?: boolean;
+ fontSize?: number;
+ parentRef: RefObject;
+ } & React.DOMAttributes,
+) {
+ const mdRef = useRef(null);
+
+ const parent = props.parentRef.current;
+ const md = mdRef.current;
+ const rendered = useRef(true); // disable lazy loading for bad ux
+ const [counter, setCounter] = useState(0);
useEffect(() => {
- const observer = new IntersectionObserver(([entry]) => {
- if (entry.isIntersecting) {
- setIntersecting(true);
- observer.disconnect();
+ // to triggr rerender
+ setCounter(counter + 1);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [props.loading]);
+
+ const inView =
+ rendered.current ||
+ (() => {
+ if (parent && md) {
+ const parentBounds = parent.getBoundingClientRect();
+ const mdBounds = md.getBoundingClientRect();
+ const isInRange = (x: number) =>
+ x <= parentBounds.bottom && x >= parentBounds.top;
+ const inView = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
+
+ if (inView) {
+ rendered.current = true;
+ }
+
+ return inView;
}
- });
+ })();
- if (ref.current) {
- observer.observe(ref.current);
- }
+ const shouldLoading = props.loading || !inView;
- return () => {
- observer.disconnect();
- };
- }, [ref]);
-
- return isIntersecting;
-};
-
-export function Markdown(props: { content: string }) {
return (
-
- {props.content}
-
+ {shouldLoading ? (
+
+ ) : (
+
+ {props.content}
+
+ )}
+
);
}
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 0d1c1d05c..e418d4843 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,7 @@ 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)}
+ />
+
+
+