import ascendLogo from '@/assets/logo/ascend.png'; import CambriconPNG from '@/assets/logo/cambricon.png'; import hyponPNG from '@/assets/logo/hygon.png'; import iluvatarWEBP from '@/assets/logo/Iluvatar.png'; import jupyterLogo from '@/assets/logo/jupyter_logo.png'; import metaxLogo from '@/assets/logo/metax.png'; import mooreLogo from '@/assets/logo/moore-logo.png'; import nvidiaLogo from '@/assets/logo/nvidia.png'; import pytorchBlackLogo from '@/assets/logo/pytorch_black.png'; import pytorchLightLogo from '@/assets/logo/pytorch_light.png'; import sgLangLogo from '@/assets/logo/sglang.png'; import theadLogoEN from '@/assets/logo/t-head-en.png'; import theadLogoZH from '@/assets/logo/t-head-zh.png'; import tensorflowkLogo from '@/assets/logo/tensorflow.svg'; import ubuntuLogo from '@/assets/logo/ubuntu_logo.png'; import vllmLogo from '@/assets/logo/vllm.png'; import PluginExtraFields from '@/components/plugin-extra-fields'; import useUserSettings from '@/hooks/use-user-settings'; import OwnerTag from '@/pages/gpu-service/components/owner-tag'; import { GPUsConfigs, manfacturerValueMap } from '@/pages/resources/config/gpu-driver'; import { AutoTooltip, DropdownActions, IconFont, TemplateCard, ThemeTag } from '@gpustack/core-ui'; import { useAccess, useIntl } from '@umijs/max'; import { Button, Tag } from 'antd'; import { useMemo } from 'react'; import styled from 'styled-components'; import { manufactureColorMap, templateActions } from '../config'; import { ListItem } from '../config/types'; // Light theme logos const imageLogoLightMap = { vllm: vllmLogo, sglang: sgLangLogo, jupyter: jupyterLogo, pytorch: pytorchBlackLogo, tensorflow: tensorflowkLogo, ubuntu: ubuntuLogo } as const; // Dark theme logos: inherit light, override only the ones that need a variant const imageLogoDarkMap: typeof imageLogoLightMap = { ...imageLogoLightMap, pytorch: pytorchLightLogo }; const matchImageLogo = ( image: string | undefined, isDark: boolean ): { logo: string; type: string } | null => { if (!image) return null; const logoMap = imageLogoLightMap; const lower = image.toLowerCase(); let matched: keyof typeof logoMap | null = null; let earliest = Infinity; (Object.keys(logoMap) as Array).forEach((key) => { const idx = lower.indexOf(key); if (idx !== -1 && idx < earliest) { earliest = idx; matched = key; } }); return matched ? { logo: logoMap[matched], type: matched } : null; }; const StyledCard = styled(TemplateCard)` &:hover { .operations { background-color: var(--ant-color-fill-tertiary); border-radius: var(--ant-border-radius-lg); } } `; const Header = styled.div` display: flex; align-items: center; justify-content: space-between; height: 24px; width: 100%; .title { display: flex; align-items: center; gap: 8px; min-width: 0; } `; const CardName = styled.div` font-weight: 500; font-size: 14px; display: flex; align-items: center; color: var(--ant-color-text); gap: 8px; width: 100%; min-width: 0; `; const Content = styled.div` display: flex; flex-direction: column; align-items: flex-start; margin-top: 12px; gap: 12px; color: var(--ant-color-text-secondary); `; const InfoItem = styled.div` display: grid; width: 100%; grid-template-columns: max-content minmax(0, 1fr); align-items: center; gap: 8px; color: var(--ant-color-text-tertiary); .icon { color: var(--ant-color-text-quaternary); } .value { color: var(--ant-color-text-tertiary); } `; const LogoImg = styled.img` object-fit: contain; `; interface TemplateCardProps { data: ListItem; onSelect?: (item: { action: string; data: ListItem }) => void; } const TemplateCardItem: React.FC = ({ data, onSelect }) => { const intl = useIntl(); const access = useAccess(); const { isDarkTheme } = useUserSettings(); // Only an explicit NULL owner (Global, admin-curated) is admin-only. // A principal-owned row reaches a non-admin's list only when they own // it, and an absent id (single-owner builds omit it on the wire) has // no tenancy to restrict — both are manageable, so the check is strict // ``!== null``. Clone stays available regardless — it reads the source // and creates a fresh copy in the caller's own scope — so a non-admin // can fork a Global preset into their org. const canManage = !!access.canSeeAdmin || data.owner_principal_id !== null; const actions = useMemo( () => canManage ? templateActions : templateActions.filter((action) => action.key === 'clone'), [canManage] ); const manufacturerLabelMap: Record = useMemo(() => { return Object.values(GPUsConfigs).reduce( (acc, item) => { if (item.gpuVendor) acc[item.gpuVendor] = item.locales.locale ? intl.formatMessage({ id: item.locales.label }) : item.locales.label; return acc; }, { cpu: 'CPU' } as Record ); }, [intl]); const handleOnSelect = (item: any) => { onSelect?.({ action: item.key, data }); }; const handleonClickAction = (e: React.MouseEvent) => { e.stopPropagation(); }; const renderLogo = () => { const imageLogo = matchImageLogo(data.spec?.image, isDarkTheme); if (imageLogo?.logo) { return ( ); } switch (data.manufacturer) { case manfacturerValueMap.NVIDIA: return ; case manfacturerValueMap.AMD: return ( ); case manfacturerValueMap.ASCEND: return ; case manfacturerValueMap.HYGON: return ; case manfacturerValueMap.METAX: return ; case manfacturerValueMap.MOORE_THREADS: return ; case manfacturerValueMap.ILUVATAR: return ; case manfacturerValueMap.CAMBRICON: return ; case manfacturerValueMap.THEAD: return ( ); case 'cpu': return ( ); default: return null; } }; const renderManufacturerTag = () => { if (!data.manufacturer) return null; const label = manufacturerLabelMap[data.manufacturer] ?? data.manufacturer.toUpperCase(); const color = manufactureColorMap[data.manufacturer] ?? 'purple'; return ( {label} ); }; const renderActions = () => { return (