chore: depoly model ux

main
jialin 2 years ago
parent a04773d487
commit ab8483fc42

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@ -14,6 +14,10 @@
margin-left: 8px;
}
.m-l-20 {
margin-left: 20px;
}
.m-r-10 {
margin-right: 10px;
}
@ -63,6 +67,10 @@
flex-direction: column;
}
.relative {
position: relative;
}
.font-size-12 {
font-size: var(--font-size-base);
}

@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_4613488_cpb5yo1pk77.js'
scriptUrl: '//at.alicdn.com/t/c/font_4613488_rsdrbzw4fyd.js'
});
export default IconFont;

@ -0,0 +1,22 @@
.radio-button-wrap {
.item {
display: flex;
justify-content: center;
align-items: center;
font-size: var(--font-size-base);
padding: 2px;
border-radius: var(--border-radius-base);
height: 40px;
width: 40px;
border: 1px solid var(--ant-color-border);
cursor: pointer;
&.active {
background-color: var(--ant-color-fill-secondary);
}
&:hover {
background-color: var(--ant-color-fill-secondary);
}
}
}

@ -0,0 +1,29 @@
import { Space } from 'antd';
import classNames from 'classnames';
import React from 'react';
import './index.less';
interface RadioButtonsProps {
options: { value: any; label: React.ReactNode }[];
value: string;
gap?: number;
onChange: (value: string) => void;
}
const RadioButtons: React.FC<RadioButtonsProps> = (props) => {
const { options, value, onChange, gap = 12 } = props;
return (
<Space className="radio-button-wrap" size={gap}>
{options.map((option) => (
<span
key={option.value}
onClick={() => onChange(option.value)}
className={classNames('item', { active: value === option.value })}
>
{option.label}
</span>
))}
</Space>
);
};
export default RadioButtons;

@ -81,6 +81,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
>
<Input
{...rest}
placeholder={isFocus ? placeholder : ''}
ref={inputRef}
autoComplete="off"
onInput={handleInput}

@ -2,9 +2,11 @@
import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
dayjs.extend(localizedFormat);
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);

@ -116,17 +116,21 @@ export async function callHuggingfaceQuickSearch(params: any) {
});
}
export async function queryHuggingfaceModels(params: {
search: {
query: string;
tags: string[];
};
}) {
export async function queryHuggingfaceModels(
params: {
search: {
query: string;
tags: string[];
};
},
options?: any
) {
const result = [];
for await (const model of listModels({
...params,
limit: 100,
additionalFields: ['author']
...options,
limit: 50,
additionalFields: ['cardData']
})) {
result.push(model);
}

@ -9,10 +9,11 @@ import { SearchOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Form, Input, Modal } from 'antd';
import _ from 'lodash';
import { memo, useEffect, useState } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { queryHuggingfaceModelFiles, queryHuggingfaceModels } from '../apis';
import { ollamaModelOptions } from '../config';
import { modelSourceMap } from '../config';
import { FormData, ListItem } from '../config/types';
import SearchInput from './search-input';
type AddModalProps = {
title: string;
@ -24,8 +25,16 @@ type AddModalProps = {
};
const sourceOptions = [
{ label: 'Hugging Face', value: 'huggingface', key: 'huggingface' },
{ label: 'Ollama Library', value: 'ollama_library', key: 'ollama_library' }
{
label: 'Hugging Face',
value: modelSourceMap.huggingface_value,
key: 'huggingface'
},
{
label: 'Ollama Library',
value: modelSourceMap.ollama_library_value,
key: 'ollama_library'
}
];
const AddModal: React.FC<AddModalProps> = (props) => {
@ -41,17 +50,21 @@ const AddModal: React.FC<AddModalProps> = (props) => {
const [fileOptions, setFileOptions] = useState<
{ label: string; value: string }[]
>([]);
const [ollamaTags, setOllamaTags] = useState<string[]>([]);
const initFormValue = () => {
if (action === PageAction.CREATE && open) {
form.setFieldsValue({
source: 'huggingface',
source: modelSourceMap.huggingface_value,
replicas: 1
});
}
if (action === PageAction.EDIT && open) {
const list = _.split(props.data?.ollama_library_model_name, ':');
form.setFieldsValue({
...props.data
...props.data,
ollama_library_model_name: _.get(list, '0'),
tag: _.get(list, '1')
});
}
};
@ -60,8 +73,6 @@ const AddModal: React.FC<AddModalProps> = (props) => {
initFormValue();
}, [open]);
const handleInputRepoChange = (value: string) => {};
const fileNamLabel = (item: any) => {
return (
<span>
@ -149,15 +160,14 @@ const AddModal: React.FC<AddModalProps> = (props) => {
showSearch
onBlur={handleRepoOnBlur}
onSelect={handleRepoOnBlur}
onChange={handleInputRepoChange}
onSearch={debounceSearch}
options={repoOptions}
disabled={action === PageAction.EDIT}
addAfter={
<span style={{ position: 'relative', top: '2px' }}>
<SearchOutlined></SearchOutlined>
</span>
}
disabled={true}
description={intl.formatMessage({ id: 'models.form.repoid.desc' })}
>
<Input></Input>
@ -236,14 +246,26 @@ const AddModal: React.FC<AddModalProps> = (props) => {
}
]}
>
<SealAutoComplete
{/* <SealAutoComplete
filterOption
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'model.form.ollama.model' })}
placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })}
required
options={ollamaModelOptions}
></SealAutoComplete>
></SealAutoComplete> */}
<SealInput.Input
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'model.form.ollama.model' })}
placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })}
required
></SealInput.Input>
</Form.Item>
<Form.Item name="tag">
<SealInput.Input
label="Tag"
placeholder={`${_.join(ollamaTags, ',')} or another...`}
></SealInput.Input>
</Form.Item>
</>
);
@ -251,27 +273,36 @@ const AddModal: React.FC<AddModalProps> = (props) => {
const renderFieldsBySource = () => {
switch (modelSource) {
case 'huggingface':
case modelSourceMap.huggingface_value:
return renderHuggingfaceFields();
case 'ollama_library':
case modelSourceMap.ollama_library_value:
return renderOllamaModelFields();
case 's3':
case modelSourceMap.s3_value:
return renderS3Fields();
default:
return null;
}
};
const handleSourceChange = (value: string) => {
console.log('source change', value);
};
const handleOnSelectModel = useCallback((item: any) => {
const repo = item.name;
setOllamaTags(_.map(item.tags, (tag: string) => _.toLower(tag)));
if (form.getFieldValue('source') === modelSourceMap.huggingface_value) {
form.setFieldValue('huggingface_repo_id', repo);
handleFetchModelFiles(repo);
} else {
form.setFieldValue('ollama_library_model_name', repo);
}
}, []);
const handleSourceChange = useCallback((value: string) => {
form.setFieldValue('source', value);
}, []);
const handleSumit = () => {
form.submit();
};
const handleOnFinish = (values: FormData) => {
console.log('onFinish', values);
onOk(values);
};
return (
<Modal
title={title}
@ -280,7 +311,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
onOk={handleSumit}
onCancel={onCancel}
destroyOnClose={true}
closeIcon={false}
closeIcon={true}
maskClosable={false}
keyboard={false}
width={600}
@ -289,6 +320,13 @@ const AddModal: React.FC<AddModalProps> = (props) => {
<ModalFooter onCancel={onCancel} onOk={handleSumit}></ModalFooter>
}
>
{action === PageAction.CREATE && (
<SearchInput
modelSource={modelSource}
onSourceChange={handleSourceChange}
onSelectModel={handleOnSelectModel}
></SearchInput>
)}
<Form name="addModalForm" form={form} onFinish={onOk} preserve={false}>
<Form.Item<FormData>
name="name"
@ -324,16 +362,18 @@ const AddModal: React.FC<AddModalProps> = (props) => {
)
}
]}
noStyle={action === PageAction.CREATE}
>
<SealSelect
disabled={action === PageAction.EDIT}
label={intl.formatMessage({
id: 'models.form.source'
})}
options={sourceOptions}
required
onChange={handleSourceChange}
></SealSelect>
{action === PageAction.EDIT && (
<SealSelect
disabled={true}
label={intl.formatMessage({
id: 'models.form.source'
})}
options={sourceOptions}
required
></SealSelect>
)}
</Form.Item>
{renderFieldsBySource()}
<Form.Item<FormData>

@ -0,0 +1,75 @@
import { formatNumber } from '@/utils';
import {
DownloadOutlined,
FolderOutlined,
HeartOutlined
} from '@ant-design/icons';
import { Space, Tag } from 'antd';
import classNames from 'classnames';
import dayjs from 'dayjs';
import _ from 'lodash';
import { modelSourceMap } from '../config';
import '../style/hf-model-item.less';
interface HFModelItemProps {
title: string;
downloads: number;
likes: number;
lastModified: string;
active: boolean;
source?: string;
tags?: string[];
}
const HFModelItem: React.FC<HFModelItemProps> = (props) => {
return (
<div
className={classNames('hf-model-item', {
active: props.active
})}
>
<div className="title">
<FolderOutlined className="m-r-5" />
{props.title}
</div>
<div className="info">
{props.source === modelSourceMap.huggingface_value ? (
<Space size={16}>
<span>
{dayjs().to(
dayjs(dayjs(props.lastModified).format('YYYY-MM-DD HH:mm:ss'))
)}
</span>
<span>
<HeartOutlined className="m-r-5" />
{props.likes}
</span>
<span>
<DownloadOutlined className="m-r-5" />
{formatNumber(props.downloads)}
</span>
</Space>
) : (
<Space size={10}>
{_.map(props.tags, (tag: string) => {
return (
<Tag
style={{
backgroundColor: 'var(--color-white-1)',
marginRight: 0
}}
>
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
{tag}
</span>
</Tag>
);
})}
</Space>
)}
</div>
</div>
);
};
export default HFModelItem;

@ -0,0 +1,173 @@
import IconFont from '@/components/icon-font';
import RadioButtons from '@/components/radio-buttons';
import { SearchOutlined } from '@ant-design/icons';
import { Input } from 'antd';
import _ from 'lodash';
import React, { useRef, useState } from 'react';
import { queryHuggingfaceModels } from '../apis';
import { modelSourceMap, ollamaModelOptions } from '../config';
import SearchStyle from '../style/search-result.less';
import SearchResult from './search-result';
interface SearchInputProps {
modelSource: string;
onSourceChange: (source: string) => void;
onSelectModel: (model: any) => void;
}
const sourceList = [
{
label: (
<IconFont type="icon-huggingface" className="font-size-14"></IconFont>
),
value: 'huggingface',
key: 'huggingface'
},
{
label: <IconFont type="icon-ollama" className="font-size-14"></IconFont>,
value: 'ollama_library',
key: 'ollama_library'
}
];
const SearchInput: React.FC<SearchInputProps> = (props) => {
const { modelSource, onSourceChange, onSelectModel } = props;
const [showSearch, setShowSearch] = useState(false);
const [repoOptions, setRepoOptions] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const cacheRepoOptions = useRef<any[]>([]);
const axiosTokenRef = useRef<any>(null);
const handleOnSearchRepo = async (text: string) => {
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
if (loading) return;
try {
setLoading(true);
cacheRepoOptions.current = [];
const params = {
search: {
query: text,
tags: ['gguf']
}
};
const models = await queryHuggingfaceModels(params, {
signal: axiosTokenRef.current.signal
});
const list = _.map(models || [], (item: any) => {
return {
...item,
value: item.name,
label: item.name
};
});
const sortedList = _.sortBy(
list,
(item: any) => item.downloads
).reverse();
cacheRepoOptions.current = sortedList;
setRepoOptions(sortedList);
} catch (error) {
setRepoOptions([]);
cacheRepoOptions.current = [];
} finally {
setLoading(false);
}
};
const handlerSearchModels = async (e: any) => {
const text = e.target.value;
handleOnSearchRepo(text);
};
const handleOnFocus = () => {
setShowSearch(true);
if (
!repoOptions.length &&
!cacheRepoOptions.current.length &&
modelSource === modelSourceMap.huggingface_value
) {
handleOnSearchRepo('');
}
if (modelSourceMap.ollama_library_value === modelSource) {
setRepoOptions(ollamaModelOptions);
cacheRepoOptions.current = ollamaModelOptions;
}
};
const handleOnSelectModel = (item: any) => {
onSelectModel(item);
setShowSearch(false);
};
const handleOnBlur = () => {
setTimeout(() => {
setShowSearch(false);
}, 200);
};
const handleFilterModels = (e: any) => {
const text = e.target.value;
const list = _.filter(cacheRepoOptions.current, (item: any) => {
return item.name.includes(text);
});
setRepoOptions(list);
console.log('handleFilterModels', text);
};
const debounceFilter = _.debounce((e: any) => {
handleFilterModels(e);
}, 300);
const handleSourceChange = (source: string) => {
axiosTokenRef.current?.abort?.();
onSourceChange(source);
setRepoOptions([]);
cacheRepoOptions.current = [];
};
return (
<>
<div className={SearchStyle['search-bar']}>
<RadioButtons
options={sourceList}
value={modelSource}
onChange={handleSourceChange}
></RadioButtons>
<Input
onPressEnter={handlerSearchModels}
onChange={debounceFilter}
allowClear
onFocus={handleOnFocus}
onBlur={() => handleOnBlur()}
className="m-l-20"
placeholder={
modelSource === 'huggingface'
? 'Search models from hugging face '
: ''
}
prefix={
<SearchOutlined
style={{
fontSize: '16px',
color: 'var(--ant-color-text-quaternary)'
}}
/>
}
></Input>
</div>
{showSearch && (
<SearchResult
loading={loading}
resultList={repoOptions}
current=""
source={modelSource}
onSelect={handleOnSelectModel}
></SearchResult>
)}
<div style={{ height: '81px' }}></div>
</>
);
};
export default React.memo(SearchInput);

@ -0,0 +1,64 @@
import { SearchOutlined } from '@ant-design/icons';
import { Col, Empty, Row, Spin } from 'antd';
import React from 'react';
import '../style/search-result.less';
import HFModelItem from './hf-model-item';
interface SearchResultProps {
resultList: any[];
onSelect?: (item: any) => void;
current?: string;
source?: string;
style?: React.CSSProperties;
loading?: boolean;
}
const SearchResult: React.FC<SearchResultProps> = (props) => {
const { resultList, onSelect, source } = props;
const handleSelect = (e: any, item: any) => {
e.stopPropagation();
onSelect?.(item);
};
return (
<div style={{ ...props.style }} className="search-result-wrap">
<Spin spinning={props.loading} style={{ minHeight: 100 }}>
{resultList.length ? (
<Row gutter={[10, 10]}>
{resultList.map((item, index) => (
<Col span={12} key={item.name}>
<div onClick={(e) => handleSelect(e, item)}>
<HFModelItem
source={source}
tags={item.tags}
key={index}
title={item.name}
downloads={item.downloads}
likes={item.likes}
lastModified={item.lastModified}
active={item.id === props.current}
/>
</div>
</Col>
))}
</Row>
) : (
!props.loading && (
<Empty
imageStyle={{ height: 'auto', marginTop: '20px' }}
image={
<SearchOutlined
className="font-size-16"
style={{ color: 'var(--ant-color-text-tertiary)' }}
></SearchOutlined>
}
description="No models found"
/>
)
)}
</Spin>
</div>
);
};
export default React.memo(SearchResult);

@ -140,6 +140,11 @@ const Models: React.FC<ModelsProps> = ({
const handleModalOk = useCallback(
async (data: FormData) => {
try {
console.log('data:', data);
if (data.source === modelSourceMap.ollama_library_value) {
data.ollama_library_model_name = `${data.ollama_library_model_name}:${data.tag}`;
}
if (action === PageAction.CREATE) {
await createModel({ data });
}

@ -2,14 +2,45 @@ import { StatusMaps } from '@/config';
import { EditOutlined } from '@ant-design/icons';
export const ollamaModelOptions = [
{ label: 'llama3.1', value: 'llama3.1' },
{ label: 'llama3', value: 'llama3' },
{ label: 'gemma2', value: 'gemma2' },
{ label: 'mistral', value: 'mistral' },
{ label: 'qwen2', value: 'qwen2' },
{ label: 'phi3', value: 'phi3' },
{ label: 'codellama', value: 'codellama' },
{ label: 'deepseek-coder', value: 'deepseek-coder' }
{
label: 'llama3.1',
value: 'llama3.1',
name: 'llama3.1',
tags: ['Tools', '8B', '70B', '405B']
},
{ label: 'llama3', value: 'llama3', name: 'llama3', tags: ['8B', '70B'] },
{ label: 'gemma2', value: 'gemma2', name: 'gemma2', tags: ['9B', '27B'] },
{
label: 'mistral',
value: 'mistral',
name: 'mistral',
tags: ['Tools', '7B']
},
{
label: 'llava',
value: 'llava',
name: 'llava',
tags: ['Vision', '7B', '13B', '34B']
},
{
label: 'qwen2',
value: 'qwen2',
name: 'qwen2',
tags: ['0.5B', '1.5B', '7B', '72B']
},
{ label: 'phi3', value: 'phi3', name: 'phi3', tags: ['3B', '14B'] },
{
label: 'codellama',
value: 'codellama',
name: 'codellama',
tags: ['Code', '7B', '13B', '34B', '70B']
},
{
label: 'deepseek-coder',
value: 'deepseek-coder',
name: 'deepseek-coder',
tags: ['Code', '1B', '7B', '33B']
}
];
export const modelSourceMap: Record<string, string> = {

@ -0,0 +1,26 @@
.hf-model-item {
height: 80px;
display: flex;
flex-direction: column;
justify-content: space-between;
border: 1px solid var(--ant-color-border);
border-radius: var(--border-radius-base);
padding: 12px;
&:hover {
background-color: var(--color-fill-sider);
}
&.active {
background-color: var(--color-fill-sider);
}
.title {
font-size: var(--font-size-base);
font-weight: var(--font-weight-normal);
}
.info {
color: var(--ant-color-text-tertiary);
}
}

@ -0,0 +1,26 @@
.search-result-wrap {
background-color: rgba(255, 255, 255, 100%);
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 123px;
padding: 16px;
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
box-shadow: inset 0 -10px 10px rgba(5, 5, 5, 10%);
z-index: 100;
overflow-y: auto;
}
.search-bar {
display: flex;
position: absolute;
z-index: 100;
left: 0;
right: 0;
padding: 20px;
background: #fff;
border-bottom: 1px solid var(--ant-color-split);
box-shadow: 0 1px 2px rgba(5, 5, 5, 5%);
padding-top: 0;
}

@ -155,3 +155,16 @@ export const platformCall = () => {
isWin: isWin()
};
};
export const formatNumber = (num: number) => {
if (!num) {
return '0';
}
if (num >= 1000000) {
return (num / 1000000).toFixed(2) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(2) + 'k';
} else {
return num.toString();
}
};

Loading…
Cancel
Save