ToolsBar.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { apiInterceptors, clearChatHistory } from '@/client/api';
  2. import { ChatContentContext } from '@/pages/chat';
  3. import { ClearOutlined, LoadingOutlined, PauseCircleOutlined, RedoOutlined } from '@ant-design/icons';
  4. import type { UploadFile } from 'antd';
  5. import { Spin, Tooltip } from 'antd';
  6. import classNames from 'classnames';
  7. import Image from 'next/image';
  8. import React, { useContext, useMemo, useState } from 'react';
  9. import { useTranslation } from 'react-i18next';
  10. import ModelSwitcher from './ModelSwitcher';
  11. import Resource from './Resource';
  12. import Temperature from './Temperature';
  13. interface ToolsConfig {
  14. icon: React.ReactNode;
  15. can_use: boolean;
  16. key: string;
  17. tip?: string;
  18. onClick?: () => void;
  19. }
  20. const ToolsBar: React.FC<{
  21. ctrl: AbortController;
  22. }> = ({ ctrl }) => {
  23. const { t } = useTranslation();
  24. const {
  25. history,
  26. scrollRef,
  27. canAbort,
  28. replyLoading,
  29. currentDialogue,
  30. appInfo,
  31. temperatureValue,
  32. resourceValue,
  33. setTemperatureValue,
  34. refreshHistory,
  35. setCanAbort,
  36. setReplyLoading,
  37. handleChat,
  38. } = useContext(ChatContentContext);
  39. const [fileList, setFileList] = useState<UploadFile[]>([]);
  40. const [loading, setLoading] = useState<boolean>(false);
  41. const [clsLoading, setClsLoading] = useState<boolean>(false);
  42. // 左边工具栏动态可用key
  43. const paramKey: string[] = useMemo(() => {
  44. return appInfo.param_need?.map(i => i.type) || [];
  45. }, [appInfo.param_need]);
  46. const rightToolsConfig: ToolsConfig[] = useMemo(() => {
  47. return [
  48. {
  49. tip: t('stop_replying'),
  50. icon: <PauseCircleOutlined className={classNames({ 'text-[#0c75fc]': canAbort })} />,
  51. can_use: canAbort,
  52. key: 'abort',
  53. onClick: () => {
  54. if (!canAbort) {
  55. return;
  56. }
  57. ctrl.abort();
  58. setTimeout(() => {
  59. setCanAbort(false);
  60. setReplyLoading(false);
  61. }, 100);
  62. },
  63. },
  64. {
  65. tip: t('answer_again'),
  66. icon: <RedoOutlined />,
  67. can_use: !replyLoading && history.length > 0,
  68. key: 'redo',
  69. onClick: async () => {
  70. const lastHuman = history.filter(i => i.role === 'human')?.slice(-1)?.[0];
  71. handleChat(lastHuman?.context || '', {
  72. app_code: appInfo.app_code,
  73. ...(paramKey.includes('temperature') && { temperature: temperatureValue }),
  74. ...(paramKey.includes('resource') && {
  75. select_param:
  76. typeof resourceValue === 'string'
  77. ? resourceValue
  78. : JSON.stringify(resourceValue) || currentDialogue.select_param,
  79. }),
  80. });
  81. setTimeout(() => {
  82. scrollRef.current?.scrollTo({
  83. top: scrollRef.current?.scrollHeight,
  84. behavior: 'smooth',
  85. });
  86. }, 0);
  87. },
  88. },
  89. {
  90. tip: t('erase_memory'),
  91. icon: clsLoading ? (
  92. <Spin spinning={clsLoading} indicator={<LoadingOutlined style={{ fontSize: 20 }} />} />
  93. ) : (
  94. <ClearOutlined />
  95. ),
  96. can_use: history.length > 0,
  97. key: 'clear',
  98. onClick: async () => {
  99. if (clsLoading) {
  100. return;
  101. }
  102. setClsLoading(true);
  103. await apiInterceptors(clearChatHistory(currentDialogue.conv_uid)).finally(async () => {
  104. await refreshHistory();
  105. setClsLoading(false);
  106. });
  107. },
  108. },
  109. ];
  110. }, [
  111. t,
  112. canAbort,
  113. replyLoading,
  114. history,
  115. clsLoading,
  116. ctrl,
  117. setCanAbort,
  118. setReplyLoading,
  119. handleChat,
  120. appInfo.app_code,
  121. paramKey,
  122. temperatureValue,
  123. resourceValue,
  124. currentDialogue.select_param,
  125. currentDialogue.conv_uid,
  126. scrollRef,
  127. refreshHistory,
  128. ]);
  129. const returnTools = (config: ToolsConfig[]) => {
  130. return (
  131. <>
  132. {config.map(item => (
  133. <Tooltip key={item.key} title={item.tip} arrow={false} placement='bottom'>
  134. <div
  135. className={`flex w-8 h-8 items-center justify-center rounded-md hover:bg-[rgb(221,221,221,0.6)] text-lg ${
  136. item.can_use ? 'cursor-pointer' : 'opacity-30 cursor-not-allowed'
  137. }`}
  138. onClick={() => {
  139. item.onClick?.();
  140. }}
  141. >
  142. {item.icon}
  143. </div>
  144. </Tooltip>
  145. ))}
  146. </>
  147. );
  148. };
  149. const fileName = useMemo(() => {
  150. try {
  151. return JSON.parse(currentDialogue.select_param).file_name;
  152. } catch {
  153. return '';
  154. }
  155. }, [currentDialogue.select_param]);
  156. return (
  157. <div className='flex flex-col mb-2'>
  158. <div className='flex items-center justify-between h-full w-full'>
  159. <div className='flex gap-3 text-lg'>
  160. <ModelSwitcher />
  161. <Resource fileList={fileList} setFileList={setFileList} setLoading={setLoading} fileName={fileName} />
  162. <Temperature temperatureValue={temperatureValue} setTemperatureValue={setTemperatureValue} />
  163. </div>
  164. <div className='flex gap-1'>{returnTools(rightToolsConfig)}</div>
  165. </div>
  166. {(fileName || fileList[0]?.name) && (
  167. <div className='group/item flex mt-2'>
  168. <div className='flex items-center justify-between w-64 border border-[#e3e4e6] dark:border-[rgba(255,255,255,0.6)] rounded-lg p-2'>
  169. <div className='flex items-center'>
  170. <Image src={`/icons/chat/excel.png`} width={20} height={20} alt='file-icon' className='mr-2' />
  171. <span className='text-sm text-[#1c2533] dark:text-white line-clamp-1'>
  172. {fileName || fileList[0]?.name}
  173. </span>
  174. </div>
  175. <Spin spinning={loading} indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />
  176. </div>
  177. </div>
  178. )}
  179. </div>
  180. );
  181. };
  182. export default ToolsBar;