chore: add cancel query model

This commit is contained in:
jialin
2024-08-21 18:09:13 +08:00
parent ba1670e56d
commit abb87e71ed
23 changed files with 523 additions and 179 deletions
+11 -10
View File
@@ -1,5 +1,6 @@
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import 'highlight.js/styles/atom-one-dark.css'; import 'highlight.js/styles/atom-one-dark.css';
import { memo, useMemo } from 'react';
import CopyButton from '../copy-button'; import CopyButton from '../copy-button';
import { escapeHtml } from './utils'; import { escapeHtml } from './utils';
@@ -19,7 +20,7 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
copyable = true copyable = true
} = props || {}; } = props || {};
const renderCode = () => { const highlightedCode = useMemo(() => {
const autodetectLang = autodetect && !lang; const autodetectLang = autodetect && !lang;
const cannotDetectLanguage = !autodetectLang && !hljs.getLanguage(lang); const cannotDetectLanguage = !autodetectLang && !hljs.getLanguage(lang);
let className = ''; let className = '';
@@ -52,9 +53,7 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
value: result.value, value: result.value,
className: className className: className
}; };
}; }, [code, lang, autodetect, ignoreIllegals]);
const highlightedCode = renderCode();
return ( return (
<pre className="code-pre"> <pre className="code-pre">
@@ -64,13 +63,15 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
__html: highlightedCode.value __html: highlightedCode.value
}} }}
></code> ></code>
<CopyButton {copyable && (
text={code} <CopyButton
size="small" text={code}
style={{ color: '#abb2bf' }} size="small"
></CopyButton> style={{ color: '#abb2bf' }}
></CopyButton>
)}
</pre> </pre>
); );
}; };
export default CodeViewer; export default memo(CodeViewer);
+3 -2
View File
@@ -4,12 +4,13 @@ import './style.less';
const HighlightCode: React.FC<{ const HighlightCode: React.FC<{
code: string; code: string;
lang?: string; lang?: string;
copyable?: boolean;
}> = (props) => { }> = (props) => {
const { code, lang = 'bash' } = props; const { code, lang = 'bash', copyable = true } = props;
return ( return (
<div className="high-light-wrapper"> <div className="high-light-wrapper">
<CodeViewer lang={lang} code={code} /> <CodeViewer lang={lang} code={code} copyable={copyable} />
</div> </div>
); );
}; };
+8
View File
@@ -16,5 +16,13 @@
top: 0; top: 0;
border-radius: 0 8px 0 4px; border-radius: 0 8px 0 4px;
} }
.simplebar-scrollbar::before {
background: var(--color-scroll-bg);
}
.simplebar-content {
width: max-content;
}
} }
} }
+10
View File
@@ -11,6 +11,16 @@
font-size: var(--font-size-base); font-size: var(--font-size-base);
overflow: hidden; overflow: hidden;
.txt {
display: flex;
align-items: center;
height: 20px;
&.err {
cursor: default;
}
}
&.download { &.download {
border: 1px solid #93ecc5 !important; border: 1px solid #93ecc5 !important;
} }
+18 -6
View File
@@ -4,6 +4,8 @@ import { InfoCircleOutlined } from '@ant-design/icons';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css';
import CopyButton from '../copy-button'; import CopyButton from '../copy-button';
import CopyStyle from './copy.less'; import CopyStyle from './copy.less';
import './index.less'; import './index.less';
@@ -70,7 +72,14 @@ const StatusTag: React.FC<StatusTagProps> = ({
size="small" size="small"
></CopyButton> ></CopyButton>
</div> </div>
<div>{statusValue.message}</div>
<SimpleBar style={{ maxHeight: 200 }}>
<div
style={{ width: 'max-content', maxWidth: 250, paddingInline: 10 }}
>
{statusValue.message}
</div>
</SimpleBar>
</div> </div>
); );
}, [statusValue]); }, [statusValue]);
@@ -87,15 +96,18 @@ const StatusTag: React.FC<StatusTagProps> = ({
{statusValue.message ? ( {statusValue.message ? (
<Tooltip <Tooltip
title={renderTitle} title={renderTitle}
overlayInnerStyle={{ maxHeight: 200, overflow: 'auto' }} destroyTooltipOnHide={true}
overlayInnerStyle={{ paddingInline: 0 }}
> >
<span className="m-r-5"> <span className="txt err">
<InfoCircleOutlined /> <span className="m-r-5">
<InfoCircleOutlined />
</span>
{renderContent()}
</span> </span>
{renderContent()}
</Tooltip> </Tooltip>
) : ( ) : (
renderContent() <span className="txt">{renderContent()}</span>
)} )}
</span> </span>
); );
+3 -1
View File
@@ -11,5 +11,7 @@ export default {
EDIT: ['ctrl+e', 'meta+e'], EDIT: ['ctrl+e', 'meta+e'],
SEARCH: ['ctrl+f', 'meta+f'], SEARCH: ['ctrl+f', 'meta+f'],
RESET: ['ctrl+shift+r', 'meta+shift+r'], RESET: ['ctrl+shift+r', 'meta+shift+r'],
INPUT: ['ctrl+k', 'meta+k'] INPUT: ['ctrl+k', 'meta+k'],
NEW1: ['ctrl+1', 'meta+1'],
NEW2: ['ctrl+2', 'meta+2']
}; };
+1
View File
@@ -12,6 +12,7 @@ html {
--color-fill-1: var(--ant-color-fill-tertiary); --color-fill-1: var(--ant-color-fill-tertiary);
// --color-fill-1: #fff; // --color-fill-1: #fff;
--ant-color-text: #000; --ant-color-text: #000;
--color-scroll-bg: #d9d9d9;
--color-fill-2: #fff; --color-fill-2: #fff;
--color-fill-3: #f3f6fa; --color-fill-3: #f3f6fa;
--color-logs-bg: #1e1e1e; --color-logs-bg: #1e1e1e;
+1 -2
View File
@@ -1,6 +1,5 @@
// @ts-nocheck // @ts-nocheck
// This file is generated by Umi automatically
// DO NOT CHANGE IT MANUALLY!
import { history, useIntl, type IRoute } from '@umijs/max'; import { history, useIntl, type IRoute } from '@umijs/max';
import { Button, Result } from 'antd'; import { Button, Result } from 'antd';
import React from 'react'; import React from 'react';
+11 -1
View File
@@ -18,5 +18,15 @@ export default {
'model.deploy.sort': 'Sort', 'model.deploy.sort': 'Sort',
'model.deploy.search.placeholder': 'Search models from Hugging Face', 'model.deploy.search.placeholder': 'Search models from Hugging Face',
'model.form.ollamatips': 'model.form.ollamatips':
'Tip: The following are the preconfigured Ollama models in GPUStack. Please select the model you want, or directly enter the model you wish to deploy in the 【{name}】 input box on the right.' 'Tip: The following are the preconfigured Ollama models in GPUStack. Please select the model you want, or directly enter the model you wish to deploy in the 【{name}】 input box on the right.',
'models.sort.name': 'Name',
'models.sort.size': 'Size',
'models.sort.likes': 'Likes',
'models.sort.downloads': 'Downloads',
'models.sort.updated': 'Updated',
'models.search.result': '{count} results',
'models.data.card': 'Model Card',
'models.available.files': 'Available Files',
'models.viewin.hf': 'View in Hugging Face',
'models.architecture': 'Architecture'
}; };
+11 -1
View File
@@ -18,5 +18,15 @@ export default {
'model.deploy.sort': '排序', 'model.deploy.sort': '排序',
'model.deploy.search.placeholder': '从 Hugging Face 搜索模型', 'model.deploy.search.placeholder': '从 Hugging Face 搜索模型',
'model.form.ollamatips': 'model.form.ollamatips':
'提示:以下为 GPUStack 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。' '提示:以下为 GPUStack 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。',
'models.sort.name': '名称',
'models.sort.size': '大小',
'models.sort.likes': '喜欢',
'models.sort.downloads': '下载',
'models.sort.updated': '更新时间',
'models.search.result': '{count} 个结果',
'models.data.card': '模型简介',
'models.available.files': '可用文件',
'models.viewin.hf': '在 Hugging Face 中查看',
'models.architecture': '架构'
}; };
+47 -6
View File
@@ -1,4 +1,4 @@
import { listFiles, listModels } from '@huggingface/hub'; import { downloadFile, listFiles, listModels } from '@huggingface/hub';
import { PipelineType } from '@huggingface/tasks'; import { PipelineType } from '@huggingface/tasks';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { import {
@@ -119,9 +119,13 @@ export async function callHuggingfaceQuickSearch(params: any) {
const HUGGINGFACE_API = 'https://huggingface.co/api/models'; const HUGGINGFACE_API = 'https://huggingface.co/api/models';
export async function queryHuggingfaceModelDetail(params: { repo: string }) { export async function queryHuggingfaceModelDetail(
params: { repo: string },
options?: any
) {
return request(`${HUGGINGFACE_API}/${params.repo}`, { return request(`${HUGGINGFACE_API}/${params.repo}`, {
method: 'GET' method: 'GET',
cancelToken: options?.token
}); });
} }
@@ -139,7 +143,7 @@ export async function queryHuggingfaceModels(
for await (const model of listModels({ for await (const model of listModels({
...params, ...params,
...options, ...options,
limit: 100, limit: 500,
additionalFields: ['sha'], additionalFields: ['sha'],
fetch(url: string, config: any) { fetch(url: string, config: any) {
try { try {
@@ -148,6 +152,7 @@ export async function queryHuggingfaceModels(
signal: options.signal signal: options.signal
}); });
} catch (error) { } catch (error) {
console.log('queryHuggingfaceModels error===', error);
// ignore // ignore
return []; return [];
} }
@@ -158,12 +163,48 @@ export async function queryHuggingfaceModels(
return result; return result;
} }
export async function queryHuggingfaceModelFiles(params: { repo: string }) { export async function queryHuggingfaceModelFiles(
params: { repo: string },
options?: any
) {
const result = []; const result = [];
for await (const fileInfo of listFiles({ for await (const fileInfo of listFiles({
...params ...params,
fetch(url: string, config: any) {
try {
return fetch(url, {
...config,
signal: options?.signal
});
} catch (error) {
console.log('queryHuggingfaceModels error===', error);
// ignore
return [];
}
}
})) { })) {
result.push(fileInfo); result.push(fileInfo);
} }
return result; return result;
} }
export async function downloadModelFile(
params: { repo: string; revision: string; path: string },
options?: any
) {
const { repo, revision, path } = params;
const res = await (
await downloadFile({
repo,
revision: revision,
path: path,
fetch(url: string, config: any) {
return fetch(url, {
...config,
signal: options?.signal
});
}
})
)?.text();
return res;
}
@@ -3,13 +3,16 @@ import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css'; import 'simplebar-react/dist/simplebar.min.css';
import '../style/column-wrapper.less'; import '../style/column-wrapper.less';
const ColumnWrapper: React.FC<any> = ({ children, footer }) => { const ColumnWrapper: React.FC<any> = ({ children, footer, height }) => {
if (footer) { if (footer) {
return ( return (
<div className="column-wrapper-footer"> <div className="column-wrapper-footer">
<div className="column-wrapper"> <div className="column-wrapper">
<SimpleBar <SimpleBar
style={{ height: 'calc(100vh - 89px)', paddingBottom: '50px' }} style={{
height: height || 'calc(100vh - 89px)',
paddingBottom: '50px'
}}
> >
{children} {children}
</SimpleBar> </SimpleBar>
@@ -20,7 +23,9 @@ const ColumnWrapper: React.FC<any> = ({ children, footer }) => {
} }
return ( return (
<div className="column-wrapper"> <div className="column-wrapper">
<SimpleBar style={{ height: 'calc(100vh - 89px)' }}>{children}</SimpleBar> <SimpleBar style={{ height: height || 'calc(100vh - 89px)' }}>
{children}
</SimpleBar>
</div> </div>
); );
}; };
+17 -6
View File
@@ -3,7 +3,7 @@ import { PageActionType } from '@/config/types';
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Drawer } from 'antd'; import { Button, Drawer } from 'antd';
import { useCallback, useEffect, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { modelSourceMap } from '../config'; import { modelSourceMap } from '../config';
import { FormData, ListItem } from '../config/types'; import { FormData, ListItem } from '../config/types';
import ColumnWrapper from './column-wrapper'; import ColumnWrapper from './column-wrapper';
@@ -38,6 +38,8 @@ const AddModal: React.FC<AddModalProps> = (props) => {
const form = useRef<any>({}); const form = useRef<any>({});
const intl = useIntl(); const intl = useIntl();
const [huggingfaceRepoId, setHuggingfaceRepoId] = useState<string>(''); const [huggingfaceRepoId, setHuggingfaceRepoId] = useState<string>('');
const [collapsed, setCollapsed] = useState<boolean>(false);
const [loadingModel, setLoadingModel] = useState<boolean>(false);
const handleSelectModelFile = useCallback((item: any) => { const handleSelectModelFile = useCallback((item: any) => {
form.current?.setFieldValue?.('huggingface_filename', item.path); form.current?.setFieldValue?.('huggingface_filename', item.path);
@@ -100,15 +102,22 @@ const AddModal: React.FC<AddModalProps> = (props) => {
<SearchModel <SearchModel
modelSource={props.source} modelSource={props.source}
onSelectModel={handleOnSelectModel} onSelectModel={handleOnSelectModel}
setLoadingModel={setLoadingModel}
></SearchModel> ></SearchModel>
</ColumnWrapper> </ColumnWrapper>
)} )}
{props.source === modelSourceMap.huggingface_value && ( {props.source === modelSourceMap.huggingface_value && (
<ColumnWrapper> <ColumnWrapper>
<ModelCard repo={huggingfaceRepoId}></ModelCard> <ModelCard
repo={huggingfaceRepoId}
onCollapse={setCollapsed}
collapsed={collapsed}
></ModelCard>
<HFModelFile <HFModelFile
repo={huggingfaceRepoId} repo={huggingfaceRepoId}
onSelectFile={handleSelectModelFile} onSelectFile={handleSelectModelFile}
collapsed={collapsed}
loadingModel={loadingModel}
></HFModelFile> ></HFModelFile>
</ColumnWrapper> </ColumnWrapper>
)} )}
@@ -126,9 +135,11 @@ const AddModal: React.FC<AddModalProps> = (props) => {
} }
> >
<> <>
<TitleWrapper> {source === modelSourceMap.huggingface_value && (
{intl.formatMessage({ id: 'models.form.configurations' })} <TitleWrapper>
</TitleWrapper> {intl.formatMessage({ id: 'models.form.configurations' })}
</TitleWrapper>
)}
<DataForm <DataForm
source={source} source={source}
action={action} action={action}
@@ -143,4 +154,4 @@ const AddModal: React.FC<AddModalProps> = (props) => {
); );
}; };
export default AddModal; export default memo(AddModal);
+121 -70
View File
@@ -1,10 +1,12 @@
import { convertFileSize } from '@/utils'; import { convertFileSize } from '@/utils';
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Col, Empty, Row, Space, Spin, Tag } from 'antd'; import { Col, Empty, Row, Select, Space, Spin, Tag } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css';
import { queryHuggingfaceModelFiles } from '../apis'; import { queryHuggingfaceModelFiles } from '../apis';
import FileType from '../config/file-type'; import FileType from '../config/file-type';
import '../style/hf-model-file.less'; import '../style/hf-model-file.less';
@@ -12,16 +14,31 @@ import TitleWrapper from './title-wrapper';
interface HFModelFileProps { interface HFModelFileProps {
repo: string; repo: string;
collapsed?: boolean;
loadingModel?: boolean;
onSelectFile?: (file: any) => void; onSelectFile?: (file: any) => void;
} }
const HFModelFile: React.FC<HFModelFileProps> = (props) => { const HFModelFile: React.FC<HFModelFileProps> = (props) => {
const { collapsed, loadingModel } = props;
const intl = useIntl(); const intl = useIntl();
const [dataSource, setDataSource] = useState<any>({ const [dataSource, setDataSource] = useState<any>({
fileList: [], fileList: [],
loading: false loading: false
}); });
const [sortType, setSortType] = useState<string>('size');
const [current, setCurrent] = useState<string>(''); const [current, setCurrent] = useState<string>('');
const modelFilesSortOptions = useRef<any[]>([
{
label: intl.formatMessage({ id: 'models.sort.size' }),
value: 'size'
},
{
label: intl.formatMessage({ id: 'models.sort.name' }),
value: 'name'
}
]);
const axiosTokenRef = useRef<any>(null);
const handleSelectModelFile = (item: any) => { const handleSelectModelFile = (item: any) => {
props.onSelectFile?.(item); props.onSelectFile?.(item);
@@ -34,14 +51,23 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
handleSelectModelFile({}); handleSelectModelFile({});
return; return;
} }
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
setDataSource({ ...dataSource, loading: true }); setDataSource({ ...dataSource, loading: true });
setCurrent(''); setCurrent('');
try { try {
const res = await queryHuggingfaceModelFiles({ repo: props.repo }); const res = await queryHuggingfaceModelFiles(
{ repo: props.repo },
{
signal: axiosTokenRef.current.signal
}
);
const list = _.filter(res, (file: any) => { const list = _.filter(res, (file: any) => {
return _.endsWith(file.path, '.gguf') || _.includes(file.path, '.gguf'); return _.endsWith(file.path, '.gguf') || _.includes(file.path, '.gguf');
}); });
const sortList = _.sortBy(list, (item: any) => item.size); const sortList = _.sortBy(list, (item: any) => {
return sortType === 'size' ? item.size : item.path;
});
setDataSource({ fileList: sortList, loading: false }); setDataSource({ fileList: sortList, loading: false });
handleSelectModelFile(list[0]); handleSelectModelFile(list[0]);
} catch (error) { } catch (error) {
@@ -50,6 +76,14 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
} }
}; };
const handleSortChange = (value: string) => {
const list = _.sortBy(dataSource.fileList, (item: any) => {
return value === 'size' ? item.size : item.path;
});
setSortType(value);
setDataSource({ ...dataSource, fileList: list });
};
const getModelQuantizationType = (item: any) => { const getModelQuantizationType = (item: any) => {
const name = _.split(item.path, '.').slice(0, -1).join('.'); const name = _.split(item.path, '.').slice(0, -1).join('.');
let quanType = _.toUpper(name.split('-').slice(-1)[0]); let quanType = _.toUpper(name.split('-').slice(-1)[0]);
@@ -77,76 +111,93 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
useEffect(() => { useEffect(() => {
handleFetchModelFiles(); handleFetchModelFiles();
}, [props.repo]); }, [props.repo]);
useEffect(() => {
return () => {
axiosTokenRef.current?.abort?.();
};
}, []);
return ( return (
<div> <div>
<TitleWrapper> <TitleWrapper>
<span>Available Files ({dataSource.fileList.length || 0})</span> <span>
{intl.formatMessage({ id: 'models.available.files' })} (
{dataSource.fileList.length || 0})
</span>
<Select
value={sortType}
onChange={handleSortChange}
labelRender={({ label }) => {
return (
<span>
{intl.formatMessage({ id: 'model.deploy.sort' })}: {label}
</span>
);
}}
options={modelFilesSortOptions.current}
size="middle"
style={{ width: '120px' }}
></Select>
</TitleWrapper> </TitleWrapper>
<div style={{ padding: '16px 24px' }}> <SimpleBar
<Spin spinning={dataSource.loading} style={{ minHeight: 100 }}> style={{ maxHeight: collapsed ? 'max-content' : 'calc(100vh - 330px)' }}
{dataSource.fileList.length ? ( >
<Row gutter={[16, 24]}> <div style={{ padding: '16px 24px' }}>
{_.map(dataSource.fileList, (item: any) => { <Spin
return ( spinning={dataSource.loading || loadingModel}
<Col span={24} key={item.path}> style={{ minHeight: 100 }}
<div >
className={classNames('hf-model-file', { {dataSource.fileList.length ? (
active: item.path === current <Row gutter={[16, 24]}>
})} {_.map(dataSource.fileList, (item: any) => {
tabIndex={0} return (
onClick={() => handleSelectModelFile(item)} <Col span={24} key={item.path}>
onKeyDown={(e) => handleOnEnter(e, item)} <div
> className={classNames('hf-model-file', {
<div className="title">{item.path}</div> active: item.path === current
<Space className="tags"> })}
{/* <span className="tag-item"> tabIndex={0}
{convertFileSize(item.size)} onClick={() => handleSelectModelFile(item)}
</span> */} onKeyDown={(e) => handleOnEnter(e, item)}
<Tag >
className="tag-item" <div className="title">{item.path}</div>
color="green" <Space className="tags">
style={{ <Tag
marginRight: 0 className="tag-item"
}} color="green"
> style={{
<span style={{ opacity: 0.65 }}> marginRight: 0
{convertFileSize(item.size)} }}
</span> >
</Tag> <span style={{ opacity: 0.65 }}>
{getModelQuantizationType(item)} {convertFileSize(item.size)}
</Space> </span>
<div className="btn"> </Tag>
{/* <Button size="middle"> {getModelQuantizationType(item)}
{item.path === current </Space>
? intl.formatMessage({ <div className="btn"></div>
id: 'common.button.selected'
})
: intl.formatMessage({
id: 'common.button.select'
})}
</Button> */}
</div> </div>
</div> </Col>
</Col> );
); })}
})} </Row>
</Row> ) : (
) : ( !dataSource.loading && (
!dataSource.loading && ( <Empty
<Empty imageStyle={{ height: 'auto', marginTop: '20px' }}
imageStyle={{ height: 'auto', marginTop: '20px' }} image={
image={ <SearchOutlined
<SearchOutlined className="font-size-16"
className="font-size-16" style={{ color: 'var(--ant-color-text-tertiary)' }}
style={{ color: 'var(--ant-color-text-tertiary)' }} ></SearchOutlined>
></SearchOutlined> }
} description="No files found"
description="No files found" />
/> )
) )}
)} </Spin>
</Spin> </div>
</div> </SimpleBar>
</div> </div>
); );
}; };
@@ -40,7 +40,7 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
<div className="info"> <div className="info">
{props.source === modelSourceMap.huggingface_value ? ( {props.source === modelSourceMap.huggingface_value ? (
<Space size={16}> <Space size={16}>
{props.task && ( {/* {props.task && (
<Tag <Tag
className="tag-item" className="tag-item"
color="gold" color="gold"
@@ -50,7 +50,7 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
> >
<span style={{ opacity: 0.65 }}>{props.task}</span> <span style={{ opacity: 0.65 }}>{props.task}</span>
</Tag> </Tag>
)} )} */}
<span> <span>
{dayjs().to( {dayjs().to(
dayjs(dayjs(props.updatedAt).format('YYYY-MM-DD HH:mm:ss')) dayjs(dayjs(props.updatedAt).format('YYYY-MM-DD HH:mm:ss'))
+126 -27
View File
@@ -1,20 +1,51 @@
import HighlightCode from '@/components/highlight-code';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import { downloadFile } from '@huggingface/hub'; import useRequestToken from '@/hooks/use-request-token';
import { Button, Empty, Tag } from 'antd'; import {
import React, { useEffect, useState } from 'react'; DownOutlined,
import { queryHuggingfaceModelDetail } from '../apis'; FileTextOutlined,
RightOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Empty, Tag, Tooltip } from 'antd';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css';
import { downloadModelFile, queryHuggingfaceModelDetail } from '../apis';
import '../style/model-card.less'; import '../style/model-card.less';
import TitleWrapper from './title-wrapper'; import TitleWrapper from './title-wrapper';
const ModelCard: React.FC<{ repo: string }> = (props) => { const ModelCard: React.FC<{
const { repo } = props; repo: string;
onCollapse: (flag: boolean) => void;
collapsed: boolean;
}> = (props) => {
const { repo, onCollapse, collapsed } = props;
const intl = useIntl();
const requestSource = useRequestToken();
const [modelData, setModelData] = useState<any>({}); const [modelData, setModelData] = useState<any>({});
const [readmeText, setReadmeText] = useState<string | null>(null);
const requestToken = useRef<any>(null);
const axiosTokenRef = useRef<any>(null);
const loadFile = async (repo: string, sha: string) => { const loadFile = async (repo: string, sha: string) => {
const res = await ( try {
await downloadFile({ repo, revision: sha, path: 'README.md' }) axiosTokenRef.current?.abort?.();
)?.text(); axiosTokenRef.current = new AbortController();
return res; const res = await downloadModelFile(
{
repo,
revision: sha,
path: 'README.md'
},
{
signal: axiosTokenRef.current.signal
}
);
return res || '';
} catch (error) {
return '';
}
}; };
const getModelCardData = async () => { const getModelCardData = async () => {
@@ -22,42 +53,110 @@ const ModelCard: React.FC<{ repo: string }> = (props) => {
setModelData(null); setModelData(null);
return; return;
} }
requestToken.current?.cancel?.();
requestToken.current = requestSource();
try { try {
const res = await queryHuggingfaceModelDetail({ repo }); const [modelcard, readme] = await Promise.all([
queryHuggingfaceModelDetail(
{ repo },
{
token: requestToken.current.token
}
),
loadFile(repo, 'main')
]);
setModelData(res); setModelData(modelcard);
setReadmeText(readme);
} catch (error) { } catch (error) {
setModelData({}); setModelData({});
} }
}; };
const handleCollapse = useCallback(() => {
onCollapse(!collapsed);
}, [collapsed]);
useEffect(() => { useEffect(() => {
getModelCardData(); getModelCardData();
}, [repo]); }, [repo]);
useEffect(() => {
if (!readmeText) {
onCollapse(false);
}
}, [readmeText]);
useEffect(() => {
return () => {
requestToken.current?.cancel?.();
axiosTokenRef.current?.abort?.();
};
}, []);
return ( return (
<> <>
<TitleWrapper> <TitleWrapper>
<span>Model Card</span> <span>{intl.formatMessage({ id: 'models.data.card' })}</span>
</TitleWrapper> </TitleWrapper>
<div className="wrapper"> <div className="wrapper">
{modelData ? ( {modelData ? (
<div className="model-card-wrap"> <div className="model-card-wrap">
<div className="title">{modelData.id}</div> <div className="title">
<div className="flex-between flex-center"> {modelData.id}{' '}
<Tag className="tag-item"> <Tooltip title={intl.formatMessage({ id: 'models.viewin.hf' })}>
<span className="m-r-5">Architecture:</span> <Button
{modelData.config?.model_type} size="small"
</Tag> type="link"
<Button target="_blank"
type="link" href={`https://huggingface.co/${modelData.id}`}
target="_blank" >
href={`https://huggingface.co/${modelData.id}`} <IconFont type="icon-external-link"></IconFont>
> </Button>
View in Hugging Face </Tooltip>
<IconFont type="icon-external-link"></IconFont>
</Button>
</div> </div>
<div className="flex-between flex-center">
{modelData.config?.model_type && (
<Tag className="tag-item" color="gold">
<span style={{ opacity: 0.65 }}>
<span className="m-r-5">
{intl.formatMessage({ id: 'models.architecture' })}:
</span>
{modelData.config?.model_type}
</span>
</Tag>
)}
</div>
{readmeText && (
<div
style={{
borderRadius: 4,
backgroundColor: '#282c34',
marginTop: 16,
overflow: 'hidden'
}}
>
<span className="mkd-title" onClick={handleCollapse}>
<span>
<FileTextOutlined className="m-r-5" /> README.md
</span>
<span>
{collapsed ? <DownOutlined /> : <RightOutlined />}
</span>
</span>
<SimpleBar
style={{
maxHeight: collapsed ? 300 : 0
}}
>
<HighlightCode
code={readmeText}
lang="markdown"
copyable={false}
></HighlightCode>
</SimpleBar>
</div>
)}
</div> </div>
) : ( ) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty> <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty>
+29 -11
View File
@@ -5,17 +5,14 @@ import { Button, Input, Select } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { queryHuggingfaceModels } from '../apis'; import { queryHuggingfaceModels } from '../apis';
import { import { modelSourceMap, ollamaModelOptions } from '../config';
modelFilesSortOptions,
modelSourceMap,
ollamaModelOptions
} from '../config';
import SearchStyle from '../style/search-result.less'; import SearchStyle from '../style/search-result.less';
import SearchInput from './search-input'; import SearchInput from './search-input';
import SearchResult from './search-result'; import SearchResult from './search-result';
interface SearchInputProps { interface SearchInputProps {
modelSource: string; modelSource: string;
setLoadingModel?: (flag: boolean) => void;
onSourceChange?: (source: string) => void; onSourceChange?: (source: string) => void;
onSelectModel: (model: any) => void; onSelectModel: (model: any) => void;
} }
@@ -38,7 +35,7 @@ const sourceList = [
const SearchModel: React.FC<SearchInputProps> = (props) => { const SearchModel: React.FC<SearchInputProps> = (props) => {
console.log('SearchModel======'); console.log('SearchModel======');
const intl = useIntl(); const intl = useIntl();
const { modelSource, onSourceChange, onSelectModel } = props; const { modelSource, setLoadingModel, onSourceChange, onSelectModel } = props;
const [dataSource, setDataSource] = useState<{ const [dataSource, setDataSource] = useState<{
repoOptions: any[]; repoOptions: any[];
loading: boolean; loading: boolean;
@@ -51,6 +48,17 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
const cacheRepoOptions = useRef<any[]>([]); const cacheRepoOptions = useRef<any[]>([]);
const axiosTokenRef = useRef<any>(null); const axiosTokenRef = useRef<any>(null);
const customOllamaModelRef = useRef<any>(null); const customOllamaModelRef = useRef<any>(null);
const modelFilesSortOptions = useRef<any[]>([
{ label: intl.formatMessage({ id: 'models.sort.likes' }), value: 'likes' },
{
label: intl.formatMessage({ id: 'models.sort.downloads' }),
value: 'downloads'
},
{
label: intl.formatMessage({ id: 'models.sort.updated' }),
value: 'updatedAt'
}
]);
const handleOnSelectModel = useCallback((item: any) => { const handleOnSelectModel = useCallback((item: any) => {
onSelectModel(item); onSelectModel(item);
@@ -67,6 +75,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
pre.loading = true; pre.loading = true;
return { ...pre }; return { ...pre };
}); });
setLoadingModel?.(true);
cacheRepoOptions.current = []; cacheRepoOptions.current = [];
const params = { const params = {
search: { search: {
@@ -93,12 +102,15 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
repoOptions: sortedList, repoOptions: sortedList,
loading: false loading: false
}); });
setLoadingModel?.(false);
handleOnSelectModel(sortedList[0]); handleOnSelectModel(sortedList[0]);
} catch (error) { } catch (error) {
console.log('queryHuggingfaceModels error===', error);
setDataSource({ setDataSource({
repoOptions: [], repoOptions: [],
loading: false loading: false
}); });
setLoadingModel?.(false);
handleOnSelectModel({}); handleOnSelectModel({});
cacheRepoOptions.current = []; cacheRepoOptions.current = [];
} }
@@ -184,15 +196,18 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
loading: false loading: false
}); });
}; };
const renderHFSearch = () => { const renderHFSearch = () => {
return ( return (
<> <>
<SearchInput onSearch={handlerSearchModels}></SearchInput> <SearchInput onSearch={handlerSearchModels}></SearchInput>
<div className={SearchStyle.filter}> <div className={SearchStyle.filter}>
<span> <span>
<span className="value">{dataSource.repoOptions.length}</span> <span className="value">
results {intl.formatMessage(
{ id: 'models.search.result' },
{ count: dataSource.repoOptions.length }
)}
</span>
</span> </span>
<Select <Select
value={sortType} value={sortType}
@@ -204,7 +219,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
</span> </span>
); );
}} }}
options={modelFilesSortOptions} options={modelFilesSortOptions.current}
size="middle" size="middle"
style={{ width: '150px' }} style={{ width: '150px' }}
></Select> ></Select>
@@ -233,10 +248,13 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
useEffect(() => { useEffect(() => {
handleOnOpen(); handleOnOpen();
console.log('SearchModel useEffect', modelSource); console.log('SearchModel useEffect', modelSource);
}, [modelSource]);
useEffect(() => {
return () => { return () => {
axiosTokenRef.current?.abort?.(); axiosTokenRef.current?.abort?.();
}; };
}, [modelSource]); }, []);
return ( return (
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
+56 -4
View File
@@ -5,11 +5,12 @@ import PageTools from '@/components/page-tools';
import SealTable from '@/components/seal-table'; import SealTable from '@/components/seal-table';
import SealColumn from '@/components/seal-table/components/seal-column'; import SealColumn from '@/components/seal-table/components/seal-column';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import HotKeys from '@/config/hotkeys';
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys'; import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
import useTableRowSelection from '@/hooks/use-table-row-selection'; import useTableRowSelection from '@/hooks/use-table-row-selection';
import useTableSort from '@/hooks/use-table-sort'; import useTableSort from '@/hooks/use-table-sort';
import ViewCodeModal from '@/pages/playground/components/view-code-modal'; import ViewCodeModal from '@/pages/playground/components/view-code-modal';
import { handleBatchRequest } from '@/utils'; import { handleBatchRequest, platformCall } from '@/utils';
import { import {
DeleteOutlined, DeleteOutlined,
DownOutlined, DownOutlined,
@@ -23,6 +24,7 @@ import { Button, Dropdown, Input, Space, Tag, message } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
import { memo, useCallback, useRef, useState } from 'react'; import { memo, useCallback, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { import {
MODELS_API, MODELS_API,
MODEL_INSTANCE_API, MODEL_INSTANCE_API,
@@ -64,6 +66,7 @@ const Models: React.FC<ModelsProps> = ({
total total
}) => { }) => {
console.log('model list====2'); console.log('model list====2');
const platform = platformCall();
const access = useAccess(); const access = useAccess();
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -77,7 +80,6 @@ const Models: React.FC<ModelsProps> = ({
params: {}, params: {},
show: false show: false
}); });
const [openViewCodeModal, setOpenViewCodeModal] = useState(false);
const [openLogModal, setOpenLogModal] = useState(false); const [openLogModal, setOpenLogModal] = useState(false);
const [openAddModal, setOpenAddModal] = useState(false); const [openAddModal, setOpenAddModal] = useState(false);
const [openDeployModal, setOpenDeployModal] = useState<any>({ const [openDeployModal, setOpenDeployModal] = useState<any>({
@@ -92,9 +94,46 @@ const Models: React.FC<ModelsProps> = ({
const [currentInstanceUrl, setCurrentInstanceUrl] = useState<string>(''); const [currentInstanceUrl, setCurrentInstanceUrl] = useState<string>('');
const modalRef = useRef<any>(null); const modalRef = useRef<any>(null);
useHotkeys(
HotKeys.NEW1.join(','),
() => {
setOpenDeployModal({
show: true,
width: 'calc(100vw - 220px)',
source: modelSourceMap.huggingface_value
});
},
{ preventDefault: true }
);
useHotkeys(
HotKeys.NEW2.join(','),
() => {
setOpenDeployModal({
show: true,
width: 600,
source: modelSourceMap.ollama_library_value
});
},
{ preventDefault: true }
);
const sourceOptions = [ const sourceOptions = [
{ {
label: 'Hugging Face', label: (
<span className="flex-center flex-between">
<span>Hugging Face</span>
<Tag style={{ marginRight: 0 }} className="m-l-10">
{platform.isMac ? (
<>
<IconFont type="icon-command"></IconFont> + 1
</>
) : (
<>CTRL + 1</>
)}
</Tag>
</span>
),
value: modelSourceMap.huggingface_value, value: modelSourceMap.huggingface_value,
key: 'huggingface', key: 'huggingface',
icon: <IconFont type="icon-huggingface"></IconFont>, icon: <IconFont type="icon-huggingface"></IconFont>,
@@ -107,7 +146,20 @@ const Models: React.FC<ModelsProps> = ({
} }
}, },
{ {
label: 'Ollama Library', label: (
<span className="flex-center flex-between">
<span>Ollama Library</span>
<Tag style={{ marginRight: 0 }} className="m-l-10">
{platform.isMac ? (
<>
<IconFont type="icon-command"></IconFont> + 2
</>
) : (
<>CTRL + 2</>
)}
</Tag>
</span>
),
value: modelSourceMap.ollama_library_value, value: modelSourceMap.ollama_library_value,
key: 'ollama_library', key: 'ollama_library',
icon: <IconFont type="icon-ollama"></IconFont>, icon: <IconFont type="icon-ollama"></IconFont>,
@@ -2,11 +2,7 @@ import React from 'react';
import '../style/title-wrapper.less'; import '../style/title-wrapper.less';
const TitleWrapper: React.FC<any> = ({ children }) => { const TitleWrapper: React.FC<any> = ({ children }) => {
return ( return <h3 className="h3">{children}</h3>;
<h3 className="h3">
<span>{children}</span>
</h3>
);
}; };
export default TitleWrapper; export default TitleWrapper;
+20 -20
View File
@@ -44,13 +44,13 @@ export const ollamaModelOptions = [
tags: ['7B'], tags: ['7B'],
id: 'mistral' id: 'mistral'
}, },
{ // {
label: 'llava', // label: 'llava',
value: 'llava', // value: 'llava',
name: 'llava', // name: 'llava',
tags: ['7B', '13B', '34B'], // tags: ['7B', '13B', '34B'],
id: 'llava' // id: 'llava'
}, // },
{ {
label: 'qwen2', label: 'qwen2',
value: 'qwen2', value: 'qwen2',
@@ -59,11 +59,11 @@ export const ollamaModelOptions = [
id: 'qwen2' id: 'qwen2'
}, },
{ {
label: 'phi3', label: 'phi3.5',
value: 'phi3', value: 'phi3.5',
name: 'phi3', name: 'phi3.5',
tags: ['3B', '14B'], tags: ['3B'],
id: 'phi3' id: 'phi3.5'
}, },
{ {
label: 'codellama', label: 'codellama',
@@ -73,11 +73,11 @@ export const ollamaModelOptions = [
id: 'codellama' id: 'codellama'
}, },
{ {
label: 'deepseek-coder', label: 'deepseek-coder-v2',
value: 'deepseek-coder', value: 'deepseek-coder-v2',
name: 'deepseek-coder', name: 'deepseek-coder-v2',
tags: ['1B', '7B', '33B'], tags: ['16B', '236B'],
id: 'deepseek-coder' id: 'deepseek-coder-v2'
} }
]; ];
@@ -143,7 +143,7 @@ export const ActionList = [
export const modelFilesSortOptions = [ export const modelFilesSortOptions = [
// { label: 'Trending', value: 'trendingScore' }, // { label: 'Trending', value: 'trendingScore' },
{ label: 'Likes', value: 'likes' }, { label: 'models.sort.likes', value: 'likes' },
{ label: 'Downloads', value: 'downloads' }, { label: 'models.sort.downloads', value: 'downloads' },
{ label: 'Updated', value: 'updatedAt' } { label: 'models.sort.updated', value: 'updatedAt' }
]; ];
+1
View File
@@ -69,6 +69,7 @@ const Models: React.FC = () => {
} else { } else {
setDataSource({ setDataSource({
...dataSource, ...dataSource,
loading: !!res.items.length,
total: res.pagination.total total: res.pagination.total
}); });
} }
+17 -2
View File
@@ -8,6 +8,9 @@
.title { .title {
margin-bottom: 10px; margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
} }
.tag-item { .tag-item {
@@ -18,12 +21,24 @@
border-radius: 4px; border-radius: 4px;
font-size: 12px; font-size: 12px;
height: 20px; height: 20px;
border: 1px solid var(--ant-color-border);
color: var(--ant-color-text-secondary);
} }
.btn { .btn {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
.mkd-title {
cursor: pointer;
color: rgba(255, 255, 255, 80%);
border-bottom: 1px solid rgba(255, 255, 255, 10%);
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
.simplebar-scrollbar::before {
background: var(--color-scroll-bg);
}
} }
@@ -6,6 +6,7 @@
z-index: 100; z-index: 100;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
font-size: 14px; font-size: 14px;
padding: @padding; padding: @padding;
padding-top: 0; padding-top: 0;