172 lines
4.4 KiB
TypeScript
172 lines
4.4 KiB
TypeScript
import {
|
|
AutoTooltip,
|
|
Input as CInput,
|
|
Cascader as SealCascader
|
|
} from '@gpustack/core-ui';
|
|
import { useIntl } from '@umijs/max';
|
|
import _ from 'lodash';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import useQueryModelLoraList, {
|
|
LoraOptionGroup
|
|
} from '../services/use-query-lora-list';
|
|
import loraSelectionStyles from '../style/lora-selection.less';
|
|
|
|
interface LoraListItemProps {
|
|
item: { value: any[]; lora_name: string; source: string };
|
|
base: string;
|
|
defaultDataList: LoraOptionGroup[];
|
|
selectedRepoNames: Set<string>;
|
|
duplicateNames: Set<string>;
|
|
validated: boolean;
|
|
onChange: (partial: {
|
|
value?: any[];
|
|
lora_name?: string;
|
|
source?: string;
|
|
}) => void;
|
|
}
|
|
|
|
const LoraListItem: React.FC<LoraListItemProps> = ({
|
|
item,
|
|
base,
|
|
defaultDataList,
|
|
selectedRepoNames,
|
|
duplicateNames,
|
|
validated,
|
|
onChange
|
|
}) => {
|
|
const intl = useIntl();
|
|
const { dataList: ownSearchList, fetchData } = useQueryModelLoraList();
|
|
const [hasSearched, setHasSearched] = useState(false);
|
|
|
|
const itemDataList = hasSearched ? ownSearchList : defaultDataList;
|
|
|
|
const debouncedSearch = useMemo(
|
|
() =>
|
|
_.debounce((q: string) => {
|
|
fetchData({ base, q });
|
|
}, 300),
|
|
[base]
|
|
);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
debouncedSearch.cancel();
|
|
};
|
|
}, [debouncedSearch]);
|
|
|
|
useEffect(() => {
|
|
setHasSearched(false);
|
|
}, [base]);
|
|
|
|
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 handleSearch = (q: string) => {
|
|
if (!base || !q) {
|
|
setHasSearched(false);
|
|
debouncedSearch.cancel();
|
|
return;
|
|
}
|
|
setHasSearched(true);
|
|
debouncedSearch(q);
|
|
};
|
|
|
|
const handleCascaderChange = (value: any, selectedOptions?: any[]) => {
|
|
const leaf = selectedOptions?.[selectedOptions.length - 1];
|
|
onChange({ value: value || [], source: leaf?.source || '' });
|
|
};
|
|
|
|
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
onChange({ lora_name: e.target.value });
|
|
};
|
|
|
|
const cascaderEmpty = !item.value || item.value.length === 0;
|
|
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 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 = (
|
|
<span>
|
|
{left}/{right}
|
|
</span>
|
|
);
|
|
return (
|
|
<AutoTooltip ghost maxWidth={300} title={content}>
|
|
{content}
|
|
</AutoTooltip>
|
|
);
|
|
};
|
|
|
|
const optionNode = (option: any) => {
|
|
const { data } = option;
|
|
return (
|
|
<AutoTooltip ghost maxWidth={'100%'}>
|
|
{data.label}
|
|
</AutoTooltip>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className={loraSelectionStyles.item}>
|
|
<SealCascader
|
|
expandTrigger="click"
|
|
multiple={false}
|
|
alwaysFocus={true}
|
|
status={cascaderStatus}
|
|
value={item.value}
|
|
options={groupedOptions}
|
|
onChange={handleCascaderChange}
|
|
showSearch={{
|
|
onSearch: handleSearch
|
|
}}
|
|
placeholder={intl.formatMessage({ id: 'models.form.lora.select' })}
|
|
showCheckedStrategy="SHOW_CHILD"
|
|
displayRender={displayRender}
|
|
optionNode={optionNode}
|
|
classNames={{
|
|
popup: {
|
|
root: 'cascader-popup-wrapper'
|
|
}
|
|
}}
|
|
styles={{
|
|
popup: {
|
|
listItem: {
|
|
padding: '5px 10px'
|
|
}
|
|
}
|
|
}}
|
|
getPopupContainer={(triggerNode) => triggerNode.parentNode}
|
|
></SealCascader>
|
|
<CInput.Input
|
|
status={inputStatus}
|
|
value={item.lora_name}
|
|
onChange={handleNameChange}
|
|
placeholder={intl.formatMessage({ id: 'models.form.lora.name' })}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LoraListItem;
|