Files
gpustack-ui/src/pages/llmodels/components/model-item.tsx
T

254 lines
6.3 KiB
TypeScript

import {
IconFont,
StatusTag,
TagsWrapper,
TemplateCard,
ThemeTag
} from '@gpustack/core-ui';
import { useIntl, useNavigate } from '@umijs/max';
import { Button } from 'antd';
import _ from 'lodash';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { categoryConfig } from '../../_components/model-tag';
import {
modelCategories,
modelCategoriesMap,
modelSourceMap,
MyModelsStatusLabelMap,
MyModelsStatusMap,
MyModelsStatusValueMap
} from '../config';
import { categoryToPathMap } from '../config/button-actions';
const CardWrapper = styled.div`
&:hover {
.content {
.btn {
display: block;
}
}
}
`;
const Dot = styled.span`
background-color: var(--ant-color-text-quaternary);
border-radius: 50%;
flex: none;
height: 3px;
width: 3px;
`;
const ModelItemContent = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
cursor: default;
.content {
display: flex;
justify-content: space-between;
flex: 1;
}
.footer {
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-end;
.btn {
display: none;
}
}
.time {
color: var(--ant-color-text-secondary);
font-size: var(--font-size-small);
font-weight: 400;
}
.extra-info {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
.tag-item {
margin-right: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
height: 22px;
opacity: 0.7;
}
}
`;
const Header = styled.div`
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.anticon {
font-size: 16px;
color: var(--ant-color-text-secondary);
}
.text {
display: flex;
align-items: center;
font-size: var(--font-size-middle);
font-weight: 500;
color: var(--ant-color-text);
}
`;
const sourceIconMap = {
[modelSourceMap.local_path_value]: 'icon-hard-disk',
[modelSourceMap.huggingface_value]: 'icon-huggingface',
[modelSourceMap.modelscope_value]: 'icon-tu2'
};
const renderTag = (item: any, index = 0) => {
return (
<ThemeTag key={item} className="tag-item" color="purple">
{item}
</ThemeTag>
);
};
const ModelItem: React.FC<{
model: Record<string, any>;
onClick: (model: any) => void;
}> = (props) => {
const { model, onClick } = props;
const intl = useIntl();
const navigate = useNavigate();
const handleOpenPlayGround = () => {
for (const [category, path] of Object.entries(categoryToPathMap)) {
if (
model.categories?.includes(category) &&
[
modelCategoriesMap.text_to_speech,
modelCategoriesMap.speech_to_text
].includes(category)
) {
navigate(`${path}&model=${model.name}`);
return;
}
if (model.categories?.includes(category)) {
navigate(`${path}?model=${model.name}`);
return;
}
}
navigate(`/playground/chat?model=${model.name}`);
};
// context length
const maxToken = useMemo(() => {
const meta = model.meta || {};
const { max_model_len, n_ctx, n_slot, max_total_tokens } = meta || {};
let max_tokens: number = 0;
if (n_ctx && n_slot) {
max_tokens = _.divide(n_ctx, n_slot);
} else if (max_model_len) {
max_tokens = max_model_len;
} else if (max_total_tokens) {
max_tokens = max_total_tokens;
}
return _.round(max_tokens / 1024);
}, [model]);
return (
<CardWrapper>
<TemplateCard
height={140}
onClick={() => onClick(model)}
clickable={false}
hoverable={true}
ghost
header={
<Header>
<span className="text gap-8">
<IconFont
type={sourceIconMap[model.source]}
style={{ fontSize: 24 }}
/>
<span>{model.name}</span>
</span>
<StatusTag
maxTooltipWidth={400}
statusValue={{
status: MyModelsStatusMap[model.status],
text: intl.formatMessage({
id: MyModelsStatusLabelMap[model.status] || ''
}),
message: model.state_message
}}
></StatusTag>
</Header>
}
>
<ModelItemContent>
<div className="content">
<div className="footer">
<div className="extra-info">
{model.categories?.length > 0 &&
model.categories.map((sItem: string) => {
return (
<ThemeTag
icon={categoryConfig[sItem]?.icon}
key={sItem}
className="tag-item"
color={categoryConfig[sItem]?.color || 'blue'}
opacity={0.7}
>
{_.find(modelCategories, { value: sItem })?.label ||
sItem}
</ThemeTag>
);
})}
{maxToken > 0 && (
<>
<Dot></Dot>
<ThemeTag className="tag-item" color="purple">
{maxToken}K context
</ThemeTag>
</>
)}
{model.meta?.voices?.length > 0 && (
<>
<Dot></Dot>
<TagsWrapper
gap={8}
dataList={model.meta?.voices}
renderTag={renderTag}
></TagsWrapper>
</>
)}
</div>
{[MyModelsStatusValueMap.Active].includes(model.status) && (
<Button
size="middle"
className="btn"
type="primary"
onClick={handleOpenPlayGround}
>
{intl.formatMessage({ id: 'models.openinplayground' })}
</Button>
)}
</div>
</div>
</ModelItemContent>
</TemplateCard>
</CardWrapper>
);
};
export default ModelItem;