{title}
@@ -227,6 +233,7 @@ export function SideBar(props: { className?: string }) {
title="NextChat"
subTitle="Build your own AI assistant."
logo={
}
+ shouldNarrow={shouldNarrow}
>
{
const isAlibaba = !!process.env.ALIBABA_API_KEY;
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
const isIflytek = !!process.env.IFLYTEK_API_KEY;
+ const isXAI = !!process.env.XAI_API_KEY;
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
// const randomIndex = Math.floor(Math.random() * apiKeys.length);
@@ -208,6 +213,10 @@ export const getServerSideConfig = () => {
iflytekApiKey: process.env.IFLYTEK_API_KEY,
iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
+ isXAI,
+ xaiUrl: process.env.XAI_URL,
+ xaiApiKey: getApiKey(process.env.XAI_API_KEY),
+
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
diff --git a/app/constant.ts b/app/constant.ts
index a06b8f050..9774bb594 100644
--- a/app/constant.ts
+++ b/app/constant.ts
@@ -28,6 +28,8 @@ export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
+export const XAI_BASE_URL = "https://api.x.ai";
+
export const CACHE_URL_PREFIX = "/api/cache";
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
@@ -59,6 +61,7 @@ export enum ApiPath {
Iflytek = "/api/iflytek",
Stability = "/api/stability",
Artifacts = "/api/artifacts",
+ XAI = "/api/xai",
}
export enum SlotID {
@@ -111,6 +114,7 @@ export enum ServiceProvider {
Moonshot = "Moonshot",
Stability = "Stability",
Iflytek = "Iflytek",
+ XAI = "XAI",
}
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
@@ -133,6 +137,7 @@ export enum ModelProvider {
Hunyuan = "Hunyuan",
Moonshot = "Moonshot",
Iflytek = "Iflytek",
+ XAI = "XAI",
}
export const Stability = {
@@ -215,6 +220,11 @@ export const Iflytek = {
ChatPath: "v1/chat/completions",
};
+export const XAI = {
+ ExampleEndpoint: XAI_BASE_URL,
+ ChatPath: "v1/chat/completions",
+};
+
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
// export const DEFAULT_SYSTEM_TEMPLATE = `
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
@@ -364,6 +374,8 @@ const iflytekModels = [
"4.0Ultra",
];
+const xAIModes = ["grok-beta"];
+
let seq = 1000; // 内置的模型序号生成器从1000开始
export const DEFAULT_MODELS = [
...openaiModels.map((name) => ({
@@ -476,6 +488,17 @@ export const DEFAULT_MODELS = [
sorted: 10,
},
})),
+ ...xAIModes.map((name) => ({
+ name,
+ available: true,
+ sorted: seq++,
+ provider: {
+ id: "xai",
+ providerName: "XAI",
+ providerType: "xai",
+ sorted: 11,
+ },
+ })),
] as const;
export const CHAT_PAGE_SIZE = 15;
diff --git a/app/global.d.ts b/app/global.d.ts
index 8ee636bcd..897871fec 100644
--- a/app/global.d.ts
+++ b/app/global.d.ts
@@ -26,6 +26,13 @@ declare interface Window {
isPermissionGranted(): Promise;
sendNotification(options: string | Options): void;
};
+ updater: {
+ checkUpdate(): Promise;
+ installUpdate(): Promise;
+ onUpdaterEvent(
+ handler: (status: UpdateStatusResult) => void,
+ ): Promise;
+ };
http: {
fetch(
url: string,
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 83fcef5d1..bcaa6c811 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -206,6 +206,8 @@ const cn = {
IsChecking: "正在检查更新...",
FoundUpdate: (x: string) => `发现新版本:${x}`,
GoToUpdate: "前往更新",
+ Success: "更新成功!",
+ Failed: "更新失败",
},
SendKey: "发送键",
Theme: "主题",
@@ -465,6 +467,17 @@ const cn = {
SubTitle: "样例:",
},
},
+ XAI: {
+ ApiKey: {
+ Title: "接口密钥",
+ SubTitle: "使用自定义XAI API Key",
+ Placeholder: "XAI API Key",
+ },
+ Endpoint: {
+ Title: "接口地址",
+ SubTitle: "样例:",
+ },
+ },
Stability: {
ApiKey: {
Title: "接口密钥",
@@ -500,8 +513,8 @@ const cn = {
Model: "模型 (model)",
CompressModel: {
- Title: "压缩模型",
- SubTitle: "用于压缩历史记录的模型",
+ Title: "对话摘要模型",
+ SubTitle: "用于压缩历史记录、生成对话标题的模型",
},
Temperature: {
Title: "随机性 (temperature)",
@@ -670,6 +683,10 @@ const cn = {
Title: "启用Artifacts",
SubTitle: "启用之后可以直接渲染HTML页面",
},
+ CodeFold: {
+ Title: "启用代码折叠",
+ SubTitle: "启用之后可以自动折叠/展开过长的代码块",
+ },
Share: {
Title: "分享此面具",
SubTitle: "生成此面具的直达链接",
diff --git a/app/locales/en.ts b/app/locales/en.ts
index b6e7ed367..ac247f8d2 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -208,6 +208,8 @@ const en: LocaleType = {
IsChecking: "Checking update...",
FoundUpdate: (x: string) => `Found new version: ${x}`,
GoToUpdate: "Update",
+ Success: "Update Successful.",
+ Failed: "Update Failed.",
},
SendKey: "Send Key",
Theme: "Theme",
@@ -450,6 +452,17 @@ const en: LocaleType = {
SubTitle: "Example: ",
},
},
+ XAI: {
+ ApiKey: {
+ Title: "XAI API Key",
+ SubTitle: "Use a custom XAI API Key",
+ Placeholder: "XAI API Key",
+ },
+ Endpoint: {
+ Title: "Endpoint Address",
+ SubTitle: "Example: ",
+ },
+ },
Stability: {
ApiKey: {
Title: "Stability API Key",
@@ -506,8 +519,8 @@ const en: LocaleType = {
Model: "Model",
CompressModel: {
- Title: "Compression Model",
- SubTitle: "Model used to compress history",
+ Title: "Summary Model",
+ SubTitle: "Model used to compress history and generate title",
},
Temperature: {
Title: "Temperature",
@@ -681,6 +694,11 @@ const en: LocaleType = {
Title: "Enable Artifacts",
SubTitle: "Can render HTML page when enable artifacts.",
},
+ CodeFold: {
+ Title: "Enable CodeFold",
+ SubTitle:
+ "Automatically collapse/expand overly long code blocks when CodeFold is enabled",
+ },
Share: {
Title: "Share This Mask",
SubTitle: "Generate a link to this mask",
diff --git a/app/store/access.ts b/app/store/access.ts
index 5d7d36c3e..3ab7ef84e 100644
--- a/app/store/access.ts
+++ b/app/store/access.ts
@@ -13,6 +13,7 @@ import {
MOONSHOT_BASE_URL,
STABILITY_BASE_URL,
IFLYTEK_BASE_URL,
+ XAI_BASE_URL,
} from "../constant";
import { getHeaders } from "../client/api";
import { getClientConfig } from "../config/client";
@@ -44,6 +45,8 @@ const DEFAULT_STABILITY_URL = isApp ? STABILITY_BASE_URL : ApiPath.Stability;
const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
+const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI;
+
const DEFAULT_ACCESS_STATE = {
accessCode: "",
useCustomConfig: false,
@@ -101,6 +104,10 @@ const DEFAULT_ACCESS_STATE = {
iflytekApiKey: "",
iflytekApiSecret: "",
+ // xai
+ xaiUrl: DEFAULT_XAI_URL,
+ xaiApiKey: "",
+
// server config
needCode: true,
hideUserApiKey: false,
@@ -169,6 +176,10 @@ export const useAccessStore = createPersistStore(
return ensure(get(), ["iflytekApiKey"]);
},
+ isValidXAI() {
+ return ensure(get(), ["xaiApiKey"]);
+ },
+
isAuthorized() {
this.fetch();
@@ -184,6 +195,7 @@ export const useAccessStore = createPersistStore(
this.isValidTencent() ||
this.isValidMoonshot() ||
this.isValidIflytek() ||
+ this.isValidXAI() ||
!this.enabledAccessControl() ||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
);
diff --git a/app/store/chat.ts b/app/store/chat.ts
index 9c6de27db..92727c6ee 100644
--- a/app/store/chat.ts
+++ b/app/store/chat.ts
@@ -447,22 +447,16 @@ export const useChatStore = createPersistStore(
if (attachImages && attachImages.length > 0) {
mContent = [
- {
- type: "text",
- text: userContent,
- },
+ ...(userContent
+ ? [{ type: "text" as const, text: userContent }]
+ : []),
+ ...attachImages.map((url) => ({
+ type: "image_url" as const,
+ image_url: { url },
+ })),
];
- mContent = mContent.concat(
- attachImages.map((url) => {
- return {
- type: "image_url",
- image_url: {
- url: url,
- },
- };
- }),
- );
}
+
let userMessage: ChatMessage = createMessage({
role: "user",
content: mContent,
diff --git a/app/store/config.ts b/app/store/config.ts
index f9ddce4a8..f14793c28 100644
--- a/app/store/config.ts
+++ b/app/store/config.ts
@@ -52,6 +52,8 @@ export const DEFAULT_CONFIG = {
enableArtifacts: true, // show artifacts config
+ enableCodeFold: true, // code fold config
+
disablePromptHint: false,
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
diff --git a/app/store/mask.ts b/app/store/mask.ts
index 0c74a892e..850abeef6 100644
--- a/app/store/mask.ts
+++ b/app/store/mask.ts
@@ -19,6 +19,7 @@ export type Mask = {
builtin: boolean;
plugin?: string[];
enableArtifacts?: boolean;
+ enableCodeFold?: boolean;
};
export const DEFAULT_MASK_STATE = {
diff --git a/app/store/update.ts b/app/store/update.ts
index e68fde369..327dc5e88 100644
--- a/app/store/update.ts
+++ b/app/store/update.ts
@@ -6,6 +6,7 @@ import {
} from "../constant";
import { getClientConfig } from "../config/client";
import { createPersistStore } from "../utils/store";
+import { clientUpdate } from "../utils";
import ChatGptIcon from "../icons/chatgpt.png";
import Locale from "../locales";
import { ClientApi } from "../client/api";
@@ -119,6 +120,7 @@ export const useUpdateStore = createPersistStore(
icon: `${ChatGptIcon.src}`,
sound: "Default",
});
+ clientUpdate();
}
}
});
diff --git a/app/utils.ts b/app/utils.ts
index d7c9ae43e..a59947f1e 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -399,3 +399,37 @@ export function getOperationId(operation: {
`${operation.method.toUpperCase()}${operation.path.replaceAll("/", "_")}`
);
}
+
+export function clientUpdate() {
+ // this a wild for updating client app
+ return window.__TAURI__?.updater
+ .checkUpdate()
+ .then((updateResult) => {
+ if (updateResult.shouldUpdate) {
+ window.__TAURI__?.updater
+ .installUpdate()
+ .then((result) => {
+ showToast(Locale.Settings.Update.Success);
+ })
+ .catch((e) => {
+ console.error("[Install Update Error]", e);
+ showToast(Locale.Settings.Update.Failed);
+ });
+ }
+ })
+ .catch((e) => {
+ console.error("[Check Update Error]", e);
+ showToast(Locale.Settings.Update.Failed);
+ });
+}
+
+// https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb
+export function semverCompare(a: string, b: string) {
+ if (a.startsWith(b + "-")) return -1;
+ if (b.startsWith(a + "-")) return 1;
+ return a.localeCompare(b, undefined, {
+ numeric: true,
+ sensitivity: "case",
+ caseFirst: "upper",
+ });
+}
diff --git a/app/utils/stream.ts b/app/utils/stream.ts
index 2eda768f3..782634595 100644
--- a/app/utils/stream.ts
+++ b/app/utils/stream.ts
@@ -100,7 +100,8 @@ export function fetch(url: string, options?: RequestInit): Promise {
})
.catch((e) => {
console.error("stream error", e);
- throw e;
+ // throw e;
+ return new Response("", { status: 599 });
});
}
return window.fetch(url, options);
diff --git a/package.json b/package.json
index 6db49241f..803c0d1a4 100644
--- a/package.json
+++ b/package.json
@@ -6,13 +6,13 @@
"mask": "npx tsx app/masks/build.ts",
"mask:watch": "npx watch \"yarn mask\" app/masks",
"dev": "concurrently -r \"yarn run mask:watch\" \"next dev\"",
- "build": "yarn test:ci && yarn mask && cross-env BUILD_MODE=standalone next build",
+ "build": "yarn mask && cross-env BUILD_MODE=standalone next build",
"start": "next start",
"lint": "next lint",
- "export": "yarn test:ci && yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
+ "export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
- "app:build": "yarn test:ci && yarn mask && yarn tauri build",
+ "app:build": "yarn mask && yarn tauri build",
"prompts": "node ./scripts/fetch-prompts.mjs",
"prepare": "husky install",
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
@@ -58,7 +58,7 @@
"@tauri-apps/cli": "1.5.11",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
- "@types/jest": "^29.5.12",
+ "@types/jest": "^29.5.13",
"@types/js-yaml": "4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.30",
@@ -88,4 +88,4 @@
"lint-staged/yaml": "^2.2.2"
},
"packageManager": "yarn@1.22.19"
-}
\ No newline at end of file
+}
diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs
index d2c0726b0..8320db3e4 100644
--- a/src-tauri/src/stream.rs
+++ b/src-tauri/src/stream.rs
@@ -119,11 +119,22 @@ pub async fn stream_fetch(
}
}
Err(err) => {
- println!("Error response: {:?}", err.source().expect("REASON").to_string());
+ let error: String = err.source()
+ .map(|e| e.to_string())
+ .unwrap_or_else(|| "Unknown error occurred".to_string());
+ println!("Error response: {:?}", error);
+ tauri::async_runtime::spawn( async move {
+ if let Err(e) = window.emit(event_name, ChunkPayload{ request_id, chunk: error.into() }) {
+ println!("Failed to emit chunk payload: {:?}", e);
+ }
+ if let Err(e) = window.emit(event_name, EndPayload{ request_id, status: 0 }) {
+ println!("Failed to emit end payload: {:?}", e);
+ }
+ });
StreamResponse {
request_id,
status: 599,
- status_text: err.source().expect("REASON").to_string(),
+ status_text: "Error".to_string(),
headers: HashMap::new(),
}
}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index cc137ee8a..415825b13 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -9,7 +9,7 @@
},
"package": {
"productName": "NextChat",
- "version": "2.15.4"
+ "version": "2.15.6"
},
"tauri": {
"allowlist": {
@@ -99,7 +99,7 @@
"endpoints": [
"https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/releases/latest/download/latest.json"
],
- "dialog": false,
+ "dialog": true,
"windows": {
"installMode": "passive"
},
diff --git a/yarn.lock b/yarn.lock
index 0f6a29d6d..ef296924e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2263,10 +2263,10 @@
dependencies:
"@types/istanbul-lib-report" "*"
-"@types/jest@^29.5.12":
- version "29.5.12"
- resolved "https://registry.npmmirror.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544"
- integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==
+"@types/jest@^29.5.13":
+ version "29.5.13"
+ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.13.tgz#8bc571659f401e6a719a7bf0dbcb8b78c71a8adc"
+ integrity sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==
dependencies:
expect "^29.0.0"
pretty-format "^29.0.0"