李富豪 9 ヶ月 前
コミット
cb8298609d

+ 19 - 3
app/client/platforms/deepSeek.ts

@@ -1,5 +1,6 @@
 "use client";
 import { REQUEST_TIMEOUT_MS } from "@/app/constant";
+import { useChatStore } from "@/app/store";
 import {
   ChatOptions,
   LLMApi,
@@ -12,6 +13,7 @@ import {
 } from "@fortaine/fetch-event-source";
 import { prettyObject } from "@/app/utils/format";
 import { getMessageTextContent } from "@/app/utils";
+import api from "@/app/api/api";
 
 export class DeepSeekApi implements LLMApi {
   public apiPath: string;
@@ -93,8 +95,6 @@ export class DeepSeekApi implements LLMApi {
       const finish = () => {
         if (!finished) {
           finished = true;
-          console.log(remainText, 'remainText');
-
           let text = responseText + remainText;
           options.onFinish(text);
         }
@@ -151,8 +151,24 @@ export class DeepSeekApi implements LLMApi {
           }
           remainText += currentData;
         },
-        onclose() {
+        async onclose() {
           finish();
+          const session = useChatStore.getState().sessions[0];
+          const data = {
+            id: session.id,
+            appId: session.appId,
+            userId: undefined,
+            dialogName: session.topic,
+            messages: session.messages.map(item => ({
+              id: item.id,
+              date: item.date,
+              role: item.role,
+              content: item.content,
+            })),
+          };
+          if (session.appId) {
+            await api.post('bigmodel/api/dialog/save', data);
+          }
         },
         onerror(e) {
           options.onError?.(e);

+ 8 - 3
app/components/DeekSeek.tsx → app/components/DeekSeekHome.tsx

@@ -1,11 +1,11 @@
 import * as React from 'react';
 import { useNavigate } from "react-router-dom";
-import { Chat } from './DeepSeekChat';
+import { Chat } from './DeepSeekHomeChat';
 import whiteLogo from "../icons/whiteLogo.png";
 import jkxz from "../icons/jkxz.png";
 import { useChatStore } from "../store";
 import { useMobileScreen } from '../utils';
-import './deepSeek.scss';
+import './deepSeekHome.scss';
 
 const DeekSeek: React.FC = () => {
     const chatStore = useChatStore();
@@ -17,8 +17,13 @@ const DeekSeek: React.FC = () => {
 
     React.useEffect(() => {
         chatStore.clearSessions();
-        chatStore.setModel('DeepSeek');
         setList([
+            {
+                title: '智能问答',
+                onClick: () => {
+                    navigate({ pathname: '/deepseekChat' })
+                }
+            },
             {
                 title: '知识库问答',
                 onClick: () => {

+ 27 - 15
app/components/DeepSeekChat.tsx

@@ -8,7 +8,7 @@ import React, {
   Fragment,
   RefObject,
 } from "react";
-
+import LeftIcon from "../icons/left.svg";
 import SendWhiteIcon from "../icons/send-white.svg";
 import BrainIcon from "../icons/brain.svg";
 import CopyIcon from "../icons/copy.svg";
@@ -803,20 +803,11 @@ function _Chat() {
     { leading: true, trailing: true },
   );
 
-  const [loading, setLoading] = useState<boolean>(false);
-
-  type AppList = {
-    label: string,
-    value: string,
-  }[];
-
-  const [appList, setAppList] = useState<AppList>([]);
-  const [appValue, setAppValue] = useState<string>();
-  const globalStore = useGlobalStore();
-  type QuestionList = string[];
-  const [questionList, setQuestionList] = useState<QuestionList>([]);
-  const location = useLocation();
-
+  useEffect(() => {
+    chatStore.updateCurrentSession((session) => {
+      session.appId = '1881269958412521255';
+    });
+  }, [])
 
   const [inputRows, setInputRows] = useState(2);
   const measure = useDebouncedCallback(
@@ -1312,6 +1303,22 @@ function _Chat() {
 
   return (
     <div className={styles.chat} key={session.id}>
+      {
+        isMobileScreen &&
+        <div className="window-header" data-tauri-drag-region>
+          <div style={{ display: 'flex', alignItems: 'center' }}
+            className={`window-header-title ${styles["chat-body-title"]}`}>
+            <div>
+              <IconButton
+                style={{ padding: 0, marginRight: 20 }}
+                icon={<LeftIcon />}
+                text={Locale.NewChat.Return}
+                onClick={() => navigate('/deepseekChat')}
+              />
+            </div>
+          </div>
+        </div>
+      }
       <div
         className={styles["chat-body"]}
         ref={scrollRef}
@@ -1511,5 +1518,10 @@ function _Chat() {
 export function Chat() {
   const chatStore = useChatStore();
   const sessionIndex = chatStore.currentSessionIndex;
+
+  useEffect(() => {
+    chatStore.setModel('DeepSeek');
+  }, []);
+
   return <_Chat key={sessionIndex}></_Chat>;
 }

+ 1505 - 0
app/components/DeepSeekHomeChat.tsx

@@ -0,0 +1,1505 @@
+import { useDebouncedCallback } from "use-debounce";
+import React, {
+  useState,
+  useRef,
+  useEffect,
+  useMemo,
+  useCallback,
+  Fragment,
+  RefObject,
+} from "react";
+
+import SendWhiteIcon from "../icons/send-white.svg";
+import BrainIcon from "../icons/brain.svg";
+import CopyIcon from "../icons/copy.svg";
+import LoadingIcon from "../icons/three-dots.svg";
+import ResetIcon from "../icons/reload.svg";
+import DeleteIcon from "../icons/clear.svg";
+import ConfirmIcon from "../icons/confirm.svg";
+import CancelIcon from "../icons/cancel.svg";
+import SizeIcon from "../icons/size.svg";
+import avatar from "../icons/aiIcon.png";
+
+import {
+  SubmitKey,
+  useChatStore,
+  useAccessStore,
+  Theme,
+  useAppConfig,
+  DEFAULT_TOPIC,
+  ModelType,
+} from "../store";
+
+import {
+  copyToClipboard,
+  selectOrCopy,
+  autoGrowTextArea,
+  useMobileScreen,
+  getMessageTextContent,
+  getMessageImages,
+  isVisionModel,
+  isDalle3,
+} from "../utils";
+
+import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
+
+import dynamic from "next/dynamic";
+
+import { ChatControllerPool } from "../client/controller";
+import { DalleSize } from "../typing";
+import type { RequestMessage } from "../client/api";
+import { Prompt, usePromptStore } from "../store/prompt";
+import { useGlobalStore } from "../store";
+import Locale from "../locales";
+
+import { IconButton } from "./button";
+import styles from "./chat.module.scss";
+
+import {
+  List,
+  ListItem,
+  Modal,
+  Selector,
+  showConfirm,
+  showToast,
+} from "./ui-lib";
+import { useNavigate, useLocation } from "react-router-dom";
+import {
+  CHAT_PAGE_SIZE,
+  LAST_INPUT_KEY,
+  Path,
+  REQUEST_TIMEOUT_MS,
+  UNFINISHED_INPUT,
+  ServiceProvider,
+  Plugin,
+} from "../constant";
+import { ContextPrompts, MaskConfig } from "./mask";
+import { useMaskStore } from "../store/mask";
+import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
+import { prettyObject } from "../utils/format";
+import { ExportMessageModal } from "./exporter";
+import { getClientConfig } from "../config/client";
+import { useAllModels } from "../utils/hooks";
+import { nanoid } from "nanoid";
+
+export function createMessage(override: Partial<ChatMessage>): ChatMessage {
+  return {
+    id: nanoid(),
+    date: new Date().toLocaleString(),
+    role: "user",
+    content: "",
+    ...override,
+  };
+}
+
+export type ChatMessage = RequestMessage & {
+  date: string;
+  streaming?: boolean;
+  isError?: boolean;
+  id: string;
+  model?: ModelType;
+};
+
+export const BOT_HELLO: ChatMessage = createMessage({
+  role: "assistant",
+  content: '您好,我是小智',
+});
+
+const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
+  loading: () => <LoadingIcon />,
+});
+
+export function SessionConfigModel(props: { onClose: () => void }) {
+  const chatStore = useChatStore();
+  const session = chatStore.currentSession();
+  const maskStore = useMaskStore();
+  const navigate = useNavigate();
+
+  return (
+    <div className="modal-mask">
+      <Modal
+        title={Locale.Context.Edit}
+        onClose={() => props.onClose()}
+        actions={[
+          <IconButton
+            key="reset"
+            icon={<ResetIcon />}
+            bordered
+            text={Locale.Chat.Config.Reset}
+            onClick={async () => {
+              if (await showConfirm(Locale.Memory.ResetConfirm)) {
+                chatStore.updateCurrentSession(
+                  (session) => (session.memoryPrompt = ""),
+                );
+              }
+            }}
+          />,
+          <IconButton
+            key="copy"
+            icon={<CopyIcon />}
+            bordered
+            text={Locale.Chat.Config.SaveAs}
+            onClick={() => {
+              navigate(Path.Masks);
+              setTimeout(() => {
+                maskStore.create(session.mask);
+              }, 500);
+            }}
+          />,
+        ]}
+      >
+        <MaskConfig
+          mask={session.mask}
+          updateMask={(updater) => {
+            const mask = { ...session.mask };
+            updater(mask);
+            chatStore.updateCurrentSession((session) => (session.mask = mask));
+          }}
+          shouldSyncFromGlobal
+          extraListItems={
+            session.mask.modelConfig.sendMemory ? (
+              <ListItem
+                className="copyable"
+                title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
+                subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
+              ></ListItem>
+            ) : (
+              <></>
+            )
+          }
+        ></MaskConfig>
+      </Modal>
+    </div>
+  );
+}
+
+// 提示词
+const CallWord = (props: {
+  setUserInput: (value: string) => void,
+  doSubmit: (userInput: string) => void,
+}) => {
+  const { setUserInput, doSubmit } = props
+  const list = [
+    {
+      title: '信息公布',
+      // text: '在哪里查看招聘信息?',
+      text: '今年上海建科工程咨询的校园招聘什么时候开始?如何查阅相关招聘信息?',
+    },
+    {
+      title: '招聘岗位',
+      // text: '今年招聘的岗位有哪些?',
+      text: '今年招聘的岗位有哪些?',
+    },
+    {
+      title: '专业要求',
+      // text: '招聘的岗位有什么专业要求?',
+      text: '招聘的岗位有什么专业要求?',
+    },
+    {
+      title: '工作地点',
+      // text: '全国都有工作地点吗?',
+      text: '工作地点是如何确定的?',
+    },
+    {
+      title: '薪资待遇',
+      // text: '企业可提供的薪资与福利待遇如何?',
+      text: '企业可提供的薪资与福利待遇如何?',
+    },
+    {
+      title: '职业发展',
+      // text: '我应聘贵单位,你们能提供怎样的职业发展规划?',
+      text: '公司有哪些职业发展通道?',
+    },
+    {
+      title: '落户政策',
+      // text: '公司是否能协助我落户?',
+      text: '关于落户支持?',
+    }
+  ]
+
+  return (
+    <>
+      {
+        list.map((item, index) => {
+          return <span
+            key={index}
+            style={{
+              padding: '5px 10px',
+              background: '#f6f7f8',
+              color: '#5e5e66',
+              borderRadius: 4,
+              margin: '0 5px 10px 0',
+              cursor: 'pointer',
+              fontSize: 12
+            }}
+            onClick={() => {
+              const plan: string = '2';
+              if (plan === '1') {
+                // 方案1.点击后出现在输入框内,用户自己点击发送
+                setUserInput(item.text);
+              } else {
+                // 方案2.点击后直接发送
+                doSubmit(item.text)
+              }
+            }}
+          >
+            {item.title}
+          </span>
+        })
+      }
+    </>
+  )
+}
+
+function PromptToast(props: {
+  showToast?: boolean;
+  showModal?: boolean;
+  setShowModal: (_: boolean) => void;
+}) {
+  const chatStore = useChatStore();
+  const session = chatStore.currentSession();
+  const context = session.mask.context;
+
+  return (
+    <div className={styles["prompt-toast"]} key="prompt-toast">
+      {props.showToast && (
+        <div
+          className={styles["prompt-toast-inner"] + " clickable"}
+          role="button"
+          onClick={() => props.setShowModal(true)}
+        >
+          <BrainIcon />
+          <span className={styles["prompt-toast-content"]}>
+            {Locale.Context.Toast(context.length)}
+          </span>
+        </div>
+      )}
+      {props.showModal && (
+        <SessionConfigModel onClose={() => props.setShowModal(false)} />
+      )}
+    </div>
+  );
+}
+
+function useSubmitHandler() {
+  const config = useAppConfig();
+  const submitKey = config.submitKey;
+  const isComposing = useRef(false);
+
+  useEffect(() => {
+    const onCompositionStart = () => {
+      isComposing.current = true;
+    };
+    const onCompositionEnd = () => {
+      isComposing.current = false;
+    };
+
+    window.addEventListener("compositionstart", onCompositionStart);
+    window.addEventListener("compositionend", onCompositionEnd);
+
+    return () => {
+      window.removeEventListener("compositionstart", onCompositionStart);
+      window.removeEventListener("compositionend", onCompositionEnd);
+    };
+  }, []);
+
+  const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+    // Fix Chinese input method "Enter" on Safari
+    if (e.keyCode == 229) return false;
+    if (e.key !== "Enter") return false;
+    if (e.key === "Enter" && (e.nativeEvent.isComposing || isComposing.current))
+      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 type RenderPrompt = Pick<Prompt, "title" | "content">;
+
+export function PromptHints(props: {
+  prompts: RenderPrompt[];
+  onPromptSelect: (prompt: RenderPrompt) => void;
+}) {
+  const noPrompts = props.prompts.length === 0;
+  const [selectIndex, setSelectIndex] = useState(0);
+  const selectedRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    setSelectIndex(0);
+  }, [props.prompts.length]);
+
+  useEffect(() => {
+    const onKeyDown = (e: KeyboardEvent) => {
+      if (noPrompts || e.metaKey || e.altKey || e.ctrlKey) {
+        return;
+      }
+      // arrow up / down to select prompt
+      const changeIndex = (delta: number) => {
+        e.stopPropagation();
+        e.preventDefault();
+        const nextIndex = Math.max(
+          0,
+          Math.min(props.prompts.length - 1, selectIndex + delta),
+        );
+        setSelectIndex(nextIndex);
+        selectedRef.current?.scrollIntoView({
+          block: "center",
+        });
+      };
+
+      if (e.key === "ArrowUp") {
+        changeIndex(1);
+      } else if (e.key === "ArrowDown") {
+        changeIndex(-1);
+      } else if (e.key === "Enter") {
+        const selectedPrompt = props.prompts.at(selectIndex);
+        if (selectedPrompt) {
+          props.onPromptSelect(selectedPrompt);
+        }
+      }
+    };
+
+    window.addEventListener("keydown", onKeyDown);
+
+    return () => window.removeEventListener("keydown", onKeyDown);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [props.prompts.length, selectIndex]);
+
+  if (noPrompts) return null;
+  return (
+    <div className={styles["prompt-hints"]}>
+      {props.prompts.map((prompt, i) => (
+        <div
+          ref={i === selectIndex ? selectedRef : null}
+          className={
+            styles["prompt-hint"] +
+            ` ${i === selectIndex ? styles["prompt-hint-selected"] : ""}`
+          }
+          key={prompt.title + i.toString()}
+          onClick={() => props.onPromptSelect(prompt)}
+          onMouseEnter={() => setSelectIndex(i)}
+        >
+          <div className={styles["hint-title"]}>{prompt.title}</div>
+          <div className={styles["hint-content"]}>{prompt.content}</div>
+        </div>
+      ))}
+    </div>
+  );
+}
+
+function ClearContextDivider() {
+  const chatStore = useChatStore();
+
+  return (
+    <div
+      className={styles["clear-context"]}
+      onClick={() =>
+        chatStore.updateCurrentSession(
+          (session) => (session.clearContextIndex = undefined),
+        )
+      }
+    >
+      <div className={styles["clear-context-tips"]}>{Locale.Context.Clear}</div>
+      <div className={styles["clear-context-revert-btn"]}>
+        {Locale.Context.Revert}
+      </div>
+    </div>
+  );
+}
+
+export function ChatAction(props: {
+  text: string;
+  icon: JSX.Element;
+  onClick: () => void;
+}) {
+  const iconRef = useRef<HTMLDivElement>(null);
+  const textRef = useRef<HTMLDivElement>(null);
+  const [width, setWidth] = useState({
+    full: 16,
+    icon: 16,
+  });
+
+  function updateWidth() {
+    if (!iconRef.current || !textRef.current) return;
+    const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width;
+    const textWidth = getWidth(textRef.current);
+    const iconWidth = getWidth(iconRef.current);
+    setWidth({
+      full: textWidth + iconWidth,
+      icon: iconWidth,
+    });
+  }
+
+  return (
+    <div
+      className={`${styles["chat-input-action"]} clickable`}
+      onClick={() => {
+        props.onClick();
+        setTimeout(updateWidth, 1);
+      }}
+      onMouseEnter={updateWidth}
+      onTouchStart={updateWidth}
+      style={
+        {
+          "--icon-width": `${width.icon}px`,
+          "--full-width": `${width.full}px`,
+        } as React.CSSProperties
+      }
+    >
+      <div ref={iconRef} className={styles["icon"]}>
+        {props.icon}
+      </div>
+      <div className={styles["text"]} ref={textRef}>
+        {props.text}
+      </div>
+    </div>
+  );
+}
+
+function useScrollToBottom(
+  scrollRef: RefObject<HTMLDivElement>,
+  detach: boolean = false,
+) {
+  // for auto-scroll
+
+  const [autoScroll, setAutoScroll] = useState(true);
+
+  function scrollDomToBottom() {
+    const dom = scrollRef.current;
+    if (dom) {
+      requestAnimationFrame(() => {
+        setAutoScroll(true);
+        dom.scrollTo(0, dom.scrollHeight);
+      });
+    }
+  }
+
+  // auto scroll
+  useEffect(() => {
+    if (autoScroll && !detach) {
+      scrollDomToBottom();
+    }
+  });
+
+  return {
+    scrollRef,
+    autoScroll,
+    setAutoScroll,
+    scrollDomToBottom,
+  };
+}
+
+export function ChatActions(props: {
+  setUserInput: (value: string) => void;
+  doSubmit: (userInput: string) => void;
+  uploadImage: () => void;
+  setAttachImages: (images: string[]) => void;
+  setUploading: (uploading: boolean) => void;
+  showPromptModal: () => void;
+  scrollToBottom: () => void;
+  showPromptHints: () => void;
+  hitBottom: boolean;
+  uploading: boolean;
+}) {
+  const config = useAppConfig();
+  const navigate = useNavigate();
+  const chatStore = useChatStore();
+
+  // switch themes
+  const theme = config.theme;
+
+  function nextTheme() {
+    const themes = [Theme.Auto, Theme.Light, Theme.Dark];
+    const themeIndex = themes.indexOf(theme);
+    const nextIndex = (themeIndex + 1) % themes.length;
+    const nextTheme = themes[nextIndex];
+    config.update((config) => (config.theme = nextTheme));
+  }
+
+  // stop all responses
+  const couldStop = ChatControllerPool.hasPending();
+  const stopAll = () => ChatControllerPool.stopAll();
+
+  // switch model
+  const currentModel = chatStore.currentSession().mask.modelConfig.model;
+  const currentProviderName =
+    chatStore.currentSession().mask.modelConfig?.providerName ||
+    ServiceProvider.OpenAI;
+  const allModels = useAllModels();
+  const models = useMemo(() => {
+    const filteredModels = allModels.filter((m) => m.available);
+    const defaultModel = filteredModels.find((m) => m.isDefault);
+
+    if (defaultModel) {
+      const arr = [
+        defaultModel,
+        ...filteredModels.filter((m) => m !== defaultModel),
+      ];
+      return arr;
+    } else {
+      return filteredModels;
+    }
+  }, [allModels]);
+  const currentModelName = useMemo(() => {
+    const model = models.find(
+      (m) =>
+        m.name == currentModel &&
+        m?.provider?.providerName == currentProviderName,
+    );
+    return model?.displayName ?? "";
+  }, [models, currentModel, currentProviderName]);
+  const [showModelSelector, setShowModelSelector] = useState(false);
+  const [showPluginSelector, setShowPluginSelector] = useState(false);
+  const [showUploadImage, setShowUploadImage] = useState(false);
+
+  type GuessList = string[]
+  const [guessList, setGuessList] = useState<GuessList>([]);
+
+  const [showSizeSelector, setShowSizeSelector] = useState(false);
+  const dalle3Sizes: DalleSize[] = ["1024x1024", "1792x1024", "1024x1792"];
+  const currentSize =
+    chatStore.currentSession().mask.modelConfig?.size ?? "1024x1024";
+  const session = chatStore.currentSession();
+
+  useEffect(() => {
+    const show = isVisionModel(currentModel);
+    setShowUploadImage(show);
+    if (!show) {
+      props.setAttachImages([]);
+      props.setUploading(false);
+    }
+
+    // if current model is not available
+    // switch to first available model
+    const isUnavaliableModel = !models.some((m) => m.name === currentModel);
+    if (isUnavaliableModel && models.length > 0) {
+      // show next model to default model if exist
+      let nextModel = models.find((model) => model.isDefault) || models[0];
+      chatStore.updateCurrentSession((session) => {
+        session.mask.modelConfig.model = nextModel.name;
+        session.mask.modelConfig.providerName = nextModel?.provider?.providerName as ServiceProvider;
+      });
+      showToast(
+        nextModel?.provider?.providerName == "ByteDance"
+          ? nextModel.displayName
+          : nextModel.name,
+      );
+    }
+  }, [chatStore, currentModel, models]);
+
+  return (
+    <div className={styles["chat-input-actions"]}>
+      {showModelSelector && (
+        <Selector
+          defaultSelectedValue={`${currentModel}@${currentProviderName}`}
+          items={models.map((m) => ({
+            title: `${m.displayName}${m?.provider?.providerName
+              ? "(" + m?.provider?.providerName + ")"
+              : ""
+              }`,
+            value: `${m.name}@${m?.provider?.providerName}`,
+          }))}
+          onClose={() => setShowModelSelector(false)}
+          onSelection={(s) => {
+            if (s.length === 0) return;
+            const [model, providerName] = s[0].split("@");
+            chatStore.updateCurrentSession((session) => {
+              session.mask.modelConfig.model = model as ModelType;
+              session.mask.modelConfig.providerName =
+                providerName as ServiceProvider;
+              session.mask.syncGlobalConfig = false;
+            });
+            if (providerName == "ByteDance") {
+              const selectedModel = models.find(
+                (m) =>
+                  m.name == model && m?.provider?.providerName == providerName,
+              );
+              showToast(selectedModel?.displayName ?? "");
+            } else {
+              showToast(model);
+            }
+          }}
+        />
+      )}
+
+      {isDalle3(currentModel) && (
+        <ChatAction
+          onClick={() => setShowSizeSelector(true)}
+          text={currentSize}
+          icon={<SizeIcon />}
+        />
+      )}
+
+      {showSizeSelector && (
+        <Selector
+          defaultSelectedValue={currentSize}
+          items={dalle3Sizes.map((m) => ({
+            title: m,
+            value: m,
+          }))}
+          onClose={() => setShowSizeSelector(false)}
+          onSelection={(s) => {
+            if (s.length === 0) return;
+            const size = s[0];
+            chatStore.updateCurrentSession((session) => {
+              session.mask.modelConfig.size = size;
+            });
+            showToast(size);
+          }}
+        />
+      )}
+
+      {showPluginSelector && (
+        <Selector
+          multiple
+          defaultSelectedValue={chatStore.currentSession().mask?.plugin}
+          items={[
+            {
+              title: Locale.Plugin.Artifacts,
+              value: Plugin.Artifacts,
+            },
+          ]}
+          onClose={() => setShowPluginSelector(false)}
+          onSelection={(s) => {
+            const plugin = s[0];
+            chatStore.updateCurrentSession((session) => {
+              session.mask.plugin = s;
+            });
+            if (plugin) {
+              showToast(plugin);
+            }
+          }}
+        />
+      )}
+    </div>
+  );
+}
+
+export function EditMessageModal(props: { onClose: () => void }) {
+  const chatStore = useChatStore();
+  const session = chatStore.currentSession();
+  const [messages, setMessages] = useState(session.messages.slice());
+
+  return (
+    <div className="modal-mask">
+      <Modal
+        title={Locale.Chat.EditMessage.Title}
+        onClose={props.onClose}
+        actions={[
+          <IconButton
+            text={Locale.UI.Cancel}
+            icon={<CancelIcon />}
+            key="cancel"
+            onClick={() => {
+              props.onClose();
+            }}
+          />,
+          <IconButton
+            type="primary"
+            text={Locale.UI.Confirm}
+            icon={<ConfirmIcon />}
+            key="ok"
+            onClick={() => {
+              chatStore.updateCurrentSession(
+                (session) => (session.messages = messages),
+              );
+              props.onClose();
+            }}
+          />,
+        ]}
+      >
+        <List>
+          <ListItem
+            title={Locale.Chat.EditMessage.Topic.Title}
+            subTitle={Locale.Chat.EditMessage.Topic.SubTitle}
+          >
+            <input
+              type="text"
+              value={session.topic}
+              onInput={(e) =>
+                chatStore.updateCurrentSession(
+                  (session) => (session.topic = e.currentTarget.value),
+                )
+              }
+            ></input>
+          </ListItem>
+        </List>
+        <ContextPrompts
+          context={messages}
+          updateContext={(updater) => {
+            const newMessages = messages.slice();
+            updater(newMessages);
+            setMessages(newMessages);
+          }}
+        />
+      </Modal>
+    </div>
+  );
+}
+
+export function DeleteImageButton(props: { deleteImage: () => void }) {
+  return (
+    <div className={styles["delete-image"]} onClick={props.deleteImage}>
+      <DeleteIcon />
+    </div>
+  );
+}
+
+function _Chat() {
+  type RenderMessage = ChatMessage & { preview?: boolean };
+
+  const chatStore = useChatStore();
+  const session = chatStore.currentSession();
+  const config = useAppConfig();
+  const fontSize = config.fontSize;
+  const fontFamily = config.fontFamily;
+
+  const [showExport, setShowExport] = useState(false);
+
+  const inputRef = useRef<HTMLTextAreaElement>(null);
+  const [userInput, setUserInput] = useState("");
+  const [isLoading, setIsLoading] = useState(false);
+  const { submitKey, shouldSubmit } = useSubmitHandler();
+  const scrollRef = useRef<HTMLDivElement>(null);
+  const isScrolledToBottom = scrollRef?.current
+    ? Math.abs(
+      scrollRef.current.scrollHeight -
+      (scrollRef.current.scrollTop + scrollRef.current.clientHeight),
+    ) <= 1
+    : false;
+  const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(
+    scrollRef,
+    isScrolledToBottom,
+  );
+  const [hitBottom, setHitBottom] = useState(true);
+  const isMobileScreen = useMobileScreen();
+  const navigate = useNavigate();
+  const [attachImages, setAttachImages] = useState<string[]>([]);
+  const [uploading, setUploading] = useState(false);
+
+  // prompt hints
+  const promptStore = usePromptStore();
+  const [promptHints, setPromptHints] = useState<RenderPrompt[]>([]);
+  const onSearch = useDebouncedCallback(
+    (text: string) => {
+      const matchedPrompts = promptStore.search(text);
+      setPromptHints(matchedPrompts);
+    },
+    100,
+    { leading: true, trailing: true },
+  );
+
+  const [inputRows, setInputRows] = useState(2);
+  const measure = useDebouncedCallback(
+    () => {
+      const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
+      const inputRows = Math.min(
+        20,
+        Math.max(2 + Number(!isMobileScreen), rows),
+      );
+      setInputRows(inputRows);
+    },
+    100,
+    {
+      leading: true,
+      trailing: true,
+    },
+  );
+
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  useEffect(measure, [userInput]);
+
+  // chat commands shortcuts
+  const chatCommands = useChatCommand({
+    new: () => chatStore.newSession(),
+    newm: () => navigate(Path.NewChat),
+    prev: () => chatStore.nextSession(-1),
+    next: () => chatStore.nextSession(1),
+    clear: () =>
+      chatStore.updateCurrentSession(
+        (session) => (session.clearContextIndex = session.messages.length),
+      ),
+    del: () => chatStore.deleteSession(chatStore.currentSessionIndex),
+  });
+
+  // only search prompts when user input is short
+  const SEARCH_TEXT_LIMIT = 30;
+  const onInput = (text: string) => {
+    setUserInput(text);
+    const n = text.trim().length;
+
+    // clear search results
+    if (n === 0) {
+      setPromptHints([]);
+    } else if (text.match(ChatCommandPrefix)) {
+      setPromptHints(chatCommands.search(text));
+    } else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
+      // check if need to trigger auto completion
+      if (text.startsWith("/")) {
+        let searchText = text.slice(1);
+        onSearch(searchText);
+      }
+    }
+  };
+
+  const doSubmit = (userInput: string) => {
+    if (userInput.trim() === "") return;
+    const matchCommand = chatCommands.match(userInput);
+    if (matchCommand.matched) {
+      setUserInput("");
+      setPromptHints([]);
+      matchCommand.invoke();
+      return;
+    }
+    setIsLoading(true);
+    chatStore.onUserInput(userInput, attachImages).then(() => setIsLoading(false));
+    setAttachImages([]);
+    localStorage.setItem(LAST_INPUT_KEY, userInput);
+    setUserInput("");
+    setPromptHints([]);
+    if (!isMobileScreen) inputRef.current?.focus();
+    setAutoScroll(true);
+  };
+
+  const onPromptSelect = (prompt: RenderPrompt) => {
+    setTimeout(() => {
+      setPromptHints([]);
+
+      const matchedChatCommand = chatCommands.match(prompt.content);
+      if (matchedChatCommand.matched) {
+        // if user is selecting a chat command, just trigger it
+        matchedChatCommand.invoke();
+        setUserInput("");
+      } else {
+        // or fill the prompt
+        setUserInput(prompt.content);
+      }
+      inputRef.current?.focus();
+    }, 30);
+  };
+
+  // stop response
+  const onUserStop = (messageId: string) => {
+    ChatControllerPool.stop(session.id, messageId);
+  };
+
+  useEffect(() => {
+    chatStore.updateCurrentSession((session) => {
+      const stopTiming = Date.now() - REQUEST_TIMEOUT_MS;
+      session.messages.forEach((m) => {
+        // check if should stop all stale messages
+        if (m.isError || new Date(m.date).getTime() < stopTiming) {
+          if (m.streaming) {
+            m.streaming = false;
+          }
+
+          if (m.content.length === 0) {
+            m.isError = true;
+            m.content = prettyObject({
+              error: true,
+              message: "empty response",
+            });
+          }
+        }
+      });
+
+      // auto sync mask config from global config
+      if (session.mask.syncGlobalConfig) {
+        console.log("[Mask] syncing from global, name = ", session.mask.name);
+        session.mask.modelConfig = { ...config.modelConfig };
+      }
+    });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  // check if should send message
+  const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+    if (
+      e.key === "ArrowUp" &&
+      userInput.length <= 0 &&
+      !(e.metaKey || e.altKey || e.ctrlKey)
+    ) {
+      setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
+      e.preventDefault();
+      return;
+    }
+    if (shouldSubmit(e) && promptHints.length === 0) {
+      doSubmit(userInput);
+      e.preventDefault();
+    }
+  };
+
+  const onRightClick = (e: any, message: ChatMessage) => {
+    // copy to clipboard
+    if (selectOrCopy(e.currentTarget, getMessageTextContent(message))) {
+      if (userInput.length === 0) {
+        setUserInput(getMessageTextContent(message));
+      }
+
+      e.preventDefault();
+    }
+  };
+
+  const deleteMessage = (msgId?: string) => {
+    chatStore.updateCurrentSession(
+      (session) =>
+        (session.messages = session.messages.filter((m) => m.id !== msgId)),
+    );
+  };
+
+  const onDelete = (msgId: string) => {
+    deleteMessage(msgId);
+  };
+
+  const onResend = (message: ChatMessage) => {
+    // when it is resending a message
+    // 1. for a user's message, find the next bot response
+    // 2. for a bot's message, find the last user's input
+    // 3. delete original user input and bot's message
+    // 4. resend the user's input
+
+    const resendingIndex = session.messages.findIndex(
+      (m) => m.id === message.id,
+    );
+
+    if (resendingIndex < 0 || resendingIndex >= session.messages.length) {
+      console.error("[Chat] failed to find resending message", message);
+      return;
+    }
+
+    let userMessage: ChatMessage | undefined;
+    let botMessage: ChatMessage | undefined;
+
+    if (message.role === "assistant") {
+      // if it is resending a bot's message, find the user input for it
+      botMessage = message;
+      for (let i = resendingIndex; i >= 0; i -= 1) {
+        if (session.messages[i].role === "user") {
+          userMessage = session.messages[i];
+          break;
+        }
+      }
+    } else if (message.role === "user") {
+      // if it is resending a user's input, find the bot's response
+      userMessage = message;
+      for (let i = resendingIndex; i < session.messages.length; i += 1) {
+        if (session.messages[i].role === "assistant") {
+          botMessage = session.messages[i];
+          break;
+        }
+      }
+    }
+
+    if (userMessage === undefined) {
+      console.error("[Chat] failed to resend", message);
+      return;
+    }
+
+    // delete the original messages
+    deleteMessage(userMessage.id);
+    deleteMessage(botMessage?.id);
+
+    // resend the message
+    setIsLoading(true);
+    const textContent = getMessageTextContent(userMessage);
+    const images = getMessageImages(userMessage);
+    chatStore.onUserInput(textContent, images).then(() => setIsLoading(false));
+    inputRef.current?.focus();
+  };
+
+  const onPinMessage = (message: ChatMessage) => {
+    chatStore.updateCurrentSession((session) =>
+      session.mask.context.push(message),
+    );
+
+    showToast(Locale.Chat.Actions.PinToastContent, {
+      text: Locale.Chat.Actions.PinToastAction,
+      onClick: () => {
+        setShowPromptModal(true);
+      },
+    });
+  };
+
+  const context: RenderMessage[] = useMemo(() => {
+    return session.mask.hideContext ? [] : session.mask.context.slice();
+  }, [session.mask.context, session.mask.hideContext]);
+  const accessStore = useAccessStore();
+
+  if (
+    context.length === 0 &&
+    session.messages.at(0)?.content !== BOT_HELLO.content
+  ) {
+    const copiedHello = Object.assign({}, BOT_HELLO);
+    if (!accessStore.isAuthorized()) {
+      copiedHello.content = Locale.Error.Unauthorized;
+    }
+    context.push(copiedHello);
+  }
+
+  // preview messages
+  const renderMessages = useMemo(() => {
+    return context.concat(session.messages as RenderMessage[]).concat(
+      isLoading
+        ? [
+          {
+            ...createMessage({
+              role: "assistant",
+              content: "……",
+            }),
+            preview: true,
+          },
+        ]
+        : [],
+    ).concat(
+      userInput.length > 0 && config.sendPreviewBubble
+        ? [
+          {
+            ...createMessage({
+              role: "user",
+              content: userInput,
+            }),
+            preview: true,
+          },
+        ]
+        : [],
+    );
+  }, [
+    config.sendPreviewBubble,
+    context,
+    isLoading,
+    session.messages,
+    userInput,
+  ]);
+
+  const [msgRenderIndex, _setMsgRenderIndex] = useState(
+    Math.max(0, renderMessages.length - CHAT_PAGE_SIZE),
+  );
+
+  function setMsgRenderIndex(newIndex: number) {
+    newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex);
+    newIndex = Math.max(0, newIndex);
+    _setMsgRenderIndex(newIndex);
+  }
+
+  const messages = useMemo(() => {
+    const endRenderIndex = Math.min(
+      msgRenderIndex + 3 * CHAT_PAGE_SIZE,
+      renderMessages.length,
+    );
+    return renderMessages.slice(msgRenderIndex, endRenderIndex);
+  }, [msgRenderIndex, renderMessages]);
+
+  const onChatBodyScroll = (e: HTMLElement) => {
+    const bottomHeight = e.scrollTop + e.clientHeight;
+    const edgeThreshold = e.clientHeight;
+
+    const isTouchTopEdge = e.scrollTop <= edgeThreshold;
+    const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
+    const isHitBottom =
+      bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10);
+
+    const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
+    const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
+
+    if (isTouchTopEdge && !isTouchBottomEdge) {
+      setMsgRenderIndex(prevPageMsgIndex);
+    } else if (isTouchBottomEdge) {
+      setMsgRenderIndex(nextPageMsgIndex);
+    }
+
+    setHitBottom(isHitBottom);
+    setAutoScroll(isHitBottom);
+  };
+
+  function scrollToBottom() {
+    setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
+    scrollDomToBottom();
+  }
+
+  // clear context index = context length + index in messages
+  const clearContextIndex =
+    (session.clearContextIndex ?? -1) >= 0
+      ? session.clearContextIndex! + context.length - msgRenderIndex
+      : -1;
+
+  const [showPromptModal, setShowPromptModal] = useState(false);
+
+  const clientConfig = useMemo(() => getClientConfig(), []);
+
+  const autoFocus = !isMobileScreen; // wont auto focus on mobile screen
+  const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
+
+  useCommand({
+    fill: setUserInput,
+    submit: (text) => {
+      doSubmit(text);
+    },
+    code: (text) => {
+      if (accessStore.disableFastLink) return;
+      console.log("[Command] got code from url: ", text);
+      showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => {
+        if (res) {
+          accessStore.update((access) => (access.accessCode = text));
+        }
+      });
+    },
+    settings: (text) => {
+      if (accessStore.disableFastLink) return;
+
+      try {
+        const payload = JSON.parse(text) as {
+          key?: string;
+          url?: string;
+        };
+
+        console.log("[Command] got settings from url: ", payload);
+
+        if (payload.key || payload.url) {
+          showConfirm(
+            Locale.URLCommand.Settings +
+            `\n${JSON.stringify(payload, null, 4)}`,
+          ).then((res) => {
+            if (!res) return;
+            if (payload.key) {
+              accessStore.update(
+                (access) => (access.openaiApiKey = payload.key!),
+              );
+            }
+            if (payload.url) {
+              accessStore.update((access) => (access.openaiUrl = payload.url!));
+            }
+            accessStore.update((access) => (access.useCustomConfig = true));
+          });
+        }
+      } catch {
+        console.error("[Command] failed to get settings from url: ", text);
+      }
+    },
+  });
+
+  // edit / insert message modal
+  const [isEditingMessage, setIsEditingMessage] = useState(false);
+
+  // remember unfinished input
+  useEffect(() => {
+    // try to load from local storage
+    const key = UNFINISHED_INPUT(session.id);
+    const mayBeUnfinishedInput = localStorage.getItem(key);
+    if (mayBeUnfinishedInput && userInput.length === 0) {
+      setUserInput(mayBeUnfinishedInput);
+      localStorage.removeItem(key);
+    }
+
+    const dom = inputRef.current;
+    return () => {
+      localStorage.setItem(key, dom?.value ?? "");
+    };
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const handlePaste = useCallback(
+    async (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
+      const currentModel = chatStore.currentSession().mask.modelConfig.model;
+      if (!isVisionModel(currentModel)) {
+        return;
+      }
+      const items = (event.clipboardData || window.clipboardData).items;
+      for (const item of items) {
+        if (item.kind === "file" && item.type.startsWith("image/")) {
+          event.preventDefault();
+          const file = item.getAsFile();
+          if (file) {
+            const images: string[] = [];
+            images.push(...attachImages);
+            images.push(
+              ...(await new Promise<string[]>((res, rej) => {
+                setUploading(true);
+                const imagesData: string[] = [];
+                uploadImageRemote(file).then((dataUrl) => {
+                  imagesData.push(dataUrl);
+                  setUploading(false);
+                  res(imagesData);
+                }).catch((e) => {
+                  setUploading(false);
+                  rej(e);
+                });
+              })),
+            );
+            const imagesLength = images.length;
+
+            if (imagesLength > 3) {
+              images.splice(3, imagesLength - 3);
+            }
+            setAttachImages(images);
+          }
+        }
+      }
+    },
+    [attachImages, chatStore],
+  );
+
+  async function uploadImage() {
+    const images: string[] = [];
+    images.push(...attachImages);
+
+    images.push(
+      ...(await new Promise<string[]>((res, rej) => {
+        const fileInput = document.createElement("input");
+        fileInput.type = "file";
+        fileInput.accept =
+          "image/png, image/jpeg, image/webp, image/heic, image/heif";
+        fileInput.multiple = true;
+        fileInput.onchange = (event: any) => {
+          setUploading(true);
+          const files = event.target.files;
+          const imagesData: string[] = [];
+          for (let i = 0; i < files.length; i++) {
+            const file = event.target.files[i];
+            uploadImageRemote(file).then((dataUrl) => {
+              imagesData.push(dataUrl);
+              if (
+                imagesData.length === 3 ||
+                imagesData.length === files.length
+              ) {
+                setUploading(false);
+                res(imagesData);
+              }
+            }).catch((e) => {
+              setUploading(false);
+              rej(e);
+            });
+          }
+        };
+        fileInput.click();
+      })),
+    );
+
+    const imagesLength = images.length;
+    if (imagesLength > 3) {
+      images.splice(3, imagesLength - 3);
+    }
+    setAttachImages(images);
+  }
+
+  return (
+    <div className={styles.chat} key={session.id}>
+      <div
+        className={styles["chat-body"]}
+        ref={scrollRef}
+        onScroll={(e) => onChatBodyScroll(e.currentTarget)}
+        onMouseDown={() => inputRef.current?.blur()}
+        onTouchStart={() => {
+          inputRef.current?.blur();
+          setAutoScroll(false);
+        }}
+      >
+        <>
+          {messages.map((message, i) => {
+            const isUser = message.role === "user";
+            const isContext = i < context.length;
+            const showActions =
+              i > 0 &&
+              !(message.preview || message.content.length === 0) &&
+              !isContext;
+            const showTyping = message.preview || message.streaming;
+
+            const shouldShowClearContextDivider = i === clearContextIndex - 1;
+
+            return (
+              <Fragment key={message.id}>
+                <div
+                  className={
+                    isUser ? styles["chat-message-user"] : styles["chat-message"]
+                  }
+                >
+                  <div className={styles["chat-message-container"]} style={{ display: 'flex', flexDirection: 'row' }}>
+                    <div className={styles["chat-message-header"]}>
+                      <div className={styles["chat-message-avatar"]}>
+                        {isUser ? null : (
+                          <img src={avatar.src} style={{ width: 40, marginRight: 10 }} />
+                        )}
+                      </div>
+                    </div>
+                    {/* {showTyping && (
+                      <div className={styles["chat-message-status"]}>
+                        正在输入…
+                      </div>
+                    )} */}
+                    <div className={styles["chat-message-item"]} style={{ marginTop: 20 }}>
+                      <Markdown
+                        key={message.streaming ? "loading" : "done"}
+                        content={getMessageTextContent(message)}
+                        loading={
+                          (message.preview || message.streaming) &&
+                          message.content.length === 0 &&
+                          !isUser
+                        }
+                        onDoubleClickCapture={() => {
+                          if (!isMobileScreen) return;
+                          setUserInput(getMessageTextContent(message));
+                        }}
+                        fontSize={fontSize}
+                        fontFamily={fontFamily}
+                        parentRef={scrollRef}
+                        defaultShow={i >= messages.length - 6}
+                      />
+                      {getMessageImages(message).length == 1 && (
+                        <img
+                          className={styles["chat-message-item-image"]}
+                          src={getMessageImages(message)[0]}
+                          alt=""
+                        />
+                      )}
+                      {getMessageImages(message).length > 1 && (
+                        <div
+                          className={styles["chat-message-item-images"]}
+                          style={
+                            {
+                              "--image-count": getMessageImages(message).length,
+                            } as React.CSSProperties
+                          }
+                        >
+                          {getMessageImages(message).map((image, index) => {
+                            return (
+                              <img
+                                className={
+                                  styles["chat-message-item-image-multi"]
+                                }
+                                key={index}
+                                src={image}
+                                alt=""
+                              />
+                            );
+                          })}
+                        </div>
+                      )}
+                    </div>
+                  </div>
+                </div>
+                {shouldShowClearContextDivider && <ClearContextDivider />}
+              </Fragment>
+            );
+          })}
+        </>
+      </div>
+
+      <div className={styles["chat-input-panel"]}>
+        <ChatActions
+          setUserInput={setUserInput}
+          doSubmit={doSubmit}
+          uploadImage={uploadImage}
+          setAttachImages={setAttachImages}
+          setUploading={setUploading}
+          showPromptModal={() => setShowPromptModal(true)}
+          scrollToBottom={scrollToBottom}
+          hitBottom={hitBottom}
+          uploading={uploading}
+          showPromptHints={() => {
+            if (promptHints.length > 0) {
+              setPromptHints([]);
+              return;
+            }
+
+            inputRef.current?.focus();
+            setUserInput("/");
+            onSearch("");
+          }}
+        />
+        <label
+          className={`${styles["chat-input-panel-inner"]} ${attachImages.length != 0
+            ? styles["chat-input-panel-inner-attach"]
+            : ""
+            }`}
+          htmlFor="chat-input"
+        >
+          <textarea
+            id="chat-input"
+            ref={inputRef}
+            className={styles["chat-input2"]}
+            placeholder={Locale.Chat.Input(submitKey)}
+            onInput={(e) => onInput(e.currentTarget.value)}
+            value={userInput}
+            onKeyDown={onInputKeyDown}
+            onFocus={scrollToBottom}
+            onClick={scrollToBottom}
+            onPaste={handlePaste}
+            rows={inputRows}
+            autoFocus={autoFocus}
+            style={{
+              fontSize: config.fontSize,
+              fontFamily: config.fontFamily,
+            }}
+          />
+          {attachImages.length != 0 && (
+            <div className={styles["attach-images"]}>
+              {attachImages.map((image, index) => {
+                return (
+                  <div
+                    key={index}
+                    className={styles["attach-image"]}
+                    style={{ backgroundImage: `url("${image}")` }}
+                  >
+                    <div className={styles["attach-image-mask"]}>
+                      <DeleteImageButton
+                        deleteImage={() => {
+                          setAttachImages(
+                            attachImages.filter((_, i) => i !== index),
+                          );
+                        }}
+                      />
+                    </div>
+                  </div>
+                );
+              })}
+            </div>
+          )}
+          <IconButton
+            style={{ background: '#4360ee' }}
+            icon={<SendWhiteIcon />}
+            text='发送'
+            className={styles["chat-input-send"]}
+            type="primary"
+            onClick={() => doSubmit(userInput)}
+          />
+        </label>
+      </div>
+
+      {showExport && (
+        <ExportMessageModal onClose={() => setShowExport(false)} />
+      )}
+
+      {isEditingMessage && (
+        <EditMessageModal
+          onClose={() => {
+            setIsEditingMessage(false);
+          }}
+        />
+      )}
+    </div>
+  );
+}
+
+export function Chat() {
+  const chatStore = useChatStore();
+  const sessionIndex = chatStore.currentSessionIndex;
+
+  useEffect(() => {
+    chatStore.setModel('DeepSeek');
+  }, []);
+
+  return <_Chat key={sessionIndex}></_Chat>;
+}

+ 0 - 41
app/components/chat.tsx

@@ -1542,18 +1542,6 @@ function _Chat() {
   return (
     <div className={styles.chat} key={session.id}>
       <div className="window-header" data-tauri-drag-region>
-        {/* {isMobileScreen && (
-          <div className="window-actions">
-            <div className={"window-action-button"}>
-              <IconButton
-                icon={<ReturnIcon />}
-                bordered
-                title={Locale.Chat.Actions.ChatList}
-                onClick={() => navigate(Path.Home)}
-              />
-            </div>
-          </div>
-        )} */}
         <div style={{ display: 'flex', alignItems: 'center' }}
           className={`window-header-title ${styles["chat-body-title"]}`}>
           <div>
@@ -1590,35 +1578,6 @@ function _Chat() {
           </div>
         </div>
         <div className="window-actions">
-          {/* <IconButton
-            icon={<AddIcon />}
-            bordered
-            title='新建对话'
-            aria='新建对话'
-            onClick={() => {
-              localStorage.clear()
-              location.reload()
-            }}
-          /> */}
-          {/* {!isMobileScreen && (
-            <div className="window-action-button">
-              <IconButton
-                icon={<RenameIcon />}
-                bordered
-                title={Locale.Chat.EditMessage.Title}
-                aria={Locale.Chat.EditMessage.Title}
-                onClick={() => setIsEditingMessage(true)}
-              />
-            </div>
-          )} */}
-          <IconButton
-            icon={<HomeOutlined />}
-            bordered
-            title="首页"
-            onClick={() => {
-              navigate({ pathname: '/' });
-            }}
-          />
           <div className="window-action-button">
             <Popover
               trigger="click"

+ 0 - 0
app/components/deepSeek.scss → app/components/deepSeekHome.scss


+ 14 - 2
app/components/home.tsx

@@ -110,6 +110,16 @@ const Chat = dynamic(
   }
 );
 
+const DeepSeekChat = dynamic(
+  async () => {
+    await delayer();
+    return (await import("./DeepSeekChat")).Chat
+  },
+  {
+    loading: () => <Loading />,
+  }
+);
+
 const Record = dynamic(
   async () => {
     await delayer();
@@ -122,7 +132,7 @@ const Record = dynamic(
 
 const HomeApp = dynamic(
   async () => {
-    return (await import("./DeekSeek"))
+    return (await import("./DeekSeekHome"))
   },
   {
     loading: () => <Loading />,
@@ -267,13 +277,15 @@ function Screen() {
       <>
         {
           location.pathname !== '/' &&
-          <SideBar className={location.pathname === '/knowledgeChat' ? styles["sidebar-show"] : ""} />
+          <SideBar className={(location.pathname === '/knowledgeChat' || location.pathname === '/deepseekChat') ? styles["sidebar-show"] : ""} />
         }
         <WindowContent>
           <Routes>
             <Route path='/' element={<HomeApp />} />
             <Route path='/knowledgeChat' element={<Chat />} />
             <Route path='/newChat' element={<Chat />} />
+            <Route path='/deepseekChat' element={<DeepSeekChat />} />
+            <Route path='/newDeepseekChat' element={<DeepSeekChat />} />
           </Routes>
         </WindowContent>
       </>

+ 43 - 19
app/components/sidebar.tsx

@@ -10,7 +10,7 @@ import {
   MIN_SIDEBAR_WIDTH,
   NARROW_SIDEBAR_WIDTH,
 } from "../constant";
-import { useNavigate } from "react-router-dom";
+import { useLocation, useNavigate } from "react-router-dom";
 import { isIOS, useMobileScreen } from "../utils";
 import api from "@/app/api/api";
 import { Button, Dropdown, Form, Input, Menu, Modal } from "antd";
@@ -197,7 +197,7 @@ export const SideBar = (props: { className?: string }) => {
   const { onDragStart, shouldNarrow } = useDragSideBar();
   const [showPluginSelector, setShowPluginSelector] = useState(false);
   const navigate = useNavigate();
-  const config = useAppConfig();
+  const location = useLocation();
   const chatStore = useChatStore();
   const globalStore = useGlobalStore();
 
@@ -208,7 +208,13 @@ export const SideBar = (props: { className?: string }) => {
   // 获取聊天列表
   const fetchChatList = async () => {
     try {
-      const res = await api.get(`/bigmodel/api/dialog/list/${globalStore.selectedAppId}`);
+      let appId = '';
+      if (['/knowledgeChat', '/newChat'].includes(location.pathname)) {
+        appId = globalStore.selectedAppId;
+      } else if (['/deepseekChat', '/newDeepseekChat'].includes(location.pathname)) {
+        appId = '1881269958412521255';
+      }
+      const res = await api.get(`/bigmodel/api/dialog/list/${appId}`);
       const list = res.data.map((item: any) => {
         return {
           ...item,
@@ -303,22 +309,36 @@ export const SideBar = (props: { className?: string }) => {
     >
       <SideBarHeader
         title="问答历史"
-        logo={<img style={{ height: 40 }} src={faviconSrc.src} />}
+        logo={<img style={{ height: 40, cursor: 'pointer' }} src={faviconSrc.src} />}
       >
-        <Button
-          type="primary"
-          style={{ marginBottom: 10 }}
-          onClick={async () => {
-            chatStore.clearSessions();
-            chatStore.updateCurrentSession((value) => {
-              value.appId = globalStore.selectedAppId;
-            });
-            navigate({ pathname: '/newChat' });
-            await fetchChatList()
-          }}
-        >
-          新建对话
-        </Button>
+        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}>
+          <Button
+            style={{ width: '48%' }}
+            onClick={() => {
+              navigate({ pathname: '/' });
+            }}
+          >
+            回到首页
+          </Button>
+          <Button
+            style={{ width: '48%' }}
+            type="primary"
+            onClick={async () => {
+              chatStore.clearSessions();
+              chatStore.updateCurrentSession((value) => {
+                value.appId = globalStore.selectedAppId;
+              });
+              if (['/knowledgeChat', '/newChat'].includes(location.pathname)) {
+                navigate({ pathname: '/newChat' });
+              } else if (['/deepseekChat', '/newDeepseekChat'].includes(location.pathname)) {
+                navigate({ pathname: '/newDeepseekChat' });
+              }
+              await fetchChatList()
+            }}
+          >
+            新建对话
+          </Button>
+        </div>
       </SideBarHeader>
       <Menu
         style={{ border: 'none' }}
@@ -346,7 +366,11 @@ export const SideBar = (props: { className?: string }) => {
             value.id = session.id;
             value.messages = list;
           });
-          navigate({ pathname: '/newChat' }, { state: { fromHome: true } });
+          if (['/knowledgeChat', '/newChat'].includes(location.pathname)) {
+            navigate({ pathname: '/newChat' });
+          } else if (['/deepseekChat', '/newDeepseekChat'].includes(location.pathname)) {
+            navigate({ pathname: '/newDeepseekChat' });
+          }
         }}
         mode="inline"
         items={menuList}