diff --git a/README.md b/README.md index 633124ec7..d496d68ed 100644 --- a/README.md +++ b/README.md @@ -245,13 +245,17 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model User `-all` to disable all default models, `+all` to enable all default models. -### `WHITE_WEBDEV_ENDPOINTS` (可选) +### `WHITE_WEBDEV_ENDPOINTS` (optional) You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format: - Each address must be a complete endpoint > `https://xxxx/yyy` - Multiple addresses are connected by ', ' +### `DEFAULT_INPUT_TEMPLATE` (optional) + +Customize the default template used to initialize the User Input Preprocessing configuration item in Settings. + ## Requirements NodeJS >= 18, Docker >= 20 diff --git a/README_CN.md b/README_CN.md index 10b5fd035..6811102b6 100644 --- a/README_CN.md +++ b/README_CN.md @@ -156,6 +156,9 @@ anthropic claude Api Url. 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 +### `DEFAULT_INPUT_TEMPLATE` (可选) +自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项 + ## 开发 点击下方按钮,开始二次开发: diff --git a/app/api/webdav/[...path]/route.ts b/app/api/webdav/[...path]/route.ts index 3dd9ca3cd..816c2046b 100644 --- a/app/api/webdav/[...path]/route.ts +++ b/app/api/webdav/[...path]/route.ts @@ -1,12 +1,12 @@ import { NextRequest, NextResponse } from "next/server"; -import { STORAGE_KEY, internalWhiteWebDavEndpoints } from "../../../constant"; +import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant"; import { getServerSideConfig } from "@/app/config/server"; const config = getServerSideConfig(); -const mergedWhiteWebDavEndpoints = [ - ...internalWhiteWebDavEndpoints, - ...config.whiteWebDevEndpoints, +const mergedAllowedWebDavEndpoints = [ + ...internalAllowedWebDavEndpoints, + ...config.allowedWebDevEndpoints, ].filter((domain) => Boolean(domain.trim())); async function handle( @@ -24,7 +24,9 @@ async function handle( // Validate the endpoint to prevent potential SSRF attacks if ( - !mergedWhiteWebDavEndpoints.some((white) => endpoint?.startsWith(white)) + !mergedAllowedWebDavEndpoints.some( + (allowedEndpoint) => endpoint?.startsWith(allowedEndpoint), + ) ) { return NextResponse.json( { diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index ba07dcc76..e90c8f057 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -161,6 +161,13 @@ export class ClaudeApi implements LLMApi { }; }); + if (prompt[0]?.role === "assistant") { + prompt.unshift({ + role: "user", + content: ";", + }); + } + const requestBody: AnthropicChatRequest = { messages: prompt, stream: shouldStream, diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 1ab36db25..b6eb8d3df 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -21,11 +21,10 @@ export class GeminiProApi implements LLMApi { } async chat(options: ChatOptions): Promise { // const apiClient = this; - const visionModel = isVisionModel(options.config.model); let multimodal = false; const messages = options.messages.map((v) => { let parts: any[] = [{ text: getMessageTextContent(v) }]; - if (visionModel) { + if (isVisionModel(options.config.model)) { const images = getMessageImages(v); if (images.length > 0) { multimodal = true; @@ -117,17 +116,14 @@ export class GeminiProApi implements LLMApi { const controller = new AbortController(); options.onController?.(controller); try { - let googleChatPath = visionModel - ? Google.VisionChatPath(modelConfig.model) - : Google.ChatPath(modelConfig.model); - let chatPath = this.path(googleChatPath); - // let baseUrl = accessStore.googleUrl; if (!baseUrl) { baseUrl = isApp - ? DEFAULT_API_HOST + "/api/proxy/google/" + googleChatPath - : chatPath; + ? DEFAULT_API_HOST + + "/api/proxy/google/" + + Google.ChatPath(modelConfig.model) + : this.path(Google.ChatPath(modelConfig.model)); } if (isApp) { @@ -145,6 +141,7 @@ export class GeminiProApi implements LLMApi { () => controller.abort(), REQUEST_TIMEOUT_MS, ); + if (shouldStream) { let responseText = ""; let remainText = ""; diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index ca8bc2ebe..f35992630 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -129,7 +129,7 @@ export class ChatGPTApi implements LLMApi { }; // add max_tokens to vision model - if (visionModel) { + if (visionModel && modelConfig.model.includes("preview")) { requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000); } diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 85df5b9a8..061192504 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -59,9 +59,10 @@ import { getMessageTextContent, getMessageImages, isVisionModel, - compressImage, } from "../utils"; +import { compressImage } from "@/app/utils/chat"; + import dynamic from "next/dynamic"; import { ChatControllerPool } from "../client/controller"; @@ -1088,6 +1089,7 @@ function _Chat() { if (payload.url) { accessStore.update((access) => (access.openaiUrl = payload.url!)); } + accessStore.update((access) => (access.useCustomConfig = true)); }); } } catch { diff --git a/app/config/build.ts b/app/config/build.ts index 7a93ad02c..b2b1ad49d 100644 --- a/app/config/build.ts +++ b/app/config/build.ts @@ -1,4 +1,5 @@ import tauriConfig from "../../src-tauri/tauri.conf.json"; +import { DEFAULT_INPUT_TEMPLATE } from "../constant"; export const getBuildConfig = () => { if (typeof process === "undefined") { @@ -38,6 +39,7 @@ export const getBuildConfig = () => { ...commitInfo, buildMode, isApp, + template: process.env.DEFAULT_INPUT_TEMPLATE ?? DEFAULT_INPUT_TEMPLATE, }; }; diff --git a/app/config/server.ts b/app/config/server.ts index 618112172..b7c85ce6a 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -34,6 +34,9 @@ declare global { // google tag manager GTM_ID?: string; + + // custom template for preprocessing user input + DEFAULT_INPUT_TEMPLATE?: string; } } } @@ -51,6 +54,22 @@ const ACCESS_CODES = (function getAccessCodes(): Set { } })(); +function getApiKey(keys?: string) { + const apiKeyEnvVar = keys ?? ""; + const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); + const randomIndex = Math.floor(Math.random() * apiKeys.length); + const apiKey = apiKeys[randomIndex]; + if (apiKey) { + console.log( + `[Server Config] using ${randomIndex + 1} of ${ + apiKeys.length + } api key - ${apiKey}`, + ); + } + + return apiKey; +} + export const getServerSideConfig = () => { if (typeof process === "undefined") { throw Error( @@ -74,34 +93,34 @@ export const getServerSideConfig = () => { const isGoogle = !!process.env.GOOGLE_API_KEY; const isAnthropic = !!process.env.ANTHROPIC_API_KEY; - const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; - const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); - const randomIndex = Math.floor(Math.random() * apiKeys.length); - const apiKey = apiKeys[randomIndex]; - console.log( - `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`, - ); + // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; + // const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); + // const randomIndex = Math.floor(Math.random() * apiKeys.length); + // const apiKey = apiKeys[randomIndex]; + // console.log( + // `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`, + // ); - const whiteWebDevEndpoints = (process.env.WHITE_WEBDEV_ENDPOINTS ?? "").split( - ",", - ); + const allowedWebDevEndpoints = ( + process.env.WHITE_WEBDEV_ENDPOINTS ?? "" + ).split(","); return { baseUrl: process.env.BASE_URL, - apiKey, + apiKey: getApiKey(process.env.OPENAI_API_KEY), openaiOrgId: process.env.OPENAI_ORG_ID, isAzure, azureUrl: process.env.AZURE_URL, - azureApiKey: process.env.AZURE_API_KEY, + azureApiKey: getApiKey(process.env.AZURE_API_KEY), azureApiVersion: process.env.AZURE_API_VERSION, isGoogle, - googleApiKey: process.env.GOOGLE_API_KEY, + googleApiKey: getApiKey(process.env.GOOGLE_API_KEY), googleUrl: process.env.GOOGLE_URL, isAnthropic, - anthropicApiKey: process.env.ANTHROPIC_API_KEY, + anthropicApiKey: getApiKey(process.env.ANTHROPIC_API_KEY), anthropicApiVersion: process.env.ANTHROPIC_API_VERSION, anthropicUrl: process.env.ANTHROPIC_URL, @@ -120,6 +139,6 @@ export const getServerSideConfig = () => { disableFastLink: !!process.env.DISABLE_FAST_LINK, customModels, defaultModel, - whiteWebDevEndpoints, + allowedWebDevEndpoints, }; }; diff --git a/app/constant.ts b/app/constant.ts index 40b8fdb05..80042c99f 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -107,8 +107,6 @@ export const Azure = { export const Google = { ExampleEndpoint: "https://generativelanguage.googleapis.com/", ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`, - VisionChatPath: (modelName: string) => - `v1beta/models/${modelName}:generateContent`, }; export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang @@ -137,8 +135,8 @@ export const KnowledgeCutOffDate: Record = { "gpt-4-turbo": "2023-12", "gpt-4-turbo-2024-04-09": "2023-12", "gpt-4-turbo-preview": "2023-12", - "gpt-4-1106-preview": "2023-04", - "gpt-4-0125-preview": "2023-12", + "gpt-4o": "2023-10", + "gpt-4o-2024-05-13": "2023-10", "gpt-4-vision-preview": "2023-04", // After improvements, // it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously. @@ -148,22 +146,16 @@ export const KnowledgeCutOffDate: Record = { const openaiModels = [ "gpt-3.5-turbo", - "gpt-3.5-turbo-0301", - "gpt-3.5-turbo-0613", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125", - "gpt-3.5-turbo-16k", - "gpt-3.5-turbo-16k-0613", "gpt-4", - "gpt-4-0314", "gpt-4-0613", - "gpt-4-1106-preview", - "gpt-4-0125-preview", "gpt-4-32k", - "gpt-4-32k-0314", "gpt-4-32k-0613", "gpt-4-turbo", "gpt-4-turbo-preview", + "gpt-4o", + "gpt-4o-2024-05-13", "gpt-4-vision-preview", "gpt-4-turbo-2024-04-09", ]; @@ -171,6 +163,7 @@ const openaiModels = [ const googleModels = [ "gemini-1.0-pro", "gemini-1.5-pro-latest", + "gemini-1.5-flash-latest", "gemini-pro-vision", ]; @@ -217,7 +210,7 @@ export const CHAT_PAGE_SIZE = 15; export const MAX_RENDER_MSG_COUNT = 45; // some famous webdav endpoints -export const internalWhiteWebDavEndpoints = [ +export const internalAllowedWebDavEndpoints = [ "https://dav.jianguoyun.com/dav/", "https://dav.dropdav.com/", "https://dav.box.com/dav", diff --git a/app/containers/Chat/ChatPanel.tsx b/app/containers/Chat/ChatPanel.tsx index 0ba590c07..0711575b8 100644 --- a/app/containers/Chat/ChatPanel.tsx +++ b/app/containers/Chat/ChatPanel.tsx @@ -8,7 +8,7 @@ import { ModelType, } from "@/app/store"; import Locale from "@/app/locales"; -import { Selector, showConfirm, showToast } from "@/app/components/ui-lib"; +import { showConfirm } from "@/app/components/ui-lib"; import { CHAT_PAGE_SIZE, REQUEST_TIMEOUT_MS, @@ -25,7 +25,6 @@ import ChatInputPanel, { ChatInputPanelInstance, } from "./components/ChatInputPanel"; import ChatMessagePanel, { RenderMessage } from "./components/ChatMessagePanel"; -import { useAllModels } from "@/app/utils/hooks"; import useRows from "@/app/hooks/useRows"; import SessionConfigModel from "./components/SessionConfigModal"; import useScrollToBottom from "@/app/hooks/useScrollToBottom"; diff --git a/app/hooks/usePaste.ts b/app/hooks/usePaste.ts index 4c5281813..85ebddf05 100644 --- a/app/hooks/usePaste.ts +++ b/app/hooks/usePaste.ts @@ -1,4 +1,5 @@ -import { compressImage, isVisionModel } from "@/app/utils"; +import { isVisionModel } from "@/app/utils"; +import { compressImage } from "@/app/utils/chat"; import { useCallback, useRef } from "react"; import { useChatStore } from "../store/chat"; diff --git a/app/hooks/useUploadImage.ts b/app/hooks/useUploadImage.ts index 2405fdca4..8d898e3c5 100644 --- a/app/hooks/useUploadImage.ts +++ b/app/hooks/useUploadImage.ts @@ -1,4 +1,4 @@ -import { compressImage } from "@/app/utils"; +import { compressImage } from "@/app/utils/chat"; import { useCallback, useRef } from "react"; interface UseUploadImageOptions { diff --git a/app/locales/en.ts b/app/locales/en.ts index dc0db3faa..1862fbeb8 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -317,7 +317,7 @@ const en: LocaleType = { Endpoint: { Title: "OpenAI Endpoint", - SubTitle: "Must starts with http(s):// or use /api/openai as default", + SubTitle: "Must start with http(s):// or use /api/openai as default", }, }, Azure: { diff --git a/app/store/chat.ts b/app/store/chat.ts index d1de2a8f5..f2190d840 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -21,6 +21,8 @@ import { estimateTokenLength } from "../utils/token"; import { nanoid } from "nanoid"; import { createPersistStore } from "../utils/store"; import { identifyDefaultClaudeModel } from "../utils/checkers"; +import { collectModelsWithDefaultModel } from "../utils/model"; +import { useAccessStore } from "./access"; export type ChatMessage = RequestMessage & { date: string; @@ -104,9 +106,19 @@ function createEmptySession(): ChatSession { function getSummarizeModel(currentModel: string) { // if it is using gpt-* models, force to use 3.5 to summarize if (currentModel.startsWith("gpt")) { - return SUMMARIZE_MODEL; + 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-pro")) { + if (currentModel.startsWith("gemini")) { return GEMINI_SUMMARIZE_MODEL; } return currentModel; @@ -433,14 +445,13 @@ export const useChatStore = createPersistStore( getMemoryPrompt() { const session = get().currentSession(); - return { - role: "system", - content: - session.memoryPrompt.length > 0 - ? Locale.Store.Prompt.History(session.memoryPrompt) - : "", - date: "", - } as ChatMessage; + if (session.memoryPrompt.length) { + return { + role: "system", + content: Locale.Store.Prompt.History(session.memoryPrompt), + date: "", + } as ChatMessage; + } }, getMessagesWithMemory() { @@ -476,16 +487,15 @@ export const useChatStore = createPersistStore( systemPrompts.at(0)?.content ?? "empty", ); } - + const memoryPrompt = get().getMemoryPrompt(); // long term memory const shouldSendLongTermMemory = modelConfig.sendMemory && session.memoryPrompt && session.memoryPrompt.length > 0 && session.lastSummarizeIndex > clearContextIndex; - const longTermMemoryPrompts = shouldSendLongTermMemory - ? [get().getMemoryPrompt()] - : []; + const longTermMemoryPrompts = + shouldSendLongTermMemory && memoryPrompt ? [memoryPrompt] : []; const longTermMemoryStartIndex = session.lastSummarizeIndex; // short term memory @@ -610,9 +620,11 @@ export const useChatStore = createPersistStore( Math.max(0, n - modelConfig.historyMessageCount), ); } - - // add memory prompt - toBeSummarizedMsgs.unshift(get().getMemoryPrompt()); + const memoryPrompt = get().getMemoryPrompt(); + if (memoryPrompt) { + // add memory prompt + toBeSummarizedMsgs.unshift(memoryPrompt); + } const lastSummarizeIndex = session.messages.length; diff --git a/app/store/config.ts b/app/store/config.ts index fa743570a..c7c38214c 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -41,6 +41,7 @@ export const ThemeConfig = { title: "Dark model", }, }; +const config = getClientConfig(); export const DEFAULT_CONFIG = { lastUpdate: Date.now(), // timestamp, to merge state @@ -49,7 +50,7 @@ export const DEFAULT_CONFIG = { avatar: "1f603", fontSize: 14, theme: Theme.Auto as Theme, - tightBorder: !!getClientConfig()?.isApp, + tightBorder: !!config?.isApp, sendPreviewBubble: true, enableAutoGenerateTitle: true, sidebarWidth: DEFAULT_SIDEBAR_WIDTH, @@ -75,7 +76,7 @@ export const DEFAULT_CONFIG = { historyMessageCount: 4, compressMessageLengthThreshold: 1000, enableInjectSystemPrompts: true, - template: DEFAULT_INPUT_TEMPLATE, + template: config?.template ?? DEFAULT_INPUT_TEMPLATE, }, }; @@ -151,7 +152,7 @@ export const useAppConfig = createPersistStore( }), { name: StoreKey.Config, - version: 3.8, + version: 3.9, migrate(persistedState, version) { const state = persistedState as ChatConfig; @@ -182,6 +183,13 @@ export const useAppConfig = createPersistStore( state.lastUpdate = Date.now(); } + if (version < 3.9) { + state.modelConfig.template = + state.modelConfig.template !== DEFAULT_INPUT_TEMPLATE + ? state.modelConfig.template + : config?.template ?? DEFAULT_INPUT_TEMPLATE; + } + return state as any; }, }, diff --git a/app/store/sync.ts b/app/store/sync.ts index 8ee6c1819..77f7b9cdd 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -97,11 +97,20 @@ export const useSyncStore = createPersistStore( const client = this.getClient(); try { - const remoteState = JSON.parse( - await client.get(config.username), - ) as AppState; - mergeAppState(localState, remoteState); - setLocalAppState(localState); + const remoteState = await client.get(config.username); + if (!remoteState || remoteState === "") { + await client.set(config.username, JSON.stringify(localState)); + console.log( + "[Sync] Remote state is empty, using local state instead.", + ); + return; + } else { + const parsedRemoteState = JSON.parse( + await client.get(config.username), + ) as AppState; + mergeAppState(localState, parsedRemoteState); + setLocalAppState(localState); + } } catch (e) { console.log("[Sync] failed to get remote state", e); throw e; diff --git a/app/styles/globals.scss b/app/styles/globals.scss index 8e2c744a2..56db01b16 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -82,6 +82,7 @@ @include dark; } } + html { height: var(--full-height); @@ -106,6 +107,10 @@ body { @media only screen and (max-width: 600px) { background-color: var(--second); } + + *:focus-visible { + outline: none; + } } ::-webkit-scrollbar { diff --git a/app/utils.ts b/app/utils.ts index d42233c7f..062a04106 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -84,48 +84,6 @@ export async function downloadAs(text: string, filename: string) { } } -export function compressImage(file: File, maxSize: number): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (readerEvent: any) => { - const image = new Image(); - image.onload = () => { - let canvas = document.createElement("canvas"); - let ctx = canvas.getContext("2d"); - let width = image.width; - let height = image.height; - let quality = 0.9; - let dataUrl; - - do { - canvas.width = width; - canvas.height = height; - ctx?.clearRect(0, 0, canvas.width, canvas.height); - ctx?.drawImage(image, 0, 0, width, height); - dataUrl = canvas.toDataURL("image/jpeg", quality); - - if (dataUrl.length < maxSize) break; - - if (quality > 0.5) { - // Prioritize quality reduction - quality -= 0.1; - } else { - // Then reduce the size - width *= 0.9; - height *= 0.9; - } - } while (dataUrl.length > maxSize); - - resolve(dataUrl); - }; - image.onerror = reject; - image.src = readerEvent.target.result; - }; - reader.onerror = reject; - reader.readAsDataURL(file); - }); -} - export function readFromFile() { return new Promise((res, rej) => { const fileInput = document.createElement("input"); @@ -291,18 +249,21 @@ export function getMessageImages(message: RequestMessage): string[] { } export function isVisionModel(model: string) { - // 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", "gemini-1.5-pro", + "gemini-1.5-flash", + "gpt-4o", ]; + const isGpt4Turbo = + model.includes("gpt-4-turbo") && !model.includes("preview"); - const isGpt4Turbo = model.includes("gpt-4-turbo") && !model.includes("preview"); - - return visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo; + return ( + visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo + ); } export function getTime(dateTime: string) { diff --git a/app/utils/chat.ts b/app/utils/chat.ts new file mode 100644 index 000000000..991d06b73 --- /dev/null +++ b/app/utils/chat.ts @@ -0,0 +1,54 @@ +import heic2any from "heic2any"; + +export function compressImage(file: File, maxSize: number): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (readerEvent: any) => { + const image = new Image(); + image.onload = () => { + let canvas = document.createElement("canvas"); + let ctx = canvas.getContext("2d"); + let width = image.width; + let height = image.height; + let quality = 0.9; + let dataUrl; + + do { + canvas.width = width; + canvas.height = height; + ctx?.clearRect(0, 0, canvas.width, canvas.height); + ctx?.drawImage(image, 0, 0, width, height); + dataUrl = canvas.toDataURL("image/jpeg", quality); + + if (dataUrl.length < maxSize) break; + + if (quality > 0.5) { + // Prioritize quality reduction + quality -= 0.1; + } else { + // Then reduce the size + width *= 0.9; + height *= 0.9; + } + } while (dataUrl.length > maxSize); + + resolve(dataUrl); + }; + image.onerror = reject; + image.src = readerEvent.target.result; + }; + reader.onerror = reject; + + if (file.type.includes("heic")) { + heic2any({ blob: file, toType: "image/jpeg" }) + .then((blob) => { + reader.readAsDataURL(blob as Blob); + }) + .catch((e) => { + reject(e); + }); + } + + reader.readAsDataURL(file); + }); +} diff --git a/app/utils/cloud/upstash.ts b/app/utils/cloud/upstash.ts index bf6147bd4..8d84adbde 100644 --- a/app/utils/cloud/upstash.ts +++ b/app/utils/cloud/upstash.ts @@ -93,14 +93,17 @@ export function createUpstashClient(store: SyncStore) { } let url; - if (proxyUrl.length > 0 || proxyUrl === "/") { - let u = new URL(proxyUrl + "/api/upstash/" + path); + const pathPrefix = "/api/upstash/"; + + try { + let u = new URL(proxyUrl + pathPrefix + path); // add query params u.searchParams.append("endpoint", config.endpoint); url = u.toString(); - } else { - url = "/api/upstash/" + path + "?endpoint=" + config.endpoint; + } catch (e) { + url = pathPrefix + path + "?endpoint=" + config.endpoint; } + return url; }, }; diff --git a/app/utils/model.ts b/app/utils/model.ts index 6477640aa..056fff2e9 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -64,13 +64,10 @@ export function collectModelTableWithDefaultModel( ) { let modelTable = collectModelTable(models, customModels); if (defaultModel && defaultModel !== "") { - delete modelTable[defaultModel]; modelTable[defaultModel] = { + ...modelTable[defaultModel], name: defaultModel, - displayName: defaultModel, available: true, - provider: - modelTable[defaultModel]?.provider ?? customProvider(defaultModel), isDefault: true, }; } diff --git a/package.json b/package.json index cd3ad8029..f1d30fce8 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,13 @@ "dayjs": "^1.11.10", "emoji-picker-react": "^4.9.2", "fuse.js": "^7.0.0", + "heic2any": "^0.0.4", "html-to-image": "^1.11.11", "install": "^0.13.0", "lodash-es": "^4.17.21", "mermaid": "^10.6.1", "nanoid": "^5.0.3", - "next": "^13.4.9", + "next": "^14.1.1", "node-fetch": "^3.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d00d4a677..e68b06c3e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "NextChat", - "version": "2.11.3" + "version": "2.12.3" }, "tauri": { "allowlist": { @@ -116,4 +116,4 @@ } ] } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index f5bb72677..7b6e4f5b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1269,10 +1269,10 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@next/env@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.9.tgz#b77759514dd56bfa9791770755a2482f4d6ca93e" - integrity sha512-vuDRK05BOKfmoBYLNi2cujG2jrYbEod/ubSSyqgmEx9n/W3eZaJQdRNhTfumO+qmq/QTzLurW487n/PM/fHOkw== +"@next/env@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.1.tgz#80150a8440eb0022a73ba353c6088d419b908bac" + integrity sha512-7CnQyD5G8shHxQIIg3c7/pSeYFeMhsNbpU/bmvH7ZnDql7mNRgg8O2JZrhrc/soFnfBnKP4/xXNiiSIPn2w8gA== "@next/eslint-plugin-next@13.4.19": version "13.4.19" @@ -1281,50 +1281,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.9.tgz#0ed408d444bbc6b0a20f3506a9b4222684585677" - integrity sha512-TVzGHpZoVBk3iDsTOQA/R6MGmFp0+17SWXMEWd6zG30AfuELmSSMe2SdPqxwXU0gbpWkJL1KgfLzy5ReN0crqQ== +"@next/swc-darwin-arm64@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.1.tgz#b74ba7c14af7d05fa2848bdeb8ee87716c939b64" + integrity sha512-yDjSFKQKTIjyT7cFv+DqQfW5jsD+tVxXTckSe1KIouKk75t1qZmj/mV3wzdmFb0XHVGtyRjDMulfVG8uCKemOQ== -"@next/swc-darwin-x64@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.9.tgz#a08fccdee68201522fe6618ec81f832084b222f8" - integrity sha512-aSfF1fhv28N2e7vrDZ6zOQ+IIthocfaxuMWGReB5GDriF0caTqtHttAvzOMgJgXQtQx6XhyaJMozLTSEXeNN+A== +"@next/swc-darwin-x64@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.1.tgz#82c3e67775e40094c66e76845d1a36cc29c9e78b" + integrity sha512-KCQmBL0CmFmN8D64FHIZVD9I4ugQsDBBEJKiblXGgwn7wBCSe8N4Dx47sdzl4JAg39IkSN5NNrr8AniXLMb3aw== -"@next/swc-linux-arm64-gnu@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.9.tgz#1798c2341bb841e96521433eed00892fb24abbd1" - integrity sha512-JhKoX5ECzYoTVyIy/7KykeO4Z2lVKq7HGQqvAH+Ip9UFn1MOJkOnkPRB7v4nmzqAoY+Je05Aj5wNABR1N18DMg== +"@next/swc-linux-arm64-gnu@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.1.tgz#4f4134457b90adc5c3d167d07dfb713c632c0caa" + integrity sha512-YDQfbWyW0JMKhJf/T4eyFr4b3tceTorQ5w2n7I0mNVTFOvu6CGEzfwT3RSAQGTi/FFMTFcuspPec/7dFHuP7Eg== -"@next/swc-linux-arm64-musl@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.9.tgz#cee04c51610eddd3638ce2499205083656531ea0" - integrity sha512-OOn6zZBIVkm/4j5gkPdGn4yqQt+gmXaLaSjRSO434WplV8vo2YaBNbSHaTM9wJpZTHVDYyjzuIYVEzy9/5RVZw== +"@next/swc-linux-arm64-musl@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.1.tgz#594bedafaeba4a56db23a48ffed2cef7cd09c31a" + integrity sha512-fiuN/OG6sNGRN/bRFxRvV5LyzLB8gaL8cbDH5o3mEiVwfcMzyE5T//ilMmaTrnA8HLMS6hoz4cHOu6Qcp9vxgQ== -"@next/swc-linux-x64-gnu@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.9.tgz#1932d0367916adbc6844b244cda1d4182bd11f7a" - integrity sha512-iA+fJXFPpW0SwGmx/pivVU+2t4zQHNOOAr5T378PfxPHY6JtjV6/0s1vlAJUdIHeVpX98CLp9k5VuKgxiRHUpg== +"@next/swc-linux-x64-gnu@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.1.tgz#cb4e75f1ff2b9bcadf2a50684605928ddfc58528" + integrity sha512-rv6AAdEXoezjbdfp3ouMuVqeLjE1Bin0AuE6qxE6V9g3Giz5/R3xpocHoAi7CufRR+lnkuUjRBn05SYJ83oKNQ== -"@next/swc-linux-x64-musl@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.9.tgz#a66aa8c1383b16299b72482f6360facd5cde3c7a" - integrity sha512-rlNf2WUtMM+GAQrZ9gMNdSapkVi3koSW3a+dmBVp42lfugWVvnyzca/xJlN48/7AGx8qu62WyO0ya1ikgOxh6A== +"@next/swc-linux-x64-musl@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.1.tgz#15f26800df941b94d06327f674819ab64b272e25" + integrity sha512-YAZLGsaNeChSrpz/G7MxO3TIBLaMN8QWMr3X8bt6rCvKovwU7GqQlDu99WdvF33kI8ZahvcdbFsy4jAFzFX7og== -"@next/swc-win32-arm64-msvc@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.9.tgz#39482ee856c867177a612a30b6861c75e0736a4a" - integrity sha512-5T9ybSugXP77nw03vlgKZxD99AFTHaX8eT1ayKYYnGO9nmYhJjRPxcjU5FyYI+TdkQgEpIcH7p/guPLPR0EbKA== +"@next/swc-win32-arm64-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.1.tgz#060c134fa7fa843666e3e8574972b2b723773dd9" + integrity sha512-1L4mUYPBMvVDMZg1inUYyPvFSduot0g73hgfD9CODgbr4xiTYe0VOMTZzaRqYJYBA9mana0x4eaAaypmWo1r5A== -"@next/swc-win32-ia32-msvc@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.9.tgz#29db85e34b597ade1a918235d16a760a9213c190" - integrity sha512-ojZTCt1lP2ucgpoiFgrFj07uq4CZsq4crVXpLGgQfoFq00jPKRPgesuGPaz8lg1yLfvafkU3Jd1i8snKwYR3LA== +"@next/swc-win32-ia32-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.1.tgz#5c06889352b1f77e3807834a0d0afd7e2d2d1da2" + integrity sha512-jvIE9tsuj9vpbbXlR5YxrghRfMuG0Qm/nZ/1KDHc+y6FpnZ/apsgh+G6t15vefU0zp3WSpTMIdXRUsNl/7RSuw== -"@next/swc-win32-x64-msvc@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.9.tgz#0c2758164cccd61bc5a1c6cd8284fe66173e4a2b" - integrity sha512-QbT03FXRNdpuL+e9pLnu+XajZdm/TtIXVYY4lA9t+9l0fLZbHXDYEKitAqxrOj37o3Vx5ufxiRAniaIebYDCgw== +"@next/swc-win32-x64-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.1.tgz#d38c63a8f9b7f36c1470872797d3735b4a9c5c52" + integrity sha512-S6K6EHDU5+1KrBDLko7/c1MNy/Ya73pIAmvKeFwsF4RmBFJSO7/7YeD4FnZ4iBdzE69PpQ4sOMU9ORKeNuxe8A== "@next/third-parties@^14.1.0": version "14.1.0" @@ -1720,10 +1720,10 @@ "@svgr/plugin-jsx" "^6.5.1" "@svgr/plugin-svgo" "^6.5.1" -"@swc/helpers@0.5.1": - version "0.5.1" - resolved "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a" - integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg== +"@swc/helpers@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" + integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== dependencies: tslib "^2.4.0" @@ -2494,10 +2494,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503: - version "1.0.30001509" - resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001509.tgz#2b7ad5265392d6d2de25cd8776d1ab3899570d14" - integrity sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA== +caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001579: + version "1.0.30001617" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz#809bc25f3f5027ceb33142a7d6c40759d7a901eb" + integrity sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA== caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599: version "1.0.30001608" @@ -3999,7 +3999,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -4150,6 +4150,11 @@ heap@^0.2.6: resolved "https://registry.npmmirror.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc" integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg== +heic2any@^0.0.4: + version "0.0.4" + resolved "https://registry.npmmirror.com/heic2any/-/heic2any-0.0.4.tgz#eddb8e6fec53c8583a6e18b65069bb5e8d19028a" + integrity sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA== + highlight.js@~11.7.0: version "11.7.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e" @@ -5307,10 +5312,10 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@^3.3.4: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^3.3.6: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== nanoid@^3.3.7: version "3.3.7" @@ -5332,29 +5337,28 @@ neo-async@^2.6.2: resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -next@^13.4.9: - version "13.4.9" - resolved "https://registry.yarnpkg.com/next/-/next-13.4.9.tgz#473de5997cb4c5d7a4fb195f566952a1cbffbeba" - integrity sha512-vtefFm/BWIi/eWOqf1GsmKG3cjKw1k3LjuefKRcL3iiLl3zWzFdPG3as6xtxrGO6gwTzzaO1ktL4oiHt/uvTjA== +next@^14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/next/-/next-14.1.1.tgz#92bd603996c050422a738e90362dff758459a171" + integrity sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww== dependencies: - "@next/env" "13.4.9" - "@swc/helpers" "0.5.1" + "@next/env" "14.1.1" + "@swc/helpers" "0.5.2" busboy "1.6.0" - caniuse-lite "^1.0.30001406" - postcss "8.4.14" + caniuse-lite "^1.0.30001579" + graceful-fs "^4.2.11" + postcss "8.4.31" styled-jsx "5.1.1" - watchpack "2.4.0" - zod "3.21.4" optionalDependencies: - "@next/swc-darwin-arm64" "13.4.9" - "@next/swc-darwin-x64" "13.4.9" - "@next/swc-linux-arm64-gnu" "13.4.9" - "@next/swc-linux-arm64-musl" "13.4.9" - "@next/swc-linux-x64-gnu" "13.4.9" - "@next/swc-linux-x64-musl" "13.4.9" - "@next/swc-win32-arm64-msvc" "13.4.9" - "@next/swc-win32-ia32-msvc" "13.4.9" - "@next/swc-win32-x64-msvc" "13.4.9" + "@next/swc-darwin-arm64" "14.1.1" + "@next/swc-darwin-x64" "14.1.1" + "@next/swc-linux-arm64-gnu" "14.1.1" + "@next/swc-linux-arm64-musl" "14.1.1" + "@next/swc-linux-x64-gnu" "14.1.1" + "@next/swc-linux-x64-musl" "14.1.1" + "@next/swc-win32-arm64-msvc" "14.1.1" + "@next/swc-win32-ia32-msvc" "14.1.1" + "@next/swc-win32-x64-msvc" "14.1.1" node-domexception@^1.0.0: version "1.0.0" @@ -5672,12 +5676,12 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: resolved "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.14: - version "8.4.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" - integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== +postcss@8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: - nanoid "^3.3.4" + nanoid "^3.3.6" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -6858,7 +6862,7 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" -watchpack@2.4.0, watchpack@^2.4.0: +watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== @@ -7027,11 +7031,6 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zod@3.21.4: - version "3.21.4" - resolved "https://registry.npmmirror.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" - integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== - zustand@^4.3.8: version "4.3.8" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.8.tgz#37113df8e9e1421b0be1b2dca02b49b76210e7c4"