DeepSeekChat.tsx 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897
  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.networkInfo && message.networkInfo.list.length > 0 &&
  1456. <div style={{ marginTop: 10 }}>
  1457. <Button
  1458. icon={<RightOutlined />}
  1459. iconPosition='end'
  1460. onClick={() => {
  1461. setDrawerList(message.networkInfo!.list);
  1462. setDrawerOpen(true);
  1463. }}
  1464. >
  1465. 搜索到{message.networkInfo.list.length}篇相关资料
  1466. </Button>
  1467. {
  1468. drawerOpen &&
  1469. <NetworkDrawer
  1470. list={message.networkInfo.list}
  1471. />
  1472. }
  1473. </div>
  1474. }
  1475. <div className={styles["chat-message-item"]}>
  1476. <Markdown
  1477. key={message.streaming ? "loading" : "done"}
  1478. content={getMessageTextContent(message)}
  1479. loading={
  1480. (message.preview || message.streaming) &&
  1481. message.content.length === 0 &&
  1482. !isUser
  1483. }
  1484. onDoubleClickCapture={() => {
  1485. if (!isMobileScreen) return;
  1486. setUserInput(getMessageTextContent(message));
  1487. }}
  1488. fontSize={fontSize}
  1489. fontFamily={fontFamily}
  1490. parentRef={scrollRef}
  1491. defaultShow={i >= messages.length - 6}
  1492. />
  1493. {getMessageImages(message).length == 1 && (
  1494. <img
  1495. className={styles["chat-message-item-image"]}
  1496. src={getMessageImages(message)[0]}
  1497. alt=""
  1498. />
  1499. )}
  1500. {getMessageImages(message).length > 1 && (
  1501. <div
  1502. className={styles["chat-message-item-images"]}
  1503. style={
  1504. {
  1505. "--image-count": getMessageImages(message).length,
  1506. } as React.CSSProperties
  1507. }
  1508. >
  1509. {getMessageImages(message).map((image, index) => {
  1510. return (
  1511. <img
  1512. className={
  1513. styles["chat-message-item-image-multi"]
  1514. }
  1515. key={index}
  1516. src={image}
  1517. alt=""
  1518. />
  1519. );
  1520. })}
  1521. </div>
  1522. )}
  1523. </div>
  1524. </div>
  1525. </div>
  1526. {shouldShowClearContextDivider && <ClearContextDivider />}
  1527. </Fragment>
  1528. );
  1529. })}
  1530. </>
  1531. </div>
  1532. <div className={styles["chat-input-panel"]}>
  1533. <ChatActions
  1534. setUserInput={setUserInput}
  1535. doSubmit={doSubmit}
  1536. uploadImage={uploadImage}
  1537. setAttachImages={setAttachImages}
  1538. setUploading={setUploading}
  1539. showPromptModal={() => setShowPromptModal(true)}
  1540. scrollToBottom={scrollToBottom}
  1541. hitBottom={hitBottom}
  1542. uploading={uploading}
  1543. showPromptHints={() => {
  1544. if (promptHints.length > 0) {
  1545. setPromptHints([]);
  1546. return;
  1547. }
  1548. inputRef.current?.focus();
  1549. setUserInput("/");
  1550. onSearch("");
  1551. }}
  1552. />
  1553. {
  1554. fileList.length > 0 &&
  1555. <div style={{ marginBottom: 20 }}>
  1556. <Upload
  1557. fileList={fileList}
  1558. onRemove={(file) => {
  1559. setFileList(fileList.filter(item => item.uid !== file.uid));
  1560. }}
  1561. />
  1562. </div>
  1563. }
  1564. <label
  1565. className={`${styles["chat-input-panel-inner"]} ${attachImages.length != 0
  1566. ? styles["chat-input-panel-inner-attach"]
  1567. : ""
  1568. }`}
  1569. htmlFor="chat-input"
  1570. >
  1571. <textarea
  1572. id="chat-input"
  1573. ref={inputRef}
  1574. className={styles["chat-input2"]}
  1575. placeholder={Locale.Chat.Input(submitKey)}
  1576. onInput={(e) => onInput(e.currentTarget.value)}
  1577. value={userInput}
  1578. onKeyDown={onInputKeyDown}
  1579. onFocus={scrollToBottom}
  1580. onClick={scrollToBottom}
  1581. onPaste={handlePaste}
  1582. rows={inputRows}
  1583. autoFocus={autoFocus}
  1584. style={{
  1585. fontSize: config.fontSize,
  1586. fontFamily: config.fontFamily,
  1587. }}
  1588. />
  1589. {attachImages.length != 0 && (
  1590. <div className={styles["attach-images"]}>
  1591. {attachImages.map((image, index) => {
  1592. return (
  1593. <div
  1594. key={index}
  1595. className={styles["attach-image"]}
  1596. style={{ backgroundImage: `url("${image}")` }}
  1597. >
  1598. <div className={styles["attach-image-mask"]}>
  1599. <DeleteImageButton
  1600. deleteImage={() => {
  1601. setAttachImages(
  1602. attachImages.filter((_, i) => i !== index),
  1603. );
  1604. }}
  1605. />
  1606. </div>
  1607. </div>
  1608. );
  1609. })}
  1610. </div>
  1611. )}
  1612. {/* 修改样式:输入框内部按钮区域 */}
  1613. {/* <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 10 }}> */}
  1614. <div className={styles["chat-input-bottom-bar"]}>
  1615. {/* <div style={{ display: 'flex', alignItems: 'center' }}> */}
  1616. <div className={styles["left-options"]}>
  1617. {/*深度思考R1按钮*/}
  1618. <Tooltip
  1619. title={
  1620. <span style={{ fontSize: 12, lineHeight: 1.4, minHeight: 24, padding: '4px 8px' }}>
  1621. {isDeepThink ? '关闭深度思考模式' : '启用深度思考模式'}
  1622. </span>
  1623. }
  1624. placement="left"
  1625. >
  1626. <div
  1627. // className={styles["option-item"]}
  1628. style={{
  1629. padding: '0 12px',
  1630. height: 28,
  1631. borderRadius: 18,
  1632. fontSize: 12,
  1633. display: 'flex',
  1634. justifyContent: 'center',
  1635. alignItems: 'center',
  1636. // marginRight: 10,
  1637. cursor: 'pointer',
  1638. background: isDeepThink ? '#dee9fc' : '#f3f4f6',
  1639. color: isDeepThink ? '#3875f6' : '#000000',
  1640. // border: `1px solid ${isDeepThink ? '#3875f6' : 'transparent'}`,
  1641. transition: 'all 0.2s ease',
  1642. userSelect: 'none'
  1643. }}
  1644. onClick={() => {
  1645. setIsDeepThink(!isDeepThink);
  1646. chatStore.setIsDeepThink(!isDeepThink);
  1647. }}
  1648. >
  1649. <img src={isDeepThink ? sdsk_selected.src : sdsk.src}
  1650. style={{
  1651. width: 16,
  1652. height: 16,
  1653. }}
  1654. />
  1655. <span style={{ fontSize: 11, marginLeft: 5 }}>
  1656. 深度思考
  1657. </span>
  1658. </div>
  1659. </Tooltip>
  1660. {/*联网搜索按钮*/}
  1661. <div style={{
  1662. padding: '0 12px',
  1663. height: 28,
  1664. borderRadius: 18,
  1665. fontSize: 12,
  1666. display: 'flex',
  1667. justifyContent: 'center',
  1668. alignItems: 'center',
  1669. cursor: 'pointer',
  1670. background: webSearch ? '#dee9fc' : '#f3f4f6',
  1671. color: webSearch ? '#3875f6' : '#000000',
  1672. transition: 'all 0.2s ease',
  1673. userSelect: 'none'
  1674. }}
  1675. onClick={() => {
  1676. setWebSearch(!webSearch);
  1677. chatStore.setWebSearch(!webSearch);
  1678. }}
  1679. >
  1680. <img src={webSearch ? hlw_selected.src : hlw.src}
  1681. style={{
  1682. width: 16,
  1683. height: 16,
  1684. }}
  1685. />
  1686. <span style={{ fontSize: 11, marginLeft: 5, marginRight: 10 }}>
  1687. 联网搜索
  1688. </span>
  1689. </div>
  1690. </div>
  1691. <div style={{ display: 'flex', alignItems: 'center' }}>
  1692. <div
  1693. style={{
  1694. width: 28,
  1695. height: 28,
  1696. borderRadius: '50%',
  1697. background: '#4357d2',
  1698. display: 'flex',
  1699. justifyContent: 'center',
  1700. alignItems: 'center',
  1701. cursor: 'pointer',
  1702. }}
  1703. onClick={() => {
  1704. if (couldStop) {
  1705. stopAll();
  1706. } else {
  1707. doSubmit(userInput);
  1708. }
  1709. }}
  1710. >
  1711. {
  1712. couldStop ?
  1713. <div style={{ width: 13, height: 13, background: '#FFFFFF', borderRadius: 2 }}></div>
  1714. :
  1715. <div style={{ transform: 'rotate(-45deg)', padding: '0px 0px 3px 5px' }}>
  1716. <SendOutlined style={{ color: '#FFFFFF' }} />
  1717. </div>
  1718. }
  1719. </div>
  1720. </div>
  1721. </div>
  1722. </label>
  1723. <div style={{ marginTop: 8, textAlign: 'center', color: '#888888', fontSize: 12 }}>
  1724. 内容由AI生成,仅供参考
  1725. </div>
  1726. </div>
  1727. {
  1728. showExport && (
  1729. <ExportMessageModal onClose={() => setShowExport(false)} />
  1730. )
  1731. }
  1732. {
  1733. isEditingMessage && (
  1734. <EditMessageModal
  1735. onClose={() => {
  1736. setIsEditingMessage(false);
  1737. }}
  1738. />
  1739. )
  1740. }
  1741. </div>
  1742. );
  1743. }
  1744. export function Chat() {
  1745. const globalStore = useGlobalStore();
  1746. const chatStore = useChatStore();
  1747. const sessionIndex = chatStore.currentSessionIndex;
  1748. useEffect(() => {
  1749. globalStore.setShowMenu(true);
  1750. chatStore.setModel('DeepSeek');
  1751. chatStore.setWebSearch(false);
  1752. }, []);
  1753. return <_Chat key={sessionIndex}></_Chat>;
  1754. }