diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40a267c4..1ed83526 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,6 +128,9 @@ dependencies: rc-resize-observer: specifier: ^1.4.0 version: 1.4.0(react-dom@18.2.0)(react@18.2.0) + rc-tabs: + specifier: ^15.5.0 + version: 15.5.0(react-dom@18.2.0)(react@18.2.0) rc-virtual-list: specifier: ^3.14.8 version: 3.14.8(react-dom@18.2.0)(react@18.2.0) @@ -5208,7 +5211,7 @@ packages: resolution: {integrity: sha512-hy8b7Y1J8OGe6LbAjj3xniQrj3v6lsivCcrmf4TzSgPzLkhIeKgc5IZnT7ReIqmEuodjfO8EYAuoFvIrHi/+jQ==, tarball: https://registry.npmjs.org/@types/history/-/history-5.0.0.tgz} deprecated: This is a stub types definition. history provides its own type definitions, so you do not need this installed. dependencies: - history: 4.10.1 + history: 5.3.0 dev: false /@types/hoist-non-react-statics@3.3.5: @@ -5354,7 +5357,7 @@ packages: '@types/history': 4.7.11 '@types/react': 18.3.16 '@types/react-router': 5.1.20 - redux: 3.7.2 + redux: 4.2.1 dev: false /@types/react-router@5.1.20: @@ -9076,7 +9079,7 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==, tarball: https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz} engines: {node: '>=0.11'} dependencies: - '@babel/runtime': 7.25.7 + '@babel/runtime': 7.26.0 dev: false /dayjs@1.11.11: @@ -15567,6 +15570,22 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /rc-menu@9.16.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-vAL0yqPkmXWk3+YKRkmIR8TYj3RVdEt3ptG2jCJXWNAvQbT0VJJdRyHZ7kG/l1JsZlB+VJq/VcYOo69VR4oD+w==, tarball: https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.0.tgz} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + dependencies: + '@babel/runtime': 7.26.0 + '@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0) + classnames: 2.5.1 + rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) + rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.44.2(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /rc-menu@9.8.4(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-lmw2j8I2fhdIzHmC9ajfImfckt0WDb2KVJJBBRIsxPEw2kGkEfjLMUoB1NgiNT/Q5cC8PdjGOGQjHJIJMwyNMw==, tarball: https://registry.npmjs.org/rc-menu/-/rc-menu-9.8.4.tgz} peerDependencies: @@ -16050,6 +16069,24 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /rc-tabs@15.5.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-NrDcTaUJLh9UuDdMBkjKTn97U9iXG44s9D03V5NHkhEDWO5/nC6PwC3RhkCWFMKB9hh+ryqgZ+TIr1b9Jd/hnQ==, tarball: https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.0.tgz} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + dependencies: + '@babel/runtime': 7.26.0 + classnames: 2.5.1 + rc-dropdown: 4.2.0(react-dom@18.2.0)(react@18.2.0) + rc-menu: 9.16.0(react-dom@18.2.0)(react@18.2.0) + rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0) + rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.44.2(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /rc-textarea@0.4.7(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-IQPd1CDI3mnMlkFyzt2O4gQ2lxUsnBAeJEoZGJnkkXgORNqyM9qovdrCj9NzcRfpHgLdzaEbU3AmobNFGUznwQ==, tarball: https://registry.npmjs.org/rc-textarea/-/rc-textarea-0.4.7.tgz} peerDependencies: @@ -16804,7 +16841,7 @@ packages: /regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==, tarball: https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz} dependencies: - '@babel/runtime': 7.25.7 + '@babel/runtime': 7.26.0 dev: false /regexp-tree@0.1.27: diff --git a/src/components/status-tag/copy.less b/src/components/status-tag/copy.less index ff9ddfe8..d05c2567 100644 --- a/src/components/status-tag/copy.less +++ b/src/components/status-tag/copy.less @@ -11,13 +11,13 @@ :global { .copy-button-wrapper { display: none; - background-color: #383838; + background-color: rgba(255, 255, 255, 20%); position: absolute; z-index: 10; - right: -28px; + right: 0; top: 0; padding: 2px; - border-radius: 4px; + border-radius: 0 4px; } .simplebar-content { diff --git a/src/components/status-tag/index.tsx b/src/components/status-tag/index.tsx index 056a01b5..8ba9c12a 100644 --- a/src/components/status-tag/index.tsx +++ b/src/components/status-tag/index.tsx @@ -28,6 +28,7 @@ type StatusTagProps = { download?: { percent: number; }; + extra?: React.ReactNode; actions?: { label: string; icon?: React.ReactNode; @@ -40,6 +41,7 @@ type StatusTagProps = { const StatusTag: React.FC = ({ statusValue, download, + extra, actions = [], type = 'tag' }) => { @@ -78,7 +80,6 @@ const StatusTag: React.FC = ({ style={{ color: 'rgba(255,255,255,.8)' }} text={statusValue.message || ''} size="small" - placement="right" > {actions?.map((item) => { return ( @@ -104,7 +105,7 @@ const StatusTag: React.FC = ({ })} - +
= ({ }} > {statusValue.message} + {extra}
diff --git a/src/components/tags-wrapper/index.less b/src/components/tags-wrapper/index.less index 49eafbbe..4eca0bf6 100644 --- a/src/components/tags-wrapper/index.less +++ b/src/components/tags-wrapper/index.less @@ -13,4 +13,8 @@ height: 22px; opacity: 0.7; } + + .tags-content { + display: flex; + } } diff --git a/src/components/tags-wrapper/index.tsx b/src/components/tags-wrapper/index.tsx index c2766825..6c31eb7b 100644 --- a/src/components/tags-wrapper/index.tsx +++ b/src/components/tags-wrapper/index.tsx @@ -1,99 +1,126 @@ import { MoreOutlined } from '@ant-design/icons'; import { Dropdown, Tag } from 'antd'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import _ from 'lodash'; +import ResizeObserver from 'rc-resize-observer'; +import React, { useEffect, useRef, useState } from 'react'; import './index.less'; -const TagsWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { +interface TagsWrapperProps { + gap?: number; + dataList: any[]; + renderTag: (item: any) => React.ReactNode; +} + +const TagsWrapper: React.FC = (props) => { + const { gap = 0, dataList, renderTag } = props; + const tagsContentRef = useRef(null); + const [hiddenIndices, setHiddenIndices] = useState({ + start: 0, + end: dataList.length + }); const wrapperRef = useRef(null); - const observer = useRef(null); - const resizeObserver = useRef(null); - const [hiddenIndex, setHiddenIndex] = useState(4); - const uid = useRef(0); + const moreBtnRef = useRef(null); + const moreButtonWidth = useRef(0); + const nodeSizeList = useRef([]); - const updateUid = () => { - uid.current += 1; - return uid.current; - }; + const calculateHiddenIndices = () => { + const wrapperWidth = wrapperRef.current?.offsetWidth || 0; + const childNodes = tagsContentRef.current?.childNodes; + let sizeList = nodeSizeList.current; - const dropItems = useMemo(() => { - return React.Children.toArray(children) - .slice(hiddenIndex) - .map((child) => ({ - label: child, - key: updateUid() - })); - }, [children, hiddenIndex]); + if (!sizeList.length && childNodes?.length) { + sizeList = _.map(childNodes, (node: HTMLDivElement, index: number) => { + if (index === childNodes.length - 1) { + return node.offsetWidth; + } + return node.offsetWidth + gap; + }); + nodeSizeList.current = sizeList; + } - const updateHiddenIndex = () => { - if (!wrapperRef.current || !observer.current) return; + if (wrapperWidth === 0 || !sizeList.length) return; - const childrenList = Array.from(wrapperRef.current.children); - let newHiddenIndex = 0; + // cache more button width + if (moreBtnRef.current?.offsetWidth) { + moreButtonWidth.current = moreBtnRef.current?.offsetWidth + gap; + } - childrenList.forEach((child, index) => { - const rect = child.getBoundingClientRect(); - // visible in wrapperRef - const issivible = - rect.top < wrapperRef.current!.clientHeight && - rect.bottom > 0 && - rect.left < wrapperRef.current!.clientWidth && - rect.right > 0; + let totalWidth = 0; + let start = 0; + let end = dataList.length; - if (!issivible) { - newHiddenIndex = Math.max(newHiddenIndex, index + 1); + for (let i = 0; i < sizeList.length; i++) { + const nodeWidth = sizeList[i]; + + if (totalWidth + moreButtonWidth.current >= wrapperWidth) { + end = i - 1; + break; } - }); + totalWidth += nodeWidth; - setHiddenIndex(newHiddenIndex); + if (totalWidth >= wrapperWidth) { + end = i; + break; + } + } + + if (hiddenIndices.start !== start || hiddenIndices.end !== end) { + setHiddenIndices({ + start, + end + }); + } }; useEffect(() => { - if (!wrapperRef.current) return; + if (tagsContentRef.current) { + calculateHiddenIndices(); + } + }, [dataList, gap]); - // observer.current = new IntersectionObserver( - // (entries) => { - // const lastEntry = entries[entries.length - 1]; - // if (lastEntry && lastEntry.intersectionRatio < 1) { - // setHiddenIndex((prev) => - // Math.max(prev, React.Children.count(children)) - // ); - // } - // }, - // { root: wrapperRef.current, threshold: 1 } - // ); - - // const childrenList = wrapperRef.current.children; - // if (childrenList.length > 0) { - // observer.current.observe(childrenList[childrenList.length - 1]); - // } - - resizeObserver.current = new ResizeObserver(() => { - // updateHiddenIndex(); - }); - resizeObserver.current.observe(wrapperRef.current); - - return () => { - observer.current?.disconnect(); - resizeObserver.current?.disconnect(); - }; - }, [children]); + const handleContentResize = _.throttle(() => { + calculateHiddenIndices(); + }, 200); return ( -
- {React.Children.toArray(children).slice(0, hiddenIndex)} - {React.Children.toArray(children).length > 4 && ( - - - - - - )} -
+ +
+ +
+ {_.map( + _.slice(dataList, hiddenIndices.start, hiddenIndices.end), + (item: any, index: number) => { + return {renderTag?.(item)}; + } + )} +
+
+ {hiddenIndices.end < dataList.length && ( + { + return { + label: renderTag?.(item), + key: index + }; + } + ) + }} + > + + + + + )} +
+
); }; diff --git a/src/components/tags-wrapper/more-dropdown.tsx b/src/components/tags-wrapper/more-dropdown.tsx new file mode 100644 index 00000000..6634cc39 --- /dev/null +++ b/src/components/tags-wrapper/more-dropdown.tsx @@ -0,0 +1,29 @@ +import { MoreOutlined } from '@ant-design/icons'; +import { Dropdown, Tag } from 'antd'; +import React from 'react'; + +interface MoreDropdownProps { + items: any[]; +} + +const MoreDropdown: React.FC = (props) => { + const { items } = props; + return ( + <> + {items && items.length > 0 ? ( + + + + + + ) : null} + + ); +}; + +export default React.memo(MoreDropdown); diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index 19a6a23b..cf16403a 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -89,5 +89,6 @@ export default { 'models.form.search.gguftips': 'If using macOS or Windows as a worker, check GGUF (uncheck for audio models).', 'models.form.button.addlabel': 'Add Label', - 'models.filter.category': 'Filter by category' + 'models.filter.category': 'Filter by category', + 'models.list.more.logs': 'View More' }; diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index 80487802..3d44c17f 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -85,5 +85,6 @@ export default { 'models.form.search.gguftips': '当 macOS 或 Windows 作 worker 时勾选 GGUF(搜索音频模型时取消勾选)', 'models.form.button.addlabel': '添加标签', - 'models.filter.category': '按类别筛选' + 'models.filter.category': '按类别筛选', + 'models.list.more.logs': '查看更多' }; diff --git a/src/pages/llmodels/components/catalog-item.tsx b/src/pages/llmodels/components/catalog-item.tsx index 44198d33..89cc0442 100644 --- a/src/pages/llmodels/components/catalog-item.tsx +++ b/src/pages/llmodels/components/catalog-item.tsx @@ -27,6 +27,28 @@ const CatalogItem: React.FC = (props) => { e.target.src = fallbackImg; }; + const renderTag = (sItem: any) => { + return ( + + {sItem}B + + ); + }; + return (
= (props) => { {_.map(data.licenses, (license: string, index: number) => { return ( - + {license} @@ -113,15 +135,11 @@ const CatalogItem: React.FC = (props) => { <>
- - {data.sizes.map((sItem, i) => { - return ( - - {sItem}B - - ); - })} - +
)} diff --git a/src/pages/llmodels/components/instance-item.tsx b/src/pages/llmodels/components/instance-item.tsx index 4282d621..bb3be2ff 100644 --- a/src/pages/llmodels/components/instance-item.tsx +++ b/src/pages/llmodels/components/instance-item.tsx @@ -15,7 +15,7 @@ import { ThunderboltFilled } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Col, Divider, Row, Space, Tag, Tooltip } from 'antd'; +import { Button, Col, Divider, Row, Space, Tag, Tooltip } from 'antd'; import dayjs from 'dayjs'; import _ from 'lodash'; import React, { useCallback } from 'react'; @@ -282,6 +282,21 @@ const InstanceItem: React.FC = ({ ? { percent: item.download_progress } : undefined } + extra={ + item.state === InstanceStatusMap.Error ? ( + + ) : null + } statusValue={{ status: item.state === InstanceStatusMap.Downloading && diff --git a/src/pages/playground/embedding.tsx b/src/pages/playground/embedding.tsx index 29a895c0..4e64248a 100644 --- a/src/pages/playground/embedding.tsx +++ b/src/pages/playground/embedding.tsx @@ -31,7 +31,8 @@ const PlaygroundEmbedding: React.FC = () => { const getModelListByEmbedding = async () => { try { const params = { - categories: modelCategoriesMap.embedding + categories: modelCategoriesMap.embedding, + with_meta: true }; const res = await queryModelsList(params); const list = _.map(res.data || [], (item: any) => { diff --git a/src/pages/playground/rerank.tsx b/src/pages/playground/rerank.tsx index c3d1212e..c18ad7fb 100644 --- a/src/pages/playground/rerank.tsx +++ b/src/pages/playground/rerank.tsx @@ -33,7 +33,8 @@ const PlaygroundRerank: React.FC = () => { const getModelListByReranker = async () => { try { const params = { - categories: modelCategoriesMap.reranker + categories: modelCategoriesMap.reranker, + with_meta: true }; const res = await queryModelsList(params); const list = _.map(res.data || [], (item: any) => { diff --git a/src/pages/resources/components/container-install.tsx b/src/pages/resources/components/container-install.tsx index b816a147..ea71002d 100644 --- a/src/pages/resources/components/container-install.tsx +++ b/src/pages/resources/components/container-install.tsx @@ -59,7 +59,6 @@ const AddWorker: React.FC = (props) => { { version: versionInfo.version } )} -
  • {intl.formatMessage({ id: 'resources.worker.select.command' })}
  • {intl.formatMessage({ id: 'resources.worker.driver.install' })} diff --git a/src/pages/resources/config/index.ts b/src/pages/resources/config/index.ts index e26b2592..735340ad 100644 --- a/src/pages/resources/config/index.ts +++ b/src/pages/resources/config/index.ts @@ -57,6 +57,11 @@ export const addWorkerGuide: Record = { return `docker run -d --ipc=host --network=host gpustack/gpustack:${params.tag} --server-url ${params.server} --token ${params.token}`; } }, + rocm: { + registerWorker(params: { server: string; tag: string; token: string }) { + return `docker run -d --network=host --ipc=host --group-add=video --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --device /dev/kfd --device /dev/dri gpustack/gpustack:${params.tag} --server-url ${params.server} --token ${params.token}`; + } + }, container: { getToken: 'docker run -it ${gpustack_container_id} cat /var/lib/gpustack/token' @@ -65,6 +70,7 @@ export const addWorkerGuide: Record = { export const containerInstallOptions = [ { label: 'CUDA', value: 'cuda' }, + { label: 'AMD', value: 'rocm' }, { label: 'CANN', value: 'npu' }, { label: 'MUSA', value: 'musa' }, { label: 'CPU', value: 'cpu' }