DeepSeekChat.tsx 58 KB

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