Compare commits
62 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7f3cbaa064 | ||
|
eb72c83b7e | ||
|
e93ea0fa97 | ||
|
3b6f93afdf | ||
|
4597a2286a | ||
|
780968979d | ||
|
adc0db4c74 | ||
|
f0dd95a2a3 | ||
|
6155a190ac | ||
|
493aa8c591 | ||
|
6c82f804ae | ||
|
a2807c9815 | ||
|
d822f333c2 | ||
|
8f498075b9 | ||
|
c4bf6a6383 | ||
|
939402b2d9 | ||
|
684a3c41ef | ||
|
306f0850e9 | ||
|
55f37248f7 | ||
|
c93a46a02f | ||
|
77a3fdea5f | ||
|
cc1a1d4f3c | ||
|
0463b350d8 | ||
|
8f87a68f72 | ||
|
60f27fdfbb | ||
|
d17706636b | ||
|
9570691d5b | ||
|
efe4fcc188 | ||
|
efaf6590ef | ||
|
fb06fb8c38 | ||
|
f188841188 | ||
|
5593c067c4 | ||
|
689b7bab26 | ||
|
a81e7394f0 | ||
|
492fed6802 | ||
|
bdf17fafff | ||
|
58baa23199 | ||
|
dd80c4563d | ||
|
785372ad73 | ||
|
d8e4808316 | ||
|
6446692db0 | ||
|
cd73c3a7cb | ||
|
d0eee767fa | ||
|
e880df6db9 | ||
|
96c4f5bbd9 | ||
|
2645540721 | ||
|
b1f27aaf93 | ||
|
fb2d281aac | ||
|
84d73fa1f2 | ||
|
f858407f9a | ||
|
b57663bf02 | ||
|
e57bd51809 | ||
|
df66eef919 | ||
|
f1b6641f19 | ||
|
bb45c62a81 | ||
|
1e89fe14ac | ||
|
86507fa569 | ||
|
4180363f58 | ||
|
a5ec15236a | ||
|
8d0d08725d | ||
|
43b6835564 | ||
|
3f865ffa1e |
1
.eslintignore
Normal file
@@ -0,0 +1 @@
|
||||
public/serviceWorker.js
|
33
.github/workflows/docker.yml
vendored
Normal 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
@@ -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"]
|
61
README.md
@@ -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)
|
||||
|
||||
[](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
|
||||
|
||||

|
||||
|
||||
## 说明 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)
|
||||
|
@@ -14,3 +14,4 @@ export function getAccessCodes(): Set<string> {
|
||||
}
|
||||
|
||||
export const ACCESS_CODES = getAccessCodes();
|
||||
export const IS_IN_DOCKER = process.env.DOCKER;
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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(
|
||||
|
@@ -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>
|
||||
|
@@ -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: "现在是受控访问状态",
|
||||
|
@@ -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",
|
||||
|
@@ -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 (
|
||||
|
@@ -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}`;
|
||||
},
|
||||
};
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -14,4 +14,8 @@ const nextConfig = {
|
||||
}
|
||||
};
|
||||
|
||||
if (process.env.DOCKER) {
|
||||
nextConfig.output = 'standalone'
|
||||
}
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
@@ -2,6 +2,7 @@
|
||||
"name": "chatgpt-next-web",
|
||||
"version": "1.1",
|
||||
"private": false,
|
||||
"license": "Anti 996",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 657 B After Width: | Height: | Size: 633 B |
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 15 KiB |
4
public/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
User-agent: vitals.vercel-insights.com
|
||||
Allow: /
|
24
public/serviceWorker.js
Normal 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);
|
||||
})
|
||||
);
|
||||
});
|
9
public/serviceWorkerRegister.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
@@ -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
@@ -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
|