chore: drawer ux

This commit is contained in:
jialin
2024-08-09 10:26:46 +08:00
parent 8490e32ef1
commit 77f38394e6
22 changed files with 547 additions and 150 deletions
+1
View File
@@ -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",
+7
View File
@@ -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'}
+2 -1
View File
@@ -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']
}; };
+3 -1
View File
@@ -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.'
}; };
+3 -1
View File
@@ -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}】 输入框中输入你要部署的模型。'
}; };
+3 -1
View File
@@ -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;
+52 -31
View File
@@ -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>
); );
+67 -44
View File
@@ -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>
); );
+43 -20
View File
@@ -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);
+96 -32
View File
@@ -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>
+1 -1
View File
@@ -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;
+39 -8
View File
@@ -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;
}
+28
View File
@@ -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;
}
}
+23 -7
View File
@@ -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);
}