index.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import { ChatContext } from '@/app/chat-context';
  2. import { IChatDialogueMessageSchema } from '@/types/chat';
  3. import {
  4. CheckOutlined,
  5. ClockCircleOutlined,
  6. CloseOutlined,
  7. CodeOutlined,
  8. LoadingOutlined,
  9. RobotOutlined,
  10. UserOutlined,
  11. } from '@ant-design/icons';
  12. import { GPTVis } from '@antv/gpt-vis';
  13. import { Tag } from 'antd';
  14. import classNames from 'classnames';
  15. import { PropsWithChildren, ReactNode, memo, useContext, useMemo } from 'react';
  16. import rehypeRaw from 'rehype-raw';
  17. import remarkGfm from 'remark-gfm';
  18. import { renderModelIcon } from '../header/model-selector';
  19. import markdownComponents from './config';
  20. interface Props {
  21. content: Omit<IChatDialogueMessageSchema, 'context'> & {
  22. context:
  23. | string
  24. | {
  25. template_name: string;
  26. template_introduce: string;
  27. };
  28. };
  29. isChartChat?: boolean;
  30. onLinkClick?: () => void;
  31. }
  32. type MarkdownComponent = Parameters<typeof GPTVis>['0']['components'];
  33. type DBGPTView = {
  34. name: string;
  35. status: 'todo' | 'runing' | 'failed' | 'completed' | (string & {});
  36. result?: string;
  37. err_msg?: string;
  38. };
  39. const pluginViewStatusMapper: Record<DBGPTView['status'], { bgClass: string; icon: ReactNode }> = {
  40. todo: {
  41. bgClass: 'bg-gray-500',
  42. icon: <ClockCircleOutlined className='ml-2' />,
  43. },
  44. runing: {
  45. bgClass: 'bg-blue-500',
  46. icon: <LoadingOutlined className='ml-2' />,
  47. },
  48. failed: {
  49. bgClass: 'bg-red-500',
  50. icon: <CloseOutlined className='ml-2' />,
  51. },
  52. completed: {
  53. bgClass: 'bg-green-500',
  54. icon: <CheckOutlined className='ml-2' />,
  55. },
  56. };
  57. function formatMarkdownVal(val: string) {
  58. return val
  59. .replaceAll('\\n', '\n')
  60. .replace(/<table(\w*=[^>]+)>/gi, '<table $1>')
  61. .replace(/<tr(\w*=[^>]+)>/gi, '<tr $1>');
  62. }
  63. function ChatContent({ children, content, isChartChat, onLinkClick }: PropsWithChildren<Props>) {
  64. const { scene } = useContext(ChatContext);
  65. const { context, model_name, role } = content;
  66. const isRobot = role === 'view';
  67. const { relations, value, cachePluginContext } = useMemo<{
  68. relations: string[];
  69. value: string;
  70. cachePluginContext: DBGPTView[];
  71. }>(() => {
  72. if (typeof context !== 'string') {
  73. return {
  74. relations: [],
  75. value: '',
  76. cachePluginContext: [],
  77. };
  78. }
  79. const [value, relation] = context.split('\trelations:');
  80. const relations = relation ? relation.split(',') : [];
  81. const cachePluginContext: DBGPTView[] = [];
  82. let cacheIndex = 0;
  83. const result = value.replace(/<dbgpt-view[^>]*>[^<]*<\/dbgpt-view>/gi, matchVal => {
  84. try {
  85. const pluginVal = matchVal.replaceAll('\n', '\\n').replace(/<[^>]*>|<\/[^>]*>/gm, '');
  86. const pluginContext = JSON.parse(pluginVal) as DBGPTView;
  87. const replacement = `<custom-view>${cacheIndex}</custom-view>`;
  88. cachePluginContext.push({
  89. ...pluginContext,
  90. result: formatMarkdownVal(pluginContext.result ?? ''),
  91. });
  92. cacheIndex++;
  93. return replacement;
  94. } catch (e) {
  95. console.log((e as any).message, e);
  96. return matchVal;
  97. }
  98. });
  99. return {
  100. relations,
  101. cachePluginContext,
  102. value: result,
  103. };
  104. }, [context]);
  105. const extraMarkdownComponents = useMemo<MarkdownComponent>(
  106. () => ({
  107. 'custom-view'({ children }) {
  108. const index = +children.toString();
  109. if (!cachePluginContext[index]) {
  110. return children;
  111. }
  112. const { name, status, err_msg, result } = cachePluginContext[index];
  113. const { bgClass, icon } = pluginViewStatusMapper[status] ?? {};
  114. return (
  115. <div className='bg-white dark:bg-[#212121] rounded-lg overflow-hidden my-2 flex flex-col lg:max-w-[80%]'>
  116. <div className={classNames('flex px-4 md:px-6 py-2 items-center text-white text-sm', bgClass)}>
  117. {name}
  118. {icon}
  119. </div>
  120. {result ? (
  121. <div className='px-4 md:px-6 py-4 text-sm'>
  122. <GPTVis components={markdownComponents} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}>
  123. {result ?? ''}
  124. </GPTVis>
  125. </div>
  126. ) : (
  127. <div className='px-4 md:px-6 py-4 text-sm'>{err_msg}</div>
  128. )}
  129. </div>
  130. );
  131. },
  132. }),
  133. [context, cachePluginContext],
  134. );
  135. if (!isRobot && !context) return <div className='h-12'></div>;
  136. return (
  137. <div
  138. className={classNames('relative flex flex-wrap w-full p-2 md:p-4 rounded-xl break-words', {
  139. 'bg-white dark:bg-[#232734]': isRobot,
  140. 'lg:w-full xl:w-full pl-0': ['chat_with_db_execute', 'chat_dashboard'].includes(scene),
  141. })}
  142. >
  143. <div className='mr-2 flex flex-shrink-0 items-center justify-center h-7 w-7 rounded-full text-lg sm:mr-4'>
  144. {isRobot ? renderModelIcon(model_name) || <RobotOutlined /> : <UserOutlined />}
  145. </div>
  146. <div className='flex-1 overflow-hidden items-center text-md leading-8 pb-2'>
  147. {/* User Input */}
  148. {!isRobot && typeof context === 'string' && context}
  149. {/* Render Report */}
  150. {isRobot && isChartChat && typeof context === 'object' && (
  151. <div>
  152. {`[${context.template_name}]: `}
  153. <span className='text-theme-primary cursor-pointer' onClick={onLinkClick}>
  154. <CodeOutlined className='mr-1' />
  155. {context.template_introduce || 'More Details'}
  156. </span>
  157. </div>
  158. )}
  159. {/* Markdown */}
  160. {isRobot && typeof context === 'string' && (
  161. <GPTVis
  162. components={{ ...markdownComponents, ...extraMarkdownComponents }}
  163. rehypePlugins={[rehypeRaw]}
  164. remarkPlugins={[remarkGfm]}
  165. >
  166. {formatMarkdownVal(value)}
  167. </GPTVis>
  168. )}
  169. {!!relations?.length && (
  170. <div className='flex flex-wrap mt-2'>
  171. {relations?.map((value, index) => (
  172. <Tag color='#108ee9' key={value + index}>
  173. {value}
  174. </Tag>
  175. ))}
  176. </div>
  177. )}
  178. </div>
  179. {children}
  180. </div>
  181. );
  182. }
  183. export default memo(ChatContent);