chore: drawer ux
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"@ant-design/pro-components": "^2.7.1",
|
"@ant-design/pro-components": "^2.7.1",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
|
"@huggingface/tasks": "^0.11.6",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@types/lodash": "^4.17.4",
|
"@types/lodash": "^4.17.4",
|
||||||
"@umijs/max": "^4.2.11",
|
"@umijs/max": "^4.2.11",
|
||||||
|
|||||||
Generated
+7
@@ -17,6 +17,9 @@ dependencies:
|
|||||||
'@huggingface/hub':
|
'@huggingface/hub':
|
||||||
specifier: ^0.15.1
|
specifier: ^0.15.1
|
||||||
version: 0.15.1
|
version: 0.15.1
|
||||||
|
'@huggingface/tasks':
|
||||||
|
specifier: ^0.11.6
|
||||||
|
version: 0.11.6
|
||||||
'@monaco-editor/react':
|
'@monaco-editor/react':
|
||||||
specifier: ^4.6.0
|
specifier: ^4.6.0
|
||||||
version: 4.6.0(monaco-editor@0.50.0)(react-dom@18.2.0)(react@18.2.0)
|
version: 4.6.0(monaco-editor@0.50.0)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -4050,6 +4053,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-8Q3aqTO+ldTTqtK4OfMz/h5DiiMBzUnZKdV0Dq2+JX+UXvqnTDVOk+bJd0QVytJYyNeZgKsj7XQHvEQGyo9cFg==, tarball: https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.10.14.tgz}
|
resolution: {integrity: sha512-8Q3aqTO+ldTTqtK4OfMz/h5DiiMBzUnZKdV0Dq2+JX+UXvqnTDVOk+bJd0QVytJYyNeZgKsj7XQHvEQGyo9cFg==, tarball: https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.10.14.tgz}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@huggingface/tasks@0.11.6:
|
||||||
|
resolution: {integrity: sha512-jIPlnJjSOqQCTpyCyIZCyamw3vOvMZrlaEdoB/PInHLnoaoqJKVIc0ijULKJxC3ClkgmehdoOu4J/yU+eGQLRw==, tarball: https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.11.6.tgz}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@humanwhocodes/config-array@0.11.14:
|
/@humanwhocodes/config-array@0.11.14:
|
||||||
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ export default {
|
|||||||
REFRESH: ['ctrl+r', 'meta+r'],
|
REFRESH: ['ctrl+r', 'meta+r'],
|
||||||
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']
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,5 +13,7 @@ export default {
|
|||||||
'models.openinplayground': 'Open in Playground',
|
'models.openinplayground': 'Open in Playground',
|
||||||
'models.instances': 'instances',
|
'models.instances': 'instances',
|
||||||
'model.form.ollama.model': 'Ollama Model',
|
'model.form.ollama.model': 'Ollama Model',
|
||||||
'model.form.ollamaholder': 'Please select or input model name'
|
'model.form.ollamaholder': 'Please select or input model name',
|
||||||
|
'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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,5 +13,7 @@ export default {
|
|||||||
'models.openinplayground': '在 Playground 中打开',
|
'models.openinplayground': '在 Playground 中打开',
|
||||||
'models.instances': '实例',
|
'models.instances': '实例',
|
||||||
'model.form.ollama.model': 'Ollama 模型',
|
'model.form.ollama.model': 'Ollama 模型',
|
||||||
'model.form.ollamaholder': '请选择或输入模型名称'
|
'model.form.ollamaholder': '请选择或输入模型名称',
|
||||||
|
'model.form.ollamatips':
|
||||||
|
'提示:以下为 GPUStack 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { listFiles, listModels } from '@huggingface/hub';
|
import { listFiles, listModels } from '@huggingface/hub';
|
||||||
|
import { PipelineType } from '@huggingface/tasks';
|
||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
import {
|
import {
|
||||||
FormData,
|
FormData,
|
||||||
@@ -129,6 +130,7 @@ export async function queryHuggingfaceModels(
|
|||||||
search: {
|
search: {
|
||||||
query: string;
|
query: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
task?: PipelineType;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
options?: any
|
options?: any
|
||||||
@@ -137,7 +139,7 @@ export async function queryHuggingfaceModels(
|
|||||||
for await (const model of listModels({
|
for await (const model of listModels({
|
||||||
...params,
|
...params,
|
||||||
...options,
|
...options,
|
||||||
limit: 50,
|
limit: 100,
|
||||||
additionalFields: ['sha'],
|
additionalFields: ['sha'],
|
||||||
fetch(url: string, config: any) {
|
fetch(url: string, config: any) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../style/column-wrapper.less';
|
||||||
|
|
||||||
|
const ColumnWrapper: React.FC<any> = ({ children, footer }) => {
|
||||||
|
if (footer) {
|
||||||
|
return (
|
||||||
|
<div className="column-wrapper-footer">
|
||||||
|
<div className="column-wrapper">{children}</div>
|
||||||
|
{<div className="footer">{footer}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div className="column-wrapper">{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColumnWrapper;
|
||||||
@@ -5,14 +5,17 @@ import { PageAction } from '@/config';
|
|||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import { convertFileSize } from '@/utils';
|
import { convertFileSize } from '@/utils';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Divider, Drawer, Form } from 'antd';
|
import { Drawer, Form } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
import { queryHuggingfaceModelFiles, queryHuggingfaceModels } from '../apis';
|
import { queryHuggingfaceModelFiles, queryHuggingfaceModels } from '../apis';
|
||||||
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 HFModelFile from './hf-model-file';
|
import HFModelFile from './hf-model-file';
|
||||||
|
import ModelCard from './model-card';
|
||||||
import SearchModel from './search-model';
|
import SearchModel from './search-model';
|
||||||
|
import TitleWrapper from './title-wrapper';
|
||||||
|
|
||||||
type AddModalProps = {
|
type AddModalProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -250,10 +253,20 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
|
|
||||||
const handleOnSelectModel = useCallback((item: any) => {
|
const handleOnSelectModel = useCallback((item: any) => {
|
||||||
const repo = item.name;
|
const repo = item.name;
|
||||||
|
let name = _.split(item.name, '/').slice(-1)[0];
|
||||||
|
const reg = /(-gguf)$/i;
|
||||||
|
name = _.toLower(name).replace(reg, '');
|
||||||
|
|
||||||
if (form.getFieldValue('source') === modelSourceMap.huggingface_value) {
|
if (form.getFieldValue('source') === modelSourceMap.huggingface_value) {
|
||||||
form.setFieldValue('huggingface_repo_id', repo);
|
form.setFieldsValue({
|
||||||
|
huggingface_repo_id: repo,
|
||||||
|
name: name
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
form.setFieldValue('ollama_library_model_name', repo);
|
form.setFieldsValue({
|
||||||
|
ollama_library_model_name: repo,
|
||||||
|
name: name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -272,42 +285,50 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
keyboard={false}
|
keyboard={false}
|
||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
height: 'calc(100vh - 110px)'
|
height: 'calc(100vh - 53px)',
|
||||||
|
padding: '16px 0'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
width="90%"
|
width="90%"
|
||||||
footer={
|
footer={false}
|
||||||
<ModalFooter
|
|
||||||
onCancel={onCancel}
|
|
||||||
onOk={handleSumit}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end'
|
|
||||||
}}
|
|
||||||
></ModalFooter>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<div style={{ flex: 1 }}>
|
<ColumnWrapper>
|
||||||
<SearchModel
|
<SearchModel
|
||||||
modelSource={modelSource}
|
modelSource={modelSource}
|
||||||
onSelectModel={handleOnSelectModel}
|
onSelectModel={handleOnSelectModel}
|
||||||
></SearchModel>
|
></SearchModel>
|
||||||
</div>
|
</ColumnWrapper>
|
||||||
|
{modelSource === modelSourceMap.huggingface_value && (
|
||||||
<Divider type="vertical"></Divider>
|
<ColumnWrapper>
|
||||||
<div style={{ flex: 1 }}>
|
<ModelCard repo={huggingfaceRepoId}></ModelCard>
|
||||||
<HFModelFile
|
<HFModelFile
|
||||||
repo={huggingfaceRepoId}
|
repo={huggingfaceRepoId}
|
||||||
onSelectFile={handleSelectModelFile}
|
onSelectFile={handleSelectModelFile}
|
||||||
></HFModelFile>
|
></HFModelFile>
|
||||||
</div>
|
</ColumnWrapper>
|
||||||
<Divider type="vertical"></Divider>
|
)}
|
||||||
<div style={{ width: 450 }}>
|
<ColumnWrapper
|
||||||
<h3 className="flex-between font-size-14">
|
footer={
|
||||||
<span>Configuration</span>
|
<ModalFooter
|
||||||
</h3>
|
onCancel={onCancel}
|
||||||
<Form name="deployModel" form={form} onFinish={onOk} preserve={false}>
|
onOk={handleSumit}
|
||||||
|
style={{
|
||||||
|
padding: '16px 20px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end'
|
||||||
|
}}
|
||||||
|
></ModalFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TitleWrapper>Configuration</TitleWrapper>
|
||||||
|
<Form
|
||||||
|
name="deployModel"
|
||||||
|
form={form}
|
||||||
|
onFinish={onOk}
|
||||||
|
preserve={false}
|
||||||
|
style={{ padding: '16px 20px' }}
|
||||||
|
>
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name="name"
|
name="name"
|
||||||
rules={[
|
rules={[
|
||||||
@@ -386,7 +407,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
></SealInput.TextArea>
|
></SealInput.TextArea>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</ColumnWrapper>
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { convertFileSize } from '@/utils';
|
import { convertFileSize } from '@/utils';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
GGMLQuantizationType,
|
||||||
|
GGUF_QUANT_DESCRIPTIONS
|
||||||
|
} from '@huggingface/gguf';
|
||||||
import { Button, Col, Empty, Row, Space, Spin } from 'antd';
|
import { Button, Col, Empty, Row, Space, Spin } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { queryHuggingfaceModelFiles } from '../apis';
|
import { queryHuggingfaceModelFiles } from '../apis';
|
||||||
import '../style/hf-model-file.less';
|
import '../style/hf-model-file.less';
|
||||||
|
import TitleWrapper from './title-wrapper';
|
||||||
|
|
||||||
interface HFModelFileProps {
|
interface HFModelFileProps {
|
||||||
repo: string;
|
repo: string;
|
||||||
@@ -36,7 +41,8 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
|||||||
const list = _.filter(res, (file: any) => {
|
const list = _.filter(res, (file: any) => {
|
||||||
return _.endsWith(file.path, '.gguf');
|
return _.endsWith(file.path, '.gguf');
|
||||||
});
|
});
|
||||||
setDataSource({ fileList: list, loading: false });
|
const sortList = _.sortBy(list, (item: any) => item.size);
|
||||||
|
setDataSource({ fileList: sortList, loading: false });
|
||||||
handleSelectModelFile(list[0]);
|
handleSelectModelFile(list[0]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setDataSource({ fileList: [], loading: false });
|
setDataSource({ fileList: [], loading: false });
|
||||||
@@ -44,55 +50,72 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getModelQuantizationType = (item: any) => {
|
||||||
|
const name = _.split(item.path, '.').slice(0, -1).join('.');
|
||||||
|
let quanType = _.toUpper(name.split('-').slice(-1)[0]);
|
||||||
|
if (quanType.indexOf('.') > -1) {
|
||||||
|
quanType = _.split(quanType, '.')[1];
|
||||||
|
}
|
||||||
|
if (_.get(GGUF_QUANT_DESCRIPTIONS, GGMLQuantizationType[quanType])) {
|
||||||
|
return <span className="tag-item">{quanType}</span>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleFetchModelFiles();
|
handleFetchModelFiles();
|
||||||
}, [props.repo]);
|
}, [props.repo]);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="flex-between font-size-14">
|
<TitleWrapper>
|
||||||
<span>Available Files({dataSource.fileList.length || 0})</span>
|
<span>Available Files ({dataSource.fileList.length || 0})</span>
|
||||||
</h3>
|
</TitleWrapper>
|
||||||
<div>
|
<div style={{ padding: '16px 20px' }}>
|
||||||
<Spin spinning={dataSource.loading} style={{ minHeight: 100 }}></Spin>
|
<Spin spinning={dataSource.loading} style={{ minHeight: 100 }}>
|
||||||
{dataSource.fileList.length ? (
|
{dataSource.fileList.length ? (
|
||||||
<Row gutter={[10, 10]}>
|
<Row gutter={[16, 16]}>
|
||||||
{_.map(dataSource.fileList, (item: any) => {
|
{_.map(dataSource.fileList, (item: any) => {
|
||||||
return (
|
return (
|
||||||
<Col span={24} key={item.path}>
|
<Col span={24} key={item.path}>
|
||||||
<div
|
<div
|
||||||
className={classNames('hf-model-file', {
|
tabIndex={0}
|
||||||
active: item.path === current
|
className={classNames('hf-model-file', {
|
||||||
})}
|
active: item.path === current
|
||||||
onClick={() => handleSelectModelFile(item)}
|
})}
|
||||||
>
|
onClick={() => handleSelectModelFile(item)}
|
||||||
<div className="title">{item.path}</div>
|
>
|
||||||
<Space className="tags">
|
<div className="title">{item.path}</div>
|
||||||
<span className="tag-item">
|
<Space className="tags">
|
||||||
{convertFileSize(item.size)}
|
<span className="tag-item">
|
||||||
</span>
|
{convertFileSize(item.size)}
|
||||||
</Space>
|
</span>
|
||||||
<div className="btn">
|
{getModelQuantizationType(item)}
|
||||||
<Button size="middle">Install</Button>
|
</Space>
|
||||||
|
<div className="btn">
|
||||||
|
<Button size="middle">
|
||||||
|
{item.path === current ? 'Selected' : 'Select'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</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 models found"
|
/>
|
||||||
/>
|
)
|
||||||
)
|
)}
|
||||||
)}
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
HeartOutlined
|
HeartOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Space, Tag } from 'antd';
|
import { Button, Space, Tag } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@@ -15,7 +15,8 @@ interface HFModelItemProps {
|
|||||||
title: string;
|
title: string;
|
||||||
downloads: number;
|
downloads: number;
|
||||||
likes: number;
|
likes: number;
|
||||||
lastModified: string;
|
task?: string;
|
||||||
|
updatedAt: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
source?: string;
|
source?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
@@ -29,15 +30,30 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="title">
|
<div className="title">
|
||||||
<FolderOutlined className="m-r-5" />
|
<FolderOutlined
|
||||||
|
className="m-r-5"
|
||||||
|
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||||
|
/>
|
||||||
{props.title}
|
{props.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="info">
|
<div className="info">
|
||||||
{props.source === modelSourceMap.huggingface_value ? (
|
{props.source === modelSourceMap.huggingface_value ? (
|
||||||
<Space size={16}>
|
<Space size={16}>
|
||||||
|
{props.task && (
|
||||||
|
<Tag
|
||||||
|
color="rgb(236, 240, 242)"
|
||||||
|
style={{
|
||||||
|
marginRight: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
|
||||||
|
{props.task}
|
||||||
|
</span>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
<span>
|
<span>
|
||||||
{dayjs().to(
|
{dayjs().to(
|
||||||
dayjs(dayjs(props.lastModified).format('YYYY-MM-DD HH:mm:ss'))
|
dayjs(dayjs(props.updatedAt).format('YYYY-MM-DD HH:mm:ss'))
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
@@ -50,22 +66,29 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
</Space>
|
</Space>
|
||||||
) : (
|
) : (
|
||||||
<Space size={10}>
|
<div className="flex-between">
|
||||||
{_.map(props.tags, (tag: string) => {
|
<Space size={10}>
|
||||||
return (
|
{_.map(props.tags, (tag: string) => {
|
||||||
<Tag
|
return (
|
||||||
style={{
|
<Tag
|
||||||
backgroundColor: 'var(--color-white-1)',
|
style={{
|
||||||
marginRight: 0
|
backgroundColor: 'var(--color-white-1)',
|
||||||
}}
|
marginRight: 0
|
||||||
>
|
}}
|
||||||
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
|
>
|
||||||
{tag}
|
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
|
||||||
</span>
|
{tag}
|
||||||
</Tag>
|
</span>
|
||||||
);
|
</Tag>
|
||||||
})}
|
);
|
||||||
</Space>
|
})}
|
||||||
|
</Space>
|
||||||
|
<div className="btn">
|
||||||
|
<Button size="middle">
|
||||||
|
{props.active ? 'Selected' : 'Select'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Space, Tag } from 'antd';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { queryHuggingfaceModelDetail } from '../apis';
|
||||||
|
import '../style/model-card.less';
|
||||||
|
import TitleWrapper from './title-wrapper';
|
||||||
|
|
||||||
|
const ModelCard: React.FC<{ repo: string }> = (props) => {
|
||||||
|
const { repo } = props;
|
||||||
|
const [modelData, setModelData] = useState<any>({});
|
||||||
|
|
||||||
|
const getModelCardData = async () => {
|
||||||
|
if (!repo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await queryHuggingfaceModelDetail({ repo });
|
||||||
|
console.log('modelcarddata==========', res);
|
||||||
|
setModelData(res);
|
||||||
|
} catch (error) {
|
||||||
|
setModelData({});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getModelCardData();
|
||||||
|
}, [repo]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TitleWrapper>
|
||||||
|
<span>Model Card</span>
|
||||||
|
</TitleWrapper>
|
||||||
|
<div className="wrapper">
|
||||||
|
<div className="model-card-wrap">
|
||||||
|
<div className="title">{modelData.id}</div>
|
||||||
|
<Space>
|
||||||
|
<Tag className="tag-item">
|
||||||
|
<span className="m-r-5">Architecture:</span>
|
||||||
|
{modelData.config?.model_type}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ModelCard);
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import IconFont from '@/components/icon-font';
|
||||||
|
import hotkeys from '@/config/hotkeys';
|
||||||
|
import { platformCall } from '@/utils';
|
||||||
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
|
import { Input, Tag } from 'antd';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
|
const SearchInput: React.FC<{
|
||||||
|
onSearch: (e: any) => void;
|
||||||
|
}> = (props) => {
|
||||||
|
const { onSearch } = props;
|
||||||
|
const [isFocus, setIsFocus] = useState(false);
|
||||||
|
const inputRef = useRef<any>(null);
|
||||||
|
const platform = platformCall();
|
||||||
|
|
||||||
|
useHotkeys(hotkeys.INPUT.join(','), () => {
|
||||||
|
inputRef.current?.focus?.();
|
||||||
|
setIsFocus(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
onPressEnter={onSearch}
|
||||||
|
onFocus={() => setIsFocus(true)}
|
||||||
|
onBlur={() => setIsFocus(false)}
|
||||||
|
allowClear
|
||||||
|
placeholder="Search models from Hugging Face"
|
||||||
|
suffix={
|
||||||
|
!isFocus && (
|
||||||
|
<Tag style={{ marginRight: 0 }}>
|
||||||
|
{platform.isMac ? (
|
||||||
|
<>
|
||||||
|
<IconFont type="icon-command"></IconFont> + K
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>CTRL + K</>
|
||||||
|
)}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
prefix={
|
||||||
|
<SearchOutlined
|
||||||
|
style={{
|
||||||
|
fontSize: '16px',
|
||||||
|
color: 'var(--ant-color-text-quaternary)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
></Input>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(SearchInput);
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import IconFont from '@/components/icon-font';
|
import IconFont from '@/components/icon-font';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Input } from 'antd';
|
import { Button, Input, Select } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { queryHuggingfaceModels } from '../apis';
|
import { queryHuggingfaceModels } from '../apis';
|
||||||
import { modelSourceMap, ollamaModelOptions } from '../config';
|
import {
|
||||||
|
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 SearchResult from './search-result';
|
import SearchResult from './search-result';
|
||||||
|
|
||||||
interface SearchInputProps {
|
interface SearchInputProps {
|
||||||
@@ -29,14 +34,16 @@ const sourceList = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const SearchInput: React.FC<SearchInputProps> = (props) => {
|
const SearchModel: React.FC<SearchInputProps> = (props) => {
|
||||||
|
const intl = useIntl();
|
||||||
const { modelSource, onSourceChange, onSelectModel } = props;
|
const { modelSource, onSourceChange, onSelectModel } = props;
|
||||||
const [showSearch, setShowSearch] = useState(false);
|
|
||||||
const [repoOptions, setRepoOptions] = useState<any[]>([]);
|
const [repoOptions, setRepoOptions] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [current, setCurrent] = useState<string>('');
|
||||||
|
const [sortType, setSortType] = useState<string>('downloads');
|
||||||
const cacheRepoOptions = useRef<any[]>([]);
|
const cacheRepoOptions = useRef<any[]>([]);
|
||||||
const axiosTokenRef = useRef<any>(null);
|
const axiosTokenRef = useRef<any>(null);
|
||||||
const [current, setCurrent] = useState<string>('');
|
const customOllamaModelRef = useRef<any>(null);
|
||||||
|
|
||||||
const handleOnSelectModel = (item: any) => {
|
const handleOnSelectModel = (item: any) => {
|
||||||
onSelectModel(item);
|
onSelectModel(item);
|
||||||
@@ -68,7 +75,7 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
|
|||||||
});
|
});
|
||||||
const sortedList = _.sortBy(
|
const sortedList = _.sortBy(
|
||||||
list,
|
list,
|
||||||
(item: any) => item.downloads
|
(item: any) => item[sortType]
|
||||||
).reverse();
|
).reverse();
|
||||||
cacheRepoOptions.current = sortedList;
|
cacheRepoOptions.current = sortedList;
|
||||||
setRepoOptions(sortedList);
|
setRepoOptions(sortedList);
|
||||||
@@ -82,12 +89,12 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlerSearchModels = async (e: any) => {
|
const handlerSearchModels = useCallback(async (e: any) => {
|
||||||
const text = e.target.value;
|
const text = e.target.value;
|
||||||
handleOnSearchRepo(text);
|
handleOnSearchRepo(text);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleOnFocus = () => {
|
const handleOnOpen = () => {
|
||||||
if (
|
if (
|
||||||
!repoOptions.length &&
|
!repoOptions.length &&
|
||||||
!cacheRepoOptions.current.length &&
|
!cacheRepoOptions.current.length &&
|
||||||
@@ -98,6 +105,7 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
|
|||||||
if (modelSourceMap.ollama_library_value === modelSource) {
|
if (modelSourceMap.ollama_library_value === modelSource) {
|
||||||
setRepoOptions(ollamaModelOptions);
|
setRepoOptions(ollamaModelOptions);
|
||||||
cacheRepoOptions.current = ollamaModelOptions;
|
cacheRepoOptions.current = ollamaModelOptions;
|
||||||
|
handleOnSelectModel(ollamaModelOptions[0]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,7 +115,6 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
|
|||||||
return item.name.includes(text);
|
return item.name.includes(text);
|
||||||
});
|
});
|
||||||
setRepoOptions(list);
|
setRepoOptions(list);
|
||||||
console.log('handleFilterModels', text);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const debounceFilter = _.debounce((e: any) => {
|
const debounceFilter = _.debounce((e: any) => {
|
||||||
@@ -121,8 +128,73 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
|
|||||||
cacheRepoOptions.current = [];
|
cacheRepoOptions.current = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: any) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
customOllamaModelRef.current = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const model = {
|
||||||
|
label: customOllamaModelRef.current,
|
||||||
|
value: customOllamaModelRef.current,
|
||||||
|
name: customOllamaModelRef.current,
|
||||||
|
id: ''
|
||||||
|
};
|
||||||
|
onSelectModel(model);
|
||||||
|
setCurrent('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSortChange = (value: string) => {
|
||||||
|
const sortedList = _.sortBy(
|
||||||
|
repoOptions,
|
||||||
|
(item: any) => item[value]
|
||||||
|
).reverse();
|
||||||
|
setSortType(value);
|
||||||
|
setRepoOptions(sortedList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderHFSearch = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SearchInput onSearch={handlerSearchModels}></SearchInput>
|
||||||
|
<div className={SearchStyle.filter}>
|
||||||
|
<span>
|
||||||
|
<span className="value">{repoOptions.length}</span>results
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
value={sortType}
|
||||||
|
onChange={handleSortChange}
|
||||||
|
labelRender={({ label }) => {
|
||||||
|
return <span>Sort: {label}</span>;
|
||||||
|
}}
|
||||||
|
options={modelFilesSortOptions}
|
||||||
|
size="middle"
|
||||||
|
style={{ width: '150px' }}
|
||||||
|
></Select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderOllamaCustom = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
allowClear
|
||||||
|
placeholder="Input ollama model name"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
></Input>
|
||||||
|
<div className={SearchStyle.filter}>
|
||||||
|
<Button type="primary" onClick={handleConfirm}>
|
||||||
|
{intl.formatMessage({ id: 'common.button.confirm' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleOnFocus();
|
handleOnOpen();
|
||||||
return () => {
|
return () => {
|
||||||
axiosTokenRef.current?.abort?.();
|
axiosTokenRef.current?.abort?.();
|
||||||
};
|
};
|
||||||
@@ -136,24 +208,16 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
|
|||||||
value={modelSource}
|
value={modelSource}
|
||||||
onChange={handleSourceChange}
|
onChange={handleSourceChange}
|
||||||
></RadioButtons> */}
|
></RadioButtons> */}
|
||||||
<Input
|
{modelSource === modelSourceMap.huggingface_value ? (
|
||||||
onPressEnter={handlerSearchModels}
|
renderHFSearch()
|
||||||
onChange={debounceFilter}
|
) : (
|
||||||
allowClear
|
<div style={{ lineHeight: '18px' }}>
|
||||||
placeholder={
|
{intl.formatMessage(
|
||||||
modelSource === 'huggingface'
|
{ id: 'model.form.ollamatips' },
|
||||||
? 'Search models from hugging face '
|
{ name: intl.formatMessage({ id: 'model.form.ollama.model' }) }
|
||||||
: ''
|
)}
|
||||||
}
|
</div>
|
||||||
prefix={
|
)}
|
||||||
<SearchOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: '16px',
|
|
||||||
color: 'var(--ant-color-text-quaternary)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
></Input>
|
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
<SearchResult
|
<SearchResult
|
||||||
@@ -168,4 +232,4 @@ const SearchInput: React.FC<SearchInputProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(SearchInput);
|
export default React.memo(SearchModel);
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
|
|||||||
<div style={{ ...props.style }} className="search-result-wrap">
|
<div style={{ ...props.style }} className="search-result-wrap">
|
||||||
<Spin spinning={props.loading} style={{ minHeight: 100 }}>
|
<Spin spinning={props.loading} style={{ minHeight: 100 }}>
|
||||||
{resultList.length ? (
|
{resultList.length ? (
|
||||||
<Row gutter={[10, 10]}>
|
<Row gutter={[16, 16]}>
|
||||||
{resultList.map((item, index) => (
|
{resultList.map((item, index) => (
|
||||||
<Col span={24} key={item.name}>
|
<Col span={24} key={item.name}>
|
||||||
<div onClick={(e) => handleSelect(e, item)}>
|
<div onClick={(e) => handleSelect(e, item)} tabIndex={0}>
|
||||||
<HFModelItem
|
<HFModelItem
|
||||||
source={source}
|
source={source}
|
||||||
tags={item.tags}
|
tags={item.tags}
|
||||||
@@ -35,7 +35,8 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
|
|||||||
title={item.name}
|
title={item.name}
|
||||||
downloads={item.downloads}
|
downloads={item.downloads}
|
||||||
likes={item.likes}
|
likes={item.likes}
|
||||||
lastModified={item.lastModified}
|
task={item.task}
|
||||||
|
updatedAt={item.updatedAt}
|
||||||
active={item.id === props.current}
|
active={item.id === props.current}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
<Space size={20}>
|
<Space size={20}>
|
||||||
<Dropdown menu={{ items: sourceOptions }}>
|
<Dropdown menu={{ items: sourceOptions }} placement="bottomRight">
|
||||||
<Button
|
<Button
|
||||||
icon={<DownOutlined></DownOutlined>}
|
icon={<DownOutlined></DownOutlined>}
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../style/title-wrapper.less';
|
||||||
|
|
||||||
|
const TitleWrapper: React.FC<any> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<h3 className="h3">
|
||||||
|
<span>{children}</span>
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TitleWrapper;
|
||||||
@@ -6,40 +6,64 @@ export const ollamaModelOptions = [
|
|||||||
label: 'llama3.1',
|
label: 'llama3.1',
|
||||||
value: 'llama3.1',
|
value: 'llama3.1',
|
||||||
name: 'llama3.1',
|
name: 'llama3.1',
|
||||||
|
id: 'llama3.1',
|
||||||
tags: ['8B', '70B', '405B']
|
tags: ['8B', '70B', '405B']
|
||||||
},
|
},
|
||||||
{ label: 'llama3', value: 'llama3', name: 'llama3', tags: ['8B', '70B'] },
|
{
|
||||||
{ label: 'gemma2', value: 'gemma2', name: 'gemma2', tags: ['9B', '27B'] },
|
label: 'llama3',
|
||||||
|
value: 'llama3',
|
||||||
|
name: 'llama3',
|
||||||
|
tags: ['8B', '70B'],
|
||||||
|
id: 'llama3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'gemma2',
|
||||||
|
value: 'gemma2',
|
||||||
|
name: 'gemma2',
|
||||||
|
tags: ['9B', '27B'],
|
||||||
|
id: 'gemma2'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'mistral',
|
label: 'mistral',
|
||||||
value: 'mistral',
|
value: 'mistral',
|
||||||
name: 'mistral',
|
name: 'mistral',
|
||||||
tags: ['7B']
|
tags: ['7B'],
|
||||||
|
id: 'mistral'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'llava',
|
label: 'llava',
|
||||||
value: 'llava',
|
value: 'llava',
|
||||||
name: 'llava',
|
name: 'llava',
|
||||||
tags: ['7B', '13B', '34B']
|
tags: ['7B', '13B', '34B'],
|
||||||
|
id: 'llava'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'qwen2',
|
label: 'qwen2',
|
||||||
value: 'qwen2',
|
value: 'qwen2',
|
||||||
name: 'qwen2',
|
name: 'qwen2',
|
||||||
tags: ['0.5B', '1.5B', '7B', '72B']
|
tags: ['0.5B', '1.5B', '7B', '72B'],
|
||||||
|
id: 'qwen2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'phi3',
|
||||||
|
value: 'phi3',
|
||||||
|
name: 'phi3',
|
||||||
|
tags: ['3B', '14B'],
|
||||||
|
id: 'phi3'
|
||||||
},
|
},
|
||||||
{ label: 'phi3', value: 'phi3', name: 'phi3', tags: ['3B', '14B'] },
|
|
||||||
{
|
{
|
||||||
label: 'codellama',
|
label: 'codellama',
|
||||||
value: 'codellama',
|
value: 'codellama',
|
||||||
name: 'codellama',
|
name: 'codellama',
|
||||||
tags: ['7B', '13B', '34B', '70B']
|
tags: ['7B', '13B', '34B', '70B'],
|
||||||
|
id: 'codellama'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'deepseek-coder',
|
label: 'deepseek-coder',
|
||||||
value: 'deepseek-coder',
|
value: 'deepseek-coder',
|
||||||
name: 'deepseek-coder',
|
name: 'deepseek-coder',
|
||||||
tags: ['1B', '7B', '33B']
|
tags: ['1B', '7B', '33B'],
|
||||||
|
id: 'deepseek-coder'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -102,3 +126,10 @@ export const ActionList = [
|
|||||||
icon: EditOutlined
|
icon: EditOutlined
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const modelFilesSortOptions = [
|
||||||
|
// { label: 'Trending', value: 'trendingScore' },
|
||||||
|
{ label: 'Likes', value: 'likes' },
|
||||||
|
{ label: 'Downloads', value: 'downloads' },
|
||||||
|
{ label: 'Updated', value: 'updatedAt' }
|
||||||
|
];
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.column-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100vh - 85px);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
border-left: 1px solid var(--ant-color-split);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-wrapper-footer {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
border-left: 1px solid var(--ant-color-split);
|
||||||
|
|
||||||
|
.column-wrapper {
|
||||||
|
padding-bottom: 50px;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background-color: var(--color-white-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
height: 20px;
|
||||||
border: 1px solid var(--ant-color-border);
|
border: 1px solid var(--ant-color-border);
|
||||||
color: var(--ant-color-text-secondary);
|
color: var(--ant-color-text-secondary);
|
||||||
}
|
}
|
||||||
@@ -33,3 +34,7 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
.model-card-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 20px;
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,35 @@
|
|||||||
.search-result-wrap {
|
.search-result-wrap {
|
||||||
background-color: rgba(255, 255, 255, 100%);
|
background-color: rgba(255, 255, 255, 100%);
|
||||||
padding: 16px;
|
padding: 16px 20px;
|
||||||
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
display: flex;
|
position: sticky;
|
||||||
// position: absolute;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 20px;
|
padding-inline: 20px;
|
||||||
background: #fff;
|
background: var(--color-white-1);
|
||||||
// border-bottom: 1px solid var(--ant-color-split);
|
border-bottom: 1px solid var(--ant-color-split);
|
||||||
// box-shadow: 0 1px 2px rgba(5, 5, 5, 5%);
|
// box-shadow: 0 1px 2px rgba(5, 5, 5, 5%);
|
||||||
padding-top: 0;
|
padding-bottom: 16px;
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.value {
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
margin-right: 6px;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.h3 {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
padding-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
background-color: var(--color-white-1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user