DeepSeekChat.tsx 57 KB

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