DeepSeekChat.tsx 66 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968
  1. // 第三方库导入
  2. import { useDebouncedCallback } from "use-debounce";
  3. import React, {
  4. useState,
  5. useRef,
  6. useEffect,
  7. useMemo,
  8. useCallback,
  9. Fragment,
  10. RefObject,
  11. } from "react";
  12. import dynamic from "next/dynamic";
  13. import { useNavigate, useLocation } from "react-router-dom";
  14. // 本地组件和工具导入
  15. import { IconButton } from "./button";
  16. import { MaskAvatar } from "./mask";
  17. import styles from "./chat.module.scss";
  18. // 图标资源导入
  19. import LeftIcon from "../icons/left.svg";
  20. import SendWhiteIcon from "../icons/send-white.svg";
  21. import BrainIcon from "../icons/brain.svg";
  22. import CopyIcon from "../icons/copy.svg";
  23. import LoadingIcon from "../icons/three-dots.svg";
  24. import ResetIcon from "../icons/reload.svg";
  25. import DeleteIcon from "../icons/clear.svg";
  26. import ConfirmIcon from "../icons/confirm.svg";
  27. import CancelIcon from "../icons/cancel.svg";
  28. import SizeIcon from "../icons/size.svg";
  29. import avatar from "../icons/aiIcon.png";
  30. import sdsk from "../icons/sdsk.png";
  31. import sdsk_selected from "../icons/sdsk_selected.png";
  32. import hlw from "../icons/hlw.png";
  33. import hlw_selected from "../icons/hlw_selected.png";
  34. import BotIcon from "../icons/bot.svg";
  35. import BlackBotIcon from "../icons/black-bot.svg";
  36. import avatarSrc from "../icons/avatar.png";
  37. // 状态管理和类型导入
  38. import {
  39. ChatMessage,
  40. SubmitKey,
  41. useChatStore,
  42. useAccessStore,
  43. Theme,
  44. useAppConfig,
  45. DEFAULT_TOPIC,
  46. ModelType,
  47. useGlobalStore,
  48. } from "../store";
  49. import { Prompt, usePromptStore } from "../store/prompt";
  50. // 工具函数导入
  51. import {
  52. copyToClipboard,
  53. selectOrCopy,
  54. autoGrowTextArea,
  55. useMobileScreen,
  56. getMessageTextContent,
  57. getMessageImages,
  58. isVisionModel,
  59. isDalle3,
  60. } from "../utils";
  61. import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
  62. // 客户端和类型导入
  63. import { ChatControllerPool } from "../client/controller";
  64. import { DalleSize } from "../typing";
  65. import type { RequestMessage } from "../client/api";
  66. // UI 组件导入
  67. import {
  68. List,
  69. ListItem,
  70. Modal,
  71. Selector,
  72. showConfirm,
  73. showToast,
  74. } from "./ui-lib";
  75. // 常量和本地化
  76. import Locale from "../locales";
  77. import {
  78. CHAT_PAGE_SIZE,
  79. LAST_INPUT_KEY,
  80. Path,
  81. REQUEST_TIMEOUT_MS,
  82. UNFINISHED_INPUT,
  83. ServiceProvider,
  84. Plugin,
  85. } from "../constant";
  86. import { ContextPrompts, MaskConfig } from "./mask";
  87. import { useMaskStore } from "../store/mask";
  88. import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
  89. import { prettyObject } from "../utils/format";
  90. import { ExportMessageModal } from "./exporter";
  91. import { getClientConfig } from "../config/client";
  92. import { useAllModels } from "../utils/hooks";
  93. import { nanoid } from "nanoid";
  94. import { message, Upload, UploadProps, Tooltip, Drawer, Button } from "antd";
  95. import {
  96. PaperClipOutlined,
  97. SendOutlined,
  98. FileOutlined,
  99. FilePdfOutlined,
  100. FileTextOutlined,
  101. FileWordOutlined,
  102. RightOutlined
  103. } from '@ant-design/icons';
  104. // Avatar组件替代实现
  105. function Avatar( props : { model? : string; avatar? : string } ) {
  106. if ( props.model ) {
  107. return (
  108. <div className="no-dark">
  109. { props.model?.startsWith( "gpt-4" ) ? (
  110. <BlackBotIcon className="user-avatar" />
  111. ) : (
  112. <BotIcon className="user-avatar" />
  113. ) }
  114. </div>
  115. );
  116. }
  117. return (
  118. <div className="user-avatar">
  119. {/* 移除emoji头像,使用默认bot图标 */ }
  120. <BotIcon className="user-avatar" />
  121. </div>
  122. );
  123. }
  124. export function createMessage( override : Partial<ChatMessage> ) : ChatMessage {
  125. return {
  126. id: nanoid(),
  127. date: new Date().toLocaleString(),
  128. role: "user",
  129. content: "",
  130. ...override,
  131. };
  132. }
  133. export const BOT_HELLO : ChatMessage = createMessage( {
  134. role: "assistant",
  135. content: '你好,我是小智~\n' +
  136. '我可以帮助你快速查询作业指导书、规范条文、公司信息等内容,如需获取上述内容,请点击上方导航栏中的「专业知识」或「职能管理」,选择相应的智能体进行提问。无论是现场技术,还是制度流程,我都会尽力为你解答!\n' +
  137. '请注意:在这个对话框内,我只能请DeepSeek来帮忙回答常见通用问题哦!',
  138. } );
  139. const Markdown = dynamic( async () => ( await import("./markdown") ).Markdown, {
  140. loading: () => <LoadingIcon />,
  141. } );
  142. export function SessionConfigModel( props : { onClose : () => void } ) {
  143. const chatStore = useChatStore();
  144. const session = chatStore.currentSession();
  145. const maskStore = useMaskStore();
  146. const navigate = useNavigate();
  147. return (
  148. <div className="modal-mask">
  149. <Modal
  150. title={ Locale.Context.Edit }
  151. onClose={ () => props.onClose() }
  152. actions={ [
  153. <IconButton
  154. key="reset"
  155. icon={ <ResetIcon /> }
  156. bordered
  157. text={ Locale.Chat.Config.Reset }
  158. onClick={ async () => {
  159. if ( await showConfirm( Locale.Memory.ResetConfirm ) ) {
  160. chatStore.updateCurrentSession(
  161. ( session ) => ( session.memoryPrompt = "" ),
  162. );
  163. }
  164. } }
  165. />,
  166. <IconButton
  167. key="copy"
  168. icon={ <CopyIcon /> }
  169. bordered
  170. text={ Locale.Chat.Config.SaveAs }
  171. onClick={ () => {
  172. navigate( Path.Masks );
  173. setTimeout( () => {
  174. maskStore.create( session.mask );
  175. }, 500 );
  176. } }
  177. />,
  178. ] }
  179. >
  180. <MaskConfig
  181. mask={ session.mask }
  182. updateMask={ ( updater ) => {
  183. const mask = { ...session.mask };
  184. updater( mask );
  185. chatStore.updateCurrentSession( ( session ) => ( session.mask = mask ) );
  186. } }
  187. shouldSyncFromGlobal
  188. extraListItems={
  189. session.mask.modelConfig.sendMemory ? (
  190. <ListItem
  191. className="copyable"
  192. title={ `${ Locale.Memory.Title } (${ session.lastSummarizeIndex } of ${ session.messages.length })` }
  193. subTitle={ session.memoryPrompt || Locale.Memory.EmptyContent }
  194. ></ListItem>
  195. ) : (
  196. <></>
  197. )
  198. }
  199. ></MaskConfig>
  200. </Modal>
  201. </div>
  202. );
  203. }
  204. // 提示词
  205. const CallWord = ( props : {
  206. setUserInput : ( value : string ) => void,
  207. doSubmit : ( userInput : string ) => void,
  208. } ) => {
  209. const { setUserInput, doSubmit } = props
  210. const list = [
  211. {
  212. title: '信息公布',
  213. // text: '在哪里查看招聘信息?',
  214. text: '今年上海盈科工程咨询的校园招聘什么时候开始?如何查阅相关招聘信息?',
  215. },
  216. {
  217. title: '招聘岗位',
  218. // text: '今年招聘的岗位有哪些?',
  219. text: '今年招聘的岗位有哪些?',
  220. },
  221. {
  222. title: '专业要求',
  223. // text: '招聘的岗位有什么专业要求?',
  224. text: '招聘的岗位有什么专业要求?',
  225. },
  226. {
  227. title: '工作地点',
  228. // text: '全国都有工作地点吗?',
  229. text: '工作地点是如何确定的?',
  230. },
  231. {
  232. title: '薪资待遇',
  233. // text: '企业可提供的薪资与福利待遇如何?',
  234. text: '企业可提供的薪资与福利待遇如何?',
  235. },
  236. {
  237. title: '职业发展',
  238. // text: '我应聘贵单位,你们能提供怎样的职业发展规划?',
  239. text: '公司有哪些职业发展通道?',
  240. },
  241. {
  242. title: '落户政策',
  243. // text: '公司是否能协助我落户?',
  244. text: '关于落户支持?',
  245. }
  246. ]
  247. return (
  248. <>
  249. {
  250. list.map( ( item, index ) => {
  251. return <span
  252. key={ index }
  253. style={ {
  254. padding: '5px 10px',
  255. background: '#f6f7f8',
  256. color: '#5e5e66',
  257. borderRadius: 4,
  258. margin: '0 5px 10px 0',
  259. cursor: 'pointer',
  260. fontSize: 12
  261. } }
  262. onClick={ () => {
  263. const plan : string = '2';
  264. if ( plan === '1' ) {
  265. // 方案1.点击后出现在输入框内,用户自己点击发送
  266. setUserInput( item.text );
  267. } else {
  268. // 方案2.点击后直接发送
  269. doSubmit( item.text )
  270. }
  271. } }
  272. >
  273. { item.title }
  274. </span>
  275. } )
  276. }
  277. </>
  278. )
  279. }
  280. function PromptToast( props : {
  281. showToast? : boolean;
  282. showModal? : boolean;
  283. setShowModal : ( _ : boolean ) => void;
  284. } ) {
  285. const chatStore = useChatStore();
  286. const session = chatStore.currentSession();
  287. const context = session.mask.context;
  288. return (
  289. <div className={ styles[ "prompt-toast" ] } key="prompt-toast">
  290. { props.showToast && (
  291. <div
  292. className={ styles[ "prompt-toast-inner" ] + " clickable" }
  293. role="button"
  294. onClick={ () => props.setShowModal( true ) }
  295. >
  296. <BrainIcon />
  297. <span className={ styles[ "prompt-toast-content" ] }>
  298. { Locale.Context.Toast( context.length ) }
  299. </span>
  300. </div>
  301. ) }
  302. { props.showModal && (
  303. <SessionConfigModel onClose={ () => props.setShowModal( false ) } />
  304. ) }
  305. </div>
  306. );
  307. }
  308. function useSubmitHandler() {
  309. const config = useAppConfig();
  310. const submitKey = config.submitKey;
  311. const isComposing = useRef( false );
  312. useEffect( () => {
  313. const onCompositionStart = () => {
  314. isComposing.current = true;
  315. };
  316. const onCompositionEnd = () => {
  317. isComposing.current = false;
  318. };
  319. window.addEventListener( "compositionstart", onCompositionStart );
  320. window.addEventListener( "compositionend", onCompositionEnd );
  321. return () => {
  322. window.removeEventListener( "compositionstart", onCompositionStart );
  323. window.removeEventListener( "compositionend", onCompositionEnd );
  324. };
  325. }, [] );
  326. const shouldSubmit = ( e : React.KeyboardEvent<HTMLTextAreaElement> ) => {
  327. // Fix Chinese input method "Enter" on Safari
  328. if ( e.keyCode == 229 ) return false;
  329. if ( e.key !== "Enter" ) return false;
  330. if ( e.key === "Enter" && ( e.nativeEvent.isComposing || isComposing.current ) )
  331. return false;
  332. return (
  333. ( config.submitKey === SubmitKey.AltEnter && e.altKey ) ||
  334. ( config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey ) ||
  335. ( config.submitKey === SubmitKey.ShiftEnter && e.shiftKey ) ||
  336. ( config.submitKey === SubmitKey.MetaEnter && e.metaKey ) ||
  337. ( config.submitKey === SubmitKey.Enter &&
  338. !e.altKey &&
  339. !e.ctrlKey &&
  340. !e.shiftKey &&
  341. !e.metaKey )
  342. );
  343. };
  344. return {
  345. submitKey,
  346. shouldSubmit,
  347. };
  348. }
  349. export type RenderPrompt = Pick<Prompt, "title" | "content">;
  350. export function PromptHints( props : {
  351. prompts : RenderPrompt[];
  352. onPromptSelect : ( prompt : RenderPrompt ) => void;
  353. } ) {
  354. const noPrompts = props.prompts.length === 0;
  355. const [ selectIndex, setSelectIndex ] = useState( 0 );
  356. const selectedRef = useRef<HTMLDivElement>( null );
  357. useEffect( () => {
  358. setSelectIndex( 0 );
  359. }, [ props.prompts.length ] );
  360. useEffect( () => {
  361. const onKeyDown = ( e : KeyboardEvent ) => {
  362. if ( noPrompts || e.metaKey || e.altKey || e.ctrlKey ) {
  363. return;
  364. }
  365. // arrow up / down to select prompt
  366. const changeIndex = ( delta : number ) => {
  367. e.stopPropagation();
  368. e.preventDefault();
  369. const nextIndex = Math.max(
  370. 0,
  371. Math.min( props.prompts.length - 1, selectIndex + delta ),
  372. );
  373. setSelectIndex( nextIndex );
  374. selectedRef.current?.scrollIntoView( {
  375. block: "center",
  376. } );
  377. };
  378. if ( e.key === "ArrowUp" ) {
  379. changeIndex( 1 );
  380. } else if ( e.key === "ArrowDown" ) {
  381. changeIndex( - 1 );
  382. } else if ( e.key === "Enter" ) {
  383. const selectedPrompt = props.prompts.at( selectIndex );
  384. if ( selectedPrompt ) {
  385. props.onPromptSelect( selectedPrompt );
  386. }
  387. }
  388. };
  389. window.addEventListener( "keydown", onKeyDown );
  390. return () => window.removeEventListener( "keydown", onKeyDown );
  391. // eslint-disable-next-line react-hooks/exhaustive-deps
  392. }, [ props.prompts.length, selectIndex ] );
  393. if ( noPrompts ) return null;
  394. return (
  395. <div className={ styles[ "prompt-hints" ] }>
  396. { props.prompts.map( ( prompt, i ) => (
  397. <div
  398. ref={ i === selectIndex ? selectedRef : null }
  399. className={
  400. styles[ "prompt-hint" ] +
  401. ` ${ i === selectIndex ? styles[ "prompt-hint-selected" ] : "" }`
  402. }
  403. key={ prompt.title + i.toString() }
  404. onClick={ () => props.onPromptSelect( prompt ) }
  405. onMouseEnter={ () => setSelectIndex( i ) }
  406. >
  407. <div className={ styles[ "hint-title" ] }>{ prompt.title }</div>
  408. <div className={ styles[ "hint-content" ] }>{ prompt.content }</div>
  409. </div>
  410. ) ) }
  411. </div>
  412. );
  413. }
  414. function ClearContextDivider() {
  415. const chatStore = useChatStore();
  416. return (
  417. <div
  418. className={ styles[ "clear-context" ] }
  419. onClick={ () =>
  420. chatStore.updateCurrentSession(
  421. ( session ) => ( session.clearContextIndex = undefined ),
  422. )
  423. }
  424. >
  425. <div className={ styles[ "clear-context-tips" ] }>{ Locale.Context.Clear }</div>
  426. <div className={ styles[ "clear-context-revert-btn" ] }>
  427. { Locale.Context.Revert }
  428. </div>
  429. </div>
  430. );
  431. }
  432. export function ChatAction( props : {
  433. text : string;
  434. icon : JSX.Element;
  435. onClick : () => void;
  436. } ) {
  437. const iconRef = useRef<HTMLDivElement>( null );
  438. const textRef = useRef<HTMLDivElement>( null );
  439. const [ width, setWidth ] = useState( {
  440. full: 16,
  441. icon: 16,
  442. } );
  443. function updateWidth() {
  444. if ( !iconRef.current || !textRef.current ) return;
  445. const getWidth = ( dom : HTMLDivElement ) => dom.getBoundingClientRect().width;
  446. const textWidth = getWidth( textRef.current );
  447. const iconWidth = getWidth( iconRef.current );
  448. setWidth( {
  449. full: textWidth + iconWidth,
  450. icon: iconWidth,
  451. } );
  452. }
  453. return (
  454. <div
  455. className={ `${ styles[ "chat-input-action" ] } clickable` }
  456. onClick={ () => {
  457. props.onClick();
  458. setTimeout( updateWidth, 1 );
  459. } }
  460. onMouseEnter={ updateWidth }
  461. onTouchStart={ updateWidth }
  462. style={
  463. {
  464. "--icon-width": `${ width.icon }px`,
  465. "--full-width": `${ width.full }px`,
  466. } as React.CSSProperties
  467. }
  468. >
  469. <div ref={ iconRef } className={ styles[ "icon" ] }>
  470. { props.icon }
  471. </div>
  472. <div className={ styles[ "text" ] } ref={ textRef }>
  473. { props.text }
  474. </div>
  475. </div>
  476. );
  477. }
  478. function useScrollToBottom(
  479. scrollRef : RefObject<HTMLDivElement>,
  480. detach : boolean = false,
  481. ) {
  482. // for auto-scroll
  483. const [ autoScroll, setAutoScroll ] = useState( true );
  484. function scrollDomToBottom() {
  485. const dom = scrollRef.current;
  486. if ( dom ) {
  487. requestAnimationFrame( () => {
  488. setAutoScroll( true );
  489. dom.scrollTo( 0, dom.scrollHeight );
  490. } );
  491. }
  492. }
  493. // auto scroll
  494. useEffect( () => {
  495. if ( autoScroll && !detach ) {
  496. scrollDomToBottom();
  497. }
  498. } );
  499. return {
  500. scrollRef,
  501. autoScroll,
  502. setAutoScroll,
  503. scrollDomToBottom,
  504. };
  505. }
  506. export function ChatActions( props : {
  507. setUserInput : ( value : string ) => void;
  508. doSubmit : ( userInput : string ) => void;
  509. uploadImage : () => void;
  510. setAttachImages : ( images : string[] ) => void;
  511. setUploading : ( uploading : boolean ) => void;
  512. showPromptModal : () => void;
  513. scrollToBottom : () => void;
  514. showPromptHints : () => void;
  515. hitBottom : boolean;
  516. uploading : boolean;
  517. } ) {
  518. const config = useAppConfig();
  519. const navigate = useNavigate();
  520. const chatStore = useChatStore();
  521. // switch themes
  522. const theme = config.theme;
  523. function nextTheme() {
  524. const themes = [ Theme.Auto, Theme.Light, Theme.Dark ];
  525. const themeIndex = themes.indexOf( theme );
  526. const nextIndex = ( themeIndex + 1 ) % themes.length;
  527. const nextTheme = themes[ nextIndex ];
  528. config.update( ( config ) => ( config.theme = nextTheme ) );
  529. }
  530. // stop all responses
  531. const couldStop = ChatControllerPool.hasPending();
  532. const stopAll = () => ChatControllerPool.stopAll();
  533. // switch model
  534. const currentModel = chatStore.currentSession().mask.modelConfig.model;
  535. const currentProviderName =
  536. chatStore.currentSession().mask.modelConfig?.providerName ||
  537. ServiceProvider.OpenAI;
  538. const allModels = useAllModels();
  539. const models = useMemo( () => {
  540. const filteredModels = allModels.filter( ( m ) => m.available );
  541. const defaultModel = filteredModels.find( ( m ) => m.isDefault );
  542. if ( defaultModel ) {
  543. const arr = [
  544. defaultModel,
  545. ...filteredModels.filter( ( m ) => m !== defaultModel ),
  546. ];
  547. return arr;
  548. } else {
  549. return filteredModels;
  550. }
  551. }, [ allModels ] );
  552. const currentModelName = useMemo( () => {
  553. const model = models.find(
  554. ( m ) =>
  555. m.name == currentModel &&
  556. m?.provider?.providerName == currentProviderName,
  557. );
  558. return model?.displayName ?? "";
  559. }, [ models, currentModel, currentProviderName ] );
  560. const [ showModelSelector, setShowModelSelector ] = useState( false );
  561. const [ showPluginSelector, setShowPluginSelector ] = useState( false );
  562. const [ showUploadImage, setShowUploadImage ] = useState( false );
  563. type GuessList = string[]
  564. const [ guessList, setGuessList ] = useState<GuessList>( [] );
  565. const [ showSizeSelector, setShowSizeSelector ] = useState( false );
  566. const dalle3Sizes : DalleSize[] = [ "1024x1024", "1792x1024", "1024x1792" ];
  567. const currentSize =
  568. chatStore.currentSession().mask.modelConfig?.size ?? "1024x1024";
  569. const session = chatStore.currentSession();
  570. useEffect( () => {
  571. const show = isVisionModel( currentModel );
  572. setShowUploadImage( show );
  573. if ( !show ) {
  574. props.setAttachImages( [] );
  575. props.setUploading( false );
  576. }
  577. // if current model is not available
  578. // switch to first available model
  579. const isUnavaliableModel = !models.some( ( m ) => m.name === currentModel );
  580. if ( isUnavaliableModel && models.length > 0 ) {
  581. // show next model to default model if exist
  582. let nextModel = models.find( ( model ) => model.isDefault ) || models[ 0 ];
  583. chatStore.updateCurrentSession( ( session ) => {
  584. session.mask.modelConfig.model = nextModel.name;
  585. session.mask.modelConfig.providerName = nextModel?.provider?.providerName as ServiceProvider;
  586. } );
  587. showToast(
  588. nextModel?.provider?.providerName == "ByteDance"
  589. ? nextModel.displayName
  590. : nextModel.name,
  591. );
  592. }
  593. }, [ chatStore, currentModel, models ] );
  594. return (
  595. <div className={ styles[ "chat-input-actions" ] }>
  596. { showModelSelector && (
  597. <Selector
  598. defaultSelectedValue={ `${ currentModel }@${ currentProviderName }` }
  599. items={ models.map( ( m ) => ( {
  600. title: `${ m.displayName }${ m?.provider?.providerName
  601. ? "(" + m?.provider?.providerName + ")"
  602. : ""
  603. }`,
  604. value: `${ m.name }@${ m?.provider?.providerName }`,
  605. } ) ) }
  606. onClose={ () => setShowModelSelector( false ) }
  607. onSelection={ ( s ) => {
  608. if ( s.length === 0 ) return;
  609. const [ model, providerName ] = s[ 0 ].split( "@" );
  610. chatStore.updateCurrentSession( ( session ) => {
  611. session.mask.modelConfig.model = model as ModelType;
  612. session.mask.modelConfig.providerName =
  613. providerName as ServiceProvider;
  614. session.mask.syncGlobalConfig = false;
  615. } );
  616. if ( providerName == "ByteDance" ) {
  617. const selectedModel = models.find(
  618. ( m ) =>
  619. m.name == model && m?.provider?.providerName == providerName,
  620. );
  621. showToast( selectedModel?.displayName ?? "" );
  622. } else {
  623. showToast( model );
  624. }
  625. } }
  626. />
  627. ) }
  628. { isDalle3( currentModel ) && (
  629. <ChatAction
  630. onClick={ () => setShowSizeSelector( true ) }
  631. text={ currentSize }
  632. icon={ <SizeIcon /> }
  633. />
  634. ) }
  635. { showSizeSelector && (
  636. <Selector
  637. defaultSelectedValue={ currentSize }
  638. items={ dalle3Sizes.map( ( m ) => ( {
  639. title: m,
  640. value: m,
  641. } ) ) }
  642. onClose={ () => setShowSizeSelector( false ) }
  643. onSelection={ ( s ) => {
  644. if ( s.length === 0 ) return;
  645. const size = s[ 0 ];
  646. chatStore.updateCurrentSession( ( session ) => {
  647. session.mask.modelConfig.size = size;
  648. } );
  649. showToast( size );
  650. } }
  651. />
  652. ) }
  653. { showPluginSelector && (
  654. <Selector
  655. multiple
  656. defaultSelectedValue={ chatStore.currentSession().mask?.plugin }
  657. items={ [
  658. {
  659. title: Locale.Plugin.Artifacts,
  660. value: Plugin.Artifacts,
  661. },
  662. ] }
  663. onClose={ () => setShowPluginSelector( false ) }
  664. onSelection={ ( s ) => {
  665. const plugin = s[ 0 ];
  666. chatStore.updateCurrentSession( ( session ) => {
  667. session.mask.plugin = s;
  668. } );
  669. if ( plugin ) {
  670. showToast( plugin );
  671. }
  672. } }
  673. />
  674. ) }
  675. </div>
  676. );
  677. }
  678. export function EditMessageModal( props : { onClose : () => void } ) {
  679. const chatStore = useChatStore();
  680. const session = chatStore.currentSession();
  681. const [ messages, setMessages ] = useState( session.messages.slice() );
  682. return (
  683. <div className="modal-mask">
  684. <Modal
  685. title={ Locale.Chat.EditMessage.Title }
  686. onClose={ props.onClose }
  687. actions={ [
  688. <IconButton
  689. text={ Locale.UI.Cancel }
  690. icon={ <CancelIcon /> }
  691. key="cancel"
  692. onClick={ () => {
  693. props.onClose();
  694. } }
  695. />,
  696. <IconButton
  697. type="primary"
  698. text={ Locale.UI.Confirm }
  699. icon={ <ConfirmIcon /> }
  700. key="ok"
  701. onClick={ () => {
  702. chatStore.updateCurrentSession(
  703. ( session ) => ( session.messages = messages ),
  704. );
  705. props.onClose();
  706. } }
  707. />,
  708. ] }
  709. >
  710. <List>
  711. <ListItem
  712. title={ Locale.Chat.EditMessage.Topic.Title }
  713. subTitle={ Locale.Chat.EditMessage.Topic.SubTitle }
  714. >
  715. <input
  716. type="text"
  717. value={ session.topic }
  718. onInput={ ( e ) =>
  719. chatStore.updateCurrentSession(
  720. ( session ) => ( session.topic = e.currentTarget.value ),
  721. )
  722. }
  723. ></input>
  724. </ListItem>
  725. </List>
  726. <ContextPrompts
  727. context={ messages }
  728. updateContext={ ( updater ) => {
  729. const newMessages = messages.slice();
  730. updater( newMessages );
  731. setMessages( newMessages );
  732. } }
  733. />
  734. </Modal>
  735. </div>
  736. );
  737. }
  738. export function DeleteImageButton( props : { deleteImage : () => void } ) {
  739. return (
  740. <div className={ styles[ "delete-image" ] } onClick={ props.deleteImage }>
  741. <DeleteIcon />
  742. </div>
  743. );
  744. }
  745. function _Chat() {
  746. type RenderMessage = ChatMessage & { preview? : boolean };
  747. const chatStore = useChatStore();
  748. const session = chatStore.currentSession();
  749. const config = useAppConfig();
  750. config.sendPreviewBubble = false;
  751. const fontSize = config.fontSize;
  752. const fontFamily = config.fontFamily;
  753. const [ showExport, setShowExport ] = useState( false );
  754. const inputRef = useRef<HTMLTextAreaElement>( null );
  755. const [ userInput, setUserInput ] = useState( "" );
  756. const [ isLoading, setIsLoading ] = useState( false );
  757. const { submitKey, shouldSubmit } = useSubmitHandler();
  758. const scrollRef = useRef<HTMLDivElement>( null );
  759. const isScrolledToBottom = scrollRef?.current
  760. ? Math.abs(
  761. scrollRef.current.scrollHeight -
  762. ( scrollRef.current.scrollTop + scrollRef.current.clientHeight ),
  763. ) <= 1
  764. : false;
  765. const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(
  766. scrollRef,
  767. isScrolledToBottom,
  768. );
  769. const [ hitBottom, setHitBottom ] = useState( true );
  770. const isMobileScreen = useMobileScreen();
  771. const navigate = useNavigate();
  772. const [ attachImages, setAttachImages ] = useState<string[]>( [] );
  773. const [ uploading, setUploading ] = useState( false );
  774. // prompt hints
  775. const promptStore = usePromptStore();
  776. const [ promptHints, setPromptHints ] = useState<RenderPrompt[]>( [] );
  777. const onSearch = useDebouncedCallback(
  778. ( text : string ) => {
  779. const matchedPrompts = promptStore.search( text );
  780. setPromptHints( matchedPrompts );
  781. },
  782. 100,
  783. { leading: true, trailing: true },
  784. );
  785. useEffect( () => {
  786. chatStore.updateCurrentSession( ( session ) => {
  787. session.appId = '1881234567890';
  788. } );
  789. }, [] )
  790. const [ inputRows, setInputRows ] = useState( 2 );
  791. const measure = useDebouncedCallback(
  792. () => {
  793. const rows = inputRef.current ? autoGrowTextArea( inputRef.current ) : 1;
  794. const inputRows = Math.min(
  795. 20,
  796. Math.max( 2 + Number( !isMobileScreen ), rows ),
  797. );
  798. setInputRows( inputRows );
  799. },
  800. 100,
  801. {
  802. leading: true,
  803. trailing: true,
  804. },
  805. );
  806. // eslint-disable-next-line react-hooks/exhaustive-deps
  807. useEffect( measure, [ userInput ] );
  808. // chat commands shortcuts
  809. const chatCommands = useChatCommand( {
  810. new: () => chatStore.newSession(),
  811. // newm: () => navigate(Path.MaskChat), // 关闭mask入口 ,后续有需求再二开
  812. prev: () => chatStore.nextSession( - 1 ),
  813. next: () => chatStore.nextSession( 1 ),
  814. clear: () =>
  815. chatStore.updateCurrentSession(
  816. ( session ) => ( session.clearContextIndex = session.messages.length ),
  817. ),
  818. del: () => chatStore.deleteSession( chatStore.currentSessionIndex ),
  819. } );
  820. // only search prompts when user input is short
  821. const SEARCH_TEXT_LIMIT = 30;
  822. const onInput = ( text : string ) => {
  823. setUserInput( text );
  824. const n = text.trim().length;
  825. // clear search results
  826. if ( n === 0 ) {
  827. setPromptHints( [] );
  828. } else if ( text.match( ChatCommandPrefix ) ) {
  829. setPromptHints( chatCommands.search( text ) );
  830. } else if ( !config.disablePromptHint && n < SEARCH_TEXT_LIMIT ) {
  831. // check if need to trigger auto completion
  832. if ( text.startsWith( "/" ) ) {
  833. let searchText = text.slice( 1 );
  834. onSearch( searchText );
  835. }
  836. }
  837. };
  838. const doSubmit = ( userInput : string ) => {
  839. if ( userInput.trim() === "" ) return;
  840. const matchCommand = chatCommands.match( userInput );
  841. if ( matchCommand.matched ) {
  842. setUserInput( "" );
  843. setPromptHints( [] );
  844. matchCommand.invoke();
  845. return;
  846. }
  847. setIsLoading( true );
  848. chatStore.onUserInput( fileList, userInput, attachImages ).then( () => setIsLoading( false ) );
  849. setAttachImages( [] );
  850. localStorage.setItem( LAST_INPUT_KEY, userInput );
  851. setUserInput( "" );
  852. setPromptHints( [] );
  853. if ( !isMobileScreen ) inputRef.current?.focus();
  854. setAutoScroll( true );
  855. };
  856. const onPromptSelect = ( prompt : RenderPrompt ) => {
  857. setTimeout( () => {
  858. setPromptHints( [] );
  859. const matchedChatCommand = chatCommands.match( prompt.content );
  860. if ( matchedChatCommand.matched ) {
  861. // if user is selecting a chat command, just trigger it
  862. matchedChatCommand.invoke();
  863. setUserInput( "" );
  864. } else {
  865. // or fill the prompt
  866. setUserInput( prompt.content );
  867. }
  868. inputRef.current?.focus();
  869. }, 30 );
  870. };
  871. // stop response
  872. const onUserStop = ( messageId : string ) => {
  873. ChatControllerPool.stop( session.id, messageId );
  874. };
  875. useEffect( () => {
  876. chatStore.updateCurrentSession( ( session ) => {
  877. const stopTiming = Date.now() - REQUEST_TIMEOUT_MS;
  878. session.messages.forEach( ( m ) => {
  879. // check if should stop all stale messages
  880. if ( m.isError || new Date( m.date ).getTime() < stopTiming ) {
  881. if ( m.streaming ) {
  882. m.streaming = false;
  883. }
  884. if ( m.content.length === 0 ) {
  885. m.isError = true;
  886. m.content = prettyObject( {
  887. error: true,
  888. message: "empty response",
  889. } );
  890. }
  891. }
  892. } );
  893. // auto sync mask config from global config
  894. if ( session.mask.syncGlobalConfig ) {
  895. console.log( "[Mask] syncing from global, name = ", session.mask.name );
  896. session.mask.modelConfig = { ...config.modelConfig };
  897. }
  898. } );
  899. // eslint-disable-next-line react-hooks/exhaustive-deps
  900. }, [] );
  901. // check if should send message
  902. const onInputKeyDown = ( e : React.KeyboardEvent<HTMLTextAreaElement> ) => {
  903. if (
  904. e.key === "ArrowUp" &&
  905. userInput.length <= 0 &&
  906. !( e.metaKey || e.altKey || e.ctrlKey )
  907. ) {
  908. setUserInput( localStorage.getItem( LAST_INPUT_KEY ) ?? "" );
  909. e.preventDefault();
  910. return;
  911. }
  912. if ( shouldSubmit( e ) && promptHints.length === 0 ) {
  913. doSubmit( userInput );
  914. e.preventDefault();
  915. }
  916. };
  917. const onRightClick = ( e : any, message : ChatMessage ) => {
  918. // copy to clipboard
  919. if ( selectOrCopy( e.currentTarget, getMessageTextContent( message ) ) ) {
  920. if ( userInput.length === 0 ) {
  921. setUserInput( getMessageTextContent( message ) );
  922. }
  923. e.preventDefault();
  924. }
  925. };
  926. const deleteMessage = ( msgId? : string ) => {
  927. chatStore.updateCurrentSession(
  928. ( session ) =>
  929. ( session.messages = session.messages.filter( ( m ) => m.id !== msgId ) ),
  930. );
  931. };
  932. const onDelete = ( msgId : string ) => {
  933. deleteMessage( msgId );
  934. };
  935. const onResend = ( message : ChatMessage ) => {
  936. // when it is resending a message
  937. // 1. for a user's message, find the next bot response
  938. // 2. for a bot's message, find the last user's input
  939. // 3. delete original user input and bot's message
  940. // 4. resend the user's input
  941. const resendingIndex = session.messages.findIndex(
  942. ( m ) => m.id === message.id,
  943. );
  944. if ( resendingIndex < 0 || resendingIndex >= session.messages.length ) {
  945. console.error( "[Chat] failed to find resending message", message );
  946. return;
  947. }
  948. let userMessage : ChatMessage | undefined;
  949. let botMessage : ChatMessage | undefined;
  950. if ( message.role === "assistant" ) {
  951. // if it is resending a bot's message, find the user input for it
  952. botMessage = message;
  953. for ( let i = resendingIndex; i >= 0; i -= 1 ) {
  954. if ( session.messages[ i ].role === "user" ) {
  955. userMessage = session.messages[ i ];
  956. break;
  957. }
  958. }
  959. } else if ( message.role === "user" ) {
  960. // if it is resending a user's input, find the bot's response
  961. userMessage = message;
  962. for ( let i = resendingIndex; i < session.messages.length; i += 1 ) {
  963. if ( session.messages[ i ].role === "assistant" ) {
  964. botMessage = session.messages[ i ];
  965. break;
  966. }
  967. }
  968. }
  969. if ( userMessage === undefined ) {
  970. console.error( "[Chat] failed to resend", message );
  971. return;
  972. }
  973. // delete the original messages
  974. deleteMessage( userMessage.id );
  975. deleteMessage( botMessage?.id );
  976. // resend the message
  977. setIsLoading( true );
  978. const textContent = getMessageTextContent( userMessage );
  979. const images = getMessageImages( userMessage );
  980. chatStore.onUserInput( [], textContent, images ).then( () => setIsLoading( false ) );
  981. inputRef.current?.focus();
  982. };
  983. const onPinMessage = ( message : ChatMessage ) => {
  984. chatStore.updateCurrentSession( ( session ) =>
  985. session.mask.context.push( message ),
  986. );
  987. showToast( Locale.Chat.Actions.PinToastContent, {
  988. text: Locale.Chat.Actions.PinToastAction,
  989. onClick: () => {
  990. setShowPromptModal( true );
  991. },
  992. } );
  993. };
  994. const context : RenderMessage[] = useMemo( () => {
  995. return session.mask.hideContext ? [] : session.mask.context.slice();
  996. }, [ session.mask.context, session.mask.hideContext ] );
  997. const accessStore = useAccessStore();
  998. if (
  999. context.length === 0 &&
  1000. session.messages.at( 0 )?.content !== BOT_HELLO.content
  1001. ) {
  1002. const copiedHello = Object.assign( {}, BOT_HELLO );
  1003. if ( !accessStore.isAuthorized() ) {
  1004. copiedHello.content = Locale.Error.Unauthorized;
  1005. }
  1006. context.push( copiedHello );
  1007. }
  1008. // preview messages
  1009. const renderMessages = useMemo( () => {
  1010. return context.concat( session.messages as RenderMessage[] ).concat(
  1011. isLoading
  1012. ? [
  1013. {
  1014. ...createMessage( {
  1015. role: "assistant",
  1016. content: "……",
  1017. } ),
  1018. preview: true,
  1019. },
  1020. ]
  1021. : [],
  1022. ).concat(
  1023. userInput.length > 0 && config.sendPreviewBubble
  1024. ? [
  1025. {
  1026. ...createMessage( {
  1027. role: "user",
  1028. content: userInput,
  1029. } ),
  1030. preview: true,
  1031. },
  1032. ]
  1033. : [],
  1034. );
  1035. }, [
  1036. config.sendPreviewBubble,
  1037. context,
  1038. isLoading,
  1039. session.messages,
  1040. userInput,
  1041. ] );
  1042. const [ msgRenderIndex, _setMsgRenderIndex ] = useState(
  1043. Math.max( 0, renderMessages.length - CHAT_PAGE_SIZE ),
  1044. );
  1045. function setMsgRenderIndex( newIndex : number ) {
  1046. newIndex = Math.min( renderMessages.length - CHAT_PAGE_SIZE, newIndex );
  1047. newIndex = Math.max( 0, newIndex );
  1048. _setMsgRenderIndex( newIndex );
  1049. }
  1050. const messages = useMemo( () => {
  1051. const endRenderIndex = Math.min(
  1052. msgRenderIndex + 3 * CHAT_PAGE_SIZE,
  1053. renderMessages.length,
  1054. );
  1055. return renderMessages.slice( msgRenderIndex, endRenderIndex );
  1056. }, [ msgRenderIndex, renderMessages ] );
  1057. const onChatBodyScroll = ( e : HTMLElement ) => {
  1058. const bottomHeight = e.scrollTop + e.clientHeight;
  1059. const edgeThreshold = e.clientHeight;
  1060. const isTouchTopEdge = e.scrollTop <= edgeThreshold;
  1061. const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
  1062. const isHitBottom =
  1063. bottomHeight >= e.scrollHeight - ( isMobileScreen ? 4 : 10 );
  1064. const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
  1065. const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
  1066. if ( isTouchTopEdge && !isTouchBottomEdge ) {
  1067. setMsgRenderIndex( prevPageMsgIndex );
  1068. } else if ( isTouchBottomEdge ) {
  1069. setMsgRenderIndex( nextPageMsgIndex );
  1070. }
  1071. setHitBottom( isHitBottom );
  1072. setAutoScroll( isHitBottom );
  1073. };
  1074. function scrollToBottom() {
  1075. setMsgRenderIndex( renderMessages.length - CHAT_PAGE_SIZE );
  1076. scrollDomToBottom();
  1077. }
  1078. // clear context index = context length + index in messages
  1079. const clearContextIndex =
  1080. ( session.clearContextIndex ?? - 1 ) >= 0
  1081. ? session.clearContextIndex! + context.length - msgRenderIndex
  1082. : - 1;
  1083. const [ showPromptModal, setShowPromptModal ] = useState( false );
  1084. const clientConfig = useMemo( () => getClientConfig(), [] );
  1085. const autoFocus = !isMobileScreen; // wont auto focus on mobile screen
  1086. const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
  1087. useCommand( {
  1088. fill: setUserInput,
  1089. submit: ( text ) => {
  1090. doSubmit( text );
  1091. },
  1092. code: ( text ) => {
  1093. if ( accessStore.disableFastLink ) return;
  1094. console.log( "[Command] got code from url: ", text );
  1095. showConfirm( Locale.URLCommand.Code + `code = ${ text }` ).then( ( res ) => {
  1096. if ( res ) {
  1097. accessStore.update( ( access ) => ( access.accessCode = text ) );
  1098. }
  1099. } );
  1100. },
  1101. settings: ( text ) => {
  1102. if ( accessStore.disableFastLink ) return;
  1103. try {
  1104. const payload = JSON.parse( text ) as {
  1105. key? : string;
  1106. url? : string;
  1107. };
  1108. console.log( "[Command] got settings from url: ", payload );
  1109. if ( payload.key || payload.url ) {
  1110. showConfirm(
  1111. Locale.URLCommand.Settings +
  1112. `\n${ JSON.stringify( payload, null, 4 ) }`,
  1113. ).then( ( res ) => {
  1114. if ( !res ) return;
  1115. if ( payload.key ) {
  1116. accessStore.update(
  1117. ( access ) => ( access.openaiApiKey = payload.key! ),
  1118. );
  1119. }
  1120. if ( payload.url ) {
  1121. accessStore.update( ( access ) => ( access.openaiUrl = payload.url! ) );
  1122. }
  1123. accessStore.update( ( access ) => ( access.useCustomConfig = true ) );
  1124. } );
  1125. }
  1126. } catch {
  1127. console.error( "[Command] failed to get settings from url: ", text );
  1128. }
  1129. },
  1130. } );
  1131. // edit / insert message modal
  1132. const [ isEditingMessage, setIsEditingMessage ] = useState( false );
  1133. // remember unfinished input
  1134. useEffect( () => {
  1135. // try to load from local storage
  1136. const key = UNFINISHED_INPUT( session.id );
  1137. const mayBeUnfinishedInput = localStorage.getItem( key );
  1138. if ( mayBeUnfinishedInput && userInput.length === 0 ) {
  1139. setUserInput( mayBeUnfinishedInput );
  1140. localStorage.removeItem( key );
  1141. }
  1142. const dom = inputRef.current;
  1143. return () => {
  1144. localStorage.setItem( key, dom?.value ?? "" );
  1145. };
  1146. // eslint-disable-next-line react-hooks/exhaustive-deps
  1147. }, [] );
  1148. const handlePaste = useCallback(
  1149. async ( event : React.ClipboardEvent<HTMLTextAreaElement> ) => {
  1150. const currentModel = chatStore.currentSession().mask.modelConfig.model;
  1151. if ( !isVisionModel( currentModel ) ) {
  1152. return;
  1153. }
  1154. const items = ( event.clipboardData || window.clipboardData ).items;
  1155. for ( const item of items ) {
  1156. if ( item.kind === "file" && item.type.startsWith( "image/" ) ) {
  1157. event.preventDefault();
  1158. const file = item.getAsFile();
  1159. if ( file ) {
  1160. const images : string[] = [];
  1161. images.push( ...attachImages );
  1162. images.push(
  1163. ...( await new Promise<string[]>( ( res, rej ) => {
  1164. setUploading( true );
  1165. const imagesData : string[] = [];
  1166. uploadImageRemote( file ).then( ( dataUrl ) => {
  1167. imagesData.push( dataUrl );
  1168. setUploading( false );
  1169. res( imagesData );
  1170. } ).catch( ( e ) => {
  1171. setUploading( false );
  1172. rej( e );
  1173. } );
  1174. } ) ),
  1175. );
  1176. const imagesLength = images.length;
  1177. if ( imagesLength > 3 ) {
  1178. images.splice( 3, imagesLength - 3 );
  1179. }
  1180. setAttachImages( images );
  1181. }
  1182. }
  1183. }
  1184. },
  1185. [ attachImages, chatStore ],
  1186. );
  1187. async function uploadImage() {
  1188. const images : string[] = [];
  1189. images.push( ...attachImages );
  1190. images.push(
  1191. ...( await new Promise<string[]>( ( res, rej ) => {
  1192. const fileInput = document.createElement( "input" );
  1193. fileInput.type = "file";
  1194. fileInput.accept =
  1195. "image/png, image/jpeg, image/webp, image/heic, image/heif";
  1196. fileInput.multiple = true;
  1197. fileInput.onchange = ( event : any ) => {
  1198. setUploading( true );
  1199. const files = event.target.files;
  1200. const imagesData : string[] = [];
  1201. for ( let i = 0; i < files.length; i ++ ) {
  1202. const file = event.target.files[ i ];
  1203. uploadImageRemote( file ).then( ( dataUrl ) => {
  1204. imagesData.push( dataUrl );
  1205. if (
  1206. imagesData.length === 3 ||
  1207. imagesData.length === files.length
  1208. ) {
  1209. setUploading( false );
  1210. res( imagesData );
  1211. }
  1212. } ).catch( ( e ) => {
  1213. setUploading( false );
  1214. rej( e );
  1215. } );
  1216. }
  1217. };
  1218. fileInput.click();
  1219. } ) ),
  1220. );
  1221. const imagesLength = images.length;
  1222. if ( imagesLength > 3 ) {
  1223. images.splice( 3, imagesLength - 3 );
  1224. }
  1225. setAttachImages( images );
  1226. }
  1227. const [ fileList, setFileList ] = useState<any[]>( [] );
  1228. // 上传配置
  1229. const uploadConfig : UploadProps = {
  1230. action: '/deepseek-api' + '/upload/file',
  1231. method: 'POST',
  1232. accept: [ '.pdf', '.txt', '.doc', '.docx' ].join( ',' ),
  1233. };
  1234. interface FileIconProps {
  1235. fileName : string;
  1236. }
  1237. const FileIcon : React.FC<FileIconProps> = ( props : FileIconProps ) => {
  1238. const style = {
  1239. fontSize: '30px',
  1240. color: '#3875f6',
  1241. }
  1242. let icon = <FileOutlined style={ style } />
  1243. if ( props.fileName ) {
  1244. const suffix = props.fileName.split( '.' ).pop() || '';
  1245. switch ( suffix ) {
  1246. case 'pdf':
  1247. icon = <FilePdfOutlined style={ style } />
  1248. break;
  1249. case 'txt':
  1250. icon = <FileTextOutlined style={ style } />
  1251. break;
  1252. case 'doc':
  1253. case 'docx':
  1254. icon = <FileWordOutlined style={ style } />
  1255. break;
  1256. default:
  1257. break;
  1258. }
  1259. }
  1260. return icon;
  1261. }
  1262. const [ isDeepThink, setIsDeepThink ] = useState<boolean>( chatStore.isDeepThink );
  1263. // 切换聊天窗口后清理上传文件信息
  1264. useEffect( () => {
  1265. setFileList( [] )
  1266. }, [ chatStore.currentSession() ] )
  1267. const couldStop = ChatControllerPool.hasPending();
  1268. const stopAll = () => ChatControllerPool.stopAll();
  1269. // 切换聊天窗口后清理上传文件信息
  1270. useEffect( () => {
  1271. setWebSearch( false );
  1272. }, [ chatStore.currentSession() ] )
  1273. const [ webSearch, setWebSearch ] = useState<boolean>( chatStore.web_search );
  1274. const [ drawerOpen, setDrawerOpen ] = useState( false );
  1275. type DrawerList = {
  1276. title : string,
  1277. content : string,
  1278. web_url : string,
  1279. }[]
  1280. const [ drawerList, setDrawerList ] = useState<DrawerList>( [] );
  1281. interface NetworkDrawerProps {
  1282. list : DrawerList,
  1283. }
  1284. const NetworkDrawer : React.FC<NetworkDrawerProps> = ( props ) => {
  1285. return (
  1286. <Drawer
  1287. title='网页搜索'
  1288. open={ drawerOpen }
  1289. onClose={ () => {
  1290. setDrawerOpen( false );
  1291. } }
  1292. >
  1293. { props.list.map( ( item, index ) => {
  1294. return <div
  1295. style={ {
  1296. padding: 10,
  1297. background: '#fafafa',
  1298. borderRadius: 4,
  1299. marginBottom: 10,
  1300. cursor: 'pointer',
  1301. } }
  1302. key={ index }
  1303. onClick={ () => {
  1304. window.open( item.web_url );
  1305. } }
  1306. >
  1307. <div style={ {
  1308. margin: '5px 0',
  1309. fontSize: 16,
  1310. display: '-webkit-box',
  1311. WebkitBoxOrient: 'vertical',
  1312. WebkitLineClamp: 2,// 限制显示两行
  1313. overflow: 'hidden',
  1314. } }>
  1315. { item.title }
  1316. </div>
  1317. <div style={ {
  1318. color: '#afafaf',
  1319. display: '-webkit-box',
  1320. WebkitBoxOrient: 'vertical',
  1321. WebkitLineClamp: 4,// 限制显示两行
  1322. overflow: 'hidden',
  1323. textOverflow: 'ellipsis',
  1324. } }>
  1325. { item.content }
  1326. </div>
  1327. </div>
  1328. } )
  1329. }
  1330. </Drawer>
  1331. )
  1332. }
  1333. return (
  1334. <div className={ styles.chat } style={messages.length === 1 ? { justifyContent:'center',alignItems:'center' } : {}} key={ session.id }>
  1335. {
  1336. isMobileScreen && location.pathname !== '/' &&
  1337. <div className="window-header" data-tauri-drag-region>
  1338. <div style={ { display: 'flex', alignItems: 'center' } }
  1339. className={ `window-header-title ${ styles[ "chat-body-title" ] }` }>
  1340. <div>
  1341. <IconButton
  1342. style={ { padding: 0, marginRight: 20 } }
  1343. icon={ <LeftIcon /> }
  1344. text={ Locale.NewChat.Return }
  1345. onClick={ () => navigate( '/deepseekChat' ) }
  1346. />
  1347. </div>
  1348. </div>
  1349. </div>
  1350. }
  1351. {messages.length > 1?<div
  1352. className={ styles[ "chat-body" ] }
  1353. ref={ scrollRef }
  1354. onScroll={ ( e ) => onChatBodyScroll( e.currentTarget ) }
  1355. onMouseDown={ () => inputRef.current?.blur() }
  1356. onTouchStart={ () => {
  1357. inputRef.current?.blur();
  1358. setAutoScroll( false );
  1359. } }
  1360. >
  1361. <>
  1362. { messages.map( ( message, i ) => {
  1363. const isUser = message.role === "user";
  1364. const isContext = i < context.length;
  1365. const showActions =
  1366. i > 0 &&
  1367. !( message.preview || message.content.length === 0 ) &&
  1368. !isContext;
  1369. const showTyping = message.preview || message.streaming;
  1370. const shouldShowClearContextDivider = i === clearContextIndex - 1;
  1371. return (
  1372. <Fragment key={ message.id }>
  1373. <div
  1374. className={
  1375. isUser ? styles[ "chat-message-user" ] : styles[ "chat-message" ]
  1376. }
  1377. >
  1378. <div className={ styles[ "chat-message-container" ] }
  1379. style={ { display: 'flex', flexDirection: 'column' } }>
  1380. <div className={ styles[ "chat-message-header" ] }>
  1381. <div className={ styles[ "chat-message-avatar" ] }>
  1382. { isUser ? (
  1383. // 在这里换头像
  1384. <div style={ { position: 'relative' } }>
  1385. <div
  1386. style={ {
  1387. position: 'absolute',
  1388. zIndex: 2,
  1389. top: '50%',
  1390. left: '50%',
  1391. transform: ' translate(-110%, -100%)',
  1392. fontSize: 14,
  1393. } }>
  1394. </div>
  1395. </div>
  1396. ) : (
  1397. <>
  1398. { [ "system" ].includes( message.role ) ? (
  1399. <Avatar avatar="2699-fe0f" />
  1400. ) : (
  1401. <MaskAvatar
  1402. avatar={ session.mask.avatar }
  1403. model={
  1404. message.model || session.mask.modelConfig.model
  1405. }
  1406. />
  1407. ) }
  1408. </>
  1409. ) }
  1410. </div>
  1411. </div>
  1412. {
  1413. isUser && message.document && message.document.id &&
  1414. <a style={ {
  1415. padding: '10px',
  1416. background: '#f7f7f7',
  1417. borderRadius: '10px',
  1418. textDecoration: 'none',
  1419. color: '#24292f',
  1420. display: 'flex',
  1421. alignItems: 'center'
  1422. } } href={ message.document.url } target="_blank">
  1423. <FileIcon fileName={ message.document.name } />
  1424. <div style={ { marginLeft: 8, fontSize: '14px' } }>
  1425. { message.document.name }
  1426. </div>
  1427. </a>
  1428. }
  1429. {/* {showTyping && (
  1430. <div className={styles["chat-message-status"]}>
  1431. 正在输入…
  1432. </div>
  1433. )} */ }
  1434. {
  1435. message.networkInfo && message.networkInfo.list.length > 0 &&
  1436. <div style={ { marginTop: 10 } }>
  1437. <Button
  1438. icon={ <RightOutlined /> }
  1439. iconPosition='end'
  1440. onClick={ () => {
  1441. setDrawerList( message.networkInfo!.list );
  1442. setDrawerOpen( true );
  1443. } }
  1444. >
  1445. 搜索到{ message.networkInfo.list.length }篇相关资料
  1446. </Button>
  1447. {
  1448. drawerOpen &&
  1449. <NetworkDrawer
  1450. list={ message.networkInfo.list }
  1451. />
  1452. }
  1453. </div>
  1454. }
  1455. <div className={ styles[ "chat-message-item" ] }>
  1456. <Markdown
  1457. key={ message.streaming ? "loading" : "done" }
  1458. content={ getMessageTextContent( message ) }
  1459. loading={
  1460. ( message.preview || message.streaming ) &&
  1461. message.content.length === 0 &&
  1462. !isUser
  1463. }
  1464. onDoubleClickCapture={ () => {
  1465. if ( !isMobileScreen ) return;
  1466. setUserInput( getMessageTextContent( message ) );
  1467. } }
  1468. fontSize={ fontSize }
  1469. fontFamily={ fontFamily }
  1470. parentRef={ scrollRef }
  1471. defaultShow={ i >= messages.length - 6 }
  1472. />
  1473. { getMessageImages( message ).length == 1 && (
  1474. <img
  1475. className={ styles[ "chat-message-item-image" ] }
  1476. src={ getMessageImages( message )[ 0 ] }
  1477. alt=""
  1478. />
  1479. ) }
  1480. { getMessageImages( message ).length > 1 && (
  1481. <div
  1482. className={ styles[ "chat-message-item-images" ] }
  1483. style={
  1484. {
  1485. "--image-count": getMessageImages( message ).length,
  1486. } as React.CSSProperties
  1487. }
  1488. >
  1489. { getMessageImages( message ).map( ( image, index ) => {
  1490. return (
  1491. <img
  1492. className={
  1493. styles[ "chat-message-item-image-multi" ]
  1494. }
  1495. key={ index }
  1496. src={ image }
  1497. alt=""
  1498. />
  1499. );
  1500. } ) }
  1501. </div>
  1502. ) }
  1503. </div>
  1504. </div>
  1505. </div>
  1506. { shouldShowClearContextDivider && <ClearContextDivider /> }
  1507. </Fragment>
  1508. );
  1509. } ) }
  1510. </>
  1511. </div>:
  1512. <>
  1513. <div style={{ padding: '0 20px' }}>
  1514. <div style={{ display: 'flex', justifyContent: 'center' }}>
  1515. <img style={{ width: 80, height: 80 }} src={avatarSrc.src} />
  1516. </div>
  1517. <p style={{ textAlign: 'center' }}>
  1518. 你好 我是小智
  1519. </p>
  1520. </div>
  1521. </>}
  1522. {/* 底部按钮 */}
  1523. <div style={messages.length === 1 ? { border:'none', width:'60%' } : {} }
  1524. className={ styles[ "chat-input-panel" ] } >
  1525. <ChatActions
  1526. setUserInput={ setUserInput }
  1527. doSubmit={ doSubmit }
  1528. uploadImage={ uploadImage }
  1529. setAttachImages={ setAttachImages }
  1530. setUploading={ setUploading }
  1531. showPromptModal={ () => setShowPromptModal( true ) }
  1532. scrollToBottom={ scrollToBottom }
  1533. hitBottom={ hitBottom }
  1534. uploading={ uploading }
  1535. showPromptHints={ () => {
  1536. if ( promptHints.length > 0 ) {
  1537. setPromptHints( [] );
  1538. return;
  1539. }
  1540. inputRef.current?.focus();
  1541. setUserInput( "/" );
  1542. onSearch( "" );
  1543. } }
  1544. />
  1545. {
  1546. fileList.length > 0 &&
  1547. <div style={ { marginBottom: 20 } }>
  1548. <Upload
  1549. fileList={ fileList }
  1550. onRemove={ ( file ) => {
  1551. setFileList( fileList.filter( item => item.uid !== file.uid ) );
  1552. } }
  1553. />
  1554. </div>
  1555. }
  1556. <label
  1557. className={ `${ styles[ "chat-input-panel-inner" ] } ${ attachImages.length != 0
  1558. ? styles[ "chat-input-panel-inner-attach" ]
  1559. : ""
  1560. }` }
  1561. htmlFor="chat-input"
  1562. >
  1563. <textarea
  1564. id="chat-input"
  1565. ref={ inputRef }
  1566. className={ styles[ "chat-input2" ] }
  1567. placeholder={ Locale.Chat.Input( submitKey ) }
  1568. onInput={ ( e ) => onInput( e.currentTarget.value ) }
  1569. value={ userInput }
  1570. onKeyDown={ onInputKeyDown }
  1571. onFocus={ scrollToBottom }
  1572. onClick={ scrollToBottom }
  1573. onPaste={ handlePaste }
  1574. rows={ inputRows }
  1575. autoFocus={ autoFocus }
  1576. style={ {
  1577. fontSize: config.fontSize,
  1578. fontFamily: config.fontFamily,
  1579. } }
  1580. />
  1581. { attachImages.length != 0 && (
  1582. <div className={ styles[ "attach-images" ] }>
  1583. { attachImages.map( ( image, index ) => {
  1584. return (
  1585. <div
  1586. key={ index }
  1587. className={ styles[ "attach-image" ] }
  1588. style={ { backgroundImage: `url("${ image }")` } }
  1589. >
  1590. <div className={ styles[ "attach-image-mask" ] }>
  1591. <DeleteImageButton
  1592. deleteImage={ () => {
  1593. setAttachImages(
  1594. attachImages.filter( ( _, i ) => i !== index ),
  1595. );
  1596. } }
  1597. />
  1598. </div>
  1599. </div>
  1600. );
  1601. } ) }
  1602. </div>
  1603. ) }
  1604. {/* 修改样式:输入框内部按钮区域 */ }
  1605. {/* <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 10 }}> */ }
  1606. {true&&<div className={ styles[ "chat-input-bottom-bar" ] }>
  1607. {/* <div style={{ display: 'flex', alignItems: 'center' }}> */ }
  1608. <div className={ styles[ "left-options" ] }>
  1609. {/*深度思考R1按钮*/ }
  1610. {false&&<Tooltip
  1611. title={
  1612. <span style={ { fontSize: 12, lineHeight: 1.4, minHeight: 24, padding: '4px 8px' } }>
  1613. { isDeepThink ? '关闭深度思考模式' : '启用深度思考模式' }
  1614. </span>
  1615. }
  1616. placement="left"
  1617. >
  1618. <div
  1619. // className={styles["option-item"]}
  1620. style={ {
  1621. padding: '0 12px',
  1622. height: 28,
  1623. borderRadius: 18,
  1624. fontSize: 12,
  1625. display: 'flex',
  1626. justifyContent: 'center',
  1627. alignItems: 'center',
  1628. // marginRight: 10,
  1629. cursor: 'pointer',
  1630. background: isDeepThink ? '#dee9fc' : '#f3f4f6',
  1631. color: isDeepThink ? '#3875f6' : '#000000',
  1632. // border: `1px solid ${isDeepThink ? '#3875f6' : 'transparent'}`,
  1633. transition: 'all 0.2s ease',
  1634. userSelect: 'none'
  1635. } }
  1636. onClick={ () => {
  1637. setIsDeepThink( !isDeepThink );
  1638. chatStore.setIsDeepThink( !isDeepThink );
  1639. } }
  1640. >
  1641. <img src={ isDeepThink ? sdsk_selected.src : sdsk.src }
  1642. style={ {
  1643. width: 16,
  1644. height: 16,
  1645. } }
  1646. />
  1647. <span style={ { fontSize: 11, marginLeft: 5 } }>
  1648. 深度思考
  1649. </span>
  1650. </div>
  1651. </Tooltip>}
  1652. {/*联网搜索按钮*/ }
  1653. <div style={ {
  1654. padding: '0 12px',
  1655. height: 28,
  1656. borderRadius: 18,
  1657. fontSize: 12,
  1658. display: 'flex',
  1659. justifyContent: 'center',
  1660. alignItems: 'center',
  1661. cursor: 'pointer',
  1662. background: webSearch ? '#dee9fc' : '#f3f4f6',
  1663. color: webSearch ? '#3875f6' : '#000000',
  1664. transition: 'all 0.2s ease',
  1665. userSelect: 'none'
  1666. } }
  1667. onClick={ () => {
  1668. setWebSearch( !webSearch );
  1669. chatStore.setWebSearch( !webSearch );
  1670. } }
  1671. >
  1672. <img src={ webSearch ? hlw_selected.src : hlw.src }
  1673. style={ {
  1674. width: 16,
  1675. height: 16,
  1676. } }
  1677. />
  1678. <span style={ { fontSize: 11, marginLeft: 5, marginRight: 10 } }>
  1679. 联网搜索
  1680. </span>
  1681. </div>
  1682. </div>
  1683. <div style={ { display: 'flex', alignItems: 'center' } }>
  1684. {
  1685. !webSearch &&
  1686. <div style={ { marginRight: 10 } }>
  1687. <Upload
  1688. { ...uploadConfig }
  1689. showUploadList={ false }
  1690. maxCount={ 1 }
  1691. onChange={ ( info ) => {
  1692. const fileList = info.fileList.map( ( file ) => {
  1693. const data = file.response;
  1694. return {
  1695. ...file,
  1696. url: data?.document_url || file.url,
  1697. documentId: data?.document_id || '',
  1698. }
  1699. } );
  1700. setFileList( fileList );
  1701. if ( info.file.status === 'done' ) {// 上传成功
  1702. const { code, message: msg } = info.file.response;
  1703. if ( code === 200 ) {
  1704. message.success( '上传成功' );
  1705. } else {
  1706. message.error( msg );
  1707. }
  1708. } else if ( info.file.status === 'error' ) {// 上传失败
  1709. message.error( '上传失败' );
  1710. }
  1711. } }
  1712. >
  1713. <Tooltip
  1714. title={
  1715. <div style={ { padding: '4px 8px' } }>
  1716. <div style={ {
  1717. fontSize: 12,
  1718. lineHeight: 1.4,
  1719. marginBottom: 6,
  1720. } }>
  1721. 上传附件 (识别文本和图表中的内容)
  1722. </div>
  1723. <div style={ {
  1724. fontSize: 10,
  1725. color: '#8c8c8c',
  1726. lineHeight: 1.4,
  1727. }}>
  1728. <span>
  1729. 仅支持单个PDF/Word/TXT文件格式
  1730. </span>
  1731. <span>
  1732. (单个文件≤50MB)
  1733. </span>
  1734. </div>
  1735. </div>}
  1736. placement="top"
  1737. >
  1738. <div
  1739. style={ {
  1740. width: 28,
  1741. height: 28,
  1742. borderRadius: '50%',
  1743. background: '#4357d2',
  1744. display: 'flex',
  1745. justifyContent: 'center',
  1746. alignItems: 'center',
  1747. cursor: 'pointer',
  1748. transition: 'all 0.2s ease',
  1749. userSelect: 'none'
  1750. } }
  1751. >
  1752. <PaperClipOutlined style={ { color: '#FFFFFF', fontSize: '18px' } } />
  1753. </div>
  1754. </Tooltip>
  1755. </Upload>
  1756. </div>
  1757. }
  1758. <div
  1759. style={ {
  1760. width: 28,
  1761. height: 28,
  1762. borderRadius: '50%',
  1763. background: '#4357d2',
  1764. display: 'flex',
  1765. justifyContent: 'center',
  1766. alignItems: 'center',
  1767. cursor: 'pointer',
  1768. } }
  1769. onClick={ () => {
  1770. if ( couldStop ) {
  1771. stopAll();
  1772. } else {
  1773. doSubmit( userInput );
  1774. }
  1775. } }
  1776. >
  1777. {
  1778. couldStop ?
  1779. <div style={ { width: 13, height: 13, background: '#FFFFFF', borderRadius: 2 } }></div>
  1780. :
  1781. <div style={ { transform: 'rotate(-45deg)', padding: '0px 0px 3px 5px' } }>
  1782. <SendOutlined style={ { color: '#FFFFFF' } } />
  1783. </div>
  1784. }
  1785. </div>
  1786. </div>
  1787. </div>}
  1788. </label>
  1789. <div style={ { marginTop: 8, textAlign: 'center', color: '#888888', fontSize: 12 } }>
  1790. 内容由AI生成,仅供参考
  1791. </div>
  1792. </div>
  1793. {
  1794. showExport && (
  1795. <ExportMessageModal onClose={ () => setShowExport( false ) } />
  1796. )
  1797. }
  1798. {
  1799. isEditingMessage && (
  1800. <EditMessageModal
  1801. onClose={ () => {
  1802. setIsEditingMessage( false );
  1803. } }
  1804. />
  1805. )
  1806. }
  1807. </div>
  1808. );
  1809. }
  1810. export function Chat() {
  1811. const globalStore = useGlobalStore();
  1812. const chatStore = useChatStore();
  1813. const sessionIndex = chatStore.currentSessionIndex;
  1814. useEffect( () => {
  1815. globalStore.setShowMenu( true );
  1816. chatStore.setModel( 'DeepSeek' );
  1817. chatStore.setWebSearch( false );
  1818. }, [] );
  1819. return <_Chat key={ sessionIndex }></_Chat>;
  1820. }