DeepSeekHomeOnlyChat.tsx 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966
  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 "./chatHomeOnly.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(props?: { onMessageSent?: () => void }) {
  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( isMobileScreen ? 1 : 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( isMobileScreen ? 1 : (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. // 取消输入框的选定状态
  853. inputRef.current?.blur();
  854. setAutoScroll( true );
  855. // 通知父组件消息已发送
  856. props?.onMessageSent?.();
  857. };
  858. const onPromptSelect = ( prompt : RenderPrompt ) => {
  859. setTimeout( () => {
  860. setPromptHints( [] );
  861. const matchedChatCommand = chatCommands.match( prompt.content );
  862. if ( matchedChatCommand.matched ) {
  863. // if user is selecting a chat command, just trigger it
  864. matchedChatCommand.invoke();
  865. setUserInput( "" );
  866. } else {
  867. // or fill the prompt
  868. setUserInput( prompt.content );
  869. }
  870. inputRef.current?.focus();
  871. }, 30 );
  872. };
  873. // stop response
  874. const onUserStop = ( messageId : string ) => {
  875. ChatControllerPool.stop( session.id, messageId );
  876. };
  877. useEffect( () => {
  878. chatStore.updateCurrentSession( ( session ) => {
  879. const stopTiming = Date.now() - REQUEST_TIMEOUT_MS;
  880. session.messages.forEach( ( m ) => {
  881. // check if should stop all stale messages
  882. if ( m.isError || new Date( m.date ).getTime() < stopTiming ) {
  883. if ( m.streaming ) {
  884. m.streaming = false;
  885. }
  886. if ( m.content.length === 0 ) {
  887. m.isError = true;
  888. m.content = prettyObject( {
  889. error: true,
  890. message: "empty response",
  891. } );
  892. }
  893. }
  894. } );
  895. // auto sync mask config from global config
  896. if ( session.mask.syncGlobalConfig ) {
  897. console.log( "[Mask] syncing from global, name = ", session.mask.name );
  898. session.mask.modelConfig = { ...config.modelConfig };
  899. }
  900. } );
  901. // eslint-disable-next-line react-hooks/exhaustive-deps
  902. }, [] );
  903. // check if should send message
  904. const onInputKeyDown = ( e : React.KeyboardEvent<HTMLTextAreaElement> ) => {
  905. if (
  906. e.key === "ArrowUp" &&
  907. userInput.length <= 0 &&
  908. !( e.metaKey || e.altKey || e.ctrlKey )
  909. ) {
  910. setUserInput( localStorage.getItem( LAST_INPUT_KEY ) ?? "" );
  911. e.preventDefault();
  912. return;
  913. }
  914. if ( shouldSubmit( e ) && promptHints.length === 0 ) {
  915. doSubmit( userInput );
  916. e.preventDefault();
  917. }
  918. };
  919. const onRightClick = ( e : any, message : ChatMessage ) => {
  920. // copy to clipboard
  921. if ( selectOrCopy( e.currentTarget, getMessageTextContent( message ) ) ) {
  922. if ( userInput.length === 0 ) {
  923. setUserInput( getMessageTextContent( message ) );
  924. }
  925. e.preventDefault();
  926. }
  927. };
  928. const deleteMessage = ( msgId? : string ) => {
  929. chatStore.updateCurrentSession(
  930. ( session ) =>
  931. ( session.messages = session.messages.filter( ( m ) => m.id !== msgId ) ),
  932. );
  933. };
  934. const onDelete = ( msgId : string ) => {
  935. deleteMessage( msgId );
  936. };
  937. const onResend = ( message : ChatMessage ) => {
  938. // when it is resending a message
  939. // 1. for a user's message, find the next bot response
  940. // 2. for a bot's message, find the last user's input
  941. // 3. delete original user input and bot's message
  942. // 4. resend the user's input
  943. const resendingIndex = session.messages.findIndex(
  944. ( m ) => m.id === message.id,
  945. );
  946. if ( resendingIndex < 0 || resendingIndex >= session.messages.length ) {
  947. console.error( "[Chat] failed to find resending message", message );
  948. return;
  949. }
  950. let userMessage : ChatMessage | undefined;
  951. let botMessage : ChatMessage | undefined;
  952. if ( message.role === "assistant" ) {
  953. // if it is resending a bot's message, find the user input for it
  954. botMessage = message;
  955. for ( let i = resendingIndex; i >= 0; i -= 1 ) {
  956. if ( session.messages[ i ].role === "user" ) {
  957. userMessage = session.messages[ i ];
  958. break;
  959. }
  960. }
  961. } else if ( message.role === "user" ) {
  962. // if it is resending a user's input, find the bot's response
  963. userMessage = message;
  964. for ( let i = resendingIndex; i < session.messages.length; i += 1 ) {
  965. if ( session.messages[ i ].role === "assistant" ) {
  966. botMessage = session.messages[ i ];
  967. break;
  968. }
  969. }
  970. }
  971. if ( userMessage === undefined ) {
  972. console.error( "[Chat] failed to resend", message );
  973. return;
  974. }
  975. // delete the original messages
  976. deleteMessage( userMessage.id );
  977. deleteMessage( botMessage?.id );
  978. // resend the message
  979. setIsLoading( true );
  980. const textContent = getMessageTextContent( userMessage );
  981. const images = getMessageImages( userMessage );
  982. chatStore.onUserInput( [], textContent, images ).then( () => setIsLoading( false ) );
  983. inputRef.current?.focus();
  984. };
  985. const onPinMessage = ( message : ChatMessage ) => {
  986. chatStore.updateCurrentSession( ( session ) =>
  987. session.mask.context.push( message ),
  988. );
  989. showToast( Locale.Chat.Actions.PinToastContent, {
  990. text: Locale.Chat.Actions.PinToastAction,
  991. onClick: () => {
  992. setShowPromptModal( true );
  993. },
  994. } );
  995. };
  996. const context : RenderMessage[] = useMemo( () => {
  997. return session.mask.hideContext ? [] : session.mask.context.slice();
  998. }, [ session.mask.context, session.mask.hideContext ] );
  999. const accessStore = useAccessStore();
  1000. if (
  1001. context.length === 0 &&
  1002. session.messages.at( 0 )?.content !== BOT_HELLO.content
  1003. ) {
  1004. const copiedHello = Object.assign( {}, BOT_HELLO );
  1005. if ( !accessStore.isAuthorized() ) {
  1006. copiedHello.content = Locale.Error.Unauthorized;
  1007. }
  1008. context.push( copiedHello );
  1009. }
  1010. // preview messages
  1011. const renderMessages = useMemo( () => {
  1012. return context.concat( session.messages as RenderMessage[] ).concat(
  1013. isLoading
  1014. ? [
  1015. {
  1016. ...createMessage( {
  1017. role: "assistant",
  1018. content: "……",
  1019. } ),
  1020. preview: true,
  1021. },
  1022. ]
  1023. : [],
  1024. ).concat(
  1025. userInput.length > 0 && config.sendPreviewBubble
  1026. ? [
  1027. {
  1028. ...createMessage( {
  1029. role: "user",
  1030. content: userInput,
  1031. } ),
  1032. preview: true,
  1033. },
  1034. ]
  1035. : [],
  1036. );
  1037. }, [
  1038. config.sendPreviewBubble,
  1039. context,
  1040. isLoading,
  1041. session.messages,
  1042. userInput,
  1043. ] );
  1044. const [ msgRenderIndex, _setMsgRenderIndex ] = useState(
  1045. Math.max( 0, renderMessages.length - CHAT_PAGE_SIZE ),
  1046. );
  1047. function setMsgRenderIndex( newIndex : number ) {
  1048. newIndex = Math.min( renderMessages.length - CHAT_PAGE_SIZE, newIndex );
  1049. newIndex = Math.max( 0, newIndex );
  1050. _setMsgRenderIndex( newIndex );
  1051. }
  1052. const messages = useMemo( () => {
  1053. const endRenderIndex = Math.min(
  1054. msgRenderIndex + 3 * CHAT_PAGE_SIZE,
  1055. renderMessages.length,
  1056. );
  1057. return renderMessages.slice( msgRenderIndex, endRenderIndex );
  1058. }, [ msgRenderIndex, renderMessages ] );
  1059. const onChatBodyScroll = ( e : HTMLElement ) => {
  1060. const bottomHeight = e.scrollTop + e.clientHeight;
  1061. const edgeThreshold = e.clientHeight;
  1062. const isTouchTopEdge = e.scrollTop <= edgeThreshold;
  1063. const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
  1064. const isHitBottom =
  1065. bottomHeight >= e.scrollHeight - ( isMobileScreen ? 4 : 10 );
  1066. const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
  1067. const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
  1068. if ( isTouchTopEdge && !isTouchBottomEdge ) {
  1069. setMsgRenderIndex( prevPageMsgIndex );
  1070. } else if ( isTouchBottomEdge ) {
  1071. setMsgRenderIndex( nextPageMsgIndex );
  1072. }
  1073. setHitBottom( isHitBottom );
  1074. setAutoScroll( isHitBottom );
  1075. };
  1076. function scrollToBottom() {
  1077. setMsgRenderIndex( renderMessages.length - CHAT_PAGE_SIZE );
  1078. scrollDomToBottom();
  1079. }
  1080. // clear context index = context length + index in messages
  1081. const clearContextIndex =
  1082. ( session.clearContextIndex ?? - 1 ) >= 0
  1083. ? session.clearContextIndex! + context.length - msgRenderIndex
  1084. : - 1;
  1085. const [ showPromptModal, setShowPromptModal ] = useState( false );
  1086. const clientConfig = useMemo( () => getClientConfig(), [] );
  1087. const autoFocus = !isMobileScreen; // wont auto focus on mobile screen
  1088. const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
  1089. useCommand( {
  1090. fill: setUserInput,
  1091. submit: ( text ) => {
  1092. doSubmit( text );
  1093. },
  1094. code: ( text ) => {
  1095. if ( accessStore.disableFastLink ) return;
  1096. console.log( "[Command] got code from url: ", text );
  1097. showConfirm( Locale.URLCommand.Code + `code = ${ text }` ).then( ( res ) => {
  1098. if ( res ) {
  1099. accessStore.update( ( access ) => ( access.accessCode = text ) );
  1100. }
  1101. } );
  1102. },
  1103. settings: ( text ) => {
  1104. if ( accessStore.disableFastLink ) return;
  1105. try {
  1106. const payload = JSON.parse( text ) as {
  1107. key? : string;
  1108. url? : string;
  1109. };
  1110. console.log( "[Command] got settings from url: ", payload );
  1111. if ( payload.key || payload.url ) {
  1112. showConfirm(
  1113. Locale.URLCommand.Settings +
  1114. `\n${ JSON.stringify( payload, null, 4 ) }`,
  1115. ).then( ( res ) => {
  1116. if ( !res ) return;
  1117. if ( payload.key ) {
  1118. accessStore.update(
  1119. ( access ) => ( access.openaiApiKey = payload.key! ),
  1120. );
  1121. }
  1122. if ( payload.url ) {
  1123. accessStore.update( ( access ) => ( access.openaiUrl = payload.url! ) );
  1124. }
  1125. accessStore.update( ( access ) => ( access.useCustomConfig = true ) );
  1126. } );
  1127. }
  1128. } catch {
  1129. console.error( "[Command] failed to get settings from url: ", text );
  1130. }
  1131. },
  1132. } );
  1133. // edit / insert message modal
  1134. const [ isEditingMessage, setIsEditingMessage ] = useState( false );
  1135. // remember unfinished input
  1136. useEffect( () => {
  1137. // try to load from local storage
  1138. const key = UNFINISHED_INPUT( session.id );
  1139. const mayBeUnfinishedInput = localStorage.getItem( key );
  1140. if ( mayBeUnfinishedInput && userInput.length === 0 ) {
  1141. setUserInput( mayBeUnfinishedInput );
  1142. localStorage.removeItem( key );
  1143. }
  1144. const dom = inputRef.current;
  1145. return () => {
  1146. localStorage.setItem( key, dom?.value ?? "" );
  1147. };
  1148. // eslint-disable-next-line react-hooks/exhaustive-deps
  1149. }, [] );
  1150. const handlePaste = useCallback(
  1151. async ( event : React.ClipboardEvent<HTMLTextAreaElement> ) => {
  1152. const currentModel = chatStore.currentSession().mask.modelConfig.model;
  1153. if ( !isVisionModel( currentModel ) ) {
  1154. return;
  1155. }
  1156. const items = ( event.clipboardData || window.clipboardData ).items;
  1157. for ( const item of items ) {
  1158. if ( item.kind === "file" && item.type.startsWith( "image/" ) ) {
  1159. event.preventDefault();
  1160. const file = item.getAsFile();
  1161. if ( file ) {
  1162. const images : string[] = [];
  1163. images.push( ...attachImages );
  1164. images.push(
  1165. ...( await new Promise<string[]>( ( res, rej ) => {
  1166. setUploading( true );
  1167. const imagesData : string[] = [];
  1168. uploadImageRemote( file ).then( ( dataUrl ) => {
  1169. imagesData.push( dataUrl );
  1170. setUploading( false );
  1171. res( imagesData );
  1172. } ).catch( ( e ) => {
  1173. setUploading( false );
  1174. rej( e );
  1175. } );
  1176. } ) ),
  1177. );
  1178. const imagesLength = images.length;
  1179. if ( imagesLength > 3 ) {
  1180. images.splice( 3, imagesLength - 3 );
  1181. }
  1182. setAttachImages( images );
  1183. }
  1184. }
  1185. }
  1186. },
  1187. [ attachImages, chatStore ],
  1188. );
  1189. async function uploadImage() {
  1190. const images : string[] = [];
  1191. images.push( ...attachImages );
  1192. images.push(
  1193. ...( await new Promise<string[]>( ( res, rej ) => {
  1194. const fileInput = document.createElement( "input" );
  1195. fileInput.type = "file";
  1196. fileInput.accept =
  1197. "image/png, image/jpeg, image/webp, image/heic, image/heif";
  1198. fileInput.multiple = true;
  1199. fileInput.onchange = ( event : any ) => {
  1200. setUploading( true );
  1201. const files = event.target.files;
  1202. const imagesData : string[] = [];
  1203. for ( let i = 0; i < files.length; i ++ ) {
  1204. const file = event.target.files[ i ];
  1205. uploadImageRemote( file ).then( ( dataUrl ) => {
  1206. imagesData.push( dataUrl );
  1207. if (
  1208. imagesData.length === 3 ||
  1209. imagesData.length === files.length
  1210. ) {
  1211. setUploading( false );
  1212. res( imagesData );
  1213. }
  1214. } ).catch( ( e ) => {
  1215. setUploading( false );
  1216. rej( e );
  1217. } );
  1218. }
  1219. };
  1220. fileInput.click();
  1221. } ) ),
  1222. );
  1223. const imagesLength = images.length;
  1224. if ( imagesLength > 3 ) {
  1225. images.splice( 3, imagesLength - 3 );
  1226. }
  1227. setAttachImages( images );
  1228. }
  1229. const [ fileList, setFileList ] = useState<any[]>( [] );
  1230. // 上传配置
  1231. const uploadConfig : UploadProps = {
  1232. action: '/deepseek-api' + '/upload/file',
  1233. method: 'POST',
  1234. accept: [ '.pdf', '.txt', '.doc', '.docx' ].join( ',' ),
  1235. };
  1236. interface FileIconProps {
  1237. fileName : string;
  1238. }
  1239. const FileIcon : React.FC<FileIconProps> = ( props : FileIconProps ) => {
  1240. const style = {
  1241. fontSize: '30px',
  1242. color: '#3875f6',
  1243. }
  1244. let icon = <FileOutlined style={ style } />
  1245. if ( props.fileName ) {
  1246. const suffix = props.fileName.split( '.' ).pop() || '';
  1247. switch ( suffix ) {
  1248. case 'pdf':
  1249. icon = <FilePdfOutlined style={ style } />
  1250. break;
  1251. case 'txt':
  1252. icon = <FileTextOutlined style={ style } />
  1253. break;
  1254. case 'doc':
  1255. case 'docx':
  1256. icon = <FileWordOutlined style={ style } />
  1257. break;
  1258. default:
  1259. break;
  1260. }
  1261. }
  1262. return icon;
  1263. }
  1264. const [ isDeepThink, setIsDeepThink ] = useState<boolean>( chatStore.isDeepThink );
  1265. // 切换聊天窗口后清理上传文件信息
  1266. useEffect( () => {
  1267. setFileList( [] )
  1268. }, [ chatStore.currentSession() ] )
  1269. const couldStop = ChatControllerPool.hasPending();
  1270. const stopAll = () => ChatControllerPool.stopAll();
  1271. // 切换聊天窗口后清理上传文件信息
  1272. useEffect( () => {
  1273. setWebSearch( false );
  1274. }, [ chatStore.currentSession() ] )
  1275. const [ webSearch, setWebSearch ] = useState<boolean>( chatStore.web_search );
  1276. const [ drawerOpen, setDrawerOpen ] = useState( false );
  1277. type DrawerList = {
  1278. title : string,
  1279. content : string,
  1280. web_url : string,
  1281. }[]
  1282. const [ drawerList, setDrawerList ] = useState<DrawerList>( [] );
  1283. interface NetworkDrawerProps {
  1284. list : DrawerList,
  1285. }
  1286. const NetworkDrawer : React.FC<NetworkDrawerProps> = ( props ) => {
  1287. return (
  1288. <Drawer
  1289. title='网页搜索'
  1290. open={ drawerOpen }
  1291. onClose={ () => {
  1292. setDrawerOpen( false );
  1293. } }
  1294. >
  1295. { props.list.map( ( item, index ) => {
  1296. return <div
  1297. style={ {
  1298. padding: 10,
  1299. background: '#fafafa',
  1300. borderRadius: 4,
  1301. marginBottom: 10,
  1302. cursor: 'pointer',
  1303. } }
  1304. key={ index }
  1305. onClick={ () => {
  1306. window.open( item.web_url );
  1307. } }
  1308. >
  1309. <div style={ {
  1310. margin: '5px 0',
  1311. fontSize: 16,
  1312. display: '-webkit-box',
  1313. WebkitBoxOrient: 'vertical',
  1314. WebkitLineClamp: 2,// 限制显示两行
  1315. overflow: 'hidden',
  1316. } }>
  1317. { item.title }
  1318. </div>
  1319. <div style={ {
  1320. color: '#afafaf',
  1321. display: '-webkit-box',
  1322. WebkitBoxOrient: 'vertical',
  1323. WebkitLineClamp: 4,// 限制显示两行
  1324. overflow: 'hidden',
  1325. textOverflow: 'ellipsis',
  1326. } }>
  1327. { item.content }
  1328. </div>
  1329. </div>
  1330. } )
  1331. }
  1332. </Drawer>
  1333. )
  1334. }
  1335. const location = useLocation();
  1336. return (
  1337. <div className={ styles.chat } key={ session.id }>
  1338. {
  1339. isMobileScreen && location.pathname !== '/' &&
  1340. <div className="window-header" data-tauri-drag-region>
  1341. <div style={ { display: 'flex', alignItems: 'center' } }
  1342. className={ `window-header-title ${ styles[ "chat-body-title" ] }` }>
  1343. <div>
  1344. <IconButton
  1345. style={ { padding: 0, marginRight: 20 } }
  1346. icon={ <LeftIcon /> }
  1347. text={ Locale.NewChat.Return }
  1348. onClick={ () => navigate( '/deepseekChat' ) }
  1349. />
  1350. </div>
  1351. </div>
  1352. </div>
  1353. }
  1354. <div
  1355. className={ styles[ "chat-body" ] }
  1356. ref={ scrollRef }
  1357. onScroll={ ( e ) => onChatBodyScroll( e.currentTarget ) }
  1358. onMouseDown={ () => inputRef.current?.blur() }
  1359. onTouchStart={ () => {
  1360. inputRef.current?.blur();
  1361. setAutoScroll( false );
  1362. } }
  1363. >
  1364. <>
  1365. { messages.map( ( message, i ) => {
  1366. const isUser = message.role === "user";
  1367. const isContext = i < context.length;
  1368. const showActions =
  1369. i > 0 &&
  1370. !( message.preview || message.content.length === 0 ) &&
  1371. !isContext;
  1372. const showTyping = message.preview || message.streaming;
  1373. const shouldShowClearContextDivider = i === clearContextIndex - 1;
  1374. return (
  1375. <Fragment key={ message.id }>
  1376. <div
  1377. className={
  1378. isUser ? styles[ "chat-message-user" ] : styles[ "chat-message" ]
  1379. }
  1380. >
  1381. <div className={ styles[ "chat-message-container" ] }
  1382. style={ { display: 'flex', flexDirection: 'column' } }>
  1383. <div className={ styles[ "chat-message-header" ] }>
  1384. <div className={ styles[ "chat-message-avatar" ] }>
  1385. { isUser ? (
  1386. // 在这里换头像
  1387. <div style={ { position: 'relative' } }>
  1388. <div
  1389. style={ {
  1390. position: 'absolute',
  1391. zIndex: 2,
  1392. top: '50%',
  1393. left: '50%',
  1394. transform: ' translate(-110%, -100%)',
  1395. fontSize: 14,
  1396. } }>
  1397. </div>
  1398. </div>
  1399. ) : (
  1400. <>
  1401. { [ "system" ].includes( message.role ) ? (
  1402. <Avatar avatar="2699-fe0f" />
  1403. ) : (
  1404. <MaskAvatar
  1405. avatar={ session.mask.avatar }
  1406. model={
  1407. message.model || session.mask.modelConfig.model
  1408. }
  1409. />
  1410. ) }
  1411. </>
  1412. ) }
  1413. </div>
  1414. </div>
  1415. {
  1416. isUser && message.document && message.document.id &&
  1417. <a style={ {
  1418. padding: '10px',
  1419. background: '#f7f7f7',
  1420. borderRadius: '10px',
  1421. textDecoration: 'none',
  1422. color: '#24292f',
  1423. display: 'flex',
  1424. alignItems: 'center'
  1425. } } href={ message.document.url } target="_blank">
  1426. <FileIcon fileName={ message.document.name } />
  1427. <div style={ { marginLeft: 8, fontSize: '14px' } }>
  1428. { message.document.name }
  1429. </div>
  1430. </a>
  1431. }
  1432. {/* {showTyping && (
  1433. <div className={styles["chat-message-status"]}>
  1434. 正在输入…
  1435. </div>
  1436. )} */ }
  1437. {
  1438. message.networkInfo && message.networkInfo.list.length > 0 &&
  1439. <div style={ { marginTop: 10 } }>
  1440. <Button
  1441. icon={ <RightOutlined /> }
  1442. iconPosition='end'
  1443. onClick={ () => {
  1444. setDrawerList( message.networkInfo!.list );
  1445. setDrawerOpen( true );
  1446. } }
  1447. >
  1448. 搜索到{ message.networkInfo.list.length }篇相关资料
  1449. </Button>
  1450. {
  1451. drawerOpen &&
  1452. <NetworkDrawer
  1453. list={ message.networkInfo.list }
  1454. />
  1455. }
  1456. </div>
  1457. }
  1458. <div className={ styles[ "chat-message-item" ] }>
  1459. <Markdown
  1460. key={ message.streaming ? "loading" : "done" }
  1461. content={ getMessageTextContent( message ) }
  1462. loading={
  1463. ( message.preview || message.streaming ) &&
  1464. message.content.length === 0 &&
  1465. !isUser
  1466. }
  1467. onDoubleClickCapture={ () => {
  1468. if ( !isMobileScreen ) return;
  1469. setUserInput( getMessageTextContent( message ) );
  1470. } }
  1471. fontSize={ fontSize }
  1472. fontFamily={ fontFamily }
  1473. parentRef={ scrollRef }
  1474. defaultShow={ i >= messages.length - 6 }
  1475. />
  1476. { getMessageImages( message ).length == 1 && (
  1477. <img
  1478. className={ styles[ "chat-message-item-image" ] }
  1479. src={ getMessageImages( message )[ 0 ] }
  1480. alt=""
  1481. />
  1482. ) }
  1483. { getMessageImages( message ).length > 1 && (
  1484. <div
  1485. className={ styles[ "chat-message-item-images" ] }
  1486. style={
  1487. {
  1488. "--image-count": getMessageImages( message ).length,
  1489. } as React.CSSProperties
  1490. }
  1491. >
  1492. { getMessageImages( message ).map( ( image, index ) => {
  1493. return (
  1494. <img
  1495. className={
  1496. styles[ "chat-message-item-image-multi" ]
  1497. }
  1498. key={ index }
  1499. src={ image }
  1500. alt=""
  1501. />
  1502. );
  1503. } ) }
  1504. </div>
  1505. ) }
  1506. </div>
  1507. </div>
  1508. </div>
  1509. { shouldShowClearContextDivider && <ClearContextDivider /> }
  1510. </Fragment>
  1511. );
  1512. } ) }
  1513. </>
  1514. </div>
  1515. <div className={ styles[ "chat-input-panel" ] }>
  1516. <ChatActions
  1517. setUserInput={ setUserInput }
  1518. doSubmit={ doSubmit }
  1519. uploadImage={ uploadImage }
  1520. setAttachImages={ setAttachImages }
  1521. setUploading={ setUploading }
  1522. showPromptModal={ () => setShowPromptModal( true ) }
  1523. scrollToBottom={ scrollToBottom }
  1524. hitBottom={ hitBottom }
  1525. uploading={ uploading }
  1526. showPromptHints={ () => {
  1527. if ( promptHints.length > 0 ) {
  1528. setPromptHints( [] );
  1529. return;
  1530. }
  1531. inputRef.current?.focus();
  1532. setUserInput( "/" );
  1533. onSearch( "" );
  1534. } }
  1535. />
  1536. {
  1537. fileList.length > 0 &&
  1538. <div style={ { marginBottom: 20 } }>
  1539. <Upload
  1540. fileList={ fileList }
  1541. onRemove={ ( file ) => {
  1542. setFileList( fileList.filter( item => item.uid !== file.uid ) );
  1543. } }
  1544. />
  1545. </div>
  1546. }
  1547. <label
  1548. className={ `${ styles[ "chat-input-panel-inner" ] } ${ attachImages.length != 0
  1549. ? styles[ "chat-input-panel-inner-attach" ]
  1550. : ""
  1551. }` }
  1552. htmlFor="chat-input"
  1553. >
  1554. <textarea
  1555. id="chat-input"
  1556. ref={ inputRef }
  1557. className={ styles[ "chat-input2" ] }
  1558. placeholder={ Locale.Chat.Input( submitKey ) }
  1559. onInput={ ( e ) => onInput( e.currentTarget.value ) }
  1560. value={ userInput }
  1561. onKeyDown={ onInputKeyDown }
  1562. onFocus={ scrollToBottom }
  1563. onClick={ scrollToBottom }
  1564. onPaste={ handlePaste }
  1565. rows={ inputRows }
  1566. autoFocus={ autoFocus }
  1567. style={ {
  1568. fontSize: config.fontSize,
  1569. fontFamily: config.fontFamily,
  1570. } }
  1571. />
  1572. { attachImages.length != 0 && (
  1573. <div className={ styles[ "attach-images" ] }>
  1574. { attachImages.map( ( image, index ) => {
  1575. return (
  1576. <div
  1577. key={ index }
  1578. className={ styles[ "attach-image" ] }
  1579. style={ { backgroundImage: `url("${ image }")` } }
  1580. >
  1581. <div className={ styles[ "attach-image-mask" ] }>
  1582. <DeleteImageButton
  1583. deleteImage={ () => {
  1584. setAttachImages(
  1585. attachImages.filter( ( _, i ) => i !== index ),
  1586. );
  1587. } }
  1588. />
  1589. </div>
  1590. </div>
  1591. );
  1592. } ) }
  1593. </div>
  1594. ) }
  1595. {/* 修改样式:输入框内部按钮区域 */ }
  1596. {/* <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 10 }}> */ }
  1597. <div className={ styles[ "chat-input-bottom-bar" ] }>
  1598. {/* <div style={{ display: 'flex', alignItems: 'center' }}> */ }
  1599. <div className={ styles[ "left-options" ] }>
  1600. {/*深度思考R1按钮*/ }
  1601. {false&&<Tooltip
  1602. title={
  1603. <span style={ { fontSize: 12, lineHeight: 1.4, minHeight: 24, padding: '4px 8px' } }>
  1604. { isDeepThink ? '关闭深度思考模式' : '启用深度思考模式' }
  1605. </span>
  1606. }
  1607. placement="left"
  1608. >
  1609. <div
  1610. // className={styles["option-item"]}
  1611. style={ {
  1612. padding: '0 12px',
  1613. height: 28,
  1614. borderRadius: 18,
  1615. fontSize: 12,
  1616. display: 'flex',
  1617. justifyContent: 'center',
  1618. alignItems: 'center',
  1619. // marginRight: 10,
  1620. cursor: 'pointer',
  1621. background: isDeepThink ? '#dee9fc' : '#f3f4f6',
  1622. color: isDeepThink ? '#3875f6' : '#000000',
  1623. // border: `1px solid ${isDeepThink ? '#3875f6' : 'transparent'}`,
  1624. transition: 'all 0.2s ease',
  1625. userSelect: 'none'
  1626. } }
  1627. onClick={ () => {
  1628. setIsDeepThink( !isDeepThink );
  1629. chatStore.setIsDeepThink( !isDeepThink );
  1630. } }
  1631. >
  1632. <img src={ isDeepThink ? sdsk_selected.src : sdsk.src }
  1633. style={ {
  1634. width: 16,
  1635. height: 16,
  1636. } }
  1637. />
  1638. <span style={ { fontSize: 11, marginLeft: 5 } }>
  1639. 深度思考
  1640. </span>
  1641. </div>
  1642. </Tooltip>}
  1643. {/*联网搜索按钮*/ }
  1644. <div style={ {
  1645. padding: isMobileScreen ? '0 8px' : '0 12px',
  1646. height: 28,
  1647. borderRadius: 18,
  1648. fontSize: 12,
  1649. display: 'flex',
  1650. justifyContent: 'center',
  1651. alignItems: 'center',
  1652. cursor: 'pointer',
  1653. background: webSearch ? '#dee9fc' : '#f3f4f6',
  1654. color: webSearch ? '#3875f6' : '#000000',
  1655. transition: 'all 0.2s ease',
  1656. userSelect: 'none'
  1657. } }
  1658. onClick={ () => {
  1659. setWebSearch( !webSearch );
  1660. chatStore.setWebSearch( !webSearch );
  1661. } }
  1662. >
  1663. <img src={ webSearch ? hlw_selected.src : hlw.src }
  1664. style={ {
  1665. width: 16,
  1666. height: 16,
  1667. } }
  1668. />
  1669. {!isMobileScreen && (
  1670. <span style={ { fontSize: 11, marginLeft: 5, marginRight: 10 } }>
  1671. 联网搜索
  1672. </span>
  1673. )}
  1674. </div>
  1675. </div>
  1676. <div style={ { display: 'flex', alignItems: 'center' } }>
  1677. {
  1678. !webSearch &&
  1679. <div style={ { marginRight: 10 } }>
  1680. <Upload
  1681. { ...uploadConfig }
  1682. showUploadList={ false }
  1683. maxCount={ 1 }
  1684. onChange={ ( info ) => {
  1685. const fileList = info.fileList.map( ( file ) => {
  1686. const data = file.response;
  1687. return {
  1688. ...file,
  1689. url: data?.document_url || file.url,
  1690. documentId: data?.document_id || '',
  1691. }
  1692. } );
  1693. setFileList( fileList );
  1694. if ( info.file.status === 'done' ) {// 上传成功
  1695. const { code, message: msg } = info.file.response;
  1696. if ( code === 200 ) {
  1697. message.success( '上传成功' );
  1698. } else {
  1699. message.error( msg );
  1700. }
  1701. } else if ( info.file.status === 'error' ) {// 上传失败
  1702. message.error( '上传失败' );
  1703. }
  1704. } }
  1705. >
  1706. <Tooltip
  1707. title={
  1708. <div style={ { padding: '4px 8px' } }>
  1709. <div style={ {
  1710. fontSize: 12,
  1711. lineHeight: 1.4,
  1712. marginBottom: 6,
  1713. } }>
  1714. 上传附件 (识别文本和图表中的内容)
  1715. </div>
  1716. <div style={ {
  1717. fontSize: 10,
  1718. color: '#8c8c8c',
  1719. lineHeight: 1.4,
  1720. }}>
  1721. <span>
  1722. 仅支持单个PDF/Word/TXT文件格式
  1723. </span>
  1724. <span>
  1725. (单个文件≤50MB)
  1726. </span>
  1727. </div>
  1728. </div>}
  1729. placement="top"
  1730. >
  1731. <div
  1732. style={ {
  1733. width: 28,
  1734. height: 28,
  1735. borderRadius: '50%',
  1736. background: '#4357d2',
  1737. display: 'flex',
  1738. justifyContent: 'center',
  1739. alignItems: 'center',
  1740. cursor: 'pointer',
  1741. transition: 'all 0.2s ease',
  1742. userSelect: 'none'
  1743. } }
  1744. >
  1745. <PaperClipOutlined style={ { color: '#FFFFFF', fontSize: '18px' } } />
  1746. </div>
  1747. </Tooltip>
  1748. </Upload>
  1749. </div>
  1750. }
  1751. <div
  1752. style={ {
  1753. width: 28,
  1754. height: 28,
  1755. borderRadius: '50%',
  1756. background: '#4357d2',
  1757. display: 'flex',
  1758. justifyContent: 'center',
  1759. alignItems: 'center',
  1760. cursor: 'pointer',
  1761. } }
  1762. onClick={ ( e ) => {
  1763. e.preventDefault();
  1764. e.stopPropagation();
  1765. if ( couldStop ) {
  1766. stopAll();
  1767. } else {
  1768. doSubmit( userInput );
  1769. }
  1770. } }
  1771. >
  1772. {
  1773. couldStop ?
  1774. <div style={ { width: 13, height: 13, background: '#FFFFFF', borderRadius: 2 } }></div>
  1775. :
  1776. <div style={ { transform: 'rotate(-45deg)', padding: '0px 0px 3px 5px' } }>
  1777. <SendOutlined style={ { color: '#FFFFFF' } } />
  1778. </div>
  1779. }
  1780. </div>
  1781. </div>
  1782. </div>
  1783. </label>
  1784. {!isMobileScreen && (
  1785. <div style={ { marginTop: 8, textAlign: 'center', color: '#888888', fontSize: 12 } }>
  1786. 内容由AI生成,仅供参考
  1787. </div>
  1788. )}
  1789. </div>
  1790. {
  1791. showExport && (
  1792. <ExportMessageModal onClose={ () => setShowExport( false ) } />
  1793. )
  1794. }
  1795. {
  1796. isEditingMessage && (
  1797. <EditMessageModal
  1798. onClose={ () => {
  1799. setIsEditingMessage( false );
  1800. } }
  1801. />
  1802. )
  1803. }
  1804. </div>
  1805. );
  1806. }
  1807. export function Chat(props?: { onMessageSent?: () => void }) {
  1808. const globalStore = useGlobalStore();
  1809. const chatStore = useChatStore();
  1810. const sessionIndex = chatStore.currentSessionIndex;
  1811. useEffect( () => {
  1812. globalStore.setShowMenu( true );
  1813. chatStore.setModel( 'DeepSeek' );
  1814. chatStore.setWebSearch( false );
  1815. }, [] );
  1816. return <_Chat key={ sessionIndex } onMessageSent={props?.onMessageSent}></_Chat>;
  1817. }