diff --git a/.dockerignore b/.dockerignore index 60da41dd8..95ed9e268 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,97 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Node.js dependencies +/node_modules +/jspm_packages + +# TypeScript v1 declaration files +typings + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.test + # local env files .env*.local -# docker-compose env files -.env +# Next.js build output +.next +out +# Nuxt.js build output +.nuxt +dist + +# Gatsby files +.cache/ + + +# Vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Temporary folders +tmp +temp + +# IDE and editor directories +.idea +.vscode +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +Thumbs.db + +# secret key *.key -*.key.pub \ No newline at end of file +*.key.pub diff --git a/.env.template b/.env.template index 166cc4ef4..b2a0438d9 100644 --- a/.env.template +++ b/.env.template @@ -2,7 +2,7 @@ # Your openai api key. (required) OPENAI_API_KEY=sk-xxxx -# Access passsword, separated by comma. (optional) +# Access password, separated by comma. (optional) CODE=your-password # You can start service behind a proxy @@ -47,3 +47,17 @@ ENABLE_BALANCE_QUERY= # If you want to disable parse settings from url, set this value to 1. DISABLE_FAST_LINK= + +# anthropic claude Api Key.(optional) +ANTHROPIC_API_KEY= + +### anthropic claude Api version. (optional) +ANTHROPIC_API_VERSION= + + + +### anthropic claude Api url (optional) +ANTHROPIC_URL= + +### (optional) +WHITE_WEBDEV_ENDPOINTS= \ No newline at end of file diff --git a/README.md b/README.md index 3ac537abc..633124ec7 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) @@ -200,6 +200,18 @@ Google Gemini Pro Api Key. Google Gemini Pro Api Url. +### `ANTHROPIC_API_KEY` (optional) + +anthropic claude Api Key. + +### `ANTHROPIC_API_VERSION` (optional) + +anthropic claude Api version. + +### `ANTHROPIC_URL` (optional) + +anthropic claude Api Url. + ### `HIDE_USER_API_KEY` (optional) > Default: Empty @@ -216,7 +228,7 @@ If you do not want users to use GPT-4, set this value to 1. > Default: Empty -If you do want users to query balance, set this value to 1, or you should set it to 0. +If you do want users to query balance, set this value to 1. ### `DISABLE_FAST_LINK` (optional) @@ -233,6 +245,13 @@ 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` (可选) + +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 ', ' + ## Requirements NodeJS >= 18, Docker >= 20 diff --git a/README_CN.md b/README_CN.md index 4acefefa5..10b5fd035 100644 --- a/README_CN.md +++ b/README_CN.md @@ -114,6 +114,18 @@ Google Gemini Pro 密钥. Google Gemini Pro Api Url. +### `ANTHROPIC_API_KEY` (optional) + +anthropic claude Api Key. + +### `ANTHROPIC_API_VERSION` (optional) + +anthropic claude Api version. + +### `ANTHROPIC_URL` (optional) + +anthropic claude Api Url. + ### `HIDE_USER_API_KEY` (可选) 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 @@ -130,6 +142,13 @@ Google Gemini Pro Api Url. 如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。 +### `WHITE_WEBDEV_ENDPOINTS` (可选) + +如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求: +- 每一个地址必须是一个完整的 endpoint +> `https://xxxx/xxx` +- 多个地址以`,`相连 + ### `CUSTOM_MODELS` (可选) > 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。 diff --git a/app/api/anthropic/[...path]/route.ts b/app/api/anthropic/[...path]/route.ts new file mode 100644 index 000000000..4264893d9 --- /dev/null +++ b/app/api/anthropic/[...path]/route.ts @@ -0,0 +1,189 @@ +import { getServerSideConfig } from "@/app/config/server"; +import { + ANTHROPIC_BASE_URL, + Anthropic, + ApiPath, + DEFAULT_MODELS, + ModelProvider, +} from "@/app/constant"; +import { prettyObject } from "@/app/utils/format"; +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "../../auth"; +import { collectModelTable } from "@/app/utils/model"; + +const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]); + +async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[Anthropic Route] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const subpath = params.path.join("/"); + + if (!ALLOWD_PATH.has(subpath)) { + console.log("[Anthropic Route] forbidden path ", subpath); + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + subpath, + }, + { + status: 403, + }, + ); + } + + const authResult = auth(req, ModelProvider.Claude); + if (authResult.error) { + return NextResponse.json(authResult, { + status: 401, + }); + } + + try { + const response = await request(req); + return response; + } catch (e) { + console.error("[Anthropic] ", e); + return NextResponse.json(prettyObject(e)); + } +} + +export const GET = handle; +export const POST = handle; + +export const runtime = "edge"; +export const preferredRegion = [ + "arn1", + "bom1", + "cdg1", + "cle1", + "cpt1", + "dub1", + "fra1", + "gru1", + "hnd1", + "iad1", + "icn1", + "kix1", + "lhr1", + "pdx1", + "sfo1", + "sin1", + "syd1", +]; + +const serverConfig = getServerSideConfig(); + +async function request(req: NextRequest) { + const controller = new AbortController(); + + let authHeaderName = "x-api-key"; + let authValue = + req.headers.get(authHeaderName) || + req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() || + serverConfig.anthropicApiKey || + ""; + + let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, ""); + + let baseUrl = + serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL; + + if (!baseUrl.startsWith("http")) { + baseUrl = `https://${baseUrl}`; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, -1); + } + + console.log("[Proxy] ", path); + console.log("[Base Url]", baseUrl); + + const timeoutId = setTimeout( + () => { + controller.abort(); + }, + 10 * 60 * 1000, + ); + + const fetchUrl = `${baseUrl}${path}`; + + const fetchOptions: RequestInit = { + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-store", + [authHeaderName]: authValue, + "anthropic-version": + req.headers.get("anthropic-version") || + serverConfig.anthropicApiVersion || + Anthropic.Vision, + }, + method: req.method, + body: req.body, + redirect: "manual", + // @ts-ignore + duplex: "half", + signal: controller.signal, + }; + + // #1815 try to refuse some request to some models + if (serverConfig.customModels && req.body) { + try { + const modelTable = collectModelTable( + DEFAULT_MODELS, + serverConfig.customModels, + ); + const clonedBody = await req.text(); + fetchOptions.body = clonedBody; + + const jsonBody = JSON.parse(clonedBody) as { model?: string }; + + // not undefined and is false + if (modelTable[jsonBody?.model ?? ""].available === false) { + return NextResponse.json( + { + error: true, + message: `you are not allowed to use ${jsonBody?.model} model`, + }, + { + status: 403, + }, + ); + } + } catch (e) { + console.error(`[Anthropic] filter`, e); + } + } + console.log("[Anthropic request]", fetchOptions.headers, req.method); + try { + const res = await fetch(fetchUrl, fetchOptions); + + console.log( + "[Anthropic response]", + res.status, + " ", + res.headers, + res.url, + ); + // to prevent browser prompt for credentials + const newHeaders = new Headers(res.headers); + newHeaders.delete("www-authenticate"); + // to disable nginx buffering + newHeaders.set("X-Accel-Buffering", "no"); + + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: newHeaders, + }); + } finally { + clearTimeout(timeoutId); + } +} diff --git a/app/api/auth.ts b/app/api/auth.ts index 16c8034eb..b750f2d17 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -57,12 +57,31 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { if (!apiKey) { const serverConfig = getServerSideConfig(); - const systemApiKey = - modelProvider === ModelProvider.GeminiPro - ? serverConfig.googleApiKey - : serverConfig.isAzure - ? serverConfig.azureApiKey - : serverConfig.apiKey; + // const systemApiKey = + // modelProvider === ModelProvider.GeminiPro + // ? serverConfig.googleApiKey + // : serverConfig.isAzure + // ? serverConfig.azureApiKey + // : serverConfig.apiKey; + + let systemApiKey: string | undefined; + + switch (modelProvider) { + case ModelProvider.GeminiPro: + systemApiKey = serverConfig.googleApiKey; + break; + case ModelProvider.Claude: + systemApiKey = serverConfig.anthropicApiKey; + break; + case ModelProvider.GPT: + default: + if (serverConfig.isAzure) { + systemApiKey = serverConfig.azureApiKey; + } else { + systemApiKey = serverConfig.apiKey; + } + } + if (systemApiKey) { console.log("[Auth] use system api key"); req.headers.set("Authorization", `Bearer ${systemApiKey}`); diff --git a/app/api/common.ts b/app/api/common.ts index ca8406bb3..a75f2de5c 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -43,10 +43,6 @@ export async function requestOpenai(req: NextRequest) { console.log("[Proxy] ", path); console.log("[Base Url]", baseUrl); - // this fix [Org ID] undefined in server side if not using custom point - if (serverConfig.openaiOrgId !== undefined) { - console.log("[Org ID]", serverConfig.openaiOrgId); - } const timeoutId = setTimeout( () => { @@ -116,18 +112,37 @@ export async function requestOpenai(req: NextRequest) { try { const res = await fetch(fetchUrl, fetchOptions); + // Extract the OpenAI-Organization header from the response + const openaiOrganizationHeader = res.headers.get("OpenAI-Organization"); + + // Check if serverConfig.openaiOrgId is defined and not an empty string + if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") { + // If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present + console.log("[Org ID]", openaiOrganizationHeader); + } else { + console.log("[Org ID] is not set up."); + } + // 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"); + + // Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV) + // Also, this is to prevent the header from being sent to the client + if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") { + newHeaders.delete("OpenAI-Organization"); + } + // 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, 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..f64a9ef13 --- /dev/null +++ b/app/api/webdav/[...path]/route.ts @@ -0,0 +1,131 @@ +import { NextRequest, NextResponse } from "next/server"; +import { STORAGE_KEY, internalWhiteWebDavEndpoints } from "../../../constant"; +import { getServerSideConfig } from "@/app/config/server"; + +const config = getServerSideConfig(); + +const mergedWhiteWebDavEndpoints = [ + ...internalWhiteWebDavEndpoints, + ...config.whiteWebDevEndpoints, +].filter((domain) => Boolean(domain.trim())); + +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"); + + // Validate the endpoint to prevent potential SSRF attacks + if ( + !mergedWhiteWebDavEndpoints.some((white) => endpoint?.startsWith(white)) + ) { + return NextResponse.json( + { + error: true, + msg: "Invalid endpoint", + }, + { + status: 400, + }, + ); + } + + if (!endpoint?.endsWith("/")) { + endpoint += "/"; + } + + const endpointPath = params.path.join("/"); + const targetPath = `${endpoint}/${endpointPath}`; + + // 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 " + targetPath, + }, + { + status: 403, + }, + ); + } + + // for MKCOL request, only allow request ${folder} + if (req.method === "MKCOL" && !targetPath.endsWith(folder)) { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + targetPath, + }, + { + status: 403, + }, + ); + } + + // for GET request, only allow request ending with fileName + if (req.method === "GET" && !targetPath.endsWith(fileName)) { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + targetPath, + }, + { + status: 403, + }, + ); + } + + // for PUT request, only allow request ending with fileName + if (req.method === "PUT" && !targetPath.endsWith(fileName)) { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + targetPath, + }, + { + 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, + redirect: "manual", + 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 PUT = handle; +export const GET = handle; +export const OPTIONS = handle; + +export const runtime = "edge"; diff --git a/app/client/api.ts b/app/client/api.ts index c4d548a41..7bee546b4 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -8,6 +8,7 @@ import { import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store"; import { ChatGPTApi } from "./platforms/openai"; import { GeminiProApi } from "./platforms/google"; +import { ClaudeApi } from "./platforms/anthropic"; export const ROLES = ["system", "user", "assistant"] as const; export type MessageRole = (typeof ROLES)[number]; @@ -94,11 +95,16 @@ export class ClientApi { public llm: LLMApi; constructor(provider: ModelProvider = ModelProvider.GPT) { - if (provider === ModelProvider.GeminiPro) { - this.llm = new GeminiProApi(); - return; + switch (provider) { + case ModelProvider.GeminiPro: + this.llm = new GeminiProApi(); + break; + case ModelProvider.Claude: + this.llm = new ClaudeApi(); + break; + default: + this.llm = new ChatGPTApi(); } - this.llm = new ChatGPTApi(); } config() {} diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts new file mode 100644 index 000000000..fea3d8654 --- /dev/null +++ b/app/client/platforms/anthropic.ts @@ -0,0 +1,404 @@ +import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant"; +import { ChatOptions, LLMApi, MultimodalContent } from "../api"; +import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; +import { getClientConfig } from "@/app/config/client"; +import { DEFAULT_API_HOST } from "@/app/constant"; +import { RequestMessage } from "@/app/typing"; +import { + EventStreamContentType, + fetchEventSource, +} from "@fortaine/fetch-event-source"; + +import Locale from "../../locales"; +import { prettyObject } from "@/app/utils/format"; +import { getMessageTextContent, isVisionModel } from "@/app/utils"; + +export type MultiBlockContent = { + type: "image" | "text"; + source?: { + type: string; + media_type: string; + data: string; + }; + text?: string; +}; + +export type AnthropicMessage = { + role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper]; + content: string | MultiBlockContent[]; +}; + +export interface AnthropicChatRequest { + model: string; // The model that will complete your prompt. + messages: AnthropicMessage[]; // The prompt that you want Claude to complete. + max_tokens: number; // The maximum number of tokens to generate before stopping. + stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text. + temperature?: number; // Amount of randomness injected into the response. + top_p?: number; // Use nucleus sampling. + top_k?: number; // Only sample from the top K options for each subsequent token. + metadata?: object; // An object describing metadata about the request. + stream?: boolean; // Whether to incrementally stream the response using server-sent events. +} + +export interface ChatRequest { + model: string; // The model that will complete your prompt. + prompt: string; // The prompt that you want Claude to complete. + max_tokens_to_sample: number; // The maximum number of tokens to generate before stopping. + stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text. + temperature?: number; // Amount of randomness injected into the response. + top_p?: number; // Use nucleus sampling. + top_k?: number; // Only sample from the top K options for each subsequent token. + metadata?: object; // An object describing metadata about the request. + stream?: boolean; // Whether to incrementally stream the response using server-sent events. +} + +export interface ChatResponse { + completion: string; + stop_reason: "stop_sequence" | "max_tokens"; + model: string; +} + +export type ChatStreamResponse = ChatResponse & { + stop?: string; + log_id: string; +}; + +const ClaudeMapper = { + assistant: "assistant", + user: "user", + system: "user", +} as const; + +const keys = ["claude-2, claude-instant-1"]; + +export class ClaudeApi implements LLMApi { + extractMessage(res: any) { + console.log("[Response] claude response: ", res); + + return res?.content?.[0]?.text; + } + async chat(options: ChatOptions): Promise { + const visionModel = isVisionModel(options.config.model); + + const accessStore = useAccessStore.getState(); + + const shouldStream = !!options.config.stream; + + const modelConfig = { + ...useAppConfig.getState().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, + ...{ + model: options.config.model, + }, + }; + + const messages = [...options.messages]; + + const keys = ["system", "user"]; + + // roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages + for (let i = 0; i < messages.length - 1; i++) { + const message = messages[i]; + const nextMessage = messages[i + 1]; + + if (keys.includes(message.role) && keys.includes(nextMessage.role)) { + messages[i] = [ + message, + { + role: "assistant", + content: ";", + }, + ] as any; + } + } + + const prompt = messages + .flat() + .filter((v) => { + if (!v.content) return false; + if (typeof v.content === "string" && !v.content.trim()) return false; + return true; + }) + .map((v) => { + const { role, content } = v; + const insideRole = ClaudeMapper[role] ?? "user"; + + if (!visionModel || typeof content === "string") { + return { + role: insideRole, + content: getMessageTextContent(v), + }; + } + return { + role: insideRole, + content: content + .filter((v) => v.image_url || v.text) + .map(({ type, text, image_url }) => { + if (type === "text") { + return { + type, + text: text!, + }; + } + const { url = "" } = image_url || {}; + const colonIndex = url.indexOf(":"); + const semicolonIndex = url.indexOf(";"); + const comma = url.indexOf(","); + + const mimeType = url.slice(colonIndex + 1, semicolonIndex); + const encodeType = url.slice(semicolonIndex + 1, comma); + const data = url.slice(comma + 1); + + return { + type: "image" as const, + source: { + type: encodeType, + media_type: mimeType, + data, + }, + }; + }), + }; + }); + + const requestBody: AnthropicChatRequest = { + messages: prompt, + stream: shouldStream, + + model: modelConfig.model, + max_tokens: modelConfig.max_tokens, + temperature: modelConfig.temperature, + top_p: modelConfig.top_p, + // top_k: modelConfig.top_k, + top_k: 5, + }; + + const path = this.path(Anthropic.ChatPath); + + const controller = new AbortController(); + options.onController?.(controller); + + const payload = { + method: "POST", + body: JSON.stringify(requestBody), + signal: controller.signal, + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "x-api-key": accessStore.anthropicApiKey, + "anthropic-version": accessStore.anthropicApiVersion, + 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; + }; + try { + chunkJson = JSON.parse(msg.data); + } catch (e) { + console.error("[Response] parse error", msg.data); + } + + 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); + } + } else { + try { + controller.signal.onabort = () => options.onFinish(""); + + const res = await fetch(path, payload); + const resJson = await res.json(); + + const message = this.extractMessage(resJson); + options.onFinish(message); + } catch (e) { + console.error("failed to chat", e); + options.onError?.(e as Error); + } + } + } + async usage() { + return { + used: 0, + total: 0, + }; + } + async models() { + // const provider = { + // id: "anthropic", + // providerName: "Anthropic", + // providerType: "anthropic", + // }; + + return [ + // { + // name: "claude-instant-1.2", + // available: true, + // provider, + // }, + // { + // name: "claude-2.0", + // available: true, + // provider, + // }, + // { + // name: "claude-2.1", + // available: true, + // provider, + // }, + // { + // name: "claude-3-opus-20240229", + // available: true, + // provider, + // }, + // { + // name: "claude-3-sonnet-20240229", + // available: true, + // provider, + // }, + // { + // name: "claude-3-haiku-20240307", + // available: true, + // provider, + // }, + ]; + } + path(path: string): string { + const accessStore = useAccessStore.getState(); + + let baseUrl: string = accessStore.anthropicUrl; + + // if endpoint is empty, use default endpoint + if (baseUrl.trim().length === 0) { + const isApp = !!getClientConfig()?.isApp; + + baseUrl = isApp + ? DEFAULT_API_HOST + "/api/proxy/anthropic" + : ApiPath.Anthropic; + } + + if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) { + baseUrl = "https://" + baseUrl; + } + + baseUrl = trimEnd(baseUrl, "/"); + + return `${baseUrl}/${path}`; + } +} + +function trimEnd(s: string, end = " ") { + if (end.length === 0) return s; + + while (s.endsWith(end)) { + s = s.slice(0, -end.length); + } + + return s; +} + +function bearer(value: string) { + return `Bearer ${value.trim()}`; +} + +function getAuthKey(apiKey = "") { + const accessStore = useAccessStore.getState(); + const isApp = !!getClientConfig()?.isApp; + let authKey = ""; + + if (apiKey) { + // use user's api key first + authKey = bearer(apiKey); + } else if ( + accessStore.enabledAccessControl() && + !isApp && + !!accessStore.accessCode + ) { + // or use access code + authKey = bearer(ACCESS_CODE_PREFIX + accessStore.accessCode); + } + + return authKey; +} diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 629158843..7652ba0f2 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -40,6 +40,20 @@ export interface OpenAIListModelResponse { }>; } +interface RequestPayload { + messages: { + role: "system" | "user" | "assistant"; + content: string | MultimodalContent[]; + }[]; + stream?: boolean; + model: string; + temperature: number; + presence_penalty: number; + frequency_penalty: number; + top_p: number; + max_tokens?: number; +} + export class ChatGPTApi implements LLMApi { private disableListModels = true; @@ -98,7 +112,7 @@ export class ChatGPTApi implements LLMApi { }, }; - const requestPayload = { + const requestPayload: RequestPayload = { messages, stream: options.config.stream, model: modelConfig.model, @@ -112,12 +126,7 @@ export class ChatGPTApi implements LLMApi { // add max_tokens to vision model if (visionModel) { - Object.defineProperty(requestPayload, "max_tokens", { - enumerable: true, - configurable: true, - writable: true, - value: modelConfig.max_tokens, - }); + requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000); } console.log("[Request] openai payload: ", requestPayload); @@ -151,6 +160,9 @@ export class ChatGPTApi implements LLMApi { 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; } @@ -225,19 +237,31 @@ export class ChatGPTApi implements LLMApi { } const text = msg.data; try { - const json = JSON.parse(text) as { - choices: Array<{ - delta: { - content: string; - }; - }>; - }; - const delta = json.choices[0]?.delta?.content; + 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 ( + 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); + console.error("[Request] parse error", text, msg); } }, onclose() { diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index 33967717d..7ef6e7b83 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -12,7 +12,7 @@ import { import { useChatStore } from "../store"; import Locale from "../locales"; -import { Link, useNavigate } from "react-router-dom"; +import { Link, useLocation, useNavigate } from "react-router-dom"; import { Path } from "../constant"; import { MaskAvatar } from "./mask"; import { Mask } from "../store/mask"; @@ -40,12 +40,16 @@ export function ChatItem(props: { }); } }, [props.selected]); + + const { pathname: currentPath } = useLocation(); return ( {(provided) => (
{ diff --git a/app/components/chat.tsx b/app/components/chat.tsx index bcd0e605d..b9750f285 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; 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 ( (await import("./markdown")).Markdown, { loading: () => , @@ -315,6 +316,8 @@ export function PreviewActions(props: { var api: ClientApi; if (config.modelConfig.model.startsWith("gemini")) { api = new ClientApi(ModelProvider.GeminiPro); + } else if (identifyDefaultClaudeModel(config.modelConfig.model)) { + api = new ClientApi(ModelProvider.Claude); } else { api = new ClientApi(ModelProvider.GPT); } diff --git a/app/components/home.tsx b/app/components/home.tsx index 8386ba144..ffac64fda 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -29,6 +29,7 @@ import { AuthPage } from "./auth"; import { getClientConfig } from "../config/client"; import { ClientApi } from "../client/api"; import { useAccessStore } from "../store"; +import { identifyDefaultClaudeModel } from "../utils/checkers"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -173,6 +174,8 @@ export function useLoadData() { var api: ClientApi; if (config.modelConfig.model.startsWith("gemini")) { api = new ClientApi(ModelProvider.GeminiPro); + } else if (identifyDefaultClaudeModel(config.modelConfig.model)) { + api = new ClientApi(ModelProvider.Claude); } else { api = new ClientApi(ModelProvider.GPT); } diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index f3a916cc5..1afd7de3b 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -116,11 +116,28 @@ function escapeDollarNumber(text: string) { return escapedText; } -function _MarkDownContent(props: { content: string }) { - const escapedContent = useMemo( - () => escapeDollarNumber(props.content), - [props.content], +function escapeBrackets(text: string) { + const pattern = + /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g; + return text.replace( + pattern, + (match, codeBlock, squareBracket, roundBracket) => { + if (codeBlock) { + return codeBlock; + } else if (squareBracket) { + return `$$${squareBracket}$$`; + } else if (roundBracket) { + return `$${roundBracket}$`; + } + return match; + }, ); +} + +function _MarkDownContent(props: { content: string }) { + const escapedContent = useMemo(() => { + return escapeBrackets(escapeDollarNumber(props.content)); + }, [props.content]); return ( (); + 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 allMasks = maskStore .getAll() diff --git a/app/components/message-selector.tsx b/app/components/message-selector.tsx index 840e480cb..8198a3cd4 100644 --- a/app/components/message-selector.tsx +++ b/app/components/message-selector.tsx @@ -227,7 +227,7 @@ export function MessageSelector(props: {
- +
); diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss index 1eac17c16..c6aec4203 100644 --- a/app/components/settings.module.scss +++ b/app/components/settings.module.scss @@ -5,6 +5,8 @@ .avatar { cursor: pointer; + position: relative; + z-index: 1; } .edit-prompt-modal { diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 84ae7edf6..db08b48a9 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -51,6 +51,7 @@ import Locale, { import { copyToClipboard } from "../utils"; import Link from "next/link"; import { + Anthropic, Azure, Google, OPENAI_BASE_URL, @@ -693,7 +694,9 @@ export function Settings() { >
setShowEmojiPicker(true)} + onClick={() => { + setShowEmojiPicker(!showEmojiPicker); + }} >
@@ -961,7 +964,7 @@ export function Settings() { - {accessStore.provider === "OpenAI" ? ( + {accessStore.provider === ServiceProvider.OpenAI && ( <> - ) : accessStore.provider === "Azure" ? ( + )} + {accessStore.provider === ServiceProvider.Azure && ( <> - ) : accessStore.provider === "Google" ? ( + )} + {accessStore.provider === ServiceProvider.Google && ( <> - ) : null} + )} + {accessStore.provider === ServiceProvider.Anthropic && ( + <> + + + accessStore.update( + (access) => + (access.anthropicUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.anthropicApiKey = + e.currentTarget.value), + ); + }} + /> + + + + accessStore.update( + (access) => + (access.anthropicApiVersion = + e.currentTarget.value), + ) + } + > + + + )} )} 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 dffc2563e..c27ef5e44 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -69,6 +69,7 @@ export const getServerSideConfig = () => { const isAzure = !!process.env.AZURE_URL; 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()); @@ -78,6 +79,10 @@ export const getServerSideConfig = () => { `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`, ); + const whiteWebDevEndpoints = (process.env.WHITE_WEBDEV_ENDPOINTS ?? "").split( + ",", + ); + return { baseUrl: process.env.BASE_URL, apiKey, @@ -92,6 +97,11 @@ export const getServerSideConfig = () => { googleApiKey: process.env.GOOGLE_API_KEY, googleUrl: process.env.GOOGLE_URL, + isAnthropic, + anthropicApiKey: process.env.ANTHROPIC_API_KEY, + anthropicApiVersion: process.env.ANTHROPIC_API_VERSION, + anthropicUrl: process.env.ANTHROPIC_URL, + gtmId: process.env.GTM_ID, needCode: ACCESS_CODES.size > 0, @@ -106,5 +116,6 @@ export const getServerSideConfig = () => { hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, disableFastLink: !!process.env.DISABLE_FAST_LINK, customModels, + whiteWebDevEndpoints, }; }; diff --git a/app/constant.ts b/app/constant.ts index c1f91d31c..1ad76870f 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -10,6 +10,7 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; export const DEFAULT_API_HOST = "https://api.nextchat.dev"; export const OPENAI_BASE_URL = "https://api.openai.com"; +export const ANTHROPIC_BASE_URL = "https://api.anthropic.com"; export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/"; @@ -23,8 +24,9 @@ export enum Path { } export enum ApiPath { - Cors = "/api/cors", + Cors = "", OpenAI = "/api/openai", + Anthropic = "/api/anthropic", } export enum SlotID { @@ -67,13 +69,22 @@ export enum ServiceProvider { OpenAI = "OpenAI", Azure = "Azure", Google = "Google", + Anthropic = "Anthropic", } export enum ModelProvider { GPT = "GPT", GeminiPro = "GeminiPro", + Claude = "Claude", } +export const Anthropic = { + ChatPath: "v1/messages", + ChatPath1: "v1/complete", + ExampleEndpoint: "https://api.anthropic.com", + Vision: "2023-06-01", +}; + export const OpenaiPath = { ChatPath: "v1/chat/completions", UsagePath: "dashboard/billing/usage", @@ -94,12 +105,20 @@ export const Google = { }; export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang +// export const DEFAULT_SYSTEM_TEMPLATE = ` +// You are ChatGPT, a large language model trained by {{ServiceProvider}}. +// Knowledge cutoff: {{cutoff}} +// Current model: {{model}} +// Current time: {{time}} +// Latex inline: $x^2$ +// Latex block: $$e=mc^2$$ +// `; export const DEFAULT_SYSTEM_TEMPLATE = ` You are ChatGPT, a large language model trained by {{ServiceProvider}}. Knowledge cutoff: {{cutoff}} Current model: {{model}} Current time: {{time}} -Latex inline: $x^2$ +Latex inline: \\(x^2\\) Latex block: $$e=mc^2$$ `; @@ -289,7 +308,73 @@ export const DEFAULT_MODELS = [ providerType: "google", }, }, + { + name: "claude-instant-1.2", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, + { + name: "claude-2.0", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, + { + name: "claude-2.1", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, + { + name: "claude-3-opus-20240229", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, + { + name: "claude-3-sonnet-20240229", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, + { + name: "claude-3-haiku-20240307", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, ] as const; export const CHAT_PAGE_SIZE = 15; export const MAX_RENDER_MSG_COUNT = 45; + +// some famous webdav endpoints +export const internalWhiteWebDavEndpoints = [ + "https://dav.jianguoyun.com/dav/", + "https://dav.dropdav.com/", + "https://dav.box.com/dav", + "https://nanao.teracloud.jp/dav/", + "https://webdav.4shared.com/", + "https://dav.idrivesync.com", + "https://webdav.yandex.com", + "https://app.koofr.net/dav/Koofr", +]; 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/layout.tsx b/app/layout.tsx index 2c89ba494..5898b21a1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -36,6 +36,7 @@ export default function RootLayout({ + diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 5d0c28428..2ff94e32d 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -313,6 +313,23 @@ const cn = { SubTitle: "选择指定的部分版本", }, }, + Anthropic: { + ApiKey: { + Title: "接口密钥", + SubTitle: "使用自定义 Anthropic Key 绕过密码访问限制", + Placeholder: "Anthropic API Key", + }, + + Endpoint: { + Title: "接口地址", + SubTitle: "样例:", + }, + + ApiVerion: { + Title: "接口版本 (claude api version)", + SubTitle: "选择一个特定的 API 版本输入", + }, + }, Google: { ApiKey: { Title: "API 密钥", diff --git a/app/locales/en.ts b/app/locales/en.ts index 79a91d7cc..59636db7b 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -316,6 +316,24 @@ const en: LocaleType = { SubTitle: "Check your api version from azure console", }, }, + Anthropic: { + ApiKey: { + Title: "Anthropic API Key", + SubTitle: + "Use a custom Anthropic Key to bypass password access restrictions", + Placeholder: "Anthropic API Key", + }, + + Endpoint: { + Title: "Endpoint Address", + SubTitle: "Example:", + }, + + ApiVerion: { + Title: "API Version (claude api version)", + SubTitle: "Select and input a specific API version", + }, + }, CustomModel: { Title: "Custom Models", SubTitle: "Custom model options, seperated by comma", diff --git a/app/locales/pt.ts b/app/locales/pt.ts index 85226ed50..8151b7aa4 100644 --- a/app/locales/pt.ts +++ b/app/locales/pt.ts @@ -316,6 +316,23 @@ const pt: PartialLocaleType = { SubTitle: "Verifique sua versão API do console Azure", }, }, + Anthropic: { + ApiKey: { + Title: "Chave API Anthropic", + SubTitle: "Verifique sua chave API do console Anthropic", + Placeholder: "Chave API Anthropic", + }, + + Endpoint: { + Title: "Endpoint Address", + SubTitle: "Exemplo: ", + }, + + ApiVerion: { + Title: "Versão API (Versão api claude)", + SubTitle: "Verifique sua versão API do console Anthropic", + }, + }, CustomModel: { Title: "Modelos Personalizados", SubTitle: "Opções de modelo personalizado, separados por vírgula", diff --git a/app/locales/sk.ts b/app/locales/sk.ts index 025519e77..a97b7175c 100644 --- a/app/locales/sk.ts +++ b/app/locales/sk.ts @@ -317,6 +317,23 @@ const sk: PartialLocaleType = { SubTitle: "Skontrolujte svoju verziu API v Azure konzole", }, }, + Anthropic: { + ApiKey: { + Title: "API kľúč Anthropic", + SubTitle: "Skontrolujte svoj API kľúč v Anthropic konzole", + Placeholder: "API kľúč Anthropic", + }, + + Endpoint: { + Title: "Adresa koncového bodu", + SubTitle: "Príklad:", + }, + + ApiVerion: { + Title: "Verzia API (claude verzia API)", + SubTitle: "Vyberte špecifickú verziu časti", + }, + }, CustomModel: { Title: "Vlastné modely", SubTitle: "Možnosti vlastného modelu, oddelené čiarkou", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index b20ff6c80..96811ae7e 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -314,6 +314,23 @@ const tw = { SubTitle: "選擇指定的部分版本", }, }, + Anthropic: { + ApiKey: { + Title: "API 密鑰", + SubTitle: "從 Anthropic AI 獲取您的 API 密鑰", + Placeholder: "Anthropic API Key", + }, + + Endpoint: { + Title: "終端地址", + SubTitle: "示例:", + }, + + ApiVerion: { + Title: "API 版本 (claude api version)", + SubTitle: "選擇一個特定的 API 版本输入", + }, + }, Google: { ApiKey: { Title: "API 密鑰", @@ -467,12 +484,12 @@ const tw = { type DeepPartial = T extends object ? { - [P in keyof T]?: DeepPartial; - } + [P in keyof T]?: DeepPartial; + } : T; export type LocaleType = typeof tw; export type PartialLocaleType = DeepPartial; export default tw; -// Translated by @chunkiuuu, feel free the submit new pr if there are typo/incorrect translations :D \ No newline at end of file +// Translated by @chunkiuuu, feel free the submit new pr if there are typo/incorrect translations :D diff --git a/app/store/access.ts b/app/store/access.ts index 6884e71e3..163666402 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -36,6 +36,11 @@ const DEFAULT_ACCESS_STATE = { googleApiKey: "", googleApiVersion: "v1", + // anthropic + anthropicApiKey: "", + anthropicApiVersion: "2023-06-01", + anthropicUrl: "", + // server config needCode: true, hideUserApiKey: false, @@ -67,6 +72,10 @@ export const useAccessStore = createPersistStore( return ensure(get(), ["googleApiKey"]); }, + isValidAnthropic() { + return ensure(get(), ["anthropicApiKey"]); + }, + isAuthorized() { this.fetch(); @@ -75,6 +84,7 @@ export const useAccessStore = createPersistStore( this.isValidOpenAI() || this.isValidAzure() || this.isValidGoogle() || + this.isValidAnthropic() || !this.enabledAccessControl() || (this.enabledAccessControl() && ensure(get(), ["accessCode"])) ); diff --git a/app/store/chat.ts b/app/store/chat.ts index f97d7d725..eeddd8463 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -20,6 +20,7 @@ import { prettyObject } from "../utils/format"; import { estimateTokenLength } from "../utils/token"; import { nanoid } from "nanoid"; import { createPersistStore } from "../utils/store"; +import { identifyDefaultClaudeModel } from "../utils/checkers"; export type ChatMessage = RequestMessage & { date: string; @@ -126,6 +127,11 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) { let output = modelConfig.template ?? DEFAULT_INPUT_TEMPLATE; + // remove duplicate + if (input.startsWith(output)) { + output = ""; + } + // must contains {{input}} const inputVar = "{{input}}"; if (!output.includes(inputVar)) { @@ -348,6 +354,8 @@ export const useChatStore = createPersistStore( var api: ClientApi; if (modelConfig.model.startsWith("gemini")) { api = new ClientApi(ModelProvider.GeminiPro); + } else if (identifyDefaultClaudeModel(modelConfig.model)) { + api = new ClientApi(ModelProvider.Claude); } else { api = new ClientApi(ModelProvider.GPT); } @@ -494,7 +502,6 @@ export const useChatStore = createPersistStore( tokenCount += estimateTokenLength(getMessageTextContent(msg)); reversedRecentMessages.push(msg); } - // concat all messages const recentMessages = [ ...systemPrompts, @@ -533,6 +540,8 @@ export const useChatStore = createPersistStore( var api: ClientApi; if (modelConfig.model.startsWith("gemini")) { api = new ClientApi(ModelProvider.GeminiPro); + } else if (identifyDefaultClaudeModel(modelConfig.model)) { + api = new ClientApi(ModelProvider.Claude); } else { api = new ClientApi(ModelProvider.GPT); } @@ -557,6 +566,7 @@ export const useChatStore = createPersistStore( messages: topicMessages, config: { model: getSummarizeModel(session.mask.modelConfig.model), + stream: false, }, onFinish(message) { get().updateCurrentSession( @@ -600,6 +610,10 @@ export const useChatStore = createPersistStore( historyMsgLength > modelConfig.compressMessageLengthThreshold && modelConfig.sendMemory ) { + /** Destruct max_tokens while summarizing + * this param is just shit + **/ + const { max_tokens, ...modelcfg } = modelConfig; api.llm.chat({ messages: toBeSummarizedMsgs.concat( createMessage({ @@ -609,7 +623,7 @@ export const useChatStore = createPersistStore( }), ), config: { - ...modelConfig, + ...modelcfg, stream: true, model: getSummarizeModel(session.mask.modelConfig.model), }, 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/typing.ts b/app/typing.ts index 25e474abf..b09722ab9 100644 --- a/app/typing.ts +++ b/app/typing.ts @@ -1 +1,9 @@ export type Updater = (updater: (value: T) => void) => void; + +export const ROLES = ["system", "user", "assistant"] as const; +export type MessageRole = (typeof ROLES)[number]; + +export interface RequestMessage { + role: MessageRole; + content: string; +} diff --git a/app/utils.ts b/app/utils.ts index b484e8386..2745f5ca2 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -2,16 +2,17 @@ import { useEffect, useState } from "react"; import { showToast } from "./components/ui-lib"; import Locale from "./locales"; import { RequestMessage } from "./client/api"; -import { DEFAULT_MODELS } from "./constant"; export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language // 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 - // fix for gemini - .replace(/^["“”*]+|["“”*]+$/g, "") - .replace(/[,。!?”“"、,.!?*]*$/, ""); + return ( + topic + // fix for gemini + .replace(/^["“”*]+|["“”*]+$/g, "") + .replace(/[,。!?”“"、,.!?*]*$/, "") + ); } export async function copyToClipboard(text: string) { @@ -57,10 +58,7 @@ export async function downloadAs(text: string, filename: string) { if (result !== null) { try { - await window.__TAURI__.fs.writeBinaryFile( - result, - new Uint8Array([...text].map((c) => c.charCodeAt(0))), - ); + await window.__TAURI__.fs.writeTextFile(result, text); showToast(Locale.Download.Success); } catch (error) { showToast(Locale.Download.Failed); @@ -292,9 +290,8 @@ export function getMessageImages(message: RequestMessage): string[] { } export function isVisionModel(model: string) { - return ( - // model.startsWith("gpt-4-vision") || - // model.startsWith("gemini-pro-vision") || - model.includes("vision") - ); + // 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/checkers.ts b/app/utils/checkers.ts new file mode 100644 index 000000000..4496e1039 --- /dev/null +++ b/app/utils/checkers.ts @@ -0,0 +1,21 @@ +import { useAccessStore } from "../store/access"; +import { useAppConfig } from "../store/config"; +import { collectModels } from "./model"; + +export function identifyDefaultClaudeModel(modelName: string) { + const accessStore = useAccessStore.getState(); + const configStore = useAppConfig.getState(); + + const allModals = collectModels( + configStore.models, + [configStore.customModels, accessStore.customModels].join(","), + ); + + const modelMeta = allModals.find((m) => m.name === modelName); + + return ( + modelName.startsWith("claude") && + modelMeta && + modelMeta.provider?.providerType === "anthropic" + ); +} 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..e01c193fe 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,13 +14,19 @@ 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); + const success = [201, 200, 404, 405, 301, 302, 307, 308].includes( + res.status, + ); + console.log( + `[WebDav] check ${success ? "success" : "failed"}, ${res.status} ${ + res.statusText + }`, + ); + return success; } catch (e) { console.error("[WebDav] failed to check", e); } @@ -30,10 +35,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 +46,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 +62,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/app/utils/model.ts b/app/utils/model.ts index b2a42ef02..378fc498e 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -22,6 +22,12 @@ export function collectModelTable( }; }); + const customProvider = (modelName: string) => ({ + id: modelName, + providerName: "", + providerType: "custom", + }); + // server custom models customModels .split(",") @@ -34,13 +40,15 @@ export function collectModelTable( // enable or disable all models if (name === "all") { - Object.values(modelTable).forEach((model) => (model.available = available)); + Object.values(modelTable).forEach( + (model) => (model.available = available), + ); } else { modelTable[name] = { name, displayName: displayName || name, available, - provider: modelTable[name]?.provider, // Use optional chaining + provider: modelTable[name]?.provider ?? customProvider(name), // Use optional chaining }; } }); diff --git a/app/utils/object.ts b/app/utils/object.ts new file mode 100644 index 000000000..b2588779d --- /dev/null +++ b/app/utils/object.ts @@ -0,0 +1,17 @@ +export function omit( + obj: T, + ...keys: U +): Omit { + const ret: any = { ...obj }; + keys.forEach((key) => delete ret[key]); + return ret; +} + +export function pick( + obj: T, + ...keys: U +): Pick { + const ret: any = {}; + keys.forEach((key) => (ret[key] = obj[key])); + return ret; +} diff --git a/next.config.mjs b/next.config.mjs index c8e7adb83..daaeba468 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -77,6 +77,10 @@ if (mode !== "export") { source: "/api/proxy/openai/:path*", destination: "https://api.openai.com/:path*", }, + { + source: "/api/proxy/anthropic/:path*", + destination: "https://api.anthropic.com/:path*", + }, { source: "/google-fonts/:path*", destination: "https://fonts.googleapis.com/:path*", diff --git a/package.json b/package.json index b31d6a901..9dbae8208 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", "@vercel/speed-insights": "^1.0.2", - "emoji-picker-react": "^4.5.15", + "emoji-picker-react": "^4.9.2", "fuse.js": "^7.0.0", "html-to-image": "^1.11.11", "mermaid": "^10.6.1", @@ -44,9 +44,9 @@ "zustand": "^4.3.8" }, "devDependencies": { - "@tauri-apps/cli": "1.5.7", - "@types/node": "^20.9.0", - "@types/react": "^18.2.14", + "@tauri-apps/cli": "1.5.11", + "@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", @@ -54,7 +54,7 @@ "eslint": "^8.49.0", "eslint-config-next": "13.4.19", "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-prettier": "^5.1.3", "husky": "^8.0.0", "lint-staged": "^13.2.2", "prettier": "^3.0.2", @@ -63,5 +63,6 @@ }, "resolutions": { "lint-staged/yaml": "^2.2.2" - } -} \ No newline at end of file + }, + "packageManager": "yarn@1.22.19" +} diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index a76212ae0..6461c47c2 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ 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": { diff --git a/yarn.lock b/yarn.lock index db6da708b..66924bf41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1303,17 +1303,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@pkgr/utils@^2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.3.1.tgz#0a9b06ffddee364d6642b3cd562ca76f55b34a03" - integrity sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw== - dependencies: - cross-spawn "^7.0.3" - is-glob "^4.0.3" - open "^8.4.0" - picocolors "^1.0.0" - tiny-glob "^0.2.9" - tslib "^2.4.0" +"@pkgr/core@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.0.tgz#7d8dacb7fdef0e4387caf7396cbd77f179867d06" + integrity sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ== "@remix-run/router@1.8.0": version "1.8.0" @@ -1438,71 +1431,71 @@ dependencies: tslib "^2.4.0" -"@tauri-apps/cli-darwin-arm64@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.7.tgz#3435f1b6c4b431e0283f94c3a0bd486be66b24ee" - integrity sha512-eUpOUhs2IOpKaLa6RyGupP2owDLfd0q2FR/AILzryjtBtKJJRDQQvuotf+LcbEce2Nc2AHeYJIqYAsB4sw9K+g== +"@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" + integrity sha512-2NLSglDb5VfvTbMtmOKWyD+oaL/e8Z/ZZGovHtUFyUSFRabdXc6cZOlcD1BhFvYkHqm+TqGaz5qtPR5UbqDs8A== -"@tauri-apps/cli-darwin-x64@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.5.7.tgz#d3d646e790067158d14a1f631a50c67dc05e3360" - integrity sha512-zfumTv1xUuR+RB1pzhRy+51tB6cm8I76g0xUBaXOfEdOJ9FqW5GW2jdnEUbpNuU65qJ1lB8LVWHKGrSWWKazew== +"@tauri-apps/cli-darwin-x64@1.5.11": + version "1.5.11" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.5.11.tgz#0afae17fe1e84b9699a6b9824cd83b60c6ebfa59" + integrity sha512-/RQllHiJRH2fJOCudtZlaUIjofkHzP3zZgxi71ZUm7Fy80smU5TDfwpwOvB0wSVh0g/ciDjMArCSTo0MRvL+ag== -"@tauri-apps/cli-linux-arm-gnueabihf@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.5.7.tgz#049c12980cdfd67fe9e5163762bf77f3c85f6956" - integrity sha512-JngWNqS06bMND9PhiPWp0e+yknJJuSozsSbo+iMzHoJNRauBZCUx+HnUcygUR66Cy6qM4eJvLXtsRG7ApxvWmg== +"@tauri-apps/cli-linux-arm-gnueabihf@1.5.11": + version "1.5.11" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.5.11.tgz#c46166d7f6c1022105a13d530b1d1336f628981f" + integrity sha512-IlBuBPKmMm+a5LLUEK6a21UGr9ZYd6zKuKLq6IGM4tVweQa8Sf2kP2Nqs74dMGIUrLmMs0vuqdURpykQg+z4NQ== -"@tauri-apps/cli-linux-arm64-gnu@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.5.7.tgz#d1c143da15cba74eebfaaf1662f0734e30f97562" - integrity sha512-WyIYP9BskgBGq+kf4cLAyru8ArrxGH2eMYGBJvuNEuSaqBhbV0i1uUxvyWdazllZLAEz1WvSocUmSwLknr1+sQ== +"@tauri-apps/cli-linux-arm64-gnu@1.5.11": + version "1.5.11" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.5.11.tgz#fd5c539a03371e0ab6cd00563dced1610ceb8943" + integrity sha512-w+k1bNHCU/GbmXshtAhyTwqosThUDmCEFLU4Zkin1vl2fuAtQry2RN7thfcJFepblUGL/J7yh3Q/0+BCjtspKQ== -"@tauri-apps/cli-linux-arm64-musl@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.7.tgz#f79a17f5360a8ab25b90f3a8e9e6327d5378072f" - integrity sha512-OrDpihQP2MB0JY1a/wP9wsl9dDjFDpVEZOQxt4hU+UVGRCZQok7ghPBg4+Xpd1CkNkcCCuIeY8VxRvwLXpnIzg== +"@tauri-apps/cli-linux-arm64-musl@1.5.11": + version "1.5.11" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.11.tgz#bf7f940c3aca981d7c240857a86568d5b6e8310f" + integrity sha512-PN6/dl+OfYQ/qrAy4HRAfksJ2AyWQYn2IA/2Wwpaa7SDRz2+hzwTQkvajuvy0sQ5L2WCG7ymFYRYMbpC6Hk9Pg== -"@tauri-apps/cli-linux-x64-gnu@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.5.7.tgz#2cbd17998dcfc8a465d61f30ac9e99ae65e2c2e8" - integrity sha512-4T7FAYVk76rZi8VkuLpiKUAqaSxlva86C1fHm/RtmoTKwZEV+MI3vIMoVg+AwhyWIy9PS55C75nF7+OwbnFnvQ== +"@tauri-apps/cli-linux-x64-gnu@1.5.11": + version "1.5.11" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.5.11.tgz#17323105e3863a3f36d51771e642e489037ba59b" + integrity sha512-MTVXLi89Nj7Apcvjezw92m7ZqIDKT5SFKZtVPCg6RoLUBTzko/BQoXYIRWmdoz2pgkHDUHgO2OMJ8oKzzddXbw== -"@tauri-apps/cli-linux-x64-musl@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.7.tgz#d5d4ddded945cc781568d72b7eba367121f28525" - integrity sha512-LL9aMK601BmQjAUDcKWtt5KvAM0xXi0iJpOjoUD3LPfr5dLvBMTflVHQDAEtuZexLQyqpU09+60781PrI/FCTw== +"@tauri-apps/cli-linux-x64-musl@1.5.11": + version "1.5.11" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.11.tgz#83e22026771ec8ab094922ab114a7385532aa16c" + integrity sha512-kwzAjqFpz7rvTs7WGZLy/a5nS5t15QKr3E9FG95MNF0exTl3d29YoAUAe1Mn0mOSrTJ9Z+vYYAcI/QdcsGBP+w== -"@tauri-apps/cli-win32-arm64-msvc@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.5.7.tgz#05a1bd4e2bc692bad995edb9d07e616cc5682fd5" - integrity sha512-TmAdM6GVkfir3AUFsDV2gyc25kIbJeAnwT72OnmJGAECHs/t/GLP9IkFLLVcFKsiosRf8BXhVyQ84NYkSWo14w== +"@tauri-apps/cli-win32-arm64-msvc@1.5.11": + version "1.5.11" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.5.11.tgz#817874d230fdb09e7211013006a9a22f66ace573" + integrity sha512-L+5NZ/rHrSUrMxjj6YpFYCXp6wHnq8c8SfDTBOX8dO8x+5283/vftb4vvuGIsLS4UwUFXFnLt3XQr44n84E67Q== -"@tauri-apps/cli-win32-ia32-msvc@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.5.7.tgz#8c832f4dc88374255ef1cda4d2d6a6d61a921388" - integrity sha512-bqWfxwCfLmrfZy69sEU19KHm5TFEaMb8KIekd4aRq/kyOlrjKLdZxN1PyNRP8zpJA1lTiRHzfUDfhpmnZH/skg== +"@tauri-apps/cli-win32-ia32-msvc@1.5.11": + version "1.5.11" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.5.11.tgz#dee1a00eb9e216415d9d6ab9386c35849613c560" + integrity sha512-oVlD9IVewrY0lZzTdb71kNXkjdgMqFq+ohb67YsJb4Rf7o8A9DTlFds1XLCe3joqLMm4M+gvBKD7YnGIdxQ9vA== -"@tauri-apps/cli-win32-x64-msvc@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.5.7.tgz#adfcce46f796dd22ef69fb26ad8c6972a3263985" - integrity sha512-OxLHVBNdzyQ//xT3kwjQFnJTn/N5zta/9fofAkXfnL7vqmVn6s/RY1LDa3sxCHlRaKw0n3ShpygRbM9M8+sO9w== +"@tauri-apps/cli-win32-x64-msvc@1.5.11": + version "1.5.11" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.5.11.tgz#c003ce00b36d056a8b08e0ecf4633c2bba00c497" + integrity sha512-1CexcqUFCis5ypUIMOKllxUBrna09McbftWENgvVXMfA+SP+yPDPAVb8fIvUcdTIwR/yHJwcIucmTB4anww4vg== -"@tauri-apps/cli@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-1.5.7.tgz#8f9a8bf577a39b7f7c0e5b125e7b5b3e149cfb5a" - integrity sha512-z7nXLpDAYfQqR5pYhQlWOr88DgPq1AfQyxHhGiakiVgWlaG0ikEfQxop2txrd52H0TRADG0JHR9vFrVFPv4hVQ== +"@tauri-apps/cli@1.5.11": + version "1.5.11" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-1.5.11.tgz#02beb559b3b55836c90a1ba9121b3fc50e3760cd" + integrity sha512-B475D7phZrq5sZ3kDABH4g2mEoUIHtnIO+r4ZGAAfsjMbZCwXxR/jlMGTEL+VO3YzjpF7gQe38IzB4vLBbVppw== optionalDependencies: - "@tauri-apps/cli-darwin-arm64" "1.5.7" - "@tauri-apps/cli-darwin-x64" "1.5.7" - "@tauri-apps/cli-linux-arm-gnueabihf" "1.5.7" - "@tauri-apps/cli-linux-arm64-gnu" "1.5.7" - "@tauri-apps/cli-linux-arm64-musl" "1.5.7" - "@tauri-apps/cli-linux-x64-gnu" "1.5.7" - "@tauri-apps/cli-linux-x64-musl" "1.5.7" - "@tauri-apps/cli-win32-arm64-msvc" "1.5.7" - "@tauri-apps/cli-win32-ia32-msvc" "1.5.7" - "@tauri-apps/cli-win32-x64-msvc" "1.5.7" + "@tauri-apps/cli-darwin-arm64" "1.5.11" + "@tauri-apps/cli-darwin-x64" "1.5.11" + "@tauri-apps/cli-linux-arm-gnueabihf" "1.5.11" + "@tauri-apps/cli-linux-arm64-gnu" "1.5.11" + "@tauri-apps/cli-linux-arm64-musl" "1.5.11" + "@tauri-apps/cli-linux-x64-gnu" "1.5.11" + "@tauri-apps/cli-linux-x64-musl" "1.5.11" + "@tauri-apps/cli-win32-arm64-msvc" "1.5.11" + "@tauri-apps/cli-win32-ia32-msvc" "1.5.11" + "@tauri-apps/cli-win32-x64-msvc" "1.5.11" "@trysound/sax@0.2.0": version "0.2.0" @@ -1601,10 +1594,10 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node@*", "@types/node@^20.9.0": - version "20.9.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.0.tgz#bfcdc230583aeb891cf51e73cfdaacdd8deae298" - integrity sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw== +"@types/node@*", "@types/node@^20.11.30": + version "20.11.30" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f" + integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw== dependencies: undici-types "~5.26.4" @@ -1632,10 +1625,10 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^18.2.14": - version "18.2.14" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.14.tgz#fa7a6fecf1ce35ca94e74874f70c56ce88f7a127" - integrity sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g== +"@types/react@*", "@types/react@^18.2.70": + version "18.2.70" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.70.tgz#89a37f9e0a6a4931f4259c598f40fd44dd6abf71" + integrity sha512-hjlM2hho2vqklPhopNkXkdkeq6Lv8WSZTpr7956zY+3WS5cfYUewtCzsJLsbW5dEv3lfSeQ4W14ZFeKC437JRQ== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2752,11 +2745,6 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== - define-properties@^1.1.3, define-properties@^1.1.4: version "1.2.0" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" @@ -2858,10 +2846,12 @@ elkjs@^0.8.2: resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e" integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ== -emoji-picker-react@^4.5.15: - version "4.5.15" - resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.15.tgz#e12797c50584cb8af8aee7eb6c7c8fd953e41f7e" - integrity sha512-BTqo+pNUE8kqX8BKFTbD4fhlxcA69qfie5En4PerReLaaPfXVyRlDJ1uf85nKj2u5esUQ999iUf8YyqcPsM2Qw== +emoji-picker-react@^4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.9.2.tgz#5118c5e1028ce4a96c94eb7c9bef09d30b08742c" + integrity sha512-pdvLKpto0DMrjE+/8V9QeYjrMcOkJmqBn3GyCSG2zanY32rN2cnWzBUmzArvapAjzBvgf7hNmJP8xmsdu0cmJA== + dependencies: + flairup "0.0.38" emoji-regex@^8.0.0: version "8.0.0" @@ -3103,12 +3093,13 @@ eslint-plugin-jsx-a11y@^6.5.1: object.fromentries "^2.0.6" semver "^6.3.0" -eslint-plugin-prettier@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" - integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== +eslint-plugin-prettier@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz#17cfade9e732cef32b5f5be53bd4e07afd8e67e1" + integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw== dependencies: prettier-linter-helpers "^1.0.0" + synckit "^0.8.6" "eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": version "4.6.0" @@ -3338,6 +3329,11 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +flairup@0.0.38: + version "0.0.38" + resolved "https://registry.yarnpkg.com/flairup/-/flairup-0.0.38.tgz#62216990a8317a1b07d1d816033624c5b2130f31" + integrity sha512-W9QA5TM7eYNlGoBYwfVn/o6v4yWBCxfq4+EJ5w774oFeyWvVWnYq6Dgt4CJltjG9y/lPwbOqz3jSSr8K66ToGg== + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -3499,11 +3495,6 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" -globalyzer@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" - integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== - globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -3527,11 +3518,6 @@ globby@^13.1.3: merge2 "^1.4.1" slash "^4.0.0" -globrex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" - integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -3850,11 +3836,6 @@ is-date-object@^1.0.1, is-date-object@^1.0.5: dependencies: has-tostringtag "^1.0.0" -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -3979,13 +3960,6 @@ is-weakset@^2.0.1: call-bind "^1.0.2" get-intrinsic "^1.1.1" -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -4960,15 +4934,6 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" -open@^8.4.0: - version "8.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" - integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -5748,13 +5713,13 @@ svgo@^2.8.0: picocolors "^1.0.0" stable "^0.1.8" -synckit@^0.8.5: - version "0.8.5" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.5.tgz#b7f4358f9bb559437f9f167eb6bc46b3c9818fa3" - integrity sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q== +synckit@^0.8.5, synckit@^0.8.6: + version "0.8.8" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7" + integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ== dependencies: - "@pkgr/utils" "^2.3.1" - tslib "^2.5.0" + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" @@ -5797,14 +5762,6 @@ through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -tiny-glob@^0.2.9: - version "0.2.9" - resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" - integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== - dependencies: - globalyzer "0.1.0" - globrex "^0.1.2" - tiny-invariant@^1.0.6: version "1.3.1" resolved "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" @@ -5852,11 +5809,16 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.0: +tslib@^2.1.0, tslib@^2.4.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== +tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"