Browse Source

Merge branch 'main' into bugfix-0503

Yidadaa 1 year ago
parent
commit
f250594e97

+ 4 - 1
app/api/common.ts

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

+ 8 - 5
app/components/chat-list.tsx

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

+ 6 - 8
app/components/chat.tsx

@@ -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 =
@@ -394,7 +391,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 && (

+ 66 - 49
app/components/markdown.tsx

@@ -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,94 @@ 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,
+        a: (aProps) => {
+          const href = aProps.href || "";
+          const isInternal = /^\/#/i.test(href);
+          const target = isInternal ? "_self" : aProps.target ?? "_blank";
+          return <a {...aProps} target={target} />;
+        },
+      }}
+    >
+      {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 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) {
-          rendered.current = true;
-        }
+  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);
+    }
 
-        return inView;
-      }
-    })();
+    if (inView.current && md) {
+      renderedHeight.current = Math.max(
+        renderedHeight.current,
+        md.getBoundingClientRect().height,
+      );
+    }
+  };
 
-  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>
   );
 }

+ 4 - 20
app/components/mask.module.scss

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

+ 10 - 8
app/components/mask.tsx

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

+ 2 - 3
app/components/new-chat.module.scss

@@ -59,10 +59,9 @@
     display: flex;
     justify-content: center;
 
-    .search-bar {
+    .more {
       font-size: 12px;
-      margin-right: 10px;
-      width: 40vw;
+      margin-left: 10px;
     }
   }
 

+ 13 - 10
app/components/new-chat.tsx

@@ -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"]}>

+ 37 - 28
app/components/settings.module.scss

@@ -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,48 +32,43 @@
   }
 
   .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: 14px;
+          line-height: 2;
+          font-weight: bold;
+        }
+        .user-prompt-content {
           font-size: 12px;
-          text-align: left;
         }
+      }
 
-        .user-prompt-buttons {
-          display: flex;
-          align-items: center;
+      .user-prompt-buttons {
+        display: flex;
+        align-items: center;
 
-          .user-prompt-button {
-            height: 100%;
+        .user-prompt-button {
+          height: 100%;
 
-            &:not(:last-child) {
-              margin-right: 5px;
-            }
+          &:not(:last-child) {
+            margin-right: 5px;
           }
         }
       }
-
-      .user-prompt-content {
-        width: 100%;
-        box-sizing: border-box;
-        padding: 5px;
-        margin-right: 10px;
-        font-size: 12px;
-        flex-grow: 1;
-      }
     }
   }
 

+ 96 - 44
app/components/settings.tsx

@@ -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!)}
-                      />
-                    )}
+                  <div className={styles["user-prompt-title"]}>{v.title}</div>
+                  <div className={styles["user-prompt-content"] + " one-line"}>
+                    {v.content}
+                  </div>
+                </div>
+
+                <div className={styles["user-prompt-buttons"]}>
+                  {v.isUser && (
                     <IconButton
-                      icon={<CopyIcon />}
-                      bordered
+                      icon={<ClearIcon />}
                       className={styles["user-prompt-button"]}
-                      onClick={() => copyToClipboard(v.content)}
+                      onClick={() => promptStore.remove(v.id!)}
                     />
-                  </div>
+                  )}
+                  {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>
-                <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>
             ))}
           </div>
         </div>
       </Modal>
+
+      {editingPromptId !== undefined && (
+        <EditPromptModal
+          id={editingPromptId!}
+          onClose={() => setEditingPromptId(undefined)}
+        />
+      )}
     </div>
   );
 }

+ 5 - 1
app/components/sidebar.tsx

@@ -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"]}>

+ 6 - 2
app/components/ui-lib.tsx

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

+ 13 - 2
app/locales/cn.ts

@@ -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: "编辑",
   },
 };
 

+ 11 - 0
app/locales/de.ts

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

+ 11 - 0
app/locales/en.ts

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

+ 11 - 0
app/locales/es.ts

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

+ 11 - 0
app/locales/it.ts

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

+ 11 - 0
app/locales/jp.ts

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

+ 11 - 0
app/locales/tr.ts

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

+ 10 - 0
app/locales/tw.ts

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

+ 9 - 17
app/requests.ts

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

+ 59 - 68
app/store/chat.ts

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

+ 0 - 1
app/store/config.ts

@@ -17,7 +17,6 @@ export enum Theme {
 }
 
 export const DEFAULT_CONFIG = {
-  sendBotMessages: true as boolean,
   submitKey: SubmitKey.CtrlEnter as SubmitKey,
   avatar: "1f603",
   fontSize: 14,

+ 13 - 2
app/store/prompt.ts

@@ -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 - 0
scripts/init-proxy.sh

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