Compare commits
71 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 | ||
|
5cdcaac35b | ||
|
d0d1673ccc | ||
|
3136d6d3fd | ||
|
2c899cf00e | ||
|
15e49e8b46 | ||
|
a5b3998304 | ||
|
9681fd8e14 | ||
|
8399677350 | ||
|
b1670b3558 |
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"]
|
85
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)
|
||||
|
||||
@@ -51,14 +51,49 @@ One-Click to deploy your own ChatGPT web UI.
|
||||
|
||||
## 保持更新 Keep Updated
|
||||
|
||||
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
|
||||
推荐你按照下列步骤重新部署:
|
||||
|
||||
- 删除掉原先的 repo;
|
||||
- fork 本项目;
|
||||
- 前往 vercel 控制台,删除掉原先的 project,然后新建 project,选择你刚刚 fork 出来的项目重新进行部署即可;
|
||||
- 在重新部署的过程中,请手动添加名为 `OPENAI_API_KEY` 的环境变量,并填入你的 api key 作为值。
|
||||
|
||||
本项目会持续更新,如果你想让代码库总是保持更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步,建议定期进行同步操作以获得新功能。
|
||||
|
||||
你可以 star/watch 本项目或者 follow 作者来及时获得新功能更新通知。
|
||||
|
||||
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.
|
||||
|
||||
This project will be continuously maintained. If you want to keep the code repository up to date, 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. It is recommended to perform synchronization operations regularly.
|
||||
|
||||
You can star or watch this project or follow author to get release notifictions in time.
|
||||
|
||||
## 访问控制 Access Control
|
||||
|
||||
本项目提供有限的权限控制功能,请在环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义控制码:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
code1,code2,code3
|
||||
```
|
||||
|
||||
After adding or modifying this environment variable, please redeploy the project for the changes to take effect.
|
||||
|
||||
## 开发 Development
|
||||
|
||||
点击下方按钮,开始二次开发:
|
||||
@@ -67,18 +102,64 @@ You can star or watch this project or follow author to get release notifictions
|
||||
|
||||
在开始写代码之前,需要在项目根目录新建一个 `.env.local` 文件,里面填入环境变量:
|
||||
|
||||
Before development, you must create a new `.env.local` file at project root, and place your api key into it:
|
||||
Before starting development, you must create a new `.env.local` file at project root, and place your api key into it:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=<your api key here>
|
||||
```
|
||||
|
||||
### 本地开发 Local Development
|
||||
|
||||
> 如果你是中国大陆用户,不建议在本地进行开发,除非你能够独立解决 OpenAI API 本地代理问题。
|
||||
|
||||
1. 安装 nodejs 和 yarn,具体细节请询问 ChatGPT;
|
||||
2. 执行 `yarn install && yarn dev` 即可。
|
||||
|
||||
### 本地部署 Local Deployment
|
||||
```shell
|
||||
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
||||
```
|
||||
|
||||
|
||||
### 容器部署 Docker Deployment
|
||||
|
||||
```shell
|
||||
docker pull yidadaa/chatgpt-next-web
|
||||
|
||||
docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
## 截图 Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 说明 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)
|
||||
|
17
app/api/access.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import md5 from "spark-md5";
|
||||
|
||||
export function getAccessCodes(): Set<string> {
|
||||
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;
|
@@ -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>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef, useMemo } from "react";
|
||||
|
||||
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Theme,
|
||||
ALL_MODELS,
|
||||
useUpdateStore,
|
||||
useAccessStore,
|
||||
} from "../store";
|
||||
import { Avatar } from "./home";
|
||||
|
||||
@@ -38,7 +39,7 @@ function SettingItem(props: {
|
||||
<div className={styles["settings-sub-title"]}>{props.subTitle}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>{props.children}</div>
|
||||
{props.children}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
@@ -71,6 +72,12 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
checkUpdate();
|
||||
}, []);
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const enabledAccessControl = useMemo(
|
||||
() => accessStore.enabledAccessControl(),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles["window-header"]}>
|
||||
@@ -232,6 +239,38 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
</div>
|
||||
</List>
|
||||
<List>
|
||||
{enabledAccessControl ? (
|
||||
<SettingItem
|
||||
title={Locale.Settings.AccessCode.Title}
|
||||
subTitle={Locale.Settings.AccessCode.SubTitle}
|
||||
>
|
||||
<input
|
||||
value={accessStore.accessCode}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.AccessCode.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.updateCode(e.currentTarget.value);
|
||||
}}
|
||||
></input>
|
||||
</SettingItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<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}
|
||||
@@ -315,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(
|
||||
|
@@ -1,24 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
|
||||
height="16" viewBox="0 0 16 16" fill="none">
|
||||
<defs>
|
||||
<rect id="path_0" x="0" y="0" width="16" height="16" />
|
||||
</defs>
|
||||
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
|
||||
<mask id="bg-mask-0" fill="white">
|
||||
<use xlink:href="#path_0"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-0)">
|
||||
<path id="路径 1"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2 9) rotate(0 6 2.6666666666666665)"
|
||||
d="M12,0C12,2 10.67,5.33 6,5.33C1.33,5.33 0,2 0,0 " />
|
||||
<path id="路径 2"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(8.0026 1.7001966666666668) rotate(0 0 4.649918333333334)"
|
||||
d="M0,0L0,9.3 " />
|
||||
<path id="路径 3"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(4 1.6666666666666665) rotate(0 4 2)" d="M0,4L4,0L8,4 " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.2400716519614834 2.3333321805983163) rotate(0 6.785117896431597 4.552683909700841)" d="M12.27,9.11C13.36,8.34 13.83,6.94 13.43,5.67C13.02,4.39 11.78,3.69 10.44,3.69L9.67,3.69C9.16,1.72 7.5,0.27 5.47,0.03C3.45,-0.2 1.5,0.84 0.56,2.64C-0.38,4.45 -0.11,6.64 1.23,8.17 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 7.666666666666666) rotate(0 0.00140000000000029 3)" d="M0,6L0,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.8786 11.5454) rotate(0 2.1213333333333333 1.0606666666666662)" d="M4.24,0L2.12,2.12L0,0 " /></g></g></svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,13 +1,44 @@
|
||||
/* eslint-disable @next/next/no-page-custom-font */
|
||||
import "./styles/globals.scss";
|
||||
import "./styles/markdown.scss";
|
||||
import "./styles/prism.scss";
|
||||
import process from "child_process";
|
||||
import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
|
||||
|
||||
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."
|
||||
description: "Your personal ChatGPT Chat Bot.",
|
||||
appleWebApp: {
|
||||
title: "ChatGPT Next Web",
|
||||
statusBarStyle: "black-translucent",
|
||||
},
|
||||
themeColor: "#fafafa"
|
||||
};
|
||||
|
||||
const COMMIT_ID = process.env.COMMIT_ID
|
||||
function Meta() {
|
||||
const metas = {
|
||||
version: COMMIT_ID ?? "unknown",
|
||||
access: (ACCESS_CODES.size > 0 || IS_IN_DOCKER) ? "enabled" : "disabled",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(metas).map(([k, v]) => (
|
||||
<meta name={k} content={v} key={k} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@@ -21,7 +52,7 @@ export default function RootLayout({
|
||||
name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
<meta name="version" content={COMMIT_ID} />
|
||||
<Meta />
|
||||
<link rel="manifest" href="/site.webmanifest"></link>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"></link>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com"></link>
|
||||
@@ -29,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>
|
||||
|
@@ -1,5 +1,8 @@
|
||||
const cn = {
|
||||
WIP: "该功能仍在开发中……",
|
||||
Error: {
|
||||
Unauthorized: "现在是未授权状态,请在设置页填写授权码。",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} 条对话`,
|
||||
},
|
||||
@@ -11,6 +14,7 @@ const cn = {
|
||||
Export: "导出聊天记录",
|
||||
Copy: "复制",
|
||||
Stop: "停止",
|
||||
Retry: "重试",
|
||||
},
|
||||
Typing: "正在输入…",
|
||||
Input: (submitKey: string) => `输入消息,${submitKey} 发送`,
|
||||
@@ -65,6 +69,16 @@ const cn = {
|
||||
Title: "历史消息长度压缩阈值",
|
||||
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
|
||||
},
|
||||
Token: {
|
||||
Title: "API Key",
|
||||
SubTitle: "使用自己的 Key 可绕过受控访问限制",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "访问码",
|
||||
SubTitle: "现在是受控访问状态",
|
||||
Placeholder: "请输入访问码",
|
||||
},
|
||||
Model: "模型 (model)",
|
||||
Temperature: {
|
||||
Title: "随机性 (temperature)",
|
||||
|
@@ -2,6 +2,10 @@ import type { LocaleType } from "./index";
|
||||
|
||||
const en: LocaleType = {
|
||||
WIP: "WIP...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Unauthorized access, please enter access code in settings page.",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} messages`,
|
||||
},
|
||||
@@ -13,6 +17,7 @@ const en: LocaleType = {
|
||||
Export: "Export All Messages as Markdown",
|
||||
Copy: "Copy",
|
||||
Stop: "Stop",
|
||||
Retry: "Retry",
|
||||
},
|
||||
Typing: "Typing…",
|
||||
Input: (submitKey: string) =>
|
||||
@@ -69,6 +74,16 @@ 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",
|
||||
Placeholder: "Need Access Code",
|
||||
},
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Temperature",
|
||||
|
@@ -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 (
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import type { ChatRequest, ChatReponse } from "./api/chat/typing";
|
||||
import { filterConfig, Message, ModelConfig } from "./store";
|
||||
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
||||
import Locale from "./locales";
|
||||
|
||||
const TIME_OUT_MS = 30000;
|
||||
|
||||
@@ -26,6 +27,21 @@ const makeRequestParam = (
|
||||
};
|
||||
};
|
||||
|
||||
function getHeaders() {
|
||||
const accessStore = useAccessStore.getState();
|
||||
let headers: Record<string, string> = {};
|
||||
|
||||
if (accessStore.enabledAccessControl()) {
|
||||
headers["access-code"] = accessStore.accessCode;
|
||||
}
|
||||
|
||||
if (accessStore.token && accessStore.token.length > 0) {
|
||||
headers["token"] = accessStore.token;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function requestChat(messages: Message[]) {
|
||||
const req: ChatRequest = makeRequestParam(messages, { filterBot: true });
|
||||
|
||||
@@ -33,6 +49,7 @@ export async function requestChat(messages: Message[]) {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...getHeaders(),
|
||||
},
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
@@ -47,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, {
|
||||
@@ -69,6 +87,7 @@ export async function requestChatStream(
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...getHeaders(),
|
||||
},
|
||||
body: JSON.stringify(req),
|
||||
signal: controller.signal,
|
||||
@@ -86,6 +105,8 @@ export async function requestChatStream(
|
||||
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);
|
||||
@@ -102,14 +123,18 @@ export async function requestChatStream(
|
||||
}
|
||||
}
|
||||
|
||||
finish();
|
||||
} else if (res.status === 401) {
|
||||
console.error("Anauthorized");
|
||||
responseText = Locale.Error.Unauthorized;
|
||||
finish();
|
||||
} else {
|
||||
console.error("Stream Error");
|
||||
options?.onError(new Error("Stream Error"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("NetWork Error");
|
||||
options?.onError(new Error("NetWork Error"));
|
||||
console.error("NetWork Error", err);
|
||||
options?.onError(err as Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,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}`;
|
||||
},
|
||||
};
|
||||
|
36
app/store/access.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { queryMeta } from "../utils";
|
||||
|
||||
export interface AccessControlStore {
|
||||
accessCode: string;
|
||||
token: string;
|
||||
|
||||
updateToken: (_: string) => void;
|
||||
updateCode: (_: string) => void;
|
||||
enabledAccessControl: () => boolean;
|
||||
}
|
||||
|
||||
export const ACCESS_KEY = "access-control";
|
||||
|
||||
export const useAccessStore = create<AccessControlStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
token: "",
|
||||
accessCode: "",
|
||||
enabledAccessControl() {
|
||||
return queryMeta("access") === "enabled";
|
||||
},
|
||||
updateCode(code: string) {
|
||||
set((state) => ({ accessCode: code }));
|
||||
},
|
||||
updateToken(token: string) {
|
||||
set((state) => ({ token }));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: ACCESS_KEY,
|
||||
version: 1,
|
||||
}
|
||||
)
|
||||
);
|
@@ -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,12 +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(() => ({}));
|
||||
@@ -318,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,
|
||||
|
@@ -1,2 +1,3 @@
|
||||
export * from "./app";
|
||||
export * from "./update";
|
||||
export * from "./access";
|
||||
|
@@ -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 {
|
||||
@@ -167,7 +174,8 @@ input[type="range"]::-webkit-slider-thumb:hover {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
input[type="number"],
|
||||
input[type="text"] {
|
||||
appearance: none;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
@@ -176,6 +184,7 @@ input[type="number"] {
|
||||
background: var(--white);
|
||||
color: var(--black);
|
||||
padding: 0 10px;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
div.math {
|
||||
@@ -204,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;
|
||||
}
|
||||
}
|
||||
|
23
app/utils.ts
@@ -57,20 +57,27 @@ export function selectOrCopy(el: HTMLElement, content: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function queryMeta(key: string, defaultValue?: string): string {
|
||||
let ret: string;
|
||||
if (document) {
|
||||
const meta = document.head.querySelector(
|
||||
`meta[name='${key}']`
|
||||
) as HTMLMetaElement;
|
||||
ret = meta?.content ?? "";
|
||||
} else {
|
||||
ret = defaultValue ?? "";
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
let currentId: string;
|
||||
export function getCurrentCommitId() {
|
||||
if (currentId) {
|
||||
return currentId;
|
||||
}
|
||||
|
||||
if (document) {
|
||||
const meta = document.head.querySelector(
|
||||
"meta[name='version']"
|
||||
) as HTMLMetaElement;
|
||||
currentId = meta?.content ?? "";
|
||||
} else {
|
||||
currentId = process.env.COMMIT_ID ?? "";
|
||||
}
|
||||
currentId = queryMeta("version");
|
||||
|
||||
return currentId;
|
||||
}
|
||||
|
31
middleware.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { ACCESS_CODES } from "./app/api/access";
|
||||
import md5 from "spark-md5";
|
||||
|
||||
export const config = {
|
||||
matcher: ["/api/chat", "/api/chat-stream"],
|
||||
};
|
||||
|
||||
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) && !token) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
needAccessCode: true,
|
||||
hint: "Please go settings page and fill your access code.",
|
||||
},
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
@@ -14,4 +14,8 @@ const nextConfig = {
|
||||
}
|
||||
};
|
||||
|
||||
if (process.env.DOCKER) {
|
||||
nextConfig.output = 'standalone'
|
||||
}
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
10
package.json
@@ -2,10 +2,11 @@
|
||||
"name": "chatgpt-next-web",
|
||||
"version": "1.1",
|
||||
"private": false,
|
||||
"license": "Anti 996",
|
||||
"scripts": {
|
||||
"dev": "cross-env COMMIT_ID=$(git rev-parse --short HEAD) next dev",
|
||||
"build": "cross-env COMMIT_ID=$(git rev-parse --short HEAD) next build",
|
||||
"start": "cross-env COMMIT_ID=$(git rev-parse --short HEAD) next start",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -14,13 +15,13 @@
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-katex": "^3.0.0",
|
||||
"@types/spark-md5": "^3.0.2",
|
||||
"@vercel/analytics": "^0.1.11",
|
||||
"cross-env": "^7.0.3",
|
||||
"emoji-picker-react": "^4.4.7",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-config-next": "13.2.3",
|
||||
"eventsource-parser": "^0.1.0",
|
||||
"git-rev-sync": "^3.0.2",
|
||||
"next": "^13.2.3",
|
||||
"openai": "^3.2.1",
|
||||
"react": "^18.2.0",
|
||||
@@ -31,6 +32,7 @@
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"sass": "^1.59.2",
|
||||
"spark-md5": "^3.0.2",
|
||||
"typescript": "4.9.5",
|
||||
"zustand": "^4.3.6"
|
||||
}
|
||||
|
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
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "ES2015",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
51
yarn.lock
@@ -1390,6 +1390,11 @@
|
||||
resolved "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
||||
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
||||
|
||||
"@types/spark-md5@^3.0.2":
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.2.tgz#da2e8a778a20335fc4f40b6471c4b0d86b70da55"
|
||||
integrity sha512-82E/lVRaqelV9qmRzzJ1PKTpyrpnT7mwdneKNJB9hUtypZDMggloDfFUCIqRRx3lYRxteCwXSq9c+W71Vf0QnQ==
|
||||
|
||||
"@types/unist@*", "@types/unist@^2.0.0":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.npmmirror.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
||||
@@ -2133,7 +2138,7 @@ escalade@^3.1.1:
|
||||
resolved "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
|
||||
|
||||
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5:
|
||||
escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
|
||||
@@ -2521,15 +2526,6 @@ get-tsconfig@^4.2.0:
|
||||
resolved "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.4.0.tgz#64eee64596668a81b8fce18403f94f245ee0d4e5"
|
||||
integrity sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ==
|
||||
|
||||
git-rev-sync@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/git-rev-sync/-/git-rev-sync-3.0.2.tgz#9763c730981187c3419b75dd270088cc5f0e161b"
|
||||
integrity sha512-Nd5RiYpyncjLv0j6IONy0lGzAqdRXUaBctuGBbrEA2m6Bn4iDrN/9MeQTXuiquw8AEKL9D2BW0nw5m/lQvxqnQ==
|
||||
dependencies:
|
||||
escape-string-regexp "1.0.5"
|
||||
graceful-fs "4.1.15"
|
||||
shelljs "0.8.5"
|
||||
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
@@ -2556,7 +2552,7 @@ glob@7.1.7:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.0.0, glob@^7.1.3:
|
||||
glob@^7.1.3:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||
@@ -2627,11 +2623,6 @@ gopd@^1.0.1:
|
||||
dependencies:
|
||||
get-intrinsic "^1.1.3"
|
||||
|
||||
graceful-fs@4.1.15:
|
||||
version "4.1.15"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
|
||||
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
|
||||
|
||||
graceful-fs@^4.2.4:
|
||||
version "4.2.10"
|
||||
resolved "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
|
||||
@@ -2799,11 +2790,6 @@ internal-slot@^1.0.3, internal-slot@^1.0.4:
|
||||
has "^1.0.3"
|
||||
side-channel "^1.0.4"
|
||||
|
||||
interpret@^1.0.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
|
||||
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
|
||||
|
||||
is-alphabetical@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b"
|
||||
@@ -4009,13 +3995,6 @@ readdirp@~3.6.0:
|
||||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
rechoir@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
|
||||
integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==
|
||||
dependencies:
|
||||
resolve "^1.1.6"
|
||||
|
||||
refractor@^4.7.0:
|
||||
version "4.8.1"
|
||||
resolved "https://registry.yarnpkg.com/refractor/-/refractor-4.8.1.tgz#fbdd889333a3d86c9c864479622855c9b38e9d42"
|
||||
@@ -4163,7 +4142,7 @@ resolve-from@^4.0.0:
|
||||
resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||
|
||||
resolve@^1.1.6, resolve@^1.14.2, resolve@^1.22.1:
|
||||
resolve@^1.14.2, resolve@^1.22.1:
|
||||
version "1.22.1"
|
||||
resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
|
||||
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
|
||||
@@ -4256,15 +4235,6 @@ shebang-regex@^3.0.0:
|
||||
resolved "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shelljs@0.8.5:
|
||||
version "0.8.5"
|
||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
|
||||
integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
|
||||
dependencies:
|
||||
glob "^7.0.0"
|
||||
interpret "^1.0.0"
|
||||
rechoir "^0.6.2"
|
||||
|
||||
side-channel@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
|
||||
@@ -4299,6 +4269,11 @@ space-separated-tokens@^2.0.0:
|
||||
resolved "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f"
|
||||
integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
|
||||
|
||||
spark-md5@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc"
|
||||
integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==
|
||||
|
||||
stable@^0.1.8:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.npmmirror.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
|
||||
|