mirror of
https://github.com/labring/FastGPT.git
synced 2025-12-26 04:32:50 +00:00
perf: move structured render components (#5710)
* perf: move structured render components * chore: remove useless code
This commit is contained in:
parent
7be8ece638
commit
8a68e9c208
|
|
@ -18,7 +18,7 @@ const IndicatorCard: React.FC<IndicatorCardProps> = ({ dataList }) => {
|
|||
return (
|
||||
<VStack align="stretch">
|
||||
{dataList.map((indicator, index) => (
|
||||
<Flex align="stretch" w="250px" key={index} gap={1} mt={1}>
|
||||
<Flex align="stretch" minW="250px" key={index} gap={1} mt={1}>
|
||||
<Flex w="5px" bg="blue.500"></Flex>
|
||||
<Flex
|
||||
flex="1"
|
||||
|
|
@ -29,32 +29,30 @@ const IndicatorCard: React.FC<IndicatorCardProps> = ({ dataList }) => {
|
|||
overflow="hidden"
|
||||
>
|
||||
{/* indicator name */}
|
||||
<Flex w="full">
|
||||
<Text
|
||||
color="gray.800"
|
||||
fontSize="sm"
|
||||
fontWeight="normal"
|
||||
textAlign="right"
|
||||
flex="1"
|
||||
noOfLines={1}
|
||||
>
|
||||
{indicator.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Box
|
||||
color="gray.800"
|
||||
fontSize="sm"
|
||||
fontWeight="normal"
|
||||
textAlign="right"
|
||||
flex="1"
|
||||
noOfLines={1}
|
||||
my={'4px'}
|
||||
>
|
||||
{indicator.name}
|
||||
</Box>
|
||||
|
||||
{/* indicator value and unit */}
|
||||
<Flex w="full">
|
||||
<Text
|
||||
color="blue.500"
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
textAlign="right"
|
||||
flex="1"
|
||||
noOfLines={1}
|
||||
>
|
||||
{indicator.value || t('common:core.chat.indicator.no_data')}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Box
|
||||
my={'4px'}
|
||||
color="blue.500"
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
textAlign="right"
|
||||
flex="1"
|
||||
noOfLines={1}
|
||||
>
|
||||
{indicator.value || t('common:core.chat.indicator.no_data')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
))}
|
||||
|
|
@ -4,26 +4,25 @@ import Icon from '@fastgpt/web/components/common/Icon';
|
|||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation';
|
||||
|
||||
const TableBlock: React.FC<{ code: string }> = ({ code }) => {
|
||||
const TableBlock = ({ data }: { data: any }) => {
|
||||
const { t } = useSafeTranslation();
|
||||
const tableData = JSON.parse(code);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
|
||||
const headers = Object.keys(tableData[0]);
|
||||
const headers = Object.keys(data[0]);
|
||||
|
||||
// calculate paginated data
|
||||
const { paginatedData, totalPages } = useMemo(() => {
|
||||
const total = Math.ceil(tableData.length / perPage);
|
||||
const total = Math.ceil(data.length / perPage);
|
||||
const startIndex = (currentPage - 1) * perPage;
|
||||
const endIndex = startIndex + perPage;
|
||||
const paginated = tableData.slice(startIndex, endIndex);
|
||||
const paginated = data.slice(startIndex, endIndex);
|
||||
|
||||
return {
|
||||
paginatedData: paginated,
|
||||
totalPages: total
|
||||
};
|
||||
}, [tableData, currentPage, perPage]);
|
||||
}, [data, currentPage, perPage]);
|
||||
|
||||
const handlePrevPage = () => {
|
||||
setCurrentPage((prev) => Math.max(prev - 1, 1));
|
||||
|
|
@ -36,16 +36,15 @@ const TextBlock: React.FC<{ content: string }> = ({ content }) => {
|
|||
|
||||
return (
|
||||
<Box
|
||||
w="90%"
|
||||
mx="auto"
|
||||
mt={2}
|
||||
p={4}
|
||||
p={2}
|
||||
bg="gray.200"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
borderRadius="md"
|
||||
fontSize="sm"
|
||||
lineHeight="1.6"
|
||||
lineHeight="1.2"
|
||||
sx={{ p: { marginBlock: '6px', padding: '1rem' } }}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[RemarkMath, [RemarkGfm, { singleTilde: false }], RemarkBreaks]}
|
||||
|
|
@ -57,7 +56,7 @@ const TextBlock: React.FC<{ content: string }> = ({ content }) => {
|
|||
{hasNewlines && (
|
||||
<>
|
||||
{!isExpanded && (
|
||||
<Flex justify="flex-end">
|
||||
<Flex justify="flex-end" p="2">
|
||||
<Button {...buttonProps} onClick={() => setIsExpanded(true)}>
|
||||
{t('common:core.chat.response.Read complete response')}
|
||||
</Button>
|
||||
|
|
@ -72,7 +71,7 @@ const TextBlock: React.FC<{ content: string }> = ({ content }) => {
|
|||
>
|
||||
{detail}
|
||||
</ReactMarkdown>
|
||||
<Flex justify="flex-end">
|
||||
<Flex justify="flex-end" p="2">
|
||||
<Button {...buttonProps} onClick={() => setIsExpanded(false)}>
|
||||
{t('common:core.chat.response.Fold response')}
|
||||
</Button>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Text, Flex } from '@chakra-ui/react';
|
||||
import { Flex, Box } from '@chakra-ui/react';
|
||||
import Icon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
interface TipsProps {
|
||||
|
|
@ -12,7 +12,8 @@ const Tips: React.FC<TipsProps> = ({ content, type = 'error' }) => {
|
|||
|
||||
return (
|
||||
<Flex
|
||||
align="center"
|
||||
maxW="200px"
|
||||
alignItems="start"
|
||||
p={4}
|
||||
bg={isError ? 'red.50' : 'yellow.50'}
|
||||
border="1px solid"
|
||||
|
|
@ -26,9 +27,9 @@ const Tips: React.FC<TipsProps> = ({ content, type = 'error' }) => {
|
|||
h="20px"
|
||||
color={isError ? 'red.500' : 'yellow.500'}
|
||||
/>
|
||||
<Text color={isError ? 'red.700' : 'yellow.700'} fontSize="sm" fontWeight="medium">
|
||||
<Box color={isError ? 'red.700' : 'yellow.700'} fontSize="sm" fontWeight="medium">
|
||||
{content}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const Table = dynamic(() => import('./Table'), { ssr: false });
|
||||
const Indicator = dynamic(() => import('./Indicator'), { ssr: false });
|
||||
const Link = dynamic(() => import('./Link'), { ssr: false });
|
||||
const Tips = dynamic(() => import('./Tips'), { ssr: false });
|
||||
const Divider = dynamic(() => import('./Divider'), { ssr: false });
|
||||
const TextBlock = dynamic(() => import('./TextBlock'), { ssr: false });
|
||||
const EChartsCodeBlock = dynamic(() => import('../../img/EChartsCodeBlock'), { ssr: false });
|
||||
|
||||
type RenderTypeEnum = {
|
||||
table: 'TABLE';
|
||||
indicator: 'INDICATOR';
|
||||
link: 'LINK';
|
||||
error_tips: 'ERROR_TIPS';
|
||||
warning_tips: 'WARNING_TIPS';
|
||||
divider: 'DIVIDER';
|
||||
textblock: 'TEXTBLOCK';
|
||||
chart: 'CHART';
|
||||
};
|
||||
|
||||
type renderType =
|
||||
| {
|
||||
type: RenderTypeEnum['table'];
|
||||
content: {
|
||||
data: Array<Record<string, string>>;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: RenderTypeEnum['indicator'];
|
||||
content: {
|
||||
dataList: {
|
||||
name: string;
|
||||
value: string | number;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: RenderTypeEnum['link'];
|
||||
content: { text: string; url: string };
|
||||
}
|
||||
| {
|
||||
type: RenderTypeEnum['error_tips'];
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
type: RenderTypeEnum['warning_tips'];
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
type: RenderTypeEnum['divider'];
|
||||
content: null;
|
||||
}
|
||||
| {
|
||||
type: RenderTypeEnum['textblock'];
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
type: RenderTypeEnum['chart'];
|
||||
content: {
|
||||
chartStructInfo: any;
|
||||
echartsData: any;
|
||||
};
|
||||
};
|
||||
|
||||
// convert single item to Markdown
|
||||
const StructureRender = ({ code }: { code: string }) => {
|
||||
const jsonObjList: renderType[] | string = useMemo(() => {
|
||||
try {
|
||||
const jsonObj = JSON.parse(code);
|
||||
if (Array.isArray(jsonObj)) {
|
||||
return jsonObj;
|
||||
}
|
||||
return [jsonObj];
|
||||
} catch {
|
||||
return code;
|
||||
}
|
||||
}, [code]);
|
||||
|
||||
if (typeof jsonObjList === 'string') {
|
||||
return String(code);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{jsonObjList.map((jsonObj, index) => {
|
||||
const { type, content } = jsonObj;
|
||||
if (type === 'TABLE') return <Table data={content.data} key={index}></Table>;
|
||||
if (type === 'INDICATOR')
|
||||
return <Indicator dataList={content.dataList} key={index}></Indicator>;
|
||||
if (type === 'LINK') return <Link data={content} key={index}></Link>;
|
||||
if (type === 'ERROR_TIPS') return <Tips content={content} key={index} type="error"></Tips>;
|
||||
if (type === 'WARNING_TIPS')
|
||||
return <Tips content={content} key={index} type="warning"></Tips>;
|
||||
if (type === 'DIVIDER') return <Divider key={index}></Divider>;
|
||||
if (type === 'TEXTBLOCK') return <TextBlock content={content} key={index}></TextBlock>;
|
||||
if (type === 'CHART')
|
||||
return (
|
||||
<EChartsCodeBlock
|
||||
code={JSON.stringify(content.echartsData)}
|
||||
key={index}
|
||||
></EChartsCodeBlock>
|
||||
);
|
||||
return String(JSON.stringify(jsonObj));
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StructureRender;
|
||||
|
|
@ -7,42 +7,18 @@ import { useMount } from 'ahooks';
|
|||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useScreen } from '@fastgpt/web/hooks/useScreen';
|
||||
|
||||
type EChartsGrid = {
|
||||
top: string;
|
||||
left: string;
|
||||
bottom: string;
|
||||
right: string;
|
||||
containLabel: boolean;
|
||||
};
|
||||
|
||||
type EChartsSeries = {
|
||||
data: number[];
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
type EChartsConfig = {
|
||||
xAxis: { data: string[]; type: string }[];
|
||||
yAxis: { type: string }[];
|
||||
grid: EChartsGrid;
|
||||
legend: { show: boolean };
|
||||
series: EChartsSeries[];
|
||||
tooltip: { trigger: string };
|
||||
dataZoom: unknown[];
|
||||
};
|
||||
|
||||
const EChartsCodeBlock = ({ code }: { code: string }) => {
|
||||
const EChartsCodeBlock = ({
|
||||
code,
|
||||
echartConfig
|
||||
}: {
|
||||
code?: string;
|
||||
echartConfig?: echarts.EChartsOption;
|
||||
}) => {
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
const eChart = useRef<ECharts>();
|
||||
const { isPc } = useSystem();
|
||||
const [option, setOption] = useState<EChartsConfig>();
|
||||
const [option, setOption] = useState<echarts.EChartsOption>();
|
||||
const [width, setWidth] = useState(400);
|
||||
const [dataRange, setDataRange] = useState({ start: 0, end: 100 });
|
||||
const [totalDataLength, setTotalDataLength] = useState(0);
|
||||
const [originalXData, setOriginalXData] = useState<string[]>([]);
|
||||
const [originalYData, setOriginalYData] = useState<number[]>([]);
|
||||
const dragStartTime = useRef<number>(0);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const findMarkdownDom = useCallback(() => {
|
||||
if (!chartRef.current) return;
|
||||
|
|
@ -59,176 +35,83 @@ const EChartsCodeBlock = ({ code }: { code: string }) => {
|
|||
return parent?.parentElement;
|
||||
}, [isPc]);
|
||||
|
||||
// filter data
|
||||
const filterDataByRange = useCallback(
|
||||
(originalXData: string[], originalYData: number[], range: { start: number; end: number }) => {
|
||||
if (!originalXData.length || !originalYData.length) return { xData: [], yData: [] };
|
||||
|
||||
const totalLength = Math.min(originalXData.length, originalYData.length);
|
||||
|
||||
const startIndex = Math.floor((range.start / 100) * totalLength);
|
||||
const endIndex = Math.min(totalLength, Math.ceil((range.end / 100) * totalLength));
|
||||
|
||||
const actualEndIndex = Math.max(startIndex + 1, endIndex);
|
||||
|
||||
// slice data
|
||||
const filteredXData = originalXData.slice(startIndex, actualEndIndex);
|
||||
const filteredYData = originalYData.slice(startIndex, actualEndIndex);
|
||||
|
||||
return { xData: filteredXData, yData: filteredYData };
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// x and y data extraction
|
||||
const extractXYData = useCallback((echartsConfig: EChartsConfig) => {
|
||||
const emptyResult = {
|
||||
xData: [] as string[],
|
||||
yData: [] as number[],
|
||||
chartContent: null as EChartsConfig | null
|
||||
};
|
||||
|
||||
if (echartsConfig?.series?.length > 0 && echartsConfig?.xAxis?.length > 0) {
|
||||
const series = echartsConfig.series[0];
|
||||
const xAxis = echartsConfig.xAxis[0];
|
||||
|
||||
return {
|
||||
xData: xAxis.data || [],
|
||||
yData: series.data || [],
|
||||
chartContent: echartsConfig
|
||||
};
|
||||
}
|
||||
|
||||
return emptyResult;
|
||||
}, []);
|
||||
|
||||
// abstract chart render function
|
||||
const createChartOption = useCallback(
|
||||
(xData: string[], yData: number[], chartContent?: EChartsConfig | null) => {
|
||||
if (chartContent) {
|
||||
return {
|
||||
...chartContent,
|
||||
xAxis: chartContent.xAxis.map((axis) => ({
|
||||
...axis,
|
||||
data: xData
|
||||
})),
|
||||
series: chartContent.series.map((series) => ({
|
||||
...series,
|
||||
data: yData
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
// fallback to default config
|
||||
return {
|
||||
grid: {
|
||||
bottom: '15%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xData,
|
||||
boundaryGap: true,
|
||||
axisTick: {
|
||||
alignWithLabel: true,
|
||||
interval: 0
|
||||
},
|
||||
axisLabel: {
|
||||
interval: (() => {
|
||||
const dataLength = xData.length;
|
||||
if (dataLength <= 10) return 0;
|
||||
if (dataLength <= 20) return 1;
|
||||
if (dataLength <= 50) return Math.floor(dataLength / 10);
|
||||
return Math.floor(dataLength / 15);
|
||||
})(),
|
||||
rotate: 45,
|
||||
fontSize: 10,
|
||||
formatter: (value: string) => {
|
||||
return value && value.length > 20 ? `${value.substring(0, 20)}...` : value;
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
scale: true
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: yData,
|
||||
barCategoryGap: '20%',
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
name: 'Data'
|
||||
}
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function (params: Array<{ name: string; seriesName: string; value: number }>) {
|
||||
if (Array.isArray(params) && params.length > 0) {
|
||||
const param = params[0];
|
||||
return `${param.name}<br/>${param.seriesName}: ${param.value}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useMount(() => {
|
||||
// @ts-ignore
|
||||
import('echarts-gl');
|
||||
});
|
||||
|
||||
// generate and update chart option
|
||||
useLayoutEffect(() => {
|
||||
try {
|
||||
const rawConfig: EChartsConfig = json5.parse(code.trim());
|
||||
|
||||
const { xData, yData, chartContent } = extractXYData(rawConfig);
|
||||
|
||||
if (xData.length === 0 || yData.length === 0) {
|
||||
return;
|
||||
const defaultOptions = {
|
||||
xAxis: {
|
||||
axisLabel: {
|
||||
rotate: 45,
|
||||
interval: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setOriginalXData(xData);
|
||||
setOriginalYData(yData);
|
||||
setTotalDataLength(Math.min(xData.length, yData.length));
|
||||
const rawOption = (() => {
|
||||
try {
|
||||
return echartConfig ? echartConfig : (json5.parse(code!.trim()) as echarts.EChartsOption);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
|
||||
const { xData: filteredXData, yData: filteredYData } = filterDataByRange(
|
||||
xData,
|
||||
yData,
|
||||
dataRange
|
||||
);
|
||||
const xAxis = rawOption.xAxis;
|
||||
|
||||
const chartOption = createChartOption(filteredXData, filteredYData, chartContent);
|
||||
if (Array.isArray(xAxis)) {
|
||||
xAxis.forEach((item) => {
|
||||
item.axisLabel = {
|
||||
...defaultOptions.xAxis.axisLabel,
|
||||
...item.axisLabel
|
||||
};
|
||||
});
|
||||
} else if (xAxis) {
|
||||
xAxis.axisLabel = {
|
||||
...defaultOptions.xAxis.axisLabel,
|
||||
...xAxis.axisLabel
|
||||
};
|
||||
}
|
||||
|
||||
// Add toolbox for image saving
|
||||
const RenderOption = {
|
||||
...chartOption,
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: {}
|
||||
const option = {
|
||||
...rawOption,
|
||||
grid: {
|
||||
left: '10%', // 左边距
|
||||
right: '5%', // 右边距
|
||||
top: '15%', // 顶边距
|
||||
bottom: '25%' // 底边距
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'slider',
|
||||
xAxisIndex: 0,
|
||||
start: 0,
|
||||
end: 100,
|
||||
show: true,
|
||||
realtime: true, // 实时渲染
|
||||
zoomLock: false
|
||||
}
|
||||
],
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: {
|
||||
show: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setOption(RenderOption as EChartsConfig);
|
||||
|
||||
if (chartRef.current) {
|
||||
if (!eChart.current) {
|
||||
eChart.current = echarts.init(chartRef.current);
|
||||
}
|
||||
eChart.current.setOption(RenderOption);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ECharts render failed:', error);
|
||||
};
|
||||
setOption(option);
|
||||
|
||||
if (!rawOption) return;
|
||||
|
||||
if (chartRef.current) {
|
||||
try {
|
||||
eChart.current = echarts.init(chartRef.current);
|
||||
eChart.current.setOption(option);
|
||||
} catch (error) {
|
||||
console.error('ECharts render failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
findMarkdownDom();
|
||||
|
|
@ -236,159 +119,25 @@ const EChartsCodeBlock = ({ code }: { code: string }) => {
|
|||
return () => {
|
||||
if (eChart.current) {
|
||||
eChart.current.dispose();
|
||||
eChart.current = undefined;
|
||||
}
|
||||
};
|
||||
}, [code, findMarkdownDom, filterDataByRange, dataRange, createChartOption, extractXYData]);
|
||||
}, [code, echartConfig, findMarkdownDom]);
|
||||
|
||||
const { screenWidth } = useScreen();
|
||||
useEffect(() => {
|
||||
findMarkdownDom();
|
||||
}, [screenWidth, findMarkdownDom]);
|
||||
}, [screenWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
eChart.current?.resize();
|
||||
}, [width]);
|
||||
|
||||
// slider control
|
||||
const handleRangeChange = useCallback((newRange: { start: number; end: number }) => {
|
||||
setDataRange(newRange);
|
||||
}, []);
|
||||
// handle drag
|
||||
const handleDrag = useCallback(
|
||||
(type: 'left' | 'right' | 'range', e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
setIsDragging(false);
|
||||
dragStartTime.current = Date.now();
|
||||
const startX = e.clientX;
|
||||
const { start: startValue, end: endValue } = dataRange;
|
||||
const rangeWidth = endValue - startValue;
|
||||
|
||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||
const deltaX = Math.abs(moveEvent.clientX - startX);
|
||||
const timeDelta = Date.now() - dragStartTime.current;
|
||||
|
||||
if (deltaX > 5 || timeDelta > 100) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
|
||||
const deltaPercent = ((moveEvent.clientX - startX) / Math.max(width, 400)) * 100;
|
||||
|
||||
// drag handle
|
||||
if (type === 'left') {
|
||||
const newStart = Math.max(0, Math.min(startValue + deltaPercent, endValue));
|
||||
handleRangeChange({ start: newStart, end: endValue });
|
||||
} else if (type === 'right') {
|
||||
const newEnd = Math.min(100, Math.max(endValue + deltaPercent, startValue));
|
||||
handleRangeChange({ start: startValue, end: newEnd });
|
||||
} else if (type === 'range') {
|
||||
const newStart = Math.max(0, Math.min(startValue + deltaPercent, 100 - rangeWidth));
|
||||
handleRangeChange({ start: newStart, end: newStart + rangeWidth });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
setTimeout(() => setIsDragging(false), 100);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
},
|
||||
[dataRange, width, handleRangeChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box overflowX={'auto'} bg={'white'} borderRadius={'md'}>
|
||||
<Box h={'400px'} w={`${width}px`} ref={chartRef} />
|
||||
{!option && (
|
||||
<Skeleton isLoaded={true} fadeDuration={2} h={'400px'} w={`${width}px`}></Skeleton>
|
||||
)}
|
||||
|
||||
{/* data range slider */}
|
||||
{option && totalDataLength > 1 && (
|
||||
<Box borderTop="1px solid #e2e8f0">
|
||||
<Box
|
||||
position="relative"
|
||||
h="40px"
|
||||
w={`${width}px`}
|
||||
minW="400px"
|
||||
bg="gray.50"
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
cursor="pointer"
|
||||
onClick={(e) => {
|
||||
if (!isDragging) {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const percentage = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const halfWidth = 10;
|
||||
|
||||
const newStart = Math.max(0, Math.min(percentage - halfWidth, 80));
|
||||
const newEnd = Math.min(100, Math.max(percentage + halfWidth, 20));
|
||||
handleRangeChange({ start: newStart, end: newEnd });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* data thumbnail */}
|
||||
{originalYData.length > 0 && (
|
||||
<svg width="100%" height="100%" style={{ position: 'absolute' }}>
|
||||
<polyline
|
||||
points={(() => {
|
||||
const maxVal = Math.max(...originalYData);
|
||||
const minVal = Math.min(...originalYData);
|
||||
const range = maxVal - minVal || 1;
|
||||
|
||||
return originalYData
|
||||
.map((value, index) => {
|
||||
const x = (index / (originalYData.length - 1)) * (width - 8) + 4;
|
||||
const y = 36 - ((value - minVal) / range) * 32;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
.join(' ');
|
||||
})()}
|
||||
fill="none"
|
||||
stroke="#93c5fd"
|
||||
strokeWidth="1.5"
|
||||
opacity="0.7"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{/* select area */}
|
||||
<Box
|
||||
position="absolute"
|
||||
left={`${dataRange.start}%`}
|
||||
width={`${dataRange.end - dataRange.start}%`}
|
||||
h="100%"
|
||||
bg="rgba(59, 130, 246, 0.2)"
|
||||
cursor="ew-resize"
|
||||
onMouseDown={(e) => handleDrag('range', e)}
|
||||
/>
|
||||
|
||||
{/* left and right drag handle */}
|
||||
<Box
|
||||
position="absolute"
|
||||
left={`${dataRange.start}%`}
|
||||
w="8px"
|
||||
h="100%"
|
||||
cursor="ew-resize"
|
||||
transform="translateX(-50%)"
|
||||
onMouseDown={(e) => handleDrag('left', e)}
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
left={`${dataRange.end}%`}
|
||||
w="8px"
|
||||
h="100%"
|
||||
cursor="ew-resize"
|
||||
transform="translateX(-50%)"
|
||||
onMouseDown={(e) => handleDrag('right', e)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import styles from './index.module.scss';
|
|||
import { Box } from '@chakra-ui/react';
|
||||
import { useCreation } from 'ahooks';
|
||||
import type { AProps } from './A';
|
||||
import { CodeClassNameEnum } from './utils';
|
||||
import { CodeClassNameEnum, mdTextFormat } from './utils';
|
||||
|
||||
import DomPurify from 'dompurify';
|
||||
|
||||
|
|
@ -26,19 +26,12 @@ const IframeCodeBlock = dynamic(() => import('./codeBlock/Iframe'), { ssr: false
|
|||
const IframeHtmlCodeBlock = dynamic(() => import('./codeBlock/iframe-html'), { ssr: false });
|
||||
const VideoBlock = dynamic(() => import('./codeBlock/Video'), { ssr: false });
|
||||
const AudioBlock = dynamic(() => import('./codeBlock/Audio'), { ssr: false });
|
||||
const TableBlock = dynamic(() => import('./codeBlock/Table'), { ssr: false });
|
||||
const IndicatorCard = dynamic(() => import('./codeBlock/IndicatorCard'), { ssr: false });
|
||||
const LinkBlock = dynamic(() => import('./codeBlock/Link'), { ssr: false });
|
||||
const Tips = dynamic(() => import('./codeBlock/Tips'), { ssr: false });
|
||||
const Divider = dynamic(() => import('./codeBlock/Divider'), { ssr: false });
|
||||
const TextBlock = dynamic(() => import('./codeBlock/TextBlock'), { ssr: false });
|
||||
const StructureRender = dynamic(() => import('./codeBlock/StructureRender'), { ssr: false });
|
||||
|
||||
const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false });
|
||||
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false });
|
||||
const A = dynamic(() => import('./A'), { ssr: false });
|
||||
|
||||
const formatCodeBlock = (lang: string, content: string) => `\`\`\`${lang}\n${content}\n\`\`\``;
|
||||
|
||||
type Props = {
|
||||
source?: string;
|
||||
showAnimation?: boolean;
|
||||
|
|
@ -79,75 +72,11 @@ const MarkdownRender = ({
|
|||
};
|
||||
}, [chatAuthData, onOpenCiteModal, showAnimation]);
|
||||
|
||||
// convert single item to Markdown
|
||||
const convertRenderBlockToMarkdown = useCallback((jsonContent: string): string => {
|
||||
const converItem = (type: string, content: any) => {
|
||||
switch (type) {
|
||||
case 'TEXT':
|
||||
return (typeof content === 'string' ? content : JSON.stringify(content)) + '\n\n';
|
||||
|
||||
case 'CHART':
|
||||
return content?.hasChart && content?.echartsData
|
||||
? `\`\`\`echarts\n${JSON.stringify(content.echartsData, null, 2)}\n\`\`\`\n\n`
|
||||
: '';
|
||||
|
||||
case 'TABLE':
|
||||
return content?.data
|
||||
? `\`\`\`table\n${JSON.stringify(content.data, null, 2)}\n\`\`\`\n\n`
|
||||
: '';
|
||||
|
||||
case 'INDICATOR':
|
||||
return content?.dataList
|
||||
? `\`\`\`indicator\n${JSON.stringify(content.dataList, null, 2)}\n\`\`\`\n\n`
|
||||
: '';
|
||||
|
||||
case 'LINK':
|
||||
return content?.text && content?.url
|
||||
? `\`\`\`link\n${JSON.stringify(content, null, 2)}\n\`\`\`\n\n`
|
||||
: '';
|
||||
|
||||
case 'ERROR_TIPS':
|
||||
return content ? `\`\`\`error_tips\n${content}\n\`\`\`\n\n` : '';
|
||||
|
||||
case 'WARNING_TIPS':
|
||||
return content ? `\`\`\`warning_tips\n${content}\n\`\`\`\n\n` : '';
|
||||
|
||||
case 'DIVIDER':
|
||||
return `\`\`\`divider\n\n\`\`\`\n\n`;
|
||||
|
||||
case 'TEXTBLOCK':
|
||||
return content ? `\`\`\`textblock\n${content}\n\`\`\`\n\n` : '';
|
||||
|
||||
default:
|
||||
return formatCodeBlock('json', jsonContent);
|
||||
}
|
||||
};
|
||||
try {
|
||||
const jsonObj = JSON.parse(jsonContent);
|
||||
if (Array.isArray(jsonObj)) {
|
||||
return jsonObj.map((item) => converItem(item.type, item.content)).join(`\n\n`);
|
||||
} else {
|
||||
return converItem(jsonObj.type, jsonObj.content);
|
||||
}
|
||||
} catch {
|
||||
return formatCodeBlock('json', jsonContent);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const formatSource = useMemo(() => {
|
||||
if (showAnimation || forbidZhFormat) return source;
|
||||
|
||||
const result = source.replace(/```RENDER([\s\S]*?)```/g, (match, p1) => {
|
||||
// p1: the content inside ```RENDER ... ```
|
||||
const cleanedContent = p1
|
||||
.replace(/^```[\s\S]*?(\n)?/, '')
|
||||
.replace(/```$/, '')
|
||||
.trim();
|
||||
return convertRenderBlockToMarkdown(cleanedContent);
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [convertRenderBlockToMarkdown, forbidZhFormat, showAnimation, source]);
|
||||
return mdTextFormat(source);
|
||||
}, [forbidZhFormat, showAnimation, source]);
|
||||
|
||||
const sanitizedSource = useMemo(() => {
|
||||
return DomPurify.sanitize(formatSource);
|
||||
|
|
@ -238,28 +167,9 @@ function Code(e: any) {
|
|||
if (codeType === CodeClassNameEnum.audio) {
|
||||
return <AudioBlock code={strChildren} />;
|
||||
}
|
||||
if (codeType === CodeClassNameEnum.table) {
|
||||
return <TableBlock code={strChildren} />;
|
||||
if (codeType === CodeClassNameEnum.render) {
|
||||
return <StructureRender code={strChildren} />;
|
||||
}
|
||||
if (codeType === CodeClassNameEnum.indicator) {
|
||||
return <IndicatorCard dataList={JSON.parse(strChildren)} />;
|
||||
}
|
||||
if (codeType === CodeClassNameEnum.link) {
|
||||
return <LinkBlock data={JSON.parse(strChildren)} />;
|
||||
}
|
||||
if (codeType === CodeClassNameEnum.error_tips) {
|
||||
return <Tips content={strChildren} type="error" />;
|
||||
}
|
||||
if (codeType === CodeClassNameEnum.warning_tips) {
|
||||
return <Tips content={strChildren} type="warning" />;
|
||||
}
|
||||
if (codeType === CodeClassNameEnum.divider) {
|
||||
return <Divider />;
|
||||
}
|
||||
if (codeType === CodeClassNameEnum.textblock) {
|
||||
return <TextBlock content={strChildren} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeLight className={className} codeBlock={codeBlock} match={match}>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,7 @@ export enum CodeClassNameEnum {
|
|||
svg = 'svg',
|
||||
video = 'video',
|
||||
audio = 'audio',
|
||||
table = 'table',
|
||||
indicator = 'indicator',
|
||||
link = 'link',
|
||||
error_tips = 'error_tips',
|
||||
warning_tips = 'warning_tips',
|
||||
divider = 'divider',
|
||||
textblock = 'textblock'
|
||||
render = 'render'
|
||||
}
|
||||
|
||||
export const mdTextFormat = (text: string) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue