From c99cd31b6b7fbc00d3b5f386c5b7b9b7af31df90 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sat, 24 Aug 2024 15:34:33 +0800 Subject: [PATCH 01/79] add openapi-client-axios --- package.json | 2 ++ yarn.lock | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index eb0a5ef67..6907bbf45 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", "@vercel/speed-insights": "^1.0.2", + "axios": "^1.7.5", "emoji-picker-react": "^4.9.2", "fuse.js": "^7.0.0", "heic2any": "^0.0.4", @@ -33,6 +34,7 @@ "nanoid": "^5.0.3", "next": "^14.1.1", "node-fetch": "^3.3.1", + "openapi-client-axios": "^7.5.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^8.0.7", diff --git a/yarn.lock b/yarn.lock index 793c845d7..6d8c07cc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2138,6 +2138,11 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -2148,6 +2153,15 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece" integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== +axios@^1.7.5: + version "1.7.5" + resolved "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz#21eed340eb5daf47d29b6e002424b3e88c8c54b1" + integrity sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" @@ -2189,6 +2203,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bath-es5@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz#4e2808e8b33b4a5e3328ec1e9032f370f042193d" + integrity sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -2392,6 +2411,13 @@ colorette@^2.0.19: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + comma-separated-tokens@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" @@ -2925,11 +2951,21 @@ delaunator@5: dependencies: robust-predicates "^3.0.0" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + dequal@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +dereference-json-schema@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/dereference-json-schema/-/dereference-json-schema-0.2.1.tgz#fcad3c98e0116f7124b0989d39d947fa318cae09" + integrity sha512-uzJsrg225owJyRQ8FNTPHIuBOdSzIZlHhss9u6W8mp7jJldHqGuLv9cULagP/E26QVJDnjtG8U7Dw139mM1ydA== + diff@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" @@ -3548,6 +3584,11 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -3555,6 +3596,15 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + format@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" @@ -4937,7 +4987,7 @@ mime-db@1.52.0: resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.27: +mime-types@^2.1.12, mime-types@^2.1.27: version "2.1.35" resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -5161,6 +5211,20 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +openapi-client-axios@^7.5.5: + version "7.5.5" + resolved "https://registry.npmjs.org/openapi-client-axios/-/openapi-client-axios-7.5.5.tgz#4cb2bb7484ff9d1c92d9ff509db235cc35d64f38" + integrity sha512-pgCo1z+rxtYmGQXzB+N5DiXvRurTP6JqV+Ao/wtaGUMIIIM+znh3nTztps+FZS8mZgWnDHpdEzL9bWtZuWuvoA== + dependencies: + bath-es5 "^3.0.3" + dereference-json-schema "^0.2.1" + openapi-types "^12.1.3" + +openapi-types@^12.1.3: + version "12.1.3" + resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" + integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== + optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -5303,6 +5367,11 @@ property-information@^6.0.0: resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d" integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + punycode@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" From 4060e367ad90be23b9a94c241e2251d952520ea4 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Mon, 26 Aug 2024 21:13:35 +0800 Subject: [PATCH 02/79] feat: add indexDB --- app/utils/indexDB-storage.ts | 20 ++++++++++++++++++++ app/utils/store.ts | 4 +++- package.json | 1 + yarn.lock | 5 +++++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 app/utils/indexDB-storage.ts diff --git a/app/utils/indexDB-storage.ts b/app/utils/indexDB-storage.ts new file mode 100644 index 000000000..ef9927923 --- /dev/null +++ b/app/utils/indexDB-storage.ts @@ -0,0 +1,20 @@ +import { StateStorage } from "zustand/middleware"; +import { get, set, del } from "idb-keyval"; + +class IndexDBStorage implements StateStorage { + constructor() {} + + public async getItem(name: string): Promise { + return (await get(name)) || localStorage.getItem(name); + } + + public async setItem(name: string, value: string): Promise { + await set(name, value); + } + + public async removeItem(name: string): Promise { + await del(name); + } +} + +export const indexDBStorage = new IndexDBStorage(); diff --git a/app/utils/store.ts b/app/utils/store.ts index 684a19112..1a7e0d24b 100644 --- a/app/utils/store.ts +++ b/app/utils/store.ts @@ -1,7 +1,8 @@ import { create } from "zustand"; -import { combine, persist } from "zustand/middleware"; +import { combine, persist, createJSONStorage } from "zustand/middleware"; import { Updater } from "../typing"; import { deepClone } from "./clone"; +import { indexDBStorage } from "@/app/utils/indexDB-storage"; type SecondParam = T extends ( _f: infer _F, @@ -31,6 +32,7 @@ export function createPersistStore( ) => M, persistOptions: SecondParam>>, ) { + persistOptions.storage = createJSONStorage(() => indexDBStorage); return create( persist( combine( diff --git a/package.json b/package.json index eb0a5ef67..1c6d78c20 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "fuse.js": "^7.0.0", "heic2any": "^0.0.4", "html-to-image": "^1.11.11", + "idb-keyval": "^6.2.1", "lodash-es": "^4.17.21", "mermaid": "^10.6.1", "nanoid": "^5.0.3", diff --git a/yarn.lock b/yarn.lock index 793c845d7..1c7f834e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3926,6 +3926,11 @@ iconv-lite@0.6: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +idb-keyval@^6.2.1: + version "6.2.1" + resolved "https://registry.npmmirror.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" + integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== + ignore@^5.2.0: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" From 492b55c8939593f9eddef084f99e14a5d4a5033b Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Mon, 26 Aug 2024 21:20:07 +0800 Subject: [PATCH 03/79] feat: add indexDB --- app/store/chat.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index 653926d1b..ffaf8c996 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -27,6 +27,7 @@ import { createPersistStore } from "../utils/store"; import { collectModelsWithDefaultModel } from "../utils/model"; import { useAccessStore } from "./access"; import { isDalle3 } from "../utils"; +import { clear } from "idb-keyval"; export type ChatMessage = RequestMessage & { date: string; @@ -665,7 +666,8 @@ export const useChatStore = createPersistStore( set(() => ({ sessions })); }, - clearAllData() { + async clearAllData() { + await clear(); localStorage.clear(); location.reload(); }, From 0b758941a4104ee6fdcb58431ac7ebc5c69f2323 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Mon, 26 Aug 2024 21:23:21 +0800 Subject: [PATCH 04/79] feat: clear indexDB --- app/store/chat.ts | 4 ++-- app/utils/indexDB-storage.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index ffaf8c996..a6e4ee796 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -27,7 +27,7 @@ import { createPersistStore } from "../utils/store"; import { collectModelsWithDefaultModel } from "../utils/model"; import { useAccessStore } from "./access"; import { isDalle3 } from "../utils"; -import { clear } from "idb-keyval"; +import { indexDBStorage } from "@/app/utils/indexDB-storage"; export type ChatMessage = RequestMessage & { date: string; @@ -667,7 +667,7 @@ export const useChatStore = createPersistStore( }, async clearAllData() { - await clear(); + await indexDBStorage.clear(); localStorage.clear(); location.reload(); }, diff --git a/app/utils/indexDB-storage.ts b/app/utils/indexDB-storage.ts index ef9927923..5dee4c856 100644 --- a/app/utils/indexDB-storage.ts +++ b/app/utils/indexDB-storage.ts @@ -1,9 +1,7 @@ import { StateStorage } from "zustand/middleware"; -import { get, set, del } from "idb-keyval"; +import { get, set, del, clear } from "idb-keyval"; class IndexDBStorage implements StateStorage { - constructor() {} - public async getItem(name: string): Promise { return (await get(name)) || localStorage.getItem(name); } @@ -15,6 +13,10 @@ class IndexDBStorage implements StateStorage { public async removeItem(name: string): Promise { await del(name); } + + public async clear(): Promise { + await clear(); + } } export const indexDBStorage = new IndexDBStorage(); From c2fc0b49797ef8b016949d9051bfad140326bdef Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Tue, 27 Aug 2024 09:57:07 +0800 Subject: [PATCH 05/79] feat: try catch indexedDB error --- app/store/chat.ts | 4 ++-- app/utils/indexDB-storage.ts | 22 -------------------- app/utils/indexedDB-storage.ts | 38 ++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 24 deletions(-) delete mode 100644 app/utils/indexDB-storage.ts create mode 100644 app/utils/indexedDB-storage.ts diff --git a/app/store/chat.ts b/app/store/chat.ts index a6e4ee796..de2a63078 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -27,7 +27,7 @@ import { createPersistStore } from "../utils/store"; import { collectModelsWithDefaultModel } from "../utils/model"; import { useAccessStore } from "./access"; import { isDalle3 } from "../utils"; -import { indexDBStorage } from "@/app/utils/indexDB-storage"; +import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; export type ChatMessage = RequestMessage & { date: string; @@ -667,7 +667,7 @@ export const useChatStore = createPersistStore( }, async clearAllData() { - await indexDBStorage.clear(); + await indexedDBStorage.clear(); localStorage.clear(); location.reload(); }, diff --git a/app/utils/indexDB-storage.ts b/app/utils/indexDB-storage.ts deleted file mode 100644 index 5dee4c856..000000000 --- a/app/utils/indexDB-storage.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { StateStorage } from "zustand/middleware"; -import { get, set, del, clear } from "idb-keyval"; - -class IndexDBStorage implements StateStorage { - public async getItem(name: string): Promise { - return (await get(name)) || localStorage.getItem(name); - } - - public async setItem(name: string, value: string): Promise { - await set(name, value); - } - - public async removeItem(name: string): Promise { - await del(name); - } - - public async clear(): Promise { - await clear(); - } -} - -export const indexDBStorage = new IndexDBStorage(); diff --git a/app/utils/indexedDB-storage.ts b/app/utils/indexedDB-storage.ts new file mode 100644 index 000000000..51ee92b81 --- /dev/null +++ b/app/utils/indexedDB-storage.ts @@ -0,0 +1,38 @@ +import { StateStorage } from "zustand/middleware"; +import { get, set, del, clear } from "idb-keyval"; + +class IndexedDBStorage implements StateStorage { + public async getItem(name: string): Promise { + try { + return (await get(name)) || localStorage.getItem(name); + } catch (error) { + return localStorage.getItem(name); + } + } + + public async setItem(name: string, value: string): Promise { + try { + await set(name, value); + } catch (error) { + localStorage.setItem(name, value); + } + } + + public async removeItem(name: string): Promise { + try { + await del(name); + } catch (error) { + localStorage.removeItem(name); + } + } + + public async clear(): Promise { + try { + await clear(); + } catch (error) { + localStorage.clear(); + } + } +} + +export const indexedDBStorage = new IndexedDBStorage(); From 7b6fe66f2a1a7f227f7116b72f9dd4e10207cd44 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Tue, 27 Aug 2024 10:05:37 +0800 Subject: [PATCH 06/79] feat: try catch indexedDB error --- app/utils/store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/utils/store.ts b/app/utils/store.ts index 1a7e0d24b..13bef6d5d 100644 --- a/app/utils/store.ts +++ b/app/utils/store.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { combine, persist, createJSONStorage } from "zustand/middleware"; import { Updater } from "../typing"; import { deepClone } from "./clone"; -import { indexDBStorage } from "@/app/utils/indexDB-storage"; +import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; type SecondParam = T extends ( _f: infer _F, @@ -32,7 +32,7 @@ export function createPersistStore( ) => M, persistOptions: SecondParam>>, ) { - persistOptions.storage = createJSONStorage(() => indexDBStorage); + persistOptions.storage = createJSONStorage(() => indexedDBStorage); return create( persist( combine( From f5209fc344b281cf148c6ce4eddea248c106140e Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 28 Aug 2024 23:58:46 +0800 Subject: [PATCH 07/79] stash code --- app/api/common.ts | 5 +- app/client/api.ts | 10 +- app/client/platforms/openai.ts | 278 +++++++++++++++++++++++--------- app/components/chat.module.scss | 17 +- app/components/chat.tsx | 22 ++- app/store/chat.ts | 29 ++++ 6 files changed, 276 insertions(+), 85 deletions(-) diff --git a/app/api/common.ts b/app/api/common.ts index 24453dd96..25decbf62 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -32,10 +32,7 @@ export async function requestOpenai(req: NextRequest) { authHeaderName = "Authorization"; } - let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll( - "/api/openai/", - "", - ); + let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", ""); let baseUrl = (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL; diff --git a/app/client/api.ts b/app/client/api.ts index d7fb023a2..cecc453ba 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -5,7 +5,13 @@ import { ModelProvider, ServiceProvider, } from "../constant"; -import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store"; +import { + ChatMessageTool, + ChatMessage, + ModelType, + useAccessStore, + useChatStore, +} from "../store"; import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai"; import { GeminiProApi } from "./platforms/google"; import { ClaudeApi } from "./platforms/anthropic"; @@ -56,6 +62,8 @@ export interface ChatOptions { onFinish: (message: string) => void; onError?: (err: Error) => void; onController?: (controller: AbortController) => void; + onBeforeTool?: (tool: ChatMessageTool) => void; + onAfterTool?: (tool: ChatMessageTool) => void; } export interface LLMUsage { diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index d4e262c16..03bc3e09f 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -250,6 +250,8 @@ export class ChatGPTApi implements LLMApi { let responseText = ""; let remainText = ""; let finished = false; + let running = false; + let runTools = []; // animate response to make it looks smooth function animateResponseText() { @@ -276,8 +278,70 @@ export class ChatGPTApi implements LLMApi { // start animaion animateResponseText(); + // TODO 后面这里是从选择的plugins中获取function列表 + const funcs = { + get_current_weather: (args) => { + console.log("call get_current_weather", args); + return "30"; + }, + }; const finish = () => { if (!finished) { + console.log("try run tools", runTools.length, finished, running); + if (!running && runTools.length > 0) { + const toolCallMessage = { + role: "assistant", + tool_calls: [...runTools], + }; + running = true; + runTools.splice(0, runTools.length); // empty runTools + return Promise.all( + toolCallMessage.tool_calls.map((tool) => { + options?.onBeforeTool(tool); + return Promise.resolve( + funcs[tool.function.name]( + JSON.parse(tool.function.arguments), + ), + ) + .then((content) => { + options?.onAfterTool({ + ...tool, + content, + isError: false, + }); + return content; + }) + .catch((e) => { + options?.onAfterTool({ ...tool, isError: true }); + return e.toString(); + }) + .then((content) => ({ + role: "tool", + content, + tool_call_id: tool.id, + })); + }), + ).then((toolCallResult) => { + console.log("end runTools", toolCallMessage, toolCallResult); + requestPayload["messages"].splice( + requestPayload["messages"].length, + 0, + toolCallMessage, + ...toolCallResult, + ); + setTimeout(() => { + // call again + console.log("start again"); + running = false; + chatApi(chatPath, requestPayload); // call fetchEventSource + }, 0); + }); + console.log("try run tools", runTools.length, finished); + return; + } + if (running) { + return; + } finished = true; options.onFinish(responseText + remainText); } @@ -285,90 +349,148 @@ export class ChatGPTApi implements LLMApi { controller.signal.onabort = finish; - fetchEventSource(chatPath, { - ...chatPayload, - async onopen(res) { - clearTimeout(requestTimeoutId); - const contentType = res.headers.get("content-type"); - console.log( - "[OpenAI] request response content type: ", - contentType, - ); + function chatApi(chatPath, requestPayload) { + const chatPayload = { + method: "POST", + body: JSON.stringify({ + ...requestPayload, + // TODO 这里暂时写死的,后面从store.tools中按照当前session中选择的获取 + tools: [ + { + type: "function", + function: { + name: "get_current_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: + "The city and country, eg. San Francisco, USA", + }, + format: { + type: "string", + enum: ["celsius", "fahrenheit"], + }, + }, + required: ["location", "format"], + }, + }, + }, + ], + }), + signal: controller.signal, + headers: getHeaders(), + }; + console.log("chatApi", chatPath, requestPayload, chatPayload); + fetchEventSource(chatPath, { + ...chatPayload, + async onopen(res) { + clearTimeout(requestTimeoutId); + const contentType = res.headers.get("content-type"); + console.log( + "[OpenAI] request response content type: ", + contentType, + ); - if (contentType?.startsWith("text/plain")) { - responseText = await res.clone().text(); - return finish(); - } - - if ( - !res.ok || - !res.headers - .get("content-type") - ?.startsWith(EventStreamContentType) || - res.status !== 200 - ) { - const responseTexts = [responseText]; - let extraInfo = await res.clone().text(); - try { - const resJson = await res.clone().json(); - extraInfo = prettyObject(resJson); - } catch {} - - if (res.status === 401) { - responseTexts.push(Locale.Error.Unauthorized); - } - - if (extraInfo) { - responseTexts.push(extraInfo); - } - - responseText = responseTexts.join("\n\n"); - - return finish(); - } - }, - onmessage(msg) { - if (msg.data === "[DONE]" || finished) { - return finish(); - } - const text = msg.data; - try { - const json = JSON.parse(text); - const choices = json.choices as Array<{ - delta: { content: string }; - }>; - const delta = choices[0]?.delta?.content; - const textmoderation = json?.prompt_filter_results; - - if (delta) { - remainText += delta; + if (contentType?.startsWith("text/plain")) { + responseText = await res.clone().text(); + return finish(); } if ( - textmoderation && - textmoderation.length > 0 && - ServiceProvider.Azure + !res.ok || + !res.headers + .get("content-type") + ?.startsWith(EventStreamContentType) || + res.status !== 200 ) { - const contentFilterResults = - textmoderation[0]?.content_filter_results; - console.log( - `[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`, - contentFilterResults, - ); + const responseTexts = [responseText]; + let extraInfo = await res.clone().text(); + try { + const resJson = await res.clone().json(); + extraInfo = prettyObject(resJson); + } catch {} + + if (res.status === 401) { + responseTexts.push(Locale.Error.Unauthorized); + } + + if (extraInfo) { + responseTexts.push(extraInfo); + } + + responseText = responseTexts.join("\n\n"); + + return finish(); } - } catch (e) { - console.error("[Request] parse error", text, msg); - } - }, - onclose() { - finish(); - }, - onerror(e) { - options.onError?.(e); - throw e; - }, - openWhenHidden: true, - }); + }, + onmessage(msg) { + if (msg.data === "[DONE]" || finished) { + return finish(); + } + const text = msg.data; + try { + const json = JSON.parse(text); + const choices = json.choices as Array<{ + delta: { content: string }; + }>; + console.log("choices", choices); + const delta = choices[0]?.delta?.content; + const tool_calls = choices[0]?.delta?.tool_calls; + const textmoderation = json?.prompt_filter_results; + + if (delta) { + remainText += delta; + } + 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, + arguments: args, + }, + }); + } else { + runTools[index]["function"]["arguments"] += args; + } + } + + console.log("runTools", runTools); + + if ( + textmoderation && + textmoderation.length > 0 && + ServiceProvider.Azure + ) { + const contentFilterResults = + textmoderation[0]?.content_filter_results; + console.log( + `[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`, + contentFilterResults, + ); + } + } catch (e) { + console.error("[Request] parse error", text, msg); + } + }, + onclose() { + finish(); + }, + onerror(e) { + options.onError?.(e); + throw e; + }, + openWhenHidden: true, + }); + } + chatApi(chatPath, requestPayload); // call fetchEventSource } else { const res = await fetch(chatPath, chatPayload); clearTimeout(requestTimeoutId); diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 3b5c143b9..33ccaf523 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -413,6 +413,21 @@ margin-top: 5px; } +.chat-message-tools { + font-size: 12px; + color: #aaa; + line-height: 1.5; + margin-top: 5px; + .chat-message-tool { + display: inline-flex; + align-items: end; + svg { + margin-left: 5px; + margin-right: 5px; + } + } +} + .chat-message-item { box-sizing: border-box; max-width: 100%; @@ -630,4 +645,4 @@ .chat-input-send { bottom: 30px; } -} \ No newline at end of file +} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index ed5b06799..3ad8cd5c9 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -28,6 +28,7 @@ import DeleteIcon from "../icons/clear.svg"; import PinIcon from "../icons/pin.svg"; import EditIcon from "../icons/rename.svg"; import ConfirmIcon from "../icons/confirm.svg"; +import CloseIcon from "../icons/close.svg"; import CancelIcon from "../icons/cancel.svg"; import ImageIcon from "../icons/image.svg"; @@ -1573,11 +1574,30 @@ function _Chat() { )} - {showTyping && ( + {message?.tools?.length == 0 && showTyping && (
{Locale.Chat.Typing}
)} + {message?.tools?.length > 0 && ( +
+ {message?.tools?.map((tool) => ( +
+ {tool.isError === false ? ( + + ) : tool.isError === true ? ( + + ) : ( + + )} + {tool.function.name} +
+ ))} +
+ )}
): ChatMessage { @@ -389,6 +401,23 @@ export const useChatStore = createPersistStore( } ChatControllerPool.remove(session.id, botMessage.id); }, + onBeforeTool(tool: ChatMessageTool) { + (botMessage.tools = botMessage?.tools || []).push(tool); + get().updateCurrentSession((session) => { + session.messages = session.messages.concat(); + }); + }, + onAfterTool(tool: ChatMessageTool) { + console.log("onAfterTool", botMessage); + botMessage?.tools?.forEach((t, i, tools) => { + if (tool.id == t.id) { + tools[i] = { ...tool }; + } + }); + get().updateCurrentSession((session) => { + session.messages = session.messages.concat(); + }); + }, onError(error) { const isAborted = error.message.includes("aborted"); botMessage.content += From 29b5cd9436d0aa0bbd0dcc79fd06440ca5a73469 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 29 Aug 2024 00:21:26 +0800 Subject: [PATCH 08/79] ts error --- app/client/platforms/openai.ts | 39 ++++++++++++++++++++++------------ app/components/chat.tsx | 3 ++- app/store/chat.ts | 1 + 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 03bc3e09f..5e98845e0 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -9,7 +9,12 @@ import { REQUEST_TIMEOUT_MS, ServiceProvider, } from "@/app/constant"; -import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; +import { + ChatMessageTool, + useAccessStore, + useAppConfig, + useChatStore, +} from "@/app/store"; import { collectModelsWithDefaultModel } from "@/app/utils/model"; import { preProcessImageContent, @@ -251,7 +256,7 @@ export class ChatGPTApi implements LLMApi { let remainText = ""; let finished = false; let running = false; - let runTools = []; + let runTools: ChatMessageTool[] = []; // animate response to make it looks smooth function animateResponseText() { @@ -280,7 +285,7 @@ export class ChatGPTApi implements LLMApi { // TODO 后面这里是从选择的plugins中获取function列表 const funcs = { - get_current_weather: (args) => { + get_current_weather: (args: any) => { console.log("call get_current_weather", args); return "30"; }, @@ -297,14 +302,16 @@ export class ChatGPTApi implements LLMApi { runTools.splice(0, runTools.length); // empty runTools return Promise.all( toolCallMessage.tool_calls.map((tool) => { - options?.onBeforeTool(tool); + options?.onBeforeTool?.(tool); return Promise.resolve( + // @ts-ignore funcs[tool.function.name]( + // @ts-ignore JSON.parse(tool.function.arguments), ), ) .then((content) => { - options?.onAfterTool({ + options?.onAfterTool?.({ ...tool, content, isError: false, @@ -312,7 +319,7 @@ export class ChatGPTApi implements LLMApi { return content; }) .catch((e) => { - options?.onAfterTool({ ...tool, isError: true }); + options?.onAfterTool?.({ ...tool, isError: true }); return e.toString(); }) .then((content) => ({ @@ -323,8 +330,10 @@ export class ChatGPTApi implements LLMApi { }), ).then((toolCallResult) => { console.log("end runTools", toolCallMessage, toolCallResult); - requestPayload["messages"].splice( - requestPayload["messages"].length, + // @ts-ignore + requestPayload?.messages?.splice( + // @ts-ignore + requestPayload?.messages?.length, 0, toolCallMessage, ...toolCallResult, @@ -333,7 +342,7 @@ export class ChatGPTApi implements LLMApi { // call again console.log("start again"); running = false; - chatApi(chatPath, requestPayload); // call fetchEventSource + chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource }, 0); }); console.log("try run tools", runTools.length, finished); @@ -349,7 +358,7 @@ export class ChatGPTApi implements LLMApi { controller.signal.onabort = finish; - function chatApi(chatPath, requestPayload) { + function chatApi(chatPath: string, requestPayload: RequestPayload) { const chatPayload = { method: "POST", body: JSON.stringify({ @@ -434,7 +443,10 @@ export class ChatGPTApi implements LLMApi { try { const json = JSON.parse(text); const choices = json.choices as Array<{ - delta: { content: string }; + delta: { + content: string; + tool_calls: ChatMessageTool[]; + }; }>; console.log("choices", choices); const delta = choices[0]?.delta?.content; @@ -453,11 +465,12 @@ export class ChatGPTApi implements LLMApi { id, type: tool_calls[0]?.type, function: { - name: tool_calls[0]?.function?.name, + name: tool_calls[0]?.function?.name as string, arguments: args, }, }); } else { + // @ts-ignore runTools[index]["function"]["arguments"] += args; } } @@ -490,7 +503,7 @@ export class ChatGPTApi implements LLMApi { openWhenHidden: true, }); } - chatApi(chatPath, requestPayload); // call fetchEventSource + chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource } else { const res = await fetch(chatPath, chatPayload); clearTimeout(requestTimeoutId); diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 3ad8cd5c9..2ad579aa5 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1579,6 +1579,7 @@ function _Chat() { {Locale.Chat.Typing}
)} + {/*@ts-ignore*/} {message?.tools?.length > 0 && (
{message?.tools?.map((tool) => ( @@ -1593,7 +1594,7 @@ function _Chat() { ) : ( )} - {tool.function.name} + {tool?.function?.name}
))} diff --git a/app/store/chat.ts b/app/store/chat.ts index b035e51af..c2d199519 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -30,6 +30,7 @@ import { isDalle3 } from "../utils"; export type ChatMessageTool = { id: string; + index?: number; type?: string; function?: { name: string; From f3f6dc57c32173d37ad50a38b9d15166247bebe7 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 29 Aug 2024 00:32:35 +0800 Subject: [PATCH 09/79] stash code --- app/client/platforms/openai.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 5e98845e0..f3b488163 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -343,7 +343,7 @@ export class ChatGPTApi implements LLMApi { console.log("start again"); running = false; chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource - }, 0); + }, 5); }); console.log("try run tools", runTools.length, finished); return; From d212df8b952636937814a22f40b1b8d46f38d3d1 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 29 Aug 2024 00:39:51 +0800 Subject: [PATCH 10/79] stash code --- app/client/platforms/openai.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index f3b488163..0eea4331c 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -338,12 +338,12 @@ export class ChatGPTApi implements LLMApi { toolCallMessage, ...toolCallResult, ); - setTimeout(() => { + requestAnimationFrame(() => { // call again console.log("start again"); running = false; chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource - }, 5); + }); }); console.log("try run tools", runTools.length, finished); return; From f7a5f836db61c81cee25f862230c6b8952751dfe Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 29 Aug 2024 00:56:20 +0800 Subject: [PATCH 11/79] stash code --- app/client/platforms/openai.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 0eea4331c..4146ec9c1 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -338,12 +338,12 @@ export class ChatGPTApi implements LLMApi { toolCallMessage, ...toolCallResult, ); - requestAnimationFrame(() => { + setTimeout(() => { // call again console.log("start again"); running = false; chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource - }); + }, 60); }); console.log("try run tools", runTools.length, finished); return; From d58b99d6023ad4681e24f3fabcf34fd525cc8672 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 29 Aug 2024 01:00:16 +0800 Subject: [PATCH 12/79] stash code --- app/client/platforms/openai.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 4146ec9c1..fe1ef38d7 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -287,7 +287,9 @@ export class ChatGPTApi implements LLMApi { const funcs = { get_current_weather: (args: any) => { console.log("call get_current_weather", args); - return "30"; + return new Promise((resolve) => { + setTimeout(() => resolve("30"), 3000); + }); }, }; const finish = () => { From 341a52a61532c51489fe5b66109737ec9fccf2ad Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 29 Aug 2024 01:35:41 +0800 Subject: [PATCH 13/79] stash code --- app/components/chat.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 33ccaf523..7176399cc 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -419,7 +419,7 @@ line-height: 1.5; margin-top: 5px; .chat-message-tool { - display: inline-flex; + display: flex; align-items: end; svg { margin-left: 5px; From 7fc0d11931e6a5330850a5f903f5337f09ca3eb2 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 29 Aug 2024 17:14:23 +0800 Subject: [PATCH 14/79] create common function stream for fetchEventSource --- app/client/platforms/openai.ts | 327 ++++++++------------------------- app/utils/chat.ts | 207 ++++++++++++++++++++- 2 files changed, 279 insertions(+), 255 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index fe1ef38d7..edac751b0 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -20,6 +20,7 @@ import { preProcessImageContent, uploadImage, base64Image2Blob, + stream, } from "@/app/utils/chat"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing"; @@ -238,52 +239,30 @@ export class ChatGPTApi implements LLMApi { isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath, ); } - const chatPayload = { - method: "POST", - body: JSON.stringify(requestPayload), - signal: controller.signal, - headers: getHeaders(), - }; - - // make a fetch request - const requestTimeoutId = setTimeout( - () => controller.abort(), - isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow. - ); - if (shouldStream) { - let responseText = ""; - let remainText = ""; - let finished = false; - let running = false; - let runTools: ChatMessageTool[] = []; - - // animate response to make it looks smooth - function animateResponseText() { - if (finished || controller.signal.aborted) { - responseText += remainText; - console.log("[Response Animation] finished"); - if (responseText?.length === 0) { - options.onError?.(new Error("empty response from server")); - } - return; - } - - if (remainText.length > 0) { - const fetchCount = Math.max(1, Math.round(remainText.length / 60)); - const fetchText = remainText.slice(0, fetchCount); - responseText += fetchText; - remainText = remainText.slice(fetchCount); - options.onUpdate?.(responseText, fetchText); - } - - requestAnimationFrame(animateResponseText); - } - - // start animaion - animateResponseText(); - - // TODO 后面这里是从选择的plugins中获取function列表 + const tools = [ + { + type: "function", + function: { + name: "get_current_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and country, eg. San Francisco, USA", + }, + format: { + type: "string", + enum: ["celsius", "fahrenheit"], + }, + }, + required: ["location", "format"], + }, + }, + }, + ]; const funcs = { get_current_weather: (args: any) => { console.log("call get_current_weather", args); @@ -292,221 +271,61 @@ export class ChatGPTApi implements LLMApi { }); }, }; - const finish = () => { - if (!finished) { - console.log("try run tools", runTools.length, finished, running); - if (!running && runTools.length > 0) { - const toolCallMessage = { - role: "assistant", - tool_calls: [...runTools], + stream( + chatPath, + requestPayload, + getHeaders(), + tools, + funcs, + controller, + (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[]; }; - running = true; - runTools.splice(0, runTools.length); // empty runTools - return Promise.all( - toolCallMessage.tool_calls.map((tool) => { - options?.onBeforeTool?.(tool); - return Promise.resolve( - // @ts-ignore - funcs[tool.function.name]( - // @ts-ignore - JSON.parse(tool.function.arguments), - ), - ) - .then((content) => { - options?.onAfterTool?.({ - ...tool, - content, - isError: false, - }); - return content; - }) - .catch((e) => { - options?.onAfterTool?.({ ...tool, isError: true }); - return e.toString(); - }) - .then((content) => ({ - role: "tool", - content, - tool_call_id: tool.id, - })); - }), - ).then((toolCallResult) => { - console.log("end runTools", toolCallMessage, toolCallResult); + }>; + 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 - requestPayload?.messages?.splice( - // @ts-ignore - requestPayload?.messages?.length, - 0, - toolCallMessage, - ...toolCallResult, - ); - setTimeout(() => { - // call again - console.log("start again"); - running = false; - chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource - }, 60); - }); - console.log("try run tools", runTools.length, finished); - return; + runTools[index]["function"]["arguments"] += args; + } } - if (running) { - return; - } - finished = true; - options.onFinish(responseText + remainText); - } + + console.log("runTools", runTools); + return choices[0]?.delta?.content; + }, + options, + ); + } else { + const chatPayload = { + method: "POST", + body: JSON.stringify(requestPayload), + signal: controller.signal, + headers: getHeaders(), }; - controller.signal.onabort = finish; + // make a fetch request + const requestTimeoutId = setTimeout( + () => controller.abort(), + isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow. + ); - function chatApi(chatPath: string, requestPayload: RequestPayload) { - const chatPayload = { - method: "POST", - body: JSON.stringify({ - ...requestPayload, - // TODO 这里暂时写死的,后面从store.tools中按照当前session中选择的获取 - tools: [ - { - type: "function", - function: { - name: "get_current_weather", - description: "Get the current weather", - parameters: { - type: "object", - properties: { - location: { - type: "string", - description: - "The city and country, eg. San Francisco, USA", - }, - format: { - type: "string", - enum: ["celsius", "fahrenheit"], - }, - }, - required: ["location", "format"], - }, - }, - }, - ], - }), - signal: controller.signal, - headers: getHeaders(), - }; - console.log("chatApi", chatPath, requestPayload, chatPayload); - fetchEventSource(chatPath, { - ...chatPayload, - async onopen(res) { - clearTimeout(requestTimeoutId); - const contentType = res.headers.get("content-type"); - console.log( - "[OpenAI] request response content type: ", - contentType, - ); - - if (contentType?.startsWith("text/plain")) { - responseText = await res.clone().text(); - return finish(); - } - - if ( - !res.ok || - !res.headers - .get("content-type") - ?.startsWith(EventStreamContentType) || - res.status !== 200 - ) { - const responseTexts = [responseText]; - let extraInfo = await res.clone().text(); - try { - const resJson = await res.clone().json(); - extraInfo = prettyObject(resJson); - } catch {} - - if (res.status === 401) { - responseTexts.push(Locale.Error.Unauthorized); - } - - if (extraInfo) { - responseTexts.push(extraInfo); - } - - responseText = responseTexts.join("\n\n"); - - return finish(); - } - }, - onmessage(msg) { - if (msg.data === "[DONE]" || finished) { - return finish(); - } - const text = msg.data; - try { - const json = JSON.parse(text); - const choices = json.choices as Array<{ - delta: { - content: string; - tool_calls: ChatMessageTool[]; - }; - }>; - console.log("choices", choices); - const delta = choices[0]?.delta?.content; - const tool_calls = choices[0]?.delta?.tool_calls; - const textmoderation = json?.prompt_filter_results; - - if (delta) { - remainText += delta; - } - 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; - } - } - - console.log("runTools", runTools); - - if ( - textmoderation && - textmoderation.length > 0 && - ServiceProvider.Azure - ) { - const contentFilterResults = - textmoderation[0]?.content_filter_results; - console.log( - `[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`, - contentFilterResults, - ); - } - } catch (e) { - console.error("[Request] parse error", text, msg); - } - }, - onclose() { - finish(); - }, - onerror(e) { - options.onError?.(e); - throw e; - }, - openWhenHidden: true, - }); - } - chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource - } else { const res = await fetch(chatPath, chatPayload); clearTimeout(requestTimeoutId); diff --git a/app/utils/chat.ts b/app/utils/chat.ts index 6a296e576..1289695b9 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -1,5 +1,15 @@ -import { CACHE_URL_PREFIX, UPLOAD_URL } from "@/app/constant"; +import { + CACHE_URL_PREFIX, + UPLOAD_URL, + REQUEST_TIMEOUT_MS, +} from "@/app/constant"; import { RequestMessage } from "@/app/client/api"; +import Locale from "@/app/locales"; +import { + EventStreamContentType, + fetchEventSource, +} from "@fortaine/fetch-event-source"; +import { prettyObject } from "./format"; export function compressImage(file: Blob, maxSize: number): Promise { return new Promise((resolve, reject) => { @@ -142,3 +152,198 @@ export function removeImage(imageUrl: string) { credentials: "include", }); } + +export function stream( + chatPath: string, + requestPayload: any, + headers: any, + tools: any[], + funcs: any, + controller: AbortController, + parseSSE: (text: string, runTools: any[]) => string | undefined, + options: any, +) { + let responseText = ""; + let remainText = ""; + let finished = false; + let running = false; + let runTools: any[] = []; + + // animate response to make it looks smooth + function animateResponseText() { + if (finished || controller.signal.aborted) { + responseText += remainText; + console.log("[Response Animation] finished"); + if (responseText?.length === 0) { + options.onError?.(new Error("empty response from server")); + } + return; + } + + if (remainText.length > 0) { + const fetchCount = Math.max(1, Math.round(remainText.length / 60)); + const fetchText = remainText.slice(0, fetchCount); + responseText += fetchText; + remainText = remainText.slice(fetchCount); + options.onUpdate?.(responseText, fetchText); + } + + requestAnimationFrame(animateResponseText); + } + + // start animaion + animateResponseText(); + + const finish = () => { + if (!finished) { + console.log("try run tools", runTools.length, finished, running); + if (!running && runTools.length > 0) { + const toolCallMessage = { + role: "assistant", + tool_calls: [...runTools], + }; + running = true; + runTools.splice(0, runTools.length); // empty runTools + return Promise.all( + toolCallMessage.tool_calls.map((tool) => { + options?.onBeforeTool?.(tool); + return Promise.resolve( + // @ts-ignore + funcs[tool.function.name]( + // @ts-ignore + JSON.parse(tool.function.arguments), + ), + ) + .then((content) => { + options?.onAfterTool?.({ + ...tool, + content, + isError: false, + }); + return content; + }) + .catch((e) => { + options?.onAfterTool?.({ ...tool, isError: true }); + return e.toString(); + }) + .then((content) => ({ + role: "tool", + content, + tool_call_id: tool.id, + })); + }), + ).then((toolCallResult) => { + console.log("end runTools", toolCallMessage, toolCallResult); + // @ts-ignore + requestPayload?.messages?.splice( + // @ts-ignore + requestPayload?.messages?.length, + 0, + toolCallMessage, + ...toolCallResult, + ); + setTimeout(() => { + // call again + console.log("start again"); + running = false; + chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource + }, 60); + }); + console.log("try run tools", runTools.length, finished); + return; + } + if (running) { + return; + } + finished = true; + options.onFinish(responseText + remainText); + } + }; + + controller.signal.onabort = finish; + + function chatApi( + chatPath: string, + headers: any, + requestPayload: any, + tools: any, + ) { + const chatPayload = { + method: "POST", + body: JSON.stringify({ + ...requestPayload, + tools, + }), + signal: controller.signal, + headers, + }; + const requestTimeoutId = setTimeout( + () => controller.abort(), + REQUEST_TIMEOUT_MS, + ); + fetchEventSource(chatPath, { + ...chatPayload, + async onopen(res) { + clearTimeout(requestTimeoutId); + const contentType = res.headers.get("content-type"); + console.log("[Request] response content type: ", contentType); + + if (contentType?.startsWith("text/plain")) { + responseText = await res.clone().text(); + return finish(); + } + + if ( + !res.ok || + !res.headers + .get("content-type") + ?.startsWith(EventStreamContentType) || + res.status !== 200 + ) { + const responseTexts = [responseText]; + let extraInfo = await res.clone().text(); + try { + const resJson = await res.clone().json(); + extraInfo = prettyObject(resJson); + } catch {} + + if (res.status === 401) { + responseTexts.push(Locale.Error.Unauthorized); + } + + if (extraInfo) { + responseTexts.push(extraInfo); + } + + responseText = responseTexts.join("\n\n"); + + return finish(); + } + }, + onmessage(msg) { + if (msg.data === "[DONE]" || finished) { + return finish(); + } + const text = msg.data; + try { + const chunk = parseSSE(msg.data, runTools); + if (chunk) { + remainText += chunk; + } + } catch (e) { + console.error("[Request] parse error", text, msg); + } + }, + onclose() { + finish(); + }, + onerror(e) { + options?.onError?.(e); + throw e; + }, + openWhenHidden: true, + }); + console.log("chatApi", chatPath, requestPayload, tools); + } + chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource +} From d2cb984cedf688e44d36a483871bd384009affb8 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 29 Aug 2024 17:28:15 +0800 Subject: [PATCH 15/79] add processToolMessage callback --- app/client/platforms/openai.ts | 21 ++++++++++++++++++--- app/utils/chat.ts | 22 +++++++++------------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index edac751b0..69dbad005 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -240,6 +240,7 @@ export class ChatGPTApi implements LLMApi { ); } if (shouldStream) { + // TODO mock tools and funcs const tools = [ { type: "function", @@ -278,8 +279,9 @@ export class ChatGPTApi implements LLMApi { tools, funcs, controller, + // parseSSE (text: string, runTools: ChatMessageTool[]) => { - console.log("parseSSE", text, runTools); + // console.log("parseSSE", text, runTools); const json = JSON.parse(text); const choices = json.choices as Array<{ delta: { @@ -306,10 +308,23 @@ export class ChatGPTApi implements LLMApi { runTools[index]["function"]["arguments"] += args; } } - - console.log("runTools", runTools); 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 { diff --git a/app/utils/chat.ts b/app/utils/chat.ts index 1289695b9..aa268a9fe 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -161,6 +161,11 @@ export function stream( funcs: any, controller: AbortController, parseSSE: (text: string, runTools: any[]) => string | undefined, + processToolMessage: ( + requestPayload: any, + toolCallMessage: any, + toolCallResult: any[], + ) => void, options: any, ) { let responseText = ""; @@ -196,7 +201,6 @@ export function stream( const finish = () => { if (!finished) { - console.log("try run tools", runTools.length, finished, running); if (!running && runTools.length > 0) { const toolCallMessage = { role: "assistant", @@ -233,28 +237,20 @@ export function stream( })); }), ).then((toolCallResult) => { - console.log("end runTools", toolCallMessage, toolCallResult); - // @ts-ignore - requestPayload?.messages?.splice( - // @ts-ignore - requestPayload?.messages?.length, - 0, - toolCallMessage, - ...toolCallResult, - ); + processToolMessage(requestPayload, toolCallMessage, toolCallResult); setTimeout(() => { // call again - console.log("start again"); + console.debug("[ChatAPI] restart"); running = false; chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource }, 60); }); - console.log("try run tools", runTools.length, finished); return; } if (running) { return; } + console.debug("[ChatAPI] end"); finished = true; options.onFinish(responseText + remainText); } @@ -343,7 +339,7 @@ export function stream( }, openWhenHidden: true, }); - console.log("chatApi", chatPath, requestPayload, tools); } + console.debug("[ChatAPI] start"); chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource } From 571ce11e5322dbad657b561e93b457c6531c3d35 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 29 Aug 2024 19:55:09 +0800 Subject: [PATCH 16/79] stash code --- app/client/platforms/openai.ts | 10 ++- app/components/chat.tsx | 79 +++++++++++++++++++- app/constant.ts | 1 + app/store/index.ts | 1 + app/store/plugin.ts | 127 +++++++++++++++++++++++++++++++++ 5 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 app/store/plugin.ts diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 69dbad005..b96e128f1 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -14,6 +14,7 @@ import { useAccessStore, useAppConfig, useChatStore, + usePluginStore, } from "@/app/store"; import { collectModelsWithDefaultModel } from "@/app/utils/model"; import { @@ -240,6 +241,11 @@ export class ChatGPTApi implements LLMApi { ); } if (shouldStream) { + const [tools1, funcs2] = usePluginStore + .getState() + .getAsTools(useChatStore.getState().currentSession().mask?.plugin); + console.log("getAsTools", tools1, funcs2); + // return // TODO mock tools and funcs const tools = [ { @@ -276,8 +282,8 @@ export class ChatGPTApi implements LLMApi { chatPath, requestPayload, getHeaders(), - tools, - funcs, + tools1, + funcs2, controller, // parseSSE (text: string, runTools: ChatMessageTool[]) => { diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 2ad579aa5..9d3b86f4e 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -54,6 +54,7 @@ import { useAppConfig, DEFAULT_TOPIC, ModelType, + usePluginStore, } from "../store"; import { @@ -440,6 +441,71 @@ export function ChatActions(props: { const config = useAppConfig(); const navigate = useNavigate(); const chatStore = useChatStore(); + const pluginStore = usePluginStore(); + console.log("pluginStore", pluginStore.getAll()); + // test + if (pluginStore.getAll().length == 0) { + pluginStore.create({ + title: "Pet API", + version: "1.0.0", + content: `{ + "openapi": "3.0.2", + "info": { + "title": "Pet API", + "version": "1.0.0" + }, + "paths": { + "/api/pets": { + "get": { + "operationId": "getPets", + "description": "Returns all pets from the system that the user has access to", + "responses": { + "200": { + "description": "List of Pets", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "cat", + "dog" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "type" + ] + } + } + } +}`, + }); + } // switch themes const theme = config.theme; @@ -738,15 +804,22 @@ export function ChatActions(props: { title: Locale.Plugin.Artifacts, value: Plugin.Artifacts, }, - ]} + ].concat( + pluginStore + .getAll() + .map((item) => ({ + title: `${item.title}@${item.version}`, + value: item.id, + })), + )} onClose={() => setShowPluginSelector(false)} onSelection={(s) => { const plugin = s[0]; chatStore.updateCurrentSession((session) => { session.mask.plugin = s; }); - if (plugin) { - showToast(plugin); + if (s.includes(Plugin.Artifacts)) { + showToast(Plugin.Artifacts); } }} /> diff --git a/app/constant.ts b/app/constant.ts index e88d497ca..db365f0fc 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -78,6 +78,7 @@ export enum Plugin { export enum StoreKey { Chat = "chat-next-web-store", + Plugin = "chat-next-web-plugin", Access = "access-control", Config = "app-config", Mask = "mask-store", diff --git a/app/store/index.ts b/app/store/index.ts index 0760f48ca..122afd5d3 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -2,3 +2,4 @@ export * from "./chat"; export * from "./update"; export * from "./access"; export * from "./config"; +export * from "./plugin"; diff --git a/app/store/plugin.ts b/app/store/plugin.ts new file mode 100644 index 000000000..d93044c4d --- /dev/null +++ b/app/store/plugin.ts @@ -0,0 +1,127 @@ +import OpenAPIClientAxios from "openapi-client-axios"; +import { getLang, Lang } from "../locales"; +import { StoreKey, Plugin } from "../constant"; +import { nanoid } from "nanoid"; +import { createPersistStore } from "../utils/store"; +import yaml from "js-yaml"; + +export type Plugin = { + id: string; + createdAt: number; + title: string; + version: string; + context: string; + builtin: boolean; +}; + +export const createEmptyPlugin = () => + ({ + id: nanoid(), + title: "", + version: "", + context: "", + builtin: false, + createdAt: Date.now(), + }) as Plugin; + +export const DEFAULT_PLUGIN_STATE = { + plugins: {} as Record, +}; + +export const usePluginStore = createPersistStore( + { ...DEFAULT_PLUGIN_STATE }, + + (set, get) => ({ + create(plugin?: Partial) { + const plugins = get().plugins; + const id = nanoid(); + plugins[id] = { + ...createEmptyPlugin(), + ...plugin, + id, + builtin: false, + }; + + set(() => ({ plugins })); + get().markUpdate(); + + return plugins[id]; + }, + updatePlugin(id: string, updater: (plugin: Plugin) => void) { + const plugins = get().plugins; + const plugin = plugins[id]; + if (!plugin) return; + const updatePlugin = { ...plugin }; + updater(updatePlugin); + plugins[id] = updatePlugin; + set(() => ({ plugins })); + get().markUpdate(); + }, + delete(id: string) { + const plugins = get().plugins; + delete plugins[id]; + set(() => ({ plugins })); + get().markUpdate(); + }, + + getAsTools(ids: string[]) { + const plugins = get().plugins; + const selected = ids + .map((id) => plugins[id]) + .filter((i) => i) + .map((i) => [ + i, + new OpenAPIClientAxios({ definition: yaml.load(i.content) }), + ]) + .map(([item, api]) => { + api.initSync(); + const operations = api.getOperations().map((o) => { + const parameters = o.parameters; + return [ + { + type: "function", + function: { + name: o.operationId, + description: o.description, + parameters: o.parameters, + }, + }, + api.client[o.operationId], + ]; + // return [{ + // }, function(arg) { + // const args = [] + // for (const p in parameters) { + // if (p.type === "object") { + // const a = {} + // for (const n of p.) + // } + // } + // }] + }); + return [item, api, operations]; + }); + console.log("selected", selected); + const result = selected.reduce((s, i) => s.concat(i[2]), []); + return [ + result.map(([t, _]) => t), + result.reduce((s, i) => { + s[i[0].function.name] = i[1]; + return s; + }, {}), + ]; + }, + get(id?: string) { + return get().plugins[id ?? 1145141919810]; + }, + getAll() { + return Object.values(get().plugins).sort( + (a, b) => b.createdAt - a.createdAt, + ); + }, + }), + { + name: StoreKey.Plugin, + version: 1, + }, +); From 19c7a84548b55aa348e009611d4ac766e6b23af0 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Thu, 29 Aug 2024 20:48:04 +0800 Subject: [PATCH 17/79] fix: right click --- app/components/chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index ed5b06799..5be8b4d3d 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1587,7 +1587,7 @@ function _Chat() { message.content.length === 0 && !isUser } - onContextMenu={(e) => onRightClick(e, message)} + // onContextMenu={(e) => onRightClick(e, message)} // hard to use onDoubleClickCapture={() => { if (!isMobileScreen) return; setUserInput(getMessageTextContent(message)); From cac99e390855482c782d2f11af81c2c595164190 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 30 Aug 2024 13:02:03 +0800 Subject: [PATCH 18/79] add Plugin page --- app/components/home.tsx | 5 ++ app/components/plugin.tsx | 149 ++++++++++++++++++++++++++++++++++++++ app/constant.ts | 2 + app/locales/en.ts | 41 +++++++++++ 4 files changed, 197 insertions(+) create mode 100644 app/components/plugin.tsx diff --git a/app/components/home.tsx b/app/components/home.tsx index 24e71b9e5..465ad0f1e 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -59,6 +59,10 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, { loading: () => , }); +const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, { + loading: () => , +}); + const SearchChat = dynamic( async () => (await import("./search-chat")).SearchChatPage, { @@ -181,6 +185,7 @@ function Screen() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx new file mode 100644 index 000000000..769e02f2a --- /dev/null +++ b/app/components/plugin.tsx @@ -0,0 +1,149 @@ +import { IconButton } from "./button"; +import { ErrorBoundary } from "./error"; + +import styles from "./mask.module.scss"; + +import DownloadIcon from "../icons/download.svg"; +import EditIcon from "../icons/edit.svg"; +import AddIcon from "../icons/add.svg"; +import CloseIcon from "../icons/close.svg"; +import DeleteIcon from "../icons/delete.svg"; +import EyeIcon from "../icons/eye.svg"; +import CopyIcon from "../icons/copy.svg"; + +import { Plugin, usePluginStore } from "../store/plugin"; +import { + Input, + List, + ListItem, + Modal, + Popover, + Select, + showConfirm, +} from "./ui-lib"; +import Locale from "../locales"; +import { useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { Path } from "../constant"; +import { nanoid } from "nanoid"; + +export function PluginPage() { + const navigate = useNavigate(); + const pluginStore = usePluginStore(); + const plugins = pluginStore.getAll(); + + const [editingPluginId, setEditingPluginId] = useState(); + const editingPlugin = pluginStore.get(editingPluginId); + const closePluginModal = () => setEditingPluginId(undefined); + + return ( + +
+
+
+
+ {Locale.Plugin.Page.Title} +
+
+ {Locale.Plugin.Page.SubTitle(plugins.length)} +
+
+ +
+
+ } + bordered + onClick={() => navigate(-1)} + /> +
+
+
+ +
+
+ {plugins.map((m) => ( +
+
+
+
+
+ {m.title}@{m.version} +
+
+ {`${Locale.Plugin.Item.Info(m.content.length)} / / `} +
+
+
+
+ {m.builtin ? ( + } + text={Locale.Plugin.Item.View} + onClick={() => setEditingPluginId(m.id)} + /> + ) : ( + } + text={Locale.Plugin.Item.Edit} + onClick={() => setEditingPluginId(m.id)} + /> + )} + {!m.builtin && ( + } + text={Locale.Plugin.Item.Delete} + onClick={async () => { + if ( + await showConfirm(Locale.Plugin.Item.DeleteConfirm) + ) { + pluginStore.delete(m.id); + } + }} + /> + )} +
+
+ ))} +
+
+
+ + {editingPlugin && ( +
+ } + text={Locale.Plugin.EditModal.Download} + key="export" + bordered + onClick={() => + downloadAs( + JSON.stringify(editingPlugin), + `${editingPlugin.name}.json`, + ) + } + />, + } + bordered + text={Locale.Plugin.EditModal.Clone} + onClick={() => { + navigate(Path.Plugins); + pluginStore.create(editingPlugin); + setEditingPluginId(undefined); + }} + />, + ]} + > + PluginConfig + +
+ )} +
+ ); +} diff --git a/app/constant.ts b/app/constant.ts index db365f0fc..0fa14dca5 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -39,6 +39,7 @@ export enum Path { Settings = "/settings", NewChat = "/new-chat", Masks = "/masks", + Plugins = "/plugins", Auth = "/auth", Sd = "/sd", SdNew = "/sd-new", @@ -480,6 +481,7 @@ export const internalAllowedWebDavEndpoints = [ export const DEFAULT_GA_ID = "G-89WN60ZK2E"; export const PLUGINS = [ + { name: "Plugins", path: Path.Plugins }, { name: "Stable Diffusion", path: Path.Sd }, { name: "Search Chat", path: Path.SearchChat }, ]; diff --git a/app/locales/en.ts b/app/locales/en.ts index 77f3a700a..ea098c0f3 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -542,6 +542,47 @@ const en: LocaleType = { View: "View", }, }, + Plugin: { + Name: "Plugin", + Page: { + Title: "Plugins", + SubTitle: (count: number) => `${count} plugins`, + Search: "Search Plugin", + Create: "Create", + }, + Item: { + Info: (count: number) => `${count} plugins`, + Chat: "Chat", + View: "View", + Edit: "Edit", + Delete: "Delete", + DeleteConfirm: "Confirm to delete?", + }, + EditModal: { + Title: (readonly: boolean) => + `Edit Plugin ${readonly ? "(readonly)" : ""}`, + Download: "Download", + Clone: "Clone", + }, + Config: { + Avatar: "Bot Avatar", + Name: "Bot Name", + Sync: { + Title: "Use Global Config", + SubTitle: "Use global config in this chat", + Confirm: "Confirm to override custom config with global config?", + }, + HideContext: { + Title: "Hide Context Prompts", + SubTitle: "Do not show in-context prompts in chat", + }, + Share: { + Title: "Share This Plugin", + SubTitle: "Generate a link to this mask", + Action: "Copy Link", + }, + }, + }, Mask: { Name: "Mask", Page: { From 271f58d9cf1219cfe74b041ad90b162f2c519a53 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 30 Aug 2024 17:31:20 +0800 Subject: [PATCH 19/79] stash code --- app/client/platforms/openai.ts | 41 +--------- app/components/chat.tsx | 74 +---------------- app/components/plugin.module.scss | 15 ++++ app/components/plugin.tsx | 129 ++++++++++++++++++++++++++---- app/locales/cn.ts | 28 ++++++- app/locales/en.ts | 30 ++----- app/store/plugin.ts | 123 ++++++++++++++++++---------- 7 files changed, 246 insertions(+), 194 deletions(-) create mode 100644 app/components/plugin.module.scss diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index b96e128f1..f0c577c75 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -241,49 +241,16 @@ export class ChatGPTApi implements LLMApi { ); } if (shouldStream) { - const [tools1, funcs2] = usePluginStore + const [tools, funcs] = usePluginStore .getState() .getAsTools(useChatStore.getState().currentSession().mask?.plugin); - console.log("getAsTools", tools1, funcs2); - // return - // TODO mock tools and funcs - const tools = [ - { - type: "function", - function: { - name: "get_current_weather", - description: "Get the current weather", - parameters: { - type: "object", - properties: { - location: { - type: "string", - description: "The city and country, eg. San Francisco, USA", - }, - format: { - type: "string", - enum: ["celsius", "fahrenheit"], - }, - }, - required: ["location", "format"], - }, - }, - }, - ]; - const funcs = { - get_current_weather: (args: any) => { - console.log("call get_current_weather", args); - return new Promise((resolve) => { - setTimeout(() => resolve("30"), 3000); - }); - }, - }; + console.log("getAsTools", tools, funcs); stream( chatPath, requestPayload, getHeaders(), - tools1, - funcs2, + tools, + funcs, controller, // parseSSE (text: string, runTools: ChatMessageTool[]) => { diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 9d3b86f4e..0e2aa3e2f 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -442,70 +442,6 @@ export function ChatActions(props: { const navigate = useNavigate(); const chatStore = useChatStore(); const pluginStore = usePluginStore(); - console.log("pluginStore", pluginStore.getAll()); - // test - if (pluginStore.getAll().length == 0) { - pluginStore.create({ - title: "Pet API", - version: "1.0.0", - content: `{ - "openapi": "3.0.2", - "info": { - "title": "Pet API", - "version": "1.0.0" - }, - "paths": { - "/api/pets": { - "get": { - "operationId": "getPets", - "description": "Returns all pets from the system that the user has access to", - "responses": { - "200": { - "description": "List of Pets", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pet" - } - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "Pet": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "cat", - "dog" - ] - }, - "name": { - "type": "string" - } - }, - "required": [ - "id", - "type" - ] - } - } - } -}`, - }); - } // switch themes const theme = config.theme; @@ -805,12 +741,10 @@ export function ChatActions(props: { value: Plugin.Artifacts, }, ].concat( - pluginStore - .getAll() - .map((item) => ({ - title: `${item.title}@${item.version}`, - value: item.id, - })), + pluginStore.getAll().map((item) => ({ + title: `${item.title}@${item.version}`, + value: item.id, + })), )} onClose={() => setShowPluginSelector(false)} onSelection={(s) => { diff --git a/app/components/plugin.module.scss b/app/components/plugin.module.scss new file mode 100644 index 000000000..53c632468 --- /dev/null +++ b/app/components/plugin.module.scss @@ -0,0 +1,15 @@ +.plugin-title { + font-weight: bolder; + font-size: 16px; + margin: 10px 0; +} +.plugin-content { + font-size: 14px; + font-family: inherit; + pre code { + max-height: 240px; + overflow-y: auto; + white-space: pre-wrap; + } +} + diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx index 769e02f2a..247cba257 100644 --- a/app/components/plugin.tsx +++ b/app/components/plugin.tsx @@ -1,7 +1,11 @@ +import { useDebouncedCallback } from "use-debounce"; +import OpenAPIClientAxios from "openapi-client-axios"; +import yaml from "js-yaml"; import { IconButton } from "./button"; import { ErrorBoundary } from "./error"; import styles from "./mask.module.scss"; +import pluginStyles from "./plugin.module.scss"; import DownloadIcon from "../icons/download.svg"; import EditIcon from "../icons/edit.svg"; @@ -11,7 +15,7 @@ import DeleteIcon from "../icons/delete.svg"; import EyeIcon from "../icons/eye.svg"; import CopyIcon from "../icons/copy.svg"; -import { Plugin, usePluginStore } from "../store/plugin"; +import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin"; import { Input, List, @@ -20,7 +24,9 @@ import { Popover, Select, showConfirm, + showToast, } from "./ui-lib"; +import { downloadAs } from "../utils"; import Locale from "../locales"; import { useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; @@ -30,12 +36,56 @@ import { nanoid } from "nanoid"; export function PluginPage() { const navigate = useNavigate(); const pluginStore = usePluginStore(); - const plugins = pluginStore.getAll(); + + const allPlugins = pluginStore.getAll(); + const [searchPlugins, setSearchPlugins] = useState([]); + const [searchText, setSearchText] = useState(""); + const plugins = searchText.length > 0 ? searchPlugins : allPlugins; + + // refactored already, now it accurate + const onSearch = (text: string) => { + setSearchText(text); + if (text.length > 0) { + const result = allPlugins.filter((m) => + m.title.toLowerCase().includes(text.toLowerCase()), + ); + setSearchPlugins(result); + } else { + setSearchPlugins(allPlugins); + } + }; const [editingPluginId, setEditingPluginId] = useState(); const editingPlugin = pluginStore.get(editingPluginId); + const editingPluginTool = FunctionToolService.get(editingPlugin?.id); const closePluginModal = () => setEditingPluginId(undefined); + const onChangePlugin = useDebouncedCallback((editingPlugin, e) => { + const content = e.target.innerText; + try { + const api = new OpenAPIClientAxios({ definition: yaml.load(content) }); + api + .init() + .then(() => { + if (content != editingPlugin.content) { + pluginStore.updatePlugin(editingPlugin.id, (plugin) => { + plugin.content = content; + const tool = FunctionToolService.add(plugin, true); + plugin.title = tool.api.definition.info.title; + plugin.version = tool.api.definition.info.version; + }); + } + }) + .catch((e) => { + console.error(e); + showToast(Locale.Plugin.EditModal.Error); + }); + } catch (e) { + console.error(e); + showToast(Locale.Plugin.EditModal.Error); + } + }, 100).bind(null, editingPlugin); + return (
@@ -61,6 +111,27 @@ export function PluginPage() {
+
+ onSearch(e.currentTarget.value)} + /> + + } + text={Locale.Plugin.Page.Create} + bordered + onClick={() => { + const createdPlugin = pluginStore.create(); + setEditingPluginId(createdPlugin.id); + }} + /> +
+
{plugins.map((m) => (
@@ -71,7 +142,9 @@ export function PluginPage() { {m.title}@{m.version}
- {`${Locale.Plugin.Item.Info(m.content.length)} / / `} + {Locale.Plugin.Item.Info( + FunctionToolService.add(m).length, + )}
@@ -123,24 +196,48 @@ export function PluginPage() { onClick={() => downloadAs( JSON.stringify(editingPlugin), - `${editingPlugin.name}.json`, + `${editingPlugin.title}@${editingPlugin.version}.json`, ) } />, - } - bordered - text={Locale.Plugin.EditModal.Clone} - onClick={() => { - navigate(Path.Plugins); - pluginStore.create(editingPlugin); - setEditingPluginId(undefined); - }} - />, ]} > - PluginConfig +
+
+ {Locale.Plugin.EditModal.Content} +
+
+
+                  
+                
+
+
+ {Locale.Plugin.EditModal.Method} +
+
+ {editingPluginTool?.tools.map((tool, index) => ( +
+
+
+
+ {tool?.function?.name} +
+
+ {tool?.function?.description} +
+
+
+
+ ))} +
+
)} diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 9a3227d68..f0ff705c1 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -509,10 +509,6 @@ const cn = { Clear: "上下文已清除", Revert: "恢复上下文", }, - Plugin: { - Name: "插件", - Artifacts: "Artifacts", - }, Discovery: { Name: "发现", }, @@ -534,6 +530,30 @@ const cn = { View: "查看", }, }, + Plugin: { + Name: "插件", + Artifacts: "Artifacts", + Page: { + Title: "插件", + SubTitle: (count: number) => `${count} 个插件`, + Search: "搜索插件", + Create: "新建", + }, + Item: { + Info: (count: number) => `${count} 方法`, + View: "查看", + Edit: "编辑", + Delete: "删除", + DeleteConfirm: "确认删除?", + }, + EditModal: { + Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`, + Download: "下载", + Content: "OpenAPI Schema", + Method: "方法", + Error: "格式错误", + }, + }, Mask: { Name: "面具", Page: { diff --git a/app/locales/en.ts b/app/locales/en.ts index ea098c0f3..15db8190a 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -517,10 +517,6 @@ const en: LocaleType = { Clear: "Context Cleared", Revert: "Revert", }, - Plugin: { - Name: "Plugin", - Artifacts: "Artifacts", - }, Discovery: { Name: "Discovery", }, @@ -544,6 +540,7 @@ const en: LocaleType = { }, Plugin: { Name: "Plugin", + Artifacts: "Artifacts", Page: { Title: "Plugins", SubTitle: (count: number) => `${count} plugins`, @@ -551,8 +548,7 @@ const en: LocaleType = { Create: "Create", }, Item: { - Info: (count: number) => `${count} plugins`, - Chat: "Chat", + Info: (count: number) => `${count} method`, View: "View", Edit: "Edit", Delete: "Delete", @@ -562,25 +558,9 @@ const en: LocaleType = { Title: (readonly: boolean) => `Edit Plugin ${readonly ? "(readonly)" : ""}`, Download: "Download", - Clone: "Clone", - }, - Config: { - Avatar: "Bot Avatar", - Name: "Bot Name", - Sync: { - Title: "Use Global Config", - SubTitle: "Use global config in this chat", - Confirm: "Confirm to override custom config with global config?", - }, - HideContext: { - Title: "Hide Context Prompts", - SubTitle: "Do not show in-context prompts in chat", - }, - Share: { - Title: "Share This Plugin", - SubTitle: "Generate a link to this mask", - Action: "Copy Link", - }, + Content: "OpenAPI Schema", + Method: "Method", + Error: "OpenAPI Schema Error", }, }, Mask: { diff --git a/app/store/plugin.ts b/app/store/plugin.ts index d93044c4d..b25b162a3 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -10,16 +10,91 @@ export type Plugin = { createdAt: number; title: string; version: string; - context: string; + content: string; builtin: boolean; }; +export type FunctionToolItem = { + type: string; + function: { + name: string; + description?: string; + parameters: Object; + }; +}; + +type FunctionToolServiceItem = { + api: OpenAPIClientAxios; + tools: FunctionToolItem[]; + funcs: Function[]; +}; + +export const FunctionToolService = { + tools: {} as Record, + add(plugin: Plugin, replace = false) { + if (!replace && this.tools[plugin.id]) return this.tools[plugin.id]; + const api = new OpenAPIClientAxios({ + definition: yaml.load(plugin.content), + }); + console.log("add", plugin, api); + try { + api.initSync(); + } catch (e) {} + const operations = api.getOperations(); + return (this.tools[plugin.id] = { + api, + length: operations.length, + tools: operations.map((o) => { + const parameters = o?.requestBody?.content["application/json"] + ?.schema || { + type: "object", + properties: {}, + }; + if (!parameters["required"]) { + parameters["required"] = []; + } + if (o.parameters instanceof Array) { + o.parameters.forEach((p) => { + if (p.in == "query" || p.in == "path") { + // const name = `${p.in}__${p.name}` + const name = p.name; + console.log("p", p, p.schema); + parameters["properties"][name] = { + type: p.schema.type, + description: p.description, + }; + if (p.required) { + parameters["required"].push(name); + } + } + }); + } + return { + type: "function", + function: { + name: o.operationId, + description: o.description, + parameters: parameters, + }, + }; + }), + funcs: operations.reduce((s, o) => { + s[o.operationId] = api.client[o.operationId]; + return s; + }, {}), + }); + }, + get(id) { + return this.tools[id]; + }, +}; + export const createEmptyPlugin = () => ({ id: nanoid(), title: "", - version: "", - context: "", + version: "1.0.0", + content: "", builtin: false, createdAt: Date.now(), }) as Plugin; @@ -69,46 +144,10 @@ export const usePluginStore = createPersistStore( const selected = ids .map((id) => plugins[id]) .filter((i) => i) - .map((i) => [ - i, - new OpenAPIClientAxios({ definition: yaml.load(i.content) }), - ]) - .map(([item, api]) => { - api.initSync(); - const operations = api.getOperations().map((o) => { - const parameters = o.parameters; - return [ - { - type: "function", - function: { - name: o.operationId, - description: o.description, - parameters: o.parameters, - }, - }, - api.client[o.operationId], - ]; - // return [{ - // }, function(arg) { - // const args = [] - // for (const p in parameters) { - // if (p.type === "object") { - // const a = {} - // for (const n of p.) - // } - // } - // }] - }); - return [item, api, operations]; - }); - console.log("selected", selected); - const result = selected.reduce((s, i) => s.concat(i[2]), []); + .map((p) => FunctionToolService.add(p)); return [ - result.map(([t, _]) => t), - result.reduce((s, i) => { - s[i[0].function.name] = i[1]; - return s; - }, {}), + selected.reduce((s, i) => s.concat(i.tools), []), + selected.reduce((s, i) => Object.assign(s, i.funcs), {}), ]; }, get(id?: string) { From 9326ff9d0873e2fe684dbd5df0eccdc3e1ae3403 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 30 Aug 2024 23:39:08 +0800 Subject: [PATCH 20/79] ts error --- app/client/platforms/openai.ts | 6 ++++-- app/components/chat.tsx | 17 +++++++++-------- app/components/markdown.tsx | 4 ++-- app/components/plugin.tsx | 8 +++++--- app/constant.ts | 2 +- app/store/mask.ts | 6 +++--- app/store/plugin.ts | 24 ++++++++++++++++-------- app/utils/chat.ts | 2 +- package.json | 1 + yarn.lock | 5 +++++ 10 files changed, 47 insertions(+), 28 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index f0c577c75..4c5831fe3 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -243,13 +243,15 @@ export class ChatGPTApi implements LLMApi { if (shouldStream) { const [tools, funcs] = usePluginStore .getState() - .getAsTools(useChatStore.getState().currentSession().mask?.plugin); + .getAsTools( + useChatStore.getState().currentSession().mask?.plugin as string[], + ); console.log("getAsTools", tools, funcs); stream( chatPath, requestPayload, getHeaders(), - tools, + tools as any, funcs, controller, // parseSSE diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 0e2aa3e2f..7bac62bc4 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -97,7 +97,7 @@ import { REQUEST_TIMEOUT_MS, UNFINISHED_INPUT, ServiceProvider, - Plugin, + ArtifactsPlugin, } from "../constant"; import { Avatar } from "./emoji"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; @@ -738,22 +738,23 @@ export function ChatActions(props: { items={[ { title: Locale.Plugin.Artifacts, - value: Plugin.Artifacts, + value: ArtifactsPlugin.Artifacts as string, }, ].concat( pluginStore.getAll().map((item) => ({ - title: `${item.title}@${item.version}`, - value: item.id, + // @ts-ignore + title: `${item?.title}@${item?.version}`, + // @ts-ignore + value: item?.id, })), )} onClose={() => setShowPluginSelector(false)} onSelection={(s) => { - const plugin = s[0]; chatStore.updateCurrentSession((session) => { - session.mask.plugin = s; + session.mask.plugin = s as string[]; }); - if (s.includes(Plugin.Artifacts)) { - showToast(Plugin.Artifacts); + if (s.includes(ArtifactsPlugin.Artifacts)) { + showToast(ArtifactsPlugin.Artifacts); } }} /> diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 500af7175..58579ab47 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -19,7 +19,7 @@ import { HTMLPreview, HTMLPreviewHander, } from "./artifacts"; -import { Plugin } from "../constant"; +import { ArtifactsPlugin } from "../constant"; import { useChatStore } from "../store"; import { IconButton } from "./button"; @@ -95,7 +95,7 @@ export function PreCode(props: { children: any }) { }, 600); const enableArtifacts = useMemo( - () => plugins?.includes(Plugin.Artifacts), + () => plugins?.includes(ArtifactsPlugin.Artifacts), [plugins], ); diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx index 247cba257..35cda9089 100644 --- a/app/components/plugin.tsx +++ b/app/components/plugin.tsx @@ -46,8 +46,8 @@ export function PluginPage() { const onSearch = (text: string) => { setSearchText(text); if (text.length > 0) { - const result = allPlugins.filter((m) => - m.title.toLowerCase().includes(text.toLowerCase()), + const result = allPlugins.filter( + (m) => m?.title.toLowerCase().includes(text.toLowerCase()), ); setSearchPlugins(result); } else { @@ -63,7 +63,9 @@ export function PluginPage() { const onChangePlugin = useDebouncedCallback((editingPlugin, e) => { const content = e.target.innerText; try { - const api = new OpenAPIClientAxios({ definition: yaml.load(content) }); + const api = new OpenAPIClientAxios({ + definition: yaml.load(content) as any, + }); api .init() .then(() => { diff --git a/app/constant.ts b/app/constant.ts index 0fa14dca5..f6026d671 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -73,7 +73,7 @@ export enum FileName { Prompts = "prompts.json", } -export enum Plugin { +export enum ArtifactsPlugin { Artifacts = "artifacts", } diff --git a/app/store/mask.ts b/app/store/mask.ts index a790f89f8..05f511b0f 100644 --- a/app/store/mask.ts +++ b/app/store/mask.ts @@ -2,7 +2,7 @@ import { BUILTIN_MASKS } from "../masks"; import { getLang, Lang } from "../locales"; import { DEFAULT_TOPIC, ChatMessage } from "./chat"; import { ModelConfig, useAppConfig } from "./config"; -import { StoreKey, Plugin } from "../constant"; +import { StoreKey, ArtifactsPlugin } from "../constant"; import { nanoid } from "nanoid"; import { createPersistStore } from "../utils/store"; @@ -17,7 +17,7 @@ export type Mask = { modelConfig: ModelConfig; lang: Lang; builtin: boolean; - plugin?: Plugin[]; + plugin?: string[]; }; export const DEFAULT_MASK_STATE = { @@ -38,7 +38,7 @@ export const createEmptyMask = () => lang: getLang(), builtin: false, createdAt: Date.now(), - plugin: [Plugin.Artifacts], + plugin: [ArtifactsPlugin.Artifacts as string], }) as Mask; export const useMaskStore = createPersistStore( diff --git a/app/store/plugin.ts b/app/store/plugin.ts index b25b162a3..031a2aaf5 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -1,6 +1,6 @@ import OpenAPIClientAxios from "openapi-client-axios"; import { getLang, Lang } from "../locales"; -import { StoreKey, Plugin } from "../constant"; +import { StoreKey } from "../constant"; import { nanoid } from "nanoid"; import { createPersistStore } from "../utils/store"; import yaml from "js-yaml"; @@ -25,8 +25,9 @@ export type FunctionToolItem = { type FunctionToolServiceItem = { api: OpenAPIClientAxios; + length: number; tools: FunctionToolItem[]; - funcs: Function[]; + funcs: Record; }; export const FunctionToolService = { @@ -34,7 +35,7 @@ export const FunctionToolService = { add(plugin: Plugin, replace = false) { if (!replace && this.tools[plugin.id]) return this.tools[plugin.id]; const api = new OpenAPIClientAxios({ - definition: yaml.load(plugin.content), + definition: yaml.load(plugin.content) as any, }); console.log("add", plugin, api); try { @@ -45,6 +46,7 @@ export const FunctionToolService = { api, length: operations.length, tools: operations.map((o) => { + // @ts-ignore const parameters = o?.requestBody?.content["application/json"] ?.schema || { type: "object", @@ -55,14 +57,18 @@ export const FunctionToolService = { } if (o.parameters instanceof Array) { o.parameters.forEach((p) => { - if (p.in == "query" || p.in == "path") { + // @ts-ignore + if (p?.in == "query" || p?.in == "path") { // const name = `${p.in}__${p.name}` - const name = p.name; - console.log("p", p, p.schema); + // @ts-ignore + const name = p?.name; parameters["properties"][name] = { + // @ts-ignore type: p.schema.type, + // @ts-ignore description: p.description, }; + // @ts-ignore if (p.required) { parameters["required"].push(name); } @@ -76,15 +82,16 @@ export const FunctionToolService = { description: o.description, parameters: parameters, }, - }; + } as FunctionToolItem; }), funcs: operations.reduce((s, o) => { + // @ts-ignore s[o.operationId] = api.client[o.operationId]; return s; }, {}), }); }, - get(id) { + get(id: string) { return this.tools[id]; }, }; @@ -146,6 +153,7 @@ export const usePluginStore = createPersistStore( .filter((i) => i) .map((p) => FunctionToolService.add(p)); return [ + // @ts-ignore selected.reduce((s, i) => s.concat(i.tools), []), selected.reduce((s, i) => Object.assign(s, i.funcs), {}), ]; diff --git a/app/utils/chat.ts b/app/utils/chat.ts index aa268a9fe..454a24771 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -158,7 +158,7 @@ export function stream( requestPayload: any, headers: any, tools: any[], - funcs: any, + funcs: Record, controller: AbortController, parseSSE: (text: string, runTools: any[]) => string | undefined, processToolMessage: ( diff --git a/package.json b/package.json index 6907bbf45..82f23a4a0 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@types/react-dom": "^18.2.7", "@types/react-katex": "^3.0.0", "@types/spark-md5": "^3.0.4", + "@types/js-yaml": "4.0.9", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "eslint": "^8.49.0", diff --git a/yarn.lock b/yarn.lock index 6d8c07cc1..f138eb957 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1684,6 +1684,11 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" +"@types/js-yaml@4.0.9": + version "4.0.9" + resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" + integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== + "@types/json-schema@*", "@types/json-schema@^7.0.8": version "7.0.12" resolved "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" From 2214689920f1531a6701171d77cc33c342d35f83 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 30 Aug 2024 23:51:03 +0800 Subject: [PATCH 21/79] add gapier proxy --- next.config.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/next.config.mjs b/next.config.mjs index 27c60dd29..6b1dd0c35 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -86,6 +86,10 @@ if (mode !== "export") { source: "/api/proxy/anthropic/:path*", destination: "https://api.anthropic.com/:path*", }, + { + source: "/api/proxy/gapier/:path*", + destination: "https://a.gapier.com/:path*", + }, { source: "/google-fonts/:path*", destination: "https://fonts.googleapis.com/:path*", From b2965e1deb48f9e63aabecb1d477185d22f19e1f Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sat, 31 Aug 2024 00:16:47 +0800 Subject: [PATCH 22/79] update --- app/utils/chat.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/utils/chat.ts b/app/utils/chat.ts index 454a24771..d8ab5770c 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -218,6 +218,13 @@ export function stream( JSON.parse(tool.function.arguments), ), ) + .then((res) => { + const content = JSON.stringify(res.data); + if (res.status >= 300) { + return Promise.reject(content); + } + return content; + }) .then((content) => { options?.onAfterTool?.({ ...tool, From f652f7326070dfbffcc5f90a999e95df21f4c650 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 2 Sep 2024 18:11:19 +0800 Subject: [PATCH 23/79] plugin add auth config --- app/api/[provider]/[...path]/route.ts | 6 +- app/api/proxy.ts | 75 +++++++++++++++ app/components/plugin.tsx | 133 ++++++++++++++++++-------- app/components/ui-lib.tsx | 2 +- app/locales/cn.ts | 11 +++ app/locales/en.ts | 11 +++ app/store/plugin.ts | 46 ++++++++- next.config.mjs | 4 - 8 files changed, 237 insertions(+), 51 deletions(-) create mode 100644 app/api/proxy.ts diff --git a/app/api/[provider]/[...path]/route.ts b/app/api/[provider]/[...path]/route.ts index 06e3e5160..24aa5ec04 100644 --- a/app/api/[provider]/[...path]/route.ts +++ b/app/api/[provider]/[...path]/route.ts @@ -10,6 +10,8 @@ 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 proxyHandler } from "../../proxy"; + async function handle( req: NextRequest, { params }: { params: { provider: string; path: string[] } }, @@ -36,8 +38,10 @@ async function handle( return stabilityHandler(req, { params }); case ApiPath.Iflytek: return iflytekHandler(req, { params }); - default: + case ApiPath.OpenAI: return openaiHandler(req, { params }); + default: + return proxyHandler(req, { params }); } } diff --git a/app/api/proxy.ts b/app/api/proxy.ts new file mode 100644 index 000000000..731003aa1 --- /dev/null +++ b/app/api/proxy.ts @@ -0,0 +1,75 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[Proxy Route] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + // remove path params from searchParams + req.nextUrl.searchParams.delete("path"); + req.nextUrl.searchParams.delete("provider"); + + const subpath = params.path.join("/"); + const fetchUrl = `${req.headers.get( + "x-base-url", + )}/${subpath}?${req.nextUrl.searchParams.toString()}`; + const skipHeaders = ["connection", "host", "origin", "referer", "cookie"]; + const headers = new Headers( + Array.from(req.headers.entries()).filter((item) => { + if ( + item[0].indexOf("x-") > -1 || + item[0].indexOf("sec-") > -1 || + skipHeaders.includes(item[0]) + ) { + return false; + } + return true; + }), + ); + const controller = new AbortController(); + const fetchOptions: RequestInit = { + headers, + method: req.method, + body: req.body, + // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body + redirect: "manual", + // @ts-ignore + duplex: "half", + signal: controller.signal, + }; + + const timeoutId = setTimeout( + () => { + controller.abort(); + }, + 10 * 60 * 1000, + ); + + 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"); + + // The latest version of the OpenAI API forced the content-encoding to be "br" in json response + // So if the streaming is disabled, we need to remove the content-encoding header + // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header + // The browser will try to decode the response with brotli and fail + newHeaders.delete("content-encoding"); + + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: newHeaders, + }); + } finally { + clearTimeout(timeoutId); + } +} diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx index 35cda9089..5aa66ce94 100644 --- a/app/components/plugin.tsx +++ b/app/components/plugin.tsx @@ -14,10 +14,12 @@ import CloseIcon from "../icons/close.svg"; import DeleteIcon from "../icons/delete.svg"; import EyeIcon from "../icons/eye.svg"; import CopyIcon from "../icons/copy.svg"; +import ConfirmIcon from "../icons/confirm.svg"; import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin"; import { Input, + PasswordInput, List, ListItem, Modal, @@ -191,55 +193,102 @@ export function PluginPage() { onClose={closePluginModal} actions={[ } - text={Locale.Plugin.EditModal.Download} + icon={} + text={Locale.UI.Confirm} key="export" bordered - onClick={() => - downloadAs( - JSON.stringify(editingPlugin), - `${editingPlugin.title}@${editingPlugin.version}.json`, - ) - } + onClick={() => setEditingPluginId("")} />, ]} > -
-
- {Locale.Plugin.EditModal.Content} -
-
+ + + + {editingPlugin.authType == "custom" && ( + + { + pluginStore.updatePlugin(editingPlugin.id, (plugin) => { + plugin.authHeader = e.target.value; + }); + }} + > + + )} + {["bearer", "basic", "custom"].includes( + editingPlugin.authType as string, + ) && ( + + { + pluginStore.updatePlugin(editingPlugin.id, (plugin) => { + plugin.authToken = e.currentTarget.value; + }); + }} + > + + )} + -
-                  
-                
-
-
- {Locale.Plugin.EditModal.Method} -
-
- {editingPluginTool?.tools.map((tool, index) => ( -
-
-
-
- {tool?.function?.name} -
-
- {tool?.function?.description} -
-
-
+ { + pluginStore.updatePlugin(editingPlugin.id, (plugin) => { + plugin.usingProxy = e.currentTarget.checked; + }); + }} + > + + + + +
+                      
+                    
- ))} -
-
+ } + > + {editingPluginTool?.tools.map((tool, index) => ( + + ))} + )} diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index fd78f9c47..828c9a27d 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -51,7 +51,7 @@ export function Card(props: { children: JSX.Element[]; className?: string }) { export function ListItem(props: { title: string; - subTitle?: string; + subTitle?: string | JSX.Element; children?: JSX.Element | JSX.Element[]; icon?: JSX.Element; className?: string; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index f0ff705c1..af600564e 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -546,9 +546,20 @@ const cn = { Delete: "删除", DeleteConfirm: "确认删除?", }, + Auth: { + None: "不需要授权", + Basic: "Basic", + Bearer: "Bearer", + Custom: "自定义", + CustomHeader: "自定义头", + Token: "Token", + Proxy: "使用代理", + ProxyDescription: "使用代理解决 CORS 错误", + }, EditModal: { Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`, Download: "下载", + Auth: "授权方式", Content: "OpenAPI Schema", Method: "方法", Error: "格式错误", diff --git a/app/locales/en.ts b/app/locales/en.ts index 15db8190a..59a284006 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -554,10 +554,21 @@ const en: LocaleType = { Delete: "Delete", DeleteConfirm: "Confirm to delete?", }, + Auth: { + None: "None", + Basic: "Basic", + Bearer: "Bearer", + Custom: "Custom", + CustomHeader: "Custom Header", + Token: "Token", + Proxy: "Using Proxy", + ProxyDescription: "Using proxies to solve CORS error", + }, EditModal: { Title: (readonly: boolean) => `Edit Plugin ${readonly ? "(readonly)" : ""}`, Download: "Download", + Auth: "Authentication Type", Content: "OpenAPI Schema", Method: "Method", Error: "OpenAPI Schema Error", diff --git a/app/store/plugin.ts b/app/store/plugin.ts index 031a2aaf5..d7b0de255 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -12,6 +12,10 @@ export type Plugin = { version: string; content: string; builtin: boolean; + authType?: string; + authHeader?: string; + authToken?: string; + usingProxy?: boolean; }; export type FunctionToolItem = { @@ -34,10 +38,30 @@ export const FunctionToolService = { tools: {} as Record, add(plugin: Plugin, replace = false) { if (!replace && this.tools[plugin.id]) return this.tools[plugin.id]; + const headerName = ( + plugin?.authType == "custom" ? plugin?.authHeader : "Authorization" + ) as string; + const tokenValue = + plugin?.authType == "basic" + ? `Basic ${plugin?.authToken}` + : plugin?.authType == "bearer" + ? ` Bearer ${plugin?.authToken}` + : plugin?.authToken; + const definition = yaml.load(plugin.content) as any; + const serverURL = definition.servers?.[0]?.url; + const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL; const api = new OpenAPIClientAxios({ definition: yaml.load(plugin.content) as any, + axiosConfigDefaults: { + baseURL, + headers: { + // 'Cache-Control': 'no-cache', + // 'Content-Type': 'application/json', // TODO + [headerName]: tokenValue, + "X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined, + }, + }, }); - console.log("add", plugin, api); try { api.initSync(); } catch (e) {} @@ -79,14 +103,29 @@ export const FunctionToolService = { type: "function", function: { name: o.operationId, - description: o.description, + description: o.description || o.summary, parameters: parameters, }, } as FunctionToolItem; }), funcs: operations.reduce((s, o) => { // @ts-ignore - s[o.operationId] = api.client[o.operationId]; + s[o.operationId] = function (args) { + const argument = []; + if (o.parameters instanceof Array) { + o.parameters.forEach((p) => { + // @ts-ignore + argument.push(args[p?.name]); + // @ts-ignore + delete args[p?.name]; + }); + } else { + argument.push(null); + } + argument.push(args); + // @ts-ignore + return api.client[o.operationId].apply(null, argument); + }; return s; }, {}), }); @@ -136,6 +175,7 @@ export const usePluginStore = createPersistStore( const updatePlugin = { ...plugin }; updater(updatePlugin); plugins[id] = updatePlugin; + FunctionToolService.add(updatePlugin, true); set(() => ({ plugins })); get().markUpdate(); }, diff --git a/next.config.mjs b/next.config.mjs index 6b1dd0c35..27c60dd29 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -86,10 +86,6 @@ if (mode !== "export") { source: "/api/proxy/anthropic/:path*", destination: "https://api.anthropic.com/:path*", }, - { - source: "/api/proxy/gapier/:path*", - destination: "https://a.gapier.com/:path*", - }, { source: "/google-fonts/:path*", destination: "https://fonts.googleapis.com/:path*", From 877668b6293a4f5a50b5da9d1c0677cf55fce7c0 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 2 Sep 2024 18:29:00 +0800 Subject: [PATCH 24/79] hotfix --- app/store/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/plugin.ts b/app/store/plugin.ts index d7b0de255..cad733a7e 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -48,7 +48,7 @@ export const FunctionToolService = { ? ` Bearer ${plugin?.authToken}` : plugin?.authToken; const definition = yaml.load(plugin.content) as any; - const serverURL = definition.servers?.[0]?.url; + const serverURL = definition?.servers?.[0]?.url; const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL; const api = new OpenAPIClientAxios({ definition: yaml.load(plugin.content) as any, From 801b62543a909e2cf6c71d635e49f839012723fa Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 2 Sep 2024 21:45:47 +0800 Subject: [PATCH 25/79] claude support function call --- app/api/auth.ts | 1 + app/client/platforms/anthropic.ts | 226 ++++++++++++++++-------------- app/client/platforms/openai.ts | 2 +- app/components/chat.tsx | 15 +- app/utils.ts | 11 ++ app/utils/chat.ts | 2 +- 6 files changed, 145 insertions(+), 112 deletions(-) diff --git a/app/api/auth.ts b/app/api/auth.ts index 95965ceec..c8fa6787d 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -38,6 +38,7 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { console.log("[Auth] hashed access code:", hashedCode); console.log("[User IP] ", getIP(req)); console.log("[Time] ", new Date().toLocaleString()); + console.log("[ModelProvider] ", modelProvider); if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) { return { diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index b079ba1ad..91434ffcc 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -1,6 +1,12 @@ import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant"; import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api"; -import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; +import { + useAccessStore, + useAppConfig, + useChatStore, + usePluginStore, + ChatMessageTool, +} from "@/app/store"; import { getClientConfig } from "@/app/config/client"; import { DEFAULT_API_HOST } from "@/app/constant"; import { @@ -11,8 +17,9 @@ import { import Locale from "../../locales"; import { prettyObject } from "@/app/utils/format"; import { getMessageTextContent, isVisionModel } from "@/app/utils"; -import { preProcessImageContent } from "@/app/utils/chat"; +import { preProcessImageContent, stream } from "@/app/utils/chat"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; +import { RequestPayload } from "./openai"; export type MultiBlockContent = { type: "image" | "text"; @@ -191,112 +198,123 @@ export class ClaudeApi implements LLMApi { const controller = new AbortController(); options.onController?.(controller); - const payload = { - method: "POST", - body: JSON.stringify(requestBody), - signal: controller.signal, - headers: { - ...getHeaders(), // get common headers - "anthropic-version": accessStore.anthropicApiVersion, - // do not send `anthropicApiKey` in browser!!! - // Authorization: getAuthKey(accessStore.anthropicApiKey), - }, - }; - if (shouldStream) { - try { - const context = { - text: "", - finished: false, - }; - - const finish = () => { - if (!context.finished) { - options.onFinish(context.text); - context.finished = true; - } - }; - - controller.signal.onabort = finish; - fetchEventSource(path, { - ...payload, - async onopen(res) { - const contentType = res.headers.get("content-type"); - console.log("response content type: ", contentType); - - if (contentType?.startsWith("text/plain")) { - context.text = await res.clone().text(); - return finish(); - } - - if ( - !res.ok || - !res.headers - .get("content-type") - ?.startsWith(EventStreamContentType) || - res.status !== 200 - ) { - const responseTexts = [context.text]; - let extraInfo = await res.clone().text(); - try { - const resJson = await res.clone().json(); - extraInfo = prettyObject(resJson); - } catch {} - - if (res.status === 401) { - responseTexts.push(Locale.Error.Unauthorized); - } - - if (extraInfo) { - responseTexts.push(extraInfo); - } - - context.text = responseTexts.join("\n\n"); - - return finish(); - } - }, - onmessage(msg) { - let chunkJson: - | undefined - | { - type: "content_block_delta" | "content_block_stop"; - delta?: { - type: "text_delta"; - text: string; - }; - index: number; + let index = -1; + const [tools, funcs] = usePluginStore + .getState() + .getAsTools( + useChatStore.getState().currentSession().mask?.plugin as string[], + ); + console.log("getAsTools", tools, funcs); + return stream( + path, + requestBody, + { + ...getHeaders(), + "anthropic-version": accessStore.anthropicApiVersion, + }, + // @ts-ignore + tools.map((tool) => ({ + name: tool?.function?.name, + description: tool?.function?.description, + input_schema: tool?.function?.parameters, + })), + funcs, + controller, + // parseSSE + (text: string, runTools: ChatMessageTool[]) => { + // console.log("parseSSE", text, runTools); + let chunkJson: + | undefined + | { + type: "content_block_delta" | "content_block_stop"; + content_block?: { + type: "tool_use"; + id: string; + name: string; }; - try { - chunkJson = JSON.parse(msg.data); - } catch (e) { - console.error("[Response] parse error", msg.data); - } + delta?: { + type: "text_delta" | "input_json_delta"; + text?: string; + partial_json?: string; + }; + index: number; + }; + chunkJson = JSON.parse(text); - if (!chunkJson || chunkJson.type === "content_block_stop") { - return finish(); - } - - const { delta } = chunkJson; - if (delta?.text) { - context.text += delta.text; - options.onUpdate?.(context.text, delta.text); - } - }, - onclose() { - finish(); - }, - onerror(e) { - options.onError?.(e); - throw e; - }, - openWhenHidden: true, - }); - } catch (e) { - console.error("failed to chat", e); - options.onError?.(e as Error); - } + if (chunkJson?.content_block?.type == "tool_use") { + index += 1; + const id = chunkJson?.content_block.id; + const name = chunkJson?.content_block.name; + runTools.push({ + id, + type: "function", + function: { + name, + arguments: "", + }, + }); + } + if ( + chunkJson?.delta?.type == "input_json_delta" && + chunkJson?.delta?.partial_json + ) { + // @ts-ignore + runTools[index]["function"]["arguments"] += + chunkJson?.delta?.partial_json; + } + return chunkJson?.delta?.text; + }, + // 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, + { + role: "assistant", + content: toolCallMessage.tool_calls.map( + (tool: ChatMessageTool) => ({ + type: "tool_use", + id: tool.id, + name: tool?.function?.name, + input: JSON.parse(tool?.function?.arguments as string), + }), + ), + }, + // @ts-ignore + ...toolCallResult.map((result) => ({ + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: result.tool_call_id, + content: result.content, + }, + ], + })), + ); + }, + options, + ); } else { + const payload = { + method: "POST", + body: JSON.stringify(requestBody), + signal: controller.signal, + headers: { + ...getHeaders(), // get common headers + "anthropic-version": accessStore.anthropicApiVersion, + // do not send `anthropicApiKey` in browser!!! + // Authorization: getAuthKey(accessStore.anthropicApiKey), + }, + }; + try { controller.signal.onabort = () => options.onFinish(""); diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 4c5831fe3..b3b306d1d 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -246,7 +246,7 @@ export class ChatGPTApi implements LLMApi { .getAsTools( useChatStore.getState().currentSession().mask?.plugin as string[], ); - console.log("getAsTools", tools, funcs); + // console.log("getAsTools", tools, funcs); stream( chatPath, requestPayload, diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 7bac62bc4..7d180f0b7 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -66,6 +66,7 @@ import { getMessageImages, isVisionModel, isDalle3, + showPlugins, } from "../utils"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; @@ -741,12 +742,14 @@ export function ChatActions(props: { value: ArtifactsPlugin.Artifacts as string, }, ].concat( - pluginStore.getAll().map((item) => ({ - // @ts-ignore - title: `${item?.title}@${item?.version}`, - // @ts-ignore - value: item?.id, - })), + showPlugins(currentProviderName, currentModel) + ? pluginStore.getAll().map((item) => ({ + // @ts-ignore + title: `${item?.title}@${item?.version}`, + // @ts-ignore + value: item?.id, + })) + : [], )} onClose={() => setShowPluginSelector(false)} onSelection={(s) => { diff --git a/app/utils.ts b/app/utils.ts index 2a2922907..b9884c706 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { showToast } from "./components/ui-lib"; import Locale from "./locales"; import { RequestMessage } from "./client/api"; +import { ServiceProvider } from "./constant"; export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language @@ -270,3 +271,13 @@ export function isVisionModel(model: string) { export function isDalle3(model: string) { return "dall-e-3" === model; } + +export function showPlugins(provider: ServiceProvider, model: string) { + if (provider == ServiceProvider.OpenAI || provider == ServiceProvider.Azure) { + return true; + } + if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) { + return true; + } + return false; +} diff --git a/app/utils/chat.ts b/app/utils/chat.ts index d8ab5770c..49e5060d4 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -334,7 +334,7 @@ export function stream( remainText += chunk; } } catch (e) { - console.error("[Request] parse error", text, msg); + console.error("[Request] parse error", text, msg, e); } }, onclose() { From 078305f5ac15f8af33ed9f8dd4ed0843543793a0 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 2 Sep 2024 21:55:17 +0800 Subject: [PATCH 26/79] kimi support function call --- app/client/platforms/moonshot.ts | 174 ++++++++++++------------------- app/utils.ts | 6 +- 2 files changed, 71 insertions(+), 109 deletions(-) diff --git a/app/client/platforms/moonshot.ts b/app/client/platforms/moonshot.ts index 7d257ccb2..311f2726a 100644 --- a/app/client/platforms/moonshot.ts +++ b/app/client/platforms/moonshot.ts @@ -8,9 +8,15 @@ import { REQUEST_TIMEOUT_MS, ServiceProvider, } from "@/app/constant"; -import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; +import { + useAccessStore, + useAppConfig, + useChatStore, + ChatMessageTool, + usePluginStore, +} from "@/app/store"; import { collectModelsWithDefaultModel } from "@/app/utils/model"; -import { preProcessImageContent } from "@/app/utils/chat"; +import { preProcessImageContent, stream } from "@/app/utils/chat"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { @@ -116,115 +122,67 @@ export class MoonshotApi implements LLMApi { ); if (shouldStream) { - let responseText = ""; - let remainText = ""; - let finished = false; - - // animate response to make it looks smooth - function animateResponseText() { - if (finished || controller.signal.aborted) { - responseText += remainText; - console.log("[Response Animation] finished"); - if (responseText?.length === 0) { - options.onError?.(new Error("empty response from server")); + const [tools, funcs] = usePluginStore + .getState() + .getAsTools( + useChatStore.getState().currentSession().mask?.plugin as string[], + ); + console.log("getAsTools", tools, funcs); + 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; - } - - if (remainText.length > 0) { - const fetchCount = Math.max(1, Math.round(remainText.length / 60)); - const fetchText = remainText.slice(0, fetchCount); - responseText += fetchText; - remainText = remainText.slice(fetchCount); - options.onUpdate?.(responseText, fetchText); - } - - requestAnimationFrame(animateResponseText); - } - - // start animaion - animateResponseText(); - - const finish = () => { - if (!finished) { - finished = true; - options.onFinish(responseText + remainText); - } - }; - - controller.signal.onabort = finish; - - fetchEventSource(chatPath, { - ...chatPayload, - async onopen(res) { - clearTimeout(requestTimeoutId); - const contentType = res.headers.get("content-type"); - console.log( - "[OpenAI] request response content type: ", - contentType, + 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, ); - - if (contentType?.startsWith("text/plain")) { - responseText = await res.clone().text(); - return finish(); - } - - if ( - !res.ok || - !res.headers - .get("content-type") - ?.startsWith(EventStreamContentType) || - res.status !== 200 - ) { - const responseTexts = [responseText]; - let extraInfo = await res.clone().text(); - try { - const resJson = await res.clone().json(); - extraInfo = prettyObject(resJson); - } catch {} - - if (res.status === 401) { - responseTexts.push(Locale.Error.Unauthorized); - } - - if (extraInfo) { - responseTexts.push(extraInfo); - } - - responseText = responseTexts.join("\n\n"); - - return finish(); - } }, - onmessage(msg) { - if (msg.data === "[DONE]" || finished) { - return finish(); - } - const text = msg.data; - try { - const json = JSON.parse(text); - const choices = json.choices as Array<{ - delta: { content: string }; - }>; - const delta = choices[0]?.delta?.content; - const textmoderation = json?.prompt_filter_results; - - if (delta) { - remainText += delta; - } - } catch (e) { - console.error("[Request] parse error", text, msg); - } - }, - onclose() { - finish(); - }, - onerror(e) { - options.onError?.(e); - throw e; - }, - openWhenHidden: true, - }); + options, + ); } else { const res = await fetch(chatPath, chatPayload); clearTimeout(requestTimeoutId); diff --git a/app/utils.ts b/app/utils.ts index b9884c706..0ac5867fa 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -273,7 +273,11 @@ export function isDalle3(model: string) { } export function showPlugins(provider: ServiceProvider, model: string) { - if (provider == ServiceProvider.OpenAI || provider == ServiceProvider.Azure) { + if ( + provider == ServiceProvider.OpenAI || + provider == ServiceProvider.Azure || + provider == ServiceProvider.Moonshot + ) { return true; } if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) { From 6435e7a30e5b0d7703ba6ccec0df9c6e767f3e8b Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 2 Sep 2024 23:42:56 +0800 Subject: [PATCH 27/79] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da1adb6a5..24a056673 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** - [x] Desktop App with tauri - [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc. - [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092) -- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) +- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - [x] artifacts - [ ] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) - [ ] local knowledge base From 3ec67f9f477976c880dba238d22e7482405af4b4 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 3 Sep 2024 00:45:11 +0800 Subject: [PATCH 28/79] add load from url --- app/components/plugin.tsx | 50 +++++++++++++++++++++++++++++++++++++-- app/components/ui-lib.tsx | 2 +- app/locales/cn.ts | 1 + app/locales/en.ts | 1 + 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx index 5aa66ce94..fcf671bff 100644 --- a/app/components/plugin.tsx +++ b/app/components/plugin.tsx @@ -15,6 +15,7 @@ import DeleteIcon from "../icons/delete.svg"; import EyeIcon from "../icons/eye.svg"; import CopyIcon from "../icons/copy.svg"; import ConfirmIcon from "../icons/confirm.svg"; +import ReloadIcon from "../icons/reload.svg"; import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin"; import { @@ -31,7 +32,7 @@ import { import { downloadAs } from "../utils"; import Locale from "../locales"; import { useNavigate } from "react-router-dom"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { Path } from "../constant"; import { nanoid } from "nanoid"; @@ -90,6 +91,37 @@ export function PluginPage() { } }, 100).bind(null, editingPlugin); + const [loadUrl, setLoadUrl] = useState(""); + const loadFromUrl = (loadUrl: string) => + fetch(loadUrl) + .catch((e) => { + const p = new URL(loadUrl); + return fetch(`/api/proxy/${p.pathname}?${p.search}`, { + headers: { + "X-Base-URL": p.origin, + }, + }); + }) + .then((res) => res.text()) + .then((content) => { + try { + return JSON.stringify(JSON.parse(content), null, " "); + } catch (e) { + return content; + } + }) + .then((content) => { + pluginStore.updatePlugin(editingPlugin.id, (plugin) => { + plugin.content = content; + const tool = FunctionToolService.add(plugin, true); + plugin.title = tool.api.definition.info.title; + plugin.version = tool.api.definition.info.version; + }); + }) + .catch((e) => { + showToast(Locale.Plugin.EditModal.Error); + }); + return (
@@ -262,8 +294,22 @@ export function PluginPage() { + +
+ setLoadUrl(e.currentTarget.value)} + > + } + text={Locale.Plugin.EditModal.Load} + bordered + onClick={() => loadFromUrl(loadUrl)} + /> +
+
Date: Tue, 3 Sep 2024 12:00:55 +0800 Subject: [PATCH 29/79] add config auth location --- app/components/plugin.module.scss | 1 + app/components/plugin.tsx | 24 +++++++++++++++++++++++ app/locales/cn.ts | 6 +++++- app/locales/en.ts | 6 +++++- app/store/plugin.ts | 32 +++++++++++++++++++------------ 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/app/components/plugin.module.scss b/app/components/plugin.module.scss index 53c632468..a179e0a07 100644 --- a/app/components/plugin.module.scss +++ b/app/components/plugin.module.scss @@ -10,6 +10,7 @@ max-height: 240px; overflow-y: auto; white-space: pre-wrap; + min-width: 300px; } } diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx index fcf671bff..35fb2abc6 100644 --- a/app/components/plugin.tsx +++ b/app/components/plugin.tsx @@ -249,6 +249,30 @@ export function PluginPage() { + {["bearer", "basic", "custom"].includes( + editingPlugin.authType as string, + ) && ( + + + + )} {editingPlugin.authType == "custom" && ( `编辑插件 ${readonly ? "(只读)" : ""}`, diff --git a/app/locales/en.ts b/app/locales/en.ts index 80666f3b2..d2b27fdcd 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -559,10 +559,14 @@ const en: LocaleType = { Basic: "Basic", Bearer: "Bearer", Custom: "Custom", - CustomHeader: "Custom Header", + CustomHeader: "Parameter Name", Token: "Token", Proxy: "Using Proxy", ProxyDescription: "Using proxies to solve CORS error", + Location: "Location", + LocationHeader: "Header", + LocationQuery: "Query", + LocationBody: "Body", }, EditModal: { Title: (readonly: boolean) => diff --git a/app/store/plugin.ts b/app/store/plugin.ts index cad733a7e..260c33c32 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -13,6 +13,7 @@ export type Plugin = { content: string; builtin: boolean; authType?: string; + authLocation?: string; authHeader?: string; authToken?: string; usingProxy?: boolean; @@ -50,16 +51,17 @@ export const FunctionToolService = { const definition = yaml.load(plugin.content) as any; const serverURL = definition?.servers?.[0]?.url; const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL; + const headers: Record = { + "X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined, + }; + if (plugin?.authLocation == "header") { + headers[headerName] = tokenValue; + } const api = new OpenAPIClientAxios({ definition: yaml.load(plugin.content) as any, axiosConfigDefaults: { baseURL, - headers: { - // 'Cache-Control': 'no-cache', - // 'Content-Type': 'application/json', // TODO - [headerName]: tokenValue, - "X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined, - }, + headers, }, }); try { @@ -111,20 +113,26 @@ export const FunctionToolService = { funcs: operations.reduce((s, o) => { // @ts-ignore s[o.operationId] = function (args) { - const argument = []; + const parameters: Record = {}; if (o.parameters instanceof Array) { o.parameters.forEach((p) => { // @ts-ignore - argument.push(args[p?.name]); + parameters[p?.name] = args[p?.name]; // @ts-ignore delete args[p?.name]; }); - } else { - argument.push(null); } - argument.push(args); + if (plugin?.authLocation == "query") { + parameters[headerName] = tokenValue; + } else if (plugin?.authLocation == "body") { + args[headerName] = tokenValue; + } // @ts-ignore - return api.client[o.operationId].apply(null, argument); + return api.client[o.operationId]( + parameters, + args, + api.axiosConfigDefaults, + ); }; return s; }, {}), From 236736deeaee40fb0241f294c9a50eef03fead90 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 3 Sep 2024 15:37:23 +0800 Subject: [PATCH 30/79] remove no need code --- app/api/auth.ts | 1 - app/components/plugin.tsx | 10 +--------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/api/auth.ts b/app/api/auth.ts index c8fa6787d..95965ceec 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -38,7 +38,6 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { console.log("[Auth] hashed access code:", hashedCode); console.log("[User IP] ", getIP(req)); console.log("[Time] ", new Date().toLocaleString()); - console.log("[ModelProvider] ", modelProvider); if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) { return { diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx index 35fb2abc6..5aad2c70a 100644 --- a/app/components/plugin.tsx +++ b/app/components/plugin.tsx @@ -7,34 +7,26 @@ import { ErrorBoundary } from "./error"; import styles from "./mask.module.scss"; import pluginStyles from "./plugin.module.scss"; -import DownloadIcon from "../icons/download.svg"; import EditIcon from "../icons/edit.svg"; import AddIcon from "../icons/add.svg"; import CloseIcon from "../icons/close.svg"; import DeleteIcon from "../icons/delete.svg"; import EyeIcon from "../icons/eye.svg"; -import CopyIcon from "../icons/copy.svg"; import ConfirmIcon from "../icons/confirm.svg"; import ReloadIcon from "../icons/reload.svg"; import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin"; import { - Input, PasswordInput, List, ListItem, Modal, - Popover, - Select, showConfirm, showToast, } from "./ui-lib"; -import { downloadAs } from "../utils"; import Locale from "../locales"; import { useNavigate } from "react-router-dom"; -import { useEffect, useState, useCallback } from "react"; -import { Path } from "../constant"; -import { nanoid } from "nanoid"; +import { useEffect, useState } from "react"; export function PluginPage() { const navigate = useNavigate(); From 4fdd997108a9b754f6c5c368e2b77b4e7fb2f435 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 3 Sep 2024 16:23:54 +0800 Subject: [PATCH 31/79] hotfix --- app/store/plugin.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/store/plugin.ts b/app/store/plugin.ts index 260c33c32..934a989dc 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -48,13 +48,14 @@ export const FunctionToolService = { : plugin?.authType == "bearer" ? ` Bearer ${plugin?.authToken}` : plugin?.authToken; + const authLocation = plugin?.authLocation || "header"; const definition = yaml.load(plugin.content) as any; const serverURL = definition?.servers?.[0]?.url; const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL; const headers: Record = { "X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined, }; - if (plugin?.authLocation == "header") { + if (authLocation == "header") { headers[headerName] = tokenValue; } const api = new OpenAPIClientAxios({ @@ -122,9 +123,9 @@ export const FunctionToolService = { delete args[p?.name]; }); } - if (plugin?.authLocation == "query") { + if (authLocation == "query") { parameters[headerName] = tokenValue; - } else if (plugin?.authLocation == "body") { + } else if (authLocation == "body") { args[headerName] = tokenValue; } // @ts-ignore From 886ffc0af89b2bf09c8a1af16648b00a629b584e Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Tue, 3 Sep 2024 17:12:48 +0800 Subject: [PATCH 32/79] fix: hydrated for indexedDB --- app/utils/indexedDB-storage.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/utils/indexedDB-storage.ts b/app/utils/indexedDB-storage.ts index 51ee92b81..b80c84ce9 100644 --- a/app/utils/indexedDB-storage.ts +++ b/app/utils/indexedDB-storage.ts @@ -4,7 +4,12 @@ import { get, set, del, clear } from "idb-keyval"; class IndexedDBStorage implements StateStorage { public async getItem(name: string): Promise { try { - return (await get(name)) || localStorage.getItem(name); + const value = (await get(name)) || localStorage.getItem(name); + const _value = JSON.parse(value); + if (_value?.state) { + _value.state._hasHydrated = true; + } + return JSON.stringify(_value); } catch (error) { return localStorage.getItem(name); } @@ -12,6 +17,10 @@ class IndexedDBStorage implements StateStorage { public async setItem(name: string, value: string): Promise { try { + const _value = JSON.parse(value); + if (!_value?.state?._hasHydrated) { + return; + } await set(name, value); } catch (error) { localStorage.setItem(name, value); From d30351e7b07c3975fb3fe21cf751c244abc02269 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 3 Sep 2024 17:18:43 +0800 Subject: [PATCH 33/79] update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 24a056673..b6f974bd4 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** - [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092) - [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - [x] artifacts - - [ ] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) + - [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - [ ] local knowledge base ## What's New @@ -126,9 +126,9 @@ For enterprise inquiries, please contact: **business@nextchat.dev** - [x] 使用 tauri 打包桌面应用 - [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm) - [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092) -- [x] 插件机制,支持 artifacts,联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) +- [x] 插件机制,支持 artifacts,联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - [x] artifacts - - [ ] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) + - [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - [ ] 本地知识库 ## 最新动态 From 0a5522d28cc594636957ada475b35a41a3d6eda2 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 3 Sep 2024 19:35:36 +0800 Subject: [PATCH 34/79] update --- app/client/platforms/anthropic.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index 91434ffcc..d6958dfc5 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -271,6 +271,8 @@ export class ClaudeApi implements LLMApi { toolCallMessage: any, toolCallResult: any[], ) => { + // reset index value + index = -1; // @ts-ignore requestPayload?.messages?.splice( // @ts-ignore From 7180ed9a60e312d8500f3e264cffb0961377ed32 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 3 Sep 2024 19:56:22 +0800 Subject: [PATCH 35/79] hotfix --- app/client/platforms/anthropic.ts | 4 +++- app/utils/chat.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index d6958dfc5..e30b53ed2 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -285,7 +285,9 @@ export class ClaudeApi implements LLMApi { type: "tool_use", id: tool.id, name: tool?.function?.name, - input: JSON.parse(tool?.function?.arguments as string), + input: tool?.function?.arguments + ? JSON.parse(tool?.function?.arguments) + : {}, }), ), }, diff --git a/app/utils/chat.ts b/app/utils/chat.ts index 49e5060d4..df266e59d 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -215,7 +215,9 @@ export function stream( // @ts-ignore funcs[tool.function.name]( // @ts-ignore - JSON.parse(tool.function.arguments), + tool?.function?.arguments + ? JSON.parse(tool?.function?.arguments) + : {}, ), ) .then((res) => { From 6ab6b3dbca566ffbe908fd918438087527b09c62 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 3 Sep 2024 20:21:37 +0800 Subject: [PATCH 36/79] remove no need code --- app/client/platforms/anthropic.ts | 1 - app/client/platforms/moonshot.ts | 1 - app/store/chat.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index e30b53ed2..fce675a16 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -205,7 +205,6 @@ export class ClaudeApi implements LLMApi { .getAsTools( useChatStore.getState().currentSession().mask?.plugin as string[], ); - console.log("getAsTools", tools, funcs); return stream( path, requestBody, diff --git a/app/client/platforms/moonshot.ts b/app/client/platforms/moonshot.ts index 311f2726a..c38d3317b 100644 --- a/app/client/platforms/moonshot.ts +++ b/app/client/platforms/moonshot.ts @@ -127,7 +127,6 @@ export class MoonshotApi implements LLMApi { .getAsTools( useChatStore.getState().currentSession().mask?.plugin as string[], ); - console.log("getAsTools", tools, funcs); return stream( chatPath, requestPayload, diff --git a/app/store/chat.ts b/app/store/chat.ts index 9e7aa043f..db7fe35f0 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -410,7 +410,6 @@ export const useChatStore = createPersistStore( }); }, onAfterTool(tool: ChatMessageTool) { - console.log("onAfterTool", botMessage); botMessage?.tools?.forEach((t, i, tools) => { if (tool.id == t.id) { tools[i] = { ...tool }; From ed9aae531e0191d8b7fcbe594e0dc4e6176450da Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Tue, 3 Sep 2024 20:29:01 +0800 Subject: [PATCH 37/79] fix: hydrated --- app/store/prompt.ts | 2 ++ app/utils/indexedDB-storage.ts | 6 +----- app/utils/store.ts | 11 +++++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/store/prompt.ts b/app/store/prompt.ts index a25cda581..29a97abba 100644 --- a/app/store/prompt.ts +++ b/app/store/prompt.ts @@ -179,6 +179,8 @@ export const usePromptStore = createPersistStore( res.en.length + res.cn.length + res.tw.length; SearchService.init(allPromptsForSearch, userPrompts); }); + + return () => state.setHasHydrated(true); }, }, ); diff --git a/app/utils/indexedDB-storage.ts b/app/utils/indexedDB-storage.ts index b80c84ce9..86b304777 100644 --- a/app/utils/indexedDB-storage.ts +++ b/app/utils/indexedDB-storage.ts @@ -5,11 +5,7 @@ class IndexedDBStorage implements StateStorage { public async getItem(name: string): Promise { try { const value = (await get(name)) || localStorage.getItem(name); - const _value = JSON.parse(value); - if (_value?.state) { - _value.state._hasHydrated = true; - } - return JSON.stringify(_value); + return value; } catch (error) { return localStorage.getItem(name); } diff --git a/app/utils/store.ts b/app/utils/store.ts index 13bef6d5d..02c8139b0 100644 --- a/app/utils/store.ts +++ b/app/utils/store.ts @@ -14,9 +14,11 @@ type SecondParam = T extends ( type MakeUpdater = { lastUpdateTime: number; + _hasHydrated: boolean; markUpdate: () => void; update: Updater; + setHasHydrated: (state: boolean) => void; }; type SetStoreState = ( @@ -33,12 +35,18 @@ export function createPersistStore( persistOptions: SecondParam>>, ) { persistOptions.storage = createJSONStorage(() => indexedDBStorage); + persistOptions.onRehydrateStorage = persistOptions.onRehydrateStorage + ? persistOptions.onRehydrateStorage + : (state) => { + return () => state.setHasHydrated(true); + }; return create( persist( combine( { ...state, lastUpdateTime: 0, + _hasHydrated: false, }, (set, get) => { return { @@ -57,6 +65,9 @@ export function createPersistStore( lastUpdateTime: Date.now(), }); }, + setHasHydrated: (state: boolean) => { + set({ _hasHydrated: state } as Partial>); + }, } as M & MakeUpdater; }, ), From 04e1ab63bbcb863721dcd50a8d1a2c862a096b96 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 4 Sep 2024 11:47:42 +0800 Subject: [PATCH 38/79] update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9811f1d35..8b6a22818 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** ## What's New +- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins) - 🚀 v2.14.0 Now supports Artifacts & SD - 🚀 v2.10.1 support Google Gemini Pro model. - 🚀 v2.9.11 you can use azure endpoint now. @@ -135,6 +136,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** ## 最新动态 +- 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins) - 🚀 v2.14.0 现在支持 Artifacts & SD 了。 - 🚀 v2.10.1 现在支持 Gemini Pro 模型。 - 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。 From 53dcae9e9ce1230f2f651b39f8f44330fe5408a4 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 4 Sep 2024 13:00:18 +0800 Subject: [PATCH 39/79] update --- app/store/prompt.ts | 2 -- app/utils/indexedDB-storage.ts | 1 + app/utils/store.ts | 11 ++++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/store/prompt.ts b/app/store/prompt.ts index 29a97abba..a25cda581 100644 --- a/app/store/prompt.ts +++ b/app/store/prompt.ts @@ -179,8 +179,6 @@ export const usePromptStore = createPersistStore( res.en.length + res.cn.length + res.tw.length; SearchService.init(allPromptsForSearch, userPrompts); }); - - return () => state.setHasHydrated(true); }, }, ); diff --git a/app/utils/indexedDB-storage.ts b/app/utils/indexedDB-storage.ts index 86b304777..da3094550 100644 --- a/app/utils/indexedDB-storage.ts +++ b/app/utils/indexedDB-storage.ts @@ -15,6 +15,7 @@ class IndexedDBStorage implements StateStorage { try { const _value = JSON.parse(value); if (!_value?.state?._hasHydrated) { + console.warn("skip setItem", name); return; } await set(name, value); diff --git a/app/utils/store.ts b/app/utils/store.ts index 02c8139b0..ff15bee14 100644 --- a/app/utils/store.ts +++ b/app/utils/store.ts @@ -35,11 +35,12 @@ export function createPersistStore( persistOptions: SecondParam>>, ) { persistOptions.storage = createJSONStorage(() => indexedDBStorage); - persistOptions.onRehydrateStorage = persistOptions.onRehydrateStorage - ? persistOptions.onRehydrateStorage - : (state) => { - return () => state.setHasHydrated(true); - }; + const oldOonRehydrateStorage = persistOptions?.onRehydrateStorage; + persistOptions.onRehydrateStorage = (state) => { + oldOonRehydrateStorage?.(state); + return () => state.setHasHydrated(true); + }; + return create( persist( combine( From f9a047aad48c6b2884e7b3d1077d27a7335884ef Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 4 Sep 2024 21:04:13 +0800 Subject: [PATCH 40/79] using tauri http api run plugin to fixed cors in App --- app/components/plugin.tsx | 33 ++++++++++++++++++--------------- app/global.d.ts | 8 +++++++- app/store/chat.ts | 2 +- app/store/plugin.ts | 2 ++ app/utils.ts | 35 ++++++++++++++++++++++++++++++++++- package.json | 3 ++- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 5 +++++ yarn.lock | 5 +++++ 9 files changed, 75 insertions(+), 20 deletions(-) diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx index 5aad2c70a..b847ad78e 100644 --- a/app/components/plugin.tsx +++ b/app/components/plugin.tsx @@ -27,6 +27,7 @@ import { import Locale from "../locales"; import { useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; +import { getClientConfig } from "../config/client"; export function PluginPage() { const navigate = useNavigate(); @@ -293,21 +294,23 @@ export function PluginPage() { > )} - - { - pluginStore.updatePlugin(editingPlugin.id, (plugin) => { - plugin.usingProxy = e.currentTarget.checked; - }); - }} - > - + {!getClientConfig()?.isApp && ( + + { + pluginStore.updatePlugin(editingPlugin.id, (plugin) => { + plugin.usingProxy = e.currentTarget.checked; + }); + }} + > + + )}
diff --git a/app/global.d.ts b/app/global.d.ts index 31e2b6e8a..8ee636bcd 100644 --- a/app/global.d.ts +++ b/app/global.d.ts @@ -21,10 +21,16 @@ declare interface Window { writeBinaryFile(path: string, data: Uint8Array): Promise; writeTextFile(path: string, data: string): Promise; }; - notification:{ + notification: { requestPermission(): Promise; isPermissionGranted(): Promise; sendNotification(options: string | Options): void; }; + http: { + fetch( + url: string, + options?: Record, + ): Promise>; + }; }; } diff --git a/app/store/chat.ts b/app/store/chat.ts index db7fe35f0..8b0cc39eb 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -420,7 +420,7 @@ export const useChatStore = createPersistStore( }); }, onError(error) { - const isAborted = error.message.includes("aborted"); + const isAborted = error.message?.includes?.("aborted"); botMessage.content += "\n\n" + prettyObject({ diff --git a/app/store/plugin.ts b/app/store/plugin.ts index 934a989dc..3bf08e68d 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -4,6 +4,7 @@ import { StoreKey } from "../constant"; import { nanoid } from "nanoid"; import { createPersistStore } from "../utils/store"; import yaml from "js-yaml"; +import { adapter } from "../utils"; export type Plugin = { id: string; @@ -61,6 +62,7 @@ export const FunctionToolService = { const api = new OpenAPIClientAxios({ definition: yaml.load(plugin.content) as any, axiosConfigDefaults: { + adapter: adapter as any, baseURL, headers, }, diff --git a/app/utils.ts b/app/utils.ts index 0ac5867fa..60041ba06 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -2,7 +2,9 @@ import { useEffect, useState } from "react"; import { showToast } from "./components/ui-lib"; import Locale from "./locales"; import { RequestMessage } from "./client/api"; -import { ServiceProvider } from "./constant"; +import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant"; +import isObject from "lodash-es/isObject"; +import { fetch as tauriFetch, Body, ResponseType } from "@tauri-apps/api/http"; export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language @@ -285,3 +287,34 @@ export function showPlugins(provider: ServiceProvider, model: string) { } return false; } + +export function fetch( + url: string, + options?: Record, +): Promise { + if (window.__TAURI__) { + const payload = options?.body || options?.data; + return tauriFetch(url, { + ...options, + body: + payload && + ({ + type: "Text", + payload, + } as any), + timeout: ((options?.timeout as number) || REQUEST_TIMEOUT_MS) / 1000, + responseType: + options?.responseType == "text" ? ResponseType.Text : ResponseType.JSON, + } as any); + } + return window.fetch(url, options); +} + +export function adapter(config: Record) { + const { baseURL, url, params, ...rest } = config; + const path = baseURL ? `${baseURL}${url}` : url; + const fetchUrl = params + ? `${path}?${new URLSearchParams(params as any).toString()}` + : path; + return fetch(fetchUrl as string, { ...rest, responseType: "text" }); +} diff --git a/package.json b/package.json index d3f062185..ca5fcc0f5 100644 --- a/package.json +++ b/package.json @@ -51,14 +51,15 @@ "zustand": "^4.3.8" }, "devDependencies": { + "@tauri-apps/api": "^1.6.0", "@tauri-apps/cli": "1.5.11", + "@types/js-yaml": "4.0.9", "@types/lodash-es": "^4.17.12", "@types/node": "^20.11.30", "@types/react": "^18.2.70", "@types/react-dom": "^18.2.7", "@types/react-katex": "^3.0.0", "@types/spark-md5": "^3.0.4", - "@types/js-yaml": "4.0.9", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "eslint": "^8.49.0", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e08925902..387584491 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,7 +17,7 @@ tauri-build = { version = "1.5.1", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.5.4", features = [ +tauri = { version = "1.5.4", features = [ "http-all", "notification-all", "fs-all", "clipboard-all", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 120ab9b5a..78807a2c5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -50,6 +50,11 @@ }, "notification": { "all": true + }, + "http": { + "all": true, + "request": true, + "scope": ["https://*", "http://*"] } }, "bundle": { diff --git a/yarn.lock b/yarn.lock index 5ca929baf..4979e4d99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1553,6 +1553,11 @@ dependencies: tslib "^2.4.0" +"@tauri-apps/api@^1.6.0": + version "1.6.0" + resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz#745b7e4e26782c3b2ad9510d558fa5bb2cf29186" + integrity sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg== + "@tauri-apps/cli-darwin-arm64@1.5.11": version "1.5.11" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.11.tgz#a831f98f685148e46e8050dbdddbf4bcdda9ddc6" From 09aec7b22e90c311e632fc64bf7aadaa6e79989d Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 4 Sep 2024 21:32:22 +0800 Subject: [PATCH 41/79] using tauri http api run plugin to fixed cors in App --- app/store/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/plugin.ts b/app/store/plugin.ts index 3bf08e68d..74f0fbe17 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -62,7 +62,7 @@ export const FunctionToolService = { const api = new OpenAPIClientAxios({ definition: yaml.load(plugin.content) as any, axiosConfigDefaults: { - adapter: adapter as any, + adapter: (window.__TAURI__ ? adapter : ["xhr"]) as any, baseURL, headers, }, From b590d0857c289259a5e3b5be330cffbce944879c Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 5 Sep 2024 13:51:22 +0800 Subject: [PATCH 42/79] disable nextjs proxy, then can using dalle as plugin --- next.config.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index 27c60dd29..26dadca4c 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -65,10 +65,10 @@ if (mode !== "export") { nextConfig.rewrites = async () => { const ret = [ // adjust for previous version directly using "/api/proxy/" as proxy base route - { - source: "/api/proxy/v1/:path*", - destination: "https://api.openai.com/v1/:path*", - }, + // { + // source: "/api/proxy/v1/:path*", + // destination: "https://api.openai.com/v1/:path*", + // }, { // https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions source: "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*", From caf50b6e6c7887587c7daa8fd83ad7b9d30ef98a Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 5 Sep 2024 14:46:16 +0800 Subject: [PATCH 43/79] move artifacts into mask settings --- app/components/chat.tsx | 37 ++++++++++++------------------------- app/components/markdown.tsx | 7 +------ app/components/mask.tsx | 16 ++++++++++++++++ app/constant.ts | 4 ---- app/locales/cn.ts | 5 ++++- app/locales/en.ts | 5 ++++- app/store/mask.ts | 5 +++-- 7 files changed, 40 insertions(+), 39 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 4011858f6..e39bc74a9 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -98,7 +98,6 @@ import { REQUEST_TIMEOUT_MS, UNFINISHED_INPUT, ServiceProvider, - ArtifactsPlugin, } from "../constant"; import { Avatar } from "./emoji"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; @@ -727,38 +726,26 @@ export function ChatActions(props: { /> )} - setShowPluginSelector(true)} - text={Locale.Plugin.Name} - icon={} - /> - {showPluginSelector && ( + {showPlugins(currentProviderName, currentModel) && ( + setShowPluginSelector(true)} + text={Locale.Plugin.Name} + icon={} + /> + )} + {showPluginSelector && showPlugins(currentProviderName, currentModel) && ( ({ - // @ts-ignore - title: `${item?.title}@${item?.version}`, - // @ts-ignore - value: item?.id, - })) - : [], - )} + items={pluginStore.getAll().map((item) => ({ + title: `${item?.title}@${item?.version}`, + value: item?.id, + }))} onClose={() => setShowPluginSelector(false)} onSelection={(s) => { chatStore.updateCurrentSession((session) => { session.mask.plugin = s as string[]; }); - if (s.includes(ArtifactsPlugin.Artifacts)) { - showToast(ArtifactsPlugin.Artifacts); - } }} /> )} diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 58579ab47..4b9e608c9 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -19,7 +19,6 @@ import { HTMLPreview, HTMLPreviewHander, } from "./artifacts"; -import { ArtifactsPlugin } from "../constant"; import { useChatStore } from "../store"; import { IconButton } from "./button"; @@ -77,7 +76,6 @@ export function PreCode(props: { children: any }) { const { height } = useWindowSize(); const chatStore = useChatStore(); const session = chatStore.currentSession(); - const plugins = session.mask?.plugin; const renderArtifacts = useDebouncedCallback(() => { if (!ref.current) return; @@ -94,10 +92,7 @@ export function PreCode(props: { children: any }) { } }, 600); - const enableArtifacts = useMemo( - () => plugins?.includes(ArtifactsPlugin.Artifacts), - [plugins], - ); + const enableArtifacts = session.mask?.enableArtifacts !== false; //Wrap the paragraph for plain-text useEffect(() => { diff --git a/app/components/mask.tsx b/app/components/mask.tsx index 8c17a544a..78b89f260 100644 --- a/app/components/mask.tsx +++ b/app/components/mask.tsx @@ -167,6 +167,22 @@ export function MaskConfig(props: { > + + { + props.updateMask((mask) => { + mask.enableArtifacts = e.currentTarget.checked; + }); + }} + > + + {!props.shouldSyncFromGlobal ? ( `${count} 个插件`, @@ -604,6 +603,10 @@ const cn = { Title: "隐藏预设对话", SubTitle: "隐藏后预设对话不会出现在聊天界面", }, + Artifacts: { + Title: "启用Artifacts", + SubTitle: "启用之后可以直接渲染HTML页面", + }, Share: { Title: "分享此面具", SubTitle: "生成此面具的直达链接", diff --git a/app/locales/en.ts b/app/locales/en.ts index d2b27fdcd..13b3fe5e6 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -540,7 +540,6 @@ const en: LocaleType = { }, Plugin: { Name: "Plugin", - Artifacts: "Artifacts", Page: { Title: "Plugins", SubTitle: (count: number) => `${count} plugins`, @@ -613,6 +612,10 @@ const en: LocaleType = { Title: "Hide Context Prompts", SubTitle: "Do not show in-context prompts in chat", }, + Artifacts: { + Title: "Enable Artifacts", + SubTitle: "Can render HTML page when enable artifacts.", + }, Share: { Title: "Share This Mask", SubTitle: "Generate a link to this mask", diff --git a/app/store/mask.ts b/app/store/mask.ts index 05f511b0f..083121b65 100644 --- a/app/store/mask.ts +++ b/app/store/mask.ts @@ -2,7 +2,7 @@ import { BUILTIN_MASKS } from "../masks"; import { getLang, Lang } from "../locales"; import { DEFAULT_TOPIC, ChatMessage } from "./chat"; import { ModelConfig, useAppConfig } from "./config"; -import { StoreKey, ArtifactsPlugin } from "../constant"; +import { StoreKey } from "../constant"; import { nanoid } from "nanoid"; import { createPersistStore } from "../utils/store"; @@ -18,6 +18,7 @@ export type Mask = { lang: Lang; builtin: boolean; plugin?: string[]; + enableArtifacts?: boolean; }; export const DEFAULT_MASK_STATE = { @@ -38,7 +39,7 @@ export const createEmptyMask = () => lang: getLang(), builtin: false, createdAt: Date.now(), - plugin: [ArtifactsPlugin.Artifacts as string], + plugin: [], }) as Mask; export const useMaskStore = createPersistStore( From 80b8f956a9aca708a2d4e1690f40c1ab11a9e781 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 5 Sep 2024 14:49:11 +0800 Subject: [PATCH 44/79] move artifacts into mask settings --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8b6a22818..c8b158956 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** - [x] Desktop App with tauri - [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc. - [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092) -- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - - [x] artifacts +- [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - [ ] local knowledge base @@ -129,8 +128,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** - [x] 使用 tauri 打包桌面应用 - [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm) - [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092) -- [x] 插件机制,支持 artifacts,联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - - [x] artifacts +- [x] 插件机制,支持`联网搜索`、`计算器`、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - [ ] 本地知识库 From 7c0acc7b77cb868e23ad517bc17b37da15b8a6c7 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 5 Sep 2024 22:02:06 +0800 Subject: [PATCH 45/79] hotfix tools empty array --- app/utils/chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/chat.ts b/app/utils/chat.ts index df266e59d..7f3bb23c5 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -277,7 +277,7 @@ export function stream( method: "POST", body: JSON.stringify({ ...requestPayload, - tools, + tools: tools && tools.length ? tools : undefined, }), signal: controller.signal, headers, From 7455978ee57b3ab7a0cb9b375ec0548fb6b19b5f Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 6 Sep 2024 09:26:06 +0800 Subject: [PATCH 46/79] default enable artifact --- app/components/mask.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/mask.tsx b/app/components/mask.tsx index 78b89f260..62503c37a 100644 --- a/app/components/mask.tsx +++ b/app/components/mask.tsx @@ -174,7 +174,7 @@ export function MaskConfig(props: { { props.updateMask((mask) => { mask.enableArtifacts = e.currentTarget.checked; From 9275f2d7531a9301fe4c430c9b01ee0200191854 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 6 Sep 2024 19:37:24 +0800 Subject: [PATCH 47/79] add awesome plugin repo url --- app/components/chat.tsx | 10 ++++++++-- app/components/plugin.tsx | 31 +++++++++++++++++++++++++++++++ app/constant.ts | 1 + app/locales/cn.ts | 1 + app/locales/en.ts | 1 + 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index e39bc74a9..dad1933ac 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -728,12 +728,18 @@ export function ChatActions(props: { {showPlugins(currentProviderName, currentModel) && ( setShowPluginSelector(true)} + onClick={() => { + if (pluginStore.getAll().length == 0) { + navigate(Path.Plugins); + } else { + setShowPluginSelector(true); + } + }} text={Locale.Plugin.Name} icon={} /> )} - {showPluginSelector && showPlugins(currentProviderName, currentModel) && ( + {showPluginSelector && (
+
} @@ -162,6 +173,26 @@ export function PluginPage() {
+ {plugins.length == 0 && ( +
+ {Locale.Plugin.Page.Find} + + } bordered /> + +
+ )} {plugins.map((m) => (
diff --git a/app/constant.ts b/app/constant.ts index cd5c79b25..90557c16c 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -3,6 +3,7 @@ import path from "path"; export const OWNER = "ChatGPTNextWeb"; export const REPO = "ChatGPT-Next-Web"; export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; +export const PLUGINS_REPO_URL = `https://github.com/${OWNER}/NextChat-Awesome-Plugins`; export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; export const UPDATE_URL = `${REPO_URL}#keep-updated`; export const RELEASE_URL = `${REPO_URL}/releases`; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 742f85952..33e368f69 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -537,6 +537,7 @@ const cn = { SubTitle: (count: number) => `${count} 个插件`, Search: "搜索插件", Create: "新建", + Find: "您可以在Github上找到优秀的插件:", }, Item: { Info: (count: number) => `${count} 方法`, diff --git a/app/locales/en.ts b/app/locales/en.ts index 13b3fe5e6..403b9b687 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -545,6 +545,7 @@ const en: LocaleType = { SubTitle: (count: number) => `${count} plugins`, Search: "Search Plugin", Create: "Create", + Find: "You can find awesome plugins on github: ", }, Item: { Info: (count: number) => `${count} method`, From 27828d9ca86112c1d179c906197ed3158c9a9f68 Mon Sep 17 00:00:00 2001 From: kosette <35268640+Kosette@users.noreply.github.com> Date: Fri, 6 Sep 2024 23:07:01 +0800 Subject: [PATCH 48/79] fix: update package 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 78807a2c5..0f2a84a53 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "NextChat", - "version": "2.14.2" + "version": "2.15.0" }, "tauri": { "allowlist": { From c1b74201e46f64c15dd63a2e876e87b601dbc81f Mon Sep 17 00:00:00 2001 From: "l.tingting" Date: Sat, 7 Sep 2024 01:42:56 +0800 Subject: [PATCH 49/79] add chatgpt-4o-latest --- app/api/openai.ts | 4 +++- app/client/platforms/openai.ts | 4 +++- app/components/emoji.tsx | 3 ++- app/config/server.ts | 7 +++++-- app/constant.ts | 2 ++ app/store/chat.ts | 5 +++-- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/app/api/openai.ts b/app/api/openai.ts index 0059ff8b4..7dfd84e17 100644 --- a/app/api/openai.ts +++ b/app/api/openai.ts @@ -13,7 +13,9 @@ function getModels(remoteModelRes: OpenAIListModelResponse) { if (config.disableGPT4) { remoteModelRes.data = remoteModelRes.data.filter( - (m) => !m.id.startsWith("gpt-4") || m.id.startsWith("gpt-4o-mini"), + (m) => + !(m.id.startsWith("gpt-4") || m.id.startsWith("chatgpt-4o")) || + m.id.startsWith("gpt-4o-mini"), ); } diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index b3b306d1d..51a1200d3 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -407,7 +407,9 @@ export class ChatGPTApi implements LLMApi { }); const resJson = (await res.json()) as OpenAIListModelResponse; - const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-")); + const chatModels = resJson.data?.filter( + (m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"), + ); console.log("[Models]", chatModels); if (!chatModels) { diff --git a/app/components/emoji.tsx b/app/components/emoji.tsx index 3b1f5e751..6db746c46 100644 --- a/app/components/emoji.tsx +++ b/app/components/emoji.tsx @@ -36,7 +36,8 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) { if (props.model) { return (
- {props.model?.startsWith("gpt-4") ? ( + {props.model?.startsWith("gpt-4") || + props.model?.startsWith("chatgpt-4o") ? ( ) : ( diff --git a/app/config/server.ts b/app/config/server.ts index e953af369..676b0174f 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -120,12 +120,15 @@ export const getServerSideConfig = () => { if (disableGPT4) { if (customModels) customModels += ","; customModels += DEFAULT_MODELS.filter( - (m) => m.name.startsWith("gpt-4") && !m.name.startsWith("gpt-4o-mini"), + (m) => + (m.name.startsWith("gpt-4") || m.name.startsWith("chatgpt-4o")) && + !m.name.startsWith("gpt-4o-mini"), ) .map((m) => "-" + m.name) .join(","); if ( - defaultModel.startsWith("gpt-4") && + (defaultModel.startsWith("gpt-4") || + defaultModel.startsWith("chatgpt-4o")) && !defaultModel.startsWith("gpt-4o-mini") ) defaultModel = ""; diff --git a/app/constant.ts b/app/constant.ts index 90557c16c..eb82d3c66 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -246,6 +246,7 @@ export const KnowledgeCutOffDate: Record = { "gpt-4o": "2023-10", "gpt-4o-2024-05-13": "2023-10", "gpt-4o-2024-08-06": "2023-10", + "chatgpt-4o-latest": "2023-10", "gpt-4o-mini": "2023-10", "gpt-4o-mini-2024-07-18": "2023-10", "gpt-4-vision-preview": "2023-04", @@ -268,6 +269,7 @@ const openaiModels = [ "gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06", + "chatgpt-4o-latest", "gpt-4o-mini", "gpt-4o-mini-2024-07-18", "gpt-4-vision-preview", diff --git a/app/store/chat.ts b/app/store/chat.ts index 8b0cc39eb..96cc40252 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -106,7 +106,7 @@ function createEmptySession(): ChatSession { function getSummarizeModel(currentModel: string) { // if it is using gpt-* models, force to use 4o-mini to summarize - if (currentModel.startsWith("gpt")) { + if (currentModel.startsWith("gpt") || currentModel.startsWith("chatgpt")) { const configStore = useAppConfig.getState(); const accessStore = useAccessStore.getState(); const allModel = collectModelsWithDefaultModel( @@ -476,7 +476,8 @@ export const useChatStore = createPersistStore( // system prompts, to get close to OpenAI Web ChatGPT const shouldInjectSystemPrompts = modelConfig.enableInjectSystemPrompts && - session.mask.modelConfig.model.startsWith("gpt-"); + (session.mask.modelConfig.model.startsWith("gpt-") || + session.mask.modelConfig.model.startsWith("chatgpt-")); var systemPrompts: ChatMessage[] = []; systemPrompts = shouldInjectSystemPrompts From cf0c057164cb84078bc729e6185cae6e6c1c0906 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sat, 7 Sep 2024 13:00:55 +0800 Subject: [PATCH 50/79] hotfix Mermaid can not render. close #5374 --- app/components/markdown.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 4b9e608c9..dc11c572d 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -163,7 +163,7 @@ export function PreCode(props: { children: any }) { ); } -function CustomCode(props: { children: any }) { +function CustomCode(props: { children: any; className?: string }) { const ref = useRef(null); const [collapsed, setCollapsed] = useState(true); const [showToggle, setShowToggle] = useState(false); @@ -182,6 +182,7 @@ function CustomCode(props: { children: any }) { return ( <> Date: Sat, 7 Sep 2024 16:24:52 +0800 Subject: [PATCH 51/79] Add crossOrigin="use-credentials" for site.webmanifest Add `crossOrigin="use-credentials"` to the `` element for `site.webmanifest` when the site is behind a proxy with authentication. --- app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/layout.tsx b/app/layout.tsx index abefd69c1..fa087636a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -41,7 +41,7 @@ export default function RootLayout({ name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> - + From db58ca6c1d59dc6410c1fa55116926a6ec5fb1c6 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sat, 7 Sep 2024 21:32:18 +0800 Subject: [PATCH 52/79] fix(#5378): default plugin ids to empty array --- app/client/platforms/anthropic.ts | 2 +- app/client/platforms/moonshot.ts | 2 +- app/client/platforms/openai.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index fce675a16..7dd39c9cd 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -203,7 +203,7 @@ export class ClaudeApi implements LLMApi { const [tools, funcs] = usePluginStore .getState() .getAsTools( - useChatStore.getState().currentSession().mask?.plugin as string[], + useChatStore.getState().currentSession().mask?.plugin || [], ); return stream( path, diff --git a/app/client/platforms/moonshot.ts b/app/client/platforms/moonshot.ts index c38d3317b..cd10d2f6c 100644 --- a/app/client/platforms/moonshot.ts +++ b/app/client/platforms/moonshot.ts @@ -125,7 +125,7 @@ export class MoonshotApi implements LLMApi { const [tools, funcs] = usePluginStore .getState() .getAsTools( - useChatStore.getState().currentSession().mask?.plugin as string[], + useChatStore.getState().currentSession().mask?.plugin || [], ); return stream( chatPath, diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index b3b306d1d..780ef0676 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -244,7 +244,7 @@ export class ChatGPTApi implements LLMApi { const [tools, funcs] = usePluginStore .getState() .getAsTools( - useChatStore.getState().currentSession().mask?.plugin as string[], + useChatStore.getState().currentSession().mask?.plugin || [], ); // console.log("getAsTools", tools, funcs); stream( From 23ac2efd89139d3112981680b7dd8c2e1b283e3a Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sat, 7 Sep 2024 22:12:42 +0800 Subject: [PATCH 53/79] hotfix and update version --- app/store/plugin.ts | 2 +- src-tauri/tauri.conf.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/store/plugin.ts b/app/store/plugin.ts index 74f0fbe17..2356c6db0 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -199,7 +199,7 @@ export const usePluginStore = createPersistStore( getAsTools(ids: string[]) { const plugins = get().plugins; - const selected = ids + const selected = (ids || []) .map((id) => plugins[id]) .filter((i) => i) .map((p) => FunctionToolService.add(p)); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0f2a84a53..78835d24d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "NextChat", - "version": "2.15.0" + "version": "2.15.1" }, "tauri": { "allowlist": { From 992c3a5d3a9fd08ecc46a26d12b91f9a1fd87c1a Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Sun, 8 Sep 2024 13:23:40 +0800 Subject: [PATCH 54/79] fix: safaLocalStorage --- app/components/chat.tsx | 7 ++-- app/components/error.tsx | 4 +-- app/components/mask.tsx | 15 ++------- app/locales/index.ts | 13 +++----- app/store/chat.ts | 10 +++++- app/store/mask.ts | 10 +++++- app/utils.ts | 60 ++++++++++++++++++++++++++++++++++ app/utils/indexedDB-storage.ts | 3 ++ 8 files changed, 96 insertions(+), 26 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index dad1933ac..77d351f30 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -67,6 +67,7 @@ import { isVisionModel, isDalle3, showPlugins, + safeLocalStorage, } from "../utils"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; @@ -109,6 +110,8 @@ import { getClientConfig } from "../config/client"; import { useAllModels } from "../utils/hooks"; import { MultimodalContent } from "../client/api"; +const localStorage = safeLocalStorage(); + const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , }); @@ -941,7 +944,7 @@ function _Chat() { .onUserInput(userInput, attachImages) .then(() => setIsLoading(false)); setAttachImages([]); - localStorage.setItem(LAST_INPUT_KEY, userInput); + chatStore.setLastInput(userInput); setUserInput(""); setPromptHints([]); if (!isMobileScreen) inputRef.current?.focus(); @@ -1007,7 +1010,7 @@ function _Chat() { userInput.length <= 0 && !(e.metaKey || e.altKey || e.ctrlKey) ) { - setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? ""); + setUserInput(chatStore.lastInput ?? ""); e.preventDefault(); return; } diff --git a/app/components/error.tsx b/app/components/error.tsx index c90997d11..4fcf759c1 100644 --- a/app/components/error.tsx +++ b/app/components/error.tsx @@ -8,6 +8,7 @@ import { ISSUE_URL } from "../constant"; import Locale from "../locales"; import { showConfirm } from "./ui-lib"; import { useSyncStore } from "../store/sync"; +import { useChatStore } from "../store/chat"; interface IErrorBoundaryState { hasError: boolean; @@ -30,8 +31,7 @@ export class ErrorBoundary extends React.Component { try { useSyncStore.getState().export(); } finally { - localStorage.clear(); - location.reload(); + useChatStore.getState().clearAllData(); } } diff --git a/app/components/mask.tsx b/app/components/mask.tsx index 62503c37a..ee6c7da97 100644 --- a/app/components/mask.tsx +++ b/app/components/mask.tsx @@ -426,16 +426,7 @@ export function MaskPage() { const maskStore = useMaskStore(); const chatStore = useChatStore(); - const [filterLang, setFilterLang] = useState( - () => localStorage.getItem("Mask-language") as Lang | undefined, - ); - useEffect(() => { - if (filterLang) { - localStorage.setItem("Mask-language", filterLang); - } else { - localStorage.removeItem("Mask-language"); - } - }, [filterLang]); + const filterLang = maskStore.language; const allMasks = maskStore .getAll() @@ -542,9 +533,9 @@ export function MaskPage() { onChange={(e) => { const value = e.currentTarget.value; if (value === Locale.Settings.Lang.All) { - setFilterLang(undefined); + maskStore.setLanguage(undefined); } else { - setFilterLang(value as Lang); + maskStore.setLanguage(value as Lang); } }} > diff --git a/app/locales/index.ts b/app/locales/index.ts index acdb3e878..ff7e3a262 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -18,10 +18,13 @@ import ar from "./ar"; import bn from "./bn"; import sk from "./sk"; import { merge } from "../utils/merge"; +import { safeLocalStorage } from "@/app/utils"; import type { LocaleType } from "./cn"; export type { LocaleType, PartialLocaleType } from "./cn"; +const localStorage = safeLocalStorage(); + const ALL_LANGS = { cn, en, @@ -82,17 +85,11 @@ merge(fallbackLang, targetLang); export default fallbackLang as LocaleType; function getItem(key: string) { - try { - return localStorage.getItem(key); - } catch { - return null; - } + return localStorage.getItem(key); } function setItem(key: string, value: string) { - try { - localStorage.setItem(key, value); - } catch {} + localStorage.setItem(key, value); } function getLanguage() { diff --git a/app/store/chat.ts b/app/store/chat.ts index 8b0cc39eb..ef483c2e7 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -26,9 +26,11 @@ import { nanoid } from "nanoid"; import { createPersistStore } from "../utils/store"; import { collectModelsWithDefaultModel } from "../utils/model"; import { useAccessStore } from "./access"; -import { isDalle3 } from "../utils"; +import { isDalle3, safeLocalStorage } from "../utils"; import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; +const localStorage = safeLocalStorage(); + export type ChatMessageTool = { id: string; index?: number; @@ -179,6 +181,7 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) { const DEFAULT_CHAT_STATE = { sessions: [createEmptySession()], currentSessionIndex: 0, + lastInput: "", }; export const useChatStore = createPersistStore( @@ -700,6 +703,11 @@ export const useChatStore = createPersistStore( localStorage.clear(); location.reload(); }, + setLastInput(lastInput: string) { + set({ + lastInput, + }); + }, }; return methods; diff --git a/app/store/mask.ts b/app/store/mask.ts index 083121b65..0c74a892e 100644 --- a/app/store/mask.ts +++ b/app/store/mask.ts @@ -23,9 +23,12 @@ export type Mask = { export const DEFAULT_MASK_STATE = { masks: {} as Record, + language: undefined as Lang | undefined, }; -export type MaskState = typeof DEFAULT_MASK_STATE; +export type MaskState = typeof DEFAULT_MASK_STATE & { + language?: Lang | undefined; +}; export const DEFAULT_MASK_AVATAR = "gpt-bot"; export const createEmptyMask = () => @@ -102,6 +105,11 @@ export const useMaskStore = createPersistStore( search(text: string) { return Object.values(get().masks); }, + setLanguage(language: Lang | undefined) { + set({ + language, + }); + }, }), { name: StoreKey.Mask, diff --git a/app/utils.ts b/app/utils.ts index 60041ba06..bf7450929 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -318,3 +318,63 @@ export function adapter(config: Record) { : path; return fetch(fetchUrl as string, { ...rest, responseType: "text" }); } + +export function safeLocalStorage(): { + getItem: (key: string) => string | null; + setItem: (key: string, value: string) => void; + removeItem: (key: string) => void; + clear: () => void; +} { + let storage: Storage | null; + + try { + if (typeof window !== "undefined" && window.localStorage) { + storage = window.localStorage; + } else { + storage = null; + } + } catch (e) { + console.error("localStorage is not available:", e); + storage = null; + } + + return { + getItem(key: string): string | null { + if (storage) { + return storage.getItem(key); + } else { + console.warn( + `Attempted to get item "${key}" from localStorage, but localStorage is not available.`, + ); + return null; + } + }, + setItem(key: string, value: string): void { + if (storage) { + storage.setItem(key, value); + } else { + console.warn( + `Attempted to set item "${key}" in localStorage, but localStorage is not available.`, + ); + } + }, + removeItem(key: string): void { + if (storage) { + storage.removeItem(key); + } else { + console.warn( + `Attempted to remove item "${key}" from localStorage, but localStorage is not available.`, + ); + } + }, + clear(): void { + if (storage) { + storage.clear(); + } else { + console.warn( + "Attempted to clear localStorage, but localStorage is not available.", + ); + } + }, + }; +} diff --git a/app/utils/indexedDB-storage.ts b/app/utils/indexedDB-storage.ts index da3094550..51417e9f3 100644 --- a/app/utils/indexedDB-storage.ts +++ b/app/utils/indexedDB-storage.ts @@ -1,5 +1,8 @@ import { StateStorage } from "zustand/middleware"; import { get, set, del, clear } from "idb-keyval"; +import { safeLocalStorage } from "@/app/utils"; + +const localStorage = safeLocalStorage(); class IndexedDBStorage implements StateStorage { public async getItem(name: string): Promise { From f2195154f6a94e3ac324465c1adc6150180a186e Mon Sep 17 00:00:00 2001 From: DDMeaqua Date: Mon, 9 Sep 2024 18:55:37 +0800 Subject: [PATCH 55/79] feat: add shortcut key --- app/components/chat.module.scss | 49 +++++++++++++ app/components/chat.tsx | 118 ++++++++++++++++++++++++++++++++ app/locales/cn.ts | 9 +++ app/locales/en.ts | 8 +++ app/locales/tw.ts | 8 +++ 5 files changed, 192 insertions(+) diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 7176399cc..98972f623 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -646,3 +646,52 @@ bottom: 30px; } } + +.shortcut-key-container { + padding: 10px; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.shortcut-key-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 16px; +} + +.shortcut-key-item { + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid #ddd; + border-radius: 10px; + padding: 10px; + background-color: #fff; +} + +.shortcut-key-title { + font-size: 14px; + color: #333; + margin-bottom: 8px; +} + +.shortcut-key-keys { + display: flex; + gap: 8px; +} + +.shortcut-key { + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #ddd; + border-radius: 8px; + padding: 4px; + background-color: #f9f9f9; + min-width: 32px; +} + +.shortcut-key span { + font-size: 12px; +} \ No newline at end of file diff --git a/app/components/chat.tsx b/app/components/chat.tsx index dad1933ac..ab94ab41d 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -829,6 +829,57 @@ export function DeleteImageButton(props: { deleteImage: () => void }) { ); } +export function ShortcutKeyModal(props: { onClose: () => void }) { + const shortcuts = [ + { title: Locale.Chat.ShortcutKey.newChat, keys: ["⌘", "Shift", "o"] }, + { title: Locale.Chat.ShortcutKey.focusInput, keys: ["Shift", "Esc"] }, + { title: Locale.Chat.ShortcutKey.copyLastCode, keys: ["⌘", "Shift", ";"] }, + { + title: Locale.Chat.ShortcutKey.copyLastMessage, + keys: ["⌘", "Shift", "c"], + }, + { title: Locale.Chat.ShortcutKey.showShortcutKey, keys: ["⌘", "/"] }, + ]; + return ( +
+ } + key="ok" + onClick={() => { + props.onClose(); + }} + />, + ]} + > +
+
+ {shortcuts.map((shortcut, index) => ( +
+
+ {shortcut.title} +
+
+ {shortcut.keys.map((key, i) => ( +
+ {key} +
+ ))} +
+
+ ))} +
+
+
+
+ ); +} + function _Chat() { type RenderMessage = ChatMessage & { preview?: boolean }; @@ -1373,6 +1424,69 @@ function _Chat() { setAttachImages(images); } + // 快捷键 + const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false); + + useEffect(() => { + const handleKeyDown = (event) => { + // 打开新聊天 command + shift + o + if ( + (event.metaKey || event.ctrlKey) && + event.shiftKey && + event.key.toLowerCase() === "o" + ) { + event.preventDefault(); + setTimeout(() => { + chatStore.newSession(); + navigate(Path.Chat); + }, 10); + } + // 聚焦聊天输入 shift + esc + else if (event.shiftKey && event.key.toLowerCase() === "escape") { + event.preventDefault(); + inputRef.current?.focus(); + } + // 复制最后一个代码块 command + shift + ; + else if ( + (event.metaKey || event.ctrlKey) && + event.shiftKey && + event.code === "Semicolon" + ) { + event.preventDefault(); + const copyCodeButton = document.querySelectorAll(".copy-code-button"); + if (copyCodeButton.length > 0) { + copyCodeButton[copyCodeButton.length - 1].click(); + } + } + // 复制最后一个回复 command + shift + c + else if ( + (event.metaKey || event.ctrlKey) && + event.shiftKey && + event.key.toLowerCase() === "c" + ) { + event.preventDefault(); + const lastNonUserMessage = messages + .filter((message) => message.role !== "user") + .pop(); + if (lastNonUserMessage) { + const lastMessageContent = getMessageTextContent(lastNonUserMessage); + copyToClipboard(lastMessageContent); + } + } + // 展示快捷键 command + / + else if ((event.metaKey || event.ctrlKey) && event.key === "/") { + event.preventDefault(); + setShowShortcutKeyModal(true); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [messages, chatStore, navigate]); + return (
@@ -1760,6 +1874,10 @@ function _Chat() { }} /> )} + + {showShortcutKeyModal && ( + setShowShortcutKeyModal(false)} /> + )}
); } diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 33e368f69..24f707e40 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -1,3 +1,4 @@ +import { ShortcutKeyModal } from "../components/chat"; import { getClientConfig } from "../config/client"; import { SubmitKey } from "../store/config"; @@ -81,6 +82,14 @@ const cn = { SaveAs: "存为面具", }, IsContext: "预设提示词", + ShortcutKey: { + Title: "键盘快捷方式", + newChat: "打开新聊天", + focusInput: "聚焦输入框", + copyLastMessage: "复制最后一个回复", + copyLastCode: "复制最后一个代码块", + showShortcutKey: "显示快捷方式", + }, }, Export: { Title: "分享聊天记录", diff --git a/app/locales/en.ts b/app/locales/en.ts index 403b9b687..09b76f1fa 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -83,6 +83,14 @@ const en: LocaleType = { SaveAs: "Save as Mask", }, IsContext: "Contextual Prompt", + ShortcutKey: { + Title: "Keyboard Shortcuts", + newChat: "Open New Chat", + focusInput: "Focus Input Field", + copyLastMessage: "Copy Last Reply", + copyLastCode: "Copy Last Code Block", + showShortcutKey: "Show Shortcuts", + }, }, Export: { Title: "Export Messages", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 6b2c0fd65..c54a7b8c5 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -81,6 +81,14 @@ const tw = { SaveAs: "另存新檔", }, IsContext: "預設提示詞", + ShortcutKey: { + Title: "鍵盤快捷方式", + newChat: "打開新聊天", + focusInput: "聚焦輸入框", + copyLastMessage: "複製最後一個回覆", + copyLastCode: "複製最後一個代碼塊", + showShortcutKey: "顯示快捷方式", + }, }, Export: { Title: "將聊天記錄匯出為 Markdown", From 7804182d0d027f630497c911652cd877ea0cc30a Mon Sep 17 00:00:00 2001 From: DDMeaqua Date: Mon, 9 Sep 2024 19:18:12 +0800 Subject: [PATCH 56/79] fix: type error --- app/components/chat.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index ab94ab41d..085292585 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1428,7 +1428,7 @@ function _Chat() { const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false); useEffect(() => { - const handleKeyDown = (event) => { + const handleKeyDown = (event: any) => { // 打开新聊天 command + shift + o if ( (event.metaKey || event.ctrlKey) && @@ -1453,7 +1453,8 @@ function _Chat() { event.code === "Semicolon" ) { event.preventDefault(); - const copyCodeButton = document.querySelectorAll(".copy-code-button"); + const copyCodeButton = + document.querySelectorAll(".copy-code-button"); if (copyCodeButton.length > 0) { copyCodeButton[copyCodeButton.length - 1].click(); } From 61245e3d7e41064bc9b5a431848489a3d82c2ef5 Mon Sep 17 00:00:00 2001 From: DDMeaqua Date: Mon, 9 Sep 2024 19:29:10 +0800 Subject: [PATCH 57/79] fix: dark theme css --- app/components/chat.module.scss | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 98972f623..5145188ec 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -664,15 +664,17 @@ display: flex; flex-direction: column; overflow: hidden; - border: 1px solid #ddd; + border: var(--border-in-light); border-radius: 10px; padding: 10px; - background-color: #fff; + background-color: var(--white); + box-shadow: var(--card-shadow); + transition: background-color 0.3s ease, box-shadow 0.3s ease; } .shortcut-key-title { font-size: 14px; - color: #333; + color: var(--black); margin-bottom: 8px; } @@ -685,13 +687,14 @@ display: flex; align-items: center; justify-content: center; - border: 1px solid #ddd; + border: var(--border-in-light); border-radius: 8px; padding: 4px; - background-color: #f9f9f9; + background-color: var(--gray); min-width: 32px; } .shortcut-key span { font-size: 12px; + color: var(--black); } \ No newline at end of file From e578c5f3ade565bc179ac2b639fa4d64b7d12a05 Mon Sep 17 00:00:00 2001 From: DDMeaqua Date: Tue, 10 Sep 2024 12:01:51 +0800 Subject: [PATCH 58/79] =?UTF-8?q?chore:=20=E6=A0=B7=E5=BC=8F=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/chat.module.scss | 10 +++------- app/components/chat.tsx | 20 +++++++++++++++----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 5145188ec..73542fc67 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -656,26 +656,22 @@ .shortcut-key-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 16px; } .shortcut-key-item { display: flex; - flex-direction: column; + justify-content: space-between; + align-items: center; overflow: hidden; - border: var(--border-in-light); - border-radius: 10px; padding: 10px; background-color: var(--white); - box-shadow: var(--card-shadow); - transition: background-color 0.3s ease, box-shadow 0.3s ease; } .shortcut-key-title { font-size: 14px; color: var(--black); - margin-bottom: 8px; } .shortcut-key-keys { diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 085292585..a91c8af7e 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -830,15 +830,25 @@ export function DeleteImageButton(props: { deleteImage: () => void }) { } export function ShortcutKeyModal(props: { onClose: () => void }) { + const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; const shortcuts = [ - { title: Locale.Chat.ShortcutKey.newChat, keys: ["⌘", "Shift", "o"] }, + { + title: Locale.Chat.ShortcutKey.newChat, + keys: isMac ? ["⌘", "Shift", "O"] : ["Ctrl", "Shift", "O"], + }, { title: Locale.Chat.ShortcutKey.focusInput, keys: ["Shift", "Esc"] }, - { title: Locale.Chat.ShortcutKey.copyLastCode, keys: ["⌘", "Shift", ";"] }, + { + title: Locale.Chat.ShortcutKey.copyLastCode, + keys: isMac ? ["⌘", "Shift", ";"] : ["Ctrl", "Shift", ";"], + }, { title: Locale.Chat.ShortcutKey.copyLastMessage, - keys: ["⌘", "Shift", "c"], + keys: isMac ? ["⌘", "Shift", "C"] : ["Ctrl", "Shift", "C"], + }, + { + title: Locale.Chat.ShortcutKey.showShortcutKey, + keys: isMac ? ["⌘", "/"] : ["Ctrl", "/"], }, - { title: Locale.Chat.ShortcutKey.showShortcutKey, keys: ["⌘", "/"] }, ]; return (
@@ -1424,7 +1434,7 @@ function _Chat() { setAttachImages(images); } - // 快捷键 + // 快捷键 shortcut keys const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false); useEffect(() => { From 18e2403b01943ad245e84756695aa9db0a70ad3b Mon Sep 17 00:00:00 2001 From: DDMeaqua Date: Tue, 10 Sep 2024 14:30:51 +0800 Subject: [PATCH 59/79] =?UTF-8?q?chore:=20=E6=9B=B4=E6=8D=A2icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/chat.tsx | 9 +++++++++ app/icons/shortcutkey.svg | 1 + 2 files changed, 10 insertions(+) create mode 100644 app/icons/shortcutkey.svg diff --git a/app/components/chat.tsx b/app/components/chat.tsx index a91c8af7e..1fc10c35c 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -42,6 +42,7 @@ import SizeIcon from "../icons/size.svg"; import QualityIcon from "../icons/hd.svg"; import StyleIcon from "../icons/palette.svg"; import PluginIcon from "../icons/plugin.svg"; +import ShortcutkeyIcon from "../icons/shortcutkey.svg"; import { ChatMessage, @@ -437,6 +438,7 @@ export function ChatActions(props: { showPromptHints: () => void; hitBottom: boolean; uploading: boolean; + setShowShortcutKeyModal: () => void; }) { const config = useAppConfig(); const navigate = useNavigate(); @@ -755,6 +757,12 @@ export function ChatActions(props: { }} /> )} + + props.setShowShortcutKeyModal(true)} + text={Locale.Chat.ShortcutKey.Title} + icon={} + />
); } @@ -1814,6 +1822,7 @@ function _Chat() { setUserInput("/"); onSearch(""); }} + setShowShortcutKeyModal={setShowShortcutKeyModal} />
+
+ } + bordered + title={Locale.Chat.Actions.RefreshTitle} + onClick={() => { + showToast(Locale.Chat.Actions.RefreshToast); + chatStore.summarizeSession(true); + }} + /> +
{!isMobileScreen && (
= SUMMARIZE_MIN_LEN + (config.enableAutoGenerateTitle && + session.topic === DEFAULT_TOPIC && + countMessages(messages) >= SUMMARIZE_MIN_LEN) || + refreshTitle ) { - const topicMessages = messages.concat( - createMessage({ - role: "user", - content: Locale.Store.Prompt.Topic, - }), - ); + const topicMessages = messages + .slice( + messages.length - modelConfig.historyMessageCount, + messages.length, + ) + .concat( + createMessage({ + role: "user", + content: Locale.Store.Prompt.Topic, + }), + ); api.llm.chat({ messages: topicMessages, config: { From 37c0cfe1e9d2568138e820634ad9404c0c5fb21f Mon Sep 17 00:00:00 2001 From: skymkmk Date: Fri, 13 Sep 2024 21:13:19 +0800 Subject: [PATCH 72/79] translation: translations by claude for manual refresh --- app/locales/ar.ts | 2 ++ app/locales/bn.ts | 2 ++ app/locales/cn.ts | 2 ++ app/locales/cs.ts | 2 ++ app/locales/de.ts | 2 ++ app/locales/en.ts | 2 ++ app/locales/es.ts | 2 ++ app/locales/fr.ts | 2 ++ app/locales/id.ts | 2 ++ app/locales/it.ts | 2 ++ app/locales/jp.ts | 2 ++ app/locales/ko.ts | 2 ++ app/locales/no.ts | 2 ++ app/locales/pt.ts | 2 ++ app/locales/ru.ts | 2 ++ app/locales/sk.ts | 2 ++ app/locales/tr.ts | 2 ++ app/locales/tw.ts | 2 ++ app/locales/vi.ts | 2 ++ 19 files changed, 38 insertions(+) diff --git a/app/locales/ar.ts b/app/locales/ar.ts index 9bd491083..0c3fef720 100644 --- a/app/locales/ar.ts +++ b/app/locales/ar.ts @@ -43,6 +43,8 @@ const ar: PartialLocaleType = { PinToastAction: "عرض", Delete: "حذف", Edit: "تحرير", + RefreshTitle: "تحديث العنوان", + RefreshToast: "تم إرسال طلب تحديث العنوان", }, Commands: { new: "دردشة جديدة", diff --git a/app/locales/bn.ts b/app/locales/bn.ts index acabc8e2a..eeb1608e0 100644 --- a/app/locales/bn.ts +++ b/app/locales/bn.ts @@ -43,6 +43,8 @@ const bn: PartialLocaleType = { PinToastAction: "দেখুন", Delete: "মুছে ফেলুন", Edit: "সম্পাদনা করুন", + RefreshTitle: "শিরোনাম রিফ্রেশ করুন", + RefreshToast: "শিরোনাম রিফ্রেশ অনুরোধ পাঠানো হয়েছে", }, Commands: { new: "নতুন চ্যাট", diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 92e81bcb1..b5fcdbb8b 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -44,6 +44,8 @@ const cn = { Delete: "删除", Edit: "编辑", FullScreen: "全屏", + RefreshTitle: "刷新标题", + RefreshToast: "已发送刷新标题请求", }, Commands: { new: "新建聊天", diff --git a/app/locales/cs.ts b/app/locales/cs.ts index d16c474e8..d0c7f392c 100644 --- a/app/locales/cs.ts +++ b/app/locales/cs.ts @@ -43,6 +43,8 @@ const cs: PartialLocaleType = { PinToastAction: "Zobrazit", Delete: "Smazat", Edit: "Upravit", + RefreshTitle: "Obnovit název", + RefreshToast: "Požadavek na obnovení názvu byl odeslán", }, Commands: { new: "Nová konverzace", diff --git a/app/locales/de.ts b/app/locales/de.ts index a1f817047..da2859f01 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -43,6 +43,8 @@ const de: PartialLocaleType = { PinToastAction: "Ansehen", Delete: "Löschen", Edit: "Bearbeiten", + RefreshTitle: "Titel aktualisieren", + RefreshToast: "Anfrage zur Titelaktualisierung gesendet", }, Commands: { new: "Neues Gespräch", diff --git a/app/locales/en.ts b/app/locales/en.ts index 09b76f1fa..0a2fcc275 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -45,6 +45,8 @@ const en: LocaleType = { Delete: "Delete", Edit: "Edit", FullScreen: "FullScreen", + RefreshTitle: "Refresh Title", + RefreshToast: "Title refresh request sent", }, Commands: { new: "Start a new chat", diff --git a/app/locales/es.ts b/app/locales/es.ts index 5e4f900b7..6f2344c2c 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -44,6 +44,8 @@ const es: PartialLocaleType = { PinToastAction: "Ver", Delete: "Eliminar", Edit: "Editar", + RefreshTitle: "Actualizar título", + RefreshToast: "Se ha enviado la solicitud de actualización del título", }, Commands: { new: "Nueva conversación", diff --git a/app/locales/fr.ts b/app/locales/fr.ts index 65efc32b8..5a2a86ad9 100644 --- a/app/locales/fr.ts +++ b/app/locales/fr.ts @@ -43,6 +43,8 @@ const fr: PartialLocaleType = { PinToastAction: "Voir", Delete: "Supprimer", Edit: "Modifier", + RefreshTitle: "Actualiser le titre", + RefreshToast: "Demande d'actualisation du titre envoyée", }, Commands: { new: "Nouvelle discussion", diff --git a/app/locales/id.ts b/app/locales/id.ts index 3ac7af490..09bf0d09a 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -43,6 +43,8 @@ const id: PartialLocaleType = { PinToastAction: "Lihat", Delete: "Hapus", Edit: "Edit", + RefreshTitle: "Segarkan Judul", + RefreshToast: "Permintaan penyegaran judul telah dikirim", }, Commands: { new: "Obrolan Baru", diff --git a/app/locales/it.ts b/app/locales/it.ts index 1a54cfa43..67983a8db 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -43,6 +43,8 @@ const it: PartialLocaleType = { PinToastAction: "Visualizza", Delete: "Elimina", Edit: "Modifica", + RefreshTitle: "Aggiorna titolo", + RefreshToast: "Richiesta di aggiornamento del titolo inviata", }, Commands: { new: "Nuova chat", diff --git a/app/locales/jp.ts b/app/locales/jp.ts index 6aaf0ba67..efb080a20 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -43,6 +43,8 @@ const jp: PartialLocaleType = { PinToastAction: "見る", Delete: "削除", Edit: "編集", + RefreshTitle: "タイトルを更新", + RefreshToast: "タイトル更新リクエストが送信されました", }, Commands: { new: "新しいチャット", diff --git a/app/locales/ko.ts b/app/locales/ko.ts index 563827fb9..827c5fc0a 100644 --- a/app/locales/ko.ts +++ b/app/locales/ko.ts @@ -43,6 +43,8 @@ const ko: PartialLocaleType = { PinToastAction: "보기", Delete: "삭제", Edit: "편집", + RefreshTitle: "제목 새로고침", + RefreshToast: "제목 새로고침 요청이 전송되었습니다", }, Commands: { new: "새 채팅", diff --git a/app/locales/no.ts b/app/locales/no.ts index d7dc16b3f..0fe2ad456 100644 --- a/app/locales/no.ts +++ b/app/locales/no.ts @@ -44,6 +44,8 @@ const no: PartialLocaleType = { PinToastAction: "Se", Delete: "Slett", Edit: "Rediger", + RefreshTitle: "Oppdater tittel", + RefreshToast: "Forespørsel om titteloppdatering sendt", }, Commands: { new: "Ny samtale", diff --git a/app/locales/pt.ts b/app/locales/pt.ts index 9fd13ba1c..03610ecce 100644 --- a/app/locales/pt.ts +++ b/app/locales/pt.ts @@ -43,6 +43,8 @@ const pt: PartialLocaleType = { PinToastAction: "Visualizar", Delete: "Deletar", Edit: "Editar", + RefreshTitle: "Atualizar Título", + RefreshToast: "Solicitação de atualização de título enviada", }, Commands: { new: "Iniciar um novo chat", diff --git a/app/locales/ru.ts b/app/locales/ru.ts index e983dcddb..f4d76f810 100644 --- a/app/locales/ru.ts +++ b/app/locales/ru.ts @@ -43,6 +43,8 @@ const ru: PartialLocaleType = { PinToastAction: "Просмотреть", Delete: "Удалить", Edit: "Редактировать", + RefreshTitle: "Обновить заголовок", + RefreshToast: "Запрос на обновление заголовка отправлен", }, Commands: { new: "Новый чат", diff --git a/app/locales/sk.ts b/app/locales/sk.ts index 2586aaaa7..61ea18b78 100644 --- a/app/locales/sk.ts +++ b/app/locales/sk.ts @@ -45,6 +45,8 @@ const sk: PartialLocaleType = { PinToastAction: "Zobraziť", Delete: "Vymazať", Edit: "Upraviť", + RefreshTitle: "Obnoviť názov", + RefreshToast: "Požiadavka na obnovenie názvu bola odoslaná", }, Commands: { new: "Začať nový chat", diff --git a/app/locales/tr.ts b/app/locales/tr.ts index ac410615e..6fccdda5e 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -43,6 +43,8 @@ const tr: PartialLocaleType = { PinToastAction: "Görünüm", Delete: "Sil", Edit: "Düzenle", + RefreshTitle: "Başlığı Yenile", + RefreshToast: "Başlık yenileme isteği gönderildi", }, Commands: { new: "Yeni sohbet", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index c54a7b8c5..a080b000c 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -43,6 +43,8 @@ const tw = { PinToastAction: "檢視", Delete: "刪除", Edit: "編輯", + RefreshTitle: "刷新標題", + RefreshToast: "已發送刷新標題請求", }, Commands: { new: "新建聊天", diff --git a/app/locales/vi.ts b/app/locales/vi.ts index 9a21ee406..00934b3a6 100644 --- a/app/locales/vi.ts +++ b/app/locales/vi.ts @@ -43,6 +43,8 @@ const vi: PartialLocaleType = { PinToastAction: "Xem", Delete: "Xóa", Edit: "Chỉnh sửa", + RefreshTitle: "Làm mới tiêu đề", + RefreshToast: "Đã gửi yêu cầu làm mới tiêu đề", }, Commands: { new: "Tạo cuộc trò chuyện mới", From 93bc2f5870976a17ce9deacd29816022f5036c52 Mon Sep 17 00:00:00 2001 From: skymkmk Date: Fri, 13 Sep 2024 20:30:12 +0800 Subject: [PATCH 73/79] feat: now user can choose their own summarize model --- app/components/model-config.tsx | 25 +++++++++++ app/store/chat.ts | 75 +++++++++++++-------------------- app/store/config.ts | 13 +++++- 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 6ce25f664..948c9fb29 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -12,6 +12,7 @@ export function ModelConfigList(props: { }) { const allModels = useAllModels(); const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`; + const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`; return ( <> @@ -228,6 +229,30 @@ export function ModelConfigList(props: { } > + + + ); } diff --git a/app/store/chat.ts b/app/store/chat.ts index 58c105e7e..4332c2246 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -1,33 +1,29 @@ -import { trimTopic, getMessageTextContent } from "../utils"; +import { getMessageTextContent, trimTopic } from "../utils"; -import Locale, { getLang } from "../locales"; +import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; +import { nanoid } from "nanoid"; +import type { + ClientApi, + MultimodalContent, + RequestMessage, +} from "../client/api"; +import { getClientApi } from "../client/api"; +import { ChatControllerPool } from "../client/controller"; import { showToast } from "../components/ui-lib"; -import { ModelConfig, ModelType, useAppConfig } from "./config"; -import { createEmptyMask, Mask } from "./mask"; import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, DEFAULT_SYSTEM_TEMPLATE, KnowledgeCutOffDate, StoreKey, - SUMMARIZE_MODEL, - GEMINI_SUMMARIZE_MODEL, } from "../constant"; -import { getClientApi } from "../client/api"; -import type { - ClientApi, - RequestMessage, - MultimodalContent, -} from "../client/api"; -import { ChatControllerPool } from "../client/controller"; -import { prettyObject } from "../utils/format"; -import { estimateTokenLength } from "../utils/token"; -import { nanoid } from "nanoid"; -import { createPersistStore } from "../utils/store"; -import { collectModelsWithDefaultModel } from "../utils/model"; -import { useAccessStore } from "./access"; +import Locale, { getLang } from "../locales"; import { isDalle3, safeLocalStorage } from "../utils"; -import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; +import { prettyObject } from "../utils/format"; +import { createPersistStore } from "../utils/store"; +import { estimateTokenLength } from "../utils/token"; +import { ModelConfig, ModelType, useAppConfig } from "./config"; +import { createEmptyMask, Mask } from "./mask"; const localStorage = safeLocalStorage(); @@ -106,27 +102,6 @@ function createEmptySession(): ChatSession { }; } -function getSummarizeModel(currentModel: string) { - // if it is using gpt-* models, force to use 4o-mini to summarize - if (currentModel.startsWith("gpt") || currentModel.startsWith("chatgpt")) { - const configStore = useAppConfig.getState(); - const accessStore = useAccessStore.getState(); - const allModel = collectModelsWithDefaultModel( - configStore.models, - [configStore.customModels, accessStore.customModels].join(","), - accessStore.defaultModel, - ); - const summarizeModel = allModel.find( - (m) => m.name === SUMMARIZE_MODEL && m.available, - ); - return summarizeModel?.name ?? currentModel; - } - if (currentModel.startsWith("gemini")) { - return GEMINI_SUMMARIZE_MODEL; - } - return currentModel; -} - function countMessages(msgs: ChatMessage[]) { return msgs.reduce( (pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)), @@ -581,7 +556,7 @@ export const useChatStore = createPersistStore( return; } - const providerName = modelConfig.providerName; + const providerName = modelConfig.compressProviderName; const api: ClientApi = getClientApi(providerName); // remove error messages if any @@ -603,7 +578,7 @@ export const useChatStore = createPersistStore( api.llm.chat({ messages: topicMessages, config: { - model: getSummarizeModel(session.mask.modelConfig.model), + model: modelConfig.compressModel, stream: false, providerName, }, @@ -666,7 +641,7 @@ export const useChatStore = createPersistStore( config: { ...modelcfg, stream: true, - model: getSummarizeModel(session.mask.modelConfig.model), + model: modelConfig.compressModel, }, onUpdate(message) { session.memoryPrompt = message; @@ -715,7 +690,7 @@ export const useChatStore = createPersistStore( }, { name: StoreKey.Chat, - version: 3.1, + version: 3.2, migrate(persistedState, version) { const state = persistedState as any; const newState = JSON.parse( @@ -762,6 +737,16 @@ export const useChatStore = createPersistStore( }); } + // add default summarize model for every session + if (version < 3.2) { + newState.sessions.forEach((s) => { + const config = useAppConfig.getState(); + s.mask.modelConfig.compressModel = config.modelConfig.compressModel; + s.mask.modelConfig.compressProviderName = + config.modelConfig.compressProviderName; + }); + } + return newState as any; }, }, diff --git a/app/store/config.ts b/app/store/config.ts index e8e3c9863..9985b9e76 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -50,7 +50,7 @@ export const DEFAULT_CONFIG = { models: DEFAULT_MODELS as any as LLMModel[], modelConfig: { - model: "gpt-3.5-turbo" as ModelType, + model: "gpt-4o-mini" as ModelType, providerName: "OpenAI" as ServiceProvider, temperature: 0.5, top_p: 1, @@ -60,6 +60,8 @@ export const DEFAULT_CONFIG = { sendMemory: true, historyMessageCount: 4, compressMessageLengthThreshold: 1000, + compressModel: "gpt-4o-mini" as ModelType, + compressProviderName: "OpenAI" as ServiceProvider, enableInjectSystemPrompts: true, template: config?.template ?? DEFAULT_INPUT_TEMPLATE, size: "1024x1024" as DalleSize, @@ -140,7 +142,7 @@ export const useAppConfig = createPersistStore( }), { name: StoreKey.Config, - version: 3.9, + version: 4, migrate(persistedState, version) { const state = persistedState as ChatConfig; @@ -178,6 +180,13 @@ export const useAppConfig = createPersistStore( : config?.template ?? DEFAULT_INPUT_TEMPLATE; } + if (version < 4) { + state.modelConfig.compressModel = + DEFAULT_CONFIG.modelConfig.compressModel; + state.modelConfig.compressProviderName = + DEFAULT_CONFIG.modelConfig.compressProviderName; + } + return state as any; }, }, From 1b869d930593060d416f565a4570ab3afdcda932 Mon Sep 17 00:00:00 2001 From: skymkmk Date: Fri, 13 Sep 2024 21:13:19 +0800 Subject: [PATCH 74/79] translation: translations by claude for new writings --- app/locales/ar.ts | 4 ++++ app/locales/bn.ts | 4 ++++ app/locales/cn.ts | 4 ++++ app/locales/cs.ts | 4 ++++ app/locales/de.ts | 4 ++++ app/locales/en.ts | 4 ++++ app/locales/es.ts | 4 ++++ app/locales/fr.ts | 4 ++++ app/locales/id.ts | 4 ++++ app/locales/it.ts | 4 ++++ app/locales/jp.ts | 4 ++++ app/locales/ko.ts | 4 ++++ app/locales/no.ts | 4 ++++ app/locales/pt.ts | 4 ++++ app/locales/ru.ts | 4 ++++ app/locales/sk.ts | 4 ++++ app/locales/tr.ts | 4 ++++ app/locales/tw.ts | 4 ++++ app/locales/vi.ts | 4 ++++ 19 files changed, 76 insertions(+) diff --git a/app/locales/ar.ts b/app/locales/ar.ts index 9bd491083..fef123047 100644 --- a/app/locales/ar.ts +++ b/app/locales/ar.ts @@ -404,6 +404,10 @@ const ar: PartialLocaleType = { }, Model: "النموذج", + CompressModel: { + Title: "نموذج الضغط", + SubTitle: "النموذج المستخدم لضغط السجل التاريخي", + }, Temperature: { Title: "العشوائية (temperature)", SubTitle: "كلما زادت القيمة، زادت العشوائية في الردود", diff --git a/app/locales/bn.ts b/app/locales/bn.ts index acabc8e2a..ea66ce646 100644 --- a/app/locales/bn.ts +++ b/app/locales/bn.ts @@ -411,6 +411,10 @@ const bn: PartialLocaleType = { }, Model: "মডেল (model)", + CompressModel: { + Title: "সংকোচন মডেল", + SubTitle: "ইতিহাস সংকুচিত করার জন্য ব্যবহৃত মডেল", + }, Temperature: { Title: "যাদুকরিতা (temperature)", SubTitle: "মান বাড়ালে উত্তর বেশি এলোমেলো হবে", diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 92e81bcb1..068429585 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -470,6 +470,10 @@ const cn = { }, Model: "模型 (model)", + CompressModel: { + Title: "压缩模型", + SubTitle: "用于压缩历史记录的模型", + }, Temperature: { Title: "随机性 (temperature)", SubTitle: "值越大,回复越随机", diff --git a/app/locales/cs.ts b/app/locales/cs.ts index d16c474e8..a23367472 100644 --- a/app/locales/cs.ts +++ b/app/locales/cs.ts @@ -410,6 +410,10 @@ const cs: PartialLocaleType = { }, Model: "Model (model)", + CompressModel: { + Title: "Kompresní model", + SubTitle: "Model používaný pro kompresi historie", + }, Temperature: { Title: "Náhodnost (temperature)", SubTitle: "Čím vyšší hodnota, tím náhodnější odpovědi", diff --git a/app/locales/de.ts b/app/locales/de.ts index a1f817047..56e787381 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -421,6 +421,10 @@ const de: PartialLocaleType = { }, Model: "Modell", + CompressModel: { + Title: "Kompressionsmodell", + SubTitle: "Modell zur Komprimierung des Verlaufs", + }, Temperature: { Title: "Zufälligkeit (temperature)", SubTitle: "Je höher der Wert, desto zufälliger die Antwort", diff --git a/app/locales/en.ts b/app/locales/en.ts index 09b76f1fa..41337ac2f 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -474,6 +474,10 @@ const en: LocaleType = { }, Model: "Model", + CompressModel: { + Title: "Compression Model", + SubTitle: "Model used to compress history", + }, Temperature: { Title: "Temperature", SubTitle: "A larger value makes the more random output", diff --git a/app/locales/es.ts b/app/locales/es.ts index 5e4f900b7..8fa42b659 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -423,6 +423,10 @@ const es: PartialLocaleType = { }, Model: "Modelo (model)", + CompressModel: { + Title: "Modelo de compresión", + SubTitle: "Modelo utilizado para comprimir el historial", + }, Temperature: { Title: "Aleatoriedad (temperature)", SubTitle: "Cuanto mayor sea el valor, más aleatorio será el resultado", diff --git a/app/locales/fr.ts b/app/locales/fr.ts index 65efc32b8..198fdddd4 100644 --- a/app/locales/fr.ts +++ b/app/locales/fr.ts @@ -422,6 +422,10 @@ const fr: PartialLocaleType = { }, Model: "Modèle", + CompressModel: { + Title: "Modèle de compression", + SubTitle: "Modèle utilisé pour compresser l'historique", + }, Temperature: { Title: "Aléatoire (temperature)", SubTitle: "Plus la valeur est élevée, plus les réponses sont aléatoires", diff --git a/app/locales/id.ts b/app/locales/id.ts index 3ac7af490..61530a775 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -411,6 +411,10 @@ const id: PartialLocaleType = { }, Model: "Model", + CompressModel: { + Title: "Model Kompresi", + SubTitle: "Model yang digunakan untuk mengompres riwayat", + }, Temperature: { Title: "Randomness (temperature)", SubTitle: "Semakin tinggi nilainya, semakin acak responsnya", diff --git a/app/locales/it.ts b/app/locales/it.ts index 1a54cfa43..57d9d0f15 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -423,6 +423,10 @@ const it: PartialLocaleType = { }, Model: "Modello (model)", + CompressModel: { + Title: "Modello di compressione", + SubTitle: "Modello utilizzato per comprimere la cronologia", + }, Temperature: { Title: "Casualità (temperature)", SubTitle: "Valore più alto, risposte più casuali", diff --git a/app/locales/jp.ts b/app/locales/jp.ts index 6aaf0ba67..b09489d9f 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -407,6 +407,10 @@ const jp: PartialLocaleType = { }, Model: "モデル (model)", + CompressModel: { + Title: "圧縮モデル", + SubTitle: "履歴を圧縮するために使用されるモデル", + }, Temperature: { Title: "ランダム性 (temperature)", SubTitle: "値が大きいほど応答がランダムになります", diff --git a/app/locales/ko.ts b/app/locales/ko.ts index 563827fb9..973927ed5 100644 --- a/app/locales/ko.ts +++ b/app/locales/ko.ts @@ -404,6 +404,10 @@ const ko: PartialLocaleType = { }, Model: "모델 (model)", + CompressModel: { + Title: "압축 모델", + SubTitle: "기록을 압축하는 데 사용되는 모델", + }, Temperature: { Title: "무작위성 (temperature)", SubTitle: "값이 클수록 응답이 더 무작위적", diff --git a/app/locales/no.ts b/app/locales/no.ts index d7dc16b3f..490d2bfda 100644 --- a/app/locales/no.ts +++ b/app/locales/no.ts @@ -415,6 +415,10 @@ const no: PartialLocaleType = { }, Model: "Modell", + CompressModel: { + Title: "Komprimeringsmodell", + SubTitle: "Modell brukt for å komprimere historikken", + }, Temperature: { Title: "Tilfeldighet (temperature)", SubTitle: "Høyere verdi gir mer tilfeldige svar", diff --git a/app/locales/pt.ts b/app/locales/pt.ts index 9fd13ba1c..5dadc8e3a 100644 --- a/app/locales/pt.ts +++ b/app/locales/pt.ts @@ -346,6 +346,10 @@ const pt: PartialLocaleType = { }, Model: "Modelo", + CompressModel: { + Title: "Modelo de Compressão", + SubTitle: "Modelo usado para comprimir o histórico", + }, Temperature: { Title: "Temperatura", SubTitle: "Um valor maior torna a saída mais aleatória", diff --git a/app/locales/ru.ts b/app/locales/ru.ts index e983dcddb..4c583618b 100644 --- a/app/locales/ru.ts +++ b/app/locales/ru.ts @@ -414,6 +414,10 @@ const ru: PartialLocaleType = { }, Model: "Модель", + CompressModel: { + Title: "Модель сжатия", + SubTitle: "Модель, используемая для сжатия истории", + }, Temperature: { Title: "Случайность (temperature)", SubTitle: "Чем больше значение, тем более случайные ответы", diff --git a/app/locales/sk.ts b/app/locales/sk.ts index 2586aaaa7..4b4085f4d 100644 --- a/app/locales/sk.ts +++ b/app/locales/sk.ts @@ -365,6 +365,10 @@ const sk: PartialLocaleType = { }, Model: "Model", + CompressModel: { + Title: "Kompresný model", + SubTitle: "Model používaný na kompresiu histórie", + }, Temperature: { Title: "Teplota", SubTitle: "Vyššia hodnota robí výstup náhodnejším", diff --git a/app/locales/tr.ts b/app/locales/tr.ts index ac410615e..edb4572bf 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -414,6 +414,10 @@ const tr: PartialLocaleType = { }, Model: "Model (model)", + CompressModel: { + Title: "Sıkıştırma Modeli", + SubTitle: "Geçmişi sıkıştırmak için kullanılan model", + }, Temperature: { Title: "Rastgelelik (temperature)", SubTitle: "Değer arttıkça yanıt daha rastgele olur", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index c54a7b8c5..88b86772c 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -368,6 +368,10 @@ const tw = { }, Model: "模型 (model)", + CompressModel: { + Title: "壓縮模型", + SubTitle: "用於壓縮歷史記錄的模型", + }, Temperature: { Title: "隨機性 (temperature)", SubTitle: "值越大,回應越隨機", diff --git a/app/locales/vi.ts b/app/locales/vi.ts index 9a21ee406..3b0456d1c 100644 --- a/app/locales/vi.ts +++ b/app/locales/vi.ts @@ -410,6 +410,10 @@ const vi: PartialLocaleType = { }, Model: "Mô hình (model)", + CompressModel: { + Title: "Mô hình nén", + SubTitle: "Mô hình được sử dụng để nén lịch sử", + }, Temperature: { Title: "Độ ngẫu nhiên (temperature)", SubTitle: "Giá trị càng lớn, câu trả lời càng ngẫu nhiên", From fa48ace39badb237728188482550ae5bb8f0e47a Mon Sep 17 00:00:00 2001 From: skymkmk Date: Sat, 14 Sep 2024 07:49:26 +0800 Subject: [PATCH 75/79] fix: prevent users from setting a extremly short history that resulting in no content being sent for the title summary --- app/store/chat.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index 1609666c3..d2a1fecd6 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -595,9 +595,13 @@ export const useChatStore = createPersistStore( countMessages(messages) >= SUMMARIZE_MIN_LEN) || refreshTitle ) { + const startIndex = Math.max( + 0, + messages.length - modelConfig.historyMessageCount, + ); const topicMessages = messages .slice( - messages.length - modelConfig.historyMessageCount, + startIndex < messages.length ? startIndex : messages.length - 1, messages.length, ) .concat( From 84a7afcd948743fa8c69b712d812ad6fbd73c5db Mon Sep 17 00:00:00 2001 From: evenwan Date: Sat, 14 Sep 2024 09:31:05 +0800 Subject: [PATCH 76/79] feat: Improve setting.model selector --- app/components/model-config.tsx | 22 +++++++++++++++------- app/components/ui-lib.module.scss | 6 ++++++ app/components/ui-lib.tsx | 12 +++++++++--- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 6ce25f664..6c40ab1b2 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -5,12 +5,17 @@ import Locale from "../locales"; import { InputRange } from "./input-range"; import { ListItem, Select } from "./ui-lib"; import { useAllModels } from "../utils/hooks"; +import { groupBy } from "lodash-es"; export function ModelConfigList(props: { modelConfig: ModelConfig; updateConfig: (updater: (config: ModelConfig) => void) => void; }) { const allModels = useAllModels(); + const groupModels = groupBy( + allModels.filter((v) => v.available), + "provider.providerName", + ); const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`; return ( @@ -19,6 +24,7 @@ export function ModelConfigList(props: { , + React.SelectHTMLAttributes & { + align?: "left" | "center"; + }, HTMLSelectElement >, ) { - const { className, children, ...otherProps } = props; + const { className, children, align, ...otherProps } = props; return ( -
+
From 9a5a3d4ce4e6b1c7210fc1b9d9e78231d4e2b3a8 Mon Sep 17 00:00:00 2001 From: DDMeaqua Date: Sat, 14 Sep 2024 16:06:18 +0800 Subject: [PATCH 77/79] fix: #5429 Anthropic authentication_error CORS --- app/api/anthropic.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/anthropic.ts b/app/api/anthropic.ts index 3d49f4c88..88f217a90 100644 --- a/app/api/anthropic.ts +++ b/app/api/anthropic.ts @@ -98,6 +98,7 @@ async function request(req: NextRequest) { headers: { "Content-Type": "application/json", "Cache-Control": "no-store", + "anthropic-dangerous-direct-browser-access": true, [authHeaderName]: authValue, "anthropic-version": req.headers.get("anthropic-version") || From 8ac9141a29b049a851c51ea3c65f08d18cfd8ce6 Mon Sep 17 00:00:00 2001 From: Meaqua Date: Sun, 15 Sep 2024 14:21:27 +0800 Subject: [PATCH 78/79] fix: ts error --- app/api/anthropic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/anthropic.ts b/app/api/anthropic.ts index 88f217a90..d7b070247 100644 --- a/app/api/anthropic.ts +++ b/app/api/anthropic.ts @@ -98,7 +98,7 @@ async function request(req: NextRequest) { headers: { "Content-Type": "application/json", "Cache-Control": "no-store", - "anthropic-dangerous-direct-browser-access": true, + "anthropic-dangerous-direct-browser-access": "true", [authHeaderName]: authValue, "anthropic-version": req.headers.get("anthropic-version") || From e986088becfeee53a21d136e8ac6ab484909f2fc Mon Sep 17 00:00:00 2001 From: DDDDD12138 Date: Sun, 15 Sep 2024 21:59:21 +0800 Subject: [PATCH 79/79] chore: correct typo --- app/components/chat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 17f8d3a34..3cc02d486 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -520,8 +520,8 @@ export function ChatActions(props: { // 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 isUnavailableModel = !models.some((m) => m.name === currentModel); + if (isUnavailableModel && models.length > 0) { // show next model to default model if exist let nextModel = models.find((model) => model.isDefault) || models[0]; chatStore.updateCurrentSession((session) => {