Compare commits

..

24 Commits

Author SHA1 Message Date
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
48 changed files with 723 additions and 419 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` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list.
To control custom models, use `+` to add a custom model, use `-` to hide a model, separated by comma.
## Requirements
NodeJS >= 18, Docker >= 20

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` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-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://ab.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,7 +39,7 @@ 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",
@@ -47,11 +47,17 @@ export function auth(req: NextRequest) {
}
// 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 ?? ""] === 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 {

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;
@@ -80,7 +81,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 +106,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 +148,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,14 +431,9 @@ export function ChatActions(props: {
// switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model;
const models = useMemo(
() =>
config
.allModels()
.filter((m) => m.available)
.map((m) => m.name),
[config],
);
const models = useAllModels()
.filter((m) => m.available)
.map((m) => m.name);
const [showModelSelector, setShowModelSelector] = useState(false);
return (
@@ -1001,7 +998,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!));

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,7 +25,7 @@ export function ModelConfigList(props: {
);
}}
>
{config.allModels().map((v, i) => (
{allModels.map((v, i) => (
<option value={v.name} key={i} disabled={!v.available}>
{v.name}
</option>
@@ -75,8 +76,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

@@ -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,44 +905,152 @@ 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>
{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>
{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>
</>
)}
</>
)}
</>
) : null}
)}
{!shouldHideBalanceQuery ? (
<ListItem
@@ -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() {

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

@@ -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,6 +74,10 @@ 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.
@@ -79,7 +90,6 @@ 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

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

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

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,8 +4,9 @@ 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",
Tips: "Masukkan kode akses di bawah",
@@ -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",

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(), ["openaiUrl", "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,
[accessStore.customModels, configStore.customModels].join(","),
);
}, [accessStore.customModels, configStore.customModels, configStore.models]);
return models;
}

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

@@ -0,0 +1,40 @@
import { LLMModel } from "../client/api";
export function collectModelTable(
models: readonly LLMModel[],
customModels: string,
) {
const modelTable: Record<string, boolean> = {};
// default models
models.forEach((m) => (modelTable[m.name] = m.available));
// server custom models
customModels
.split(",")
.filter((v) => !!v && v.length > 0)
.map((m) => {
if (m.startsWith("+")) {
modelTable[m.slice(1)] = true;
} else if (m.startsWith("-")) {
modelTable[m.slice(1)] = false;
} else modelTable[m] = true;
});
return modelTable;
}
/**
* Generate full model table.
*/
export function collectModels(
models: readonly LLMModel[],
customModels: string,
) {
const modelTable = collectModelTable(models, customModels);
const allModels = Object.keys(modelTable).map((m) => ({
name: m,
available: modelTable[m],
}));
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,6 +1,6 @@
version: '3.9'
version: "3.9"
services:
chatgpt-next-web:
chatgpt-next-web:
profiles: ["no-proxy"]
container_name: chatgpt-next-web
image: yidadaa/chatgpt-next-web
@@ -13,8 +13,11 @@ 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:
chatgpt-next-web-proxy:
profiles: ["proxy"]
container_name: chatgpt-next-web-proxy
image: yidadaa/chatgpt-next-web
@@ -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,10 +31,12 @@
- `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。
14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。
15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。
16. 前往 "Deployments",点击 "Retry deployment"。
17. Enjoy.
17. Enjoy.

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,7 +31,10 @@ 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.
14. Go to "Build settings", "Functions", and find "Compatibility flags".

View File

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