Browse Source

refactor: split homt.tsx components

Yifei Zhang 1 year ago
parent
commit
6c1862797b
4 changed files with 572 additions and 531 deletions
  1. 69 0
      app/components/chat-list.tsx
  2. 499 0
      app/components/chat.tsx
  3. 3 530
      app/components/home.tsx
  4. 1 1
      app/components/settings.tsx

+ 69 - 0
app/components/chat-list.tsx

@@ -0,0 +1,69 @@
+import { useState, useRef, useEffect, useLayoutEffect } from "react";
+import DeleteIcon from "../icons/delete.svg";
+import styles from "./home.module.scss";
+
+import {
+  Message,
+  SubmitKey,
+  useChatStore,
+  ChatSession,
+  BOT_HELLO,
+} from "../store";
+
+import Locale from "../locales";
+
+export function ChatItem(props: {
+  onClick?: () => void;
+  onDelete?: () => void;
+  title: string;
+  count: number;
+  time: string;
+  selected: boolean;
+}) {
+  return (
+    <div
+      className={`${styles["chat-item"]} ${
+        props.selected && styles["chat-item-selected"]
+      }`}
+      onClick={props.onClick}
+    >
+      <div className={styles["chat-item-title"]}>{props.title}</div>
+      <div className={styles["chat-item-info"]}>
+        <div className={styles["chat-item-count"]}>
+          {Locale.ChatItem.ChatItemCount(props.count)}
+        </div>
+        <div className={styles["chat-item-date"]}>{props.time}</div>
+      </div>
+      <div className={styles["chat-item-delete"]} onClick={props.onDelete}>
+        <DeleteIcon />
+      </div>
+    </div>
+  );
+}
+
+export function ChatList() {
+  const [sessions, selectedIndex, selectSession, removeSession] = useChatStore(
+    (state) => [
+      state.sessions,
+      state.currentSessionIndex,
+      state.selectSession,
+      state.removeSession,
+    ],
+  );
+
+  return (
+    <div className={styles["chat-list"]}>
+      {sessions.map((item, i) => (
+        <ChatItem
+          title={item.topic}
+          time={item.lastUpdate}
+          count={item.messages.length}
+          key={i}
+          selected={i === selectedIndex}
+          onClick={() => selectSession(i)}
+          onDelete={() => confirm(Locale.Home.DeleteChat) && removeSession(i)}
+        />
+      ))}
+    </div>
+  );
+}

+ 499 - 0
app/components/chat.tsx

@@ -0,0 +1,499 @@
+import { useDebouncedCallback } from "use-debounce";
+import { 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 MenuIcon from "../icons/menu.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 {
+  Message,
+  SubmitKey,
+  useChatStore,
+  ChatSession,
+  BOT_HELLO,
+} from "../store";
+
+import {
+  copyToClipboard,
+  downloadAs,
+  isMobileScreen,
+  selectOrCopy,
+} from "../utils";
+
+import dynamic from "next/dynamic";
+
+import { ControllerPool } from "../requests";
+import { Prompt, usePromptStore } from "../store/prompt";
+import Locale from "../locales";
+
+import { IconButton } from "./button";
+import styles from "./home.module.scss";
+
+import { showModal, showToast } from "./ui-lib";
+
+const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
+  loading: () => <LoadingIcon />,
+});
+
+const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
+  loading: () => <LoadingIcon />,
+});
+
+export function Avatar(props: { role: Message["role"] }) {
+  const config = useChatStore((state) => state.config);
+
+  if (props.role === "assistant") {
+    return <BotIcon className={styles["user-avtar"]} />;
+  }
+
+  return (
+    <div className={styles["user-avtar"]}>
+      <Emoji unified={config.avatar} size={18} />
+    </div>
+  );
+}
+
+function exportMessages(messages: Message[], topic: string) {
+  const mdText =
+    `# ${topic}\n\n` +
+    messages
+      .map((m) => {
+        return m.role === "user" ? `## ${m.content}` : m.content.trim();
+      })
+      .join("\n\n");
+  const filename = `${topic}.md`;
+
+  showModal({
+    title: Locale.Export.Title,
+    children: (
+      <div className="markdown-body">
+        <pre className={styles["export-content"]}>{mdText}</pre>
+      </div>
+    ),
+    actions: [
+      <IconButton
+        key="copy"
+        icon={<CopyIcon />}
+        bordered
+        text={Locale.Export.Copy}
+        onClick={() => copyToClipboard(mdText)}
+      />,
+      <IconButton
+        key="download"
+        icon={<DownloadIcon />}
+        bordered
+        text={Locale.Export.Download}
+        onClick={() => downloadAs(mdText, filename)}
+      />,
+    ],
+  });
+}
+
+function showMemoryPrompt(session: ChatSession) {
+  showModal({
+    title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`,
+    children: (
+      <div className="markdown-body">
+        <pre className={styles["export-content"]}>
+          {session.memoryPrompt || Locale.Memory.EmptyContent}
+        </pre>
+      </div>
+    ),
+    actions: [
+      <IconButton
+        key="copy"
+        icon={<CopyIcon />}
+        bordered
+        text={Locale.Memory.Copy}
+        onClick={() => copyToClipboard(session.memoryPrompt)}
+      />,
+    ],
+  });
+}
+
+function useSubmitHandler() {
+  const config = useChatStore((state) => state.config);
+  const submitKey = config.submitKey;
+
+  const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+    if (e.key !== "Enter") return false;
+    if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
+    return (
+      (config.submitKey === SubmitKey.AltEnter && e.altKey) ||
+      (config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
+      (config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
+      (config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
+      (config.submitKey === SubmitKey.Enter &&
+        !e.altKey &&
+        !e.ctrlKey &&
+        !e.shiftKey &&
+        !e.metaKey)
+    );
+  };
+
+  return {
+    submitKey,
+    shouldSubmit,
+  };
+}
+
+export function PromptHints(props: {
+  prompts: Prompt[];
+  onPromptSelect: (prompt: Prompt) => void;
+}) {
+  if (props.prompts.length === 0) return null;
+
+  return (
+    <div className={styles["prompt-hints"]}>
+      {props.prompts.map((prompt, i) => (
+        <div
+          className={styles["prompt-hint"]}
+          key={prompt.title + i.toString()}
+          onClick={() => props.onPromptSelect(prompt)}
+        >
+          <div className={styles["hint-title"]}>{prompt.title}</div>
+          <div className={styles["hint-content"]}>{prompt.content}</div>
+        </div>
+      ))}
+    </div>
+  );
+}
+
+export function Chat(props: {
+  showSideBar?: () => void;
+  sideBarShowing?: boolean;
+}) {
+  type RenderMessage = Message & { preview?: boolean };
+
+  const chatStore = useChatStore();
+  const [session, sessionIndex] = useChatStore((state) => [
+    state.currentSession(),
+    state.currentSessionIndex,
+  ]);
+  const fontSize = useChatStore((state) => state.config.fontSize);
+
+  const inputRef = useRef<HTMLTextAreaElement>(null);
+  const [userInput, setUserInput] = useState("");
+  const [isLoading, setIsLoading] = useState(false);
+  const { submitKey, shouldSubmit } = useSubmitHandler();
+
+  // prompt hints
+  const promptStore = usePromptStore();
+  const [promptHints, setPromptHints] = useState<Prompt[]>([]);
+  const onSearch = useDebouncedCallback(
+    (text: string) => {
+      setPromptHints(promptStore.search(text));
+    },
+    100,
+    { leading: true, trailing: true },
+  );
+
+  const onPromptSelect = (prompt: Prompt) => {
+    setUserInput(prompt.content);
+    setPromptHints([]);
+    inputRef.current?.focus();
+  };
+
+  const scrollInput = () => {
+    const dom = inputRef.current;
+    if (!dom) return;
+    const paddingBottomNum: number = parseInt(
+      window.getComputedStyle(dom).paddingBottom,
+      10,
+    );
+    dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
+  };
+
+  // only search prompts when user input is short
+  const SEARCH_TEXT_LIMIT = 30;
+  const onInput = (text: string) => {
+    scrollInput();
+    setUserInput(text);
+    const n = text.trim().length;
+
+    // clear search results
+    if (n === 0) {
+      setPromptHints([]);
+    } else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
+      // check if need to trigger auto completion
+      if (text.startsWith("/") && text.length > 1) {
+        onSearch(text.slice(1));
+      }
+    }
+  };
+
+  // submit user input
+  const onUserSubmit = () => {
+    if (userInput.length <= 0) return;
+    setIsLoading(true);
+    chatStore.onUserInput(userInput).then(() => setIsLoading(false));
+    setUserInput("");
+    setPromptHints([]);
+    inputRef.current?.focus();
+  };
+
+  // stop response
+  const onUserStop = (messageIndex: number) => {
+    console.log(ControllerPool, sessionIndex, messageIndex);
+    ControllerPool.stop(sessionIndex, messageIndex);
+  };
+
+  // check if should send message
+  const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+    if (shouldSubmit(e)) {
+      onUserSubmit();
+      e.preventDefault();
+    }
+  };
+  const onRightClick = (e: any, message: Message) => {
+    // auto fill user input
+    if (message.role === "user") {
+      setUserInput(message.content);
+    }
+
+    // copy to clipboard
+    if (selectOrCopy(e.currentTarget, message.content)) {
+      e.preventDefault();
+    }
+  };
+
+  const onResend = (botIndex: number) => {
+    // find last user input message and resend
+    for (let i = botIndex; i >= 0; i -= 1) {
+      if (messages[i].role === "user") {
+        setIsLoading(true);
+        chatStore
+          .onUserInput(messages[i].content)
+          .then(() => setIsLoading(false));
+        inputRef.current?.focus();
+        return;
+      }
+    }
+  };
+
+  // for auto-scroll
+  const latestMessageRef = useRef<HTMLDivElement>(null);
+  const [autoScroll, setAutoScroll] = useState(true);
+
+  const config = useChatStore((state) => state.config);
+
+  // preview messages
+  const messages = (session.messages as RenderMessage[])
+    .concat(
+      isLoading
+        ? [
+            {
+              role: "assistant",
+              content: "……",
+              date: new Date().toLocaleString(),
+              preview: true,
+            },
+          ]
+        : [],
+    )
+    .concat(
+      userInput.length > 0 && config.sendPreviewBubble
+        ? [
+            {
+              role: "user",
+              content: userInput,
+              date: new Date().toLocaleString(),
+              preview: false,
+            },
+          ]
+        : [],
+    );
+
+  // auto scroll
+  useLayoutEffect(() => {
+    setTimeout(() => {
+      const dom = latestMessageRef.current;
+      const inputDom = inputRef.current;
+
+      // only scroll when input overlaped message body
+      let shouldScroll = true;
+      if (dom && inputDom) {
+        const domRect = dom.getBoundingClientRect();
+        const inputRect = inputDom.getBoundingClientRect();
+        shouldScroll = domRect.top > inputRect.top;
+      }
+
+      if (dom && autoScroll && shouldScroll) {
+        dom.scrollIntoView({
+          block: "end",
+        });
+      }
+    }, 500);
+  });
+
+  return (
+    <div className={styles.chat} key={session.id}>
+      <div className={styles["window-header"]}>
+        <div
+          className={styles["window-header-title"]}
+          onClick={props?.showSideBar}
+        >
+          <div
+            className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
+            onClick={() => {
+              const newTopic = prompt(Locale.Chat.Rename, session.topic);
+              if (newTopic && newTopic !== session.topic) {
+                chatStore.updateCurrentSession(
+                  (session) => (session.topic = newTopic!),
+                );
+              }
+            }}
+          >
+            {session.topic}
+          </div>
+          <div className={styles["window-header-sub-title"]}>
+            {Locale.Chat.SubTitle(session.messages.length)}
+          </div>
+        </div>
+        <div className={styles["window-actions"]}>
+          <div className={styles["window-action-button"] + " " + styles.mobile}>
+            <IconButton
+              icon={<MenuIcon />}
+              bordered
+              title={Locale.Chat.Actions.ChatList}
+              onClick={props?.showSideBar}
+            />
+          </div>
+          <div className={styles["window-action-button"]}>
+            <IconButton
+              icon={<BrainIcon />}
+              bordered
+              title={Locale.Chat.Actions.CompressedHistory}
+              onClick={() => {
+                showMemoryPrompt(session);
+              }}
+            />
+          </div>
+          <div className={styles["window-action-button"]}>
+            <IconButton
+              icon={<ExportIcon />}
+              bordered
+              title={Locale.Chat.Actions.Export}
+              onClick={() => {
+                exportMessages(session.messages, session.topic);
+              }}
+            />
+          </div>
+        </div>
+      </div>
+
+      <div className={styles["chat-body"]}>
+        {messages.map((message, i) => {
+          const isUser = message.role === "user";
+
+          return (
+            <div
+              key={i}
+              className={
+                isUser ? styles["chat-message-user"] : styles["chat-message"]
+              }
+            >
+              <div className={styles["chat-message-container"]}>
+                <div className={styles["chat-message-avatar"]}>
+                  <Avatar role={message.role} />
+                </div>
+                {(message.preview || message.streaming) && (
+                  <div className={styles["chat-message-status"]}>
+                    {Locale.Chat.Typing}
+                  </div>
+                )}
+                <div className={styles["chat-message-item"]}>
+                  {!isUser &&
+                    !(message.preview || message.content.length === 0) && (
+                      <div className={styles["chat-message-top-actions"]}>
+                        {message.streaming ? (
+                          <div
+                            className={styles["chat-message-top-action"]}
+                            onClick={() => onUserStop(i)}
+                          >
+                            {Locale.Chat.Actions.Stop}
+                          </div>
+                        ) : (
+                          <div
+                            className={styles["chat-message-top-action"]}
+                            onClick={() => onResend(i)}
+                          >
+                            {Locale.Chat.Actions.Retry}
+                          </div>
+                        )}
+
+                        <div
+                          className={styles["chat-message-top-action"]}
+                          onClick={() => copyToClipboard(message.content)}
+                        >
+                          {Locale.Chat.Actions.Copy}
+                        </div>
+                      </div>
+                    )}
+                  {(message.preview || message.content.length === 0) &&
+                  !isUser ? (
+                    <LoadingIcon />
+                  ) : (
+                    <div
+                      className="markdown-body"
+                      style={{ fontSize: `${fontSize}px` }}
+                      onContextMenu={(e) => onRightClick(e, message)}
+                      onDoubleClickCapture={() => {
+                        if (!isMobileScreen()) return;
+                        setUserInput(message.content);
+                      }}
+                    >
+                      <Markdown content={message.content} />
+                    </div>
+                  )}
+                </div>
+                {!isUser && !message.preview && (
+                  <div className={styles["chat-message-actions"]}>
+                    <div className={styles["chat-message-action-date"]}>
+                      {message.date.toLocaleString()}
+                    </div>
+                  </div>
+                )}
+              </div>
+            </div>
+          );
+        })}
+        <div ref={latestMessageRef} style={{ opacity: 0, height: "1px" }}>
+          -
+        </div>
+      </div>
+
+      <div className={styles["chat-input-panel"]}>
+        <PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
+        <div className={styles["chat-input-panel-inner"]}>
+          <textarea
+            ref={inputRef}
+            className={styles["chat-input"]}
+            placeholder={Locale.Chat.Input(submitKey)}
+            rows={4}
+            onInput={(e) => onInput(e.currentTarget.value)}
+            value={userInput}
+            onKeyDown={onInputKeyDown}
+            onFocus={() => setAutoScroll(true)}
+            onBlur={() => {
+              setAutoScroll(false);
+              setTimeout(() => setPromptHints([]), 500);
+            }}
+            autoFocus={!props?.sideBarShowing}
+          />
+          <IconButton
+            icon={<SendWhiteIcon />}
+            text={Locale.Chat.Send}
+            className={styles["chat-input-send"] + " no-dark"}
+            onClick={onUserSubmit}
+          />
+        </div>
+      </div>
+    </div>
+  );
+}

+ 3 - 530
app/components/home.tsx

@@ -1,7 +1,6 @@
 "use client";
 
 import { useState, useRef, useEffect, useLayoutEffect } from "react";
-import { useDebouncedCallback } from "use-debounce";
 
 import { IconButton } from "./button";
 import styles from "./home.module.scss";
@@ -9,17 +8,11 @@ import styles from "./home.module.scss";
 import SettingsIcon from "../icons/settings.svg";
 import GithubIcon from "../icons/github.svg";
 import ChatGptIcon from "../icons/chatgpt.svg";
-import SendWhiteIcon from "../icons/send-white.svg";
-import BrainIcon from "../icons/brain.svg";
-import ExportIcon from "../icons/export.svg";
+
 import BotIcon from "../icons/bot.svg";
 import AddIcon from "../icons/add.svg";
-import DeleteIcon from "../icons/delete.svg";
 import LoadingIcon from "../icons/three-dots.svg";
-import MenuIcon from "../icons/menu.svg";
 import CloseIcon from "../icons/close.svg";
-import CopyIcon from "../icons/copy.svg";
-import DownloadIcon from "../icons/download.svg";
 
 import {
   Message,
@@ -28,20 +21,18 @@ import {
   ChatSession,
   BOT_HELLO,
 } from "../store";
-import { showModal, showToast } from "./ui-lib";
 import {
   copyToClipboard,
   downloadAs,
-  isIOS,
   isMobileScreen,
   selectOrCopy,
 } from "../utils";
 import Locale from "../locales";
+import { ChatList } from "./chat-list";
+import { Chat } from "./chat";
 
 import dynamic from "next/dynamic";
 import { REPO_URL } from "../constant";
-import { ControllerPool } from "../requests";
-import { Prompt, usePromptStore } from "../store/prompt";
 
 export function Loading(props: { noLogo?: boolean }) {
   return (
@@ -52,470 +43,10 @@ export function Loading(props: { noLogo?: boolean }) {
   );
 }
 
-const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
-  loading: () => <LoadingIcon />,
-});
-
 const Settings = dynamic(async () => (await import("./settings")).Settings, {
   loading: () => <Loading noLogo />,
 });
 
-const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
-  loading: () => <LoadingIcon />,
-});
-
-export function Avatar(props: { role: Message["role"] }) {
-  const config = useChatStore((state) => state.config);
-
-  if (props.role === "assistant") {
-    return <BotIcon className={styles["user-avtar"]} />;
-  }
-
-  return (
-    <div className={styles["user-avtar"]}>
-      <Emoji unified={config.avatar} size={18} />
-    </div>
-  );
-}
-
-export function ChatItem(props: {
-  onClick?: () => void;
-  onDelete?: () => void;
-  title: string;
-  count: number;
-  time: string;
-  selected: boolean;
-}) {
-  return (
-    <div
-      className={`${styles["chat-item"]} ${
-        props.selected && styles["chat-item-selected"]
-      }`}
-      onClick={props.onClick}
-    >
-      <div className={styles["chat-item-title"]}>{props.title}</div>
-      <div className={styles["chat-item-info"]}>
-        <div className={styles["chat-item-count"]}>
-          {Locale.ChatItem.ChatItemCount(props.count)}
-        </div>
-        <div className={styles["chat-item-date"]}>{props.time}</div>
-      </div>
-      <div className={styles["chat-item-delete"]} onClick={props.onDelete}>
-        <DeleteIcon />
-      </div>
-    </div>
-  );
-}
-
-export function ChatList() {
-  const [sessions, selectedIndex, selectSession, removeSession] = useChatStore(
-    (state) => [
-      state.sessions,
-      state.currentSessionIndex,
-      state.selectSession,
-      state.removeSession,
-    ],
-  );
-
-  return (
-    <div className={styles["chat-list"]}>
-      {sessions.map((item, i) => (
-        <ChatItem
-          title={item.topic}
-          time={item.lastUpdate}
-          count={item.messages.length}
-          key={i}
-          selected={i === selectedIndex}
-          onClick={() => selectSession(i)}
-          onDelete={() => confirm(Locale.Home.DeleteChat) && removeSession(i)}
-        />
-      ))}
-    </div>
-  );
-}
-
-function useSubmitHandler() {
-  const config = useChatStore((state) => state.config);
-  const submitKey = config.submitKey;
-
-  const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
-    if (e.key !== "Enter") return false;
-    if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
-    return (
-      (config.submitKey === SubmitKey.AltEnter && e.altKey) ||
-      (config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
-      (config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
-      (config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
-      (config.submitKey === SubmitKey.Enter &&
-        !e.altKey &&
-        !e.ctrlKey &&
-        !e.shiftKey &&
-        !e.metaKey)
-    );
-  };
-
-  return {
-    submitKey,
-    shouldSubmit,
-  };
-}
-
-export function PromptHints(props: {
-  prompts: Prompt[];
-  onPromptSelect: (prompt: Prompt) => void;
-}) {
-  if (props.prompts.length === 0) return null;
-
-  return (
-    <div className={styles["prompt-hints"]}>
-      {props.prompts.map((prompt, i) => (
-        <div
-          className={styles["prompt-hint"]}
-          key={prompt.title + i.toString()}
-          onClick={() => props.onPromptSelect(prompt)}
-        >
-          <div className={styles["hint-title"]}>{prompt.title}</div>
-          <div className={styles["hint-content"]}>{prompt.content}</div>
-        </div>
-      ))}
-    </div>
-  );
-}
-
-export function Chat(props: {
-  showSideBar?: () => void;
-  sideBarShowing?: boolean;
-}) {
-  type RenderMessage = Message & { preview?: boolean };
-
-  const chatStore = useChatStore();
-  const [session, sessionIndex] = useChatStore((state) => [
-    state.currentSession(),
-    state.currentSessionIndex,
-  ]);
-  const fontSize = useChatStore((state) => state.config.fontSize);
-
-  const inputRef = useRef<HTMLTextAreaElement>(null);
-  const [userInput, setUserInput] = useState("");
-  const [isLoading, setIsLoading] = useState(false);
-  const { submitKey, shouldSubmit } = useSubmitHandler();
-
-  // prompt hints
-  const promptStore = usePromptStore();
-  const [promptHints, setPromptHints] = useState<Prompt[]>([]);
-  const onSearch = useDebouncedCallback(
-    (text: string) => {
-      setPromptHints(promptStore.search(text));
-    },
-    100,
-    { leading: true, trailing: true },
-  );
-
-  const onPromptSelect = (prompt: Prompt) => {
-    setUserInput(prompt.content);
-    setPromptHints([]);
-    inputRef.current?.focus();
-  };
-
-  const scrollInput = () => {
-    const dom = inputRef.current;
-    if (!dom) return;
-    const paddingBottomNum: number = parseInt(
-      window.getComputedStyle(dom).paddingBottom,
-      10,
-    );
-    dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
-  };
-
-  // only search prompts when user input is short
-  const SEARCH_TEXT_LIMIT = 30;
-  const onInput = (text: string) => {
-    scrollInput();
-    setUserInput(text);
-    const n = text.trim().length;
-
-    // clear search results
-    if (n === 0) {
-      setPromptHints([]);
-    } else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
-      // check if need to trigger auto completion
-      if (text.startsWith("/") && text.length > 1) {
-        onSearch(text.slice(1));
-      }
-    }
-  };
-
-  // submit user input
-  const onUserSubmit = () => {
-    if (userInput.length <= 0) return;
-    setIsLoading(true);
-    chatStore.onUserInput(userInput).then(() => setIsLoading(false));
-    setUserInput("");
-    setPromptHints([]);
-    inputRef.current?.focus();
-  };
-
-  // stop response
-  const onUserStop = (messageIndex: number) => {
-    console.log(ControllerPool, sessionIndex, messageIndex);
-    ControllerPool.stop(sessionIndex, messageIndex);
-  };
-
-  // check if should send message
-  const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
-    if (shouldSubmit(e)) {
-      onUserSubmit();
-      e.preventDefault();
-    }
-  };
-  const onRightClick = (e: any, message: Message) => {
-    // auto fill user input
-    if (message.role === "user") {
-      setUserInput(message.content);
-    }
-
-    // copy to clipboard
-    if (selectOrCopy(e.currentTarget, message.content)) {
-      e.preventDefault();
-    }
-  };
-
-  const onResend = (botIndex: number) => {
-    // find last user input message and resend
-    for (let i = botIndex; i >= 0; i -= 1) {
-      if (messages[i].role === "user") {
-        setIsLoading(true);
-        chatStore
-          .onUserInput(messages[i].content)
-          .then(() => setIsLoading(false));
-        inputRef.current?.focus();
-        return;
-      }
-    }
-  };
-
-  // for auto-scroll
-  const latestMessageRef = useRef<HTMLDivElement>(null);
-  const [autoScroll, setAutoScroll] = useState(true);
-
-  const config = useChatStore((state) => state.config);
-
-  // preview messages
-  const messages = (session.messages as RenderMessage[])
-    .concat(
-      isLoading
-        ? [
-            {
-              role: "assistant",
-              content: "……",
-              date: new Date().toLocaleString(),
-              preview: true,
-            },
-          ]
-        : [],
-    )
-    .concat(
-      userInput.length > 0 && config.sendPreviewBubble
-        ? [
-            {
-              role: "user",
-              content: userInput,
-              date: new Date().toLocaleString(),
-              preview: false,
-            },
-          ]
-        : [],
-    );
-
-  // auto scroll
-  useLayoutEffect(() => {
-    setTimeout(() => {
-      const dom = latestMessageRef.current;
-      const inputDom = inputRef.current;
-
-      // only scroll when input overlaped message body
-      let shouldScroll = true;
-      if (dom && inputDom) {
-        const domRect = dom.getBoundingClientRect();
-        const inputRect = inputDom.getBoundingClientRect();
-        shouldScroll = domRect.top > inputRect.top;
-      }
-
-      if (dom && autoScroll && shouldScroll) {
-        dom.scrollIntoView({
-          block: "end",
-        });
-      }
-    }, 500);
-  });
-
-  return (
-    <div className={styles.chat} key={session.id}>
-      <div className={styles["window-header"]}>
-        <div
-          className={styles["window-header-title"]}
-          onClick={props?.showSideBar}
-        >
-          <div
-            className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
-            onClick={() => {
-              const newTopic = prompt(Locale.Chat.Rename, session.topic);
-              if (newTopic && newTopic !== session.topic) {
-                chatStore.updateCurrentSession(
-                  (session) => (session.topic = newTopic!),
-                );
-              }
-            }}
-          >
-            {session.topic}
-          </div>
-          <div className={styles["window-header-sub-title"]}>
-            {Locale.Chat.SubTitle(session.messages.length)}
-          </div>
-        </div>
-        <div className={styles["window-actions"]}>
-          <div className={styles["window-action-button"] + " " + styles.mobile}>
-            <IconButton
-              icon={<MenuIcon />}
-              bordered
-              title={Locale.Chat.Actions.ChatList}
-              onClick={props?.showSideBar}
-            />
-          </div>
-          <div className={styles["window-action-button"]}>
-            <IconButton
-              icon={<BrainIcon />}
-              bordered
-              title={Locale.Chat.Actions.CompressedHistory}
-              onClick={() => {
-                showMemoryPrompt(session);
-              }}
-            />
-          </div>
-          <div className={styles["window-action-button"]}>
-            <IconButton
-              icon={<ExportIcon />}
-              bordered
-              title={Locale.Chat.Actions.Export}
-              onClick={() => {
-                exportMessages(session.messages, session.topic);
-              }}
-            />
-          </div>
-        </div>
-      </div>
-
-      <div className={styles["chat-body"]}>
-        {messages.map((message, i) => {
-          const isUser = message.role === "user";
-
-          return (
-            <div
-              key={i}
-              className={
-                isUser ? styles["chat-message-user"] : styles["chat-message"]
-              }
-            >
-              <div className={styles["chat-message-container"]}>
-                <div className={styles["chat-message-avatar"]}>
-                  <Avatar role={message.role} />
-                </div>
-                {(message.preview || message.streaming) && (
-                  <div className={styles["chat-message-status"]}>
-                    {Locale.Chat.Typing}
-                  </div>
-                )}
-                <div className={styles["chat-message-item"]}>
-                  {!isUser &&
-                    !(message.preview || message.content.length === 0) && (
-                      <div className={styles["chat-message-top-actions"]}>
-                        {message.streaming ? (
-                          <div
-                            className={styles["chat-message-top-action"]}
-                            onClick={() => onUserStop(i)}
-                          >
-                            {Locale.Chat.Actions.Stop}
-                          </div>
-                        ) : (
-                          <div
-                            className={styles["chat-message-top-action"]}
-                            onClick={() => onResend(i)}
-                          >
-                            {Locale.Chat.Actions.Retry}
-                          </div>
-                        )}
-
-                        <div
-                          className={styles["chat-message-top-action"]}
-                          onClick={() => copyToClipboard(message.content)}
-                        >
-                          {Locale.Chat.Actions.Copy}
-                        </div>
-                      </div>
-                    )}
-                  {(message.preview || message.content.length === 0) &&
-                  !isUser ? (
-                    <LoadingIcon />
-                  ) : (
-                    <div
-                      className="markdown-body"
-                      style={{ fontSize: `${fontSize}px` }}
-                      onContextMenu={(e) => onRightClick(e, message)}
-                      onDoubleClickCapture={() => {
-                        if (!isMobileScreen()) return;
-                        setUserInput(message.content);
-                      }}
-                    >
-                      <Markdown content={message.content} />
-                    </div>
-                  )}
-                </div>
-                {!isUser && !message.preview && (
-                  <div className={styles["chat-message-actions"]}>
-                    <div className={styles["chat-message-action-date"]}>
-                      {message.date.toLocaleString()}
-                    </div>
-                  </div>
-                )}
-              </div>
-            </div>
-          );
-        })}
-        <div ref={latestMessageRef} style={{ opacity: 0, height: "1px" }}>
-          -
-        </div>
-      </div>
-
-      <div className={styles["chat-input-panel"]}>
-        <PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
-        <div className={styles["chat-input-panel-inner"]}>
-          <textarea
-            ref={inputRef}
-            className={styles["chat-input"]}
-            placeholder={Locale.Chat.Input(submitKey)}
-            rows={4}
-            onInput={(e) => onInput(e.currentTarget.value)}
-            value={userInput}
-            onKeyDown={onInputKeyDown}
-            onFocus={() => setAutoScroll(true)}
-            onBlur={() => {
-              setAutoScroll(false);
-              setTimeout(() => setPromptHints([]), 500);
-            }}
-            autoFocus={!props?.sideBarShowing}
-          />
-          <IconButton
-            icon={<SendWhiteIcon />}
-            text={Locale.Chat.Send}
-            className={styles["chat-input-send"] + " no-dark"}
-            onClick={onUserSubmit}
-          />
-        </div>
-      </div>
-    </div>
-  );
-}
-
 function useSwitchTheme() {
   const config = useChatStore((state) => state.config);
 
@@ -537,64 +68,6 @@ function useSwitchTheme() {
   }, [config.theme]);
 }
 
-function exportMessages(messages: Message[], topic: string) {
-  const mdText =
-    `# ${topic}\n\n` +
-    messages
-      .map((m) => {
-        return m.role === "user" ? `## ${m.content}` : m.content.trim();
-      })
-      .join("\n\n");
-  const filename = `${topic}.md`;
-
-  showModal({
-    title: Locale.Export.Title,
-    children: (
-      <div className="markdown-body">
-        <pre className={styles["export-content"]}>{mdText}</pre>
-      </div>
-    ),
-    actions: [
-      <IconButton
-        key="copy"
-        icon={<CopyIcon />}
-        bordered
-        text={Locale.Export.Copy}
-        onClick={() => copyToClipboard(mdText)}
-      />,
-      <IconButton
-        key="download"
-        icon={<DownloadIcon />}
-        bordered
-        text={Locale.Export.Download}
-        onClick={() => downloadAs(mdText, filename)}
-      />,
-    ],
-  });
-}
-
-function showMemoryPrompt(session: ChatSession) {
-  showModal({
-    title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`,
-    children: (
-      <div className="markdown-body">
-        <pre className={styles["export-content"]}>
-          {session.memoryPrompt || Locale.Memory.EmptyContent}
-        </pre>
-      </div>
-    ),
-    actions: [
-      <IconButton
-        key="copy"
-        icon={<CopyIcon />}
-        bordered
-        text={Locale.Memory.Copy}
-        onClick={() => copyToClipboard(session.memoryPrompt)}
-      />,
-    ],
-  });
-}
-
 const useHasHydrated = () => {
   const [hasHydrated, setHasHydrated] = useState<boolean>(false);
 

+ 1 - 1
app/components/settings.tsx

@@ -20,7 +20,7 @@ import {
   useUpdateStore,
   useAccessStore,
 } from "../store";
-import { Avatar, PromptHints } from "./home";
+import { Avatar } from "./chat";
 
 import Locale, { AllLangs, changeLang, getLang } from "../locales";
 import { getCurrentVersion } from "../utils";