diff --git a/README.md b/README.md index 3ac537abc..0398a9bfe 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 [MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple [Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/ZBUEFA) diff --git a/app/api/cors/[...path]/route.ts b/app/api/cors/[...path]/route.ts deleted file mode 100644 index 1f70d6630..000000000 --- a/app/api/cors/[...path]/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -async function handle( - req: NextRequest, - { params }: { params: { path: string[] } }, -) { - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); - } - - const [protocol, ...subpath] = params.path; - const targetUrl = `${protocol}://${subpath.join("/")}`; - - const method = req.headers.get("method") ?? undefined; - const shouldNotHaveBody = ["get", "head"].includes( - method?.toLowerCase() ?? "", - ); - - const fetchOptions: RequestInit = { - headers: { - authorization: req.headers.get("authorization") ?? "", - }, - body: shouldNotHaveBody ? null : req.body, - method, - // @ts-ignore - duplex: "half", - }; - - const fetchResult = await fetch(targetUrl, fetchOptions); - - console.log("[Any Proxy]", targetUrl, { - status: fetchResult.status, - statusText: fetchResult.statusText, - }); - - return fetchResult; -} - -export const POST = handle; -export const GET = handle; -export const OPTIONS = handle; - -export const runtime = "edge"; diff --git a/app/api/upstash/[action]/[...key]/route.ts b/app/api/upstash/[action]/[...key]/route.ts new file mode 100644 index 000000000..fcfef4718 --- /dev/null +++ b/app/api/upstash/[action]/[...key]/route.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from "next/server"; + +async function handle( + req: NextRequest, + { params }: { params: { action: string; key: string[] } }, +) { + const requestUrl = new URL(req.url); + const endpoint = requestUrl.searchParams.get("endpoint"); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + const [...key] = params.key; + // only allow to request to *.upstash.io + if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.key.join("/"), + }, + { + status: 403, + }, + ); + } + + // only allow upstash get and set method + if (params.action !== "get" && params.action !== "set") { + console.log("[Upstash Route] forbidden action ", params.action); + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.action, + }, + { + status: 403, + }, + ); + } + + const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`; + + const method = req.method; + const shouldNotHaveBody = ["get", "head"].includes( + method?.toLowerCase() ?? "", + ); + + const fetchOptions: RequestInit = { + headers: { + authorization: req.headers.get("authorization") ?? "", + }, + body: shouldNotHaveBody ? null : req.body, + method, + // @ts-ignore + duplex: "half", + }; + + console.log("[Upstash Proxy]", targetUrl, fetchOptions); + const fetchResult = await fetch(targetUrl, fetchOptions); + + console.log("[Any Proxy]", targetUrl, { + status: fetchResult.status, + statusText: fetchResult.statusText, + }); + + return fetchResult; +} + +export const POST = handle; +export const GET = handle; +export const OPTIONS = handle; + +export const runtime = "edge"; diff --git a/app/api/webdav/[...path]/route.ts b/app/api/webdav/[...path]/route.ts new file mode 100644 index 000000000..c60ca18bb --- /dev/null +++ b/app/api/webdav/[...path]/route.ts @@ -0,0 +1,112 @@ +import { NextRequest, NextResponse } from "next/server"; +import { STORAGE_KEY } from "../../../constant"; +async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + const folder = STORAGE_KEY; + const fileName = `${folder}/backup.json`; + + const requestUrl = new URL(req.url); + let endpoint = requestUrl.searchParams.get("endpoint"); + if (!endpoint?.endsWith("/")) { + endpoint += "/"; + } + const endpointPath = params.path.join("/"); + + // only allow MKCOL, GET, PUT + if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.path.join("/"), + }, + { + status: 403, + }, + ); + } + + // for MKCOL request, only allow request ${folder} + if ( + req.method == "MKCOL" && + !new URL(endpointPath).pathname.endsWith(folder) + ) { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.path.join("/"), + }, + { + status: 403, + }, + ); + } + + // for GET request, only allow request ending with fileName + if ( + req.method == "GET" && + !new URL(endpointPath).pathname.endsWith(fileName) + ) { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.path.join("/"), + }, + { + status: 403, + }, + ); + } + + // for PUT request, only allow request ending with fileName + if ( + req.method == "PUT" && + !new URL(endpointPath).pathname.endsWith(fileName) + ) { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.path.join("/"), + }, + { + status: 403, + }, + ); + } + + const targetUrl = `${endpoint + endpointPath}`; + + const method = req.method; + const shouldNotHaveBody = ["get", "head"].includes( + method?.toLowerCase() ?? "", + ); + + const fetchOptions: RequestInit = { + headers: { + authorization: req.headers.get("authorization") ?? "", + }, + body: shouldNotHaveBody ? null : req.body, + method, + // @ts-ignore + duplex: "half", + }; + + const fetchResult = await fetch(targetUrl, fetchOptions); + + console.log("[Any Proxy]", targetUrl, { + status: fetchResult.status, + statusText: fetchResult.statusText, + }); + + return fetchResult; +} + +export const POST = handle; +export const GET = handle; +export const OPTIONS = handle; + +export const runtime = "edge"; diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 437aff582..629158843 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -116,7 +116,7 @@ export class ChatGPTApi implements LLMApi { enumerable: true, configurable: true, writable: true, - value: Math.max(modelConfig.max_tokens, 4096), + value: modelConfig.max_tokens, }); } diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 368144603..472425eae 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -219,6 +219,8 @@ function useSubmitHandler() { }, []); const shouldSubmit = (e: React.KeyboardEvent) => { + // Fix Chinese input method "Enter" on Safari + if (e.keyCode == 229) return false; if (e.key !== "Enter") return false; if (e.key === "Enter" && (e.nativeEvent.isComposing || isComposing.current)) return false; @@ -1098,6 +1100,47 @@ function _Chat() { }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + + const handlePaste = useCallback( + async (event: React.ClipboardEvent) => { + const currentModel = chatStore.currentSession().mask.modelConfig.model; + if(!isVisionModel(currentModel)){return;} + const items = (event.clipboardData || window.clipboardData).items; + for (const item of items) { + if (item.kind === "file" && item.type.startsWith("image/")) { + event.preventDefault(); + const file = item.getAsFile(); + if (file) { + const images: string[] = []; + images.push(...attachImages); + images.push( + ...(await new Promise((res, rej) => { + setUploading(true); + const imagesData: string[] = []; + compressImage(file, 256 * 1024) + .then((dataUrl) => { + imagesData.push(dataUrl); + setUploading(false); + res(imagesData); + }) + .catch((e) => { + setUploading(false); + rej(e); + }); + })), + ); + const imagesLength = images.length; + + if (imagesLength > 3) { + images.splice(3, imagesLength - 3); + } + setAttachImages(images); + } + } + } + }, + [attachImages, chatStore], + ); async function uploadImage() { const images: string[] = []; @@ -1448,6 +1491,7 @@ function _Chat() { onKeyDown={onInputKeyDown} onFocus={scrollToBottom} onClick={scrollToBottom} + onPaste={handlePaste} rows={inputRows} autoFocus={autoFocus} style={{ diff --git a/app/components/emoji.tsx b/app/components/emoji.tsx index b24349307..3b1f5e751 100644 --- a/app/components/emoji.tsx +++ b/app/components/emoji.tsx @@ -21,6 +21,7 @@ export function AvatarPicker(props: { }) { return (
setShowEmojiPicker(true)} + onClick={() => { + setShowEmojiPicker(!showEmojiPicker); + }} >
diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss index c67d352be..83c02f92a 100644 --- a/app/components/ui-lib.module.scss +++ b/app/components/ui-lib.module.scss @@ -14,17 +14,24 @@ .popover-content { position: absolute; + width: 350px; animation: slide-in 0.3s ease; right: 0; top: calc(100% + 10px); } - +@media screen and (max-width: 600px) { + .popover-content { + width: auto; + } +} .popover-mask { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; + background-color: rgba(0, 0, 0, 0.3); + backdrop-filter: blur(5px); } .list-item { diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index f7e326fd3..da700c0fb 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -26,10 +26,10 @@ export function Popover(props: {
{props.children} {props.open && ( -
-
- {props.content} -
+
+ )} + {props.open && ( +
{props.content}
)}
); diff --git a/app/config/server.ts b/app/config/server.ts index a2d325e41..dffc2563e 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -31,6 +31,7 @@ declare global { GOOGLE_API_KEY?: string; GOOGLE_URL?: string; + // google tag manager GTM_ID?: string; } } diff --git a/app/constant.ts b/app/constant.ts index c1f91d31c..904170687 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -23,7 +23,7 @@ export enum Path { } export enum ApiPath { - Cors = "/api/cors", + Cors = "", OpenAI = "/api/openai", } diff --git a/app/global.d.ts b/app/global.d.ts index e0a2c3f06..31e2b6e8a 100644 --- a/app/global.d.ts +++ b/app/global.d.ts @@ -19,6 +19,7 @@ declare interface Window { }; fs: { writeBinaryFile(path: string, data: Uint8Array): Promise; + writeTextFile(path: string, data: string): Promise; }; notification:{ requestPermission(): Promise; diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 80e1b054f..b20ff6c80 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -7,8 +7,8 @@ const tw = { WIP: "該功能仍在開發中……", Error: { Unauthorized: isApp - ? "檢測到無效 API Key,請前往[設定](/#/settings)頁檢查 API Key 是否配置正確。" - : "訪問密碼不正確或為空,請前往[登錄](/#/auth)頁輸入正確的訪問密碼,或者在[設定](/#/settings)頁填入你自己的 OpenAI API Key。", + ? "檢測到無效 API Key,請前往[設定](/#/settings)頁檢查 API Key 是否設定正確。" + : "訪問密碼不正確或為空,請前往[登入](/#/auth)頁輸入正確的訪問密碼,或者在[設定](/#/settings)頁填入你自己的 OpenAI API Key。", }, Auth: { @@ -17,7 +17,7 @@ const tw = { SubTips: "或者輸入你的 OpenAI 或 Google API 密鑰", Input: "在此處填寫訪問碼", Confirm: "確認", - Later: "稍後再說", + Later: "稍候再說", }, ChatItem: { ChatItemCount: (count: number) => `${count} 則對話`, @@ -53,8 +53,8 @@ const tw = { del: "刪除聊天", }, InputActions: { - Stop: "停止響應", - ToBottom: "滾到最新", + Stop: "停止回應", + ToBottom: "移至最新", Theme: { auto: "自動主題", light: "亮色模式", @@ -107,7 +107,7 @@ const tw = { }, }, Select: { - Search: "搜索消息", + Search: "查詢消息", All: "選取全部", Latest: "最近幾條", Clear: "清除選中", @@ -133,15 +133,15 @@ const tw = { Danger: { Reset: { Title: "重置所有設定", - SubTitle: "重置所有設定項回默認值", + SubTitle: "重置所有設定項回預設值", Action: "立即重置", Confirm: "確認重置所有設定?", }, Clear: { - Title: "清除所有數據", - SubTitle: "清除所有聊天、設定數據", + Title: "清除所有資料", + SubTitle: "清除所有聊天、設定資料", Action: "立即清除", - Confirm: "確認清除所有聊天、設定數據?", + Confirm: "確認清除所有聊天、設定資料?", }, }, Lang: { @@ -182,14 +182,14 @@ const tw = { SubTitle: "根據對話內容生成合適的標題", }, Sync: { - CloudState: "雲端數據", + CloudState: "雲端資料", NotSyncYet: "還沒有進行過同步", Success: "同步成功", Fail: "同步失敗", Config: { Modal: { - Title: "配置雲端同步", + Title: "設定雲端同步", Check: "檢查可用性", }, SyncType: { @@ -218,7 +218,7 @@ const tw = { }, }, - LocalState: "本地數據", + LocalState: "本地資料", Overview: (overview: any) => { return `${overview.chat} 次對話,${overview.message} 條消息,${overview.prompt} 條提示詞,${overview.mask} 個面具`; }, @@ -385,7 +385,7 @@ const tw = { Edit: "前置上下文和歷史記憶", Add: "新增一條", Clear: "上下文已清除", - Revert: "恢覆上下文", + Revert: "恢復上下文", }, Plugin: { Name: "外掛" }, FineTuned: { Sysmessage: "你是一個助手" }, @@ -440,8 +440,8 @@ const tw = { More: "搜尋更多", }, URLCommand: { - Code: "檢測到鏈接中已經包含訪問碼,是否自動填入?", - Settings: "檢測到鏈接中包含了預制設置,是否自動填入?", + Code: "檢測到連結中已經包含訪問碼,是否自動填入?", + Settings: "檢測到連結中包含了預設設定,是否自動填入?", }, UI: { Confirm: "確認", @@ -452,7 +452,7 @@ const tw = { Export: "導出", Import: "導入", Sync: "同步", - Config: "配置", + Config: "設定", }, Exporter: { Description: { diff --git a/app/store/sync.ts b/app/store/sync.ts index 5ff1cc6e5..674ff6744 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -118,7 +118,7 @@ export const useSyncStore = createPersistStore( }), { name: StoreKey.Sync, - version: 1.1, + version: 1.2, migrate(persistedState, version) { const newState = persistedState as typeof DEFAULT_SYNC_STATE; @@ -127,6 +127,15 @@ export const useSyncStore = createPersistStore( newState.upstash.username = STORAGE_KEY; } + if (version < 1.2) { + if ( + (persistedState as typeof DEFAULT_SYNC_STATE).proxyUrl === + "/api/cors/" + ) { + newState.proxyUrl = ""; + } + } + return newState as any; }, }, diff --git a/app/utils.ts b/app/utils.ts index 33b8eccd2..b4fc1980c 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -9,8 +9,9 @@ export function trimTopic(topic: string) { // This will remove the specified punctuation from the end of the string // and also trim quotes from both the start and end if they exist. return topic - .replace(/^["“”]+|["“”]+$/g, "") - .replace(/[,。!?”“"、,.!?]*$/, ""); + // fix for gemini + .replace(/^["“”*]+|["“”*]+$/g, "") + .replace(/[,。!?”“"、,.!?*]*$/, ""); } export async function copyToClipboard(text: string) { @@ -56,9 +57,9 @@ export async function downloadAs(text: string, filename: string) { if (result !== null) { try { - await window.__TAURI__.fs.writeBinaryFile( + await window.__TAURI__.fs.writeTextFile( result, - new Uint8Array([...text].map((c) => c.charCodeAt(0))), + text ); showToast(Locale.Download.Success); } catch (error) { @@ -291,9 +292,11 @@ export function getMessageImages(message: RequestMessage): string[] { } export function isVisionModel(model: string) { - return ( - model.startsWith("gpt-4-vision") || - model.startsWith("gemini-pro-vision") || - !DEFAULT_MODELS.find((m) => m.name == model) - ); + // Note: This is a better way using the TypeScript feature instead of `&&` or `||` (ts v5.5.0-dev.20240314 I've been using) + const visionKeywords = [ + "vision", + "claude-3", + ]; + + return visionKeywords.some(keyword => model.includes(keyword)); } diff --git a/app/utils/cloud/upstash.ts b/app/utils/cloud/upstash.ts index 5f5b9fc79..bf6147bd4 100644 --- a/app/utils/cloud/upstash.ts +++ b/app/utils/cloud/upstash.ts @@ -1,6 +1,5 @@ import { STORAGE_KEY } from "@/app/constant"; import { SyncStore } from "@/app/store/sync"; -import { corsFetch } from "../cors"; import { chunks } from "../format"; export type UpstashConfig = SyncStore["upstash"]; @@ -18,10 +17,9 @@ export function createUpstashClient(store: SyncStore) { return { async check() { try { - const res = await corsFetch(this.path(`get/${storeKey}`), { + const res = await fetch(this.path(`get/${storeKey}`, proxyUrl), { method: "GET", headers: this.headers(), - proxyUrl, }); console.log("[Upstash] check", res.status, res.statusText); return [200].includes(res.status); @@ -32,10 +30,9 @@ export function createUpstashClient(store: SyncStore) { }, async redisGet(key: string) { - const res = await corsFetch(this.path(`get/${key}`), { + const res = await fetch(this.path(`get/${key}`, proxyUrl), { method: "GET", headers: this.headers(), - proxyUrl, }); console.log("[Upstash] get key = ", key, res.status, res.statusText); @@ -45,11 +42,10 @@ export function createUpstashClient(store: SyncStore) { }, async redisSet(key: string, value: string) { - const res = await corsFetch(this.path(`set/${key}`), { + const res = await fetch(this.path(`set/${key}`, proxyUrl), { method: "POST", headers: this.headers(), body: value, - proxyUrl, }); console.log("[Upstash] set key = ", key, res.status, res.statusText); @@ -84,18 +80,28 @@ export function createUpstashClient(store: SyncStore) { Authorization: `Bearer ${config.apiKey}`, }; }, - path(path: string) { - let url = config.endpoint; - - if (!url.endsWith("/")) { - url += "/"; + path(path: string, proxyUrl: string = "") { + if (!path.endsWith("/")) { + path += "/"; } - if (path.startsWith("/")) { path = path.slice(1); } - return url + path; + if (proxyUrl.length > 0 && !proxyUrl.endsWith("/")) { + proxyUrl += "/"; + } + + let url; + if (proxyUrl.length > 0 || proxyUrl === "/") { + let u = new URL(proxyUrl + "/api/upstash/" + path); + // add query params + u.searchParams.append("endpoint", config.endpoint); + url = u.toString(); + } else { + url = "/api/upstash/" + path + "?endpoint=" + config.endpoint; + } + return url; }, }; } diff --git a/app/utils/cloud/webdav.ts b/app/utils/cloud/webdav.ts index 3a1553c10..bc569de0e 100644 --- a/app/utils/cloud/webdav.ts +++ b/app/utils/cloud/webdav.ts @@ -1,6 +1,5 @@ import { STORAGE_KEY } from "@/app/constant"; import { SyncStore } from "@/app/store/sync"; -import { corsFetch } from "../cors"; export type WebDAVConfig = SyncStore["webdav"]; export type WebDavClient = ReturnType; @@ -15,10 +14,9 @@ export function createWebDavClient(store: SyncStore) { return { async check() { try { - const res = await corsFetch(this.path(folder), { + const res = await fetch(this.path(folder, proxyUrl), { method: "MKCOL", headers: this.headers(), - proxyUrl, }); console.log("[WebDav] check", res.status, res.statusText); return [201, 200, 404, 301, 302, 307, 308].includes(res.status); @@ -30,10 +28,9 @@ export function createWebDavClient(store: SyncStore) { }, async get(key: string) { - const res = await corsFetch(this.path(fileName), { + const res = await fetch(this.path(fileName, proxyUrl), { method: "GET", headers: this.headers(), - proxyUrl, }); console.log("[WebDav] get key = ", key, res.status, res.statusText); @@ -42,11 +39,10 @@ export function createWebDavClient(store: SyncStore) { }, async set(key: string, value: string) { - const res = await corsFetch(this.path(fileName), { + const res = await fetch(this.path(fileName, proxyUrl), { method: "PUT", headers: this.headers(), body: value, - proxyUrl, }); console.log("[WebDav] set key = ", key, res.status, res.statusText); @@ -59,18 +55,28 @@ export function createWebDavClient(store: SyncStore) { authorization: `Basic ${auth}`, }; }, - path(path: string) { - let url = config.endpoint; - - if (!url.endsWith("/")) { - url += "/"; + path(path: string, proxyUrl: string = "") { + if (!path.endsWith("/")) { + path += "/"; } - if (path.startsWith("/")) { path = path.slice(1); } - return url + path; + if (proxyUrl.length > 0 && !proxyUrl.endsWith("/")) { + proxyUrl += "/"; + } + + let url; + if (proxyUrl.length > 0 || proxyUrl === "/") { + let u = new URL(proxyUrl + "/api/webdav/" + path); + // add query params + u.searchParams.append("endpoint", config.endpoint); + url = u.toString(); + } else { + url = "/api/upstash/" + path + "?endpoint=" + config.endpoint; + } + return url; }, }; } diff --git a/app/utils/cors.ts b/app/utils/cors.ts index 20b3e5160..fa348f9bf 100644 --- a/app/utils/cors.ts +++ b/app/utils/cors.ts @@ -4,6 +4,9 @@ import { ApiPath, DEFAULT_API_HOST } from "../constant"; export function corsPath(path: string) { const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_API_HOST}` : ""; + if (baseUrl === "" && path === "") { + return ""; + } if (!path.startsWith("/")) { path = "/" + path; } @@ -14,37 +17,3 @@ export function corsPath(path: string) { return `${baseUrl}${path}`; } - -export function corsFetch( - url: string, - options: RequestInit & { - proxyUrl?: string; - }, -) { - if (!url.startsWith("http")) { - throw Error("[CORS Fetch] url must starts with http/https"); - } - - let proxyUrl = options.proxyUrl ?? corsPath(ApiPath.Cors); - if (!proxyUrl.endsWith("/")) { - proxyUrl += "/"; - } - - url = url.replace("://", "/"); - - const corsOptions = { - ...options, - method: "POST", - headers: options.method - ? { - ...options.headers, - method: options.method, - } - : options.headers, - }; - - const corsUrl = proxyUrl + url; - console.info("[CORS] target = ", corsUrl); - - return fetch(corsUrl, corsOptions); -} diff --git a/package.json b/package.json index 9d71db6a5..c9262a16c 100644 --- a/package.json +++ b/package.json @@ -67,4 +67,5 @@ "resolutions": { "lint-staged/yaml": "^2.2.2" } + "packageManager": "yarn@1.22.19" } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 405d267ff..f03efb0fe 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "NextChat", - "version": "2.11.2" + "version": "2.11.3" }, "tauri": { "allowlist": {