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,181 @@
/*
* 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 { nanoid } from 'nanoid';
import { transSliceContentOutput } from '@coze-data/knowledge-modal-base';
import {
DocumentEditor,
useInitEditor,
EditorToolbar,
type Chunk,
} from '@coze-data/knowledge-common-components/text-knowledge-editor';
import { I18n } from '@coze-arch/i18n';
import { Form, Input, Modal, Toast } from '@coze-arch/coze-design';
import { MemoryApi } from '@coze-arch/bot-api';
import { useGetWebInfo } from './hooks/get-web-info';
import { editorToolbarActionRegistry } from './editor-toolbar-actions-contributes';
import { editorContextActionRegistry } from './editor-context-actions-contributes';
import styles from './index.module.less';
const MAX_DOC_NAME_LEN = 100;
export interface UseBrowseDetailModalReturnValue {
open: () => void;
node: JSX.Element;
}
export interface ViewOnlinePageDetailProps {
id?: string;
url?: string;
content?: string;
title?: string;
}
export interface BrowseUrlModalProps {
name: string;
webID?: string;
onSubmit: (name: string, content?: string) => void;
onCancel?: () => void;
}
export const BrowseUrlModal = ({
name,
webID,
onSubmit,
onCancel,
}: BrowseUrlModalProps) => {
const [docName, setDocName] = useState<string>(name);
const { data: pageList, runAsync, mutate } = useGetWebInfo();
const [initChunk, setInitChunk] = useState<Chunk>({
text_knowledge_editor_chunk_uuid: nanoid(),
content: '',
});
const { editor } = useInitEditor({
chunk: initChunk,
editorProps: {
attributes: {
class: 'h-[360px] overflow-y-auto',
},
},
onChange: v => {
mutate(
([] as ViewOnlinePageDetailProps[]).concat({
...pageList?.[0],
content: v.content ?? '',
}),
);
},
});
useEffect(() => {
setDocName(name);
}, [name]);
useEffect(() => {
if (webID) {
runAsync(webID).then(data => {
setInitChunk({
text_knowledge_editor_chunk_uuid: nanoid(),
content: data[0].content ?? '',
});
});
}
}, [webID, runAsync, editor]);
return (
<Modal
title={I18n.t('knowledge_insert_img_001')}
width={792}
visible
cancelText={I18n.t('Cancel')}
okText={I18n.t('datasets_segment_detailModel_save')}
maskClosable={false}
onOk={async () => {
const pageInfo = pageList?.[0];
const content = transSliceContentOutput(pageInfo.content as string);
await MemoryApi.SubmitWebContentV2({
web_id: pageInfo.id,
content,
});
Toast.success({
content: I18n.t('datasets_url_saveSuccess'),
showClose: false,
});
onSubmit?.(docName, content);
onCancel?.();
}}
onCancel={() => {
onCancel?.();
}}
>
<Form<Record<string, unknown>> layout="vertical" showValidateIcon={false}>
<Form.Slot
label={{ text: I18n.t('knowledge_upload_text_custom_doc_name') }}
>
<Input
className={styles['doc-name-input']}
value={docName}
onChange={(v: string) => setDocName(v)}
maxLength={MAX_DOC_NAME_LEN}
placeholder={I18n.t('knowledge_upload_text_custom_doc_name_tips')}
/>
</Form.Slot>
<Form.Slot
className={styles['form-segment-content']}
label={{ text: I18n.t('knowledge_upload_text_custom_doc_content') }}
>
<DocumentEditor
editor={editor}
placeholder={I18n.t(
'knowledge_upload_text_custom_doc_content_tips',
)}
editorContextMenuItemsRegistry={editorContextActionRegistry}
editorBottomSlot={
<EditorToolbar
editor={editor}
actionRegistry={editorToolbarActionRegistry}
/>
}
/>
{pageList?.[0]?.url ? (
<div className={styles['browse-source-url']}>
{I18n.t('knowledge_insert_img_003', {
url: pageList[0]?.url,
})}
</div>
) : null}
</Form.Slot>
</Form>
</Modal>
);
};
export const useBrowseUrlModal = (props: BrowseUrlModalProps) => {
const [visible, setVisible] = useState(false);
return {
open: () => setVisible(true),
close: () => setVisible(false),
node: visible ? (
<BrowseUrlModal {...props} onCancel={() => setVisible(false)} />
) : null,
};
};

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 { UploadImageMenu } from '@coze-data/knowledge-common-components/text-knowledge-editor/features/editor-actions/upload-image';
import {
createEditorActionFeatureRegistry,
type EditorActionRegistry,
} from '@coze-data/knowledge-common-components/text-knowledge-editor/features/editor-actions';
export const editorContextActionRegistry: EditorActionRegistry = (() => {
const editorContextActionFeatureRegistry = createEditorActionFeatureRegistry(
'editor-context-actions',
);
editorContextActionFeatureRegistry.registerSome([
{
type: 'upload-image',
module: {
Component: UploadImageMenu,
},
},
]);
return editorContextActionFeatureRegistry;
})();

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 { UploadImageButton } from '@coze-data/knowledge-common-components/text-knowledge-editor/features/editor-actions/upload-image';
import {
createEditorActionFeatureRegistry,
type EditorActionRegistry,
} from '@coze-data/knowledge-common-components/text-knowledge-editor/features/editor-actions';
export const editorToolbarActionRegistry: EditorActionRegistry = (() => {
const editorToolbarActionFeatureRegistry = createEditorActionFeatureRegistry(
'editor-toolbar-actions',
);
editorToolbarActionFeatureRegistry.registerSome([
{
type: 'upload-image',
module: {
Component: UploadImageButton,
},
},
]);
return editorToolbarActionFeatureRegistry;
})();

View File

@@ -0,0 +1,75 @@
/*
* 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 { useRequest } from 'ahooks';
import { KnowledgeApi } from '@coze-arch/bot-api';
import { type ViewOnlinePageDetailProps } from '@/types';
/**
* 将API返回的网页信息转换为视图数据
*/
const transformWebInfoToViewData = (webInfo: {
id?: string;
url?: string;
title?: string;
content?: string;
}): ViewOnlinePageDetailProps => ({
id: webInfo?.id,
url: webInfo?.url,
title: webInfo?.title,
content: webInfo?.content,
});
export const useGetWebInfo = (): {
data: ViewOnlinePageDetailProps[];
loading: boolean;
runAsync: (webID: string) => Promise<ViewOnlinePageDetailProps[]>;
mutate: (data: ViewOnlinePageDetailProps[]) => void;
} => {
const { data, mutate, loading, runAsync } = useRequest(
async (webID: string) => {
const { data: responseData } = await KnowledgeApi.GetWebInfo({
web_ids: [webID],
include_content: true,
});
// 如果没有数据,返回空数组
if (!responseData?.[webID]?.web_info) {
return [] as ViewOnlinePageDetailProps[];
}
const webInfo = responseData[webID].web_info;
const mainPageData = transformWebInfoToViewData(webInfo);
const result = [mainPageData];
// 处理子页面数据
if (webInfo?.subpages?.length) {
const subpagesData = webInfo.subpages.map(transformWebInfoToViewData);
result.push(...subpagesData);
}
return result;
},
);
return {
data: data || [],
loading,
runAsync,
mutate,
};
};

View File

@@ -0,0 +1,106 @@
/* stylelint-disable declaration-no-important */
.browse-detail-modal {
padding-bottom: 40px;
&-main {
display: flex;
align-items: center;
margin-bottom: 16px;
font-size: 14px;
font-weight: 600;
line-height: 20px;
color: rgb(28 31 35 / 100%);
&-image {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin-right: 7px;
background-color: rgb(152 205 253 / 100%);
border-radius: 2px;
}
&-title {
max-width: 40%;
}
&-url {
max-width: 40%;
margin-left: 7px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
color: rgb(28 31 35 / 35%);
}
}
&-collapse {
overflow-y: auto;
max-height: 400px;
background-color: #f5f5f5;
border-radius: 4px;
:global {
.semi-collapse-item {
border-color: #efefef !important;
}
.semi-input-textarea-wrapper {
background-color: rgb(0 0 0 / 0%) !important;
border: none !important;
}
}
}
}
.browse-modal-header {
display: flex;
flex-wrap: nowrap;
align-items: center;
padding: 24px 0;
&-left {
display: flex;
align-items: center;
&-icon {
margin-right: 8px;
}
}
}
.modal.upgrade-level {
:global {
.semi-modal-body {
height: 0;
padding-bottom: 0;
}
.semi-modal-content {
background-color: var(--semi-color-tertiary-light-default);
}
}
}
.browse-detail-tooltip {
width: 200px;
word-wrap: break-word;
}
.browse-source-url {
margin-top: 12px;
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: rgb(29 28 36 / 35%)
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { useBrowseUrlModal } from './browser-url-modal';

View File

@@ -0,0 +1,37 @@
.card-radio-group {
gap: 4px;
:global {
.semi-radio {
padding: 12px;
border-radius: 8px;
&:hover {
background-color: var(--coz-mg-secondary-hovered);
}
&:active {
background-color: var(--coz-mg-secondary-pressed);
}
&.semi-radio-checked {
padding: 11.5px;
background-color: var(--coz-mg-card);
border-width: 1.5px;
}
}
.semi-radio-content {
flex-grow: 1;
.semi-radio-addon {
font-weight: 500;
}
.semi-radio-extra {
font-size: 12px;
line-height: 16px;
}
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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 PropsWithChildren } from 'react';
import classNames from 'classnames';
import { RadioGroup, type RadioGroupProps } from '@coze-arch/coze-design';
import styles from './index.module.less';
export type CardRadioGroupProps<T = unknown> = PropsWithChildren<
Pick<RadioGroupProps, 'value' | 'className'>
> & {
onChange?: (value: T) => void;
};
/**
* 始终使用卡片风格,并符合 UI 设计样式的 {@link RadioGroup}
*/
export function CardRadioGroup<T = unknown>({
value,
onChange,
className,
children,
}: CardRadioGroupProps<T>) {
return (
<RadioGroup
type="pureCard"
direction="vertical"
value={value}
onChange={e => {
onChange?.(e.target.value as T);
}}
className={classNames(styles['card-radio-group'], className)}
>
{children}
</RadioGroup>
);
}

View File

@@ -0,0 +1,65 @@
/*
* 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, type PropsWithChildren } from 'react';
import classNames from 'classnames';
import { IconCozArrowRight } from '@coze-arch/coze-design/icons';
import { Collapsible, Typography } from '@coze-arch/coze-design';
export interface CollapsePanelProps extends PropsWithChildren {
header: React.ReactNode;
keepDOM?: boolean;
}
/**
* 用 Collapsible 封装的更符合 UI 设计的折叠面板
*/
export function CollapsePanel({
header,
keepDOM,
children,
}: CollapsePanelProps) {
const [open, setOpen] = useState(true);
return (
<div className="mb-[4px]">
<div
className={classNames(
'h-[40px] flex items-center gap-[4px] shrink-0 rounded',
'cursor-pointer hover:coz-mg-secondary-hovered active:coz-mg-secondary-pressed',
)}
onClick={() => setOpen(!open)}
>
<IconCozArrowRight
className={classNames('coz-fg-secondary text-[14px] m-[4px]', {
'rotate-90': open,
})}
/>
<Typography.Text fontSize="14px" weight={400}>
{header}
</Typography.Text>
</div>
<Collapsible
className="ml-[26px] [&>div]:pt-[4px]"
isOpen={open}
keepDOM={keepDOM}
>
{children}
</Collapsible>
</div>
);
}

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 { I18n } from '@coze-arch/i18n';
import { Banner } from '@coze-arch/coze-design';
export const ConfigurationBanner = () => (
<Banner
style={{ marginTop: '10px' }}
type="warning"
description={I18n.t('knowledge_limit_20')}
/>
);

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 { IllustrationNoResult } from '@douyinfe/semi-illustrations';
import { UIEmpty } from '@coze-arch/bot-semi';
import styles from './index.module.less';
interface ConfigurationErrorProps {
fetchTableInfo: () => void;
}
export const ConfigurationError = (props: ConfigurationErrorProps) => {
const { fetchTableInfo } = props;
return (
<UIEmpty
className={styles['load-failure']}
empty={{
title: 'Read failure',
description: '',
icon: <IllustrationNoResult></IllustrationNoResult>,
btnText: 'Retry',
btnOnClick: fetchTableInfo,
}}
/>
);
};

View File

@@ -0,0 +1,24 @@
/* stylelint-disable font-family-no-missing-generic-family-keyword */
.load-failure {
min-height: 400px;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 400px;
&-content {
margin-left: 8px;
font-family: "SF Pro Display";
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px;
color: rgb(29 28 35 / 35%);
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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 { Spin } from '@coze-arch/coze-design';
import styles from './index.module.less';
export const ConfigurationLoading = () => (
<div className={styles.loading}>
<Spin spinning></Spin>
<div className={styles['loading-content']}>
{I18n.t('knowledge_1221_03')}
</div>
</div>
);

View File

@@ -0,0 +1,53 @@
/*
* 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 cls from 'classnames';
import { Typography } from '@coze-arch/coze-design';
interface IDocumentItemProps {
id: string;
onClick: (id: string) => void;
selected: boolean;
title: string;
status?: 'pending' | 'finished' | 'failed';
addonAfter?: ReactNode;
}
export const DocumentItem: React.FC<IDocumentItemProps> = props => {
const { id, onClick, title, selected, addonAfter } = props;
return (
<div
className={cls(
'w-full h-8 px-2 py-[6px] rounded-[8px] hover:coz-mg-primary cursor-pointer flex flex-nowrap',
selected && 'coz-mg-primary',
)}
onClick={() => onClick(id)}
>
<Typography.Text
className="w-full coz-fg-primary text-[14px] leading-[20px] grow truncate"
ellipsis
>
{title}
</Typography.Text>
{addonAfter}
</div>
);
};
export default DocumentItem;

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 classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Tag, Tooltip } from '@coze-arch/coze-design';
import {
getFilterPagesString,
getSortedFilterPages,
} from '../../utils/render-document-filter-value';
import { DocumentItem } from './document-item';
interface FilterPageConfig {
pageIndex: number;
isFilter: boolean;
}
interface Document {
// TODO: 扩充
id: string;
title: string;
/** 是否存在过滤内容 */
filterPageConfigList: FilterPageConfig[];
}
interface IDocumentListProps {
documents: Document[];
value: string;
onChange: (value: string) => void;
className?: string;
}
export const DocumentList = (props: IDocumentListProps) => (
<div className={classNames('flex flex-col gap-1 h-full', props.className)}>
<div className="w-full pl-2 h-6 items-center flex">
<div className="coz-fg-secondary text-[12px] font-[400] leading-4 shrink-0">
{I18n.t('kl_write_105')}
</div>
</div>
<div className="flex flex-col gap-1 h-[150px] !overflow-scroll shrink-0">
{props.documents.map(document => {
if (!document.id) {
return null;
}
const filterPages = getSortedFilterPages(document.filterPageConfigList);
const isFiltered = Boolean(filterPages.length);
const filterPagesString = getFilterPagesString(filterPages);
return (
<DocumentItem
id={document.id}
selected={document.id === props.value}
onClick={id => {
props.onChange(id);
}}
title={document.title}
addonAfter={
isFiltered ? (
<Tooltip
content={I18n.t('data_filter_values', {
filterPages: filterPagesString,
})}
>
<Tag color="primary" className="flex-shrink-0">
{I18n.t('knowledge_new_002')}
</Tag>
</Tooltip>
) : null
}
/>
);
})}
</div>
</div>
);

View File

@@ -0,0 +1,41 @@
.auth-empty-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: calc(100% - 112px);
.auth-empty {
display: flex;
flex-direction: column;
align-items: center;
}
.auth-empty-image {
display: flex;
align-items: center;
justify-content: center;
width: 140px;
height: 140px;
}
.auth-empty-description {
margin-top: 16px;
font-size: 16px;
font-weight: 600;
line-height: 22px;
color: var(--coz-fg-primary);
}
.auth-empty-second-desc {
margin-top: 4px;
font-size: 14px;
line-height: 20px;
color: var(--coz-fg-secondary);
}
.auth-empty-button {
margin-top: 21px;
}
}

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 classNames from 'classnames';
import { Button } from '@coze-arch/coze-design';
import style from './index.module.less';
export interface CommonEmptyAuthProps {
illustrationIcon: JSX.Element;
description: string;
authUrl?: string;
btnText?: string;
className?: string;
onClick?: () => void;
secondDesc?: string;
}
export const AuthEmpty = (props: CommonEmptyAuthProps) => {
const {
description,
authUrl,
onClick,
btnText,
illustrationIcon,
className,
secondDesc,
} = props;
const handleClick = () => {
if (authUrl) {
window.open(authUrl, '_self');
} else {
onClick?.();
}
};
return (
<div className={classNames(style['auth-empty-wrapper'], className)}>
<div className={style['auth-empty']}>
<div className={style['auth-empty-image']}>{illustrationIcon}</div>
<div className={style['auth-empty-description']}>{description}</div>
{secondDesc ? (
<div className={style['auth-empty-second-desc']}>{secondDesc}</div>
) : null}
{btnText ? (
<Button
color="highlight"
className={style['auth-empty-button']}
onClick={handleClick}
>
{btnText}
</Button>
) : null}
</div>
</div>
);
};

View File

@@ -0,0 +1,18 @@
/*
* 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 { AuthEmpty } from './common-empty-auth';
export type { CommonEmptyAuthProps } from './common-empty-auth';

View File

@@ -0,0 +1,41 @@
.auth-empty-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: calc(100% - 112px);
.auth-empty {
display: flex;
flex-direction: column;
align-items: center;
}
.auth-empty-image {
display: flex;
align-items: center;
justify-content: center;
width: 140px;
height: 140px;
}
.auth-empty-description {
margin-top: 16px;
font-size: 16px;
font-weight: 600;
line-height: 22px;
color: var(--Light-usage-text---color-text-0, #1D1C23);
}
.auth-empty-second-desc {
margin-top: 4px;
font-size: 14px;
line-height: 20px;
color: var(--Light-usage-text---color-text-2, rgba(29, 28, 35, 60%));
}
.auth-empty-button {
margin-top: 21px;
}
}

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 classNames from 'classnames';
import { UIButton } from '@coze-arch/bot-semi';
import style from './index.module.less';
interface Props {
illustrationIcon: JSX.Element;
description: string;
btnText?: string;
className?: string;
onClick?: () => void;
secondDesc?: string;
}
export const Empty = (props: Props) => {
const {
description,
onClick,
btnText,
illustrationIcon,
className,
secondDesc,
} = props;
return (
<div className={classNames(style['auth-empty-wrapper'], className)}>
<div className={style['auth-empty']}>
<div className={style['auth-empty-image']}>{illustrationIcon}</div>
<div className={style['auth-empty-description']}>{description}</div>
{secondDesc ? (
<div className={style['auth-empty-second-desc']}>{secondDesc}</div>
) : null}
{btnText ? (
<UIButton
type="tertiary"
className={style['auth-empty-button']}
onClick={onClick}
>
{btnText}
</UIButton>
) : null}
</div>
</div>
);
};

View File

@@ -0,0 +1,17 @@
.frequency-form-item {
.title {
display: block;
flex-shrink: 0;
box-sizing: border-box;
margin-bottom: 8px;
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-secondary);
}
.content {
flex: 1;
}
}

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 { type FC } from 'react';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { Select, Typography } from '@coze-arch/coze-design';
import { getFrequencyMap } from '../../utils';
import { FrequencyDay } from '../../constants';
import styles from './index.module.less';
interface Props {
value: number;
onChange: (v: number) => void;
}
export const FrequencyFormItem: FC<Props> = ({ value, onChange }) => (
<div
className={styles['frequency-form-item']}
data-testid={KnowledgeE2e.OnlineUploadModalFrequencySelect}
>
<Typography.Text className={styles.title}>
{I18n.t('datasets_frequencyModal_frequency')}
</Typography.Text>
<div className={styles.content}>
<Select
placeholder={I18n.t('datasets_frequencyModal_frequency')}
style={{ width: '100%' }}
value={value}
onChange={v => onChange(v as number)}
>
{[
FrequencyDay.ZERO,
FrequencyDay.ONE,
FrequencyDay.THREE,
FrequencyDay.SEVEN,
FrequencyDay.THIRTY,
].map(frequency => (
<Select.Option key={frequency} value={frequency}>
{getFrequencyMap(frequency)}
</Select.Option>
))}
</Select>
</div>
</div>
);

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.
*/
export { UploadFooter } from './upload-footer';
export { useBrowseUrlModal } from './browser-url-modal';
export {
TableSettingBar,
type TableSettingBarProps,
type TablePreviewProps,
TablePreview,
type TableStructureProps,
TableStructure,
TableStructureTitle,
} from './table-format';
export { UnitProgress } from './unit-progress';
export { UploadUnitFile } from './upload-unit-file';
export { UploadUnitTable } from './upload-unit-table';
export { ConfigurationError } from './configuration/error';
export { ConfigurationLoading } from './configuration/loading';
export { ConfigurationBanner } from './configuration/banner';
export { AuthEmpty } from './empty-auth';
export { Empty } from './empty';
export { FrequencyFormItem } from './frequency-form-item';
export { CollapsePanel } from './collapse-panel';
export { CardRadioGroup } from './card-radio-group';
export { getProcessStatus } from './upload-unit-table/utils';
export {
type ColumnInfo,
type UploadUnitTableProps,
type RenderColumnsProps,
} from './upload-unit-table/types';
export {
ActionRenderByDelete,
ActionRenderByEditName,
ActionRenderByRetry,
} from './upload-unit-table';

View File

@@ -0,0 +1,117 @@
/* stylelint-disable declaration-no-important */
/* stylelint-disable no-duplicate-selectors */
.progress-wrap {
overflow: hidden;
display: flex;
gap: 10px;
align-items: center;
align-self: stretch;
padding: 8px 10px;
background: var(--coz-mg-card);
border:1px solid var(--coz-stroke-primary);
border-radius: var(--default, 8px);
.content {
z-index: 1;
}
.progress {
position: absolute;
top: 0;
bottom: 0;
left: 0;
background: linear-gradient(180deg, rgba(148, 152, 247, 44%), rgba(255, 255, 255, 0%));
}
.info {
.main-text {
font-size: 14px;
line-height: 20px;
color: var(--coz-fg-primary);
}
.sub-text {
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-secondary);
}
.desc {
display: block;
}
.tip-desc {
display: none;
}
:global {
.semi-image {
margin-right: 0!important;
}
}
}
&:hover {
.info {
.desc {
display: none;
}
.tip-desc {
display: block;
}
}
}
&.processing-failed {
border: 1px solid var(--coz-stroke-hglt-red);
.sub-text {
color: var(--coz-fg-hglt-red)!important;
}
}
&.processing {
&:hover {
.info {
.desc {
display: block;
}
.tip-desc {
display: none;
}
}
}
}
.right {
z-index: 2;
}
.percent {
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px;
color: var(--coz-fg-primary);
text-align: right;
text-overflow: ellipsis;
}
.actions {
display: none;
}
&:hover {
.actions {
display: block;
}
}
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ProcessProgressItem } from './process-progress-item';

View File

@@ -0,0 +1,120 @@
/*
* 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 { Fragment } from 'react';
import cls from 'classnames';
import { KnowledgeE2e } from '@coze-data/e2e';
import { Typography, Space } from '@coze-arch/coze-design';
import { ProcessStatus, type ProcessProgressItemProps } from '../../types';
import styles from './index.module.less';
export const ProcessProgressItem: React.FC<ProcessProgressItemProps> = ({
className,
style,
mainText,
subText,
percent = 10,
percentFormat,
avatar,
status,
actions,
tipText = '',
}) => {
const renderProgress = () => {
if (status === ProcessStatus.Processing) {
return (
<div className={styles.progress} style={{ width: `${percent}%` }}></div>
);
}
return null;
};
const renderActions = () => {
if (status === ProcessStatus.Processing) {
return (
<span className={styles.percent}>
{percentFormat ? percentFormat : `${percent}%`}
</span>
);
}
return (
<div className={cls(styles.actions, 'process-progress-item-actions')}>
{Array.isArray(actions) ? (
<Space spacing="tight">
{actions.map((action, idx) => (
<Fragment key={idx}>{action}</Fragment>
))}
</Space>
) : null}
</div>
);
};
return (
<div
key={mainText}
className={cls(
styles['progress-wrap'],
'flex justify-between relative mb-[8px]',
status === ProcessStatus.Failed ? styles['processing-failed'] : '',
status === ProcessStatus.Processing ? styles.processing : '',
className,
)}
style={style}
>
<div
className={cls(
styles.content,
'process-progress-item-content',
'max-w-[calc(100%-100px)]',
)}
>
<div className={cls('flex items-center', styles.info)}>
{avatar}
<div className={cls('pl-[10px] max-w-full')}>
<div className={styles['main-text']}>
<Typography.Text
data-dtestid={`${KnowledgeE2e.CreateUnitListProgressName}.${mainText}`}
className={'coz-fg-primary text-14px'}
ellipsis={{
showTooltip: {
opts: { content: mainText },
},
}}
>
{mainText}
</Typography.Text>
</div>
<div className={styles['sub-text']}>
<div className={styles.desc}>{subText}</div>
{tipText ? (
<div className={styles['tip-desc']}>{tipText}</div>
) : null}
</div>
</div>
</div>
</div>
<div className={cls(styles.right, 'process-progress-item-right')}>
{renderActions()}
</div>
{renderProgress()}
</div>
);
};

View File

@@ -0,0 +1,164 @@
/* stylelint-disable no-descending-specificity */
/* stylelint-disable max-nesting-depth */
/* stylelint-disable font-family-no-missing-generic-family-keyword */
/* stylelint-disable declaration-no-important */
.table-preview {
overflow: hidden;
display: flex;
flex: 1;
flex-direction: column;
&-title {
display: flex;
align-items: center;
height: 32px;
margin-bottom: 5px;
font-size: 14px;
font-weight: 600;
font-style: normal;
line-height: 20px;
color: var(--coz-fg-plus);
}
// .table-view-wrapper {
// border: 1px solid #1D1C231F;
// border-radius: 8px;
// :global {
// .semi-table-wrapper {
// margin-top: 0;
// }
// }
// }
.semantic-tag,
.column-type {
margin-left: 6px;
}
.preview-tips {
margin-top: 8px;
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: var(--coz-fg-dim);
text-align: left;
letter-spacing: 0;
}
.no-result {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 448px;
&-tips {
margin-top: 16px;
font-size: 16px;
font-weight: 600;
font-style: normal;
line-height: 22px;
color: var(--coz-fg-plus);
}
}
&-content {
// flex: 1;
overflow: auto;
:global {
th,tr {
background-color: transparent !important;
}
.semi-table-wrapper {
height: 100%;
margin-top: 0;
.coz-tag {
font-weight: 400;
}
}
.coz-table-wrapper {
.coz-table-list-hover .semi-table-row:hover>.semi-table-row-cell::before {
background-color: transparent
}
.coz-table-list-hover .semi-table-row:hover>.semi-table-row-cell:first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.coz-table-list {
.semi-table-thead>.semi-table-row>.semi-table-row-head {
font-size: 12px;
font-weight: 500;
color: var(--coz-fg-secondary);
}
.semi-table-tbody>.semi-table-row>.semi-table-row-cell {
height: 56px;
text-align: unset;
.semi-typography {
color: var(--coz-fg-secondary);
}
}
.semi-table-container>.semi-table-body {
padding-top: 0;
}
.semi-table-tbody>.semi-table-row:last-child>.semi-table-row-cell {
border-bottom: 1px solid var(--coz-stroke-primary);
}
}
}
.semi-table-header {
position: sticky;
z-index: 99;
overflow-y: hidden !important;
// border-top-left-radius: 8px;
// border-top-right-radius: 8px;
}
.semi-table-body {
// max-height: calc(100% - 40px) !important;
// max-height: 460px !important;
// border-bottom-right-radius: 8px;
// border-bottom-left-radius: 8px;
}
.semi-table-colgroup .semi-table-col {
min-width: 200px;
}
.semi-table-tbody>.semi-table-row>.semi-table-row-cell {
min-height: 40px;
padding: 9px 16px !important;
}
/** 去掉hover行样式 */
.semi-table-tbody .semi-table-row:hover>.semi-table-row-cell {
background-color: transparent !important;
background-image: none !important;
border-bottom: 1px solid var(--coz-stroke-primary);
}
}
}
}
.td-title {
display: flex;
align-items: center;
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useMemo } from 'react';
import { IllustrationNoResult } from '@douyinfe/semi-illustrations';
import { getDataTypeText } from '@coze-data/utils';
import { KnowledgeE2e } from '@coze-data/e2e';
import { ImageRender, type TableViewColumns } from '@coze-common/table-view';
import { ColumnType } from '@coze-arch/idl/knowledge';
import { I18n } from '@coze-arch/i18n';
import { type ColumnProps } from '@coze-arch/bot-semi/Table';
import { Table, Tag, Typography } from '@coze-arch/coze-design';
import { getSrcFromImg } from '@/utils/table';
import { type TableInfo, type TableSettings } from '@/types';
import { TableSettingFormFields } from '@/constants';
import styles from './index.module.less';
export interface TablePreviewProps {
data: TableInfo;
settings: TableSettings;
}
interface PreviewColumn {
[key: string]: string;
}
const ColumnTypeComp = (props: { columnType: ColumnType }) => (
<Tag color="primary" className={styles['column-type']} size="mini">
{getDataTypeText(props.columnType)}
</Tag>
);
const baseClassName = 'table-preview';
const maxDataLen = 10;
export const TablePreview: React.FC<TablePreviewProps> = ({
data,
settings,
}) => {
const { sheet_list = [], table_meta = {}, preview_data = {} } = data;
const startRow = Number(settings[TableSettingFormFields.DATA_START_ROW]) || 0;
// 选中的表id
const sheetId = useMemo(
() => settings[TableSettingFormFields.SHEET] || 0,
[settings],
);
// 选中的表名
const sheetName = useMemo(
() => (sheet_list || []).find(sheet => sheet?.id === sheetId)?.sheet_name,
[sheet_list, sheetId],
);
// 选中的表的数据量
const total = useMemo(
() =>
Number(
(sheet_list || []).find(sheet => sheet?.id === sheetId)?.total_row || 0,
) - startRow || 0,
[sheet_list, sheetId],
);
const newColumns: ColumnProps<PreviewColumn | TableViewColumns>[] = (
table_meta[sheetId] || []
).map(meta => {
const {
sequence,
column_name,
is_semantic,
column_type = ColumnType.Unknown,
} = meta;
if (column_type === ColumnType.Image) {
return {
title: (
<div className={styles['td-title']}>
<div>{column_name}</div>
<ColumnTypeComp columnType={column_type}></ColumnTypeComp>
</div>
),
dataIndex: sequence,
render: (text, _record) => {
const srcList = getSrcFromImg(text);
return <ImageRender srcList={srcList} editable={false} />;
},
};
}
return {
title: is_semantic ? (
<div className={styles['td-title']}>
{column_name}
{is_semantic ? (
<Tag
size="mini"
color="green"
className={styles['semantic-tag']}
data-testid={KnowledgeE2e.TableLocalPreviewSemantic}
>
{I18n.t('knowledge_1226_001')}
</Tag>
) : null}
<ColumnTypeComp columnType={column_type}></ColumnTypeComp>
</div>
) : (
<div className={styles['td-title']}>
{column_name}
<ColumnTypeComp columnType={column_type}></ColumnTypeComp>
</div>
),
width: 180,
dataIndex: sequence,
ellipsis: { showTitle: false },
render: text => (
<Typography.Text ellipsis={{ showTooltip: true }}>
{text}
</Typography.Text>
),
};
});
const dataList = useMemo(() => {
const previewData = preview_data[sheetId] || [];
return previewData
.slice(0, maxDataLen)
.sort((a, b) =>
(JSON.stringify(a) as unknown as number) >
(JSON.stringify(b) as unknown as number)
? 1
: -1,
);
}, [preview_data, sheetId]);
return (
<div className={styles[baseClassName]}>
{dataList.length ? (
<>
<div
className={styles[`${baseClassName}-title`]}
data-testid={KnowledgeE2e.TableLocalPreviewTitle}
>
{sheetName}
</div>
<div className={styles[`${baseClassName}-content`]}>
<Table
tableProps={{
dataSource: dataList,
columns: newColumns,
}}
/>
</div>
<div
className={styles['preview-tips']}
data-testid={KnowledgeE2e.TableLocalPreviewFooterTotal}
>
{I18n.t('datasets_unit_tableformat_tips1', {
TotalRows: total,
ShowRows: Number(total) > maxDataLen ? maxDataLen : total,
})}
</div>
</>
) : (
<div className={styles['no-result']}>
<IllustrationNoResult></IllustrationNoResult>
<div className={styles['no-result-tips']}>
{I18n.t('knowledge_1221_02')}
</div>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,37 @@
/* stylelint-disable font-family-no-missing-generic-family-keyword */
.table-setting-bar {
width: 100%;
margin-bottom: 30px;
:global {
.semi-form-horizontal .semi-form-field {
flex: 1;
.semi-select {
width: 100%;
border-radius: 8px;
}
}
.semi-form-field-label-text {
font-size: 14px;
font-weight: 500;
font-style: normal;
line-height: 24px;
color: var(--coz-fg-plus);
}
.semi-form-field-label {
display: flex;
align-items: center;
height: 24px;
margin-bottom: 4px;
}
.semi-form-horizontal .semi-form-field:last-child {
margin-right: 0;
padding-right: 0;
}
}
}

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 React, { useMemo, useRef } from 'react';
import { get, isNumber } from 'lodash-es';
import classNames from 'classnames';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { type FormApi } from '@coze-arch/bot-semi/Form';
import { Form } from '@coze-arch/bot-semi';
import { type GetDocumentTableInfoResponse } from '@coze-arch/bot-api/memory';
import { FormSelect } from '@coze-arch/coze-design';
import { type TableSettings } from '@/types';
import { TableSettingFormFields } from '@/constants';
import { initIndexOptions } from './utils';
import styles from './index.module.less';
export interface TableSettingBarProps {
className?: string;
data: GetDocumentTableInfoResponse;
tableSettings: TableSettings;
setTableSettings: (values: TableSettings) => void;
}
const minOptLen = 2;
export const TableSettingBar: React.FC<TableSettingBarProps> = ({
className = '',
data = {},
tableSettings,
setTableSettings,
}) => {
const { preview_data, sheet_list } = data;
if (!preview_data || !sheet_list || !sheet_list.length) {
return <></>;
}
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const formApi = useRef<FormApi<TableSettings>>();
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const sheet = useMemo(
() => get(sheet_list, tableSettings[TableSettingFormFields.SHEET]),
[sheet_list, tableSettings],
);
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const initValues = useMemo(() => tableSettings, [sheet_list]);
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
const settings = useMemo(() => {
const options =
!sheet.id && sheet.id !== 0
? []
: initIndexOptions(
Number(sheet?.total_row) > 1 ? Number(sheet.total_row) : minOptLen,
0,
);
return [
{
e2e: KnowledgeE2e.TableLocalTableConfigurationDataSheet,
field: TableSettingFormFields.SHEET,
label: I18n.t('datasets_createFileModel_tab_DataSheet'),
options: sheet_list.map(s => ({
value: s.id,
label: s.sheet_name,
})),
},
{
e2e: KnowledgeE2e.TableLocalTableConfigurationSheetHeader,
field: TableSettingFormFields.KEY_START_ROW,
label: I18n.t('datasets_createFileModel_tab_header'),
options: options.slice(0, options.length - 1),
},
{
e2e: KnowledgeE2e.TableLocalTableConfigurationStarRow,
field: TableSettingFormFields.DATA_START_ROW,
label: I18n.t('datasets_createFileModel_tab_dataStarRow'),
// 数据起始行需大于表头行
options: options.slice(
Number(tableSettings[TableSettingFormFields.KEY_START_ROW]) + 1,
),
},
];
}, [data, sheet]);
const handleFormChange = (
values: TableSettings,
changedValue: Partial<TableSettings>,
) => {
if (setTableSettings) {
setTableSettings({ ...values });
}
const curSheet = get(changedValue, TableSettingFormFields.SHEET);
const curKeyStartRow = get(
changedValue,
TableSettingFormFields.KEY_START_ROW,
);
if (isNumber(curSheet) && formApi.current) {
// 修改sheet,初始化表头行和数据行
formApi.current.setValue(TableSettingFormFields.KEY_START_ROW, 0);
formApi.current.setValue(TableSettingFormFields.DATA_START_ROW, 1);
}
if (curKeyStartRow && formApi.current) {
const dataStartRow = get(values, TableSettingFormFields.DATA_START_ROW);
if (!(curKeyStartRow < dataStartRow)) {
formApi.current.setValue(
TableSettingFormFields.DATA_START_ROW,
curKeyStartRow + 1,
);
}
}
};
return (
<div className={classNames(styles['table-setting-bar'], className)}>
<Form<typeof initValues>
layout="horizontal"
initValues={initValues}
getFormApi={api => (formApi.current = api)}
onValueChange={(values, changedValue) => {
handleFormChange(values, changedValue);
}}
>
{settings.map(setting => {
const { options, ...selectProps } = setting;
return (
<FormSelect
data-testid={setting.e2e}
key={setting.field}
optionList={options}
{...selectProps}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onChange={(v?: string | number | any[] | Record<string, any>) => {
if (!v) {
return;
}
handleFormChange(
{ ...tableSettings, [setting.field]: v as unknown as number },
{ [setting.field]: v as unknown as number },
);
}}
/>
);
})}
</Form>
</div>
);
};

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 { I18n } from '@coze-arch/i18n';
export const initIndexOptions = (length: number, start: number) => {
const MAX_VALUE = 50;
const limit = length > MAX_VALUE ? MAX_VALUE : length;
const res: Array<{
label: string;
value: number;
}> = [];
for (let i = start; i < limit; i++) {
res.push({
label: I18n.t('datasets_createFileModel_tab_dataStarRow_value', {
LineNumber: i + 1,
}),
value: i,
});
}
return res;
};

View File

@@ -0,0 +1,234 @@
/* stylelint-disable no-descending-specificity */
/* stylelint-disable no-duplicate-selectors */
/* stylelint-disable declaration-no-important */
/* stylelint-disable max-nesting-depth */
.table-structure-wrapper {
overflow: hidden;
flex: 1;
height: 100%;
}
.structure-wrapper {
overflow: hidden;
display: flex;
flex-direction: column;
}
.drag-table {
:global {
.semi-table-container {
.semi-table-tbody {
.semi-table-row {
.semi-table-row-cell {
&:first-child {
padding-left: 16px !important;
}
}
/** 暂时注释,后续还要用,待讨论 */
// &:hover {
// background: var(--coz-mg-secondary-hovered) !important;
// .semi-table-row-cell:first-child {
// border-top-left-radius: 8px !important;
// border-bottom-left-radius: 8px !important;
// }
// }
}
}
}
}
}
.table-structure {
overflow: hidden;
margin-top: 4px !important;
border-radius: 8px;
:global {
.semi-spin, .semi-spin-children, .semi-table-fixed-header,.semi-table-container {
height: 100%;
}
}
&-required-container {
display: flex;
align-items: center;
}
&-col-required {
margin-top: 4px;
font-size: 12px;
font-weight: 600;
font-style: normal;
line-height: 16px;
color: var(--coz-fg-hglt-red);
text-overflow: ellipsis;
}
.input-suffix {
display: inline-block;
margin-right: 5px;
font-size: 12px;
// color: rgb(28 29 35 / 35%);
}
.input-error-msg {
position: relative;
z-index: 100;
margin-top: 4px;
margin-bottom: -20px;
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: var(--coz-fg-hglt-red);
}
.semantic-radio {
position: relative;
display: flex;
align-items: center;
height: 100%;
// padding-top: 3px;
padding-left: 2px;
}
.column-item-action {
display: flex;
align-items: center;
height: 100%;
padding-top: 3px;
padding-left: 7px;
&-delete {
cursor: pointer;
font-size: 14px;
}
}
.column-item {
display: flex;
flex-direction: column;
height: 100%;
}
.column-item-value {
font-size: 14px;
line-height: 32px;
color: var(--coz-fg-secondary);
text-overflow: ellipsis;
}
:global {
.structure-table-drag-icon {
position: absolute;
left: -16px;
opacity: 0;
}
.semi-table-row {
&:hover,
&:focus {
.structure-table-drag-icon {
opacity: 1;
}
}
}
.semi-table-thead>.semi-table-row>.semi-table-row-head {
font-size: 12px !important;
color: var(--coz-fg-secondary) !important;
}
.semi-table-container {
.semi-table-body {
height: calc(100% - 39px) !important;
padding-bottom: 12px;
}
}
.select-error-text {
padding-top: 0 !important;
}
.singleline-select-error-content {
height: 0;
}
.semi-table-thead>.semi-table-row>.semi-table-row-head {
&:first-child {
padding-left: 8px;
}
}
/** table去掉背景色 */
.semi-table-thead>.semi-table-row>.semi-table-row-head, .semi-table-tbody>.semi-table-row, .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 {
background-color: transparent !important;
}
.semi-table-tbody {
padding: 5px 0;
.semi-table-row {
.semi-table-row-cell {
height: 56px !important;
padding: 12px 8px !important;
border-bottom: 0;
// 此处不需要 UITable 默认的 border-radius
&:first-child {
padding-right: 28px !important;
padding-left: 8px !important;
border-radius: 0 !important;
}
}
&:hover {
cursor: auto;
background: var(--coz-mg-secondary-hovered);
&>.semi-table-row-cell {
background: transparent !important;
border-bottom: 0 !important;
}
}
}
}
}
}
.table-header-tooltip {
display: flex;
align-items: center;
}
.table-structure-bar-title {
display: flex;
align-items: center;
height: 32px;
font-size: 14px;
font-weight: 600;
font-style: normal;
color: var(--coz-fg-plus);
.icon {
margin-top: 3px;
margin-left: 4px;
}
}

View File

@@ -0,0 +1,630 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable max-lines */
/* eslint-disable max-lines-per-function */
import React, {
useCallback,
useMemo,
useState,
type FC,
type CSSProperties,
} from 'react';
import classNames from 'classnames';
import { CSS as cssDndKit } from '@dnd-kit/utilities';
import {
SortableContext,
arrayMove,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import {
DndContext,
PointerSensor,
useSensors,
useSensor,
type DragEndEvent,
} from '@dnd-kit/core';
import {
DataTypeSelect,
getDataTypeOptions,
getDataTypeText,
} from '@coze-data/utils';
import { OptType } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import {
IconCozInfoCircle,
IconCozTrashCan,
} from '@coze-arch/coze-design/icons';
import {
Input,
Table,
Tooltip,
Checkbox,
type CheckboxProps,
} from '@coze-arch/coze-design';
import { type ColumnProps, type TableProps } from '@coze-arch/bot-semi/Table';
import { UIButton, Typography } from '@coze-arch/bot-semi';
import { IconDragOutlined } from '@coze-arch/bot-icons';
import { type DocTableColumn } from '@coze-arch/bot-api/memory';
import { ColumnType } from '@coze-arch/bot-api/knowledge';
import { type IValidateRes, validateField, useOptFromQuery } from '@/utils';
import { type TableItem } from '@/types/table';
import { type SemanticValidateItem } from '@/types';
import styles from './index.module.less';
type Align = 'center' | 'left' | 'right' | undefined;
enum ValidateStatus {
Error = 'error',
Default = 'default',
}
export interface TableStructureProps extends TableProps {
data: Array<
DocTableColumn & {
key?: string;
autofocus?: boolean;
errMsg?: string;
}
>;
setData: (v: Array<DocTableColumn>) => void;
initValid?: boolean;
isBlurValid?: boolean;
isPreview?: boolean;
verifyMap?: SemanticValidateItem;
baseKey?: string;
showTitle?: boolean;
tipsNode?: React.ReactNode;
isDragTable?: boolean;
}
const NAME_MAX_STR_LEN = 30;
const DESC_MAX_STR_LEN = 2000;
const baseClassName = 'table-structure';
const RequiredColRender = ({
children,
tooltip,
dataTestId,
}: {
children: React.ReactNode;
tooltip?: string | React.ReactNode;
dataTestId?: string;
}) => (
<div
className={styles[`${baseClassName}-required-container`]}
data-testid={dataTestId}
>
{children}
<span className={styles[`${baseClassName}-col-required`]}> *</span>
{tooltip}
</div>
);
interface InputRenderProps {
onChange: (v: string) => void;
validate?: (v: string, error: string) => IValidateRes;
record: TableItem & { errMsg?: string };
value: string;
autofocus: boolean;
initValid: boolean;
isBlurValid: boolean;
errorMsg?: string;
isPreview?: boolean;
placeholder?: string;
maxStrLen: number;
}
const InputRender = ({
onChange,
record,
value,
autofocus = false,
isBlurValid = false,
initValid,
validate,
isPreview,
placeholder,
maxStrLen,
}: InputRenderProps) => {
const ValidateResult = (v: string) => {
const validRes = validate?.(
v,
I18n.t('datasets_segment_tableStructure_field_errEmpty'),
);
return {
valid: initValid ? !!(validate ? validRes?.valid : v && v !== '') : true,
errorMsg:
validRes?.errorMsg ||
I18n.t('datasets_segment_tableStructure_field_errEmpty'),
};
};
const [inputValue, setInputValue] = useState(value);
const [validObj, setValidObj] = useState(() => {
const res = ValidateResult(value);
return {
valid: isBlurValid ? true : res.valid,
errorMsg: res.errorMsg,
};
});
const validateValue = (v: string) => {
const validRes = ValidateResult(v);
setValidObj(validRes);
};
const hasDisable = isPreview;
const apiErrorMsg = record?.errMsg || '';
const validateStatus = useMemo(() => {
if (apiErrorMsg) {
return ValidateStatus.Error;
}
return !validObj.valid ? ValidateStatus.Error : ValidateStatus.Default;
}, [validObj, apiErrorMsg]);
const renderErrorMsg = useCallback(() => {
if (apiErrorMsg) {
return <div className={styles['input-error-msg']}>{apiErrorMsg}</div>;
}
return (
<>
{!validObj.valid && (
<div className={styles['input-error-msg']}>{validObj.errorMsg}</div>
)}
</>
);
}, [apiErrorMsg, validObj]);
return (
<>
<Input
autoFocus={autofocus}
value={inputValue}
maxLength={maxStrLen}
onChange={v => {
setInputValue(v.substring(0, maxStrLen));
!isBlurValid && validateValue(v);
}}
disabled={hasDisable}
validateStatus={validateStatus}
suffix={
<span className={styles['input-suffix']}>
{(inputValue || '').length}/{maxStrLen}
</span>
}
onBlur={() => {
onChange(inputValue?.substring(0, maxStrLen) || '');
validateValue(inputValue);
}}
placeholder={placeholder}
/>
{renderErrorMsg()}
</>
);
};
// TODO 待解
// eslint-disable-next-line @coze-arch/max-line-per-function
export const TableStructure: React.FC<TableStructureProps> = ({
data = [],
setData,
verifyMap = {},
initValid = false,
isBlurValid = false,
isPreview = false,
baseKey,
showTitle = false,
children: childrenNode,
tipsNode,
isDragTable = false,
...tableProps
}) => {
const opt = useOptFromQuery();
const isResegment = opt === OptType.RESEGMENT;
const columns: ColumnProps<TableItem>[] = [
{
title: () => (
<RequiredColRender
dataTestId={KnowledgeE2e.TableLocalTableConfigurationIndex}
tooltip={
<Tooltip
className="whitespace-pre-line"
content={I18n.t('knowledge_multi_index')}
>
<UIButton
size="small"
theme="borderless"
type="tertiary"
style={{
marginLeft: 4,
}}
icon={<IconCozInfoCircle className="coz-fg-secondary" />}
/>
</Tooltip>
}
>
<div
className={styles['table-header-tooltip']}
data-testid={KnowledgeE2e.TableLocalTableConfigurationIndex}
>
<span>{I18n.t('knowledge_table_structure_semantic')}</span>
</div>
</RequiredColRender>
),
dataIndex: 'is_semantic',
width: 90,
align: 'left' as Align,
render: (value, record, index: number) => {
const onChange: CheckboxProps['onChange'] = e => {
const newData = [...data];
newData[index].is_semantic = Boolean(e.target.checked);
setData(newData);
};
const { sequence } = record;
function hasFormItemDisable() {
const disabled = Object.keys(verifyMap).length
? !verifyMap[sequence || index]?.valid
: false;
return disabled || isPreview;
}
const hasDisable = hasFormItemDisable();
const Wrapper = ({ children }: { children: JSX.Element }) => {
if (hasDisable && verifyMap[sequence || index]?.msg) {
return (
<Tooltip
trigger="hover"
content={verifyMap[sequence || index]?.msg}
>
{children}
</Tooltip>
);
}
return children;
};
return (
<div className={styles['semantic-radio']}>
{isDragTable ? (
<IconDragOutlined className={'structure-table-drag-icon'} />
) : null}
<Wrapper>
<Checkbox
checked={value}
disabled={hasDisable}
onChange={onChange}
data-testid={KnowledgeE2e.TableStructureIndexCheckbox}
/>
</Wrapper>
</div>
);
},
},
{
title: () => (
<RequiredColRender
dataTestId={KnowledgeE2e.TableLocalTableConfigurationColumnName}
>
{I18n.t('knowledge_table_structure_column_name')}
</RequiredColRender>
),
dataIndex: 'column_name',
align: 'left' as Align,
render: (value, record, index) => {
const { autofocus = false } = record;
const onChange = (v: string) => {
const newData = [...data];
newData[index].column_name = v;
setData(newData);
};
const validateFn = (v: string, emptyMsg: string) => {
if (data?.filter(i => i.column_name === v).length >= 2) {
return {
valid: false,
errorMsg: I18n.t('Manual_crawling_040'),
};
}
return validateField(v, emptyMsg);
};
return (
<div className={styles['column-item']}>
<InputRender
initValid={initValid}
isBlurValid={isBlurValid}
key={`${baseKey}${record?.sequence}`}
onChange={onChange}
record={record}
value={value}
validate={validateFn}
autofocus={autofocus}
isPreview={isPreview}
maxStrLen={NAME_MAX_STR_LEN}
/>
</div>
);
},
},
{
title: () => (
<div data-testid={KnowledgeE2e.TableLocalTableConfigurationDesc}>
{I18n.t('knowledge_table_structure_desc')}
</div>
),
dataIndex: 'desc',
align: 'left' as Align,
render: (value, record, index) => {
const onChange = (v: string) => {
const newData = [...data];
newData[index].desc = v;
setData(newData);
};
return (
<div className={styles['column-item']}>
<InputRender
initValid={false}
isBlurValid={false}
key={`column-desc.${baseKey}${record?.sequence}`}
placeholder={I18n.t('knowledge_variable_description_placeholder')}
onChange={onChange}
record={record}
value={value}
autofocus={false}
isPreview={isPreview}
maxStrLen={DESC_MAX_STR_LEN}
/>
</div>
);
},
},
{
title: () => (
<RequiredColRender
dataTestId={KnowledgeE2e.TableLocalTableConfigurationType}
>
{I18n.t('knowledge_table_structure_data_type')}
</RequiredColRender>
),
dataIndex: 'column_type',
align: 'left' as Align,
render: (value, record, index) => {
const valid = isBlurValid ? true : !!value;
return (
<div
className={`pr-[16px] ${styles['column-item']}`}
key={record.sequence}
>
{isPreview ? (
<Typography.Text className={styles['column-item-value']}>
{getDataTypeText(value)}
</Typography.Text>
) : (
<DataTypeSelect
value={value || ''}
selectProps={{
disabled: !record.is_new_column && isResegment,
optionList: getDataTypeOptions().map(option => {
if (option.value === ColumnType.Image) {
return {
...option,
disabled: record.is_semantic,
};
}
return option;
}),
placeholder: I18n.t('db_table_save_exception_fieldtype'),
}}
errorMsg={
valid
? undefined
: I18n.t(
'datasets_segment_tableStructure_field_type_errEmpty',
)
}
handleChange={v => {
const newData = [...data];
newData[index].column_type = v as ColumnType;
setData(newData);
}}
/>
)}
</div>
);
},
},
{
title: (
<div data-testid={KnowledgeE2e.TableLocalTableConfigurationAction}>
{I18n.t('datasets_unit_upload_field_action')}
</div>
),
dataIndex: 'operate',
width: 82,
align: 'left' as Align,
render: (_text, record, index) => (
<div
className={styles['column-item-action']}
onClick={() => {
setData(data.filter((_, i) => index !== i));
}}
>
<Tooltip
content={I18n.t(
record.is_semantic
? 'datasets_segment_tableStructure_delTips'
: 'datasets_table_title_actions_delete',
)}
>
<IconCozTrashCan
aria-disabled={Boolean(record.is_semantic)}
className={styles['column-item-action-delete']}
/>
</Tooltip>
</div>
),
},
];
// 预览场景下,不需要操作列
if (isPreview) {
columns.pop();
}
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 1 },
}),
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active && over && active.id !== over.id) {
const items = Array.from(data);
const activeIndex = items.findIndex(item => item.key === active.id);
const overIndex = items.findIndex(item => item.key === over.id);
setData(arrayMove(items, activeIndex, overIndex));
}
};
const SortableRow: FC<{
className: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
'data-row-key': string;
style: CSSProperties;
}> = props => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: props['data-row-key'],
});
const style: CSSProperties = {
...props.style,
transform: cssDndKit.Transform.toString(transform),
transition,
cursor: isDragging ? 'grabbing' : 'grab',
...(isDragging
? {
zIndex: 999,
position: 'relative',
background: 'rgba(217, 220, 250, 1)',
}
: {}),
};
return (
<tr
{...props}
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
></tr>
);
};
const renderTableContent = () => {
if (isDragTable) {
return (
<DndContext
// https://docs.dndkit.com/api-documentation/context-provider#autoscroll
autoScroll={true}
sensors={sensors}
modifiers={[restrictToVerticalAxis]}
onDragEnd={handleDragEnd}
>
<SortableContext
items={data.map(item => item.key || '')}
strategy={verticalListSortingStrategy}
>
<Table
wrapperClassName={classNames(
styles[`${baseClassName}-wrapper`],
styles['drag-table'],
)}
key={baseKey}
tableProps={{
sticky: true,
dataSource: data,
columns,
pagination: false,
className: styles[baseClassName],
components: {
body: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
row: SortableRow as any,
},
},
...tableProps,
}}
/>
</SortableContext>
</DndContext>
);
}
return (
<Table
wrapperClassName={styles[`${baseClassName}-wrapper`]}
key={baseKey}
tableProps={{
sticky: true,
dataSource: data,
columns,
pagination: false,
className: styles[baseClassName],
...tableProps,
}}
/>
);
};
return (
<div className={styles['structure-wrapper']}>
{showTitle ? <TableStructureTitle /> : null}
{tipsNode ? tipsNode : null}
{renderTableContent()}
{childrenNode}
</div>
);
};
export const TableStructureTitle = () => (
<div
className={styles['table-structure-bar-title']}
data-testid={KnowledgeE2e.TableLocalTableStructureTitle}
>
<span>{I18n.t('datasets_segment_tableStructure_title')}</span>
<Tooltip content={I18n.t('knowledge_table_structure_column_tooltip')}>
<IconCozInfoCircle
className={classNames(styles.icon, 'coz-fg-secondary')}
/>
</Tooltip>
</div>
);

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 {
TableSettingBar,
type TableSettingBarProps,
} from './components/table-setting-bar';
export {
type TablePreviewProps,
TablePreview,
} from './components/table-preview';
export {
type TableStructureProps,
TableStructure,
TableStructureTitle,
} from './components/table-structure';

View File

@@ -0,0 +1,58 @@
@import '../../assets/common.less';
.embed-progress {
flex: 1;
.progress-info {
width: 100%;
.text {
display: flex;
align-items: center;
height: 32px;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
font-style: normal;
line-height: 20px;
color: var(--coz-fg-primary);
}
.progress-list {
overflow-y: auto;
max-height: 532px;
}
}
.banner {
width: 100%;
}
.progress-success-icon {
.common-svg-icon(16px, var(--semi-color-primary));
}
}
.data-processing {
.finish-text {
overflow: hidden;
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px; /* 142.857% */
color: var(--coz-fg-primary);
text-align: right;
text-overflow: ellipsis;
}
:global {
.process-progress-item-actions {
/* stylelint-disable-next-line declaration-no-important */
display: block !important;
}
}
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { UnitProgress } from './unit-progress';

View File

@@ -0,0 +1,227 @@
/*
* 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 { getFormatTypeFromUnitType } from '@coze-data/utils';
import { useKnowledgeParams } from '@coze-data/knowledge-stores';
import {
type ProgressItem,
CreateUnitStatus,
UnitType,
} from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { DocumentStatus } from '@coze-arch/idl/knowledge';
import { I18n } from '@coze-arch/i18n';
import { formatBytes } from '@coze-arch/bot-utils';
import { getTypeIcon } from '../upload-unit-table/utils';
import { ProcessProgressItem } from '../process-progress-item';
import { getFrequencyMap } from '../../utils';
import { ProcessStatus } from '../../types';
const INIT_PERCENT = 10;
import styles from './index.module.less';
interface UnitProgressProps {
progressList: ProgressItem[];
createStatus: CreateUnitStatus;
}
const OneHundred = 100;
function hoursToDays(hours: number | string) {
const curHours = typeof hours === 'string' ? parseInt(hours) : hours;
if (isNaN(curHours)) {
return 0;
}
return curHours / 24;
}
function formatRemainingTime(remainingSeconds: number) {
const minutes = Math.floor(remainingSeconds / 60);
const seconds = remainingSeconds % 60;
return {
minutes,
seconds,
};
}
const renderSubText = (
status: ProcessStatus,
item: ProgressItem,
hasURLImport?: boolean,
) => {
const statusDesc = item?.statusDesc || I18n.t('datasets_unit_upload_fail');
if (status === ProcessStatus.Failed) {
return (
<div
data-dtestid={`${KnowledgeE2e.CreateUnitListProgressName}.${'subText'}`}
className={'text-12px'}
>
{statusDesc}
</div>
);
}
let subDesc = '';
if (hasURLImport) {
// 更新频率
const updateInterval = hoursToDays(item?.update_interval || 0);
subDesc = getFrequencyMap(updateInterval);
} else {
subDesc = formatBytes((item?.size || 0) as number);
}
return (
<div
data-dtestid={`${KnowledgeE2e.CreateUnitListProgressName}.${item?.name}`}
className={'coz-fg-secondary text-12px'}
>
{subDesc}
</div>
);
};
export const UnitProgress: React.FC<UnitProgressProps> = ({
progressList,
createStatus,
}) => {
const params = useKnowledgeParams();
const headerTitle = useMemo(() => {
let msg: string = I18n.t('datasets_createFileModel_step4_processing');
if (createStatus === CreateUnitStatus.TASK_FINISH) {
const allFailed = progressList.every(
status => status.status === DocumentStatus.Failed,
);
if (allFailed) {
msg = I18n.t('datasets_createFileModel_step4_failed');
} else {
msg = I18n.t('datasets_createFileModel_step4_Finish');
}
}
return msg;
}, [createStatus, progressList]);
const unitType = params.type as UnitType;
const formatType = getFormatTypeFromUnitType(unitType);
const hasURLImport = [
UnitType.TABLE_API,
UnitType.TABLE_FEISHU,
UnitType.TABLE_GOOGLE_DRIVE,
UnitType.TABLE_LARK,
UnitType.TEXT_FEISHU,
UnitType.TEXT_LARK,
UnitType.TEXT_NOTION,
UnitType.TEXT_URL,
UnitType.TEXT_GOOGLE_DRIVE,
].includes(unitType);
const percentFormat = (percent: number, remainingTime: number) => {
const { minutes, seconds } = formatRemainingTime(remainingTime as number);
const remainingTimeText = I18n.t('knowledge_upload_remaining_time_text', {
minutes,
seconds,
});
return percent < OneHundred
? `${percent}% ${
Number(remainingTime) > 0 ? `(${remainingTimeText})` : ''
}`
: null;
};
return (
<div className={styles['embed-progress']}>
<div className={styles['progress-info']}>
<div
className={styles.text}
data-testid={KnowledgeE2e.CreateUnitProgressTitle}
>
{headerTitle}
</div>
<div className={styles['progress-list']}>
{progressList.map(item => {
const { status } = item;
const getProcessStatus = () => {
if (
[DocumentStatus.Failed, DocumentStatus.AuditFailed].includes(
status,
)
) {
return ProcessStatus.Failed;
}
if (
[
DocumentStatus.Processing,
DocumentStatus.Resegment,
DocumentStatus.Refreshing,
].includes(status)
) {
return ProcessStatus.Processing;
}
if (
[DocumentStatus.Enable, DocumentStatus.Disable].includes(status)
) {
return ProcessStatus.Complete;
}
return ProcessStatus.Processing;
};
const curStatus = getProcessStatus();
const percent = item?.progress || INIT_PERCENT;
return (
<ProcessProgressItem
className={styles['data-processing']}
key={item?.documentId}
mainText={item?.name}
subText={renderSubText(curStatus, item, hasURLImport)}
tipText={renderSubText(curStatus, item, hasURLImport)}
status={curStatus}
avatar={getTypeIcon({
type: item?.type,
url: item?.url,
formatType,
})}
percent={percent}
percentFormat={percentFormat(
percent,
item?.remaining_time as number,
)}
actions={[
ProcessStatus.Complete ? (
<div
className={styles['finish-text']}
data-testid={`${
KnowledgeE2e.CreateUnitListProgressSuccessIcon
}.${item?.name || ''}`}
>
{I18n.t('datasets_unit_process_success')}
</div>
) : null,
]}
/>
);
})}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,17 @@
.upload-footer {
display: flex;
justify-content: flex-end;
margin-top: auto;
margin-bottom: 18px;
padding-top: 24px;
padding-bottom: 24px;
line-height: 32px;
:global {
.semi-button {
margin-left: 10px;
}
}
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { UploadFooter } from './upload-footer';

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 { isArray, isObject, get } from 'lodash-es';
import {
FooterBtnStatus,
type FooterControlsProps,
type FooterControlProp,
type FooterBtnProps,
type FooterPrefixType,
} from '@coze-data/knowledge-resource-processor-core';
import { Button, Tooltip } from '@coze-arch/coze-design';
import styles from './index.module.less';
interface UploadFooterProps {
controls: FooterControlsProps;
}
/** 类型断言 入参是不是 按钮数组 */
function isBtnArray(controls: unknown): controls is FooterBtnProps[] {
return !!controls && isArray(controls);
}
function isControlsObject(controls: unknown): controls is FooterControlProp {
return (
!!controls &&
isObject(controls) &&
!!get(controls, 'btns') &&
!!get(controls, 'prefix')
);
}
export const UploadFooter = (props: UploadFooterProps) => {
const { controls } = props;
let btns: FooterBtnProps[] = [];
let prefix: FooterPrefixType;
if (isBtnArray(controls)) {
btns = controls;
}
if (isControlsObject(controls)) {
({ btns, prefix } = controls);
}
return (
<div className={styles['upload-footer']}>
{prefix}
{btns.map(btnItem => {
const isShowHoverContent =
btnItem.disableHoverContent &&
btnItem.status === FooterBtnStatus.DISABLE;
const buttonNode = (
<Button
data-testid={btnItem.e2e}
key={btnItem.text}
disabled={btnItem.status === FooterBtnStatus.DISABLE}
loading={btnItem.status === FooterBtnStatus.LOADING}
color={btnItem.type || 'hgltplus'}
// theme={btnItem.theme || 'solid'}
onClick={btnItem.onClick}
>
{btnItem.text}
</Button>
);
if (!isShowHoverContent) {
return buttonNode;
}
return (
<Tooltip content={btnItem.disableHoverContent}>{buttonNode}</Tooltip>
);
})}
</div>
);
};

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 classNames from 'classnames';
import { getKnowledgeIDEQuery } from '@coze-data/knowledge-common-services';
import { useDataNavigate, useKnowledgeParams } from '@coze-data/knowledge-stores';
import { IconCozArrowLeft } from '@coze-arch/coze-design/icons';
import { IconButton, Typography } from '@coze-arch/coze-design';
interface UploadActionNavbarProps {
title: string;
}
// 上传页面导航栏
export const UploadActionNavbar = ({ title }: UploadActionNavbarProps) => {
const params = useKnowledgeParams();
const resourceNavigate = useDataNavigate();
// TODO: hzf biz的分化在Scene层维护
const fromProject = params.biz === 'project';
const handleBack = () => {
const query = getKnowledgeIDEQuery() as Record<string, string>;
resourceNavigate.toResource?.('knowledge', params.datasetID, query);
};
return (
<div
className={classNames(
'flex items-center justify-between shrink-0 h-[56px] coz-fg-primary',
fromProject ? 'px-[12px]' : '',
)}
>
<div className="flex items-center">
<IconButton
color="secondary"
icon={<IconCozArrowLeft className="text-[16px]" />}
iconPosition="left"
className="!p-[8px]"
onClick={handleBack}
></IconButton>
<Typography.Text fontSize="16px" weight={500} className="ml-[8px]">
{title}
</Typography.Text>
</div>
</div>
);
};

View File

@@ -0,0 +1,112 @@
/*
* 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 { UploadStatus } from '@coze-data/knowledge-resource-processor-core';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { type UploadProps } from '@coze-arch/bot-semi/Upload';
import { Toast } from '@coze-arch/coze-design';
import { getFileExtension, getUint8Array } from '../../utils';
import { UNIT_MAX_MB, PDF_MAX_PAGES } from '../../constants';
export const getBeforeUpload: (params: {
maxSizeMB: UploadProps['maxSize'];
}) => UploadProps['beforeUpload'] =
({ maxSizeMB }) =>
async fileInfo => {
// 不通过 maxSize 属性来限制的原因是
// 只有 beforeUpload 钩子能改 validateMessage
const res = {
fileInstance: fileInfo.file.fileInstance,
status: fileInfo.file.status,
validateMessage: fileInfo.file.validateMessage,
shouldUpload: true,
autoRemove: false,
};
const { fileInstance } = fileInfo.file;
if (!fileInstance) {
return {
...res,
status: UploadStatus.UPLOAD_FAIL,
shouldUpload: false,
};
}
const resultMaxSizeMB = maxSizeMB || UNIT_MAX_MB;
const maxSize = resultMaxSizeMB * 1024 * 1024;
if (fileInstance.size > maxSize) {
Toast.warning({
showClose: false,
content: I18n.t('file_too_large', {
max_size: `${resultMaxSizeMB}MB`,
}),
});
return {
...res,
shouldUpload: false,
status: UploadStatus.VALIDATE_FAIL,
validateMessage: I18n.t('file_too_large', {
max_size: `${resultMaxSizeMB}MB`,
}),
};
}
if (getFileExtension(fileInstance.name).toLowerCase() === 'pdf') {
try {
// TODO: 后续其他位置的 pdfjs 调用也都应该改成异步加载
const pdfjs = await import('@coze-arch/pdfjs-shadow');
const { getDocument, initPdfJsWorker } = pdfjs;
initPdfJsWorker();
const uint8Array = await getUint8Array(fileInstance);
const pdfDocument = await getDocument({ data: uint8Array }).promise;
if (pdfDocument.numPages > PDF_MAX_PAGES) {
Toast.warning({
showClose: false,
content: I18n.t('atasets_createpdf_over250'),
});
return {
shouldUpload: false,
status: UploadStatus.VALIDATE_FAIL,
};
}
} catch (e) {
const error = e as Error;
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
eventName: REPORT_EVENTS.KnowledgeParseFile,
error,
});
if (error?.name === 'PasswordException') {
Toast.error({
showClose: false,
content: I18n.t('pdf_encrypted'),
});
return {
shouldUpload: false,
status: UploadStatus.VALIDATE_FAIL,
};
}
}
}
return res;
};

View File

@@ -0,0 +1,93 @@
/*
* 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 { UploadStatus } from '@coze-data/knowledge-resource-processor-core';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { type UploadProps } from '@coze-arch/bot-semi/Upload';
import { CustomError } from '@coze-arch/bot-error';
import { FileBizType } from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import { getBase64, getFileExtension } from '../../utils';
export const customRequest: UploadProps['customRequest'] = async options => {
const { onSuccess, onError, onProgress, file } = options;
try {
// 业务
const { name, fileInstance } = file;
if (fileInstance) {
const extension = getFileExtension(name);
const base64 = await getBase64(fileInstance);
const result = await DeveloperApi.UploadFile(
{
file_head: {
file_type: extension,
biz_type: FileBizType.BIZ_BOT_DATASET,
},
data: base64,
},
{
onUploadProgress: e => {
const status = file?.status;
const response = file?.response;
// 成功或失败、检验失败后,或者有返回接口数据,不再更新进度条
if (
status === UploadStatus.SUCCESS ||
status === UploadStatus.UPLOAD_FAIL ||
status === UploadStatus.VALIDATE_FAIL ||
response?.upload_url
) {
return;
}
const { total, loaded } = e;
if (total !== undefined && loaded < total) {
onProgress({
total: e.total ?? fileInstance.size,
loaded: e.loaded,
});
}
},
},
);
onSuccess(result.data);
} else {
onError({
status: 0,
});
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
eventName: REPORT_EVENTS.KnowledgeUploadFile,
error: new CustomError(
REPORT_EVENTS.KnowledgeUploadFile,
`${REPORT_EVENTS.KnowledgeUploadFile}: Failed to upload file`,
),
});
}
} catch (e) {
const error = e as Error;
onError({
status: 0,
});
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
eventName: REPORT_EVENTS.KnowledgeUploadFile,
error,
});
}
};

View File

@@ -0,0 +1,57 @@
.upload {
width: 100%;
:global {
.semi-upload-drag-area {
height: 202px;
background-color: var(--coz-mg-card);
border-radius: 8px;
&:hover,
&.semi-upload-drag-area-legal {
border-radius: var(--default, 8px);
}
}
.semi-upload-drag-area-sub-text {
color: var(--coz-fg-dim);
}
.semi-button-with-icon-only {
border-radius: 4px;
}
.semi-upload-file-list {
display: none;
}
}
}
.upload-icon {
width: 32px;
height: 32px;
}
.create-enough-file {
cursor: not-allowed;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.picture {
width: 122px;
height: 122px;
margin-bottom: 14px;
}
.text {
font-size: 12px;
line-height: 16px;
color: rgb(28 31 35 / 60%);
}
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { UploadUnitFile } from './upload-unit-file';

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 { type FC } from 'react';
import { type RenderFileItemProps } from '@coze-arch/bot-semi/Upload';
import {
IconPDFFile,
IconUnknowFile as IconUnknownFile,
IconTextFile,
IconDocxFile,
} from '@coze-arch/bot-icons';
import { getFileExtension } from '../../utils/common';
import { type FileType } from './types';
export const PreviewFile: FC<RenderFileItemProps> = props => {
const type = (getFileExtension(props.name) || 'unknown') as FileType;
const components: Record<FileType, React.FC> = {
unknown: IconUnknownFile,
pdf: IconPDFFile,
text: IconTextFile,
docx: IconDocxFile,
};
const ComponentToRender = components[type] || IconUnknownFile;
return <ComponentToRender />;
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type FileType = 'unknown' | 'pdf' | 'docx' | 'text';

View File

@@ -0,0 +1,130 @@
/*
* 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, useCallback, useMemo, type FC } from 'react';
import classNames from 'classnames';
import { IllustrationSuccess } from '@douyinfe/semi-illustrations';
import { abortable, useUnmountSignal } from '@coze-data/utils';
import { type UnitItem } from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import {
type FileItem,
type UploadProps,
type OnChangeProps,
} from '@coze-arch/bot-semi/Upload';
import { IconCozUpload } from '@coze-arch/coze-design/icons';
import { Toast, Upload } from '@coze-arch/coze-design';
import { UNIT_MAX_MB } from '../../constants';
import {
filterFileListByUnitList,
filterFileList,
filterUnitList,
} from './utils';
import { PreviewFile } from './preview-file';
import { customRequest } from './custom-request';
import { getBeforeUpload } from './before-upload';
import styles from './index.module.less';
interface UploadUnitFileProps extends UploadProps {
unitList: UnitItem[];
onFinish: (unitList: UnitItem[]) => void;
limit: number;
accept: string;
setUnitList: (unitList: UnitItem[]) => void;
showIllustration?: boolean;
maxSizeMB?: number;
}
export const UploadUnitFile: FC<UploadUnitFileProps> = props => {
const {
unitList,
onFinish,
setUnitList,
showIllustration = true,
multiple = true,
maxSizeMB = UNIT_MAX_MB,
...uploadProps
} = props;
const { limit } = uploadProps;
const [fileList, setFileList] = useState<FileItem[]>([]);
useEffect(() => {
if (unitList.length < fileList.length) {
setFileList(filterFileListByUnitList(fileList, unitList));
}
}, [unitList.length]);
const handleAcceptInvalid = useCallback(() => {
Toast.warning({
showClose: false,
content: I18n.t('knowledge_upload_format_error'),
});
}, []);
const signal = useUnmountSignal();
const handleUploadProcess = abortable((data: OnChangeProps) => {
setFileList(data.fileList);
setUnitList(filterFileList(data.fileList));
}, signal);
const handleUploadSuccess = abortable(() => {
onFinish(filterUnitList(unitList, fileList));
}, signal);
const uploadDisabled = useMemo(
() => unitList.length >= limit,
[unitList, limit],
);
const beforeUpload = getBeforeUpload({ maxSizeMB });
return (
<Upload
draggable
data-testid={KnowledgeE2e.UploadUnitFile}
multiple={multiple}
fileList={fileList}
disabled={uploadDisabled}
previewFile={PreviewFile}
onAcceptInvalid={handleAcceptInvalid}
beforeUpload={beforeUpload}
customRequest={customRequest}
onChange={handleUploadProcess}
onSuccess={handleUploadSuccess}
dragIcon={<IconCozUpload className={styles['upload-icon']} />}
{...uploadProps}
className={classNames(styles.upload, uploadProps.className)}
>
{unitList.length >= limit && showIllustration ? (
<div
className={styles['create-enough-file']}
onClick={e => e.stopPropagation()}
>
<IllustrationSuccess className={styles.picture} />
<div className={styles.text}>
{I18n.t('knowledge_1218_001', {
MaxDocs: limit,
})}
</div>
</div>
) : null}
</Upload>
);
};

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 {
UploadStatus,
type UnitItem,
} from '@coze-data/knowledge-resource-processor-core';
import { KNOWLEDGE_UNIT_NAME_MAX_LEN } from '@coze-data/knowledge-modal-base';
import { type FileItem } from '@coze-arch/bot-semi/Upload';
import { getFileExtension } from '../../utils/common';
interface GetFileListMapRes {
[key: string]: FileItem;
}
const getFileName = (uri: string) => uri.substring(0, uri.lastIndexOf('.'));
const getNameFromFileList = (unitList: FileItem[], index: number) =>
unitList?.[index]?.name;
const getFileListMap = (fileList: FileItem[]): GetFileListMapRes =>
fileList.reduce((acc: GetFileListMapRes, item) => {
acc[item.uid || ''] = item;
return acc;
}, {});
/** 过滤 fileList仅保留在 unitList 中的文件,即上传成功的文件 */
export const filterFileListByUnitList = (
fileList: FileItem[],
unitList: UnitItem[],
): FileItem[] =>
fileList.filter(fileItem =>
unitList?.find(unitItem => unitItem.uri === fileItem?.response?.upload_uri),
);
const fileItem2UnitItem = (
file: FileItem,
config?: { filename?: string },
): UnitItem => ({
type: getFileExtension(file?.response?.upload_uri || file.name),
uri: file?.response?.upload_uri,
url: file?.response?.upload_url,
name: (config?.filename ?? getFileName(file.name)).slice(
0,
KNOWLEDGE_UNIT_NAME_MAX_LEN,
),
size: file.size,
status: file.status as UnitItem['status'],
percent: file.percent || 0,
fileInstance: file.fileInstance,
uid: file.uid,
validateMessage: (file.validateMessage as string) || '',
});
export const filterFileList = (fileList: FileItem[]): UnitItem[] => {
const filteredList: UnitItem[] = fileList
.filter(
item =>
!(!item.shouldUpload && item.status === UploadStatus.VALIDATE_FAIL),
)
.map((file, index) => {
const filename = getNameFromFileList(fileList, index);
return fileItem2UnitItem(file, { filename });
});
return filteredList;
};
export const filterUnitList = (
unitList: UnitItem[],
fileList: FileItem[],
): UnitItem[] => {
const fileListMap = getFileListMap(fileList);
return unitList
.filter(unit => {
const file = fileListMap[unit.uid || ''];
if (!file.shouldUpload && file.status === UploadStatus.VALIDATE_FAIL) {
return false;
}
return true;
})
.map((unit, index) => {
const file = fileListMap[unit.uid || ''];
const filename = getNameFromFileList(fileList, index);
return {
...unit,
...fileItem2UnitItem(file, { filename }),
};
});
};

View File

@@ -0,0 +1,129 @@
/*
* 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 UnitItem } from '@coze-data/knowledge-resource-processor-core';
import { useEditUnitNameModal } from '@coze-data/knowledge-modal-base';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import {
IconCozEdit,
IconCozRefresh,
IconCozTrashCan,
} from '@coze-arch/coze-design/icons';
import { IconButton, Tooltip } from '@coze-arch/coze-design';
import { type RenderColumnsProps } from '../types';
import { getFrequencyMap } from '../../../utils';
export function getFileSizeInfo(record: UnitItem) {
return (
<div
data-dtestid={`${KnowledgeE2e.LocalUploadListFileSize}.${record.name}`}
className={'coz-fg-secondary text-12px'}
>
{record?.size}
</div>
);
}
export function getFrequencyInfo(record: UnitItem) {
return (
<div
data-dtestid={`${KnowledgeE2e.LocalUploadListFrequency}.${record.name}`}
className={'coz-fg-secondary text-12px'}
>
{getFrequencyMap(record.updateInterval || 0)}
</div>
);
}
export function ActionRenderByDelete(props: RenderColumnsProps) {
const { index, record, params } = props;
const { onChange, unitList, onDelete } = params;
const handleDelete = () => {
onChange(unitList.filter((u, i) => index !== i));
if (typeof onDelete === 'function') {
onDelete?.(record, index);
}
};
return (
<Tooltip spacing={12} content={I18n.t('Delete')} position="top">
<IconButton
color="secondary"
icon={<IconCozTrashCan className="text-14px" />}
iconPosition="left"
size="small"
onClick={handleDelete}
></IconButton>
</Tooltip>
);
}
export function ActionRenderByEditName(props: RenderColumnsProps) {
const { index, record, params } = props;
const { onChange, unitList } = params;
const { node, open } = useEditUnitNameModal({
name: record?.name ?? '',
onOk: (name: string) => {
const arr = [...unitList];
arr[index].name = name;
onChange(arr);
},
});
return (
<>
<Tooltip spacing={12} content={I18n.t('Edit')} position="top">
<IconButton
color="secondary"
icon={<IconCozEdit className="text-14px" />}
iconPosition="left"
size="small"
onClick={() => open()}
/>
</Tooltip>
{node}
</>
);
}
export function ActionRenderByRetry(props: RenderColumnsProps) {
const { index, record, params } = props;
if (params.disableRetry) {
return null;
}
const { onRetry } = params;
const handleRetry = () => {
onRetry?.(record, index);
};
return (
<Tooltip
spacing={12}
content={I18n.t('datasets_unit_update_retry')}
position="top"
>
<IconButton
color="secondary"
icon={<IconCozRefresh className="text-14px" />}
iconPosition="left"
size="small"
onClick={handleRetry}
></IconButton>
</Tooltip>
);
}

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.
*/
import { type FC } from 'react';
import { UITableAction } from '@coze-arch/bot-semi';
import { type ActionProps } from '../../types';
export const Action: FC<ActionProps> = ({
onDelete,
showEdit,
deleteProps,
editProps,
}: ActionProps) => (
<UITableAction
deleteProps={{
disabled: false,
handleClick: onDelete,
...deleteProps,
}}
editProps={{
hide: !showEdit,
...editProps,
}}
/>
);

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Action } from './action';

View File

@@ -0,0 +1,19 @@
/*
* 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 { UploadStatusComp } from './upload-status';
export { UnitName } from './unit-name';
export { Action } from './action-render';

View File

@@ -0,0 +1,44 @@
.unit-name-wrap {
display: flex;
align-items: center;
margin-left: 3px;
:global {
.semi-icon {
margin-right: 12px;
}
.view-name {
display: inline-block;
height: 32px;
font-size: 14px;
line-height: 32px;
}
.input-suffix {
display: inline-block;
margin-right: 5px;
font-size: 12px;
color: rgb(28 29 35 / 35%);
}
.unit-name-input {
width: 100%;
.error {
position: absolute;
color: rgb(249 57 32 / 100%);
}
}
.unit-name-error {
display: flex;
flex-direction: column;
.error {
color: rgb(249 57 32 / 100%);
}
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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';
import { KNOWLEDGE_UNIT_NAME_MAX_LEN } from '@coze-data/knowledge-modal-base';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { UIInput } from '@coze-arch/bot-semi';
import { getTypeIcon } from '../../utils';
import { type UnitNameProps } from '../../types';
import styles from './index.module.less';
export const UnitName: React.FC<UnitNameProps> = ({
edit,
onChange,
disabled,
record,
formatType,
}) => {
const { type, name, validateMessage } = record;
const [value, setValue] = useState(name); // 需要用自身state否则出现无法输入中文的bug
const getValidateMessage = (val: string) =>
!val ? I18n.t('datasets_unit_exception_name_empty') : validateMessage;
useEffect(() => {
setValue(name);
}, [name]);
return (
<div
className={styles['unit-name-wrap']}
data-testid={`${KnowledgeE2e.FeishuUploadListName}.${name}`}
>
{getTypeIcon({ type, formatType })}
{edit ? (
<div className="unit-name-input">
<UIInput
disabled={disabled}
value={value}
onChange={val => {
setValue(val);
onChange(val);
}}
maxLength={KNOWLEDGE_UNIT_NAME_MAX_LEN}
validateStatus={!name ? 'error' : 'default'}
suffix={
<span className="input-suffix">
{(name || '').length}/{KNOWLEDGE_UNIT_NAME_MAX_LEN}
</span>
}
/>
<div className="error">{getValidateMessage(name)}</div>
</div>
) : (
<div className="unit-name-error">
<span className="view-name">{name}</span>
{validateMessage ? (
<div className="error">{validateMessage}</div>
) : null}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,108 @@
/*
* 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, type FC } from 'react';
import { KNOWLEDGE_UNIT_NAME_MAX_LEN } from '@coze-data/knowledge-modal-base';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { UIInput } from '@coze-arch/bot-semi';
import { FormatType } from '@coze-arch/bot-api/memory';
import { validateField } from '@/utils';
import { getTypeIcon } from '../../utils';
import { type UnitNameProps } from '../../types';
import styles from './index.module.less';
export const UnitName: FC<UnitNameProps> = ({
edit,
onChange,
disabled,
record,
formatType,
canValidator = true,
inModal = false,
}) => {
const { type, name, validateMessage, dynamicErrorMessage } = record;
const [value, setValue] = useState(name); // 需要用自身state否则出现无法输入中文的bug
const [validData, setValidData] = useState({ valid: true, errorMsg: '' });
const getValidateMessage = (val: string) =>
!val ? I18n.t('datasets_unit_exception_name_empty') : validateMessage;
const validator = (val: string) => {
const validObj = validateField(val, getValidateMessage(name));
setValidData(
formatType === FormatType.Table
? validObj
: {
valid: !!name,
errorMsg: '',
},
);
};
useEffect(() => {
setValue(name);
canValidator && !disabled && validator(name);
}, [name, disabled]);
return (
<div className={styles['unit-name-wrap']}>
{getTypeIcon({ type, formatType, url: record.url, inModal })}
{edit ? (
<div className="unit-name-input">
<UIInput
data-dtestid={`${KnowledgeE2e.LocalUploadListName}.${record.name}`}
disabled={disabled}
autoFocus={true}
value={value}
onChange={val => {
setValue(val);
onChange(val);
}}
onBlur={() => {
canValidator && validator(name);
}}
maxLength={KNOWLEDGE_UNIT_NAME_MAX_LEN}
validateStatus={
!validData.valid || dynamicErrorMessage ? 'error' : 'default'
}
suffix={
<span className="input-suffix">
{(name || '').length}/{KNOWLEDGE_UNIT_NAME_MAX_LEN}
</span>
}
/>
<div className="error">
{!disabled &&
(validData.errorMsg ||
getValidateMessage(name) ||
dynamicErrorMessage)}
</div>
</div>
) : (
<span
className="view-name"
data-dtestid={`${KnowledgeE2e.LocalUploadListNameView}.${record.name}`}
>
{name}
</span>
)}
</div>
);
};

View File

@@ -0,0 +1,44 @@
.upload-status-wrap {
display: flex;
align-items: center;
line-height: 22px;
:global {
.semi-icon {
float: left;
margin: 0 5px -3px 0;
}
.semi-progress-horizontal {
margin-top: 2px;
}
}
}
.retry {
cursor: pointer;
:global {
.semi-icon {
float: left;
margin: 3px 3px 0 0;
}
}
}
.retry-text {
margin-left: 3px;
color: rgb(77 83 232 / 100%);
}
.disabled-retry {
.retry-text {
cursor: not-allowed;
color: #ccc;
}
}
.no-retry-text {
cursor: not-allowed;
}

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.
*/
/* eslint-disable complexity */
import classNames from 'classnames';
import {
UploadStatus,
EntityStatus,
} from '@coze-data/knowledge-resource-processor-core';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { Tooltip, Progress, Spin } from '@coze-arch/bot-semi';
import {
IconUploadFileSuccess,
IconUploadFileFail,
} from '@coze-arch/bot-icons';
import { WebStatus } from '@coze-arch/bot-api/knowledge';
import { type UploadStateProps } from '../../types';
import styles from './index.module.less';
export const UploadStatusComp: React.FC<UploadStateProps> = ({
record,
onRetry,
index,
needLoading,
overlayClassName,
disableRetry,
noRetry,
}) => {
const { status } = record;
if (
status === UploadStatus.UPLOADING ||
status === UploadStatus.VALIDATING ||
status === UploadStatus.WAIT ||
status === WebStatus.Handling ||
status === EntityStatus.EntityStatusProcess
) {
return (
<span
data-dtestid={`${KnowledgeE2e.LocalUploadListStatus}.${record.name}`}
className={classNames(styles['upload-status-wrap'], overlayClassName)}
>
<span>{I18n.t('datasets_unit_upload_state')}</span>
{needLoading ? (
<Spin spinning={true} />
) : (
<Progress percent={record.percent} />
)}
</span>
);
}
if (
status === UploadStatus.SUCCESS ||
status === WebStatus.Finish ||
status === EntityStatus.EntityStatusSuccess
) {
return (
<span
className={styles['upload-status-wrap']}
data-dtestid={`${KnowledgeE2e.LocalUploadListStatus}.${record.name}`}
>
<IconUploadFileSuccess />
<span>{I18n.t('datasets_unit_upload_success')}</span>
</span>
);
}
if (status === UploadStatus.VALIDATE_FAIL) {
return (
<span
className={styles['upload-status-wrap']}
data-dtestid={`${KnowledgeE2e.LocalUploadListStatus}.${record.name}`}
>
<IconUploadFileFail />
</span>
);
}
if (
status === UploadStatus.UPLOAD_FAIL ||
status === WebStatus.Failed ||
status === EntityStatus.EntityStatusFail
) {
return (
<div
data-dtestid={`${KnowledgeE2e.LocalUploadListStatus}.${record.name}`}
className={classNames(
`${styles['upload-status-wrap']} ${styles.retry}`,
overlayClassName,
disableRetry ? styles['disabled-retry'] : '',
noRetry ? styles['no-retry-text'] : '',
)}
onClick={() => {
!disableRetry && !noRetry && onRetry && onRetry(record, index);
}}
>
{!record.statusDescript ? (
<IconUploadFileFail />
) : (
<Tooltip content={record.statusDescript} trigger="hover">
<IconUploadFileFail />
</Tooltip>
)}
{!noRetry && (
<div className={classNames(styles['retry-text'])}>
{I18n.t('datasets_unit_update_retry')}
</div>
)}
</div>
);
}
return null;
};

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 { UploadUnitTable } from './upload-unit-table';
export { type RenderColumnsProps } from './types';
export {
ActionRenderByDelete,
ActionRenderByEditName,
ActionRenderByRetry,
getFrequencyInfo,
getFileSizeInfo,
} from './actions';
export { getProcessStatus } from './utils';
export { type ColumnInfo, type UploadUnitTableProps } from './types';

View File

@@ -0,0 +1,95 @@
/*
* 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,
type UnitItem,
} from '@coze-data/knowledge-resource-processor-core';
import { type UIActionItemProps } from '@coze-arch/bot-semi';
import { type FormatType } from '@coze-arch/bot-api/memory';
export interface UploadStateProps {
record: UnitItem;
onRetry?: (record: UnitItem, index: number) => void;
index: number;
needLoading?: boolean;
overlayClassName?: string;
disableRetry?: boolean;
// 不显示重试文案
noRetry?: boolean;
}
export interface UnitNameProps {
canValidator?: boolean;
edit?: boolean;
disabled?: boolean;
inModal?: boolean;
formatType: FormatType;
onChange: (value: string) => void;
record: UnitItem;
}
export interface ActionProps {
showEdit?: boolean;
onDelete: () => void;
deleteProps?: UIActionItemProps;
editProps?: UIActionItemProps;
record?: UnitItem;
}
export enum ActionType {
Edit = 'edit',
Delete = 'delete',
}
export type HandleChange = (
unitList: UnitItem[],
action?: {
type: ActionType;
index: number;
},
) => void;
export interface ColumnInfo {
subText?: ReactNode;
actions?: ReactNode[];
formatType?: FormatType;
}
export interface UploadUnitTableProps {
type: UnitType;
unitList: UnitItem[];
onChange: HandleChange;
edit: boolean;
canValidator?: boolean;
onRetry?: (record: UnitItem, index: number) => void;
disableRetry?: boolean;
onDelete?: (record: UnitItem, index: number) => void;
showEdit?: boolean;
inModal?: boolean;
getColumns?: (record: UnitItem, index: number) => ColumnInfo;
}
export interface GetColumnsParams extends UploadUnitTableProps {
formatType: FormatType;
}
export interface RenderColumnsProps {
params: UploadUnitTableProps;
record: UnitItem;
index: number;
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC, type ReactNode } from 'react';
import { KnowledgeE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { ProcessProgressItem } from '../process-progress-item/process-progress-item';
import { ProcessStatus } from '../../types';
import { getProcessStatus, getTypeIcon } from './utils';
import { type ColumnInfo, type UploadUnitTableProps } from './types';
const INIT_PERCENT = 10;
const renderSubText = (
status: ProcessStatus,
statusDesc: string,
subText: ReactNode,
) => {
if (status === ProcessStatus.Failed) {
return (
<div
data-dtestid={`${KnowledgeE2e.CreateUnitListProgressName}.${'subText'}`}
className={'text-12px'}
>
{statusDesc || I18n.t('datasets_unit_upload_fail')}
</div>
);
}
return subText;
};
export const UploadUnitTable: FC<UploadUnitTableProps> = props => {
const { unitList = [], getColumns } = props;
if (unitList.length === 0) {
return null;
}
return (
<div className="upload-container">
{unitList.map((item, index) => {
const curStatus = getProcessStatus(item?.status);
const statusDescript = item?.statusDescript || '';
// 使用getColumns获取每个项目的信息
const columnInfo: ColumnInfo = getColumns
? getColumns(item, index)
: {};
const { subText, actions, formatType } = columnInfo;
return (
<ProcessProgressItem
key={item.uid}
mainText={item.name || '--'}
subText={renderSubText(curStatus, statusDescript, subText)}
tipText={
<span
data-dtestid={`${KnowledgeE2e.LocalUploadListStatus}.${item.name}`}
>
{curStatus === ProcessStatus.Failed
? statusDescript || I18n.t('datasets_unit_upload_fail')
: I18n.t('datasets_unit_upload_success')}
</span>
}
status={curStatus}
avatar={getTypeIcon({ ...item, formatType })}
actions={actions}
percent={item.percent || INIT_PERCENT}
/>
);
})}
</div>
);
};

View File

@@ -0,0 +1,138 @@
/*
* 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 {
EntityStatus,
UploadStatus,
} from '@coze-data/knowledge-resource-processor-core';
import { WebStatus } from '@coze-arch/idl/knowledge';
import { Image } from '@coze-arch/bot-semi';
import {
IconUploadPDF,
IconUploadCSV,
IconUploadDoc,
IconUploadTxt,
IconUploadTableUrl,
IconUploadXLS,
IconUploadTextUrl,
IconUploadMD,
} from '@coze-arch/bot-icons';
import { FormatType } from '@coze-arch/bot-api/knowledge';
import { ProcessStatus } from '../../types';
enum UploadType {
PDF = 'pdf',
DOCX = 'docx',
TXT = 'txt',
XLSX = 'xlsx',
XLTX = 'xltx',
CSV = 'csv',
PNG = 'png',
JPG = 'jpg',
JPEG = 'jpeg',
WEBP = 'webp',
XLS = 'xls',
MD = 'md',
}
export const getTypeIcon = (params: {
type: string | undefined;
formatType?: FormatType;
url?: string;
inModal?: boolean;
}) => {
const { type, formatType, url } = params;
// const iconClassName = inModal ? styles['icon-size-24'] : styles.icon;
if (
formatType === FormatType.Image &&
[UploadType.JPG, UploadType.JPEG, UploadType.PNG, UploadType.WEBP].includes(
type as UploadType,
)
) {
return (
<Image
src={url}
width={24}
height={24}
style={{
borderRadius: '4px',
marginRight: '12px',
flexShrink: 0,
}}
/>
);
}
if (type === UploadType.MD) {
return <IconUploadMD />;
}
if (type === UploadType.PDF) {
return <IconUploadPDF />;
}
if (type === UploadType.DOCX) {
return <IconUploadDoc />;
}
if (type === UploadType.TXT) {
return <IconUploadTxt />;
}
if (
type === UploadType.XLSX ||
type === UploadType.XLTX ||
type === UploadType.XLS
) {
return <IconUploadXLS />;
}
if (type === UploadType.CSV) {
return <IconUploadCSV />;
}
return formatType === FormatType.Table ? (
<IconUploadTableUrl />
) : (
<IconUploadTextUrl />
);
};
export const getProcessStatus = (
status: UploadStatus | WebStatus | EntityStatus,
) => {
if (
status === UploadStatus.UPLOADING ||
status === UploadStatus.VALIDATING ||
status === UploadStatus.WAIT ||
status === WebStatus.Handling ||
status === EntityStatus.EntityStatusProcess
) {
return ProcessStatus.Processing;
}
if (
status === UploadStatus.SUCCESS ||
status === WebStatus.Finish ||
status === EntityStatus.EntityStatusSuccess
) {
return ProcessStatus.Complete;
}
if (
status === UploadStatus.VALIDATE_FAIL ||
status === UploadStatus.UPLOAD_FAIL ||
status === WebStatus.Failed ||
status === EntityStatus.EntityStatusFail
) {
return ProcessStatus.Failed;
}
return ProcessStatus.Processing;
};