Files
gpustack-ui/src/pages/llmodels/components/model-card.tsx
T

407 lines
11 KiB
TypeScript

import IconFont from '@/components/icon-font';
import MarkdownViewer from '@/components/markdown-viewer';
import useRequestToken from '@/hooks/use-request-token';
import {
DownOutlined,
FileTextOutlined,
RightOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Empty, Spin, Tag, Tooltip } from 'antd';
import { some } from 'lodash';
import 'overlayscrollbars/overlayscrollbars.css';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css';
import {
downloadModelFile,
downloadModelScopeModelfile,
queryHuggingfaceModelDetail,
queryModelScopeModelDetail
} from '../apis';
import { modelSourceMap } from '../config';
import '../style/model-card.less';
import TitleWrapper from './title-wrapper';
const ModelCard: React.FC<{
onCollapse: (flag: boolean) => void;
setIsGGUF: (flag: boolean) => void;
selectedModel: any;
collapsed: boolean;
loadingModel?: boolean;
modelSource: string;
}> = (props) => {
const { onCollapse, setIsGGUF, collapsed, modelSource } = props;
const intl = useIntl();
const requestSource = useRequestToken();
const [modelData, setModelData] = useState<any>(null);
const [readmeText, setReadmeText] = useState<string | null>(null);
const requestToken = useRef<any>(null);
const axiosTokenRef = useRef<any>(null);
const loadConfigTokenRef = useRef<any>(null);
const loadConfigJsonTokenRef = useRef<any>(null);
const [isGGUFModel, setIsGGUFModel] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const modelTags = useMemo(() => {
if (modelSource === modelSourceMap.huggingface_value) {
return modelData?.pipeline_tag ? [modelData?.pipeline_tag] : [];
}
if (modelSource === modelSourceMap.modelscope_value) {
const tasks = modelData?.Tasks || [];
return tasks.map((task: any) => task?.Name)?.filter((val: string) => val);
}
return [];
}, [modelSource, modelData]);
const modelType = useMemo(() => {
if (modelSource === modelSourceMap.huggingface_value) {
return modelData?.config?.model_type || modelData?.ModelType?.[0];
}
if (modelSource === modelSourceMap.modelscope_value) {
return modelData?.ModelType?.[0];
}
}, [modelData, modelSource]);
const loadFile = useCallback(async (repo: string, sha: string) => {
try {
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
const res = await downloadModelFile(
{
repo,
revision: sha,
path: 'README.md'
},
{
signal: axiosTokenRef.current.signal
}
);
return res || '';
} catch (error) {
return '';
}
}, []);
const loadConfig = useCallback(async (repo: string, sha: string) => {
try {
loadConfigTokenRef.current?.abort?.();
loadConfigTokenRef.current = new AbortController();
const res = await downloadModelFile(
{
repo,
revision: sha,
path: 'config.json'
},
{
signal: loadConfigTokenRef.current.signal
}
);
return res || null;
} catch (error) {
console.log('error======', error);
return null;
}
}, []);
const removeMetadata = useCallback((str: string) => {
let indexes = [];
let index = str.indexOf('---');
while (index !== -1) {
indexes.push(index);
if (indexes.length >= 2) {
break;
}
index = str.indexOf('---', index + 1);
}
if (indexes.length >= 2) {
return str.slice(indexes[1] + 3);
}
return str;
}, []);
// huggingface model card data
const getHuggingfaceModelDetail = async () => {
try {
const [modelcard, readme] = await Promise.all([
queryHuggingfaceModelDetail(
{ repo: props.selectedModel.name },
{
token: requestToken.current.token
}
),
loadFile(props.selectedModel.name, 'main')
]);
setModelData(modelcard);
// remove the meta data from readme
const newReadme = removeMetadata(readme);
setReadmeText(newReadme);
const isGGUF = modelcard.tags?.includes('gguf');
console.log('modelData++++++++++++', isGGUF);
setIsGGUF(isGGUF);
setIsGGUFModel(isGGUF);
} catch (error) {
setModelData(null);
setReadmeText(null);
setIsGGUF(false);
setIsGGUFModel(false);
}
};
const loadModelscopeModelConfig = useCallback(async (name: string) => {
try {
loadConfigJsonTokenRef.current?.abort?.();
loadConfigJsonTokenRef.current = new AbortController();
return await downloadModelScopeModelfile(
{
name: name
},
{
signal: loadConfigJsonTokenRef.current.token
}
);
} catch (error) {
return null;
}
}, []);
const getModelScopeModelDetail = async () => {
try {
const data = await queryModelScopeModelDetail(
{
name: props.selectedModel.name
},
{
token: requestToken.current.token
}
);
setModelData({
...data?.Data,
name: `${data.Data?.Path}/${data.Data?.Name}`
});
setReadmeText(data?.Data?.ReadMeContent);
const isGGUF = some(
data?.Data?.Tags,
(tag: string) => tag?.indexOf('gguf') > -1
);
setIsGGUF(isGGUF);
setIsGGUFModel(isGGUF);
} catch (error) {
setModelData(null);
setReadmeText(null);
setIsGGUF(false);
setIsGGUFModel(false);
}
};
const getModelCardData = async () => {
if (!props.selectedModel.name) {
setModelData(null);
setReadmeText(null);
return;
}
requestToken.current?.cancel?.();
requestToken.current = requestSource();
setLoading(true);
if (modelSource === modelSourceMap.huggingface_value) {
await getHuggingfaceModelDetail();
} else if (modelSource === modelSourceMap.modelscope_value) {
await getModelScopeModelDetail();
}
setLoading(false);
};
const handleCollapse = useCallback(() => {
onCollapse(!collapsed);
}, [collapsed]);
const generateModelLink = () => {
const name = modelData?.id || modelData?.name;
if (!name) {
return null;
}
if (modelSource === modelSourceMap.huggingface_value) {
return (
<Tooltip title={intl.formatMessage({ id: 'models.viewin.hf' })}>
<Button
size="small"
type="link"
target="_blank"
href={`https://huggingface.co/${modelData?.id}`}
>
<IconFont
type="icon-external-link"
className="font-size-14"
></IconFont>
</Button>
</Tooltip>
);
}
if (modelSource === modelSourceMap.modelscope_value) {
return (
<Tooltip title={intl.formatMessage({ id: 'models.viewin.modelscope' })}>
<Button
size="small"
type="link"
target="_blank"
href={`https://modelscope.cn/models/${modelData?.name}`}
>
<IconFont
type="icon-external-link"
className="font-size-14"
></IconFont>
</Button>
</Tooltip>
);
}
return null;
};
const generateModeScopeImgLink = useCallback(
(imgSrc: string) => {
if (!imgSrc) {
return '';
}
if (modelSource === modelSourceMap.modelscope_value) {
return `https://modelscope.cn/api/v1/models/${modelData?.name}/repo?Revision=${modelData?.Revision}&View=true&FilePath=${imgSrc}`;
}
if (modelSource === modelSourceMap.huggingface_value) {
return `https://huggingface.co/${modelData?.id}/resolve/main/${imgSrc}`;
}
return '';
},
[modelData, modelSource]
);
useEffect(() => {
getModelCardData();
}, [props.selectedModel.name]);
useEffect(() => {
if (!readmeText) {
onCollapse(false);
}
}, [readmeText]);
useEffect(() => {
return () => {
requestToken.current?.cancel?.();
axiosTokenRef.current?.abort?.();
loadConfigTokenRef.current?.abort?.();
loadConfigJsonTokenRef.current?.abort?.();
};
}, []);
return (
<>
<TitleWrapper>
<div className="title">{modelData?.id || modelData?.name} </div>
{generateModelLink()}
</TitleWrapper>
<div className="card-wrapper">
{modelData ? (
<div className="model-card-wrap">
<div className="flex-center">
{modelType && (
<Tag className="tag-item" color="gold">
<span style={{ opacity: 0.65 }}>
<span className="m-r-5">
{intl.formatMessage({ id: 'models.architecture' })}:
</span>
{modelType}
</span>
</Tag>
)}
{isGGUFModel && (
<Tag className="tag-item" color="magenta">
<span style={{ opacity: 0.65 }}>GGUF</span>
</Tag>
)}
{!!modelTags.length &&
modelTags.map((tag: string, index: number) => {
return (
<Tag className="tag-item" color="geekblue" key={index}>
<span style={{ opacity: 0.65 }}>{tag}</span>
</Tag>
);
})}
</div>
{readmeText && isGGUFModel && (
<div
style={{
borderRadius: 4,
marginTop: 16,
overflow: 'hidden'
}}
>
<span className="mkd-title" onClick={handleCollapse}>
<span>
<FileTextOutlined className="m-r-2 text-tertiary" />{' '}
README.md
</span>
<span>
{collapsed ? <DownOutlined /> : <RightOutlined />}
</span>
</span>
<SimpleBar
style={{
paddingTop: collapsed ? 12 : 0,
maxHeight: collapsed ? 300 : 0
}}
>
<MarkdownViewer
generateImgLink={generateModeScopeImgLink}
content={readmeText}
theme="light"
></MarkdownViewer>
</SimpleBar>
</div>
)}
</div>
) : (
<>
{!loading && (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
style={{ marginBlock: 20 }}
></Empty>
)}
</>
)}
</div>
{!isGGUFModel && (
<Spin spinning={loading}>
<div style={{ minHeight: 200 }}>
{readmeText && (
<>
<TitleWrapper>
<div className="title">README.md</div>
</TitleWrapper>
<div className="card-wrapper">
<MarkdownViewer
generateImgLink={generateModeScopeImgLink}
content={readmeText}
theme="light"
></MarkdownViewer>
</div>
</>
)}
</div>
</Spin>
)}
</>
);
};
export default React.memo(ModelCard);