diff --git a/README.md b/README.md index 70fee646a..7d963a290 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,6 @@ ## 最新动态 -- 🚀 v2.9.6 版本发布 - 🚀 v2.9.5 正式版本发布 - 🚀 v2.9.1-plugin-preview 预览版发布。 diff --git a/README_CN.md b/README_CN.md index d8e9553e1..dde8c19b3 100644 --- a/README_CN.md +++ b/README_CN.md @@ -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 即可。 @@ -108,9 +122,9 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 ### `CUSTOM_MODELS` (可选) -> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`。 +> 示例:`+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`。 -用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,用英文逗号隔开。 +用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名:展示名` 来自定义模型的展示名,用英文逗号隔开。 ## 开发 @@ -124,7 +138,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 OPENAI_API_KEY= # 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址 -BASE_URL=https://nb.nextweb.fun/api/proxy +BASE_URL=https://a.nextweb.fun/api/proxy ``` ### 本地开发 diff --git a/app/api/auth.ts b/app/api/auth.ts index e0453b2b4..b41e34e05 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -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"); } diff --git a/app/api/common.ts b/app/api/common.ts index a1decd42f..dd1cc0bb8 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -1,19 +1,24 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSideConfig } from "../config/server"; import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant"; -import { collectModelTable, collectModels } from "../utils/model"; +import { collectModelTable } from "../utils/model"; +import { makeAzurePath } from "../azure"; 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 = serverConfig.baseUrl ?? OPENAI_BASE_URL; + let baseUrl = + serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL; if (!baseUrl.startsWith("http")) { baseUrl = `https://${baseUrl}`; @@ -23,7 +28,7 @@ export async function requestOpenai(req: NextRequest) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", openaiPath); + console.log("[Proxy] ", path); console.log("[Base Url]", baseUrl); console.log("[Org ID]", serverConfig.openaiOrgId); @@ -34,14 +39,24 @@ export async function requestOpenai(req: NextRequest) { 10 * 60 * 1000, ); - const fetchUrl = `${baseUrl}/${openaiPath}`; + 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 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, @@ -66,7 +81,7 @@ export async function requestOpenai(req: NextRequest) { const jsonBody = JSON.parse(clonedBody) as { model?: string }; // not undefined and is false - if (modelTable[jsonBody?.model ?? ""] === false) { + if (modelTable[jsonBody?.model ?? ""].available === false) { return NextResponse.json( { error: true, diff --git a/app/azure.ts b/app/azure.ts new file mode 100644 index 000000000..48406c55b --- /dev/null +++ b/app/azure.ts @@ -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; +} diff --git a/app/client/api.ts b/app/client/api.ts index 677d75f68..c304f6f44 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -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"; @@ -151,22 +151,26 @@ export const api = new ClientApi(); export function getHeaders() { const accessStore = useAccessStore.getState(); - let headers: Record = { + const headers: Record = { "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, ); } diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 12f95f8c5..d07d23a66 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -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"; @@ -21,6 +23,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; @@ -35,20 +38,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) { @@ -163,14 +181,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() { diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 577d77542..7962d46be 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -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() { { accessStore.update( - (access) => (access.token = e.currentTarget.value), + (access) => (access.openaiApiKey = e.currentTarget.value), ); }} /> diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index f76b369f1..33967717d 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -61,7 +61,10 @@ export function ChatItem(props: { {props.narrow ? (
- +
{props.count} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 65cd99abd..b995000eb 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -442,11 +442,27 @@ export function ChatActions(props: { // switch model const currentModel = chatStore.currentSession().mask.modelConfig.model; - const models = useAllModels() - .filter((m) => m.available) - .map((m) => m.name); + const allModels = useAllModels(); + const models = useMemo( + () => 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 (
@@ -525,8 +541,8 @@ export function ChatActions(props: { ({ - title: m, - value: m, + title: m.displayName, + value: m.name, }))} onClose={() => setShowModelSelector(false)} onSelection={(s) => { @@ -534,9 +550,6 @@ export function ChatActions(props: { chatStore.updateCurrentSession((session) => { session.mask.modelConfig.model = s[0] as ModelType; session.mask.syncGlobalConfig = false; - session.mask.usePlugins = /^gpt(?!.*03\d{2}$).*$/.test( - session.mask.modelConfig.model, - ); }); showToast(s[0]); }} @@ -1028,7 +1041,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!)); @@ -1188,7 +1203,12 @@ function _Chat() { {["system"].includes(message.role) ? ( ) : ( - + )} )} diff --git a/app/components/exporter.module.scss b/app/components/exporter.module.scss index c2046ffc0..d3bfc81ad 100644 --- a/app/components/exporter.module.scss +++ b/app/components/exporter.module.scss @@ -186,7 +186,8 @@ box-shadow: var(--card-shadow); border: var(--border-in-light); - *:not(li) { + code, + pre { overflow: hidden; } } diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 0a885d874..4ca6427a7 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -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 (
- + + {Locale.Exporter.Description.Title} +
+ } + >
@@ -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 (
@@ -437,13 +453,13 @@ export function ImagePreviewer(props: { showToast(Locale.Export.Image.Toast); const dom = previewRef.current; if (!dom) return; - + const isApp = getClientConfig()?.isApp; - + try { const blob = await toPng(dom); if (!blob) return; - + if (isMobile || (isApp && window.__TAURI__)) { if (isApp && window.__TAURI__) { const result = await window.__TAURI__.dialog.save({ @@ -459,7 +475,7 @@ export function ImagePreviewer(props: { }, ], }); - + if (result !== null) { const response = await fetch(blob); const buffer = await response.arrayBuffer(); @@ -604,8 +620,6 @@ export function MarkdownPreviewer(props: { ); } -// modified by BackTrackZ now it's looks better - export function JsonPreviewer(props: { messages: ChatMessage[]; topic: string; diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 1a1fbf416..f3a916cc5 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -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 ( - {props.content} + {escapedContent} ); } diff --git a/app/components/mask.tsx b/app/components/mask.tsx index 9fe1d485a..3f616c3ac 100644 --- a/app/components/mask.tsx +++ b/app/components/mask.tsx @@ -18,6 +18,7 @@ import { ChatMessage, createMessage, ModelConfig, + ModelType, useAppConfig, useChatStore, } from "../store"; @@ -58,11 +59,11 @@ function reorder(list: T[], startIndex: number, endIndex: number): T[] { return result; } -export function MaskAvatar(props: { mask: Mask }) { - return props.mask.avatar !== DEFAULT_MASK_AVATAR ? ( - +export function MaskAvatar(props: { avatar: string; model?: ModelType }) { + return props.avatar !== DEFAULT_MASK_AVATAR ? ( + ) : ( - + ); } @@ -123,7 +124,10 @@ export function MaskConfig(props: { onClick={() => setShowPicker(true)} style={{ cursor: "pointer" }} > - +
@@ -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() {
- +
{m.name}
diff --git a/app/components/message-selector.module.scss b/app/components/message-selector.module.scss index b4ba1a141..c8defb6b0 100644 --- a/app/components/message-selector.module.scss +++ b/app/components/message-selector.module.scss @@ -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; + } } } } diff --git a/app/components/message-selector.tsx b/app/components/message-selector.tsx index cadf52e64..c20153401 100644 --- a/app/components/message-selector.tsx +++ b/app/components/message-selector.tsx @@ -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: {
{messages.map((m, i) => { if (!isInSearchResult(m.id!)) return null; + const id = m.id ?? i; + const isSelected = props.selection.has(id); return (
{ 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" ? ( ) : ( - + )}
@@ -206,6 +222,10 @@ export function MessageSelector(props: { {m.content}
+ +
+ +
); })} diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 1c730e144..214a18c79 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -25,11 +25,13 @@ export function ModelConfigList(props: { ); }} > - {allModels.map((v, i) => ( - - ))} + {allModels + .filter((v) => v.available) + .map((v, i) => ( + + ))} void }) { return (
- +
{props.mask.name}
); diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 4f97f5abb..0e28267bf 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -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"; @@ -581,8 +584,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, @@ -878,16 +889,16 @@ export function Settings() {
- - {showAccessCode ? ( + + {showAccessCode && ( { accessStore.update( (access) => (access.accessCode = e.currentTarget.value), @@ -895,46 +906,154 @@ export function Settings() { }} /> - ) : ( - <> )} - {!accessStore.hideUserApiKey ? ( + {!accessStore.hideUserApiKey && ( <> accessStore.update( - (access) => (access.openaiUrl = e.currentTarget.value), + (access) => + (access.useCustomConfig = e.currentTarget.checked), ) } > - - { - accessStore.update( - (access) => (access.token = e.currentTarget.value), - ); - }} - /> - - - ) : null} + {accessStore.useCustomConfig && ( + <> + + + - {!shouldHideBalanceQuery ? ( + {accessStore.provider === "OpenAI" ? ( + <> + + + accessStore.update( + (access) => + (access.openaiUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.openaiApiKey = e.currentTarget.value), + ); + }} + /> + + + ) : ( + <> + + + accessStore.update( + (access) => + (access.azureUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.azureApiKey = e.currentTarget.value), + ); + }} + /> + + + + accessStore.update( + (access) => + (access.azureApiVersion = + e.currentTarget.value), + ) + } + > + + + )} + + )} + + )} + + {!shouldHideBalanceQuery && !clientConfig?.isApp ? ( - | JSX.Element - | null - | undefined; -}) { - return
{props.children}
; +export function List(props: { children: React.ReactNode; id?: string }) { + return ( +
+ {props.children} +
+ ); } 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) {
{props.children}
+ {props.footer}
{props.actions?.map((action, i) => (
diff --git a/app/config/server.ts b/app/config/server.ts index 007c39738..2f2e7d7fd 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -4,19 +4,28 @@ 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_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; } } } @@ -41,7 +50,7 @@ export const getServerSideConfig = () => { ); } - let disableGPT4 = !!process.env.DISABLE_GPT4; + const disableGPT4 = !!process.env.DISABLE_GPT4; let customModels = process.env.CUSTOM_MODELS ?? ""; if (disableGPT4) { @@ -51,15 +60,25 @@ export const getServerSideConfig = () => { .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, - openaiOrgId: process.env.OPENAI_ORG_ID, isVercel: !!process.env.VERCEL, + hideUserApiKey: !!process.env.HIDE_USER_API_KEY, disableGPT4, hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, diff --git a/app/constant.ts b/app/constant.ts index 5be0ea161..11909bea0 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -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"; @@ -24,10 +24,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 { @@ -63,6 +65,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", @@ -70,12 +77,18 @@ 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"; diff --git a/app/locales/ar.ts b/app/locales/ar.ts index d5844acd6..b58c3a2e8 100644 --- a/app/locales/ar.ts +++ b/app/locales/ar.ts @@ -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: "الحرارة", diff --git a/app/locales/bn.ts b/app/locales/bn.ts index 2db132cec..6dfb0da9b 100644 --- a/app/locales/bn.ts +++ b/app/locales/bn.ts @@ -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: "তাপমাত্রা", diff --git a/app/locales/cn.ts b/app/locales/cn.ts index f1b638271..6436f277a 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -87,8 +87,8 @@ const cn = { Copy: "全部复制", Download: "下载文件", Share: "分享到 ShareGPT", - MessageFromYou: "来自你的消息", - MessageFromChatGPT: "来自 ChatGPT 的消息", + MessageFromYou: "用户", + MessageFromChatGPT: "ChatGPT", Format: { Title: "导出格式", SubTitle: "可以导出 Markdown 文本或者 PNG 图片", @@ -260,11 +260,6 @@ const cn = { Title: "历史消息长度压缩阈值", SubTitle: "当未压缩的历史消息超过该值时,将进行压缩", }, - Token: { - Title: "API Key", - SubTitle: "使用自己的 Key 可绕过密码访问限制", - Placeholder: "OpenAI API Key", - }, Usage: { Title: "余额查询", @@ -275,19 +270,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)", @@ -443,6 +475,9 @@ const cn = { Config: "配置", }, Exporter: { + Description: { + Title: "只有清除上下文之后的消息会被展示", + }, Model: "模型", Messages: "消息", Topic: "主题", diff --git a/app/locales/cs.ts b/app/locales/cs.ts index 57aa803e4..c1a84430f 100644 --- a/app/locales/cs.ts +++ b/app/locales/cs.ts @@ -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", diff --git a/app/locales/de.ts b/app/locales/de.ts index e0bdc52b7..2fe871bc9 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -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 diff --git a/app/locales/en.ts b/app/locales/en.ts index 3f5c906c2..b4e6e8161 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -264,11 +264,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) { @@ -278,19 +274,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", @@ -444,6 +476,9 @@ const en: LocaleType = { Config: "Config", }, Exporter: { + Description: { + Title: "Only messages after clearing the context will be displayed", + }, Model: "Model", Messages: "Messages", Topic: "Topic", diff --git a/app/locales/es.ts b/app/locales/es.ts index a6ae154f4..7d742d536 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -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", diff --git a/app/locales/fr.ts b/app/locales/fr.ts index f5200f271..944754d62 100644 --- a/app/locales/fr.ts +++ b/app/locales/fr.ts @@ -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", diff --git a/app/locales/id.ts b/app/locales/id.ts index b5e4a70b7..f815a23ed 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -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", @@ -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", diff --git a/app/locales/it.ts b/app/locales/it.ts index bf20747b1..7f0a95846 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -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", diff --git a/app/locales/jp.ts b/app/locales/jp.ts index 87e8c3c13..e0ea07c75 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -147,11 +147,7 @@ const jp: PartialLocaleType = { SubTitle: "圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。", }, - Token: { - Title: "APIキー", - SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する", - Placeholder: "OpenAI APIキー", - }, + Usage: { Title: "残高照会", SubTitle(used: any, total: any) { @@ -161,11 +157,7 @@ const jp: PartialLocaleType = { Check: "再確認", NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示", }, - AccessCode: { - Title: "アクセスパスワード", - SubTitle: "暗号化アクセスが有効になっています", - Placeholder: "アクセスパスワードを入力してください", - }, + Model: "モデル (model)", Temperature: { Title: "ランダム性 (temperature)", diff --git a/app/locales/ko.ts b/app/locales/ko.ts index 717ce30b2..844459fc4 100644 --- a/app/locales/ko.ts +++ b/app/locales/ko.ts @@ -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)", diff --git a/app/locales/no.ts b/app/locales/no.ts index 43c92916f..3a0e61107 100644 --- a/app/locales/no.ts +++ b/app/locales/no.ts @@ -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", diff --git a/app/locales/ru.ts b/app/locales/ru.ts index bf98b4eb8..d12cf3e42 100644 --- a/app/locales/ru.ts +++ b/app/locales/ru.ts @@ -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: "Температура", diff --git a/app/locales/tr.ts b/app/locales/tr.ts index 06996d83d..524c1b2c5 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -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", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index e9f38d097..af47e30ff 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -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)", diff --git a/app/locales/vi.ts b/app/locales/vi.ts index 8f53a3dc1..3d95b5664 100644 --- a/app/locales/vi.ts +++ b/app/locales/vi.ts @@ -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)", diff --git a/app/store/access.ts b/app/store/access.ts index f87e44a2a..3b9008ba8 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -1,25 +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, customModels: "", - - openaiUrl: DEFAULT_OPENAI_URL, }; export const useAccessStore = createPersistStore( @@ -31,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() { @@ -64,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; + }, }, ); diff --git a/app/utils/clone.ts b/app/utils/clone.ts index 2958b6b9c..e4cd29111 100644 --- a/app/utils/clone.ts +++ b/app/utils/clone.ts @@ -1,3 +1,12 @@ export function deepClone(obj: T) { return JSON.parse(JSON.stringify(obj)); } + +export function ensure( + obj: T, + keys: Array<[keyof T][number]>, +) { + return keys.every( + (k) => obj[k] !== undefined && obj[k] !== null && obj[k] !== "", + ); +} diff --git a/app/utils/hooks.ts b/app/utils/hooks.ts index f6bfae673..35d1f53a4 100644 --- a/app/utils/hooks.ts +++ b/app/utils/hooks.ts @@ -8,7 +8,7 @@ export function useAllModels() { const models = useMemo(() => { return collectModels( configStore.models, - [accessStore.customModels, configStore.customModels].join(","), + [configStore.customModels, accessStore.customModels].join(","), ); }, [accessStore.customModels, configStore.customModels, configStore.models]); diff --git a/app/utils/model.ts b/app/utils/model.ts index 23090f9d2..d5c009c02 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -4,21 +4,34 @@ export function collectModelTable( models: readonly LLMModel[], customModels: string, ) { - const modelTable: Record = {}; + const modelTable: Record< + string, + { available: boolean; name: string; displayName: string } + > = {}; // default models - models.forEach((m) => (modelTable[m.name] = m.available)); + models.forEach( + (m) => + (modelTable[m.name] = { + ...m, + displayName: m.name, + }), + ); // 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; + 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; } @@ -31,10 +44,7 @@ export function collectModels( customModels: string, ) { const modelTable = collectModelTable(models, customModels); - const allModels = Object.keys(modelTable).map((m) => ({ - name: m, - available: modelTable[m], - })); + const allModels = Object.values(modelTable); return allModels; } diff --git a/app/utils/store.ts b/app/utils/store.ts index cd151dc49..684a19112 100644 --- a/app/utils/store.ts +++ b/app/utils/store.ts @@ -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 = ( replace?: boolean | undefined, ) => void; -export function createPersistStore( - defaultState: T, +export function createPersistStore( + state: T, methods: ( set: SetStoreState>, get: () => T & MakeUpdater, ) => M, persistOptions: SecondParam>>, ) { - return create>()( - 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 - >); + markUpdate() { + set({ lastUpdateTime: Date.now() } as Partial< + T & M & MakeUpdater + >); + }, + update(updater) { + const state = deepClone(get()); + updater(state); + set({ + ...state, + lastUpdateTime: Date.now(), + }); + }, + } as M & MakeUpdater; }, - update(updater) { - const state = deepClone(get()); - updater(state); - get().markUpdate(); - set(state); - }, - }; - }, persistOptions), + ), + persistOptions as any, + ), ); } diff --git a/package.json b/package.json index 7ea5efc8b..88ecfe5d5 100644 --- a/package.json +++ b/package.json @@ -25,16 +25,16 @@ "axios": "^1.4.0", "cheerio": "^1.0.0-rc.12", "duck-duck-scrape": "^2.2.4", - "emoji-picker-react": "^4.5.1", "encoding": "^0.1.13", + "emoji-picker-react": "^4.5.15", "fuse.js": "^6.6.2", "html-entities": "^2.4.0", "html-to-image": "^1.11.11", "html-to-text": "^9.0.5", "https-proxy-agent": "^7.0.2", "langchain": "^0.0.154", - "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", "openai": "^4.6.0", @@ -55,11 +55,11 @@ "devDependencies": { "@tauri-apps/cli": "^1.4.0", "@types/html-to-text": "^9.0.1", - "@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", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0f91501c6..2e9b46dcb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.10" + "version": "2.9.11" }, "tauri": { "allowlist": { diff --git a/yarn.lock b/yarn.lock index ef6d5bbcf..80c004250 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2621,10 +2621,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" @@ -3292,11 +3292,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" @@ -4007,12 +4002,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" @@ -5746,10 +5739,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" @@ -6158,10 +6151,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"