mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 16:23:41 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/main' into feature/aarch64
This commit is contained in:
		
							
								
								
									
										4
									
								
								.github/workflows/deploy_preview.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/deploy_preview.yml
									
									
									
									
										vendored
									
									
								
							@@ -3,9 +3,7 @@ name: VercelPreviewDeployment
 | 
			
		||||
on:
 | 
			
		||||
  pull_request_target:
 | 
			
		||||
    types:
 | 
			
		||||
      - opened
 | 
			
		||||
      - synchronize
 | 
			
		||||
      - reopened
 | 
			
		||||
      - review_requested
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
name: Run Tests
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - main
 | 
			
		||||
    tags:
 | 
			
		||||
      - "!*"
 | 
			
		||||
  pull_request:
 | 
			
		||||
    types:
 | 
			
		||||
      - review_requested
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout repository
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - name: Set up Node.js
 | 
			
		||||
        uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 18
 | 
			
		||||
          cache: "yarn"
 | 
			
		||||
 | 
			
		||||
      - name: Cache node_modules
 | 
			
		||||
        uses: actions/cache@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: node_modules
 | 
			
		||||
          key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-node_modules-
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: yarn install
 | 
			
		||||
 | 
			
		||||
      - name: Run Jest tests
 | 
			
		||||
        run: yarn test:ci
 | 
			
		||||
@@ -7,21 +7,25 @@ import {
 | 
			
		||||
  LLMUsage,
 | 
			
		||||
  SpeechOptions,
 | 
			
		||||
} from "../api";
 | 
			
		||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
			
		||||
import {
 | 
			
		||||
  useAccessStore,
 | 
			
		||||
  useAppConfig,
 | 
			
		||||
  useChatStore,
 | 
			
		||||
  usePluginStore,
 | 
			
		||||
  ChatMessageTool,
 | 
			
		||||
} from "@/app/store";
 | 
			
		||||
import { stream } from "@/app/utils/chat";
 | 
			
		||||
import { getClientConfig } from "@/app/config/client";
 | 
			
		||||
import { GEMINI_BASE_URL } from "@/app/constant";
 | 
			
		||||
import Locale from "../../locales";
 | 
			
		||||
import {
 | 
			
		||||
  EventStreamContentType,
 | 
			
		||||
  fetchEventSource,
 | 
			
		||||
} from "@fortaine/fetch-event-source";
 | 
			
		||||
import { prettyObject } from "@/app/utils/format";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  getMessageTextContent,
 | 
			
		||||
  getMessageImages,
 | 
			
		||||
  isVisionModel,
 | 
			
		||||
} from "@/app/utils";
 | 
			
		||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
			
		||||
import { nanoid } from "nanoid";
 | 
			
		||||
import { RequestPayload } from "./openai";
 | 
			
		||||
import { fetch } from "@/app/utils/stream";
 | 
			
		||||
 | 
			
		||||
export class GeminiProApi implements LLMApi {
 | 
			
		||||
@@ -178,115 +182,81 @@ export class GeminiProApi implements LLMApi {
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (shouldStream) {
 | 
			
		||||
        let responseText = "";
 | 
			
		||||
        let remainText = "";
 | 
			
		||||
        let finished = false;
 | 
			
		||||
        const [tools, funcs] = usePluginStore
 | 
			
		||||
          .getState()
 | 
			
		||||
          .getAsTools(
 | 
			
		||||
            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
			
		||||
          );
 | 
			
		||||
        return stream(
 | 
			
		||||
          chatPath,
 | 
			
		||||
          requestPayload,
 | 
			
		||||
          getHeaders(),
 | 
			
		||||
          // @ts-ignore
 | 
			
		||||
          [{ functionDeclarations: tools.map((tool) => tool.function) }],
 | 
			
		||||
          funcs,
 | 
			
		||||
          controller,
 | 
			
		||||
          // parseSSE
 | 
			
		||||
          (text: string, runTools: ChatMessageTool[]) => {
 | 
			
		||||
            // console.log("parseSSE", text, runTools);
 | 
			
		||||
            const chunkJson = JSON.parse(text);
 | 
			
		||||
 | 
			
		||||
        const finish = () => {
 | 
			
		||||
          if (!finished) {
 | 
			
		||||
            finished = true;
 | 
			
		||||
            options.onFinish(responseText + remainText);
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // animate response to make it looks smooth
 | 
			
		||||
        function animateResponseText() {
 | 
			
		||||
          if (finished || controller.signal.aborted) {
 | 
			
		||||
            responseText += remainText;
 | 
			
		||||
            finish();
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (remainText.length > 0) {
 | 
			
		||||
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
			
		||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
			
		||||
            responseText += fetchText;
 | 
			
		||||
            remainText = remainText.slice(fetchCount);
 | 
			
		||||
            options.onUpdate?.(responseText, fetchText);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          requestAnimationFrame(animateResponseText);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // start animaion
 | 
			
		||||
        animateResponseText();
 | 
			
		||||
 | 
			
		||||
        controller.signal.onabort = finish;
 | 
			
		||||
 | 
			
		||||
        fetchEventSource(chatPath, {
 | 
			
		||||
          fetch: fetch as any,
 | 
			
		||||
          ...chatPayload,
 | 
			
		||||
          async onopen(res) {
 | 
			
		||||
            clearTimeout(requestTimeoutId);
 | 
			
		||||
            const contentType = res.headers.get("content-type");
 | 
			
		||||
            console.log(
 | 
			
		||||
              "[Gemini] request response content type: ",
 | 
			
		||||
              contentType,
 | 
			
		||||
            const functionCall = chunkJson?.candidates
 | 
			
		||||
              ?.at(0)
 | 
			
		||||
              ?.content.parts.at(0)?.functionCall;
 | 
			
		||||
            if (functionCall) {
 | 
			
		||||
              const { name, args } = functionCall;
 | 
			
		||||
              runTools.push({
 | 
			
		||||
                id: nanoid(),
 | 
			
		||||
                type: "function",
 | 
			
		||||
                function: {
 | 
			
		||||
                  name,
 | 
			
		||||
                  arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
 | 
			
		||||
                },
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
            return chunkJson?.candidates?.at(0)?.content.parts.at(0)?.text;
 | 
			
		||||
          },
 | 
			
		||||
          // processToolMessage, include tool_calls message and tool call results
 | 
			
		||||
          (
 | 
			
		||||
            requestPayload: RequestPayload,
 | 
			
		||||
            toolCallMessage: any,
 | 
			
		||||
            toolCallResult: any[],
 | 
			
		||||
          ) => {
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            requestPayload?.contents?.splice(
 | 
			
		||||
              // @ts-ignore
 | 
			
		||||
              requestPayload?.contents?.length,
 | 
			
		||||
              0,
 | 
			
		||||
              {
 | 
			
		||||
                role: "model",
 | 
			
		||||
                parts: toolCallMessage.tool_calls.map(
 | 
			
		||||
                  (tool: ChatMessageTool) => ({
 | 
			
		||||
                    functionCall: {
 | 
			
		||||
                      name: tool?.function?.name,
 | 
			
		||||
                      args: JSON.parse(tool?.function?.arguments as string),
 | 
			
		||||
                    },
 | 
			
		||||
                  }),
 | 
			
		||||
                ),
 | 
			
		||||
              },
 | 
			
		||||
              // @ts-ignore
 | 
			
		||||
              ...toolCallResult.map((result) => ({
 | 
			
		||||
                role: "function",
 | 
			
		||||
                parts: [
 | 
			
		||||
                  {
 | 
			
		||||
                    functionResponse: {
 | 
			
		||||
                      name: result.name,
 | 
			
		||||
                      response: {
 | 
			
		||||
                        name: result.name,
 | 
			
		||||
                        content: result.content, // TODO just text content...
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                ],
 | 
			
		||||
              })),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (contentType?.startsWith("text/plain")) {
 | 
			
		||||
              responseText = await res.clone().text();
 | 
			
		||||
              return finish();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
              !res.ok ||
 | 
			
		||||
              !res.headers
 | 
			
		||||
                .get("content-type")
 | 
			
		||||
                ?.startsWith(EventStreamContentType) ||
 | 
			
		||||
              res.status !== 200
 | 
			
		||||
            ) {
 | 
			
		||||
              const responseTexts = [responseText];
 | 
			
		||||
              let extraInfo = await res.clone().text();
 | 
			
		||||
              try {
 | 
			
		||||
                const resJson = await res.clone().json();
 | 
			
		||||
                extraInfo = prettyObject(resJson);
 | 
			
		||||
              } catch {}
 | 
			
		||||
 | 
			
		||||
              if (res.status === 401) {
 | 
			
		||||
                responseTexts.push(Locale.Error.Unauthorized);
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              if (extraInfo) {
 | 
			
		||||
                responseTexts.push(extraInfo);
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              responseText = responseTexts.join("\n\n");
 | 
			
		||||
 | 
			
		||||
              return finish();
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          onmessage(msg) {
 | 
			
		||||
            if (msg.data === "[DONE]" || finished) {
 | 
			
		||||
              return finish();
 | 
			
		||||
            }
 | 
			
		||||
            const text = msg.data;
 | 
			
		||||
            try {
 | 
			
		||||
              const json = JSON.parse(text);
 | 
			
		||||
              const delta = apiClient.extractMessage(json);
 | 
			
		||||
 | 
			
		||||
              if (delta) {
 | 
			
		||||
                remainText += delta;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              const blockReason = json?.promptFeedback?.blockReason;
 | 
			
		||||
              if (blockReason) {
 | 
			
		||||
                // being blocked
 | 
			
		||||
                console.log(`[Google] [Safety Ratings] result:`, blockReason);
 | 
			
		||||
              }
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
              console.error("[Request] parse error", text, msg);
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          onclose() {
 | 
			
		||||
            finish();
 | 
			
		||||
          },
 | 
			
		||||
          onerror(e) {
 | 
			
		||||
            options.onError?.(e);
 | 
			
		||||
            throw e;
 | 
			
		||||
          },
 | 
			
		||||
          openWhenHidden: true,
 | 
			
		||||
        });
 | 
			
		||||
          options,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        const res = await fetch(chatPath, chatPayload);
 | 
			
		||||
        clearTimeout(requestTimeoutId);
 | 
			
		||||
 
 | 
			
		||||
@@ -352,7 +352,7 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
        // make a fetch request
 | 
			
		||||
        const requestTimeoutId = setTimeout(
 | 
			
		||||
          () => controller.abort(),
 | 
			
		||||
          isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
 | 
			
		||||
          isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const res = await fetch(chatPath, chatPayload);
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import Logo from "../icons/logo.svg";
 | 
			
		||||
import { useMobileScreen } from "@/app/utils";
 | 
			
		||||
import BotIcon from "../icons/bot.svg";
 | 
			
		||||
import { getClientConfig } from "../config/client";
 | 
			
		||||
import { PasswordInput } from "./ui-lib";
 | 
			
		||||
import LeftIcon from "@/app/icons/left.svg";
 | 
			
		||||
import { safeLocalStorage } from "@/app/utils";
 | 
			
		||||
import {
 | 
			
		||||
@@ -60,36 +61,43 @@ export function AuthPage() {
 | 
			
		||||
      <div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
 | 
			
		||||
      <div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
 | 
			
		||||
 | 
			
		||||
      <input
 | 
			
		||||
        className={styles["auth-input"]}
 | 
			
		||||
        type="password"
 | 
			
		||||
        placeholder={Locale.Auth.Input}
 | 
			
		||||
      <PasswordInput
 | 
			
		||||
        style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
			
		||||
        aria={Locale.Settings.ShowPassword}
 | 
			
		||||
        aria-label={Locale.Auth.Input}
 | 
			
		||||
        value={accessStore.accessCode}
 | 
			
		||||
        type="text"
 | 
			
		||||
        placeholder={Locale.Auth.Input}
 | 
			
		||||
        onChange={(e) => {
 | 
			
		||||
          accessStore.update(
 | 
			
		||||
            (access) => (access.accessCode = e.currentTarget.value),
 | 
			
		||||
          );
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      {!accessStore.hideUserApiKey ? (
 | 
			
		||||
        <>
 | 
			
		||||
          <div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
 | 
			
		||||
          <input
 | 
			
		||||
            className={styles["auth-input"]}
 | 
			
		||||
            type="password"
 | 
			
		||||
            placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
 | 
			
		||||
          <PasswordInput
 | 
			
		||||
            style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
			
		||||
            aria={Locale.Settings.ShowPassword}
 | 
			
		||||
            aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
 | 
			
		||||
            value={accessStore.openaiApiKey}
 | 
			
		||||
            type="text"
 | 
			
		||||
            placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              accessStore.update(
 | 
			
		||||
                (access) => (access.openaiApiKey = e.currentTarget.value),
 | 
			
		||||
              );
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <input
 | 
			
		||||
            className={styles["auth-input-second"]}
 | 
			
		||||
            type="password"
 | 
			
		||||
            placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
 | 
			
		||||
          <PasswordInput
 | 
			
		||||
            style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
			
		||||
            aria={Locale.Settings.ShowPassword}
 | 
			
		||||
            aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
 | 
			
		||||
            value={accessStore.googleApiKey}
 | 
			
		||||
            type="text"
 | 
			
		||||
            placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              accessStore.update(
 | 
			
		||||
                (access) => (access.googleApiKey = e.currentTarget.value),
 | 
			
		||||
 
 | 
			
		||||
@@ -115,11 +115,14 @@ import { getClientConfig } from "../config/client";
 | 
			
		||||
import { useAllModels } from "../utils/hooks";
 | 
			
		||||
import { MultimodalContent } from "../client/api";
 | 
			
		||||
 | 
			
		||||
const localStorage = safeLocalStorage();
 | 
			
		||||
import { ClientApi } from "../client/api";
 | 
			
		||||
import { createTTSPlayer } from "../utils/audio";
 | 
			
		||||
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
 | 
			
		||||
 | 
			
		||||
import { isEmpty } from "lodash-es";
 | 
			
		||||
 | 
			
		||||
const localStorage = safeLocalStorage();
 | 
			
		||||
 | 
			
		||||
const ttsPlayer = createTTSPlayer();
 | 
			
		||||
 | 
			
		||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
 | 
			
		||||
@@ -1015,7 +1018,7 @@ function _Chat() {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const doSubmit = (userInput: string) => {
 | 
			
		||||
    if (userInput.trim() === "") return;
 | 
			
		||||
    if (userInput.trim() === "" && isEmpty(attachImages)) return;
 | 
			
		||||
    const matchCommand = chatCommands.match(userInput);
 | 
			
		||||
    if (matchCommand.matched) {
 | 
			
		||||
      setUserInput("");
 | 
			
		||||
 
 | 
			
		||||
@@ -140,6 +140,9 @@
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  &-narrow {
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sidebar-logo {
 | 
			
		||||
 
 | 
			
		||||
@@ -169,6 +169,12 @@ export function PreCode(props: { children: any }) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CustomCode(props: { children: any; className?: string }) {
 | 
			
		||||
  const chatStore = useChatStore();
 | 
			
		||||
  const session = chatStore.currentSession();
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
  const enableCodeFold =
 | 
			
		||||
    session.mask?.enableCodeFold !== false && config.enableCodeFold;
 | 
			
		||||
 | 
			
		||||
  const ref = useRef<HTMLPreElement>(null);
 | 
			
		||||
  const [collapsed, setCollapsed] = useState(true);
 | 
			
		||||
  const [showToggle, setShowToggle] = useState(false);
 | 
			
		||||
@@ -184,25 +190,30 @@ function CustomCode(props: { children: any; className?: string }) {
 | 
			
		||||
  const toggleCollapsed = () => {
 | 
			
		||||
    setCollapsed((collapsed) => !collapsed);
 | 
			
		||||
  };
 | 
			
		||||
  const renderShowMoreButton = () => {
 | 
			
		||||
    if (showToggle && enableCodeFold && collapsed) {
 | 
			
		||||
      return (
 | 
			
		||||
        <div className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}>
 | 
			
		||||
          <button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <code
 | 
			
		||||
        className={props?.className}
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        style={{
 | 
			
		||||
          maxHeight: collapsed ? "400px" : "none",
 | 
			
		||||
          maxHeight: enableCodeFold && collapsed ? "400px" : "none",
 | 
			
		||||
          overflowY: "hidden",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {props.children}
 | 
			
		||||
      </code>
 | 
			
		||||
      {showToggle && collapsed && (
 | 
			
		||||
        <div
 | 
			
		||||
          className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}
 | 
			
		||||
        >
 | 
			
		||||
          <button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {renderShowMoreButton()}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -183,6 +183,23 @@ export function MaskConfig(props: {
 | 
			
		||||
            ></input>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
        )}
 | 
			
		||||
        {globalConfig.enableCodeFold && (
 | 
			
		||||
          <ListItem
 | 
			
		||||
            title={Locale.Mask.Config.CodeFold.Title}
 | 
			
		||||
            subTitle={Locale.Mask.Config.CodeFold.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
            <input
 | 
			
		||||
              aria-label={Locale.Mask.Config.CodeFold.Title}
 | 
			
		||||
              type="checkbox"
 | 
			
		||||
              checked={props.mask.enableCodeFold !== false}
 | 
			
		||||
              onChange={(e) => {
 | 
			
		||||
                props.updateMask((mask) => {
 | 
			
		||||
                  mask.enableCodeFold = e.currentTarget.checked;
 | 
			
		||||
                });
 | 
			
		||||
              }}
 | 
			
		||||
            ></input>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {!props.shouldSyncFromGlobal ? (
 | 
			
		||||
          <ListItem
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ import Locale, {
 | 
			
		||||
  changeLang,
 | 
			
		||||
  getLang,
 | 
			
		||||
} from "../locales";
 | 
			
		||||
import { copyToClipboard } from "../utils";
 | 
			
		||||
import { copyToClipboard, clientUpdate, semverCompare } from "../utils";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import {
 | 
			
		||||
  Anthropic,
 | 
			
		||||
@@ -585,7 +585,7 @@ export function Settings() {
 | 
			
		||||
  const [checkingUpdate, setCheckingUpdate] = useState(false);
 | 
			
		||||
  const currentVersion = updateStore.formatVersion(updateStore.version);
 | 
			
		||||
  const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
 | 
			
		||||
  const hasNewVersion = currentVersion !== remoteId;
 | 
			
		||||
  const hasNewVersion = semverCompare(currentVersion, remoteId) === -1;
 | 
			
		||||
  const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
 | 
			
		||||
 | 
			
		||||
  function checkUpdate(force = false) {
 | 
			
		||||
@@ -1357,9 +1357,17 @@ export function Settings() {
 | 
			
		||||
            {checkingUpdate ? (
 | 
			
		||||
              <LoadingIcon />
 | 
			
		||||
            ) : hasNewVersion ? (
 | 
			
		||||
              <Link href={updateUrl} target="_blank" className="link">
 | 
			
		||||
                {Locale.Settings.Update.GoToUpdate}
 | 
			
		||||
              </Link>
 | 
			
		||||
              clientConfig?.isApp ? (
 | 
			
		||||
                <IconButton
 | 
			
		||||
                  icon={<ResetIcon></ResetIcon>}
 | 
			
		||||
                  text={Locale.Settings.Update.GoToUpdate}
 | 
			
		||||
                  onClick={() => clientUpdate()}
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <Link href={updateUrl} target="_blank" className="link">
 | 
			
		||||
                  {Locale.Settings.Update.GoToUpdate}
 | 
			
		||||
                </Link>
 | 
			
		||||
              )
 | 
			
		||||
            ) : (
 | 
			
		||||
              <IconButton
 | 
			
		||||
                icon={<ResetIcon></ResetIcon>}
 | 
			
		||||
@@ -1509,6 +1517,22 @@ export function Settings() {
 | 
			
		||||
              }
 | 
			
		||||
            ></input>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
          <ListItem
 | 
			
		||||
            title={Locale.Mask.Config.CodeFold.Title}
 | 
			
		||||
            subTitle={Locale.Mask.Config.CodeFold.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
            <input
 | 
			
		||||
              aria-label={Locale.Mask.Config.CodeFold.Title}
 | 
			
		||||
              type="checkbox"
 | 
			
		||||
              checked={config.enableCodeFold}
 | 
			
		||||
              data-testid="enable-code-fold-checkbox"
 | 
			
		||||
              onChange={(e) =>
 | 
			
		||||
                updateConfig(
 | 
			
		||||
                  (config) => (config.enableCodeFold = e.currentTarget.checked),
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
            ></input>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
        </List>
 | 
			
		||||
 | 
			
		||||
        <SyncItems />
 | 
			
		||||
 
 | 
			
		||||
@@ -165,11 +165,17 @@ export function SideBarHeader(props: {
 | 
			
		||||
  subTitle?: string | React.ReactNode;
 | 
			
		||||
  logo?: React.ReactNode;
 | 
			
		||||
  children?: React.ReactNode;
 | 
			
		||||
  shouldNarrow?: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
  const { title, subTitle, logo, children } = props;
 | 
			
		||||
  const { title, subTitle, logo, children, shouldNarrow } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <Fragment>
 | 
			
		||||
      <div className={styles["sidebar-header"]} data-tauri-drag-region>
 | 
			
		||||
      <div
 | 
			
		||||
        className={`${styles["sidebar-header"]} ${
 | 
			
		||||
          shouldNarrow ? styles["sidebar-header-narrow"] : ""
 | 
			
		||||
        }`}
 | 
			
		||||
        data-tauri-drag-region
 | 
			
		||||
      >
 | 
			
		||||
        <div className={styles["sidebar-title-container"]}>
 | 
			
		||||
          <div className={styles["sidebar-title"]} data-tauri-drag-region>
 | 
			
		||||
            {title}
 | 
			
		||||
@@ -227,6 +233,7 @@ export function SideBar(props: { className?: string }) {
 | 
			
		||||
        title="NextChat"
 | 
			
		||||
        subTitle="Build your own AI assistant."
 | 
			
		||||
        logo={<ChatGptIcon />}
 | 
			
		||||
        shouldNarrow={shouldNarrow}
 | 
			
		||||
      >
 | 
			
		||||
        <div className={styles["sidebar-header-bar"]}>
 | 
			
		||||
          <IconButton
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								app/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								app/global.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -26,6 +26,13 @@ declare interface Window {
 | 
			
		||||
      isPermissionGranted(): Promise<boolean>;
 | 
			
		||||
      sendNotification(options: string | Options): void;
 | 
			
		||||
    };
 | 
			
		||||
    updater: {
 | 
			
		||||
      checkUpdate(): Promise<UpdateResult>;
 | 
			
		||||
      installUpdate(): Promise<void>;
 | 
			
		||||
      onUpdaterEvent(
 | 
			
		||||
        handler: (status: UpdateStatusResult) => void,
 | 
			
		||||
      ): Promise<UnlistenFn>;
 | 
			
		||||
    };
 | 
			
		||||
    http: {
 | 
			
		||||
      fetch<T>(
 | 
			
		||||
        url: string,
 | 
			
		||||
 
 | 
			
		||||
@@ -205,6 +205,8 @@ const cn = {
 | 
			
		||||
      IsChecking: "正在检查更新...",
 | 
			
		||||
      FoundUpdate: (x: string) => `发现新版本:${x}`,
 | 
			
		||||
      GoToUpdate: "前往更新",
 | 
			
		||||
      Success: "更新成功!",
 | 
			
		||||
      Failed: "更新失败",
 | 
			
		||||
    },
 | 
			
		||||
    SendKey: "发送键",
 | 
			
		||||
    Theme: "主题",
 | 
			
		||||
@@ -495,8 +497,8 @@ const cn = {
 | 
			
		||||
 | 
			
		||||
    Model: "模型 (model)",
 | 
			
		||||
    CompressModel: {
 | 
			
		||||
      Title: "压缩模型",
 | 
			
		||||
      SubTitle: "用于压缩历史记录的模型",
 | 
			
		||||
      Title: "对话摘要模型",
 | 
			
		||||
      SubTitle: "用于压缩历史记录、生成对话标题的模型",
 | 
			
		||||
    },
 | 
			
		||||
    Temperature: {
 | 
			
		||||
      Title: "随机性 (temperature)",
 | 
			
		||||
@@ -665,6 +667,10 @@ const cn = {
 | 
			
		||||
        Title: "启用Artifacts",
 | 
			
		||||
        SubTitle: "启用之后可以直接渲染HTML页面",
 | 
			
		||||
      },
 | 
			
		||||
      CodeFold: {
 | 
			
		||||
        Title: "启用代码折叠",
 | 
			
		||||
        SubTitle: "启用之后可以自动折叠/展开过长的代码块",
 | 
			
		||||
      },
 | 
			
		||||
      Share: {
 | 
			
		||||
        Title: "分享此面具",
 | 
			
		||||
        SubTitle: "生成此面具的直达链接",
 | 
			
		||||
 
 | 
			
		||||
@@ -207,6 +207,8 @@ const en: LocaleType = {
 | 
			
		||||
      IsChecking: "Checking update...",
 | 
			
		||||
      FoundUpdate: (x: string) => `Found new version: ${x}`,
 | 
			
		||||
      GoToUpdate: "Update",
 | 
			
		||||
      Success: "Update Successful.",
 | 
			
		||||
      Failed: "Update Failed.",
 | 
			
		||||
    },
 | 
			
		||||
    SendKey: "Send Key",
 | 
			
		||||
    Theme: "Theme",
 | 
			
		||||
@@ -500,8 +502,8 @@ const en: LocaleType = {
 | 
			
		||||
 | 
			
		||||
    Model: "Model",
 | 
			
		||||
    CompressModel: {
 | 
			
		||||
      Title: "Compression Model",
 | 
			
		||||
      SubTitle: "Model used to compress history",
 | 
			
		||||
      Title: "Summary Model",
 | 
			
		||||
      SubTitle: "Model used to compress history and generate title",
 | 
			
		||||
    },
 | 
			
		||||
    Temperature: {
 | 
			
		||||
      Title: "Temperature",
 | 
			
		||||
@@ -675,6 +677,11 @@ const en: LocaleType = {
 | 
			
		||||
        Title: "Enable Artifacts",
 | 
			
		||||
        SubTitle: "Can render HTML page when enable artifacts.",
 | 
			
		||||
      },
 | 
			
		||||
      CodeFold: {
 | 
			
		||||
        Title: "Enable CodeFold",
 | 
			
		||||
        SubTitle:
 | 
			
		||||
          "Automatically collapse/expand overly long code blocks when CodeFold is enabled",
 | 
			
		||||
      },
 | 
			
		||||
      Share: {
 | 
			
		||||
        Title: "Share This Mask",
 | 
			
		||||
        SubTitle: "Generate a link to this mask",
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,12 @@ const tw = {
 | 
			
		||||
  Error: {
 | 
			
		||||
    Unauthorized: isApp
 | 
			
		||||
      ? `😆 對話遇到了一些問題,不用慌:
 | 
			
		||||
    \\ 1️⃣ 想要零配置開箱即用,[點擊這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
 | 
			
		||||
    \\ 2️⃣ 如果你想消耗自己的 OpenAI 資源,點擊[這裡](/#/settings)修改設定 ⚙️`
 | 
			
		||||
    \\ 1️⃣ 想要無須設定開箱即用,[點選這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
 | 
			
		||||
    \\ 2️⃣ 如果你想消耗自己的 OpenAI 資源,點選[這裡](/#/settings)修改設定 ⚙️`
 | 
			
		||||
      : `😆 對話遇到了一些問題,不用慌:
 | 
			
		||||
    \ 1️⃣ 想要零配置開箱即用,[點擊這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
 | 
			
		||||
    \ 2️⃣ 如果你正在使用私有部署版本,點擊[這裡](/#/auth)輸入訪問秘鑰 🔑
 | 
			
		||||
    \ 3️⃣ 如果你想消耗自己的 OpenAI 資源,點擊[這裡](/#/settings)修改設定 ⚙️
 | 
			
		||||
    \ 1️⃣ 想要無須設定開箱即用,[點選這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
 | 
			
		||||
    \ 2️⃣ 如果你正在使用私有部署版本,點選[這裡](/#/auth)輸入存取金鑰 🔑
 | 
			
		||||
    \ 3️⃣ 如果你想消耗自己的 OpenAI 資源,點選[這裡](/#/settings)修改設定 ⚙️
 | 
			
		||||
 `,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@@ -25,9 +25,9 @@ const tw = {
 | 
			
		||||
    Confirm: "確認",
 | 
			
		||||
    Later: "稍候再說",
 | 
			
		||||
    Return: "返回",
 | 
			
		||||
    SaasTips: "配置太麻煩,想要立即使用",
 | 
			
		||||
    SaasTips: "設定太麻煩,想要立即使用",
 | 
			
		||||
    TopTips:
 | 
			
		||||
      "🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
 | 
			
		||||
      "🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新的大型語言模型",
 | 
			
		||||
  },
 | 
			
		||||
  ChatItem: {
 | 
			
		||||
    ChatItemCount: (count: number) => `${count} 則對話`,
 | 
			
		||||
@@ -53,8 +53,8 @@ const tw = {
 | 
			
		||||
      PinToastAction: "檢視",
 | 
			
		||||
      Delete: "刪除",
 | 
			
		||||
      Edit: "編輯",
 | 
			
		||||
      RefreshTitle: "刷新標題",
 | 
			
		||||
      RefreshToast: "已發送刷新標題請求",
 | 
			
		||||
      RefreshTitle: "重新整理標題",
 | 
			
		||||
      RefreshToast: "已傳送重新整理標題請求",
 | 
			
		||||
    },
 | 
			
		||||
    Commands: {
 | 
			
		||||
      new: "新建聊天",
 | 
			
		||||
@@ -95,10 +95,10 @@ const tw = {
 | 
			
		||||
    IsContext: "預設提示詞",
 | 
			
		||||
    ShortcutKey: {
 | 
			
		||||
      Title: "鍵盤快捷方式",
 | 
			
		||||
      newChat: "打開新聊天",
 | 
			
		||||
      newChat: "開啟新聊天",
 | 
			
		||||
      focusInput: "聚焦輸入框",
 | 
			
		||||
      copyLastMessage: "複製最後一個回覆",
 | 
			
		||||
      copyLastCode: "複製最後一個代碼塊",
 | 
			
		||||
      copyLastCode: "複製最後一個程式碼區塊",
 | 
			
		||||
      showShortcutKey: "顯示快捷方式",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
@@ -174,9 +174,9 @@ const tw = {
 | 
			
		||||
      SubTitle: "聊天內容的字型大小",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "聊天字體",
 | 
			
		||||
      SubTitle: "聊天內容的字體,若置空則應用全局默認字體",
 | 
			
		||||
      Placeholder: "字體名稱",
 | 
			
		||||
      Title: "聊天字型",
 | 
			
		||||
      SubTitle: "聊天內容的字型,若留空則套用全域預設字型",
 | 
			
		||||
      Placeholder: "字型名稱",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "匯入系統提示",
 | 
			
		||||
@@ -301,8 +301,8 @@ const tw = {
 | 
			
		||||
        Title: "使用 NextChat AI",
 | 
			
		||||
        Label: "(性價比最高的方案)",
 | 
			
		||||
        SubTitle:
 | 
			
		||||
          "由 NextChat 官方維護,零配置開箱即用,支持 OpenAI o1、GPT-4o、Claude-3.5 等最新大模型",
 | 
			
		||||
        ChatNow: "立刻對話",
 | 
			
		||||
          "由 NextChat 官方維護,無須設定開箱即用,支援 OpenAI o1、GPT-4o、Claude-3.5 等最新的大型語言模型",
 | 
			
		||||
        ChatNow: "立刻開始對話",
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      AccessCode: {
 | 
			
		||||
@@ -485,18 +485,18 @@ const tw = {
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  SearchChat: {
 | 
			
		||||
    Name: "搜索",
 | 
			
		||||
    Name: "搜尋",
 | 
			
		||||
    Page: {
 | 
			
		||||
      Title: "搜索聊天記錄",
 | 
			
		||||
      Search: "輸入搜索關鍵詞",
 | 
			
		||||
      Title: "搜尋聊天記錄",
 | 
			
		||||
      Search: "輸入搜尋關鍵詞",
 | 
			
		||||
      NoResult: "沒有找到結果",
 | 
			
		||||
      NoData: "沒有數據",
 | 
			
		||||
      Loading: "加載中",
 | 
			
		||||
      NoData: "沒有資料",
 | 
			
		||||
      Loading: "載入中",
 | 
			
		||||
 | 
			
		||||
      SubTitle: (count: number) => `找到 ${count} 條結果`,
 | 
			
		||||
    },
 | 
			
		||||
    Item: {
 | 
			
		||||
      View: "查看",
 | 
			
		||||
      View: "檢視",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  NewChat: {
 | 
			
		||||
 
 | 
			
		||||
@@ -372,22 +372,16 @@ export const useChatStore = createPersistStore(
 | 
			
		||||
 | 
			
		||||
        if (attachImages && attachImages.length > 0) {
 | 
			
		||||
          mContent = [
 | 
			
		||||
            {
 | 
			
		||||
              type: "text",
 | 
			
		||||
              text: userContent,
 | 
			
		||||
            },
 | 
			
		||||
            ...(userContent
 | 
			
		||||
              ? [{ type: "text" as const, text: userContent }]
 | 
			
		||||
              : []),
 | 
			
		||||
            ...attachImages.map((url) => ({
 | 
			
		||||
              type: "image_url" as const,
 | 
			
		||||
              image_url: { url },
 | 
			
		||||
            })),
 | 
			
		||||
          ];
 | 
			
		||||
          mContent = mContent.concat(
 | 
			
		||||
            attachImages.map((url) => {
 | 
			
		||||
              return {
 | 
			
		||||
                type: "image_url",
 | 
			
		||||
                image_url: {
 | 
			
		||||
                  url: url,
 | 
			
		||||
                },
 | 
			
		||||
              };
 | 
			
		||||
            }),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let userMessage: ChatMessage = createMessage({
 | 
			
		||||
          role: "user",
 | 
			
		||||
          content: mContent,
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,8 @@ export const DEFAULT_CONFIG = {
 | 
			
		||||
 | 
			
		||||
  enableArtifacts: true, // show artifacts config
 | 
			
		||||
 | 
			
		||||
  enableCodeFold: true, // code fold config
 | 
			
		||||
 | 
			
		||||
  disablePromptHint: false,
 | 
			
		||||
 | 
			
		||||
  dontShowMaskSplashScreen: false, // dont show splash screen when create chat
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ export type Mask = {
 | 
			
		||||
  builtin: boolean;
 | 
			
		||||
  plugin?: string[];
 | 
			
		||||
  enableArtifacts?: boolean;
 | 
			
		||||
  enableCodeFold?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_MASK_STATE = {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import {
 | 
			
		||||
} from "../constant";
 | 
			
		||||
import { getClientConfig } from "../config/client";
 | 
			
		||||
import { createPersistStore } from "../utils/store";
 | 
			
		||||
import { clientUpdate } from "../utils";
 | 
			
		||||
import ChatGptIcon from "../icons/chatgpt.png";
 | 
			
		||||
import Locale from "../locales";
 | 
			
		||||
import { ClientApi } from "../client/api";
 | 
			
		||||
@@ -119,6 +120,7 @@ export const useUpdateStore = createPersistStore(
 | 
			
		||||
                          icon: `${ChatGptIcon.src}`,
 | 
			
		||||
                          sound: "Default",
 | 
			
		||||
                        });
 | 
			
		||||
                        clientUpdate();
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  });
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								app/utils.ts
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								app/utils.ts
									
									
									
									
									
								
							@@ -285,6 +285,9 @@ export function showPlugins(provider: ServiceProvider, model: string) {
 | 
			
		||||
  if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (provider == ServiceProvider.Google && !model.includes("vision")) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -383,3 +386,37 @@ export function getOperationId(operation: {
 | 
			
		||||
    `${operation.method.toUpperCase()}${operation.path.replaceAll("/", "_")}`
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function clientUpdate() {
 | 
			
		||||
  // this a wild for updating client app
 | 
			
		||||
  return window.__TAURI__?.updater
 | 
			
		||||
    .checkUpdate()
 | 
			
		||||
    .then((updateResult) => {
 | 
			
		||||
      if (updateResult.shouldUpdate) {
 | 
			
		||||
        window.__TAURI__?.updater
 | 
			
		||||
          .installUpdate()
 | 
			
		||||
          .then((result) => {
 | 
			
		||||
            showToast(Locale.Settings.Update.Success);
 | 
			
		||||
          })
 | 
			
		||||
          .catch((e) => {
 | 
			
		||||
            console.error("[Install Update Error]", e);
 | 
			
		||||
            showToast(Locale.Settings.Update.Failed);
 | 
			
		||||
          });
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch((e) => {
 | 
			
		||||
      console.error("[Check Update Error]", e);
 | 
			
		||||
      showToast(Locale.Settings.Update.Failed);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb
 | 
			
		||||
export function semverCompare(a: string, b: string) {
 | 
			
		||||
  if (a.startsWith(b + "-")) return -1;
 | 
			
		||||
  if (b.startsWith(a + "-")) return 1;
 | 
			
		||||
  return a.localeCompare(b, undefined, {
 | 
			
		||||
    numeric: true,
 | 
			
		||||
    sensitivity: "case",
 | 
			
		||||
    caseFirst: "upper",
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -223,6 +223,11 @@ export function stream(
 | 
			
		||||
            )
 | 
			
		||||
              .then((res) => {
 | 
			
		||||
                let content = res.data || res?.statusText;
 | 
			
		||||
                // hotfix #5614
 | 
			
		||||
                content =
 | 
			
		||||
                  typeof content === "string"
 | 
			
		||||
                    ? content
 | 
			
		||||
                    : JSON.stringify(content);
 | 
			
		||||
                if (res.status >= 300) {
 | 
			
		||||
                  return Promise.reject(content);
 | 
			
		||||
                }
 | 
			
		||||
@@ -245,6 +250,7 @@ export function stream(
 | 
			
		||||
                return e.toString();
 | 
			
		||||
              })
 | 
			
		||||
              .then((content) => ({
 | 
			
		||||
                name: tool.function.name,
 | 
			
		||||
                role: "tool",
 | 
			
		||||
                content,
 | 
			
		||||
                tool_call_id: tool.id,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								jest.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								jest.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
import type { Config } from "jest";
 | 
			
		||||
import nextJest from "next/jest.js";
 | 
			
		||||
 | 
			
		||||
const createJestConfig = nextJest({
 | 
			
		||||
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
 | 
			
		||||
  dir: "./",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Add any custom config to be passed to Jest
 | 
			
		||||
const config: Config = {
 | 
			
		||||
  coverageProvider: "v8",
 | 
			
		||||
  testEnvironment: "jsdom",
 | 
			
		||||
  testMatch: ["**/*.test.js", "**/*.test.ts", "**/*.test.jsx", "**/*.test.tsx"],
 | 
			
		||||
  setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
 | 
			
		||||
  moduleNameMapper: {
 | 
			
		||||
    "^@/(.*)$": "<rootDir>/$1",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
 | 
			
		||||
export default createJestConfig(config);
 | 
			
		||||
							
								
								
									
										2
									
								
								jest.setup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								jest.setup.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
// Learn more: https://github.com/testing-library/jest-dom
 | 
			
		||||
import "@testing-library/jest-dom";
 | 
			
		||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							@@ -15,7 +15,9 @@
 | 
			
		||||
    "app:build": "yarn mask && yarn tauri build",
 | 
			
		||||
    "prompts": "node ./scripts/fetch-prompts.mjs",
 | 
			
		||||
    "prepare": "husky install",
 | 
			
		||||
    "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
 | 
			
		||||
    "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
 | 
			
		||||
    "test": "jest --watch",
 | 
			
		||||
    "test:ci": "jest --ci"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@fortaine/fetch-event-source": "^3.0.6",
 | 
			
		||||
@@ -54,6 +56,9 @@
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@tauri-apps/api": "^1.6.0",
 | 
			
		||||
    "@tauri-apps/cli": "1.5.11",
 | 
			
		||||
    "@testing-library/jest-dom": "^6.4.8",
 | 
			
		||||
    "@testing-library/react": "^16.0.0",
 | 
			
		||||
    "@types/jest": "^29.5.13",
 | 
			
		||||
    "@types/js-yaml": "4.0.9",
 | 
			
		||||
    "@types/lodash-es": "^4.17.12",
 | 
			
		||||
    "@types/node": "^20.11.30",
 | 
			
		||||
@@ -69,8 +74,11 @@
 | 
			
		||||
    "eslint-plugin-prettier": "^5.1.3",
 | 
			
		||||
    "eslint-plugin-unused-imports": "^3.2.0",
 | 
			
		||||
    "husky": "^8.0.0",
 | 
			
		||||
    "jest": "^29.7.0",
 | 
			
		||||
    "jest-environment-jsdom": "^29.7.0",
 | 
			
		||||
    "lint-staged": "^13.2.2",
 | 
			
		||||
    "prettier": "^3.0.2",
 | 
			
		||||
    "ts-node": "^10.9.2",
 | 
			
		||||
    "tsx": "^4.16.0",
 | 
			
		||||
    "typescript": "5.2.2",
 | 
			
		||||
    "watch": "^1.0.2",
 | 
			
		||||
 
 | 
			
		||||
@@ -99,7 +99,7 @@
 | 
			
		||||
      "endpoints": [
 | 
			
		||||
        "https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/releases/latest/download/latest.json"
 | 
			
		||||
      ],
 | 
			
		||||
      "dialog": false,
 | 
			
		||||
      "dialog": true,
 | 
			
		||||
      "windows": {
 | 
			
		||||
        "installMode": "passive"
 | 
			
		||||
      },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								test/sum-module.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								test/sum-module.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
function sum(a: number, b: number) {
 | 
			
		||||
  return a + b;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe("sum module", () => {
 | 
			
		||||
  test("adds 1 + 2 to equal 3", () => {
 | 
			
		||||
    expect(sum(1, 2)).toBe(3);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user