Compare commits

...

63 Commits

Author SHA1 Message Date
Yifei Zhang
401fa198c9 Update tauri.conf.json 2023-11-17 11:29:52 +08:00
Yifei Zhang
600df4f63a Merge pull request #3290 from Eric-2369/main 2023-11-17 11:26:21 +08:00
Eric-2369
74eb42c111 config: exclude hkg1 from the deployment regions of the edge function 2023-11-16 11:33:10 +08:00
Yidadaa
9876a1aeca fix: #3275 refuse on server side if hide user api key 2023-11-16 00:53:11 +08:00
Yifei Zhang
d898ffce23 Merge pull request #3261 from H0llyW00dzZ/clients 2023-11-14 16:07:16 +08:00
H0llyW00dzZ
f1772f4625 Fix UI/UX Page [Settings]
[+] fix(settings.tsx): fix condition to show balance query in settings component
[+] fix(settings.tsx): add condition to hide balance query for app clients
2023-11-14 15:04:57 +07:00
Yifei Zhang
9da455a7ea Merge pull request #3248 from Yidadaa/dependabot/npm_and_yarn/nanoid-5.0.3 2023-11-14 01:24:10 +08:00
Yifei Zhang
2bd6342309 Merge pull request #3249 from Yidadaa/dependabot/npm_and_yarn/emoji-picker-react-4.5.15 2023-11-14 01:23:50 +08:00
Yifei Zhang
5e73577088 Merge pull request #3250 from Yidadaa/dependabot/npm_and_yarn/types/node-20.9.0 2023-11-14 01:23:37 +08:00
Yifei Zhang
5156a80738 Merge pull request #3251 from Yidadaa/dependabot/npm_and_yarn/types/spark-md5-3.0.4 2023-11-14 01:23:17 +08:00
dependabot[bot]
a9d605ed30 chore(deps): bump nanoid from 4.0.2 to 5.0.3
Bumps [nanoid](https://github.com/ai/nanoid) from 4.0.2 to 5.0.3.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/4.0.2...5.0.3)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 17:22:51 +00:00
dependabot[bot]
7d1fae32cd chore(deps): bump emoji-picker-react from 4.5.1 to 4.5.15
Bumps [emoji-picker-react](https://github.com/ealush/emoji-picker-react) from 4.5.1 to 4.5.15.
- [Release notes](https://github.com/ealush/emoji-picker-react/releases)
- [Commits](https://github.com/ealush/emoji-picker-react/commits)

---
updated-dependencies:
- dependency-name: emoji-picker-react
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 17:22:50 +00:00
Yifei Zhang
9f17b45bc4 Merge pull request #3252 from Yidadaa/dependabot/npm_and_yarn/mermaid-10.6.1 2023-11-14 01:21:11 +08:00
dependabot[bot]
a64c9dae42 chore(deps): bump mermaid from 10.3.1 to 10.6.1
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 10.3.1 to 10.6.1.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/v10.3.1...v10.6.1)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 10:45:59 +00:00
dependabot[bot]
5fbf4c394c chore(deps-dev): bump @types/spark-md5 from 3.0.2 to 3.0.4
Bumps [@types/spark-md5](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/spark-md5) from 3.0.2 to 3.0.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/spark-md5)

---
updated-dependencies:
- dependency-name: "@types/spark-md5"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 10:45:47 +00:00
dependabot[bot]
1e5153173c chore(deps-dev): bump @types/node from 20.3.3 to 20.9.0
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.3.3 to 20.9.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 10:45:37 +00:00
Yifei Zhang
011b52d07d Update README.md 2023-11-13 16:53:36 +08:00
Yifei Zhang
d033168d80 fix: #3241 should not ensure openai url non-empty 2023-11-13 10:53:30 +08:00
Yifei Zhang
fdca9e59de Merge pull request #3233 from nanaya-tachibana/nanaya/model-selection 2023-11-13 10:44:03 +08:00
Yifei Zhang
549a2fd206 Merge pull request #3237 from Yidadaa/latex 2023-11-12 19:47:26 +08:00
Yidadaa
a0cd939bfd fix: #2841 dollar sign conflict with latex math 2023-11-12 19:45:58 +08:00
Yifei Zhang
4f52679ec6 Merge pull request #3236 from Yidadaa/latex 2023-11-12 19:35:22 +08:00
Yidadaa
b52e237044 fix: #3189 should correct math eq in exporter 2023-11-12 19:33:19 +08:00
Yifei Zhang
17b329bb63 Merge pull request #3232 from H0llyW00dzZ/language 2023-11-12 18:00:02 +08:00
nanaya
3a654ba199 UI (model selection): hide unavailable model options 2023-11-12 11:18:14 +08:00
H0llyW00dzZ
5ba3fc9321 Revert "Fix UI/UX Page Local Language [Exporter Message]"
This reverts commit a46f08154e.
Reason : better after instead of before
2023-11-12 01:56:31 +07:00
H0llyW00dzZ
a46f08154e Fix UI/UX Page Local Language [Exporter Message]
fix(locales): fix incorrect description title in en.ts and id.ts
 - Change "Only messages after clearing the context will be displayed" to "Only messages before clearing the context will be displayed" in en.ts
 - Change "Hanya pesan setelah menghapus konteks yang akan ditampilkan" to "Hanya pesan sebelum menghapus konteks yang akan ditampilkan" in id.ts
2023-11-12 01:50:35 +07:00
H0llyW00dzZ
0f6ed9c293 Feat UI/UX Page Local Language [Exporter Message]
[+] fix(exporter.tsx): update the text in the ExportMessageModal component to use the localized title from the locale file
[+] feat(cn.ts, en.ts, id.ts): add localized title for the Exporter Description in the respective locale files
2023-11-12 00:53:15 +07:00
Yifei Zhang
295864d36b Merge pull request #3231 from Yidadaa/bugfix-1112 2023-11-12 01:23:56 +08:00
Yidadaa
be6d45e49f feat: close #3222 share message list should start from clear context index 2023-11-12 01:21:39 +08:00
Yifei Zhang
22b6987249 Merge pull request #3230 from Yidadaa/bugfix-1112 2023-11-12 00:52:21 +08:00
Yidadaa
64647b0bb3 chore: update doc for mapped CUSTOM_MODELS 2023-11-12 00:49:58 +08:00
Yidadaa
a5a1f2e8ad feat: CUSTOM_MODELS support mapper 2023-11-12 00:46:21 +08:00
Yifei Zhang
8bd39f33e0 Merge pull request #3229 from Yidadaa/bugfix-1112 2023-11-12 00:31:15 +08:00
Yidadaa
be9774943b feat: #3224 auto switch to first avaliable model 2023-11-12 00:29:36 +08:00
Yifei Zhang
943214c6a7 Update README_CN.md 2023-11-10 18:21:22 +08:00
Yifei Zhang
ca792669fc Update constant.ts 2023-11-10 18:20:51 +08:00
Yifei Zhang
78d68a9949 Merge pull request #3214 from Ensteinjun/main 2023-11-10 15:58:28 +08:00
Ensteinjun
6b2db97347 fixbug 2023-11-10 15:44:07 +08:00
Yifei Zhang
2bfb362832 Update tauri.conf.json 2023-11-10 15:19:31 +08:00
Yidadaa
cb140e482f fix: #3207 ensure corner case 2023-11-10 15:15:43 +08:00
Yifei Zhang
e6b72ac1ff Update README.md 2023-11-10 02:59:47 +08:00
Yifei Zhang
8032e6d68d Update README.md 2023-11-10 02:59:30 +08:00
Yidadaa
c7e0a6f37f doc: update azure env vars 2023-11-10 02:57:46 +08:00
Yifei Zhang
1141cd2e6e Merge pull request #3206 from Yidadaa/azure 2023-11-10 02:55:12 +08:00
Yidadaa
c9dd953817 fixup 2023-11-10 02:50:50 +08:00
Yidadaa
b7ffca031e feat: close #935 add azure support 2023-11-10 02:43:37 +08:00
Yifei Zhang
fd2f441e02 feat: wont send max_tokens 2023-11-09 20:45:25 +08:00
Yifei Zhang
3b3ebda34b fix: #3196 3.5-turbo-1106 should use old cutoff date 2023-11-09 11:01:41 +08:00
Yifei Zhang
87e3d663a2 fix: #3192 use smaller max_tokens as default 2023-11-09 11:00:09 +08:00
Yidadaa
d0a1d910d4 fix: #3186 enable max_tokens in chat payload 2023-11-09 03:19:13 +08:00
Yifei Zhang
33b97082fa Merge pull request #3192 from Yidadaa/bugfix-1109 2023-11-09 03:05:41 +08:00
Yidadaa
d93f05f511 feat: close #3187 use CUSTOM_MODELS to control model list 2023-11-09 03:01:29 +08:00
Yifei Zhang
3dc29197e1 Merge pull request #3191 from Yidadaa/bugfix-1109 2023-11-09 02:23:20 +08:00
Yidadaa
fbc0236748 fix: #3174 should prompt to confirm to delete chat 2023-11-09 02:03:05 +08:00
Yifei Zhang
1304a3943a Merge pull request #3190 from Yidadaa/bugfix-1109 2023-11-09 01:54:09 +08:00
Yidadaa
8c0ba1aee2 feat: close #2954 chat summary should be copyable 2023-11-09 01:51:33 +08:00
Yifei Zhang
47154773f2 Merge pull request #3181 from AnYiEE/docs-typo 2023-11-08 18:37:59 +08:00
Yifei Zhang
deca9bf06d Merge pull request #3183 from hekbobo/patch-1 2023-11-08 18:37:35 +08:00
bob
6ab4c9be2e Update cloudflare-pages-cn.md
cloudflare 构建命令
2023-11-08 17:40:28 +08:00
安忆
3a519612a0 docs: fix typo in #3171 2023-11-08 15:53:45 +08:00
Yifei Zhang
d1ec26ae83 Merge pull request #3171 from AnYiEE/docs-update 2023-11-08 15:31:17 +08:00
安忆
9cb889c34f docs: up to date 2023-11-08 07:09:52 +08:00
58 changed files with 928 additions and 507 deletions

View File

@@ -17,11 +17,6 @@ BASE_URL=
# Default: Empty
OPENAI_ORG_ID=
# (optional)
# Default: Empty
# If you do not want users to input their own API key, set this value to 1.
HIDE_USER_API_KEY=
# (optional)
# Default: Empty
# If you do not want users to use GPT-4, set this value to 1.
@@ -29,5 +24,15 @@ DISABLE_GPT4=
# (optional)
# Default: Empty
# If you do not want users to query balance, set this value to 1.
HIDE_BALANCE_QUERY=
# If you do not want users to input their own API key, set this value to 1.
HIDE_USER_API_KEY=
# (optional)
# Default: Empty
# If you do want users to query balance, set this value to 1.
ENABLE_BALANCE_QUERY=
# (optional)
# Default: Empty
# If you want to disable parse settings from url, set this value to 1.
DISABLE_FAST_LINK=

View File

@@ -3,7 +3,7 @@
<h1 align="center">ChatGPT Next Web</h1>
English / [简体中文](./README_CN.md) / [日本語](./README_JA.md)
English / [简体中文](./README_CN.md)
One-Click to get well-designed cross-platform ChatGPT web UI.
@@ -62,6 +62,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
- 🚀 v2.8 now we have a client that runs across all platforms!
- 🚀 v2.9.11 you can use azure endpoint now.
## 主要功能
@@ -93,6 +94,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
- 💡 想要更方便地随时随地使用本项目可以试下这款桌面插件https://github.com/mushan0x0/AI0x0.com
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
## Get Started
@@ -153,14 +155,14 @@ After adding or modifying this environment variable, please redeploy the project
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
### `OPENAI_API_KEY` (required)
Your openai api key.
### `CODE` (optional)
Access password, separated by comma.
### `OPENAI_API_KEY` (required)
Your openai api key.
### `BASE_URL` (optional)
> Default: `https://api.openai.com`
@@ -173,6 +175,20 @@ Override openai api request base url.
Specify OpenAI organization ID.
### `AZURE_URL` (optional)
> Example: https://{azure-resource-url}/openai/deployments/{deploy-name}
Azure deploy url.
### `AZURE_API_KEY` (optional)
Azure Api Key.
### `AZURE_API_VERSION` (optional)
Azure Api Version, find it at [Azure Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions).
### `HIDE_USER_API_KEY` (optional)
> Default: Empty
@@ -197,6 +213,13 @@ If you do want users to query balance, set this value to 1, or you should set it
If you want to disable parse settings from url, set this to 1.
### `CUSTOM_MODELS` (optional)
> Default: Empty
> Example: `+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview:gpt-4-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list, and display `gpt-4-1106-preview` as `gpt-4-turbo`.
To control custom models, use `+` to add a custom model, use `-` to hide a model, use `name:displayName` to customize model name, separated by comma.
## Requirements
NodeJS >= 18, Docker >= 20
@@ -320,6 +343,7 @@ If you want to add a new translation, read this [document](./docs/translation.md
[@synwith](https://github.com/synwith)
[@piksonGit](https://github.com/piksonGit)
[@ouyangzhiping](https://github.com/ouyangzhiping)
[@wenjiavv](https://github.com/wenjiavv)
### Contributor

View File

@@ -90,6 +90,20 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
指定 OpenAI 中的组织 ID。
### `AZURE_URL` (可选)
> 形如https://{azure-resource-url}/openai/deployments/{deploy-name}
Azure 部署地址。
### `AZURE_API_KEY` (可选)
Azure 密钥。
### `AZURE_API_VERSION` (可选)
Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。
### `HIDE_USER_API_KEY` (可选)
如果你不想让用户自行填入 API Key将此环境变量设置为 1 即可。
@@ -106,6 +120,12 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
### `CUSTOM_MODELS` (可选)
> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview:gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名:展示名` 来自定义模型的展示名,用英文逗号隔开。
## 开发
点击下方按钮,开始二次开发:
@@ -118,7 +138,7 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
OPENAI_API_KEY=<your api key here>
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
BASE_URL=https://nb.nextweb.fun/api/proxy
BASE_URL=https://a.nextweb.fun/api/proxy
```
### 本地开发

View File

@@ -28,7 +28,7 @@ export function auth(req: NextRequest) {
const authToken = req.headers.get("Authorization") ?? "";
// check if it is openai api key or user token
const { accessCode, apiKey: token } = parseApiKey(authToken);
const { accessCode, apiKey } = parseApiKey(authToken);
const hashedCode = md5.hash(accessCode ?? "").trim();
@@ -39,19 +39,32 @@ export function auth(req: NextRequest) {
console.log("[User IP] ", getIP(req));
console.log("[Time] ", new Date().toLocaleString());
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
return {
error: true,
msg: !accessCode ? "empty access code" : "wrong access code",
};
}
if (serverConfig.hideUserApiKey && !!apiKey) {
return {
error: true,
msg: "you are not allowed to access openai with your own api key",
};
}
// if user does not provide an api key, inject system api key
if (!token) {
const apiKey = serverConfig.apiKey;
if (apiKey) {
if (!apiKey) {
const serverApiKey = serverConfig.isAzure
? serverConfig.azureApiKey
: serverConfig.apiKey;
if (serverApiKey) {
console.log("[Auth] use system api key");
req.headers.set("Authorization", `Bearer ${apiKey}`);
req.headers.set(
"Authorization",
`${serverConfig.isAzure ? "" : "Bearer "}${serverApiKey}`,
);
} else {
console.log("[Auth] admin did not provide an api key");
}

View File

@@ -1,48 +1,62 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../config/server";
import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant";
import { collectModelTable } from "../utils/model";
import { makeAzurePath } from "../azure";
export 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;
const DISABLE_GPT4 = !!process.env.DISABLE_GPT4;
const serverConfig = getServerSideConfig();
export async function requestOpenai(req: NextRequest) {
const controller = new AbortController();
const authValue = req.headers.get("Authorization") ?? "";
const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
const authHeaderName = serverConfig.isAzure ? "api-key" : "Authorization";
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
"/api/openai/",
"",
);
let baseUrl = BASE_URL;
let baseUrl =
serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `${PROTOCOL}://${baseUrl}`;
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith('/')) {
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", openaiPath);
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log("[Org ID]", serverConfig.openaiOrgId);
if (process.env.OPENAI_ORG_ID) {
console.log("[Org ID]", process.env.OPENAI_ORG_ID);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
if (serverConfig.isAzure) {
if (!serverConfig.azureApiVersion) {
return NextResponse.json({
error: true,
message: `missing AZURE_API_VERSION in server env vars`,
});
}
path = makeAzurePath(path, serverConfig.azureApiVersion);
}
const timeoutId = setTimeout(() => {
controller.abort();
}, 10 * 60 * 1000);
const fetchUrl = `${baseUrl}/${openaiPath}`;
const fetchUrl = `${baseUrl}/${path}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
Authorization: authValue,
...(process.env.OPENAI_ORG_ID && {
"OpenAI-Organization": process.env.OPENAI_ORG_ID,
[authHeaderName]: authValue,
...(serverConfig.openaiOrgId && {
"OpenAI-Organization": serverConfig.openaiOrgId,
}),
},
method: req.method,
@@ -55,18 +69,23 @@ export async function requestOpenai(req: NextRequest) {
};
// #1815 try to refuse gpt4 request
if (DISABLE_GPT4 && req.body) {
if (serverConfig.customModels && req.body) {
try {
const modelTable = collectModelTable(
DEFAULT_MODELS,
serverConfig.customModels,
);
const clonedBody = await req.text();
fetchOptions.body = clonedBody;
const jsonBody = JSON.parse(clonedBody);
const jsonBody = JSON.parse(clonedBody) as { model?: string };
if ((jsonBody?.model ?? "").includes("gpt-4")) {
// not undefined and is false
if (modelTable[jsonBody?.model ?? ""].available === false) {
return NextResponse.json(
{
error: true,
message: "you are not allowed to use gpt-4 model",
message: `you are not allowed to use ${jsonBody?.model} model`,
},
{
status: 403,

View File

@@ -12,6 +12,7 @@ const DANGER_CONFIG = {
disableGPT4: serverConfig.disableGPT4,
hideBalanceQuery: serverConfig.hideBalanceQuery,
disableFastLink: serverConfig.disableFastLink,
customModels: serverConfig.customModels,
};
declare global {

View File

@@ -75,3 +75,4 @@ export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = ['arn1', 'bom1', 'cdg1', 'cle1', 'cpt1', 'dub1', 'fra1', 'gru1', 'hnd1', 'iad1', 'icn1', 'kix1', 'lhr1', 'pdx1', 'sfo1', 'sin1', 'syd1'];

9
app/azure.ts Normal file
View File

@@ -0,0 +1,9 @@
export function makeAzurePath(path: string, apiVersion: string) {
// should omit /v1 prefix
path = path.replaceAll("v1/", "");
// should add api-key to query string
path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`;
return path;
}

View File

@@ -1,5 +1,5 @@
import { getClientConfig } from "../config/client";
import { ACCESS_CODE_PREFIX } from "../constant";
import { ACCESS_CODE_PREFIX, Azure, ServiceProvider } from "../constant";
import { ChatMessage, ModelType, useAccessStore } from "../store";
import { ChatGPTApi } from "./platforms/openai";
@@ -127,22 +127,26 @@ export const api = new ClientApi();
export function getHeaders() {
const accessStore = useAccessStore.getState();
let headers: Record<string, string> = {
const headers: Record<string, string> = {
"Content-Type": "application/json",
"x-requested-with": "XMLHttpRequest",
};
const makeBearer = (token: string) => `Bearer ${token.trim()}`;
const isAzure = accessStore.provider === ServiceProvider.Azure;
const authHeader = isAzure ? "api-key" : "Authorization";
const apiKey = isAzure ? accessStore.azureApiKey : accessStore.openaiApiKey;
const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`;
const validString = (x: string) => x && x.length > 0;
// use user's api key first
if (validString(accessStore.token)) {
headers.Authorization = makeBearer(accessStore.token);
if (validString(apiKey)) {
headers[authHeader] = makeBearer(apiKey);
} else if (
accessStore.enabledAccessControl() &&
validString(accessStore.accessCode)
) {
headers.Authorization = makeBearer(
headers[authHeader] = makeBearer(
ACCESS_CODE_PREFIX + accessStore.accessCode,
);
}

View File

@@ -1,8 +1,10 @@
import {
ApiPath,
DEFAULT_API_HOST,
DEFAULT_MODELS,
OpenaiPath,
REQUEST_TIMEOUT_MS,
ServiceProvider,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
@@ -14,6 +16,7 @@ import {
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { makeAzurePath } from "@/app/azure";
export interface OpenAIListModelResponse {
object: string;
@@ -28,20 +31,35 @@ export class ChatGPTApi implements LLMApi {
private disableListModels = true;
path(path: string): string {
let openaiUrl = useAccessStore.getState().openaiUrl;
const apiPath = "/api/openai";
const accessStore = useAccessStore.getState();
if (openaiUrl.length === 0) {
const isAzure = accessStore.provider === ServiceProvider.Azure;
if (isAzure && !accessStore.isValidAzure()) {
throw Error(
"incomplete azure config, please check it in your settings page",
);
}
let baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
openaiUrl = isApp ? DEFAULT_API_HOST : apiPath;
baseUrl = isApp ? DEFAULT_API_HOST : ApiPath.OpenAI;
}
if (openaiUrl.endsWith("/")) {
openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1);
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!openaiUrl.startsWith("http") && !openaiUrl.startsWith(apiPath)) {
openaiUrl = "https://" + openaiUrl;
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) {
baseUrl = "https://" + baseUrl;
}
return [openaiUrl, path].join("/");
if (isAzure) {
path = makeAzurePath(path, accessStore.azureApiVersion);
}
return [baseUrl, path].join("/");
}
extractMessage(res: any) {
@@ -70,6 +88,8 @@ export class ChatGPTApi implements LLMApi {
presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p,
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
};
console.log("[Request] openai payload: ", requestPayload);
@@ -154,14 +174,20 @@ export class ChatGPTApi implements LLMApi {
}
const text = msg.data;
try {
const json = JSON.parse(text);
const delta = json.choices[0].delta.content;
const json = JSON.parse(text) as {
choices: Array<{
delta: {
content: string;
};
}>;
};
const delta = json.choices[0]?.delta?.content;
if (delta) {
responseText += delta;
options.onUpdate?.(responseText, delta);
}
} catch (e) {
console.error("[Request] parse error", text, msg);
console.error("[Request] parse error", text);
}
},
onclose() {

View File

@@ -18,7 +18,7 @@ export function AuthPage() {
const goChat = () => navigate(Path.Chat);
const resetAccessCode = () => {
accessStore.update((access) => {
access.token = "";
access.openaiApiKey = "";
access.accessCode = "";
});
}; // Reset access code to empty string
@@ -56,11 +56,11 @@ export function AuthPage() {
<input
className={styles["auth-input"]}
type="password"
placeholder={Locale.Settings.Token.Placeholder}
value={accessStore.token}
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
value={accessStore.openaiApiKey}
onChange={(e) => {
accessStore.update(
(access) => (access.token = e.currentTarget.value),
(access) => (access.openaiApiKey = e.currentTarget.value),
);
}}
/>

View File

@@ -18,6 +18,7 @@ import { MaskAvatar } from "./mask";
import { Mask } from "../store/mask";
import { useRef, useEffect } from "react";
import { showConfirm } from "./ui-lib";
import { useMobileScreen } from "../utils";
export function ChatItem(props: {
onClick?: () => void;
@@ -60,7 +61,10 @@ export function ChatItem(props: {
{props.narrow ? (
<div className={styles["chat-item-narrow"]}>
<div className={styles["chat-item-avatar"] + " no-dark"}>
<MaskAvatar mask={props.mask} />
<MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
</div>
<div className={styles["chat-item-narrow-count"]}>
{props.count}
@@ -80,7 +84,11 @@ export function ChatItem(props: {
<div
className={styles["chat-item-delete"]}
onClickCapture={props.onDelete}
onClickCapture={(e) => {
props.onDelete?.();
e.preventDefault();
e.stopPropagation();
}}
>
<DeleteIcon />
</div>
@@ -101,6 +109,7 @@ export function ChatList(props: { narrow?: boolean }) {
);
const chatStore = useChatStore();
const navigate = useNavigate();
const isMobileScreen = useMobileScreen();
const onDragEnd: OnDragEndResponder = (result) => {
const { destination, source } = result;
@@ -142,7 +151,7 @@ export function ChatList(props: { narrow?: boolean }) {
}}
onDelete={async () => {
if (
!props.narrow ||
(!props.narrow && !isMobileScreen) ||
(await showConfirm(Locale.Home.DeleteChat))
) {
chatStore.deleteSession(i);

View File

@@ -88,6 +88,7 @@ import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
import { prettyObject } from "../utils/format";
import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client";
import { useAllModels } from "../utils/hooks";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
@@ -143,6 +144,7 @@ export function SessionConfigModel(props: { onClose: () => void }) {
extraListItems={
session.mask.modelConfig.sendMemory ? (
<ListItem
className="copyable"
title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
></ListItem>
@@ -429,16 +431,27 @@ export function ChatActions(props: {
// switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model;
const allModels = useAllModels();
const models = useMemo(
() =>
config
.allModels()
.filter((m) => m.available)
.map((m) => m.name),
[config],
() => allModels.filter((m) => m.available),
[allModels],
);
const [showModelSelector, setShowModelSelector] = useState(false);
useEffect(() => {
// if current model is not available
// switch to first available model
const isUnavaliableModel = !models.some((m) => m.name === currentModel);
if (isUnavaliableModel && models.length > 0) {
const nextModel = models[0].name as ModelType;
chatStore.updateCurrentSession(
(session) => (session.mask.modelConfig.model = nextModel),
);
showToast(nextModel);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentModel, models]);
return (
<div className={styles["chat-input-actions"]}>
{couldStop && (
@@ -518,8 +531,8 @@ export function ChatActions(props: {
<Selector
defaultSelectedValue={currentModel}
items={models.map((m) => ({
title: m,
value: m,
title: m.displayName,
value: m.name,
}))}
onClose={() => setShowModelSelector(false)}
onSelection={(s) => {
@@ -1001,7 +1014,9 @@ function _Chat() {
).then((res) => {
if (!res) return;
if (payload.key) {
accessStore.update((access) => (access.token = payload.key!));
accessStore.update(
(access) => (access.openaiApiKey = payload.key!),
);
}
if (payload.url) {
accessStore.update((access) => (access.openaiUrl = payload.url!));
@@ -1161,7 +1176,12 @@ function _Chat() {
{["system"].includes(message.role) ? (
<Avatar avatar="2699-fe0f" />
) : (
<MaskAvatar mask={session.mask} />
<MaskAvatar
avatar={session.mask.avatar}
model={
message.model || session.mask.modelConfig.model
}
/>
)}
</>
)}

View File

@@ -186,7 +186,8 @@
box-shadow: var(--card-shadow);
border: var(--border-in-light);
*:not(li) {
code,
pre {
overflow: hidden;
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable @next/next/no-img-element */
import { ChatMessage, useAppConfig, useChatStore } from "../store";
import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store";
import Locale from "../locales";
import styles from "./exporter.module.scss";
import {
@@ -27,7 +27,7 @@ import { Avatar } from "./emoji";
import dynamic from "next/dynamic";
import NextImage from "next/image";
import { toBlob, toJpeg, toPng } from "html-to-image";
import { toBlob, toPng } from "html-to-image";
import { DEFAULT_MASK_AVATAR } from "../store/mask";
import { api } from "../client/api";
import { prettyObject } from "../utils/format";
@@ -41,7 +41,22 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
export function ExportMessageModal(props: { onClose: () => void }) {
return (
<div className="modal-mask">
<Modal title={Locale.Export.Title} onClose={props.onClose}>
<Modal
title={Locale.Export.Title}
onClose={props.onClose}
footer={
<div
style={{
width: "100%",
textAlign: "center",
fontSize: 14,
opacity: 0.5,
}}
>
{Locale.Exporter.Description.Title}
</div>
}
>
<div style={{ minHeight: "40vh" }}>
<MessageExporter />
</div>
@@ -149,7 +164,7 @@ export function MessageExporter() {
if (exportConfig.includeContext) {
ret.push(...session.mask.context);
}
ret.push(...session.messages.filter((m, i) => selection.has(m.id)));
ret.push(...session.messages.filter((m) => selection.has(m.id)));
return ret;
}, [
exportConfig.includeContext,
@@ -260,7 +275,8 @@ export function RenderExport(props: {
});
props.onRender(renderMsgs);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div ref={domRef}>
@@ -604,8 +620,6 @@ export function MarkdownPreviewer(props: {
);
}
// modified by BackTrackZ now it's looks better
export function JsonPreviewer(props: {
messages: ChatMessage[];
topic: string;

View File

@@ -5,13 +5,13 @@ import RemarkBreaks from "remark-breaks";
import RehypeKatex from "rehype-katex";
import RemarkGfm from "remark-gfm";
import RehypeHighlight from "rehype-highlight";
import { useRef, useState, RefObject, useEffect } from "react";
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
import { copyToClipboard } from "../utils";
import mermaid from "mermaid";
import LoadingIcon from "../icons/three-dots.svg";
import React from "react";
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
import { useDebouncedCallback } from "use-debounce";
import { showImageModal } from "./ui-lib";
export function Mermaid(props: { code: string }) {
@@ -99,7 +99,29 @@ export function PreCode(props: { children: any }) {
);
}
function escapeDollarNumber(text: string) {
let escapedText = "";
for (let i = 0; i < text.length; i += 1) {
let char = text[i];
const nextChar = text[i + 1] || " ";
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
char = "\\$";
}
escapedText += char;
}
return escapedText;
}
function _MarkDownContent(props: { content: string }) {
const escapedContent = useMemo(
() => escapeDollarNumber(props.content),
[props.content],
);
return (
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
@@ -124,7 +146,7 @@ function _MarkDownContent(props: { content: string }) {
},
}}
>
{props.content}
{escapedContent}
</ReactMarkdown>
);
}

View File

@@ -18,6 +18,7 @@ import {
ChatMessage,
createMessage,
ModelConfig,
ModelType,
useAppConfig,
useChatStore,
} from "../store";
@@ -58,11 +59,11 @@ function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
return result;
}
export function MaskAvatar(props: { mask: Mask }) {
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
<Avatar avatar={props.mask.avatar} />
export function MaskAvatar(props: { avatar: string; model?: ModelType }) {
return props.avatar !== DEFAULT_MASK_AVATAR ? (
<Avatar avatar={props.avatar} />
) : (
<Avatar model={props.mask.modelConfig.model} />
<Avatar model={props.model} />
);
}
@@ -123,7 +124,10 @@ export function MaskConfig(props: {
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
>
<MaskAvatar mask={props.mask} />
<MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
</div>
</Popover>
</ListItem>
@@ -398,7 +402,7 @@ export function MaskPage() {
setSearchText(text);
if (text.length > 0) {
const result = allMasks.filter((m) =>
m.name.toLowerCase().includes(text.toLowerCase())
m.name.toLowerCase().includes(text.toLowerCase()),
);
setSearchMasks(result);
} else {
@@ -523,7 +527,7 @@ export function MaskPage() {
<div className={styles["mask-item"]} key={m.id}>
<div className={styles["mask-header"]}>
<div className={styles["mask-icon"]}>
<MaskAvatar mask={m} />
<MaskAvatar avatar={m.avatar} model={m.modelConfig.model} />
</div>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{m.name}</div>

View File

@@ -58,8 +58,8 @@
}
.body {
flex-grow: 1;
max-width: calc(100% - 40px);
flex: 1;
max-width: calc(100% - 80px);
.date {
font-size: 12px;
@@ -71,6 +71,12 @@
font-size: 12px;
}
}
.checkbox {
display: flex;
justify-content: flex-end;
flex: 1;
}
}
}
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { ChatMessage, useAppConfig, useChatStore } from "../store";
import { Updater } from "../typing";
import { IconButton } from "./button";
@@ -73,11 +73,23 @@ export function MessageSelector(props: {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
const messages = session.messages.filter(
(m, i) =>
m.id && // message must have id
isValid(m) &&
(i >= session.messages.length - 1 || isValid(session.messages[i + 1])),
const allMessages = useMemo(() => {
let startIndex = Math.max(0, session.clearContextIndex ?? 0);
if (startIndex === session.messages.length - 1) {
startIndex = 0;
}
return session.messages.slice(startIndex);
}, [session.messages, session.clearContextIndex]);
const messages = useMemo(
() =>
allMessages.filter(
(m, i) =>
m.id && // message must have id
isValid(m) &&
(i >= allMessages.length - 1 || isValid(allMessages[i + 1])),
),
[allMessages],
);
const messageCount = messages.length;
const config = useAppConfig();
@@ -176,6 +188,8 @@ export function MessageSelector(props: {
<div className={styles["messages"]}>
{messages.map((m, i) => {
if (!isInSearchResult(m.id!)) return null;
const id = m.id ?? i;
const isSelected = props.selection.has(id);
return (
<div
@@ -185,7 +199,6 @@ export function MessageSelector(props: {
key={i}
onClick={() => {
props.updateSelection((selection) => {
const id = m.id ?? i;
selection.has(id) ? selection.delete(id) : selection.add(id);
});
onClickIndex(i);
@@ -195,7 +208,10 @@ export function MessageSelector(props: {
{m.role === "user" ? (
<Avatar avatar={config.avatar}></Avatar>
) : (
<MaskAvatar mask={session.mask} />
<MaskAvatar
avatar={session.mask.avatar}
model={m.model || session.mask.modelConfig.model}
/>
)}
</div>
<div className={styles["body"]}>
@@ -206,6 +222,10 @@ export function MessageSelector(props: {
{m.content}
</div>
</div>
<div className={styles["checkbox"]}>
<input type="checkbox" checked={isSelected}></input>
</div>
</div>
);
})}

View File

@@ -1,14 +1,15 @@
import { ModalConfigValidator, ModelConfig, useAppConfig } from "../store";
import { ModalConfigValidator, ModelConfig } from "../store";
import Locale from "../locales";
import { InputRange } from "./input-range";
import { ListItem, Select } from "./ui-lib";
import { useAllModels } from "../utils/hooks";
export function ModelConfigList(props: {
modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void;
}) {
const config = useAppConfig();
const allModels = useAllModels();
return (
<>
@@ -24,11 +25,13 @@ export function ModelConfigList(props: {
);
}}
>
{config.allModels().map((v, i) => (
<option value={v.name} key={i} disabled={!v.available}>
{v.name}
</option>
))}
{allModels
.filter((v) => v.available)
.map((v, i) => (
<option value={v.name} key={i}>
{v.displayName}
</option>
))}
</Select>
</ListItem>
<ListItem
@@ -75,8 +78,8 @@ export function ModelConfigList(props: {
>
<input
type="number"
min={100}
max={100000}
min={1024}
max={512000}
value={props.modelConfig.max_tokens}
onChange={(e) =>
props.updateConfig(

View File

@@ -17,21 +17,13 @@ import { useCommand } from "../command";
import { showConfirm } from "./ui-lib";
import { BUILTIN_MASK_STORE } from "../masks";
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
const xmin = Math.max(aRect.x, bRect.x);
const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
const ymin = Math.max(aRect.y, bRect.y);
const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
const width = xmax - xmin;
const height = ymax - ymin;
const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
return intersectionArea;
}
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
return (
<div className={styles["mask"]} onClick={props.onClick}>
<MaskAvatar mask={props.mask} />
<MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
</div>
);

View File

@@ -51,10 +51,13 @@ import Locale, {
import { copyToClipboard } from "../utils";
import Link from "next/link";
import {
Azure,
OPENAI_BASE_URL,
Path,
RELEASE_URL,
STORAGE_KEY,
ServiceProvider,
SlotID,
UPDATE_URL,
} from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
@@ -580,8 +583,16 @@ export function Settings() {
const accessStore = useAccessStore();
const shouldHideBalanceQuery = useMemo(() => {
const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
return accessStore.hideBalanceQuery || isOpenAiUrl;
}, [accessStore.hideBalanceQuery, accessStore.openaiUrl]);
return (
accessStore.hideBalanceQuery ||
isOpenAiUrl ||
accessStore.provider === ServiceProvider.Azure
);
}, [
accessStore.hideBalanceQuery,
accessStore.openaiUrl,
accessStore.provider,
]);
const usage = {
used: updateStore.used,
@@ -877,16 +888,16 @@ export function Settings() {
</ListItem>
</List>
<List>
{showAccessCode ? (
<List id={SlotID.CustomModel}>
{showAccessCode && (
<ListItem
title={Locale.Settings.AccessCode.Title}
subTitle={Locale.Settings.AccessCode.SubTitle}
title={Locale.Settings.Access.AccessCode.Title}
subTitle={Locale.Settings.Access.AccessCode.SubTitle}
>
<PasswordInput
value={accessStore.accessCode}
type="text"
placeholder={Locale.Settings.AccessCode.Placeholder}
placeholder={Locale.Settings.Access.AccessCode.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.accessCode = e.currentTarget.value),
@@ -894,46 +905,154 @@ export function Settings() {
}}
/>
</ListItem>
) : (
<></>
)}
{!accessStore.hideUserApiKey ? (
{!accessStore.hideUserApiKey && (
<>
<ListItem
title={Locale.Settings.Endpoint.Title}
subTitle={Locale.Settings.Endpoint.SubTitle}
title={Locale.Settings.Access.CustomEndpoint.Title}
subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
>
<input
type="text"
value={accessStore.openaiUrl}
placeholder="https://api.openai.com/"
type="checkbox"
checked={accessStore.useCustomConfig}
onChange={(e) =>
accessStore.update(
(access) => (access.openaiUrl = e.currentTarget.value),
(access) =>
(access.useCustomConfig = e.currentTarget.checked),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Token.Title}
subTitle={Locale.Settings.Token.SubTitle}
>
<PasswordInput
value={accessStore.token}
type="text"
placeholder={Locale.Settings.Token.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.token = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
) : null}
{accessStore.useCustomConfig && (
<>
<ListItem
title={Locale.Settings.Access.Provider.Title}
subTitle={Locale.Settings.Access.Provider.SubTitle}
>
<Select
value={accessStore.provider}
onChange={(e) => {
accessStore.update(
(access) =>
(access.provider = e.target
.value as ServiceProvider),
);
}}
>
{Object.entries(ServiceProvider).map(([k, v]) => (
<option value={v} key={k}>
{k}
</option>
))}
</Select>
</ListItem>
{!shouldHideBalanceQuery ? (
{accessStore.provider === "OpenAI" ? (
<>
<ListItem
title={Locale.Settings.Access.OpenAI.Endpoint.Title}
subTitle={
Locale.Settings.Access.OpenAI.Endpoint.SubTitle
}
>
<input
type="text"
value={accessStore.openaiUrl}
placeholder={OPENAI_BASE_URL}
onChange={(e) =>
accessStore.update(
(access) =>
(access.openaiUrl = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.OpenAI.ApiKey.Title}
subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
>
<PasswordInput
value={accessStore.openaiApiKey}
type="text"
placeholder={
Locale.Settings.Access.OpenAI.ApiKey.Placeholder
}
onChange={(e) => {
accessStore.update(
(access) =>
(access.openaiApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
) : (
<>
<ListItem
title={Locale.Settings.Access.Azure.Endpoint.Title}
subTitle={
Locale.Settings.Access.Azure.Endpoint.SubTitle +
Azure.ExampleEndpoint
}
>
<input
type="text"
value={accessStore.azureUrl}
placeholder={Azure.ExampleEndpoint}
onChange={(e) =>
accessStore.update(
(access) =>
(access.azureUrl = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.Azure.ApiKey.Title}
subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
>
<PasswordInput
value={accessStore.azureApiKey}
type="text"
placeholder={
Locale.Settings.Access.Azure.ApiKey.Placeholder
}
onChange={(e) => {
accessStore.update(
(access) =>
(access.azureApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.Access.Azure.ApiVerion.Title}
subTitle={
Locale.Settings.Access.Azure.ApiVerion.SubTitle
}
>
<input
type="text"
value={accessStore.azureApiVersion}
placeholder="2023-08-01-preview"
onChange={(e) =>
accessStore.update(
(access) =>
(access.azureApiVersion =
e.currentTarget.value),
)
}
></input>
</ListItem>
</>
)}
</>
)}
</>
)}
{!shouldHideBalanceQuery && !clientConfig?.isApp ? (
<ListItem
title={Locale.Settings.Usage.Title}
subTitle={
@@ -960,8 +1079,8 @@ export function Settings() {
) : null}
<ListItem
title={Locale.Settings.CustomModel.Title}
subTitle={Locale.Settings.CustomModel.SubTitle}
title={Locale.Settings.Access.CustomModel.Title}
subTitle={Locale.Settings.Access.CustomModel.SubTitle}
>
<input
type="text"

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback, useMemo } from "react";
import { useEffect, useRef, useMemo } from "react";
import styles from "./home.module.scss";
@@ -8,6 +8,7 @@ import GithubIcon from "../icons/github.svg";
import ChatGptIcon from "../icons/chatgpt.svg";
import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg";
import MaskIcon from "../icons/mask.svg";
import PluginIcon from "../icons/plugin.svg";
import DragIcon from "../icons/drag.svg";
@@ -202,7 +203,7 @@ export function SideBar(props: { className?: string }) {
<div className={styles["sidebar-actions"]}>
<div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton
icon={<CloseIcon />}
icon={<DeleteIcon />}
onClick={async () => {
if (await showConfirm(Locale.Home.DeleteChat)) {
chatStore.deleteSession(chatStore.currentSessionIndex);

View File

@@ -235,7 +235,7 @@
.select-with-icon-select {
height: 100%;
border: var(--border-in-light);
padding: 10px 25px 10px 10px;
padding: 10px 35px 10px 10px;
border-radius: 10px;
appearance: none;
cursor: pointer;

View File

@@ -70,14 +70,12 @@ export function ListItem(props: {
);
}
export function List(props: {
children:
| Array<JSX.Element | null | undefined>
| JSX.Element
| null
| undefined;
}) {
return <div className={styles.list}>{props.children}</div>;
export function List(props: { children: React.ReactNode; id?: string }) {
return (
<div className={styles.list} id={props.id}>
{props.children}
</div>
);
}
export function Loading() {
@@ -99,8 +97,9 @@ export function Loading() {
interface ModalProps {
title: string;
children?: any;
actions?: JSX.Element[];
actions?: React.ReactNode[];
defaultMax?: boolean;
footer?: React.ReactNode;
onClose?: () => void;
}
export function Modal(props: ModalProps) {
@@ -149,6 +148,7 @@ export function Modal(props: ModalProps) {
<div className={styles["modal-content"]}>{props.children}</div>
<div className={styles["modal-footer"]}>
{props.footer}
<div className={styles["modal-actions"]}>
{props.actions?.map((action, i) => (
<div key={i} className={styles["modal-action"]}>

View File

@@ -1,19 +1,31 @@
import md5 from "spark-md5";
import { DEFAULT_MODELS } from "../constant";
declare global {
namespace NodeJS {
interface ProcessEnv {
PROXY_URL?: string; // docker only
OPENAI_API_KEY?: string;
CODE?: string;
BASE_URL?: string;
PROXY_URL?: string;
OPENAI_ORG_ID?: string; // openai only
VERCEL?: string;
HIDE_USER_API_KEY?: string; // disable user's api key input
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
BUILD_MODE?: "standalone" | "export";
BUILD_APP?: string; // is building desktop app
HIDE_USER_API_KEY?: string; // disable user's api key input
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
CUSTOM_MODELS?: string; // to control custom models
// azure only
AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
AZURE_API_KEY?: string;
AZURE_API_VERSION?: string;
}
}
}
@@ -38,17 +50,39 @@ export const getServerSideConfig = () => {
);
}
const disableGPT4 = !!process.env.DISABLE_GPT4;
let customModels = process.env.CUSTOM_MODELS ?? "";
if (disableGPT4) {
if (customModels) customModels += ",";
customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4"))
.map((m) => "-" + m.name)
.join(",");
}
const isAzure = !!process.env.AZURE_URL;
return {
baseUrl: process.env.BASE_URL,
apiKey: process.env.OPENAI_API_KEY,
openaiOrgId: process.env.OPENAI_ORG_ID,
isAzure,
azureUrl: process.env.AZURE_URL,
azureApiKey: process.env.AZURE_API_KEY,
azureApiVersion: process.env.AZURE_API_VERSION,
needCode: ACCESS_CODES.size > 0,
code: process.env.CODE,
codes: ACCESS_CODES,
needCode: ACCESS_CODES.size > 0,
baseUrl: process.env.BASE_URL,
proxyUrl: process.env.PROXY_URL,
isVercel: !!process.env.VERCEL,
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
disableGPT4: !!process.env.DISABLE_GPT4,
disableGPT4,
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
disableFastLink: !!process.env.DISABLE_FAST_LINK,
customModels,
};
};

View File

@@ -8,7 +8,7 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
export const DEFAULT_CORS_HOST = "https://ab.nextweb.fun";
export const DEFAULT_CORS_HOST = "https://a.nextweb.fun";
export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
export const OPENAI_BASE_URL = "https://api.openai.com";
@@ -23,10 +23,12 @@ export enum Path {
export enum ApiPath {
Cors = "/api/cors",
OpenAI = "/api/openai",
}
export enum SlotID {
AppBody = "app-body",
CustomModel = "custom-model",
}
export enum FileName {
@@ -60,6 +62,11 @@ export const REQUEST_TIMEOUT_MS = 60000;
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
export enum ServiceProvider {
OpenAI = "OpenAI",
Azure = "Azure",
}
export const OpenaiPath = {
ChatPath: "v1/chat/completions",
UsagePath: "dashboard/billing/usage",
@@ -67,19 +74,24 @@ export const OpenaiPath = {
ListModelPath: "v1/models",
};
export const Azure = {
ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}",
};
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
export const DEFAULT_SYSTEM_TEMPLATE = `
You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: {{cutoff}}
Current model: {{model}}
Current time: {{time}}
Latex inline: $x^2$
Latex block: $$e=mc^2$$
`;
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
export const KnowledgeCutOffDate: Record<string, string> = {
default: "2021-09",
"gpt-3.5-turbo-1106": "2023-04",
"gpt-4-1106-preview": "2023-04",
"gpt-4-vision-preview": "2023-04",
};

View File

@@ -167,11 +167,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
Title: "حد الضغط للتاريخ",
SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد",
},
Token: {
Title: "مفتاح API",
SubTitle: "استخدم مفتاحك لتجاوز حد رمز الوصول",
Placeholder: "مفتاح OpenAI API",
},
Usage: {
Title: "رصيد الحساب",
SubTitle(used: any, total: any) {
@@ -181,15 +177,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
Check: "التحقق",
NoAccess: "أدخل مفتاح API للتحقق من الرصيد",
},
AccessCode: {
Title: "رمز الوصول",
SubTitle: "تم تمكين التحكم في الوصول",
Placeholder: "رمز الوصول المطلوب",
},
Endpoint: {
Title: "نقطة النهاية",
SubTitle: "يجب أن تبدأ نقطة النهاية المخصصة بـ http(s)://",
},
Model: "النموذج",
Temperature: {
Title: "الحرارة",

View File

@@ -199,11 +199,7 @@ const bn: PartialLocaleType = {
SubTitle:
"নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে",
},
Token: {
Title: "অ্যাপি কী",
SubTitle: "অ্যাক্সেস কোড সীমা উপেক্ষা করতে আপনার কীটি ব্যবহার করুন",
Placeholder: "OpenAI API কী",
},
Usage: {
Title: "একাউন্ট ব্যালেন্স",
SubTitle(used: any, total: any) {
@@ -213,15 +209,7 @@ const bn: PartialLocaleType = {
Check: "চেক",
NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন",
},
AccessCode: {
Title: "অ্যাক্সেস কোড",
SubTitle: "অ্যাক্সেস নিয়ন্ত্রণ সক্রিয়",
Placeholder: "অ্যাক্সেস কোড প্রয়োজন",
},
Endpoint: {
Title: "ইনটারপয়েন্ট",
SubTitle: "কাস্টম এন্ডপয়েন্টটি হতে হবে http(s):// দিয়ে শুরু হতে হবে",
},
Model: "মডেল",
Temperature: {
Title: "তাপমাত্রা",

View File

@@ -85,8 +85,8 @@ const cn = {
Copy: "全部复制",
Download: "下载文件",
Share: "分享到 ShareGPT",
MessageFromYou: "来自你的消息",
MessageFromChatGPT: "来自 ChatGPT 的消息",
MessageFromYou: "用户",
MessageFromChatGPT: "ChatGPT",
Format: {
Title: "导出格式",
SubTitle: "可以导出 Markdown 文本或者 PNG 图片",
@@ -258,11 +258,6 @@ const cn = {
Title: "历史消息长度压缩阈值",
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
},
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可绕过密码访问限制",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "余额查询",
@@ -273,19 +268,56 @@ const cn = {
Check: "重新检查",
NoAccess: "输入 API Key 或访问密码查看余额",
},
AccessCode: {
Title: "访问密码",
SubTitle: "管理员已开启加密访问",
Placeholder: "请输入访问密码",
},
Endpoint: {
Title: "接口地址",
SubTitle: "除默认地址外,必须包含 http(s)://",
},
CustomModel: {
Title: "自定义模型名",
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
Access: {
AccessCode: {
Title: "访问密码",
SubTitle: "管理员已开启加密访问",
Placeholder: "请输入访问密码",
},
CustomEndpoint: {
Title: "自定义接口",
SubTitle: "是否使用自定义 Azure 或 OpenAI 服务",
},
Provider: {
Title: "模型服务商",
SubTitle: "切换不同的服务商",
},
OpenAI: {
ApiKey: {
Title: "API Key",
SubTitle: "使用自定义 OpenAI Key 绕过密码访问限制",
Placeholder: "OpenAI API Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "除默认地址外,必须包含 http(s)://",
},
},
Azure: {
ApiKey: {
Title: "接口密钥",
SubTitle: "使用自定义 Azure Key 绕过密码访问限制",
Placeholder: "Azure API Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "样例:",
},
ApiVerion: {
Title: "接口版本 (azure api version)",
SubTitle: "选择指定的部分版本",
},
},
CustomModel: {
Title: "自定义模型名",
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
},
},
Model: "模型 (model)",
Temperature: {
Title: "随机性 (temperature)",
@@ -409,6 +441,9 @@ const cn = {
Config: "配置",
},
Exporter: {
Description : {
Title: "只有清除上下文之后的消息会被展示"
},
Model: "模型",
Messages: "消息",
Topic: "主题",

View File

@@ -124,11 +124,7 @@ const cs: PartialLocaleType = {
SubTitle:
"Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu",
},
Token: {
Title: "API klíč",
SubTitle: "Použitím klíče ignorujete omezení přístupového kódu",
Placeholder: "Klíč API OpenAI",
},
Usage: {
Title: "Stav účtu",
SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const cs: PartialLocaleType = {
Check: "Zkontrolovat",
NoAccess: "Pro kontrolu zůstatku zadejte klíč API",
},
AccessCode: {
Title: "Přístupový kód",
SubTitle: "Kontrola přístupu povolena",
Placeholder: "Potřebujete přístupový kód",
},
Model: "Model",
Temperature: {
Title: "Teplota",

View File

@@ -124,12 +124,7 @@ const de: PartialLocaleType = {
SubTitle:
"Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet",
},
Token: {
Title: "API-Schlüssel",
SubTitle:
"Verwenden Sie Ihren Schlüssel, um das Zugangscode-Limit zu ignorieren",
Placeholder: "OpenAI API-Schlüssel",
},
Usage: {
Title: "Kontostand",
SubTitle(used: any, total: any) {
@@ -139,11 +134,6 @@ const de: PartialLocaleType = {
Check: "Erneut prüfen",
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
},
AccessCode: {
Title: "Zugangscode",
SubTitle: "Zugangskontrolle aktiviert",
Placeholder: "Zugangscode erforderlich",
},
Model: "Modell",
Temperature: {
Title: "Temperature", //Temperatur

View File

@@ -262,11 +262,7 @@ 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",
},
Usage: {
Title: "Account Balance",
SubTitle(used: any, total: any) {
@@ -276,19 +272,55 @@ const en: LocaleType = {
Check: "Check",
NoAccess: "Enter API Key to check balance",
},
AccessCode: {
Title: "Access Code",
SubTitle: "Access control enabled",
Placeholder: "Need Access Code",
},
Endpoint: {
Title: "Endpoint",
SubTitle: "Custom endpoint must start with http(s)://",
},
CustomModel: {
Title: "Custom Models",
SubTitle: "Add extra model options, separate by comma",
Access: {
AccessCode: {
Title: "Access Code",
SubTitle: "Access control Enabled",
Placeholder: "Enter Code",
},
CustomEndpoint: {
Title: "Custom Endpoint",
SubTitle: "Use custom Azure or OpenAI service",
},
Provider: {
Title: "Model Provider",
SubTitle: "Select Azure or OpenAI",
},
OpenAI: {
ApiKey: {
Title: "OpenAI API Key",
SubTitle: "User custom OpenAI Api Key",
Placeholder: "sk-xxx",
},
Endpoint: {
Title: "OpenAI Endpoint",
SubTitle: "Must starts with http(s):// or use /api/openai as default",
},
},
Azure: {
ApiKey: {
Title: "Azure Api Key",
SubTitle: "Check your api key from Azure console",
Placeholder: "Azure Api Key",
},
Endpoint: {
Title: "Azure Endpoint",
SubTitle: "Example: ",
},
ApiVerion: {
Title: "Azure Api Version",
SubTitle: "Check your api version from azure console",
},
},
CustomModel: {
Title: "Custom Models",
SubTitle: "Custom model options, seperated by comma",
},
},
Model: "Model",
Temperature: {
Title: "Temperature",
@@ -410,6 +442,9 @@ const en: LocaleType = {
Config: "Config",
},
Exporter: {
Description: {
Title: "Only messages after clearing the context will be displayed"
},
Model: "Model",
Messages: "Messages",
Topic: "Topic",

View File

@@ -124,11 +124,7 @@ const es: PartialLocaleType = {
SubTitle:
"Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor",
},
Token: {
Title: "Clave de API",
SubTitle: "Utiliza tu clave para ignorar el límite de código de acceso",
Placeholder: "Clave de la API de OpenAI",
},
Usage: {
Title: "Saldo de la cuenta",
SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const es: PartialLocaleType = {
Check: "Comprobar de nuevo",
NoAccess: "Introduzca la clave API para comprobar el saldo",
},
AccessCode: {
Title: "Código de acceso",
SubTitle: "Control de acceso habilitado",
Placeholder: "Necesita código de acceso",
},
Model: "Modelo",
Temperature: {
Title: "Temperatura",

View File

@@ -173,11 +173,7 @@ const fr: PartialLocaleType = {
SubTitle:
"Comprimera si la longueur des messages non compressés dépasse cette valeur",
},
Token: {
Title: "Clé API",
SubTitle: "Utilisez votre clé pour ignorer la limite du code d'accès",
Placeholder: "Clé OpenAI API",
},
Usage: {
Title: "Solde du compte",
SubTitle(used: any, total: any) {
@@ -187,11 +183,7 @@ const fr: PartialLocaleType = {
Check: "Vérifier",
NoAccess: "Entrez la clé API pour vérifier le solde",
},
AccessCode: {
Title: "Code d'accès",
SubTitle: "Contrôle d'accès activé",
Placeholder: "Code d'accès requis",
},
Model: "Modèle",
Temperature: {
Title: "Température",

View File

@@ -4,7 +4,8 @@ import { PartialLocaleType } from "./index";
const id: PartialLocaleType = {
WIP: "Coming Soon...",
Error: {
Unauthorized: "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).",
Unauthorized:
"Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).",
},
Auth: {
Title: "Diperlukan Kode Akses",
@@ -237,11 +238,7 @@ const id: PartialLocaleType = {
SubTitle:
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi",
},
Token: {
Title: "Kunci API",
SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses",
Placeholder: "Kunci API OpenAI",
},
Usage: {
Title: "Saldo Akun",
SubTitle(used: any, total: any) {
@@ -251,15 +248,7 @@ const id: PartialLocaleType = {
Check: "Periksa",
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
},
AccessCode: {
Title: "Kode Akses",
SubTitle: "Kontrol akses diaktifkan",
Placeholder: "Diperlukan kode akses",
},
Endpoint: {
Title: "Endpoint",
SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom",
},
Model: "Model",
Temperature: {
Title: "Suhu",
@@ -379,6 +368,9 @@ const id: PartialLocaleType = {
Edit: "Edit",
},
Exporter: {
Description: {
Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan"
},
Model: "Model",
Messages: "Pesan",
Topic: "Topik",

View File

@@ -124,12 +124,7 @@ const it: PartialLocaleType = {
SubTitle:
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
},
Token: {
Title: "API Key",
SubTitle:
"Utilizzare la chiave per ignorare il limite del codice di accesso",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "Bilancio Account",
SubTitle(used: any, total: any) {
@@ -139,11 +134,7 @@ const it: PartialLocaleType = {
Check: "Controlla ancora",
NoAccess: "Inserire la chiave API per controllare il saldo",
},
AccessCode: {
Title: "Codice d'accesso",
SubTitle: "Controllo d'accesso abilitato",
Placeholder: "Inserisci il codice d'accesso",
},
Model: "Modello GPT",
Temperature: {
Title: "Temperature",

View File

@@ -20,7 +20,8 @@ const jp: PartialLocaleType = {
Stop: "停止",
Retry: "リトライ",
Pin: "ピン",
PinToastContent: "コンテキストプロンプトに1つのメッセージをピン留めしました",
PinToastContent:
"コンテキストプロンプトに1つのメッセージをピン留めしました",
PinToastAction: "表示",
Delete: "削除",
Edit: "編集",
@@ -146,11 +147,7 @@ const jp: PartialLocaleType = {
SubTitle:
"圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。",
},
Token: {
Title: "APIキー",
SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する",
Placeholder: "OpenAI APIキー",
},
Usage: {
Title: "残高照会",
SubTitle(used: any, total: any) {
@@ -160,11 +157,7 @@ const jp: PartialLocaleType = {
Check: "再確認",
NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示",
},
AccessCode: {
Title: "アクセスパスワード",
SubTitle: "暗号化アクセスが有効になっています",
Placeholder: "アクセスパスワードを入力してください",
},
Model: "モデル (model)",
Temperature: {
Title: "ランダム性 (temperature)",

View File

@@ -124,11 +124,7 @@ const ko: PartialLocaleType = {
Title: "기록 압축 임계값",
SubTitle: "미압축 메시지 길이가 임계값을 초과하면 압축됨",
},
Token: {
Title: "API 키",
SubTitle: "액세스 코드 제한을 무시하기 위해 키 사용",
Placeholder: "OpenAI API 키",
},
Usage: {
Title: "계정 잔액",
SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const ko: PartialLocaleType = {
Check: "확인",
NoAccess: "잔액 확인을 위해 API 키를 입력하세요.",
},
AccessCode: {
Title: "액세스 코드",
SubTitle: "액세스 제어가 활성화됨",
Placeholder: "액세스 코드 입력",
},
Model: "모델",
Temperature: {
Title: "온도 (temperature)",

View File

@@ -106,12 +106,7 @@ const no: PartialLocaleType = {
SubTitle:
"Komprimer dersom ikke-komprimert lengde på meldinger overskrider denne verdien",
},
Token: {
Title: "API Key",
SubTitle:
"Bruk din egen API-nøkkel for å ignorere tilgangskoden begrensning",
Placeholder: "OpenAI API-nøkkel",
},
Usage: {
Title: "Saldo for konto",
SubTitle(used: any, total: any) {
@@ -121,11 +116,7 @@ const no: PartialLocaleType = {
Check: "Sjekk",
NoAccess: "Skriv inn API-nøkkelen for å sjekke saldo",
},
AccessCode: {
Title: "Tilgangskode",
SubTitle: "Tilgangskontroll på",
Placeholder: "Trenger tilgangskode",
},
Model: "Model",
Temperature: {
Title: "Temperatur",

View File

@@ -125,11 +125,7 @@ const ru: PartialLocaleType = {
SubTitle:
"Будет сжимать, если длина несжатых сообщений превышает указанное значение",
},
Token: {
Title: "API ключ",
SubTitle: "Используйте свой ключ, чтобы игнорировать лимит доступа",
Placeholder: "API ключ OpenAI",
},
Usage: {
Title: "Баланс аккаунта",
SubTitle(used: any, total: any) {
@@ -139,11 +135,7 @@ const ru: PartialLocaleType = {
Check: "Проверить",
NoAccess: "Введите API ключ, чтобы проверить баланс",
},
AccessCode: {
Title: "Код доступа",
SubTitle: "Контроль доступа включен",
Placeholder: "Требуется код доступа",
},
Model: "Модель",
Temperature: {
Title: "Температура",

View File

@@ -124,11 +124,7 @@ const tr: PartialLocaleType = {
SubTitle:
"Sıkıştırılmamış mesajların uzunluğu bu değeri aşarsa sıkıştırılır",
},
Token: {
Title: "API Anahtarı",
SubTitle: "Erişim kodu sınırını yoksaymak için anahtarınızı kullanın",
Placeholder: "OpenAI API Anahtarı",
},
Usage: {
Title: "Hesap Bakiyesi",
SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const tr: PartialLocaleType = {
Check: "Tekrar Kontrol Et",
NoAccess: "Bakiyeyi kontrol etmek için API anahtarını girin",
},
AccessCode: {
Title: "Erişim Kodu",
SubTitle: "Erişim kontrolü etkinleştirme",
Placeholder: "Erişim Kodu Gerekiyor",
},
Model: "Model",
Temperature: {
Title: "Gerçeklik",

View File

@@ -120,11 +120,7 @@ const tw: PartialLocaleType = {
Title: "歷史訊息長度壓縮閾值",
SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮",
},
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可規避授權存取限制",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "帳戶餘額",
SubTitle(used: any, total: any) {
@@ -134,11 +130,7 @@ const tw: PartialLocaleType = {
Check: "重新檢查",
NoAccess: "輸入 API Key 檢視餘額",
},
AccessCode: {
Title: "授權碼",
SubTitle: "目前是未授權存取狀態",
Placeholder: "請輸入授權碼",
},
Model: "模型 (model)",
Temperature: {
Title: "隨機性 (temperature)",

View File

@@ -123,11 +123,7 @@ const vi: PartialLocaleType = {
Title: "Ngưỡng nén lịch sử tin nhắn",
SubTitle: "Thực hiện nén nếu số lượng tin nhắn chưa nén vượt quá ngưỡng",
},
Token: {
Title: "API Key",
SubTitle: "Sử dụng khóa của bạn để bỏ qua giới hạn mã truy cập",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "Hạn mức tài khoản",
SubTitle(used: any, total: any) {
@@ -137,11 +133,7 @@ const vi: PartialLocaleType = {
Check: "Kiểm tra",
NoAccess: "Nhập API Key để kiểm tra hạn mức",
},
AccessCode: {
Title: "Mã truy cập",
SubTitle: "Đã bật kiểm soát truy cập",
Placeholder: "Nhập mã truy cập",
},
Model: "Mô hình",
Temperature: {
Title: "Tính ngẫu nhiên (temperature)",

View File

@@ -1,24 +1,41 @@
import { DEFAULT_API_HOST, DEFAULT_MODELS, StoreKey } from "../constant";
import {
ApiPath,
DEFAULT_API_HOST,
ServiceProvider,
StoreKey,
} from "../constant";
import { getHeaders } from "../client/api";
import { getClientConfig } from "../config/client";
import { createPersistStore } from "../utils/store";
import { ensure } from "../utils/clone";
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
const DEFAULT_OPENAI_URL =
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/";
console.log("[API] default openai url", DEFAULT_OPENAI_URL);
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : ApiPath.OpenAI;
const DEFAULT_ACCESS_STATE = {
token: "",
accessCode: "",
useCustomConfig: false,
provider: ServiceProvider.OpenAI,
// openai
openaiUrl: DEFAULT_OPENAI_URL,
openaiApiKey: "",
// azure
azureUrl: "",
azureApiKey: "",
azureApiVersion: "2023-08-01-preview",
// server config
needCode: true,
hideUserApiKey: false,
hideBalanceQuery: false,
disableGPT4: false,
disableFastLink: false,
openaiUrl: DEFAULT_OPENAI_URL,
customModels: "",
};
export const useAccessStore = createPersistStore(
@@ -30,12 +47,24 @@ export const useAccessStore = createPersistStore(
return get().needCode;
},
isValidOpenAI() {
return ensure(get(), ["openaiApiKey"]);
},
isValidAzure() {
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
},
isAuthorized() {
this.fetch();
// has token or has code or disabled access control
return (
!!get().token || !!get().accessCode || !this.enabledAccessControl()
this.isValidOpenAI() ||
this.isValidAzure() ||
!this.enabledAccessControl() ||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
);
},
fetch() {
@@ -52,12 +81,6 @@ export const useAccessStore = createPersistStore(
.then((res: DangerConfig) => {
console.log("[Config] got config from server", res);
set(() => ({ ...res }));
if (res.disableGPT4) {
DEFAULT_MODELS.forEach(
(m: any) => (m.available = !m.name.startsWith("gpt-4")),
);
}
})
.catch(() => {
console.error("[Config] failed to fetch config");
@@ -69,6 +92,19 @@ export const useAccessStore = createPersistStore(
}),
{
name: StoreKey.Access,
version: 1,
version: 2,
migrate(persistedState, version) {
if (version < 2) {
const state = persistedState as {
token: string;
openaiApiKey: string;
azureApiVersion: string;
};
state.openaiApiKey = state.token;
state.azureApiVersion = "2023-08-01-preview";
}
return persistedState as any;
},
},
);

View File

@@ -85,33 +85,6 @@ function getSummarizeModel(currentModel: string) {
return currentModel.startsWith("gpt") ? SUMMARIZE_MODEL : currentModel;
}
interface ChatStore {
sessions: ChatSession[];
currentSessionIndex: number;
clearSessions: () => void;
moveSession: (from: number, to: number) => void;
selectSession: (index: number) => void;
newSession: (mask?: Mask) => void;
deleteSession: (index: number) => void;
currentSession: () => ChatSession;
nextSession: (delta: number) => void;
onNewMessage: (message: ChatMessage) => void;
onUserInput: (content: string) => Promise<void>;
summarizeSession: () => void;
updateStat: (message: ChatMessage) => void;
updateCurrentSession: (updater: (session: ChatSession) => void) => void;
updateMessage: (
sessionIndex: number,
messageIndex: number,
updater: (message?: ChatMessage) => void,
) => void;
resetSession: () => void;
getMessagesWithMemory: () => ChatMessage[];
getMemoryPrompt: () => ChatMessage;
clearAllData: () => void;
}
function countMessages(msgs: ChatMessage[]) {
return msgs.reduce((pre, cur) => pre + estimateTokenLength(cur.content), 0);
}

View File

@@ -49,7 +49,7 @@ export const DEFAULT_CONFIG = {
model: "gpt-3.5-turbo" as ModelType,
temperature: 0.5,
top_p: 1,
max_tokens: 2000,
max_tokens: 4000,
presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true,
@@ -82,7 +82,7 @@ export const ModalConfigValidator = {
return x as ModelType;
},
max_tokens(x: number) {
return limitNumber(x, 0, 100000, 2000);
return limitNumber(x, 0, 512000, 1024);
},
presence_penalty(x: number) {
return limitNumber(x, -2, 2, 0);
@@ -128,15 +128,7 @@ export const useAppConfig = createPersistStore(
}));
},
allModels() {
const customModels = get()
.customModels.split(",")
.filter((v) => !!v && v.length > 0)
.map((m) => ({ name: m, available: true }));
const allModels = get().models.concat(customModels);
allModels.sort((a, b) => (a.name < b.name ? -1 : 1));
return allModels;
},
allModels() {},
}),
{
name: StoreKey.Config,

View File

@@ -357,3 +357,7 @@ pre {
overflow: hidden;
text-overflow: ellipsis;
}
.copyable {
user-select: text;
}

View File

@@ -1,3 +1,12 @@
export function deepClone<T>(obj: T) {
return JSON.parse(JSON.stringify(obj));
}
export function ensure<T extends object>(
obj: T,
keys: Array<[keyof T][number]>,
) {
return keys.every(
(k) => obj[k] !== undefined && obj[k] !== null && obj[k] !== "",
);
}

16
app/utils/hooks.ts Normal file
View File

@@ -0,0 +1,16 @@
import { useMemo } from "react";
import { useAccessStore, useAppConfig } from "../store";
import { collectModels } from "./model";
export function useAllModels() {
const accessStore = useAccessStore();
const configStore = useAppConfig();
const models = useMemo(() => {
return collectModels(
configStore.models,
[configStore.customModels, accessStore.customModels].join(","),
);
}, [accessStore.customModels, configStore.customModels, configStore.models]);
return models;
}

50
app/utils/model.ts Normal file
View File

@@ -0,0 +1,50 @@
import { LLMModel } from "../client/api";
export function collectModelTable(
models: readonly LLMModel[],
customModels: string,
) {
const modelTable: Record<
string,
{ available: boolean; name: string; displayName: string }
> = {};
// default models
models.forEach(
(m) =>
(modelTable[m.name] = {
...m,
displayName: m.name,
}),
);
// server custom models
customModels
.split(",")
.filter((v) => !!v && v.length > 0)
.map((m) => {
const available = !m.startsWith("-");
const nameConfig =
m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m;
const [name, displayName] = nameConfig.split(":");
modelTable[name] = {
name,
displayName: displayName || name,
available,
};
});
return modelTable;
}
/**
* Generate full model table.
*/
export function collectModels(
models: readonly LLMModel[],
customModels: string,
) {
const modelTable = collectModelTable(models, customModels);
const allModels = Object.values(modelTable);
return allModels;
}

View File

@@ -1,5 +1,5 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { combine, persist } from "zustand/middleware";
import { Updater } from "../typing";
import { deepClone } from "./clone";
@@ -23,33 +23,42 @@ type SetStoreState<T> = (
replace?: boolean | undefined,
) => void;
export function createPersistStore<T, M>(
defaultState: T,
export function createPersistStore<T extends object, M>(
state: T,
methods: (
set: SetStoreState<T & MakeUpdater<T>>,
get: () => T & MakeUpdater<T>,
) => M,
persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
) {
return create<T & M & MakeUpdater<T>>()(
persist((set, get) => {
return {
...defaultState,
...methods(set as any, get),
return create(
persist(
combine(
{
...state,
lastUpdateTime: 0,
},
(set, get) => {
return {
...methods(set, get as any),
lastUpdateTime: 0,
markUpdate() {
set({ lastUpdateTime: Date.now() } as Partial<
T & M & MakeUpdater<T>
>);
markUpdate() {
set({ lastUpdateTime: Date.now() } as Partial<
T & M & MakeUpdater<T>
>);
},
update(updater) {
const state = deepClone(get());
updater(state);
set({
...state,
lastUpdateTime: Date.now(),
});
},
} as M & MakeUpdater<T>;
},
update(updater) {
const state = deepClone(get());
updater(state);
get().markUpdate();
set(state);
},
};
}, persistOptions),
),
persistOptions as any,
),
);
}

View File

@@ -1,4 +1,4 @@
version: '3.9'
version: "3.9"
services:
chatgpt-next-web:
profiles: ["no-proxy"]
@@ -13,6 +13,9 @@ services:
- OPENAI_ORG_ID=$OPENAI_ORG_ID
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY
- DISABLE_GPT4=$DISABLE_GPT4
- ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY
- DISABLE_FAST_LINK=$DISABLE_FAST_LINK
- OPENAI_SB=$OPENAI_SB
chatgpt-next-web-proxy:
profiles: ["proxy"]
@@ -28,3 +31,6 @@ services:
- OPENAI_ORG_ID=$OPENAI_ORG_ID
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY
- DISABLE_GPT4=$DISABLE_GPT4
- ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY
- DISABLE_FAST_LINK=$DISABLE_FAST_LINK
- OPENAI_SB=$OPENAI_SB

View File

@@ -1,6 +1,7 @@
# Cloudflare Pages 部署指南
## 如何新建项目
在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。
1. 点击 "Create a project"。
@@ -12,7 +13,7 @@
7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。
8. 由于 node:buffer 的 bug暂时不要使用默认的 "Build command"。请使用以下命令:
```
npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify
npx @cloudflare/next-on-pages@1.5.0
```
9. 对于 "Build output directory",使用默认值并且不要修改。
10. 不要修改 "Root Directory"。
@@ -30,6 +31,8 @@
- `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID`
- `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key`
- `DISABLE_GPT4=1 可选,不让用户使用 GPT-4`
- `ENABLE_BALANCE_QUERY=1 可选,启用余额查询功能`
- `DISABLE_FAST_LINK=1 可选,禁用从链接解析预制设置`
12. 点击 "Save and Deploy"。
13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。

View File

@@ -1,6 +1,7 @@
# Cloudflare Pages Deployment Guide
## How to create a new project
Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
1. Click "Create a project".
@@ -11,12 +12,13 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
6. For "Project name" and "Production branch", use the default values or change them as needed.
7. In "Build Settings", choose the "Framework presets" option and select "Next.js".
8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command:
```
npx @cloudflare/next-on-pages --experimental-minify
```
```
npx @cloudflare/next-on-pages --experimental-minify
```
9. For "Build output directory", use the default value and do not modify it.
10. Do not modify "Root Directory".
11. For "Environment variables", click ">" and then "Add variable". Fill in the following information:
- `NODE_VERSION=20.1`
- `NEXT_TELEMETRY_DISABLE=1`
- `OPENAI_API_KEY=your_own_API_key`
@@ -29,6 +31,9 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
- `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI`
- `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key`
- `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4`
- `ENABLE_BALANCE_QUERY=1 Optional, allow users to query balance`
- `DISABLE_FAST_LINK=1 Optional, disable parse settings from url`
- `OPENAI_SB=1 Optionaluse the third-party OpenAI-SB API`
12. Click "Save and Deploy".
13. Click "Cancel deployment" because you need to fill in Compatibility flags.

View File

@@ -20,11 +20,11 @@
"@hello-pangea/dnd": "^16.3.0",
"@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.11",
"emoji-picker-react": "^4.5.1",
"emoji-picker-react": "^4.5.15",
"fuse.js": "^6.6.2",
"html-to-image": "^1.11.11",
"mermaid": "^10.3.1",
"nanoid": "^4.0.2",
"mermaid": "^10.6.1",
"nanoid": "^5.0.3",
"next": "^13.4.9",
"node-fetch": "^3.3.1",
"react": "^18.2.0",
@@ -43,11 +43,11 @@
},
"devDependencies": {
"@tauri-apps/cli": "^1.4.0",
"@types/node": "^20.3.3",
"@types/node": "^20.9.0",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.7",
"@types/react-katex": "^3.0.0",
"@types/spark-md5": "^3.0.2",
"@types/spark-md5": "^3.0.4",
"cross-env": "^7.0.3",
"eslint": "^8.49.0",
"eslint-config-next": "13.4.19",

View File

@@ -9,7 +9,7 @@
},
"package": {
"productName": "ChatGPT Next Web",
"version": "2.9.10"
"version": "2.9.12"
},
"tauri": {
"allowlist": {

View File

@@ -1507,10 +1507,12 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
"@types/node@*", "@types/node@^20.3.3":
version "20.3.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6"
integrity sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==
"@types/node@*", "@types/node@^20.9.0":
version "20.9.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.0.tgz#bfcdc230583aeb891cf51e73cfdaacdd8deae298"
integrity sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==
dependencies:
undici-types "~5.26.4"
"@types/parse-json@^4.0.0":
version "4.0.0"
@@ -1550,10 +1552,10 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
"@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/spark-md5@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.4.tgz#c1221d63c069d95aba0c06a765b80661cacc12bf"
integrity sha512-qtOaDz+IXiNndPgYb6t1YoutnGvFRtWSNzpVjkAPCfB2UzTyybuD4Tjgs7VgRawum3JnJNRwNQd4N//SvrHg1Q==
"@types/unist@*", "@types/unist@^2.0.0":
version "2.0.6"
@@ -2126,11 +2128,6 @@ client-only@0.0.1:
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
clsx@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -2762,12 +2759,10 @@ elkjs@^0.8.2:
resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
emoji-picker-react@^4.5.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.1.tgz#341f27dc86ad09340a316e0632484fcb9aff7195"
integrity sha512-zpm0ui0TWkXZDUIevyNM0rC9Jyqc08RvVXH0KgsbSkDr+VgMQmYLu6UeI4SIWMZKsKMjQwujPpncRCFlEeykjw==
dependencies:
clsx "^1.2.1"
emoji-picker-react@^4.5.15:
version "4.5.15"
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.15.tgz#e12797c50584cb8af8aee7eb6c7c8fd953e41f7e"
integrity sha512-BTqo+pNUE8kqX8BKFTbD4fhlxcA69qfie5En4PerReLaaPfXVyRlDJ1uf85nKj2u5esUQ999iUf8YyqcPsM2Qw==
emoji-regex@^8.0.0:
version "8.0.0"
@@ -4314,10 +4309,10 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
mermaid@^10.3.1:
version "10.3.1"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.3.1.tgz#2f3c7e9f6bd7a8da2bef71cce2a542c8eba2a62e"
integrity sha512-hkenh7WkuRWPcob3oJtrN3W+yzrrIYuWF1OIfk/d0xGE8UWlvDhfexaHmDwwe8DKQgqMLI8DWEPwGprxkumjuw==
mermaid@^10.6.1:
version "10.6.1"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.6.1.tgz#701f4160484137a417770ce757ce1887a98c00fc"
integrity sha512-Hky0/RpOw/1il9X8AvzOEChfJtVvmXm+y7JML5C//ePYMy0/9jCEmW1E1g86x9oDfW9+iVEdTV/i+M6KWRNs4A==
dependencies:
"@braintree/sanitize-url" "^6.0.1"
"@types/d3-scale" "^4.0.3"
@@ -4690,10 +4685,10 @@ nanoid@^3.3.4:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
nanoid@^4.0.2:
version "4.0.2"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e"
integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==
nanoid@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.3.tgz#6c97f53d793a7a1de6a38ebb46f50f95bf9793c7"
integrity sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==
natural-compare@^1.4.0:
version "1.4.0"
@@ -5799,6 +5794,11 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"