diff --git a/src/pages/llmodels/forms/lora-list-item.tsx b/src/pages/llmodels/forms/lora-list-item.tsx index f95fe950..ac7eb583 100644 --- a/src/pages/llmodels/forms/lora-list-item.tsx +++ b/src/pages/llmodels/forms/lora-list-item.tsx @@ -1,11 +1,14 @@ +import { LoadingOutlined } from '@ant-design/icons'; import { AutoTooltip, Input as CInput, - Cascader as SealCascader + Select as SealSelect } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; +import { Button } from 'antd'; import _ from 'lodash'; import { useEffect, useMemo, useState } from 'react'; +import { modelSourceMap } from '../config'; import useQueryModelLoraList, { LoraOptionGroup } from '../services/use-query-lora-list'; @@ -25,6 +28,12 @@ interface LoraListItemProps { }) => void; } +type RenderableGroup = { + label: string; + groupValue: string; + options: { label: string; value: string; source: string }[]; +}; + const LoraListItem: React.FC = ({ item, base, @@ -35,7 +44,11 @@ const LoraListItem: React.FC = ({ onChange }) => { const intl = useIntl(); - const { dataList: ownSearchList, fetchData } = useQueryModelLoraList(); + const { + dataList: ownSearchList, + fetchData, + loading + } = useQueryModelLoraList(); const [hasSearched, setHasSearched] = useState(false); const itemDataList = hasSearched ? ownSearchList : defaultDataList; @@ -58,18 +71,44 @@ const LoraListItem: React.FC = ({ setHasSearched(false); }, [base]); - const groupedOptions = useMemo(() => { + const groupedOptions = useMemo(() => { const currentRepo = item.value?.[1]; - return itemDataList - .map((group) => ({ - ...group, - children: group.children.filter( - (child) => - !selectedRepoNames.has(child.value) || child.value === currentRepo - ) - })) - .filter((group) => group.children.length > 0); - }, [itemDataList, selectedRepoNames, item.value]); + const sourcePrefix = intl.formatMessage({ id: 'models.form.source' }); + const groups: RenderableGroup[] = []; + let hasLocal = false; + + itemDataList.forEach((group) => { + const isLocal = group.value === modelSourceMap.local_path_value; + if (isLocal) hasLocal = true; + const filtered = group.children.filter( + (child) => + !selectedRepoNames.has(child.value) || child.value === currentRepo + ); + const labelText = isLocal + ? intl.formatMessage({ id: 'menu.resources.modelfiles' }) + : group.label; + groups.push({ + label: `${sourcePrefix}: ${labelText}`, + groupValue: group.value, + options: filtered.map((c) => ({ + label: c.label, + value: c.value, + source: c.source, + data: { source: c.source, groupValue: group.value } + })) + }); + }); + + if (!hasLocal) { + groups.push({ + label: `${sourcePrefix}: ${intl.formatMessage({ id: 'menu.resources.modelfiles' })}`, + groupValue: modelSourceMap.local_path_value, + options: [] + }); + } + + return groups; + }, [itemDataList, selectedRepoNames, item.value, intl]); const handleSearch = (q: string) => { if (!base || !q) { @@ -81,83 +120,64 @@ const LoraListItem: React.FC = ({ debouncedSearch(q); }; - const handleCascaderChange = (value: any, selectedOptions?: any[]) => { - const leaf = selectedOptions?.[selectedOptions.length - 1]; - onChange({ value: value || [], source: leaf?.source || '' }); + const handleSelectChange = (value: string, option: any) => { + if (!value) { + onChange({ value: [], source: '' }); + return; + } + const data = option?.data || {}; + const groupValue = data.groupValue || modelSourceMap.local_path_value; + const source = data.source || modelSourceMap.local_path_value; + onChange({ value: [groupValue, value], source }); }; const handleNameChange = (e: React.ChangeEvent) => { onChange({ lora_name: e.target.value }); }; - const cascaderEmpty = !item.value || item.value.length === 0; + const optionRender = (option: any) => { + return {option.label}; + }; + + const labelRender = (option: any) => { + return option.label; + }; + + const valueEmpty = !item.value?.[1]; const nameEmpty = !item.lora_name?.trim(); const isDuplicate = !!item.lora_name?.trim() && duplicateNames.has(item.lora_name.trim()); - const cascaderStatus = - validated && cascaderEmpty ? ('error' as const) : 'success'; + const selectStatus = validated && valueEmpty ? ('error' as const) : 'success'; const inputStatus = validated && (nameEmpty || isDuplicate) ? ('error' as const) : 'success'; - const displayRender = (labels: any[]) => { - const left = - typeof labels[0] === 'string' ? labels[0].replace(/\/+$/, '') : labels[0]; - const right = - typeof labels[1] === 'string' ? labels[1].replace(/^\/+/, '') : labels[1]; - const content = ( - - {left}/{right} - - ); - return ( - - {content} - - ); - }; - - const optionNode = (option: any) => { - const { data } = option; - return ( - - {data.label} - - ); - }; - return ( -
- +
+ + + + ) : null } - }} - styles={{ - popup: { - listItem: { - padding: '5px 10px' - } - } - }} - getPopupContainer={(triggerNode) => triggerNode.parentNode} - > + value={item.value?.[1] || undefined} + onChange={handleSelectChange} + onSearch={handleSearch} + labelRender={labelRender} + optionRender={optionRender} + placeholder={intl.formatMessage({ id: 'models.form.lora.select' })} + options={groupedOptions} + > +