mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	feat: add session config modal
This commit is contained in:
		@@ -53,6 +53,20 @@
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.section-title {
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
 | 
			
		||||
  .section-title-action {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.context-prompt {
 | 
			
		||||
  .context-prompt-row {
 | 
			
		||||
    display: flex;
 | 
			
		||||
@@ -81,25 +95,13 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.memory-prompt {
 | 
			
		||||
  margin-top: 20px;
 | 
			
		||||
 | 
			
		||||
  .memory-prompt-title {
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    .memory-prompt-action {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  margin: 20px 0;
 | 
			
		||||
 | 
			
		||||
  .memory-prompt-content {
 | 
			
		||||
    background-color: var(--gray);
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    background-color: var(--white);
 | 
			
		||||
    color: var(--black);
 | 
			
		||||
    border: var(--border-in-light);
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    user-select: text;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { useDebounce, useDebouncedCallback } from "use-debounce";
 | 
			
		||||
import { useDebouncedCallback } from "use-debounce";
 | 
			
		||||
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
 | 
			
		||||
 | 
			
		||||
import SendWhiteIcon from "../icons/send-white.svg";
 | 
			
		||||
@@ -9,8 +9,6 @@ import ReturnIcon from "../icons/return.svg";
 | 
			
		||||
import CopyIcon from "../icons/copy.svg";
 | 
			
		||||
import DownloadIcon from "../icons/download.svg";
 | 
			
		||||
import LoadingIcon from "../icons/three-dots.svg";
 | 
			
		||||
import BotIcon from "../icons/bot.svg";
 | 
			
		||||
import BlackBotIcon from "../icons/black-bot.svg";
 | 
			
		||||
import AddIcon from "../icons/add.svg";
 | 
			
		||||
import DeleteIcon from "../icons/delete.svg";
 | 
			
		||||
import MaxIcon from "../icons/max.svg";
 | 
			
		||||
@@ -33,12 +31,13 @@ import {
 | 
			
		||||
  Theme,
 | 
			
		||||
  ModelType,
 | 
			
		||||
  useAppConfig,
 | 
			
		||||
  ModelConfig,
 | 
			
		||||
  DEFAULT_TOPIC,
 | 
			
		||||
} from "../store";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  copyToClipboard,
 | 
			
		||||
  downloadAs,
 | 
			
		||||
  getEmojiUrl,
 | 
			
		||||
  selectOrCopy,
 | 
			
		||||
  autoGrowTextArea,
 | 
			
		||||
  useMobileScreen,
 | 
			
		||||
@@ -54,10 +53,11 @@ import { IconButton } from "./button";
 | 
			
		||||
import styles from "./home.module.scss";
 | 
			
		||||
import chatStyle from "./chat.module.scss";
 | 
			
		||||
 | 
			
		||||
import { Input, Modal, showModal } from "./ui-lib";
 | 
			
		||||
import { Input, List, ListItem, Modal, Popover, showModal } from "./ui-lib";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { Path } from "../constant";
 | 
			
		||||
 | 
			
		||||
import { ModelConfigList } from "./model-config";
 | 
			
		||||
import { AvatarPicker } from "./emoji";
 | 
			
		||||
const Markdown = dynamic(
 | 
			
		||||
  async () => memo((await import("./markdown")).Markdown),
 | 
			
		||||
  {
 | 
			
		||||
@@ -65,32 +65,10 @@ const Markdown = dynamic(
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
 | 
			
		||||
const Avatar = dynamic(async () => (await import("./emoji")).Avatar, {
 | 
			
		||||
  loading: () => <LoadingIcon />,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function Avatar(props: { role: Message["role"]; model?: ModelType }) {
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
 | 
			
		||||
  if (props.role !== "user") {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="no-dark">
 | 
			
		||||
        {props.model?.startsWith("gpt-4") ? (
 | 
			
		||||
          <BlackBotIcon className={styles["user-avtar"]} />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <BotIcon className={styles["user-avtar"]} />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles["user-avtar"]}>
 | 
			
		||||
      <Emoji unified={config.avatar} size={18} getEmojiUrl={getEmojiUrl} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function exportMessages(messages: Message[], topic: string) {
 | 
			
		||||
  const mdText =
 | 
			
		||||
    `# ${topic}\n\n` +
 | 
			
		||||
@@ -129,15 +107,13 @@ function exportMessages(messages: Message[], topic: string) {
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function PromptToast(props: {
 | 
			
		||||
  showToast?: boolean;
 | 
			
		||||
  showModal?: boolean;
 | 
			
		||||
  setShowModal: (_: boolean) => void;
 | 
			
		||||
}) {
 | 
			
		||||
function ContextPrompts() {
 | 
			
		||||
  const chatStore = useChatStore();
 | 
			
		||||
  const session = chatStore.currentSession();
 | 
			
		||||
  const context = session.context;
 | 
			
		||||
 | 
			
		||||
  const [showPicker, setShowPicker] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const addContextPrompt = (prompt: Message) => {
 | 
			
		||||
    chatStore.updateCurrentSession((session) => {
 | 
			
		||||
      session.context.push(prompt);
 | 
			
		||||
@@ -157,46 +133,8 @@ function PromptToast(props: {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={chatStyle["prompt-toast"]} key="prompt-toast">
 | 
			
		||||
      {props.showToast && (
 | 
			
		||||
        <div
 | 
			
		||||
          className={chatStyle["prompt-toast-inner"] + " clickable"}
 | 
			
		||||
          role="button"
 | 
			
		||||
          onClick={() => props.setShowModal(true)}
 | 
			
		||||
        >
 | 
			
		||||
          <BrainIcon />
 | 
			
		||||
          <span className={chatStyle["prompt-toast-content"]}>
 | 
			
		||||
            {Locale.Context.Toast(context.length)}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      {props.showModal && (
 | 
			
		||||
        <div className="modal-mask">
 | 
			
		||||
          <Modal
 | 
			
		||||
            title={Locale.Context.Edit}
 | 
			
		||||
            onClose={() => props.setShowModal(false)}
 | 
			
		||||
            actions={[
 | 
			
		||||
              <IconButton
 | 
			
		||||
                key="reset"
 | 
			
		||||
                icon={<CopyIcon />}
 | 
			
		||||
                bordered
 | 
			
		||||
                text={Locale.Memory.Reset}
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  confirm(Locale.Memory.ResetConfirm) &&
 | 
			
		||||
                  chatStore.resetSession()
 | 
			
		||||
                }
 | 
			
		||||
              />,
 | 
			
		||||
              <IconButton
 | 
			
		||||
                key="copy"
 | 
			
		||||
                icon={<CopyIcon />}
 | 
			
		||||
                bordered
 | 
			
		||||
                text={Locale.Memory.Copy}
 | 
			
		||||
                onClick={() => copyToClipboard(session.memoryPrompt)}
 | 
			
		||||
              />,
 | 
			
		||||
            ]}
 | 
			
		||||
          >
 | 
			
		||||
    <>
 | 
			
		||||
              <div className={chatStyle["context-prompt"]}>
 | 
			
		||||
      <div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
 | 
			
		||||
        {context.map((c, i) => (
 | 
			
		||||
          <div className={chatStyle["context-prompt-row"]} key={i}>
 | 
			
		||||
            <select
 | 
			
		||||
@@ -252,34 +190,123 @@ function PromptToast(props: {
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
              <div className={chatStyle["memory-prompt"]}>
 | 
			
		||||
                <div className={chatStyle["memory-prompt-title"]}>
 | 
			
		||||
                  <span>
 | 
			
		||||
                    {Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
 | 
			
		||||
                    {session.messages.length})
 | 
			
		||||
                  </span>
 | 
			
		||||
 | 
			
		||||
                  <label className={chatStyle["memory-prompt-action"]}>
 | 
			
		||||
                    {Locale.Memory.Send}
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="checkbox"
 | 
			
		||||
                      checked={session.sendMemory}
 | 
			
		||||
                      onChange={() =>
 | 
			
		||||
      <List>
 | 
			
		||||
        <ListItem title={"角色头像"}>
 | 
			
		||||
          <Popover
 | 
			
		||||
            content={
 | 
			
		||||
              <AvatarPicker
 | 
			
		||||
                onEmojiClick={(emoji) =>
 | 
			
		||||
                  chatStore.updateCurrentSession(
 | 
			
		||||
                          (session) =>
 | 
			
		||||
                            (session.sendMemory = !session.sendMemory),
 | 
			
		||||
                    (session) => (session.avatar = emoji),
 | 
			
		||||
                  )
 | 
			
		||||
                }
 | 
			
		||||
              ></AvatarPicker>
 | 
			
		||||
            }
 | 
			
		||||
            open={showPicker}
 | 
			
		||||
            onClose={() => setShowPicker(false)}
 | 
			
		||||
          >
 | 
			
		||||
            <div onClick={() => setShowPicker(true)}>
 | 
			
		||||
              {session.avatar ? (
 | 
			
		||||
                <Avatar avatar={session.avatar} />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <Avatar model={session.modelConfig.model} />
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </Popover>
 | 
			
		||||
        </ListItem>
 | 
			
		||||
        <ListItem title={"对话标题"}>
 | 
			
		||||
          <input
 | 
			
		||||
            type="text"
 | 
			
		||||
            value={session.topic}
 | 
			
		||||
            onInput={(e) =>
 | 
			
		||||
              chatStore.updateCurrentSession(
 | 
			
		||||
                (session) => (session.topic = e.currentTarget.value),
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
          ></input>
 | 
			
		||||
                  </label>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className={chatStyle["memory-prompt-content"]}>
 | 
			
		||||
                  {session.memoryPrompt || Locale.Memory.EmptyContent}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
        </ListItem>
 | 
			
		||||
        <ListItem
 | 
			
		||||
          title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of 
 | 
			
		||||
          ${session.messages.length})`}
 | 
			
		||||
          subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
 | 
			
		||||
        ></ListItem>
 | 
			
		||||
      </List>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function SessionConfigModel(props: { onClose: () => void }) {
 | 
			
		||||
  const chatStore = useChatStore();
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
  const session = chatStore.currentSession();
 | 
			
		||||
  const context = session.context;
 | 
			
		||||
 | 
			
		||||
  const updateConfig = (updater: (config: ModelConfig) => void) => {
 | 
			
		||||
    const config = { ...session.modelConfig };
 | 
			
		||||
    updater(config);
 | 
			
		||||
    chatStore.updateCurrentSession((session) => (session.modelConfig = config));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="modal-mask">
 | 
			
		||||
      <Modal
 | 
			
		||||
        title={Locale.Context.Edit}
 | 
			
		||||
        onClose={() => props.onClose()}
 | 
			
		||||
        actions={[
 | 
			
		||||
          <IconButton
 | 
			
		||||
            key="reset"
 | 
			
		||||
            icon={<CopyIcon />}
 | 
			
		||||
            bordered
 | 
			
		||||
            text="重置预设"
 | 
			
		||||
            onClick={() =>
 | 
			
		||||
              confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession()
 | 
			
		||||
            }
 | 
			
		||||
          />,
 | 
			
		||||
          <IconButton
 | 
			
		||||
            key="copy"
 | 
			
		||||
            icon={<CopyIcon />}
 | 
			
		||||
            bordered
 | 
			
		||||
            text="保存预设"
 | 
			
		||||
            onClick={() => copyToClipboard(session.memoryPrompt)}
 | 
			
		||||
          />,
 | 
			
		||||
        ]}
 | 
			
		||||
      >
 | 
			
		||||
        <ContextPrompts />
 | 
			
		||||
 | 
			
		||||
        <ModelConfigList
 | 
			
		||||
          modelConfig={session.modelConfig}
 | 
			
		||||
          updateConfig={updateConfig}
 | 
			
		||||
        />
 | 
			
		||||
      </Modal>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function PromptToast(props: {
 | 
			
		||||
  showToast?: boolean;
 | 
			
		||||
  showModal?: boolean;
 | 
			
		||||
  setShowModal: (_: boolean) => void;
 | 
			
		||||
}) {
 | 
			
		||||
  const chatStore = useChatStore();
 | 
			
		||||
  const session = chatStore.currentSession();
 | 
			
		||||
  const context = session.context;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={chatStyle["prompt-toast"]} key="prompt-toast">
 | 
			
		||||
      {props.showToast && (
 | 
			
		||||
        <div
 | 
			
		||||
          className={chatStyle["prompt-toast-inner"] + " clickable"}
 | 
			
		||||
          role="button"
 | 
			
		||||
          onClick={() => props.setShowModal(true)}
 | 
			
		||||
        >
 | 
			
		||||
          <BrainIcon />
 | 
			
		||||
          <span className={chatStyle["prompt-toast-content"]}>
 | 
			
		||||
            {Locale.Context.Toast(context.length)}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      {props.showModal && (
 | 
			
		||||
        <SessionConfigModel onClose={() => props.setShowModal(false)} />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
@@ -654,7 +681,7 @@ export function Chat() {
 | 
			
		||||
            className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
 | 
			
		||||
            onClickCapture={renameSession}
 | 
			
		||||
          >
 | 
			
		||||
            {session.topic}
 | 
			
		||||
            {!session.topic ? DEFAULT_TOPIC : session.topic}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className={styles["window-header-sub-title"]}>
 | 
			
		||||
            {Locale.Chat.SubTitle(session.messages.length)}
 | 
			
		||||
@@ -739,7 +766,13 @@ export function Chat() {
 | 
			
		||||
            >
 | 
			
		||||
              <div className={styles["chat-message-container"]}>
 | 
			
		||||
                <div className={styles["chat-message-avatar"]}>
 | 
			
		||||
                  <Avatar role={message.role} model={message.model} />
 | 
			
		||||
                  {message.role === "user" ? (
 | 
			
		||||
                    <Avatar avatar={config.avatar} />
 | 
			
		||||
                  ) : session.avatar ? (
 | 
			
		||||
                    <Avatar avatar={session.avatar} />
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <Avatar model={message.model ?? "gpt-3.5-turbo"} />
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
                {showTyping && (
 | 
			
		||||
                  <div className={styles["chat-message-status"]}>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										59
									
								
								app/components/emoji.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								app/components/emoji.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
import EmojiPicker, {
 | 
			
		||||
  Emoji,
 | 
			
		||||
  EmojiStyle,
 | 
			
		||||
  Theme as EmojiTheme,
 | 
			
		||||
} from "emoji-picker-react";
 | 
			
		||||
 | 
			
		||||
import { ModelType } from "../store";
 | 
			
		||||
 | 
			
		||||
import BotIcon from "../icons/bot.svg";
 | 
			
		||||
import BlackBotIcon from "../icons/black-bot.svg";
 | 
			
		||||
 | 
			
		||||
export function getEmojiUrl(unified: string, style: EmojiStyle) {
 | 
			
		||||
  return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function AvatarPicker(props: {
 | 
			
		||||
  onEmojiClick: (emojiId: string) => void;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <EmojiPicker
 | 
			
		||||
      lazyLoadEmojis
 | 
			
		||||
      theme={EmojiTheme.AUTO}
 | 
			
		||||
      getEmojiUrl={getEmojiUrl}
 | 
			
		||||
      onEmojiClick={(e) => {
 | 
			
		||||
        props.onEmojiClick(e.unified);
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Avatar(props: { model?: ModelType; avatar?: string }) {
 | 
			
		||||
  if (props.model) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="no-dark">
 | 
			
		||||
        {props.model?.startsWith("gpt-4") ? (
 | 
			
		||||
          <BlackBotIcon className="user-avtar" />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <BotIcon className="user-avtar" />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="user-avtar">
 | 
			
		||||
      {props.avatar && <EmojiAvatar avatar={props.avatar} />}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function EmojiAvatar(props: { avatar: string; size?: number }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Emoji
 | 
			
		||||
      unified={props.avatar}
 | 
			
		||||
      size={props.size ?? 18}
 | 
			
		||||
      getEmojiUrl={getEmojiUrl}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -368,17 +368,6 @@
 | 
			
		||||
  margin-top: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.user-avtar {
 | 
			
		||||
  height: 30px;
 | 
			
		||||
  width: 30px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  border: var(--border-in-light);
 | 
			
		||||
  box-shadow: var(--card-shadow);
 | 
			
		||||
  border-radius: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chat-message-item {
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										141
									
								
								app/components/model-config.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								app/components/model-config.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,141 @@
 | 
			
		||||
import styles from "./settings.module.scss";
 | 
			
		||||
import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
 | 
			
		||||
 | 
			
		||||
import Locale from "../locales";
 | 
			
		||||
import { InputRange } from "./input-range";
 | 
			
		||||
import { List, ListItem } from "./ui-lib";
 | 
			
		||||
 | 
			
		||||
export function ModelConfigList(props: {
 | 
			
		||||
  modelConfig: ModelConfig;
 | 
			
		||||
  updateConfig: (updater: (config: ModelConfig) => void) => void;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <List>
 | 
			
		||||
      <ListItem title={Locale.Settings.Model}>
 | 
			
		||||
        <select
 | 
			
		||||
          value={props.modelConfig.model}
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) =>
 | 
			
		||||
                (config.model = ModalConfigValidator.model(
 | 
			
		||||
                  e.currentTarget.value,
 | 
			
		||||
                )),
 | 
			
		||||
            );
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {ALL_MODELS.map((v) => (
 | 
			
		||||
            <option value={v.name} key={v.name} disabled={!v.available}>
 | 
			
		||||
              {v.name}
 | 
			
		||||
            </option>
 | 
			
		||||
          ))}
 | 
			
		||||
        </select>
 | 
			
		||||
      </ListItem>
 | 
			
		||||
      <ListItem
 | 
			
		||||
        title={Locale.Settings.Temperature.Title}
 | 
			
		||||
        subTitle={Locale.Settings.Temperature.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <InputRange
 | 
			
		||||
          value={props.modelConfig.temperature?.toFixed(1)}
 | 
			
		||||
          min="0"
 | 
			
		||||
          max="2"
 | 
			
		||||
          step="0.1"
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) =>
 | 
			
		||||
                (config.temperature = ModalConfigValidator.temperature(
 | 
			
		||||
                  e.currentTarget.valueAsNumber,
 | 
			
		||||
                )),
 | 
			
		||||
            );
 | 
			
		||||
          }}
 | 
			
		||||
        ></InputRange>
 | 
			
		||||
      </ListItem>
 | 
			
		||||
      <ListItem
 | 
			
		||||
        title={Locale.Settings.MaxTokens.Title}
 | 
			
		||||
        subTitle={Locale.Settings.MaxTokens.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <input
 | 
			
		||||
          type="number"
 | 
			
		||||
          min={100}
 | 
			
		||||
          max={32000}
 | 
			
		||||
          value={props.modelConfig.max_tokens}
 | 
			
		||||
          onChange={(e) =>
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) =>
 | 
			
		||||
                (config.max_tokens = ModalConfigValidator.max_tokens(
 | 
			
		||||
                  e.currentTarget.valueAsNumber,
 | 
			
		||||
                )),
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        ></input>
 | 
			
		||||
      </ListItem>
 | 
			
		||||
      <ListItem
 | 
			
		||||
        title={Locale.Settings.PresencePenlty.Title}
 | 
			
		||||
        subTitle={Locale.Settings.PresencePenlty.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <InputRange
 | 
			
		||||
          value={props.modelConfig.presence_penalty?.toFixed(1)}
 | 
			
		||||
          min="-2"
 | 
			
		||||
          max="2"
 | 
			
		||||
          step="0.1"
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) =>
 | 
			
		||||
                (config.presence_penalty =
 | 
			
		||||
                  ModalConfigValidator.presence_penalty(
 | 
			
		||||
                    e.currentTarget.valueAsNumber,
 | 
			
		||||
                  )),
 | 
			
		||||
            );
 | 
			
		||||
          }}
 | 
			
		||||
        ></InputRange>
 | 
			
		||||
      </ListItem>
 | 
			
		||||
 | 
			
		||||
      <ListItem
 | 
			
		||||
        title={Locale.Settings.HistoryCount.Title}
 | 
			
		||||
        subTitle={Locale.Settings.HistoryCount.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <InputRange
 | 
			
		||||
          title={props.modelConfig.historyMessageCount.toString()}
 | 
			
		||||
          value={props.modelConfig.historyMessageCount}
 | 
			
		||||
          min="0"
 | 
			
		||||
          max="25"
 | 
			
		||||
          step="1"
 | 
			
		||||
          onChange={(e) =>
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) => (config.historyMessageCount = e.target.valueAsNumber),
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        ></InputRange>
 | 
			
		||||
      </ListItem>
 | 
			
		||||
 | 
			
		||||
      <ListItem
 | 
			
		||||
        title={Locale.Settings.CompressThreshold.Title}
 | 
			
		||||
        subTitle={Locale.Settings.CompressThreshold.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <input
 | 
			
		||||
          type="number"
 | 
			
		||||
          min={500}
 | 
			
		||||
          max={4000}
 | 
			
		||||
          value={props.modelConfig.compressMessageLengthThreshold}
 | 
			
		||||
          onChange={(e) =>
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) =>
 | 
			
		||||
                (config.compressMessageLengthThreshold =
 | 
			
		||||
                  e.currentTarget.valueAsNumber),
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        ></input>
 | 
			
		||||
      </ListItem>
 | 
			
		||||
      <ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
 | 
			
		||||
        <input
 | 
			
		||||
          type="checkbox"
 | 
			
		||||
          checked={props.modelConfig.sendMemory}
 | 
			
		||||
          onChange={(e) =>
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) => (config.sendMemory = e.currentTarget.checked),
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        ></input>
 | 
			
		||||
      </ListItem>
 | 
			
		||||
    </List>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -5,16 +5,6 @@
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.settings-title {
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  font-weight: bolder;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.settings-sub-title {
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.avatar {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
 | 
			
		||||
 | 
			
		||||
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
 | 
			
		||||
 | 
			
		||||
import styles from "./settings.module.scss";
 | 
			
		||||
 | 
			
		||||
import ResetIcon from "../icons/reload.svg";
 | 
			
		||||
@@ -10,30 +8,27 @@ import CopyIcon from "../icons/copy.svg";
 | 
			
		||||
import ClearIcon from "../icons/clear.svg";
 | 
			
		||||
import EditIcon from "../icons/edit.svg";
 | 
			
		||||
import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib";
 | 
			
		||||
import { ModelConfigList } from "./model-config";
 | 
			
		||||
 | 
			
		||||
import { IconButton } from "./button";
 | 
			
		||||
import {
 | 
			
		||||
  SubmitKey,
 | 
			
		||||
  useChatStore,
 | 
			
		||||
  Theme,
 | 
			
		||||
  ALL_MODELS,
 | 
			
		||||
  useUpdateStore,
 | 
			
		||||
  useAccessStore,
 | 
			
		||||
  ModalConfigValidator,
 | 
			
		||||
  useAppConfig,
 | 
			
		||||
  ChatConfig,
 | 
			
		||||
  ModelConfig,
 | 
			
		||||
} from "../store";
 | 
			
		||||
import { Avatar } from "./chat";
 | 
			
		||||
 | 
			
		||||
import Locale, { AllLangs, changeLang, getLang } from "../locales";
 | 
			
		||||
import { copyToClipboard, getEmojiUrl } from "../utils";
 | 
			
		||||
import { copyToClipboard } from "../utils";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import { Path, UPDATE_URL } from "../constant";
 | 
			
		||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
 | 
			
		||||
import { ErrorBoundary } from "./error";
 | 
			
		||||
import { InputRange } from "./input-range";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { Avatar, AvatarPicker } from "./emoji";
 | 
			
		||||
 | 
			
		||||
function UserPromptModal(props: { onClose?: () => void }) {
 | 
			
		||||
  const promptStore = usePromptStore();
 | 
			
		||||
@@ -136,148 +131,6 @@ function UserPromptModal(props: { onClose?: () => void }) {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SettingItem(props: {
 | 
			
		||||
  title: string;
 | 
			
		||||
  subTitle?: string;
 | 
			
		||||
  children: JSX.Element;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <ListItem>
 | 
			
		||||
      <div className={styles["settings-title"]}>
 | 
			
		||||
        <div>{props.title}</div>
 | 
			
		||||
        {props.subTitle && (
 | 
			
		||||
          <div className={styles["settings-sub-title"]}>{props.subTitle}</div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </ListItem>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ModelConfigList(props: {
 | 
			
		||||
  modelConfig: ModelConfig;
 | 
			
		||||
  updateConfig: (updater: (config: ModelConfig) => void) => void;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <SettingItem title={Locale.Settings.Model}>
 | 
			
		||||
        <select
 | 
			
		||||
          value={props.modelConfig.model}
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) =>
 | 
			
		||||
                (config.model = ModalConfigValidator.model(
 | 
			
		||||
                  e.currentTarget.value,
 | 
			
		||||
                )),
 | 
			
		||||
            );
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {ALL_MODELS.map((v) => (
 | 
			
		||||
            <option value={v.name} key={v.name} disabled={!v.available}>
 | 
			
		||||
              {v.name}
 | 
			
		||||
            </option>
 | 
			
		||||
          ))}
 | 
			
		||||
        </select>
 | 
			
		||||
      </SettingItem>
 | 
			
		||||
      <SettingItem
 | 
			
		||||
        title={Locale.Settings.Temperature.Title}
 | 
			
		||||
        subTitle={Locale.Settings.Temperature.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <InputRange
 | 
			
		||||
          value={props.modelConfig.temperature?.toFixed(1)}
 | 
			
		||||
          min="0"
 | 
			
		||||
          max="2"
 | 
			
		||||
          step="0.1"
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) =>
 | 
			
		||||
                (config.temperature = ModalConfigValidator.temperature(
 | 
			
		||||
                  e.currentTarget.valueAsNumber,
 | 
			
		||||
                )),
 | 
			
		||||
            );
 | 
			
		||||
          }}
 | 
			
		||||
        ></InputRange>
 | 
			
		||||
      </SettingItem>
 | 
			
		||||
      <SettingItem
 | 
			
		||||
        title={Locale.Settings.MaxTokens.Title}
 | 
			
		||||
        subTitle={Locale.Settings.MaxTokens.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <input
 | 
			
		||||
          type="number"
 | 
			
		||||
          min={100}
 | 
			
		||||
          max={32000}
 | 
			
		||||
          value={props.modelConfig.max_tokens}
 | 
			
		||||
          onChange={(e) =>
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) =>
 | 
			
		||||
                (config.max_tokens = ModalConfigValidator.max_tokens(
 | 
			
		||||
                  e.currentTarget.valueAsNumber,
 | 
			
		||||
                )),
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        ></input>
 | 
			
		||||
      </SettingItem>
 | 
			
		||||
      <SettingItem
 | 
			
		||||
        title={Locale.Settings.PresencePenlty.Title}
 | 
			
		||||
        subTitle={Locale.Settings.PresencePenlty.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <InputRange
 | 
			
		||||
          value={props.modelConfig.presence_penalty?.toFixed(1)}
 | 
			
		||||
          min="-2"
 | 
			
		||||
          max="2"
 | 
			
		||||
          step="0.1"
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) =>
 | 
			
		||||
                (config.presence_penalty =
 | 
			
		||||
                  ModalConfigValidator.presence_penalty(
 | 
			
		||||
                    e.currentTarget.valueAsNumber,
 | 
			
		||||
                  )),
 | 
			
		||||
            );
 | 
			
		||||
          }}
 | 
			
		||||
        ></InputRange>
 | 
			
		||||
      </SettingItem>
 | 
			
		||||
 | 
			
		||||
      <SettingItem
 | 
			
		||||
        title={Locale.Settings.HistoryCount.Title}
 | 
			
		||||
        subTitle={Locale.Settings.HistoryCount.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <InputRange
 | 
			
		||||
          title={props.modelConfig.historyMessageCount.toString()}
 | 
			
		||||
          value={props.modelConfig.historyMessageCount}
 | 
			
		||||
          min="0"
 | 
			
		||||
          max="25"
 | 
			
		||||
          step="1"
 | 
			
		||||
          onChange={(e) =>
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) => (config.historyMessageCount = e.target.valueAsNumber),
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        ></InputRange>
 | 
			
		||||
      </SettingItem>
 | 
			
		||||
 | 
			
		||||
      <SettingItem
 | 
			
		||||
        title={Locale.Settings.CompressThreshold.Title}
 | 
			
		||||
        subTitle={Locale.Settings.CompressThreshold.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <input
 | 
			
		||||
          type="number"
 | 
			
		||||
          min={500}
 | 
			
		||||
          max={4000}
 | 
			
		||||
          value={props.modelConfig.compressMessageLengthThreshold}
 | 
			
		||||
          onChange={(e) =>
 | 
			
		||||
            props.updateConfig(
 | 
			
		||||
              (config) =>
 | 
			
		||||
                (config.compressMessageLengthThreshold =
 | 
			
		||||
                  e.currentTarget.valueAsNumber),
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        ></input>
 | 
			
		||||
      </SettingItem>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Settings() {
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const [showEmojiPicker, setShowEmojiPicker] = useState(false);
 | 
			
		||||
@@ -401,16 +254,13 @@ export function Settings() {
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className={styles["settings"]}>
 | 
			
		||||
        <List>
 | 
			
		||||
          <SettingItem title={Locale.Settings.Avatar}>
 | 
			
		||||
          <ListItem title={Locale.Settings.Avatar}>
 | 
			
		||||
            <Popover
 | 
			
		||||
              onClose={() => setShowEmojiPicker(false)}
 | 
			
		||||
              content={
 | 
			
		||||
                <EmojiPicker
 | 
			
		||||
                  lazyLoadEmojis
 | 
			
		||||
                  theme={EmojiTheme.AUTO}
 | 
			
		||||
                  getEmojiUrl={getEmojiUrl}
 | 
			
		||||
                  onEmojiClick={(e) => {
 | 
			
		||||
                    updateConfig((config) => (config.avatar = e.unified));
 | 
			
		||||
                <AvatarPicker
 | 
			
		||||
                  onEmojiClick={(avatar: string) => {
 | 
			
		||||
                    updateConfig((config) => (config.avatar = avatar));
 | 
			
		||||
                    setShowEmojiPicker(false);
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
@@ -421,12 +271,12 @@ export function Settings() {
 | 
			
		||||
                className={styles.avatar}
 | 
			
		||||
                onClick={() => setShowEmojiPicker(true)}
 | 
			
		||||
              >
 | 
			
		||||
                <Avatar role="user" />
 | 
			
		||||
                <Avatar avatar={config.avatar} />
 | 
			
		||||
              </div>
 | 
			
		||||
            </Popover>
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
 | 
			
		||||
          <SettingItem
 | 
			
		||||
          <ListItem
 | 
			
		||||
            title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
 | 
			
		||||
            subTitle={
 | 
			
		||||
              checkingUpdate
 | 
			
		||||
@@ -449,9 +299,9 @@ export function Settings() {
 | 
			
		||||
                onClick={() => checkUpdate(true)}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
 | 
			
		||||
          <SettingItem title={Locale.Settings.SendKey}>
 | 
			
		||||
          <ListItem title={Locale.Settings.SendKey}>
 | 
			
		||||
            <select
 | 
			
		||||
              value={config.submitKey}
 | 
			
		||||
              onChange={(e) => {
 | 
			
		||||
@@ -467,12 +317,9 @@ export function Settings() {
 | 
			
		||||
                </option>
 | 
			
		||||
              ))}
 | 
			
		||||
            </select>
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
 | 
			
		||||
          <ListItem>
 | 
			
		||||
            <div className={styles["settings-title"]}>
 | 
			
		||||
              {Locale.Settings.Theme}
 | 
			
		||||
            </div>
 | 
			
		||||
          <ListItem title={Locale.Settings.Theme}>
 | 
			
		||||
            <select
 | 
			
		||||
              value={config.theme}
 | 
			
		||||
              onChange={(e) => {
 | 
			
		||||
@@ -489,7 +336,7 @@ export function Settings() {
 | 
			
		||||
            </select>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
 | 
			
		||||
          <SettingItem title={Locale.Settings.Lang.Name}>
 | 
			
		||||
          <ListItem title={Locale.Settings.Lang.Name}>
 | 
			
		||||
            <select
 | 
			
		||||
              value={getLang()}
 | 
			
		||||
              onChange={(e) => {
 | 
			
		||||
@@ -502,9 +349,9 @@ export function Settings() {
 | 
			
		||||
                </option>
 | 
			
		||||
              ))}
 | 
			
		||||
            </select>
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
 | 
			
		||||
          <SettingItem
 | 
			
		||||
          <ListItem
 | 
			
		||||
            title={Locale.Settings.FontSize.Title}
 | 
			
		||||
            subTitle={Locale.Settings.FontSize.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
@@ -521,9 +368,9 @@ export function Settings() {
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
            ></InputRange>
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
 | 
			
		||||
          <SettingItem title={Locale.Settings.TightBorder}>
 | 
			
		||||
          <ListItem title={Locale.Settings.TightBorder}>
 | 
			
		||||
            <input
 | 
			
		||||
              type="checkbox"
 | 
			
		||||
              checked={config.tightBorder}
 | 
			
		||||
@@ -533,9 +380,9 @@ export function Settings() {
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
            ></input>
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
 | 
			
		||||
          <SettingItem title={Locale.Settings.SendPreviewBubble}>
 | 
			
		||||
          <ListItem title={Locale.Settings.SendPreviewBubble}>
 | 
			
		||||
            <input
 | 
			
		||||
              type="checkbox"
 | 
			
		||||
              checked={config.sendPreviewBubble}
 | 
			
		||||
@@ -546,12 +393,12 @@ export function Settings() {
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
            ></input>
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
        </List>
 | 
			
		||||
 | 
			
		||||
        <List>
 | 
			
		||||
          {enabledAccessControl ? (
 | 
			
		||||
            <SettingItem
 | 
			
		||||
            <ListItem
 | 
			
		||||
              title={Locale.Settings.AccessCode.Title}
 | 
			
		||||
              subTitle={Locale.Settings.AccessCode.SubTitle}
 | 
			
		||||
            >
 | 
			
		||||
@@ -563,12 +410,12 @@ export function Settings() {
 | 
			
		||||
                  accessStore.updateCode(e.currentTarget.value);
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </SettingItem>
 | 
			
		||||
            </ListItem>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <></>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <SettingItem
 | 
			
		||||
          <ListItem
 | 
			
		||||
            title={Locale.Settings.Token.Title}
 | 
			
		||||
            subTitle={Locale.Settings.Token.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
@@ -580,9 +427,9 @@ export function Settings() {
 | 
			
		||||
                accessStore.updateToken(e.currentTarget.value);
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
 | 
			
		||||
          <SettingItem
 | 
			
		||||
          <ListItem
 | 
			
		||||
            title={Locale.Settings.Usage.Title}
 | 
			
		||||
            subTitle={
 | 
			
		||||
              showUsage
 | 
			
		||||
@@ -604,11 +451,11 @@ export function Settings() {
 | 
			
		||||
                onClick={checkUsage}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
        </List>
 | 
			
		||||
 | 
			
		||||
        <List>
 | 
			
		||||
          <SettingItem
 | 
			
		||||
          <ListItem
 | 
			
		||||
            title={Locale.Settings.Prompt.Disable.Title}
 | 
			
		||||
            subTitle={Locale.Settings.Prompt.Disable.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
@@ -622,9 +469,9 @@ export function Settings() {
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
            ></input>
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
 | 
			
		||||
          <SettingItem
 | 
			
		||||
          <ListItem
 | 
			
		||||
            title={Locale.Settings.Prompt.List}
 | 
			
		||||
            subTitle={Locale.Settings.Prompt.ListCount(
 | 
			
		||||
              builtinCount,
 | 
			
		||||
@@ -636,10 +483,9 @@ export function Settings() {
 | 
			
		||||
              text={Locale.Settings.Prompt.Edit}
 | 
			
		||||
              onClick={() => setShowPromptModal(true)}
 | 
			
		||||
            />
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
          </ListItem>
 | 
			
		||||
        </List>
 | 
			
		||||
 | 
			
		||||
        <List>
 | 
			
		||||
        <ModelConfigList
 | 
			
		||||
          modelConfig={config.modelConfig}
 | 
			
		||||
          updateConfig={(upater) => {
 | 
			
		||||
@@ -648,7 +494,6 @@ export function Settings() {
 | 
			
		||||
            config.update((config) => (config.modelConfig = modelConfig));
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        </List>
 | 
			
		||||
 | 
			
		||||
        {shouldShowPromptModal && (
 | 
			
		||||
          <UserPromptModal onClose={() => setShowPromptModal(false)} />
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,16 @@
 | 
			
		||||
  border-bottom: var(--border-in-light);
 | 
			
		||||
  padding: 10px 20px;
 | 
			
		||||
  animation: slide-in ease 0.6s;
 | 
			
		||||
 | 
			
		||||
  .list-item-title {
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    font-weight: bolder;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .list-item-sub-title {
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list {
 | 
			
		||||
@@ -89,6 +99,8 @@
 | 
			
		||||
    padding: var(--modal-padding);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    border-top: var(--border-in-light);
 | 
			
		||||
    box-shadow: var(--shadow);
 | 
			
		||||
 | 
			
		||||
    .modal-actions {
 | 
			
		||||
      display: flex;
 | 
			
		||||
 
 | 
			
		||||
@@ -33,12 +33,22 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ListItem(props: { children: JSX.Element[] }) {
 | 
			
		||||
  if (props.children.length > 2) {
 | 
			
		||||
    throw Error("Only Support Two Children");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <div className={styles["list-item"]}>{props.children}</div>;
 | 
			
		||||
export function ListItem(props: {
 | 
			
		||||
  title: string;
 | 
			
		||||
  subTitle?: string;
 | 
			
		||||
  children?: JSX.Element | JSX.Element[];
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles["list-item"]}>
 | 
			
		||||
      <div className={styles["list-item-title"]}>
 | 
			
		||||
        <div>{props.title}</div>
 | 
			
		||||
        {props.subTitle && (
 | 
			
		||||
          <div className={styles["list-item-sub-title"]}>{props.subTitle}</div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function List(props: { children: JSX.Element[] | JSX.Element }) {
 | 
			
		||||
@@ -63,7 +73,7 @@ export function Loading() {
 | 
			
		||||
 | 
			
		||||
interface ModalProps {
 | 
			
		||||
  title: string;
 | 
			
		||||
  children?: JSX.Element;
 | 
			
		||||
  children?: JSX.Element | JSX.Element[];
 | 
			
		||||
  actions?: JSX.Element[];
 | 
			
		||||
  onClose?: () => void;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ const cn = {
 | 
			
		||||
  },
 | 
			
		||||
  Memory: {
 | 
			
		||||
    Title: "历史摘要",
 | 
			
		||||
    EmptyContent: "尚未总结",
 | 
			
		||||
    EmptyContent: "对话内容过短,无需总结",
 | 
			
		||||
    Send: "启用总结并发送摘要",
 | 
			
		||||
    Copy: "复制摘要",
 | 
			
		||||
    Reset: "重置对话",
 | 
			
		||||
@@ -172,8 +172,8 @@ const cn = {
 | 
			
		||||
  },
 | 
			
		||||
  Context: {
 | 
			
		||||
    Toast: (x: any) => `已设置 ${x} 条前置上下文`,
 | 
			
		||||
    Edit: "前置上下文和历史记忆",
 | 
			
		||||
    Add: "新增一条",
 | 
			
		||||
    Edit: "当前对话设置",
 | 
			
		||||
    Add: "新增预设对话",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import { isMobileScreen, trimTopic } from "../utils";
 | 
			
		||||
 | 
			
		||||
import Locale from "../locales";
 | 
			
		||||
import { showToast } from "../components/ui-lib";
 | 
			
		||||
import { ModelType, useAppConfig } from "./config";
 | 
			
		||||
import { ModelConfig, ModelType, useAppConfig } from "./config";
 | 
			
		||||
 | 
			
		||||
export type Message = ChatCompletionResponseMessage & {
 | 
			
		||||
  date: string;
 | 
			
		||||
@@ -42,16 +42,18 @@ export interface ChatStat {
 | 
			
		||||
export interface ChatSession {
 | 
			
		||||
  id: number;
 | 
			
		||||
  topic: string;
 | 
			
		||||
  sendMemory: boolean;
 | 
			
		||||
  avatar?: string;
 | 
			
		||||
  memoryPrompt: string;
 | 
			
		||||
  context: Message[];
 | 
			
		||||
  messages: Message[];
 | 
			
		||||
  stat: ChatStat;
 | 
			
		||||
  lastUpdate: string;
 | 
			
		||||
  lastSummarizeIndex: number;
 | 
			
		||||
 | 
			
		||||
  modelConfig: ModelConfig;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
 | 
			
		||||
export const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
 | 
			
		||||
export const BOT_HELLO: Message = createMessage({
 | 
			
		||||
  role: "assistant",
 | 
			
		||||
  content: Locale.Store.BotHello,
 | 
			
		||||
@@ -63,7 +65,6 @@ function createEmptySession(): ChatSession {
 | 
			
		||||
  return {
 | 
			
		||||
    id: Date.now(),
 | 
			
		||||
    topic: DEFAULT_TOPIC,
 | 
			
		||||
    sendMemory: true,
 | 
			
		||||
    memoryPrompt: "",
 | 
			
		||||
    context: [],
 | 
			
		||||
    messages: [],
 | 
			
		||||
@@ -74,6 +75,8 @@ function createEmptySession(): ChatSession {
 | 
			
		||||
    },
 | 
			
		||||
    lastUpdate: createDate,
 | 
			
		||||
    lastSummarizeIndex: 0,
 | 
			
		||||
 | 
			
		||||
    modelConfig: useAppConfig.getState().modelConfig,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -32,6 +32,7 @@ const DEFAULT_CONFIG = {
 | 
			
		||||
    temperature: 1,
 | 
			
		||||
    max_tokens: 2000,
 | 
			
		||||
    presence_penalty: 0,
 | 
			
		||||
    sendMemory: true,
 | 
			
		||||
    historyMessageCount: 4,
 | 
			
		||||
    compressMessageLengthThreshold: 1000,
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
export * from "./app";
 | 
			
		||||
export * from "./chat";
 | 
			
		||||
export * from "./update";
 | 
			
		||||
export * from "./access";
 | 
			
		||||
export * from "./config";
 | 
			
		||||
 
 | 
			
		||||
@@ -325,3 +325,14 @@ pre {
 | 
			
		||||
    min-width: 80%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.user-avtar {
 | 
			
		||||
  height: 30px;
 | 
			
		||||
  width: 30px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  border: var(--border-in-light);
 | 
			
		||||
  box-shadow: var(--card-shadow);
 | 
			
		||||
  border-radius: 10px;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import { EmojiStyle } from "emoji-picker-react";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { showToast } from "./components/ui-lib";
 | 
			
		||||
import Locale from "./locales";
 | 
			
		||||
@@ -90,10 +89,6 @@ export function selectOrCopy(el: HTMLElement, content: string) {
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getEmojiUrl(unified: string, style: EmojiStyle) {
 | 
			
		||||
  return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getDomContentWidth(dom: HTMLElement) {
 | 
			
		||||
  const style = window.getComputedStyle(dom);
 | 
			
		||||
  const paddingWidth =
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user