From 7d55a6d0e441bddaf9870c9adfa88f1f72c600a5 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Tue, 8 Oct 2024 21:31:01 +0800 Subject: [PATCH 01/32] feat: allow send image only --- app/components/chat.tsx | 7 +++++-- app/store/chat.ts | 22 ++++++++-------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 3d519dee7..77b48abee 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -115,11 +115,14 @@ import { getClientConfig } from "../config/client"; import { useAllModels } from "../utils/hooks"; import { MultimodalContent } from "../client/api"; -const localStorage = safeLocalStorage(); import { ClientApi } from "../client/api"; import { createTTSPlayer } from "../utils/audio"; import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts"; +import { isEmpty } from "lodash-es"; + +const localStorage = safeLocalStorage(); + const ttsPlayer = createTTSPlayer(); const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { @@ -1015,7 +1018,7 @@ function _Chat() { }; const doSubmit = (userInput: string) => { - if (userInput.trim() === "") return; + if (userInput.trim() === "" && isEmpty(attachImages)) return; const matchCommand = chatCommands.match(userInput); if (matchCommand.matched) { setUserInput(""); diff --git a/app/store/chat.ts b/app/store/chat.ts index 968d8cb64..fc1fb23ba 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -337,22 +337,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, From 2eebfcf6fe873595a69ac0de805811fa7ed94f12 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 11 Oct 2024 11:29:22 +0800 Subject: [PATCH 02/32] client app tauri updater #2966 --- app/components/settings.tsx | 16 ++++++++++++---- app/global.d.ts | 7 +++++++ app/locales/cn.ts | 2 ++ app/locales/en.ts | 2 ++ app/store/update.ts | 2 ++ app/utils.ts | 23 +++++++++++++++++++++++ 6 files changed, 48 insertions(+), 4 deletions(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 9f338718e..9ee919b8d 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -49,7 +49,7 @@ import Locale, { changeLang, getLang, } from "../locales"; -import { copyToClipboard } from "../utils"; +import { copyToClipboard, clientUpdate } from "../utils"; import Link from "next/link"; import { Anthropic, @@ -1357,9 +1357,17 @@ export function Settings() { {checkingUpdate ? ( ) : hasNewVersion ? ( - - {Locale.Settings.Update.GoToUpdate} - + clientConfig?.isApp ? ( + } + text={Locale.Settings.Update.GoToUpdate} + onClick={() => clientUpdate()} + /> + ) : ( + + {Locale.Settings.Update.GoToUpdate} + + ) ) : ( } 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 e5bcca0ed..3086cc63e 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -205,6 +205,8 @@ const cn = { IsChecking: "正在检查更新...", FoundUpdate: (x: string) => `发现新版本:${x}`, GoToUpdate: "前往更新", + Success: "Update Succesfull.", + Failed: "Update Failed.", }, SendKey: "发送键", Theme: "主题", diff --git a/app/locales/en.ts b/app/locales/en.ts index 120457522..a025a522f 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -207,6 +207,8 @@ const en: LocaleType = { IsChecking: "Checking update...", FoundUpdate: (x: string) => `Found new version: ${x}`, GoToUpdate: "Update", + Success: "Update Succesfull.", + Failed: "Update Failed.", }, SendKey: "Send Key", Theme: "Theme", 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 5d4501710..9e75ffc7f 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -386,3 +386,26 @@ 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); + }); +} From bd9de4dc4db95a6d9aca4567d6147cb5b55b38ab Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 11 Oct 2024 11:42:36 +0800 Subject: [PATCH 03/32] fix version compare --- app/components/settings.tsx | 4 ++-- app/utils.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 9ee919b8d..c5653543c 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -49,7 +49,7 @@ import Locale, { changeLang, getLang, } from "../locales"; -import { copyToClipboard, clientUpdate } from "../utils"; +import { copyToClipboard, clientUpdate, semverCompare } from "../utils"; import Link from "next/link"; import { Anthropic, @@ -585,7 +585,7 @@ export function Settings() { const [checkingUpdate, setCheckingUpdate] = useState(false); const currentVersion = updateStore.formatVersion(updateStore.version); const remoteId = updateStore.formatVersion(updateStore.remoteVersion); - const hasNewVersion = currentVersion !== remoteId; + const hasNewVersion = semverCompare(currentVersion, remoteId) === -1; const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL; function checkUpdate(force = false) { diff --git a/app/utils.ts b/app/utils.ts index 9e75ffc7f..9e9e90f66 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -409,3 +409,14 @@ export function clientUpdate() { showToast(Locale.Settings.Update.Failed); }); } + +// https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb +export function semverCompare(a, b) { + if (a.startsWith(b + "-")) return -1; + if (b.startsWith(a + "-")) return 1; + return a.localeCompare(b, undefined, { + numeric: true, + sensitivity: "case", + caseFirst: "upper", + }); +} From a0d4a04192e2221f0ca969cab3236d4090a85955 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 11 Oct 2024 11:52:24 +0800 Subject: [PATCH 04/32] update --- app/locales/en.ts | 2 +- app/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/locales/en.ts b/app/locales/en.ts index a025a522f..40471536f 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -207,7 +207,7 @@ const en: LocaleType = { IsChecking: "Checking update...", FoundUpdate: (x: string) => `Found new version: ${x}`, GoToUpdate: "Update", - Success: "Update Succesfull.", + Success: "Update Successful.", Failed: "Update Failed.", }, SendKey: "Send Key", diff --git a/app/utils.ts b/app/utils.ts index 9e9e90f66..d8fc46330 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -411,7 +411,7 @@ export function clientUpdate() { } // https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb -export function semverCompare(a, b) { +export function semverCompare(a: string, b: string) { if (a.startsWith(b + "-")) return -1; if (b.startsWith(a + "-")) return 1; return a.localeCompare(b, undefined, { From be98aa20785c852ec8338090ed12798d34a50fba Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Fri, 11 Oct 2024 15:17:38 +0800 Subject: [PATCH 05/32] chore: improve test --- .github/workflows/test.yml | 23 +++++++++++++++++++++++ package.json | 8 ++++---- 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..b2f37cfb4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: Run Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: "yarn" + + - name: Install dependencies + run: yarn install + + - name: Run Jest tests + run: yarn test:ci diff --git a/package.json b/package.json index 6db49241f..e43344534 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", @@ -88,4 +88,4 @@ "lint-staged/yaml": "^2.2.2" }, "packageManager": "yarn@1.22.19" -} \ No newline at end of file +} From bd43af3a8d57d06fd74ca6556641a7397e383684 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Fri, 11 Oct 2024 15:41:46 +0800 Subject: [PATCH 06/32] chore: cache node_modules --- .github/workflows/test.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b2f37cfb4..fc885b293 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,12 @@ name: Run Tests -on: [push, pull_request] +on: + push: + branches: + - main + tags: + - "!*" + pull_request: jobs: test: @@ -16,6 +22,14 @@ jobs: node-version: 18 cache: "yarn" + - name: Cache node_modules + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node_modules- + - name: Install dependencies run: yarn install From c98dc31cdfd8bb92d11cc19cb577cb4d99356a72 Mon Sep 17 00:00:00 2001 From: code-october <148516338+code-october@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:03:20 +0000 Subject: [PATCH 07/32] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=BF=E9=97=AE?= =?UTF-8?q?=E7=A0=81=E8=BE=93=E5=85=A5=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/auth.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/components/auth.tsx b/app/components/auth.tsx index e19512d87..6c5e75d79 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -11,6 +11,7 @@ import Logo from "../icons/logo.svg"; import { useMobileScreen } from "@/app/utils"; import BotIcon from "../icons/bot.svg"; import { getClientConfig } from "../config/client"; +import { PasswordInput } from "./ui-lib"; import LeftIcon from "@/app/icons/left.svg"; import { safeLocalStorage } from "@/app/utils"; import { @@ -60,17 +61,20 @@ export function AuthPage() {
{Locale.Auth.Title}
{Locale.Auth.Tips}
- { accessStore.update( (access) => (access.accessCode = e.currentTarget.value), ); }} /> + {!accessStore.hideUserApiKey ? ( <>
{Locale.Auth.SubTips}
From 4a7fd3a3803b229f6ecaeea03a0589017130470c Mon Sep 17 00:00:00 2001 From: code-october <148516338+code-october@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:36:11 +0000 Subject: [PATCH 08/32] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A6=96=E9=A1=B5=20ap?= =?UTF-8?q?i=20=E8=BE=93=E5=85=A5=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/auth.tsx | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 6c5e75d79..539a52eec 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -78,22 +78,26 @@ export function AuthPage() { {!accessStore.hideUserApiKey ? ( <>
{Locale.Auth.SubTips}
- { accessStore.update( (access) => (access.openaiApiKey = e.currentTarget.value), ); }} /> - { accessStore.update( (access) => (access.googleApiKey = e.currentTarget.value), From 6792d6e475756b188f90c1f56d19188eabb7b55f Mon Sep 17 00:00:00 2001 From: code-october <148516338+code-october@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:19:36 +0000 Subject: [PATCH 09/32] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E4=BD=BF=E8=83=BD/=E7=A6=81=E7=94=A8=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=8A=98=E5=8F=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/markdown.tsx | 10 ++++++++-- app/components/mask.tsx | 17 +++++++++++++++++ app/components/settings.tsx | 15 +++++++++++++++ app/locales/cn.ts | 4 ++++ app/locales/en.ts | 5 +++++ app/store/config.ts | 2 ++ app/store/mask.ts | 1 + 7 files changed, 52 insertions(+), 2 deletions(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index a25b8537b..22664df8a 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -169,6 +169,12 @@ export function PreCode(props: { children: any }) { } function CustomCode(props: { children: any; className?: string }) { + const chatStore = useChatStore(); + const session = chatStore.currentSession(); + const config = useAppConfig(); + const enableCodeFold = + session.mask?.enableCodeFold != false && config.enableCodeFold; + const ref = useRef(null); const [collapsed, setCollapsed] = useState(true); const [showToggle, setShowToggle] = useState(false); @@ -190,13 +196,13 @@ function CustomCode(props: { children: any; className?: string }) { className={props?.className} ref={ref} style={{ - maxHeight: collapsed ? "400px" : "none", + maxHeight: enableCodeFold && collapsed ? "400px" : "none", overflowY: "hidden", }} > {props.children} - {showToggle && collapsed && ( + {showToggle && enableCodeFold && collapsed && (
diff --git a/app/components/mask.tsx b/app/components/mask.tsx index c60e7a528..12b19e335 100644 --- a/app/components/mask.tsx +++ b/app/components/mask.tsx @@ -183,6 +183,23 @@ export function MaskConfig(props: { > )} + {globalConfig.enableCodeFold && ( + + { + props.updateMask((mask) => { + mask.enableCodeFold = e.currentTarget.checked; + }); + }} + > + + )} {!props.shouldSyncFromGlobal ? ( + + + updateConfig( + (config) => (config.enableCodeFold = e.currentTarget.checked), + ) + } + > + diff --git a/app/locales/cn.ts b/app/locales/cn.ts index e5bcca0ed..3ba8dd80b 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -665,6 +665,10 @@ const cn = { Title: "启用Artifacts", SubTitle: "启用之后可以直接渲染HTML页面", }, + CodeFold: { + Title: "启用CodeFold", + SubTitle: "启用之后可以折叠/展开过长的代码块", + }, Share: { Title: "分享此面具", SubTitle: "生成此面具的直达链接", diff --git a/app/locales/en.ts b/app/locales/en.ts index 120457522..40782be7a 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -675,6 +675,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 block when enable CodeFold", + }, Share: { Title: "Share This Mask", SubTitle: "Generate a link to this mask", 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 = { From 8fd843d228e75068c1233738d4ce6507e2bddc4c Mon Sep 17 00:00:00 2001 From: code-october <148516338+code-october@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:38:52 +0000 Subject: [PATCH 10/32] =?UTF-8?q?=E5=8F=82=E8=80=83coderabbitai=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE=E8=A7=84=E8=8C=83=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/markdown.tsx | 19 ++++++++++++------- app/components/settings.tsx | 1 + app/locales/cn.ts | 4 ++-- app/locales/en.ts | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 22664df8a..8e744b441 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -190,6 +190,16 @@ function CustomCode(props: { children: any; className?: string }) { const toggleCollapsed = () => { setCollapsed((collapsed) => !collapsed); }; + const renderShowMoreButton = () => { + if (showToggle && enableCodeFold && collapsed) { + return ( +
+ +
+ ); + } + return null; + }; return ( <> {props.children} - {showToggle && enableCodeFold && collapsed && ( -
- -
- )} + + {renderShowMoreButton()} ); } diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 5f478374e..e24644813 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -1517,6 +1517,7 @@ export function Settings() { aria-label={Locale.Mask.Config.CodeFold.Title} type="checkbox" checked={config.enableCodeFold} + data-testid="enable-code-fold-checkbox" onChange={(e) => updateConfig( (config) => (config.enableCodeFold = e.currentTarget.checked), diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 3ba8dd80b..09eafe492 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -666,8 +666,8 @@ const cn = { SubTitle: "启用之后可以直接渲染HTML页面", }, CodeFold: { - Title: "启用CodeFold", - SubTitle: "启用之后可以折叠/展开过长的代码块", + Title: "启用代码折叠", + SubTitle: "启用之后可以自动折叠/展开过长的代码块", }, Share: { Title: "分享此面具", diff --git a/app/locales/en.ts b/app/locales/en.ts index 40782be7a..8dfe8ed93 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -678,7 +678,7 @@ const en: LocaleType = { CodeFold: { Title: "Enable CodeFold", SubTitle: - "Automatically collapse/expand overly long code block when enable CodeFold", + "Automatically collapse/expand overly long code blocks when CodeFold is enabled", }, Share: { Title: "Share This Mask", From 4a1319f2c0db25e5609553e2938761a4455ff6b8 Mon Sep 17 00:00:00 2001 From: code-october <148516338+code-october@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:57:23 +0000 Subject: [PATCH 11/32] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/markdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 8e744b441..9841a196d 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -173,7 +173,7 @@ function CustomCode(props: { children: any; className?: string }) { const session = chatStore.currentSession(); const config = useAppConfig(); const enableCodeFold = - session.mask?.enableCodeFold != false && config.enableCodeFold; + session.mask?.enableCodeFold !== false && config.enableCodeFold; const ref = useRef(null); const [collapsed, setCollapsed] = useState(true); @@ -212,7 +212,7 @@ function CustomCode(props: { children: any; className?: string }) { > {props.children} - + {renderShowMoreButton()} ); From 819238acaf5114329168f2c95da74d747795daa1 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Fri, 11 Oct 2024 20:49:43 +0800 Subject: [PATCH 12/32] fix: i18n --- app/locales/cn.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 3086cc63e..1e0116ec9 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -205,8 +205,8 @@ const cn = { IsChecking: "正在检查更新...", FoundUpdate: (x: string) => `发现新版本:${x}`, GoToUpdate: "前往更新", - Success: "Update Succesfull.", - Failed: "Update Failed.", + Success: "更新成功!", + Failed: "更新失败", }, SendKey: "发送键", Theme: "主题", From 9961b513cc0bfa1db8e1865af4099fdd9b78c15d Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Sat, 12 Oct 2024 14:54:22 +0800 Subject: [PATCH 13/32] fix: sidebar style --- app/components/home.module.scss | 3 +++ app/components/sidebar.tsx | 11 +++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/components/home.module.scss b/app/components/home.module.scss index b31334568..381b6a9b9 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -140,6 +140,9 @@ display: flex; justify-content: space-between; align-items: center; + &-narrow { + justify-content: center; + } } .sidebar-logo { diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 493b1103b..2a5c308b7 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -165,11 +165,17 @@ export function SideBarHeader(props: { subTitle?: string | React.ReactNode; logo?: React.ReactNode; children?: React.ReactNode; + shouldNarrow?: boolean; }) { - const { title, subTitle, logo, children } = props; + const { title, subTitle, logo, children, shouldNarrow } = props; return ( -
+
{title} @@ -227,6 +233,7 @@ export function SideBar(props: { className?: string }) { title="NextChat" subTitle="Build your own AI assistant." logo={} + shouldNarrow={shouldNarrow} >
Date: Sat, 12 Oct 2024 16:49:24 +0000 Subject: [PATCH 14/32] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E2=80=9C=E5=8E=8B?= =?UTF-8?q?=E7=BC=A9=E6=A8=A1=E5=9E=8B=E2=80=9D=E5=90=8D=E7=A7=B0=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E2=80=9C=E7=94=9F=E6=88=90=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E2=80=9D=E7=9A=84=E5=8A=9F=E8=83=BD=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/locales/cn.ts | 4 ++-- app/locales/en.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 09eafe492..b7debe805 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -495,8 +495,8 @@ const cn = { Model: "模型 (model)", CompressModel: { - Title: "压缩模型", - SubTitle: "用于压缩历史记录的模型", + Title: "对话摘要模型", + SubTitle: "用于压缩历史记录、生成对话标题的模型", }, Temperature: { Title: "随机性 (temperature)", diff --git a/app/locales/en.ts b/app/locales/en.ts index 8dfe8ed93..5cc296f1e 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -500,8 +500,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", From 12e7caa20977d8fbee2d16ec3d33ae3d94472603 Mon Sep 17 00:00:00 2001 From: ccq18 <348578429@qq.com> Date: Mon, 14 Oct 2024 16:03:01 +0800 Subject: [PATCH 15/32] =?UTF-8?q?fix=20=E9=BB=98=E8=AE=A4=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E6=97=B6=E9=97=B4=E6=94=B9=E4=B8=BA3=E5=88=86?= =?UTF-8?q?=E9=92=9F=EF=BC=8C=E6=94=AF=E6=8C=81o1-mini?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/constant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/constant.ts b/app/constant.ts index a06b8f050..856370511 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -95,7 +95,7 @@ export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id; export const STORAGE_KEY = "chatgpt-next-web"; -export const REQUEST_TIMEOUT_MS = 60000; +export const REQUEST_TIMEOUT_MS = 180000; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; From 592f62005b474b78042a373cbfd7361f3de9b323 Mon Sep 17 00:00:00 2001 From: ccq18 <348578429@qq.com> Date: Mon, 14 Oct 2024 16:31:17 +0800 Subject: [PATCH 16/32] =?UTF-8?q?=E4=BB=85=E4=BF=AE=E6=94=B9o1=E7=9A=84?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E6=97=B6=E9=97=B4=E4=B8=BA4=E5=88=86?= =?UTF-8?q?=E9=92=9F=EF=BC=8C=E5=87=8F=E5=B0=91o1=E7=B3=BB=E5=88=97?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E8=AF=B7=E6=B1=82=E5=A4=B1=E8=B4=A5=E7=9A=84?= =?UTF-8?q?=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/client/platforms/openai.ts | 2 +- app/constant.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index a22633611..76bac59e8 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -352,7 +352,7 @@ export class ChatGPTApi implements LLMApi { // make a fetch request const requestTimeoutId = setTimeout( () => controller.abort(), - isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow. + isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow. ); const res = await fetch(chatPath, chatPayload); diff --git a/app/constant.ts b/app/constant.ts index 856370511..a06b8f050 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -95,7 +95,7 @@ export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id; export const STORAGE_KEY = "chatgpt-next-web"; -export const REQUEST_TIMEOUT_MS = 180000; +export const REQUEST_TIMEOUT_MS = 60000; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; From 8c39a687b5d8bb44dfcc103ac69910d97220bea7 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 14 Oct 2024 16:53:46 +0800 Subject: [PATCH 17/32] update deploy_preview run target --- .github/workflows/deploy_preview.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/deploy_preview.yml b/.github/workflows/deploy_preview.yml index 30d9b85b4..b98845243 100644 --- a/.github/workflows/deploy_preview.yml +++ b/.github/workflows/deploy_preview.yml @@ -3,9 +3,7 @@ name: VercelPreviewDeployment on: pull_request_target: types: - - opened - - synchronize - - reopened + - review_requested env: VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }} From 103106bb9371748b3f1ab3f304846546297657cc Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 14 Oct 2024 17:09:56 +0800 Subject: [PATCH 18/32] update test run target --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fc885b293..faf7205d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,8 @@ on: tags: - "!*" pull_request: + types: + - review_requested jobs: test: From 7f454cbcec0d5c735726808ccbe75ba897142a31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:49:46 +0000 Subject: [PATCH 19/32] Bump @types/jest from 29.5.12 to 29.5.13 Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.12 to 29.5.13. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest) --- updated-dependencies: - dependency-name: "@types/jest" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e43344534..803c0d1a4 100644 --- a/package.json +++ b/package.json @@ -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", 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" From 87d85c10c3b3dcd7a62e174dabb6338f005ce9b7 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 14 Oct 2024 21:48:36 +0800 Subject: [PATCH 20/32] update --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index cc137ee8a..56a5b46a9 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -99,7 +99,7 @@ "endpoints": [ "https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/releases/latest/download/latest.json" ], - "dialog": false, + "dialog": true, "windows": { "installMode": "passive" }, From 463fa743e987624ab357c4335de0b133ccfa8fd0 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 15 Oct 2024 16:10:44 +0800 Subject: [PATCH 21/32] update version --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 56a5b46a9..b2c3e04b0 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.5" }, "tauri": { "allowlist": { From deb1e76c41ec156450db10872f88f84d2865d450 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Wed, 16 Oct 2024 21:57:07 +0800 Subject: [PATCH 22/32] fix: use tauri fetch --- app/client/platforms/anthropic.ts | 1 + app/client/platforms/moonshot.ts | 1 + app/client/platforms/openai.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index 1a83bd53a..3645cbe6e 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -13,6 +13,7 @@ import { getMessageTextContent, isVisionModel } from "@/app/utils"; import { preProcessImageContent, stream } from "@/app/utils/chat"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { RequestPayload } from "./openai"; +import { fetch } from "@/app/utils/stream"; export type MultiBlockContent = { type: "image" | "text"; diff --git a/app/client/platforms/moonshot.ts b/app/client/platforms/moonshot.ts index e0ef3494f..22a34b2e2 100644 --- a/app/client/platforms/moonshot.ts +++ b/app/client/platforms/moonshot.ts @@ -24,6 +24,7 @@ import { import { getClientConfig } from "@/app/config/client"; import { getMessageTextContent } from "@/app/utils"; import { RequestPayload } from "./openai"; +import { fetch } from "@/app/utils/stream"; export class MoonshotApi implements LLMApi { private disableListModels = true; diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 76bac59e8..30f7415c1 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -42,6 +42,7 @@ import { isVisionModel, isDalle3 as _isDalle3, } from "@/app/utils"; +import { fetch } from "@/app/utils/stream"; export interface OpenAIListModelResponse { object: string; From 8455fefc8aeada7e4204099f1548908320c4c1b7 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 23 Oct 2024 11:40:06 +0800 Subject: [PATCH 23/32] add xai --- app/api/[provider]/[...path]/route.ts | 3 + app/api/auth.ts | 5 +- app/api/xai.ts | 128 +++++++++++++++++ app/client/api.ts | 10 ++ app/client/platforms/xai.ts | 195 ++++++++++++++++++++++++++ app/components/settings.tsx | 41 ++++++ app/config/server.ts | 9 ++ app/constant.ts | 23 +++ app/locales/cn.ts | 11 ++ app/locales/en.ts | 11 ++ app/store/access.ts | 12 ++ 11 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 app/api/xai.ts create mode 100644 app/client/platforms/xai.ts diff --git a/app/api/[provider]/[...path]/route.ts b/app/api/[provider]/[...path]/route.ts index dffb3e9da..5ac248d0c 100644 --- a/app/api/[provider]/[...path]/route.ts +++ b/app/api/[provider]/[...path]/route.ts @@ -10,6 +10,7 @@ import { handle as alibabaHandler } from "../../alibaba"; import { handle as moonshotHandler } from "../../moonshot"; import { handle as stabilityHandler } from "../../stability"; import { handle as iflytekHandler } from "../../iflytek"; +import { handle as xaiHandler } from "../../xai"; import { handle as proxyHandler } from "../../proxy"; async function handle( @@ -38,6 +39,8 @@ async function handle( return stabilityHandler(req, { params }); case ApiPath.Iflytek: return iflytekHandler(req, { params }); + case ApiPath.XAI: + return xaiHandler(req, { params }); case ApiPath.OpenAI: return openaiHandler(req, { params }); default: diff --git a/app/api/auth.ts b/app/api/auth.ts index 95965ceec..fb147cf51 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -92,6 +92,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { systemApiKey = serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret; break; + case ModelProvider.XAI: + systemApiKey = serverConfig.xaiApiKey; + break; case ModelProvider.GPT: default: if (req.nextUrl.pathname.includes("azure/deployments")) { @@ -102,7 +105,7 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { } if (systemApiKey) { - console.log("[Auth] use system api key"); + console.log("[Auth] use system api key", systemApiKey); req.headers.set("Authorization", `Bearer ${systemApiKey}`); } else { console.log("[Auth] admin did not provide an api key"); diff --git a/app/api/xai.ts b/app/api/xai.ts new file mode 100644 index 000000000..a4ee8b397 --- /dev/null +++ b/app/api/xai.ts @@ -0,0 +1,128 @@ +import { getServerSideConfig } from "@/app/config/server"; +import { + XAI_BASE_URL, + ApiPath, + ModelProvider, + ServiceProvider, +} from "@/app/constant"; +import { prettyObject } from "@/app/utils/format"; +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/app/api/auth"; +import { isModelAvailableInServer } from "@/app/utils/model"; + +const serverConfig = getServerSideConfig(); + +export async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[XAI Route] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const authResult = auth(req, ModelProvider.XAI); + if (authResult.error) { + return NextResponse.json(authResult, { + status: 401, + }); + } + + try { + const response = await request(req); + return response; + } catch (e) { + console.error("[XAI] ", e); + return NextResponse.json(prettyObject(e)); + } +} + +async function request(req: NextRequest) { + const controller = new AbortController(); + + // alibaba use base url or just remove the path + let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, ""); + + let baseUrl = serverConfig.xaiUrl || XAI_BASE_URL; + + if (!baseUrl.startsWith("http")) { + baseUrl = `https://${baseUrl}`; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, -1); + } + + console.log("[Proxy] ", path); + console.log("[Base Url]", baseUrl); + + const timeoutId = setTimeout( + () => { + controller.abort(); + }, + 10 * 60 * 1000, + ); + + const fetchUrl = `${baseUrl}${path}`; + const fetchOptions: RequestInit = { + headers: { + "Content-Type": "application/json", + Authorization: req.headers.get("Authorization") ?? "", + }, + method: req.method, + body: req.body, + redirect: "manual", + // @ts-ignore + duplex: "half", + signal: controller.signal, + }; + + // #1815 try to refuse some request to some models + if (serverConfig.customModels && req.body) { + try { + const clonedBody = await req.text(); + fetchOptions.body = clonedBody; + + const jsonBody = JSON.parse(clonedBody) as { model?: string }; + + // not undefined and is false + if ( + isModelAvailableInServer( + serverConfig.customModels, + jsonBody?.model as string, + ServiceProvider.XAI as string, + ) + ) { + return NextResponse.json( + { + error: true, + message: `you are not allowed to use ${jsonBody?.model} model`, + }, + { + status: 403, + }, + ); + } + } catch (e) { + console.error(`[XAI] filter`, e); + } + } + try { + const res = await fetch(fetchUrl, fetchOptions); + + // to prevent browser prompt for credentials + const newHeaders = new Headers(res.headers); + newHeaders.delete("www-authenticate"); + // to disable nginx buffering + newHeaders.set("X-Accel-Buffering", "no"); + + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: newHeaders, + }); + } finally { + clearTimeout(timeoutId); + } +} diff --git a/app/client/api.ts b/app/client/api.ts index 7a242ea99..4238c2a26 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -20,6 +20,7 @@ import { QwenApi } from "./platforms/alibaba"; import { HunyuanApi } from "./platforms/tencent"; import { MoonshotApi } from "./platforms/moonshot"; import { SparkApi } from "./platforms/iflytek"; +import { XAIApi } from "./platforms/xai"; export const ROLES = ["system", "user", "assistant"] as const; export type MessageRole = (typeof ROLES)[number]; @@ -152,6 +153,9 @@ export class ClientApi { case ModelProvider.Iflytek: this.llm = new SparkApi(); break; + case ModelProvider.XAI: + this.llm = new XAIApi(); + break; default: this.llm = new ChatGPTApi(); } @@ -239,6 +243,7 @@ export function getHeaders(ignoreHeaders: boolean = false) { const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba; const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot; const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek; + const isXAI = modelConfig.providerName === ServiceProvider.XAI; const isEnabledAccessControl = accessStore.enabledAccessControl(); const apiKey = isGoogle ? accessStore.googleApiKey @@ -252,6 +257,8 @@ export function getHeaders(ignoreHeaders: boolean = false) { ? accessStore.alibabaApiKey : isMoonshot ? accessStore.moonshotApiKey + : isXAI + ? accessStore.xaiApiKey : isIflytek ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret @@ -266,6 +273,7 @@ export function getHeaders(ignoreHeaders: boolean = false) { isAlibaba, isMoonshot, isIflytek, + isXAI, apiKey, isEnabledAccessControl, }; @@ -328,6 +336,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi { return new ClientApi(ModelProvider.Moonshot); case ServiceProvider.Iflytek: return new ClientApi(ModelProvider.Iflytek); + case ServiceProvider.XAI: + return new ClientApi(ModelProvider.XAI); default: return new ClientApi(ModelProvider.GPT); } diff --git a/app/client/platforms/xai.ts b/app/client/platforms/xai.ts new file mode 100644 index 000000000..69f80e9fc --- /dev/null +++ b/app/client/platforms/xai.ts @@ -0,0 +1,195 @@ +"use client"; +// azure and openai, using same models. so using same LLMApi. +import { ApiPath, XAI_BASE_URL, XAI, REQUEST_TIMEOUT_MS } from "@/app/constant"; +import { + useAccessStore, + useAppConfig, + useChatStore, + ChatMessageTool, + usePluginStore, +} from "@/app/store"; +import { stream } from "@/app/utils/chat"; +import { + ChatOptions, + getHeaders, + LLMApi, + LLMModel, + SpeechOptions, +} from "../api"; +import { getClientConfig } from "@/app/config/client"; +import { getMessageTextContent } from "@/app/utils"; +import { RequestPayload } from "./openai"; +import { fetch } from "@/app/utils/stream"; + +export class XAIApi implements LLMApi { + private disableListModels = true; + + path(path: string): string { + const accessStore = useAccessStore.getState(); + + let baseUrl = ""; + + if (accessStore.useCustomConfig) { + baseUrl = accessStore.xaiUrl; + } + + if (baseUrl.length === 0) { + const isApp = !!getClientConfig()?.isApp; + const apiPath = ApiPath.XAI; + baseUrl = isApp ? XAI_BASE_URL : apiPath; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, baseUrl.length - 1); + } + if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) { + baseUrl = "https://" + baseUrl; + } + + console.log("[Proxy Endpoint] ", baseUrl, path); + + return [baseUrl, path].join("/"); + } + + extractMessage(res: any) { + return res.choices?.at(0)?.message?.content ?? ""; + } + + speech(options: SpeechOptions): Promise { + throw new Error("Method not implemented."); + } + + async chat(options: ChatOptions) { + const messages: ChatOptions["messages"] = []; + for (const v of options.messages) { + const content = getMessageTextContent(v); + messages.push({ role: v.role, content }); + } + + const modelConfig = { + ...useAppConfig.getState().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, + ...{ + model: options.config.model, + providerName: options.config.providerName, + }, + }; + + const requestPayload: RequestPayload = { + messages, + stream: options.config.stream, + model: modelConfig.model, + temperature: modelConfig.temperature, + 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); + + const shouldStream = !!options.config.stream; + const controller = new AbortController(); + options.onController?.(controller); + + try { + const chatPath = this.path(XAI.ChatPath); + const chatPayload = { + method: "POST", + body: JSON.stringify(requestPayload), + signal: controller.signal, + headers: getHeaders(), + }; + + // make a fetch request + const requestTimeoutId = setTimeout( + () => controller.abort(), + REQUEST_TIMEOUT_MS, + ); + + if (shouldStream) { + const [tools, funcs] = usePluginStore + .getState() + .getAsTools( + useChatStore.getState().currentSession().mask?.plugin || [], + ); + return stream( + chatPath, + requestPayload, + getHeaders(), + tools as any, + funcs, + controller, + // parseSSE + (text: string, runTools: ChatMessageTool[]) => { + // console.log("parseSSE", text, runTools); + const json = JSON.parse(text); + const choices = json.choices as Array<{ + delta: { + content: string; + tool_calls: ChatMessageTool[]; + }; + }>; + const tool_calls = choices[0]?.delta?.tool_calls; + if (tool_calls?.length > 0) { + const index = tool_calls[0]?.index; + const id = tool_calls[0]?.id; + const args = tool_calls[0]?.function?.arguments; + if (id) { + runTools.push({ + id, + type: tool_calls[0]?.type, + function: { + name: tool_calls[0]?.function?.name as string, + arguments: args, + }, + }); + } else { + // @ts-ignore + runTools[index]["function"]["arguments"] += args; + } + } + return choices[0]?.delta?.content; + }, + // processToolMessage, include tool_calls message and tool call results + ( + requestPayload: RequestPayload, + toolCallMessage: any, + toolCallResult: any[], + ) => { + // @ts-ignore + requestPayload?.messages?.splice( + // @ts-ignore + requestPayload?.messages?.length, + 0, + toolCallMessage, + ...toolCallResult, + ); + }, + options, + ); + } else { + const res = await fetch(chatPath, chatPayload); + clearTimeout(requestTimeoutId); + + const resJson = await res.json(); + const message = this.extractMessage(resJson); + options.onFinish(message); + } + } catch (e) { + console.log("[Request] failed to make a chat request", e); + options.onError?.(e as Error); + } + } + async usage() { + return { + used: 0, + total: 0, + }; + } + + async models(): Promise { + return []; + } +} diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 82ce70e5a..6ce71b5ef 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -59,6 +59,7 @@ import { ByteDance, Alibaba, Moonshot, + XAI, Google, GoogleSafetySettingsThreshold, OPENAI_BASE_URL, @@ -1194,6 +1195,45 @@ export function Settings() { ); + const XAIConfigComponent = accessStore.provider === ServiceProvider.XAI && ( + <> + + + accessStore.update( + (access) => (access.moonshotUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => (access.moonshotApiKey = e.currentTarget.value), + ); + }} + /> + + + ); + const stabilityConfigComponent = accessStore.provider === ServiceProvider.Stability && ( <> @@ -1652,6 +1692,7 @@ export function Settings() { {moonshotConfigComponent} {stabilityConfigComponent} {lflytekConfigComponent} + {XAIConfigComponent} )} diff --git a/app/config/server.ts b/app/config/server.ts index 6544fe564..eac4ba0cf 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -71,6 +71,10 @@ declare global { IFLYTEK_API_KEY?: string; IFLYTEK_API_SECRET?: string; + // xai only + XAI_URL?: string; + XAI_API_KEY?: string; + // custom template for preprocessing user input DEFAULT_INPUT_TEMPLATE?: string; } @@ -146,6 +150,7 @@ export const getServerSideConfig = () => { 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/locales/cn.ts b/app/locales/cn.ts index e514eb4fe..006fc8162 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -462,6 +462,17 @@ const cn = { SubTitle: "样例:", }, }, + XAI: { + ApiKey: { + Title: "接口密钥", + SubTitle: "使用自定义XAI API Key", + Placeholder: "XAI API Key", + }, + Endpoint: { + Title: "接口地址", + SubTitle: "样例:", + }, + }, Stability: { ApiKey: { Title: "接口密钥", diff --git a/app/locales/en.ts b/app/locales/en.ts index c86cc08f0..7204bd946 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -446,6 +446,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", diff --git a/app/store/access.ts b/app/store/access.ts index dec3a7258..1a27deb1c 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: "", + // moonshot + 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"])) ); From e791cd441d544a18126ddb825651d0e6274020e9 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 23 Oct 2024 11:55:25 +0800 Subject: [PATCH 24/32] add xai --- app/api/auth.ts | 2 +- app/client/platforms/xai.ts | 4 +--- app/store/access.ts | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/api/auth.ts b/app/api/auth.ts index fb147cf51..d4ac66a11 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -105,7 +105,7 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { } if (systemApiKey) { - console.log("[Auth] use system api key", systemApiKey); + console.log("[Auth] use system api key"); req.headers.set("Authorization", `Bearer ${systemApiKey}`); } else { console.log("[Auth] admin did not provide an api key"); diff --git a/app/client/platforms/xai.ts b/app/client/platforms/xai.ts index 69f80e9fc..deb74e66c 100644 --- a/app/client/platforms/xai.ts +++ b/app/client/platforms/xai.ts @@ -83,11 +83,9 @@ export class XAIApi 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); + console.log("[Request] xai payload: ", requestPayload); const shouldStream = !!options.config.stream; const controller = new AbortController(); diff --git a/app/store/access.ts b/app/store/access.ts index 1a27deb1c..b3d412a2d 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -104,7 +104,7 @@ const DEFAULT_ACCESS_STATE = { iflytekApiKey: "", iflytekApiSecret: "", - // moonshot + // xai xaiUrl: DEFAULT_XAI_URL, xaiApiKey: "", From 65bb962fc0b6eaa0cb1e15451d954df216b1956f Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 23 Oct 2024 12:00:59 +0800 Subject: [PATCH 25/32] hotfix --- app/components/settings.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 6ce71b5ef..666caece8 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -1206,11 +1206,11 @@ export function Settings() { accessStore.update( - (access) => (access.moonshotUrl = e.currentTarget.value), + (access) => (access.xaiUrl = e.currentTarget.value), ) } > @@ -1221,12 +1221,12 @@ export function Settings() { > { accessStore.update( - (access) => (access.moonshotApiKey = e.currentTarget.value), + (access) => (access.xaiApiKey = e.currentTarget.value), ); }} /> From 4745706c42a390117e5e0f700af3d5f06e18f312 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 24 Oct 2024 15:32:27 +0800 Subject: [PATCH 26/32] update version to v2.15.6 --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b2c3e04b0..415825b13 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "NextChat", - "version": "2.15.5" + "version": "2.15.6" }, "tauri": { "allowlist": { From e3ca7e8b4433bea43376035b9417fe233fe5f6f0 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 25 Oct 2024 17:52:08 +0800 Subject: [PATCH 27/32] hotfix for statusText is non ISO-8859-1 #5717 --- src-tauri/src/stream.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index d2c0726b0..d31dd67c3 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -119,11 +119,20 @@ pub async fn stream_fetch( } } Err(err) => { - println!("Error response: {:?}", err.source().expect("REASON").to_string()); + let error: String = err.source().expect("REASON").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(), } } From 2c745590101b5201c677243f151616cb7023186e Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 25 Oct 2024 18:02:51 +0800 Subject: [PATCH 28/32] hitfix --- app/utils/stream.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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); From 90ced9287626492898f2eb9bfd3b079171faf6ea Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 25 Oct 2024 18:05:29 +0800 Subject: [PATCH 29/32] update --- src-tauri/src/stream.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index d31dd67c3..8320db3e4 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -119,7 +119,9 @@ pub async fn stream_fetch( } } Err(err) => { - let error: String = 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() }) { From f89872b833d27c48b33281e60157640037e17a99 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 25 Oct 2024 18:12:09 +0800 Subject: [PATCH 30/32] hotfix for gemini invald argument #5715 --- app/client/platforms/google.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 7265a500b..14fecb8f2 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -192,7 +192,9 @@ export class GeminiProApi implements LLMApi { requestPayload, getHeaders(), // @ts-ignore - [{ functionDeclarations: tools.map((tool) => tool.function) }], + tools.length > 0 + ? [{ functionDeclarations: tools.map((tool) => tool.function) }] + : [], funcs, controller, // parseSSE From f0b3e10a6caf55bf91325183b5ad84de2a05db04 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 25 Oct 2024 18:19:22 +0800 Subject: [PATCH 31/32] hotfix for gemini invald argument #5715 --- app/client/platforms/google.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 14fecb8f2..a4b594ddf 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -193,7 +193,8 @@ export class GeminiProApi implements LLMApi { getHeaders(), // @ts-ignore tools.length > 0 - ? [{ functionDeclarations: tools.map((tool) => tool.function) }] + ? // @ts-ignore + [{ functionDeclarations: tools.map((tool) => tool.function) }] : [], funcs, controller, From 45db20c1c37279ebfe610d75a80dc09a21a14c54 Mon Sep 17 00:00:00 2001 From: ElricLiu <20209191+ElricLiu@users.noreply.github.com> Date: Sat, 26 Oct 2024 11:16:43 +0800 Subject: [PATCH 32/32] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d370000fa..b9e994e50 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 [MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple [Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu -[Deploy on Zeabur](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [Deploy on Zeabur](https://zeabur.com/templates/ZBUEFA) [Open in Gitpod](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) +[Deploy on Zeabur](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [Deploy on Zeabur](https://zeabur.com/templates/ZBUEFA) [Open in Gitpod](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) [Open in Gitpod](https://www.bt.cn/new/download.html) [](https://monica.im/?utm=nxcrp)