feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
||||
|
||||
import { debounce } from 'lodash-es';
|
||||
import { PUBLIC_SPACE_ID } from '@coze-workflow/base/constants';
|
||||
import { concatTestId } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Typography, Spin, Avatar, Select } from '@coze-arch/bot-semi';
|
||||
import {
|
||||
ListBotDraftType,
|
||||
PublishStatus,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
import { IconSearch } from '@douyinfe/semi-icons';
|
||||
|
||||
import { useGlobalState } from '../../hooks';
|
||||
import { useExtraBotOption } from './use-extra-bot-option';
|
||||
import type { IBotSelectOption } from './types';
|
||||
|
||||
import styles from './bots.module.less';
|
||||
|
||||
type IBotSelectOptions = IBotSelectOption[];
|
||||
|
||||
const RenderCustomOption = item => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Select.Option
|
||||
data-testid={concatTestId(
|
||||
'workflow',
|
||||
'playground',
|
||||
'testrun',
|
||||
'bot-select',
|
||||
'option',
|
||||
item.value,
|
||||
)}
|
||||
value={item.value}
|
||||
showTick={true}
|
||||
key={item.value}
|
||||
className={styles['bot-option']}
|
||||
>
|
||||
<div className="flex" style={{ width: '100%' }}>
|
||||
<Avatar
|
||||
size="extra-extra-small"
|
||||
style={{ flexShrink: 0, marginRight: 8 }}
|
||||
shape="square"
|
||||
src={item.avatar}
|
||||
/>
|
||||
<div className="flex" style={{ flexGrow: 1, flexShrink: 1, width: 0 }}>
|
||||
<Typography.Text
|
||||
ellipsis={{ showTooltip: true }}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#1D1C23',
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</Select.Option>
|
||||
);
|
||||
};
|
||||
|
||||
const RenderFootLoading = ({
|
||||
onObserver,
|
||||
}: {
|
||||
onObserver: () => Promise<void>;
|
||||
}) => {
|
||||
const indicatorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = entries => {
|
||||
if (entries[0].isIntersecting) {
|
||||
onObserver?.();
|
||||
}
|
||||
};
|
||||
const loadingObserver = new IntersectionObserver(callback);
|
||||
indicatorRef.current && loadingObserver.observe(indicatorRef.current);
|
||||
return () => loadingObserver.disconnect();
|
||||
}, []);
|
||||
return (
|
||||
<div className={styles['loading-tag']} ref={indicatorRef}>
|
||||
<Spin style={{ marginRight: 10 }} size="small" />
|
||||
<span>{I18n.t('workflow_add_common_loading')}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface BotsProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export const Bots: React.FC<BotsProps> = ({ value, onChange, ...props }) => {
|
||||
const globalState = useGlobalState();
|
||||
const DebounceTime = 500;
|
||||
|
||||
const isLoadMoreDate = useRef(false);
|
||||
const [selectList = [], setSelectList] = useState<IBotSelectOptions>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isShowFoot, setIsShowFoot] = useState<boolean>(false);
|
||||
const [pageIndex, setPageIndex] = useState<number>(1);
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [searchTotal, setTotal] = useState(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 由于分页限制 选中的botId可能找不到对应的option 需要额外添加
|
||||
const extraBotOption = useExtraBotOption(selectList, value);
|
||||
|
||||
// 接口得到的总数并非真实的总数,前端可能会拼接 options
|
||||
const listMaxHeight = useMemo(() => {
|
||||
const realTotal = extraBotOption ? searchTotal + 1 : searchTotal;
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
return realTotal < 7 ? realTotal * 32 : 208;
|
||||
}, [searchTotal, extraBotOption]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBotList();
|
||||
}, []);
|
||||
|
||||
const fetchBotList = async (
|
||||
index?: number,
|
||||
query?: string,
|
||||
isReset = false,
|
||||
) => {
|
||||
if (query) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
|
||||
const res = await DeveloperApi.GetDraftBotList({
|
||||
space_id:
|
||||
globalState.spaceId === PUBLIC_SPACE_ID
|
||||
? globalState.personalSpaceId
|
||||
: globalState.spaceId,
|
||||
bot_name: query ?? search,
|
||||
order_by: 0,
|
||||
team_bot_type: ListBotDraftType.TeamBots,
|
||||
page_index: index ?? pageIndex,
|
||||
page_size: 30,
|
||||
is_publish: PublishStatus.All,
|
||||
});
|
||||
const { bot_draft_list, total = 0 } = res?.data ?? {};
|
||||
const list: IBotSelectOptions = (bot_draft_list ?? []).map(it => ({
|
||||
name: it.name ?? '',
|
||||
value: it.id ?? '',
|
||||
avatar: it.icon_url ?? '',
|
||||
}));
|
||||
const totalList = isReset ? list : [...selectList, ...list];
|
||||
|
||||
setTotal(total);
|
||||
setSelectList(totalList);
|
||||
setIsShowFoot(totalList.length < total);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const loadMoreData = async () => {
|
||||
if (isLoadMoreDate.current) {
|
||||
return;
|
||||
}
|
||||
isLoadMoreDate.current = true;
|
||||
const newPageIndex = pageIndex + 1;
|
||||
setPageIndex(newPageIndex);
|
||||
await fetchBotList(newPageIndex);
|
||||
isLoadMoreDate.current = false;
|
||||
};
|
||||
|
||||
const handleSearch = query => {
|
||||
setSearch(query);
|
||||
setPageIndex(1);
|
||||
fetchBotList(1, query, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles['select-wrapper']} ref={containerRef}>
|
||||
<Select
|
||||
value={value}
|
||||
data-testid={concatTestId(
|
||||
'workflow',
|
||||
'playground',
|
||||
'testerun',
|
||||
'bot-select',
|
||||
)}
|
||||
dropdownClassName={styles.dropdown}
|
||||
showClear
|
||||
filter
|
||||
remote
|
||||
placeholder={I18n.t('workflow_detail_testrun_variable_node_select')}
|
||||
emptyContent={I18n.t('agentflow_addbot_select_empty_no_bot')}
|
||||
onSearch={debounce(handleSearch, DebounceTime)}
|
||||
prefix={<IconSearch />}
|
||||
loading={isLoading}
|
||||
style={{ width: '100%' }}
|
||||
virtualize={{
|
||||
height: listMaxHeight,
|
||||
width: '100%',
|
||||
itemSize: 32,
|
||||
}}
|
||||
onChange={newValue => onChange?.(newValue as string)}
|
||||
{...props}
|
||||
>
|
||||
{[extraBotOption, ...selectList]
|
||||
.filter(item => item)
|
||||
.map(item => RenderCustomOption(item))}
|
||||
|
||||
{isShowFoot ? (
|
||||
<Select.Option
|
||||
value={new Date().getTime()}
|
||||
key={new Date().getTime()}
|
||||
className={styles['bot-foot-loading']}
|
||||
disabled
|
||||
>
|
||||
<RenderFootLoading onObserver={loadMoreData} />
|
||||
</Select.Option>
|
||||
) : null}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user