Compare commits

...

62 Commits
v1.2 ... v1.6

Author SHA1 Message Date
Yifei Zhang
7f3cbaa064 Merge pull request #100 from iSource/fix-docker-access-code
fix: docker access code setting missing
2023-03-28 18:52:46 +08:00
iSource
eb72c83b7e fix: docker access code setting missing 2023-03-28 17:55:03 +08:00
Yifei Zhang
e93ea0fa97 Update README.md 2023-03-28 17:21:27 +08:00
Yifei Zhang
3b6f93afdf feat: add one-key setup script 2023-03-28 16:51:23 +08:00
Yifei Zhang
4597a2286a Update README.md 2023-03-28 14:53:07 +08:00
Yifei Zhang
780968979d Update README.md 2023-03-28 13:36:08 +08:00
Yifei Zhang
adc0db4c74 Merge pull request #76 from xiaotianxt/main
feat: CmdEnter config for submitkey, bug fix for auto scrolling
2023-03-28 13:35:36 +08:00
Yifei Zhang
f0dd95a2a3 Merge pull request #87 from Yidadaa/bugfix-0328
fix: light theme code highlight
2023-03-28 13:34:39 +08:00
Yifei Zhang
6155a190ac fix: light theme code highlight 2023-03-28 05:24:51 +00:00
xiaotianxt
493aa8c591 refactor: refocus textarea after btn clicked 2023-03-28 13:02:49 +08:00
xiaotianxt
6c82f804ae fix: Enter key bug 2023-03-28 12:56:36 +08:00
xiaotianxt
a2807c9815 fix(scroll): scroll after click submit button
The behavior of SubmitKey and SubmitButton should be the same
2023-03-28 12:56:36 +08:00
xiaotianxt
d822f333c2 feat(SubmitKey): add MetaEnter option
Add another option for MacOS user who prefer Cmd+Enter
or Linux user who prefer Meta+Enter.
2023-03-28 12:56:36 +08:00
Yifei Zhang
8f498075b9 Merge pull request #81 from iSource/fix-code-copy-button
fix: code copy button position
2023-03-28 11:57:03 +08:00
Yifei Zhang
c4bf6a6383 Merge pull request #84 from iSource/add-robots-txt
feat: add robots.txt
2023-03-28 11:56:47 +08:00
Yifei Zhang
939402b2d9 Merge pull request #85 from AprilNEA/fix-mobile
fix: fix #82, close sidebar after new session
2023-03-28 11:56:29 +08:00
AprilNEA
684a3c41ef fix: fix #82, close sidebar after new session 2023-03-28 11:51:36 +08:00
iSource
306f0850e9 feat: add robots.txt 2023-03-28 11:25:47 +08:00
iSource
55f37248f7 fix: code copy button position 2023-03-28 10:50:50 +08:00
Yifei Zhang
c93a46a02f Update README.md 2023-03-28 02:15:52 +08:00
Yifei Zhang
77a3fdea5f Update README.md 2023-03-27 23:02:39 +08:00
Yifei Zhang
cc1a1d4f3c feat: #27 add docker image publish actions 2023-03-27 14:46:30 +00:00
Yifei Zhang
0463b350d8 feat: #24 docker publish actions 2023-03-27 22:44:09 +08:00
Yifei Zhang
8f87a68f72 Merge pull request #39 from AprilNEA/docker
feat: add docker deployment support
2023-03-27 22:41:20 +08:00
Yifei Zhang
60f27fdfbb Merge pull request #68 from AprilNEA/main
fix(#65): fix unknown git commit id
2023-03-27 22:01:19 +08:00
Yifei Zhang
d17706636b Merge pull request #66 from iFwu/main
fix: resolve hydration error
2023-03-27 21:59:19 +08:00
Yifei Zhang
9570691d5b Delete userRole.tsx 2023-03-27 21:52:50 +08:00
AprilNEA
efe4fcc188 Merge branch 'Yidadaa:main' into main 2023-03-27 18:31:47 +08:00
AprilNEA
efaf6590ef fix(#65): fix unknown git commit id 2023-03-27 18:31:04 +08:00
伏晓
fb06fb8c38 fix: resolve hydration error 2023-03-27 18:22:55 +08:00
Yifei Zhang
f188841188 Merge pull request #67 from iSource/pwa-support
feat: add PWA support
2023-03-27 18:19:49 +08:00
jianjian.ma
5593c067c4 feat: add PWA support 2023-03-27 18:02:35 +08:00
jianjian.ma
689b7bab26 feat: add PWA support 2023-03-27 17:53:32 +08:00
Yifei Zhang
a81e7394f0 Update README.md 2023-03-27 17:45:36 +08:00
Yifei Zhang
492fed6802 Merge pull request #64 from AprilNEA/fix-theme
Fix broswer tasklist and support safari webapp #54
2023-03-27 17:44:21 +08:00
jianjian.ma
bdf17fafff feat: add PWA support 2023-03-27 17:41:44 +08:00
AprilNEA
58baa23199 Merge branch 'fix-theme' of https://github.com/AprilNEA/ChatGPT-Next-Web into fix-theme 2023-03-27 17:05:15 +08:00
AprilNEA
dd80c4563d feat: adding iOS Webapp support
- fix media query about background-color
- Use colors from CSS files instead of fixed values
2023-03-27 17:04:11 +08:00
AprilNEA
785372ad73 fix: fix the different colors on mobile 2023-03-27 15:47:46 +08:00
AprilNEA
d8e4808316 Merge branch 'Yidadaa:main' into fix-theme 2023-03-27 15:10:10 +08:00
AprilNEA
6446692db0 feat: support safari appleWebApp 2023-03-27 15:02:12 +08:00
AprilNEA
cd73c3a7cb fix: taskbar color follow(#54) 2023-03-27 15:01:35 +08:00
AprilNEA
d0eee767fa fix: resolve conflict 2023-03-27 14:01:03 +08:00
AprilNEA
e880df6db9 docs: add instructions for docker deployment 2023-03-27 13:59:23 +08:00
AprilNEA
96c4f5bbd9 Merge branch 'Yidadaa:main' into docker 2023-03-27 13:53:46 +08:00
AprilNEA
2645540721 perf: build in stages to reduce container size 2023-03-27 13:49:26 +08:00
Yifei Zhang
b1f27aaf93 Merge pull request #61 from Yidadaa/bugfix-0327
fix: #7 disable light code theme
2023-03-27 11:07:07 +08:00
Yifei Zhang
fb2d281aac fix: #7 disable light code theme 2023-03-27 03:02:25 +00:00
Yifei Zhang
84d73fa1f2 Merge pull request #48 from Yidadaa/custom-token
v1.4 Custom Api Key & Copy Code Button
2023-03-26 20:36:45 +08:00
Yifei Zhang
f858407f9a fixup 2023-03-26 12:35:15 +00:00
Yifei Zhang
b57663bf02 feat: now support gpt-4 model 2023-03-26 12:32:22 +00:00
Yifei Zhang
e57bd51809 feat: #9 add copy code button 2023-03-26 12:29:02 +00:00
Yifei Zhang
df66eef919 feat: support using user api key 2023-03-26 11:58:25 +00:00
Yifei Zhang
f1b6641f19 Update README.md 2023-03-26 19:28:30 +08:00
Yifei Zhang
bb45c62a81 Merge pull request #45 from Yidadaa/bugfix-0326
v1.3 Stop and Retry Button
2023-03-26 19:16:02 +08:00
Yifei Zhang
1e89fe14ac fix: #34 only auto scroll when textbox is focused 2023-03-26 11:10:34 +00:00
Yifei Zhang
86507fa569 feat: #2 #8 add stop and retry button 2023-03-26 10:59:09 +00:00
Yifei Zhang
4180363f58 Update README.md 2023-03-26 17:51:46 +08:00
Yifei Zhang
a5ec15236a fix: #38 high resolution favicon 2023-03-26 09:32:47 +00:00
AprilNEA
8d0d08725d feat: add Dockerfile for docker deployment support 2023-03-26 16:56:05 +08:00
Yifei Zhang
43b6835564 Update README.md 2023-03-26 16:52:00 +08:00
Yifei Zhang
3f865ffa1e Update README.md 2023-03-26 15:18:34 +08:00
34 changed files with 548 additions and 98 deletions

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
public/serviceWorker.js

33
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Publish Docker image
on:
release:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: yidadaa/chatgpt-next-web
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

44
Dockerfile Normal file
View File

@@ -0,0 +1,44 @@
FROM node:18-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base AS builder
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
COPY . .
RUN yarn build
FROM base AS runner
WORKDIR /app
ENV OPENAI_API_KEY=""
ENV CODE=""
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/server ./.next/server
EXPOSE 3000
CMD ["node","server.js"]

View File

@@ -7,7 +7,7 @@
One-Click to deploy your own ChatGPT web UI.
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈问题 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 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)
[![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&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
@@ -17,15 +17,6 @@ One-Click to deploy your own ChatGPT web UI.
</div>
## 重要说明 Attention
本项目的演示地址所用的 OpenAI 账户的免费额度将于 2023-04-01 过期,届时将无法通过演示地址在线体验。
如果你想贡献出自己的 API Key可以通过作者主页的邮箱发送给作者并标注过期时间在此提前感谢
The free trial of the OpenAI account used by the demo will expire on April 1, 2023, and the demo will not be available at that time.
If you would like to contribute your API key, you can email it to the author and indicate the expiration date of the API key. Thank you in advance!
## 主要功能
- 在 1 分钟内使用 Vercel **免费一键部署**
@@ -93,7 +84,7 @@ You can star or watch this project or follow author to get release notifictions
code1,code2,code3
```
增加或修改该环境变量后,请重新部署项目使改动生效。
增加或修改该环境变量后,请**重新部署**项目使改动生效。
This project provides limited access control. Please add an environment variable named `CODE` on the environment variables page. The value should be a custom control code separated by comma like this:
@@ -118,31 +109,24 @@ OPENAI_API_KEY=<your api key here>
```
### 本地开发 Local Development
> 如果你是中国大陆用户,不建议在本地进行开发,除非你能够独立解决 OpenAI API 本地代理问题。
1. 安装 nodejs 和 yarn具体细节请询问 ChatGPT
2. 执行 `yarn install && yarn dev` 即可。
### 本地部署 Local Deployment
请直接询问 ChatGPT使用下列 Prompt
```
如何使用 pm2 和 yarn 部署 nextjs 项目到 ubuntu 服务器上,项目编译命令为 yarn build启动命令为 yarn start启动时需要设置环境变量为 OPENAI_API_KEY端口为 3000使用 ngnix 做反向代理
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
Please ask ChatGPT with prompt:
```
how to deploy nextjs project with pm2 and yarn on my ubuntu server, the build command is `yarn build`, the start command is `yarn start`, the project must start with env var named `OPENAI_API_KEY`, the port is 3000, use ngnix
```
### Docker Deployment
请直接询问 ChatGPT使用下列 Prompt
```
如何使用 docker 部署 nextjs 项目到 ubuntu 服务器上,项目编译命令为 yarn build启动命令为 yarn start启动时需要设置环境变量为 OPENAI_API_KEY端口为 3000使用 ngnix 做反向代理
```
### 容器部署 Docker Deployment
Please ask ChatGPT with prompt:
```
how to deploy nextjs project with docker on my ubuntu server, the build command is `yarn build`, the start command is `yarn start`, the project must start with env var named `OPENAI_API_KEY`, the port is 3000, use ngnix
```shell
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-web
```
## 截图 Screenshots
@@ -151,6 +135,31 @@ how to deploy nextjs project with docker on my ubuntu server, the build command
![更多展示 More](./static/more.png)
## 说明 Attention
本项目的演示地址所用的 OpenAI 账户的免费额度将于 2023-04-01 过期,届时将无法通过演示地址在线体验。
如果你想贡献出自己的 API Key可以通过作者主页的邮箱发送给作者并标注过期时间。
The free trial of the OpenAI account used by the demo will expire on April 1, 2023, and the demo will not be available at that time.
If you would like to contribute your API key, you can email it to the author and indicate the expiration date of the API key.
## 鸣谢 Special Thanks
### 捐赠者 Sponsor
[@mushan0x0](https://github.com/mushan0x0)
[@ClarenceDan](https://github.com/ClarenceDan)
[@zhangjia](https://github.com/zhangjia)
### 贡献者 Contributor
[@AprilNEA](https://github.com/AprilNEA)
[@iSource](https://github.com/iSource)
[@iFwu](https://github.com/iFwu)
[@xiaotianxt](https://github.com/xiaotianxt)
## LICENSE
- [Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)

View File

@@ -14,3 +14,4 @@ export function getAccessCodes(): Set<string> {
}
export const ACCESS_CODES = getAccessCodes();
export const IS_IN_DOCKER = process.env.DOCKER;

View File

@@ -1,20 +1,25 @@
import type { ChatRequest } from "../chat/typing";
import { createParser } from "eventsource-parser";
import { NextRequest } from "next/server";
const apiKey = process.env.OPENAI_API_KEY;
async function createStream(payload: ReadableStream<Uint8Array>) {
async function createStream(req: NextRequest) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
let apiKey = process.env.OPENAI_API_KEY;
const userApiKey = req.headers.get("token");
if (userApiKey) {
apiKey = userApiKey;
console.log("[Stream] using user api key");
}
const res = await fetch("https://api.openai.com/v1/chat/completions", {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
method: "POST",
body: payload,
body: req.body,
});
const stream = new ReadableStream({
@@ -49,7 +54,7 @@ async function createStream(payload: ReadableStream<Uint8Array>) {
export async function POST(req: NextRequest) {
try {
const stream = await createStream(req.body!);
const stream = await createStream(req);
return new Response(stream);
} catch (error) {
console.error("[Chat Stream]", error);

View File

@@ -1,23 +1,26 @@
import { OpenAIApi, Configuration } from "openai";
import { ChatRequest } from "./typing";
const apiKey = process.env.OPENAI_API_KEY;
const openai = new OpenAIApi(
new Configuration({
apiKey,
})
);
export async function POST(req: Request) {
try {
const requestBody = (await req.json()) as ChatRequest;
const completion = await openai!.createChatCompletion(
{
...requestBody,
}
let apiKey = process.env.OPENAI_API_KEY;
const userApiKey = req.headers.get("token");
if (userApiKey) {
apiKey = userApiKey;
}
const openai = new OpenAIApi(
new Configuration({
apiKey,
})
);
const requestBody = (await req.json()) as ChatRequest;
const completion = await openai!.createChatCompletion({
...requestBody,
});
return new Response(JSON.stringify(completion.data));
} catch (e) {
console.error("[Chat] ", e);

View File

@@ -27,6 +27,7 @@ import Locale from "../locales";
import dynamic from "next/dynamic";
import { REPO_URL } from "../constant";
import { ControllerPool } from "../requests";
export function Loading(props: { noLogo?: boolean }) {
return (
@@ -130,10 +131,12 @@ function useSubmitHandler() {
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
(config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
(config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
(config.submitKey === SubmitKey.Enter &&
!e.altKey &&
!e.ctrlKey &&
!e.shiftKey)
!e.shiftKey &&
!e.metaKey)
);
};
@@ -146,28 +149,69 @@ function useSubmitHandler() {
export function Chat(props: { showSideBar?: () => void }) {
type RenderMessage = Message & { preview?: boolean };
const session = useChatStore((state) => state.currentSession());
const [session, sessionIndex] = useChatStore((state) => [
state.currentSession(),
state.currentSessionIndex,
]);
const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
const onUserInput = useChatStore((state) => state.onUserInput);
// submit user input
const onUserSubmit = () => {
if (userInput.length <= 0) return;
setIsLoading(true);
onUserInput(userInput).then(() => setIsLoading(false));
setUserInput("");
inputRef.current?.focus();
};
// stop response
const onUserStop = (messageIndex: number) => {
console.log(ControllerPool, sessionIndex, messageIndex);
ControllerPool.stop(sessionIndex, messageIndex);
};
// check if should send message
const onInputKeyDown = (e: KeyboardEvent) => {
if (shouldSubmit(e)) {
onUserSubmit();
e.preventDefault();
}
};
const onRightClick = (e: any, message: Message) => {
// auto fill user input
if (message.role === "user") {
setUserInput(message.content);
}
// copy to clipboard
if (selectOrCopy(e.currentTarget, message.content)) {
e.preventDefault();
}
};
const onResend = (botIndex: number) => {
// find last user input message and resend
for (let i = botIndex; i >= 0; i -= 1) {
if (messages[i].role === "user") {
setIsLoading(true);
onUserInput(messages[i].content).then(() => setIsLoading(false));
return;
}
}
};
// for auto-scroll
const latestMessageRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [hoveringMessage, setHoveringMessage] = useState(false);
// wont scroll while hovering messages
const [autoScroll, setAutoScroll] = useState(false);
// preview messages
const messages = (session.messages as RenderMessage[])
.concat(
isLoading
@@ -194,10 +238,11 @@ export function Chat(props: { showSideBar?: () => void }) {
: []
);
// auto scroll
useLayoutEffect(() => {
setTimeout(() => {
const dom = latestMessageRef.current;
if (dom && !isIOS() && !hoveringMessage) {
if (dom && !isIOS() && autoScroll) {
dom.scrollIntoView({
behavior: "smooth",
block: "end",
@@ -252,15 +297,7 @@ export function Chat(props: { showSideBar?: () => void }) {
</div>
</div>
<div
className={styles["chat-body"]}
onMouseOver={() => {
setHoveringMessage(true);
}}
onMouseOut={() => {
setHoveringMessage(false);
}}
>
<div className={styles["chat-body"]}>
{messages.map((message, i) => {
const isUser = message.role === "user";
@@ -283,13 +320,20 @@ export function Chat(props: { showSideBar?: () => void }) {
<div className={styles["chat-message-item"]}>
{!isUser && (
<div className={styles["chat-message-top-actions"]}>
{message.streaming && (
{message.streaming ? (
<div
className={styles["chat-message-top-action"]}
onClick={() => showToast(Locale.WIP)}
onClick={() => onUserStop(i)}
>
{Locale.Chat.Actions.Stop}
</div>
) : (
<div
className={styles["chat-message-top-action"]}
onClick={() => onResend(i)}
>
{Locale.Chat.Actions.Retry}
</div>
)}
<div
@@ -306,11 +350,7 @@ export function Chat(props: { showSideBar?: () => void }) {
) : (
<div
className="markdown-body"
onContextMenu={(e) => {
if (selectOrCopy(e.currentTarget, message.content)) {
e.preventDefault();
}
}}
onContextMenu={(e) => onRightClick(e, message)}
>
<Markdown content={message.content} />
</div>
@@ -335,12 +375,16 @@ export function Chat(props: { showSideBar?: () => void }) {
<div className={styles["chat-input-panel"]}>
<div className={styles["chat-input-panel-inner"]}>
<textarea
ref={inputRef}
className={styles["chat-input"]}
placeholder={Locale.Chat.Input(submitKey)}
rows={3}
onInput={(e) => setUserInput(e.currentTarget.value)}
value={userInput}
onKeyDown={(e) => onInputKeyDown(e as any)}
onFocus={() => setAutoScroll(true)}
onBlur={() => setAutoScroll(false)}
autoFocus
/>
<IconButton
icon={<SendWhiteIcon />}
@@ -360,11 +404,16 @@ function useSwitchTheme() {
useEffect(() => {
document.body.classList.remove("light");
document.body.classList.remove("dark");
if (config.theme === "dark") {
document.body.classList.add("dark");
} else if (config.theme === "light") {
document.body.classList.add("light");
}
const themeColor = getComputedStyle(document.body).getPropertyValue("--theme-color").trim();
const metaDescription = document.querySelector('meta[name="theme-color"]');
metaDescription?.setAttribute('content', themeColor);
}, [config.theme]);
}
@@ -426,6 +475,16 @@ function showMemoryPrompt(session: ChatSession) {
});
}
const useHasHydrated = () => {
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
useEffect(() => {
setHasHydrated(true);
}, []);
return hasHydrated;
};
export function Home() {
const [createNewSession, currentIndex, removeSession] = useChatStore(
(state) => [
@@ -434,7 +493,7 @@ export function Home() {
state.removeSession,
]
);
const loading = !useChatStore?.persist?.hasHydrated();
const loading = !useHasHydrated();
const [showSideBar, setShowSideBar] = useState(true);
// setting
@@ -507,7 +566,10 @@ export function Home() {
<IconButton
icon={<AddIcon />}
text={Locale.Home.NewChat}
onClick={createNewSession}
onClick={()=>{
createNewSession();
setShowSideBar(false);
}}
/>
</div>
</div>

View File

@@ -4,15 +4,36 @@ import RemarkMath from "remark-math";
import RehypeKatex from "rehype-katex";
import RemarkGfm from "remark-gfm";
import RehypePrsim from "rehype-prism-plus";
import { useRef } from "react";
import { copyToClipboard } from "../utils";
export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null);
return (
<pre ref={ref}>
<span
className="copy-code-button"
onClick={() => {
if (ref.current) {
const code = ref.current.innerText;
copyToClipboard(code);
}
}}
></span>
{props.children}
</pre>
);
}
export function Markdown(props: { content: string }) {
return (
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm]}
rehypePlugins={[
RehypeKatex,
[RehypePrsim, { ignoreMissing: true }],
]}
rehypePlugins={[RehypeKatex, [RehypePrsim, { ignoreMissing: true }]]}
components={{
pre: PreCode,
}}
>
{props.content}
</ReactMarkdown>

View File

@@ -257,6 +257,20 @@ export function Settings(props: { closeSettings: () => void }) {
<></>
)}
<SettingItem
title={Locale.Settings.Token.Title}
subTitle={Locale.Settings.Token.SubTitle}
>
<input
value={accessStore.token}
type="text"
placeholder={Locale.Settings.Token.Placeholder}
onChange={(e) => {
accessStore.updateToken(e.currentTarget.value);
}}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
@@ -340,7 +354,7 @@ export function Settings(props: { closeSettings: () => void }) {
<input
type="number"
min={100}
max={4000}
max={4096}
value={config.modelConfig.max_tokens}
onChange={(e) =>
updateConfig(

View File

@@ -3,22 +3,32 @@ import "./styles/globals.scss";
import "./styles/markdown.scss";
import "./styles/prism.scss";
import process from "child_process";
import { ACCESS_CODES } from "./api/access";
import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
const COMMIT_ID = process
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
let COMMIT_ID: string | undefined;
try {
COMMIT_ID = process
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
} catch (e) {
console.error("No git or not from git repo.")
}
export const metadata = {
title: "ChatGPT Next Web",
description: "Your personal ChatGPT Chat Bot.",
appleWebApp: {
title: "ChatGPT Next Web",
statusBarStyle: "black-translucent",
},
themeColor: "#fafafa"
};
function Meta() {
const metas = {
version: COMMIT_ID,
access: ACCESS_CODES.size > 0 ? "enabled" : "disabled",
version: COMMIT_ID ?? "unknown",
access: (ACCESS_CODES.size > 0 || IS_IN_DOCKER) ? "enabled" : "disabled",
};
return (
@@ -50,6 +60,7 @@ export default function RootLayout({
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap"
rel="stylesheet"
></link>
<script src="/serviceWorkerRegister.js" defer></script>
</head>
<body>{children}</body>
</html>

View File

@@ -14,6 +14,7 @@ const cn = {
Export: "导出聊天记录",
Copy: "复制",
Stop: "停止",
Retry: "重试",
},
Typing: "正在输入…",
Input: (submitKey: string) => `输入消息,${submitKey} 发送`,
@@ -68,6 +69,11 @@ const cn = {
Title: "历史消息长度压缩阈值",
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
},
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可绕过受控访问限制",
Placeholder: "OpenAI API Key",
},
AccessCode: {
Title: "访问码",
SubTitle: "现在是受控访问状态",

View File

@@ -17,6 +17,7 @@ const en: LocaleType = {
Export: "Export All Messages as Markdown",
Copy: "Copy",
Stop: "Stop",
Retry: "Retry",
},
Typing: "Typing…",
Input: (submitKey: string) =>
@@ -73,6 +74,11 @@ const en: LocaleType = {
SubTitle:
"Will compress if uncompressed messages length exceeds the value",
},
Token: {
Title: "API Key",
SubTitle: "Use your key to ignore access code limit",
Placeholder: "OpenAI API Key",
},
AccessCode: {
Title: "Access Code",
SubTitle: "Access control enabled",

View File

@@ -1,5 +1,5 @@
import { Analytics } from "@vercel/analytics/react";
import { Home } from './components/home'
import { Home } from "./components/home";
export default function App() {
return (

View File

@@ -35,6 +35,10 @@ function getHeaders() {
headers["access-code"] = accessStore.accessCode;
}
if (accessStore.token && accessStore.token.length > 0) {
headers["token"] = accessStore.token;
}
return headers;
}
@@ -60,6 +64,7 @@ export async function requestChatStream(
modelConfig?: ModelConfig;
onMessage: (message: string, done: boolean) => void;
onError: (error: Error) => void;
onController?: (controller: AbortController) => void;
}
) {
const req = makeRequestParam(messages, {
@@ -96,12 +101,12 @@ export async function requestChatStream(
controller.abort();
};
console.log(res);
if (res.ok) {
const reader = res.body?.getReader();
const decoder = new TextDecoder();
options?.onController?.(controller);
while (true) {
// handle time out, will stop if no response in 10 secs
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
@@ -146,3 +151,34 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
return res.choices.at(0)?.message?.content ?? "";
}
// To store message streaming controller
export const ControllerPool = {
controllers: {} as Record<string, AbortController>,
addController(
sessionIndex: number,
messageIndex: number,
controller: AbortController
) {
const key = this.key(sessionIndex, messageIndex);
this.controllers[key] = controller;
return key;
},
stop(sessionIndex: number, messageIndex: number) {
const key = this.key(sessionIndex, messageIndex);
const controller = this.controllers[key];
console.log(controller);
controller?.abort();
},
remove(sessionIndex: number, messageIndex: number) {
const key = this.key(sessionIndex, messageIndex);
delete this.controllers[key];
},
key(sessionIndex: number, messageIndex: number) {
return `${sessionIndex},${messageIndex}`;
},
};

View File

@@ -4,7 +4,9 @@ import { queryMeta } from "../utils";
export interface AccessControlStore {
accessCode: string;
token: string;
updateToken: (_: string) => void;
updateCode: (_: string) => void;
enabledAccessControl: () => boolean;
}
@@ -14,6 +16,7 @@ export const ACCESS_KEY = "access-control";
export const useAccessStore = create<AccessControlStore>()(
persist(
(set, get) => ({
token: "",
accessCode: "",
enabledAccessControl() {
return queryMeta("access") === "enabled";
@@ -21,6 +24,9 @@ export const useAccessStore = create<AccessControlStore>()(
updateCode(code: string) {
set((state) => ({ accessCode: code }));
},
updateToken(token: string) {
set((state) => ({ token }));
},
}),
{
name: ACCESS_KEY,

View File

@@ -2,7 +2,11 @@ import { create } from "zustand";
import { persist } from "zustand/middleware";
import { type ChatCompletionResponseMessage } from "openai";
import { requestChatStream, requestWithPrompt } from "../requests";
import {
ControllerPool,
requestChatStream,
requestWithPrompt,
} from "../requests";
import { trimTopic } from "../utils";
import Locale from "../locales";
@@ -17,6 +21,7 @@ export enum SubmitKey {
CtrlEnter = "Ctrl + Enter",
ShiftEnter = "Shift + Enter",
AltEnter = "Alt + Enter",
MetaEnter = "Meta + Enter",
}
export enum Theme {
@@ -45,22 +50,24 @@ export interface ChatConfig {
export type ModelConfig = ChatConfig["modelConfig"];
const ENABLE_GPT4 = true;
export const ALL_MODELS = [
{
name: "gpt-4",
available: false,
available: ENABLE_GPT4,
},
{
name: "gpt-4-0314",
available: false,
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k",
available: false,
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k-0314",
available: false,
available: ENABLE_GPT4,
},
{
name: "gpt-3.5-turbo",
@@ -296,6 +303,8 @@ export const useChatStore = create<ChatStore>()(
// get recent messages
const recentMessages = get().getMessagesWithMemory();
const sendMessages = recentMessages.concat(userMessage);
const sessionIndex = get().currentSessionIndex;
const messageIndex = get().currentSession().messages.length + 1;
// save user's and bot's message
get().updateCurrentSession((session) => {
@@ -303,13 +312,16 @@ export const useChatStore = create<ChatStore>()(
session.messages.push(botMessage);
});
// make request
console.log("[User Input] ", sendMessages);
requestChatStream(sendMessages, {
onMessage(content, done) {
// stream response
if (done) {
botMessage.streaming = false;
botMessage.content = content;
get().onNewMessage(botMessage);
ControllerPool.remove(sessionIndex, messageIndex);
} else {
botMessage.content = content;
set(() => ({}));
@@ -319,6 +331,15 @@ export const useChatStore = create<ChatStore>()(
botMessage.content += "\n\n" + Locale.Store.Error;
botMessage.streaming = false;
set(() => ({}));
ControllerPool.remove(sessionIndex, messageIndex);
},
onController(controller) {
// collect controller for stop/retry
ControllerPool.addController(
sessionIndex,
messageIndex,
controller
);
},
filterBot: !get().config.sendBotMessages,
modelConfig: get().config.modelConfig,

View File

@@ -7,6 +7,7 @@
--second: rgb(231, 248, 255);
--hover-color: #f3f3f3;
--bar-color: rgba(0, 0, 0, 0.1);
--theme-color: var(--gray);
/* shadow */
--shadow: 50px 50px 100px 10px rgb(0, 0, 0, 0.1);
@@ -28,6 +29,8 @@
--bar-color: rgba(255, 255, 255, 0.1);
--border-in-light: 1px solid rgba(255, 255, 255, 0.192);
--theme-color: var(--gray);
}
.light {
@@ -84,7 +87,11 @@ body {
align-items: center;
user-select: none;
font-family: "Noto Sans SC", "SF Pro SC", "SF Pro Text", "SF Pro Icons",
"PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
"PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
@media only screen and (max-width: 600px) {
background-color: var(--second);
}
}
::-webkit-scrollbar {
@@ -206,3 +213,36 @@ div.math {
text-decoration: underline;
}
}
pre {
position: relative;
&:hover .copy-code-button {
pointer-events: all;
transform: translateX(0px);
opacity: 0.5;
}
.copy-code-button {
position: absolute;
right: 10px;
cursor: pointer;
padding: 0px 5px;
background-color: var(--black);
color: var(--white);
border: var(--border-in-light);
border-radius: 10px;
transform: translateX(10px);
pointer-events: none;
opacity: 0;
transition: all ease 0.3s;
&:after {
content: "copy";
}
&:hover {
opacity: 1;
}
}
}

View File

@@ -839,21 +839,20 @@
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
padding: 16px 16px 8px 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--color-canvas-subtle);
border-radius: 6px;
}
.markdown-body pre code,
.markdown-body pre tt {
display: inline;
max-width: auto;
display: inline-block;
max-width: 100%;
padding: 0;
margin: 0;
overflow: visible;
overflow-x: scroll;
line-height: inherit;
word-wrap: normal;
background-color: transparent;

View File

@@ -1,4 +1,9 @@
.markdown-body {
pre {
background: #282a36;
color: #f8f8f2;
}
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
@@ -117,13 +122,13 @@
}
@mixin light {
.markdown-body pre[class*="language-"] {
filter: invert(1) hue-rotate(50deg) brightness(1.3);
.markdown-body pre {
filter: invert(1) hue-rotate(90deg) brightness(1.3);
}
}
@mixin dark {
.markdown-body pre[class*="language-"] {
.markdown-body pre {
filter: none;
}
}

View File

@@ -8,13 +8,14 @@ export const config = {
export function middleware(req: NextRequest, res: NextResponse) {
const accessCode = req.headers.get("access-code");
const token = req.headers.get("token");
const hashedCode = md5.hash(accessCode ?? "").trim();
console.log("[Auth] allowed hashed codes: ", [...ACCESS_CODES]);
console.log("[Auth] got access code:", accessCode);
console.log("[Auth] hashed access code:", hashedCode);
if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode)) {
if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) {
return NextResponse.json(
{
needAccessCode: true,

View File

@@ -14,4 +14,8 @@ const nextConfig = {
}
};
if (process.env.DOCKER) {
nextConfig.output = 'standalone'
}
module.exports = nextConfig;

View File

@@ -2,6 +2,7 @@
"name": "chatgpt-next-web",
"version": "1.1",
"private": false,
"license": "Anti 996",
"scripts": {
"dev": "next dev",
"build": "next build",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 15 KiB

4
public/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Disallow: /
User-agent: vitals.vercel-insights.com
Allow: /

24
public/serviceWorker.js Normal file
View File

@@ -0,0 +1,24 @@
const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache";
self.addEventListener('activate', function (event) {
console.log('ServiceWorker activated.');
});
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(CHATGPT_NEXT_WEB_CACHE)
.then(function (cache) {
return cache.addAll([
]);
})
);
});
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
return response || fetch(event.request);
})
);
});

View File

@@ -0,0 +1,9 @@
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/serviceWorker.js').then(function (registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function (err) {
console.error('ServiceWorker registration failed: ', err);
});
});
}

View File

@@ -1 +1,21 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{
"name": "ChatGPT Next Web",
"short_name": "ChatGPT",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

64
scripts/setup.sh Normal file
View File

@@ -0,0 +1,64 @@
#!/bin/bash
# Check if running on a supported system
case "$(uname -s)" in
Linux)
if [[ -f "/etc/lsb-release" ]]; then
. /etc/lsb-release
if [[ "$DISTRIB_ID" != "Ubuntu" ]]; then
echo "This script only works on Ubuntu, not $DISTRIB_ID."
exit 1
fi
else
if [[ ! "$(cat /etc/*-release | grep '^ID=')" =~ ^(ID=\"ubuntu\")|(ID=\"centos\")|(ID=\"arch\")$ ]]; then
echo "Unsupported Linux distribution."
exit 1
fi
fi
;;
Darwin)
echo "Running on MacOS."
;;
*)
echo "Unsupported operating system."
exit 1
;;
esac
# Check if needed dependencies are installed and install if necessary
if ! command -v node >/dev/null || ! command -v git >/dev/null || ! command -v yarn >/dev/null; then
case "$(uname -s)" in
Linux)
if [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"ubuntu\"" ]]; then
sudo apt-get update
sudo apt-get -y install nodejs git yarn
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"centos\"" ]]; then
sudo yum -y install epel-release
sudo yum -y install nodejs git yarn
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"arch\"" ]]; then
sudo pacman -Syu -y
sudo pacman -S -y nodejs git yarn
else
echo "Unsupported Linux distribution"
exit 1
fi
;;
Darwin)
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install node git yarn
;;
esac
fi
# Clone the repository and install dependencies
git clone https://github.com/Yidadaa/ChatGPT-Next-Web
cd ChatGPT-Next-Web
yarn install
# Prompt user for environment variables
read -p "Enter OPENAI_API_KEY: " OPENAI_API_KEY
read -p "Enter CODE: " CODE
read -p "Enter PORT: " PORT
# Build and run the project using the environment variables
OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn build && OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn start