chore: model list ux

This commit is contained in:
jialin
2024-06-13 19:39:39 +08:00
parent c9ea96e078
commit 73c68dee1b
26 changed files with 589 additions and 204 deletions
+4 -2
View File
@@ -8,6 +8,7 @@ interface EditorwrapProps {
children: React.ReactNode; children: React.ReactNode;
showHeader?: boolean; showHeader?: boolean;
copyText: string; copyText: string;
defaultValue?: string;
langOptions?: { label: string; value: string }[]; langOptions?: { label: string; value: string }[];
onChangeLang?: (value: string) => void; onChangeLang?: (value: string) => void;
} }
@@ -17,6 +18,7 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
copyText, copyText,
langOptions, langOptions,
onChangeLang, onChangeLang,
defaultValue,
showHeader = true showHeader = true
}) => { }) => {
const handleChangeLang = (value: string) => { const handleChangeLang = (value: string) => {
@@ -30,7 +32,7 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
return ( return (
<div className="editor-header"> <div className="editor-header">
<Select <Select
defaultValue="JSON" defaultValue={defaultValue}
style={{ width: 120 }} style={{ width: 120 }}
size="middle" size="middle"
variant="filled" variant="filled"
@@ -51,4 +53,4 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
); );
}; };
export default EditorWrap; export default React.memo(EditorWrap);
+8
View File
@@ -5,6 +5,8 @@ type FormButtonsProps = {
onCancel?: () => void; onCancel?: () => void;
cancelText?: string; cancelText?: string;
okText?: string; okText?: string;
showOk?: boolean;
showCancel?: boolean;
htmlType?: 'submit' | 'button'; htmlType?: 'submit' | 'button';
}; };
const FormButtons: React.FC<FormButtonsProps> = ({ const FormButtons: React.FC<FormButtonsProps> = ({
@@ -12,10 +14,13 @@ const FormButtons: React.FC<FormButtonsProps> = ({
onCancel, onCancel,
cancelText, cancelText,
okText, okText,
showCancel = true,
showOk = true,
htmlType = 'button' htmlType = 'button'
}) => { }) => {
return ( return (
<Space size={40} style={{ marginTop: '80px' }}> <Space size={40} style={{ marginTop: '80px' }}>
{showOk && (
<Button <Button
type="primary" type="primary"
onClick={onOk} onClick={onOk}
@@ -24,9 +29,12 @@ const FormButtons: React.FC<FormButtonsProps> = ({
> >
{okText || '保存'} {okText || '保存'}
</Button> </Button>
)}
{showCancel && (
<Button onClick={onCancel} style={{ width: '98px' }}> <Button onClick={onCancel} style={{ width: '98px' }}>
{cancelText || '取消'} {cancelText || '取消'}
</Button> </Button>
)}
</Space> </Space>
); );
}; };
+36
View File
@@ -0,0 +1,36 @@
import { Progress } from 'antd';
import { memo, useMemo } from 'react';
const RenderProgress = memo(
(props: { percent: number; steps?: number; download?: boolean }) => {
const { percent, steps = 5, download } = props;
const strokeColor = useMemo(() => {
if (download) {
return 'var(--ant-color-primary)';
}
if (percent <= 50) {
return 'var(--ant-color-primary)';
}
if (percent <= 80) {
return 'var(--ant-color-warning)';
}
return 'var(--ant-color-error)';
}, [percent]);
return (
<Progress
steps={steps}
format={() => {
return (
<span style={{ color: 'var(--ant-color-text)' }}>{percent}%</span>
);
}}
percent={percent}
strokeColor={strokeColor}
/>
);
}
);
export default RenderProgress;
@@ -0,0 +1,86 @@
import { AutoComplete, Form } from 'antd';
import type { AutoCompleteProps } from 'antd/lib';
import { useEffect, useRef, useState } from 'react';
import Wrapper from './components/wrapper';
import { SealFormItemProps } from './types';
const SealAutoComplete: React.FC<AutoCompleteProps & SealFormItemProps> = (
props
) => {
const {
label,
placeholder,
required,
description,
isInFormItems = true,
...rest
} = props;
const [isFocus, setIsFocus] = useState(false);
const inputRef = useRef<any>(null);
let status = '';
if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || '';
}
useEffect(() => {
if (props.value) {
setIsFocus(true);
}
}, [props.value]);
const handleClickWrapper = () => {
if (!props.disabled && !isFocus) {
inputRef.current?.focus?.();
setIsFocus(true);
}
};
const handleChange = (value: string, option: any) => {
props.onChange?.(value, option);
};
const handleOnFocus = (e: any) => {
setIsFocus(true);
props.onFocus?.(e);
};
const handleOnBlur = (e: any) => {
if (!props.value) {
setIsFocus(false);
props.onBlur?.(e);
}
};
const handleSearch = (text: string) => {
props.onSearch?.(text);
};
const handleOnSelect = (value: any, option: any) => {
props.onSelect?.(value, option);
};
return (
<Wrapper
status={status}
label={label || (placeholder as string)}
isFocus={isFocus}
required={required}
description={description}
disabled={props.disabled}
onClick={handleClickWrapper}
>
<AutoComplete
{...rest}
ref={inputRef}
onSelect={handleOnSelect}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onSearch={handleSearch}
onChange={handleChange}
></AutoComplete>
</Wrapper>
);
};
export default SealAutoComplete;
@@ -5,7 +5,7 @@ import './label-info.less';
interface NoteInfoProps { interface NoteInfoProps {
required?: boolean; required?: boolean;
label: string; label: React.ReactNode;
description?: React.ReactNode; description?: React.ReactNode;
} }
const NoteInfo: React.FC<NoteInfoProps> = (props) => { const NoteInfo: React.FC<NoteInfoProps> = (props) => {
@@ -4,7 +4,7 @@ import LabelInfo from './label-info';
import wrapperStyle from './wrapper.less'; import wrapperStyle from './wrapper.less';
interface WrapperProps { interface WrapperProps {
children: React.ReactNode; children: React.ReactNode;
label: string; label: React.ReactNode;
isFocus: boolean; isFocus: boolean;
status?: string; status?: string;
required?: boolean; required?: boolean;
+2 -1
View File
@@ -1,9 +1,10 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react';
import LabelInfo from './components/label-info'; import LabelInfo from './components/label-info';
import wrapperStyle from './components/wrapper.less'; import wrapperStyle from './components/wrapper.less';
interface WrapperProps { interface WrapperProps {
children: React.ReactNode; children: React.ReactNode;
label: string; label: React.ReactNode;
status?: string; status?: string;
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
-4
View File
@@ -64,10 +64,6 @@ const SealTextArea: React.FC<TextAreaProps & SealFormItemProps> = (props) => {
const handleOnBlur = useCallback( const handleOnBlur = useCallback(
(e: any) => { (e: any) => {
console.log(
'inputRef.current==========',
inputRef.current?.resizableTextArea?.textArea?.value
);
if (!inputRef.current?.resizableTextArea?.textArea?.value) { if (!inputRef.current?.resizableTextArea?.textArea?.value) {
setIsFocus(false); setIsFocus(false);
onBlur?.(e); onBlur?.(e);
+1
View File
@@ -5,5 +5,6 @@
padding: 2px 10px; padding: 2px 10px;
border-radius: 20px; border-radius: 20px;
width: fit-content; width: fit-content;
height: 26px;
font-size: var(--font-size-base); font-size: var(--font-size-base);
} }
+18 -7
View File
@@ -1,7 +1,8 @@
import { StatusColorMap } from '@/config'; import { StatusColorMap } from '@/config';
import { StatusType } from '@/config/types'; import { StatusType } from '@/config/types';
import classNames from 'classnames';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import styles from './index.less'; import './index.less';
export const StatusMaps = { export const StatusMaps = {
transitioning: 'blue', transitioning: 'blue',
@@ -16,9 +17,12 @@ type StatusTagProps = {
status: StatusType; status: StatusType;
text: string; text: string;
}; };
download?: {
percent: number;
};
}; };
const StatusTag: React.FC<StatusTagProps> = ({ statusValue }) => { const StatusTag: React.FC<StatusTagProps> = ({ statusValue, download }) => {
const { text, status } = statusValue; const { text, status } = statusValue;
const [statusColor, setStatusColor] = useState<{ text: string; bg: string }>({ const [statusColor, setStatusColor] = useState<{ text: string; bg: string }>({
text: '', text: '',
@@ -31,11 +35,18 @@ const StatusTag: React.FC<StatusTagProps> = ({ statusValue }) => {
return ( return (
<span <span
className={styles['status-tag']} className={classNames('status-tag', { download: download })}
style={{ style={
color: statusColor.text, download
border: `1px solid ${statusColor.text}` ? {
}} color: StatusColorMap['success']['text'],
border: `1px solid ${StatusColorMap['success']['text']}`
}
: {
color: statusColor?.text,
border: `1px solid ${statusColor?.text}`
}
}
> >
{text} {text}
</span> </span>
+2 -13
View File
@@ -1,4 +1,5 @@
import PageTools from '@/components/page-tools'; import PageTools from '@/components/page-tools';
import ProgressBar from '@/components/progress-bar';
import StatusTag from '@/components/status-tag'; import StatusTag from '@/components/status-tag';
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';
@@ -6,7 +7,6 @@ import { SyncOutlined } from '@ant-design/icons';
import { Button, Input, Space, Table } from 'antd'; import { Button, Input, Space, Table } from 'antd';
import { useState } from 'react'; import { useState } from 'react';
import { Gpu } from '../config/types'; import { Gpu } from '../config/types';
import RenderProgress from './render-progress';
const { Column } = Table; const { Column } = Table;
const dataSource: Gpu[] = [ const dataSource: Gpu[] = [
@@ -205,18 +205,7 @@ const Models: React.FC = () => {
dataIndex="GRAM" dataIndex="GRAM"
key="VRAM" key="VRAM"
render={(text, record: Gpu) => { render={(text, record: Gpu) => {
return <RenderProgress percent={0}></RenderProgress>; return <ProgressBar percent={0}></ProgressBar>;
}}
/>
<Column
title="Operation"
key="operation"
render={(text, record) => {
return (
<Space>
<Button size="middle">Logs</Button>
</Space>
);
}} }}
/> />
</Table> </Table>
+8 -19
View File
@@ -1,4 +1,5 @@
import PageTools from '@/components/page-tools'; import PageTools from '@/components/page-tools';
import ProgressBar from '@/components/progress-bar';
import StatusTag from '@/components/status-tag'; import StatusTag from '@/components/status-tag';
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';
@@ -8,7 +9,6 @@ import _ from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { queryNodesList } from '../apis'; import { queryNodesList } from '../apis';
import { ListItem } from '../config/types'; import { ListItem } from '../config/types';
import RenderProgress from './render-progress';
const { Column } = Table; const { Column } = Table;
const Models: React.FC = () => { const Models: React.FC = () => {
@@ -138,9 +138,9 @@ const Models: React.FC = () => {
key="CPU" key="CPU"
render={(text, record: ListItem) => { render={(text, record: ListItem) => {
return ( return (
<RenderProgress <ProgressBar
percent={_.round(record?.status?.cpu.utilization_rate, 2)} percent={_.round(record?.status?.cpu.utilization_rate, 2)}
></RenderProgress> ></ProgressBar>
); );
}} }}
/> />
@@ -150,12 +150,12 @@ const Models: React.FC = () => {
key="Memory" key="Memory"
render={(text, record: ListItem) => { render={(text, record: ListItem) => {
return ( return (
<RenderProgress <ProgressBar
percent={formateUtilazation( percent={formateUtilazation(
record?.status?.memory.used, record?.status?.memory.used,
record?.status?.memory.total record?.status?.memory.total
)} )}
></RenderProgress> ></ProgressBar>
); );
}} }}
/> />
@@ -165,14 +165,14 @@ const Models: React.FC = () => {
key="GPU" key="GPU"
render={(text, record: ListItem) => { render={(text, record: ListItem) => {
return ( return (
<RenderProgress <ProgressBar
percent={_.get(record, [ percent={_.get(record, [
'status', 'status',
'gpu', 'gpu',
'0', '0',
'core_utilization_rate' 'core_utilization_rate'
])} ])}
></RenderProgress> ></ProgressBar>
); );
}} }}
/> />
@@ -181,18 +181,7 @@ const Models: React.FC = () => {
dataIndex="VRAM" dataIndex="VRAM"
key="VRAM" key="VRAM"
render={(text, record: ListItem) => { render={(text, record: ListItem) => {
return <RenderProgress percent={0}></RenderProgress>; return <ProgressBar percent={0}></ProgressBar>;
}}
/>
<Column
title="Operation"
key="operation"
render={(text, record) => {
return (
<Space>
<Button size="middle">Logs</Button>
</Space>
);
}} }}
/> />
</Table> </Table>
@@ -1,30 +0,0 @@
import { Progress } from 'antd';
import { memo, useMemo } from 'react';
const RenderProgress = memo((props: { percent: number }) => {
const { percent } = props;
console.log('percent====', percent);
const strokeColor = useMemo(() => {
if (percent <= 50) {
return 'var(--ant-color-primary)';
}
if (percent <= 80) {
return 'var(--ant-color-warning)';
}
return 'var(--ant-color-error)';
}, [percent]);
return (
<Progress
steps={10}
format={() => {
return (
<span style={{ color: 'var(--ant-color-text)' }}>{percent}%</span>
);
}}
percent={percent}
strokeColor={strokeColor}
/>
);
});
export default RenderProgress;
+23 -2
View File
@@ -1,5 +1,10 @@
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { FormData, ListItem, ModelInstanceListItem } from '../config/types'; import {
FormData,
ListItem,
ModelInstanceFormData,
ModelInstanceListItem
} from '../config/types';
export const MODELS_API = '/models'; export const MODELS_API = '/models';
@@ -55,7 +60,9 @@ export async function queryModelInstancesList(
); );
} }
export async function createModelInstance(params: { data: FormData }) { export async function createModelInstance(params: {
data: ModelInstanceFormData;
}) {
return request(`${MODEL_INSTANCE_API}`, { return request(`${MODEL_INSTANCE_API}`, {
method: 'POST', method: 'POST',
data: params.data data: params.data
@@ -91,3 +98,17 @@ export async function queryModelInstanceLogs(id: number) {
} }
// ===================== Model Instances end ===================== // ===================== Model Instances end =====================
// ===================== call huggingface quicksearch api =====================
export async function callHuggingfaceQuickSearch(params: any) {
return request<{
models: Array<{
id: string;
_id: string;
}>;
}>(`https://huggingface.co/api/quicksearch`, {
method: 'GET',
params
});
}
+82 -7
View File
@@ -1,10 +1,13 @@
import ModalFooter from '@/components/modal-footer'; import ModalFooter from '@/components/modal-footer';
import SealAutoComplete from '@/components/seal-form/auto-complete';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { Form, Modal } from 'antd'; import { Form, Modal } from 'antd';
import { useEffect } from 'react'; import _ from 'lodash';
import { useEffect, useState } from 'react';
import { callHuggingfaceQuickSearch } from '../apis';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
type AddModalProps = { type AddModalProps = {
@@ -17,16 +20,20 @@ type AddModalProps = {
const sourceOptions = [ const sourceOptions = [
{ label: 'Huggingface', value: 'huggingface', key: 'huggingface' }, { label: 'Huggingface', value: 'huggingface', key: 'huggingface' },
{ label: 'Ollama', value: 'ollama_library', key: 'ollama_library' },
{ label: 'S3', value: 's3', key: 's3' } { label: 'S3', value: 's3', key: 's3' }
]; ];
const AddModal: React.FC<AddModalProps> = (props) => { const AddModal: React.FC<AddModalProps> = (props) => {
const { title, action, open, onOk, onCancel } = props || {}; const { title, action, open, onOk, onCancel } = props || {};
if (!open) {
return null;
}
const [form] = Form.useForm(); const [form] = Form.useForm();
const modelSource = Form.useWatch('source', form); const modelSource = Form.useWatch('source', form);
const [repoOptions, setRepoOptions] = useState<
{ label: string; value: string }[]
>([]);
const [fileOptions, setFileOptions] = useState<
{ label: string; value: string }[]
>([]);
const initFormValue = () => { const initFormValue = () => {
if (action === PageAction.CREATE && open) { if (action === PageAction.CREATE && open) {
@@ -40,6 +47,33 @@ const AddModal: React.FC<AddModalProps> = (props) => {
initFormValue(); initFormValue();
}, [open]); }, [open]);
const handleInputRepoChange = (value: string) => {
console.log('repo change', value);
};
const debounceSearch = _.debounce((text: string) => {
handleOnSearchRepo(text);
}, 300);
const handleOnSearchRepo = async (text: string) => {
try {
const params = {
q: text,
type: 'model'
};
const res = await callHuggingfaceQuickSearch(params);
const list = _.map(res.models || [], (item: any) => {
return {
value: item.id,
label: item.id
};
});
setRepoOptions(list);
} catch (error) {
setRepoOptions([]);
}
};
const renderHuggingfaceFields = () => { const renderHuggingfaceFields = () => {
return ( return (
<> <>
@@ -47,11 +81,18 @@ const AddModal: React.FC<AddModalProps> = (props) => {
name="huggingface_repo_id" name="huggingface_repo_id"
rules={[{ required: true }]} rules={[{ required: true }]}
> >
<SealInput.Input label="Repo ID" required></SealInput.Input> <SealAutoComplete
label="Repo ID"
required
showSearch
onChange={handleInputRepoChange}
onSearch={debounceSearch}
options={repoOptions}
></SealAutoComplete>
</Form.Item> </Form.Item>
<Form.Item<FormData> <Form.Item<FormData>
name="huggingface_filename" name="huggingface_filename"
rules={[{ required: false }]} rules={[{ required: true }]}
> >
<SealInput.Input label="File Name" required></SealInput.Input> <SealInput.Input label="File Name" required></SealInput.Input>
</Form.Item> </Form.Item>
@@ -69,6 +110,32 @@ const AddModal: React.FC<AddModalProps> = (props) => {
); );
}; };
const renderOllamaModelFields = () => {
return (
<>
<Form.Item<FormData>
name="ollama_library_model_name"
rules={[{ required: true }]}
>
<SealInput.Input label="Model Name" required></SealInput.Input>
</Form.Item>
</>
);
};
const renderFieldsBySource = () => {
switch (modelSource) {
case 'huggingface':
return renderHuggingfaceFields();
case 'ollama_library':
return renderOllamaModelFields();
case 's3':
return renderS3Fields();
default:
return null;
}
};
const handleSourceChange = (value: string) => { const handleSourceChange = (value: string) => {
console.log('source change', value); console.log('source change', value);
}; };
@@ -111,7 +178,15 @@ const AddModal: React.FC<AddModalProps> = (props) => {
onChange={handleSourceChange} onChange={handleSourceChange}
></SealSelect> ></SealSelect>
</Form.Item> </Form.Item>
{modelSource === 's3' ? renderS3Fields() : renderHuggingfaceFields()} {renderFieldsBySource()}
<Form.Item<FormData> name="replicas" rules={[{ required: true }]}>
<SealInput.Number
style={{ width: '100%' }}
label="Replicas"
required
min={1}
></SealInput.Number>
</Form.Item>
<Form.Item<FormData> name="description"> <Form.Item<FormData> name="description">
<SealInput.TextArea label="Description"></SealInput.TextArea> <SealInput.TextArea label="Description"></SealInput.TextArea>
</Form.Item> </Form.Item>
@@ -0,0 +1,35 @@
import { Modal } from 'antd';
import React from 'react';
type ViewModalProps = {
content?: string;
title: string;
open: boolean;
onCancel: () => void;
};
const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
const { title, open, onCancel, content } = props || {};
if (!open) {
return null;
}
return (
<Modal
title={title}
open={open}
onCancel={onCancel}
destroyOnClose={true}
closeIcon={true}
maskClosable={false}
keyboard={false}
width={600}
style={{ top: '80px' }}
footer={null}
>
<div>{content}</div>
</Modal>
);
};
export default ViewCodeModal;
+10
View File
@@ -15,7 +15,9 @@ export interface FormData {
huggingface_repo_id: string; huggingface_repo_id: string;
huggingface_filename: string; huggingface_filename: string;
s3_address: string; s3_address: string;
ollama_library_model_name: 'string';
name: string; name: string;
replicas: number;
description: string; description: string;
} }
@@ -36,3 +38,11 @@ export interface ModelInstanceListItem {
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
export interface ModelInstanceFormData {
model_id: number;
model_name: string;
source: string;
huggingface_repo_id: 'string';
huggingface_filename: 'string';
}
+70 -9
View File
@@ -1,4 +1,5 @@
import PageTools from '@/components/page-tools'; import PageTools from '@/components/page-tools';
import ProgressBar from '@/components/progress-bar';
import SealTable from '@/components/seal-table'; import SealTable from '@/components/seal-table';
import RowChildren from '@/components/seal-table/components/row-children'; import RowChildren from '@/components/seal-table/components/row-children';
import SealColumn from '@/components/seal-table/components/seal-column'; import SealColumn from '@/components/seal-table/components/seal-column';
@@ -33,12 +34,15 @@ import _ from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
createModel, createModel,
createModelInstance,
deleteModel, deleteModel,
deleteModelInstance, deleteModelInstance,
queryModelInstanceLogs,
queryModelInstancesList, queryModelInstancesList,
queryModelsList queryModelsList
} from './apis'; } from './apis';
import AddModal from './components/add-modal'; import AddModal from './components/add-modal';
import ViewLogsModal from './components/view-logs-modal';
import { status } from './config'; import { status } from './config';
import { FormData, ListItem, ModelInstanceListItem } from './config/types'; import { FormData, ListItem, ModelInstanceListItem } from './config/types';
@@ -51,6 +55,9 @@ const Models: React.FC = () => {
const { sortOrder, setSortOrder } = useTableSort({ const { sortOrder, setSortOrder } = useTableSort({
defaultSortOrder: 'descend' defaultSortOrder: 'descend'
}); });
const [logContent, setLogContent] = useState('');
const [openLogModal, setOpenLogModal] = useState(false);
const [hoverChildIndex, setHoverChildIndex] = useState(-1);
const [total, setTotal] = useState(100); const [total, setTotal] = useState(100);
const [openAddModal, setOpenAddModal] = useState(false); const [openAddModal, setOpenAddModal] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -132,6 +139,10 @@ const Models: React.FC = () => {
console.log('handleModalCancel'); console.log('handleModalCancel');
setOpenAddModal(false); setOpenAddModal(false);
}; };
const handleLogModalCancel = () => {
setOpenLogModal(false);
};
const handleDelete = async (row: any) => { const handleDelete = async (row: any) => {
Modal.confirm({ Modal.confirm({
title: '', title: '',
@@ -165,8 +176,26 @@ const Models: React.FC = () => {
navigate(`/playground?model=${row.name}`); navigate(`/playground?model=${row.name}`);
}; };
const handleViewLogs = (row: any) => { const handleDeployInstance = async (row: any) => {
console.log('handleViewLogs', row); try {
const data = {
model_id: row.id,
model_name: row.name,
huggingface_repo_id: row.huggingface_repo_id,
huggingface_filename: row.huggingface_filename,
source: row.source
};
await createModelInstance({ data });
message.success('successfully!');
} catch (error) {}
};
const handleViewLogs = async (row: any) => {
try {
const data = await queryModelInstanceLogs(row.id);
setLogContent(data);
setOpenLogModal(true);
} catch (error) {}
}; };
const handleDeleteInstace = (row: any) => { const handleDeleteInstace = (row: any) => {
Modal.confirm({ Modal.confirm({
@@ -184,6 +213,14 @@ const Models: React.FC = () => {
}); });
}; };
const handleOnMouseEnter = (index: number) => {
setHoverChildIndex(index);
};
const handleOnMouseLeave = () => {
setHoverChildIndex(-1);
};
const getModelInstances = async (row: any) => { const getModelInstances = async (row: any) => {
const params = { const params = {
id: row.id, id: row.id,
@@ -196,28 +233,45 @@ const Models: React.FC = () => {
const renderChildren = (list: any) => { const renderChildren = (list: any) => {
return ( return (
<> <Space size={16} direction="vertical" style={{ width: '100%' }}>
{_.map(list, (item: ModelInstanceListItem) => { {_.map(list, (item: ModelInstanceListItem, index: number) => {
return ( return (
<RowChildren key={item.id}> <div
key={`${item.id}`}
onMouseEnter={() => handleOnMouseEnter(index)}
onMouseLeave={handleOnMouseLeave}
>
<RowChildren>
<Row style={{ width: '100%' }} align="middle"> <Row style={{ width: '100%' }} align="middle">
<Col span={4}> <Col span={4}>
{item.node_ip}:{item.port} {item.node_ip}:{item.port}
</Col> </Col>
<Col span={5}>{item.huggingface_repo_id}</Col> <Col span={5}>
<span>{item.huggingface_repo_id}</span>
<div style={{ marginTop: '4px' }}>
<ProgressBar
download
percent={item.download_progress || 0}
></ProgressBar>
</div>
</Col>
<Col span={4}> <Col span={4}>
{dayjs(item.updated_at).format('YYYY-MM-DD HH:mm:ss')} {dayjs(item.updated_at).format('YYYY-MM-DD HH:mm:ss')}
</Col> </Col>
<Col span={4}> <Col span={4}>
{item.state && (
<StatusTag <StatusTag
download={{ percent: 10 }}
statusValue={{ statusValue={{
status: status[item.state] as any, status: status[item.state] as any,
text: item.state text: item.state
}} }}
></StatusTag> ></StatusTag>
)}
</Col> </Col>
<Col span={7}> <Col span={7}>
<Space> {hoverChildIndex === index && (
<Space size={20}>
<Tooltip title="Delete"> <Tooltip title="Delete">
<Button <Button
size="small" size="small"
@@ -234,12 +288,14 @@ const Models: React.FC = () => {
></Button> ></Button>
</Tooltip> </Tooltip>
</Space> </Space>
)}
</Col> </Col>
</Row> </Row>
</RowChildren> </RowChildren>
</div>
); );
})} })}
</> </Space>
); );
}; };
@@ -356,7 +412,7 @@ const Models: React.FC = () => {
key="operation" key="operation"
render={(text, record) => { render={(text, record) => {
return !record.transition ? ( return !record.transition ? (
<Space> <Space size={20}>
<Tooltip title="Open in PlayGround"> <Tooltip title="Open in PlayGround">
<Button <Button
size="small" size="small"
@@ -387,6 +443,11 @@ const Models: React.FC = () => {
onCancel={handleModalCancel} onCancel={handleModalCancel}
onOk={handleModalOk} onOk={handleModalOk}
></AddModal> ></AddModal>
<ViewLogsModal
title="View Logs"
open={openLogModal}
onCancel={handleLogModalCancel}
></ViewLogsModal>
</> </>
); );
}; };
@@ -48,7 +48,7 @@ const ChatFooter: React.FC<ChatFooterProps> = (props) => {
onClick={onView} onClick={onView}
disabled={disabled} disabled={disabled}
> >
View View Code
</Button> </Button>
<Button <Button
disabled={disabled} disabled={disabled}
@@ -179,6 +179,9 @@ const MessageList: React.FC<MessageProps> = (props) => {
</div> </div>
<ViewCodeModal <ViewCodeModal
open={show} open={show}
systemMessage={systemMessage}
messageList={messageList}
parameters={parameters}
onCancel={handleCloseViewCode} onCancel={handleCloseViewCode}
title="View code" title="View code"
></ViewCodeModal> ></ViewCodeModal>
@@ -22,11 +22,6 @@ type ParamsSettingsProps = {
params?: ParamsSettingsFormProps; params?: ParamsSettingsFormProps;
setParams: (params: any) => void; setParams: (params: any) => void;
}; };
// const dataList = [
// { value: 'llama3:latest', label: 'llama3:latest' },
// { value: 'wangfuyun/AnimateLCM', label: 'wangfuyun/AnimateLCM' },
// { value: 'Revanthraja/Text_to_Vision', label: 'Revanthraja/Text_to_Vision' }
// ];
const ParamsSettings: React.FC<ParamsSettingsProps> = ({ const ParamsSettings: React.FC<ParamsSettingsProps> = ({
onClose, onClose,
@@ -126,7 +121,6 @@ const ParamsSettings: React.FC<ParamsSettingsProps> = ({
max={2} max={2}
step={0.1} step={0.1}
style={{ marginBottom: 0 }} style={{ marginBottom: 0 }}
tooltip={{ open: true }}
></Slider> ></Slider>
</FieldWrapper> </FieldWrapper>
</Form.Item> </Form.Item>
@@ -149,7 +143,6 @@ const ParamsSettings: React.FC<ParamsSettingsProps> = ({
max={1} max={1}
step={0.1} step={0.1}
style={{ marginBottom: 0 }} style={{ marginBottom: 0 }}
tooltip={{ open: true }}
></Slider> ></Slider>
</FieldWrapper> </FieldWrapper>
</Form.Item> </Form.Item>
@@ -1,50 +1,135 @@
import EditorWrap from '@/components/editor-wrap'; import EditorWrap from '@/components/editor-wrap';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import { Modal } from 'antd'; import { Modal, Spin } from 'antd';
import React, { useRef, useState } from 'react'; import _ from 'lodash';
import React, { useEffect, useRef, useState } from 'react';
type ViewModalProps = { type ViewModalProps = {
systemMessage?: string;
messageList: any[];
parameters: any;
title: string; title: string;
open: boolean; open: boolean;
onCancel: () => void; onCancel: () => void;
}; };
const ViewCodeModal: React.FC<ViewModalProps> = (props) => { const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
const { title, open, onCancel } = props || {}; const {
if (!open) { title,
return null; open,
} onCancel,
systemMessage,
messageList,
parameters = {}
} = props || {};
const editorRef = useRef(null); const editorRef = useRef(null);
const [loaded, setLoaded] = useState(false);
const [codeValue, setCodeValue] = useState(''); const [codeValue, setCodeValue] = useState('');
const [lang, setLang] = useState('json'); const [lang, setLang] = useState('shell');
const langOptions = [ const langOptions = [
{ label: 'curl', value: 'curl' }, { label: 'Curl', value: 'shell' },
{ label: 'JSON', value: 'json' }, { label: 'Python', value: 'python' },
{ label: 'JavaScript', value: 'javascript' }, { label: 'Nodejs', value: 'javascript' }
{ label: 'Python', value: 'python' }
]; ];
useEffect(() => {
generateCode();
}, [lang, systemMessage, messageList, parameters]);
const generateCode = () => {
if (lang === 'shell') {
const systemList = systemMessage
? [{ role: 'system', content: systemMessage }]
: [];
const code = `curl ${window.location.origin}/v1/chat/completions \n-H "Content-Type: application/json" \n-H "Authorization: Bearer $\{GPUSTACK_API_KEY}\" \n-d '${JSON.stringify(
{
...parameters,
messages: [...systemList, ...messageList]
},
null,
2
)}'`;
setCodeValue(code);
} else if (lang === 'javascript') {
const systemList = systemMessage
? [{ role: 'system', content: systemMessage }]
: [];
const code = `import OpenAI from "openai";\nconst openai = new OpenAI();\n\nasync function main(){\nconst params = ${JSON.stringify(
{
...parameters,
messages: [...systemList, ...messageList]
},
null,
2
)};\nconst chatCompletion = await openai.chat.completions.create(params);\nfor await (const chunk of chatCompletion) {\n process.stdout.write(chunk.choices[0]?.delta?.content || '');\n}\n}\nmain();`;
setCodeValue(code);
} else if (lang === 'python') {
const formattedParams = _.keys(parameters).reduce(
(acc: string, key: string) => {
if (parameters[key] === null) {
return acc;
}
const value =
typeof parameters[key] === 'string'
? `"${parameters[key]}"`
: parameters[key];
return acc + ` ${key}=${value},\n`;
},
''
);
const systemList = systemMessage
? [{ role: 'system', content: systemMessage }]
: [];
const code = `from openai import OpenAI\nclient = OpenAI()\n\ncompletion = client.chat.completions.create(\n${formattedParams} messages=${JSON.stringify([...systemList, ...messageList], null, 2)})\nprint(completion.choices[0].message)`;
setCodeValue(code);
}
formatCode();
};
const handleEditorDidMount = (editor: any, monaco: any) => { const handleEditorDidMount = (editor: any, monaco: any) => {
editorRef.current = editor; editorRef.current = editor;
setLoaded(true);
}; };
function formatCode() {
if (editorRef.current) {
setTimeout(() => {
editorRef.current
?.getAction?.('editor.action.formatDocument')
?.run()
.then(() => {
console.log('format success');
});
}, 100);
}
}
const handleOnChangeLang = (value: string) => { const handleOnChangeLang = (value: string) => {
setLang(value); setLang(value);
}; };
const handleClose = () => {
setLang('shell');
onCancel();
};
const editorConfig = { const editorConfig = {
minimap: { minimap: {
enabled: false enabled: false
}, },
formatOnType: true,
formatOnPaste: true,
scrollbar: { scrollbar: {
verticalSliderSize: 8 verticalSliderSize: 8
} }
}; };
return ( return (
<>
<Modal <Modal
title={title} title={title}
open={open} open={open}
onCancel={onCancel} onCancel={handleClose}
destroyOnClose={true} destroyOnClose={true}
closeIcon={true} closeIcon={true}
maskClosable={false} maskClosable={false}
@@ -54,30 +139,35 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
footer={null} footer={null}
> >
<div style={{ marginBottom: '10px' }}> <div style={{ marginBottom: '10px' }}>
You can use the following code to start integrating your current prompt You can use the following code to start integrating your current
and settings into your application. prompt and settings into your application.
</div> </div>
<Spin spinning={!loaded}>
<EditorWrap <EditorWrap
copyText={codeValue} copyText={codeValue}
langOptions={langOptions} langOptions={langOptions}
defaultValue="shell"
showHeader={loaded}
onChangeLang={handleOnChangeLang} onChangeLang={handleOnChangeLang}
> >
<Editor <Editor
height="400px" height="400px"
theme="vs-dark" theme="vs-dark"
className="monaco-editor" className="monaco-editor"
defaultLanguage="javascript" defaultLanguage="shell"
defaultValue="// some comment"
language={lang} language={lang}
value={codeValue}
options={editorConfig} options={editorConfig}
onMount={handleEditorDidMount} onMount={handleEditorDidMount}
/> />
</EditorWrap> </EditorWrap>
</Spin>
<div style={{ marginTop: '10px' }}> <div style={{ marginTop: '10px' }}>
our API Key can be foundhere You should use environment variables or a our API Key can be foundhere You should use environment variables or a
secret management tool to expose your key to your applications. secret management tool to expose your key to your applications.
</div> </div>
</Modal> </Modal>
</>
); );
}; };
+5 -1
View File
@@ -72,7 +72,11 @@ const Profile: React.FC = () => {
style={{ width: INPUT_WIDTH.default }} style={{ width: INPUT_WIDTH.default }}
></SealInput.Password> ></SealInput.Password>
</Form.Item> </Form.Item>
<FormButtons htmlType="submit" onCancel={handleCancel}></FormButtons> <FormButtons
htmlType="submit"
onCancel={handleCancel}
showCancel={false}
></FormButtons>
</Form> </Form>
</PageContainer> </PageContainer>
</StrictMode> </StrictMode>
+3 -2
View File
@@ -214,7 +214,7 @@ const Models: React.FC = () => {
onChange: handlePageChange onChange: handlePageChange
}} }}
> >
<Column title="Name" dataIndex="name" key="name" width={400} /> <Column title="Name" dataIndex="name" key="name" width={200} />
<Column <Column
title="Create Time" title="Create Time"
dataIndex="created_at" dataIndex="created_at"
@@ -243,9 +243,10 @@ const Models: React.FC = () => {
<Column <Column
title="Operation" title="Operation"
key="operation" key="operation"
width={200}
render={(text, record: ListItem) => { render={(text, record: ListItem) => {
return ( return (
<Space> <Space size={20}>
<Tooltip title="编辑"> <Tooltip title="编辑">
<Button <Button
size="small" size="small"
-4
View File
@@ -1,4 +0,0 @@
// 示例方法,没有实际意义
export function trim(str: string) {
return str.trim();
}
+7
View File
@@ -4,3 +4,10 @@ export const isNotEmptyValue = (value: any) => {
} }
return value !== null && value !== undefined && value !== ''; return value !== null && value !== undefined && value !== '';
}; };
export const handleBatchRequest = async (
list: any[],
fn: (args: any) => void
) => {
return Promise.all(list.map((item) => fn(item)));
};