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,243 @@
/* stylelint-disable declaration-no-important */
.photo-list {
overflow: auto;
display: flex;
flex-direction: column;
height: calc(100vh - 170px);
padding: 0 24px;
.photo-list-spin {
flex: 1
}
}
.footer {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
.spin {
width: 100%;
height: 60px;
font-size: 14px;
}
}
.card-group {
margin-bottom: 25px;
}
// Card 整体
.card {
cursor: auto;
width: 222px;
height: 258px;
border-radius: 8px;
&:hover {
box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 8%);
}
:global {
// 固定高度142超出高度的图片截取居中部分展示
.semi-card-cover {
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
height: 142px;
}
}
}
.card-disabled {
:global {
.semi-card-cover {
background: var(--Light-usage-fill---color-fill-0, rgba(46, 46, 57, 4%));
border-radius: var(--default, 8px) var(--default, 8px) 0 0;
svg {
width: 32px;
height: 32px;
}
}
}
}
// Card 封面
.card-cover {
cursor: pointer;
border-radius: 8px 8px 0 0;
// 设置最小高度142保证填满封面
img {
min-height: 142px;
}
}
.prohibit-cover {
overflow: hidden;
font-size: 12px;
line-height: 16px; /* 133.333% */
color: var(--Light-usage-text---color-text-3, rgba(29, 28, 36, 35%));
text-align: center;
text-overflow: ellipsis;
}
// Card 内容区 (title + description)
.card-content {
display: flex;
flex-direction: column;
gap: 8px;
.photo-name {
font-weight: 600;
line-height: 20px;
color: rgba(29, 28, 35, 100%);
}
.photo-description {
height: 32px;
font-size: 12px;
line-height: 16px;
.failed-tag {
padding: 0 6px;
color: rgba(219, 46, 19, 100%);
background-color: rgba(255, 224, 210, 100%);
border-radius: 4px;
}
.processing-tag {
padding: 0 6px;
color: #304cdb;
background-color: #d9e2ff;
border-radius: 4px;
}
}
}
// Card 底部栏(时间 + 操作按钮)
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
height: 24px;
.create-time {
font-size: 12px;
line-height: 16px;
color: rgba(28, 29, 35, 35%)
}
}
// UI稿的 confirm modal 和 semi 默认的不一样,需要手动调整样式
.confirm-modal {
:global {
.semi-modal-header {
margin-bottom: 16px;
// icon 和 title 的间距
.semi-modal-icon-wrapper {
margin-right: 8px;
}
// title 颜色
.semi-modal-confirm-title-text {
color: rgba(29, 28, 35, 100%);
}
// 关闭 icon 的 hover 颜色
.semi-button:hover {
background-color: rgba(46, 46, 56, 8%);
}
}
.semi-modal-body {
margin: 0;
padding: 16px 0;
.semi-modal-confirm-content {
color: rgba(29, 28, 35, 100%)
}
}
.semi-modal-footer button {
min-width: 96px;
margin-left: 16px;
}
}
}
// 编辑 photo 信息的弹窗
.modal-content {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
// photo 大图展示
// 用 flex 解决宽高和内部 image 不一致的问题
.photo-large {
position: relative;
display: flex;
align-items: center;
justify-content: center;
.arrow-button {
position: absolute;
top: 126px;
width: 48px;
height: 48px;
padding: 12px;
color: #1D1C23;
background-color: rgba(255, 255, 255, 50%);
border: none;
border-radius: 26px;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 20%), 0 0 1px 0 rgba(0, 0, 0, 20%);
transition: all 0.3s;
&:hover {
color: #4D53E8;
background-color: rgba(255, 255, 255, 80%);
}
}
}
// photo 描述输入框
.photo-caption-textarea {
position: relative;
:global {
.semi-input-textarea-counter {
height: 40px;
}
}
.ai-generate-button {
position: absolute;
bottom: 8px;
left: 12px;
}
// 解决 disabled button 样式错位的问题
>span {
display: inline !important;
}
}
}

View File

@@ -0,0 +1,386 @@
/*
* 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 @coze-arch/max-line-per-function */
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable max-lines-per-function */
import { useSearchParams } from 'react-router-dom';
import { useEffect, useState, type FC, useRef } from 'react';
import dayjs from 'dayjs';
import classNames from 'classnames';
import { DataNamespace, dataReporter } from '@coze-data/reporter';
import {
useKnowledgeParams,
useKnowledgeStore,
} from '@coze-data/knowledge-stores';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import {
Card,
CardGroup,
Image,
Space,
Spin,
Tooltip,
Typography,
UIIconButton,
UIModal,
UIEmpty,
} from '@coze-arch/bot-semi';
import {
IconCloseKnowledge,
IconDeleteOutline,
IconEdit,
IconWaringRed,
IconSegmentEmpty,
IconImageFailOutlined,
} from '@coze-arch/bot-icons';
import { DocumentStatus, type PhotoInfo } from '@coze-arch/bot-api/knowledge';
import { KnowledgeApi } from '@coze-arch/bot-api';
import { type ProgressItem, type ProgressMap } from '@/types';
import { usePhotoDetailModal } from '@/components/photo-detail-modal';
import { usePhotoList } from './use-photo-list';
import styles from './index.module.less';
export interface ImageKnowledgeWorkspaceProps {
progressMap: ProgressMap;
}
export const ImageKnowledgeWorkspace: FC<
ImageKnowledgeWorkspaceProps
> = props => {
const { progressMap } = props;
const [_, setSearchParams] = useSearchParams();
const params = useKnowledgeParams();
const firstAutoOpenEditDocumentId = params.first_auto_open_edit_document_id;
const [currentPhotoId, setCurrentPhotoId] = useState<string>('');
const [currentHoverCardId, setCurrentHoverCardId] = useState<string>('');
const dataSetDetail = useKnowledgeStore(state => state.dataSetDetail);
const canEdit = useKnowledgeStore(state => state.canEdit);
const searchValue = useKnowledgeStore(state => state.searchValue);
const photoFilterValue = useKnowledgeStore(state => state.photoFilterValue);
const ref = useRef<HTMLDivElement>(null);
const { data, loading, loadingMore, reloadAsync, noMore } = usePhotoList(
{
// @ts-expect-error -- linter-disable-autofix
datasetID: dataSetDetail?.dataset_id,
filterPhotoType: photoFilterValue,
searchValue,
},
{
// @ts-expect-error -- linter-disable-autofix
isNoMore: d => d?.list.length >= d?.total,
target: ref,
},
);
const photoList = data?.list;
const shouldAutoOpenDetailModal = photoList?.find(
i => i.document_id === firstAutoOpenEditDocumentId,
);
const previewPhotoList = photoList?.filter(
photo => photo.status !== DocumentStatus.AuditFailed,
);
const curPhoto = previewPhotoList?.find(
i => i.document_id === currentPhotoId,
);
const resetUrlQueryParams = () => {
setSearchParams((state: URLSearchParams) => {
state.delete('action_type');
return state;
});
};
const { node, open } = usePhotoDetailModal({
photo: curPhoto,
progressMap,
photoList: previewPhotoList,
canEdit: !!canEdit,
setCurrentPhotoId,
reload: reloadAsync,
onCancel: () => {
// 重置url参数
resetUrlQueryParams();
},
onSubmit: () => {
resetUrlQueryParams();
},
});
// 手动控制 data 加载时机
useEffect(() => {
if (dataSetDetail?.dataset_id) {
reloadAsync();
// 重新加载时,回到最顶部
ref.current?.scrollTo?.({
top: 0,
behavior: 'smooth',
});
}
}, [searchValue, photoFilterValue, dataSetDetail?.dataset_id]);
// 自动打开编辑弹窗
useEffect(() => {
if (shouldAutoOpenDetailModal) {
if (firstAutoOpenEditDocumentId) {
setCurrentPhotoId(firstAutoOpenEditDocumentId);
}
open();
}
}, [firstAutoOpenEditDocumentId, shouldAutoOpenDetailModal]);
return (
<>
<div className={styles['photo-list']} ref={ref}>
<Spin
spinning={loading}
wrapperClassName={styles['photo-list-spin']}
childStyle={{
height: '100%',
width: '100%',
}}
>
{/* @ts-expect-error -- linter-disable-autofix */}
{photoList?.length <= 0 ? (
<UIEmpty
empty={{
icon: <IconSegmentEmpty />,
title: I18n.t('query_data_empty'),
}}
/>
) : (
<CardGroup spacing={12} className={styles['card-group']}>
{photoList?.map(item => {
const {
url,
document_id,
name,
update_time,
caption: originCaption,
status: originStatus,
} = item;
// 此处使用 progressMap 可以保持不断刷新直至完成
// @ts-expect-error -- linter-disable-autofix
const status = progressMap[document_id]?.status || originStatus;
// 使用 progressMap 获取最新的caption
const caption =
// @ts-expect-error -- linter-disable-autofix
(progressMap[document_id] as ProgressItem & PhotoInfo)
?.caption || originCaption;
const hasCaption =
typeof caption === 'string' && Boolean(caption);
const handleEdit = () => {
// @ts-expect-error -- linter-disable-autofix
setCurrentPhotoId(document_id);
open();
};
const handleDelete = () => {
UIModal.error({
// 必填参数,统一 confirm modal 的样式
className: styles['confirm-modal'],
closeIcon: <IconCloseKnowledge />,
// 自定义参数
title: I18n.t('kl2_007'),
content: I18n.t(
'dataset_detail_table_deleteModel_description',
),
icon: <IconWaringRed />,
cancelText: I18n.t('Cancel'),
okText: I18n.t('Delete'),
okButtonProps: {
type: 'danger',
},
onOk: async () => {
try {
await KnowledgeApi.DeleteDocument({
// @ts-expect-error -- linter-disable-autofix
document_ids: [document_id],
});
await reloadAsync();
} catch (error) {
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
eventName: REPORT_EVENTS.KnowledgeDeleteDocument,
error: error as Error,
});
}
},
});
};
const onMouseEnter = () => {
// @ts-expect-error -- linter-disable-autofix
setCurrentHoverCardId(document_id);
};
const onMouseLeave = () => {
setCurrentHoverCardId('');
};
const isHover = currentHoverCardId === document_id;
const isAudiFailed =
originStatus === DocumentStatus.AuditFailed;
const getCaption = () => {
// 违规图片
if (isAudiFailed) {
return (
<span>
{I18n.t('knowledge_content_illegal_error_msg')}
</span>
);
}
// 处理失败
if (status === DocumentStatus.Failed) {
return (
<span className={styles['failed-tag']}>
{I18n.t('dataset_process_fail')}
</span>
);
}
// 处理中
if (status === DocumentStatus.Processing) {
return (
<span className={styles['processing-tag']}>
{I18n.t('datasets_segment_tag_processing')}
</span>
);
}
// 已标注
if (hasCaption) {
return caption;
}
// 未标注
return I18n.t('knowledge_photo_016');
};
return (
<Card
key={document_id}
cover={
isAudiFailed ? (
<div className={styles['prohibit-cover']}>
<IconImageFailOutlined size={'extra-small'} />
<p>{I18n.t('knowledge_photo_illegal_error_msg')}</p>
</div>
) : (
<Image
src={url}
// 仅设置宽度,高度会按图片原比例自动缩放
width={222}
preview={false}
onClick={handleEdit}
className={styles['card-cover']}
/>
)
}
headerLine={false}
bodyStyle={{
padding: '12px 16px',
}}
className={classNames(
styles.card,
isAudiFailed ? styles['card-disabled'] : '',
)}
>
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={styles['card-content']}
>
<Typography.Text
className={styles['photo-name']}
ellipsis={{
showTooltip: true,
}}
>
{name}
</Typography.Text>
<Typography.Paragraph
className={styles['photo-description']}
ellipsis={{
showTooltip: true,
rows: 2,
}}
style={{
color: hasCaption
? 'rgba(29, 28, 35, 0.6)'
: 'rgba(255, 178, 51, 1)',
}}
>
{getCaption()}
</Typography.Paragraph>
<div className={styles['card-footer']}>
<Typography.Text className={styles['create-time']}>
{/* @ts-expect-error -- linter-disable-autofix */}
{dayjs.unix(update_time).format('YYYY-MM-DD HH:mm')}
</Typography.Text>
{isHover && canEdit ? (
<Space spacing={12}>
<Tooltip content={I18n.t('Edit')}>
<UIIconButton
icon={<IconEdit />}
disabled={isAudiFailed}
onClick={handleEdit}
/>
</Tooltip>
<Tooltip content={I18n.t('Delete')}>
<UIIconButton
icon={<IconDeleteOutline />}
onClick={handleDelete}
/>
</Tooltip>
</Space>
) : null}
</div>
</div>
</Card>
);
})}
</CardGroup>
)}
</Spin>
<div className={styles.footer}>
{!noMore && (
<Spin
spinning={loadingMore}
tip={I18n.t('loading')}
wrapperClassName={styles.spin}
/>
)}
</div>
</div>
{node}
</>
);
};

View File

@@ -0,0 +1,89 @@
/*
* 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 InfiniteScrollOptions } from 'ahooks/lib/useInfiniteScroll/types';
import { useInfiniteScroll } from 'ahooks';
import { DataNamespace, dataReporter } from '@coze-data/reporter';
import { KNOWLEDGE_MAX_DOC_SIZE } from '@coze-data/knowledge-modal-base';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { type PhotoInfo } from '@coze-arch/bot-api/knowledge';
import { KnowledgeApi } from '@coze-arch/bot-api';
import { FilterPhotoType } from '@/types';
export interface UsePhotoListParams {
datasetID: string;
searchValue?: string;
filterPhotoType?: FilterPhotoType;
}
interface Result {
list: PhotoInfo[];
total: number;
}
const PAGE_SIZE = 20;
export const usePhotoList = (
params: UsePhotoListParams,
options: InfiniteScrollOptions<Result>,
) => {
const { datasetID, searchValue, filterPhotoType } = params;
const fetchPhotoList = async (
page: number,
pageSize: number,
// @ts-expect-error -- linter-disable-autofix
): Promise<Result> => {
try {
const res = await KnowledgeApi.ListPhoto({
page: page ?? 1,
size: pageSize ?? KNOWLEDGE_MAX_DOC_SIZE,
dataset_id: datasetID,
filter: {
keyword: searchValue,
has_caption:
filterPhotoType === FilterPhotoType.HasCaption
? true
: filterPhotoType === FilterPhotoType.NoCaption
? false
: // 传 undefined 代表返回全部
undefined,
},
});
return {
list: res.photo_infos || [],
total: res.total || 0,
};
} catch (error) {
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
eventName: REPORT_EVENTS.KnowledgePhotoList,
error: error as Error,
});
}
};
return useInfiniteScroll<Result>(
async d => {
const p = d ? Math.ceil(d.list.length / PAGE_SIZE) + 1 : 1;
return fetchPhotoList(p, PAGE_SIZE);
},
{
manual: true,
...options,
},
);
};

View File

@@ -0,0 +1,117 @@
/*
* 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 {
useDataNavigate,
useKnowledgeStore,
} from '@coze-data/knowledge-stores';
import { OptType } from '@coze-data/knowledge-resource-processor-core';
import { getKnowledgeIDEQuery } from '@coze-data/knowledge-common-services/use-case';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozArrowDown } from '@coze-arch/bot-icons';
import { FormatType } from '@coze-arch/bot-api/knowledge';
import { IconCozArrowUp } from '@coze-arch/coze-design/icons';
import { Button, Tooltip } from '@coze-arch/coze-design';
import { ImportKnowledgeSourceMenu } from '@/features/import-knowledge-source-menu';
import { type ImportKnowledgeSourceButtonProps } from '../module';
import {
createBtnDisableToolTip,
getTableFormatTooltip,
getDefaultFormatTooltip,
} from './services/use-case/disabled-tooltip';
export const ImportKnowledgeSourceButton = ({
disabledTooltip: disabledTooltipProp,
onSourceChange,
}: ImportKnowledgeSourceButtonProps) => {
const documentList = useKnowledgeStore(state => state.documentList);
const dataSetDetail = useKnowledgeStore(state => state.dataSetDetail);
const [visible, setVisible] = useState<boolean>(false);
const resourceNavigate = useDataNavigate();
const disabledTooltip =
disabledTooltipProp ?? createBtnDisableToolTip(dataSetDetail, documentList);
const query = getKnowledgeIDEQuery() as Record<string, string>;
if (disabledTooltip) {
return (
<Tooltip
content={disabledTooltip}
arrowPointAtCenter={false}
position="top"
>
<Button
data-testid={KnowledgeE2e.SegmentDetailAddBtn}
color="hgltplus"
disabled
iconPosition="right"
icon={<IconCozArrowDown className={'text-[12px]'} />}
>
{I18n.t('knowledg_unit_add_segments')}
</Button>
</Tooltip>
);
}
return (
<ImportKnowledgeSourceMenu
onVisibleChange={dropVisible => {
setVisible(dropVisible);
}}
onChange={unitType => {
if (onSourceChange) {
onSourceChange(unitType);
return;
}
/** 默认跳转到upload */
const formatType = dataSetDetail?.format_type;
const docID = documentList?.[0]?.document_id;
const params: Record<string, string> = {
type: unitType,
...query,
};
if (formatType === FormatType.Table && docID) {
params.opt = OptType.INCREMENTAL;
params.doc_id = docID;
}
resourceNavigate.upload?.(params);
}}
triggerComponent={
<Button
data-testid={KnowledgeE2e.SegmentDetailAddBtn}
iconPosition="right"
icon={
visible ? (
<IconCozArrowUp className={'text-[12px]'} />
) : (
<IconCozArrowDown className={'text-[12px]'} />
)
}
>
{I18n.t('knowledg_unit_add_segments')}
</Button>
}
/>
);
};
export {
getTableFormatTooltip,
getDefaultFormatTooltip,
createBtnDisableToolTip,
};

View File

@@ -0,0 +1,85 @@
/*
* 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 {
KNOWLEDGE_MAX_DOC_SIZE,
KNOWLEDGE_MAX_SLICE_COUNT,
} from '@coze-data/knowledge-modal-base';
import { I18n } from '@coze-arch/i18n';
import { type Dataset, type DocumentInfo } from '@coze-arch/bot-api/knowledge';
import { DocumentStatus, FormatType } from '@coze-arch/bot-api/knowledge';
/**
* 处理表格类型数据集的禁用提示
*/
export const getTableFormatTooltip = (documentList: DocumentInfo[]): string => {
const docInfo = documentList?.[0];
if (!docInfo) {
return '';
}
if (docInfo.status === DocumentStatus.Processing) {
return I18n.t('knowledge_add_content_processing_tips');
}
// @ts-expect-error -- linter-disable-autofix
if (docInfo?.slice_count >= KNOWLEDGE_MAX_SLICE_COUNT) {
return I18n.t('kl2_002');
}
return '';
};
/**
* 处理默认类型数据集的禁用提示
*/
export const getDefaultFormatTooltip = (dataSetDetail: Dataset): string => {
// @ts-expect-error -- linter-disable-autofix
if (dataSetDetail?.doc_count >= KNOWLEDGE_MAX_DOC_SIZE) {
return I18n.t('kl2_003');
}
if (dataSetDetail?.processing_file_id_list?.length) {
return I18n.t('knowledge_add_content_processing_tips');
}
return '';
};
/**
* 创建按钮禁用时的提示文本
* @param dataSetDetail - 数据集详情
* @param documentList - 文档列表
* @returns 提示文本
*/
export const createBtnDisableToolTip = (
dataSetDetail: Dataset,
documentList: DocumentInfo[],
): string => {
const formatType = dataSetDetail?.format_type;
const tooltipHandlers: Record<string, () => string> = {
[FormatType.Table]: () => getTableFormatTooltip(documentList),
default: () => getDefaultFormatTooltip(dataSetDetail),
};
if (!formatType) {
return tooltipHandlers.default();
}
const handler = tooltipHandlers[formatType] || tooltipHandlers.default;
return handler();
};

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 {
getTableFormatTooltip,
getDefaultFormatTooltip,
createBtnDisableToolTip,
} from './disabled-tooltip';

View File

@@ -0,0 +1,64 @@
/*
* 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 { useShallow } from 'zustand/react/shallow';
import { useKnowledgeParams, useKnowledgeStore } from '@coze-data/knowledge-stores';
import { type UnitType } from '@coze-data/knowledge-resource-processor-core';
import { useKnowledgeNavigate } from '@coze-data/knowledge-common-hooks/use-case';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { type FormatType } from '@coze-arch/bot-api/knowledge';
import { getAddContentUrl } from '@/utils';
import { ActionType } from '@/types';
import { type ImportKnowledgeSourceButtonProps } from '../module';
import { ImportKnowledgeSourceButton } from '../base';
export type BizAgentIdeImportKnowledgeSourceButtonProps =
ImportKnowledgeSourceButtonProps;
export const BizAgentIdeImportKnowledgeSourceButton = ({
disabledTooltip,
}: BizAgentIdeImportKnowledgeSourceButtonProps) => {
const navigate = useKnowledgeNavigate();
const { documentList, dataSetDetail } = useKnowledgeStore(
useShallow(state => ({
documentList: state.documentList,
dataSetDetail: state.dataSetDetail,
})),
);
const params = useKnowledgeParams();
const spaceId = useSpaceStore(item => item.space.id);
return (
<ImportKnowledgeSourceButton
disabledTooltip={disabledTooltip}
onSourceChange={unitType => {
navigate(
getAddContentUrl({
spaceID: spaceId as string,
datasetID: dataSetDetail?.dataset_id as string,
docID: documentList?.[0]?.document_id,
formatType: dataSetDetail?.format_type as FormatType,
type: unitType as UnitType,
pageMode: 'modal',
botId: params.botID,
actionType: ActionType.ADD,
}),
);
}}
/>
);
};

View File

@@ -0,0 +1,20 @@
/*
* 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 interface ImportKnowledgeSourceButtonProps {
disabledTooltip?: string;
onSourceChange?: (source: string) => void;
}

View File

@@ -0,0 +1,49 @@
/*
* 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 { ReactNode } from 'react';
import { type UnitType } from '@coze-data/knowledge-resource-processor-core';
import { useKnowledgeIDERegistry } from '../../context/knowledge-ide-registry-context';
import { KnowledgeSourceMenu as KnowledgeSourceMenuComponent } from '../../components/knowledge-source-menu';
export interface ImportKnowledgeSourceMenuProps {
triggerComponent?: ReactNode;
onVisibleChange?: (visible: boolean) => void;
onChange?: (val: UnitType) => void;
}
export const ImportKnowledgeSourceMenu = (
props: ImportKnowledgeSourceMenuProps,
) => {
const { triggerComponent, onVisibleChange, onChange } = props;
const { importKnowledgeMenuSourceFeatureRegistry } =
useKnowledgeIDERegistry();
return (
<KnowledgeSourceMenuComponent
triggerComponent={triggerComponent}
onVisibleChange={onVisibleChange}
>
{importKnowledgeMenuSourceFeatureRegistry
?.entries()
.map(([key, { Component }]) => (
<Component key={key} onClick={value => onChange?.(value)} />
))}
</KnowledgeSourceMenuComponent>
);
};

View File

@@ -0,0 +1,36 @@
/*
* 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 { ImageLocalModule } from '@/features/import-knowledge-sources/radio/image-local';
import {
createImportKnowledgeSourceRadioFeatureRegistry,
type ImportKnowledgeRadioSourceFeatureRegistry,
} from '@/features/import-knowledge-sources/radio';
export const importImageKnowledgeSourceRadioGroupContributes: ImportKnowledgeRadioSourceFeatureRegistry =
(() => {
const importKnowledgeRadioSourceFeatureRegistry =
createImportKnowledgeSourceRadioFeatureRegistry(
'import-knowledge-source-image-radio-group',
);
importKnowledgeRadioSourceFeatureRegistry.registerSome([
{
type: 'image-local',
module: ImageLocalModule,
},
]);
return importKnowledgeRadioSourceFeatureRegistry;
})();

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 { TableLocalModule } from '@coze-data/knowledge-ide-base/features/import-knowledge-sources/radio/table-local';
import {
createImportKnowledgeSourceRadioFeatureRegistry,
type ImportKnowledgeRadioSourceFeatureRegistry,
} from '@coze-data/knowledge-ide-base/features/import-knowledge-sources/radio';
import { TableCustomModule } from '@coze-data/knowledge-ide-base/features/import-knowledge-sources/radio';
export const importTableKnowledgeSourceRadioGroupContributes: ImportKnowledgeRadioSourceFeatureRegistry =
(() => {
const importKnowledgeRadioSourceFeatureRegistry =
createImportKnowledgeSourceRadioFeatureRegistry(
'import-knowledge-source-table-radio-group',
);
importKnowledgeRadioSourceFeatureRegistry.registerSome([
{
type: 'table-local',
module: TableLocalModule,
},
{
type: 'table-custom',
module: TableCustomModule,
},
]);
return importKnowledgeRadioSourceFeatureRegistry;
})();

View File

@@ -0,0 +1,43 @@
/*
* 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 {
createImportKnowledgeSourceRadioFeatureRegistry,
type ImportKnowledgeRadioSourceFeatureRegistry,
} from '@coze-data/knowledge-ide-base/features/import-knowledge-sources/radio';
import {
TextCustomModule,
TextLocalModule,
} from '@coze-data/knowledge-ide-base/features/import-knowledge-sources/radio';
export const importTextKnowledgeSourceRadioGroupContributes: ImportKnowledgeRadioSourceFeatureRegistry =
(() => {
const importKnowledgeRadioSourceFeatureRegistry =
createImportKnowledgeSourceRadioFeatureRegistry(
'import-knowledge-source-text-radio-group',
);
importKnowledgeRadioSourceFeatureRegistry.registerSome([
{
type: 'text-local',
module: TextLocalModule,
},
{
type: 'text-custom',
module: TextCustomModule,
},
]);
return importKnowledgeRadioSourceFeatureRegistry;
})();

View File

@@ -0,0 +1,66 @@
/*
* 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 { useMemo } from 'react';
import { type UnitType } from '@coze-data/knowledge-resource-processor-core';
import { FormatType } from '@coze-arch/bot-api/knowledge';
import { type ImportKnowledgeRadioSourceFeatureRegistry } from '../import-knowledge-sources/radio/registry';
import { KnowledgeSourceRadioGroup as KnowledgeSourceRadioGroupComponent } from '../../components/knowledge-source-radio-group';
import { importTextKnowledgeSourceRadioGroupContributes } from './import-text-knowledge-source-contributes';
import { importTableKnowledgeSourceRadioGroupContributes } from './import-table-knowledge-source-contributes';
import { importImageKnowledgeSourceRadioGroupContributes } from './import-image-knowledge-source-contributes';
export interface ImportKnowledgeSourceRadioGroupProps {
formatType: FormatType;
value?: UnitType;
importKnowledgeSourceRegistry: ImportKnowledgeRadioSourceFeatureRegistry;
onChange?: (val: UnitType) => void;
}
export const ImportKnowledgeSourceRadioGroup = (
props: ImportKnowledgeSourceRadioGroupProps,
) => {
const { value, onChange, formatType } = props;
const importKnowledgeSourceRegistry = useMemo(() => {
if (formatType === FormatType.Text) {
return importTextKnowledgeSourceRadioGroupContributes;
}
if (formatType === FormatType.Table) {
return importTableKnowledgeSourceRadioGroupContributes;
}
if (formatType === FormatType.Image) {
return importImageKnowledgeSourceRadioGroupContributes;
}
}, [formatType]);
if (!importKnowledgeSourceRegistry) {
return null;
}
return (
<KnowledgeSourceRadioGroupComponent
value={value}
onChange={e => {
onChange?.(e.target.value);
}}
>
{importKnowledgeSourceRegistry.entries().map(([key, { Component }]) => (
<Component key={key} />
))}
</KnowledgeSourceRadioGroupComponent>
);
};

View File

@@ -0,0 +1,44 @@
/*
* 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 { UnitType } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozDocument } from '@coze-arch/coze-design/icons';
import { KnowledgeSourceMenuItem } from '@/components/knowledge-source-menu-item';
import {
type ImportKnowledgeMenuSourceModule,
type ImportKnowledgeMenuSourceModuleProps,
} from '../module';
export const ImageLocal = (props: ImportKnowledgeMenuSourceModuleProps) => {
const { onClick } = props;
return (
<KnowledgeSourceMenuItem
title={I18n.t('knowledge_photo_002')}
icon={<IconCozDocument className="w-4 h-4" />}
testId={`${KnowledgeE2e.SegmentDetailDropdownItem}.${UnitType.IMAGE_FILE}`}
value={UnitType.IMAGE_FILE}
onClick={() => onClick(UnitType.IMAGE_FILE)}
/>
);
};
export const ImageLocalModule: ImportKnowledgeMenuSourceModule = {
Component: ImageLocal,
};

View File

@@ -0,0 +1,30 @@
/*
* 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 { TableCustomModule } from './table-custom';
export { TableLocalModule } from './table-local';
export { TextCustomModule } from './text-custom';
export { TextLocalModule } from './text-local';
export { ImageLocalModule } from './image-local';
export type {
ImportKnowledgeMenuSourceModuleProps,
ImportKnowledgeMenuSourceModule,
} from './module';
export {
createImportKnowledgeMenuSourceFeatureRegistry,
type ImportKnowledgeMenuSourceFeatureType,
type ImportKnowledgeMenuSourceRegistry,
} from './registry';

View File

@@ -0,0 +1,25 @@
/*
* 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 { UnitType } from '@coze-data/knowledge-resource-processor-core';
export interface ImportKnowledgeMenuSourceModuleProps {
onClick: (item: UnitType) => void;
}
export interface ImportKnowledgeMenuSourceModule {
Component: React.ComponentType<ImportKnowledgeMenuSourceModuleProps>;
}

View File

@@ -0,0 +1,42 @@
/*
* 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 { FeatureRegistry } from '@coze-data/feature-register';
import { type ImportKnowledgeMenuSourceModule } from './module';
export type ImportKnowledgeMenuSourceFeatureType =
| 'text-local'
| 'text-custom'
| 'table-custom'
| 'table-local'
| 'image-local'
| 'image-custom';
export type ImportKnowledgeMenuSourceRegistry = FeatureRegistry<
ImportKnowledgeMenuSourceFeatureType,
ImportKnowledgeMenuSourceModule
>;
export const createImportKnowledgeMenuSourceFeatureRegistry = (
name: string,
): ImportKnowledgeMenuSourceRegistry =>
new FeatureRegistry<
ImportKnowledgeMenuSourceFeatureType,
ImportKnowledgeMenuSourceModule
>({
name,
});

View File

@@ -0,0 +1,44 @@
/*
* 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 { UnitType } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozPencilPaper } from '@coze-arch/coze-design/icons';
import { KnowledgeSourceMenuItem } from '@/components/knowledge-source-menu-item';
import {
type ImportKnowledgeMenuSourceModule,
type ImportKnowledgeMenuSourceModuleProps,
} from '../module';
export const TableCustom = (props: ImportKnowledgeMenuSourceModuleProps) => {
const { onClick } = props;
return (
<KnowledgeSourceMenuItem
title={I18n.t('datasets_createFileModel_step1_TabCustomTitle')}
icon={<IconCozPencilPaper className="w-4 h-4" />}
testId={`${KnowledgeE2e.SegmentDetailDropdownItem}.${UnitType.TABLE_CUSTOM}`}
value={UnitType.TABLE_CUSTOM}
onClick={() => onClick(UnitType.TABLE_CUSTOM)}
/>
);
};
export const TableCustomModule: ImportKnowledgeMenuSourceModule = {
Component: TableCustom,
};

View File

@@ -0,0 +1,44 @@
/*
* 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 { UnitType } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozDocument } from '@coze-arch/coze-design/icons';
import { KnowledgeSourceMenuItem } from '@/components/knowledge-source-menu-item';
import {
type ImportKnowledgeMenuSourceModule,
type ImportKnowledgeMenuSourceModuleProps,
} from '../module';
export const TableLocal = (props: ImportKnowledgeMenuSourceModuleProps) => {
const { onClick } = props;
return (
<KnowledgeSourceMenuItem
title={I18n.t('datasets_createFileModel_step1_TabLocalTitle')}
icon={<IconCozDocument className="w-4 h-4" />}
testId={`${KnowledgeE2e.SegmentDetailDropdownItem}.${UnitType.TABLE_DOC}`}
value={UnitType.TABLE_DOC}
onClick={() => onClick(UnitType.TABLE_DOC)}
/>
);
};
export const TableLocalModule: ImportKnowledgeMenuSourceModule = {
Component: TableLocal,
};

View File

@@ -0,0 +1,44 @@
/*
* 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 { UnitType } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozPencilPaper } from '@coze-arch/coze-design/icons';
import { KnowledgeSourceMenuItem } from '@/components/knowledge-source-menu-item';
import {
type ImportKnowledgeMenuSourceModule,
type ImportKnowledgeMenuSourceModuleProps,
} from '../module';
export const TextCustom = (props: ImportKnowledgeMenuSourceModuleProps) => {
const { onClick } = props;
return (
<KnowledgeSourceMenuItem
title={I18n.t('datasets_createFileModel_step1_CustomTitle')}
icon={<IconCozPencilPaper className="w-4 h-4" />}
testId={`${KnowledgeE2e.SegmentDetailDropdownItem}.${UnitType.TEXT_CUSTOM}`}
value={UnitType.TEXT_CUSTOM}
onClick={() => onClick(UnitType.TEXT_CUSTOM)}
/>
);
};
export const TextCustomModule: ImportKnowledgeMenuSourceModule = {
Component: TextCustom,
};

View File

@@ -0,0 +1,44 @@
/*
* 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 { UnitType } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozDocument } from '@coze-arch/coze-design/icons';
import { KnowledgeSourceMenuItem } from '@/components/knowledge-source-menu-item';
import {
type ImportKnowledgeMenuSourceModule,
type ImportKnowledgeMenuSourceModuleProps,
} from '../module';
export const TextLocal = (props: ImportKnowledgeMenuSourceModuleProps) => {
const { onClick } = props;
return (
<KnowledgeSourceMenuItem
title={I18n.t('datasets_createFileModel_step1_LocalTitle')}
icon={<IconCozDocument className="w-4 h-4" />}
testId={`${KnowledgeE2e.SegmentDetailDropdownItem}.${UnitType.TEXT_DOC}`}
value={UnitType.TEXT_DOC}
onClick={() => onClick(UnitType.TEXT_DOC)}
/>
);
};
export const TextLocalModule: ImportKnowledgeMenuSourceModule = {
Component: TextLocal,
};

View File

@@ -0,0 +1,39 @@
/*
* 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 { UnitType } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozDocument } from '@coze-arch/coze-design/icons';
import { KnowledgeSourceRadio } from '@/components/knowledge-source-radio';
import { type ImportKnowledgeRadioSourceModule } from '../module';
export const ImageLocal: ImportKnowledgeRadioSourceModule['Component'] = () => (
<KnowledgeSourceRadio
title={I18n.t('knowledge_photo_002')}
description={I18n.t('knowledge_photo_003')}
icon={<IconCozDocument className="w-4 h-4" />}
e2e={KnowledgeE2e.CreateKnowledgeModalPhotoImgRadio}
key={UnitType.IMAGE_FILE}
value={UnitType.IMAGE_FILE}
/>
);
export const ImageLocalModule: ImportKnowledgeRadioSourceModule = {
Component: ImageLocal,
};

View File

@@ -0,0 +1,27 @@
/*
* 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 { TableCustomModule } from './table-custom';
export { TableLocalModule } from './table-local';
export { TextCustomModule } from './text-custom';
export { TextLocalModule } from './text-local';
export { ImageLocalModule } from './image-local';
export type { ImportKnowledgeRadioSourceModule } from './module';
export {
createImportKnowledgeSourceRadioFeatureRegistry,
type ImportKnowledgeRadioSourceFeatureType,
ImportKnowledgeRadioSourceFeatureRegistry,
} from './registry';

View File

@@ -0,0 +1,25 @@
/*
* 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 { ComponentProps, ReactElement } from 'react';
import type { KnowledgeSourceRadio } from '@/components/knowledge-source-radio';
export interface ImportKnowledgeRadioSourceModule {
Component: () => ReactElement<
ComponentProps<typeof KnowledgeSourceRadio>
> | null;
}

View File

@@ -0,0 +1,47 @@
/*
* 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 { FeatureRegistry } from '@coze-data/feature-register';
import { TextLocalModule } from './text-local';
import { type ImportKnowledgeRadioSourceModule } from './module';
export type ImportKnowledgeRadioSourceFeatureType =
| 'text-local'
| 'text-custom'
| 'table-custom'
| 'table-local'
| 'image-local'
| 'image-custom';
export type ImportKnowledgeRadioSourceFeatureRegistry = FeatureRegistry<
ImportKnowledgeRadioSourceFeatureType,
ImportKnowledgeRadioSourceModule
>;
export const createImportKnowledgeSourceRadioFeatureRegistry = (
name: string,
): ImportKnowledgeRadioSourceFeatureRegistry =>
new FeatureRegistry<
ImportKnowledgeRadioSourceFeatureType,
ImportKnowledgeRadioSourceModule
>({
name,
defaultFeature: {
type: 'text-local',
module: TextLocalModule,
},
});

View File

@@ -0,0 +1,42 @@
/*
* 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 { UnitType } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozPencilPaper } from '@coze-arch/coze-design/icons';
import { KnowledgeSourceRadio } from '@/components/knowledge-source-radio';
import { type ImportKnowledgeRadioSourceModule } from '../module';
export const TableCustom: ImportKnowledgeRadioSourceModule['Component'] =
() => (
<KnowledgeSourceRadio
title={I18n.t('datasets_createFileModel_step1_TabCustomTitle')}
description={I18n.t(
'datasets_createFileModel_step1_TabCustomDescription',
)}
icon={<IconCozPencilPaper className="w-4 h-4" />}
e2e={KnowledgeE2e.CreateKnowledgeModalTableCustomRadio}
key={UnitType.TABLE_CUSTOM}
value={UnitType.TABLE_CUSTOM}
/>
);
export const TableCustomModule: ImportKnowledgeRadioSourceModule = {
Component: TableCustom,
};

View File

@@ -0,0 +1,38 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import { IconCozDocument } from '@coze-arch/coze-design/icons';
import { UnitType } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { KnowledgeSourceRadio } from '@/components/knowledge-source-radio';
import { type ImportKnowledgeRadioSourceModule } from '../module';
export const TableLocal: ImportKnowledgeRadioSourceModule['Component'] = () => (
<KnowledgeSourceRadio
title={I18n.t('datasets_createFileModel_step1_TabLocalTitle')}
description={I18n.t('datasets_createFileModel_step1_TabLocalDescription')}
icon={<IconCozDocument className="w-4 h-4" />}
e2e={KnowledgeE2e.CreateKnowledgeModalTableLocalRadio}
key={UnitType.TABLE_DOC}
value={UnitType.TABLE_DOC}
/>
);
export const TableLocalModule: ImportKnowledgeRadioSourceModule = {
Component: TableLocal,
};

View File

@@ -0,0 +1,38 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import { IconCozPencilPaper } from '@coze-arch/coze-design/icons';
import { UnitType } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { type ImportKnowledgeRadioSourceModule } from '../module';
import { KnowledgeSourceRadio } from '@/components/knowledge-source-radio';
export const TextCustom: ImportKnowledgeRadioSourceModule['Component'] = () => (
<KnowledgeSourceRadio
title={I18n.t('datasets_createFileModel_step1_CustomTitle')}
description={I18n.t('datasets_createFileModel_step1_CustomDescription')}
icon={<IconCozPencilPaper className="w-4 h-4" />}
e2e={KnowledgeE2e.CreateKnowledgeModalTextCustomRadio}
key={UnitType.TEXT_CUSTOM}
value={UnitType.TEXT_CUSTOM}
/>
);
export const TextCustomModule: ImportKnowledgeRadioSourceModule = {
Component: TextCustom,
};

View File

@@ -0,0 +1,38 @@
/*
* 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 { UnitType } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozDocument } from '@coze-arch/coze-design/icons';
import { KnowledgeSourceRadio } from '@/components/knowledge-source-radio';
import { type ImportKnowledgeRadioSourceModule } from '../module';
export const TextLocal: ImportKnowledgeRadioSourceModule['Component'] = () => (
<KnowledgeSourceRadio
title={I18n.t('datasets_createFileModel_step1_LocalTitle')}
description={I18n.t('datasets_createFileModel_step1_LocalDescription')}
icon={<IconCozDocument className="w-4 h-4" />}
e2e={KnowledgeE2e.CreateKnowledgeModalTextLocalRadio}
key={UnitType.TEXT_DOC}
value={UnitType.TEXT_DOC}
/>
);
export const TextLocalModule: ImportKnowledgeRadioSourceModule = {
Component: TextLocal,
};

View File

@@ -0,0 +1,57 @@
/*
* 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 { useDataNavigate } from '@coze-data/knowledge-stores';
import { OptType } from '@coze-data/knowledge-resource-processor-core';
import { I18n } from '@coze-arch/i18n';
import { UpdateType } from '@coze-arch/bot-api/knowledge';
import { Menu } from '@coze-arch/coze-design';
import {
type TableConfigMenuModule,
type TableConfigMenuModuleProps,
} from '../module';
export const ConfigurationTableStructure = (
props: TableConfigMenuModuleProps,
) => {
const { documentInfo } = props;
const resourceNavigate = useDataNavigate();
if (
documentInfo.update_type !== undefined &&
documentInfo.update_type !== UpdateType.NoUpdate
) {
return null;
}
const handleClick = () => {
resourceNavigate.upload?.({
type: 'table',
opt: OptType.RESEGMENT,
doc_id: documentInfo?.document_id ?? '',
});
};
return (
<Menu.Item onClick={handleClick}>
{I18n.t('knowledge_segment_config_table')}
</Menu.Item>
);
};
export const ConfigurationTableStructureModule: TableConfigMenuModule = {
Component: ConfigurationTableStructure,
};

View File

@@ -0,0 +1,26 @@
/*
* 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 {
createTableConfigMenuRegistry,
type TableConfigMenuRegistry,
type TableConfigMenuFeatureType,
} from './registry';
export {
type TableConfigMenuModule,
type TableConfigMenuModuleProps,
} from './module';
export { ConfigurationTableStructureModule } from './configuration-table-structure';

View File

@@ -0,0 +1,26 @@
/*
* 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 { DocumentInfo } from '@coze-arch/bot-api/knowledge';
export interface TableConfigMenuModuleProps {
documentInfo: DocumentInfo;
reload?: () => void;
onChangeDocList?: (docList: DocumentInfo[]) => void;
}
export interface TableConfigMenuModule {
Component: React.ComponentType<TableConfigMenuModuleProps>;
}

View File

@@ -0,0 +1,37 @@
/*
* 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 { FeatureRegistry } from '@coze-data/feature-register';
import { type TableConfigMenuModule } from './module';
export type TableConfigMenuFeatureType =
| 'configuration-table-structure'
| 'update-frequency'
| 'fetch-slice'
| 'view-source';
export type TableConfigMenuRegistry = FeatureRegistry<
TableConfigMenuFeatureType,
TableConfigMenuModule
>;
export const createTableConfigMenuRegistry = (
name: string,
): TableConfigMenuRegistry =>
new FeatureRegistry<TableConfigMenuFeatureType, TableConfigMenuModule>({
name,
});

View File

@@ -0,0 +1,57 @@
/*
* 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 { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { type DocumentInfo } from '@coze-arch/bot-api/knowledge';
import { useReloadKnowledgeIDE } from '@/hooks/use-case/use-reload-knowledge-ide';
import { type TableConfigMenuRegistry } from '../../knowledge-ide-table-config-menus';
import { KnowledgeConfigMenu as KnowledgeConfigMenuComponent } from '../../../components/knowledge-config-menu';
export interface TableConfigButtonProps {
knowledgeTableConfigMenuContributes?: TableConfigMenuRegistry;
onChangeDocList?: (docList: DocumentInfo[]) => void;
}
export const TableConfigButton = (props: TableConfigButtonProps) => {
const { knowledgeTableConfigMenuContributes, onChangeDocList } = props;
const documentList = useKnowledgeStore(state => state.documentList);
const documentInfo = documentList?.[0];
const canEdit = useKnowledgeStore(state => state.canEdit);
const { reload } = useReloadKnowledgeIDE();
if (!knowledgeTableConfigMenuContributes) {
return null;
}
return (
<KnowledgeConfigMenuComponent>
{canEdit
? knowledgeTableConfigMenuContributes
?.entries()
.map(([key, { Component }]) => (
<Component
key={key}
documentInfo={documentInfo}
onChangeDocList={onChangeDocList}
reload={reload}
/>
))
: null}
</KnowledgeConfigMenuComponent>
);
};

View File

@@ -0,0 +1,46 @@
/*
* 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 { useMemo } from 'react';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { UnitType } from '@coze-data/knowledge-resource-processor-core';
import { FormatType } from '@coze-arch/bot-api/knowledge';
import { getUnitType } from '@/utils';
import { TableLocalTableConfigButton } from './table-local';
import { TableCustomTableConfigButton } from './table-custom';
import { type TableConfigButtonProps } from './base';
export const KnowledgeIDETableConfig = (props: TableConfigButtonProps) => {
const documentInfo = useKnowledgeStore(state => state.documentList?.[0]);
const unitType = useMemo(() => {
if (documentInfo) {
return getUnitType({
format_type: FormatType.Table,
source_type: documentInfo?.source_type,
});
}
return UnitType.TABLE_API;
}, [documentInfo]);
if (unitType === UnitType.TABLE_CUSTOM) {
return <TableCustomTableConfigButton {...props} />;
}
if (unitType === UnitType.TABLE_DOC) {
return <TableLocalTableConfigButton {...props} />;
}
return null;
};

View File

@@ -0,0 +1,24 @@
/*
* 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 { TableConfigButton, type TableConfigButtonProps } from '../base';
import { knowledgeTableConfigMenuContributes } from './knowledge-ide-table-config-menu-contributes';
export const TableCustomTableConfigButton = (props: TableConfigButtonProps) => (
<TableConfigButton
{...props}
knowledgeTableConfigMenuContributes={knowledgeTableConfigMenuContributes}
/>
);

View File

@@ -0,0 +1,35 @@
/*
* 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 {
ConfigurationTableStructureModule,
createTableConfigMenuRegistry,
type TableConfigMenuRegistry,
} from '@/features/knowledge-ide-table-config-menus';
export const knowledgeTableConfigMenuContributes: TableConfigMenuRegistry =
(() => {
const knowledgeTableConfigMenuRegistry = createTableConfigMenuRegistry(
'knowledge-ide-table-custom-config-menu',
);
knowledgeTableConfigMenuRegistry.registerSome([
{
type: 'configuration-table-structure',
module: ConfigurationTableStructureModule,
},
]);
return knowledgeTableConfigMenuRegistry;
})();

View File

@@ -0,0 +1,26 @@
/*
* 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 { TableConfigButton, type TableConfigButtonProps } from '../base';
import { knowledgeTableLocalConfigMenuContributes } from './knowledge-ide-table-config-menu-contributes';
export const TableLocalTableConfigButton = (props: TableConfigButtonProps) => (
<TableConfigButton
{...props}
knowledgeTableConfigMenuContributes={
knowledgeTableLocalConfigMenuContributes
}
/>
);

View File

@@ -0,0 +1,35 @@
/*
* 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 {
ConfigurationTableStructureModule,
createTableConfigMenuRegistry,
type TableConfigMenuRegistry,
} from '@/features/knowledge-ide-table-config-menus';
export const knowledgeTableLocalConfigMenuContributes: TableConfigMenuRegistry =
(() => {
const knowledgeTableConfigMenuRegistry = createTableConfigMenuRegistry(
'knowledge-ide-table-local-config-menu',
);
knowledgeTableConfigMenuRegistry.registerSome([
{
type: 'configuration-table-structure',
module: ConfigurationTableStructureModule,
},
]);
return knowledgeTableConfigMenuRegistry;
})();

View File

@@ -0,0 +1,275 @@
/*
* 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 { useEffect, useState } from 'react';
import { useKnowledgeParams } from '@coze-data/knowledge-stores';
import { I18n } from '@coze-arch/i18n';
import { SceneType, usePageJumpService } from '@coze-arch/bot-hooks';
import {
type Knowledge,
type GetDraftBotInfoAgwData,
type KnowledgeInfo,
ReferenceUpdateType,
BotMode,
} from '@coze-arch/bot-api/playground_api';
import {
type Dataset,
DatasetStatus,
StorageLocation,
} from '@coze-arch/bot-api/knowledge';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { Toast, Button } from '@coze-arch/coze-design';
import { ActionType } from '@/types';
type BotDataset =
| Knowledge
| {
dataset: KnowledgeInfo[];
};
interface UpdateDatasetForBot {
botId: string;
agentId: string;
dataset: BotDataset;
updatedDatasetList?: KnowledgeInfo[];
spaceId: string;
dataSetDetail?: Dataset;
}
export const useFetchBotInfo = (spaceId, botId) => {
const [botInfo, setBotInfo] = useState<GetDraftBotInfoAgwData>();
const fetchBotInfo = async () => {
try {
const { data } = await PlaygroundApi.GetDraftBotInfoAgw({
bot_id: botId,
});
setBotInfo(data ?? {});
} catch (error) {
console.error(error);
}
};
useEffect(() => {
if (spaceId && botId) {
fetchBotInfo();
}
}, [spaceId, botId]);
return botInfo;
};
const updateDatasetForBot = async ({
botId,
agentId,
dataset,
botInfo,
updatedDatasetList,
spaceId,
}: UpdateDatasetForBot & {
botInfo?: GetDraftBotInfoAgwData;
}) => {
if (botInfo?.bot_info.bot_mode === BotMode.SingleMode) {
await PlaygroundApi.UpdateDraftBotInfoAgw({
bot_info: {
bot_id: botId,
knowledge: {
...dataset,
knowledge_info: updatedDatasetList,
},
},
});
} else {
const currentAgent = botInfo?.bot_info?.agents?.find(
agent => agent.agent_id === agentId,
);
if (currentAgent?.agent_id) {
await PlaygroundApi.UpdateAgentV2({
...currentAgent,
current_version:
currentAgent.update_type === ReferenceUpdateType.AutoUpdate
? '0'
: currentAgent.current_version,
id: currentAgent?.agent_id,
space_id: spaceId,
bot_id: botId,
knowledge: {
...dataset,
knowledge_info: updatedDatasetList,
},
});
}
}
};
export const getUpdatedDataset = (
dataset: BotDataset,
actionType: ActionType,
dataSetDetail: Dataset,
): KnowledgeInfo[] => {
// 更新后的bot知识库
let updatedDatasetList: KnowledgeInfo[] = [];
// 原本的bot知识库内容
let originDataset: KnowledgeInfo[] = [];
// 兼容json版本的datasetFG全量后删除
if ('dataset' in dataset) {
originDataset = dataset?.dataset ?? [];
} else {
originDataset = dataset?.knowledge_info ?? [];
}
if (actionType === ActionType.REMOVE) {
updatedDatasetList = originDataset?.filter(
item => item.id !== dataSetDetail.dataset_id,
);
} else {
updatedDatasetList = [
...originDataset,
{ name: dataSetDetail.name, id: dataSetDetail.dataset_id },
];
}
return updatedDatasetList;
};
// 更新bot知识库逻辑
export const handleDatasetUpdate = async ({
botInfo,
botId,
agentId,
dataSetDetail = {},
dataset,
actionType,
spaceId,
updateSuccess,
}: UpdateDatasetForBot & {
botInfo?: GetDraftBotInfoAgwData;
updateSuccess: () => void;
actionType: ActionType;
}) => {
const updatedDatasetList = getUpdatedDataset(
dataset,
actionType,
dataSetDetail,
);
const updateBotParams = {
spaceId,
botId,
agentId,
updatedDatasetList,
dataset,
};
await updateDatasetForBot({ ...updateBotParams, botInfo });
updateSuccess();
};
// 根据不同botInfo信息 获取不同的bot原有的dataset
export const getDatasetInfo = (
botInfo: GetDraftBotInfoAgwData | undefined,
agentId: string,
): BotDataset => {
if (agentId) {
return (
botInfo?.bot_info?.agents?.find(item => item.agent_id === agentId)
?.knowledge ?? {}
);
}
return botInfo?.bot_info?.knowledge ?? {};
};
export const NavBarActionButton = ({
dataSetDetail,
}: {
dataSetDetail: Dataset;
}) => {
const [loading, setLoading] = useState(false);
const { jump } = usePageJumpService();
const params = useKnowledgeParams();
const { spaceID, botID, agentID, actionType } = params;
const botInfo = useFetchBotInfo(spaceID, botID);
const dataset = getDatasetInfo(botInfo, agentID ?? '');
const updateSuccess = () => {
Toast.success(
I18n.t(
actionType === ActionType.REMOVE
? 'bot_edit_dataset_removed_toast'
: 'bot_edit_dataset_added_toast',
{ dataset_name: dataSetDetail.name },
),
);
jump(SceneType.KNOWLEDGE__BACK__BOT, {
spaceID,
botID,
mode:
dataSetDetail.storage_location === StorageLocation.Douyin
? 'douyin'
: 'bot',
});
};
const handleActionClick = async () => {
setLoading(true);
try {
await handleDatasetUpdate({
botInfo,
botId: botID ?? '',
agentId: agentID ?? '',
dataSetDetail,
dataset,
actionType: actionType ?? ActionType.ADD,
spaceId: spaceID ?? '',
updateSuccess,
});
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
return (
<Button
loading={loading}
disabled={dataSetDetail?.status === DatasetStatus.DatasetForbid}
onClick={handleActionClick}
>
{actionType === ActionType.REMOVE &&
dataSetDetail?.storage_location === StorageLocation.Douyin
? I18n.t('dy_avatar_resource_delete')
: null}
{actionType === ActionType.REMOVE &&
dataSetDetail?.storage_location !== StorageLocation.Douyin
? I18n.t('kl2_014')
: null}
{actionType !== ActionType.REMOVE &&
dataSetDetail?.storage_location === StorageLocation.Douyin
? I18n.t('dy_avatar_resource_add')
: null}
{actionType !== ActionType.REMOVE &&
dataSetDetail?.storage_location !== StorageLocation.Douyin
? I18n.t('kl2_013')
: null}
</Button>
);
};

View File

@@ -0,0 +1,43 @@
/*
* 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 { useShallow } from 'zustand/react/shallow';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { ImportKnowledgeSourceButton } from '@/features/import-knowledge-source-button/base';
import { KnowledgeIDENavBar as KnowledgeIDENavBarComponent } from '@/components/knowledge-nav-bar';
import { type KnowledgeIDENavBarProps } from '../module';
export const BaseKnowledgeIDENavBar = (props: KnowledgeIDENavBarProps) => {
const { progressMap, hideBackButton, importKnowledgeSourceButton } = props;
const { setDataSetDetail } = useKnowledgeStore(
useShallow(state => ({
setDataSetDetail: state.setDataSetDetail,
})),
);
return (
<KnowledgeIDENavBarComponent
{...props}
importKnowledgeSourceButton={
importKnowledgeSourceButton ?? <ImportKnowledgeSourceButton />
}
onChangeDataset={setDataSetDetail}
progressMap={progressMap}
hideBackButton={hideBackButton}
/>
);
};

View File

@@ -0,0 +1,143 @@
/*
* 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 { useMemo, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useKnowledgeParams, useKnowledgeStore } from '@coze-data/knowledge-stores';
import { I18n } from '@coze-arch/i18n';
import { SceneType, usePageJumpService } from '@coze-arch/bot-hooks';
import { StorageLocation } from '@coze-arch/bot-api/knowledge';
import { Modal, Toast } from '@coze-arch/coze-design';
import { ActionType } from '@/types';
import {
useFetchBotInfo,
getDatasetInfo,
handleDatasetUpdate,
} from '@/features/nav-bar-action-button';
export const useBeforeKnowledgeIDEClose = ({
onBack,
}: {
onBack?: () => void;
}) => {
const [loading, setLoading] = useState(false);
const {
spaceID: spaceId,
agentID: agentId,
botID: botId,
actionType,
} = useKnowledgeParams();
const { dataSetDetail } = useKnowledgeStore(
useShallow(state => ({
dataSetDetail: state.dataSetDetail,
})),
);
const { jump } = usePageJumpService();
const botInfo = useFetchBotInfo(spaceId, botId);
const dataset = getDatasetInfo(botInfo, agentId ?? '');
const hasAddDataset = useMemo(() => {
let datasetIds: string[] = [];
if ('dataset' in dataset) {
datasetIds = (dataset?.dataset || []).map(item => item.id ?? '');
}
if ('knowledge_info' in dataset) {
datasetIds = (dataset?.knowledge_info || []).map(item => item.id ?? '');
}
return !datasetIds.includes(dataSetDetail?.dataset_id || '');
}, [dataset, dataSetDetail?.dataset_id]);
const updateSuccessJump = () => {
jump(SceneType.KNOWLEDGE__BACK__BOT, {
spaceID: spaceId,
botID: botId,
mode:
dataSetDetail?.storage_location === StorageLocation.Douyin
? 'douyin'
: 'bot',
});
};
const handleFullModalBack = () => {
if (onBack) {
onBack?.();
} else {
updateSuccessJump();
}
};
const updateSuccess = () => {
Toast.success(
I18n.t(
actionType === ActionType.REMOVE
? 'bot_edit_dataset_removed_toast'
: 'bot_edit_dataset_added_toast',
{ dataset_name: dataSetDetail?.name },
),
);
updateSuccessJump();
};
const handleBotIdeBack = () => {
// Bot IDE检查是否有绑定knowledge如果有绑定知识库正常关闭没有绑定确认提示处理
if (hasAddDataset) {
Modal.confirm({
title: I18n.t('bot_ide_knowledge_confirm_title'),
content:
dataSetDetail?.storage_location === StorageLocation.Douyin
? I18n.t('dy_avatar_resource_add_tip')
: I18n.t('bot_ide_knowledge_confirm_content'),
okText: I18n.t('bot_ide_knowledge_confirm_ok'),
cancelText: I18n.t('bot_ide_knowledge_confirm_cancel'),
confirmLoading: loading,
onOk: async () => {
setLoading(true);
try {
await handleDatasetUpdate({
botInfo,
botId: botId ?? '',
agentId: agentId ?? '',
dataSetDetail,
dataset,
actionType: actionType ?? ActionType.ADD,
spaceId: spaceId ?? '',
updateSuccess,
});
} catch (error) {
console.error(error);
} finally {
setLoading(false);
// 无论成功无论都跳转一次
handleFullModalBack();
}
},
onCancel: () => {
// 取消,正常跳转
handleFullModalBack();
},
});
} else {
// 正常绑定不做弹窗拦截
handleFullModalBack();
}
};
return handleBotIdeBack;
};

View File

@@ -0,0 +1,62 @@
/*
* 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 { useShallow } from 'zustand/react/shallow';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { NavBarActionButton } from '@/features/nav-bar-action-button';
import { BizAgentIdeImportKnowledgeSourceButton } from '@/features/import-knowledge-source-button/biz-agent-ide';
import { KnowledgeModalNavBar as KnowledgeModalNavBarComponent } from '@/components/knowledge-modal-nav-bar';
import { type KnowledgeIDENavBarProps } from '../module';
import { useBeforeKnowledgeIDEClose } from './hooks/use-case/use-before-knowledgeide-close';
export type BizAgentIdeKnowledgeIDENavBarProps = KnowledgeIDENavBarProps;
export const BizAgentIdeKnowledgeIDENavBar = (
props: BizAgentIdeKnowledgeIDENavBarProps,
) => {
const { onBack, importKnowledgeSourceButton } = props;
const { dataSetDetail, documentList } = useKnowledgeStore(
useShallow(state => ({
dataSetDetail: state.dataSetDetail,
documentList: state.documentList,
})),
);
const handleBotIdeBack = useBeforeKnowledgeIDEClose({
onBack,
});
return (
<KnowledgeModalNavBarComponent
title={dataSetDetail?.name as string}
onBack={onBack}
datasetDetail={dataSetDetail}
docInfo={documentList?.[0]}
actionButtons={
<NavBarActionButton
key={dataSetDetail?.dataset_id}
dataSetDetail={dataSetDetail}
/>
}
importKnowledgeSourceButton={
importKnowledgeSourceButton ?? (
<BizAgentIdeImportKnowledgeSourceButton />
)
}
beforeBack={handleBotIdeBack}
/>
);
};

View File

@@ -0,0 +1,24 @@
/*
* 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 KnowledgeIDENavBarProps } from '../module';
import { BaseKnowledgeIDENavBar } from '../base';
export type BizLibraryKnowledgeIDENavBarProps = KnowledgeIDENavBarProps;
export const BizLibraryKnowledgeIDENavBar = (
props: BizLibraryKnowledgeIDENavBarProps,
) => <BaseKnowledgeIDENavBar {...props} />;

View File

@@ -0,0 +1,24 @@
/*
* 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 KnowledgeIDENavBarProps } from '../module';
import { BaseKnowledgeIDENavBar } from '../base';
export type BizProjectKnowledgeIDENavBarProps = KnowledgeIDENavBarProps;
export const BizProjectKnowledgeIDENavBar = (
props: BizProjectKnowledgeIDENavBarProps,
) => <BaseKnowledgeIDENavBar {...props} hideBackButton />;

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 { useShallow } from 'zustand/react/shallow';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { KnowledgeModalNavBar as KnowledgeModalNavBarComponent } from '@/components/knowledge-modal-nav-bar';
import { type KnowledgeIDENavBarProps } from '../module';
export type BizWorkflowKnowledgeIDENavBarProps = KnowledgeIDENavBarProps;
export const BizWorkflowKnowledgeIDENavBar = (
props: BizWorkflowKnowledgeIDENavBarProps,
) => {
const { onBack, actionButtons } = props;
const { dataSetDetail, documentList } = useKnowledgeStore(
useShallow(state => ({
dataSetDetail: state.dataSetDetail,
documentList: state.documentList,
})),
);
return (
<KnowledgeModalNavBarComponent
title={dataSetDetail?.name as string}
onBack={onBack}
datasetDetail={dataSetDetail}
docInfo={documentList?.[0]}
actionButtons={actionButtons}
/>
);
};

View File

@@ -0,0 +1,27 @@
/*
* 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 ProgressMap } from '@/types';
export interface KnowledgeIDENavBarProps {
progressMap: ProgressMap;
hideBackButton?: boolean;
textConfigButton?: React.ReactNode;
tableConfigButton?: React.ReactNode;
importKnowledgeSourceButton?: React.ReactNode;
actionButtons?: React.ReactNode;
onBack?: () => void;
}

View File

@@ -0,0 +1,164 @@
/*
* 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 { useMemo, useRef, useState } from 'react';
import classnames from 'classnames';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { type TableViewMethods } from '@coze-common/table-view';
import { I18n } from '@coze-arch/i18n';
import { EmptyState, Spin, Layout } from '@coze-arch/coze-design';
import { IconSegmentEmpty } from '@coze-arch/bot-icons';
import { type DocumentInfo } from '@coze-arch/bot-api/knowledge';
import styles from '../styles/index.module.less';
import { useGetSliceListData } from '../hooks/inner/use-get-slice-list-data';
import { TableUIContext } from '../context/table-ui-context';
import { TableDataContext } from '../context/table-data-context';
import { TableActionsContext } from '../context/table-actions-context';
import { TableDataView } from './table-data-view';
const MAX_TOTAL = 1000;
export interface TableKnowledgeWorkspaceProps {
onChangeDocList?: (docList: DocumentInfo[]) => void;
reload?: () => void;
isReloading: boolean;
}
export const TableKnowledgeWorkspace = ({
isReloading,
}: TableKnowledgeWorkspaceProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const contentWrapperRef = useRef<HTMLDivElement>(null);
const tableViewRef = useRef<TableViewMethods>(null);
const canEdit = useKnowledgeStore(state => state.canEdit);
const documentList = useKnowledgeStore(state => state.documentList)?.filter(
doc => doc.document_id,
) as { document_id: string }[];
const [curIndex, setCurIndex] = useState(0);
const [curSliceId, setCurSliceId] = useState('');
const [delSliceIds, setDelSliceIds] = useState<string[]>([]);
const {
sliceListData,
mutateSliceListData,
loadMoreSliceList,
isLoadingSliceList,
isLoadingMoreSliceList,
} = useGetSliceListData();
const slices = sliceListData?.list;
const isShowAddBtn = useMemo(
() =>
Boolean(
canEdit &&
!sliceListData?.hasMore &&
sliceListData?.total &&
sliceListData?.total < MAX_TOTAL,
),
[canEdit, sliceListData],
);
const hasShowEmptyContent = useMemo(() => {
if (!documentList?.length && !isReloading) {
return true;
}
return (
sliceListData?.ready &&
!slices?.length &&
!(isLoadingMoreSliceList || isLoadingSliceList)
);
}, [
sliceListData,
documentList,
isReloading,
isLoadingMoreSliceList,
isLoadingSliceList,
slices,
]);
// 创建 UI Context 值
const uiContextValue = {
tableViewRef,
isLoadingMoreSliceList,
isLoadingSliceList,
isShowAddBtn,
};
// 创建数据 Context 值
const dataContextValue = {
sliceListData: sliceListData || { list: [], total: 0 },
curIndex,
curSliceId,
delSliceIds,
};
// 创建操作 Context 值
const actionsContextValue = {
setCurIndex,
setCurSliceId,
setDelSliceIds,
loadMoreSliceList,
mutateSliceListData,
};
return (
<TableUIContext.Provider value={uiContextValue}>
<TableDataContext.Provider value={dataContextValue}>
<TableActionsContext.Provider value={actionsContextValue}>
<Layout.Content
ref={containerRef}
className={classnames(
styles['slice-list-ui-content'],
'knowledge-ide-base-slice-list-ui-content',
)}
>
<Spin
spinning={isLoadingSliceList}
wrapperClassName={styles.spin}
size="large"
style={{ width: '100%', height: '100%' }}
>
{slices?.length ? (
<div
ref={contentWrapperRef}
className={styles['slice-list-table']}
>
<TableDataView />
</div>
) : null}
{hasShowEmptyContent && !isLoadingSliceList ? (
<div className={styles['empty-content']}>
<EmptyState
size="large"
icon={
<IconSegmentEmpty
style={{ width: 150, height: '100%' }}
/>
}
title={I18n.t('dataset_segment_empty_desc')}
/>
</div>
) : null}
</Spin>
</Layout.Content>
</TableActionsContext.Provider>
</TableDataContext.Provider>
</TableUIContext.Provider>
);
};

View File

@@ -0,0 +1,183 @@
/*
* 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 {
useKnowledgeParamsStore,
useKnowledgeStore,
} from '@coze-data/knowledge-stores';
import { KnowledgeE2e } from '@coze-data/e2e';
import { TableView } from '@coze-common/table-view';
import { I18n } from '@coze-arch/i18n';
import { IconCozPlus } from '@coze-arch/coze-design/icons';
import { Button } from '@coze-arch/coze-design';
import { DocumentStatus } from '@coze-arch/bot-api/knowledge';
import styles from '../styles/index.module.less';
import { getTableRenderColumnsData } from '../service/use-case/get-table-render-columns-data';
import { useTableSliceOperations } from '../hooks/use-case/use-table-slice-operations';
import { useTableSegmentModal } from '../hooks/use-case/use-table-segment-modal';
import { useDeleteSliceModal } from '../hooks/use-case/use-delete-slice-modal';
import { useAddRow } from '../hooks/use-case/use-add-row';
import { useTableHeight } from '../hooks/inner/use-table-height';
import { useScroll } from '../hooks/inner/use-scroll';
import { useTableUI } from '../context/table-ui-context';
import { useTableData } from '../context/table-data-context';
import { useTableActions } from '../context/table-actions-context';
// 表格内容组件
const TableContent = () => {
const knowledgeIDEBiz = useKnowledgeParamsStore(state => state.params.biz);
const documentList = useKnowledgeStore(state => state.documentList);
const curDoc = documentList?.[0];
const { tableViewRef, isLoadingMoreSliceList, isLoadingSliceList } =
useTableUI();
const { sliceListData } = useTableData();
const slices = sliceListData?.list;
const { loadMoreSliceList } = useTableActions();
const canEdit = Boolean(useKnowledgeStore(state => state.canEdit));
// 删除切片弹窗
const { deleteSliceModalNode, openDeleteSliceModal } = useDeleteSliceModal();
// 编辑slice弹窗
const { tableSegmentModalNode, openTableSegmentModal } =
useTableSegmentModal();
// 获取表格操作方法
const { deleteSlice, rowUpdateSliceData, modalEditSlice } =
useTableSliceOperations({
openDeleteSliceModal,
openTableSegmentModal,
});
const { tableH } = useTableHeight();
// 如果没有数据,直接返回空
if (!slices?.length) {
return null;
}
const tableKey = curDoc?.document_id;
const { data: dataSource, columns } = getTableRenderColumnsData({
sliceList: slices,
metaData: curDoc?.table_meta,
onEdit: modalEditSlice,
onDelete: deleteSlice,
onUpdate: rowUpdateSliceData,
canEdit,
tableKey: tableKey || '',
});
return (
<div
className={classnames(
styles['table-view-container-box'],
'table-view-box',
)}
style={{ height: tableH }}
>
<TableView
tableKey={tableKey}
ref={tableViewRef}
className={classnames(
`${styles['unit-table-view']} ${
isLoadingMoreSliceList ? styles['table-view-loading'] : ''
}`,
knowledgeIDEBiz === 'project'
? styles['table-preview-max']
: styles['table-preview-secondary'],
)}
resizable
dataSource={dataSource}
loading={isLoadingSliceList}
columns={columns}
rowSelect={canEdit}
isVirtualized
rowOperation={canEdit}
scrollToBottom={() => {
if (!isLoadingSliceList && !isLoadingMoreSliceList) {
loadMoreSliceList();
}
}}
editProps={{
onDelete: indexs => deleteSlice(indexs as number[]),
onEdit: (record, index) => {
modalEditSlice(record, index as number);
},
}}
/>
{deleteSliceModalNode}
{tableSegmentModalNode}
</div>
);
};
// 添加行按钮组件
const AddRowButton = () => {
const { isShowAddBtn } = useTableUI();
const documentList = useKnowledgeStore(state => state.documentList);
const curDoc = documentList?.[0];
const { increaseTableHeight } = useTableHeight();
const { scrollTableBodyToBottom } = useScroll();
const { handleAddRow } = useAddRow({
increaseTableHeight,
scrollTableBodyToBottom,
});
if (!isShowAddBtn) {
return null;
}
return (
<div className={styles['add-row-btn']}>
<Button
disabled={curDoc?.status === DocumentStatus.Processing}
data-testid={KnowledgeE2e.SegmentDetailContentAddRowBtn}
color="primary"
size="default"
icon={<IconCozPlus />}
onClick={handleAddRow}
>
{I18n.t('knowledge_optimize_0010')}
</Button>
</div>
);
};
// 主组件
export const TableDataView = () => {
const { sliceListData } = useTableData();
const slices = sliceListData?.list;
// 如果没有数据,只显示添加按钮
if (!slices?.length) {
return <AddRowButton />;
}
return (
<>
<TableContent />
<AddRowButton />
</>
);
};

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 { createContext, useContext } from 'react';
import { type DatasetDataScrollList } from '@/service';
// 表格操作相关的 Context
interface TableActionsContextType {
setCurIndex: (index: number | ((prev: number) => number)) => void;
setCurSliceId: (id: string | ((prev: string) => string)) => void;
setDelSliceIds: (ids: string[] | ((prev: string[]) => string[])) => void;
loadMoreSliceList: () => void;
mutateSliceListData: (data: DatasetDataScrollList) => void;
}
export const TableActionsContext =
createContext<TableActionsContextType | null>(null);
export const useTableActions = () => {
const context = useContext(TableActionsContext);
if (!context) {
throw new Error(
'useTableActions must be used within a TableActionsProvider',
);
}
return context;
};

View File

@@ -0,0 +1,39 @@
/*
* 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 { createContext, useContext } from 'react';
import { type DatasetDataScrollList } from '@/service';
// 表格数据相关的 Context
interface TableDataContextType {
sliceListData: DatasetDataScrollList;
curIndex: number;
curSliceId: string;
delSliceIds: string[];
}
export const TableDataContext = createContext<TableDataContextType | null>(
null,
);
export const useTableData = () => {
const context = useContext(TableDataContext);
if (!context) {
throw new Error('useTableData must be used within a TableDataProvider');
}
return context;
};

View File

@@ -0,0 +1,38 @@
/*
* 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 { createContext, useContext } from 'react';
import { type MutableRefObject } from 'react';
import { type TableViewMethods } from '@coze-common/table-view';
// 表格 UI 相关的 Context
interface TableUIContextType {
tableViewRef: MutableRefObject<TableViewMethods | null>;
isLoadingMoreSliceList: boolean;
isLoadingSliceList: boolean;
isShowAddBtn: boolean;
}
export const TableUIContext = createContext<TableUIContextType | null>(null);
export const useTableUI = () => {
const context = useContext(TableUIContext);
if (!context) {
throw new Error('useTableUI must be used within a TableUIProvider');
}
return context;
};

View File

@@ -0,0 +1,76 @@
/*
* 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 { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { type ISliceInfo } from '@/types/slice';
import { useCreateSlice as useCreateSliceService } from '@/service/slice';
import { useTableData } from '../../context/table-data-context';
import { useTableActions } from '../../context/table-actions-context';
export const useCreateSlice = () => {
const dataSetDetail = useKnowledgeStore(state => state.dataSetDetail);
const setDataSetDetail = useKnowledgeStore(state => state.setDataSetDetail);
const { sliceListData } = useTableData();
const { mutateSliceListData } = useTableActions();
const documentList = useKnowledgeStore(state => state.documentList);
const curDoc = documentList?.[0];
// 创建切片
const { createSlice } = useCreateSliceService({
onReload: (createItem: ISliceInfo) => {
const list =
(sliceListData?.list ?? []).filter(item => !item.addId) ?? [];
const createSliceContent = JSON.parse(createItem.content ?? '{}');
const itemContent = (curDoc?.table_meta ?? []).reduce(
(
prev: { column_name: string; column_id: string; value: string }[],
cur,
) => {
prev.push({
column_name: cur?.column_name ?? '',
column_id: cur?.id ?? '',
value: cur.id ? createSliceContent[cur.id] : '',
});
return prev;
},
[],
);
list.push({
...createItem,
content: JSON.stringify(itemContent),
});
mutateSliceListData({
...sliceListData,
total: Number(sliceListData?.total ?? '0'),
list,
});
if (dataSetDetail) {
setDataSetDetail({
...dataSetDetail,
slice_count: list?.length ?? 0,
});
}
},
});
return {
createSlice,
};
};

View File

@@ -0,0 +1,60 @@
/*
* 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 { DataNamespace, dataReporter } from '@coze-data/reporter';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { useScrollListSliceReq } from '@/service';
export const useGetSliceListData = () => {
const documentList = useKnowledgeStore(state => state.documentList);
const curDocId = documentList?.[0]?.document_id;
// 加载数据
const {
data: sliceListData,
mutate: mutateSliceListData,
reload: reloadSliceList,
loadMore: loadMoreSliceList,
loading: isLoadingSliceList,
loadingMore: isLoadingMoreSliceList,
} = useScrollListSliceReq({
params: {
document_id: curDocId,
},
reloadDeps: [curDocId],
target: null,
onError: error => {
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
eventName: ReportEventNames.KnowledgeGetSliceList,
error,
});
Toast.error(I18n.t('knowledge_document_view'));
},
});
return {
sliceListData,
mutateSliceListData,
reloadSliceList,
loadMoreSliceList,
isLoadingMoreSliceList,
isLoadingSliceList,
};
};

View File

@@ -0,0 +1,36 @@
/*
* 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 { useTableData } from '../../context/table-data-context';
const ADD_BTN_HEIGHT = 56;
export const useScroll = () => {
const { sliceListData } = useTableData();
// 滚动表格到底部
const scrollTableBodyToBottom = () => {
const bodyDom = document.querySelector(
'.table-view-box .semi-table-container>.semi-table-body',
);
if (bodyDom && sliceListData?.list.length) {
bodyDom.scrollTop = sliceListData?.list.length * ADD_BTN_HEIGHT;
}
};
return {
scrollTableBodyToBottom,
};
};

View File

@@ -0,0 +1,43 @@
/*
* 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 { useEffect, useState } from 'react';
import { useTableUI } from '../../context/table-ui-context';
import { useTableData } from '../../context/table-data-context';
export const useTableHeight = () => {
const { tableViewRef, isShowAddBtn } = useTableUI();
const { sliceListData } = useTableData();
const [tableH, setTableHeight] = useState<number | string>(0);
// 更新表格高度
useEffect(() => {
const h = tableViewRef?.current?.getTableHeight();
if (h) {
setTableHeight(isShowAddBtn ? h : '100%');
}
}, [sliceListData, isShowAddBtn, tableViewRef]);
const increaseTableHeight = (addBtnHeight: number) => {
setTableHeight(Number(tableH ?? '0') + addBtnHeight);
};
return {
tableH,
increaseTableHeight,
};
};

View File

@@ -0,0 +1,63 @@
/*
* 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 { nanoid } from 'nanoid';
import { type ISliceInfo } from '@/types/slice';
import { useTableData } from '../../context/table-data-context';
import { useTableActions } from '../../context/table-actions-context';
const ADD_BTN_HEIGHT = 56;
interface UseAddRowProps {
increaseTableHeight: (height: number) => void;
scrollTableBodyToBottom: () => void;
}
export const useAddRow = ({
increaseTableHeight,
scrollTableBodyToBottom,
}: UseAddRowProps) => {
const { sliceListData } = useTableData();
const { mutateSliceListData } = useTableActions();
const handleAddRow = () => {
/** 先增加容器的高度 */
increaseTableHeight(ADD_BTN_HEIGHT);
const items = JSON.parse(sliceListData?.list[0]?.content ?? '[]');
const addItemContent = items?.map(v => ({
...v,
value: '',
char_count: 0,
hit_count: 0,
}));
mutateSliceListData({
...sliceListData,
total: Number(sliceListData?.total ?? '0'),
list: sliceListData?.list.concat([
{ content: JSON.stringify(addItemContent), addId: nanoid() },
]) as ISliceInfo[],
});
scrollTableBodyToBottom();
};
return {
handleAddRow,
};
};

View File

@@ -0,0 +1,94 @@
/*
* 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 { DataNamespace, dataReporter } from '@coze-data/reporter';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { useSliceDeleteModal } from '@coze-data/knowledge-modal-base';
import { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { delSlice } from '@/service';
import { useTableUI } from '../../context/table-ui-context';
import { useTableData } from '../../context/table-data-context';
import { useTableActions } from '../../context/table-actions-context';
export const useDeleteSliceModal = () => {
// 外部数据
const dataSetDetail = useKnowledgeStore(state => state.dataSetDetail);
const setDataSetDetail = useKnowledgeStore(state => state.setDataSetDetail);
const documentList = useKnowledgeStore(state => state.documentList);
// 内部数据
const { sliceListData, delSliceIds } = useTableData();
const { tableViewRef } = useTableUI();
const { mutateSliceListData } = useTableActions();
const curDoc = documentList?.[0];
const slices = sliceListData?.list;
// 删除切片弹窗
const { node: deleteSliceModalNode, delete: openDeleteSliceModal } =
useSliceDeleteModal({
onDel: async () => {
try {
await delSlice(delSliceIds);
if (!curDoc) {
return;
}
Toast.success({
content: I18n.t('Delete_success'),
showClose: false,
});
console.log('oldList', slices);
const newList = (slices || []).filter(
lItem =>
!delSliceIds.includes(lItem.slice_id ?? '') &&
!delSliceIds.includes(lItem.addId ?? ''),
);
mutateSliceListData({
...sliceListData,
list: newList,
});
console.log('newList', newList);
tableViewRef?.current?.resetSelected();
if (dataSetDetail && typeof dataSetDetail.slice_count === 'number') {
setDataSetDetail({
...dataSetDetail,
slice_count:
dataSetDetail.slice_count > -1
? Math.max(
0,
dataSetDetail.slice_count - (delSliceIds?.length || 0),
)
: 0,
});
}
} catch (error) {
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
eventName: ReportEventNames.KnowledgeDeleteSlice,
error: error as Error,
});
}
},
});
return {
deleteSliceModalNode,
openDeleteSliceModal,
};
};

View File

@@ -0,0 +1,87 @@
/*
* 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 { useKnowledgeStore } from '@coze-data/knowledge-stores';
import {
ModalActionType,
useTableSegmentModal as useBaseTableSegmentModal,
} from '@coze-data/knowledge-modal-base';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { SliceStatus } from '@coze-arch/bot-api/knowledge';
import { useTableData } from '../../context/table-data-context';
import { useTableActions } from '../../context/table-actions-context';
export const useTableSegmentModal = () => {
const documentList = useKnowledgeStore(state => state.documentList);
const { sliceListData, curIndex, curSliceId } = useTableData();
const { mutateSliceListData } = useTableActions();
const curDoc = documentList?.[0];
// 表格分段弹窗
const {
node: tableSegmentModalNode,
edit: openTableSegmentModal,
fetchCreateTableSegment,
fetchUpdateTableSegment,
} = useBaseTableSegmentModal({
title:
curIndex > -1 ? (
<div className="slice-modal-title">
{I18n.t('datasets_segment_detailModel_title', { num: curIndex + 1 })}
</div>
) : (
I18n.t('dataset_segment_content')
),
meta: curDoc?.table_meta || [],
canEdit: true,
onSubmit: async (actionType, tData) => {
if (actionType === ModalActionType.Create && curDoc?.document_id) {
await fetchCreateTableSegment(curDoc?.document_id, tData);
} else if (actionType === ModalActionType.Edit && curSliceId) {
await fetchUpdateTableSegment(curSliceId, tData);
}
},
onFinish: (actionType, tData) => {
if (actionType === ModalActionType.Create) {
Toast.success({
content: I18n.t('knowledge_tableview_03'),
showClose: false,
});
} else if (actionType === ModalActionType.Edit) {
if (sliceListData) {
const updateContent = JSON.stringify(tData);
const newList = sliceListData.list;
newList[curIndex].content = updateContent;
newList[curIndex].status = SliceStatus.FinishVectoring;
mutateSliceListData({
...sliceListData,
list: newList,
});
}
}
},
});
return {
tableSegmentModalNode,
openTableSegmentModal,
fetchCreateTableSegment,
fetchUpdateTableSegment,
};
};

View File

@@ -0,0 +1,176 @@
/*
* 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 { cloneDeep } from 'lodash-es';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { transSliceContentOutput } from '@coze-data/knowledge-modal-base';
import { type TableViewRecord } from '@coze-common/table-view';
import { ColumnType } from '@coze-arch/bot-api/knowledge';
import { type DatasetDataScrollList, updateSlice } from '@/service';
import { useCreateSlice } from '../inner/use-create-slice';
import { useTableUI } from '../../context/table-ui-context';
import { useTableData } from '../../context/table-data-context';
import { useTableActions } from '../../context/table-actions-context';
export const useTableSliceOperations = ({
openDeleteSliceModal,
openTableSegmentModal,
}: {
openDeleteSliceModal: () => void;
openTableSegmentModal: (content: string) => void;
}) => {
const documentList = useKnowledgeStore(state => state.documentList);
const { mutateSliceListData, setDelSliceIds, setCurIndex, setCurSliceId } =
useTableActions();
const { createSlice } = useCreateSlice();
const { sliceListData } = useTableData();
const { tableViewRef } = useTableUI();
const curDoc = documentList?.[0];
const slices = sliceListData?.list;
const handleDeleteSlice = (indexs: number[]) => {
if (!slices) {
return;
}
/** 新增的行 */
const addIndex = indexs.filter(i => !slices[i].slice_id);
const addIds = addIndex.map(i => slices[i]?.addId);
const oldIndex = indexs.filter(v => !addIndex.includes(v));
// 确保过滤掉undefined值
const sliceIds = oldIndex
.map(i => slices[i].slice_id)
.filter(Boolean) as string[];
if (addIds.length && sliceIds.length <= 0) {
mutateSliceListData({
...sliceListData,
total: Number(sliceListData?.total ?? '0'),
list: slices.filter(item => !addIds.includes(item?.addId)),
} satisfies DatasetDataScrollList);
tableViewRef?.current?.resetSelected();
}
if (sliceIds.length) {
setDelSliceIds(sliceIds);
openDeleteSliceModal();
}
};
const handleCreateSlice = (createParams: string) => {
if (!curDoc?.document_id) {
return;
}
try {
return {
document_id: curDoc.document_id,
raw_text: createParams,
};
} catch (error) {
console.log('error', error);
return null;
}
};
const handleUpdateSliceData = async (
record: TableViewRecord,
index: number,
updateValue?: string,
) => {
if (!slices) {
return;
}
const oldData = cloneDeep(sliceListData);
try {
const sliceId = slices[index].slice_id;
const filterRecord = Object.fromEntries(
Object.entries(record).filter(
([key]) => !['tableViewKey', 'char_count', 'hit_count'].includes(key),
),
);
const ImageIds: string[] = [];
curDoc?.table_meta?.forEach(meta => {
if (meta.column_type === ColumnType.Image) {
ImageIds.push(meta.id as string);
}
});
const formatRecord = Object.fromEntries(
Object.entries(filterRecord).map(([key, value]) => {
if (ImageIds.includes(key)) {
return [key, transSliceContentOutput(value as string)];
}
return [key, value];
}),
);
const updateParams = { ...formatRecord };
delete updateParams.status;
const updateContent = JSON.stringify(updateParams);
if (sliceId) {
await updateSlice(sliceId as string, updateContent, updateValue);
} else {
/** 新增分片 */
const createParams = await handleCreateSlice(updateContent);
if (createParams && createSlice && curDoc?.document_id) {
// 调用传入的createSlice方法创建新的分片
try {
// 这里需要使用props中传入的createSlice方法来调用API
await createSlice({
document_id: curDoc.document_id,
raw_text: updateContent,
});
} catch (error) {
console.log('createSlice error:', error);
}
}
}
// 改为接口请求成功后才更新
if (slices) {
return true;
}
} catch (error) {
console.log(error);
mutateSliceListData(oldData);
throw Error(error as string);
}
};
/** 弹窗编辑切片 */
const handleModalEditSlice = (_record: TableViewRecord, index: number) => {
if (!slices || index < 0 || index >= slices.length) {
return;
}
setCurIndex(index);
setCurSliceId(slices[index]?.slice_id || '');
openTableSegmentModal(slices[index]?.content || '');
};
return {
deleteSlice: handleDeleteSlice,
createSlice: handleCreateSlice,
modalEditSlice: handleModalEditSlice,
rowUpdateSliceData: handleUpdateSliceData,
};
};

View File

@@ -0,0 +1,398 @@
/* stylelint-disable declaration-no-important */
/* stylelint-disable no-descending-specificity */
@import '../../assets/common.less';
.slice-list-ui-content {
overflow: auto;
flex: 1;
padding: 0;
}
.empty-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding-bottom: 5%;
}
.slice-modal-title {
display: flex;
align-items: center;
.tag {
margin-left: 6px;
}
}
.serial-number-text {
font-size: 12px;
color: var(--coz-fg-secondary);
}
.unit-table-view {
.serial-number {
padding-left: 0 !important;
font-size: 14px !important;
color: var(--coz-fg-secondary);
}
:global {
.coz-tag {
font-weight: 400;
}
.data-tags {
min-width: 173px;
}
.semi-table-wrapper {
margin-top: 0 !important;
}
}
}
.slice-article {
position: relative;
overflow-y: auto;
display: flex;
flex-direction: column;
height: calc(100% - 24px);
padding: 0 16px;
background-color: var(--coz-bg-plus);
border: 1px solid var(--coz-stroke-primary);
border-radius: 8px;
.slice-article-content {
padding: 12px 2px;
}
}
.text-slice-toolbar {
position: sticky;
z-index: 10;
top: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
background-color: var(--coz-bg-plus);
border-bottom: 1px solid var(--coz-stroke-primary);
.content-left-slot {
display: flex;
flex-direction: row;
align-items: center;
}
}
.doc-tag-wrapper {
margin-left: 12px;
}
.menu_wrapper {
border-radius: 8px !important;
}
.menu {
width: 160px;
padding: 4px;
:global {
.semi-dropdown-item {
height: 32px;
padding: 8px;
font-size: 12px;
border-radius: 6px;
}
.semi-icon>svg {
width: 14px;
height: 14px;
}
.semi-dropdown-item-disabled>.semi-dropdown-item-icon {
opacity: 0.3;
}
}
}
.slice-list-table {
overflow-y: hidden;
grid-auto-rows: min-content;
grid-gap: 16px;
height: 100%;
}
.table-view-container-box {
max-height: calc(100% - 51px);
// overflow-y: scroll;
}
.add-row-btn {
display: flex;
align-items: center;
height: 48px;
margin-top: 8px;
}
.spin {
:global {
.semi-spin-wrapper {
position: absolute;
}
.semi-tabs-content {
padding: 0;
}
.semi-spin-children {
height: 100%;
}
}
}
.doc-selector-dropdown {
:global {
.option-prefix-icon {
margin-right: 8px;
}
.semi-select-option {
cursor: pointer;
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: center;
box-sizing: border-box;
padding: 8px 16px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
word-break: break-word;
border-radius: 4px;
}
}
}
.doc-selector-dropdown,
.doc-selector {
overflow: hidden;
max-width: 723px;
text-overflow: ellipsis;
white-space: nowrap;
:global {
.semi-input-wrapper {
width: 100% !important;
min-width: 256px;
}
.coz-select-option-item.selected {
.doc-name-item {
font-weight: 500;
}
}
}
}
.doc-option {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
max-width: 603px;
color: var(--coz-fg-primary);
}
.doc-name {
overflow: hidden;
display: flex;
flex: 1;
margin: 0 8px;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: var(--coz-fg-primary);
&-selected {
font-weight: 500;
}
&-prev {
overflow: hidden;
display: inline-block;
flex: 1;
text-overflow: ellipsis;
white-space:nowrap;
}
}
.borderless-filter-render {
overflow: hidden;
display: flex;
align-items: center;
.borderless-filter-text {
font-weight: 600;
color: #1c1f23;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.filter-icon {
margin-bottom: -2px;
margin-left: 2px;
}
.doc-selector-trigger {
cursor: pointer;
display: flex;
align-items: center;
height: 32px;
padding: 0 12px;
font-size: 14px;
color: var(--coz-fg-primary);
border-radius: 8px;
&:hover {
background-color: var(--coz-mg-secondary-pressed, rgba(6, 7, 9, 14%));
}
&-label {
font-weight: 600;
}
&-icon {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
&:hover {
background-color: var(--coz-mg-secondary-pressed);
}
}
}
.edit-doc-name-container {
&-title {
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
line-height: 20px;
color: var(--coz-fg-plus);
}
&-input {
padding-bottom: 12px;
&-error {
padding: 2px 8px 0;
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-hglt-red);
}
}
}
.table-preview-max {
:global(.table-wrapper) {
:global {
// 为什么这么写 请看 packages/components/table-view/src/components/table-view/index.module.less(15-21)
.semi-table-tbody>.semi-table-row,
.semi-table-thead>.semi-table-row>.semi-table-row-head,
.semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-left,
.semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-left::before,
.semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-right,
.semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-right::before {
background-color: #FFF !important;
}
.semi-table-tbody {
.semi-table-row {
&:hover {
>.semi-table-row-cell {
background-color: #F2F3F7 !important;
}
}
}
.semi-table-row-selected {
.semi-table-row-cell,
.semi-table-column-selection {
background: #D8DBFB !important;
}
}
}
}
}
}
.table-preview-secondary {
:global(.table-wrapper) {
:global {
// 为什么这么写 请看 packages/components/table-view/src/components/table-view/index.module.less(15-21)
.semi-table-tbody>.semi-table-row,
.semi-table-thead>.semi-table-row>.semi-table-row-head,
.semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-left,
.semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-left::before,
.semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-right,
.semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-right::before {
background-color: #FCFCFF !important;
}
.semi-table-tbody {
.semi-table-row {
&:hover {
>.semi-table-row-cell {
background-color: #EFF0F7 !important;
}
}
}
.semi-table-row-selected {
.semi-table-row-cell,
.semi-table-column-selection {
background: #D9DDFF !important;
}
}
}
}
}
}

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.
*/
import { TableKnowledgeWorkspace } from './components/main';
// 导出组件
export { TableKnowledgeWorkspace };
export type { TableKnowledgeWorkspaceProps } from './components/main';

View File

@@ -0,0 +1,271 @@
/*
* 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 @coze-arch/max-line-per-function */
import { get } from 'lodash-es';
import { getDataTypeText } from '@coze-data/utils';
import { getSrcFromImg } from '@coze-data/knowledge-modal-base';
import { KnowledgeE2e } from '@coze-data/e2e';
import {
TextRender,
ActionsRender,
TagRender,
ImageRender,
type TableViewRecord,
type TableViewColumns,
colWidthCacheService,
} from '@coze-common/table-view';
import { I18n } from '@coze-arch/i18n';
import { Tag, Tooltip, Typography } from '@coze-arch/coze-design';
import { safeJSONParse } from '@coze-arch/bot-utils';
import { ColumnType, SliceStatus } from '@coze-arch/bot-api/knowledge';
import { type TranSliceListParams } from '@/types/slice';
const MAX_WIDTH = 1400;
const MIN_WIDTH = 200;
const DIFF_WIDTH = 397;
const READONLY_DIFF_WIDTH = 259;
export function isNoMore(data, pageSize) {
return Boolean(
!data?.total || (data.nextPageIndex - 1) * pageSize >= data.total,
);
}
export function isStop(res) {
return res?.list?.length || res?.total;
}
const ColumnTypeComp = (props: { columnType: ColumnType }) => (
<Tag color="primary" className="ml-[6px] text-xs" size="mini">
{getDataTypeText(props.columnType)}
</Tag>
);
const getTableCacheWidthMap = (tableKey: string) => {
try {
return colWidthCacheService.getTableWidthMap(tableKey) ?? {};
} catch (e) {
console.log('getTableCacheWidthMap error', e);
return {};
}
};
/**
* slice 数据转换为 TableView 组件接收的数据类型
*/
export const getTableRenderColumnsData = ({
sliceList,
metaData = [],
onEdit,
onUpdate,
onDelete,
canEdit,
tableKey,
}: TranSliceListParams): {
data: TableViewRecord[];
columns: TableViewColumns[];
} => {
try {
const dom = document.getElementsByClassName(
'knowledge-ide-base-slice-list-ui-content',
)[0];
const cacheWidthMap = getTableCacheWidthMap(tableKey);
const maxWidth = dom
? (dom as HTMLElement).offsetWidth -
(canEdit ? DIFF_WIDTH : READONLY_DIFF_WIDTH)
: MAX_WIDTH;
const res: TableViewRecord[] = sliceList.map(slice => {
const { char_count, hit_count, status } = slice;
const record = { char_count, hit_count, status };
const sliceArr = safeJSONParse(slice.content);
if (Array.isArray(sliceArr)) {
sliceArr.forEach(sliceData => {
record[sliceData.column_id] = sliceData.value;
});
}
return record;
});
const dataWidth =
maxWidth / metaData.length > MIN_WIDTH
? maxWidth / metaData.length
: MIN_WIDTH;
const columns: TableViewColumns[] = metaData.map((meta, columnIndex) => ({
dataIndex: meta.id,
title: (
<div className="flex flex-row items-center">
<Typography.Text
className="cursor-pointer"
ellipsis={{
showTooltip: {
opts: { content: meta.column_name },
},
}}
>
{meta.column_name}
</Typography.Text>
{meta.is_semantic ? (
<Tag
size="mini"
color="green"
className="ml-2"
data-testid={KnowledgeE2e.TableLocalPreviewSemantic}
>
{I18n.t('knowledge_1226_001')}
</Tag>
) : null}
{meta.column_type ? (
<ColumnTypeComp columnType={meta.column_type} />
) : null}
</div>
),
width: get(cacheWidthMap, meta.id || '') ?? dataWidth,
render: (text, record, index) => {
const isEditing =
columnIndex === 0 &&
index === sliceList.length - 1 &&
!!sliceList[index].addId;
if (meta.column_type === ColumnType.Image) {
const srcList = getSrcFromImg(text);
return (
<ImageRender
srcList={srcList}
onChange={(src, tosKey) => {
let val = '';
if (src || tosKey) {
val = `<img src="${src ?? ''}" ${
tosKey ? `data-tos-key="${tosKey}"` : ''
}>`;
}
const newRecord = { ...record, [meta?.id as string]: val };
onUpdate?.(newRecord, index);
}}
/>
);
}
// 针对违规内容高亮处理
const isAudiFailed = record?.status === SliceStatus.AuditFailed;
const textRender = () => (
<div className={`w-full ${isAudiFailed ? 'text-red-500' : ''}`}>
<TextRender
dataIndex={meta.id}
value={text}
record={record}
index={index}
isEditing={isEditing}
editable={canEdit}
validator={{
validate: value => {
if (meta.is_semantic) {
return !value || value === '';
}
return false;
},
errorMsg: I18n.t('datasets_url_empty'),
}}
onBlur={async (_text, updateRecord) =>
await onUpdate?.(updateRecord, index, _text as string)
}
/>
</div>
);
if (isAudiFailed) {
return (
<Tooltip
content={I18n.t('knowledge_content_illegal_error_msg')}
trigger="hover"
position="top"
getPopupContainer={() => document.body}
>
{textRender()}
</Tooltip>
);
}
return textRender();
},
}));
columns.push({
title: '',
className: 'not-resize-handle data-tags',
resize: false,
render: (_text, record, _index) => {
const { char_count, hit_count } = record;
return (
<div className="flex gap-3">
<TagRender
value={`${char_count ?? 0} ${I18n.t('datasets_segment_card_bit', {
num: char_count ?? 0,
})}`}
/>
<TagRender
value={I18n.t('datasets_segment_card_hit', {
num: hit_count ?? 0,
})}
/>
</div>
);
},
});
if (canEdit) {
columns.push({
title: '',
width: 100,
className: 'not-resize-handle',
resize: false,
render: (_text, record, index) => (
<ActionsRender
record={record}
index={index}
editProps={{
disabled: false,
onEdit: () => {
onEdit?.(record, index);
},
}}
deleteProps={{
disabled: false,
onDelete: () => {
onDelete?.([index]);
},
}}
/>
),
});
}
columns.unshift({
title: '',
width: 68,
fixed: true,
resize: false,
className: 'pl-0 text-sm not-resize-handle',
render: (_text, _record, index) => (
<div className="text-xs coz-fg-secondary">{index + 1}</div>
),
});
return {
data: res,
columns,
};
} catch (error) {
console.log('transSliceList handler error', error);
return {
data: [],
columns: [],
};
}
};

View File

@@ -0,0 +1,398 @@
/* stylelint-disable declaration-no-important */
/* stylelint-disable no-descending-specificity */
@import '../../../assets/common.less';
.slice-list-ui-content {
overflow: auto;
flex: 1;
padding: 0;
}
.empty-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding-bottom: 5%;
}
.slice-modal-title {
display: flex;
align-items: center;
.tag {
margin-left: 6px;
}
}
.serial-number-text {
font-size: 12px;
color: var(--coz-fg-secondary);
}
.unit-table-view {
.serial-number {
padding-left: 0 !important;
font-size: 14px !important;
color: var(--coz-fg-secondary);
}
:global {
.coz-tag {
font-weight: 400;
}
.data-tags {
min-width: 173px;
}
.semi-table-wrapper {
margin-top: 0 !important;
}
}
}
.slice-article {
position: relative;
overflow-y: auto;
display: flex;
flex-direction: column;
height: calc(100% - 24px);
padding: 0 16px;
background-color: var(--coz-bg-plus);
border: 1px solid var(--coz-stroke-primary);
border-radius: 8px;
.slice-article-content {
padding: 12px 2px;
}
}
.text-slice-toolbar {
position: sticky;
z-index: 10;
top: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
background-color: var(--coz-bg-plus);
border-bottom: 1px solid var(--coz-stroke-primary);
.content-left-slot {
display: flex;
flex-direction: row;
align-items: center;
}
}
.doc-tag-wrapper {
margin-left: 12px;
}
.menu_wrapper {
border-radius: 8px !important;
}
.menu {
width: 160px;
padding: 4px;
:global {
.semi-dropdown-item {
height: 32px;
padding: 8px;
font-size: 12px;
border-radius: 6px;
}
.semi-icon>svg {
width: 14px;
height: 14px;
}
.semi-dropdown-item-disabled>.semi-dropdown-item-icon {
opacity: 0.3;
}
}
}
.slice-list-table {
overflow-y: hidden;
grid-auto-rows: min-content;
grid-gap: 16px;
height: 100%;
}
.table-view-container-box {
max-height: calc(100% - 51px);
// overflow-y: scroll;
}
.add-row-btn {
display: flex;
align-items: center;
height: 48px;
margin-top: 8px;
}
.spin {
:global {
.semi-spin-wrapper {
position: absolute;
}
.semi-tabs-content {
padding: 0;
}
.semi-spin-children {
height: 100%;
}
}
}
.doc-selector-dropdown {
:global {
.option-prefix-icon {
margin-right: 8px;
}
.semi-select-option {
cursor: pointer;
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: center;
box-sizing: border-box;
padding: 8px 16px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
word-break: break-word;
border-radius: 4px;
}
}
}
.doc-selector-dropdown,
.doc-selector {
overflow: hidden;
max-width: 723px;
text-overflow: ellipsis;
white-space: nowrap;
:global {
.semi-input-wrapper {
width: 100% !important;
min-width: 256px;
}
.coz-select-option-item.selected {
.doc-name-item {
font-weight: 500;
}
}
}
}
.doc-option {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
max-width: 603px;
color: var(--coz-fg-primary);
}
.doc-name {
overflow: hidden;
display: flex;
flex: 1;
margin: 0 8px;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: var(--coz-fg-primary);
&-selected {
font-weight: 500;
}
&-prev {
overflow: hidden;
display: inline-block;
flex: 1;
text-overflow: ellipsis;
white-space:nowrap;
}
}
.borderless-filter-render {
overflow: hidden;
display: flex;
align-items: center;
.borderless-filter-text {
font-weight: 600;
color: #1c1f23;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.filter-icon {
margin-bottom: -2px;
margin-left: 2px;
}
.doc-selector-trigger {
cursor: pointer;
display: flex;
align-items: center;
height: 32px;
padding: 0 12px;
font-size: 14px;
color: var(--coz-fg-primary);
border-radius: 8px;
&:hover {
background-color: var(--coz-mg-secondary-pressed, rgba(6, 7, 9, 14%));
}
&-label {
font-weight: 600;
}
&-icon {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
&:hover {
background-color: var(--coz-mg-secondary-pressed);
}
}
}
.edit-doc-name-container {
&-title {
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
line-height: 20px;
color: var(--coz-fg-plus);
}
&-input {
padding-bottom: 12px;
&-error {
padding: 2px 8px 0;
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-hglt-red);
}
}
}
.table-preview-max {
:global(.table-wrapper) {
:global {
// 为什么这么写 请看 packages/components/table-view/src/components/table-view/index.module.less(15-21)
.semi-table-tbody>.semi-table-row,
.semi-table-thead>.semi-table-row>.semi-table-row-head,
.semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-left,
.semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-left::before,
.semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-right,
.semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-right::before {
background-color: #FFF !important;
}
.semi-table-tbody {
.semi-table-row {
&:hover {
>.semi-table-row-cell {
background-color: #F2F3F7 !important;
}
}
}
.semi-table-row-selected {
.semi-table-row-cell,
.semi-table-column-selection {
background: #D8DBFB !important;
}
}
}
}
}
}
.table-preview-secondary {
:global(.table-wrapper) {
:global {
// 为什么这么写 请看 packages/components/table-view/src/components/table-view/index.module.less(15-21)
.semi-table-tbody>.semi-table-row,
.semi-table-thead>.semi-table-row>.semi-table-row-head,
.semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-left,
.semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-left::before,
.semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-right,
.semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-right::before {
background-color: #FCFCFF !important;
}
.semi-table-tbody {
.semi-table-row {
&:hover {
>.semi-table-row-cell {
background-color: #EFF0F7 !important;
}
}
}
.semi-table-row-selected {
.semi-table-row-cell,
.semi-table-column-selection {
background: #D9DDFF !important;
}
}
}
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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 { IllustrationNoResult } from '@douyinfe/semi-illustrations';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { type DocumentChunk } from '@coze-data/knowledge-common-components/text-knowledge-editor/scenes/base';
import { BaseTextKnowledgeEditor } from '@coze-data/knowledge-common-components/text-knowledge-editor';
import { I18n } from '@coze-arch/i18n';
import { EmptyState } from '@coze-arch/coze-design';
import { IconSegmentEmpty } from '@coze-arch/bot-icons';
import styles from '../styles/index.module.less';
export interface BaseContentProps {
loading: boolean;
isProcessing: boolean;
documentId: string;
renderData: DocumentChunk[];
onContentChange: (chunks: DocumentChunk[]) => void;
onAddChunk: () => void;
onDeleteChunk: (chunk: DocumentChunk) => void;
}
export const BaseContent: React.FC<BaseContentProps> = ({
loading,
isProcessing,
documentId,
renderData,
onContentChange,
onAddChunk,
onDeleteChunk,
}) => {
const canEdit = useKnowledgeStore(state => state.canEdit);
const searchValue = useKnowledgeStore(state => state.searchValue);
if (renderData?.length === 0 && !loading) {
return (
<div className={classnames(styles['empty-content'])}>
<EmptyState
size="large"
icon={
searchValue ? (
<IllustrationNoResult style={{ width: 150, height: '100%' }} />
) : (
<IconSegmentEmpty style={{ width: 150, height: '100%' }} />
)
}
title={
isProcessing
? I18n.t('content_view_003')
: searchValue
? I18n.t('knowledge_no_result')
: I18n.t('dataset_segment_empty_desc')
}
/>
</div>
);
}
return (
<div className={styles['slice-article-content']}>
<BaseTextKnowledgeEditor
chunks={renderData}
documentId={documentId}
readonly={!canEdit}
onChange={onContentChange}
onAddChunk={onAddChunk}
onDeleteChunk={onDeleteChunk}
/>
</div>
);
};

View File

@@ -0,0 +1,228 @@
/*
* 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 @coze-arch/max-line-per-function */
import { useState } from 'react';
import classNames from 'classnames';
import { useDebounceFn } from 'ahooks';
import { KnowledgeE2e } from '@coze-data/e2e';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { IconCozEdit } from '@coze-arch/coze-design/icons';
import {
Popover,
Select,
Tooltip,
Button,
TextArea,
Search,
} from '@coze-arch/coze-design';
import { type OptionProps } from '@coze-arch/bot-semi/Select';
import { type FormatType } from '@coze-arch/bot-api/knowledge';
import styles from '../styles/index.module.less';
export interface DocSelectorProps<T> {
type: FormatType;
options: OptionProps[];
value: T;
canEdit?: boolean;
onChange: (v: T) => void;
onRename: (docId: string, newName: string) => void;
}
const DOC_NAME_MAX_LEN = 100;
export const DocSelector = ({
type,
options,
value,
onChange,
canEdit,
onRename,
}: DocSelectorProps<typeof value>) => {
const [search, setSearch] = useState('');
const [searchValue, setSearchValue] = useState('');
const [docName, setDocName] = useState('');
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const debounceSetSearch = useDebounceFn(
(v?: string) => {
setSearchValue(v || '');
},
{
wait: 300,
},
);
const handledOptions = options.filter(op => {
if (!searchValue) {
return true;
}
try {
const regx = new RegExp(searchValue);
// 搜索结果不展示「全部内容」选项
return (
(op.value !== 'all' && op.value === value) ||
(op?.text as string)?.match(regx)
);
} catch (e) {
return true;
}
});
const setDocNameFn = () => {
const name = options.find(op => op.value === value)?.text;
if (name) {
setDocName(name as string);
}
};
const handleEditDocName = e => {
e.stopPropagation();
setVisible(true);
};
const handleSaveDocName = () => {
if (!docName) {
return;
}
setLoading(true);
try {
onRename(value as string, docName);
setVisible(false);
} finally {
setLoading(false);
}
};
const triggerRender = ({ value: values }) => (
<Popover
clickToHide={true}
visible={visible}
onClickOutSide={() => setVisible(false)}
trigger="custom"
style={{ padding: 16, width: 320 }}
onVisibleChange={v => {
if (v) {
setDocNameFn();
}
}}
content={
<div
className={styles['edit-doc-name-container']}
onClick={e => e.stopPropagation()}
>
<div className={styles['edit-doc-name-container-title']}>
{I18n.t('knowledge_detail_doc_rename')}
</div>
<div className={styles['edit-doc-name-container-input']}>
<TextArea
value={docName}
maxLength={DOC_NAME_MAX_LEN}
maxCount={DOC_NAME_MAX_LEN}
validateStatus={!docName ? 'error' : 'default'}
placeholder={I18n.t('knowledge_upload_text_custom_doc_name_tips')}
onChange={v => setDocName(v)}
/>
{!docName && (
<div className={styles['edit-doc-name-container-input-error']}>
{I18n.t('knowledge_upload_text_custom_doc_name_tips')}
</div>
)}
</div>
<div className="text-right">
<Button
disabled={!docName}
onClick={handleSaveDocName}
size="small"
loading={loading}
>
{I18n.t('Save')}
</Button>
</div>
</div>
}
position="bottomLeft"
>
<div
onClick={() => {
setVisible(false);
}}
className={styles['doc-selector-trigger']}
data-testid={KnowledgeE2e.SegmentDetailContentSelectTrigger}
>
<div className={styles['doc-selector-trigger-label']}>
{values.map(item => (
<div key={item.value}>{item.label}</div>
))}
</div>
{!canEdit || !value ? null : (
<div>
<Tooltip
clickToHide
theme="dark"
content={I18n.t('knowledge_detail_doc_rename')}
>
<div
className={styles['doc-selector-trigger-icon']}
onClick={handleEditDocName}
data-testid={
KnowledgeE2e.SegmentDetailContentSelectTriggerEditIcon
}
>
<IconCozEdit className={'text-[14px]'} />
</div>
</Tooltip>
</div>
)}
</div>
</Popover>
);
return (
<Select
clickToHide={true}
maxHeight={670}
disabled
className={classNames(styles['doc-selector'])}
dropdownClassName={styles['doc-selector-dropdown']}
optionList={handledOptions}
value={value}
onChange={onChange}
getPopupContainer={() => document.body}
triggerRender={triggerRender}
innerTopSlot={
<Search
value={search}
placeholder={I18n.t('datasets_placeholder_search')}
onChange={v => {
setSearch(v);
}}
onSearch={v => {
logger.info({
message: 'onSearch',
meta: { v },
});
debounceSetSearch.run(v);
}}
/>
}
/>
);
};

View File

@@ -0,0 +1,91 @@
/*
* 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 { useMemo } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Space, Tag } from '@coze-arch/coze-design';
import {
ChunkType,
DocumentSource,
FormatType,
type DocumentInfo,
} from '@coze-arch/bot-api/knowledge';
import { getSourceName } from '@/utils';
import { DOCUMENT_UPDATE_TYPE_MAP } from '@/constant';
import styles from '../styles/index.module.less';
export interface DocTagProps {
documentInfo?: DocumentInfo;
}
export const DocTag: React.FC<DocTagProps> = ({ documentInfo }) => {
const updateFrequencyStr = useMemo(() => {
let str: string = DOCUMENT_UPDATE_TYPE_MAP[documentInfo?.update_type ?? ''];
if (documentInfo?.update_interval) {
str = `${I18n.t('datasets_segment_tag_updateFrequency', {
num: documentInfo.update_interval,
})}`;
}
return str;
}, [documentInfo]);
if (!documentInfo) {
return null;
}
const renderSegmentTag = () => {
const { format_type, chunk_strategy } = documentInfo;
if (format_type !== FormatType.Text || !chunk_strategy) {
return null;
}
if (chunk_strategy.chunk_type === ChunkType.CustomChunk) {
return (
<Tag color="primary" size="mini">
{I18n.t('datasets_segment_tag_custom')}
</Tag>
);
}
if (chunk_strategy.chunk_type === ChunkType.LevelChunk) {
return (
<Tag color="primary" size="mini">
{I18n.t('knowledge_level_016')}
</Tag>
);
}
return (
<Tag color="primary" size="mini">
{I18n.t('datasets_segment_tag_auto')}
</Tag>
);
};
return (
<Space className={styles['doc-tag-wrapper']} spacing={4}>
<Tag color="primary" size="mini">
{getSourceName(documentInfo)}
</Tag>
{documentInfo.source_type === DocumentSource.Web && (
<Tag color="primary" size="mini">
{updateFrequencyStr}
</Tag>
)}
{renderSegmentTag()}
</Space>
);
};

View File

@@ -0,0 +1,126 @@
/*
* 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 {
usePreviewPdf,
PreviewMd,
PreviewTxt,
} from '@coze-data/knowledge-common-components';
import {
IconCozArrowLeft,
IconCozArrowRight,
IconCozMinus,
IconCozPlus,
} from '@coze-arch/coze-design/icons';
import { IconButton } from '@coze-arch/coze-design';
export interface FilePreviewProps {
fileType?: string;
fileUrl: string;
visible: boolean;
}
export const FilePreview: React.FC<FilePreviewProps> = ({
fileType,
fileUrl,
visible,
}) => {
const {
pdfNode,
numPages,
currentPage,
onNext,
onBack,
scale,
increaseScale,
decreaseScale,
} = usePreviewPdf({
fileUrl,
});
return (
<div
className={classnames(
'w-full h-full',
'border border-solid coz-stroke-primary border-t-0 border-b-0 border-l-0',
'flex flex-col items-center overflow-auto',
!visible && 'hidden',
)}
>
{(() => {
if (fileType === 'md') {
return <PreviewMd fileUrl={fileUrl} />;
}
if (fileType === 'txt') {
return <PreviewTxt fileUrl={fileUrl} />;
}
if (['docx', 'pdf', 'doc'].includes(fileType ?? '')) {
return (
<div className="grow w-full relative">
{numPages >= 1 ? (
<div
className={classnames(
'flex w-fit h-[32px] items-center justify-center gap-[3px] absolute top-[8px] right-[8px]',
'coz-bg-max rounded-[8px] coz-shadow-default',
'z-10',
'px-[8px]',
)}
>
<IconButton
icon={<IconCozArrowLeft />}
size="small"
color="secondary"
onClick={onBack}
></IconButton>
<div className="coz-fg-secondary text-[12px] font-[400] leading-[24px]">
{currentPage} / {numPages}
</div>
<IconButton
icon={<IconCozArrowRight />}
size="small"
color="secondary"
onClick={onNext}
/>
<div className="w-[1px] h-[12px] coz-mg-primary"></div>
<IconButton
icon={<IconCozMinus />}
size="small"
color="secondary"
onClick={decreaseScale}
/>
<div className="coz-fg-secondary text-[12px] font-[400] leading-[16px]">
{Math.round(scale * 100)}%
</div>
<IconButton
icon={<IconCozPlus />}
size="small"
color="secondary"
onClick={increaseScale}
/>
</div>
) : null}
{pdfNode}
</div>
);
}
return null;
})()}
</div>
);
};

View File

@@ -0,0 +1,90 @@
/*
* 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 { IllustrationNoResult } from '@douyinfe/semi-illustrations';
import {
useKnowledgeStore,
type ILevelSegment,
} from '@coze-data/knowledge-stores';
import { LevelTextKnowledgeEditor } from '@coze-data/knowledge-common-components/text-knowledge-editor';
import { I18n } from '@coze-arch/i18n';
import { EmptyState } from '@coze-arch/coze-design';
import { IconSegmentEmpty } from '@coze-arch/bot-icons';
import { createLevelDocumentChunkByLevelSegment } from '../utils/document-utils';
import styles from '../styles/index.module.less';
export interface LevelContentProps {
isProcessing: boolean;
documentId: string;
levelSegments: ILevelSegment[];
selectionIDs: string[];
onLevelSegmentsChange: (chunks: ILevelSegment[]) => void;
onLevelSegmentDelete: (chunk: ILevelSegment) => void;
}
export const LevelContent: React.FC<LevelContentProps> = ({
isProcessing,
documentId,
levelSegments,
selectionIDs,
onLevelSegmentsChange,
onLevelSegmentDelete,
}) => {
const canEdit = useKnowledgeStore(state => state.canEdit);
const searchValue = useKnowledgeStore(state => state.searchValue);
// 转换层级分段数据为编辑器可用格式
const renderLevelSegmentsData = levelSegments.map(item =>
createLevelDocumentChunkByLevelSegment(item),
);
if (levelSegments.length === 0) {
return (
<div className={classnames(styles['empty-content'])}>
<EmptyState
size="large"
icon={
searchValue ? (
<IllustrationNoResult style={{ width: 150, height: '100%' }} />
) : (
<IconSegmentEmpty style={{ width: 150, height: '100%' }} />
)
}
title={
isProcessing
? I18n.t('content_view_003')
: searchValue
? I18n.t('knowledge_no_result')
: I18n.t('dataset_segment_empty_desc')
}
/>
</div>
);
}
return (
<LevelTextKnowledgeEditor
chunks={renderLevelSegmentsData}
selectionIDs={selectionIDs}
documentId={documentId}
readonly={!canEdit}
onChange={onLevelSegmentsChange}
onDeleteChunk={onLevelSegmentDelete}
/>
);
};

View File

@@ -0,0 +1,298 @@
/*
* 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 @coze-arch/max-line-per-function */
/* eslint-disable complexity */
import { type ReactNode, useRef } from 'react';
import classnames from 'classnames';
import {
useDataNavigate,
useKnowledgeParams,
useKnowledgeStore,
} from '@coze-data/knowledge-stores';
import {
OptType,
UnitType,
} from '@coze-data/knowledge-resource-processor-core';
import { SegmentMenu } from '@coze-data/knowledge-common-components';
import { Spin } from '@coze-arch/coze-design';
import {
ChunkType,
DocumentSource,
type DocumentInfo,
} from '@coze-arch/bot-api/knowledge';
import { type ProgressMap } from '@/types';
import { getDocumentOptions } from '../utils/document-utils';
import { useModals } from '../hooks/use-case/use-modals';
import { useInitSelectFirstDoc } from '../hooks/use-case/use-init-select-first-doc';
import { useFilePreview } from '../hooks/use-case/use-file-preview';
import { useDocumentManagement } from '../hooks/use-case/use-document-management';
import {
useDocumentInfo,
useSliceData,
useLevelSegments,
useSliceCounter,
} from '../hooks';
import { TextToolbar } from './text-toolbar';
import { LevelContent } from './level-content';
import { FilePreview } from './file-preview';
import { BaseContent } from './base-content';
export interface TextKnowledgeWorkspaceProps {
onChangeDocList?: (docList: DocumentInfo[]) => void;
reload?: () => void;
progressMap: ProgressMap;
linkOriginUrlButton?: ReactNode;
fetchSliceButton?: ReactNode;
}
export const TextKnowledgeWorkspace = ({
onChangeDocList,
reload: reloadDataset,
progressMap,
linkOriginUrlButton,
fetchSliceButton,
}: TextKnowledgeWorkspaceProps) => {
const contentWrapperRef = useRef<HTMLDivElement>(null);
const knowledgeParams = useKnowledgeParams();
const documentList = useKnowledgeStore(state => state.documentList);
const resourceNavigate = useDataNavigate();
// 初始化选择第一个文档
useInitSelectFirstDoc();
// 文档管理
const {
handleSelectDocument,
handleRenameDocument,
handleUpdateDocumentFrequency,
rollbackDocumentSelection,
} = useDocumentManagement({
reloadDataset,
});
// 文档基本信息
const { curDoc, curDocId, isProcessing, processFinished, datasetId } =
useDocumentInfo(progressMap);
// 文件预览
const { showOriginalFile, handleToggleOriginalFile } =
useFilePreview(curDocId);
// 文档片段数据
const { loading, renderData, handleContentChange, reload } = useSliceData({
curDocId,
datasetId,
curChunkType: curDoc?.chunk_strategy?.chunk_type,
processFinished,
target: contentWrapperRef,
rollbackDocumentSelection,
});
// 层级分段数据
const {
levelSegments,
selectionIDs,
setSelectionIDs,
tosLoading,
handleLevelSegmentsChange,
handleLevelSegmentDelete,
} = useLevelSegments({
curDoc,
});
// 片段计数器
const { handleIncreaseSliceCount, handleDecreaseSliceCount } =
useSliceCounter();
// 模态框
const {
deleteModalNode,
showDeleteModal,
updateFrequencyModalNode,
showUpdateFrequencyModal,
} = useModals({
docId: curDoc?.document_id,
documentType: curDoc?.format_type,
documentSource: curDoc?.source_type,
onDelete: () => {
reloadDataset?.();
handleSelectDocument('');
},
onUpdateFrequency: formData => {
if (!onChangeDocList || !curDoc) {
return;
}
const updatedDocList = handleUpdateDocumentFrequency(
curDoc.document_id ?? '',
formData,
);
if (updatedDocList) {
onChangeDocList(updatedDocList);
}
},
});
// 文档选项
const docOptions = getDocumentOptions(documentList, progressMap);
// 处理重新分段
const handleResegment = () => {
const isLocalText = Boolean(
curDoc?.source_type === DocumentSource.Document,
);
resourceNavigate.upload?.({
type: isLocalText ? UnitType.TEXT_DOC : UnitType.TEXT,
opt: OptType.RESEGMENT,
doc_id: curDocId ?? '',
page_mode: knowledgeParams.pageMode ?? '',
bot_id: knowledgeParams.botID ?? '',
});
};
const fromProject = knowledgeParams.biz === 'project';
return (
<>
<div
className={classnames(
'flex grow border-solid coz-stroke-primary coz-bg-max',
fromProject
? 'h-[calc(100%-64px)] border-0 border-t'
: 'h-[calc(100%-112px)] border rounded-[8px]',
)}
>
<div
className={classnames(
'w-[300px] h-full shrink-0 overflow-auto p-[12px]',
'border-0 border-r border-solid coz-stroke-primary',
)}
>
<SegmentMenu
isSearchable
list={(documentList ?? []).map(item => ({
id: item.document_id ?? '',
title: item.name ?? '',
label: docOptions.find(opt => opt.value === item.document_id)
?.label,
}))}
selectedID={curDocId}
onClick={id => {
if (id !== curDocId) {
handleSelectDocument(id);
}
}}
levelSegments={levelSegments}
setSelectionIDs={setSelectionIDs}
treeDisabled
treeVisible={
curDoc?.chunk_strategy?.chunk_type === ChunkType.LevelChunk
}
/>
</div>
<Spin
spinning={loading || tosLoading}
size="large"
wrapperClassName="h-full !w-full grow rounded-r-[8px] overflow-hidden"
childStyle={{ height: '100%', flexGrow: 1, width: '100%' }}
>
<TextToolbar
documentData={{
curDoc,
curDocId,
curFormatType: curDoc?.format_type,
docOptions,
}}
filePreviewData={{
showOriginalFile,
fileUrl: curDoc?.preview_tos_url,
}}
documentActions={{
onChangeDoc: handleSelectDocument,
onRenameDoc: handleRenameDocument,
onToggleOriginalFile: handleToggleOriginalFile,
onResegment: handleResegment,
onUpdateFrequency: () =>
showUpdateFrequencyModal({
updateInterval: curDoc?.update_interval,
updateType: curDoc?.update_type,
}),
onDelete: showDeleteModal,
reloadDataset,
}}
customUIElements={{
linkOriginUrlButton,
fetchSliceButton,
}}
/>
<div className="flex h-[calc(100%-56px)] grow w-full">
<FilePreview
fileType={curDoc?.type}
fileUrl={curDoc?.preview_tos_url || ''}
visible={showOriginalFile}
/>
<div
ref={contentWrapperRef}
className={classnames(
'w-full grow h-full overflow-auto',
'px-[16px] pt-[16px]',
)}
>
{curDoc?.chunk_strategy?.chunk_type === ChunkType.LevelChunk ? (
<LevelContent
isProcessing={isProcessing}
documentId={curDocId}
levelSegments={levelSegments}
selectionIDs={selectionIDs}
onLevelSegmentsChange={handleLevelSegmentsChange}
onLevelSegmentDelete={handleLevelSegmentDelete}
/>
) : (
<BaseContent
loading={loading}
isProcessing={isProcessing}
documentId={curDocId}
renderData={renderData}
onContentChange={handleContentChange}
onAddChunk={() => {
reload();
handleIncreaseSliceCount();
}}
onDeleteChunk={() => {
handleDecreaseSliceCount();
}}
/>
)}
</div>
</div>
</Spin>
</div>
{deleteModalNode}
{updateFrequencyModalNode}
</>
);
};

View File

@@ -0,0 +1,191 @@
/*
* 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 complexity */
import { type ReactNode } from 'react';
import classnames from 'classnames';
import { isFeishuOrLarkDocumentSource } from '@coze-data/utils';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import {
IconCozAdjust,
IconCozHistory,
IconCozTrashCan,
} from '@coze-arch/coze-design/icons';
import { Space, Tooltip, IconButton, Switch } from '@coze-arch/coze-design';
import { type OptionProps } from '@coze-arch/bot-semi/Select';
import {
DocumentSource,
FormatType,
type DocumentInfo,
} from '@coze-arch/bot-api/knowledge';
import { DocTag } from './doc-tag';
import { DocSelector } from './doc-selector';
// 文档基本信息
export interface DocumentData {
curDoc?: DocumentInfo;
curDocId: string;
curFormatType?: FormatType;
docOptions: OptionProps[];
}
// 文件预览相关
export interface FilePreviewData {
showOriginalFile: boolean;
fileUrl?: string;
}
// 文档操作回调
export interface DocumentActions {
onChangeDoc: (docId: string) => void;
onRenameDoc: (docId: string, newName: string) => void;
onToggleOriginalFile: (checked: boolean) => void;
onResegment: () => void;
onUpdateFrequency: () => void;
onDelete: () => void;
reloadDataset?: () => void;
}
// 自定义UI元素
export interface CustomUIElements {
linkOriginUrlButton?: ReactNode;
fetchSliceButton?: ReactNode;
}
export interface TextToolbarProps {
documentData: DocumentData;
filePreviewData: FilePreviewData;
documentActions: DocumentActions;
customUIElements: CustomUIElements;
}
export const TextToolbar: React.FC<TextToolbarProps> = ({
documentData: { curDoc, curDocId, curFormatType, docOptions },
filePreviewData: { showOriginalFile, fileUrl },
documentActions: {
onChangeDoc,
onRenameDoc,
onToggleOriginalFile,
onResegment,
onUpdateFrequency,
onDelete,
},
customUIElements: { linkOriginUrlButton, fetchSliceButton },
}) => {
const canEdit = useKnowledgeStore(state => state.canEdit);
// 控制按钮显示逻辑
const showUpdateFreBtn =
canEdit &&
curDoc &&
(curDoc.source_type === DocumentSource.Web ||
isFeishuOrLarkDocumentSource(curDoc?.source_type));
const showDeleteDocBtn = curDoc && canEdit;
const showResegmentButton = curDoc?.format_type === FormatType.Text;
const showFetchSliceBtn =
canEdit &&
curDoc &&
![DocumentSource.Custom, DocumentSource.Document].includes(
curDoc.source_type as DocumentSource,
);
return (
<div
className={classnames(
'w-full flex items-center justify-between py-[12px] px-[16px]',
'border border-solid coz-stroke-primary border-l-0 border-t-0 border-r-0',
)}
>
<div className={classnames('flex items-center')}>
<DocSelector
type={curFormatType as FormatType}
options={docOptions}
canEdit={canEdit}
value={curDocId}
onChange={onChangeDoc}
onRename={onRenameDoc}
/>
<DocTag documentInfo={curDoc} />
</div>
<Space spacing={8}>
{fileUrl ? (
<div className="flex items-center gap-2">
<span className="coz-fg-secondary text-[12px] leading-[16px]">
{I18n.t('knowledge_level_030')}
</span>
<Switch
size="mini"
checked={showOriginalFile}
onChange={onToggleOriginalFile}
></Switch>
</div>
) : null}
{showResegmentButton && canEdit ? (
<Tooltip theme="dark" content={I18n.t('knowledge_new_001')}>
<IconButton
data-testid={KnowledgeE2e.SegmentDetailUpdateBtn}
iconPosition="left"
color="secondary"
size="small"
icon={<IconCozAdjust />}
onClick={onResegment}
/>
</Tooltip>
) : null}
{showUpdateFreBtn ? (
<Tooltip
theme="dark"
content={I18n.t('datasets_unit_upload_field_update_frequency')}
>
<IconButton
data-dtestid={`${KnowledgeE2e.SegmentDetailContentItemFrequencyIcon}.${curDoc?.document_id}`}
icon={<IconCozHistory className="text-[14px]" />}
iconPosition="left"
color="secondary"
size="small"
onClick={onUpdateFrequency}
></IconButton>
</Tooltip>
) : null}
{showFetchSliceBtn ? fetchSliceButton : null}
{linkOriginUrlButton}
{showDeleteDocBtn ? (
<Tooltip theme="dark" content={I18n.t('kl2_006')}>
<IconButton
data-testid={KnowledgeE2e.SegmentDetailContentDeleteIcon}
icon={<IconCozTrashCan className="text-[14px]" />}
color="secondary"
iconPosition="left"
size="small"
onClick={onDelete}
></IconButton>
</Tooltip>
) : null}
</Space>
</div>
);
};

View File

@@ -0,0 +1,234 @@
/*
* 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 @coze-arch/max-line-per-function */
import { useMemo, useState } from 'react';
import classNames from 'classnames';
import { useDebounceFn } from 'ahooks';
import { KnowledgeE2e } from '@coze-data/e2e';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { type OptionProps } from '@coze-arch/bot-semi/Select';
import { type FormatType } from '@coze-arch/bot-api/knowledge';
import {
// IconCozArrowDownFill,
IconCozEdit,
} from '@coze-arch/coze-design/icons';
import {
Popover,
Select,
Toast,
Tooltip,
Button,
TextArea,
Search,
} from '@coze-arch/coze-design';
import { useUpdateDocument } from '@/service/document';
import styles from './index.module.less';
export interface DocSelectorProps<T> {
type: FormatType;
options: OptionProps[];
value: T;
canEdit?: boolean;
onChange: (v: T) => void;
reload: () => void;
}
const DOC_NAME_MAX_LEN = 100;
export const DocSelector = ({
type,
options,
value,
onChange,
reload,
canEdit,
}: DocSelectorProps<typeof value>) => {
const [search, setSearch] = useState('');
const [searchValue, setSearchValue] = useState('');
const [docName, setDocName] = useState('');
const [visible, setVisible] = useState(false);
const debounceSetSearch = useDebounceFn(
(v?: string) => {
setSearchValue(v || '');
},
{
wait: 300,
},
);
const handledOptions = useMemo(() => {
if (!searchValue) {
return options;
}
try {
const regx = new RegExp(searchValue);
const newOptions = options.filter(
op =>
// 搜索结果不展示「全部内容」选项
(op.value !== 'all' && op.value === value) ||
(op?.text as string)?.match(regx),
);
return newOptions;
// eslint-disable-next-line @coze-arch/use-error-in-catch
} catch (e) {
return options;
}
}, [searchValue, options]);
const setDocNameFn = () => {
const name = options.find(op => op.value === value)?.text;
if (name) {
setDocName(name as string);
}
};
const { run, loading } = useUpdateDocument({
onSuccess: () => {
Toast.success(I18n.t('Update_success'));
setVisible(false);
reload();
},
});
const handleEditDocName = e => {
e.stopPropagation();
setVisible(true);
// TODO 打开弹框
};
const triggerRender = ({ value: values }) => (
<Popover
// showArrow
clickToHide={true}
visible={visible}
onClickOutSide={() => setVisible(false)}
trigger="custom"
style={{ padding: 16, width: 320 }}
onVisibleChange={v => {
if (v) {
setDocNameFn();
}
}}
content={
<div
className={styles['edit-doc-name-container']}
onClick={e => e.stopPropagation()}
>
<div className={styles['edit-doc-name-container-title']}>
{I18n.t('knowledge_detail_doc_rename')}
</div>
<div className={styles['edit-doc-name-container-input']}>
<TextArea
value={docName}
maxLength={DOC_NAME_MAX_LEN}
maxCount={DOC_NAME_MAX_LEN}
validateStatus={!docName ? 'error' : 'default'}
placeholder={I18n.t('knowledge_upload_text_custom_doc_name_tips')}
onChange={v => setDocName(v)}
/>
{!docName && (
<div className={styles['edit-doc-name-container-input-error']}>
{I18n.t('knowledge_upload_text_custom_doc_name_tips')}
</div>
)}
</div>
<div className="text-right">
<Button
disabled={!docName}
onClick={() => {
run({
document_id: value,
document_name: docName,
});
}}
size="small"
loading={loading}
>
{I18n.t('Save')}
</Button>
</div>
</div>
}
position="bottomLeft"
>
<div
onClick={() => {
setVisible(false);
}}
className={styles['doc-selector-trigger']}
data-testid={KnowledgeE2e.SegmentDetailContentSelectTrigger}
>
<div className={styles['doc-selector-trigger-label']}>
{values.map(item => (
<div>{item.label}</div>
))}
</div>
{!canEdit || !value ? null : (
<div>
<Tooltip
clickToHide
theme="dark"
content={I18n.t('knowledge_detail_doc_rename')}
>
<div
className={styles['doc-selector-trigger-icon']}
onClick={handleEditDocName}
data-testid={
KnowledgeE2e.SegmentDetailContentSelectTriggerEditIcon
}
>
<IconCozEdit className={'text-[14px]'} />
</div>
</Tooltip>
</div>
)}
{/* <IconCozArrowDownFill className={'text-[16px] ml-[8px] p-[2px]'} /> */}
</div>
</Popover>
);
// TODO: 交互改版,这里做了个假的
return (
<Select
clickToHide={true}
maxHeight={670}
disabled
className={classNames(styles['doc-selector'])}
dropdownClassName={styles['doc-selector-dropdown']}
optionList={handledOptions}
value={value}
onChange={onChange}
getPopupContainer={() => document.body}
triggerRender={triggerRender}
innerTopSlot={
<Search
value={search}
placeholder={I18n.t('datasets_placeholder_search')}
onChange={v => {
setSearch(v);
}}
onSearch={v => {
logger.info({
message: 'onSearch',
meta: { v },
});
debounceSetSearch.run(v);
}}
/>
}
/>
);
};

View File

@@ -0,0 +1,90 @@
/*
* 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 { useMemo } from 'react';
import { I18n } from '@coze-arch/i18n';
import {
ChunkType,
DocumentSource,
FormatType,
type DocumentInfo,
} from '@coze-arch/bot-api/knowledge';
import { Space, Tag } from '@coze-arch/coze-design';
import { getSourceName } from '@/utils';
import { DOCUMENT_UPDATE_TYPE_MAP } from '@/constant';
import styles from './index.module.less';
export interface DocTagProps {
documentInfo?: DocumentInfo;
}
export const DocTag: React.FC<DocTagProps> = ({ documentInfo }) => {
const updateFrequencyStr = useMemo(() => {
let str: string = DOCUMENT_UPDATE_TYPE_MAP[documentInfo?.update_type ?? ''];
if (documentInfo?.update_interval) {
str = `${I18n.t('datasets_segment_tag_updateFrequency', {
num: documentInfo.update_interval,
})}`;
}
return str;
}, [documentInfo]);
if (!documentInfo) {
return null;
}
const renderSegmentTag = () => {
const { format_type, chunk_strategy } = documentInfo;
if (format_type !== FormatType.Text || !chunk_strategy) {
return null;
}
if (chunk_strategy.chunk_type === ChunkType.CustomChunk) {
return (
<Tag color="primary" size="mini">
{I18n.t('datasets_segment_tag_custom')}
</Tag>
);
}
if (chunk_strategy.chunk_type === ChunkType.LevelChunk) {
return (
<Tag color="primary" size="mini">
{I18n.t('knowledge_level_016')}
</Tag>
);
}
return (
<Tag color="primary" size="mini">
{I18n.t('datasets_segment_tag_auto')}
</Tag>
);
};
return (
<Space className={styles['doc-tag-wrapper']} spacing={4}>
<Tag color="primary" size="mini">
{getSourceName(documentInfo)}
</Tag>
{documentInfo.source_type === DocumentSource.Web && (
<Tag color="primary" size="mini">
{updateFrequencyStr}
</Tag>
)}
{renderSegmentTag()}
</Space>
);
};

View File

@@ -0,0 +1,40 @@
/*
* 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.
*/
/**
* 按照功能导出所有 hooks
*/
// 文档管理
export { useDocumentManagement } from './use-case/use-document-management';
// 文档信息
export { useDocumentInfo } from './life-cycle/use-document-info';
// 文档片段数据
export { useSliceData } from './life-cycle/use-slice-data';
// 层级分段数据
export { useLevelSegments } from './use-case/use-level-segments';
// 文档片段计数
export { useSliceCounter } from './use-case/use-slice-counter';
// 文件预览
export { useFilePreview } from './use-case/use-file-preview';
// 模态框
export { useModals } from './use-case/use-modals';

View File

@@ -0,0 +1,54 @@
/*
* 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 { useShallow } from 'zustand/react/shallow';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { DocumentStatus } from '@coze-arch/bot-api/knowledge';
import { type ProgressMap } from '@/types';
/**
* 处理文档基本信息的 hook
*/
export const useDocumentInfo = (progressMap: ProgressMap) => {
const { documentList, dataSetDetail, curDocId } = useKnowledgeStore(
useShallow(state => ({
curDocId: state.curDocId,
documentList: state.documentList,
dataSetDetail: state.dataSetDetail,
})),
);
// 当前文档
const curDoc = documentList?.find(i => i.document_id === curDocId);
// 处理状态
const isProcessing = curDoc?.status === DocumentStatus.Processing;
const processFinished = curDocId
? progressMap[curDocId]?.status === DocumentStatus.Enable
: false;
// 数据集ID
const datasetId = dataSetDetail?.dataset_id ?? '';
return {
curDoc,
curDocId,
isProcessing,
processFinished,
datasetId,
};
};

View File

@@ -0,0 +1,110 @@
/*
* 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 { useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { type BasicTarget } from 'ahooks/lib/utils/domTarget';
import { DataNamespace, dataReporter } from '@coze-data/reporter';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { type DocumentChunk } from '@coze-data/knowledge-common-components/text-knowledge-editor/scenes/base';
import { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { ChunkType } from '@coze-arch/bot-api/knowledge';
import { useScrollListSliceReq } from '@/service';
import { createBaseDocumentChunkBySliceInfo } from '../../utils/document-utils';
interface UseSliceDataParams {
curDocId: string;
datasetId: string;
curChunkType?: ChunkType;
processFinished: boolean;
target?: BasicTarget;
rollbackDocumentSelection: () => void;
}
/**
* 处理文档片段数据获取的 hook
*/
export const useSliceData = ({
curDocId,
datasetId,
curChunkType,
processFinished,
target,
rollbackDocumentSelection,
}: UseSliceDataParams) => {
const { searchValue } = useKnowledgeStore(
useShallow(state => ({
searchValue: state.searchValue,
})),
);
// 获取文档内容
const {
loading,
data: sliceData,
mutate,
reload,
} = useScrollListSliceReq({
target,
params: {
keyword: searchValue,
document_id:
// 如果是层级分段则不请求
curChunkType !== ChunkType.LevelChunk ? curDocId : '',
},
reloadDeps: [searchValue, curDocId, datasetId, processFinished],
onError: error => {
/** 拉取 slice 失败时,回退 curDocId避免文档标题和内容不一致用户迷惑 */
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
eventName: ReportEventNames.KnowledgeGetSliceList,
error,
});
Toast.error(I18n.t('knowledge_document_view'));
rollbackDocumentSelection();
},
});
// 使用 useMemo 缓存 chunks 数组,避免因 progressMap 更新导致不必要的重新创建
const renderData = useMemo(
() =>
sliceData?.list.map(item => createBaseDocumentChunkBySliceInfo(item)) ??
[],
[sliceData?.list],
);
// 处理内容变化
const handleContentChange = (chunks: DocumentChunk[]) => {
mutate({
...sliceData,
list: chunks,
total: Number(sliceData?.total ?? '0'),
});
};
return {
loading,
sliceData,
renderData,
handleContentChange,
reload,
};
};

View File

@@ -0,0 +1,103 @@
/*
* 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 { useRef } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { UpdateType } from '@coze-arch/bot-api/knowledge';
import { useUpdateDocument } from '@/service/document';
export const useDocumentManagement = (props?: {
reloadDataset?: () => void;
}) => {
const { curDocId, setCurDocId, documentList } = useKnowledgeStore(
useShallow(state => ({
curDocId: state.curDocId,
setCurDocId: state.setCurDocId,
documentList: state.documentList,
})),
);
// 缓存上一个文档ID用于加载失败后回滚
const prevDocIdRef = useRef<string | null>(null);
// 更新文档名称
const { run: updateDocument } = useUpdateDocument({
onSuccess: () => {
Toast.success(I18n.t('Update_success'));
props?.reloadDataset?.();
},
});
// 选择文档
const handleSelectDocument = (docId: string) => {
prevDocIdRef.current = curDocId || null;
setCurDocId(docId);
};
// 重命名文档
const handleRenameDocument = (docId: string, newName: string) => {
updateDocument({
document_id: docId,
document_name: newName,
});
};
// 更新文档频率
const handleUpdateDocumentFrequency = (
docId: string,
formData: { updateInterval?: number; updateType?: UpdateType },
) => {
if (!documentList) {
return;
}
const updatedDocList = documentList.map(doc => {
if (doc.document_id === docId) {
return {
...doc,
update_interval: formData?.updateInterval,
update_type: formData.updateInterval
? UpdateType.Cover
: UpdateType.NoUpdate,
};
}
return doc;
});
return updatedDocList;
};
// 回滚文档选择
const rollbackDocumentSelection = () => {
if (prevDocIdRef.current) {
setCurDocId(prevDocIdRef.current);
}
};
return {
prevDocIdRef,
updateDocument,
handleSelectDocument,
handleRenameDocument,
handleUpdateDocumentFrequency,
rollbackDocumentSelection,
};
};

View File

@@ -0,0 +1,37 @@
/*
* 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, useEffect } from 'react';
export const useFilePreview = (curDocId: string) => {
const [showOriginalFile, setShowOriginalFile] = useState(false);
// 切换文档时,重置预览状态
useEffect(() => {
if (showOriginalFile) {
setShowOriginalFile(false);
}
}, [curDocId]);
const handleToggleOriginalFile = (checked: boolean) => {
setShowOriginalFile(checked);
};
return {
showOriginalFile,
handleToggleOriginalFile,
};
};

View File

@@ -0,0 +1,31 @@
/*
* 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 { useEffect } from 'react';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { useDocumentManagement } from './use-document-management';
export const useInitSelectFirstDoc = () => {
const documentList = useKnowledgeStore(state => state.documentList);
const { handleSelectDocument } = useDocumentManagement();
useEffect(() => {
if (documentList?.length) {
handleSelectDocument(documentList[0]?.document_id ?? '');
}
}, [documentList]);
};

View File

@@ -0,0 +1,86 @@
/*
* 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, useMemo, useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
import { type ILevelSegment } from '@coze-data/knowledge-stores';
import { useTosContent } from '@coze-data/knowledge-common-hooks';
import { withTitle } from '@coze-data/knowledge-common-components/text-knowledge-editor/scenes/level';
import { ChunkType, type DocumentInfo } from '@coze-arch/bot-api/knowledge';
import { createLevelDocumentChunkByLevelSegment } from '../../utils/document-utils';
interface UseLevelSegmentsParams {
curDoc?: DocumentInfo;
}
/**
* 处理层级分段数据的 hook
*/
export const useLevelSegments = ({ curDoc }: UseLevelSegmentsParams) => {
// 用于层级分段选中滚动
const [selectionIDs, setSelectionIDs] = useState<string[]>([]);
const { levelSegments, setLevelSegments } = useKnowledgeStore(
useShallow(state => ({
levelSegments: state.levelSegments,
setLevelSegments: state.setLevelSegments,
})),
);
// 获取层级分段 slice 列表
const { content: treeContent, loading: tosLoading } = useTosContent(
curDoc?.chunk_strategy?.chunk_type === ChunkType.LevelChunk
? curDoc?.doc_tree_tos_url
: undefined,
);
// 使用 useMemo 缓存转换后的层级分段数据
const renderLevelSegmentsData = useMemo(
() =>
levelSegments.map(item => createLevelDocumentChunkByLevelSegment(item)),
[levelSegments],
);
// 处理层级分段变更
const handleLevelSegmentsChange = (chunks: ILevelSegment[]) => {
setLevelSegments(chunks);
};
// 处理删除层级分段
const handleLevelSegmentDelete = (chunk: ILevelSegment) => {
setLevelSegments(
levelSegments.filter(item => item.slice_id !== chunk.slice_id),
);
};
// 初始化时加载层级分段
useEffect(() => {
setLevelSegments(withTitle(treeContent?.chunks ?? [], curDoc?.name ?? ''));
}, [treeContent]);
return {
levelSegments,
renderLevelSegmentsData,
selectionIDs,
setSelectionIDs,
tosLoading,
handleLevelSegmentsChange,
handleLevelSegmentDelete,
};
};

View File

@@ -0,0 +1,81 @@
/*
* 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 ReactNode } from 'react';
import {
useDeleteUnitModal,
useUpdateFrequencyModal,
} from '@coze-data/knowledge-modal-base';
import {
type FormatType,
type DocumentSource,
type UpdateType,
} from '@coze-arch/bot-api/knowledge';
export interface UseModalsProps {
docId?: string;
documentType?: FormatType;
documentSource?: DocumentSource;
onDelete?: () => void;
onUpdateFrequency?: (formData: {
updateInterval?: number;
updateType?: UpdateType;
}) => void;
}
export interface UseModalsReturn {
deleteModalNode: ReactNode;
showDeleteModal: () => void;
updateFrequencyModalNode: ReactNode;
showUpdateFrequencyModal: (params: {
updateInterval?: number;
updateType?: UpdateType;
}) => void;
}
export const useModals = (props: UseModalsProps): UseModalsReturn => {
const { docId, documentType, documentSource, onDelete, onUpdateFrequency } =
props;
// 删除模态框
const { node: deleteModalNode, delete: showDeleteModal } = useDeleteUnitModal(
{
docId,
onDel: () => {
onDelete?.();
},
},
);
// 更新频率模态框
const { node: updateFrequencyModalNode, edit: showUpdateFrequencyModal } =
useUpdateFrequencyModal({
docId,
onFinish: formData => {
onUpdateFrequency?.(formData);
},
type: documentType,
documentSource,
});
return {
deleteModalNode,
showDeleteModal,
updateFrequencyModalNode,
showUpdateFrequencyModal,
};
};

View File

@@ -0,0 +1,69 @@
/*
* 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 { useShallow } from 'zustand/react/shallow';
import { useKnowledgeStore } from '@coze-data/knowledge-stores';
/**
* 处理文档片段计数的 hook
*/
export const useSliceCounter = () => {
const { dataSetDetail, setDataSetDetail } = useKnowledgeStore(
useShallow(state => ({
dataSetDetail: state.dataSetDetail,
setDataSetDetail: state.setDataSetDetail,
})),
);
// 处理添加块时更新计数
const handleIncreaseSliceCount = () => {
if (!dataSetDetail) {
return;
}
setDataSetDetail({
...dataSetDetail,
slice_count:
// @ts-expect-error -- linter-disable-autofix
dataSetDetail.slice_count > -1
? // @ts-expect-error -- linter-disable-autofix
dataSetDetail.slice_count + 1
: 0,
});
};
// 处理删除块时更新计数
const handleDecreaseSliceCount = () => {
if (!dataSetDetail) {
return;
}
setDataSetDetail({
...dataSetDetail,
slice_count:
// @ts-expect-error -- linter-disable-autofix
dataSetDetail.slice_count > -1
? // @ts-expect-error -- linter-disable-autofix
dataSetDetail.slice_count - 1
: 0,
});
};
return {
handleIncreaseSliceCount,
handleDecreaseSliceCount,
};
};

View File

@@ -0,0 +1,344 @@
/* stylelint-disable declaration-no-important */
@import '../../assets/common.less';
.slice-list-ui-content {
overflow: auto;
flex: 1;
padding: 0;
}
.empty-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding-bottom: 5%;
}
.slice-modal-title {
display: flex;
align-items: center;
.tag {
margin-left: 6px;
}
}
.serial-number-text {
font-size: 12px;
color: var(--coz-fg-secondary);
}
.unit-table-view {
@apply !coz-bg-max;
.serial-number {
padding-left: 0 !important;
font-size: 14px !important;
color: var(--coz-fg-secondary);
}
:global {
.coz-tag {
font-weight: 400;
}
.data-tags {
min-width: 173px;
}
.semi-table-wrapper {
margin-top: 0 !important;
}
}
}
.slice-article {
position: relative;
overflow-y: auto;
display: flex;
flex-direction: column;
height: calc(100% - 24px);
padding: 0 16px;
background-color: var(--coz-bg-plus);
border: 1px solid var(--coz-stroke-primary);
border-radius: 8px;
.slice-article-content {
padding: 12px 2px;
}
}
.text-slice-toolbar {
position: sticky;
z-index: 10;
top: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
background-color: var(--coz-bg-plus);
border-bottom: 1px solid var(--coz-stroke-primary);
.content-left-slot {
display: flex;
flex-direction: row;
align-items: center;
}
}
.doc-tag-wrapper {
margin-left: 4px;
:global {
.semi-tag-content {
font-weight: 500;
}
}
}
.menu_wrapper {
border-radius: 8px !important;
}
.menu {
width: 160px;
padding: 4px;
:global {
.semi-dropdown-item {
height: 32px;
padding: 8px;
font-size: 12px;
border-radius: 6px;
}
.semi-icon>svg {
width: 14px;
height: 14px;
}
.semi-dropdown-item-disabled>.semi-dropdown-item-icon {
opacity: 0.3;
}
}
}
.slice-list-table {
overflow-y: hidden;
grid-auto-rows: min-content;
grid-gap: 16px;
height: 100%;
padding: 0 24px 13px;
}
.table-view-container-box {
max-height: calc(100% - 51px);
// overflow-y: scroll;
}
.add-row-btn {
display: flex;
align-items: center;
height: 48px;
margin-top: 8px;
}
.spin {
:global {
.semi-spin-wrapper {
position: absolute;
}
.semi-tabs-content {
padding: 0;
}
.semi-spin-children {
height: 100%;
}
}
}
.doc-selector-dropdown {
:global {
.option-prefix-icon {
margin-right: 8px;
}
.semi-select-option {
cursor: pointer;
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: center;
box-sizing: border-box;
padding: 8px 16px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
word-break: break-word;
border-radius: 4px;
}
}
}
.doc-selector-dropdown,
.doc-selector {
overflow: hidden;
max-width: 723px;
text-overflow: ellipsis;
white-space: nowrap;
opacity: 1;
:global {
.semi-input-wrapper {
width: 100% !important;
min-width: 256px;
}
.coz-select-option-item.selected {
.doc-name-item {
font-weight: 500;
}
}
}
}
.doc-option {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
max-width: 603px;
color: var(--coz-fg-primary);
}
.doc-name {
overflow: hidden;
display: flex;
flex: 1;
margin: 0 8px;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: var(--coz-fg-primary);
&-selected {
font-weight: 500;
}
&-prev {
overflow: hidden;
display: inline-block;
flex: 1;
text-overflow: ellipsis;
white-space:nowrap;
}
}
.borderless-filter-render {
overflow: hidden;
display: flex;
align-items: center;
.borderless-filter-text {
font-weight: 600;
color: #1c1f23;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.filter-icon {
margin-bottom: -2px;
margin-left: 2px;
}
.doc-selector-trigger {
cursor: pointer;
display: flex;
gap: 4px;
align-items: center;
height: 32px;
font-size: 14px;
color: var(--coz-fg-primary);
border-radius: 8px;
&:hover {
// background-color: var(--coz-mg-secondary-pressed, rgba(6, 7, 9, 14%));
cursor: default;
}
&-icon {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
&:hover {
background-color: var(--coz-mg-secondary-pressed);
}
}
}
.edit-doc-name-container {
&-title {
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
line-height: 20px;
color: var(--coz-fg-plus);
}
&-input {
padding-bottom: 12px;
&-error {
padding: 2px 8px 0;
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-hglt-red);
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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 { TextKnowledgeWorkspace } from './components/main';
export { TextKnowledgeWorkspace };
export type { TextKnowledgeWorkspaceProps } from './components/main';

View File

@@ -0,0 +1,344 @@
/* stylelint-disable declaration-no-important */
@import '../../../assets/common.less';
.slice-list-ui-content {
overflow: auto;
flex: 1;
padding: 0;
}
.empty-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding-bottom: 5%;
}
.slice-modal-title {
display: flex;
align-items: center;
.tag {
margin-left: 6px;
}
}
.serial-number-text {
font-size: 12px;
color: var(--coz-fg-secondary);
}
.unit-table-view {
@apply !coz-bg-max;
.serial-number {
padding-left: 0 !important;
font-size: 14px !important;
color: var(--coz-fg-secondary);
}
:global {
.coz-tag {
font-weight: 400;
}
.data-tags {
min-width: 173px;
}
.semi-table-wrapper {
margin-top: 0 !important;
}
}
}
.slice-article {
position: relative;
overflow-y: auto;
display: flex;
flex-direction: column;
height: calc(100% - 24px);
padding: 0 16px;
background-color: var(--coz-bg-plus);
border: 1px solid var(--coz-stroke-primary);
border-radius: 8px;
.slice-article-content {
padding: 12px 2px;
}
}
.text-slice-toolbar {
position: sticky;
z-index: 10;
top: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
background-color: var(--coz-bg-plus);
border-bottom: 1px solid var(--coz-stroke-primary);
.content-left-slot {
display: flex;
flex-direction: row;
align-items: center;
}
}
.doc-tag-wrapper {
margin-left: 4px;
:global {
.semi-tag-content {
font-weight: 500;
}
}
}
.menu_wrapper {
border-radius: 8px !important;
}
.menu {
width: 160px;
padding: 4px;
:global {
.semi-dropdown-item {
height: 32px;
padding: 8px;
font-size: 12px;
border-radius: 6px;
}
.semi-icon>svg {
width: 14px;
height: 14px;
}
.semi-dropdown-item-disabled>.semi-dropdown-item-icon {
opacity: 0.3;
}
}
}
.slice-list-table {
overflow-y: hidden;
grid-auto-rows: min-content;
grid-gap: 16px;
height: 100%;
padding: 0 24px 13px;
}
.table-view-container-box {
max-height: calc(100% - 51px);
// overflow-y: scroll;
}
.add-row-btn {
display: flex;
align-items: center;
height: 48px;
margin-top: 8px;
}
.spin {
:global {
.semi-spin-wrapper {
position: absolute;
}
.semi-tabs-content {
padding: 0;
}
.semi-spin-children {
height: 100%;
}
}
}
.doc-selector-dropdown {
:global {
.option-prefix-icon {
margin-right: 8px;
}
.semi-select-option {
cursor: pointer;
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: center;
box-sizing: border-box;
padding: 8px 16px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
word-break: break-word;
border-radius: 4px;
}
}
}
.doc-selector-dropdown,
.doc-selector {
overflow: hidden;
max-width: 723px;
text-overflow: ellipsis;
white-space: nowrap;
opacity: 1;
:global {
.semi-input-wrapper {
width: 100% !important;
min-width: 256px;
}
.coz-select-option-item.selected {
.doc-name-item {
font-weight: 500;
}
}
}
}
.doc-option {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
max-width: 603px;
color: var(--coz-fg-primary);
}
.doc-name {
overflow: hidden;
display: flex;
flex: 1;
margin: 0 8px;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: var(--coz-fg-primary);
&-selected {
font-weight: 500;
}
&-prev {
overflow: hidden;
display: inline-block;
flex: 1;
text-overflow: ellipsis;
white-space:nowrap;
}
}
.borderless-filter-render {
overflow: hidden;
display: flex;
align-items: center;
.borderless-filter-text {
font-weight: 600;
color: #1c1f23;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.filter-icon {
margin-bottom: -2px;
margin-left: 2px;
}
.doc-selector-trigger {
cursor: pointer;
display: flex;
gap: 4px;
align-items: center;
height: 32px;
font-size: 14px;
color: var(--coz-fg-primary);
border-radius: 8px;
&:hover {
// background-color: var(--coz-mg-secondary-pressed, rgba(6, 7, 9, 14%));
cursor: default;
}
&-icon {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
&:hover {
background-color: var(--coz-mg-secondary-pressed);
}
}
}
.edit-doc-name-container {
&-title {
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
line-height: 20px;
color: var(--coz-fg-plus);
}
&-input {
padding-bottom: 12px;
&-error {
padding: 2px 8px 0;
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-hglt-red);
}
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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 { nanoid } from 'nanoid';
import { type DocumentChunk } from '@coze-data/knowledge-common-components/text-knowledge-editor/scenes/base';
import { type SliceInfo } from '@coze-arch/bot-api/knowledge';
export const createBaseDocumentChunkBySliceInfo = (
props: SliceInfo,
): DocumentChunk => ({
text_knowledge_editor_chunk_uuid: nanoid(),
...props,
});

View File

@@ -0,0 +1,79 @@
/*
* 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 complexity */
import { I18n } from '@coze-arch/i18n';
import {
DocumentStatus,
type DocumentInfo,
} from '@coze-arch/bot-api/knowledge';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tag, Tooltip, Typography } from '@coze-arch/coze-design';
import { getBasicConfig } from '@/utils/preview';
import { getUnitType } from '@/utils';
import { type ProgressMap } from '@/types';
const FINISH_PROGRESS = 100;
export const getDocumentOptions = (
documentList: DocumentInfo[],
progressMap: ProgressMap = {},
) => {
const basicConfig = getBasicConfig();
return documentList.map(doc => {
const unitType = getUnitType({
format_type: doc?.format_type,
source_type: doc?.source_type,
});
const config = basicConfig[unitType];
return {
value: doc.document_id,
text: doc.name,
label: (
<div
className="flex flex-row items-center justify-center max-w-[603px] coz-fg-primary"
key={doc?.document_id}
>
<div className="flex text-[16px]">{config?.icon}</div>
<Typography.Text
ellipsis={{ showTooltip: { opts: { theme: 'dark' } } }}
fontSize="14px"
className="w-full grow truncate ml-[8px]"
>
{doc.name}
</Typography.Text>
<div className="flex items-center shrink-0 ml-[4px]">
{Object.keys(progressMap).includes(doc?.document_id ?? '') &&
progressMap?.[doc?.document_id ?? '']?.progress <
FINISH_PROGRESS ? (
<Tag color="blue" size="mini" className="font-medium">
{I18n.t('datasets_segment_tag_processing')}
{` ${progressMap[doc?.document_id ?? '']?.progress}%`}
</Tag>
) : null}
{doc?.status === DocumentStatus.Failed ? (
<Tooltip theme="dark" content={doc?.status_descript}>
<IconCozInfoCircle className="coz-fg-hglt-red" />
</Tooltip>
) : null}
</div>
</div>
),
};
});
};

View File

@@ -0,0 +1,110 @@
/*
* 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 { nanoid } from 'nanoid';
import { type ILevelSegment } from '@coze-data/knowledge-stores';
import { type DocumentChunk } from '@coze-data/knowledge-common-components/text-knowledge-editor/scenes/base';
import { type LevelDocumentChunk } from '@coze-data/knowledge-common-components/text-knowledge-editor';
import { I18n } from '@coze-arch/i18n';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tag, Tooltip, Typography } from '@coze-arch/coze-design';
import { type OptionProps } from '@coze-arch/bot-semi/Select';
import {
DocumentStatus,
type DocumentInfo,
type SliceInfo,
} from '@coze-arch/bot-api/knowledge';
import { getBasicConfig } from '@/utils/preview';
import { getUnitType } from '@/utils';
import { type ProgressMap } from '@/types';
const FINISH_PROGRESS = 100;
/**
* 创建基础文档块
*/
export const createBaseDocumentChunkBySliceInfo = (
props: SliceInfo,
): DocumentChunk => ({
text_knowledge_editor_chunk_uuid: nanoid(),
...props,
});
/**
* 创建层级文档块
*/
export const createLevelDocumentChunkByLevelSegment = (
props: ILevelSegment,
): LevelDocumentChunk => ({
text_knowledge_editor_chunk_uuid: nanoid(),
sequence: props.slice_sequence?.toString(),
content: props.text,
...props,
});
/**
* 获取文档选项
*/
export const getDocumentOptions = (
documentList: DocumentInfo[],
progressMap: ProgressMap = {},
): OptionProps[] => {
const basicConfig = getBasicConfig();
return documentList.map(doc => {
const unitType = getUnitType({
format_type: doc?.format_type,
source_type: doc?.source_type,
});
const config = basicConfig[unitType];
return {
value: doc.document_id,
text: doc.name,
label: (
<div
className="flex flex-row items-center justify-center max-w-[603px] coz-fg-primary"
key={doc?.document_id}
>
<div className="flex text-[16px]">{config?.icon}</div>
<Typography.Text
ellipsis={{ showTooltip: { opts: { theme: 'dark' } } }}
fontSize="14px"
className="w-full grow truncate ml-[8px]"
>
{doc.name}
</Typography.Text>
<div className="flex items-center shrink-0 ml-[4px]">
{Object.keys(progressMap).includes(doc?.document_id ?? '') &&
progressMap?.[doc?.document_id ?? '']?.progress <
FINISH_PROGRESS ? (
<Tag color="blue" size="mini" className="font-medium">
{I18n.t('datasets_segment_tag_processing')}
{` ${progressMap[doc?.document_id ?? '']?.progress}%`}
</Tag>
) : null}
{doc?.status === DocumentStatus.Failed ? (
<Tooltip theme="dark" content={doc?.status_descript}>
<IconCozInfoCircle className="coz-fg-hglt-red" />
</Tooltip>
) : null}
</div>
</div>
),
};
});
};

View File

@@ -0,0 +1,28 @@
/*
* 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 { nanoid } from 'nanoid';
import { type ILevelSegment } from '@coze-data/knowledge-stores';
import { type LevelDocumentChunk } from '@coze-data/knowledge-common-components/text-knowledge-editor';
export const createLevelDocumentChunkByLevelSegment = (
props: ILevelSegment,
): LevelDocumentChunk => ({
text_knowledge_editor_chunk_uuid: nanoid(),
sequence: props.slice_sequence?.toString(),
content: props.text,
...props,
});