feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
/* stylelint-disable declaration-no-important */
.dataset-header {
padding-right: 12px !important;
padding-left: 0 !important;
}
.modal.upgrade-level {
:global {
.semi-modal-body {
height: 0;
padding-bottom: 0;
}
.semi-modal-content {
@apply bg-white-1;
}
}
}

View File

@@ -0,0 +1,184 @@
/*
* 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 { useState } from 'react';
import classNames from 'classnames';
import { FilterKnowledgeType } from '@coze-data/utils';
import { type UnitType } from '@coze-data/knowledge-resource-processor-core';
import { I18n } from '@coze-arch/i18n';
import {
type UIModalProps,
UICompositionModal,
UICompositionModalSider,
UICompositionModalMain,
} from '@coze-arch/bot-semi';
import { type Dataset } from '@coze-arch/bot-api/knowledge';
import { DATA_REFACTOR_CLASS_NAME } from '@/constant';
import {
useKnowledgeListModalContent,
KnowledgeListModalContent,
} from './use-content';
import SiderCategory from './sider-category';
import styles from './index.module.less';
export interface UseKnowledgeListModalParams {
datasetList: Dataset[];
onDatasetListChange: (list: Dataset[]) => void;
onClickAddKnowledge?: (
datasetId: string,
type: UnitType,
shouldUpload?: boolean,
) => void;
beforeCreate?: (shouldUpload: boolean) => void;
onClickKnowledgeDetail?: (knowledgeID: string) => void;
modalProps?: UIModalProps;
canCreate?: boolean;
defaultType?: FilterKnowledgeType;
knowledgeTypeConfigList?: FilterKnowledgeType[];
projectID?: string;
hideCreate?: boolean;
createKnowledgeModal?: {
modal: React.ReactNode;
open: () => void;
close: () => void;
};
}
export interface UseKnowledgeListReturnValue {
node: JSX.Element;
open: () => void;
close: () => void;
}
export const useKnowledgeListModal = ({
datasetList,
onDatasetListChange,
onClickAddKnowledge,
beforeCreate,
onClickKnowledgeDetail,
modalProps,
canCreate = true,
defaultType,
knowledgeTypeConfigList,
projectID,
hideCreate,
createKnowledgeModal,
}: UseKnowledgeListModalParams): UseKnowledgeListReturnValue => {
const [visible, setVisible] = useState(false);
const [category, setCategory] = useState<'library' | 'project'>(
projectID ? 'project' : 'library',
);
const handleClose = () => {
setVisible(false);
};
const handleOpen = () => {
setVisible(true);
};
const { renderContent, renderSearch, renderCreateBtn, renderFilters } =
useKnowledgeListModalContent({
hideHeader: true,
showFilters: ['scope-type', 'search-type'],
datasetList,
onDatasetListChange,
onClickAddKnowledge,
beforeCreate,
onClickKnowledgeDetail,
canCreate,
defaultType,
knowledgeTypeConfigList,
// 需要优化属性选择方式
projectID: category === 'project' ? projectID : '',
createKnowledgeModal,
});
return {
node: (
<UICompositionModal
type="base-composition"
header={I18n.t('dataset_set_title')}
visible={visible}
className={classNames(
styles.modal,
styles['upgrade-level'],
DATA_REFACTOR_CLASS_NAME,
)}
centered
onCancel={handleClose}
filter={
<div className="flex justify-between gap-[24px]">
{renderFilters()}
</div>
}
sider={
<UICompositionModalSider className="!pt-[16px]">
<UICompositionModalSider.Header className="flex flex-col gap-[16px]">
{renderSearch()}
{hideCreate ? null : renderCreateBtn()}
</UICompositionModalSider.Header>
<UICompositionModalSider.Content className="flex flex-col gap-[4px] mt-[16px]">
<SiderCategory
label={I18n.t('project_resource_modal_library_resources', {
resource: I18n.t('resource_type_knowledge'),
})}
onClick={() => {
setCategory('library');
}}
selected={category === 'library'}
/>
{projectID ? (
<SiderCategory
label={I18n.t('project_resource_modal_project_resources', {
resource: I18n.t('resource_type_knowledge'),
})}
onClick={() => {
setCategory('project');
}}
selected={category === 'project'}
/>
) : null}
</UICompositionModalSider.Content>
</UICompositionModalSider>
}
content={
<UICompositionModalMain className="px-[12px]">
{renderContent()}
</UICompositionModalMain>
}
{...modalProps}
></UICompositionModal>
),
close: handleClose,
open: handleOpen,
};
};
export { KnowledgeCard } from './knowledge-card';
export {
KnowledgeListModalContent,
useKnowledgeListModalContent,
FilterKnowledgeType,
};
export { KnowledgeCardListVertical } from './knowledge-card-list';

View File

@@ -0,0 +1,9 @@
.popover {
padding: 16px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
color: #2e3238;
white-space: pre-line;
}

View File

@@ -0,0 +1,41 @@
/*
* 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 { type FC, type PropsWithChildren } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Popover } from '@coze-arch/bot-semi';
import styles from './index.module.less';
export const FilePopover: FC<
PropsWithChildren<{
fileNames: string[];
showTitle?: boolean;
}>
> = ({ fileNames = [], showTitle = true, children }) => (
<Popover
className={styles.popover}
content={
<div>
{showTitle ? <p>{I18n.t('datasets_processing_notice')}</p> : null}
<p>{fileNames.join('\n')}</p>
</div>
}
>
{children}
</Popover>
);

View File

@@ -0,0 +1,17 @@
/*
* 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.
*/
export { FilePopover } from './file-popover';

View File

@@ -0,0 +1,220 @@
/* stylelint-disable max-nesting-depth */
/* stylelint-disable no-descending-specificity */
/* stylelint-disable selector-class-pattern */
.container {
display: flex;
flex-direction: column;
}
.item:hover {
cursor: pointer;
background: var(--light-usage-fill-color-fill-0, rgb(46 47 56 / 5%));
border-bottom: 1px solid transparent;
border-radius: var(--spacing-tight, 8px);
}
.item:hover::before {
content: '';
position: absolute;
top: -1px;
left: 0;
width: 100%;
border-top: 1px solid rgb(245 247 250);
}
.item {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 10px 8px;
border-bottom: 1px solid rgba(29, 28, 35, 8%);
.left {
box-sizing: border-box;
width: 36px;
height: 36px;
background-color: #fff;
border: 1px solid rgb(237 237 238);
border-radius: 6px;
&>img {
width: 36px;
height: 36px;
}
}
.content {
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
width: 0;
height: 92px;
margin: 0 16px;
}
.right {
flex-shrink: 0;
}
.title {
font-size: 14px;
font-weight: 600;
line-height: 22px;
color: var(--light-usage-text-color-text-0, #1c1d23);
}
.description {
width: 100%;
margin-top: 4px;
font-size: 12px;
line-height: 16px;
color: var(--light-usage-text-color-text-1, rgb(28 29 35 / 80%));
letter-spacing: 0.12px;
}
.tags-wapper {
margin-top: 8px;
}
.tags {
.file-list {
color: var(--light-color-teal-teal-6, #00a794);
background-color: #e4e6e9;
}
:global {
.semi-tag-square {
border-radius: 4px;
}
}
}
.info {
display: flex;
align-items: center;
margin-top: 8px;
}
.creator {
padding-left: 4px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
color: var(--light-usage-text-color-text-3, rgb(28 29 35 / 35%));
}
.border-right {
width: 1px;
height: 8px;
margin: 0 4px 0 8px;
background-color: rgb(28 29 35 / 12%);
}
}
button.button {
flex-shrink: 0;
width: 80px;
&.added {
color: var(--light-usage-primary-color-primary-disabled, #b4baf6);
background: var(--light-usage-bg-color-bg-0, #fff);
border: 1px solid var(--light-usage-disabled-color-disabled-border, #f0f0f5);
}
&.addedMouseIn {
color: var(--light-color-red-red-5, #ff441e);
background: #fff;
border: 1px solid var(--light-usage-border-color-border-1, rgb(29 28 35 / 12%));
}
}
.file-list-details {
max-width: 335px;
.dataset-name {
padding: 9px 12px;
font-size: 14px;
font-weight: 600;
line-height: 22px;
color: var(--light-usage-text-color-text-0,
var(--light-usage-text-color-text-0, #1c1f23));
}
.file-info {
overflow-y: auto;
max-height: 400px;
&-item {
display: flex;
align-items: center;
padding: 9px 10px;
font-size: 14px;
line-height: 22px;
color: var(--light-usage-text-color-text-0,
var(--light-usage-text-color-text-0, #1c1f23));
.icon-note {
margin-right: 8px;
>svg {
width: 16px;
height: 16px;
>path {
fill: #3370ff;
}
}
}
}
}
}
.popover {
padding: 12px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
color: #2e3238;
}
.pointer {
cursor: pointer;
}
.loading-more,
.no-more {
position: relative;
display: flex;
grid-column: 1 / -1;
align-items: center;
justify-content: center;
width: 100%;
padding: 13px 0;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: var(--light-usage-text-color-text-2,
var(--light-usage-text-color-text-2, rgb(28 31 35 / 60%)));
}

View File

@@ -0,0 +1,21 @@
/*
* 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.
*/
export {
KnowledgeCardListVertical,
type DatasetCardListVerticalOperations,
type DatasetCardListVerticalProps,
} from './vertical';

View File

@@ -0,0 +1,280 @@
/*
* 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 { type FC } from 'react';
import { unix } from 'dayjs';
import cs from 'classnames';
import { useBoolean } from 'ahooks';
import { IconSpin } from '@douyinfe/semi-icons';
import { BotE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { type ButtonProps } from '@coze-arch/bot-semi/Button';
import {
UITag,
UIButton,
Typography,
Space,
Avatar,
Popover,
} from '@coze-arch/bot-semi';
import { IconNote } from '@coze-arch/bot-icons';
import {
OrderField,
type Dataset,
DatasetStatus,
StorageLocation,
} from '@coze-arch/bot-api/knowledge';
import { SpaceType } from '@coze-arch/bot-api/developer_api';
import { getEllipsisCount, formatBytes } from '../../utils';
import { FilePopover } from './components';
import styles from './index.module.less';
const { Text } = Typography;
export interface DatasetCardListVerticalOperations {
onAdd: (dataset: Dataset) => void | Promise<void>;
onRemove: (dataset: Dataset) => void | Promise<void>;
isAdded: (id: string) => boolean;
}
function AddedButton(buttonProps: ButtonProps) {
const [isMouseIn, { setFalse, setTrue }] = useBoolean(false);
const onMouseEnter = () => {
setTrue();
};
const onMouseLeave = () => {
setFalse();
};
return (
<UIButton
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
{...buttonProps}
className={cs({
[buttonProps.className || '']: Boolean(buttonProps.className),
[styles.addedMouseIn]: isMouseIn,
})}
>
{isMouseIn ? I18n.t('Remove') : I18n.t('Added')}
</UIButton>
);
}
export type DatasetCardListVerticalProps = DatasetCardListVerticalOperations & {
list: Dataset[];
loading: boolean;
noMore: boolean;
searchType: OrderField;
onClickKnowledgeDetail?: (knowledgeID: string) => void;
};
const DEFAULT_BOT_NUM = 99;
const SpaceTags = (item: Dataset) => (
<Space className={styles.tags} wrap>
{item.processing_file_list?.length ? (
<FilePopover fileNames={item.processing_file_list || []}>
<UITag color="teal" className={styles['file-list']}>
{I18n.t('dataset_data_processing_tag', {
num: item.processing_file_list?.length || 0,
})}
</UITag>
</FilePopover>
) : null}
<UITag color="grey">
{formatBytes(parseInt(String(item.all_file_size)))}
</UITag>
{item.file_list?.length ? (
<Popover
trigger="hover"
showArrow
content={
<div className={styles['file-list-details']}>
<div className={styles['dataset-name']}>{item.name || ''}</div>
<div className={styles['file-info']}>
{item.file_list?.map(fileInfo => (
<div className={styles['file-info-item']} key={fileInfo}>
<IconNote className={styles['icon-note']} />
{fileInfo}
</div>
))}
</div>
</div>
}
>
<UITag color="grey">
{I18n.t('dataset_bot_count_tag', {
num: getEllipsisCount(item.file_list?.length || 0, DEFAULT_BOT_NUM),
})}
</UITag>
</Popover>
) : (
<UITag color="grey">
{I18n.t('dataset_bot_count_tag', {
num: getEllipsisCount(item.file_list?.length || 0, DEFAULT_BOT_NUM),
})}
</UITag>
)}
{item.storage_location === StorageLocation.OpenSearch ? (
<UITag color="cyan">{I18n.t('knowledge_es_001')}</UITag>
) : null}
</Space>
);
export const KnowledgeCardListVertical: FC<DatasetCardListVerticalProps> = ({
list,
loading,
noMore,
onAdd,
onRemove,
isAdded,
searchType,
onClickKnowledgeDetail,
}) => {
const { id: spaceId, space_type } = useSpaceStore(s => s.space);
const isPersonal = space_type === SpaceType.Personal;
const handleRow = (e: { stopPropagation: () => void }, id: string) => {
e.stopPropagation();
if (onClickKnowledgeDetail) {
onClickKnowledgeDetail(id);
} else {
window.open(`/space/${spaceId}/knowledge/${id}`);
}
};
return (
<div className={styles.container}>
{list.map(item => (
<div
className={styles.item}
key={item.dataset_id || ''}
onClick={e => handleRow(e, item?.dataset_id || '')}
>
<Avatar shape="square" src={item.icon_url} className={styles.left} />
<div
className={styles.content}
data-testid={`${BotE2e.BotKnowledgeSelectListModalName}.${item.name}`}
data-dtestid={`${BotE2e.BotKnowledgeSelectListModalName}.${item.name}`}
>
<Text className={styles.title} ellipsis={{ showTooltip: true }}>
{item.name || ''}
</Text>
{item.description ? (
<Typography.Text
className={styles.description}
ellipsis={{ rows: 1 }}
>
{item.description}
</Typography.Text>
) : null}
{!item.description && !!item.file_list?.length && (
<Typography.Text
className={styles.description}
ellipsis={{ rows: 1 }}
>
{item.file_list?.join('、')}
</Typography.Text>
)}
<div className={styles['tags-wapper']}>
<SpaceTags {...item}></SpaceTags>
<div className={styles.info}>
{!isPersonal && (
<>
<Avatar
src={item.avatar_url}
style={{ width: 14, height: 14 }}
/>
<Text
className={cs(styles.creator)}
ellipsis={{ showTooltip: true }}
>
{item.creator_name || ''}
</Text>
<span className={styles['border-right']}></span>
</>
)}
{searchType === OrderField.CreateTime ? (
<span className={styles.creator}>
{I18n.t('dataset_bot_create_time_knowledge', {
time: unix(item.create_time || 0).format(
'YYYY-MM-DD HH:mm',
),
})}
</span>
) : (
<span className={styles.creator}>
{I18n.t('dataset_bot_update_time_knowledge', {
time: unix(item.update_time || 0).format(
'YYYY-MM-DD HH:mm',
),
})}
</span>
)}
</div>
</div>
</div>
<div
className={styles.right}
onClick={e => e.stopPropagation()}
data-testid={`${BotE2e.BotKnowledgeSelectListModalAddBtn}.${item.name}`}
>
{isAdded(item.dataset_id || '') ? (
<AddedButton
className={cs(styles.button, styles.added)}
onClick={() => onRemove(item)}
>
{I18n.t('Added')}
</AddedButton>
) : (
<UIButton
disabled={item.status === DatasetStatus.DatasetForbid}
className={styles.button}
onClick={() => onAdd(item)}
data-testid="bot.database.add.modal.add.button"
>
{I18n.t('Add_2')}
</UIButton>
)}
</div>
</div>
))}
{loading ? (
<div className={styles['loading-more']}>
<IconSpin spin style={{ marginRight: '4px' }} />
<div>{I18n.t('Loading')}</div>
</div>
) : null}
{noMore ? (
<div className={styles['no-more']}>
<div>{I18n.t('No_more')}</div>
</div>
) : null}
</div>
);
};

View File

@@ -0,0 +1,266 @@
@common-box-shadow: 0px 2px 8px 0px rgba(31, 35, 41, 0.02),
0px 2px 4px 0px rgba(31, 35, 41, 0.02), 0px 2px 2px 0px rgba(31, 35, 41, 0.02);
.common-svg-icon(@size: 14px, @color: #3370ff) {
>svg {
width: @size;
height: @size;
>path {
fill: @color;
}
}
}
.data-set-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 48px;
margin-bottom: 4px;
padding: 8px;
background: rgba(6, 7, 9, 2%);
border-radius: var(--default, 8px);
.data-set-item-right {
display: none;
}
&:hover {
background: rgba(6, 7, 9, 14%);
.data-set-item-right {
display: flex;
gap: 4px;
align-items: center;
}
}
}
.data-set-item-left {
cursor: pointer;
display: flex;
align-items: center;
width: calc(100% - 60px);
margin-right: 20px;
.minus {
flex-shrink: 0;
margin-left: auto;
}
.data-set-name {
overflow: hidden;
flex: 1;
/* 142.857% */
padding-left: 8px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: rgba(6, 7, 9, 80%);
text-overflow: ellipsis;
}
.data-set-desc {
overflow: hidden;
padding-left: 8px;
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: rgba(6, 7, 9, 50%);
text-overflow: ellipsis;
}
}
.icon-note {
/* stylelint-disable-next-line declaration-no-important */
width: 24px !important;
/* stylelint-disable-next-line declaration-no-important */
height: 24px !important;
/* stylelint-disable-next-line declaration-no-important */
border-radius: 6px !important;
}
.card-content {
display: flex;
flex-direction: column;
max-width: calc(100% - 24px);
}
.icon-no {
.common-svg-icon(14px, rgba(107, 109, 117, 1));
&:hover {
background-color: var(--semi-color-fill-0);
}
}
.between {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.icon-copy {
.common-svg-icon(14px, rgba(107, 109, 117, 1));
&:hover {
background-color: var(--semi-color-fill-0);
}
}
.data-set-content {
.dataset-setting-tip {
margin: 8px 0 20px;
padding: 8px;
font-size: 12px;
line-height: 16px;
color: var(--light-usage-text-color-text-1, rgb(28 29 35 / 80%));
background: var(--light-usage-fill-color-fill-0, rgb(46 46 56 / 4%));
border-radius: 8px;
.copy-trigger {
cursor: pointer;
margin: 0 4px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--light-color-brand-brand-5, #4d53e8);
.icon-copy {
.common-svg-icon(14px, var(--light-color-brand-brand-5, #4d53e8));
/* stylelint-disable-next-line declaration-no-important */
margin-right: 0 !important;
}
}
:global {
.semi-tag-grey-light {
/* stylelint-disable-next-line declaration-no-important */
background: #fff !important;
}
}
}
}
.failed-tag,
.processing-tag {
font-weight: 500;
line-height: 16px;
}
.processing-tag {
color: var(--light-color-green-green-6, #32A247);
background: var(--light-color-green-green-1, #D2F3D5);
}
.failed-tag {
color: var(--light-color-red-red-6, #DB2E13);
background: var(--light-color-red-red-1, #FFE0D2);
}
// .default-text {
// .tip-text;
// }
.setting-trigger {
cursor: pointer;
display: flex;
column-gap: 4px;
align-items: center;
margin-left: 8px;
font-size: 12px;
font-weight: 600;
font-style: normal;
line-height: 16px;
color: var(--light-color-brand-brand-5, #4d53e8);
&-icon {
svg {
width: 10px;
height: 10px;
}
}
:global {
.semi-button-content-right {
display: flex;
align-items: center;
}
}
}
.setting-content-popover {
background: #f7f7fa;
border-radius: 12px;
}
.setting {
overflow-y: auto;
height: 454px;
padding: 24px;
font-size: 14px;
line-height: 20px;
color: var(--light-usage-text-color-text-0, #1f2329);
.setting-title {
font-size: 18px;
font-weight: 600;
line-height: 24px;
}
.setting-item {
display: flex;
align-items: self-start;
margin-top: 16px;
.setting-item-copy {
cursor: pointer;
margin: 0 4px;
padding: 2px 4px 2px 8px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--light-color-brand-brand-5, #4d53e8);
border-radius: 6px;
.icon-copy {
.common-svg-icon(14px, var(--light-color-brand-brand-5, #4d53e8));
margin: 0 0 0 4px;
}
}
:global {
.semi-tag-grey-light {
/* stylelint-disable-next-line declaration-no-important */
background: var(--light-color-brand-brand-1, #d9dcfa) !important;
}
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* 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 copy from 'copy-to-clipboard';
import { useDataNavigate } from '@coze-data/knowledge-stores';
import { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { UIIconButton, Typography, Toast, Avatar } from '@coze-arch/bot-semi';
import { CustomError } from '@coze-arch/bot-error';
import { type Dataset } from '@coze-arch/bot-api/knowledge';
import { IconCozCopy, IconCozMinusCircle } from '@coze-arch/coze-design/icons';
import { Tooltip } from '@coze-arch/coze-design';
import styles from './index.module.less';
export interface DataSetItemProps {
dataSet: Dataset;
isReadonly?: boolean;
onRemove: () => void;
onClick?: (datasetID: string) => void;
}
export const KnowledgeCard: React.FC<DataSetItemProps> = ({
dataSet,
isReadonly,
onRemove,
onClick,
}) => {
const { name, description, icon_url, dataset_id: id } = dataSet;
const resourceNavigate = useDataNavigate();
const navigateToKnowledgePage = (): void => {
resourceNavigate.toResource?.('knowledge', id);
};
const onCopy = (text: string) => {
const res = copy(text);
if (!res) {
throw new CustomError(ReportEventNames.parmasValidation, 'empty copy');
}
Toast.success({
content: I18n.t('copy_success'),
showClose: false,
id: 'dataset_copy_id',
});
};
return (
<div className={styles['data-set-item']}>
<div
className={styles['data-set-item-left']}
onClick={() => {
if (!id) {
return;
}
onClick ? onClick(id) : navigateToKnowledgePage();
}}
>
<Avatar shape="square" src={icon_url} className={styles['icon-note']} />
<div className={styles['card-content']}>
<Typography.Text
className={styles['data-set-name']}
ellipsis={{ showTooltip: true }}
>
{name}
</Typography.Text>
<Typography.Text
className={styles['data-set-desc']}
ellipsis={{ showTooltip: true }}
>
{description}
</Typography.Text>
</div>
</div>
<div className={styles['data-set-item-right']}>
{!isReadonly && (
<Tooltip content={I18n.t('Copy_name')}>
<UIIconButton
// wrapperClass={commonStyles['icon-button-16']}
iconSize="small"
icon={<IconCozCopy className={styles['icon-copy']} />}
onClick={() => name && onCopy(name)}
/>
</Tooltip>
)}
{!isReadonly && (
<Tooltip content={I18n.t('remove_dataset')}>
<UIIconButton
// wrapperClass={commonStyles['icon-button-16']}
iconSize="small"
icon={<IconCozMinusCircle className={styles['icon-no']} />}
onClick={onRemove}
/>
</Tooltip>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,45 @@
/*
* 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 classNames from 'classnames';
import { IconCozKnowledgeFill } from '@coze-arch/coze-design/icons';
interface SiderCategoryProps {
label: string;
selected: boolean;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}
const SiderCategory = ({ label, onClick, selected }: SiderCategoryProps) => (
<div
onClick={onClick}
className={classNames([
'flex items-center gap-[8px] px-[12px]',
'px-[12px] py-[6px] rounded-[8px]',
'cursor-pointer',
'hover:text-[var(--light-usage-text-color-text-0,#1c1f23)]',
'hover:bg-[var(--light-usage-fill-color-fill-0,rgba(46,50,56,5%))]',
selected &&
'text-[var(--light-usage-text-color-text-0,#1c1d23)] bg-[var(--light-usage-fill-color-fill-0,rgba(46,47,56,5%))]',
])}
>
<IconCozKnowledgeFill />
{label}
</div>
);
export default SiderCategory;

View File

@@ -0,0 +1,177 @@
/*
* 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 type { FC, ReactNode } from 'react';
import classNames from 'classnames';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { type FilterKnowledgeType } from '@coze-data/utils';
import { type UnitType } from '@coze-data/knowledge-resource-processor-core';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { type Dataset } from '@coze-arch/bot-api/knowledge';
import { DATA_REFACTOR_CLASS_NAME } from '@/constant';
import {
useKnowledgeFilter,
Scene,
type DatasetFilterType,
} from './use-knowledge-filter';
import { KnowledgeCardListVertical } from './knowledge-card-list';
import s from './index.module.less';
export interface DataSetModalContentProps {
datasetList: Dataset[];
onDatasetListChange: (list: Dataset[]) => void;
onClickAddKnowledge?: (
datasetId: string,
type: UnitType,
shouldUpload?: boolean,
) => void;
beforeCreate?: (shouldUpload: boolean) => void;
onClickKnowledgeDetail?: (knowledgeID: string) => void;
canCreate?: boolean;
defaultType?: FilterKnowledgeType;
knowledgeTypeConfigList?: FilterKnowledgeType[];
projectID?: string;
showFilters?: DatasetFilterType[];
hideHeader?: boolean;
createKnowledgeModal?: {
modal: ReactNode;
open: () => void;
close: () => void;
};
}
const useKnowledgeListModalContent = ({
datasetList,
onDatasetListChange,
onClickAddKnowledge,
beforeCreate,
onClickKnowledgeDetail,
canCreate = true,
defaultType,
knowledgeTypeConfigList,
projectID,
showFilters = ['scope-type', 'search-type', 'query-input'],
hideHeader,
createKnowledgeModal,
}: DataSetModalContentProps) => {
const botId = useBotInfoStore(state => state.botId);
const { renderContentFilter, renderSearch, renderCreateBtn, renderFilters } =
useKnowledgeFilter({
hideHeader,
showFilters,
scene: Scene.MODAL,
headerClassName: classNames(
s['dataset-header'],
DATA_REFACTOR_CLASS_NAME,
),
onClickAddKnowledge,
beforeCreate,
canCreate,
defaultType,
knowledgeTypeConfigList,
projectID,
createKnowledgeModal,
children: ({ list, loading, noMore, searchType }) => (
<KnowledgeCardListVertical
searchType={searchType}
noMore={noMore}
list={list}
loading={loading}
onAdd={async dataset => {
await onDatasetListChange([...datasetList, dataset]);
sendTeaEvent(EVENT_NAMES.click_database_select, {
operation: 'add',
bot_id: botId,
});
// Toast.success({
// showClose: false,
// content: I18n.t('bot_edit_dataset_added_toast', {
// dataset_name: dataset.name || '',
// }),
// style: {
// wordWrap: 'break-word',
// },
// });
}}
onRemove={dataset => {
onDatasetListChange(
datasetList.filter(
item => item.dataset_id !== dataset.dataset_id,
),
);
sendTeaEvent(EVENT_NAMES.click_database_select, {
operation: 'remove',
bot_id: botId,
});
// Toast.success({
// showClose: false,
// content: I18n.t('bot_edit_dataset_removed_toast', {
// dataset_name: dataset.name || '',
// }),
// style: {
// wordWrap: 'break-word',
// },
// });
}}
isAdded={id => datasetList.some(dataset => dataset.dataset_id === id)}
onClickKnowledgeDetail={onClickKnowledgeDetail}
/>
),
});
return {
renderContent: renderContentFilter,
renderSearch,
renderCreateBtn,
renderFilters,
};
};
const KnowledgeListModalContent: FC<DataSetModalContentProps> = ({
datasetList,
onDatasetListChange,
onClickAddKnowledge,
beforeCreate,
onClickKnowledgeDetail,
canCreate = true,
defaultType,
knowledgeTypeConfigList,
projectID,
createKnowledgeModal,
}) => {
const { renderContent } = useKnowledgeListModalContent({
datasetList,
onDatasetListChange,
onClickAddKnowledge,
beforeCreate,
onClickKnowledgeDetail,
canCreate,
defaultType,
knowledgeTypeConfigList,
projectID,
createKnowledgeModal,
});
return <>{renderContent()}</>;
};
export { useKnowledgeListModalContent, KnowledgeListModalContent };

View File

@@ -0,0 +1,120 @@
/* stylelint-disable declaration-no-important */
.spin {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100% !important;
height: 100% !important;
:global {
.semi-spin-children {
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
height: 100%;
}
}
}
.empty {
&-image {
width: 200px;
height: 200px;
}
&-content {
width: 100%;
text-align: center;
}
}
.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
&>* {
width: 100%;
}
.new-filter-header {
justify-content: space-between !important;
}
.header {
display: flex;
flex-direction: row;
flex-shrink: 0;
justify-content: flex-end;
height: fit-content;
padding: 0 36px 8px;
.select {
width: 160px;
:global {
.semi-select-selection-text {
color: rgba(28, 31, 35, 60%);
}
}
}
.input {
width: 260px;
background: #fff;
}
.tab-select {
margin-right: 10px;
}
}
.content {
flex: 1;
&.scrollable {
overflow: auto;
}
&.centered {
display: flex;
flex-direction: column;
justify-content: center;
}
}
.footer {
display: flex;
flex-direction: row;
flex-shrink: 0;
justify-content: flex-end;
height: fit-content;
padding: 24px;
}
}
.file-type-tab {
display: flex;
margin-left: 8px;
padding: 6px 0;
&-item {
cursor: pointer;
font-weight: 600;
color: #1D1C2399;
&-active {
cursor: pointer;
font-weight: 600;
color: #4D53E8;
}
}
}

View File

@@ -0,0 +1,629 @@
/*
* 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.
*/
/* eslint-disable max-lines-per-function */
/* eslint-disable max-lines -- 待拆分 */
/* eslint-disable @coze-arch/max-line-per-function */
import {
type FC,
useEffect,
useState,
useRef,
type ReactNode,
useMemo,
} from 'react';
import { isFunction, uniq, debounce } from 'lodash-es';
import cs from 'classnames';
import {
useInfiniteScroll,
useUpdateEffect,
useDocumentVisibility,
} from 'ahooks';
import { FilterKnowledgeType } from '@coze-data/utils';
import { DataNamespace, dataReporter } from '@coze-data/reporter';
import { type UnitType } from '@coze-data/knowledge-resource-processor-core';
import { BotE2e } from '@coze-data/e2e';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import {
UIButton,
UIEmpty,
UISelect,
Spin,
UISearch,
Divider,
} from '@coze-arch/bot-semi';
import {
OrderField,
type Dataset,
DatasetScopeType,
FormatType,
} from '@coze-arch/bot-api/knowledge';
import { SpaceType } from '@coze-arch/bot-api/developer_api';
import { KnowledgeApi } from '@coze-arch/bot-api';
import { Input } from '@coze-arch/coze-design';
import { DATA_REFACTOR_CLASS_NAME } from '../../constant';
import styles from './index.module.less';
interface GetDatasetListData {
list: Dataset[];
nextPageIndex: number;
total: number;
}
const DEFAULT_PAGE_SIZE = 20;
const getDatasetList = async (
props: {
query?: string;
search_type?: OrderField;
space_id: string;
scope_type?: DatasetScopeType;
format_type?: FormatType;
projectID?: string;
},
pageIndex = 1,
) => {
const { query, search_type, space_id, scope_type, format_type, projectID } =
props;
const resp = await KnowledgeApi.ListDataset({
space_id,
page: pageIndex,
size: DEFAULT_PAGE_SIZE,
filter: {
name: query,
scope_type,
format_type,
},
order_field: search_type,
project_id: projectID,
});
return {
list: resp?.dataset_list || [],
nextPageIndex: pageIndex + 1,
total: Number(resp?.total),
};
};
const DEFAULT_SEARCH_TYPE = OrderField.CreateTime;
interface CreateKnowledgeModalProps {
modal: ReactNode;
open: () => void;
close: () => void;
}
const EmptyToCreate: FC<{
onAdd: () => void;
scene: Scene;
canCreate: boolean;
createKnowledgeModal?: CreateKnowledgeModalProps;
}> = ({ onAdd, scene, canCreate, createKnowledgeModal }) => {
const handleAdd = () => {
if (scene === Scene.MODAL) {
onAdd();
return;
}
createKnowledgeModal?.open();
};
return (
<>
<div className={cs(styles.content, styles.centered)}>
<UIEmpty
className={styles.empty}
empty={{
...(canCreate
? {
btnText: I18n.t('datasets_create_btn'),
btnOnClick: handleAdd,
}
: {}),
title: I18n.t('datasets_empty_title'),
description: I18n.t('datasets_empty_description'),
}}
/>
</div>
{createKnowledgeModal?.modal}
</>
);
};
export interface DatasetFilterAction {
list: Dataset[];
size: number;
query: string | undefined;
searchType: OrderField;
loading: boolean;
noMore: boolean;
resetFilter: () => void;
refresh: () => void;
createDataset?: (name: string, source_type: number) => Promise<void>;
deleteDataset?: (id: string) => Promise<void>;
updateDataset?: (id: string, name: string) => Promise<void>;
}
export type DatasetFilterType = 'scope-type' | 'search-type' | 'query-input';
export interface DatasetFilterProps {
hideHeader?: boolean;
children:
| ((action: DatasetFilterAction) => React.ReactNode)
| React.ReactNode;
showFilters?: DatasetFilterType[];
headerClassName?: string;
scene?: Scene;
onClickAddKnowledge?: (
datasetId: string,
type: UnitType,
shouldUpload?: boolean,
) => void;
beforeCreate?: (shouldUpload: boolean) => void;
canCreate: boolean;
defaultType?: FilterKnowledgeType;
knowledgeTypeConfigList?: FilterKnowledgeType[];
projectID?: string;
createKnowledgeModal?: CreateKnowledgeModalProps;
}
export enum Scene {
PAGE = 'page',
MODAL = 'modal',
}
const defaultKnowledgeTypeFallback = (param: FilterKnowledgeType[]) => {
if (param.includes(FilterKnowledgeType.ALL)) {
return FilterKnowledgeType.ALL;
}
return param.at(0) ?? FilterKnowledgeType.ALL;
};
const useKnowledgeFilter = ({
hideHeader,
children,
showFilters,
headerClassName,
scene = Scene.PAGE,
onClickAddKnowledge,
canCreate,
defaultType,
knowledgeTypeConfigList = [
FilterKnowledgeType.ALL,
FilterKnowledgeType.TEXT,
FilterKnowledgeType.TABLE,
FilterKnowledgeType.IMAGE,
],
projectID,
beforeCreate,
createKnowledgeModal,
}: DatasetFilterProps) => {
const uniqKnowledgeTypeConfigList = uniq(knowledgeTypeConfigList);
const [currentKnowledgeType, setCurrentKnowledgeType] = useState(
defaultType || defaultKnowledgeTypeFallback(uniqKnowledgeTypeConfigList),
);
const [query, setQuery] = useState<string>();
const [searchType, setSearchType] = useState<OrderField>(DEFAULT_SEARCH_TYPE);
const [scopeType, setScopeType] = useState<DatasetScopeType>(
projectID ? DatasetScopeType.ScopeSelf : DatasetScopeType.ScopeAll,
);
const scopeOptions = [
{
label: I18n.t('scope_all'),
value: DatasetScopeType.ScopeAll,
},
{
label: I18n.t('scope_self'),
value: DatasetScopeType.ScopeSelf,
},
];
const { id, space_type } = useSpaceStore(s => s.space);
const isPersonal = space_type === SpaceType.Personal;
const containerRef = useRef<HTMLDivElement>(null);
const { loading, data, loadingMore, noMore, reload } =
useInfiniteScroll<GetDatasetListData>(
(newData?: GetDatasetListData): Promise<GetDatasetListData> => {
if (!newData || newData.nextPageIndex === 1) {
containerRef.current?.scroll(0, 0);
}
return getDatasetList(
{
space_id: id || '',
query,
search_type: searchType,
scope_type: isPersonal ? DatasetScopeType.ScopeSelf : scopeType,
format_type:
currentKnowledgeType === FilterKnowledgeType.ALL
? undefined
: {
[FilterKnowledgeType.TABLE]: FormatType.Table,
[FilterKnowledgeType.TEXT]: FormatType.Text,
[FilterKnowledgeType.IMAGE]: FormatType.Image,
}[currentKnowledgeType],
projectID,
},
newData?.nextPageIndex,
);
},
{
manual: true,
isNoMore: newData =>
Boolean(
!newData?.total ||
(newData.nextPageIndex - 1) * DEFAULT_PAGE_SIZE >= newData.total,
),
onError: error => {
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
eventName: REPORT_EVENTS.KnowledgeGetDataSetList,
error,
});
},
target: containerRef,
reloadDeps: [query, searchType, scopeType, projectID],
},
);
useUpdateEffect(() => {
handleResetFilter();
}, [id]);
const documentVisibility = useDocumentVisibility();
useEffect(() => {
if (documentVisibility === 'visible') {
reload();
}
}, [documentVisibility]);
const handleResetFilter = () => {
setQuery(undefined);
setSearchType(DEFAULT_SEARCH_TYPE);
};
const handleSearchTypeChange = (value: OrderField) => {
setSearchType(value);
};
const handleQueryChange = (value = '') => {
setQuery(value);
};
const handleAdd = () => {
createKnowledgeModal?.open();
};
const renderContent = () => {
/** 有数据则展示列表 */
if (data?.total) {
return (
<>
<div
className={cs(styles.content, styles.scrollable)}
ref={containerRef}
>
{isFunction(children)
? children({
size: DEFAULT_PAGE_SIZE,
query,
searchType,
loading: loadingMore,
list: data.list,
noMore,
resetFilter: handleResetFilter,
refresh: reload,
})
: children}
</div>
</>
);
}
/** 无数据且未在加载则展示空状态 */
if (!loading) {
return (
<EmptyToCreate
scene={scene}
onAdd={() => {
handleAdd();
}}
canCreate={canCreate}
createKnowledgeModal={createKnowledgeModal}
/>
);
}
/** 无数据且加载中则不展示 */
return null;
};
const renderSearch = useMemo(
() => () => (
<Input
autoFocus
key="query-input"
placeholder={I18n.t('db2_014')}
onChange={debounce(handleQueryChange, 500)}
/>
),
[],
);
const renderCreateBtn = useMemo(
() => () => (
<UIButton
theme="solid"
onClick={handleAdd}
data-testid={BotE2e.BotKnowledgeSelectListModalCreateBtn}
>
{I18n.t('datasets_create_btn')}
</UIButton>
),
[handleAdd],
);
const renderFilters = useMemo(
() => () => (
<>
<div className={styles['file-type-tab']}>
{uniqKnowledgeTypeConfigList.reduce<ReactNode[]>(
(
accumulator: ReactNode[],
currentValue: FilterKnowledgeType,
currentIndex: number,
) => {
const reactNode = renderKnowledgeTypeConfigNode(currentValue);
if (currentIndex !== 0) {
return accumulator.concat([
<Divider layout="vertical" margin="12px" />,
reactNode,
]);
}
return accumulator.concat([reactNode]);
},
[],
)}
</div>
<div className={'flex'}>
{uniq(showFilters).map((filterType: DatasetFilterType) => {
if (filterType === 'scope-type') {
return !isPersonal ? (
<UISelect
label={I18n.t('Creator')}
showClear={false}
value={scopeType}
optionList={scopeOptions}
onChange={v => {
setScopeType(v as DatasetScopeType);
}}
/>
) : null;
} else if (filterType === 'search-type') {
return (
<UISelect
data-testid={
BotE2e.BotKnowledgeSelectListModalCreateDateSelect
}
label={I18n.t('Sort')}
showClear={false}
value={searchType}
optionList={[
{
label: I18n.t('Create_time'),
value: OrderField.CreateTime,
},
{
label: I18n.t('Update_time'),
value: OrderField.UpdateTime,
},
]}
onChange={v => {
handleSearchTypeChange(v as OrderField);
}}
/>
);
}
})}
</div>
</>
),
[
headerClassName,
handleSearchTypeChange,
scopeType,
scopeOptions,
isPersonal,
showFilters,
uniqKnowledgeTypeConfigList,
],
);
useEffect(() => {
reload();
}, [currentKnowledgeType]);
const renderKnowledgeTypeConfigNode = (type: FilterKnowledgeType) => {
if (type === FilterKnowledgeType.ALL) {
return (
<div
data-testid={BotE2e.BotKnowledgeSelectListModalAllTab}
key={FilterKnowledgeType.ALL}
onClick={() => setCurrentKnowledgeType(FilterKnowledgeType.ALL)}
className={
currentKnowledgeType === FilterKnowledgeType.ALL
? styles['file-type-tab-item-active']
: styles['file-type-tab-item']
}
>
{I18n.t('kl2_010')}
</div>
);
}
if (type === FilterKnowledgeType.TEXT) {
return (
<div
data-testid={BotE2e.BotKnowledgeSelectListModalTextTab}
key={FilterKnowledgeType.TEXT}
onClick={() => setCurrentKnowledgeType(FilterKnowledgeType.TEXT)}
className={
currentKnowledgeType === FilterKnowledgeType.TEXT
? styles['file-type-tab-item-active']
: styles['file-type-tab-item']
}
>
{I18n.t('kl2_011')}
</div>
);
}
if (type === FilterKnowledgeType.TABLE) {
return (
<div
data-testid={BotE2e.BotKnowledgeSelectListModalTableTab}
key={FilterKnowledgeType.TABLE}
onClick={() => setCurrentKnowledgeType(FilterKnowledgeType.TABLE)}
className={
currentKnowledgeType === FilterKnowledgeType.TABLE
? styles['file-type-tab-item-active']
: styles['file-type-tab-item']
}
>
{I18n.t('kl2_012')}
</div>
);
}
if (type === FilterKnowledgeType.IMAGE) {
return (
<div
data-testid={BotE2e.BotKnowledgeSelectListModalPhotoTab}
key={FilterKnowledgeType.IMAGE}
onClick={() => setCurrentKnowledgeType(FilterKnowledgeType.IMAGE)}
className={
currentKnowledgeType === FilterKnowledgeType.IMAGE
? styles['file-type-tab-item-active']
: styles['file-type-tab-item']
}
>
{I18n.t('knowledge_photo_025')}
</div>
);
}
return null;
};
const renderContentFilter = () => (
<Spin spinning={loading} wrapperClassName={styles.spin}>
<div className={cs(styles.container, DATA_REFACTOR_CLASS_NAME)}>
{!hideHeader && showFilters?.length ? (
<div
className={cs(
styles.header,
headerClassName,
styles['new-filter-header'],
)}
>
<div className={styles['file-type-tab']}>
{uniqKnowledgeTypeConfigList.reduce<ReactNode[]>(
(
accumulator: ReactNode[],
currentValue: FilterKnowledgeType,
currentIndex: number,
) => {
const reactNode = renderKnowledgeTypeConfigNode(currentValue);
if (currentIndex !== 0) {
return accumulator.concat([
<Divider layout="vertical" margin="12px" />,
reactNode,
]);
}
return accumulator.concat([reactNode]);
},
[],
)}
</div>
<div className="flex gap-[8px]">
{uniq(showFilters).map((filterType: DatasetFilterType) => {
if (filterType === 'scope-type') {
return !isPersonal ? (
<UISelect
label={I18n.t('Creator')}
showClear={false}
value={scopeType}
optionList={scopeOptions}
onChange={v => {
setScopeType(v as DatasetScopeType);
}}
/>
) : null;
} else if (filterType === 'search-type') {
return (
<UISelect
data-testid={
BotE2e.BotKnowledgeSelectListModalCreateDateSelect
}
label={I18n.t('Sort')}
showClear={false}
value={searchType}
optionList={[
{
label: I18n.t('Create_time'),
value: OrderField.CreateTime,
},
{
label: I18n.t('Update_time'),
value: OrderField.UpdateTime,
},
]}
onChange={v => {
handleSearchTypeChange(v as OrderField);
}}
/>
);
} else if (filterType === 'query-input') {
return (
<UISearch
key="filterType"
loading={loading}
onSearch={handleQueryChange}
/>
);
}
})}
{scene === Scene.MODAL && canCreate ? (
<UIButton
theme="solid"
onClick={handleAdd}
data-testid={BotE2e.BotKnowledgeSelectListModalCreateBtn}
>
{I18n.t('datasets_create_btn')}
</UIButton>
) : null}
</div>
</div>
) : null}
{renderContent()}
</div>
{createKnowledgeModal?.modal}
</Spin>
);
return { renderContentFilter, renderSearch, renderCreateBtn, renderFilters };
};
export { useKnowledgeFilter };