mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-07-25 13:06:53 +00:00
267 lines
8.9 KiB
TypeScript
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);
|