Bladeren bron

Merge remote-tracking branch 'origin/main' into gpt-4o

# Conflicts:
#	public/apple-touch-icon.png
Hao Jia 1 jaar geleden
bovenliggende
commit
01c9dbc1fd

+ 92 - 3
.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
+*.key.pub

+ 15 - 1
.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=

+ 20 - 1
README.md

@@ -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

+ 19 - 0
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`。

+ 189 - 0
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);
+  }
+}

+ 25 - 6
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}`);

+ 17 - 4
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,12 +112,29 @@ 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

+ 1 - 0
app/api/config/route.ts

@@ -13,6 +13,7 @@ const DANGER_CONFIG = {
   hideBalanceQuery: serverConfig.hideBalanceQuery,
   disableFastLink: serverConfig.disableFastLink,
   customModels: serverConfig.customModels,
+  defaultModel: serverConfig.defaultModel,
 };
 
 declare global {

+ 56 - 24
app/api/webdav/[...path]/route.ts

@@ -1,5 +1,14 @@
 import { NextRequest, NextResponse } from "next/server";
-import { STORAGE_KEY } from "../../../constant";
+import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant";
+import { getServerSideConfig } from "@/app/config/server";
+
+const config = getServerSideConfig();
+
+const mergedAllowedWebDavEndpoints = [
+  ...internalAllowedWebDavEndpoints,
+  ...config.allowedWebDevEndpoints,
+].filter((domain) => Boolean(domain.trim()));
+
 async function handle(
   req: NextRequest,
   { params }: { params: { path: string[] } },
@@ -12,17 +21,37 @@ async function handle(
 
   const requestUrl = new URL(req.url);
   let endpoint = requestUrl.searchParams.get("endpoint");
+
+  // Validate the endpoint to prevent potential SSRF attacks
+  if (
+    !mergedAllowedWebDavEndpoints.some(
+      (allowedEndpoint) => endpoint?.startsWith(allowedEndpoint),
+    )
+  ) {
+    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 " + params.path.join("/"),
+        msg: "you are not allowed to request " + targetPath,
       },
       {
         status: 403,
@@ -31,14 +60,11 @@ async function handle(
   }
 
   // for MKCOL request, only allow request ${folder}
-  if (
-    req.method == "MKCOL" &&
-    !new URL(endpointPath).pathname.endsWith(folder)
-  ) {
+  if (req.method === "MKCOL" && !targetPath.endsWith(folder)) {
     return NextResponse.json(
       {
         error: true,
-        msg: "you are not allowed to request " + params.path.join("/"),
+        msg: "you are not allowed to request " + targetPath,
       },
       {
         status: 403,
@@ -47,14 +73,11 @@ async function handle(
   }
 
   // for GET request, only allow request ending with fileName
-  if (
-    req.method == "GET" &&
-    !new URL(endpointPath).pathname.endsWith(fileName)
-  ) {
+  if (req.method === "GET" && !targetPath.endsWith(fileName)) {
     return NextResponse.json(
       {
         error: true,
-        msg: "you are not allowed to request " + params.path.join("/"),
+        msg: "you are not allowed to request " + targetPath,
       },
       {
         status: 403,
@@ -63,14 +86,11 @@ async function handle(
   }
 
   //   for PUT request, only allow request ending with fileName
-  if (
-    req.method == "PUT" &&
-    !new URL(endpointPath).pathname.endsWith(fileName)
-  ) {
+  if (req.method === "PUT" && !targetPath.endsWith(fileName)) {
     return NextResponse.json(
       {
         error: true,
-        msg: "you are not allowed to request " + params.path.join("/"),
+        msg: "you are not allowed to request " + targetPath,
       },
       {
         status: 403,
@@ -78,7 +98,7 @@ async function handle(
     );
   }
 
-  const targetUrl = `${endpoint + endpointPath}`;
+  const targetUrl = targetPath;
 
   const method = req.method;
   const shouldNotHaveBody = ["get", "head"].includes(
@@ -90,22 +110,34 @@ async function handle(
       authorization: req.headers.get("authorization") ?? "",
     },
     body: shouldNotHaveBody ? null : req.body,
+    redirect: "manual",
     method,
     // @ts-ignore
     duplex: "half",
   };
 
-  const fetchResult = await fetch(targetUrl, fetchOptions);
+  let fetchResult;
 
-  console.log("[Any Proxy]", targetUrl, {
-    status: fetchResult.status,
-    statusText: fetchResult.statusText,
-  });
+  try {
+    fetchResult = await fetch(targetUrl, fetchOptions);
+  } finally {
+    console.log(
+      "[Any Proxy]",
+      targetUrl,
+      {
+        method: req.method,
+      },
+      {
+        status: fetchResult?.status,
+        statusText: fetchResult?.statusText,
+      },
+    );
+  }
 
   return fetchResult;
 }
 
-export const POST = handle;
+export const PUT = handle;
 export const GET = handle;
 export const OPTIONS = handle;
 

+ 10 - 4
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() {}

+ 415 - 0
app/client/platforms/anthropic.ts

@@ -0,0 +1,415 @@
+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<void> {
+    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,
+                },
+              };
+            }),
+        };
+      });
+
+    if (prompt[0]?.role === "assistant") {
+      prompt.unshift({
+        role: "user",
+        content: ";",
+      });
+    }
+
+    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 = "";
+
+    if (accessStore.useCustomConfig) {
+      baseUrl = 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;
+}

+ 13 - 10
app/client/platforms/google.ts

@@ -21,11 +21,10 @@ export class GeminiProApi implements LLMApi {
   }
   async chat(options: ChatOptions): Promise<void> {
     // 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;
@@ -104,24 +103,27 @@ export class GeminiProApi implements LLMApi {
     };
 
     const accessStore = useAccessStore.getState();
-    let baseUrl = accessStore.googleUrl;
+
+    let baseUrl = "";
+
+    if (accessStore.useCustomConfig) {
+      baseUrl = accessStore.googleUrl;
+    }
+
     const isApp = !!getClientConfig()?.isApp;
 
     let shouldStream = !!options.config.stream;
     const controller = new AbortController();
     options.onController?.(controller);
     try {
-      let googleChatPath = visionModel
-        ? Google.VisionChatPath
-        : Google.ChatPath;
-      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) {
@@ -139,6 +141,7 @@ export class GeminiProApi implements LLMApi {
         () => controller.abort(),
         REQUEST_TIMEOUT_MS,
       );
+
       if (shouldStream) {
         let responseText = "";
         let remainText = "";

+ 56 - 28
app/client/platforms/openai.ts

@@ -40,21 +40,43 @@ 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;
 
   path(path: string): string {
     const accessStore = useAccessStore.getState();
 
-    const isAzure = accessStore.provider === ServiceProvider.Azure;
+    let baseUrl = "";
 
-    if (isAzure && !accessStore.isValidAzure()) {
-      throw Error(
-        "incomplete azure config, please check it in your settings page",
-      );
-    }
+    if (accessStore.useCustomConfig) {
+      const isAzure = accessStore.provider === ServiceProvider.Azure;
 
-    let baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
+      if (isAzure && !accessStore.isValidAzure()) {
+        throw Error(
+          "incomplete azure config, please check it in your settings page",
+        );
+      }
+
+      if (isAzure) {
+        path = makeAzurePath(path, accessStore.azureApiVersion);
+      }
+
+      baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
+    }
 
     if (baseUrl.length === 0) {
       const isApp = !!getClientConfig()?.isApp;
@@ -70,10 +92,6 @@ export class ChatGPTApi implements LLMApi {
       baseUrl = "https://" + baseUrl;
     }
 
-    if (isAzure) {
-      path = makeAzurePath(path, accessStore.azureApiVersion);
-    }
-
     console.log("[Proxy Endpoint] ", baseUrl, path);
 
     return [baseUrl, path].join("/");
@@ -98,7 +116,7 @@ export class ChatGPTApi implements LLMApi {
       },
     };
 
-    const requestPayload = {
+    const requestPayload: RequestPayload = {
       messages,
       stream: options.config.stream,
       model: modelConfig.model,
@@ -111,13 +129,8 @@ 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,
-      });
+    if (visionModel && modelConfig.model.includes("preview")) {
+      requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
     }
 
     console.log("[Request] openai payload: ", requestPayload);
@@ -151,6 +164,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 +241,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() {

+ 6 - 2
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 (
     <Draggable draggableId={`${props.id}`} index={props.index}>
       {(provided) => (
         <div
           className={`${styles["chat-item"]} ${
-            props.selected && styles["chat-item-selected"]
+            props.selected &&
+            (currentPath === Path.Chat || currentPath === Path.Home) &&
+            styles["chat-item-selected"]
           }`}
           onClick={props.onClick}
           ref={(ele) => {

+ 23 - 7
app/components/chat.tsx

@@ -448,10 +448,20 @@ export function ChatActions(props: {
   // switch model
   const currentModel = chatStore.currentSession().mask.modelConfig.model;
   const allModels = useAllModels();
-  const models = useMemo(
-    () => allModels.filter((m) => m.available),
-    [allModels],
-  );
+  const models = useMemo(() => {
+    const filteredModels = allModels.filter((m) => m.available);
+    const defaultModel = filteredModels.find((m) => m.isDefault);
+
+    if (defaultModel) {
+      const arr = [
+        defaultModel,
+        ...filteredModels.filter((m) => m !== defaultModel),
+      ];
+      return arr;
+    } else {
+      return filteredModels;
+    }
+  }, [allModels]);
   const [showModelSelector, setShowModelSelector] = useState(false);
   const [showUploadImage, setShowUploadImage] = useState(false);
 
@@ -467,7 +477,10 @@ export function ChatActions(props: {
     // switch to first available model
     const isUnavaliableModel = !models.some((m) => m.name === currentModel);
     if (isUnavaliableModel && models.length > 0) {
-      const nextModel = models[0].name as ModelType;
+      // show next model to default model if exist
+      let nextModel: ModelType = (
+        models.find((model) => model.isDefault) || models[0]
+      ).name;
       chatStore.updateCurrentSession(
         (session) => (session.mask.modelConfig.model = nextModel),
       );
@@ -1075,6 +1088,7 @@ function _Chat() {
             if (payload.url) {
               accessStore.update((access) => (access.openaiUrl = payload.url!));
             }
+            accessStore.update((access) => (access.useCustomConfig = true));
           });
         }
       } catch {
@@ -1102,11 +1116,13 @@ function _Chat() {
     };
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
-  
+
   const handlePaste = useCallback(
     async (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
       const currentModel = chatStore.currentSession().mask.modelConfig.model;
-      if(!isVisionModel(currentModel)){return;}
+      if (!isVisionModel(currentModel)) {
+        return;
+      }
       const items = (event.clipboardData || window.clipboardData).items;
       for (const item of items) {
         if (item.kind === "file" && item.type.startsWith("image/")) {

+ 3 - 0
app/components/exporter.tsx

@@ -40,6 +40,7 @@ import { EXPORT_MESSAGE_CLASS_NAME, ModelProvider } from "../constant";
 import { getClientConfig } from "../config/client";
 import { ClientApi } from "../client/api";
 import { getMessageTextContent } from "../utils";
+import { identifyDefaultClaudeModel } from "../utils/checkers";
 
 const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
   loading: () => <LoadingIcon />,
@@ -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);
     }

+ 3 - 0
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);
   }

+ 21 - 4
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 (
     <ReactMarkdown

+ 10 - 1
app/components/mask.tsx

@@ -404,7 +404,16 @@ export function MaskPage() {
   const maskStore = useMaskStore();
   const chatStore = useChatStore();
 
-  const [filterLang, setFilterLang] = useState<Lang>();
+  const [filterLang, setFilterLang] = useState<Lang | undefined>(
+    () => 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()

+ 1 - 1
app/components/message-selector.tsx

@@ -227,7 +227,7 @@ export function MessageSelector(props: {
               </div>
 
               <div className={styles["checkbox"]}>
-                <input type="checkbox" checked={isSelected}></input>
+                <input type="checkbox" checked={isSelected} readOnly></input>
               </div>
             </div>
           );

+ 70 - 4
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,
@@ -963,7 +964,7 @@ export function Settings() {
                     </Select>
                   </ListItem>
 
-                  {accessStore.provider === "OpenAI" ? (
+                  {accessStore.provider === ServiceProvider.OpenAI && (
                     <>
                       <ListItem
                         title={Locale.Settings.Access.OpenAI.Endpoint.Title}
@@ -1002,7 +1003,8 @@ export function Settings() {
                         />
                       </ListItem>
                     </>
-                  ) : accessStore.provider === "Azure" ? (
+                  )}
+                  {accessStore.provider === ServiceProvider.Azure && (
                     <>
                       <ListItem
                         title={Locale.Settings.Access.Azure.Endpoint.Title}
@@ -1061,7 +1063,8 @@ export function Settings() {
                         ></input>
                       </ListItem>
                     </>
-                  ) : accessStore.provider === "Google" ? (
+                  )}
+                  {accessStore.provider === ServiceProvider.Google && (
                     <>
                       <ListItem
                         title={Locale.Settings.Access.Google.Endpoint.Title}
@@ -1120,7 +1123,70 @@ export function Settings() {
                         ></input>
                       </ListItem>
                     </>
-                  ) : null}
+                  )}
+                  {accessStore.provider === ServiceProvider.Anthropic && (
+                    <>
+                      <ListItem
+                        title={Locale.Settings.Access.Anthropic.Endpoint.Title}
+                        subTitle={
+                          Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
+                          Anthropic.ExampleEndpoint
+                        }
+                      >
+                        <input
+                          type="text"
+                          value={accessStore.anthropicUrl}
+                          placeholder={Anthropic.ExampleEndpoint}
+                          onChange={(e) =>
+                            accessStore.update(
+                              (access) =>
+                                (access.anthropicUrl = e.currentTarget.value),
+                            )
+                          }
+                        ></input>
+                      </ListItem>
+                      <ListItem
+                        title={Locale.Settings.Access.Anthropic.ApiKey.Title}
+                        subTitle={
+                          Locale.Settings.Access.Anthropic.ApiKey.SubTitle
+                        }
+                      >
+                        <PasswordInput
+                          value={accessStore.anthropicApiKey}
+                          type="text"
+                          placeholder={
+                            Locale.Settings.Access.Anthropic.ApiKey.Placeholder
+                          }
+                          onChange={(e) => {
+                            accessStore.update(
+                              (access) =>
+                                (access.anthropicApiKey =
+                                  e.currentTarget.value),
+                            );
+                          }}
+                        />
+                      </ListItem>
+                      <ListItem
+                        title={Locale.Settings.Access.Anthropic.ApiVerion.Title}
+                        subTitle={
+                          Locale.Settings.Access.Anthropic.ApiVerion.SubTitle
+                        }
+                      >
+                        <input
+                          type="text"
+                          value={accessStore.anthropicApiVersion}
+                          placeholder={Anthropic.Vision}
+                          onChange={(e) =>
+                            accessStore.update(
+                              (access) =>
+                                (access.anthropicApiVersion =
+                                  e.currentTarget.value),
+                            )
+                          }
+                        ></input>
+                      </ListItem>
+                    </>
+                  )}
                 </>
               )}
             </>

+ 41 - 10
app/config/server.ts

@@ -21,6 +21,7 @@ declare global {
       ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
       DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
       CUSTOM_MODELS?: string; // to control custom models
+      DEFAULT_MODEL?: string; // to cnntrol default model in every new chat window
 
       // azure only
       AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
@@ -50,6 +51,22 @@ const ACCESS_CODES = (function getAccessCodes(): Set<string> {
   }
 })();
 
+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(
@@ -59,39 +76,51 @@ export const getServerSideConfig = () => {
 
   const disableGPT4 = !!process.env.DISABLE_GPT4;
   let customModels = process.env.CUSTOM_MODELS ?? "";
+  let defaultModel = process.env.DEFAULT_MODEL ?? "";
 
   if (disableGPT4) {
     if (customModels) customModels += ",";
     customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4"))
       .map((m) => "-" + m.name)
       .join(",");
+    if (defaultModel.startsWith("gpt-4")) defaultModel = "";
   }
 
   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());
-  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 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: getApiKey(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 +135,7 @@ export const getServerSideConfig = () => {
     hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
     disableFastLink: !!process.env.DISABLE_FAST_LINK,
     customModels,
+    defaultModel,
+    allowedWebDevEndpoints,
   };
 };

+ 82 - 163
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/";
 
@@ -25,6 +26,7 @@ export enum Path {
 export enum ApiPath {
   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",
@@ -87,19 +98,24 @@ export const Azure = {
 
 export const Google = {
   ExampleEndpoint: "https://generativelanguage.googleapis.com/",
-  ChatPath: "v1beta/models/gemini-pro:generateContent",
-  VisionChatPath: "v1beta/models/gemini-pro-vision:generateContent",
-
-  // /api/openai/v1/chat/completions
+  ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`,
 };
 
 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$$
 `;
 
@@ -108,188 +124,91 @@ export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
 
 export const KnowledgeCutOffDate: Record<string, string> = {
   default: "2021-09",
+  "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.
   "gemini-pro": "2023-12",
+  "gemini-pro-vision": "2023-12",
 };
 
+const openaiModels = [
+  "gpt-3.5-turbo",
+  "gpt-3.5-turbo-1106",
+  "gpt-3.5-turbo-0125",
+  "gpt-4",
+  "gpt-4-0613",
+  "gpt-4-32k",
+  "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",
+];
+
+const googleModels = [
+  "gemini-1.0-pro",
+  "gemini-1.5-pro-latest",
+  "gemini-1.5-flash-latest",
+  "gemini-pro-vision",
+];
+
+const anthropicModels = [
+  "claude-instant-1.2",
+  "claude-2.0",
+  "claude-2.1",
+  "claude-3-sonnet-20240229",
+  "claude-3-opus-20240229",
+  "claude-3-haiku-20240307",
+];
+
 export const DEFAULT_MODELS = [
-  {
-    name: "gpt-4",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-4-0314",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-4-0613",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-4-32k",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-4-32k-0314",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-4-32k-0613",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-4-turbo-preview",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-4-1106-preview",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-4-0125-preview",
+  ...openaiModels.map((name) => ({
+    name,
     available: true,
     provider: {
       id: "openai",
       providerName: "OpenAI",
       providerType: "openai",
     },
-  },
-  {
-    name: "gpt-4-vision-preview",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-3.5-turbo",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-3.5-turbo-0125",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-3.5-turbo-0301",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-3.5-turbo-0613",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-3.5-turbo-1106",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-3.5-turbo-16k",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gpt-3.5-turbo-16k-0613",
-    available: true,
-    provider: {
-      id: "openai",
-      providerName: "OpenAI",
-      providerType: "openai",
-    },
-  },
-  {
-    name: "gemini-pro",
+  })),
+  ...googleModels.map((name) => ({
+    name,
     available: true,
     provider: {
       id: "google",
       providerName: "Google",
       providerType: "google",
     },
-  },
-  {
-    name: "gemini-pro-vision",
+  })),
+  ...anthropicModels.map((name) => ({
+    name,
     available: true,
     provider: {
-      id: "google",
-      providerName: "Google",
-      providerType: "google",
+      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 internalAllowedWebDavEndpoints = [
+  "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",
+];

+ 4 - 0
app/layout.tsx

@@ -36,6 +36,10 @@ export default function RootLayout({
     <html lang="en">
       <head>
         <meta name="config" content={JSON.stringify(getClientConfig())} />
+        <meta
+          name="viewport"
+          content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
+        />
         <link rel="manifest" href="/site.webmanifest"></link>
         <script src="/serviceWorkerRegister.js" defer></script>
       </head>

+ 17 - 0
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 密钥",

+ 19 - 1
app/locales/en.ts

@@ -296,7 +296,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: {
@@ -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",

+ 17 - 0
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",

+ 17 - 0
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",

+ 88 - 71
app/locales/tw.ts

@@ -8,14 +8,14 @@ const tw = {
   Error: {
     Unauthorized: isApp
       ? "檢測到無效 API Key,請前往[設定](/#/settings)頁檢查 API Key 是否設定正確。"
-      : "訪問密碼不正確或為空,請前往[登入](/#/auth)頁輸入正確的訪問密碼,或者在[設定](/#/settings)頁填入你自己的 OpenAI API Key。",
+      : "存取密碼不正確或未填寫,請前往[登入](/#/auth)頁輸入正確的存取密碼,或者在[設定](/#/settings)頁填入你自己的 OpenAI API Key。",
   },
 
   Auth: {
     Title: "需要密碼",
-    Tips: "管理員開啟了密碼驗證,請在下方填入訪問碼",
-    SubTips: "或者輸入你的 OpenAI 或 Google API 鑰",
-    Input: "在此處填寫訪問碼",
+    Tips: "管理員開啟了密碼驗證,請在下方填入存取密碼",
+    SubTips: "或者輸入你的 OpenAI 或 Google API 鑰",
+    Input: "在此處填寫存取密碼",
     Confirm: "確認",
     Later: "稍候再說",
   },
@@ -25,10 +25,10 @@ const tw = {
   Chat: {
     SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 則對話`,
     EditMessage: {
-      Title: "編輯息記錄",
+      Title: "編輯息記錄",
       Topic: {
         Title: "聊天主題",
-        SubTitle: "更改前聊天主題",
+        SubTitle: "更改前聊天主題",
       },
     },
     Actions: {
@@ -40,13 +40,13 @@ const tw = {
       Retry: "重試",
       Pin: "固定",
       PinToastContent: "已將 1 條對話固定至預設提示詞",
-      PinToastAction: "查看",
+      PinToastAction: "檢視",
       Delete: "刪除",
       Edit: "編輯",
     },
     Commands: {
       new: "新建聊天",
-      newm: "從面具新建聊天",
+      newm: "從角色範本新建聊天",
       next: "下一個聊天",
       prev: "上一個聊天",
       clear: "清除上下文",
@@ -61,7 +61,7 @@ const tw = {
         dark: "深色模式",
       },
       Prompt: "快捷指令",
-      Masks: "所有面具",
+      Masks: "所有角色範本",
       Clear: "清除聊天",
       Settings: "對話設定",
       UploadImage: "上傳圖片",
@@ -90,27 +90,27 @@ const tw = {
     MessageFromYou: "來自您的訊息",
     MessageFromChatGPT: "來自 ChatGPT 的訊息",
     Format: {
-      Title: "出格式",
-      SubTitle: "可以導出 Markdown 文本或者 PNG 圖片",
+      Title: "出格式",
+      SubTitle: "可以匯出 Markdown 文字檔或者 PNG 圖片",
     },
     IncludeContext: {
-      Title: "包含面具上下文",
-      SubTitle: "是否在消息中展示面具上下文",
+      Title: "包含角色範本上下文",
+      SubTitle: "是否在訊息中顯示角色範本上下文",
     },
     Steps: {
       Select: "選取",
       Preview: "預覽",
     },
     Image: {
-      Toast: "正在生截圖",
-      Modal: "長按或右鍵保存圖片",
+      Toast: "正在生截圖",
+      Modal: "長按或按右鍵儲存圖片",
     },
   },
   Select: {
-    Search: "查詢息",
+    Search: "查詢息",
     All: "選取全部",
     Latest: "最近幾條",
-    Clear: "清除選",
+    Clear: "清除選",
   },
   Memory: {
     Title: "上下文記憶 Prompt",
@@ -121,7 +121,7 @@ const tw = {
     ResetConfirm: "重設後將清除目前對話記錄以及歷史記憶,確認重設?",
   },
   Home: {
-    NewChat: "新對話",
+    NewChat: "新對話",
     DeleteChat: "確定要刪除選取的對話嗎?",
     DeleteToast: "已刪除對話",
     Revert: "撤銷",
@@ -132,10 +132,10 @@ const tw = {
 
     Danger: {
       Reset: {
-        Title: "重所有設定",
-        SubTitle: "重所有設定項回預設值",
-        Action: "立即重",
-        Confirm: "確認重所有設定?",
+        Title: "重所有設定",
+        SubTitle: "重所有設定項回預設值",
+        Action: "立即重",
+        Confirm: "確認重所有設定?",
       },
       Clear: {
         Title: "清除所有資料",
@@ -158,8 +158,8 @@ const tw = {
       SubTitle: "強制在每個請求的訊息列表開頭新增一個模擬 ChatGPT 的系統提示",
     },
     InputTemplate: {
-      Title: "用戶輸入預處理",
-      SubTitle: "用戶最新的一條消息會填充到此模板",
+      Title: "使用者輸入預處理",
+      SubTitle: "使用者最新的一條訊息會填充到此範本",
     },
 
     Update: {
@@ -178,8 +178,8 @@ const tw = {
       SubTitle: "在預覽氣泡中預覽 Markdown 內容",
     },
     AutoGenerateTitle: {
-      Title: "自動生標題",
-      SubTitle: "根據對話內容生合適的標題",
+      Title: "自動生標題",
+      SubTitle: "根據對話內容生合適的標題",
     },
     Sync: {
       CloudState: "雲端資料",
@@ -194,7 +194,7 @@ const tw = {
         },
         SyncType: {
           Title: "同步類型",
-          SubTitle: "選擇喜愛的同步服器",
+          SubTitle: "選擇喜愛的同步服器",
         },
         Proxy: {
           Title: "啟用代理",
@@ -202,12 +202,12 @@ const tw = {
         },
         ProxyUrl: {
           Title: "代理地址",
-          SubTitle: "僅適用於本項目自帶的跨域代理",
+          SubTitle: "僅適用於本專案自帶的跨域代理",
         },
 
         WebDav: {
           Endpoint: "WebDAV 地址",
-          UserName: "用戶名",
+          UserName: "使用者名稱",
           Password: "密碼",
         },
 
@@ -220,18 +220,18 @@ const tw = {
 
       LocalState: "本地資料",
       Overview: (overview: any) => {
-        return `${overview.chat} 次對話,${overview.message} 條消息,${overview.prompt} 條提示詞,${overview.mask} 個面具`;
+        return `${overview.chat} 次對話,${overview.message} 條訊息,${overview.prompt} 條提示詞,${overview.mask} 個角色範本`;
       },
-      ImportFailed: "入失敗",
+      ImportFailed: "入失敗",
     },
     Mask: {
       Splash: {
-        Title: "面具啟動頁面",
-        SubTitle: "新增聊天時,呈現面具啟動頁面",
+        Title: "角色範本啟動頁面",
+        SubTitle: "新增聊天時,呈現角色範本啟動頁面",
       },
       Builtin: {
-        Title: "隱藏內置面具",
-        SubTitle: "在所有面具列表中隱藏內置面具",
+        Title: "隱藏內建角色範本",
+        SubTitle: "在所有角色範本列表中隱藏內建角色範本",
       },
     },
     Prompt: {
@@ -273,12 +273,12 @@ const tw = {
 
     Access: {
       AccessCode: {
-        Title: "訪問密碼",
-        SubTitle: "管理員已開啟加密訪問",
-        Placeholder: "請輸入訪問密碼",
+        Title: "存取密碼",
+        SubTitle: "管理員已開啟加密存取",
+        Placeholder: "請輸入存取密碼",
       },
       CustomEndpoint: {
-        Title: "自定義接口 (Endpoint)",
+        Title: "自定義介面 (Endpoint)",
         SubTitle: "是否使用自定義 Azure 或 OpenAI 服務",
       },
       Provider: {
@@ -288,42 +288,59 @@ const tw = {
       OpenAI: {
         ApiKey: {
           Title: "API Key",
-          SubTitle: "使用自定義 OpenAI Key 繞過密碼訪問限制",
+          SubTitle: "使用自定義 OpenAI Key 繞過密碼存取限制",
           Placeholder: "OpenAI API Key",
         },
 
         Endpoint: {
-          Title: "接口(Endpoint) 地址",
-          SubTitle: "除默認地址外,必須包含 http(s)://",
+          Title: "介面(Endpoint) 地址",
+          SubTitle: "除預設地址外,必須包含 http(s)://",
         },
       },
       Azure: {
         ApiKey: {
-          Title: "接口密鑰",
-          SubTitle: "使用自定義 Azure Key 繞過密碼訪問限制",
+          Title: "介面金鑰",
+          SubTitle: "使用自定義 Azure Key 繞過密碼存取限制",
           Placeholder: "Azure API Key",
         },
 
         Endpoint: {
-          Title: "接口(Endpoint) 地址",
+          Title: "介面(Endpoint) 地址",
           SubTitle: "樣例:",
         },
 
         ApiVerion: {
-          Title: "接口版本 (azure api version)",
+          Title: "介面版本 (azure api version)",
           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 密鑰",
-          SubTitle: "從 Google AI 獲取您的 API 密鑰",
-          Placeholder: "輸入您的 Google AI Studio API 密鑰",
+          Title: "API 鑰",
+          SubTitle: "從 Google AI 取得您的 API 金鑰",
+          Placeholder: "輸入您的 Google AI Studio API 鑰",
         },
 
         Endpoint: {
           Title: "終端地址",
-          SubTitle: "示例:",
+          SubTitle: "例:",
         },
 
         ApiVersion: {
@@ -343,7 +360,7 @@ const tw = {
       SubTitle: "值越大,回應越隨機",
     },
     TopP: {
-      Title: "核樣 (top_p)",
+      Title: "核心採樣 (top_p)",
       SubTitle: "與隨機性類似,但不要和隨機性一起更改",
     },
     MaxTokens: {
@@ -390,11 +407,11 @@ const tw = {
   Plugin: { Name: "外掛" },
   FineTuned: { Sysmessage: "你是一個助手" },
   Mask: {
-    Name: "面具",
+    Name: "角色範本",
     Page: {
-      Title: "預設角色面具",
+      Title: "預設角色角色範本",
       SubTitle: (count: number) => `${count} 個預設角色定義`,
-      Search: "搜尋角色面具",
+      Search: "搜尋角色角色範本",
       Create: "新增",
     },
     Item: {
@@ -407,7 +424,7 @@ const tw = {
     },
     EditModal: {
       Title: (readonly: boolean) =>
-        `編輯預設面具 ${readonly ? "(只讀)" : ""}`,
+        `編輯預設角色範本 ${readonly ? "(唯讀)" : ""}`,
       Download: "下載預設",
       Clone: "複製預設",
     },
@@ -415,18 +432,18 @@ const tw = {
       Avatar: "角色頭像",
       Name: "角色名稱",
       Sync: {
-        Title: "使用全設定",
-        SubTitle: "當前對話是否使用全局模型設定",
-        Confirm: "當前對話的自定義設定將會被自動覆蓋,確認啟用全局設定?",
+        Title: "使用全域性設定",
+        SubTitle: "目前對話是否使用全域性模型設定",
+        Confirm: "目前對話的自定義設定將會被自動覆蓋,確認啟用全域性設定?",
       },
       HideContext: {
         Title: "隱藏預設對話",
-        SubTitle: "隱藏後預設對話不會出現在聊天面",
+        SubTitle: "隱藏後預設對話不會出現在聊天面",
       },
       Share: {
-        Title: "分享此面具",
-        SubTitle: "生成此面具的直達鏈接",
-        Action: "覆制鏈接",
+        Title: "分享此角色範本",
+        SubTitle: "產生此角色範本的直達連結",
+        Action: "複製連結",
       },
     },
   },
@@ -435,12 +452,12 @@ const tw = {
     Skip: "跳過",
     NotShow: "不再呈現",
     ConfirmNoShow: "確認停用?停用後可以隨時在設定中重新啟用。",
-    Title: "挑選一個面具",
-    SubTitle: "現在開始,與面具背後的靈魂思維碰撞",
+    Title: "挑選一個角色範本",
+    SubTitle: "現在開始,與角色範本背後的靈魂思維碰撞",
     More: "搜尋更多",
   },
   URLCommand: {
-    Code: "檢測到連結中已經包含訪問碼,是否自動填入?",
+    Code: "檢測到連結中已經包含存取密碼,是否自動填入?",
     Settings: "檢測到連結中包含了預設設定,是否自動填入?",
   },
   UI: {
@@ -449,14 +466,14 @@ const tw = {
     Close: "關閉",
     Create: "新增",
     Edit: "編輯",
-    Export: "出",
-    Import: "入",
+    Export: "出",
+    Import: "入",
     Sync: "同步",
     Config: "設定",
   },
   Exporter: {
     Description: {
-      Title: "只有清除上下文之後的消息會被展示",
+      Title: "只有清除上下文之後的訊息會被顯示",
     },
     Model: "模型",
     Messages: "訊息",
@@ -467,12 +484,12 @@ const tw = {
 
 type DeepPartial<T> = T extends object
   ? {
-    [P in keyof T]?: DeepPartial<T[P]>;
-  }
+      [P in keyof T]?: DeepPartial<T[P]>;
+    }
   : T;
 
 export type LocaleType = typeof tw;
 export type PartialLocaleType = DeepPartial<typeof tw>;
 
 export default tw;
-// Translated by @chunkiuuu, feel free the submit new pr if there are typo/incorrect translations :D
+// Translated by @chunkiuuu, feel free the submit new pr if there are typo/incorrect translations :D

+ 6 - 3
app/masks/index.ts

@@ -1,5 +1,6 @@
 import { Mask } from "../store/mask";
 import { CN_MASKS } from "./cn";
+import { TW_MASKS } from "./tw";
 import { EN_MASKS } from "./en";
 
 import { type BuiltinMask } from "./typing";
@@ -21,6 +22,8 @@ export const BUILTIN_MASK_STORE = {
   },
 };
 
-export const BUILTIN_MASKS: BuiltinMask[] = [...CN_MASKS, ...EN_MASKS].map(
-  (m) => BUILTIN_MASK_STORE.add(m),
-);
+export const BUILTIN_MASKS: BuiltinMask[] = [
+  ...CN_MASKS,
+  ...TW_MASKS,
+  ...EN_MASKS,
+].map((m) => BUILTIN_MASK_STORE.add(m));

File diff suppressed because it is too large
+ 420 - 0
app/masks/tw.ts


+ 19 - 0
app/store/access.ts

@@ -8,6 +8,7 @@ import { getHeaders } from "../client/api";
 import { getClientConfig } from "../config/client";
 import { createPersistStore } from "../utils/store";
 import { ensure } from "../utils/clone";
+import { DEFAULT_CONFIG } from "./config";
 
 let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
 
@@ -36,6 +37,11 @@ const DEFAULT_ACCESS_STATE = {
   googleApiKey: "",
   googleApiVersion: "v1",
 
+  // anthropic
+  anthropicApiKey: "",
+  anthropicApiVersion: "2023-06-01",
+  anthropicUrl: "",
+
   // server config
   needCode: true,
   hideUserApiKey: false,
@@ -43,6 +49,7 @@ const DEFAULT_ACCESS_STATE = {
   disableGPT4: false,
   disableFastLink: false,
   customModels: "",
+  defaultModel: "",
 };
 
 export const useAccessStore = createPersistStore(
@@ -67,6 +74,10 @@ export const useAccessStore = createPersistStore(
       return ensure(get(), ["googleApiKey"]);
     },
 
+    isValidAnthropic() {
+      return ensure(get(), ["anthropicApiKey"]);
+    },
+
     isAuthorized() {
       this.fetch();
 
@@ -75,6 +86,7 @@ export const useAccessStore = createPersistStore(
         this.isValidOpenAI() ||
         this.isValidAzure() ||
         this.isValidGoogle() ||
+        this.isValidAnthropic() ||
         !this.enabledAccessControl() ||
         (this.enabledAccessControl() && ensure(get(), ["accessCode"]))
       );
@@ -90,6 +102,13 @@ export const useAccessStore = createPersistStore(
         },
       })
         .then((res) => res.json())
+        .then((res) => {
+          // Set default model from env request
+          let defaultModel = res.defaultModel ?? "";
+          DEFAULT_CONFIG.modelConfig.model =
+            defaultModel !== "" ? defaultModel : "gpt-3.5-turbo";
+          return res;
+        })
         .then((res: DangerConfig) => {
           console.log("[Config] got config from server", res);
           set(() => ({ ...res }));

+ 31 - 5
app/store/chat.ts

@@ -20,6 +20,9 @@ import { prettyObject } from "../utils/format";
 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;
@@ -86,9 +89,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;
@@ -119,13 +132,18 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
     ServiceProvider: serviceProvider,
     cutoff,
     model: modelConfig.model,
-    time: new Date().toLocaleString(),
+    time: new Date().toString(),
     lang: getLang(),
     input: input,
   };
 
   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 +366,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 +514,6 @@ export const useChatStore = createPersistStore(
           tokenCount += estimateTokenLength(getMessageTextContent(msg));
           reversedRecentMessages.push(msg);
         }
-
         // concat all messages
         const recentMessages = [
           ...systemPrompts,
@@ -533,6 +552,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 +578,7 @@ export const useChatStore = createPersistStore(
             messages: topicMessages,
             config: {
               model: getSummarizeModel(session.mask.modelConfig.model),
+              stream: false,
             },
             onFinish(message) {
               get().updateCurrentSession(
@@ -600,6 +622,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 +635,7 @@ export const useChatStore = createPersistStore(
               }),
             ),
             config: {
-              ...modelConfig,
+              ...modelcfg,
               stream: true,
               model: getSummarizeModel(session.mask.modelConfig.model),
             },

+ 15 - 5
app/store/sync.ts

@@ -97,13 +97,23 @@ 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;
       }
 
       await client.set(config.username, JSON.stringify(localState));

+ 5 - 0
app/styles/globals.scss

@@ -86,6 +86,7 @@
     @include dark;
   }
 }
+
 html {
   height: var(--full-height);
 
@@ -110,6 +111,10 @@ body {
   @media only screen and (max-width: 600px) {
     background-color: var(--second);
   }
+
+  *:focus-visible {
+    outline: none;
+  }
 }
 
 ::-webkit-scrollbar {

+ 8 - 0
app/typing.ts

@@ -1 +1,9 @@
 export type Updater<T> = (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;
+}

+ 16 - 10
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.writeTextFile(
-          result,
-          text
-        );
+        await window.__TAURI__.fs.writeTextFile(result, text);
         showToast(Locale.Download.Success);
       } catch (error) {
         showToast(Locale.Download.Failed);
@@ -293,10 +291,18 @@ 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");
 
-  return visionKeywords.some(keyword => model.includes(keyword));
+  return (
+    visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo
+  );
 }

+ 21 - 0
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"
+  );
+}

+ 18 - 11
app/utils/cloud/webdav.ts

@@ -18,8 +18,15 @@ export function createWebDavClient(store: SyncStore) {
           method: "MKCOL",
           headers: this.headers(),
         });
-        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);
       }
@@ -56,26 +63,26 @@ export function createWebDavClient(store: SyncStore) {
       };
     },
     path(path: string, proxyUrl: string = "") {
-      if (!path.endsWith("/")) {
-        path += "/";
-      }
       if (path.startsWith("/")) {
         path = path.slice(1);
       }
 
-      if (proxyUrl.length > 0 && !proxyUrl.endsWith("/")) {
-        proxyUrl += "/";
+      if (proxyUrl.endsWith("/")) {
+        proxyUrl = proxyUrl.slice(0, -1);
       }
 
       let url;
-      if (proxyUrl.length > 0 || proxyUrl === "/") {
-        let u = new URL(proxyUrl + "/api/webdav/" + path);
+      const pathPrefix = "/api/webdav/";
+
+      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;
     },
   };

+ 3 - 2
app/utils/hooks.ts

@@ -1,14 +1,15 @@
 import { useMemo } from "react";
 import { useAccessStore, useAppConfig } from "../store";
-import { collectModels } from "./model";
+import { collectModels, collectModelsWithDefaultModel } from "./model";
 
 export function useAllModels() {
   const accessStore = useAccessStore();
   const configStore = useAppConfig();
   const models = useMemo(() => {
-    return collectModels(
+    return collectModelsWithDefaultModel(
       configStore.models,
       [configStore.customModels, accessStore.customModels].join(","),
+      accessStore.defaultModel,
     );
   }, [accessStore.customModels, configStore.customModels, configStore.models]);
 

+ 43 - 2
app/utils/model.ts

@@ -1,5 +1,11 @@
 import { LLMModel } from "../client/api";
 
+const customProvider = (modelName: string) => ({
+  id: modelName,
+  providerName: "",
+  providerType: "custom",
+});
+
 export function collectModelTable(
   models: readonly LLMModel[],
   customModels: string,
@@ -11,6 +17,7 @@ export function collectModelTable(
       name: string;
       displayName: string;
       provider?: LLMModel["provider"]; // Marked as optional
+      isDefault?: boolean;
     }
   > = {};
 
@@ -34,16 +41,36 @@ 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
         };
       }
     });
+
+  return modelTable;
+}
+
+export function collectModelTableWithDefaultModel(
+  models: readonly LLMModel[],
+  customModels: string,
+  defaultModel: string,
+) {
+  let modelTable = collectModelTable(models, customModels);
+  if (defaultModel && defaultModel !== "") {
+    modelTable[defaultModel] = {
+      ...modelTable[defaultModel],
+      name: defaultModel,
+      available: true,
+      isDefault: true,
+    };
+  }
   return modelTable;
 }
 
@@ -59,3 +86,17 @@ export function collectModels(
 
   return allModels;
 }
+
+export function collectModelsWithDefaultModel(
+  models: readonly LLMModel[],
+  customModels: string,
+  defaultModel: string,
+) {
+  const modelTable = collectModelTableWithDefaultModel(
+    models,
+    customModels,
+    defaultModel,
+  );
+  const allModels = Object.values(modelTable);
+  return allModels;
+}

+ 17 - 0
app/utils/object.ts

@@ -0,0 +1,17 @@
+export function omit<T extends object, U extends (keyof T)[]>(
+  obj: T,
+  ...keys: U
+): Omit<T, U[number]> {
+  const ret: any = { ...obj };
+  keys.forEach((key) => delete ret[key]);
+  return ret;
+}
+
+export function pick<T extends object, U extends (keyof T)[]>(
+  obj: T,
+  ...keys: U
+): Pick<T, U[number]> {
+  const ret: any = {};
+  keys.forEach((key) => (ret[key] = obj[key]));
+  return ret;
+}

+ 4 - 0
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*",

+ 6 - 6
package.json

@@ -22,12 +22,12 @@
     "@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",
     "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",
@@ -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",

+ 2 - 2
src-tauri/tauri.conf.json

@@ -9,7 +9,7 @@
   },
   "package": {
     "productName": "NextChat",
-    "version": "2.11.3"
+    "version": "2.12.3"
   },
   "tauri": {
     "allowlist": {
@@ -112,4 +112,4 @@
       }
     ]
   }
-}
+}

+ 189 - 233
yarn.lock

@@ -1218,10 +1218,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"
@@ -1230,50 +1230,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-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-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-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-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-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-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-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-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-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@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@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@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@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@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@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@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@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"
@@ -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"
@@ -1431,78 +1424,78 @@
     "@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"
 
-"@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-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-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-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-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-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-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-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-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-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@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-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.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.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.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.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.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.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.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.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.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.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" "*"
@@ -2137,10 +2130,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==
 
 ccount@^2.0.0:
   version "2.0.1"
@@ -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"
@@ -3539,7 +3525,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==
@@ -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"
@@ -4779,10 +4753,10 @@ ms@^2.1.1:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
-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@^5.0.3:
   version "5.0.3"
@@ -4799,29 +4773,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"
@@ -4960,15 +4933,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"
@@ -5071,12 +5035,12 @@ pidtree@^0.6.0:
   resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c"
   integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==
 
-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"
 
@@ -5748,13 +5712,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 +5761,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 +5808,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"
@@ -6077,7 +6038,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==
@@ -6223,11 +6184,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"

Some files were not shown because too many files changed in this diff