Browse Source

feat: add session config modal

Yidadaa 1 year ago
parent
commit
7345639af3

+ 19 - 17
app/components/chat.module.scss

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

+ 178 - 145
app/components/chat.tsx

@@ -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);
@@ -156,6 +132,165 @@ function PromptToast(props: {
     });
   };
 
+  return (
+    <>
+      <div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
+        {context.map((c, i) => (
+          <div className={chatStyle["context-prompt-row"]} key={i}>
+            <select
+              value={c.role}
+              className={chatStyle["context-role"]}
+              onChange={(e) =>
+                updateContextPrompt(i, {
+                  ...c,
+                  role: e.target.value as any,
+                })
+              }
+            >
+              {ROLES.map((r) => (
+                <option key={r} value={r}>
+                  {r}
+                </option>
+              ))}
+            </select>
+            <Input
+              value={c.content}
+              type="text"
+              className={chatStyle["context-content"]}
+              rows={1}
+              onInput={(e) =>
+                updateContextPrompt(i, {
+                  ...c,
+                  content: e.currentTarget.value as any,
+                })
+              }
+            />
+            <IconButton
+              icon={<DeleteIcon />}
+              className={chatStyle["context-delete-button"]}
+              onClick={() => removeContextPrompt(i)}
+              bordered
+            />
+          </div>
+        ))}
+
+        <div className={chatStyle["context-prompt-row"]}>
+          <IconButton
+            icon={<AddIcon />}
+            text={Locale.Context.Add}
+            bordered
+            className={chatStyle["context-prompt-button"]}
+            onClick={() =>
+              addContextPrompt({
+                role: "system",
+                content: "",
+                date: "",
+              })
+            }
+          />
+        </div>
+      </div>
+      <List>
+        <ListItem title={"角色头像"}>
+          <Popover
+            content={
+              <AvatarPicker
+                onEmojiClick={(emoji) =>
+                  chatStore.updateCurrentSession(
+                    (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>
+        </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 && (
@@ -171,115 +306,7 @@ function PromptToast(props: {
         </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"]}>
-                {context.map((c, i) => (
-                  <div className={chatStyle["context-prompt-row"]} key={i}>
-                    <select
-                      value={c.role}
-                      className={chatStyle["context-role"]}
-                      onChange={(e) =>
-                        updateContextPrompt(i, {
-                          ...c,
-                          role: e.target.value as any,
-                        })
-                      }
-                    >
-                      {ROLES.map((r) => (
-                        <option key={r} value={r}>
-                          {r}
-                        </option>
-                      ))}
-                    </select>
-                    <Input
-                      value={c.content}
-                      type="text"
-                      className={chatStyle["context-content"]}
-                      rows={1}
-                      onInput={(e) =>
-                        updateContextPrompt(i, {
-                          ...c,
-                          content: e.currentTarget.value as any,
-                        })
-                      }
-                    />
-                    <IconButton
-                      icon={<DeleteIcon />}
-                      className={chatStyle["context-delete-button"]}
-                      onClick={() => removeContextPrompt(i)}
-                      bordered
-                    />
-                  </div>
-                ))}
-
-                <div className={chatStyle["context-prompt-row"]}>
-                  <IconButton
-                    icon={<AddIcon />}
-                    text={Locale.Context.Add}
-                    bordered
-                    className={chatStyle["context-prompt-button"]}
-                    onClick={() =>
-                      addContextPrompt({
-                        role: "system",
-                        content: "",
-                        date: "",
-                      })
-                    }
-                  />
-                </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={() =>
-                        chatStore.updateCurrentSession(
-                          (session) =>
-                            (session.sendMemory = !session.sendMemory),
-                        )
-                      }
-                    ></input>
-                  </label>
-                </div>
-                <div className={chatStyle["memory-prompt-content"]}>
-                  {session.memoryPrompt || Locale.Memory.EmptyContent}
-                </div>
-              </div>
-            </>
-          </Modal>
-        </div>
+        <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 - 0
app/components/emoji.tsx

@@ -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}
+    />
+  );
+}

+ 0 - 11
app/components/home.module.scss

@@ -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 - 0
app/components/model-config.tsx

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

+ 0 - 10
app/components/settings.module.scss

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

+ 40 - 195
app/components/settings.tsx

@@ -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,19 +483,17 @@ export function Settings() {
               text={Locale.Settings.Prompt.Edit}
               onClick={() => setShowPromptModal(true)}
             />
-          </SettingItem>
+          </ListItem>
         </List>
 
-        <List>
-          <ModelConfigList
-            modelConfig={config.modelConfig}
-            updateConfig={(upater) => {
-              const modelConfig = { ...config.modelConfig };
-              upater(modelConfig);
-              config.update((config) => (config.modelConfig = modelConfig));
-            }}
-          />
-        </List>
+        <ModelConfigList
+          modelConfig={config.modelConfig}
+          updateConfig={(upater) => {
+            const modelConfig = { ...config.modelConfig };
+            upater(modelConfig);
+            config.update((config) => (config.modelConfig = modelConfig));
+          }}
+        />
 
         {shouldShowPromptModal && (
           <UserPromptModal onClose={() => setShowPromptModal(false)} />

+ 12 - 0
app/components/ui-lib.module.scss

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

+ 17 - 7
app/components/ui-lib.tsx

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

+ 3 - 3
app/locales/cn.ts

@@ -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: "新增预设对话",
   },
 };
 

+ 7 - 4
app/store/app.ts → app/store/chat.ts

@@ -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,
   };
 }
 

+ 1 - 0
app/store/config.ts

@@ -32,6 +32,7 @@ const DEFAULT_CONFIG = {
     temperature: 1,
     max_tokens: 2000,
     presence_penalty: 0,
+    sendMemory: true,
     historyMessageCount: 4,
     compressMessageLengthThreshold: 1000,
   },

+ 1 - 1
app/store/index.ts

@@ -1,4 +1,4 @@
-export * from "./app";
+export * from "./chat";
 export * from "./update";
 export * from "./access";
 export * from "./config";

+ 11 - 0
app/styles/globals.scss

@@ -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;
+}

+ 0 - 5
app/utils.ts

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