feat: clean markdown content

This commit is contained in:
jialin
2025-06-25 14:35:00 +08:00
parent 830ee45161
commit a6825f7ee3
7 changed files with 70 additions and 44 deletions
+1
View File
@@ -38,6 +38,7 @@
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"dompurify": "^3.2.6",
"driver.js": "^1.3.1", "driver.js": "^1.3.1",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"epubjs": "^0.3.93", "epubjs": "^0.3.93",
+15
View File
@@ -75,6 +75,9 @@ dependencies:
dayjs: dayjs:
specifier: ^1.11.11 specifier: ^1.11.11
version: 1.11.11 version: 1.11.11
dompurify:
specifier: ^3.2.6
version: 3.2.6
driver.js: driver.js:
specifier: ^1.3.1 specifier: ^1.3.1
version: 1.3.1 version: 1.3.1
@@ -5377,6 +5380,12 @@ packages:
/@types/stylis@4.2.6: /@types/stylis@4.2.6:
resolution: {integrity: sha512-4nebF2ZJGzQk0ka0O6+FZUWceyFv4vWq/0dXBMmrSeAwzOuOd/GxE5Pa64d/ndeNLG73dXoBsRzvtsVsYUv6Uw==, tarball: https://registry.npmjs.org/@types/stylis/-/stylis-4.2.6.tgz} resolution: {integrity: sha512-4nebF2ZJGzQk0ka0O6+FZUWceyFv4vWq/0dXBMmrSeAwzOuOd/GxE5Pa64d/ndeNLG73dXoBsRzvtsVsYUv6Uw==, tarball: https://registry.npmjs.org/@types/stylis/-/stylis-4.2.6.tgz}
/@types/trusted-types@2.0.7:
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==, tarball: https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz}
requiresBuild: true
dev: false
optional: true
/@types/unist@2.0.10: /@types/unist@2.0.10:
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==, tarball: https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz} resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==, tarball: https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz}
dev: false dev: false
@@ -9395,6 +9404,12 @@ packages:
domelementtype: 2.3.0 domelementtype: 2.3.0
dev: false dev: false
/dompurify@3.2.6:
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==, tarball: https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz}
optionalDependencies:
'@types/trusted-types': 2.0.7
dev: false
/domutils@1.7.0: /domutils@1.7.0:
resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==, tarball: https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz} resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==, tarball: https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz}
dependencies: dependencies:
+22 -3
View File
@@ -1,6 +1,7 @@
import { EyeOutlined } from '@ant-design/icons'; import { EyeOutlined } from '@ant-design/icons';
import { sanitizeUrl } from '@braintree/sanitize-url'; import { sanitizeUrl } from '@braintree/sanitize-url';
import { Checkbox, Image, Typography } from 'antd'; import { Checkbox, Image, Typography } from 'antd';
import DOMPurify from 'dompurify';
import { unescape } from 'lodash'; import { unescape } from 'lodash';
import { TokensList, marked } from 'marked'; import { TokensList, marked } from 'marked';
import React, { Fragment, useCallback, useEffect } from 'react'; import React, { Fragment, useCallback, useEffect } from 'react';
@@ -16,6 +17,15 @@ interface MarkdownViewerProps {
generateImgLink?: (src: string) => string; generateImgLink?: (src: string) => string;
} }
const dompurifyOptions = {
FORBID_TAGS: ['meta', 'style', 'script', 'iframe'],
FORBID_ATTR: ['onerror', 'onclick', 'onload', 'style']
};
const cleanHtml = (html: string): string => {
return DOMPurify.sanitize(html, dompurifyOptions);
};
const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
content, content,
generateImgLink, generateImgLink,
@@ -68,11 +78,14 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
const renderItem = useCallback( const renderItem = useCallback(
(token: any, render: any) => { (token: any, render: any) => {
if (token.type === 'script' || token.type === 'style') {
return null;
}
if (!reDefineTypes.includes(token.type)) { if (!reDefineTypes.includes(token.type)) {
return ( return (
<span <span
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: marked.parser([token], { renderer }) __html: cleanHtml(marked.parser([token], { renderer }))
}} }}
></span> ></span>
); );
@@ -89,7 +102,13 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
} }
if (token.type === 'html') { if (token.type === 'html') {
htmlstr = <div dangerouslySetInnerHTML={{ __html: token.text }} />; htmlstr = (
<div
dangerouslySetInnerHTML={{
__html: cleanHtml(token.text)
}}
/>
);
} }
if (token.type === 'list') { if (token.type === 'list') {
htmlstr = token.order ? ( htmlstr = token.order ? (
@@ -158,7 +177,7 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
if (token.type === 'link') { if (token.type === 'link') {
htmlstr = ( htmlstr = (
<Link <Link
href={token.href} href={sanitizeUrl(token.href || '')}
title={token.title || ''} title={token.title || ''}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
+16 -15
View File
@@ -242,13 +242,6 @@ const AddModal: FC<AddModalProps> = (props) => {
); );
const handleSelectModelFile = async (item: any, requestModelId: number) => { const handleSelectModelFile = async (item: any, requestModelId: number) => {
console.log(
'handleSelectModelFile:',
item,
requestModelId,
getRequestId(),
evaluateStateRef.current
);
if (requestModelId !== getRequestId()) { if (requestModelId !== getRequestId()) {
return; return;
} }
@@ -261,8 +254,6 @@ const AddModal: FC<AddModalProps> = (props) => {
categories: getCategory(item) categories: getCategory(item)
}); });
console.log('handleSelectModelFile>>>>>>>>>>>>', item);
// evaluate the form data when select a model file // evaluate the form data when select a model file
if (item.fakeName) { if (item.fakeName) {
onSelectFile(item, modelInfo); onSelectFile(item, modelInfo);
@@ -273,7 +264,19 @@ const AddModal: FC<AddModalProps> = (props) => {
cancelEvaluate(); cancelEvaluate();
modelFileRef.current?.cancelRequest(); modelFileRef.current?.cancelRequest();
}; };
const handleOnSelectModel = async (item: any) => {
const generateNameValue = (
item: any,
modelName: string,
manual?: boolean
) => {
if (item.name === currentSelectedModel.current.name) {
return manual ? modelName : form.current?.getFieldValue?.('name');
}
return modelName;
};
const handleOnSelectModel = async (item: any, manual?: boolean) => {
// If the item is empty or the same as the selected model, do nothing // If the item is empty or the same as the selected model, do nothing
handleCancelFiles(); handleCancelFiles();
@@ -324,7 +327,7 @@ const AddModal: FC<AddModalProps> = (props) => {
); );
}; };
const handleOnSelectModelAfterEvaluate = (item: any) => { const handleOnSelectModelAfterEvaluate = (item: any, manual?: boolean) => {
console.log( console.log(
'handleOnSelectModelAfterEvaluate:', 'handleOnSelectModelAfterEvaluate:',
item.name, item.name,
@@ -352,9 +355,8 @@ const AddModal: FC<AddModalProps> = (props) => {
handleShowCompatibleAlert(item.evaluateResult); handleShowCompatibleAlert(item.evaluateResult);
form.current?.setFieldsValue?.({ form.current?.setFieldsValue?.({
...getDefaultSpec(item), ...getDefaultSpec(item),
...(item.name === currentSelectedModel.current.name ...modelInfo,
? _.omit(modelInfo, ['name']) name: generateNameValue(item, modelInfo.name, manual),
: modelInfo),
categories: getCategory(item) categories: getCategory(item)
}); });
} }
@@ -521,7 +523,6 @@ const AddModal: FC<AddModalProps> = (props) => {
handleOnSelectModelAfterEvaluate handleOnSelectModelAfterEvaluate
} }
displayEvaluateStatus={displayEvaluateStatus} displayEvaluateStatus={displayEvaluateStatus}
unlockWarningStatus={unlockWarningStatus}
gpuOptions={props.gpuOptions} gpuOptions={props.gpuOptions}
></SearchModel> ></SearchModel>
</ColumnWrapper> </ColumnWrapper>
+4 -17
View File
@@ -9,7 +9,6 @@ import {
RightOutlined RightOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { useBoolean } from 'ahooks';
import { Button, Empty, Spin, Tooltip } from 'antd'; import { Button, Empty, Spin, Tooltip } from 'antd';
import { some } from 'lodash'; import { some } from 'lodash';
import 'overlayscrollbars/overlayscrollbars.css'; import 'overlayscrollbars/overlayscrollbars.css';
@@ -73,7 +72,6 @@ const ModelCard: React.FC<{
loadingModel?: boolean; loadingModel?: boolean;
modelSource: string; modelSource: string;
}> = (props) => { }> = (props) => {
const [hideMd, { toggle }] = useBoolean();
const { onCollapse, setIsGGUF, collapsed, modelSource } = props; const { onCollapse, setIsGGUF, collapsed, modelSource } = props;
const intl = useIntl(); const intl = useIntl();
const requestSource = useRequestToken(); const requestSource = useRequestToken();
@@ -324,7 +322,7 @@ const ModelCard: React.FC<{
<div className="card-wrapper"> <div className="card-wrapper">
{modelData ? ( {modelData ? (
<div className="model-card-wrap"> <div className="model-card-wrap">
<div className="flex-center"> <div className="flex-center flex-wrap gap-8">
{modelType && ( {modelType && (
<ThemeTag className="tag-item" color="gold" opacity={0.65}> <ThemeTag className="tag-item" color="gold" opacity={0.65}>
<span className="m-r-5"> <span className="m-r-5">
@@ -399,20 +397,9 @@ const ModelCard: React.FC<{
{readmeText && ( {readmeText && (
<> <>
<TitleWrapper> <TitleWrapper>
<div className="flex-center gap-8"> <span className="title">README.md</span>
<span className="title">README.md</span>
{/* <Button
onClick={toggle}
size="small"
type="text"
icon={hideMd ? <EyeOutlined /> : <EyeInvisibleOutlined />}
></Button> */}
</div>
</TitleWrapper> </TitleWrapper>
<div <div className="card-wrapper">
className="card-wrapper"
style={{ width: hideMd ? 0 : 'auto', overflow: 'hidden' }}
>
<MarkdownViewer <MarkdownViewer
generateImgLink={generateModeScopeImgLink} generateImgLink={generateModeScopeImgLink}
content={readmeText} content={readmeText}
@@ -428,4 +415,4 @@ const ModelCard: React.FC<{
); );
}; };
export default React.memo(ModelCard); export default ModelCard;
+11 -9
View File
@@ -47,9 +47,8 @@ interface SearchInputProps {
gpuOptions?: any[]; gpuOptions?: any[];
setLoadingModel?: (flag: boolean) => void; setLoadingModel?: (flag: boolean) => void;
onSourceChange?: (source: string) => void; onSourceChange?: (source: string) => void;
onSelectModel: (model: any) => void; onSelectModel: (model: any, manul?: boolean) => void;
onSelectModelAfterEvaluate: (model: any) => void; onSelectModelAfterEvaluate: (model: any, manual?: boolean) => void;
unlockWarningStatus?: () => void;
displayEvaluateStatus?: ( displayEvaluateStatus?: (
data: MessageStatus, data: MessageStatus,
options?: WarningStausOptions options?: WarningStausOptions
@@ -66,8 +65,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
setLoadingModel, setLoadingModel,
onSelectModel, onSelectModel,
onSelectModelAfterEvaluate, onSelectModelAfterEvaluate,
displayEvaluateStatus, displayEvaluateStatus
unlockWarningStatus
} = props; } = props;
const [dataSource, setDataSource] = useState<{ const [dataSource, setDataSource] = useState<{
@@ -142,12 +140,12 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
return isGGUF || isGGUFFromMs; return isGGUF || isGGUFFromMs;
}; };
const handleOnSelectModel = (model: any) => { const handleOnSelectModel = (model: any, manual?: boolean) => {
const item = model || {}; const item = model || {};
if (item.evaluated && !item.isGGUF) { if (item.evaluated && !item.isGGUF) {
onSelectModelAfterEvaluate(item); onSelectModelAfterEvaluate(item, manual);
} else { } else {
onSelectModel(item); onSelectModel(item, manual);
} }
setCurrent(item.id); setCurrent(item.id);
currentRef.current = item.id; currentRef.current = item.id;
@@ -497,6 +495,10 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
} }
}; };
const handleSelectModelManually = (model: any) => {
handleOnSelectModel(model, true);
};
const renderGGUFTips = useMemo(() => { const renderGGUFTips = useMemo(() => {
return ( return (
<Tooltip <Tooltip
@@ -596,7 +598,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
current={current} current={current}
source={modelSource} source={modelSource}
isEvaluating={isEvaluating} isEvaluating={isEvaluating}
onSelect={handleOnSelectModel} onSelect={handleSelectModelManually}
></SearchResult> ></SearchResult>
</div> </div>
); );
+1
View File
@@ -18,6 +18,7 @@
border-radius: 4px; border-radius: 4px;
font-size: 12px; font-size: 12px;
height: 20px; height: 20px;
margin-right: 0;
} }
.btn { .btn {