Prechádzať zdrojové kódy

Merge branch 'app-permission' of https://git.zyuas.com/LLM/chat-client-web into app-permission

sunsheng 3 týždňov pred
rodič
commit
86d4ae2c75

+ 2 - 1
app/client/platforms/deepSeek.ts

@@ -63,7 +63,8 @@ export class DeepSeekApi implements LLMApi {
     // 参数
     const params = {
       // model: 'DeepSeek-R1-Distill-Qwen-14B',
-      model: 'Qwen3-30B',
+      // model: 'Qwen3-30B',
+      model: 'Qwen3-30B-vl-chat',
       enable_think: isDeepThink,
       messages: userMessages,
       stream: true,

+ 135 - 46
app/components/DeekSeekHome.tsx

@@ -1,7 +1,7 @@
 import * as React from 'react';
 import { useNavigate } from "react-router-dom";
-import { Dropdown, Spin,Tooltip } from 'antd';
-import { Chat } from './DeepSeekChat';
+import { Dropdown, Spin, Tooltip } from 'antd';
+import { Chat } from './DeepSeekHomeOnlyChat';
 import whiteLogo from "../icons/whiteLogo.png";
 import jkxz from "../icons/jkxz.png";
 import { useChatStore } from "../store";
@@ -16,6 +16,8 @@ const DeekSeek: React.FC = () => {
     const navigate = useNavigate();
 
     const [listLoading, setListLoading] = React.useState(false);
+    const [hideTitleOnMobile, setHideTitleOnMobile] = React.useState(false);
+    const [activeNavIndex, setActiveNavIndex] = React.useState<number | null>(null);
 
     type List = {
         title: string,
@@ -52,6 +54,7 @@ const DeekSeek: React.FC = () => {
             ],
             "title": "专业知识"
         },
+
         {
             "children": [
                 {
@@ -75,6 +78,7 @@ const DeekSeek: React.FC = () => {
             ],
             "title": "职能管理"
         },
+        
         {
             "children": [],
             "title": "项目级应用"
@@ -102,19 +106,58 @@ const DeekSeek: React.FC = () => {
         }
     }, []);
 
+    const formatMenuTitle = React.useCallback(
+        (title: string) => {
+            if (isMobileScreen) {
+                return title;
+            }
+            return title.length > 7 ? `${title.slice(0, 7)}...` : title;
+        },
+        [isMobileScreen]
+    );
+
+    const dropdownAlign = React.useMemo(() => {
+        if (isMobileScreen) {
+            return undefined;
+        }
+        return {
+            points: ['tl', 'bl'],
+            offset: [0, 8],
+            overflow: {
+                adjustY: false,
+                adjustX: true,
+            },
+        };
+    }, [isMobileScreen]);
+
     return (
         <Spin spinning={listLoading}>
             <div className='deekSeek'>
-                <div className='deekSeek-header' style={{ justifyContent: isMobileScreen ? 'flex-start' : 'center' }}>
-                    <div style={{ display: 'flex', alignItems: 'center', margin: '0 20px' }}>
-                        <img src={whiteLogo.src} style={{ width: 20, marginRight: 10 }} />
-                        <div style={{ whiteSpace: 'nowrap' }}>
-                            上海建科
-                        </div>
+                <div className='deekSeek-header' style={{ justifyContent: isMobileScreen ? "space-between" : "center" }}>
+                    <div
+                        style={{
+                            display: 'flex',
+                            alignItems: 'center',
+                            margin: isMobileScreen ? 0 : '0 20px',
+                            fontSize: isMobileScreen ? undefined : 14,
+                            fontWeight: isMobileScreen ? undefined : 600,
+                        }}
+                    >
+                        <img src={whiteLogo.src} style={{ width: 20, marginRight: isMobileScreen ? 0 : 10 }} />
+                        {!isMobileScreen && (
+                            <div style={{ whiteSpace: 'nowrap' }}>
+                                上海建科
+                            </div>
+                        )}
                     </div>
                     {
                         list.map((item, index) => {
                             return <Dropdown
+                                onOpenChange={(open) => {
+                                    if (open) {
+                                        setActiveNavIndex(index);
+                                    }
+                                }}
                                 menu={{
                                     items: item.children.map((child, i) => {
                                         return {
@@ -131,35 +174,57 @@ const DeekSeek: React.FC = () => {
                                                     }
                                                 }}
                                             >
-                                                <Tooltip placement="left" title={child.title}>
-                                                    {child.title} 
+                                                <Tooltip placement="top" title={child.title}>
+                                                    {formatMenuTitle(child.title)}
                                                 </Tooltip>
                                             </div>,
                                             children: child.children ? child.children.map((record, ind) => {
                                                 return {
                                                     key: 'record' + ind,
-                                                    label: <div
-                                                        onClick={() => {
-                                                            const search = `?showMenu=${record.showMenu}&chatMode=${record.chatMode}&appId=${record.appId}`;
-                                                            if (record.appId) {
-                                                                navigate({
-                                                                    pathname: '/knowledgeChat',
-                                                                    search: search,
-                                                                })
-                                                            }
-                                                        }}
-                                                    >
-                                                        {record.title}
-                                                    </div>
+                                                    label: (
+                                                        <Tooltip placement="bottom" title={record.title}>
+                                                            <div
+                                                                onClick={() => {
+                                                                    const search = `?showMenu=${record.showMenu}&chatMode=${record.chatMode}&appId=${record.appId}`;
+                                                                    if (record.appId) {
+                                                                        navigate({
+                                                                            pathname: '/knowledgeChat',
+                                                                            search: search,
+                                                                        })
+                                                                    }
+                                                                }}
+                                                            >
+                                                                {formatMenuTitle(record.title)}
+                                                            </div>
+                                                        </Tooltip>
+                                                    )
                                                 };
                                             }) : undefined,
                                         };
                                     })
                                 }}
+                                overlayClassName="deekSeek-dropdown"
+                                placement="bottomLeft"
+                                align={dropdownAlign}
+                                getPopupContainer={(triggerNode) => {
+                                    if (isMobileScreen) {
+                                        return document.body;
+                                    }
+                                    return triggerNode.parentElement || document.body;
+                                }}
                                 key={index}
                             >
-                                <div style={{ whiteSpace: 'nowrap', marginRight: 20, color: '#fff', cursor: 'pointer' }}>
-                                    {item.title}
+                                <div
+                                    className={`deekSeek-header__item ${activeNavIndex === index ? 'active' : ''}`}
+                                    style={{
+                                        marginRight: isMobileScreen ? 0 : 20,
+                                        fontSize: isMobileScreen ? undefined : 14,
+                                        fontWeight: isMobileScreen ? undefined : 600,
+                                    }}
+                                    title={item.title}
+                                    onClick={() => setActiveNavIndex(index)}
+                                >
+                                    {formatMenuTitle(item.title)}
                                 </div>
                             </Dropdown>
                         })
@@ -173,30 +238,54 @@ const DeekSeek: React.FC = () => {
                     {/*</div>*/}
 
                     {/* 右侧区域 - 开放平台按钮 */}
-                    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
-                        <button
-                            className='open-platform-btn'
-                            onClick={() => {
-                                // 跳转到其他平台的逻辑,这里使用window.open举例
-                                const userInfo = localStorage.getItem('userInfo');
-                                const tokenParam = userInfo ? "?token=" + JSON.parse(userInfo).token : "";
-                                window.open('https://llm.jkec.info:11431/deepseek/questionAnswer' + tokenParam, '_blank');
-                            }}
-                        >
-                            更多
-                        </button>
-                    </div>
+                    {!isMobileScreen && (
+                        <div style={{ display: 'flex', alignItems: 'center', marginRight: 20 }}>
+                            <button
+                                className='open-platform-btn'
+                                onClick={() => {
+                                    // 跳转到其他平台的逻辑,这里使用window.open举例
+                                    const userInfo = localStorage.getItem('userInfo');
+                                    const tokenParam = userInfo ? "?token=" + JSON.parse(userInfo).token : "";
+                                    window.open('https://llm.jkec.info:11431/deepseek/questionAnswer' + tokenParam, '_blank');
+                                }}
+                            >
+                                更多
+                            </button>
+                        </div>
+                    )}
                 </div>
                 <div className='deekSeek-content'>
-                    <div className='deekSeek-content-title'>
-                        <img src={jkxz.src} />
-                    </div>
-                    <div className='deekSeek-content-title-sm' style={{ marginBottom: isMobileScreen ? 14 : 20 }}>
-                        智能问答助手
-                    </div>
-                    <div className={isMobileScreen ? 'deekSeek-content-mobile' : 'deekSeek-content-pc'}>
-                        <Chat />
+                    {(!isMobileScreen || !hideTitleOnMobile) && (
+                        <>
+                            <div className='deekSeek-content-title'>
+                                <img src={jkxz.src} />
+                            </div>
+                            <div className='deekSeek-content-title-sm' style={{ marginBottom: isMobileScreen ? 14 : 20 }}>
+                                智能问答助手
+                            </div>
+                        </>
+                    )}
+                    <div 
+                        className={isMobileScreen ? 'deekSeek-content-mobile' : 'deekSeek-content-pc'} 
+                        style={isMobileScreen && hideTitleOnMobile ? { height: 'calc(100% - 16px)', marginTop: 16 } : undefined}
+                    >
+                        <Chat onMessageSent={() => {
+                            if (isMobileScreen) {
+                                setHideTitleOnMobile(true);
+                            }
+                        }} />
                     </div>
+                    {isMobileScreen && (
+                        <div style={{ 
+                            marginTop: 8, 
+                            textAlign: 'center', 
+                            color: '#FFFFFF', 
+                            fontSize: 12,
+                            opacity: 0.8
+                        }}>
+                            内容由AI生成,仅供参考
+                        </div>
+                    )}
                 </div>
             </div>
         </Spin>

+ 27 - 16
app/components/DeepSeekChat.tsx

@@ -15,7 +15,7 @@ import { useNavigate, useLocation } from "react-router-dom";
 // 本地组件和工具导入
 import { IconButton } from "./button";
 import { MaskAvatar } from "./mask";
-import styles from "./chat.module.scss";
+import styles from "./chatHomeOnly.module.scss";
 
 // 图标资源导入
 import LeftIcon from "../icons/left.svg";
@@ -801,7 +801,7 @@ export function DeleteImageButton( props : { deleteImage : () => void } ) {
   );
 }
 
-function _Chat() {
+function _Chat(props?: { onMessageSent?: () => void }) {
   type RenderMessage = ChatMessage & { preview? : boolean };
   
   const chatStore = useChatStore();
@@ -852,13 +852,13 @@ function _Chat() {
     } );
   }, [] )
   
-  const [ inputRows, setInputRows ] = useState( 2 );
+  const [ inputRows, setInputRows ] = useState( isMobileScreen ? 1 : 2 );
   const measure = useDebouncedCallback(
       () => {
         const rows = inputRef.current ? autoGrowTextArea( inputRef.current ) : 1;
         const inputRows = Math.min(
             20,
-            Math.max( 2 + Number( !isMobileScreen ), rows ),
+            Math.max( isMobileScreen ? 1 : (2 + Number( !isMobileScreen )), rows ),
         );
         setInputRows( inputRows );
       },
@@ -920,8 +920,11 @@ function _Chat() {
     localStorage.setItem( LAST_INPUT_KEY, userInput );
     setUserInput( "" );
     setPromptHints( [] );
-    if ( !isMobileScreen ) inputRef.current?.focus();
+    // 取消输入框的选定状态
+    inputRef.current?.blur();
     setAutoScroll( true );
+    // 通知父组件消息已发送
+    props?.onMessageSent?.();
   };
   
   const onPromptSelect = ( prompt : RenderPrompt ) => {
@@ -1463,6 +1466,8 @@ function _Chat() {
     )
   }
   
+  const location = useLocation();
+  
   return (
       <div className={ styles.chat } key={ session.id }>
         {
@@ -1475,7 +1480,7 @@ function _Chat() {
                       style={ { padding: 0, marginRight: 20 } }
                       icon={ <LeftIcon /> }
                       text={ Locale.NewChat.Return }
-                      onClick={ () => navigate( '/deepseekChat' ) }
+                      onClick={ () => navigate( '/' ) }
                   />
                 </div>
               </div>
@@ -1777,7 +1782,7 @@ function _Chat() {
                 </Tooltip>}
                 {/*联网搜索按钮*/ }
                 <div style={ {
-                  padding: '0 12px',
+                  padding: isMobileScreen ? '0 8px' : '0 12px',
                   height: 28,
                   borderRadius: 18,
                   fontSize: 12,
@@ -1802,9 +1807,11 @@ function _Chat() {
                          height: 16,
                        } }
                   />
-                  <span style={ { fontSize: 11, marginLeft: 5, marginRight: 10 } }>
-                    联网搜索
-                  </span>
+                  {!isMobileScreen && (
+                    <span style={ { fontSize: 11, marginLeft: 5, marginRight: 10 } }>
+                      联网搜索
+                    </span>
+                  )}
                 </div>
               </div>
               
@@ -1895,7 +1902,9 @@ function _Chat() {
                       alignItems: 'center',
                       cursor: 'pointer',
                     } }
-                    onClick={ () => {
+                    onClick={ ( e ) => {
+                      e.preventDefault();
+                      e.stopPropagation();
                       if ( couldStop ) {
                         stopAll();
                       } else {
@@ -1917,9 +1926,11 @@ function _Chat() {
             </div>
           </label>
           
-          <div style={ { marginTop: 8, textAlign: 'center', color: '#888888', fontSize: 12 } }>
-            内容由AI生成,仅供参考
-          </div>
+          
+            <div style={ { marginTop: 8, textAlign: 'center', color: '#888888', fontSize: 12 } }>
+              内容由AI生成,仅供参考
+            </div>
+         
         </div>
         {
             showExport && (
@@ -1939,7 +1950,7 @@ function _Chat() {
   );
 }
 
-export function Chat() {
+export function Chat(props?: { onMessageSent?: () => void }) {
   const globalStore = useGlobalStore();
   const chatStore = useChatStore();
   const sessionIndex = chatStore.currentSessionIndex;
@@ -1950,5 +1961,5 @@ export function Chat() {
     chatStore.setWebSearch( false );
   }, [] );
   
-  return <_Chat key={ sessionIndex }></_Chat>;
+  return <_Chat key={ sessionIndex } onMessageSent={props?.onMessageSent}></_Chat>;
 }

+ 1966 - 0
app/components/DeepSeekHomeOnlyChat.tsx

@@ -0,0 +1,1966 @@
+// 第三方库导入
+import { useDebouncedCallback } from "use-debounce";
+import React, {
+  useState,
+  useRef,
+  useEffect,
+  useMemo,
+  useCallback,
+  Fragment,
+  RefObject,
+} from "react";
+import dynamic from "next/dynamic";
+import { useNavigate, useLocation } from "react-router-dom";
+
+// 本地组件和工具导入
+import { IconButton } from "./button";
+import { MaskAvatar } from "./mask";
+import styles from "./chatHomeOnly.module.scss";
+
+// 图标资源导入
+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";
+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 sdsk from "../icons/sdsk.png";
+import sdsk_selected from "../icons/sdsk_selected.png";
+import hlw from "../icons/hlw.png";
+import hlw_selected from "../icons/hlw_selected.png";
+import BotIcon from "../icons/bot.svg";
+import BlackBotIcon from "../icons/black-bot.svg";
+
+// 状态管理和类型导入
+import {
+  ChatMessage,
+  SubmitKey,
+  useChatStore,
+  useAccessStore,
+  Theme,
+  useAppConfig,
+  DEFAULT_TOPIC,
+  ModelType,
+  useGlobalStore,
+} from "../store";
+import { Prompt, usePromptStore } from "../store/prompt";
+
+// 工具函数导入
+import {
+  copyToClipboard,
+  selectOrCopy,
+  autoGrowTextArea,
+  useMobileScreen,
+  getMessageTextContent,
+  getMessageImages,
+  isVisionModel,
+  isDalle3,
+} from "../utils";
+import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
+
+// 客户端和类型导入
+import { ChatControllerPool } from "../client/controller";
+import { DalleSize } from "../typing";
+import type { RequestMessage } from "../client/api";
+
+// UI 组件导入
+import {
+  List,
+  ListItem,
+  Modal,
+  Selector,
+  showConfirm,
+  showToast,
+} from "./ui-lib";
+
+// 常量和本地化
+import Locale from "../locales";
+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";
+import { message, Upload, UploadProps, Tooltip, Drawer, Button } from "antd";
+import {
+  PaperClipOutlined,
+  SendOutlined,
+  FileOutlined,
+  FilePdfOutlined,
+  FileTextOutlined,
+  FileWordOutlined,
+  RightOutlined
+} from '@ant-design/icons';
+
+// Avatar组件替代实现
+function Avatar( props : { model? : string; avatar? : string } ) {
+  if ( props.model ) {
+    return (
+        <div className="no-dark">
+          { props.model?.startsWith( "gpt-4" ) ? (
+              <BlackBotIcon className="user-avatar" />
+          ) : (
+              <BotIcon className="user-avatar" />
+          ) }
+        </div>
+    );
+  }
+  
+  return (
+      <div className="user-avatar">
+        {/* 移除emoji头像,使用默认bot图标 */ }
+        <BotIcon className="user-avatar" />
+      </div>
+  );
+}
+
+export function createMessage( override : Partial<ChatMessage> ) : ChatMessage {
+  return {
+    id: nanoid(),
+    date: new Date().toLocaleString(),
+    role: "user",
+    content: "",
+    ...override,
+  };
+}
+
+export const BOT_HELLO : ChatMessage = createMessage( {
+  role: "assistant",
+  content: '你好,我是小智~\n' +
+      '我可以帮助你快速查询作业指导书、规范条文、公司信息等内容,如需获取上述内容,请点击上方导航栏中的「专业知识」或「职能管理」,选择相应的智能体进行提问。无论是现场技术,还是制度流程,我都会尽力为你解答!\n' +
+      '请注意:在这个对话框内,我只能请DeepSeek来帮忙回答常见通用问题哦!',
+} );
+
+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(props?: { onMessageSent?: () => void }) {
+  type RenderMessage = ChatMessage & { preview? : boolean };
+  
+  const chatStore = useChatStore();
+  const session = chatStore.currentSession();
+  const config = useAppConfig();
+  config.sendPreviewBubble = false;
+  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 },
+  );
+  
+  useEffect( () => {
+    chatStore.updateCurrentSession( ( session ) => {
+      session.appId = '1881269958412521255';
+    } );
+  }, [] )
+  
+  const [ inputRows, setInputRows ] = useState( isMobileScreen ? 1 : 2 );
+  const measure = useDebouncedCallback(
+      () => {
+        const rows = inputRef.current ? autoGrowTextArea( inputRef.current ) : 1;
+        const inputRows = Math.min(
+            20,
+            Math.max( isMobileScreen ? 1 : (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.MaskChat),  // 关闭mask入口 ,后续有需求再二开
+    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( fileList, userInput, attachImages ).then( () => setIsLoading( false ) );
+    setAttachImages( [] );
+    localStorage.setItem( LAST_INPUT_KEY, userInput );
+    setUserInput( "" );
+    setPromptHints( [] );
+    // 取消输入框的选定状态
+    inputRef.current?.blur();
+    setAutoScroll( true );
+    // 通知父组件消息已发送
+    props?.onMessageSent?.();
+  };
+  
+  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 );
+  }
+  
+  const [ fileList, setFileList ] = useState<any[]>( [] );
+  
+  // 上传配置
+  const uploadConfig : UploadProps = {
+    action: '/deepseek-api' + '/upload/file',
+    method: 'POST',
+    accept: [ '.pdf', '.txt', '.doc', '.docx' ].join( ',' ),
+  };
+  
+  interface FileIconProps {
+    fileName : string;
+  }
+  
+  const FileIcon : React.FC<FileIconProps> = ( props : FileIconProps ) => {
+    const style = {
+      fontSize: '30px',
+      color: '#3875f6',
+    }
+    
+    let icon = <FileOutlined style={ style } />
+    if ( props.fileName ) {
+      const suffix = props.fileName.split( '.' ).pop() || '';
+      switch ( suffix ) {
+        case 'pdf':
+          icon = <FilePdfOutlined style={ style } />
+          break;
+        case 'txt':
+          icon = <FileTextOutlined style={ style } />
+          break;
+        case 'doc':
+        case 'docx':
+          icon = <FileWordOutlined style={ style } />
+          break;
+        default:
+          break;
+      }
+    }
+    return icon;
+  }
+  
+  const [ isDeepThink, setIsDeepThink ] = useState<boolean>( chatStore.isDeepThink );
+  
+  // 切换聊天窗口后清理上传文件信息
+  useEffect( () => {
+    setFileList( [] )
+  }, [ chatStore.currentSession() ] )
+  
+  const couldStop = ChatControllerPool.hasPending();
+  const stopAll = () => ChatControllerPool.stopAll();
+  
+  // 切换聊天窗口后清理上传文件信息
+  useEffect( () => {
+    setWebSearch( false );
+  }, [ chatStore.currentSession() ] )
+  
+  const [ webSearch, setWebSearch ] = useState<boolean>( chatStore.web_search );
+  
+  const [ drawerOpen, setDrawerOpen ] = useState( false );
+  type DrawerList = {
+    title : string,
+    content : string,
+    web_url : string,
+  }[]
+  const [ drawerList, setDrawerList ] = useState<DrawerList>( [] );
+  
+  interface NetworkDrawerProps {
+    list : DrawerList,
+  }
+  
+  const NetworkDrawer : React.FC<NetworkDrawerProps> = ( props ) => {
+    return (
+        <Drawer
+            title='网页搜索'
+            open={ drawerOpen }
+            onClose={ () => {
+              setDrawerOpen( false );
+            } }
+        >
+          { props.list.map( ( item, index ) => {
+            return <div
+                style={ {
+                  padding: 10,
+                  background: '#fafafa',
+                  borderRadius: 4,
+                  marginBottom: 10,
+                  cursor: 'pointer',
+                } }
+                key={ index }
+                onClick={ () => {
+                  window.open( item.web_url );
+                } }
+            >
+              <div style={ {
+                margin: '5px 0',
+                fontSize: 16,
+                display: '-webkit-box',
+                WebkitBoxOrient: 'vertical',
+                WebkitLineClamp: 2,// 限制显示两行
+                overflow: 'hidden',
+              } }>
+                { item.title }
+              </div>
+              <div style={ {
+                color: '#afafaf',
+                display: '-webkit-box',
+                WebkitBoxOrient: 'vertical',
+                WebkitLineClamp: 4,// 限制显示两行
+                overflow: 'hidden',
+                textOverflow: 'ellipsis',
+              } }>
+                { item.content }
+              </div>
+            </div>
+          } )
+          }
+        </Drawer>
+    )
+  }
+  
+  const location = useLocation();
+  
+  return (
+      <div className={ styles.chat } key={ session.id }>
+        {
+            isMobileScreen && location.pathname !== '/' &&
+            <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 }
+            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: 'column' } }>
+                        <div className={ styles[ "chat-message-header" ] }>
+                          <div className={ styles[ "chat-message-avatar" ] }>
+                            { isUser ? (
+                                // 在这里换头像
+                                <div style={ { position: 'relative' } }>
+                                  <div
+                                      style={ {
+                                        position: 'absolute',
+                                        zIndex: 2,
+                                        top: '50%',
+                                        left: '50%',
+                                        transform: ' translate(-110%, -100%)',
+                                        fontSize: 14,
+                                      } }>
+                                    我
+                                  </div>
+                                </div>
+                            ) : (
+                                <>
+                                  { [ "system" ].includes( message.role ) ? (
+                                      <Avatar avatar="2699-fe0f" />
+                                  ) : (
+                                      <MaskAvatar
+                                          avatar={ session.mask.avatar }
+                                          model={
+                                              message.model || session.mask.modelConfig.model
+                                          }
+                                      />
+                                  ) }
+                                </>
+                            ) }
+                          </div>
+                        </div>
+                        {
+                            isUser && message.document && message.document.id &&
+                            <a style={ {
+                              padding: '10px',
+                              background: '#f7f7f7',
+                              borderRadius: '10px',
+                              textDecoration: 'none',
+                              color: '#24292f',
+                              display: 'flex',
+                              alignItems: 'center'
+                            } } href={ message.document.url } target="_blank">
+                              <FileIcon fileName={ message.document.name } />
+                              <div style={ { marginLeft: 8, fontSize: '14px' } }>
+                                { message.document.name }
+                              </div>
+                            </a>
+                        }
+                        {/* {showTyping && (
+                      <div className={styles["chat-message-status"]}>
+                        正在输入…
+                      </div>
+                    )} */ }
+                        {
+                            message.networkInfo && message.networkInfo.list.length > 0 &&
+                            <div style={ { marginTop: 10 } }>
+                              <Button
+                                  icon={ <RightOutlined /> }
+                                  iconPosition='end'
+                                  onClick={ () => {
+                                    setDrawerList( message.networkInfo!.list );
+                                    setDrawerOpen( true );
+                                  } }
+                              >
+                                搜索到{ message.networkInfo.list.length }篇相关资料
+                              </Button>
+                              {
+                                  drawerOpen &&
+                                  <NetworkDrawer
+                                      list={ message.networkInfo.list }
+                                  />
+                              }
+                            </div>
+                        }
+                        <div className={ styles[ "chat-message-item" ] }>
+                          <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( "" );
+              } }
+          />
+          {
+              fileList.length > 0 &&
+              <div style={ { marginBottom: 20 } }>
+                <Upload
+                    fileList={ fileList }
+                    onRemove={ ( file ) => {
+                      setFileList( fileList.filter( item => item.uid !== file.uid ) );
+                    } }
+                />
+              </div>
+          }
+          <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>
+            ) }
+            {/* 修改样式:输入框内部按钮区域 */ }
+            {/* <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 10 }}> */ }
+            <div className={ styles[ "chat-input-bottom-bar" ] }>
+              {/* <div style={{ display: 'flex', alignItems: 'center' }}> */ }
+              <div className={ styles[ "left-options" ] }>
+                
+                {/*深度思考R1按钮*/ }
+                
+               {false&&<Tooltip
+                    title={
+                      <span style={ { fontSize: 12, lineHeight: 1.4, minHeight: 24, padding: '4px 8px' } }>
+                    { isDeepThink ? '关闭深度思考模式' : '启用深度思考模式' }
+                  </span>
+                    }
+                    placement="left"
+                >
+                  <div
+                      // className={styles["option-item"]}
+                      style={ {
+                        padding: '0 12px',
+                        height: 28,
+                        borderRadius: 18,
+                        fontSize: 12,
+                        display: 'flex',
+                        justifyContent: 'center',
+                        alignItems: 'center',
+                        // marginRight: 10,
+                        cursor: 'pointer',
+                        background: isDeepThink ? '#dee9fc' : '#f3f4f6',
+                        color: isDeepThink ? '#3875f6' : '#000000',
+                        // border: `1px solid ${isDeepThink ? '#3875f6' : 'transparent'}`,
+                        transition: 'all 0.2s ease',
+                        userSelect: 'none'
+                      } }
+                      onClick={ () => {
+                        setIsDeepThink( !isDeepThink );
+                        chatStore.setIsDeepThink( !isDeepThink );
+                      } }
+                  >
+                    <img src={ isDeepThink ? sdsk_selected.src : sdsk.src }
+                         style={ {
+                           width: 16,
+                           height: 16,
+                         } }
+                    />
+                    <span style={ { fontSize: 11, marginLeft: 5 } }>
+                      深度思考
+                    </span>
+                  </div>
+                </Tooltip>}
+                {/*联网搜索按钮*/ }
+                <div style={ {
+                  padding: isMobileScreen ? '0 8px' : '0 12px',
+                  height: 28,
+                  borderRadius: 18,
+                  fontSize: 12,
+                  display: 'flex',
+                  justifyContent: 'center',
+                  alignItems: 'center',
+                  cursor: 'pointer',
+                  background: webSearch ? '#dee9fc' : '#f3f4f6',
+                  color: webSearch ? '#3875f6' : '#000000',
+                  transition: 'all 0.2s ease',
+                  userSelect: 'none'
+                } }
+                     onClick={ () => {
+                       setWebSearch( !webSearch );
+                       chatStore.setWebSearch( !webSearch );
+                     } }
+                >
+                  
+                  <img src={ webSearch ? hlw_selected.src : hlw.src }
+                       style={ {
+                         width: 16,
+                         height: 16,
+                       } }
+                  />
+                  {!isMobileScreen && (
+                    <span style={ { fontSize: 11, marginLeft: 5, marginRight: 10 } }>
+                      联网搜索
+                    </span>
+                  )}
+                </div>
+              </div>
+              
+              <div style={ { display: 'flex', alignItems: 'center' } }>
+                {
+                    !webSearch &&
+                    <div style={ { marginRight: 10 } }>
+                      <Upload
+                          { ...uploadConfig }
+                          showUploadList={ false }
+                          maxCount={ 1 }
+                          onChange={ ( info ) => {
+                            const fileList = info.fileList.map( ( file ) => {
+                              const data = file.response;
+                              return {
+                                ...file,
+                                url: data?.document_url || file.url,
+                                documentId: data?.document_id || '',
+                              }
+                            } );
+                            setFileList( fileList );
+                            if ( info.file.status === 'done' ) {// 上传成功
+                              const { code, message: msg } = info.file.response;
+                              if ( code === 200 ) {
+                                message.success( '上传成功' );
+                              } else {
+                                message.error( msg );
+                              }
+                            } else if ( info.file.status === 'error' ) {// 上传失败
+                              message.error( '上传失败' );
+                            }
+                          } }
+                      >
+                        <Tooltip
+                            title={
+                              <div style={ { padding: '4px 8px' } }>
+                                <div style={ {
+                                  fontSize: 12,
+                                  lineHeight: 1.4,
+                                  marginBottom: 6,
+                                } }>
+                                  上传附件 (识别文本和图表中的内容)
+                                </div>
+                                <div style={ {
+                                  fontSize: 10,
+                                  color: '#8c8c8c',
+                                  lineHeight: 1.4,
+                            }}>
+                              <span>
+                              仅支持单个PDF/Word/TXT文件格式
+                              </span>
+                              <span>
+                              (单个文件≤50MB)
+                              </span>
+
+                              </div>
+                              </div>}
+                            placement="top"
+                        >
+                          <div
+                              style={ {
+                                width: 28,
+                                height: 28,
+                                borderRadius: '50%',
+                                background: '#4357d2',
+                                display: 'flex',
+                                justifyContent: 'center',
+                                alignItems: 'center',
+                                cursor: 'pointer',
+                                transition: 'all 0.2s ease',
+                                userSelect: 'none'
+                              } }
+                          >
+                            <PaperClipOutlined style={ { color: '#FFFFFF', fontSize: '18px' } } />
+                          </div>
+                        </Tooltip>
+                      </Upload>
+                    </div>
+                }
+                <div
+                    style={ {
+                      width: 28,
+                      height: 28,
+                      borderRadius: '50%',
+                      background: '#4357d2',
+                      display: 'flex',
+                      justifyContent: 'center',
+                      alignItems: 'center',
+                      cursor: 'pointer',
+                    } }
+                    onClick={ ( e ) => {
+                      e.preventDefault();
+                      e.stopPropagation();
+                      if ( couldStop ) {
+                        stopAll();
+                      } else {
+                        doSubmit( userInput );
+                      }
+                    } }
+                >
+                  {
+                    couldStop ?
+                        <div style={ { width: 13, height: 13, background: '#FFFFFF', borderRadius: 2 } }></div>
+                        :
+                        <div style={ { transform: 'rotate(-45deg)', padding: '0px 0px 3px 5px' } }>
+                          <SendOutlined style={ { color: '#FFFFFF' } } />
+                        </div>
+                  }
+                
+                </div>
+              </div>
+            </div>
+          </label>
+          
+          {!isMobileScreen && (
+            <div style={ { marginTop: 8, textAlign: 'center', color: '#888888', fontSize: 12 } }>
+              内容由AI生成,仅供参考
+            </div>
+          )}
+        </div>
+        {
+            showExport && (
+                <ExportMessageModal onClose={ () => setShowExport( false ) } />
+            )
+        }
+        {
+            isEditingMessage && (
+                <EditMessageModal
+                    onClose={ () => {
+                      setIsEditingMessage( false );
+                    } }
+                />
+            )
+        }
+      </div>
+  );
+}
+
+export function Chat(props?: { onMessageSent?: () => void }) {
+  const globalStore = useGlobalStore();
+  const chatStore = useChatStore();
+  const sessionIndex = chatStore.currentSessionIndex;
+  
+  useEffect( () => {
+    globalStore.setShowMenu( true );
+    chatStore.setModel( 'DeepSeek' );
+    chatStore.setWebSearch( false );
+  }, [] );
+  
+  return <_Chat key={ sessionIndex } onMessageSent={props?.onMessageSent}></_Chat>;
+}
+

+ 21 - 0
app/components/chat-custom.module.scss

@@ -0,0 +1,21 @@
+// 自定义样式文件,专门用于 chat.tsx 组件
+// 导入原始样式作为基础
+@import "./chat.module.scss";
+
+// 移动端适配
+@media only screen and (max-width: 600px) {
+  // 移动端输入框容器高度调整
+  .chat-input-panel-inner {
+    min-height: 50px !important;
+    max-height: 50px !important;
+  }
+
+  // 移动端输入框高度调整为一行
+  .chat-input {
+    min-height: 50px !important;
+    padding: 14px 60px 14px 16px !important; // 右侧留出发送按钮空间
+    font-size: 16px !important;
+    line-height: 22px !important;
+  }
+}
+

+ 37 - 9
app/components/chat.module.scss

@@ -280,6 +280,7 @@
   flex-direction: column;
   position: relative;
   height: 100%;
+  background-color: #fdfdfd;
   // background-image: url("/chat-bg.jpg");
   /* 使背景图片按比例填充容器 */
   background-size: cover;
@@ -295,6 +296,16 @@
   overflow-x: hidden;
   padding: 20px;
   padding-bottom: 40px;
+  
+  // 移动端隐藏滚动条
+  @media only screen and (max-width: 600px) {
+    scrollbar-width: none; /* Firefox */
+    -ms-overflow-style: none; /* IE and Edge */
+    
+    &::-webkit-scrollbar {
+      display: none; /* Chrome, Safari and Opera */
+    }
+  }
 }
 
 .chat-body-main-title {
@@ -314,6 +325,7 @@
 .chat-message {
   display: flex;
   flex-direction: row;
+  margin-bottom: 24px;
 
   &:last-child {
     animation: slide-in ease 0.3s;
@@ -323,6 +335,7 @@
 .chat-message-user {
   display: flex;
   flex-direction: row-reverse;
+  margin-bottom: 24px;
 
   .chat-message-header {
     flex-direction: row-reverse;
@@ -417,14 +430,16 @@
   max-width: 70%;
   margin-top: 10px;
   border-radius: 10px;
+  border-top-left-radius: 0; // AI消息左上角直角(靠近头像)
   background-color: #FFFFFF;
   padding: 10px;
   font-size: 14px;
   user-select: text;
   word-break: break-word;
-  border: var(--border-in-light);
+   border: var(--border-in-light);
   position: relative;
   transition: all ease 0.3s;
+  // box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
 }
 
 .chat-message-item-image {
@@ -501,6 +516,8 @@
 
 .chat-message-user>.chat-message-container>.chat-message-item {
   background-color: var(--second);
+  border-top-left-radius: 10px; // 恢复左上角圆角
+  border-top-right-radius: 0; // 用户消息右上角直角(靠近头像)
 
   &:hover {
     min-width: 0;
@@ -517,8 +534,8 @@
   background: transparent;
   // box-shadow: var(--card-shadow);
   // 下方线条
-  border-top: 1px solid #dedede;
-  // border-top: none;
+  // border-top: 1px solid #dedede;
+  border-top: none;
 
   // 背景
   // background: rgba(155, 155, 255, 0.2);
@@ -592,12 +609,14 @@
   flex-direction: column; // 垂直布局
   flex: 1;
   border-radius: 16px;
-  border: var(--border-in-light);
+  border: none;
   position: relative;
   // min-height: 100px;
   max-height: 120px;
   overflow-y: auto;
   overflow: hidden;
+  box-shadow: 0 4px 26px rgba(0, 0, 0, 0.12);
+  transition: box-shadow 0.3s ease;
 }
 
 .chat-input-panel-inner-attach {
@@ -605,11 +624,11 @@
 }
 
 .chat-input-panel-inner:has(.chat-input:focus) {
-  border: 1px solid var(--primary);
+  box-shadow: 0 6px 20px rgba(73, 95, 230, 0.2);
 }
 
 .chat-input-panel-inner:has(.chat-input2:focus) {
-  border-color: #495fe6;
+  box-shadow: 0 6px 20px rgba(73, 95, 230, 0.2);
 }
 
 .chat-input {
@@ -622,11 +641,11 @@
   box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
   color: var(--black) !important;
   font-family: inherit;
-  padding: 16px 16px 8px 16px;
+  padding: 12px 16px 0px;
   resize: none;
   outline: none;
   box-sizing: border-box;
-  min-height: 60px;
+  min-height: 50px;
   font-size: 14px;
   line-height: 1.5;
   // 文字颜色
@@ -635,6 +654,14 @@
 
 .chat-input2 {
   @extend .chat-input;
+  min-height: auto; // 允许输入框最小高度为1行
+  
+  // 移动端适配:允许输入框最小高度为1行
+  @media screen and (max-width: 600px) {
+    min-height: auto;
+    padding: 10px 16px 0px; // 减少移动端上下内边距
+    font-size: 16px; // 防止iOS自动缩放
+  }
 }
 
 // placeholder颜色
@@ -859,4 +886,5 @@
       justify-content: center !important;
     }
   }
-}
+}
+

+ 31 - 9
app/components/chat.tsx

@@ -80,7 +80,7 @@ import { useGlobalStore } from "../store";
 import Locale from "../locales";
 
 import { IconButton } from "./button";
-import styles from "./chat.module.scss";
+import styles from "./chat-custom.module.scss";
 
 import {
   List,
@@ -137,7 +137,8 @@ import {
   FileOutlined,
   FilePdfOutlined,
   FileTextOutlined,
-  FileWordOutlined
+  FileWordOutlined,
+  SendOutlined
 } from '@ant-design/icons';
 import { RightOutlined, CheckCircleOutlined, CaretRightOutlined, StarTwoTone } from '@ant-design/icons';
 import api from "@/app/api/api";
@@ -2342,12 +2343,24 @@ function _Chat() {
               })}
             </div>
           )}
-          <IconButton
-            icon={couldStop ? <div style={{ width: 13, height: 13, background: '#FFFFFF', borderRadius: 2 }}></div> : <SendWhiteIcon />}
-            text={couldStop ? '停止' : '发送'}
-            className={styles["chat-input-send"]}
-            type="primary"
-            onClick={() => {
+          <div
+            style={{
+              width: 28,
+              height: 28,
+              borderRadius: '50%',
+              background: '#4357d2',
+              display: 'flex',
+              justifyContent: 'center',
+              alignItems: 'center',
+              cursor: 'pointer',
+              position: 'absolute',
+              right: 16,
+              top: '50%',
+              transform: 'translateY(-50%)',
+            }}
+            onClick={(e) => {
+              e.preventDefault();
+              e.stopPropagation();
               if (couldStop) {
                 stopAll();
                 setIsClickStop(true);
@@ -2355,7 +2368,16 @@ function _Chat() {
                 doSubmit(userInput);
               }
             }}
-          />
+          >
+            {
+              couldStop ?
+                <div style={{ width: 13, height: 13, background: '#FFFFFF', borderRadius: 2 }}></div>
+                :
+                <div style={{ transform: 'rotate(-45deg)', padding: '0px 0px 3px 5px' }}>
+                  <SendOutlined style={{ color: '#FFFFFF' }} />
+                </div>
+            }
+          </div>
         </label>
       </div>
       <div style={{ paddingBottom: 12, textAlign: 'center', color: '#888888', fontSize: 12 }}>

+ 890 - 0
app/components/chatHomeOnly.module.scss

@@ -0,0 +1,890 @@
+@import "../styles/animation.scss";
+
+.attach-images {
+  position: absolute;
+  left: 30px;
+  bottom: 32px;
+  display: flex;
+}
+
+.attach-image {
+  cursor: default;
+  width: 64px;
+  height: 64px;
+  border: rgba($color: #888, $alpha: 0.2) 1px solid;
+  border-radius: 5px;
+  margin-right: 10px;
+  background-size: cover;
+  background-position: center;
+  background-color: var(--white);
+
+  .attach-image-mask {
+    width: 100%;
+    height: 100%;
+    opacity: 0;
+    transition: all ease 0.2s;
+  }
+
+  .attach-image-mask:hover {
+    opacity: 1;
+  }
+
+  .delete-image {
+    width: 24px;
+    height: 24px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 5px;
+    float: right;
+    background-color: var(--white);
+  }
+}
+
+.chat-input-actions {
+  display: flex;
+  flex-wrap: wrap;
+
+  .chat-input-action {
+    display: inline-flex;
+    border-radius: 20px;
+    font-size: 12px;
+    background-color: var(--white);
+    color: var(--black);
+    border: var(--border-in-light);
+    padding: 4px 10px;
+    animation: slide-in ease 0.3s;
+    box-shadow: var(--card-shadow);
+    transition: width ease 0.3s;
+    align-items: center;
+    height: 16px;
+    width: var(--icon-width);
+    overflow: hidden;
+
+    &:not(:last-child) {
+      margin-right: 5px;
+    }
+
+    .text {
+      white-space: nowrap;
+      padding-left: 5px;
+      opacity: 0;
+      transform: translateX(-5px);
+      transition: all ease 0.3s;
+      pointer-events: none;
+    }
+
+    &:hover {
+      --delay: 0.5s;
+      width: var(--full-width);
+      transition-delay: var(--delay);
+
+      .text {
+        transition-delay: var(--delay);
+        opacity: 1;
+        transform: translate(0);
+      }
+    }
+
+    .text,
+    .icon {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+}
+
+.prompt-toast {
+  position: absolute;
+  bottom: -50px;
+  z-index: 999;
+  display: flex;
+  justify-content: center;
+  width: calc(100% - 40px);
+
+  .prompt-toast-inner {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 12px;
+    background-color: var(--white);
+    color: var(--black);
+
+    border: var(--border-in-light);
+    box-shadow: var(--card-shadow);
+    padding: 10px 20px;
+    border-radius: 100px;
+
+    animation: slide-in-from-top ease 0.3s;
+
+    .prompt-toast-content {
+      margin-left: 10px;
+    }
+  }
+}
+
+.section-title {
+  font-size: 12px;
+  font-weight: bold;
+  margin-bottom: 10px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  .section-title-action {
+    display: flex;
+    align-items: center;
+  }
+}
+
+.context-prompt {
+  .context-prompt-insert {
+    display: flex;
+    justify-content: center;
+    padding: 4px;
+    opacity: 0.2;
+    transition: all ease 0.3s;
+    background-color: rgba(0, 0, 0, 0);
+    cursor: pointer;
+    border-radius: 4px;
+    margin-top: 4px;
+    margin-bottom: 4px;
+
+    &:hover {
+      opacity: 1;
+      background-color: rgba(0, 0, 0, 0.05);
+    }
+  }
+
+  .context-prompt-row {
+    display: flex;
+    justify-content: center;
+    width: 100%;
+
+    &:hover {
+      .context-drag {
+        opacity: 1;
+      }
+    }
+
+    .context-drag {
+      display: flex;
+      align-items: center;
+      opacity: 0.5;
+      transition: all ease 0.3s;
+    }
+
+    .context-role {
+      margin-right: 10px;
+    }
+
+    .context-content {
+      flex: 1;
+      max-width: 100%;
+      text-align: left;
+    }
+
+    .context-delete-button {
+      margin-left: 10px;
+    }
+  }
+
+  .context-prompt-button {
+    flex: 1;
+  }
+}
+
+.memory-prompt {
+  margin: 20px 0;
+
+  .memory-prompt-content {
+    background-color: var(--white);
+    color: var(--black);
+    border: var(--border-in-light);
+    border-radius: 10px;
+    padding: 10px;
+    font-size: 12px;
+    user-select: text;
+  }
+}
+
+.clear-context {
+  margin: 20px 0 0 0;
+  padding: 4px 0;
+
+  border-top: var(--border-in-light);
+  border-bottom: var(--border-in-light);
+  box-shadow: var(--card-shadow) inset;
+
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  color: var(--black);
+  transition: all ease 0.3s;
+  cursor: pointer;
+  overflow: hidden;
+  position: relative;
+  font-size: 12px;
+
+  animation: slide-in ease 0.3s;
+
+  $linear: linear-gradient(to right,
+      rgba(0, 0, 0, 0),
+      rgba(0, 0, 0, 1),
+      rgba(0, 0, 0, 0));
+  mask-image: $linear;
+
+  @mixin show {
+    transform: translateY(0);
+    position: relative;
+    transition: all ease 0.3s;
+    opacity: 1;
+  }
+
+  @mixin hide {
+    transform: translateY(-50%);
+    position: absolute;
+    transition: all ease 0.1s;
+    opacity: 0;
+  }
+
+  &-tips {
+    @include show;
+    opacity: 0.5;
+  }
+
+  &-revert-btn {
+    color: var(--primary);
+    @include hide;
+  }
+
+  &:hover {
+    opacity: 1;
+    border-color: var(--primary);
+
+    .clear-context-tips {
+      @include hide;
+    }
+
+    .clear-context-revert-btn {
+      @include show;
+    }
+  }
+}
+
+.chat {
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  height: 100%;
+  background-color: #fdfdfd;
+  // background-image: url("/chat-bg.jpg");
+  /* 使背景图片按比例填充容器 */
+  background-size: cover;
+  /* 居中显示背景图片 */
+  background-position: center;
+  /* 避免重复 */
+  background-repeat: no-repeat;
+}
+
+.chat-body {
+  flex: 1;
+  overflow: auto;
+  overflow-x: hidden;
+  padding: 20px;
+  padding-bottom: 40px;
+  
+  // 移动端隐藏滚动条
+  @media only screen and (max-width: 600px) {
+    scrollbar-width: none; /* Firefox */
+    -ms-overflow-style: none; /* IE and Edge */
+    
+    &::-webkit-scrollbar {
+      display: none; /* Chrome, Safari and Opera */
+    }
+  }
+}
+
+.chat-body-main-title {
+  cursor: pointer;
+
+  &:hover {
+    text-decoration: none;
+  }
+}
+
+@media only screen and (max-width: 600px) {
+  .chat-body-title {
+    text-align: center;
+  }
+}
+
+.chat-message {
+  display: flex;
+  flex-direction: row;
+  margin-bottom: 24px;
+
+  &:last-child {
+    animation: slide-in ease 0.3s;
+  }
+}
+
+.chat-message-user {
+  display: flex;
+  flex-direction: row-reverse;
+  margin-bottom: 24px;
+
+  .chat-message-header {
+    flex-direction: row-reverse;
+  }
+}
+
+.chat-message-header {
+  margin-top: 20px;
+  display: flex;
+  align-items: center;
+
+  .chat-message-actions {
+    display: flex;
+    box-sizing: border-box;
+    font-size: 12px;
+    align-items: flex-end;
+    justify-content: space-between;
+    transition: all ease 0.3s;
+    transform: scale(0.9) translateY(5px);
+    margin: 0 10px;
+    opacity: 0;
+    pointer-events: none;
+
+    .chat-input-actions {
+      display: flex;
+      flex-wrap: nowrap;
+    }
+  }
+}
+
+.chat-message-container {
+  max-width: var(--message-max-width);
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+
+  &:hover {
+    .chat-message-edit {
+      opacity: 0.9;
+    }
+
+    .chat-message-actions {
+      opacity: 1;
+      pointer-events: all;
+      transform: scale(1) translateY(0);
+    }
+  }
+}
+
+.chat-message-user>.chat-message-container {
+  align-items: flex-end;
+}
+
+.chat-message-avatar {
+  position: relative;
+
+  .chat-message-edit {
+    position: absolute;
+    height: 100%;
+    width: 100%;
+    overflow: hidden;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    transition: all ease 0.3s;
+
+    button {
+      padding: 7px;
+    }
+  }
+
+  /* Specific styles for iOS devices */
+  @media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
+    @supports (-webkit-touch-callout: none) {
+      .chat-message-edit {
+        top: -8%;
+      }
+    }
+  }
+}
+
+.chat-message-status {
+  font-size: 12px;
+  color: #aaa;
+  line-height: 1.5;
+  margin-top: 5px;
+}
+
+.chat-message-item {
+  box-sizing: border-box;
+  max-width: 100%;
+  margin-top: 10px;
+  border-radius: 10px;
+  border-top-left-radius: 0; // AI消息左上角直角(靠近头像)
+  background-color: #FFFFFF;
+  padding: 10px;
+  font-size: 14px;
+  user-select: text;
+  word-break: break-word;
+   border: var(--border-in-light);
+  position: relative;
+  transition: all ease 0.3s;
+  // box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.chat-message-item-image {
+  width: 100%;
+  margin-top: 10px;
+}
+
+.chat-message-item-images {
+  width: 100%;
+  display: grid;
+  justify-content: left;
+  grid-gap: 10px;
+  grid-template-columns: repeat(var(--image-count), auto);
+  margin-top: 10px;
+}
+
+.chat-message-item-image-multi {
+  object-fit: cover;
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+}
+
+.chat-message-item-image,
+.chat-message-item-image-multi {
+  box-sizing: border-box;
+  border-radius: 10px;
+  border: rgba($color: #888, $alpha: 0.2) 1px solid;
+}
+
+
+@media only screen and (max-width: 600px) {
+  $calc-image-width: calc(100vw / 3 * 2 / var(--image-count));
+
+  .chat-message-item-image-multi {
+    width: $calc-image-width;
+    height: $calc-image-width;
+  }
+
+  .chat-message-item-image {
+    max-width: calc(100vw / 3 * 2);
+  }
+}
+
+@media screen and (min-width: 600px) {
+  $max-image-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count));
+  $image-width: calc(calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 / var(--image-count));
+
+  .chat-message-item-image-multi {
+    width: $image-width;
+    height: $image-width;
+    max-width: $max-image-width;
+    max-height: $max-image-width;
+  }
+
+  .chat-message-item-image {
+    max-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2);
+  }
+}
+
+.chat-message-action-date {
+  font-size: 12px;
+  opacity: 0.2;
+  white-space: nowrap;
+  transition: all ease 0.6s;
+  color: var(--black);
+  text-align: right;
+  width: 100%;
+  box-sizing: border-box;
+  padding-right: 10px;
+  pointer-events: none;
+  z-index: 1;
+}
+
+.chat-message-user>.chat-message-container>.chat-message-item {
+  background-color: var(--second);
+  border-top-left-radius: 10px; // 恢复左上角圆角
+  border-top-right-radius: 0; // 用户消息右上角直角(靠近头像)
+
+  &:hover {
+    min-width: 0;
+  }
+}
+
+.chat-input-panel {
+  position: relative;
+  width: 100%;
+  padding: 16px;
+  padding-top: 16px;
+  box-sizing: border-box;
+  flex-direction: column;
+  background: transparent;
+  // box-shadow: var(--card-shadow);
+  // 下方线条
+  // border-top: 1px solid #dedede;
+  border-top: none;
+
+  // 背景
+  // background: rgba(155, 155, 255, 0.2);
+  // 背景模糊程度
+  // backdrop-filter: blur(2px);
+
+  .chat-input-actions {
+    .chat-input-action {
+      margin-bottom: 10px;
+    }
+  }
+}
+
+@mixin single-line {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.prompt-hints {
+  min-height: 20px;
+  width: 100%;
+  max-height: 50vh;
+  overflow: auto;
+  display: flex;
+  flex-direction: column-reverse;
+
+  background-color: var(--white);
+  border: var(--border-in-light);
+  border-radius: 10px;
+  margin-bottom: 10px;
+  box-shadow: var(--shadow);
+
+  .prompt-hint {
+    color: var(--black);
+    padding: 6px 10px;
+    animation: slide-in ease 0.3s;
+    cursor: pointer;
+    transition: all ease 0.3s;
+    border: transparent 1px solid;
+    margin: 4px;
+    border-radius: 8px;
+
+    &:not(:last-child) {
+      margin-top: 0;
+    }
+
+    .hint-title {
+      font-size: 12px;
+      font-weight: bolder;
+
+      @include single-line();
+    }
+
+    .hint-content {
+      font-size: 12px;
+
+      @include single-line();
+    }
+
+    &-selected,
+    &:hover {
+      border-color: var(--primary);
+    }
+  }
+}
+
+.chat-input-panel-inner {
+  cursor: text;
+  display: flex;
+  flex-direction: column; // 垂直布局
+  flex: 1;
+  border-radius: 16px;
+  border: none;
+  position: relative;
+  // min-height: 100px;
+  max-height: 120px;
+  overflow-y: auto;
+  overflow: hidden;
+  box-shadow: 0 4px 26px rgba(0, 0, 0, 0.12);
+  transition: box-shadow 0.3s ease;
+}
+
+.chat-input-panel-inner-attach {
+  padding-bottom: 80px;
+}
+
+.chat-input-panel-inner:has(.chat-input:focus) {
+  box-shadow: 0 6px 20px rgba(73, 95, 230, 0.2);
+}
+
+.chat-input-panel-inner:has(.chat-input2:focus) {
+  box-shadow: 0 6px 20px rgba(73, 95, 230, 0.2);
+}
+
+.chat-input {
+  flex: 1;
+  height: 100%;
+  width: 100%;
+  // border-radius: 16px;
+  border: none;
+  background: transparent;
+  box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
+  color: var(--black) !important;
+  font-family: inherit;
+  padding: 12px 16px 0px;
+  resize: none;
+  outline: none;
+  box-sizing: border-box;
+  min-height: 50px;
+  font-size: 14px;
+  line-height: 1.5;
+  // 文字颜色
+  // color: #FFFFFF;
+}
+
+.chat-input2 {
+  @extend .chat-input;
+  min-height: auto; // 允许输入框最小高度为1行
+  
+  // 移动端适配:允许输入框最小高度为1行
+  @media screen and (max-width: 600px) {
+    min-height: auto;
+    padding: 10px 16px 0px; // 减少移动端上下内边距
+    font-size: 16px; // 防止iOS自动缩放
+  }
+}
+
+// placeholder颜色
+.chat-input::placeholder {
+  color: #FFFFFF;
+}
+
+.chat-input-send {
+  background-color: var(--primary);
+  color: white;
+  position: absolute;
+  right: 30px;
+  bottom: 32px;
+}
+// 新增:输入框内部按钮区域样式
+.chat-input-bottom-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 16px 16px 16px;
+  flex-shrink: 0;
+  // border-top: 1px solid #f3f4f6;
+  margin-top: auto;
+  // 确保按钮区域布局稳定
+  position: relative;
+  
+  // 移动端优化
+  @media only screen and (max-width: 600px) {
+    justify-content: flex-start;
+    gap: 8px;
+    flex-wrap: wrap;
+    
+    // 左侧选项区域
+    > div:first-child {
+      flex: 1;
+      min-width: 0;
+    }
+    
+    // 右侧按钮区域(包含上传和发送按钮)
+    > div:last-child {
+      flex-shrink: 0;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+  }
+  
+  .left-options {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    
+    .option-item { //  未使用
+      display: flex;
+      height: 28px;
+      border-radius: 16px;
+      font-size: 12px;
+      padding: 0px 12px;
+      justify-content: center;
+      align-items: center;
+      margin-right: 10px;
+      cursor: pointer;
+      background: #f3f4f6;  // 默认状态
+      color: #000000;        // 默认状态
+      // border: 1px solid transparent;  // 默认状态
+      transition: all 0.2s ease;
+      user-select: none;
+      
+      // 激活状态
+      &.active {
+        background: #dee9fc;
+        color: #3875f6;
+        // border: 1px solid #3875f6;
+      }
+      
+      &:hover {
+        background-color: #f9fafb;
+        color: #374151;
+      }
+      
+      // .chevron {
+      //   margin-left: 4px;
+      //   font-size: 10px;
+      //   opacity: 0.6;
+      // }
+    }
+  }
+  
+  .middle-action {
+    .primary-button {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      padding: 6px 12px;
+      background: #dbeafe;
+      color: #1e40af;
+      border: none;
+      border-radius: 8px;
+      font-size: 12px;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      
+      &:hover {
+        background: #bfdbfe;
+      }
+      
+      .chevron {
+        font-size: 10px;
+        opacity: 0.7;
+      }
+    }
+  }
+  
+  .right-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    
+    .action-icon {
+      width: 24px;
+      height: 24px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 50%;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      
+      &:hover {
+        background-color: #f3f4f6;
+      }
+      
+      &.send-button {
+        background: #6b7280;
+        color: white;
+        
+        &:hover {
+          background: #4b5563;
+        }
+      }
+    }
+  }
+}
+
+@media only screen and (max-width: 600px) {
+  .chat-input {
+    font-size: 16px;
+  }
+
+  .chat-input-send {
+    bottom: 30px;
+  }
+  
+  // 移动端输入框内部优化
+  .chat-input-panel-inner {
+    min-height: 100px;
+    max-height: 120px;
+  }
+  
+
+  
+  // 针对特定手机屏幕的额外优化
+  @media only screen and (max-width: 480px) {
+    // 针对DeepSeekHomeChat组件的按钮优化
+    .chat-input-panel + div[style*="display: flex"] {
+      padding: 4px 8px !important;
+      gap: 4px !important;
+      
+      > div:first-child {
+        gap: 8px !important;
+        flex-wrap: wrap !important;
+        
+        > div {
+          padding: 0 8px !important;
+          height: 26px !important;
+          font-size: 11px !important;
+          margin-right: 8px !important;
+          
+          img {
+            height: 18px !important;
+          }
+        }
+      }
+      
+      > div:last-child {
+        width: 28px !important;
+        height: 28px !important;
+        flex-shrink: 0 !important;
+      }
+    }
+  }
+  
+  // 移动端输入框优化
+  .chat-input2 {
+    font-size: 16px; // 防止iOS缩放
+    line-height: 1.4;
+    padding: 12px 16px;
+  }
+  
+  // 通用移动端按钮容器优化
+  .chat-input-panel + div[style*="display: flex"] {
+    display: flex !important;
+    justify-content: space-between !important;
+    align-items: center !important;
+    flex-wrap: nowrap !important;
+    padding: 6px 12px !important;
+    gap: 8px !important;
+    
+    > div:first-child {
+      display: flex !important;
+      align-items: center !important;
+      gap: 8px !important;
+      flex-wrap: wrap !important;
+      flex: 1 !important;
+      min-width: 0 !important;
+    }
+    
+    > div:last-child {
+      flex-shrink: 0 !important;
+      display: flex !important;
+      align-items: center !important;
+      justify-content: center !important;
+    }
+  }
+}
+

+ 49 - 11
app/components/deepSeekHome.scss

@@ -11,18 +11,22 @@
   &-header {
     width: 100%;
     height: 60px;
-    border: 1px solid rgba(24, 126, 255, 0.5);
+    // border: 1px solid rgba(24, 126, 255, 0.5);
     display: flex;
     color: #FFFFFF;
-    // justify-content: center;
-    justify-content: space-between;
     align-items: center;
     overflow-x: auto;
     overflow-y: hidden;
     box-sizing: border-box;
-    // position: relative;
     background-color: blur(10px);
-    background: rgba(24, 126, 255, 0.3);
+    background: rgba(15, 23, 57, 0.596);
+    padding: 0 16px;
+    box-shadow: 0 4px 26px rgba(0, 0, 0, 0.25);
+
+    @media (max-width: 768px) {
+      height: 45px;
+      padding: 0 16px;
+    }
 
     &::before {
       content: '';
@@ -31,15 +35,52 @@
       left: 0;
 
     }
+
+    &__item {
+      position: relative;
+      padding: 8px 12px;
+      padding-bottom: 12px;
+      border-radius: 999px;
+      white-space: nowrap;
+      color: #FFFFFF;
+      cursor: pointer;
+      transition: color 0.2s ease, font-weight 0.2s ease;
+
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 0;
+        left: 50%;
+        transform: translateX(-50%) scaleX(0);
+        width: 60%;
+        height: 3px;
+        background: #FFFFFF;
+        border-radius: 2px;
+        transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+        transform-origin: center;
+      }
+
+      &.active::after {
+        transform: translateX(-50%) scaleX(1);
+      }
+
+      &:hover {
+        color: #CFE4FF;
+        font-weight: 700 !important;
+      }
+    }
   }
 
   &-content {
     width: 100%;
-    height: calc(100% - 80px - env(safe-area-inset-bottom));
+    height: calc(100% - 50px - env(safe-area-inset-bottom));
     display: flex;
     justify-content: center;
     align-items: center;
     flex-direction: column;
+    padding: 16px 16px 0px;
+    box-sizing: border-box;
+    transition: all 0.3s ease;
 
     &-title {
       display: flex;
@@ -74,11 +115,12 @@
     }
 
     &-mobile {
-      width: 90%;
+      width: 100%;
       height: 82%;
       background: #FFFFFF;
       border-radius: 12px;
       overflow: hidden;
+      transition: height 0.3s ease;
     }
   }
 
@@ -93,10 +135,6 @@
 
 // 开放平台按钮样式
 .open-platform-btn {
-  @media (max-width: 768px) {
-    display: none; // 移动端隐藏
-  }
-
   background: #FFFFFF;
   color: #1890FF;
   border: 1px solid #1890FF;

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

@@ -105,6 +105,17 @@
   display: none;
 }
 
+.sidebar-overlay {
+  display: none;
+  position: fixed;
+  inset: 0;
+  height: var(--full-height);
+  background-color: rgba(0, 0, 0, 0.35);
+  backdrop-filter: blur(2px);
+  z-index: 900;
+  transition: opacity 0.2s ease;
+}
+
 @media only screen and (max-width: 600px) {
   .container {
     min-height: unset;
@@ -124,6 +135,16 @@
     box-shadow: none;
   }
 
+  .sidebar-action-group {
+    gap: 8px;
+
+    .sidebar-action-button {
+      flex: 1 1 calc(50% - 4px);
+      min-width: 0;
+      height: 40px;
+    }
+  }
+
   .sidebar-show {
     left: 0;
   }
@@ -131,6 +152,10 @@
   .mobile {
     display: block;
   }
+
+  .sidebar-overlay {
+    display: block;
+  }
 }
 
 .sidebar-header {
@@ -169,6 +194,26 @@
   overflow-x: hidden;
 }
 
+.sidebar-action-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.sidebar-action-group:last-child {
+  margin-bottom: 0;
+}
+
+.sidebar-action-button {
+  flex: 1 1 calc(50% - 6px);
+  min-width: 150px;
+  height: 36px;
+  border-radius: 6px;
+  font-size: 14px;
+  font-weight: 500;
+}
+
 .chat-item {
   padding: 10px 14px;
   background-color: var(--white);
@@ -260,6 +305,18 @@
     }
   }
 
+  .sidebar-action-group {
+    flex-direction: column;
+    flex-wrap: nowrap;
+    gap: 10px;
+
+    .sidebar-action-button {
+      flex-basis: 100%;
+      min-width: unset;
+      width: 100%;
+    }
+  }
+
   .chat-item {
     padding: 0;
     min-height: 50px;

+ 32 - 36
app/components/sidebar.tsx

@@ -103,7 +103,7 @@ export function useDragSideBar() {
     const barWidth = shouldNarrow
       ? NARROW_SIDEBAR_WIDTH
       : limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
-    const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
+    const sideBarWidth = isMobileScreen ? "60vw" : `${barWidth}px`;
     document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
   }, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
 
@@ -529,8 +529,20 @@ export const SideBar = (props: { className?: string }) => {
 
   const [drawerType, setDrawerType] = useState<'all' | 'collect'>('all');
 
+  const closeSidebar = () => {
+    globalStore.setShowMenu(false);
+  };
+
   return (
     <>
+      {
+        isMobileScreen && globalStore.showMenu && (
+          <div
+            className={styles["sidebar-overlay"]}
+            onClick={closeSidebar}
+          />
+        )
+      }
       {
         globalStore.showMenu &&
         <SideBarContainer
@@ -546,7 +558,7 @@ export const SideBar = (props: { className?: string }) => {
           }
           <SideBarHeader
             title={getType() === 'bigModel' ?
-              <div style={{ display: 'flex', alignItems: 'center' }}>
+              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                 {
                   isMobileScreen && <div>
                     <Button
@@ -558,22 +570,17 @@ export const SideBar = (props: { className?: string }) => {
                     />
                   </div>
                 }
-                问答历史
+                <img style={{ height: 32 }} src={faviconSrc.src} />
+                <span>建科•小智</span>
               </div>
               :
               ''
             }
-            logo={getType() === 'bigModel' ? <img style={{ height: 40 }} src={faviconSrc.src} /> : ''}
+            logo={getType() === 'bigModel' ? null : ''}
           >
-            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10, gap: 8 }}>
+            <div className={styles["sidebar-action-group"]}>
               <Button
-                style={{ 
-                  width: '48%', 
-                  height: 36,
-                  borderRadius: 6,
-                  fontSize: 14,
-                  fontWeight: 500
-                }}
+                className={styles["sidebar-action-button"]}
                 icon={<HomeOutlined />}
                 onClick={() => {
                   navigate({ pathname: '/' });
@@ -582,13 +589,7 @@ export const SideBar = (props: { className?: string }) => {
                 回到首页
               </Button>
               <Button
-                style={{ 
-                  width: '48%', 
-                  height: 36,
-                  borderRadius: 6,
-                  fontSize: 14,
-                  fontWeight: 500
-                }}
+                className={styles["sidebar-action-button"]}
                 icon={<PlusOutlined />}
                 onClick={async () => {
                   chatStore.clearSessions();
@@ -596,7 +597,7 @@ export const SideBar = (props: { className?: string }) => {
                     value.appId = globalStore.selectedAppId;
                   });
                   if (isMobileScreen) {
-                    globalStore.setShowMenu(false);
+                    closeSidebar();
                   }
                   if (getType() === 'bigModel') {
                     navigate({ pathname: '/newChat' });
@@ -617,15 +618,9 @@ export const SideBar = (props: { className?: string }) => {
                 新建对话
               </Button>
             </div>
-            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10, gap: 8 }}>
+            <div className={styles["sidebar-action-group"]}>
               <Button
-                style={{ 
-                  width: '48%', 
-                  height: 36,
-                  borderRadius: 6,
-                  fontSize: 14,
-                  fontWeight: 500
-                }}
+                className={styles["sidebar-action-button"]}
                 icon={<AppstoreOutlined />}
                 onClick={() => {
                   setDrawerType('all');
@@ -635,13 +630,7 @@ export const SideBar = (props: { className?: string }) => {
                 我的应用
               </Button>
               <Button
-                style={{ 
-                  width: '48%', 
-                  height: 36,
-                  borderRadius: 6,
-                  fontSize: 14,
-                  fontWeight: 500
-                }}
+                className={styles["sidebar-action-button"]}
                 icon={<StarOutlined />}
                 onClick={() => {
                   setDrawerType('collect');
@@ -652,6 +641,13 @@ export const SideBar = (props: { className?: string }) => {
               </Button>
             </div>
           </SideBarHeader>
+          <div style={{
+            fontSize: 12,
+            color: '#999999',
+            padding: '8px 0 4px',
+          }}>
+            问答历史
+          </div>
           <Menu
             style={{ border: 'none' }}
             onClick={async (info) => {
@@ -660,7 +656,7 @@ export const SideBar = (props: { className?: string }) => {
               const props = info.item.props;
               const { showMenu, chatMode, appId } = props;
               if (isMobileScreen) {
-                globalStore.setShowMenu(false);
+                closeSidebar();
               }
               let url = ``;
               if (getType() === 'bigModel') {

+ 58 - 3
app/styles/globals.scss

@@ -19,7 +19,7 @@
   --card-shadow: 0px 2px 4px 0px rgb(0, 0, 0, 0.05);
 
   /* stroke */
-  --border-in-light: 1px solid rgb(222, 222, 222);
+  --border-in-light: 1px solid #f3f3f3;
 }
 
 @mixin dark {
@@ -35,7 +35,7 @@
 
   --bar-color: rgba(255, 255, 255, 0.1);
 
-  --border-in-light: 1px solid rgba(255, 255, 255, 0.192);
+  --border-in-light: 1px solid rgba(255,255,255,0.2);
 
   --theme-color: var(--gray);
 
@@ -71,7 +71,7 @@
   :root {
     --window-width: 100vw;
     --window-height: var(--full-height);
-    --sidebar-width: 100vw;
+    --sidebar-width: 60vw;
     --window-content-width: var(--window-width);
     --message-max-width: 100%;
   }
@@ -368,3 +368,58 @@ pre {
 .copyable {
   user-select: text;
 }
+
+// Ant Design Dropdown 下拉菜单样式优化,防止数据多时被遮挡
+.deekSeek-dropdown {
+  z-index: 1050 !important;
+  
+  .ant-dropdown-menu {
+    max-height: 400px !important;
+    overflow-y: auto !important;
+    overflow-x: hidden !important;
+    
+    // 自定义滚动条样式
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+    
+    &::-webkit-scrollbar-track {
+      background: #f1f1f1;
+      border-radius: 3px;
+    }
+    
+    &::-webkit-scrollbar-thumb {
+      background: #888;
+      border-radius: 3px;
+      
+      &:hover {
+        background: #555;
+      }
+    }
+    
+    .ant-dropdown-menu-item {
+      padding: 8px 12px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      max-width: 200px;
+      
+      // 子菜单项样式
+      .ant-dropdown-menu-submenu-title {
+        padding: 8px 12px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        max-width: 200px;
+      }
+    }
+    
+    // 子菜单容器样式
+    .ant-dropdown-menu-submenu-popup {
+      .ant-dropdown-menu {
+        max-height: 300px !important;
+        overflow-y: auto !important;
+      }
+    }
+  }
+}