Browse Source

Merge pull request #659 from Yidadaa/bugfix-0409

fix: many UI bugs and resizable side bar
Yifei Zhang 1 year ago
parent
commit
601e72b56c

+ 3 - 0
app/components/button.module.scss

@@ -49,4 +49,7 @@
 .icon-button-text {
   margin-left: 5px;
   font-size: 12px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }

+ 1 - 1
app/components/chat-list.tsx

@@ -96,7 +96,7 @@ export function ChatList() {
                 index={i}
                 selected={i === selectedIndex}
                 onClick={() => selectSession(i)}
-                onDelete={chatStore.deleteSession}
+                onDelete={() => chatStore.deleteSession(i)}
               />
             ))}
             {provided.placeholder}

+ 22 - 2
app/components/chat.tsx

@@ -3,7 +3,7 @@ import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
 
 import SendWhiteIcon from "../icons/send-white.svg";
 import BrainIcon from "../icons/brain.svg";
-import ExportIcon from "../icons/export.svg";
+import ExportIcon from "../icons/share.svg";
 import ReturnIcon from "../icons/return.svg";
 import CopyIcon from "../icons/copy.svg";
 import DownloadIcon from "../icons/download.svg";
@@ -11,6 +11,8 @@ import LoadingIcon from "../icons/three-dots.svg";
 import BotIcon from "../icons/bot.svg";
 import AddIcon from "../icons/add.svg";
 import DeleteIcon from "../icons/delete.svg";
+import MaxIcon from "../icons/max.svg";
+import MinIcon from "../icons/min.svg";
 
 import {
   Message,
@@ -19,6 +21,7 @@ import {
   BOT_HELLO,
   ROLES,
   createMessage,
+  useAccessStore,
 } from "../store";
 
 import {
@@ -485,11 +488,17 @@ export function Chat(props: {
 
   const context: RenderMessage[] = session.context.slice();
 
+  const accessStore = useAccessStore();
+
   if (
     context.length === 0 &&
     session.messages.at(0)?.content !== BOT_HELLO.content
   ) {
-    context.push(BOT_HELLO);
+    const copiedHello = Object.assign({}, BOT_HELLO);
+    if (!accessStore.isAuthorized()) {
+      copiedHello.content = Locale.Error.Unauthorized;
+    }
+    context.push(copiedHello);
   }
 
   // preview messages
@@ -584,6 +593,17 @@ export function Chat(props: {
               }}
             />
           </div>
+          <div className={styles["window-action-button"]}>
+            <IconButton
+              icon={chatStore.config.tightBorder ? <MinIcon /> : <MaxIcon />}
+              bordered
+              onClick={() => {
+                chatStore.updateConfig(
+                  (config) => (config.tightBorder = !config.tightBorder),
+                );
+              }}
+            />
+          </div>
         </div>
 
         <PromptToast

+ 27 - 4
app/components/home.module.scss

@@ -10,7 +10,7 @@
   background-color: var(--white);
   min-width: 600px;
   min-height: 480px;
-  max-width: 900px;
+  max-width: 1200px;
 
   display: flex;
   overflow: hidden;
@@ -48,6 +48,27 @@
   display: flex;
   flex-direction: column;
   box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
+  position: relative;
+  transition: width ease 0.1s;
+}
+
+.sidebar-drag {
+  $width: 10px;
+
+  position: absolute;
+  top: 0;
+  right: 0;
+  height: 100%;
+  width: $width;
+  background-color: var(--black);
+  cursor: ew-resize;
+  opacity: 0;
+  transition: all ease 0.3s;
+
+  &:hover,
+  &:active {
+    opacity: 0.2;
+  }
 }
 
 .window-content {
@@ -177,10 +198,11 @@
   margin-top: 8px;
 }
 
-.chat-item-count {
-}
-
+.chat-item-count,
 .chat-item-date {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
 .sidebar-tail {
@@ -436,6 +458,7 @@
 
 .export-content {
   white-space: break-spaces;
+  padding: 10px !important;
 }
 
 .loading-content {

+ 59 - 1
app/components/home.tsx

@@ -2,7 +2,13 @@
 
 require("../polyfill");
 
-import { useState, useEffect } from "react";
+import {
+  useState,
+  useEffect,
+  useRef,
+  useCallback,
+  MouseEventHandler,
+} from "react";
 
 import { IconButton } from "./button";
 import styles from "./home.module.scss";
@@ -24,6 +30,7 @@ import { Chat } from "./chat";
 import dynamic from "next/dynamic";
 import { REPO_URL } from "../constant";
 import { ErrorBoundary } from "./error";
+import { useDebounce } from "use-debounce";
 
 export function Loading(props: { noLogo?: boolean }) {
   return (
@@ -75,6 +82,49 @@ function useSwitchTheme() {
   }, [config.theme]);
 }
 
+function useDragSideBar() {
+  const limit = (x: number) => Math.min(500, Math.max(220, x));
+
+  const chatStore = useChatStore();
+  const startX = useRef(0);
+  const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300);
+  const lastUpdateTime = useRef(Date.now());
+
+  const handleMouseMove = useRef((e: MouseEvent) => {
+    if (Date.now() < lastUpdateTime.current + 100) {
+      return;
+    }
+    lastUpdateTime.current = Date.now();
+    const d = e.clientX - startX.current;
+    const nextWidth = limit(startDragWidth.current + d);
+    chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth));
+  });
+
+  const handleMouseUp = useRef(() => {
+    startDragWidth.current = chatStore.config.sidebarWidth ?? 300;
+    window.removeEventListener("mousemove", handleMouseMove.current);
+    window.removeEventListener("mouseup", handleMouseUp.current);
+  });
+
+  const onDragMouseDown = (e: MouseEvent) => {
+    startX.current = e.clientX;
+
+    window.addEventListener("mousemove", handleMouseMove.current);
+    window.addEventListener("mouseup", handleMouseUp.current);
+  };
+
+  useEffect(() => {
+    document.documentElement.style.setProperty(
+      "--sidebar-width",
+      `${limit(chatStore.config.sidebarWidth ?? 300)}px`,
+    );
+  }, [chatStore.config.sidebarWidth]);
+
+  return {
+    onDragMouseDown,
+  };
+}
+
 const useHasHydrated = () => {
   const [hasHydrated, setHasHydrated] = useState<boolean>(false);
 
@@ -101,6 +151,9 @@ function _Home() {
   const [openSettings, setOpenSettings] = useState(false);
   const config = useChatStore((state) => state.config);
 
+  // drag side bar
+  const { onDragMouseDown } = useDragSideBar();
+
   useSwitchTheme();
 
   if (loading) {
@@ -174,6 +227,11 @@ function _Home() {
             />
           </div>
         </div>
+
+        <div
+          className={styles["sidebar-drag"]}
+          onMouseDown={(e) => onDragMouseDown(e as any)}
+        ></div>
       </div>
 
       <div className={styles["window-content"]}>

+ 6 - 1
app/components/settings.module.scss

@@ -19,11 +19,16 @@
   cursor: pointer;
 }
 
-.password-input {
+.password-input-container {
+  max-width: 50%;
   display: flex;
   justify-content: flex-end;
 
   .password-eye {
     margin-right: 4px;
   }
+
+  .password-input {
+    min-width: 80%;
+  }
 }

+ 39 - 34
app/components/settings.tsx

@@ -60,13 +60,17 @@ function PasswordInput(props: HTMLProps<HTMLInputElement>) {
   }
 
   return (
-    <div className={styles["password-input"]}>
+    <div className={styles["password-input-container"]}>
       <IconButton
         icon={visible ? <EyeIcon /> : <EyeOffIcon />}
         onClick={changeVisibility}
         className={styles["password-eye"]}
       />
-      <input {...props} type={visible ? "text" : "password"} />
+      <input
+        {...props}
+        type={visible ? "text" : "password"}
+        className={styles["password-input"]}
+      />
     </div>
   );
 }
@@ -120,8 +124,7 @@ export function Settings(props: { closeSettings: () => void }) {
   const builtinCount = SearchService.count.builtin;
   const customCount = promptStore.prompts.size ?? 0;
 
-  const showUsage = !!accessStore.token || !!accessStore.accessCode;
-
+  const showUsage = accessStore.isAuthorized();
   useEffect(() => {
     checkUpdate();
     showUsage && checkUsage();
@@ -342,37 +345,7 @@ export function Settings(props: { closeSettings: () => void }) {
             ></input>
           </SettingItem>
         </List>
-        <List>
-          <SettingItem
-            title={Locale.Settings.Prompt.Disable.Title}
-            subTitle={Locale.Settings.Prompt.Disable.SubTitle}
-          >
-            <input
-              type="checkbox"
-              checked={config.disablePromptHint}
-              onChange={(e) =>
-                updateConfig(
-                  (config) =>
-                    (config.disablePromptHint = e.currentTarget.checked),
-                )
-              }
-            ></input>
-          </SettingItem>
 
-          <SettingItem
-            title={Locale.Settings.Prompt.List}
-            subTitle={Locale.Settings.Prompt.ListCount(
-              builtinCount,
-              customCount,
-            )}
-          >
-            <IconButton
-              icon={<EditIcon />}
-              text={Locale.Settings.Prompt.Edit}
-              onClick={() => showToast(Locale.WIP)}
-            />
-          </SettingItem>
-        </List>
         <List>
           {enabledAccessControl ? (
             <SettingItem
@@ -469,6 +442,38 @@ export function Settings(props: { closeSettings: () => void }) {
           </SettingItem>
         </List>
 
+        <List>
+          <SettingItem
+            title={Locale.Settings.Prompt.Disable.Title}
+            subTitle={Locale.Settings.Prompt.Disable.SubTitle}
+          >
+            <input
+              type="checkbox"
+              checked={config.disablePromptHint}
+              onChange={(e) =>
+                updateConfig(
+                  (config) =>
+                    (config.disablePromptHint = e.currentTarget.checked),
+                )
+              }
+            ></input>
+          </SettingItem>
+
+          <SettingItem
+            title={Locale.Settings.Prompt.List}
+            subTitle={Locale.Settings.Prompt.ListCount(
+              builtinCount,
+              customCount,
+            )}
+          >
+            <IconButton
+              icon={<EditIcon />}
+              text={Locale.Settings.Prompt.Edit}
+              onClick={() => showToast(Locale.WIP)}
+            />
+          </SettingItem>
+        </List>
+
         <List>
           <SettingItem title={Locale.Settings.Model}>
             <select

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

@@ -127,6 +127,7 @@
   width: 100vw;
   display: flex;
   justify-content: center;
+  pointer-events: none;
 
   .toast-content {
     max-width: 80vw;
@@ -141,6 +142,7 @@
     margin-bottom: 20px;
     display: flex;
     align-items: center;
+    pointer-events: all;
 
     .toast-action {
       padding-left: 20px;

+ 41 - 0
app/icons/max.svg

@@ -0,0 +1,41 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
+  height="16" viewBox="0 0 16 16" fill="none">
+  <defs>
+    <rect id="path_0" x="0" y="0" width="16" height="16" />
+  </defs>
+  <g opacity="1" transform="translate(0 0)  rotate(0 8 8)">
+    <mask id="bg-mask-0" fill="white">
+      <use xlink:href="#path_0"></use>
+    </mask>
+    <g mask="url(#bg-mask-0)">
+      <path id="路径 1"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2 2)  rotate(0 1.6666666666666665 1.6499166666666665)"
+        d="M0,0L3.33,3.3 " />
+      <path id="路径 2"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2 10.666666666666666)  rotate(0 1.6666666666666665 1.6499166666666671)"
+        d="M0,3.3L3.33,0 " />
+      <path id="路径 3"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(10.700199999999999 10.666666666666666)  rotate(0 1.6499166666666671 1.6499166666666671)"
+        d="M3.3,3.3L0,0 " />
+      <path id="路径 4"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(10.666666666666666 2)  rotate(0 1.6499166666666671 1.6499166666666665)"
+        d="M3.3,0L0,3.3 " />
+      <path id="路径 5"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(11 2)  rotate(0 1.5 1.5)" d="M0,0L3,0L3,3 " />
+      <path id="路径 6"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(11 11)  rotate(0 1.5 1.5)" d="M3,0L3,3L0,3 " />
+      <path id="路径 7"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2 11)  rotate(0 1.5 1.5)" d="M3,3L0,3L0,0 " />
+      <path id="路径 8"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2 2)  rotate(0 1.5 1.5)" d="M0,3L0,0L3,0 " />
+    </g>
+  </g>
+</svg>

+ 45 - 0
app/icons/min.svg

@@ -0,0 +1,45 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
+  height="16" viewBox="0 0 16 16" fill="none">
+  <defs>
+    <rect id="path_0" x="0" y="0" width="16" height="16" />
+  </defs>
+  <g opacity="1" transform="translate(0 0)  rotate(0 8 8)">
+    <mask id="bg-mask-0" fill="white">
+      <use xlink:href="#path_0"></use>
+    </mask>
+    <g mask="url(#bg-mask-0)">
+      <path id="路径 1"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2 2)  rotate(0 1.6666666666666665 1.6499166666666665)"
+        d="M0,0L3.33,3.3 " />
+      <path id="路径 2"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2 10.666666666666666)  rotate(0 1.6666666666666665 1.6499166666666671)"
+        d="M0,3.3L3.33,0 " />
+      <path id="路径 3"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(10.700199999999999 10.666666666666666)  rotate(0 1.6499166666666671 1.6499166666666671)"
+        d="M3.3,3.3L0,0 " />
+      <path id="路径 4"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(10.666666666666666 2)  rotate(0 1.6499166666666671 1.6499166666666665)"
+        d="M3.3,0L0,3.3 " />
+      <path id="路径 5"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(10.666666666666666 2.333333333333333)  rotate(0 1.5 1.5)"
+        d="M0,0L0,3L3,3 " />
+      <path id="路径 6"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2.333333333333333 2.333333333333333)  rotate(0 1.5 1.5)"
+        d="M3,0L3,3L0,3 " />
+      <path id="路径 7"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2.333333333333333 10.666666666666666)  rotate(0 1.5 1.5)"
+        d="M3,3L3,0L0,0 " />
+      <path id="路径 8"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(10.666666666666666 10.666666666666666)  rotate(0 1.4832500000000004 1.5)"
+        d="M0,3L0,0L2.97,0 " />
+    </g>
+  </g>
+</svg>

+ 17 - 0
app/icons/share.svg

@@ -0,0 +1,17 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
+  height="16" viewBox="0 0 16 16" fill="none">
+  <defs>
+    <rect id="path_0" x="0" y="0" width="16" height="16" />
+  </defs>
+  <g opacity="1" transform="translate(0 0)  rotate(0 8 8)">
+    <mask id="bg-mask-0" fill="white">
+      <use xlink:href="#path_0"></use>
+    </mask>
+    <g mask="url(#bg-mask-0)">
+      <path id="路径 1"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2 1.3333333333333333)  rotate(0 6.333333333333333 6.5)"
+        d="M6.67,3.67C1.67,3.67 0,7.33 0,13C0,13 2,8 6.67,8L6.67,11.67L12.67,6L6.67,0L6.67,3.67Z " />
+    </g>
+  </g>
+</svg>

+ 2 - 2
app/locales/cn.ts

@@ -3,7 +3,7 @@ import { SubmitKey } from "../store/app";
 const cn = {
   WIP: "该功能仍在开发中……",
   Error: {
-    Unauthorized: "现在是未授权状态,请在设置页输入访问密码。",
+    Unauthorized: "现在是未授权状态,请点击左下角设置按钮输入访问密码。",
   },
   ChatItem: {
     ChatItemCount: (count: number) => `${count} 条对话`,
@@ -90,7 +90,7 @@ const cn = {
     },
     SendKey: "发送键",
     Theme: "主题",
-    TightBorder: "紧凑边框",
+    TightBorder: "无边框模式",
     SendPreviewBubble: "发送预览气泡",
     Prompt: {
       Disable: {

+ 6 - 2
app/store/access.ts

@@ -9,6 +9,7 @@ export interface AccessControlStore {
   updateToken: (_: string) => void;
   updateCode: (_: string) => void;
   enabledAccessControl: () => boolean;
+  isAuthorized: () => boolean;
 }
 
 export const ACCESS_KEY = "access-control";
@@ -27,10 +28,13 @@ export const useAccessStore = create<AccessControlStore>()(
       updateToken(token: string) {
         set((state) => ({ token }));
       },
+      isAuthorized() {
+        return !!get().token || !!get().accessCode;
+      },
     }),
     {
       name: ACCESS_KEY,
       version: 1,
-    }
-  )
+    },
+  ),
 );

+ 22 - 14
app/store/app.ts

@@ -53,6 +53,7 @@ export interface ChatConfig {
   theme: Theme;
   tightBorder: boolean;
   sendPreviewBubble: boolean;
+  sidebarWidth: number;
 
   disablePromptHint: boolean;
 
@@ -141,6 +142,7 @@ const DEFAULT_CONFIG: ChatConfig = {
   theme: Theme.Auto as Theme,
   tightBorder: false,
   sendPreviewBubble: true,
+  sidebarWidth: 300,
 
   disablePromptHint: false,
 
@@ -205,7 +207,7 @@ interface ChatStore {
   moveSession: (from: number, to: number) => void;
   selectSession: (index: number) => void;
   newSession: () => void;
-  deleteSession: () => void;
+  deleteSession: (index?: number) => void;
   currentSession: () => ChatSession;
   onNewMessage: (message: Message) => void;
   onUserInput: (content: string) => Promise<void>;
@@ -326,24 +328,30 @@ export const useChatStore = create<ChatStore>()(
         }));
       },
 
-      deleteSession() {
+      deleteSession(i?: number) {
         const deletedSession = get().currentSession();
-        const index = get().currentSessionIndex;
+        const index = i ?? get().currentSessionIndex;
         const isLastSession = get().sessions.length === 1;
         if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
           get().removeSession(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))),
-              }));
+
+          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,
+          );
         }
       },