DB-GPT/web/new-components/chat/content/ChatContent.tsx
2025-03-17 15:38:31 +08:00

267 lines
8.9 KiB
TypeScript

import markdownComponents, { markdownPlugins, preprocessLaTeX } from '@/components/chat/chat-content/config';
import { IChatDialogueMessageSchema } from '@/types/chat';
import { STORAGE_USERINFO_KEY } from '@/utils/constants/index';
import {
CheckOutlined,
ClockCircleOutlined,
CloseOutlined,
CodeOutlined,
CopyOutlined,
LoadingOutlined,
} from '@ant-design/icons';
import { GPTVis } from '@antv/gpt-vis';
import { message } from 'antd';
import classNames from 'classnames';
import Image from 'next/image';
import { useSearchParams } from 'next/navigation';
import React, { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import Feedback from './Feedback';
import RobotIcon from './RobotIcon';
const UserIcon: React.FC = () => {
const user = JSON.parse(localStorage.getItem(STORAGE_USERINFO_KEY) ?? '');
if (!user.avatar_url) {
return (
<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'>
{user?.nick_name}
</div>
);
}
return (
<Image
className='rounded-full border border-gray-200 object-contain bg-white inline-block'
width={32}
height={32}
src={user?.avatar_url}
alt={user?.nick_name}
/>
);
};
type DBGPTView = {
name: string;
status: 'todo' | 'runing' | 'failed' | 'completed' | (string & {});
result?: string;
err_msg?: string;
};
type MarkdownComponent = Parameters<typeof GPTVis>['0']['components'];
const pluginViewStatusMapper: Record<DBGPTView['status'], { bgClass: string; icon: React.ReactNode }> = {
todo: {
bgClass: 'bg-gray-500',
icon: <ClockCircleOutlined className='ml-2' />,
},
runing: {
bgClass: 'bg-blue-500',
icon: <LoadingOutlined className='ml-2' />,
},
failed: {
bgClass: 'bg-red-500',
icon: <CloseOutlined className='ml-2' />,
},
completed: {
bgClass: 'bg-green-500',
icon: <CheckOutlined className='ml-2' />,
},
};
const formatMarkdownVal = (val: string) => {
return val
.replaceAll('\\n', '\n')
.replace(/<table(\w*=[^>]+)>/gi, '<table $1>')
.replace(/<tr(\w*=[^>]+)>/gi, '<tr $1>');
};
const formatMarkdownValForAgent = (val: string) => {
return val?.replace(/<table(\w*=[^>]+)>/gi, '<table $1>').replace(/<tr(\w*=[^>]+)>/gi, '<tr $1>');
};
const ChatContent: React.FC<{
content: Omit<IChatDialogueMessageSchema, 'context'> & {
context:
| string
| {
template_name: string;
template_introduce: string;
};
};
onLinkClick: () => void;
}> = ({ content, onLinkClick }) => {
const { t } = useTranslation();
const searchParams = useSearchParams();
const scene = searchParams?.get('scene') ?? '';
const { context, model_name, role, thinking } = content;
const isRobot = useMemo(() => role === 'view', [role]);
const { value, cachePluginContext } = useMemo<{
relations: string[];
value: string;
cachePluginContext: DBGPTView[];
}>(() => {
if (typeof context !== 'string') {
return {
relations: [],
value: '',
cachePluginContext: [],
};
}
const [value, relation] = context.split('\trelations:');
const relations = relation ? relation.split(',') : [];
const cachePluginContext: DBGPTView[] = [];
let cacheIndex = 0;
const result = value.replace(/<dbgpt-view[^>]*>[^<]*<\/dbgpt-view>/gi, matchVal => {
try {
const pluginVal = matchVal.replaceAll('\n', '\\n').replace(/<[^>]*>|<\/[^>]*>/gm, '');
const pluginContext = JSON.parse(pluginVal) as DBGPTView;
const replacement = `<custom-view>${cacheIndex}</custom-view>`;
cachePluginContext.push({
...pluginContext,
result: formatMarkdownVal(pluginContext.result ?? ''),
});
cacheIndex++;
return replacement;
} catch (e) {
console.log((e as any).message, e);
return matchVal;
}
});
return {
relations,
cachePluginContext,
value: result,
};
}, [context]);
const extraMarkdownComponents = useMemo<MarkdownComponent>(
() => ({
'custom-view'({ children }) {
const index = +children.toString();
if (!cachePluginContext[index]) {
return children;
}
const { name, status, err_msg, result } = cachePluginContext[index];
const { bgClass, icon } = pluginViewStatusMapper[status] ?? {};
return (
<div className='bg-white dark:bg-[#212121] rounded-lg overflow-hidden my-2 flex flex-col lg:max-w-[80%]'>
<div className={classNames('flex px-4 md:px-6 py-2 items-center text-white text-sm', bgClass)}>
{name}
{icon}
</div>
{result ? (
<div className='px-4 md:px-6 py-4 text-sm'>
<GPTVis components={markdownComponents} {...markdownPlugins}>
{preprocessLaTeX(result ?? '')}
</GPTVis>
</div>
) : (
<div className='px-4 md:px-6 py-4 text-sm'>{err_msg}</div>
)}
</div>
);
},
}),
[cachePluginContext],
);
return (
<div className='flex flex-1 gap-3 mt-6'>
{/* icon */}
<div className='flex flex-shrink-0 items-start'>{isRobot ? <RobotIcon model={model_name} /> : <UserIcon />}</div>
<div className={`flex ${scene === 'chat_agent' && !thinking ? 'flex-1' : ''} overflow-hidden`}>
{/* 用户提问 */}
{!isRobot && (
<div className='flex flex-1 relative group'>
<div
className='flex-1 text-sm text-[#1c2533] dark:text-white'
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
>
{typeof context === 'string' && context}
</div>
{typeof context === 'string' && context.trim() && (
<div className='absolute right-0 top-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200'>
<button
className='flex items-center justify-center w-8 h-8 text-[#525964] dark:text-[rgba(255,255,255,0.6)] hover:text-[#1677ff] dark:hover:text-white transition-colors'
onClick={() => {
if (typeof context === 'string') {
navigator.clipboard
.writeText(context)
.then(() => {
message.success(t('copy_to_clipboard_success'));
})
.catch(err => {
console.error(t('copy_to_clipboard_failed'), err);
message.error(t('copy_to_clipboard_failed'));
});
}
}}
title={t('copy_to_clipboard')}
>
<CopyOutlined />
</button>
</div>
)}
</div>
)}
{/* ai回答 */}
{isRobot && (
<div className='flex flex-1 flex-col w-full'>
<div className='bg-white dark:bg-[rgba(255,255,255,0.16)] p-4 rounded-2xl rounded-tl-none mb-2'>
{typeof context === 'object' && (
<div>
{`[${context.template_name}]: `}
<span className='text-theme-primary cursor-pointer' onClick={onLinkClick}>
<CodeOutlined className='mr-1' />
{context.template_introduce || 'More Details'}
</span>
</div>
)}
{typeof context === 'string' && scene === 'chat_agent' && (
<GPTVis components={markdownComponents} {...markdownPlugins}>
{preprocessLaTeX(formatMarkdownValForAgent(value))}
</GPTVis>
)}
{typeof context === 'string' && scene !== 'chat_agent' && (
<div>
<GPTVis
components={{
...markdownComponents,
...extraMarkdownComponents,
}}
{...markdownPlugins}
>
{preprocessLaTeX(formatMarkdownVal(value))}
</GPTVis>
</div>
)}
{/* 正在思考 */}
{thinking && !context && (
<div className='flex items-center gap-2'>
<span className='flex text-sm text-[#1c2533] dark:text-white'>{t('thinking')}</span>
<div className='flex'>
<div className='w-1 h-1 rounded-full mx-1 animate-pulse1'></div>
<div className='w-1 h-1 rounded-full mx-1 animate-pulse2'></div>
<div className='w-1 h-1 rounded-full mx-1 animate-pulse3'></div>
</div>
</div>
)}
</div>
{/* 用户反馈 */}
<Feedback content={content} />
</div>
)}
</div>
</div>
);
};
export default memo(ChatContent);