ChatContent.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import markdownComponents from '@/components/chat/chat-content/config';
  2. import { IChatDialogueMessageSchema } from '@/types/chat';
  3. import { STORAGE_USERINFO_KEY } from '@/utils/constants/index';
  4. import { CheckOutlined, ClockCircleOutlined, CloseOutlined, CodeOutlined, LoadingOutlined } from '@ant-design/icons';
  5. import { GPTVis } from '@antv/gpt-vis';
  6. import classNames from 'classnames';
  7. import Image from 'next/image';
  8. import { useSearchParams } from 'next/navigation';
  9. import React, { memo, useMemo } from 'react';
  10. import { useTranslation } from 'react-i18next';
  11. import rehypeRaw from 'rehype-raw';
  12. import remarkGfm from 'remark-gfm';
  13. import Feedback from './Feedback';
  14. import RobotIcon from './RobotIcon';
  15. const UserIcon: React.FC = () => {
  16. const user = JSON.parse(localStorage.getItem(STORAGE_USERINFO_KEY) ?? '');
  17. if (!user.avatar_url) {
  18. return (
  19. <div className='flex items-center justify-center w-8 h-8 rounded-full bg-gradient-to-tr from-[#31afff] to-[#1677ff] text-xs text-white'>
  20. PSV
  21. </div>
  22. );
  23. }
  24. return (
  25. <Image
  26. className='rounded-full border border-gray-200 object-contain bg-white inline-block'
  27. width={32}
  28. height={32}
  29. src={user?.avatar_url}
  30. alt={user?.nick_name}
  31. />
  32. );
  33. };
  34. type DBGPTView = {
  35. name: string;
  36. status: 'todo' | 'runing' | 'failed' | 'completed' | (string & {});
  37. result?: string;
  38. err_msg?: string;
  39. };
  40. type MarkdownComponent = Parameters<typeof GPTVis>['0']['components'];
  41. const pluginViewStatusMapper: Record<DBGPTView['status'], { bgClass: string; icon: React.ReactNode }> = {
  42. todo: {
  43. bgClass: 'bg-gray-500',
  44. icon: <ClockCircleOutlined className='ml-2' />,
  45. },
  46. runing: {
  47. bgClass: 'bg-blue-500',
  48. icon: <LoadingOutlined className='ml-2' />,
  49. },
  50. failed: {
  51. bgClass: 'bg-red-500',
  52. icon: <CloseOutlined className='ml-2' />,
  53. },
  54. completed: {
  55. bgClass: 'bg-green-500',
  56. icon: <CheckOutlined className='ml-2' />,
  57. },
  58. };
  59. const formatMarkdownVal = (val: string) => {
  60. return val
  61. .replaceAll('\\n', '\n')
  62. .replace(/<table(\w*=[^>]+)>/gi, '<table $1>')
  63. .replace(/<tr(\w*=[^>]+)>/gi, '<tr $1>');
  64. };
  65. const formatMarkdownValForAgent = (val: string) => {
  66. return val?.replace(/<table(\w*=[^>]+)>/gi, '<table $1>').replace(/<tr(\w*=[^>]+)>/gi, '<tr $1>');
  67. };
  68. const ChatContent: React.FC<{
  69. content: Omit<IChatDialogueMessageSchema, 'context'> & {
  70. context:
  71. | string
  72. | {
  73. template_name: string;
  74. template_introduce: string;
  75. };
  76. };
  77. onLinkClick: () => void;
  78. }> = ({ content, onLinkClick }) => {
  79. const { t } = useTranslation();
  80. const searchParams = useSearchParams();
  81. const scene = searchParams?.get('scene') ?? '';
  82. const { context, model_name, role, thinking } = content;
  83. const isRobot = useMemo(() => role === 'view', [role]);
  84. const { value, cachePluginContext } = useMemo<{
  85. relations: string[];
  86. value: string;
  87. cachePluginContext: DBGPTView[];
  88. }>(() => {
  89. if (typeof context !== 'string') {
  90. return {
  91. relations: [],
  92. value: '',
  93. cachePluginContext: [],
  94. };
  95. }
  96. const [value, relation] = context.split('\trelations:');
  97. const relations = relation ? relation.split(',') : [];
  98. const cachePluginContext: DBGPTView[] = [];
  99. let cacheIndex = 0;
  100. const result = value.replace(/<dbgpt-view[^>]*>[^<]*<\/dbgpt-view>/gi, matchVal => {
  101. try {
  102. const pluginVal = matchVal.replaceAll('\n', '\\n').replace(/<[^>]*>|<\/[^>]*>/gm, '');
  103. const pluginContext = JSON.parse(pluginVal) as DBGPTView;
  104. const replacement = `<custom-view>${cacheIndex}</custom-view>`;
  105. cachePluginContext.push({
  106. ...pluginContext,
  107. result: formatMarkdownVal(pluginContext.result ?? ''),
  108. });
  109. cacheIndex++;
  110. return replacement;
  111. } catch (e) {
  112. console.log((e as any).message, e);
  113. return matchVal;
  114. }
  115. });
  116. return {
  117. relations,
  118. cachePluginContext,
  119. value: result,
  120. };
  121. }, [context]);
  122. const extraMarkdownComponents = useMemo<MarkdownComponent>(
  123. () => ({
  124. 'custom-view'({ children }) {
  125. const index = +children.toString();
  126. if (!cachePluginContext[index]) {
  127. return children;
  128. }
  129. const { name, status, err_msg, result } = cachePluginContext[index];
  130. const { bgClass, icon } = pluginViewStatusMapper[status] ?? {};
  131. return (
  132. <div className='bg-white dark:bg-[#212121] rounded-lg overflow-hidden my-2 flex flex-col lg:max-w-[80%]'>
  133. <div className={classNames('flex px-4 md:px-6 py-2 items-center text-white text-sm', bgClass)}>
  134. {name}
  135. {icon}
  136. </div>
  137. {result ? (
  138. <div className='px-4 md:px-6 py-4 text-sm'>
  139. <GPTVis components={markdownComponents} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}>
  140. {result ?? ''}
  141. </GPTVis>
  142. </div>
  143. ) : (
  144. <div className='px-4 md:px-6 py-4 text-sm'>{err_msg}</div>
  145. )}
  146. </div>
  147. );
  148. },
  149. }),
  150. [cachePluginContext],
  151. );
  152. return (
  153. <div className='flex flex-1 gap-3 mt-6'>
  154. {/* icon */}
  155. <div className='flex flex-shrink-0 items-start'>{isRobot ? <RobotIcon model={model_name} /> : <UserIcon />}</div>
  156. <div className={`flex ${scene === 'chat_agent' && !thinking ? 'flex-1' : ''} overflow-hidden`}>
  157. {/* 用户提问 */}
  158. {!isRobot && (
  159. <div className='flex flex-1 items-center text-sm text-[#1c2533] dark:text-white'>
  160. {typeof context === 'string' && context}
  161. </div>
  162. )}
  163. {/* ai回答 */}
  164. {isRobot && (
  165. <div className='flex flex-1 flex-col w-full'>
  166. <div className='bg-white dark:bg-[rgba(255,255,255,0.16)] p-4 rounded-2xl rounded-tl-none mb-2'>
  167. {typeof context === 'object' && (
  168. <div>
  169. {`[${context.template_name}]: `}
  170. <span className='text-theme-primary cursor-pointer' onClick={onLinkClick}>
  171. <CodeOutlined className='mr-1' />
  172. {context.template_introduce || 'More Details'}
  173. </span>
  174. </div>
  175. )}
  176. {typeof context === 'string' && scene === 'chat_agent' && (
  177. <GPTVis components={{ ...markdownComponents }} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}>
  178. {formatMarkdownValForAgent(value)}
  179. </GPTVis>
  180. )}
  181. {typeof context === 'string' && scene !== 'chat_agent' && (
  182. <div>
  183. <GPTVis
  184. components={{
  185. ...markdownComponents,
  186. ...extraMarkdownComponents,
  187. }}
  188. rehypePlugins={[rehypeRaw]}
  189. remarkPlugins={[remarkGfm]}
  190. >
  191. {formatMarkdownVal(value)}
  192. </GPTVis>
  193. </div>
  194. )}
  195. {/* 正在思考 */}
  196. {thinking && !context && (
  197. <div className='flex items-center gap-2'>
  198. <span className='flex text-sm text-[#1c2533] dark:text-white'>{t('thinking')}</span>
  199. <div className='flex'>
  200. <div className='w-1 h-1 rounded-full mx-1 animate-pulse1'></div>
  201. <div className='w-1 h-1 rounded-full mx-1 animate-pulse2'></div>
  202. <div className='w-1 h-1 rounded-full mx-1 animate-pulse3'></div>
  203. </div>
  204. </div>
  205. )}
  206. </div>
  207. {/* 用户反馈 */}
  208. <Feedback content={content} />
  209. </div>
  210. )}
  211. </div>
  212. </div>
  213. );
  214. };
  215. export default memo(ChatContent);