DeepSeekChat.tsx 65 KB

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