mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web
This commit is contained in:
		@@ -26,8 +26,11 @@ export async function requestOpenai(req: NextRequest) {
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
      Authorization: `Bearer ${apiKey}`,
 | 
			
		||||
      ...(process.env.OPENAI_ORG_ID && { "OpenAI-Organization": process.env.OPENAI_ORG_ID }),
 | 
			
		||||
      ...(process.env.OPENAI_ORG_ID && {
 | 
			
		||||
        "OpenAI-Organization": process.env.OPENAI_ORG_ID,
 | 
			
		||||
      }),
 | 
			
		||||
    },
 | 
			
		||||
    cache: "no-store",
 | 
			
		||||
    method: req.method,
 | 
			
		||||
    body: req.body,
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,10 @@ export function ChatItem(props: {
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <div className={styles["chat-item-delete"]} onClick={props.onDelete}>
 | 
			
		||||
          <div
 | 
			
		||||
            className={styles["chat-item-delete"]}
 | 
			
		||||
            onClickCapture={props.onDelete}
 | 
			
		||||
          >
 | 
			
		||||
            <DeleteIcon />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -77,14 +80,14 @@ export function ChatItem(props: {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ChatList(props: { narrow?: boolean }) {
 | 
			
		||||
  const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
 | 
			
		||||
    useChatStore((state) => [
 | 
			
		||||
  const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
 | 
			
		||||
    (state) => [
 | 
			
		||||
      state.sessions,
 | 
			
		||||
      state.currentSessionIndex,
 | 
			
		||||
      state.selectSession,
 | 
			
		||||
      state.removeSession,
 | 
			
		||||
      state.moveSession,
 | 
			
		||||
    ]);
 | 
			
		||||
    ],
 | 
			
		||||
  );
 | 
			
		||||
  const chatStore = useChatStore();
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { useDebouncedCallback } from "use-debounce";
 | 
			
		||||
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
 | 
			
		||||
import { useState, useRef, useEffect, useLayoutEffect } from "react";
 | 
			
		||||
 | 
			
		||||
import SendWhiteIcon from "../icons/send-white.svg";
 | 
			
		||||
import BrainIcon from "../icons/brain.svg";
 | 
			
		||||
@@ -64,12 +64,9 @@ import {
 | 
			
		||||
  useMaskStore,
 | 
			
		||||
} from "../store/mask";
 | 
			
		||||
 | 
			
		||||
const Markdown = dynamic(
 | 
			
		||||
  async () => memo((await import("./markdown")).Markdown),
 | 
			
		||||
  {
 | 
			
		||||
    loading: () => <LoadingIcon />,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
 | 
			
		||||
  loading: () => <LoadingIcon />,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function exportMessages(messages: Message[], topic: string) {
 | 
			
		||||
  const mdText =
 | 
			
		||||
@@ -391,7 +388,7 @@ export function Chat() {
 | 
			
		||||
  const onPromptSelect = (prompt: Prompt) => {
 | 
			
		||||
    setPromptHints([]);
 | 
			
		||||
    inputRef.current?.focus();
 | 
			
		||||
    setUserInput(prompt.content);
 | 
			
		||||
    setTimeout(() => setUserInput(prompt.content), 60);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // auto grow input
 | 
			
		||||
@@ -728,6 +725,7 @@ export function Chat() {
 | 
			
		||||
                    }}
 | 
			
		||||
                    fontSize={fontSize}
 | 
			
		||||
                    parentRef={scrollRef}
 | 
			
		||||
                    defaultShow={i >= messages.length - 10}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
                {!isUser && !message.preview && (
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import { useRef, useState, RefObject, useEffect } from "react";
 | 
			
		||||
import { copyToClipboard } from "../utils";
 | 
			
		||||
 | 
			
		||||
import LoadingIcon from "../icons/three-dots.svg";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
export function PreCode(props: { children: any }) {
 | 
			
		||||
  const ref = useRef<HTMLPreElement>(null);
 | 
			
		||||
@@ -29,78 +30,89 @@ export function PreCode(props: { children: any }) {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _MarkDownContent(props: { content: string }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <ReactMarkdown
 | 
			
		||||
      remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
 | 
			
		||||
      rehypePlugins={[
 | 
			
		||||
        RehypeKatex,
 | 
			
		||||
        [
 | 
			
		||||
          RehypeHighlight,
 | 
			
		||||
          {
 | 
			
		||||
            detect: false,
 | 
			
		||||
            ignoreMissing: true,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      ]}
 | 
			
		||||
      components={{
 | 
			
		||||
        pre: PreCode,
 | 
			
		||||
      }}
 | 
			
		||||
      linkTarget={"_blank"}
 | 
			
		||||
    >
 | 
			
		||||
      {props.content}
 | 
			
		||||
    </ReactMarkdown>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const MarkdownContent = React.memo(_MarkDownContent);
 | 
			
		||||
 | 
			
		||||
export function Markdown(
 | 
			
		||||
  props: {
 | 
			
		||||
    content: string;
 | 
			
		||||
    loading?: boolean;
 | 
			
		||||
    fontSize?: number;
 | 
			
		||||
    parentRef: RefObject<HTMLDivElement>;
 | 
			
		||||
    defaultShow?: boolean;
 | 
			
		||||
  } & React.DOMAttributes<HTMLDivElement>,
 | 
			
		||||
) {
 | 
			
		||||
  const mdRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const renderedHeight = useRef(0);
 | 
			
		||||
  const inView = useRef(!!props.defaultShow);
 | 
			
		||||
 | 
			
		||||
  const parent = props.parentRef.current;
 | 
			
		||||
  const md = mdRef.current;
 | 
			
		||||
  const rendered = useRef(true); // disable lazy loading for bad ux
 | 
			
		||||
  const [counter, setCounter] = useState(0);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // to triggr rerender
 | 
			
		||||
    setCounter(counter + 1);
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [props.loading]);
 | 
			
		||||
  const checkInView = () => {
 | 
			
		||||
    if (parent && md) {
 | 
			
		||||
      const parentBounds = parent.getBoundingClientRect();
 | 
			
		||||
      const twoScreenHeight = Math.max(500, parentBounds.height * 2);
 | 
			
		||||
      const mdBounds = md.getBoundingClientRect();
 | 
			
		||||
      const isInRange = (x: number) =>
 | 
			
		||||
        x <= parentBounds.bottom + twoScreenHeight &&
 | 
			
		||||
        x >= parentBounds.top - twoScreenHeight;
 | 
			
		||||
      inView.current = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  const inView =
 | 
			
		||||
    rendered.current ||
 | 
			
		||||
    (() => {
 | 
			
		||||
      if (parent && md) {
 | 
			
		||||
        const parentBounds = parent.getBoundingClientRect();
 | 
			
		||||
        const mdBounds = md.getBoundingClientRect();
 | 
			
		||||
        const isInRange = (x: number) =>
 | 
			
		||||
          x <= parentBounds.bottom && x >= parentBounds.top;
 | 
			
		||||
        const inView = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
 | 
			
		||||
    if (inView.current && md) {
 | 
			
		||||
      renderedHeight.current = Math.max(
 | 
			
		||||
        renderedHeight.current,
 | 
			
		||||
        md.getBoundingClientRect().height,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
        if (inView) {
 | 
			
		||||
          rendered.current = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return inView;
 | 
			
		||||
      }
 | 
			
		||||
    })();
 | 
			
		||||
 | 
			
		||||
  const shouldLoading = props.loading || !inView;
 | 
			
		||||
  checkInView();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className="markdown-body"
 | 
			
		||||
      style={{ fontSize: `${props.fontSize ?? 14}px` }}
 | 
			
		||||
      style={{
 | 
			
		||||
        fontSize: `${props.fontSize ?? 14}px`,
 | 
			
		||||
        height:
 | 
			
		||||
          !inView.current && renderedHeight.current > 0
 | 
			
		||||
            ? renderedHeight.current
 | 
			
		||||
            : "auto",
 | 
			
		||||
      }}
 | 
			
		||||
      ref={mdRef}
 | 
			
		||||
      onContextMenu={props.onContextMenu}
 | 
			
		||||
      onDoubleClickCapture={props.onDoubleClickCapture}
 | 
			
		||||
    >
 | 
			
		||||
      {shouldLoading ? (
 | 
			
		||||
        <LoadingIcon />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <ReactMarkdown
 | 
			
		||||
          remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
 | 
			
		||||
          rehypePlugins={[
 | 
			
		||||
            RehypeKatex,
 | 
			
		||||
            [
 | 
			
		||||
              RehypeHighlight,
 | 
			
		||||
              {
 | 
			
		||||
                detect: false,
 | 
			
		||||
                ignoreMissing: true,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          ]}
 | 
			
		||||
          components={{
 | 
			
		||||
            pre: PreCode,
 | 
			
		||||
          }}
 | 
			
		||||
          linkTarget={"_blank"}
 | 
			
		||||
        >
 | 
			
		||||
          {props.content}
 | 
			
		||||
        </ReactMarkdown>
 | 
			
		||||
      )}
 | 
			
		||||
      {inView.current &&
 | 
			
		||||
        (props.loading ? (
 | 
			
		||||
          <LoadingIcon />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <MarkdownContent content={props.content} />
 | 
			
		||||
        ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,4 @@
 | 
			
		||||
@import "../styles/animation.scss";
 | 
			
		||||
 | 
			
		||||
@keyframes search-in {
 | 
			
		||||
  from {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transform: translateY(5vh) scaleX(0.5);
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    transform: translateY(0) scaleX(1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mask-page {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
@@ -23,8 +11,9 @@
 | 
			
		||||
    .mask-filter {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      max-width: 100%;
 | 
			
		||||
      margin-bottom: 10px;
 | 
			
		||||
      animation: search-in ease 0.3s;
 | 
			
		||||
      margin-bottom: 20px;
 | 
			
		||||
      animation: slide-in ease 0.3s;
 | 
			
		||||
      height: 40px;
 | 
			
		||||
 | 
			
		||||
      display: flex;
 | 
			
		||||
 | 
			
		||||
@@ -32,8 +21,6 @@
 | 
			
		||||
        flex-grow: 1;
 | 
			
		||||
        max-width: 100%;
 | 
			
		||||
        min-width: 0;
 | 
			
		||||
        margin-bottom: 20px;
 | 
			
		||||
        animation: search-in ease 0.3s;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .mask-filter-lang {
 | 
			
		||||
@@ -45,10 +32,7 @@
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        margin-left: 10px;
 | 
			
		||||
        box-sizing: border-box;
 | 
			
		||||
 | 
			
		||||
        button {
 | 
			
		||||
          padding: 10px;
 | 
			
		||||
        }
 | 
			
		||||
        min-width: 80px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -291,14 +291,16 @@ export function MaskPage() {
 | 
			
		||||
              ))}
 | 
			
		||||
            </select>
 | 
			
		||||
 | 
			
		||||
            <div className={styles["mask-create"]}>
 | 
			
		||||
              <IconButton
 | 
			
		||||
                icon={<AddIcon />}
 | 
			
		||||
                text={Locale.Mask.Page.Create}
 | 
			
		||||
                bordered
 | 
			
		||||
                onClick={() => maskStore.create()}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <IconButton
 | 
			
		||||
              className={styles["mask-create"]}
 | 
			
		||||
              icon={<AddIcon />}
 | 
			
		||||
              text={Locale.Mask.Page.Create}
 | 
			
		||||
              bordered
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                const createdMask = maskStore.create();
 | 
			
		||||
                setEditingMaskId(createdMask.id);
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div>
 | 
			
		||||
 
 | 
			
		||||
@@ -59,10 +59,9 @@
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
 | 
			
		||||
    .search-bar {
 | 
			
		||||
    .more {
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      margin-right: 10px;
 | 
			
		||||
      width: 40vw;
 | 
			
		||||
      margin-left: 10px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,11 @@ import { EmojiAvatar } from "./emoji";
 | 
			
		||||
import styles from "./new-chat.module.scss";
 | 
			
		||||
 | 
			
		||||
import LeftIcon from "../icons/left.svg";
 | 
			
		||||
import AddIcon from "../icons/lightning.svg";
 | 
			
		||||
import LightningIcon from "../icons/lightning.svg";
 | 
			
		||||
import EyeIcon from "../icons/eye.svg";
 | 
			
		||||
 | 
			
		||||
import { useLocation, useNavigate } from "react-router-dom";
 | 
			
		||||
import { createEmptyMask, Mask, useMaskStore } from "../store/mask";
 | 
			
		||||
import { Mask, useMaskStore } from "../store/mask";
 | 
			
		||||
import Locale from "../locales";
 | 
			
		||||
import { useAppConfig, useChatStore } from "../store";
 | 
			
		||||
import { MaskAvatar } from "./mask";
 | 
			
		||||
@@ -148,20 +149,22 @@ export function NewChat() {
 | 
			
		||||
      <div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div>
 | 
			
		||||
 | 
			
		||||
      <div className={styles["actions"]}>
 | 
			
		||||
        <input
 | 
			
		||||
          className={styles["search-bar"]}
 | 
			
		||||
          placeholder={Locale.NewChat.More}
 | 
			
		||||
          type="text"
 | 
			
		||||
          onClick={() => navigate(Path.Masks)}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <IconButton
 | 
			
		||||
          text={Locale.NewChat.Skip}
 | 
			
		||||
          onClick={() => startChat()}
 | 
			
		||||
          icon={<AddIcon />}
 | 
			
		||||
          icon={<LightningIcon />}
 | 
			
		||||
          type="primary"
 | 
			
		||||
          shadow
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <IconButton
 | 
			
		||||
          className={styles["more"]}
 | 
			
		||||
          text={Locale.NewChat.More}
 | 
			
		||||
          onClick={() => navigate(Path.Masks)}
 | 
			
		||||
          icon={<EyeIcon />}
 | 
			
		||||
          bordered
 | 
			
		||||
          shadow
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className={styles["masks"]}>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,20 @@
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.edit-prompt-modal {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
 | 
			
		||||
  .edit-prompt-title {
 | 
			
		||||
    max-width: unset;
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
  }
 | 
			
		||||
  .edit-prompt-content {
 | 
			
		||||
    max-width: unset;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.user-prompt-modal {
 | 
			
		||||
  min-height: 40vh;
 | 
			
		||||
 | 
			
		||||
@@ -18,47 +32,42 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .user-prompt-list {
 | 
			
		||||
    padding: 10px 0;
 | 
			
		||||
    border: var(--border-in-light);
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
 | 
			
		||||
    .user-prompt-item {
 | 
			
		||||
      margin-bottom: 10px;
 | 
			
		||||
      widows: 100%;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      justify-content: space-between;
 | 
			
		||||
      padding: 10px;
 | 
			
		||||
 | 
			
		||||
      &:not(:last-child) {
 | 
			
		||||
        border-bottom: var(--border-in-light);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .user-prompt-header {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        widows: 100%;
 | 
			
		||||
        margin-bottom: 5px;
 | 
			
		||||
        max-width: calc(100% - 100px);
 | 
			
		||||
 | 
			
		||||
        .user-prompt-title {
 | 
			
		||||
          flex-grow: 1;
 | 
			
		||||
          max-width: 100%;
 | 
			
		||||
          margin-right: 5px;
 | 
			
		||||
          padding: 5px;
 | 
			
		||||
          font-size: 12px;
 | 
			
		||||
          text-align: left;
 | 
			
		||||
          font-size: 14px;
 | 
			
		||||
          line-height: 2;
 | 
			
		||||
          font-weight: bold;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .user-prompt-buttons {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
 | 
			
		||||
          .user-prompt-button {
 | 
			
		||||
            height: 100%;
 | 
			
		||||
 | 
			
		||||
            &:not(:last-child) {
 | 
			
		||||
              margin-right: 5px;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        .user-prompt-content {
 | 
			
		||||
          font-size: 12px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .user-prompt-content {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        box-sizing: border-box;
 | 
			
		||||
        padding: 5px;
 | 
			
		||||
        margin-right: 10px;
 | 
			
		||||
        font-size: 12px;
 | 
			
		||||
        flex-grow: 1;
 | 
			
		||||
      .user-prompt-buttons {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
 | 
			
		||||
        .user-prompt-button {
 | 
			
		||||
          height: 100%;
 | 
			
		||||
 | 
			
		||||
          &:not(:last-child) {
 | 
			
		||||
            margin-right: 5px;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,12 @@ import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
 | 
			
		||||
import styles from "./settings.module.scss";
 | 
			
		||||
 | 
			
		||||
import ResetIcon from "../icons/reload.svg";
 | 
			
		||||
import AddIcon from "../icons/add.svg";
 | 
			
		||||
import CloseIcon from "../icons/close.svg";
 | 
			
		||||
import CopyIcon from "../icons/copy.svg";
 | 
			
		||||
import ClearIcon from "../icons/clear.svg";
 | 
			
		||||
import EditIcon from "../icons/edit.svg";
 | 
			
		||||
import EyeIcon from "../icons/eye.svg";
 | 
			
		||||
import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib";
 | 
			
		||||
import { ModelConfigList } from "./model-config";
 | 
			
		||||
 | 
			
		||||
@@ -30,6 +32,55 @@ import { InputRange } from "./input-range";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { Avatar, AvatarPicker } from "./emoji";
 | 
			
		||||
 | 
			
		||||
function EditPromptModal(props: { id: number; onClose: () => void }) {
 | 
			
		||||
  const promptStore = usePromptStore();
 | 
			
		||||
  const prompt = promptStore.get(props.id);
 | 
			
		||||
 | 
			
		||||
  return prompt ? (
 | 
			
		||||
    <div className="modal-mask">
 | 
			
		||||
      <Modal
 | 
			
		||||
        title={Locale.Settings.Prompt.EditModal.Title}
 | 
			
		||||
        onClose={props.onClose}
 | 
			
		||||
        actions={[
 | 
			
		||||
          <IconButton
 | 
			
		||||
            key=""
 | 
			
		||||
            onClick={props.onClose}
 | 
			
		||||
            text={Locale.UI.Confirm}
 | 
			
		||||
            bordered
 | 
			
		||||
          />,
 | 
			
		||||
        ]}
 | 
			
		||||
      >
 | 
			
		||||
        <div className={styles["edit-prompt-modal"]}>
 | 
			
		||||
          <input
 | 
			
		||||
            type="text"
 | 
			
		||||
            value={prompt.title}
 | 
			
		||||
            readOnly={!prompt.isUser}
 | 
			
		||||
            className={styles["edit-prompt-title"]}
 | 
			
		||||
            onInput={(e) =>
 | 
			
		||||
              promptStore.update(
 | 
			
		||||
                props.id,
 | 
			
		||||
                (prompt) => (prompt.title = e.currentTarget.value),
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
          ></input>
 | 
			
		||||
          <Input
 | 
			
		||||
            value={prompt.content}
 | 
			
		||||
            readOnly={!prompt.isUser}
 | 
			
		||||
            className={styles["edit-prompt-content"]}
 | 
			
		||||
            rows={10}
 | 
			
		||||
            onInput={(e) =>
 | 
			
		||||
              promptStore.update(
 | 
			
		||||
                props.id,
 | 
			
		||||
                (prompt) => (prompt.content = e.currentTarget.value),
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
          ></Input>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Modal>
 | 
			
		||||
    </div>
 | 
			
		||||
  ) : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function UserPromptModal(props: { onClose?: () => void }) {
 | 
			
		||||
  const promptStore = usePromptStore();
 | 
			
		||||
  const userPrompts = promptStore.getUserPrompts();
 | 
			
		||||
@@ -39,6 +90,8 @@ function UserPromptModal(props: { onClose?: () => void }) {
 | 
			
		||||
  const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
 | 
			
		||||
  const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
 | 
			
		||||
 | 
			
		||||
  const [editingPromptId, setEditingPromptId] = useState<number>();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (searchInput.length > 0) {
 | 
			
		||||
      const searchResult = SearchService.search(searchInput);
 | 
			
		||||
@@ -56,8 +109,13 @@ function UserPromptModal(props: { onClose?: () => void }) {
 | 
			
		||||
        actions={[
 | 
			
		||||
          <IconButton
 | 
			
		||||
            key="add"
 | 
			
		||||
            onClick={() => promptStore.add({ title: "", content: "" })}
 | 
			
		||||
            icon={<ClearIcon />}
 | 
			
		||||
            onClick={() =>
 | 
			
		||||
              promptStore.add({
 | 
			
		||||
                title: "Empty Prompt",
 | 
			
		||||
                content: "Empty Prompt Content",
 | 
			
		||||
              })
 | 
			
		||||
            }
 | 
			
		||||
            icon={<AddIcon />}
 | 
			
		||||
            bordered
 | 
			
		||||
            text={Locale.Settings.Prompt.Modal.Add}
 | 
			
		||||
          />,
 | 
			
		||||
@@ -76,57 +134,51 @@ function UserPromptModal(props: { onClose?: () => void }) {
 | 
			
		||||
            {prompts.map((v, _) => (
 | 
			
		||||
              <div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
 | 
			
		||||
                <div className={styles["user-prompt-header"]}>
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    className={styles["user-prompt-title"]}
 | 
			
		||||
                    value={v.title}
 | 
			
		||||
                    readOnly={!v.isUser}
 | 
			
		||||
                    onChange={(e) => {
 | 
			
		||||
                      if (v.isUser) {
 | 
			
		||||
                        promptStore.updateUserPrompts(
 | 
			
		||||
                          v.id!,
 | 
			
		||||
                          (prompt) => (prompt.title = e.currentTarget.value),
 | 
			
		||||
                        );
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                  ></input>
 | 
			
		||||
 | 
			
		||||
                  <div className={styles["user-prompt-buttons"]}>
 | 
			
		||||
                    {v.isUser && (
 | 
			
		||||
                      <IconButton
 | 
			
		||||
                        icon={<ClearIcon />}
 | 
			
		||||
                        bordered
 | 
			
		||||
                        className={styles["user-prompt-button"]}
 | 
			
		||||
                        onClick={() => promptStore.remove(v.id!)}
 | 
			
		||||
                      />
 | 
			
		||||
                    )}
 | 
			
		||||
                    <IconButton
 | 
			
		||||
                      icon={<CopyIcon />}
 | 
			
		||||
                      bordered
 | 
			
		||||
                      className={styles["user-prompt-button"]}
 | 
			
		||||
                      onClick={() => copyToClipboard(v.content)}
 | 
			
		||||
                    />
 | 
			
		||||
                  <div className={styles["user-prompt-title"]}>{v.title}</div>
 | 
			
		||||
                  <div className={styles["user-prompt-content"] + " one-line"}>
 | 
			
		||||
                    {v.content}
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <Input
 | 
			
		||||
                  rows={2}
 | 
			
		||||
                  value={v.content}
 | 
			
		||||
                  className={styles["user-prompt-content"]}
 | 
			
		||||
                  readOnly={!v.isUser}
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    if (v.isUser) {
 | 
			
		||||
                      promptStore.updateUserPrompts(
 | 
			
		||||
                        v.id!,
 | 
			
		||||
                        (prompt) => (prompt.content = e.currentTarget.value),
 | 
			
		||||
                      );
 | 
			
		||||
                    }
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <div className={styles["user-prompt-buttons"]}>
 | 
			
		||||
                  {v.isUser && (
 | 
			
		||||
                    <IconButton
 | 
			
		||||
                      icon={<ClearIcon />}
 | 
			
		||||
                      className={styles["user-prompt-button"]}
 | 
			
		||||
                      onClick={() => promptStore.remove(v.id!)}
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                  {v.isUser ? (
 | 
			
		||||
                    <IconButton
 | 
			
		||||
                      icon={<EditIcon />}
 | 
			
		||||
                      className={styles["user-prompt-button"]}
 | 
			
		||||
                      onClick={() => setEditingPromptId(v.id)}
 | 
			
		||||
                    />
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <IconButton
 | 
			
		||||
                      icon={<EyeIcon />}
 | 
			
		||||
                      className={styles["user-prompt-button"]}
 | 
			
		||||
                      onClick={() => setEditingPromptId(v.id)}
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                  <IconButton
 | 
			
		||||
                    icon={<CopyIcon />}
 | 
			
		||||
                    className={styles["user-prompt-button"]}
 | 
			
		||||
                    onClick={() => copyToClipboard(v.content)}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Modal>
 | 
			
		||||
 | 
			
		||||
      {editingPromptId !== undefined && (
 | 
			
		||||
        <EditPromptModal
 | 
			
		||||
          id={editingPromptId!}
 | 
			
		||||
          onClose={() => setEditingPromptId(undefined)}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -138,7 +138,11 @@ export function SideBar(props: { className?: string }) {
 | 
			
		||||
          <div className={styles["sidebar-action"] + " " + styles.mobile}>
 | 
			
		||||
            <IconButton
 | 
			
		||||
              icon={<CloseIcon />}
 | 
			
		||||
              onClick={chatStore.deleteSession}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                if (confirm(Locale.Home.DeleteChat)) {
 | 
			
		||||
                  chatStore.deleteSession(chatStore.currentSessionIndex);
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className={styles["sidebar-action"]}>
 | 
			
		||||
 
 | 
			
		||||
@@ -158,6 +158,7 @@ export type ToastProps = {
 | 
			
		||||
    text: string;
 | 
			
		||||
    onClick: () => void;
 | 
			
		||||
  };
 | 
			
		||||
  onClose?: () => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function Toast(props: ToastProps) {
 | 
			
		||||
@@ -167,7 +168,10 @@ export function Toast(props: ToastProps) {
 | 
			
		||||
        <span>{props.content}</span>
 | 
			
		||||
        {props.action && (
 | 
			
		||||
          <button
 | 
			
		||||
            onClick={props.action.onClick}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              props.action?.onClick?.();
 | 
			
		||||
              props.onClose?.();
 | 
			
		||||
            }}
 | 
			
		||||
            className={styles["toast-action"]}
 | 
			
		||||
          >
 | 
			
		||||
            {props.action.text}
 | 
			
		||||
@@ -201,7 +205,7 @@ export function showToast(
 | 
			
		||||
    close();
 | 
			
		||||
  }, delay);
 | 
			
		||||
 | 
			
		||||
  root.render(<Toast content={content} action={action} />);
 | 
			
		||||
  root.render(<Toast content={content} action={action} onClose={close} />);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
 | 
			
		||||
 
 | 
			
		||||
@@ -116,9 +116,12 @@ const cn = {
 | 
			
		||||
      Edit: "编辑",
 | 
			
		||||
      Modal: {
 | 
			
		||||
        Title: "提示词列表",
 | 
			
		||||
        Add: "增加一条",
 | 
			
		||||
        Add: "新建",
 | 
			
		||||
        Search: "搜索提示词",
 | 
			
		||||
      },
 | 
			
		||||
      EditModal: {
 | 
			
		||||
        Title: "编辑提示词",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    HistoryCount: {
 | 
			
		||||
      Title: "附带历史消息数",
 | 
			
		||||
@@ -221,7 +224,15 @@ const cn = {
 | 
			
		||||
    ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。",
 | 
			
		||||
    Title: "挑选一个面具",
 | 
			
		||||
    SubTitle: "现在开始,与面具背后的灵魂思维碰撞",
 | 
			
		||||
    More: "搜索更多",
 | 
			
		||||
    More: "查看全部",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  UI: {
 | 
			
		||||
    Confirm: "确认",
 | 
			
		||||
    Cancel: "取消",
 | 
			
		||||
    Close: "关闭",
 | 
			
		||||
    Create: "新建",
 | 
			
		||||
    Edit: "编辑",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -121,6 +121,9 @@ const de: LocaleType = {
 | 
			
		||||
        Add: "Add One",
 | 
			
		||||
        Search: "Search Prompts",
 | 
			
		||||
      },
 | 
			
		||||
      EditModal: {
 | 
			
		||||
        Title: "Edit Prompt",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    HistoryCount: {
 | 
			
		||||
      Title: "Anzahl der angehängten Nachrichten",
 | 
			
		||||
@@ -230,6 +233,14 @@ const de: LocaleType = {
 | 
			
		||||
    NotShow: "Not Show Again",
 | 
			
		||||
    ConfirmNoShow: "Confirm to disable?You can enable it in settings later.",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  UI: {
 | 
			
		||||
    Confirm: "Confirm",
 | 
			
		||||
    Cancel: "Cancel",
 | 
			
		||||
    Close: "Close",
 | 
			
		||||
    Create: "Create",
 | 
			
		||||
    Edit: "Edit",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default de;
 | 
			
		||||
 
 | 
			
		||||
@@ -120,6 +120,9 @@ const en: LocaleType = {
 | 
			
		||||
        Add: "Add One",
 | 
			
		||||
        Search: "Search Prompts",
 | 
			
		||||
      },
 | 
			
		||||
      EditModal: {
 | 
			
		||||
        Title: "Edit Prompt",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    HistoryCount: {
 | 
			
		||||
      Title: "Attached Messages Count",
 | 
			
		||||
@@ -226,6 +229,14 @@ const en: LocaleType = {
 | 
			
		||||
    NotShow: "Not Show Again",
 | 
			
		||||
    ConfirmNoShow: "Confirm to disable?You can enable it in settings later.",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  UI: {
 | 
			
		||||
    Confirm: "Confirm",
 | 
			
		||||
    Cancel: "Cancel",
 | 
			
		||||
    Close: "Close",
 | 
			
		||||
    Create: "Create",
 | 
			
		||||
    Edit: "Edit",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default en;
 | 
			
		||||
 
 | 
			
		||||
@@ -120,6 +120,9 @@ const es: LocaleType = {
 | 
			
		||||
        Add: "Add One",
 | 
			
		||||
        Search: "Search Prompts",
 | 
			
		||||
      },
 | 
			
		||||
      EditModal: {
 | 
			
		||||
        Title: "Edit Prompt",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    HistoryCount: {
 | 
			
		||||
      Title: "Cantidad de mensajes adjuntos",
 | 
			
		||||
@@ -227,6 +230,14 @@ const es: LocaleType = {
 | 
			
		||||
    NotShow: "Not Show Again",
 | 
			
		||||
    ConfirmNoShow: "Confirm to disable?You can enable it in settings later.",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  UI: {
 | 
			
		||||
    Confirm: "Confirm",
 | 
			
		||||
    Cancel: "Cancel",
 | 
			
		||||
    Close: "Close",
 | 
			
		||||
    Create: "Create",
 | 
			
		||||
    Edit: "Edit",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default es;
 | 
			
		||||
 
 | 
			
		||||
@@ -120,6 +120,9 @@ const it: LocaleType = {
 | 
			
		||||
        Add: "Add One",
 | 
			
		||||
        Search: "Search Prompts",
 | 
			
		||||
      },
 | 
			
		||||
      EditModal: {
 | 
			
		||||
        Title: "Edit Prompt",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    HistoryCount: {
 | 
			
		||||
      Title: "Conteggio dei messaggi allegati",
 | 
			
		||||
@@ -228,6 +231,14 @@ const it: LocaleType = {
 | 
			
		||||
    NotShow: "Not Show Again",
 | 
			
		||||
    ConfirmNoShow: "Confirm to disable?You can enable it in settings later.",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  UI: {
 | 
			
		||||
    Confirm: "Confirm",
 | 
			
		||||
    Cancel: "Cancel",
 | 
			
		||||
    Close: "Close",
 | 
			
		||||
    Create: "Create",
 | 
			
		||||
    Edit: "Edit",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default it;
 | 
			
		||||
 
 | 
			
		||||
@@ -122,6 +122,9 @@ const jp: LocaleType = {
 | 
			
		||||
        Add: "新規追加",
 | 
			
		||||
        Search: "プロンプトワード検索",
 | 
			
		||||
      },
 | 
			
		||||
      EditModal: {
 | 
			
		||||
        Title: "编辑提示词",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    HistoryCount: {
 | 
			
		||||
      Title: "履歴メッセージ数を添付",
 | 
			
		||||
@@ -226,6 +229,14 @@ const jp: LocaleType = {
 | 
			
		||||
    NotShow: "不再展示",
 | 
			
		||||
    ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  UI: {
 | 
			
		||||
    Confirm: "确认",
 | 
			
		||||
    Cancel: "取消",
 | 
			
		||||
    Close: "关闭",
 | 
			
		||||
    Create: "新建",
 | 
			
		||||
    Edit: "编辑",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default jp;
 | 
			
		||||
 
 | 
			
		||||
@@ -120,6 +120,9 @@ const tr: LocaleType = {
 | 
			
		||||
        Add: "Add One",
 | 
			
		||||
        Search: "Search Prompts",
 | 
			
		||||
      },
 | 
			
		||||
      EditModal: {
 | 
			
		||||
        Title: "Edit Prompt",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    HistoryCount: {
 | 
			
		||||
      Title: "Ekli Mesaj Sayısı",
 | 
			
		||||
@@ -228,6 +231,14 @@ const tr: LocaleType = {
 | 
			
		||||
    NotShow: "Not Show Again",
 | 
			
		||||
    ConfirmNoShow: "Confirm to disable?You can enable it in settings later.",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  UI: {
 | 
			
		||||
    Confirm: "Confirm",
 | 
			
		||||
    Cancel: "Cancel",
 | 
			
		||||
    Close: "Close",
 | 
			
		||||
    Create: "Create",
 | 
			
		||||
    Edit: "Edit",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default tr;
 | 
			
		||||
 
 | 
			
		||||
@@ -118,6 +118,9 @@ const tw: LocaleType = {
 | 
			
		||||
        Add: "新增一條",
 | 
			
		||||
        Search: "搜尋提示詞",
 | 
			
		||||
      },
 | 
			
		||||
      EditModal: {
 | 
			
		||||
        Title: "编辑提示词",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    HistoryCount: {
 | 
			
		||||
      Title: "附帶歷史訊息數",
 | 
			
		||||
@@ -219,6 +222,13 @@ const tw: LocaleType = {
 | 
			
		||||
    NotShow: "不再展示",
 | 
			
		||||
    ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。",
 | 
			
		||||
  },
 | 
			
		||||
  UI: {
 | 
			
		||||
    Confirm: "确认",
 | 
			
		||||
    Cancel: "取消",
 | 
			
		||||
    Close: "关闭",
 | 
			
		||||
    Create: "新建",
 | 
			
		||||
    Edit: "编辑",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default tw;
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,8 @@ const TIME_OUT_MS = 60000;
 | 
			
		||||
const makeRequestParam = (
 | 
			
		||||
  messages: Message[],
 | 
			
		||||
  options?: {
 | 
			
		||||
    filterBot?: boolean;
 | 
			
		||||
    stream?: boolean;
 | 
			
		||||
    model?: ModelType;
 | 
			
		||||
    overrideModel?: ModelType;
 | 
			
		||||
  },
 | 
			
		||||
): ChatRequest => {
 | 
			
		||||
  let sendMessages = messages.map((v) => ({
 | 
			
		||||
@@ -24,18 +23,14 @@ const makeRequestParam = (
 | 
			
		||||
    content: v.content,
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  if (options?.filterBot) {
 | 
			
		||||
    sendMessages = sendMessages.filter((m) => m.role !== "assistant");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const modelConfig = {
 | 
			
		||||
    ...useAppConfig.getState().modelConfig,
 | 
			
		||||
    ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // override model config
 | 
			
		||||
  if (options?.model) {
 | 
			
		||||
    modelConfig.model = options.model;
 | 
			
		||||
  if (options?.overrideModel) {
 | 
			
		||||
    modelConfig.model = options.overrideModel;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
@@ -82,8 +77,7 @@ export async function requestChat(
 | 
			
		||||
  },
 | 
			
		||||
) {
 | 
			
		||||
  const req: ChatRequest = makeRequestParam(messages, {
 | 
			
		||||
    filterBot: true,
 | 
			
		||||
    model: options?.model,
 | 
			
		||||
    overrideModel: options?.model,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const res = await requestOpenaiClient("v1/chat/completions")(req);
 | 
			
		||||
@@ -102,11 +96,11 @@ export async function requestUsage() {
 | 
			
		||||
      .getDate()
 | 
			
		||||
      .toString()
 | 
			
		||||
      .padStart(2, "0")}`;
 | 
			
		||||
  const ONE_DAY = 2 * 24 * 60 * 60 * 1000;
 | 
			
		||||
  const now = new Date(Date.now() + ONE_DAY);
 | 
			
		||||
  const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
 | 
			
		||||
  const now = new Date();
 | 
			
		||||
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
 | 
			
		||||
  const startDate = formatDate(startOfMonth);
 | 
			
		||||
  const endDate = formatDate(now);
 | 
			
		||||
  const endDate = formatDate(new Date(Date.now() + ONE_DAY));
 | 
			
		||||
 | 
			
		||||
  const [used, subs] = await Promise.all([
 | 
			
		||||
    requestOpenaiClient(
 | 
			
		||||
@@ -149,9 +143,8 @@ export async function requestUsage() {
 | 
			
		||||
export async function requestChatStream(
 | 
			
		||||
  messages: Message[],
 | 
			
		||||
  options?: {
 | 
			
		||||
    filterBot?: boolean;
 | 
			
		||||
    modelConfig?: ModelConfig;
 | 
			
		||||
    model?: ModelType;
 | 
			
		||||
    overrideModel?: ModelType;
 | 
			
		||||
    onMessage: (message: string, done: boolean) => void;
 | 
			
		||||
    onError: (error: Error, statusCode?: number) => void;
 | 
			
		||||
    onController?: (controller: AbortController) => void;
 | 
			
		||||
@@ -159,8 +152,7 @@ export async function requestChatStream(
 | 
			
		||||
) {
 | 
			
		||||
  const req = makeRequestParam(messages, {
 | 
			
		||||
    stream: true,
 | 
			
		||||
    filterBot: options?.filterBot,
 | 
			
		||||
    model: options?.model,
 | 
			
		||||
    overrideModel: options?.overrideModel,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  console.log("[Request] ", req);
 | 
			
		||||
 
 | 
			
		||||
@@ -83,11 +83,10 @@ interface ChatStore {
 | 
			
		||||
  currentSessionIndex: number;
 | 
			
		||||
  globalId: number;
 | 
			
		||||
  clearSessions: () => void;
 | 
			
		||||
  removeSession: (index: number) => void;
 | 
			
		||||
  moveSession: (from: number, to: number) => void;
 | 
			
		||||
  selectSession: (index: number) => void;
 | 
			
		||||
  newSession: (mask?: Mask) => void;
 | 
			
		||||
  deleteSession: (index?: number) => void;
 | 
			
		||||
  deleteSession: (index: number) => void;
 | 
			
		||||
  currentSession: () => ChatSession;
 | 
			
		||||
  onNewMessage: (message: Message) => void;
 | 
			
		||||
  onUserInput: (content: string) => Promise<void>;
 | 
			
		||||
@@ -130,31 +129,6 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      removeSession(index: number) {
 | 
			
		||||
        set((state) => {
 | 
			
		||||
          let nextIndex = state.currentSessionIndex;
 | 
			
		||||
          const sessions = state.sessions;
 | 
			
		||||
 | 
			
		||||
          if (sessions.length === 1) {
 | 
			
		||||
            return {
 | 
			
		||||
              currentSessionIndex: 0,
 | 
			
		||||
              sessions: [createEmptySession()],
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          sessions.splice(index, 1);
 | 
			
		||||
 | 
			
		||||
          if (nextIndex === index) {
 | 
			
		||||
            nextIndex -= 1;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            currentSessionIndex: nextIndex,
 | 
			
		||||
            sessions,
 | 
			
		||||
          };
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      moveSession(from: number, to: number) {
 | 
			
		||||
        set((state) => {
 | 
			
		||||
          const { sessions, currentSessionIndex: oldIndex } = state;
 | 
			
		||||
@@ -197,31 +171,46 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
        }));
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      deleteSession(i?: number) {
 | 
			
		||||
        const deletedSession = get().currentSession();
 | 
			
		||||
        const index = i ?? get().currentSessionIndex;
 | 
			
		||||
        const isLastSession = get().sessions.length === 1;
 | 
			
		||||
        if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
 | 
			
		||||
          get().removeSession(index);
 | 
			
		||||
      deleteSession(index) {
 | 
			
		||||
        const deletingLastSession = get().sessions.length === 1;
 | 
			
		||||
        const deletedSession = get().sessions.at(index);
 | 
			
		||||
 | 
			
		||||
          showToast(
 | 
			
		||||
            Locale.Home.DeleteToast,
 | 
			
		||||
            {
 | 
			
		||||
              text: Locale.Home.Revert,
 | 
			
		||||
              onClick() {
 | 
			
		||||
                set((state) => ({
 | 
			
		||||
                  sessions: state.sessions
 | 
			
		||||
                    .slice(0, index)
 | 
			
		||||
                    .concat([deletedSession])
 | 
			
		||||
                    .concat(
 | 
			
		||||
                      state.sessions.slice(index + Number(isLastSession)),
 | 
			
		||||
                    ),
 | 
			
		||||
                }));
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
            5000,
 | 
			
		||||
          );
 | 
			
		||||
        if (!deletedSession) return;
 | 
			
		||||
 | 
			
		||||
        const sessions = get().sessions.slice();
 | 
			
		||||
        sessions.splice(index, 1);
 | 
			
		||||
 | 
			
		||||
        let nextIndex = Math.min(
 | 
			
		||||
          get().currentSessionIndex,
 | 
			
		||||
          sessions.length - 1,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (deletingLastSession) {
 | 
			
		||||
          nextIndex = 0;
 | 
			
		||||
          sessions.push(createEmptySession());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // for undo delete action
 | 
			
		||||
        const restoreState = {
 | 
			
		||||
          currentSessionIndex: get().currentSessionIndex,
 | 
			
		||||
          sessions: get().sessions.slice(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        set(() => ({
 | 
			
		||||
          currentSessionIndex: nextIndex,
 | 
			
		||||
          sessions,
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        showToast(
 | 
			
		||||
          Locale.Home.DeleteToast,
 | 
			
		||||
          {
 | 
			
		||||
            text: Locale.Home.Revert,
 | 
			
		||||
            onClick() {
 | 
			
		||||
              set(() => restoreState);
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          5000,
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      currentSession() {
 | 
			
		||||
@@ -247,6 +236,9 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      async onUserInput(content) {
 | 
			
		||||
        const session = get().currentSession();
 | 
			
		||||
        const modelConfig = session.mask.modelConfig;
 | 
			
		||||
 | 
			
		||||
        const userMessage: Message = createMessage({
 | 
			
		||||
          role: "user",
 | 
			
		||||
          content,
 | 
			
		||||
@@ -256,7 +248,7 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
          role: "assistant",
 | 
			
		||||
          streaming: true,
 | 
			
		||||
          id: userMessage.id! + 1,
 | 
			
		||||
          model: useAppConfig.getState().modelConfig.model,
 | 
			
		||||
          model: modelConfig.model,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // get recent messages
 | 
			
		||||
@@ -290,14 +282,16 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          onError(error, statusCode) {
 | 
			
		||||
            const isAborted = error.message.includes("aborted");
 | 
			
		||||
            if (statusCode === 401) {
 | 
			
		||||
              botMessage.content = Locale.Error.Unauthorized;
 | 
			
		||||
            } else if (!error.message.includes("aborted")) {
 | 
			
		||||
            } else if (!isAborted) {
 | 
			
		||||
              botMessage.content += "\n\n" + Locale.Store.Error;
 | 
			
		||||
            }
 | 
			
		||||
            botMessage.streaming = false;
 | 
			
		||||
            userMessage.isError = true;
 | 
			
		||||
            botMessage.isError = true;
 | 
			
		||||
            userMessage.isError = !isAborted;
 | 
			
		||||
            botMessage.isError = !isAborted;
 | 
			
		||||
 | 
			
		||||
            set(() => ({}));
 | 
			
		||||
            ControllerPool.remove(sessionIndex, botMessage.id ?? messageIndex);
 | 
			
		||||
          },
 | 
			
		||||
@@ -309,8 +303,7 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
              controller,
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
          filterBot: !useAppConfig.getState().sendBotMessages,
 | 
			
		||||
          modelConfig: useAppConfig.getState().modelConfig,
 | 
			
		||||
          modelConfig: { ...modelConfig },
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@@ -329,7 +322,7 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
 | 
			
		||||
      getMessagesWithMemory() {
 | 
			
		||||
        const session = get().currentSession();
 | 
			
		||||
        const config = useAppConfig.getState();
 | 
			
		||||
        const modelConfig = session.mask.modelConfig;
 | 
			
		||||
        const messages = session.messages.filter((msg) => !msg.isError);
 | 
			
		||||
        const n = messages.length;
 | 
			
		||||
 | 
			
		||||
@@ -337,7 +330,7 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
 | 
			
		||||
        // long term memory
 | 
			
		||||
        if (
 | 
			
		||||
          session.mask.modelConfig.sendMemory &&
 | 
			
		||||
          modelConfig.sendMemory &&
 | 
			
		||||
          session.memoryPrompt &&
 | 
			
		||||
          session.memoryPrompt.length > 0
 | 
			
		||||
        ) {
 | 
			
		||||
@@ -348,14 +341,14 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
        // get short term and unmemoried long term memory
 | 
			
		||||
        const shortTermMemoryMessageIndex = Math.max(
 | 
			
		||||
          0,
 | 
			
		||||
          n - config.modelConfig.historyMessageCount,
 | 
			
		||||
          n - modelConfig.historyMessageCount,
 | 
			
		||||
        );
 | 
			
		||||
        const longTermMemoryMessageIndex = session.lastSummarizeIndex;
 | 
			
		||||
        const oldestIndex = Math.max(
 | 
			
		||||
          shortTermMemoryMessageIndex,
 | 
			
		||||
          longTermMemoryMessageIndex,
 | 
			
		||||
        );
 | 
			
		||||
        const threshold = config.modelConfig.compressMessageLengthThreshold;
 | 
			
		||||
        const threshold = modelConfig.compressMessageLengthThreshold;
 | 
			
		||||
 | 
			
		||||
        // get recent messages as many as possible
 | 
			
		||||
        const reversedRecentMessages = [];
 | 
			
		||||
@@ -414,17 +407,17 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const config = useAppConfig.getState();
 | 
			
		||||
        const modelConfig = session.mask.modelConfig;
 | 
			
		||||
        let toBeSummarizedMsgs = session.messages.slice(
 | 
			
		||||
          session.lastSummarizeIndex,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const historyMsgLength = countMessages(toBeSummarizedMsgs);
 | 
			
		||||
 | 
			
		||||
        if (historyMsgLength > config?.modelConfig?.max_tokens ?? 4000) {
 | 
			
		||||
        if (historyMsgLength > modelConfig?.max_tokens ?? 4000) {
 | 
			
		||||
          const n = toBeSummarizedMsgs.length;
 | 
			
		||||
          toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
 | 
			
		||||
            Math.max(0, n - config.modelConfig.historyMessageCount),
 | 
			
		||||
            Math.max(0, n - modelConfig.historyMessageCount),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -437,12 +430,11 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
          "[Chat History] ",
 | 
			
		||||
          toBeSummarizedMsgs,
 | 
			
		||||
          historyMsgLength,
 | 
			
		||||
          config.modelConfig.compressMessageLengthThreshold,
 | 
			
		||||
          modelConfig.compressMessageLengthThreshold,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
          historyMsgLength >
 | 
			
		||||
            config.modelConfig.compressMessageLengthThreshold &&
 | 
			
		||||
          historyMsgLength > modelConfig.compressMessageLengthThreshold &&
 | 
			
		||||
          session.mask.modelConfig.sendMemory
 | 
			
		||||
        ) {
 | 
			
		||||
          requestChatStream(
 | 
			
		||||
@@ -452,8 +444,7 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
              date: "",
 | 
			
		||||
            }),
 | 
			
		||||
            {
 | 
			
		||||
              filterBot: false,
 | 
			
		||||
              model: "gpt-3.5-turbo",
 | 
			
		||||
              overrideModel: "gpt-3.5-turbo",
 | 
			
		||||
              onMessage(message, done) {
 | 
			
		||||
                session.memoryPrompt = message;
 | 
			
		||||
                if (done) {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,6 @@ export enum Theme {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_CONFIG = {
 | 
			
		||||
  sendBotMessages: true as boolean,
 | 
			
		||||
  submitKey: SubmitKey.CtrlEnter as SubmitKey,
 | 
			
		||||
  avatar: "1f603",
 | 
			
		||||
  fontSize: 14,
 | 
			
		||||
 
 | 
			
		||||
@@ -17,11 +17,12 @@ export interface PromptStore {
 | 
			
		||||
  prompts: Record<number, Prompt>;
 | 
			
		||||
 | 
			
		||||
  add: (prompt: Prompt) => number;
 | 
			
		||||
  get: (id: number) => Prompt | undefined;
 | 
			
		||||
  remove: (id: number) => void;
 | 
			
		||||
  search: (text: string) => Prompt[];
 | 
			
		||||
  update: (id: number, updater: (prompt: Prompt) => void) => void;
 | 
			
		||||
 | 
			
		||||
  getUserPrompts: () => Prompt[];
 | 
			
		||||
  updateUserPrompts: (id: number, updater: (prompt: Prompt) => void) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SearchService = {
 | 
			
		||||
@@ -81,6 +82,16 @@ export const usePromptStore = create<PromptStore>()(
 | 
			
		||||
        return prompt.id!;
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      get(id) {
 | 
			
		||||
        const targetPrompt = get().prompts[id];
 | 
			
		||||
 | 
			
		||||
        if (!targetPrompt) {
 | 
			
		||||
          return SearchService.builtinPrompts.find((v) => v.id === id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return targetPrompt;
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      remove(id) {
 | 
			
		||||
        const prompts = get().prompts;
 | 
			
		||||
        delete prompts[id];
 | 
			
		||||
@@ -98,7 +109,7 @@ export const usePromptStore = create<PromptStore>()(
 | 
			
		||||
        return userPrompts;
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      updateUserPrompts(id: number, updater) {
 | 
			
		||||
      update(id: number, updater) {
 | 
			
		||||
        const prompt = get().prompts[id] ?? {
 | 
			
		||||
          title: "",
 | 
			
		||||
          content: "",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
dir="$(dirname "$0")"
 | 
			
		||||
config=$dir/proxychains.conf
 | 
			
		||||
host_ip=$(grep nameserver /etc/resolv.conf | sed 's/nameserver //')
 | 
			
		||||
echo "proxying to $host_ip"
 | 
			
		||||
cp $dir/proxychains.template.conf $config 
 | 
			
		||||
sed -i "\$s/.*/http $host_ip 7890/" $config
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user