Compare commits

...

16 Commits
v1.1 ... v1.3

Author SHA1 Message Date
Yifei Zhang
bb45c62a81 Merge pull request #45 from Yidadaa/bugfix-0326
v1.3 Stop and Retry Button
2023-03-26 19:16:02 +08:00
Yifei Zhang
1e89fe14ac fix: #34 only auto scroll when textbox is focused 2023-03-26 11:10:34 +00:00
Yifei Zhang
86507fa569 feat: #2 #8 add stop and retry button 2023-03-26 10:59:09 +00:00
Yifei Zhang
4180363f58 Update README.md 2023-03-26 17:51:46 +08:00
Yifei Zhang
a5ec15236a fix: #38 high resolution favicon 2023-03-26 09:32:47 +00:00
Yifei Zhang
43b6835564 Update README.md 2023-03-26 16:52:00 +08:00
Yifei Zhang
3f865ffa1e Update README.md 2023-03-26 15:18:34 +08:00
Yifei Zhang
5cdcaac35b Merge pull request #29 from Yidadaa/bugfix-0325
v1.2 bug fix, ui improvement and access control by code
2023-03-26 15:12:46 +08:00
Yifei Zhang
d0d1673ccc fixup: disable access control when CODE is empty 2023-03-26 07:06:06 +00:00
Yifei Zhang
3136d6d3fd fix: #10 replace export icon 2023-03-26 06:57:13 +00:00
Yifei Zhang
2c899cf00e feat: #2 add access control by
access code
2023-03-26 06:53:40 +00:00
Yifei Zhang
15e49e8b46 Update README.md 2023-03-26 00:25:10 +08:00
Yidadaa
a5b3998304 fix: #23 errors when dev on windows 2023-03-25 22:24:52 +08:00
Yifei Zhang
9681fd8e14 Update README.md 2023-03-25 22:18:48 +08:00
Yifei Zhang
8399677350 Update README.md 2023-03-25 14:54:42 +08:00
Yifei Zhang
b1670b3558 chore: fix typos 2023-03-24 18:44:16 +08:00
24 changed files with 396 additions and 106 deletions

View File

@@ -7,7 +7,7 @@
One-Click to deploy your own ChatGPT web UI.
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈问题 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈问题 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
@@ -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,59 @@ 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
请直接询问 ChatGPT使用下列 Prompt
```
如何使用 pm2 和 yarn 部署 nextjs 项目到 ubuntu 服务器上,项目编译命令为 yarn build启动命令为 yarn start启动时需要设置环境变量为 OPENAI_API_KEY端口为 3000使用 ngnix 做反向代理
```
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 做反向代理
```
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
```
## 截图 Screenshots
![设置 Settings](./static/settings.png)
![更多展示 More](./static/more.png)
## 说明 Attention
本项目的演示地址所用的 OpenAI 账户的免费额度将于 2023-04-01 过期,届时将无法通过演示地址在线体验。
如果你想贡献出自己的 API Key可以通过作者主页的邮箱发送给作者并标注过期时间。
The free trial of the OpenAI account used by the demo will expire on April 1, 2023, and the demo will not be available at that time.
If you would like to contribute your API key, you can email it to the author and indicate the expiration date of the API key.
## 鸣谢 Special Thanks
### 捐赠者 Sponsor
[@mushan0x0](https://github.com/mushan0x0)
## LICENSE
- [Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)

16
app/api/access.ts Normal file
View File

@@ -0,0 +1,16 @@
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();

View File

@@ -27,6 +27,7 @@ import Locale from "../locales";
import dynamic from "next/dynamic";
import { REPO_URL } from "../constant";
import { ControllerPool } from "../requests";
export function Loading(props: { noLogo?: boolean }) {
return (
@@ -146,28 +147,67 @@ 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("");
};
// 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 [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 +234,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 +293,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 +316,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 +346,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>
@@ -341,6 +377,9 @@ export function Chat(props: { showSideBar?: () => void }) {
onInput={(e) => setUserInput(e.currentTarget.value)}
value={userInput}
onKeyDown={(e) => onInputKeyDown(e as any)}
onFocus={() => setAutoScroll(true)}
onBlur={() => setAutoScroll(false)}
autoFocus
/>
<IconButton
icon={<SendWhiteIcon />}

View File

@@ -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,24 @@ 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.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}

View File

@@ -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

View File

@@ -1,13 +1,34 @@
/* 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 } from "./api/access";
const COMMIT_ID = process
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
export const metadata = {
title: "ChatGPT Next Web",
description: "Your personal ChatGPT Chat Bot."
description: "Your personal ChatGPT Chat Bot.",
};
const COMMIT_ID = process.env.COMMIT_ID
function Meta() {
const metas = {
version: COMMIT_ID,
access: ACCESS_CODES.size > 0 ? "enabled" : "disabled",
};
return (
<>
{Object.entries(metas).map(([k, v]) => (
<meta name={k} content={v} key={k} />
))}
</>
);
}
export default function RootLayout({
children,
@@ -21,7 +42,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>

View File

@@ -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,11 @@ const cn = {
Title: "历史消息长度压缩阈值",
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
},
AccessCode: {
Title: "访问码",
SubTitle: "现在是受控访问状态",
Placeholder: "请输入访问码",
},
Model: "模型 (model)",
Temperature: {
Title: "随机性 (temperature)",

View File

@@ -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,11 @@ const en: LocaleType = {
SubTitle:
"Will compress if uncompressed messages length exceeds the value",
},
AccessCode: {
Title: "Access Code",
SubTitle: "Access control enabled",
Placeholder: "Need Access Code",
},
Model: "Model",
Temperature: {
Title: "Temperature",

View File

@@ -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,17 @@ const makeRequestParam = (
};
};
function getHeaders() {
const accessStore = useAccessStore.getState();
let headers: Record<string, string> = {};
if (accessStore.enabledAccessControl()) {
headers["access-code"] = accessStore.accessCode;
}
return headers;
}
export async function requestChat(messages: Message[]) {
const req: ChatRequest = makeRequestParam(messages, { filterBot: true });
@@ -33,6 +45,7 @@ export async function requestChat(messages: Message[]) {
method: "POST",
headers: {
"Content-Type": "application/json",
...getHeaders(),
},
body: JSON.stringify(req),
});
@@ -47,6 +60,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 +83,7 @@ export async function requestChatStream(
method: "POST",
headers: {
"Content-Type": "application/json",
...getHeaders(),
},
body: JSON.stringify(req),
signal: controller.signal,
@@ -86,6 +101,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 +119,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 +147,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}`;
},
};

30
app/store/access.ts Normal file
View File

@@ -0,0 +1,30 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { queryMeta } from "../utils";
export interface AccessControlStore {
accessCode: string;
updateCode: (_: string) => void;
enabledAccessControl: () => boolean;
}
export const ACCESS_KEY = "access-control";
export const useAccessStore = create<AccessControlStore>()(
persist(
(set, get) => ({
accessCode: "",
enabledAccessControl() {
return queryMeta("access") === "enabled";
},
updateCode(code: string) {
set((state) => ({ accessCode: code }));
},
}),
{
name: ACCESS_KEY,
version: 1,
}
)
);

View File

@@ -2,7 +2,11 @@ import { create } from "zustand";
import { persist } from "zustand/middleware";
import { type ChatCompletionResponseMessage } from "openai";
import { requestChatStream, requestWithPrompt } from "../requests";
import {
ControllerPool,
requestChatStream,
requestWithPrompt,
} from "../requests";
import { trimTopic } from "../utils";
import Locale from "../locales";
@@ -296,6 +300,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 +309,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 +328,15 @@ export const useChatStore = create<ChatStore>()(
botMessage.content += "\n\n" + Locale.Store.Error;
botMessage.streaming = false;
set(() => ({}));
ControllerPool.remove(sessionIndex, messageIndex);
},
onController(controller) {
// collect controller for stop/retry
ControllerPool.addController(
sessionIndex,
messageIndex,
controller
);
},
filterBot: !get().config.sendBotMessages,
modelConfig: get().config.modelConfig,

View File

@@ -1,2 +1,3 @@
export * from "./app";
export * from "./update";
export * from "./access";

View File

@@ -167,7 +167,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 +177,7 @@ input[type="number"] {
background: var(--white);
color: var(--black);
padding: 0 10px;
max-width: 50%;
}
div.math {

View File

@@ -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;
}

30
middleware.ts Normal file
View File

@@ -0,0 +1,30 @@
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 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)) {
return NextResponse.json(
{
needAccessCode: true,
hint: "Please go settings page and fill your access code.",
},
{
status: 401,
}
);
}
return NextResponse.next();
}

View File

@@ -3,9 +3,9 @@
"version": "1.1",
"private": false,
"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 +14,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 +31,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"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "ES2015",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

View File

@@ -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"