forked from XiaoMo/ChatGPT-Next-Web
Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web into multi-arch-docker-build
This commit is contained in:
commit
6ed61f533a
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@ -43,7 +43,7 @@ jobs:
|
|||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"./app/**/*.{js,ts,jsx,tsx,json,html,css,scss,md}": [
|
"./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [
|
||||||
"eslint --fix",
|
"eslint --fix",
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
56
README.md
56
README.md
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
One-Click to deploy your own ChatGPT web UI.
|
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) / [加入 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)
|
[演示 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) / [Donate](#捐赠-donate-usdt)
|
||||||
|
|
||||||
[![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&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
|
[![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&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
|
||||||
|
|
||||||
@ -78,9 +78,9 @@ This project will be continuously maintained. If you want to keep the code repos
|
|||||||
|
|
||||||
You can star or watch this project or follow author to get release notifictions in time.
|
You can star or watch this project or follow author to get release notifictions in time.
|
||||||
|
|
||||||
## 访问控制 Access Control
|
## 配置密码 Password
|
||||||
|
|
||||||
本项目提供有限的权限控制功能,请在环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义控制码:
|
本项目提供有限的权限控制功能,请在 Vercel 项目控制面板的环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义密码:
|
||||||
|
|
||||||
```
|
```
|
||||||
code1,code2,code3
|
code1,code2,code3
|
||||||
@ -88,7 +88,7 @@ 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:
|
This project provides limited access control. Please add an environment variable named `CODE` on the vercel environment variables page. The value should be passwords separated by comma like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
code1,code2,code3
|
code1,code2,code3
|
||||||
@ -96,6 +96,38 @@ code1,code2,code3
|
|||||||
|
|
||||||
After adding or modifying this environment variable, please redeploy the project for the changes to take effect.
|
After adding or modifying this environment variable, please redeploy the project for the changes to take effect.
|
||||||
|
|
||||||
|
## 环境变量 Environment Variables
|
||||||
|
|
||||||
|
### `OPENAI_API_KEY` (required)
|
||||||
|
|
||||||
|
OpanAI 密钥。
|
||||||
|
|
||||||
|
Your openai api key.
|
||||||
|
|
||||||
|
### `CODE` (optional)
|
||||||
|
|
||||||
|
访问密码,可选,可以使用逗号隔开多个密码。
|
||||||
|
|
||||||
|
Access passsword, separated by comma.
|
||||||
|
|
||||||
|
### `BASE_URL` (optional)
|
||||||
|
|
||||||
|
> Default: `api.openai.com`
|
||||||
|
|
||||||
|
OpenAI 接口代理 URL。
|
||||||
|
|
||||||
|
Override openai api request base url.
|
||||||
|
|
||||||
|
### `PROTOCOL` (optional)
|
||||||
|
|
||||||
|
> Default: `https`
|
||||||
|
|
||||||
|
> Values: `http` | `https`
|
||||||
|
|
||||||
|
OpenAI 接口协议。
|
||||||
|
|
||||||
|
Override openai api request protocol.
|
||||||
|
|
||||||
## 开发 Development
|
## 开发 Development
|
||||||
|
|
||||||
点击下方按钮,开始二次开发:
|
点击下方按钮,开始二次开发:
|
||||||
@ -118,11 +150,11 @@ OPENAI_API_KEY=<your api key here>
|
|||||||
2. 执行 `yarn install && yarn dev` 即可。
|
2. 执行 `yarn install && yarn dev` 即可。
|
||||||
|
|
||||||
### 本地部署 Local Deployment
|
### 本地部署 Local Deployment
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### 容器部署 Docker Deployment
|
### 容器部署 Docker Deployment
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@ -137,15 +169,12 @@ docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-
|
|||||||
|
|
||||||
![更多展示 More](./static/more.png)
|
![更多展示 More](./static/more.png)
|
||||||
|
|
||||||
## 说明 Attention
|
|
||||||
|
|
||||||
本项目的演示地址所用的 OpenAI 账户的免费额度将于 2023-04-01 过期,届时将无法通过演示地址在线体验。
|
## 捐赠 Donate USDT
|
||||||
|
> BNB Smart Chain (BEP 20)
|
||||||
如果你想贡献出自己的 API Key,可以通过作者主页的邮箱发送给作者,并标注过期时间。
|
```
|
||||||
|
0x67cD02c7EB62641De576a1fA3EdB32eA0c3ffD89
|
||||||
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
|
## 鸣谢 Special Thanks
|
||||||
|
|
||||||
@ -157,6 +186,7 @@ If you would like to contribute your API key, you can email it to the author and
|
|||||||
[@hoochanlon](https://github.com/hoochanlon)
|
[@hoochanlon](https://github.com/hoochanlon)
|
||||||
|
|
||||||
### 贡献者 Contributor
|
### 贡献者 Contributor
|
||||||
|
|
||||||
[Contributors](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
|
[Contributors](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
|
||||||
|
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
@ -1,26 +1,12 @@
|
|||||||
import { createParser } from "eventsource-parser";
|
import { createParser } from "eventsource-parser";
|
||||||
import { NextRequest } from "next/server";
|
import { NextRequest } from "next/server";
|
||||||
|
import { requestOpenai } from "../common";
|
||||||
|
|
||||||
async function createStream(req: NextRequest) {
|
async function createStream(req: NextRequest) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
let apiKey = process.env.OPENAI_API_KEY;
|
const res = await requestOpenai(req);
|
||||||
|
|
||||||
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: req.body,
|
|
||||||
});
|
|
||||||
|
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
|
1
app/api/chat/.gitignore
vendored
1
app/api/chat/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
config.ts
|
|
@ -1,29 +0,0 @@
|
|||||||
import { OpenAIApi, Configuration } from "openai";
|
|
||||||
import { ChatRequest } from "./typing";
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
|
||||||
try {
|
|
||||||
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);
|
|
||||||
return new Response(JSON.stringify(e));
|
|
||||||
}
|
|
||||||
}
|
|
22
app/api/common.ts
Normal file
22
app/api/common.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
const OPENAI_URL = "api.openai.com";
|
||||||
|
const DEFAULT_PROTOCOL = "https";
|
||||||
|
const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL;
|
||||||
|
const BASE_URL = process.env.BASE_URL ?? OPENAI_URL;
|
||||||
|
|
||||||
|
export async function requestOpenai(req: NextRequest) {
|
||||||
|
const apiKey = req.headers.get("token");
|
||||||
|
const openaiPath = req.headers.get("path");
|
||||||
|
|
||||||
|
console.log("[Proxy] ", openaiPath);
|
||||||
|
|
||||||
|
return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
method: req.method,
|
||||||
|
body: req.body,
|
||||||
|
});
|
||||||
|
}
|
28
app/api/openai/route.ts
Normal file
28
app/api/openai/route.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { requestOpenai } from "../common";
|
||||||
|
|
||||||
|
async function makeRequest(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const res = await requestOpenai(req);
|
||||||
|
return new Response(res.body);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[OpenAI] ", req.body, e);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
msg: JSON.stringify(e),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
return makeRequest(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
return makeRequest(req);
|
||||||
|
}
|
@ -221,6 +221,14 @@
|
|||||||
margin-bottom: 100px;
|
margin-bottom: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-body-title {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.chat-message {
|
.chat-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -128,7 +128,7 @@ function useSubmitHandler() {
|
|||||||
|
|
||||||
const shouldSubmit = (e: KeyboardEvent) => {
|
const shouldSubmit = (e: KeyboardEvent) => {
|
||||||
if (e.key !== "Enter") return false;
|
if (e.key !== "Enter") return false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
||||||
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
||||||
@ -170,7 +170,10 @@ export function PromptHints(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean }) {
|
export function Chat(props: {
|
||||||
|
showSideBar?: () => void;
|
||||||
|
sideBarShowing?: boolean;
|
||||||
|
}) {
|
||||||
type RenderMessage = Message & { preview?: boolean };
|
type RenderMessage = Message & { preview?: boolean };
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
@ -190,7 +193,6 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean
|
|||||||
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
|
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
|
||||||
const onSearch = useDebouncedCallback(
|
const onSearch = useDebouncedCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
if (chatStore.config.disablePromptHint) return;
|
|
||||||
setPromptHints(promptStore.search(text));
|
setPromptHints(promptStore.search(text));
|
||||||
},
|
},
|
||||||
100,
|
100,
|
||||||
@ -203,20 +205,31 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean
|
|||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scrollInput = () => {
|
||||||
|
const dom = inputRef.current;
|
||||||
|
if (!dom) return;
|
||||||
|
const paddingBottomNum: number = parseInt(
|
||||||
|
window.getComputedStyle(dom).paddingBottom,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
|
||||||
|
};
|
||||||
|
|
||||||
// only search prompts when user input is short
|
// only search prompts when user input is short
|
||||||
const SEARCH_TEXT_LIMIT = 30;
|
const SEARCH_TEXT_LIMIT = 30;
|
||||||
const onInput = (text: string) => {
|
const onInput = (text: string) => {
|
||||||
const textareaDom = inputRef.current
|
scrollInput();
|
||||||
if (textareaDom) {
|
|
||||||
const paddingBottomNum: number = parseInt(window.getComputedStyle(textareaDom).paddingBottom, 10);
|
|
||||||
textareaDom.scrollTop = textareaDom.scrollHeight - textareaDom.offsetHeight + paddingBottomNum;
|
|
||||||
}
|
|
||||||
setUserInput(text);
|
setUserInput(text);
|
||||||
const n = text.trim().length;
|
const n = text.trim().length;
|
||||||
if (n === 0 || n > SEARCH_TEXT_LIMIT) {
|
|
||||||
|
// clear search results
|
||||||
|
if (n === 0) {
|
||||||
setPromptHints([]);
|
setPromptHints([]);
|
||||||
} else {
|
} else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
||||||
onSearch(text);
|
// check if need to trigger auto completion
|
||||||
|
if (text.startsWith("/") && text.length > 1) {
|
||||||
|
onSearch(text.slice(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -226,6 +239,7 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
||||||
setUserInput("");
|
setUserInput("");
|
||||||
|
setPromptHints([]);
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -262,6 +276,7 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean
|
|||||||
chatStore
|
chatStore
|
||||||
.onUserInput(messages[i].content)
|
.onUserInput(messages[i].content)
|
||||||
.then(() => setIsLoading(false));
|
.then(() => setIsLoading(false));
|
||||||
|
inputRef.current?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -306,7 +321,6 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean
|
|||||||
const dom = latestMessageRef.current;
|
const dom = latestMessageRef.current;
|
||||||
if (dom && !isIOS() && autoScroll) {
|
if (dom && !isIOS() && autoScroll) {
|
||||||
dom.scrollIntoView({
|
dom.scrollIntoView({
|
||||||
behavior: "smooth",
|
|
||||||
block: "end",
|
block: "end",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -320,7 +334,17 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean
|
|||||||
className={styles["window-header-title"]}
|
className={styles["window-header-title"]}
|
||||||
onClick={props?.showSideBar}
|
onClick={props?.showSideBar}
|
||||||
>
|
>
|
||||||
<div className={styles["window-header-main-title"]}>
|
<div
|
||||||
|
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
||||||
|
onClick={() => {
|
||||||
|
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
||||||
|
if (newTopic && newTopic !== session.topic) {
|
||||||
|
chatStore.updateCurrentSession(
|
||||||
|
(session) => (session.topic = newTopic!)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
{session.topic}
|
{session.topic}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-header-sub-title"]}>
|
<div className={styles["window-header-sub-title"]}>
|
||||||
@ -380,32 +404,33 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles["chat-message-item"]}>
|
<div className={styles["chat-message-item"]}>
|
||||||
{(!isUser && !(message.preview || message.content.length === 0)) && (
|
{!isUser &&
|
||||||
<div className={styles["chat-message-top-actions"]}>
|
!(message.preview || message.content.length === 0) && (
|
||||||
{message.streaming ? (
|
<div className={styles["chat-message-top-actions"]}>
|
||||||
<div
|
{message.streaming ? (
|
||||||
className={styles["chat-message-top-action"]}
|
<div
|
||||||
onClick={() => onUserStop(i)}
|
className={styles["chat-message-top-action"]}
|
||||||
>
|
onClick={() => onUserStop(i)}
|
||||||
{Locale.Chat.Actions.Stop}
|
>
|
||||||
</div>
|
{Locale.Chat.Actions.Stop}
|
||||||
) : (
|
</div>
|
||||||
<div
|
) : (
|
||||||
className={styles["chat-message-top-action"]}
|
<div
|
||||||
onClick={() => onResend(i)}
|
className={styles["chat-message-top-action"]}
|
||||||
>
|
onClick={() => onResend(i)}
|
||||||
{Locale.Chat.Actions.Retry}
|
>
|
||||||
</div>
|
{Locale.Chat.Actions.Retry}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles["chat-message-top-action"]}
|
className={styles["chat-message-top-action"]}
|
||||||
onClick={() => copyToClipboard(message.content)}
|
onClick={() => copyToClipboard(message.content)}
|
||||||
>
|
>
|
||||||
{Locale.Chat.Actions.Copy}
|
{Locale.Chat.Actions.Copy}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
{(message.preview || message.content.length === 0) &&
|
{(message.preview || message.content.length === 0) &&
|
||||||
!isUser ? (
|
!isUser ? (
|
||||||
<LoadingIcon />
|
<LoadingIcon />
|
||||||
@ -430,7 +455,7 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div ref={latestMessageRef} style={{ opacity: 0, height: "2em" }}>
|
<div ref={latestMessageRef} style={{ opacity: 0, height: "4em" }}>
|
||||||
-
|
-
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -653,7 +678,11 @@ export function Home() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Chat key="chat" showSideBar={() => setShowSideBar(true)} sideBarShowing={showSideBar} />
|
<Chat
|
||||||
|
key="chat"
|
||||||
|
showSideBar={() => setShowSideBar(true)}
|
||||||
|
sideBarShowing={showSideBar}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,6 +27,7 @@ import { getCurrentCommitId } from "../utils";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { UPDATE_URL } from "../constant";
|
import { UPDATE_URL } from "../constant";
|
||||||
import { SearchService, usePromptStore } from "../store/prompt";
|
import { SearchService, usePromptStore } from "../store/prompt";
|
||||||
|
import { requestUsage } from "../requests";
|
||||||
|
|
||||||
function SettingItem(props: {
|
function SettingItem(props: {
|
||||||
title: string;
|
title: string;
|
||||||
@ -54,7 +55,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
state.updateConfig,
|
state.updateConfig,
|
||||||
state.resetConfig,
|
state.resetConfig,
|
||||||
state.clearAllData,
|
state.clearAllData,
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateStore = useUpdateStore();
|
const updateStore = useUpdateStore();
|
||||||
@ -70,14 +71,34 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [usage, setUsage] = useState<{
|
||||||
|
granted?: number;
|
||||||
|
used?: number;
|
||||||
|
}>();
|
||||||
|
const [loadingUsage, setLoadingUsage] = useState(false);
|
||||||
|
function checkUsage() {
|
||||||
|
setLoadingUsage(true);
|
||||||
|
requestUsage()
|
||||||
|
.then((res) =>
|
||||||
|
setUsage({
|
||||||
|
granted: res?.total_granted,
|
||||||
|
used: res?.total_used,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.finally(() => {
|
||||||
|
setLoadingUsage(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkUpdate();
|
checkUpdate();
|
||||||
|
checkUsage();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const enabledAccessControl = useMemo(
|
const enabledAccessControl = useMemo(
|
||||||
() => accessStore.enabledAccessControl(),
|
() => accessStore.enabledAccessControl(),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const promptStore = usePromptStore();
|
const promptStore = usePromptStore();
|
||||||
@ -179,7 +200,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
updateConfig(
|
updateConfig(
|
||||||
(config) =>
|
(config) =>
|
||||||
(config.submitKey = e.target.value as any as SubmitKey)
|
(config.submitKey = e.target.value as any as SubmitKey),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -199,7 +220,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
value={config.theme}
|
value={config.theme}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
updateConfig(
|
updateConfig(
|
||||||
(config) => (config.theme = e.target.value as any as Theme)
|
(config) => (config.theme = e.target.value as any as Theme),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -240,7 +261,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig(
|
updateConfig(
|
||||||
(config) =>
|
(config) =>
|
||||||
(config.fontSize = Number.parseInt(e.currentTarget.value))
|
(config.fontSize = Number.parseInt(e.currentTarget.value)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
@ -253,7 +274,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
checked={config.tightBorder}
|
checked={config.tightBorder}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig(
|
updateConfig(
|
||||||
(config) => (config.tightBorder = e.currentTarget.checked)
|
(config) => (config.tightBorder = e.currentTarget.checked),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
@ -271,7 +292,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig(
|
updateConfig(
|
||||||
(config) =>
|
(config) =>
|
||||||
(config.disablePromptHint = e.currentTarget.checked)
|
(config.disablePromptHint = e.currentTarget.checked),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
@ -281,7 +302,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
title={Locale.Settings.Prompt.List}
|
title={Locale.Settings.Prompt.List}
|
||||||
subTitle={Locale.Settings.Prompt.ListCount(
|
subTitle={Locale.Settings.Prompt.ListCount(
|
||||||
builtinCount,
|
builtinCount,
|
||||||
customCount
|
customCount,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -324,6 +345,28 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
></input>
|
></input>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
title={Locale.Settings.Usage.Title}
|
||||||
|
subTitle={
|
||||||
|
loadingUsage
|
||||||
|
? Locale.Settings.Usage.IsChecking
|
||||||
|
: Locale.Settings.Usage.SubTitle(
|
||||||
|
usage?.granted ?? "[?]",
|
||||||
|
usage?.used ?? "[?]",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loadingUsage ? (
|
||||||
|
<div />
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
icon={<ResetIcon></ResetIcon>}
|
||||||
|
text={Locale.Settings.Usage.Check}
|
||||||
|
onClick={checkUsage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title={Locale.Settings.HistoryCount.Title}
|
title={Locale.Settings.HistoryCount.Title}
|
||||||
subTitle={Locale.Settings.HistoryCount.SubTitle}
|
subTitle={Locale.Settings.HistoryCount.SubTitle}
|
||||||
@ -338,7 +381,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig(
|
updateConfig(
|
||||||
(config) =>
|
(config) =>
|
||||||
(config.historyMessageCount = e.target.valueAsNumber)
|
(config.historyMessageCount = e.target.valueAsNumber),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
@ -357,7 +400,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
updateConfig(
|
updateConfig(
|
||||||
(config) =>
|
(config) =>
|
||||||
(config.compressMessageLengthThreshold =
|
(config.compressMessageLengthThreshold =
|
||||||
e.currentTarget.valueAsNumber)
|
e.currentTarget.valueAsNumber),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
@ -370,7 +413,8 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
value={config.modelConfig.model}
|
value={config.modelConfig.model}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
updateConfig(
|
updateConfig(
|
||||||
(config) => (config.modelConfig.model = e.currentTarget.value)
|
(config) =>
|
||||||
|
(config.modelConfig.model = e.currentTarget.value),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -395,7 +439,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
updateConfig(
|
updateConfig(
|
||||||
(config) =>
|
(config) =>
|
||||||
(config.modelConfig.temperature =
|
(config.modelConfig.temperature =
|
||||||
e.currentTarget.valueAsNumber)
|
e.currentTarget.valueAsNumber),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
></input>
|
></input>
|
||||||
@ -413,7 +457,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
updateConfig(
|
updateConfig(
|
||||||
(config) =>
|
(config) =>
|
||||||
(config.modelConfig.max_tokens =
|
(config.modelConfig.max_tokens =
|
||||||
e.currentTarget.valueAsNumber)
|
e.currentTarget.valueAsNumber),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
@ -432,7 +476,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
updateConfig(
|
updateConfig(
|
||||||
(config) =>
|
(config) =>
|
||||||
(config.modelConfig.presence_penalty =
|
(config.modelConfig.presence_penalty =
|
||||||
e.currentTarget.valueAsNumber)
|
e.currentTarget.valueAsNumber),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
></input>
|
></input>
|
||||||
|
@ -18,6 +18,7 @@ const cn = {
|
|||||||
Stop: "停止",
|
Stop: "停止",
|
||||||
Retry: "重试",
|
Retry: "重试",
|
||||||
},
|
},
|
||||||
|
Rename: "重命名对话",
|
||||||
Typing: "正在输入…",
|
Typing: "正在输入…",
|
||||||
Input: (submitKey: string) => {
|
Input: (submitKey: string) => {
|
||||||
var inputHints = `输入消息,${submitKey} 发送`;
|
var inputHints = `输入消息,${submitKey} 发送`;
|
||||||
@ -63,6 +64,7 @@ const cn = {
|
|||||||
Title: "字体大小",
|
Title: "字体大小",
|
||||||
SubTitle: "聊天内容的字体大小",
|
SubTitle: "聊天内容的字体大小",
|
||||||
},
|
},
|
||||||
|
|
||||||
Update: {
|
Update: {
|
||||||
Version: (x: string) => `当前版本:${x}`,
|
Version: (x: string) => `当前版本:${x}`,
|
||||||
IsLatest: "已是最新版本",
|
IsLatest: "已是最新版本",
|
||||||
@ -77,7 +79,7 @@ const cn = {
|
|||||||
Prompt: {
|
Prompt: {
|
||||||
Disable: {
|
Disable: {
|
||||||
Title: "禁用提示词自动补全",
|
Title: "禁用提示词自动补全",
|
||||||
SubTitle: "禁用后将无法自动根据输入补全",
|
SubTitle: "在输入框开头输入 / 即可触发自动补全",
|
||||||
},
|
},
|
||||||
List: "自定义提示词列表",
|
List: "自定义提示词列表",
|
||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
@ -97,6 +99,14 @@ const cn = {
|
|||||||
SubTitle: "使用自己的 Key 可绕过受控访问限制",
|
SubTitle: "使用自己的 Key 可绕过受控访问限制",
|
||||||
Placeholder: "OpenAI API Key",
|
Placeholder: "OpenAI API Key",
|
||||||
},
|
},
|
||||||
|
Usage: {
|
||||||
|
Title: "账户余额",
|
||||||
|
SubTitle(granted: any, used: any) {
|
||||||
|
return `总共 $${granted},已使用 $${used}`;
|
||||||
|
},
|
||||||
|
IsChecking: "正在检查…",
|
||||||
|
Check: "重新检查",
|
||||||
|
},
|
||||||
AccessCode: {
|
AccessCode: {
|
||||||
Title: "访问码",
|
Title: "访问码",
|
||||||
SubTitle: "现在是受控访问状态",
|
SubTitle: "现在是受控访问状态",
|
||||||
@ -124,7 +134,7 @@ const cn = {
|
|||||||
History: (content: string) =>
|
History: (content: string) =>
|
||||||
"这是 ai 和用户的历史聊天总结作为前情提要:" + content,
|
"这是 ai 和用户的历史聊天总结作为前情提要:" + content,
|
||||||
Topic:
|
Topic:
|
||||||
"直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”",
|
"使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,如果没有主题,请直接返回“闲聊”",
|
||||||
Summarize:
|
Summarize:
|
||||||
"简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内",
|
"简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内",
|
||||||
},
|
},
|
||||||
|
@ -20,6 +20,7 @@ const en: LocaleType = {
|
|||||||
Stop: "Stop",
|
Stop: "Stop",
|
||||||
Retry: "Retry",
|
Retry: "Retry",
|
||||||
},
|
},
|
||||||
|
Rename: "Rename Chat",
|
||||||
Typing: "Typing…",
|
Typing: "Typing…",
|
||||||
Input: (submitKey: string) => {
|
Input: (submitKey: string) => {
|
||||||
var inputHints = `Type something and press ${submitKey} to send`;
|
var inputHints = `Type something and press ${submitKey} to send`;
|
||||||
@ -79,7 +80,7 @@ const en: LocaleType = {
|
|||||||
Prompt: {
|
Prompt: {
|
||||||
Disable: {
|
Disable: {
|
||||||
Title: "Disable auto-completion",
|
Title: "Disable auto-completion",
|
||||||
SubTitle: "After disabling, auto-completion will not be available",
|
SubTitle: "Input / to trigger auto-completion",
|
||||||
},
|
},
|
||||||
List: "Prompt List",
|
List: "Prompt List",
|
||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
@ -100,6 +101,14 @@ const en: LocaleType = {
|
|||||||
SubTitle: "Use your key to ignore access code limit",
|
SubTitle: "Use your key to ignore access code limit",
|
||||||
Placeholder: "OpenAI API Key",
|
Placeholder: "OpenAI API Key",
|
||||||
},
|
},
|
||||||
|
Usage: {
|
||||||
|
Title: "Account Balance",
|
||||||
|
SubTitle(granted: any, used: any) {
|
||||||
|
return `Total $${granted}, Used $${used}`;
|
||||||
|
},
|
||||||
|
IsChecking: "Checking...",
|
||||||
|
Check: "Check Again",
|
||||||
|
},
|
||||||
AccessCode: {
|
AccessCode: {
|
||||||
Title: "Access Code",
|
Title: "Access Code",
|
||||||
SubTitle: "Access control enabled",
|
SubTitle: "Access control enabled",
|
||||||
@ -129,7 +138,7 @@ const en: LocaleType = {
|
|||||||
"This is a summary of the chat history between the AI and the user as a recap: " +
|
"This is a summary of the chat history between the AI and the user as a recap: " +
|
||||||
content,
|
content,
|
||||||
Topic:
|
Topic:
|
||||||
"Provide a brief topic of the sentence without explanation. If there is no topic, return 'Chitchat'.",
|
"Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, or additional text. Remove enclosing quotation marks.",
|
||||||
Summarize:
|
Summarize:
|
||||||
"Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.",
|
"Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.",
|
||||||
},
|
},
|
||||||
|
@ -19,8 +19,9 @@ const tw: LocaleType = {
|
|||||||
Stop: "停止",
|
Stop: "停止",
|
||||||
Retry: "重試",
|
Retry: "重試",
|
||||||
},
|
},
|
||||||
|
Rename: "重命名對話",
|
||||||
Typing: "正在輸入…",
|
Typing: "正在輸入…",
|
||||||
Input: (submitKey: string) => {
|
Input: (submitKey: string) => {
|
||||||
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`;
|
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`;
|
||||||
if (submitKey === String(SubmitKey.Enter)) {
|
if (submitKey === String(SubmitKey.Enter)) {
|
||||||
inputHints += ",Shift + Enter 鍵換行";
|
inputHints += ",Shift + Enter 鍵換行";
|
||||||
@ -78,7 +79,7 @@ const tw: LocaleType = {
|
|||||||
Prompt: {
|
Prompt: {
|
||||||
Disable: {
|
Disable: {
|
||||||
Title: "停用提示詞自動補全",
|
Title: "停用提示詞自動補全",
|
||||||
SubTitle: "若停用後,將無法自動根據輸入進行補全",
|
SubTitle: "在輸入框開頭輸入 / 即可觸發自動補全",
|
||||||
},
|
},
|
||||||
List: "自定義提示詞列表",
|
List: "自定義提示詞列表",
|
||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
@ -98,6 +99,14 @@ const tw: LocaleType = {
|
|||||||
SubTitle: "使用自己的 Key 可規避受控訪問限制",
|
SubTitle: "使用自己的 Key 可規避受控訪問限制",
|
||||||
Placeholder: "OpenAI API Key",
|
Placeholder: "OpenAI API Key",
|
||||||
},
|
},
|
||||||
|
Usage: {
|
||||||
|
Title: "帳戶餘額",
|
||||||
|
SubTitle(granted: any, used: any) {
|
||||||
|
return `總共 $${granted},已使用 $${used}`;
|
||||||
|
},
|
||||||
|
IsChecking: "正在檢查…",
|
||||||
|
Check: "重新檢查",
|
||||||
|
},
|
||||||
AccessCode: {
|
AccessCode: {
|
||||||
Title: "訪問碼",
|
Title: "訪問碼",
|
||||||
SubTitle: "現在是受控訪問狀態",
|
SubTitle: "現在是受控訪問狀態",
|
||||||
@ -124,8 +133,7 @@ const tw: LocaleType = {
|
|||||||
Prompt: {
|
Prompt: {
|
||||||
History: (content: string) =>
|
History: (content: string) =>
|
||||||
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
|
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
|
||||||
Topic:
|
Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
|
||||||
"直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
|
|
||||||
Summarize:
|
Summarize:
|
||||||
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 50 字以內",
|
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 50 字以內",
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { ChatRequest, ChatReponse } from "./api/chat/typing";
|
import type { ChatRequest, ChatReponse } from "./api/openai/typing";
|
||||||
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ const makeRequestParam = (
|
|||||||
options?: {
|
options?: {
|
||||||
filterBot?: boolean;
|
filterBot?: boolean;
|
||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
}
|
},
|
||||||
): ChatRequest => {
|
): ChatRequest => {
|
||||||
let sendMessages = messages.map((v) => ({
|
let sendMessages = messages.map((v) => ({
|
||||||
role: v.role,
|
role: v.role,
|
||||||
@ -42,19 +42,48 @@ function getHeaders() {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function requestOpenaiClient(path: string) {
|
||||||
|
return (body: any, method = "POST") =>
|
||||||
|
fetch("/api/openai", {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
path,
|
||||||
|
...getHeaders(),
|
||||||
|
},
|
||||||
|
body: body && JSON.stringify(body),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function requestChat(messages: Message[]) {
|
export async function requestChat(messages: Message[]) {
|
||||||
const req: ChatRequest = makeRequestParam(messages, { filterBot: true });
|
const req: ChatRequest = makeRequestParam(messages, { filterBot: true });
|
||||||
|
|
||||||
const res = await fetch("/api/chat", {
|
const res = await requestOpenaiClient("v1/chat/completions")(req);
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...getHeaders(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify(req),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (await res.json()) as ChatReponse;
|
try {
|
||||||
|
const response = (await res.json()) as ChatReponse;
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Request Chat] ", error, res.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestUsage() {
|
||||||
|
const res = await requestOpenaiClient("dashboard/billing/credit_grants")(
|
||||||
|
null,
|
||||||
|
"GET",
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = (await res.json()) as {
|
||||||
|
total_available: number;
|
||||||
|
total_granted: number;
|
||||||
|
total_used: number;
|
||||||
|
};
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Request usage] ", error, res.body);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestChatStream(
|
export async function requestChatStream(
|
||||||
@ -65,7 +94,7 @@ export async function requestChatStream(
|
|||||||
onMessage: (message: string, done: boolean) => void;
|
onMessage: (message: string, done: boolean) => void;
|
||||||
onError: (error: Error) => void;
|
onError: (error: Error) => void;
|
||||||
onController?: (controller: AbortController) => void;
|
onController?: (controller: AbortController) => void;
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
const req = makeRequestParam(messages, {
|
const req = makeRequestParam(messages, {
|
||||||
stream: true,
|
stream: true,
|
||||||
@ -87,6 +116,7 @@ export async function requestChatStream(
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
path: "v1/chat/completions",
|
||||||
...getHeaders(),
|
...getHeaders(),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(req),
|
body: JSON.stringify(req),
|
||||||
@ -129,7 +159,7 @@ export async function requestChatStream(
|
|||||||
responseText = Locale.Error.Unauthorized;
|
responseText = Locale.Error.Unauthorized;
|
||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
console.error("Stream Error");
|
console.error("Stream Error", res.body);
|
||||||
options?.onError(new Error("Stream Error"));
|
options?.onError(new Error("Stream Error"));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -149,7 +179,7 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
|
|||||||
|
|
||||||
const res = await requestChat(messages);
|
const res = await requestChat(messages);
|
||||||
|
|
||||||
return res.choices.at(0)?.message?.content ?? "";
|
return res?.choices?.at(0)?.message?.content ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// To store message streaming controller
|
// To store message streaming controller
|
||||||
@ -159,7 +189,7 @@ export const ControllerPool = {
|
|||||||
addController(
|
addController(
|
||||||
sessionIndex: number,
|
sessionIndex: number,
|
||||||
messageIndex: number,
|
messageIndex: number,
|
||||||
controller: AbortController
|
controller: AbortController,
|
||||||
) {
|
) {
|
||||||
const key = this.key(sessionIndex, messageIndex);
|
const key = this.key(sessionIndex, messageIndex);
|
||||||
this.controllers[key] = controller;
|
this.controllers[key] = controller;
|
||||||
|
@ -206,6 +206,10 @@ interface ChatStore {
|
|||||||
clearAllData: () => void;
|
clearAllData: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function countMessages(msgs: Message[]) {
|
||||||
|
return msgs.reduce((pre, cur) => pre + cur.content.length, 0);
|
||||||
|
}
|
||||||
|
|
||||||
const LOCAL_KEY = "chat-next-web-store";
|
const LOCAL_KEY = "chat-next-web-store";
|
||||||
|
|
||||||
export const useChatStore = create<ChatStore>()(
|
export const useChatStore = create<ChatStore>()(
|
||||||
@ -393,8 +397,12 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
summarizeSession() {
|
summarizeSession() {
|
||||||
const session = get().currentSession();
|
const session = get().currentSession();
|
||||||
|
|
||||||
if (session.topic === DEFAULT_TOPIC && session.messages.length >= 3) {
|
// should summarize topic after chating more than 50 words
|
||||||
// should summarize topic
|
const SUMMARIZE_MIN_LEN = 50;
|
||||||
|
if (
|
||||||
|
session.topic === DEFAULT_TOPIC &&
|
||||||
|
countMessages(session.messages) >= SUMMARIZE_MIN_LEN
|
||||||
|
) {
|
||||||
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
|
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
|
||||||
(res) => {
|
(res) => {
|
||||||
get().updateCurrentSession(
|
get().updateCurrentSession(
|
||||||
@ -408,10 +416,7 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
let toBeSummarizedMsgs = session.messages.slice(
|
let toBeSummarizedMsgs = session.messages.slice(
|
||||||
session.lastSummarizeIndex,
|
session.lastSummarizeIndex,
|
||||||
);
|
);
|
||||||
const historyMsgLength = toBeSummarizedMsgs.reduce(
|
const historyMsgLength = countMessages(toBeSummarizedMsgs);
|
||||||
(pre, cur) => pre + cur.content.length,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (historyMsgLength > 4000) {
|
if (historyMsgLength > 4000) {
|
||||||
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
|
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
|
||||||
|
@ -3,10 +3,10 @@ import { ACCESS_CODES } from "./app/api/access";
|
|||||||
import md5 from "spark-md5";
|
import md5 from "spark-md5";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/api/chat", "/api/chat-stream"],
|
matcher: ["/api/openai", "/api/chat-stream"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function middleware(req: NextRequest, res: NextResponse) {
|
export function middleware(req: NextRequest) {
|
||||||
const accessCode = req.headers.get("access-code");
|
const accessCode = req.headers.get("access-code");
|
||||||
const token = req.headers.get("token");
|
const token = req.headers.get("token");
|
||||||
const hashedCode = md5.hash(accessCode ?? "").trim();
|
const hashedCode = md5.hash(accessCode ?? "").trim();
|
||||||
@ -18,14 +18,40 @@ export function middleware(req: NextRequest, res: NextResponse) {
|
|||||||
if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) {
|
if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
error: true,
|
||||||
needAccessCode: true,
|
needAccessCode: true,
|
||||||
hint: "Please go settings page and fill your access code.",
|
msg: "Please go settings page and fill your access code.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 401,
|
status: 401,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
// inject api key
|
||||||
|
if (!token) {
|
||||||
|
const apiKey = process.env.OPENAI_API_KEY;
|
||||||
|
if (apiKey) {
|
||||||
|
console.log("[Auth] set system token");
|
||||||
|
req.headers.set("token", apiKey);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
msg: "Empty Api Key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("[Auth] set user token");
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: req.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,13 @@
|
|||||||
const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache";
|
const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache";
|
||||||
|
|
||||||
self.addEventListener('activate', function (event) {
|
self.addEventListener("activate", function (event) {
|
||||||
console.log('ServiceWorker activated.');
|
console.log("ServiceWorker activated.");
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('install', function (event) {
|
self.addEventListener("install", function (event) {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open(CHATGPT_NEXT_WEB_CACHE)
|
caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) {
|
||||||
.then(function (cache) {
|
return cache.addAll([]);
|
||||||
return cache.addAll([
|
}),
|
||||||
]);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('fetch', function (event) {
|
|
||||||
event.respondWith(
|
|
||||||
caches.match(event.request)
|
|
||||||
.then(function (response) {
|
|
||||||
return response || fetch(event.request);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user