feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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 DOMPurify from 'dompurify';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { getRenderHtmlContent } from '@/text-knowledge-editor/services/use-case/get-render-editor-content';
|
||||
import { getEditorWordsCls } from '@/text-knowledge-editor/services/inner/get-editor-words-cls';
|
||||
import { getEditorTableClassname } from '@/text-knowledge-editor/services/inner/get-editor-table-cls';
|
||||
import { getEditorImgClassname } from '@/text-knowledge-editor/services/inner/get-editor-img-cls';
|
||||
|
||||
export const DocumentChunkPreview = ({
|
||||
chunk,
|
||||
locateId,
|
||||
}: {
|
||||
chunk: Chunk;
|
||||
locateId: string;
|
||||
}) => (
|
||||
<div
|
||||
id={locateId}
|
||||
className={classNames(
|
||||
// 布局
|
||||
'relative',
|
||||
// 间距
|
||||
'mb-2 p-2',
|
||||
// 文字样式
|
||||
'text-sm leading-5',
|
||||
// 颜色
|
||||
'coz-fg-primary hover:coz-mg-hglt-secondary-hovered coz-mg-secondary',
|
||||
// 边框
|
||||
'border border-solid coz-stroke-primary rounded-lg',
|
||||
// 表格样式
|
||||
getEditorTableClassname(),
|
||||
// 图片样式
|
||||
getEditorImgClassname(),
|
||||
// 换行
|
||||
getEditorWordsCls(),
|
||||
)}
|
||||
>
|
||||
<p
|
||||
// 已使用 DOMPurify 过滤 xss
|
||||
// eslint-disable-next-line risxss/catch-potential-xss-react
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
DOMPurify.sanitize(getRenderHtmlContent(chunk.content ?? ''), {
|
||||
/**
|
||||
* 1. 防止CSS注入攻击
|
||||
* 2. 防止用户误写入style标签,导致全局样式被修改,页面展示异常
|
||||
*/
|
||||
FORBID_TAGS: ['style'],
|
||||
}) ?? '',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 DOMPurify from 'dompurify';
|
||||
import cls from 'classnames';
|
||||
|
||||
export const ImageChunkPreview = ({
|
||||
base64,
|
||||
htmlText,
|
||||
link,
|
||||
caption,
|
||||
locateId,
|
||||
selected,
|
||||
}: {
|
||||
base64?: string;
|
||||
htmlText?: string;
|
||||
link?: string;
|
||||
caption?: string;
|
||||
locateId: string;
|
||||
selected?: boolean;
|
||||
}) => (
|
||||
<div
|
||||
id={locateId}
|
||||
className={cls(
|
||||
'flex items-center flex-col gap-2',
|
||||
'w-full p-2 coz-mg-secondary',
|
||||
'border border-solid coz-stroke-primary rounded-[8px]',
|
||||
selected && '!coz-mg-hglt',
|
||||
)}
|
||||
>
|
||||
{base64 ? (
|
||||
<img
|
||||
src={`data:image/jpeg;base64, ${base64}`}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
) : null}
|
||||
{htmlText ? (
|
||||
<div
|
||||
className="w-full h-full overflow-auto [&>*]:w-full [&>*]:h-full"
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(htmlText) }}
|
||||
/>
|
||||
) : null}
|
||||
{link ? (
|
||||
<div className="coz-fg-primary text-[14px] leading-[20px] font-[400] break-all">
|
||||
{link}
|
||||
</div>
|
||||
) : null}
|
||||
{caption ? (
|
||||
<div className="coz-fg-primary text-[14px] leading-[20px] font-[400] break-all">
|
||||
{caption}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 cls from 'classnames';
|
||||
|
||||
export const TitleChunkPreview = ({
|
||||
title,
|
||||
id,
|
||||
}: {
|
||||
title: string;
|
||||
id: string;
|
||||
}) => (
|
||||
<div
|
||||
id={id}
|
||||
className={cls('w-full text-[14px] font-[500] leading-[20px] coz-fg-plus')}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import mitt from 'mitt';
|
||||
import type { Emitter, Handler, EventType } from 'mitt';
|
||||
|
||||
import { type Chunk } from '../types/chunk';
|
||||
|
||||
// 定义事件名称字面量类型
|
||||
export type EventTypeName =
|
||||
| 'previewContextMenuItemAction'
|
||||
| 'hoverEditBarAction';
|
||||
|
||||
/**
|
||||
* 事件类型定义
|
||||
*/
|
||||
export interface EventTypes extends Record<EventType, unknown> {
|
||||
// 右键菜单相关事件
|
||||
previewContextMenuItemAction: {
|
||||
type: 'add-after' | 'add-before' | 'delete' | 'edit';
|
||||
targetChunk: Chunk;
|
||||
newChunk?: Chunk;
|
||||
chunks?: Chunk[];
|
||||
};
|
||||
|
||||
// 悬浮编辑栏相关事件
|
||||
hoverEditBarAction: {
|
||||
type: 'add-after' | 'add-before' | 'delete' | 'edit';
|
||||
targetChunk: Chunk;
|
||||
newChunk?: Chunk;
|
||||
chunks?: Chunk[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件处理函数类型
|
||||
*/
|
||||
export type EventHandler<T extends EventTypeName> = Handler<EventTypes[T]>;
|
||||
|
||||
/**
|
||||
* 创建事件总线实例
|
||||
*/
|
||||
export const createEventBus = (): Emitter<EventTypes> => mitt<EventTypes>();
|
||||
|
||||
/**
|
||||
* 全局事件总线实例
|
||||
*/
|
||||
export const eventBus = createEventBus();
|
||||
|
||||
/**
|
||||
* 事件总线钩子
|
||||
* 用于在组件中使用事件总线
|
||||
*/
|
||||
export const useEventBus = () => eventBus;
|
||||
|
||||
/**
|
||||
* 监听事件钩子
|
||||
* 用于在组件中监听事件
|
||||
* @param eventName 事件名称
|
||||
* @param handler 事件处理函数
|
||||
* @param deps 依赖数组,当依赖变化时重新绑定事件
|
||||
*/
|
||||
export const useEventListener = <T extends EventTypeName>(
|
||||
eventName: T,
|
||||
handler: EventHandler<T>,
|
||||
deps: React.DependencyList = [],
|
||||
) => {
|
||||
useEffect(() => {
|
||||
// 绑定事件
|
||||
eventBus.on(eventName, handler as Handler<unknown>);
|
||||
|
||||
// 组件卸载时解绑事件
|
||||
return () => {
|
||||
eventBus.off(eventName, handler as Handler<unknown>);
|
||||
};
|
||||
}, deps);
|
||||
};
|
||||
@@ -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 {
|
||||
UploadImageMenu,
|
||||
UploadImageButton,
|
||||
BaseUploadImage,
|
||||
UploadImageIcon,
|
||||
} from './upload-image';
|
||||
export {
|
||||
createEditorActionFeatureRegistry,
|
||||
EditorActionRegistry,
|
||||
} from './registry';
|
||||
export { type EditorActionModule } from './module';
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { type Editor } from '@tiptap/react';
|
||||
|
||||
export interface EditorActionProps {
|
||||
editor: Editor | null;
|
||||
disabled?: boolean;
|
||||
onlyIcon?: boolean;
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
export interface EditorActionModule {
|
||||
Component: React.ComponentType<EditorActionProps>;
|
||||
showTooltip?: boolean;
|
||||
disabled?: boolean;
|
||||
onlyIcon?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FeatureRegistry } from '@coze-data/feature-register';
|
||||
|
||||
import { type EditorActionModule } from './module';
|
||||
|
||||
export type EditorActionFeatureType = 'upload-image';
|
||||
|
||||
export type EditorActionRegistry = FeatureRegistry<
|
||||
EditorActionFeatureType,
|
||||
EditorActionModule
|
||||
>;
|
||||
|
||||
export const createEditorActionFeatureRegistry = (
|
||||
name: string,
|
||||
): EditorActionRegistry =>
|
||||
new FeatureRegistry<EditorActionFeatureType, EditorActionModule>({
|
||||
name,
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { type Editor } from '@tiptap/react';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip, type customRequestArgs } from '@coze-arch/coze-design';
|
||||
|
||||
import { type EditorActionProps } from '../module';
|
||||
import { CustomUpload, handleCustomUploadRequest } from './custom-upload';
|
||||
|
||||
export interface BaseUploadImageProps extends EditorActionProps {
|
||||
editor: Editor | null;
|
||||
renderUI: (props: {
|
||||
disabled?: boolean;
|
||||
showTooltip?: boolean;
|
||||
}) => React.ReactNode;
|
||||
}
|
||||
|
||||
export const BaseUploadImage = ({
|
||||
editor,
|
||||
disabled,
|
||||
showTooltip,
|
||||
renderUI,
|
||||
}: BaseUploadImageProps) => {
|
||||
// 处理图片上传
|
||||
const handleImageUpload = (object: customRequestArgs) => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fileInstance } = object;
|
||||
if (!fileInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
return handleCustomUploadRequest({
|
||||
object,
|
||||
options: {
|
||||
onFinish: (result: { url?: string; tosKey?: string }) => {
|
||||
if (result.url && editor) {
|
||||
// 插入图片到编辑器
|
||||
editor.chain().focus().setImage({ src: result.url }).run();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
const TooltipWrapper = showTooltip ? Tooltip : React.Fragment;
|
||||
|
||||
return (
|
||||
<CustomUpload customRequest={handleImageUpload}>
|
||||
<TooltipWrapper
|
||||
content={I18n.t('knowledge_insert_img_002')}
|
||||
clickToHide
|
||||
autoAdjustOverflow
|
||||
>
|
||||
{renderUI({ disabled, showTooltip })}
|
||||
</TooltipWrapper>
|
||||
</CustomUpload>
|
||||
);
|
||||
};
|
||||
@@ -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 { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozImage } from '@coze-arch/coze-design/icons';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
|
||||
import { BaseUploadImage, type BaseUploadImageProps } from './base';
|
||||
|
||||
export const UploadImageButton = (
|
||||
props: Omit<BaseUploadImageProps, 'renderUI'>,
|
||||
) => (
|
||||
<BaseUploadImage
|
||||
{...props}
|
||||
renderUI={({ disabled }) => (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
color="primary"
|
||||
className="coz-fg-primary leading-none"
|
||||
icon={<IconCozImage className="text-[14px]" />}
|
||||
>
|
||||
{I18n.t('knowledge_insert_img_002')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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 { DataNamespace, dataReporter } from '@coze-data/reporter';
|
||||
import { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Upload, Toast, type customRequestArgs } from '@coze-arch/coze-design';
|
||||
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,
|
||||
isValidSize,
|
||||
} from '@/text-knowledge-editor/utils/upload';
|
||||
|
||||
interface CustomUploadProps {
|
||||
customRequest: UploadProps['customRequest'];
|
||||
}
|
||||
|
||||
export const CustomUpload: React.FC<PropsWithChildren<CustomUploadProps>> = ({
|
||||
customRequest,
|
||||
children,
|
||||
}) => (
|
||||
<Upload
|
||||
accept="image/*"
|
||||
maxSize={20480}
|
||||
fileList={[]}
|
||||
customRequest={customRequest}
|
||||
onChange={fileItem => {
|
||||
const { currentFile } = fileItem;
|
||||
if (currentFile) {
|
||||
const isValid = isValidSize(currentFile?.fileInstance?.size || 0);
|
||||
if (!isValid) {
|
||||
Toast.error(I18n.t('knowledge_insert_img_013'));
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Upload>
|
||||
);
|
||||
|
||||
export interface CustomRequestParams {
|
||||
object: customRequestArgs;
|
||||
options: {
|
||||
onFinish?: (result: { url?: string; tosKey?: string }) => void;
|
||||
onFinally?: () => void;
|
||||
onBeforeUpload?: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const handleCustomUploadRequest = async ({
|
||||
object,
|
||||
options,
|
||||
}: CustomRequestParams) => {
|
||||
const { onSuccess, onProgress, file } = object;
|
||||
const { onFinish, onFinally, onBeforeUpload } = options;
|
||||
|
||||
if (typeof file === 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 业务逻辑
|
||||
onBeforeUpload?.();
|
||||
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 => {
|
||||
onProgress({
|
||||
total: e.total ?? fileInstance.size,
|
||||
loaded: e.loaded,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
onSuccess(result.data);
|
||||
if (result.data) {
|
||||
onFinish?.({
|
||||
url: result.data.upload_url,
|
||||
tosKey: result.data.upload_uri,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
|
||||
eventName: ReportEventNames.KnowledgeUploadFile,
|
||||
error: new CustomError(
|
||||
ReportEventNames.KnowledgeUploadFile,
|
||||
`${ReportEventNames.KnowledgeUploadFile}: Failed to upload image`,
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
|
||||
eventName: ReportEventNames.KnowledgeUploadFile,
|
||||
error: error as Error,
|
||||
});
|
||||
} finally {
|
||||
onFinally?.();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IconCozImage } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton } from '@coze-arch/coze-design';
|
||||
|
||||
import { BaseUploadImage, type BaseUploadImageProps } from './base';
|
||||
|
||||
export const UploadImageIcon = (
|
||||
props: Omit<BaseUploadImageProps, 'renderUI'>,
|
||||
) => (
|
||||
<BaseUploadImage
|
||||
{...props}
|
||||
renderUI={({ disabled }) => (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
color="secondary"
|
||||
iconPosition="left"
|
||||
className="coz-fg-secondary leading-none !w-6 !h-6"
|
||||
icon={<IconCozImage className="text-[14px]" />}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { UploadImageIcon } from './icon-action';
|
||||
export { UploadImageButton } from './button-action';
|
||||
export { UploadImageMenu } from './menu-action';
|
||||
export { BaseUploadImage } from './base';
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozImage } from '@coze-arch/coze-design/icons';
|
||||
import { Menu } from '@coze-arch/coze-design';
|
||||
|
||||
import { BaseUploadImage, type BaseUploadImageProps } from './base';
|
||||
|
||||
export const UploadImageMenu = (
|
||||
props: Omit<BaseUploadImageProps, 'renderUI'>,
|
||||
) => (
|
||||
<BaseUploadImage
|
||||
{...props}
|
||||
renderUI={({ disabled }) => (
|
||||
<Menu.Item
|
||||
disabled={disabled}
|
||||
icon={
|
||||
<IconCozImage
|
||||
className={classNames('w-3.5 h-3.5', {
|
||||
'opacity-30': disabled,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
className={classNames('h-8 p-2 text-xs rounded-lg', {
|
||||
'cursor-not-allowed': disabled,
|
||||
})}
|
||||
>
|
||||
{I18n.t('knowledge_insert_img_002')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { type Editor } from '@tiptap/react';
|
||||
import { Menu } from '@coze-arch/coze-design';
|
||||
|
||||
import { type EditorActionRegistry } from '@/text-knowledge-editor/features/editor-actions/registry';
|
||||
interface EditorContextMenuProps {
|
||||
x: number;
|
||||
y: number;
|
||||
editor: Editor | null;
|
||||
readonly?: boolean;
|
||||
contextMenuRef: React.RefObject<HTMLDivElement>;
|
||||
editorActionRegistry: EditorActionRegistry;
|
||||
}
|
||||
|
||||
export const EditorContextMenu: React.FC<EditorContextMenuProps> = props => {
|
||||
const { editorActionRegistry, readonly, contextMenuRef, x, y, editor } =
|
||||
props;
|
||||
|
||||
if (readonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className="absolute bg-white shadow-lg rounded-md py-1 z-50"
|
||||
style={{
|
||||
top: `${y}px`,
|
||||
left: `${x}px`,
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
visible
|
||||
clickToHide
|
||||
keepDOM
|
||||
position="bottomLeft"
|
||||
spacing={-4}
|
||||
trigger="custom"
|
||||
getPopupContainer={() => contextMenuRef.current ?? document.body}
|
||||
className={classNames('coz-shadow-large')}
|
||||
render={
|
||||
<Menu.SubMenu className={classNames('p-1')} mode="menu">
|
||||
{editorActionRegistry.entries().map(([key, { Component }]) => (
|
||||
<Component key={key} editor={editor} />
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { type Editor } from '@tiptap/react';
|
||||
|
||||
import { type EditorActionRegistry } from '../editor-actions/registry';
|
||||
|
||||
export interface EditorToolbarProps {
|
||||
editor: Editor | null;
|
||||
actionRegistry: EditorActionRegistry;
|
||||
}
|
||||
|
||||
export const EditorToolbar = ({
|
||||
editor,
|
||||
actionRegistry,
|
||||
}: EditorToolbarProps) => (
|
||||
<div className="h-[32px] box-content px-2 pt-2">
|
||||
{actionRegistry.entries().map(([key, { Component }]) => (
|
||||
<Component key={key} editor={editor} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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, { useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { EditorContent, type Editor } from '@tiptap/react';
|
||||
|
||||
import { getEditorContent } from '@/text-knowledge-editor/services/use-case/get-editor-content';
|
||||
import { getEditorWordsCls } from '@/text-knowledge-editor/services/inner/get-editor-words-cls';
|
||||
import { getEditorTableClassname } from '@/text-knowledge-editor/services/inner/get-editor-table-cls';
|
||||
import { getEditorImgClassname } from '@/text-knowledge-editor/services/inner/get-editor-img-cls';
|
||||
import { useOutEditorMode } from '@/text-knowledge-editor/hooks/inner/use-out-editor-mode';
|
||||
import { useControlContextMenu } from '@/text-knowledge-editor/hooks/inner/use-control-context-menu';
|
||||
|
||||
import { EditorContextMenu } from '../editor-context-menu';
|
||||
import { type EditorActionRegistry } from '../editor-actions/registry';
|
||||
|
||||
interface DocumentEditorProps {
|
||||
editor: Editor | null;
|
||||
placeholder?: string;
|
||||
editorContextMenuItemsRegistry?: EditorActionRegistry;
|
||||
editorBottomSlot?: React.ReactNode;
|
||||
onBlur?: (newContent: string) => void;
|
||||
}
|
||||
|
||||
export const DocumentEditor: React.FC<DocumentEditorProps> = props => {
|
||||
const {
|
||||
editor,
|
||||
placeholder,
|
||||
editorContextMenuItemsRegistry,
|
||||
editorBottomSlot,
|
||||
onBlur,
|
||||
} = props;
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
/**
|
||||
* 当右键点击编辑器时,显示上下文菜单
|
||||
*/
|
||||
const { contextMenuPosition, openContextMenu } = useControlContextMenu({
|
||||
contextMenuRef,
|
||||
});
|
||||
|
||||
/**
|
||||
* 当点击编辑器外部时
|
||||
*/
|
||||
useOutEditorMode({
|
||||
editorRef,
|
||||
exclude: [contextMenuRef],
|
||||
onExitEditMode: () => {
|
||||
const newContent = getEditorContent(editor);
|
||||
onBlur?.(newContent);
|
||||
},
|
||||
});
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={editorRef}
|
||||
className={classNames(
|
||||
// 布局
|
||||
'relative',
|
||||
// 间距
|
||||
'mb-2 p-2',
|
||||
// 文字样式
|
||||
'text-sm leading-5',
|
||||
// 颜色
|
||||
'coz-fg-primary coz-bg-max',
|
||||
// 边框
|
||||
'border border-solid coz-stroke-hglt rounded-lg',
|
||||
)}
|
||||
onContextMenu={openContextMenu}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
// 表格样式
|
||||
getEditorTableClassname(),
|
||||
// 图片样式
|
||||
getEditorImgClassname(),
|
||||
// 换行
|
||||
getEditorWordsCls(),
|
||||
)}
|
||||
>
|
||||
<EditorContent editor={editor} placeholder={placeholder} />
|
||||
{editorBottomSlot}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右键菜单 */}
|
||||
{contextMenuPosition && editorContextMenuItemsRegistry ? (
|
||||
<EditorContextMenu
|
||||
x={contextMenuPosition.x}
|
||||
y={contextMenuPosition.y}
|
||||
contextMenuRef={contextMenuRef}
|
||||
editor={editor}
|
||||
editorActionRegistry={editorContextMenuItemsRegistry}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { DocumentEditor } from './editor';
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import { KnowledgeE2e } from '@coze-data/e2e';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozDocumentAddBottom } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { useAddEmptyChunkAction } from '@/text-knowledge-editor/hooks/use-case/chunk-actions';
|
||||
import { eventBus } from '@/text-knowledge-editor/event';
|
||||
|
||||
import { type HoverEditBarActionProps } from './module';
|
||||
|
||||
export const AddAfterAction: React.FC<HoverEditBarActionProps> = ({
|
||||
chunk,
|
||||
chunks,
|
||||
disabled,
|
||||
}) => {
|
||||
// 在特定分片后添加新分片
|
||||
const { addEmptyChunkAfter } = useAddEmptyChunkAction({
|
||||
chunks: chunks || [],
|
||||
onChunksChange: ({ newChunk, chunks: newChunks }) => {
|
||||
eventBus.emit('hoverEditBarAction', {
|
||||
type: 'add-after',
|
||||
targetChunk: chunk,
|
||||
chunks: newChunks,
|
||||
newChunk,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={I18n.t('knowledge_optimize_016')}
|
||||
clickToHide
|
||||
autoAdjustOverflow
|
||||
>
|
||||
<IconButton
|
||||
data-dtestid={`${KnowledgeE2e.SegmentDetailContentItemAddBottomIcon}.${chunk.text_knowledge_editor_chunk_uuid}`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
disabled={disabled}
|
||||
icon={<IconCozDocumentAddBottom className="text-[14px]" />}
|
||||
iconPosition="left"
|
||||
className="coz-fg-secondary leading-none !w-6 !h-6"
|
||||
onClick={() => addEmptyChunkAfter(chunk)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { KnowledgeE2e } from '@coze-data/e2e';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozDocumentAddTop } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { useAddEmptyChunkAction } from '@/text-knowledge-editor/hooks/use-case/chunk-actions';
|
||||
import { eventBus } from '@/text-knowledge-editor/event';
|
||||
|
||||
import { type HoverEditBarActionProps } from './module';
|
||||
|
||||
/**
|
||||
* 在特定分片前添加新分片的操作组件
|
||||
*/
|
||||
export const AddBeforeAction: React.FC<HoverEditBarActionProps> = ({
|
||||
chunk,
|
||||
chunks = [],
|
||||
disabled,
|
||||
}) => {
|
||||
// 在特定分片前添加新分片
|
||||
const { addEmptyChunkBefore } = useAddEmptyChunkAction({
|
||||
chunks,
|
||||
onChunksChange: ({ newChunk, chunks: newChunks }) => {
|
||||
eventBus.emit('hoverEditBarAction', {
|
||||
type: 'add-before',
|
||||
targetChunk: chunk,
|
||||
chunks: newChunks,
|
||||
newChunk,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={I18n.t('knowledge_optimize_017')}
|
||||
clickToHide
|
||||
autoAdjustOverflow
|
||||
>
|
||||
<IconButton
|
||||
data-dtestid={`${KnowledgeE2e.SegmentDetailContentItemAddTopIcon}.${chunk.text_knowledge_editor_chunk_uuid}`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
disabled={disabled}
|
||||
icon={<IconCozDocumentAddTop className="text-[14px]" />}
|
||||
iconPosition="left"
|
||||
className="coz-fg-secondary leading-none !w-6 !h-6"
|
||||
onClick={() => addEmptyChunkBefore(chunk)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozTrashCan } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { useDeleteAction } from '@/text-knowledge-editor/hooks/use-case/chunk-actions';
|
||||
import { eventBus } from '@/text-knowledge-editor/event';
|
||||
|
||||
import { type HoverEditBarActionProps } from './module';
|
||||
|
||||
/**
|
||||
* 删除特定分片的操作组件
|
||||
*
|
||||
* 内部实现了删除特定分片的逻辑
|
||||
* 如果传入了 onDelete 回调,则会在点击时调用
|
||||
* 如果提供了 chunks、onChunksChange,则会在内部处理删除逻辑,
|
||||
* 无需依赖外部的 usePreviewContextMenu
|
||||
*/
|
||||
export const DeleteAction: React.FC<HoverEditBarActionProps> = ({
|
||||
chunk,
|
||||
chunks = [],
|
||||
disabled,
|
||||
}) => {
|
||||
// 删除特定分片
|
||||
const { deleteChunk } = useDeleteAction({
|
||||
chunks,
|
||||
onChunksChange: ({ chunks: newChunks }) => {
|
||||
eventBus.emit('hoverEditBarAction', {
|
||||
type: 'delete',
|
||||
targetChunk: chunk,
|
||||
chunks: newChunks,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={I18n.t('knowledge_level_028')}
|
||||
clickToHide
|
||||
autoAdjustOverflow
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="secondary"
|
||||
disabled={disabled}
|
||||
icon={<IconCozTrashCan className="text-[14px]" />}
|
||||
iconPosition="left"
|
||||
className="coz-fg-secondary leading-none !w-6 !h-6"
|
||||
onClick={() => deleteChunk(chunk)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { KnowledgeE2e } from '@coze-data/e2e';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozEdit } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { eventBus } from '@/text-knowledge-editor/event';
|
||||
|
||||
import { type HoverEditBarActionProps } from './module';
|
||||
|
||||
/**
|
||||
* 编辑操作组件
|
||||
*
|
||||
* 内部实现了激活特定分片的编辑模式的逻辑
|
||||
* 如果传入了 onEdit 回调,则会在点击时调用
|
||||
*/
|
||||
export const EditAction: React.FC<HoverEditBarActionProps> = ({
|
||||
chunk,
|
||||
disabled,
|
||||
}) => (
|
||||
<Tooltip
|
||||
content={I18n.t('datasets_segment_edit')}
|
||||
clickToHide
|
||||
autoAdjustOverflow
|
||||
>
|
||||
<IconButton
|
||||
data-dtestid={`${KnowledgeE2e.SegmentDetailContentItemEditIcon}.${chunk.text_knowledge_editor_chunk_uuid}`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
disabled={disabled}
|
||||
icon={<IconCozEdit className="text-[14px]" />}
|
||||
iconPosition="left"
|
||||
className="coz-fg-secondary leading-none !w-6 !h-6"
|
||||
onClick={() => {
|
||||
eventBus.emit('hoverEditBarAction', {
|
||||
type: 'edit',
|
||||
targetChunk: chunk,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { EditAction } from './edit-action';
|
||||
export { DeleteAction } from './delete-action';
|
||||
export { AddBeforeAction } from './add-before-action';
|
||||
export { AddAfterAction } from './add-after-action';
|
||||
|
||||
export {
|
||||
createHoverEditBarActionFeatureRegistry,
|
||||
type HoverEditBarActionFeatureType,
|
||||
type HoverEditBarActionRegistry,
|
||||
} from './registry';
|
||||
|
||||
export type {
|
||||
HoverEditBarActionModule,
|
||||
HoverEditBarActionProps,
|
||||
} from './module';
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
|
||||
export interface HoverEditBarActionProps {
|
||||
chunk: Chunk;
|
||||
chunks?: Chunk[];
|
||||
disabled?: boolean;
|
||||
onChunksChange?: (chunks: Chunk[]) => void;
|
||||
}
|
||||
|
||||
export interface HoverEditBarActionModule {
|
||||
Component: React.ComponentType<HoverEditBarActionProps>;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FeatureRegistry } from '@coze-data/feature-register';
|
||||
|
||||
import { type HoverEditBarActionModule } from './module';
|
||||
|
||||
export type HoverEditBarActionFeatureType =
|
||||
| 'edit'
|
||||
| 'delete'
|
||||
| 'add-before'
|
||||
| 'add-after';
|
||||
|
||||
export type HoverEditBarActionRegistry = FeatureRegistry<
|
||||
HoverEditBarActionFeatureType,
|
||||
HoverEditBarActionModule
|
||||
>;
|
||||
|
||||
export const createHoverEditBarActionFeatureRegistry = (
|
||||
name: string,
|
||||
): HoverEditBarActionRegistry =>
|
||||
new FeatureRegistry<HoverEditBarActionFeatureType, HoverEditBarActionModule>({
|
||||
name,
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Space, Tooltip } from '@coze-arch/coze-design';
|
||||
import { SliceStatus } from '@coze-arch/bot-api/knowledge';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { type HoverEditBarActionRegistry } from '@/text-knowledge-editor/features/hover-edit-bar-actions/registry';
|
||||
export interface HoverEditBarProps {
|
||||
chunk: Chunk;
|
||||
chunks: Chunk[];
|
||||
disabled?: boolean;
|
||||
hoverEditBarActionsRegistry: HoverEditBarActionRegistry;
|
||||
onChunksChange?: (chunks: Chunk[]) => void;
|
||||
}
|
||||
|
||||
export const HoverEditBar: React.FC<HoverEditBarProps> = ({
|
||||
chunk,
|
||||
chunks,
|
||||
disabled,
|
||||
hoverEditBarActionsRegistry,
|
||||
onChunksChange,
|
||||
}) => {
|
||||
const isAudiFailed = chunk.status === SliceStatus.AuditFailed;
|
||||
const iconButtonCommonClasses = 'coz-fg-secondary leading-none !w-6 !h-6';
|
||||
|
||||
if (!hoverEditBarActionsRegistry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute top-[2px] right-[2px] flex z-10">
|
||||
{!disabled ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'p-1 coz-bg-plus rounded-lg',
|
||||
'coz-shadow-default',
|
||||
)}
|
||||
>
|
||||
<Space spacing={3}>
|
||||
{hoverEditBarActionsRegistry
|
||||
.entries()
|
||||
.map(([key, { Component }]) => (
|
||||
<Component
|
||||
key={key}
|
||||
chunk={chunk}
|
||||
chunks={chunks}
|
||||
onChunksChange={onChunksChange}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isAudiFailed ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'p-1 coz-bg-plus rounded-lg',
|
||||
'coz-shadow-default',
|
||||
'ml-1',
|
||||
)}
|
||||
>
|
||||
<Tooltip
|
||||
content={I18n.t('community_This_is_a_toast_Machine_review_failed')}
|
||||
clickToHide
|
||||
autoAdjustOverflow
|
||||
>
|
||||
<IconButton
|
||||
icon={
|
||||
<IconCozInfoCircle className="text-[14px] coz-fg-hglt-red" />
|
||||
}
|
||||
size="small"
|
||||
color="secondary"
|
||||
className={iconButtonCommonClasses}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { HoverEditBar, type HoverEditBarProps } from './hover-edit-bar';
|
||||
|
||||
export { HoverEditBar, type HoverEditBarProps };
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozDocumentAddBottom } from '@coze-arch/coze-design/icons';
|
||||
import { Menu } from '@coze-arch/coze-design';
|
||||
|
||||
import { useAddEmptyChunkAction } from '@/text-knowledge-editor/hooks/use-case/chunk-actions';
|
||||
import { eventBus } from '@/text-knowledge-editor/event';
|
||||
|
||||
import { type PreviewContextMenuItemProps } from './module';
|
||||
|
||||
/**
|
||||
* 在特定分片后添加新分片的菜单项组件
|
||||
*/
|
||||
export const AddAfterAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
chunk,
|
||||
chunks = [],
|
||||
disabled,
|
||||
}) => {
|
||||
const getIconStyles = (isDisabled: boolean) =>
|
||||
classNames('w-3.5 h-3.5', {
|
||||
'opacity-30': isDisabled,
|
||||
});
|
||||
|
||||
const getMenuItemStyles = (isDisabled: boolean) =>
|
||||
classNames('h-8 px-2 py-2 text-xs rounded-lg', {
|
||||
'cursor-not-allowed': isDisabled,
|
||||
});
|
||||
|
||||
// 在特定分片后添加新分片
|
||||
const { addEmptyChunkAfter } = useAddEmptyChunkAction({
|
||||
chunks,
|
||||
onChunksChange: ({ newChunk, chunks: newChunks }) => {
|
||||
// 发出在特定分片后添加新分片的事件
|
||||
eventBus.emit('previewContextMenuItemAction', {
|
||||
type: 'add-after',
|
||||
newChunk,
|
||||
targetChunk: chunk,
|
||||
chunks: newChunks,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
disabled={disabled}
|
||||
icon={<IconCozDocumentAddBottom className={getIconStyles(!!disabled)} />}
|
||||
onClick={() => addEmptyChunkAfter(chunk)}
|
||||
className={getMenuItemStyles(!!disabled)}
|
||||
>
|
||||
{I18n.t('knowledge_optimize_016')}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozDocumentAddTop } from '@coze-arch/coze-design/icons';
|
||||
import { Menu } from '@coze-arch/coze-design';
|
||||
|
||||
import { useAddEmptyChunkAction } from '@/text-knowledge-editor/hooks/use-case/chunk-actions';
|
||||
import { eventBus } from '@/text-knowledge-editor/event';
|
||||
|
||||
import { type PreviewContextMenuItemProps } from './module';
|
||||
|
||||
/**
|
||||
* 在特定分片前添加新分片的菜单项组件
|
||||
*/
|
||||
export const AddBeforeAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
chunk,
|
||||
chunks = [],
|
||||
disabled,
|
||||
}) => {
|
||||
const getIconStyles = (isDisabled: boolean) =>
|
||||
classNames('w-3.5 h-3.5', {
|
||||
'opacity-30': isDisabled,
|
||||
});
|
||||
|
||||
const getMenuItemStyles = (isDisabled: boolean) =>
|
||||
classNames('h-8 px-2 py-2 text-xs rounded-lg', {
|
||||
'cursor-not-allowed': isDisabled,
|
||||
});
|
||||
|
||||
// 在特定分片前添加新分片
|
||||
const { addEmptyChunkBefore } = useAddEmptyChunkAction({
|
||||
chunks,
|
||||
onChunksChange: ({ newChunk, chunks: newChunks }) => {
|
||||
eventBus.emit('previewContextMenuItemAction', {
|
||||
type: 'add-before',
|
||||
targetChunk: chunk,
|
||||
newChunk,
|
||||
chunks: newChunks,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
disabled={disabled}
|
||||
icon={<IconCozDocumentAddTop className={getIconStyles(!!disabled)} />}
|
||||
onClick={() => addEmptyChunkBefore(chunk)}
|
||||
className={getMenuItemStyles(!!disabled)}
|
||||
>
|
||||
{I18n.t('knowledge_optimize_017')}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozTrashCan } from '@coze-arch/coze-design/icons';
|
||||
import { Menu } from '@coze-arch/coze-design';
|
||||
|
||||
import { useDeleteAction } from '@/text-knowledge-editor/hooks/use-case/chunk-actions';
|
||||
import { eventBus } from '@/text-knowledge-editor/event';
|
||||
|
||||
import { type PreviewContextMenuItemProps } from './module';
|
||||
|
||||
/**
|
||||
* 删除特定分片的菜单项组件
|
||||
*/
|
||||
export const DeleteAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
chunk,
|
||||
chunks = [],
|
||||
disabled,
|
||||
}) => {
|
||||
const getIconStyles = (isDisabled: boolean) =>
|
||||
classNames('w-3.5 h-3.5', {
|
||||
'opacity-30': isDisabled,
|
||||
});
|
||||
|
||||
const getMenuItemStyles = (isDisabled: boolean) =>
|
||||
classNames('h-8 px-2 py-2 text-xs rounded-lg', {
|
||||
'cursor-not-allowed': isDisabled,
|
||||
});
|
||||
|
||||
// 删除特定分片
|
||||
const { deleteChunk } = useDeleteAction({
|
||||
chunks,
|
||||
onChunksChange: ({ chunks: newChunks }) => {
|
||||
eventBus.emit('previewContextMenuItemAction', {
|
||||
type: 'delete',
|
||||
targetChunk: chunk,
|
||||
chunks: newChunks,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
disabled={disabled}
|
||||
icon={<IconCozTrashCan className={getIconStyles(!!disabled)} />}
|
||||
onClick={() => deleteChunk(chunk)}
|
||||
className={getMenuItemStyles(!!disabled)}
|
||||
>
|
||||
{I18n.t('Delete')}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozEdit } from '@coze-arch/coze-design/icons';
|
||||
import { Menu } from '@coze-arch/coze-design';
|
||||
|
||||
import { eventBus } from '@/text-knowledge-editor/event';
|
||||
|
||||
import { type PreviewContextMenuItemProps } from './module';
|
||||
|
||||
/**
|
||||
* 编辑操作菜单项组件
|
||||
*
|
||||
* 内部实现了激活特定分片的编辑模式的逻辑
|
||||
* 如果传入了 onEdit 回调,则会在点击时调用
|
||||
*/
|
||||
export const EditAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
chunk,
|
||||
disabled,
|
||||
}) => {
|
||||
const getIconStyles = (isDisabled: boolean) =>
|
||||
classNames('w-3.5 h-3.5', {
|
||||
'opacity-30': isDisabled,
|
||||
});
|
||||
|
||||
const getMenuItemStyles = (isDisabled: boolean) =>
|
||||
classNames('h-8 px-2 py-2 text-xs rounded-lg', {
|
||||
'cursor-not-allowed': isDisabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
disabled={disabled}
|
||||
icon={<IconCozEdit className={getIconStyles(!!disabled)} />}
|
||||
onClick={() => {
|
||||
eventBus.emit('previewContextMenuItemAction', {
|
||||
type: 'edit',
|
||||
targetChunk: chunk,
|
||||
});
|
||||
}}
|
||||
className={getMenuItemStyles(!!disabled)}
|
||||
>
|
||||
{I18n.t('Edit')}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { EditAction } from './edit-action';
|
||||
export { DeleteAction } from './delete-action';
|
||||
export { AddBeforeAction } from './add-before-action';
|
||||
export { AddAfterAction } from './add-after-action';
|
||||
|
||||
export {
|
||||
createPreviewContextMenuItemFeatureRegistry,
|
||||
type PreviewContextMenuItemFeatureType,
|
||||
type PreviewContextMenuItemRegistry,
|
||||
} from './registry';
|
||||
|
||||
export type {
|
||||
PreviewContextMenuItemModule,
|
||||
PreviewContextMenuItemProps,
|
||||
} from './module';
|
||||
@@ -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 type React from 'react';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
|
||||
export interface PreviewContextMenuItemProps {
|
||||
chunk: Chunk;
|
||||
chunks?: Chunk[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface PreviewContextMenuItemModule {
|
||||
Component: React.ComponentType<PreviewContextMenuItemProps>;
|
||||
}
|
||||
@@ -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 { FeatureRegistry } from '@coze-data/feature-register';
|
||||
|
||||
import { type PreviewContextMenuItemModule } from './module';
|
||||
|
||||
export type PreviewContextMenuItemFeatureType =
|
||||
| 'edit'
|
||||
| 'delete'
|
||||
| 'add-before'
|
||||
| 'add-after';
|
||||
|
||||
export type PreviewContextMenuItemRegistry = FeatureRegistry<
|
||||
PreviewContextMenuItemFeatureType,
|
||||
PreviewContextMenuItemModule
|
||||
>;
|
||||
|
||||
export const createPreviewContextMenuItemFeatureRegistry = (
|
||||
name: string,
|
||||
): PreviewContextMenuItemRegistry =>
|
||||
new FeatureRegistry<
|
||||
PreviewContextMenuItemFeatureType,
|
||||
PreviewContextMenuItemModule
|
||||
>({
|
||||
name,
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Menu } from '@coze-arch/coze-design';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { type PreviewContextMenuItemRegistry } from '@/text-knowledge-editor/features/preview-context-menu-items/registry';
|
||||
interface PreviewContextMenuProps {
|
||||
x: number;
|
||||
y: number;
|
||||
chunk: Chunk;
|
||||
chunks: Chunk[];
|
||||
readonly?: boolean;
|
||||
contextMenuRef: React.RefObject<HTMLDivElement>;
|
||||
previewContextMenuItemsRegistry: PreviewContextMenuItemRegistry;
|
||||
}
|
||||
|
||||
const PreviewContextMenu: React.FC<PreviewContextMenuProps> = props => {
|
||||
const {
|
||||
previewContextMenuItemsRegistry,
|
||||
chunk,
|
||||
chunks,
|
||||
readonly,
|
||||
contextMenuRef,
|
||||
x,
|
||||
y,
|
||||
} = props;
|
||||
|
||||
if (readonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className="absolute bg-white shadow-lg rounded-md py-1 z-50"
|
||||
style={{
|
||||
top: `${y}px`,
|
||||
left: `${x}px`,
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
visible
|
||||
position="bottomLeft"
|
||||
spacing={-4}
|
||||
trigger="custom"
|
||||
getPopupContainer={() => contextMenuRef.current ?? document.body}
|
||||
className={classNames('rounded-lg')}
|
||||
render={
|
||||
<Menu.SubMenu className={classNames('w-40 p-1')} mode="menu">
|
||||
{previewContextMenuItemsRegistry
|
||||
.entries()
|
||||
.map(([key, { Component }]) => (
|
||||
<Component key={key} chunk={chunk} chunks={chunks} />
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewContextMenu;
|
||||
@@ -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 { DocumentPreview } from './preview';
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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, { useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { useHoverEffect } from '@/text-knowledge-editor/hooks/inner/use-hover-effect';
|
||||
import { useControlContextMenu } from '@/text-knowledge-editor/hooks/inner/use-control-context-menu';
|
||||
import { DocumentChunkPreview } from '@/text-knowledge-editor/components/preview-chunk/document';
|
||||
|
||||
import { type PreviewContextMenuItemRegistry } from '../preview-context-menu-items/registry';
|
||||
import PreviewContextMenu from '../preview-context-menu';
|
||||
import { type HoverEditBarActionRegistry } from '../hover-edit-bar-actions/registry';
|
||||
import { HoverEditBar } from '../hover-edit-bar';
|
||||
interface DocumentPreviewProps {
|
||||
chunk: Chunk;
|
||||
chunks: Chunk[];
|
||||
readonly?: boolean;
|
||||
locateId?: string;
|
||||
hoverEditBarActionsRegistry: HoverEditBarActionRegistry;
|
||||
previewContextMenuItemsRegistry: PreviewContextMenuItemRegistry;
|
||||
onActivateEditMode?: (chunk: Chunk) => void;
|
||||
}
|
||||
|
||||
const DocumentPreviewComponent: React.FC<DocumentPreviewProps> = props => {
|
||||
const {
|
||||
chunk,
|
||||
chunks,
|
||||
readonly = false,
|
||||
locateId,
|
||||
onActivateEditMode,
|
||||
hoverEditBarActionsRegistry,
|
||||
previewContextMenuItemsRegistry,
|
||||
} = props;
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||
const { hoveredChunk, handleMouseEnter, handleMouseLeave } = useHoverEffect();
|
||||
|
||||
const { contextMenuPosition, openContextMenu } = useControlContextMenu({
|
||||
contextMenuRef,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={classNames(
|
||||
// 布局
|
||||
'relative overflow-hidden',
|
||||
)}
|
||||
onContextMenu={readonly ? undefined : e => openContextMenu(e)}
|
||||
onMouseEnter={
|
||||
readonly
|
||||
? undefined
|
||||
: () => handleMouseEnter(chunk.text_knowledge_editor_chunk_uuid)
|
||||
}
|
||||
onMouseLeave={readonly ? undefined : handleMouseLeave}
|
||||
onDoubleClick={readonly ? undefined : () => onActivateEditMode?.(chunk)}
|
||||
>
|
||||
{/* 悬停时显示的操作栏 */}
|
||||
{hoveredChunk === chunk.text_knowledge_editor_chunk_uuid &&
|
||||
!readonly ? (
|
||||
<HoverEditBar
|
||||
chunk={chunk}
|
||||
chunks={chunks}
|
||||
hoverEditBarActionsRegistry={hoverEditBarActionsRegistry}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DocumentChunkPreview chunk={chunk} locateId={locateId || ''} />
|
||||
</div>
|
||||
|
||||
{/* 右键菜单 */}
|
||||
{contextMenuPosition ? (
|
||||
<PreviewContextMenu
|
||||
previewContextMenuItemsRegistry={previewContextMenuItemsRegistry}
|
||||
x={contextMenuPosition.x}
|
||||
y={contextMenuPosition.y}
|
||||
chunk={chunk}
|
||||
chunks={chunks}
|
||||
readonly={readonly}
|
||||
contextMenuRef={contextMenuRef}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 使用React.memo包装组件,避免不必要的重新渲染
|
||||
export const DocumentPreview = React.memo(
|
||||
DocumentPreviewComponent,
|
||||
(prevProps, nextProps) => {
|
||||
// 如果分片内容变化,需要重新渲染
|
||||
if (prevProps.chunk.content !== nextProps.chunk.content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 其他情况下不需要重新渲染
|
||||
return true;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
interface UseControlContextMenuProps {
|
||||
contextMenuRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const useControlContextMenu = ({
|
||||
contextMenuRef,
|
||||
}: UseControlContextMenuProps) => {
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
// 处理右键菜单
|
||||
const openContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 计算相对于事件目标元素的位置
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const relativeX = e.clientX - rect.left;
|
||||
const relativeY = e.clientY - rect.top;
|
||||
|
||||
setContextMenuPosition({
|
||||
x: relativeX,
|
||||
y: relativeY,
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭右键菜单
|
||||
const closeContextMenu = () => {
|
||||
setContextMenuPosition(null);
|
||||
};
|
||||
|
||||
// 处理点击文档其他位置
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// 如果点击的是右键菜单外部,则关闭菜单
|
||||
if (
|
||||
contextMenuRef.current &&
|
||||
!contextMenuRef.current.contains(event.target as Node)
|
||||
) {
|
||||
closeContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
contextMenuPosition,
|
||||
openContextMenu,
|
||||
closeContextMenu,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
interface UseControlEditorContextMenuProps {
|
||||
contextMenuRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const useControlEditorContextMenu = ({
|
||||
contextMenuRef,
|
||||
}: UseControlEditorContextMenuProps) => {
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
// 处理右键菜单
|
||||
const openContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setContextMenuPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭右键菜单
|
||||
const closeContextMenu = () => {
|
||||
setContextMenuPosition(null);
|
||||
};
|
||||
|
||||
// 处理点击文档其他位置
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// 如果点击的是右键菜单外部,则关闭菜单
|
||||
if (
|
||||
contextMenuRef.current &&
|
||||
!contextMenuRef.current.contains(event.target as Node)
|
||||
) {
|
||||
closeContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
contextMenuPosition,
|
||||
openContextMenu,
|
||||
closeContextMenu,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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, useRef, useEffect } from 'react';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
|
||||
export const useControlPreviewContextMenu = () => {
|
||||
const [contextMenuInfo, setContextMenuInfo] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
chunk: Chunk;
|
||||
} | null>(null);
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 处理右键点击事件
|
||||
const openContextMenu = (e: React.MouseEvent, chunk: Chunk) => {
|
||||
e.preventDefault();
|
||||
setContextMenuInfo({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
chunk,
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭右键菜单
|
||||
const closeContextMenu = () => {
|
||||
setContextMenuInfo(null);
|
||||
};
|
||||
|
||||
// 点击文档其他位置关闭右键菜单
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
contextMenuRef.current &&
|
||||
!contextMenuRef.current.contains(event.target as Node)
|
||||
) {
|
||||
closeContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
contextMenuInfo,
|
||||
contextMenuRef,
|
||||
openContextMenu,
|
||||
closeContextMenu,
|
||||
};
|
||||
};
|
||||
@@ -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 { useRequest } from 'ahooks';
|
||||
import { DataNamespace, dataReporter } from '@coze-data/reporter';
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { KnowledgeApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { createRemoteChunk } from '@/text-knowledge-editor/services/inner/chunk-op.service';
|
||||
|
||||
export interface UseCreateChunkProps {
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
export const useCreateChunk = ({ documentId }: UseCreateChunkProps) => {
|
||||
const { runAsync } = useRequest(
|
||||
async (props: { content: string; sequence: string }) => {
|
||||
const { content, sequence } = props;
|
||||
if (!documentId) {
|
||||
throw new CustomError('normal_error', 'missing doc_id');
|
||||
}
|
||||
|
||||
const data = await KnowledgeApi.CreateSlice({
|
||||
document_id: documentId,
|
||||
raw_text: content,
|
||||
sequence,
|
||||
});
|
||||
|
||||
const chunk = createRemoteChunk({
|
||||
slice_id: data?.slice_id ?? '',
|
||||
sequence,
|
||||
content,
|
||||
});
|
||||
|
||||
return chunk;
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onError: error => {
|
||||
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
|
||||
eventName: REPORT_EVENTS.KnowledgeCreateSlice,
|
||||
error,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
createChunk: runAsync,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { KnowledgeApi } from '@coze-arch/bot-api';
|
||||
|
||||
export const useDeleteChunk = () => {
|
||||
const { runAsync: deleteSlice } = useRequest(
|
||||
async (sliceId: string) => {
|
||||
if (!sliceId) {
|
||||
throw new CustomError('normal_error', 'missing slice_id');
|
||||
}
|
||||
await KnowledgeApi.DeleteSlice({
|
||||
slice_ids: [sliceId],
|
||||
});
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
return {
|
||||
deleteSlice,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useHoverEffect = () => {
|
||||
const [hoveredChunk, setHoveredChunk] = useState<string | null>(null);
|
||||
|
||||
// 处理鼠标悬停事件
|
||||
const handleMouseEnter = (chunkId: string) => {
|
||||
setHoveredChunk(chunkId);
|
||||
};
|
||||
|
||||
// 处理鼠标离开事件
|
||||
const handleMouseLeave = () => {
|
||||
setHoveredChunk(null);
|
||||
};
|
||||
|
||||
return {
|
||||
hoveredChunk,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export interface UseOutEditorModeProps {
|
||||
editorRef: React.RefObject<HTMLDivElement>;
|
||||
exclude?: React.RefObject<HTMLDivElement>[];
|
||||
onExitEditMode?: () => void;
|
||||
}
|
||||
|
||||
export const useOutEditorMode = ({
|
||||
editorRef,
|
||||
exclude,
|
||||
onExitEditMode,
|
||||
}: UseOutEditorModeProps) => {
|
||||
// 处理点击文档其他位置
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// 如果点击的是编辑器外部,则退出编辑模式
|
||||
if (
|
||||
editorRef.current &&
|
||||
!editorRef.current.contains(event.target as Node) &&
|
||||
!exclude?.some(ref => ref.current?.contains(event.target as Node))
|
||||
) {
|
||||
onExitEditMode?.();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [editorRef, exclude, onExitEditMode]);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { KnowledgeApi } from '@coze-arch/bot-api';
|
||||
|
||||
export const useUpdateChunk = () => {
|
||||
const { runAsync: updateSlice, loading: updateLoading } = useRequest(
|
||||
async (sliceId: string, updateContent: string) => {
|
||||
if (!sliceId) {
|
||||
throw new CustomError('normal_error', 'missing slice_id');
|
||||
}
|
||||
await KnowledgeApi.UpdateSlice({
|
||||
slice_id: sliceId,
|
||||
raw_text: updateContent,
|
||||
});
|
||||
return updateContent;
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
return {
|
||||
updateSlice,
|
||||
updateLoading,
|
||||
};
|
||||
};
|
||||
@@ -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 { useAddEmptyChunkAction } from './use-add-empty-chunk-action';
|
||||
export { useDeleteAction } from './use-delete-action';
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { createLocalChunk } from '@/text-knowledge-editor/services/inner/chunk-op.service';
|
||||
|
||||
interface UseAddEmptyChunkActionProps {
|
||||
chunks: Chunk[];
|
||||
onChunksChange?: (params: { newChunk: Chunk; chunks: Chunk[] }) => void;
|
||||
}
|
||||
/**
|
||||
* 在特定分片后添加新分片的 hook
|
||||
*
|
||||
* 提供在特定分片后添加新分片的功能
|
||||
*/
|
||||
export const useAddEmptyChunkAction = ({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
}: UseAddEmptyChunkActionProps) => {
|
||||
// 使用ref保存最新的chunks引用
|
||||
const chunksRef = useRef<Chunk[]>(chunks);
|
||||
|
||||
// 每次props.chunks更新时,更新ref
|
||||
useEffect(() => {
|
||||
chunksRef.current = chunks;
|
||||
}, [chunks]);
|
||||
|
||||
/**
|
||||
* 在特定分片后添加新分片
|
||||
* @returns 包含新分片和更新后的分片列表的结果对象
|
||||
*/
|
||||
const handleAddEmptyChunkAfter = (chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
const currentChunks = chunksRef.current;
|
||||
const index = currentChunks.findIndex(
|
||||
c =>
|
||||
c.text_knowledge_editor_chunk_uuid ===
|
||||
chunk.text_knowledge_editor_chunk_uuid,
|
||||
);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sequence =
|
||||
currentChunks.find(
|
||||
c =>
|
||||
c.text_knowledge_editor_chunk_uuid ===
|
||||
chunk.text_knowledge_editor_chunk_uuid,
|
||||
)?.sequence ?? '1';
|
||||
const newChunk = createLocalChunk({
|
||||
sequence: String(Number(sequence) + 1),
|
||||
});
|
||||
|
||||
const updatedChunks = [
|
||||
...currentChunks.slice(0, index + 1),
|
||||
newChunk,
|
||||
...currentChunks.slice(index + 1),
|
||||
];
|
||||
|
||||
onChunksChange?.({
|
||||
newChunk,
|
||||
chunks: updatedChunks,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 在特定分片前添加新分片
|
||||
*/
|
||||
const handleAddEmptyChunkBefore = (chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
const currentChunks = chunksRef.current;
|
||||
const index = currentChunks.findIndex(
|
||||
c =>
|
||||
c.text_knowledge_editor_chunk_uuid ===
|
||||
chunk.text_knowledge_editor_chunk_uuid,
|
||||
);
|
||||
const sequence =
|
||||
currentChunks.find(
|
||||
c =>
|
||||
c.text_knowledge_editor_chunk_uuid ===
|
||||
chunk.text_knowledge_editor_chunk_uuid,
|
||||
)?.sequence ?? '1';
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newChunk = createLocalChunk({
|
||||
sequence,
|
||||
});
|
||||
|
||||
const updatedChunks = [
|
||||
...currentChunks.slice(0, index),
|
||||
newChunk,
|
||||
...currentChunks.slice(index),
|
||||
];
|
||||
|
||||
onChunksChange?.({
|
||||
newChunk,
|
||||
chunks: updatedChunks,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
addEmptyChunkAfter: handleAddEmptyChunkAfter,
|
||||
addEmptyChunkBefore: handleAddEmptyChunkBefore,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 { useCallback, useRef, useEffect } from 'react';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { useDeleteChunk } from '@/text-knowledge-editor/hooks/inner/use-delete-chunk';
|
||||
|
||||
interface UseDeleteActionProps {
|
||||
chunks: Chunk[];
|
||||
onChunksChange?: (params: { chunks: Chunk[]; targetChunk: Chunk }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分片的 hook
|
||||
*
|
||||
* 提供删除特定分片的功能
|
||||
*/
|
||||
export const useDeleteAction = ({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
}: UseDeleteActionProps) => {
|
||||
// 使用ref保存最新的chunks引用
|
||||
const chunksRef = useRef<Chunk[]>(chunks);
|
||||
const { deleteSlice } = useDeleteChunk();
|
||||
|
||||
// 每次props.chunks更新时,更新ref
|
||||
useEffect(() => {
|
||||
chunksRef.current = chunks;
|
||||
}, [chunks]);
|
||||
|
||||
/**
|
||||
* 删除特定分片
|
||||
*/
|
||||
const handleDeleteChunk = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
const currentChunks = chunksRef.current;
|
||||
const updatedChunks = currentChunks.filter(
|
||||
c =>
|
||||
c.text_knowledge_editor_chunk_uuid !==
|
||||
chunk.text_knowledge_editor_chunk_uuid,
|
||||
);
|
||||
if (!chunk.slice_id) {
|
||||
return;
|
||||
}
|
||||
deleteSlice(chunk.slice_id).then(() => {
|
||||
onChunksChange?.({
|
||||
chunks: updatedChunks,
|
||||
targetChunk: chunk,
|
||||
});
|
||||
});
|
||||
},
|
||||
[onChunksChange, deleteSlice],
|
||||
);
|
||||
|
||||
return {
|
||||
deleteChunk: handleDeleteChunk,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { updateLocalChunk } from '@/text-knowledge-editor/services/inner/chunk-op.service';
|
||||
import { useCreateChunk } from '@/text-knowledge-editor/hooks/inner/use-create-chunk';
|
||||
|
||||
export interface UseCreateLocalChunkProps {
|
||||
chunks: Chunk[];
|
||||
documentId: string;
|
||||
onChunksChange?: (chunks: Chunk[]) => void;
|
||||
onAddChunk?: (chunk: Chunk) => void;
|
||||
}
|
||||
|
||||
export const useCreateLocalChunk = ({
|
||||
chunks,
|
||||
documentId,
|
||||
onChunksChange,
|
||||
onAddChunk,
|
||||
}: UseCreateLocalChunkProps) => {
|
||||
const { createChunk } = useCreateChunk({
|
||||
documentId,
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理本地分片的创建操作
|
||||
*/
|
||||
const createLocalChunk = async (chunk: Chunk) => {
|
||||
if (!chunk.local_slice_id) {
|
||||
return;
|
||||
}
|
||||
const newChunk = await createChunk({
|
||||
content: chunk.content ?? '',
|
||||
sequence: chunk.sequence ?? '1',
|
||||
});
|
||||
const newChunks = updateLocalChunk({
|
||||
chunks,
|
||||
localChunkSliceId: chunk.local_slice_id,
|
||||
newChunk,
|
||||
});
|
||||
onAddChunk?.(newChunk);
|
||||
onChunksChange?.(newChunks);
|
||||
};
|
||||
|
||||
return {
|
||||
createLocalChunk,
|
||||
};
|
||||
};
|
||||
@@ -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 Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { deleteLocalChunk as deleteLocalChunkService } from '@/text-knowledge-editor/services/inner/chunk-op.service';
|
||||
|
||||
export interface UseDeleteLocalChunkProps {
|
||||
chunks: Chunk[];
|
||||
onChunksChange?: (chunks: Chunk[]) => void;
|
||||
}
|
||||
|
||||
export const useDeleteLocalChunk = ({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
}: UseDeleteLocalChunkProps) => {
|
||||
/**
|
||||
* 处理本地分片的删除操作
|
||||
*/
|
||||
const deleteLocalChunk = (chunk: Chunk) => {
|
||||
if (!chunk.local_slice_id) {
|
||||
return;
|
||||
}
|
||||
const newChunks = deleteLocalChunkService(chunks, chunk.local_slice_id);
|
||||
onChunksChange?.(newChunks);
|
||||
};
|
||||
|
||||
return {
|
||||
deleteLocalChunk,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { deleteRemoteChunk as deleteRemoteChunkService } from '@/text-knowledge-editor/services/inner/chunk-op.service';
|
||||
import { useDeleteChunk } from '@/text-knowledge-editor/hooks/inner/use-delete-chunk';
|
||||
|
||||
export interface UseDeleteRemoteChunkProps {
|
||||
chunks: Chunk[];
|
||||
onChunksChange?: (chunks: Chunk[]) => void;
|
||||
onDeleteChunk?: (chunk: Chunk) => void;
|
||||
}
|
||||
|
||||
export const useDeleteRemoteChunk = ({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
onDeleteChunk,
|
||||
}: UseDeleteRemoteChunkProps) => {
|
||||
const { deleteSlice } = useDeleteChunk();
|
||||
|
||||
/**
|
||||
* 处理远程分片的删除操作
|
||||
*/
|
||||
const deleteRemoteChunk = async (chunk: Chunk) => {
|
||||
if (!chunk.slice_id) {
|
||||
return;
|
||||
}
|
||||
await deleteSlice(chunk.slice_id);
|
||||
const newChunks = deleteRemoteChunkService(chunks, chunk.slice_id);
|
||||
onChunksChange?.(newChunks);
|
||||
onDeleteChunk?.(chunk);
|
||||
};
|
||||
|
||||
return {
|
||||
deleteRemoteChunk,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { useEditor, type Editor } from '@tiptap/react';
|
||||
import { type EditorProps } from '@tiptap/pm/view';
|
||||
import TableRow from '@tiptap/extension-table-row';
|
||||
import TableHeader from '@tiptap/extension-table-header';
|
||||
import TableCell from '@tiptap/extension-table-cell';
|
||||
import Table from '@tiptap/extension-table';
|
||||
import Image from '@tiptap/extension-image';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { getRenderHtmlContent } from '@/text-knowledge-editor/services/use-case/get-render-editor-content';
|
||||
import { getEditorContent } from '@/text-knowledge-editor/services/use-case/get-editor-content';
|
||||
|
||||
interface UseDocumentEditorProps {
|
||||
chunk: Chunk | null;
|
||||
editorProps?: EditorProps;
|
||||
onChange?: (chunk: Chunk) => void;
|
||||
}
|
||||
|
||||
export const useInitEditor = ({
|
||||
chunk,
|
||||
editorProps,
|
||||
onChange,
|
||||
}: UseDocumentEditorProps) => {
|
||||
// 创建编辑器实例
|
||||
const editor: Editor | null = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
hardBreak: {
|
||||
// 强制换行
|
||||
keepMarks: false,
|
||||
},
|
||||
paragraph: {
|
||||
// 配置段落,避免生成多余的空段落
|
||||
HTMLAttributes: {
|
||||
class: 'text-knowledge-tiptap-editor-paragraph',
|
||||
},
|
||||
},
|
||||
}),
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
}),
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
Image.configure({
|
||||
inline: false,
|
||||
allowBase64: true,
|
||||
}),
|
||||
],
|
||||
content: getRenderHtmlContent(chunk?.content || ''),
|
||||
parseOptions: {
|
||||
preserveWhitespace: 'full',
|
||||
},
|
||||
onUpdate: ({ editor: editorInstance }) => {
|
||||
if (!chunk || !editorInstance) {
|
||||
return;
|
||||
}
|
||||
const newContent = getEditorContent(editorInstance);
|
||||
onChange?.({
|
||||
...chunk,
|
||||
content: newContent,
|
||||
});
|
||||
},
|
||||
editorProps: {
|
||||
...editorProps,
|
||||
handlePaste(view, event, slice) {
|
||||
if (!editor) {
|
||||
return false;
|
||||
}
|
||||
const text = event.clipboardData?.getData('text/plain');
|
||||
|
||||
// 如果粘贴的纯文本中包含换行符
|
||||
if (text?.includes('\n')) {
|
||||
event.preventDefault(); // 阻止默认粘贴行为
|
||||
|
||||
const html = getRenderHtmlContent(text);
|
||||
|
||||
// 将转换后的 HTML 插入编辑器
|
||||
editor.chain().focus().insertContent(html).run();
|
||||
|
||||
return true; // 表示我们已处理
|
||||
}
|
||||
|
||||
return false; // 使用默认行为
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 当激活的分片改变时,更新编辑器内容
|
||||
useEffect(() => {
|
||||
if (!editor || !chunk) {
|
||||
return;
|
||||
}
|
||||
const htmlContent = getRenderHtmlContent(chunk.content || '');
|
||||
// 设置内容,保留换行符
|
||||
editor.commands.setContent(htmlContent || '', false, {
|
||||
preserveWhitespace: 'full',
|
||||
});
|
||||
}, [chunk, editor]);
|
||||
|
||||
return {
|
||||
editor,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { createLocalChunk } from '@/text-knowledge-editor/services/inner/chunk-op.service';
|
||||
|
||||
import { useDeleteChunk } from '../inner/use-delete-chunk';
|
||||
|
||||
interface UsePreviewContextMenuProps {
|
||||
chunks: Chunk[];
|
||||
documentId: string;
|
||||
onChunksChange?: (chunks: Chunk[]) => void;
|
||||
onActiveChunkChange?: (chunk: Chunk) => void;
|
||||
onAddChunk?: (chunk: Chunk) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export const usePreviewContextMenu = ({
|
||||
chunks,
|
||||
documentId,
|
||||
onChunksChange,
|
||||
onActiveChunkChange,
|
||||
onAddChunk,
|
||||
}: UsePreviewContextMenuProps) => {
|
||||
// 使用ref保存最新的chunks引用
|
||||
const chunksRef = useRef(chunks);
|
||||
const { deleteSlice } = useDeleteChunk();
|
||||
|
||||
// 每次props.chunks更新时,更新ref
|
||||
useEffect(() => {
|
||||
chunksRef.current = chunks;
|
||||
}, [chunks]);
|
||||
|
||||
// 激活特定分片的编辑模式
|
||||
const handleActivateEditMode = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
onActiveChunkChange?.(chunk);
|
||||
},
|
||||
[onActiveChunkChange],
|
||||
);
|
||||
|
||||
// 在特定分片前添加新分片
|
||||
const handleAddChunkBefore = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
const currentChunks = chunksRef.current;
|
||||
const index = currentChunks.findIndex(
|
||||
c =>
|
||||
c.text_knowledge_editor_chunk_uuid ===
|
||||
chunk.text_knowledge_editor_chunk_uuid,
|
||||
);
|
||||
const sequence =
|
||||
currentChunks.find(
|
||||
c =>
|
||||
c.text_knowledge_editor_chunk_uuid ===
|
||||
chunk.text_knowledge_editor_chunk_uuid,
|
||||
)?.sequence ?? '1';
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newChunk = createLocalChunk({
|
||||
sequence,
|
||||
});
|
||||
|
||||
const updatedChunks = [
|
||||
...currentChunks.slice(0, index),
|
||||
newChunk,
|
||||
...currentChunks.slice(index),
|
||||
];
|
||||
|
||||
onChunksChange?.(updatedChunks);
|
||||
|
||||
// 自动激活新分片的编辑模式
|
||||
onActiveChunkChange?.(newChunk);
|
||||
onAddChunk?.(newChunk);
|
||||
},
|
||||
[onChunksChange, onActiveChunkChange, documentId, onAddChunk],
|
||||
);
|
||||
|
||||
// 在特定分片后添加新分片
|
||||
const handleAddChunkAfter = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
const currentChunks = chunksRef.current;
|
||||
const index = currentChunks.findIndex(
|
||||
c =>
|
||||
c.text_knowledge_editor_chunk_uuid ===
|
||||
chunk.text_knowledge_editor_chunk_uuid,
|
||||
);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sequence =
|
||||
currentChunks.find(
|
||||
c =>
|
||||
c.text_knowledge_editor_chunk_uuid ===
|
||||
chunk.text_knowledge_editor_chunk_uuid,
|
||||
)?.sequence ?? '1';
|
||||
const newChunk = createLocalChunk({
|
||||
sequence: String(Number(sequence) + 1),
|
||||
});
|
||||
|
||||
const updatedChunks = [
|
||||
...currentChunks.slice(0, index + 1),
|
||||
newChunk,
|
||||
...currentChunks.slice(index + 1),
|
||||
];
|
||||
|
||||
// 自动激活新分片的编辑模式
|
||||
onActiveChunkChange?.(newChunk);
|
||||
onChunksChange?.(updatedChunks);
|
||||
onAddChunk?.(newChunk);
|
||||
},
|
||||
[onChunksChange, onActiveChunkChange, documentId, onAddChunk],
|
||||
);
|
||||
|
||||
// 删除特定分片
|
||||
const handleDeleteChunk = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
const currentChunks = chunksRef.current;
|
||||
const updatedChunks = currentChunks.filter(
|
||||
c =>
|
||||
c.text_knowledge_editor_chunk_uuid !==
|
||||
chunk.text_knowledge_editor_chunk_uuid,
|
||||
);
|
||||
if (!chunk.slice_id) {
|
||||
return;
|
||||
}
|
||||
deleteSlice(chunk.slice_id).then(() => {
|
||||
onChunksChange?.(updatedChunks);
|
||||
});
|
||||
},
|
||||
[onChunksChange, deleteSlice],
|
||||
);
|
||||
|
||||
return {
|
||||
handleActivateEditMode,
|
||||
handleAddChunkBefore,
|
||||
handleAddChunkAfter,
|
||||
handleDeleteChunk,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
|
||||
import { useUpdateRemoteChunk } from './use-update-remote-chunk';
|
||||
import { useDeleteRemoteChunk } from './use-delete-remote-chunk';
|
||||
import { useDeleteLocalChunk } from './use-delete-local-chunk';
|
||||
import { useCreateLocalChunk } from './use-create-local-chunk';
|
||||
|
||||
export interface UseSaveChunkProps {
|
||||
chunks: Chunk[];
|
||||
documentId: string;
|
||||
onChunksChange?: (chunks: Chunk[]) => void;
|
||||
onAddChunk?: (chunk: Chunk) => void;
|
||||
onUpdateChunk?: (chunk: Chunk) => void;
|
||||
onDeleteChunk?: (chunk: Chunk) => void;
|
||||
}
|
||||
|
||||
export const useSaveChunk = ({
|
||||
chunks,
|
||||
documentId,
|
||||
onAddChunk,
|
||||
onUpdateChunk,
|
||||
onChunksChange,
|
||||
onDeleteChunk,
|
||||
}: UseSaveChunkProps) => {
|
||||
const { createLocalChunk } = useCreateLocalChunk({
|
||||
chunks,
|
||||
documentId,
|
||||
onChunksChange,
|
||||
onAddChunk,
|
||||
});
|
||||
|
||||
const { updateRemoteChunk } = useUpdateRemoteChunk({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
onUpdateChunk,
|
||||
});
|
||||
|
||||
const { deleteLocalChunk } = useDeleteLocalChunk({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
});
|
||||
|
||||
const { deleteRemoteChunk } = useDeleteRemoteChunk({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
onDeleteChunk,
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理远程分片的保存逻辑
|
||||
*/
|
||||
const saveRemoteChunk = async (chunk: Chunk) => {
|
||||
if (chunk.content === '') {
|
||||
await deleteRemoteChunk(chunk);
|
||||
return;
|
||||
}
|
||||
await updateRemoteChunk(chunk);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理本地分片的保存逻辑
|
||||
*/
|
||||
const saveLocalChunk = async (chunk: Chunk) => {
|
||||
if (chunk.content === '') {
|
||||
deleteLocalChunk(chunk);
|
||||
} else {
|
||||
await createLocalChunk(chunk);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存分片的主函数
|
||||
*/
|
||||
const saveChunk = async (chunk: Chunk) => {
|
||||
if (!chunk.local_slice_id) {
|
||||
await saveRemoteChunk(chunk);
|
||||
return;
|
||||
}
|
||||
await saveLocalChunk(chunk);
|
||||
};
|
||||
|
||||
return {
|
||||
saveChunk,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { isEditorContentChange } from '@/text-knowledge-editor/services/use-case/is-editor-content-change';
|
||||
import { updateChunks } from '@/text-knowledge-editor/services/inner/chunk-op.service';
|
||||
import { useUpdateChunk } from '@/text-knowledge-editor/hooks/inner/use-update-chunk';
|
||||
|
||||
export interface UseUpdateRemoteChunkProps {
|
||||
chunks: Chunk[];
|
||||
onChunksChange?: (chunks: Chunk[]) => void;
|
||||
onUpdateChunk?: (chunk: Chunk) => void;
|
||||
}
|
||||
|
||||
export const useUpdateRemoteChunk = ({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
onUpdateChunk,
|
||||
}: UseUpdateRemoteChunkProps) => {
|
||||
const { updateSlice } = useUpdateChunk();
|
||||
|
||||
/**
|
||||
* 处理远程分片的更新操作
|
||||
*/
|
||||
const updateRemoteChunk = async (chunk: Chunk) => {
|
||||
if (!chunk.slice_id) {
|
||||
Toast.error('The slice ID does not exist. Please refresh the page');
|
||||
return;
|
||||
}
|
||||
if (!isEditorContentChange(chunks, chunk)) {
|
||||
onChunksChange?.(chunks);
|
||||
return;
|
||||
}
|
||||
await updateSlice(chunk.slice_id, chunk.content ?? '');
|
||||
const newChunks = updateChunks(chunks, chunk);
|
||||
onUpdateChunk?.(chunk);
|
||||
onChunksChange?.(newChunks);
|
||||
};
|
||||
|
||||
return {
|
||||
updateRemoteChunk,
|
||||
};
|
||||
};
|
||||
@@ -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 { type Chunk } from './types/chunk';
|
||||
export { DocumentEditor } from './features/editor';
|
||||
export { DocumentPreview } from './features/preview';
|
||||
export { useSaveChunk } from './hooks/use-case/use-save-chunk';
|
||||
export { useInitEditor } from './hooks/use-case/use-init-editor';
|
||||
export { EditorToolbar } from './features/editor-toolbar';
|
||||
export {
|
||||
LevelTextKnowledgeEditor,
|
||||
type LevelDocumentChunk,
|
||||
type LevelDocumentTree,
|
||||
} from './scenes/level';
|
||||
export { BaseTextKnowledgeEditor } from './scenes/base';
|
||||
export type { Editor } from '@tiptap/react';
|
||||
|
||||
// 新增组件导出
|
||||
export { HoverEditBar } from './features/hover-edit-bar/hover-edit-bar';
|
||||
export {
|
||||
EditAction,
|
||||
AddBeforeAction,
|
||||
AddAfterAction,
|
||||
DeleteAction,
|
||||
} from './features/hover-edit-bar-actions';
|
||||
|
||||
// 事件总线相关导出
|
||||
export {
|
||||
eventBus,
|
||||
createEventBus,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
type EventTypes,
|
||||
type EventTypeName,
|
||||
type EventHandler,
|
||||
} from './event';
|
||||
@@ -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 '@/text-knowledge-editor/features/editor-actions/upload-image';
|
||||
import {
|
||||
createEditorActionFeatureRegistry,
|
||||
type EditorActionRegistry,
|
||||
} from '@/text-knowledge-editor/features/editor-actions/registry';
|
||||
export const editorContextActionRegistry: EditorActionRegistry = (() => {
|
||||
const editorContextActionFeatureRegistry = createEditorActionFeatureRegistry(
|
||||
'editor-context-actions',
|
||||
);
|
||||
editorContextActionFeatureRegistry.registerSome([
|
||||
{
|
||||
type: 'upload-image',
|
||||
module: {
|
||||
Component: UploadImageMenu,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return editorContextActionFeatureRegistry;
|
||||
})();
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 {
|
||||
createHoverEditBarActionFeatureRegistry,
|
||||
type HoverEditBarActionRegistry,
|
||||
} from '@/text-knowledge-editor/features/hover-edit-bar-actions/registry';
|
||||
import { EditAction } from '@/text-knowledge-editor/features/hover-edit-bar-actions/edit-action';
|
||||
import { DeleteAction } from '@/text-knowledge-editor/features/hover-edit-bar-actions/delete-action';
|
||||
import { AddBeforeAction } from '@/text-knowledge-editor/features/hover-edit-bar-actions/add-before-action';
|
||||
import { AddAfterAction } from '@/text-knowledge-editor/features/hover-edit-bar-actions/add-after-action';
|
||||
export const hoverEditBarActionsContributes: HoverEditBarActionRegistry =
|
||||
(() => {
|
||||
const hoverEditBarActionFeatureRegistry =
|
||||
createHoverEditBarActionFeatureRegistry('hover-edit-bar-actions');
|
||||
hoverEditBarActionFeatureRegistry.registerSome([
|
||||
{
|
||||
type: 'edit',
|
||||
module: {
|
||||
Component: EditAction,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'add-before',
|
||||
module: {
|
||||
Component: AddBeforeAction,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'add-after',
|
||||
module: {
|
||||
Component: AddAfterAction,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
module: {
|
||||
Component: DeleteAction,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return hoverEditBarActionFeatureRegistry;
|
||||
})();
|
||||
@@ -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 { BaseTextKnowledgeEditor } from './main';
|
||||
export { type DocumentChunk } from './types/base-document';
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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 { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useSaveChunk } from '@/text-knowledge-editor/hooks/use-case/use-save-chunk';
|
||||
import { useInitEditor } from '@/text-knowledge-editor/hooks/use-case/use-init-editor';
|
||||
import { useEventListener } from '@/text-knowledge-editor/event';
|
||||
|
||||
import { DocumentPreview } from '../../features/preview';
|
||||
import { DocumentEditor } from '../../features/editor';
|
||||
import { type DocumentChunk } from './types/base-document';
|
||||
import { previewContextMenuItemsContributes } from './preview-context-menu-items-contributes';
|
||||
import { hoverEditBarActionsContributes } from './hover-edit-bar-actions-contributes';
|
||||
import { editorContextActionRegistry } from './editor-context-actions-contributes';
|
||||
export interface BaseTextKnowledgeEditorProps {
|
||||
chunks: DocumentChunk[];
|
||||
documentId: string;
|
||||
readonly?: boolean;
|
||||
onChange?: (chunks: DocumentChunk[]) => void;
|
||||
onAddChunk?: (chunk: DocumentChunk) => void;
|
||||
onDeleteChunk?: (chunk: DocumentChunk) => void;
|
||||
}
|
||||
|
||||
export const BaseTextKnowledgeEditor = ({
|
||||
chunks: initialChunks,
|
||||
documentId,
|
||||
readonly = false,
|
||||
onChange,
|
||||
onAddChunk,
|
||||
onDeleteChunk,
|
||||
}: BaseTextKnowledgeEditorProps) => {
|
||||
const [chunks, setChunks] = useState<DocumentChunk[]>(initialChunks);
|
||||
const [activeChunk, setActiveChunk] = useState<DocumentChunk | null>(null);
|
||||
|
||||
// 使用编辑器核心功能
|
||||
const { editor } = useInitEditor({
|
||||
chunk: activeChunk,
|
||||
});
|
||||
|
||||
// 退出新增分片功能
|
||||
const { saveChunk } = useSaveChunk({
|
||||
chunks,
|
||||
documentId,
|
||||
onChunksChange: newChunks => {
|
||||
onChange?.(newChunks);
|
||||
setActiveChunk(null);
|
||||
},
|
||||
onAddChunk,
|
||||
onDeleteChunk,
|
||||
});
|
||||
|
||||
// 监听右键菜单事件
|
||||
useEventListener(
|
||||
'previewContextMenuItemAction',
|
||||
useCallback(
|
||||
({ type, newChunk, chunks: newChunks, targetChunk }) => {
|
||||
if (type === 'add-after') {
|
||||
newChunk && setActiveChunk(newChunk);
|
||||
newChunks && setChunks(newChunks);
|
||||
}
|
||||
if (type === 'add-before') {
|
||||
newChunk && setActiveChunk(newChunk);
|
||||
newChunks && setChunks(newChunks);
|
||||
}
|
||||
if (type === 'delete') {
|
||||
onDeleteChunk?.(targetChunk);
|
||||
newChunks && onChange?.(newChunks);
|
||||
}
|
||||
if (type === 'edit') {
|
||||
setActiveChunk(targetChunk);
|
||||
}
|
||||
},
|
||||
[onDeleteChunk, onChange],
|
||||
),
|
||||
);
|
||||
|
||||
// 监听悬浮编辑栏事件
|
||||
useEventListener(
|
||||
'hoverEditBarAction',
|
||||
useCallback(
|
||||
({ type, targetChunk, chunks: newChunks, newChunk }) => {
|
||||
if (type === 'add-after') {
|
||||
newChunk && setActiveChunk(newChunk);
|
||||
newChunks && setChunks(newChunks);
|
||||
}
|
||||
if (type === 'add-before') {
|
||||
newChunk && setActiveChunk(newChunk);
|
||||
newChunks && setChunks(newChunks);
|
||||
}
|
||||
if (type === 'delete') {
|
||||
onDeleteChunk?.(targetChunk);
|
||||
newChunks && onChange?.(newChunks);
|
||||
}
|
||||
if (type === 'edit') {
|
||||
setActiveChunk(targetChunk);
|
||||
}
|
||||
},
|
||||
[onDeleteChunk, onChange],
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setChunks(initialChunks);
|
||||
}, [initialChunks]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{chunks.map(chunk => (
|
||||
<div key={chunk.text_knowledge_editor_chunk_uuid}>
|
||||
{(() => {
|
||||
if (
|
||||
chunk.text_knowledge_editor_chunk_uuid ===
|
||||
activeChunk?.text_knowledge_editor_chunk_uuid &&
|
||||
activeChunk
|
||||
) {
|
||||
return (
|
||||
<DocumentEditor
|
||||
editor={editor}
|
||||
editorContextMenuItemsRegistry={editorContextActionRegistry}
|
||||
onBlur={content => {
|
||||
saveChunk({
|
||||
...activeChunk,
|
||||
content,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DocumentPreview
|
||||
chunk={chunk}
|
||||
chunks={chunks}
|
||||
readonly={readonly}
|
||||
onActivateEditMode={setActiveChunk}
|
||||
hoverEditBarActionsRegistry={hoverEditBarActionsContributes}
|
||||
previewContextMenuItemsRegistry={
|
||||
previewContextMenuItemsContributes
|
||||
}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 {
|
||||
createPreviewContextMenuItemFeatureRegistry,
|
||||
type PreviewContextMenuItemRegistry,
|
||||
} from '@/text-knowledge-editor/features/preview-context-menu-items/registry';
|
||||
import { EditAction } from '@/text-knowledge-editor/features/preview-context-menu-items/edit-action';
|
||||
import { DeleteAction } from '@/text-knowledge-editor/features/preview-context-menu-items/delete-action';
|
||||
import { AddBeforeAction } from '@/text-knowledge-editor/features/preview-context-menu-items/add-before-action';
|
||||
import { AddAfterAction } from '@/text-knowledge-editor/features/preview-context-menu-items/add-after-action';
|
||||
export const previewContextMenuItemsContributes: PreviewContextMenuItemRegistry =
|
||||
(() => {
|
||||
const previewContextMenuItemFeatureRegistry =
|
||||
createPreviewContextMenuItemFeatureRegistry('preview-context-menu-items');
|
||||
previewContextMenuItemFeatureRegistry.registerSome([
|
||||
{
|
||||
type: 'edit',
|
||||
module: {
|
||||
Component: EditAction,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'add-before',
|
||||
module: {
|
||||
Component: AddBeforeAction,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'add-after',
|
||||
module: {
|
||||
Component: AddAfterAction,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
module: {
|
||||
Component: DeleteAction,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return previewContextMenuItemFeatureRegistry;
|
||||
})();
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type SliceInfo } from '@coze-arch/bot-api/knowledge';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
|
||||
export type DocumentChunk = Chunk & SliceInfo;
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 '@/text-knowledge-editor/features/editor-actions/upload-image';
|
||||
import {
|
||||
createEditorActionFeatureRegistry,
|
||||
type EditorActionRegistry,
|
||||
} from '@/text-knowledge-editor/features/editor-actions/registry';
|
||||
export const editorActionRegistry: EditorActionRegistry = (() => {
|
||||
const editorActionFeatureRegistry =
|
||||
createEditorActionFeatureRegistry('editor-actions');
|
||||
editorActionFeatureRegistry.registerSome([
|
||||
{
|
||||
type: 'upload-image',
|
||||
module: {
|
||||
Component: UploadImageMenu,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return editorActionFeatureRegistry;
|
||||
})();
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type LevelDocumentTreeNode } from '../../types/level-document';
|
||||
|
||||
export interface ActiveChunkInfo {
|
||||
chunk: LevelDocumentTreeNode | null;
|
||||
renderLevel: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理文档中活动的chunk
|
||||
* 使用renderLevel字段来唯一标识chunk的渲染位置
|
||||
*/
|
||||
export const useActiveChunk = () => {
|
||||
// 存储活动的chunk和它的renderLevel
|
||||
const [activeChunkInfo, setActiveChunkInfo] = useState<ActiveChunkInfo>({
|
||||
chunk: null,
|
||||
renderLevel: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* 清除活动chunk信息
|
||||
*/
|
||||
const clearActiveChunk = () => {
|
||||
setActiveChunkInfo({
|
||||
chunk: null,
|
||||
renderLevel: null,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置活动chunk和它的renderLevel
|
||||
* 在用户交互(如双击)时使用
|
||||
*/
|
||||
const setActiveChunkWithLevel = (chunk: LevelDocumentTreeNode) => {
|
||||
if (!chunk.renderLevel) {
|
||||
console.warn('Chunk does not have renderLevel field', chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveChunkInfo({
|
||||
chunk,
|
||||
renderLevel: chunk.renderLevel,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查给定的chunk是否是当前活动的chunk
|
||||
*/
|
||||
const isActiveChunk = (renderLevel: string | undefined) => {
|
||||
if (!renderLevel) {
|
||||
return false;
|
||||
}
|
||||
return renderLevel === activeChunkInfo.renderLevel;
|
||||
};
|
||||
|
||||
return {
|
||||
activeChunkInfo,
|
||||
clearActiveChunk,
|
||||
setActiveChunkWithLevel,
|
||||
isActiveChunk,
|
||||
};
|
||||
};
|
||||
@@ -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 { useState } from 'react';
|
||||
|
||||
import { type LevelDocumentChunk } from '../../types/level-document';
|
||||
|
||||
export interface ActiveChunkInfo {
|
||||
chunk: LevelDocumentChunk | null;
|
||||
renderPath: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理文档中具有相同ID的chunk的渲染路径
|
||||
* 通过为每个chunk实例分配唯一的渲染路径,解决重复ID的问题
|
||||
*/
|
||||
export const useChunkRenderPath = () => {
|
||||
// 存储活动的chunk和它的渲染路径
|
||||
const [activeChunkInfo, setActiveChunkInfo] = useState<ActiveChunkInfo>({
|
||||
chunk: null,
|
||||
renderPath: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* 设置活动chunk,但不设置渲染路径
|
||||
* 通常在外部逻辑中使用,如usePreviewContextMenu
|
||||
*/
|
||||
const setActiveChunk = (chunk: LevelDocumentChunk | null) => {
|
||||
setActiveChunkInfo(prev => ({
|
||||
...prev,
|
||||
chunk,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除活动chunk信息
|
||||
*/
|
||||
const clearActiveChunk = () => {
|
||||
setActiveChunkInfo({
|
||||
chunk: null,
|
||||
renderPath: null,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置活动chunk和它的渲染路径
|
||||
* 在用户交互(如双击)时使用
|
||||
*/
|
||||
const setActiveChunkWithPath = (
|
||||
chunk: LevelDocumentChunk,
|
||||
renderPath: string,
|
||||
) => {
|
||||
setActiveChunkInfo({
|
||||
chunk,
|
||||
renderPath,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查给定的chunk和渲染路径是否匹配当前活动的chunk
|
||||
*/
|
||||
const isActiveChunk = (chunkId: string, renderPath: string) =>
|
||||
chunkId === activeChunkInfo.chunk?.text_knowledge_editor_chunk_uuid &&
|
||||
renderPath === activeChunkInfo.renderPath;
|
||||
|
||||
/**
|
||||
* 为chunk生成唯一的渲染路径
|
||||
*/
|
||||
const generateRenderPath = (basePath: string, chunkId: string) =>
|
||||
`${basePath}-${chunkId}`;
|
||||
|
||||
return {
|
||||
activeChunkInfo,
|
||||
setActiveChunk,
|
||||
clearActiveChunk,
|
||||
setActiveChunkWithPath,
|
||||
isActiveChunk,
|
||||
generateRenderPath,
|
||||
};
|
||||
};
|
||||
@@ -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 { useScrollToSelection } from './use-scroll-to-selection';
|
||||
@@ -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 { useEffect } from 'react';
|
||||
|
||||
import { createLocateChunkId } from '../../services/locate-segment';
|
||||
|
||||
/**
|
||||
* 滚动到选中的元素
|
||||
* @param selectionIDs 选中的元素ID数组
|
||||
*/
|
||||
export const useScrollToSelection = (selectionIDs?: string[]) => {
|
||||
useEffect(() => {
|
||||
if (selectionIDs?.length) {
|
||||
const firstSelectedId = selectionIDs[0];
|
||||
const element = document.getElementById(
|
||||
createLocateChunkId(firstSelectedId),
|
||||
);
|
||||
element?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, [selectionIDs]);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createHoverEditBarActionFeatureRegistry,
|
||||
type HoverEditBarActionRegistry,
|
||||
} from '@/text-knowledge-editor/features/hover-edit-bar-actions/registry';
|
||||
import { EditAction } from '@/text-knowledge-editor/features/hover-edit-bar-actions/edit-action';
|
||||
import { DeleteAction } from '@/text-knowledge-editor/features/hover-edit-bar-actions/delete-action';
|
||||
export const hoverEditBarActionsContributes: HoverEditBarActionRegistry =
|
||||
(() => {
|
||||
const hoverEditBarActionFeatureRegistry =
|
||||
createHoverEditBarActionFeatureRegistry('hover-edit-bar-actions');
|
||||
hoverEditBarActionFeatureRegistry.registerSome([
|
||||
{
|
||||
type: 'edit',
|
||||
module: {
|
||||
Component: EditAction,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
module: {
|
||||
Component: DeleteAction,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return hoverEditBarActionFeatureRegistry;
|
||||
})();
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { LevelTextKnowledgeEditor } from './main';
|
||||
export { withTitle } from './services/chunk-op.service';
|
||||
export {
|
||||
type LevelDocumentChunk,
|
||||
type LevelDocumentTree,
|
||||
} from './types/level-document';
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* 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, useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { type Editor } from '@tiptap/react';
|
||||
|
||||
import { updateChunkContent } from '@/text-knowledge-editor/services/inner/document-editor.service';
|
||||
import { useSaveChunk } from '@/text-knowledge-editor/hooks/use-case/use-save-chunk';
|
||||
import { useInitEditor } from '@/text-knowledge-editor/hooks/use-case/use-init-editor';
|
||||
import { useEventListener } from '@/text-knowledge-editor/event';
|
||||
import { TitleChunkPreview } from '@/text-knowledge-editor/components/preview-chunk/title';
|
||||
import { ImageChunkPreview } from '@/text-knowledge-editor/components/preview-chunk/image';
|
||||
|
||||
import { editorContextActionRegistry } from '../base/editor-context-actions-contributes';
|
||||
import { DocumentPreview } from '../../features/preview';
|
||||
import { DocumentEditor } from '../../features/editor';
|
||||
import {
|
||||
type LevelDocumentChunk,
|
||||
type LevelDocumentTreeNode,
|
||||
} from './types/level-document';
|
||||
import { createLocateChunkId } from './services/locate-segment';
|
||||
import { getLevelDocumentTree } from './services/chunk-op.service';
|
||||
import { previewContextMenuItemsContributes } from './preview-context-menu-items-contributes';
|
||||
import { hoverEditBarActionsContributes } from './hover-edit-bar-actions-contributes';
|
||||
import { useScrollToSelection } from './hooks/use-case';
|
||||
import {
|
||||
useActiveChunk,
|
||||
type ActiveChunkInfo,
|
||||
} from './hooks/inner/use-active-chunk';
|
||||
|
||||
export interface LevelTextKnowledgeEditorProps {
|
||||
documentId: string;
|
||||
chunks: LevelDocumentChunk[];
|
||||
readonly?: boolean;
|
||||
selectionIDs?: string[];
|
||||
onChange?: (chunks: LevelDocumentChunk[]) => void;
|
||||
onDeleteChunk?: (chunk: LevelDocumentChunk) => void;
|
||||
}
|
||||
export const LevelTextKnowledgeEditor: React.FC<
|
||||
LevelTextKnowledgeEditorProps
|
||||
> = ({
|
||||
readonly,
|
||||
chunks: initialChunks,
|
||||
documentId,
|
||||
selectionIDs,
|
||||
onChange,
|
||||
onDeleteChunk,
|
||||
}) => {
|
||||
const [chunks, setChunks] = useState<LevelDocumentChunk[]>(initialChunks);
|
||||
const levelDocumentTree = useMemo(
|
||||
() => getLevelDocumentTree(chunks),
|
||||
[chunks],
|
||||
);
|
||||
|
||||
// 使用活动chunk hook
|
||||
const {
|
||||
activeChunkInfo,
|
||||
clearActiveChunk,
|
||||
setActiveChunkWithLevel,
|
||||
isActiveChunk,
|
||||
} = useActiveChunk();
|
||||
|
||||
// 使用编辑器核心功能
|
||||
const { editor } = useInitEditor({
|
||||
chunk: activeChunkInfo.chunk,
|
||||
});
|
||||
|
||||
// 分片功能
|
||||
const { saveChunk } = useSaveChunk({
|
||||
chunks: initialChunks,
|
||||
documentId,
|
||||
onChunksChange: newChunks => {
|
||||
onChange?.(newChunks as LevelDocumentChunk[]);
|
||||
clearActiveChunk();
|
||||
},
|
||||
onDeleteChunk: chunk => {
|
||||
onDeleteChunk?.(chunk as LevelDocumentChunk);
|
||||
},
|
||||
});
|
||||
|
||||
// 使用滚动到选中元素的hook
|
||||
useScrollToSelection(selectionIDs);
|
||||
|
||||
// 监听右键菜单事件
|
||||
useEventListener(
|
||||
'previewContextMenuItemAction',
|
||||
useCallback(({ type, targetChunk, chunks: newChunks }) => {
|
||||
if (type === 'delete') {
|
||||
onDeleteChunk?.(targetChunk as LevelDocumentChunk);
|
||||
newChunks && onChange?.(newChunks as LevelDocumentChunk[]);
|
||||
}
|
||||
if (type === 'edit') {
|
||||
setActiveChunkWithLevel(targetChunk as LevelDocumentTreeNode);
|
||||
}
|
||||
}, []),
|
||||
);
|
||||
|
||||
// 监听悬浮编辑栏事件
|
||||
useEventListener(
|
||||
'hoverEditBarAction',
|
||||
useCallback(({ type, targetChunk, chunks: newChunks }) => {
|
||||
if (type === 'delete') {
|
||||
onDeleteChunk?.(targetChunk as LevelDocumentChunk);
|
||||
newChunks && onChange?.(newChunks as LevelDocumentChunk[]);
|
||||
}
|
||||
if (type === 'edit') {
|
||||
setActiveChunkWithLevel(targetChunk as LevelDocumentTreeNode);
|
||||
}
|
||||
}, []),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setChunks(initialChunks);
|
||||
}, [initialChunks]);
|
||||
|
||||
if (!levelDocumentTree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full h-full">
|
||||
{levelDocumentTree.map(item => (
|
||||
<RenderContent
|
||||
key={item.text_knowledge_editor_chunk_uuid}
|
||||
editor={editor}
|
||||
chunks={chunks}
|
||||
levelDocumentTree={item}
|
||||
activeChunkInfo={activeChunkInfo}
|
||||
isActiveChunk={isActiveChunk}
|
||||
setActiveChunkWithLevel={setActiveChunkWithLevel}
|
||||
selectionIDs={selectionIDs}
|
||||
readonly={readonly}
|
||||
saveChunk={saveChunk}
|
||||
setChunks={setChunks}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
interface SingleContentProps {
|
||||
editor: Editor | null;
|
||||
readonly?: boolean;
|
||||
chunks: LevelDocumentChunk[];
|
||||
levelDocumentTree?: LevelDocumentTreeNode;
|
||||
selectionIDs?: string[];
|
||||
activeChunkInfo: ActiveChunkInfo;
|
||||
isActiveChunk: (renderLevel: string | undefined) => boolean;
|
||||
setActiveChunkWithLevel: (chunk: LevelDocumentTreeNode) => void;
|
||||
setChunks: (chunks: LevelDocumentChunk[]) => void;
|
||||
saveChunk: (chunk: LevelDocumentChunk) => void;
|
||||
}
|
||||
|
||||
const RenderContent = ({
|
||||
levelDocumentTree,
|
||||
selectionIDs,
|
||||
chunks,
|
||||
activeChunkInfo,
|
||||
isActiveChunk,
|
||||
setActiveChunkWithLevel,
|
||||
readonly,
|
||||
editor,
|
||||
saveChunk,
|
||||
setChunks,
|
||||
}: SingleContentProps) => {
|
||||
const childrenContent = levelDocumentTree?.children?.length ? (
|
||||
<div className="flex w-full">
|
||||
<div className="w-6 shrink-0"></div>
|
||||
<div className="flex flex-col w-[calc(100%-24px)] gap-2">
|
||||
{levelDocumentTree.children.map(item => (
|
||||
<RenderContent
|
||||
key={item.text_knowledge_editor_chunk_uuid}
|
||||
editor={editor}
|
||||
chunks={chunks}
|
||||
saveChunk={saveChunk}
|
||||
levelDocumentTree={item}
|
||||
selectionIDs={selectionIDs}
|
||||
activeChunkInfo={activeChunkInfo}
|
||||
isActiveChunk={isActiveChunk}
|
||||
setActiveChunkWithLevel={setActiveChunkWithLevel}
|
||||
readonly={readonly}
|
||||
setChunks={setChunks}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
if (!levelDocumentTree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={levelDocumentTree.text_knowledge_editor_chunk_uuid}
|
||||
className="flex flex-col gap-2 w-full"
|
||||
>
|
||||
{['title', 'section-title', 'page-title'].includes(
|
||||
levelDocumentTree.type,
|
||||
) ? (
|
||||
<TitleChunkPreview
|
||||
title={levelDocumentTree.text}
|
||||
id={createLocateChunkId(levelDocumentTree.id)}
|
||||
/>
|
||||
) : null}
|
||||
{[
|
||||
'section-text',
|
||||
'text',
|
||||
'header-footer',
|
||||
'caption',
|
||||
'header',
|
||||
'footer',
|
||||
'formula',
|
||||
'footnote',
|
||||
'toc',
|
||||
'code',
|
||||
'table',
|
||||
].includes(levelDocumentTree.type) ? (
|
||||
<div key={levelDocumentTree.text_knowledge_editor_chunk_uuid}>
|
||||
{(() => {
|
||||
// 检查这个chunk是否是当前活动的chunk,使用renderLevel字段
|
||||
const chunkIsActive = isActiveChunk(levelDocumentTree.renderLevel);
|
||||
|
||||
if (chunkIsActive) {
|
||||
return (
|
||||
<DocumentEditor
|
||||
editor={editor}
|
||||
editorContextMenuItemsRegistry={editorContextActionRegistry}
|
||||
onBlur={newContent => {
|
||||
saveChunk?.(
|
||||
updateChunkContent(
|
||||
levelDocumentTree,
|
||||
newContent,
|
||||
) as unknown as LevelDocumentChunk,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DocumentPreview
|
||||
chunk={levelDocumentTree}
|
||||
readonly={readonly}
|
||||
chunks={chunks}
|
||||
locateId={createLocateChunkId(levelDocumentTree.id)}
|
||||
hoverEditBarActionsRegistry={hoverEditBarActionsContributes}
|
||||
previewContextMenuItemsRegistry={
|
||||
previewContextMenuItemsContributes
|
||||
}
|
||||
onActivateEditMode={activedChunk => {
|
||||
// 设置活动chunk,使用chunk自身的renderLevel
|
||||
setActiveChunkWithLevel(
|
||||
activedChunk as LevelDocumentTreeNode,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{['image'].includes(levelDocumentTree.type) ? (
|
||||
<ImageChunkPreview
|
||||
base64={levelDocumentTree.image_detail.base64 ?? ''}
|
||||
htmlText={levelDocumentTree.html_text || levelDocumentTree.text}
|
||||
link={levelDocumentTree.image_detail.links?.[0] ?? ''}
|
||||
caption={levelDocumentTree.image_detail.caption ?? ''}
|
||||
locateId={createLocateChunkId(levelDocumentTree.id)}
|
||||
selected={selectionIDs?.includes(levelDocumentTree.id.toString())}
|
||||
/>
|
||||
) : null}
|
||||
{childrenContent}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createPreviewContextMenuItemFeatureRegistry,
|
||||
type PreviewContextMenuItemRegistry,
|
||||
} from '@/text-knowledge-editor/features/preview-context-menu-items/registry';
|
||||
import { EditAction } from '@/text-knowledge-editor/features/preview-context-menu-items/edit-action';
|
||||
import { DeleteAction } from '@/text-knowledge-editor/features/preview-context-menu-items/delete-action';
|
||||
export const previewContextMenuItemsContributes: PreviewContextMenuItemRegistry =
|
||||
(() => {
|
||||
const previewContextMenuItemFeatureRegistry =
|
||||
createPreviewContextMenuItemFeatureRegistry('preview-context-menu-items');
|
||||
previewContextMenuItemFeatureRegistry.registerSome([
|
||||
{
|
||||
type: 'edit',
|
||||
module: {
|
||||
Component: EditAction,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
module: {
|
||||
Component: DeleteAction,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return previewContextMenuItemFeatureRegistry;
|
||||
})();
|
||||
@@ -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 {
|
||||
type LevelDocumentTree,
|
||||
type LevelDocumentChunk,
|
||||
} from '../types/level-document';
|
||||
|
||||
export const getLevelDocumentTree = (
|
||||
segments: LevelDocumentChunk[],
|
||||
): LevelDocumentTree | [] => {
|
||||
const root = segments.find(f => f.parent === -1 && f.type === 'title');
|
||||
|
||||
if (!root) {
|
||||
return segments.map(item => ({
|
||||
...item,
|
||||
text: item.content ?? '',
|
||||
parent: item.parent?.toString(),
|
||||
children: [],
|
||||
renderLevel: `root-${item.text_knowledge_editor_chunk_uuid}`,
|
||||
}));
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
...root,
|
||||
text: root.content ?? '',
|
||||
parent: root.parent?.toString(),
|
||||
children: getChildren(root, segments, 'root'),
|
||||
renderLevel: `root-${root.text_knowledge_editor_chunk_uuid}`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/** Segments to TreeNodes */
|
||||
const getChildren = (
|
||||
target: LevelDocumentChunk,
|
||||
list: LevelDocumentChunk[],
|
||||
parentPath: string,
|
||||
): LevelDocumentTree =>
|
||||
(target.children ?? []).reduce<LevelDocumentTree>((acc, cur, index) => {
|
||||
const found = list.find(f => f.id.toString() === cur.toString());
|
||||
if (found) {
|
||||
const currentPath = `${parentPath}-${index}`;
|
||||
const renderLevel = `${currentPath}-${found.text_knowledge_editor_chunk_uuid}`;
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
...found,
|
||||
parent: found.parent?.toString(),
|
||||
text: found.content ?? '',
|
||||
children: getChildren(found, list, currentPath),
|
||||
renderLevel,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [...acc];
|
||||
}
|
||||
}, []);
|
||||
|
||||
/** 兜底情况,如果 idp 的结果没有标题,则补上标题 */
|
||||
export const withTitle = (
|
||||
segments: LevelDocumentChunk[],
|
||||
title?: string,
|
||||
): LevelDocumentChunk[] =>
|
||||
segments.map(item => {
|
||||
if (item.parent === -1 && item.type === 'title' && !item.text) {
|
||||
return {
|
||||
...item,
|
||||
text: title ?? '',
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
@@ -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 const createLocateChunkId = (id: number | string) => `segment-${id}`;
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { type ILevelSegment } from '@coze-data/knowledge-stores';
|
||||
|
||||
import { type Chunk } from '@coze-data/knowledge-common-components/text-knowledge-editor';
|
||||
|
||||
export type LevelDocumentChunk = ILevelSegment & Chunk;
|
||||
|
||||
export type LevelDocumentTreeNode = Omit<ILevelSegment, 'children' | 'parent'> &
|
||||
Chunk & {
|
||||
parent?: string;
|
||||
children?: LevelDocumentTreeNode[];
|
||||
renderLevel?: string; // 用于唯一标识chunk的渲染路径
|
||||
};
|
||||
|
||||
export type LevelDocumentTree = LevelDocumentTreeNode[];
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { SliceStatus } from '@coze-arch/bot-api/knowledge';
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
|
||||
export const createLocalChunk = (props: { sequence: string }): Chunk => {
|
||||
const localSliceId = nanoid();
|
||||
return {
|
||||
text_knowledge_editor_chunk_uuid: nanoid(),
|
||||
local_slice_id: localSliceId,
|
||||
slice_id: localSliceId,
|
||||
sequence: props.sequence,
|
||||
content: '',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新本地分片
|
||||
*/
|
||||
export const updateLocalChunk = ({
|
||||
chunks,
|
||||
localChunkSliceId,
|
||||
newChunk,
|
||||
}: {
|
||||
chunks: Chunk[];
|
||||
localChunkSliceId: string;
|
||||
newChunk: Chunk;
|
||||
}): Chunk[] =>
|
||||
chunks.map(c => (c.local_slice_id === localChunkSliceId ? newChunk : c));
|
||||
|
||||
// 删除本地分片
|
||||
export const deleteLocalChunk = (
|
||||
chunks: Chunk[],
|
||||
localChunkSliceId: string,
|
||||
): Chunk[] => chunks.filter(c => c.local_slice_id !== localChunkSliceId);
|
||||
|
||||
/**
|
||||
* 更新文档分片内容
|
||||
*/
|
||||
export const updateChunkContent = (chunk: Chunk, content: string): Chunk => ({
|
||||
...chunk,
|
||||
content,
|
||||
});
|
||||
|
||||
// 删除远程分片
|
||||
export const deleteRemoteChunk = (
|
||||
chunks: Chunk[],
|
||||
remoteChunkSliceId: string,
|
||||
): Chunk[] => chunks.filter(c => c.slice_id !== remoteChunkSliceId);
|
||||
|
||||
/**
|
||||
* 更新chunks
|
||||
*/
|
||||
export const updateChunks = (chunks: Chunk[], chunk: Chunk): Chunk[] =>
|
||||
chunks.map(c => (c.slice_id === chunk.slice_id ? chunk : c));
|
||||
|
||||
/**
|
||||
* 创建远程分片
|
||||
*/
|
||||
export const createRemoteChunk = (props: {
|
||||
sequence: string;
|
||||
content: string;
|
||||
slice_id?: string;
|
||||
}): Chunk => ({
|
||||
text_knowledge_editor_chunk_uuid: nanoid(),
|
||||
status: SliceStatus.FinishVectoring,
|
||||
content: props.content,
|
||||
sequence: props.sequence,
|
||||
slice_id: props.slice_id ?? '',
|
||||
});
|
||||
@@ -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 Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
|
||||
/**
|
||||
* 更新文档分片内容
|
||||
*/
|
||||
export const updateChunkContent = (chunk: Chunk, content: string): Chunk => ({
|
||||
...chunk,
|
||||
content,
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新chunks
|
||||
*/
|
||||
export const updateChunks = (chunks: Chunk[], chunk: Chunk): Chunk[] =>
|
||||
chunks.map(c => (c.slice_id === chunk.slice_id ? chunk : c));
|
||||
|
||||
/**
|
||||
* 获取激活的分片
|
||||
*/
|
||||
export const getActiveChunk = (
|
||||
chunks: Chunk[],
|
||||
activeChunkId: string | undefined,
|
||||
) => {
|
||||
if (!activeChunkId) {
|
||||
return undefined;
|
||||
}
|
||||
return chunks.find(chunk => chunk.slice_id === activeChunkId) || undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理编辑器输出的HTML内容
|
||||
* 移除不必要的外层<p>标签,保持与原始内容格式一致
|
||||
*/
|
||||
export const processEditorContent = (content: string): string => {
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 如果内容被<p>标签包裹,并且只有一个<p>标签
|
||||
const singleParagraphMatch = content.match(/^<p>(.*?)<\/p>$/s);
|
||||
if (singleParagraphMatch) {
|
||||
return singleParagraphMatch[1];
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
@@ -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 classNames from 'classnames';
|
||||
|
||||
export const getEditorImgClassname = () =>
|
||||
classNames(
|
||||
'[&_img]:relative [&_img]:block',
|
||||
'[&_img]:my-3 [&_img]:bg-white [&_img]:rounded-md',
|
||||
'[&_img]:max-w-[610px] [&_img]:max-h-[367px] [&_img]:w-auto',
|
||||
'[&_img.ProseMirror-selectednode]:outline-2 [&_img.ProseMirror-selectednode]:outline [&_img.ProseMirror-selectednode]:outline-blue-500',
|
||||
'[&_img.ProseMirror-selectednode]:shadow-md',
|
||||
);
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const getEditorTableClassname = () =>
|
||||
classNames(
|
||||
// 表格样式
|
||||
'[&_table]:border-collapse [&_table]:m-0 [&_table]:w-full [&_table]:table-fixed [&_table]:overflow-hidden [&_table]:text-[0.9em]',
|
||||
'[&_table_td]:border [&_table_th]:border [&_table_td]:border-[#ddd] [&_table_th]:border-[#ddd]',
|
||||
'[&_table_td]:p-2 [&_table_th]:p-2',
|
||||
'[&_table_td]:relative [&_table_th]:relative',
|
||||
'[&_table_td]:align-top [&_table_th]:align-top',
|
||||
'[&_table_td]:box-border [&_table_th]:box-border',
|
||||
'[&_table_td]:border-solid [&_table_th]:border-solid',
|
||||
'[&_table_td]:min-w-[100px] [&_table_th]:min-w-[100px]',
|
||||
);
|
||||
@@ -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 classNames from 'classnames';
|
||||
|
||||
export const getEditorWordsCls = () =>
|
||||
classNames(
|
||||
// 换行
|
||||
'[&_p]:break-words [&_p]:whitespace-pre-wrap',
|
||||
// 保留所有空格和换行符
|
||||
'[&_.ProseMirror_*]:break-words [&_.ProseMirror_*]:whitespace-pre-wrap',
|
||||
// 段落
|
||||
'[&_.editor-paragraph]:min-h-[1.5em] [&_.editor-paragraph]:leading-normal',
|
||||
// 空段落
|
||||
'[&_.editor-paragraph:empty]:min-h-[1.5em] [&_.editor-paragraph:empty]:block',
|
||||
);
|
||||
@@ -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 { type Editor } from '@tiptap/react';
|
||||
|
||||
/**
|
||||
* 获取编辑器内容
|
||||
* 返回用户真实编辑的内容,移除TipTap自动添加的外层<p>标签
|
||||
*/
|
||||
export const getEditorContent = (editor: Editor | null) => {
|
||||
if (!editor) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const content = editor.isEmpty ? '' : editor.getHTML();
|
||||
|
||||
const doc = removeEditorWrapperParagraph(content);
|
||||
|
||||
// 返回处理后的HTML
|
||||
return doc;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理编辑器输出的HTML内容
|
||||
* 移除不必要的外层<p>标签,保持与原始内容格式一致
|
||||
*/
|
||||
export const removeEditorWrapperParagraph = (content: string): string => {
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 使用DOM解析器来处理HTML
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(content, 'text/html');
|
||||
|
||||
// 找到所有编辑器生成的p标签
|
||||
const generatedParagraphs = doc.querySelectorAll(
|
||||
'p.text-knowledge-tiptap-editor-paragraph',
|
||||
);
|
||||
|
||||
// 替换这些p标签为它们的内容
|
||||
generatedParagraphs.forEach(p => {
|
||||
const parent = p.parentNode;
|
||||
if (parent) {
|
||||
// 创建一个文档片段来存储p标签的内容
|
||||
const fragment = document.createDocumentFragment();
|
||||
while (p.firstChild) {
|
||||
fragment.appendChild(p.firstChild);
|
||||
}
|
||||
// 用内容替换p标签
|
||||
parent.replaceChild(fragment, p);
|
||||
}
|
||||
});
|
||||
|
||||
return doc.body.innerHTML;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 编辑器对/n不会换行,所以需要转换为<br />标签
|
||||
*/
|
||||
export const getInitEditorContent = (content: string) => {
|
||||
if (content === '') {
|
||||
return '';
|
||||
}
|
||||
if (!content.includes('\n')) {
|
||||
return content;
|
||||
}
|
||||
return content.replace(/\n/g, '<br />');
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 { escapeHtml } from '@/text-knowledge-editor/utils/escape-html';
|
||||
|
||||
/**
|
||||
* 获取渲染后的HTML内容
|
||||
*/
|
||||
export const getRenderHtmlContent = (content: string) => {
|
||||
if (content === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 转义HTML,只允许白名单中的标签
|
||||
const htmlContent = escapeHtml(content);
|
||||
|
||||
// 编辑器对/n不会换行,所以需要转换为<br />标签
|
||||
return htmlContent.replace(/\n/g, '<br />');
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
|
||||
import { processEditorContent } from '../inner/document-editor.service';
|
||||
|
||||
/**
|
||||
* 判断内容是否改变
|
||||
*/
|
||||
export const isEditorContentChange = (
|
||||
chunks: Chunk[],
|
||||
chunk: Chunk,
|
||||
): boolean => {
|
||||
const newContent = processEditorContent(chunk.content ?? '');
|
||||
const oldContent = chunks.find(c => c.slice_id === chunk.slice_id)?.content;
|
||||
return newContent !== oldContent;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type SliceStatus } from '@coze-arch/bot-api/knowledge';
|
||||
|
||||
export interface Chunk {
|
||||
text_knowledge_editor_chunk_uuid: string;
|
||||
slice_id?: string;
|
||||
local_slice_id?: string;
|
||||
content?: string;
|
||||
sequence?: string;
|
||||
status?: SliceStatus;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* html白名单,防止XSS攻击
|
||||
*/
|
||||
|
||||
// 默认允许的HTML标签白名单
|
||||
const DEFAULT_ALLOWED_TAGS = [
|
||||
'img',
|
||||
'table',
|
||||
'colgroup',
|
||||
'col',
|
||||
'tbody',
|
||||
'thead',
|
||||
'tfoot',
|
||||
'tr',
|
||||
'td',
|
||||
'th',
|
||||
'br',
|
||||
'p',
|
||||
];
|
||||
|
||||
/**
|
||||
* 转义HTML,只允许白名单中的标签
|
||||
* @param unsafe 不安全的HTML字符串
|
||||
* @param allowedTags 允许的HTML标签数组,默认为DEFAULT_ALLOWED_TAGS
|
||||
* @returns 转义后的HTML字符串
|
||||
*/
|
||||
export function escapeHtml(
|
||||
unsafe: string,
|
||||
allowedTags: string[] = DEFAULT_ALLOWED_TAGS,
|
||||
): string {
|
||||
if (!unsafe) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 构建正则表达式模式
|
||||
const allowedTagsPattern = allowedTags.join('|');
|
||||
const tagRegex = new RegExp(
|
||||
`<(?!(${allowedTagsPattern})\\b[^>]*>|\\/(?:${allowedTagsPattern})>)`,
|
||||
'g',
|
||||
);
|
||||
|
||||
return unsafe.replace(tagRegex, '<');
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 { Toast } from '@coze-arch/coze-design';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
const LIMIT_SIZE = 20 * 1024 * 1024;
|
||||
export const isValidSize = (size: number) => LIMIT_SIZE > size;
|
||||
|
||||
export const getBase64 = (file: Blob): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = event => {
|
||||
const result = event.target?.result;
|
||||
|
||||
if (!result || typeof result !== 'string') {
|
||||
reject(new CustomError('getBase64', 'file read invalid'));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(result.replace(/^.*?,/, ''));
|
||||
};
|
||||
fileReader.onerror = () => {
|
||||
Toast.error(I18n.t('read_file_failed_please_retry'));
|
||||
reject(new CustomError('getBase64', 'file read fail'));
|
||||
};
|
||||
fileReader.onabort = () => {
|
||||
reject(new CustomError('getBase64', 'file read abort'));
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
export const getUint8Array = (file: Blob): Promise<Uint8Array> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.onload = event => {
|
||||
if (event.target?.result) {
|
||||
const arrayBuffer = event.target.result as ArrayBuffer;
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
resolve(uint8Array);
|
||||
} else {
|
||||
reject(new CustomError('getUint8Array', 'file read invalid'));
|
||||
}
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
});
|
||||
|
||||
export const getFileExtension = (name: string) => {
|
||||
const index = name.lastIndexOf('.');
|
||||
return name.slice(index + 1);
|
||||
};
|
||||
Reference in New Issue
Block a user