chore: replace all cn comments of fe to en version by volc api (#320)
This commit is contained in:
@@ -42,7 +42,7 @@ const options = {
|
||||
`//lf-cdn.coze.cn/obj/unpkg/pdfjs-dist/${pdfjs.version}/cmaps/`
|
||||
: // cp-disable-next-line
|
||||
`//sf-cdn.coze.com/obj/unpkg-va/pdfjs-dist/${pdfjs.version}/cmaps/`,
|
||||
// 提升性能
|
||||
// Boost performance
|
||||
cMapPacked: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -43,61 +43,61 @@ import styles from './common-file-picker.module.less';
|
||||
|
||||
export interface CommonFilePickerProps
|
||||
extends Omit<TreeProps, 'onChange' | 'onSelect'> {
|
||||
/** 渲染使用的树数据 */
|
||||
/** Tree data used for rendering */
|
||||
treeData: FileNode[];
|
||||
/** 提供一个定制化的 render tree node */
|
||||
/** Provide a customized render tree node */
|
||||
customTreeDataRenderer?: (
|
||||
renderProps: RenderFullLabelProps,
|
||||
) => React.ReactNode;
|
||||
|
||||
/** 是否只能选中叶子结点 如果传入 customRenderTreeData 则失效 */
|
||||
/** Whether only leaf nodes can be selected, invalid if customRenderTreeData is passed in */
|
||||
onlySelectLeaf?: boolean;
|
||||
/** 是否多选 如果自定义 customTreeDataRenderer 一定要传入, 选项会影响 customTreeDataRenderer 的入参 */
|
||||
/** Whether to multi-select, if the custom customTreeDataRenderer must be passed in, the option will affect customTreeDataRenderer imported parameter */
|
||||
multiple?: boolean;
|
||||
|
||||
/**
|
||||
* 是否开启虚拟化
|
||||
* Whether to enable virtualization
|
||||
*/
|
||||
enableVirtualize?: boolean;
|
||||
/** 虚拟化选项 */
|
||||
/** 虚拟化容器高度 */
|
||||
/** virtualization options */
|
||||
/** virtualized container height */
|
||||
virtualizeHeight?: number;
|
||||
/** 每个 item 高度 */
|
||||
/** Height of each item */
|
||||
virtualizeItemSize?: number;
|
||||
|
||||
/** 默认已经选中的内容 可以作为 initValue 使用,也可以作为 value 的代替者使用 */
|
||||
/** The content already selected by default can be used as initValue or as a substitute for value */
|
||||
defaultValue?: FileNode[] | FileId[];
|
||||
|
||||
/** 样式渲染特性 */
|
||||
/** style rendering feature */
|
||||
normalLabelStyle?: React.CSSProperties;
|
||||
selectedLabelStyle?: React.CSSProperties;
|
||||
halfSelectedLabelStyle?: React.CSSProperties;
|
||||
|
||||
/** 缩进大小 默认大小 25px 如果 backgroundMode 为 position 将会反应为 left 如果为 padding 将会反应成 padding-left */
|
||||
/** Indent size, default size 25px If backgroundMode is position, it will react to left If it is padding, it will react to padding-left. */
|
||||
indentSize?: number;
|
||||
|
||||
/** 树组件展开的 icon */
|
||||
/** Tree component expansion icon */
|
||||
expandIcon?: React.ReactNode;
|
||||
|
||||
/** onChange 业务层可以透传 */
|
||||
/** onChange business layer can be passed through */
|
||||
onChange?: (args?: FileNode[]) => void;
|
||||
|
||||
/** onSelect 业务层可以透传 */
|
||||
/** onSelect business layer can pass through */
|
||||
onSelect?: (key: string, selected: boolean, node: FileNode) => void;
|
||||
|
||||
/** 用来转换 selectedFiles, 发生在 设置选中态 到 提交给上层组件 之间 注意 处理有先后顺序,前一个中间件的返回将作为后一个的输入 */
|
||||
/** Used to convert selectedFiles, occurs in, sets the selected state, to submit to the upper component, between, note that there is a sequence of processing, the return of the previous middleware will be used as the input of the latter */
|
||||
transSelectedFilesMiddlewares?: TransSelectedFilesMiddleware[];
|
||||
|
||||
/** 设置选择态的钩子 发生在 点击选中框 到 设置选择态 之间 处理有先后顺序 */
|
||||
/** The hook for setting the selection state occurs between clicking the check box and setting the selection state, and the processing is in sequence */
|
||||
selectFilesMiddlewares?: CalcCurrentSelectFilesMiddleware[];
|
||||
|
||||
/** disable select 禁止选择 */
|
||||
/** Disable select disable select */
|
||||
disableSelect?: boolean;
|
||||
|
||||
/** checkRelation: 父子节点选中态是否关联 */
|
||||
/** checkRelation: Whether the selected state of the parent and child nodes is related */
|
||||
checkRelation?: 'related' | 'unRelated';
|
||||
|
||||
/** 默认展开的节点 key */
|
||||
/** Default expanded node key */
|
||||
defaultExpandKeys?: FileId[];
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ function transDefaultValueToFileNodes(
|
||||
const treeDataMap = levelMapTreeNodesToMap(treeData);
|
||||
return (
|
||||
defaultValue?.map(element => {
|
||||
// 因为开了 onChangeWithObject 所以这里选中态要用 object 存储
|
||||
// Because onChangeWithObject is opened, the selected state here should be stored with object.
|
||||
if (typeof element === 'string') {
|
||||
return (
|
||||
treeDataMap.get(element) ?? {
|
||||
@@ -161,7 +161,7 @@ function transDefaultValueToFileNodes(
|
||||
function transDefaultValueToRenderNode(defaultValue?: FileId[] | FileNode[]) {
|
||||
return (
|
||||
defaultValue?.map(element => {
|
||||
// 因为开了 onChangeWithObject 所以这里选中态要用 object 存储
|
||||
// Because onChangeWithObject is opened, the selected state here should be stored with object.
|
||||
if (typeof element === 'string') {
|
||||
return {
|
||||
key: element,
|
||||
@@ -175,10 +175,10 @@ function transDefaultValueToRenderNode(defaultValue?: FileId[] | FileNode[]) {
|
||||
/**
|
||||
* ------------------
|
||||
* common file picker
|
||||
* 用于数据上传选择文件
|
||||
* Select file for data upload
|
||||
* ------------------
|
||||
* useImperativeHandle:
|
||||
* search: 提供树搜索能力
|
||||
* Search: Provides tree search capability
|
||||
* ------------------
|
||||
* props: FilePickerProps
|
||||
* ------------------
|
||||
@@ -205,7 +205,7 @@ export const CommonFilePicker = React.forwardRef(
|
||||
} = props;
|
||||
|
||||
const treeRef = useRef<Tree>(null);
|
||||
// 使用受控模式
|
||||
// Use controlled mode
|
||||
const [selectValue, setSelectValue] = useState<FileNode[]>(
|
||||
transDefaultValueToRenderNode(defaultValue),
|
||||
);
|
||||
@@ -287,13 +287,13 @@ export const CommonFilePicker = React.forwardRef(
|
||||
transedChangeNodes = [changeNodes as unknown as FileNode];
|
||||
}
|
||||
|
||||
// 计算 diff
|
||||
// Calculate diff
|
||||
const [addNodes, removeNodes, retainNodes] = diffChangeNodes(
|
||||
prevChangeNodes.current,
|
||||
transedChangeNodes as FileNode[],
|
||||
);
|
||||
|
||||
// 这里的中间件更多用在定制化选中态的场景 比如想要反选所有子节点但是保持父亲节点选中
|
||||
// The middleware here is more used in scenarios where the selected state is customized, such as wanting to unselect all sub-nodes but keep the parent node selected
|
||||
transedChangeNodes = distinctFileNodes(
|
||||
selectFilesMiddlewares.reduce(
|
||||
(selectedElements, middleware) =>
|
||||
@@ -313,10 +313,10 @@ export const CommonFilePicker = React.forwardRef(
|
||||
key: transedNode.key,
|
||||
})),
|
||||
);
|
||||
// 虽然这里返回的是父节点带 children 但是因为都是后端一次接口的快照
|
||||
// 具体上报什么数据交给业务方
|
||||
// 这里的中间件主要用在定制化上报给上层组件的选中数据的场景,比如 checkRelation 'related' 模式下 上面返回的只有父亲节点的数据,但是父组件想要所有数据
|
||||
// 警告:这里如果使用 loadData 时拿不到没请求的子节点(换句话说拿到的最后一层的数据不一定是叶子结点, 同样的在这里选中之后交回给后端的其实也只是一个父节点 不能保证在一个快照里
|
||||
// Although the parent node with children is returned here, because they are all snapshots of the backend interface
|
||||
// What data to report to the business party?
|
||||
// The middleware here is mainly used in scenarios where the selected data is reported to the upper component, such as checkRelation'related 'mode, where only the data of the parent node is returned, but the parent component wants all the data
|
||||
// Warning: If you can't get the unrequested sub-node when using loadData here (in other words, the last layer of data you get is not necessarily a leaf node), the same is true here. After selecting it, it is only a parent node, and it cannot be guaranteed to be in a snapshot.
|
||||
if (transSelectedFilesMiddlewares) {
|
||||
transedChangeNodes = transSelectedFilesMiddlewares.reduce(
|
||||
(selectedElements, middleware) => middleware(selectedElements),
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// 缩进相关数据
|
||||
// Indent related data
|
||||
export const DEFAULT_FILENODE_LEVEL_INDENT = 24;
|
||||
|
||||
// 虚拟化配置相关数据
|
||||
// Virtualization configuration related data
|
||||
export const DEFAULT_VIRTUAL_CONTAINER_HEIGHT = 540;
|
||||
export const DEFAULT_VIRTUAL_ITEM_HEIGHT = 28;
|
||||
|
||||
@@ -80,7 +80,7 @@ const ActionComponent = (props: {
|
||||
<span className="action-placeholder file-selector"></span>
|
||||
);
|
||||
|
||||
// 当前整棵树是多选 并且 只能选中叶子结点
|
||||
// At present, the entire tree is multi-selected, and only leaf nodes can be selected
|
||||
if (multiple && onlySelectLeaf) {
|
||||
return isLeaf && !unCheckable ? (
|
||||
<>
|
||||
@@ -94,7 +94,7 @@ const ActionComponent = (props: {
|
||||
);
|
||||
}
|
||||
|
||||
// 当前整棵树是多选 并且 能选中所有结点
|
||||
// The current entire tree is multi-selected, and all nodes can be selected
|
||||
if (multiple && !onlySelectLeaf) {
|
||||
if (unCheckable) {
|
||||
return (
|
||||
@@ -111,7 +111,7 @@ const ActionComponent = (props: {
|
||||
);
|
||||
}
|
||||
|
||||
// 当前整棵树是单选 并且 只能选中叶子结点
|
||||
// The current entire tree is radio-selected, and only leaf nodes can be selected
|
||||
if (!multiple && onlySelectLeaf) {
|
||||
return isLeaf && !unCheckable ? (
|
||||
<>
|
||||
@@ -126,7 +126,7 @@ const ActionComponent = (props: {
|
||||
);
|
||||
}
|
||||
|
||||
// 当前整棵树是单选 并且 能选中所有结点
|
||||
// The current entire tree is radio-selected, and all nodes can be selected
|
||||
if (!multiple && !onlySelectLeaf) {
|
||||
if (unCheckable) {
|
||||
return (
|
||||
@@ -195,12 +195,12 @@ const LabelContent = (props: {
|
||||
|
||||
/**
|
||||
* -----------------------------
|
||||
* 获取默认的文件树 label renderer 这层不感知平台信息
|
||||
* Get the default file tree label renderer layer that is not platform aware
|
||||
* -----------------------------
|
||||
* @param {boolean} multiple 是否多选
|
||||
* @param {boolean} onlySelectLeaf 是否只能选中叶子结点
|
||||
* @param {{indentSize: 树组件缩进尺寸, expandIcon: 树组件可展开节点 展开图标, disableSelect: 选择的 disable 状态}} renderOption 渲染相关的自定义选项
|
||||
* @returns label render 函数
|
||||
* @param {boolean} multiple or not
|
||||
* @param {boolean} onlySelectLeaf Whether only leaf nodes can be selected
|
||||
* @param {{indentSize: tree component indent size, expandIcon: tree component expandable node, expand icon, disableSelect: selected disabled state}} renderOption Render-related customization options
|
||||
* @Returns label rendering function
|
||||
*/
|
||||
export function useDefaultLabelRenderer(
|
||||
multiple: boolean,
|
||||
@@ -243,10 +243,10 @@ export function useDefaultLabelRenderer(
|
||||
}`;
|
||||
|
||||
/**
|
||||
* 在整行点击的处理函数 不是点击 checkbox 或者 radio 或者 expandIcon 的处理函数
|
||||
* @param isLeaf: 是否是叶子结点
|
||||
* @param onExpand: 展开行 处理函数
|
||||
* @param onCheck: 选中状态的 toggle
|
||||
* The handler that clicks on the entire line, not the handler that clicks on checkbox or radio or expandIcon
|
||||
* @Param isLeaf: whether it is a leaf node
|
||||
* @Param onExpand: expand the line, handle function
|
||||
* @param onCheck: toggle in selected state
|
||||
**/
|
||||
const getItemAction = (params: {
|
||||
isLeaf: boolean;
|
||||
@@ -256,8 +256,8 @@ export function useDefaultLabelRenderer(
|
||||
}) => {
|
||||
const { onExpand, onCheck, isLeaf, unCheckable } = params;
|
||||
return function (e: React.MouseEvent<Element, MouseEvent>) {
|
||||
// 如果只能选中叶子结点 那么无论 多选 / 单选 父节点只能展开,叶子结点可以选中
|
||||
// 反之 父节点子节点 都是选中 无论多选单选,想要展开就点击 expand icon
|
||||
// If only the leaf node can be selected, then no matter whether it is multi-select/single-select, the parent node can only be expanded, and the leaf node can be selected
|
||||
// On the contrary, the parent node sub-node is selected, regardless of the multi-select radio, if you want to expand, click the expand icon.
|
||||
if (onlySelectLeaf) {
|
||||
if (!isLeaf) {
|
||||
onExpand(e);
|
||||
@@ -301,14 +301,14 @@ export function useDefaultLabelRenderer(
|
||||
readonly,
|
||||
unCheckable = false,
|
||||
} = data;
|
||||
// 单选选中选项的 key
|
||||
// Radio select the key of the selected option
|
||||
const singleSelectCheckStatus = singleSelectedKey === key;
|
||||
const rowCheckStatus = multiple
|
||||
? checkStatus
|
||||
: {
|
||||
checked: singleSelectCheckStatus,
|
||||
};
|
||||
// 只要 data isLeaf 不是空 永远先看 data.isLeaf
|
||||
// As long as data isLeaf is not empty, always look at data.isLeaf first
|
||||
const isLeaf = data?.isLeaf ?? !(data.children && data.children.length);
|
||||
const checkItem = multiple
|
||||
? (e: React.MouseEvent<Element, MouseEvent>) => {
|
||||
|
||||
@@ -30,53 +30,53 @@ export interface PickerRef {
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件选择树节点
|
||||
* file selection tree node
|
||||
*/
|
||||
export interface FileNode extends TreeNodeData {
|
||||
/** 独一无二的 key 标识 可以用 文件 id */
|
||||
/** Unique key identifier, can be used, file id */
|
||||
key: string;
|
||||
value?: string;
|
||||
label?: React.ReactNode;
|
||||
type?: TreeNodeType;
|
||||
// icon 的 URL
|
||||
// URL of the icon
|
||||
icon?: string;
|
||||
children?: FileNode[];
|
||||
/** 标识当前节点是不是叶子结点 loadData 时必备 */
|
||||
/** Required when identifying whether the current node is a leaf node loadData */
|
||||
isLeaf?: boolean;
|
||||
/** 该节点是否可以选中 */
|
||||
/** Can the node be selected? */
|
||||
selectable?: boolean;
|
||||
/** 节点的 loading 状态,开启后 loading 默认替换 icon,展示 loadingInfo,
|
||||
* 注意这个和 semi 本身带的 loading 不一样,semi 的 loading 指的是 展开的 loading 状态 */
|
||||
/** The loading status of the node, after opening, the loading default replaces the icon, and displays the loadingInfo.
|
||||
* Note that this is different from the loading of the semi itself. The loading of the semi refers to the unfolded loading state. */
|
||||
isLoading?: boolean;
|
||||
/** 节点 loading 的提示,默认是 `获取中` */
|
||||
/** Node loading prompt, the default is "getting" */
|
||||
loadingInfo?: string;
|
||||
/** 具体的文档类型 比如 doc docx txt 等 */
|
||||
/** Specific document types, such as doc docx txt, etc */
|
||||
file_type?: string;
|
||||
/** 三方文档链接 */
|
||||
/** Three-way document link */
|
||||
file_url?: string;
|
||||
/** 【飞书场景】wiki 空间id,*/
|
||||
/** [Feishu scene] wiki space id,*/
|
||||
space_id?: string;
|
||||
/** 【飞书场景】wiki 叶子id,*/
|
||||
/** [Feishu scene] wiki leaf id,*/
|
||||
obj_token?: string;
|
||||
/** 自定义渲染 Item */
|
||||
/** Custom Rendering Items */
|
||||
render?: () => ReactNode;
|
||||
/** 只读,不可交互 */
|
||||
/** Read-only, not interactive */
|
||||
readonly?: boolean;
|
||||
/** 节点是否不可选择,默认为 false */
|
||||
/** Whether the node is not selectable, the default is false */
|
||||
unCheckable?: boolean;
|
||||
}
|
||||
|
||||
export type FileId = string;
|
||||
|
||||
/**
|
||||
* 文件选择树 节点选择状态
|
||||
* File selection tree, node selection status
|
||||
*/
|
||||
export interface FileSelectCheckStatus {
|
||||
checked: boolean;
|
||||
halfChecked: boolean;
|
||||
}
|
||||
|
||||
// 三部分 当前选中的 新增选中的 较上次不选中的 较上次不变的
|
||||
// Three parts, currently selected, newly selected, unselected from last time, unchanged from last time
|
||||
export type TransSelectedFilesMiddleware = (
|
||||
fileNodes: FileNode[],
|
||||
) => FileNode[];
|
||||
@@ -88,12 +88,12 @@ export type CalcCurrentSelectFilesMiddleware = (
|
||||
retainNodes?: FileNode[],
|
||||
) => FileNode[];
|
||||
|
||||
/** 类型断言 节点是不是 fileNode */
|
||||
/** Type assertion, node is not fileNode */
|
||||
export function isFileNode(fileNode: unknown): fileNode is FileNode {
|
||||
return !!fileNode && isObject(fileNode) && !!(fileNode as FileNode).key;
|
||||
}
|
||||
|
||||
/** 类型断言 数组是不是 fileNode 数组 */
|
||||
/** Type assertion, array is not a fileNode array */
|
||||
export function isFileNodeArray(fileNodes: unknown[]): fileNodes is FileNode[] {
|
||||
return fileNodes.every(fileNode => isFileNode(fileNode));
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ const SegmentMenu: React.FC<ISegmentMenuProps> = props => {
|
||||
) : null}
|
||||
<div className="pl-2 h-6 mt-4 mb-1 flex items-center">
|
||||
<div className="coz-fg-secondary text-[12px] font-[400] leading-4 shrink-0">
|
||||
{/**文档列表 */}
|
||||
{/**document list */}
|
||||
{I18n.t('knowledge_level_012')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,7 +114,7 @@ const SegmentMenu: React.FC<ISegmentMenuProps> = props => {
|
||||
<div className="flex flex-col gap-1 !overflow-auto">
|
||||
<div className="w-full pl-2 h-6 items-center flex gap-[4px]">
|
||||
<div className="coz-fg-secondary text-[12px] font-[400] leading-4 shrink-0">
|
||||
{/**分段层级 */}
|
||||
{/**segment hierarchy */}
|
||||
{I18n.t('knowledge_level_adjust')}
|
||||
</div>
|
||||
{treeDisabled ? null : (
|
||||
|
||||
@@ -48,10 +48,10 @@ export const SegmentTree: React.FC<ISegmentTreeProps> = ({
|
||||
disabled,
|
||||
}) => {
|
||||
/**
|
||||
* 选中功能
|
||||
* select function
|
||||
*/
|
||||
const [selected, setSelected] = useState(new Set<string>());
|
||||
// 分片 id
|
||||
// Sharding id
|
||||
const [selectedThroughParent, setSelectedThroughParent] = useState(
|
||||
new Set<string>(),
|
||||
);
|
||||
|
||||
@@ -114,8 +114,8 @@ export function useSegmentContextMenu({
|
||||
onContextMenu: (e, node: NodeApi<LevelDocumentTree>) => {
|
||||
e.preventDefault();
|
||||
setTreeNode(node);
|
||||
/** 在 project ide 里面,ide 容器设置了 contain: strict, 会导致 fixed position
|
||||
* 的偏移基础不对,所以这里需要减去 ide 容器的 left 和 top 值
|
||||
/** In the project ide, the ide container is set to contain: strict, which will cause a fixed position.
|
||||
* The base of the offset is wrong, so you need to subtract the left and top values of the ide container here
|
||||
*/
|
||||
let clickX = e.pageX;
|
||||
let clickY = e.pageY;
|
||||
|
||||
@@ -140,7 +140,7 @@ export const handleTreeNodeMove = (
|
||||
};
|
||||
}
|
||||
|
||||
// 如果是同一个 parent,且拖动的位置在当前位置之前,dropIndex 减 1
|
||||
// If it is the same parent and the dragged position is before the current position, the dropIndex is reduced by 1.
|
||||
const originalIndex = parentSegment.children.indexOf(dragSegment.id);
|
||||
const dropIndex =
|
||||
originalIndex < positions.dropIndex &&
|
||||
@@ -224,7 +224,7 @@ export const handleMergeNodes = (
|
||||
};
|
||||
}
|
||||
|
||||
// 从原父节点的 children 中移除该节点
|
||||
// Remove the node from the children of the original parent
|
||||
const parentSegment = resSegments.find(
|
||||
item => item.id === segmentToMerge.parent,
|
||||
);
|
||||
@@ -235,7 +235,7 @@ export const handleMergeNodes = (
|
||||
}
|
||||
|
||||
if (!['table', 'image', 'title'].includes(segmentToMerge?.type ?? '')) {
|
||||
// 合并文本内容并删除节点
|
||||
// Merge text content and delete nodes
|
||||
mergedSegment.text += segmentToMerge.text;
|
||||
const index = resSegments.findIndex(
|
||||
item => item.id === segmentToMerge.id,
|
||||
@@ -244,7 +244,7 @@ export const handleMergeNodes = (
|
||||
resSegments.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
// 非section-text类型的节点,将其移动到合并后节点的children中
|
||||
// Nodes of type non-section-text, move them to the children of the merged node
|
||||
segmentToMerge.parent = mergedSegment.id;
|
||||
mergedSegment.children.push(segmentToMerge.id);
|
||||
}
|
||||
|
||||
@@ -33,33 +33,33 @@ export const DocumentChunkPreview = ({
|
||||
<div
|
||||
id={locateId}
|
||||
className={classNames(
|
||||
// 布局
|
||||
// layout
|
||||
'relative',
|
||||
// 间距
|
||||
// spacing
|
||||
'mb-2 p-2',
|
||||
// 文字样式
|
||||
// Text Style
|
||||
'text-sm leading-5',
|
||||
// 颜色
|
||||
// color
|
||||
'coz-fg-primary hover:coz-mg-hglt-secondary-hovered coz-mg-secondary',
|
||||
// 边框
|
||||
// border
|
||||
'border border-solid coz-stroke-primary rounded-lg',
|
||||
// 表格样式
|
||||
// table style
|
||||
getEditorTableClassname(),
|
||||
// 图片样式
|
||||
// image style
|
||||
getEditorImgClassname(),
|
||||
// 换行
|
||||
// line feed
|
||||
getEditorWordsCls(),
|
||||
)}
|
||||
>
|
||||
<p
|
||||
// 已使用 DOMPurify 过滤 xss
|
||||
// Filtered xss with DOMPurify
|
||||
// eslint-disable-next-line risxss/catch-potential-xss-react
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
DOMPurify.sanitize(getRenderHtmlContent(chunk.content ?? ''), {
|
||||
/**
|
||||
* 1. 防止CSS注入攻击
|
||||
* 2. 防止用户误写入style标签,导致全局样式被修改,页面展示异常
|
||||
* 1. Prevent CSS injection attacks
|
||||
* 2. Prevent users from writing the style tag by mistake, resulting in the global style being modified and the page display being abnormal
|
||||
*/
|
||||
FORBID_TAGS: ['style'],
|
||||
}) ?? '',
|
||||
|
||||
@@ -21,16 +21,16 @@ import type { Emitter, Handler, EventType } from 'mitt';
|
||||
|
||||
import { type Chunk } from '../types/chunk';
|
||||
|
||||
// 定义事件名称字面量类型
|
||||
// Define event name literal type
|
||||
export type EventTypeName =
|
||||
| 'previewContextMenuItemAction'
|
||||
| 'hoverEditBarAction';
|
||||
|
||||
/**
|
||||
* 事件类型定义
|
||||
* event type definition
|
||||
*/
|
||||
export interface EventTypes extends Record<EventType, unknown> {
|
||||
// 右键菜单相关事件
|
||||
// right-click menu related events
|
||||
previewContextMenuItemAction: {
|
||||
type: 'add-after' | 'add-before' | 'delete' | 'edit';
|
||||
targetChunk: Chunk;
|
||||
@@ -38,7 +38,7 @@ export interface EventTypes extends Record<EventType, unknown> {
|
||||
chunks?: Chunk[];
|
||||
};
|
||||
|
||||
// 悬浮编辑栏相关事件
|
||||
// Floating edit bar related events
|
||||
hoverEditBarAction: {
|
||||
type: 'add-after' | 'add-before' | 'delete' | 'edit';
|
||||
targetChunk: Chunk;
|
||||
@@ -48,32 +48,32 @@ export interface EventTypes extends Record<EventType, unknown> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件处理函数类型
|
||||
* event handler type
|
||||
*/
|
||||
export type EventHandler<T extends EventTypeName> = Handler<EventTypes[T]>;
|
||||
|
||||
/**
|
||||
* 创建事件总线实例
|
||||
* Create an event bus instance
|
||||
*/
|
||||
export const createEventBus = (): Emitter<EventTypes> => mitt<EventTypes>();
|
||||
|
||||
/**
|
||||
* 全局事件总线实例
|
||||
* Global Event Bus instance
|
||||
*/
|
||||
export const eventBus = createEventBus();
|
||||
|
||||
/**
|
||||
* 事件总线钩子
|
||||
* 用于在组件中使用事件总线
|
||||
* event bus hook
|
||||
* Used to use the event bus in components
|
||||
*/
|
||||
export const useEventBus = () => eventBus;
|
||||
|
||||
/**
|
||||
* 监听事件钩子
|
||||
* 用于在组件中监听事件
|
||||
* @param eventName 事件名称
|
||||
* @param handler 事件处理函数
|
||||
* @param deps 依赖数组,当依赖变化时重新绑定事件
|
||||
* listen event hook
|
||||
* Used to listen for events in a component
|
||||
* @param eventName
|
||||
* @param handler event handler
|
||||
* @Param deps dependency arrays, rebind events when dependencies change
|
||||
*/
|
||||
export const useEventListener = <T extends EventTypeName>(
|
||||
eventName: T,
|
||||
@@ -81,10 +81,10 @@ export const useEventListener = <T extends EventTypeName>(
|
||||
deps: React.DependencyList = [],
|
||||
) => {
|
||||
useEffect(() => {
|
||||
// 绑定事件
|
||||
// binding event
|
||||
eventBus.on(eventName, handler as Handler<unknown>);
|
||||
|
||||
// 组件卸载时解绑事件
|
||||
// Unbind event when component is unmounted
|
||||
return () => {
|
||||
eventBus.off(eventName, handler as Handler<unknown>);
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ export const BaseUploadImage = ({
|
||||
showTooltip,
|
||||
renderUI,
|
||||
}: BaseUploadImageProps) => {
|
||||
// 处理图片上传
|
||||
// Handle image upload
|
||||
const handleImageUpload = (object: customRequestArgs) => {
|
||||
if (!editor) {
|
||||
return;
|
||||
@@ -53,7 +53,7 @@ export const BaseUploadImage = ({
|
||||
options: {
|
||||
onFinish: (result: { url?: string; tosKey?: string }) => {
|
||||
if (result.url && editor) {
|
||||
// 插入图片到编辑器
|
||||
// Insert pictures into the editor
|
||||
editor.chain().focus().setImage({ src: result.url }).run();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -79,7 +79,7 @@ export const handleCustomUploadRequest = async ({
|
||||
}
|
||||
|
||||
try {
|
||||
// 业务逻辑
|
||||
// business logic
|
||||
onBeforeUpload?.();
|
||||
const { name, fileInstance } = file;
|
||||
|
||||
|
||||
@@ -49,14 +49,14 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = props => {
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
/**
|
||||
* 当右键点击编辑器时,显示上下文菜单
|
||||
* When right-clicking on the editor, the context menu is displayed
|
||||
*/
|
||||
const { contextMenuPosition, openContextMenu } = useControlContextMenu({
|
||||
contextMenuRef,
|
||||
});
|
||||
|
||||
/**
|
||||
* 当点击编辑器外部时
|
||||
* When clicking outside the editor
|
||||
*/
|
||||
useOutEditorMode({
|
||||
editorRef,
|
||||
@@ -76,26 +76,26 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = props => {
|
||||
<div
|
||||
ref={editorRef}
|
||||
className={classNames(
|
||||
// 布局
|
||||
// layout
|
||||
'relative',
|
||||
// 间距
|
||||
// spacing
|
||||
'mb-2 p-2',
|
||||
// 文字样式
|
||||
// Text Style
|
||||
'text-sm leading-5',
|
||||
// 颜色
|
||||
// color
|
||||
'coz-fg-primary coz-bg-max',
|
||||
// 边框
|
||||
// border
|
||||
'border border-solid coz-stroke-hglt rounded-lg',
|
||||
)}
|
||||
onContextMenu={openContextMenu}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
// 表格样式
|
||||
// table style
|
||||
getEditorTableClassname(),
|
||||
// 图片样式
|
||||
// image style
|
||||
getEditorImgClassname(),
|
||||
// 换行
|
||||
// line feed
|
||||
getEditorWordsCls(),
|
||||
)}
|
||||
>
|
||||
@@ -104,7 +104,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = props => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右键菜单 */}
|
||||
{/* right-click menu */}
|
||||
{contextMenuPosition && editorContextMenuItemsRegistry ? (
|
||||
<EditorContextMenu
|
||||
x={contextMenuPosition.x}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const AddAfterAction: React.FC<HoverEditBarActionProps> = ({
|
||||
chunks,
|
||||
disabled,
|
||||
}) => {
|
||||
// 在特定分片后添加新分片
|
||||
// Add new shardings after specific shardings
|
||||
const { addEmptyChunkAfter } = useAddEmptyChunkAction({
|
||||
chunks: chunks || [],
|
||||
onChunksChange: ({ newChunk, chunks: newChunks }) => {
|
||||
|
||||
@@ -27,14 +27,14 @@ import { eventBus } from '@/text-knowledge-editor/event';
|
||||
import { type HoverEditBarActionProps } from './module';
|
||||
|
||||
/**
|
||||
* 在特定分片前添加新分片的操作组件
|
||||
* Add the action component of a new sharding before a specific sharding
|
||||
*/
|
||||
export const AddBeforeAction: React.FC<HoverEditBarActionProps> = ({
|
||||
chunk,
|
||||
chunks = [],
|
||||
disabled,
|
||||
}) => {
|
||||
// 在特定分片前添加新分片
|
||||
// Add new shardings before specific shardings
|
||||
const { addEmptyChunkBefore } = useAddEmptyChunkAction({
|
||||
chunks,
|
||||
onChunksChange: ({ newChunk, chunks: newChunks }) => {
|
||||
|
||||
@@ -26,19 +26,19 @@ import { eventBus } from '@/text-knowledge-editor/event';
|
||||
import { type HoverEditBarActionProps } from './module';
|
||||
|
||||
/**
|
||||
* 删除特定分片的操作组件
|
||||
* Remove the action component for specific shardings
|
||||
*
|
||||
* 内部实现了删除特定分片的逻辑
|
||||
* 如果传入了 onDelete 回调,则会在点击时调用
|
||||
* 如果提供了 chunks、onChunksChange,则会在内部处理删除逻辑,
|
||||
* 无需依赖外部的 usePreviewContextMenu
|
||||
* The logic to remove specific shardings is implemented internally
|
||||
* If an onDelete callback is passed, it will be called on click
|
||||
* If chunks, onChunksChange are provided, the deletion logic is handled internally.
|
||||
* No need to rely on external usePreviewContextMenu
|
||||
*/
|
||||
export const DeleteAction: React.FC<HoverEditBarActionProps> = ({
|
||||
chunk,
|
||||
chunks = [],
|
||||
disabled,
|
||||
}) => {
|
||||
// 删除特定分片
|
||||
// Remove specific shardings
|
||||
const { deleteChunk } = useDeleteAction({
|
||||
chunks,
|
||||
onChunksChange: ({ chunks: newChunks }) => {
|
||||
|
||||
@@ -26,10 +26,10 @@ import { eventBus } from '@/text-knowledge-editor/event';
|
||||
import { type HoverEditBarActionProps } from './module';
|
||||
|
||||
/**
|
||||
* 编辑操作组件
|
||||
* Edit action component
|
||||
*
|
||||
* 内部实现了激活特定分片的编辑模式的逻辑
|
||||
* 如果传入了 onEdit 回调,则会在点击时调用
|
||||
* The logic to activate the edit mode for specific shardings is implemented internally
|
||||
* If an onEdit callback is passed, it will be called on click
|
||||
*/
|
||||
export const EditAction: React.FC<HoverEditBarActionProps> = ({
|
||||
chunk,
|
||||
|
||||
@@ -27,7 +27,7 @@ import { eventBus } from '@/text-knowledge-editor/event';
|
||||
import { type PreviewContextMenuItemProps } from './module';
|
||||
|
||||
/**
|
||||
* 在特定分片后添加新分片的菜单项组件
|
||||
* Add a new sharding's menu item component after a specific sharding
|
||||
*/
|
||||
export const AddAfterAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
chunk,
|
||||
@@ -44,11 +44,11 @@ export const AddAfterAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
'cursor-not-allowed': isDisabled,
|
||||
});
|
||||
|
||||
// 在特定分片后添加新分片
|
||||
// Add new shardings after specific shardings
|
||||
const { addEmptyChunkAfter } = useAddEmptyChunkAction({
|
||||
chunks,
|
||||
onChunksChange: ({ newChunk, chunks: newChunks }) => {
|
||||
// 发出在特定分片后添加新分片的事件
|
||||
// Issue an event to add a new sharding after a specific sharding
|
||||
eventBus.emit('previewContextMenuItemAction', {
|
||||
type: 'add-after',
|
||||
newChunk,
|
||||
|
||||
@@ -27,7 +27,7 @@ import { eventBus } from '@/text-knowledge-editor/event';
|
||||
import { type PreviewContextMenuItemProps } from './module';
|
||||
|
||||
/**
|
||||
* 在特定分片前添加新分片的菜单项组件
|
||||
* Add a new sharding's menu item component before a specific sharding
|
||||
*/
|
||||
export const AddBeforeAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
chunk,
|
||||
@@ -44,7 +44,7 @@ export const AddBeforeAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
'cursor-not-allowed': isDisabled,
|
||||
});
|
||||
|
||||
// 在特定分片前添加新分片
|
||||
// Add new shardings before specific shardings
|
||||
const { addEmptyChunkBefore } = useAddEmptyChunkAction({
|
||||
chunks,
|
||||
onChunksChange: ({ newChunk, chunks: newChunks }) => {
|
||||
|
||||
@@ -27,7 +27,7 @@ import { eventBus } from '@/text-knowledge-editor/event';
|
||||
import { type PreviewContextMenuItemProps } from './module';
|
||||
|
||||
/**
|
||||
* 删除特定分片的菜单项组件
|
||||
* Remove a specific sharding menu item component
|
||||
*/
|
||||
export const DeleteAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
chunk,
|
||||
@@ -44,7 +44,7 @@ export const DeleteAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
'cursor-not-allowed': isDisabled,
|
||||
});
|
||||
|
||||
// 删除特定分片
|
||||
// Remove specific shardings
|
||||
const { deleteChunk } = useDeleteAction({
|
||||
chunks,
|
||||
onChunksChange: ({ chunks: newChunks }) => {
|
||||
|
||||
@@ -26,10 +26,10 @@ import { eventBus } from '@/text-knowledge-editor/event';
|
||||
import { type PreviewContextMenuItemProps } from './module';
|
||||
|
||||
/**
|
||||
* 编辑操作菜单项组件
|
||||
* Edit Action Menu Item Component
|
||||
*
|
||||
* 内部实现了激活特定分片的编辑模式的逻辑
|
||||
* 如果传入了 onEdit 回调,则会在点击时调用
|
||||
* The logic to activate the edit mode for specific shardings is implemented internally
|
||||
* If an onEdit callback is passed, it will be called on click
|
||||
*/
|
||||
export const EditAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
chunk,
|
||||
|
||||
@@ -58,7 +58,7 @@ const DocumentPreviewComponent: React.FC<DocumentPreviewProps> = props => {
|
||||
<div className="relative">
|
||||
<div
|
||||
className={classNames(
|
||||
// 布局
|
||||
// layout
|
||||
'relative overflow-hidden',
|
||||
)}
|
||||
onContextMenu={readonly ? undefined : e => openContextMenu(e)}
|
||||
@@ -70,7 +70,7 @@ const DocumentPreviewComponent: React.FC<DocumentPreviewProps> = props => {
|
||||
onMouseLeave={readonly ? undefined : handleMouseLeave}
|
||||
onDoubleClick={readonly ? undefined : () => onActivateEditMode?.(chunk)}
|
||||
>
|
||||
{/* 悬停时显示的操作栏 */}
|
||||
{/* The action bar displayed when hovering */}
|
||||
{hoveredChunk === chunk.text_knowledge_editor_chunk_uuid &&
|
||||
!readonly ? (
|
||||
<HoverEditBar
|
||||
@@ -83,7 +83,7 @@ const DocumentPreviewComponent: React.FC<DocumentPreviewProps> = props => {
|
||||
<DocumentChunkPreview chunk={chunk} locateId={locateId || ''} />
|
||||
</div>
|
||||
|
||||
{/* 右键菜单 */}
|
||||
{/* right-click menu */}
|
||||
{contextMenuPosition ? (
|
||||
<PreviewContextMenu
|
||||
previewContextMenuItemsRegistry={previewContextMenuItemsRegistry}
|
||||
@@ -99,16 +99,16 @@ const DocumentPreviewComponent: React.FC<DocumentPreviewProps> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
// 使用React.memo包装组件,避免不必要的重新渲染
|
||||
// Wrap components with React.memo to avoid unnecessary re-rendering
|
||||
export const DocumentPreview = React.memo(
|
||||
DocumentPreviewComponent,
|
||||
(prevProps, nextProps) => {
|
||||
// 如果分片内容变化,需要重新渲染
|
||||
// If the sharding content changes, it needs to be re-rendered
|
||||
if (prevProps.chunk.content !== nextProps.chunk.content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 其他情况下不需要重新渲染
|
||||
// In other cases, no re-rendering is required
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -28,11 +28,11 @@ export const useControlContextMenu = ({
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
// 处理右键菜单
|
||||
// Handle right-click menus
|
||||
const openContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 计算相对于事件目标元素的位置
|
||||
// Calculate the position relative to the event target element
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const relativeX = e.clientX - rect.left;
|
||||
const relativeY = e.clientY - rect.top;
|
||||
@@ -43,15 +43,15 @@ export const useControlContextMenu = ({
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭右键菜单
|
||||
// Close the right-click menu
|
||||
const closeContextMenu = () => {
|
||||
setContextMenuPosition(null);
|
||||
};
|
||||
|
||||
// 处理点击文档其他位置
|
||||
// Process Click Document Other Locations
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// 如果点击的是右键菜单外部,则关闭菜单
|
||||
// If you click outside the right-click menu, close the menu
|
||||
if (
|
||||
contextMenuRef.current &&
|
||||
!contextMenuRef.current.contains(event.target as Node)
|
||||
|
||||
@@ -28,7 +28,7 @@ export const useControlEditorContextMenu = ({
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
// 处理右键菜单
|
||||
// Handle right-click menus
|
||||
const openContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setContextMenuPosition({
|
||||
@@ -37,15 +37,15 @@ export const useControlEditorContextMenu = ({
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭右键菜单
|
||||
// Close the right-click menu
|
||||
const closeContextMenu = () => {
|
||||
setContextMenuPosition(null);
|
||||
};
|
||||
|
||||
// 处理点击文档其他位置
|
||||
// Process Click Document Other Locations
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// 如果点击的是右键菜单外部,则关闭菜单
|
||||
// If you click outside the right-click menu, close the menu
|
||||
if (
|
||||
contextMenuRef.current &&
|
||||
!contextMenuRef.current.contains(event.target as Node)
|
||||
|
||||
@@ -26,7 +26,7 @@ export const useControlPreviewContextMenu = () => {
|
||||
} | null>(null);
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 处理右键点击事件
|
||||
// Handling right-click events
|
||||
const openContextMenu = (e: React.MouseEvent, chunk: Chunk) => {
|
||||
e.preventDefault();
|
||||
setContextMenuInfo({
|
||||
@@ -36,12 +36,12 @@ export const useControlPreviewContextMenu = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭右键菜单
|
||||
// Close the right-click menu
|
||||
const closeContextMenu = () => {
|
||||
setContextMenuInfo(null);
|
||||
};
|
||||
|
||||
// 点击文档其他位置关闭右键菜单
|
||||
// Click elsewhere in the document to close the right-click menu
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
|
||||
@@ -19,12 +19,12 @@ import { useState } from 'react';
|
||||
export const useHoverEffect = () => {
|
||||
const [hoveredChunk, setHoveredChunk] = useState<string | null>(null);
|
||||
|
||||
// 处理鼠标悬停事件
|
||||
// Handling mouse hover events
|
||||
const handleMouseEnter = (chunkId: string) => {
|
||||
setHoveredChunk(chunkId);
|
||||
};
|
||||
|
||||
// 处理鼠标离开事件
|
||||
// Handling mouse away events
|
||||
const handleMouseLeave = () => {
|
||||
setHoveredChunk(null);
|
||||
};
|
||||
|
||||
@@ -27,10 +27,10 @@ export const useOutEditorMode = ({
|
||||
exclude,
|
||||
onExitEditMode,
|
||||
}: UseOutEditorModeProps) => {
|
||||
// 处理点击文档其他位置
|
||||
// Process Click Document Other Locations
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// 如果点击的是编辑器外部,则退出编辑模式
|
||||
// If you click outside the editor, exit editing mode
|
||||
if (
|
||||
editorRef.current &&
|
||||
!editorRef.current.contains(event.target as Node) &&
|
||||
|
||||
@@ -24,28 +24,28 @@ interface UseAddEmptyChunkActionProps {
|
||||
onChunksChange?: (params: { newChunk: Chunk; chunks: Chunk[] }) => void;
|
||||
}
|
||||
/**
|
||||
* 在特定分片后添加新分片的 hook
|
||||
* Add new sharding hook after specific sharding
|
||||
*
|
||||
* 提供在特定分片后添加新分片的功能
|
||||
* Provides the ability to add new shardings after specific shardings
|
||||
*/
|
||||
export const useAddEmptyChunkAction = ({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
}: UseAddEmptyChunkActionProps) => {
|
||||
// 使用ref保存最新的chunks引用
|
||||
// Use ref to save the latest chunks reference
|
||||
const chunksRef = useRef<Chunk[]>(chunks);
|
||||
|
||||
// 每次props.chunks更新时,更新ref
|
||||
// Every time props.chunks is updated, update the ref.
|
||||
useEffect(() => {
|
||||
chunksRef.current = chunks;
|
||||
}, [chunks]);
|
||||
|
||||
/**
|
||||
* 在特定分片后添加新分片
|
||||
* @returns 包含新分片和更新后的分片列表的结果对象
|
||||
* Add new shardings after specific shardings
|
||||
* @Returns the result object containing the new sharding and the updated sharding list
|
||||
*/
|
||||
const handleAddEmptyChunkAfter = (chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
// Get the latest chunks from the ref
|
||||
const currentChunks = chunksRef.current;
|
||||
const index = currentChunks.findIndex(
|
||||
c =>
|
||||
@@ -79,10 +79,10 @@ export const useAddEmptyChunkAction = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* 在特定分片前添加新分片
|
||||
* Add new shardings before specific shardings
|
||||
*/
|
||||
const handleAddEmptyChunkBefore = (chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
// Get the latest chunks from the ref
|
||||
const currentChunks = chunksRef.current;
|
||||
const index = currentChunks.findIndex(
|
||||
c =>
|
||||
|
||||
@@ -25,29 +25,29 @@ interface UseDeleteActionProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分片的 hook
|
||||
* Remove sharding hook
|
||||
*
|
||||
* 提供删除特定分片的功能
|
||||
* Provides the ability to remove specific shardings
|
||||
*/
|
||||
export const useDeleteAction = ({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
}: UseDeleteActionProps) => {
|
||||
// 使用ref保存最新的chunks引用
|
||||
// Use ref to save the latest chunks reference
|
||||
const chunksRef = useRef<Chunk[]>(chunks);
|
||||
const { deleteSlice } = useDeleteChunk();
|
||||
|
||||
// 每次props.chunks更新时,更新ref
|
||||
// Every time props.chunks is updated, update the ref.
|
||||
useEffect(() => {
|
||||
chunksRef.current = chunks;
|
||||
}, [chunks]);
|
||||
|
||||
/**
|
||||
* 删除特定分片
|
||||
* Remove specific shardings
|
||||
*/
|
||||
const handleDeleteChunk = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
// Get the latest chunks from the ref
|
||||
const currentChunks = chunksRef.current;
|
||||
const updatedChunks = currentChunks.filter(
|
||||
c =>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const useCreateLocalChunk = ({
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理本地分片的创建操作
|
||||
* Handle the creation of local shardings
|
||||
*/
|
||||
const createLocalChunk = async (chunk: Chunk) => {
|
||||
if (!chunk.local_slice_id) {
|
||||
|
||||
@@ -27,7 +27,7 @@ export const useDeleteLocalChunk = ({
|
||||
onChunksChange,
|
||||
}: UseDeleteLocalChunkProps) => {
|
||||
/**
|
||||
* 处理本地分片的删除操作
|
||||
* Handle deletion of local shardings
|
||||
*/
|
||||
const deleteLocalChunk = (chunk: Chunk) => {
|
||||
if (!chunk.local_slice_id) {
|
||||
|
||||
@@ -32,7 +32,7 @@ export const useDeleteRemoteChunk = ({
|
||||
const { deleteSlice } = useDeleteChunk();
|
||||
|
||||
/**
|
||||
* 处理远程分片的删除操作
|
||||
* Handling remotely sharding deletion operations
|
||||
*/
|
||||
const deleteRemoteChunk = async (chunk: Chunk) => {
|
||||
if (!chunk.slice_id) {
|
||||
|
||||
@@ -40,16 +40,16 @@ export const useInitEditor = ({
|
||||
editorProps,
|
||||
onChange,
|
||||
}: UseDocumentEditorProps) => {
|
||||
// 创建编辑器实例
|
||||
// Create an editor instance
|
||||
const editor: Editor | null = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
hardBreak: {
|
||||
// 强制换行
|
||||
// force wrap
|
||||
keepMarks: false,
|
||||
},
|
||||
paragraph: {
|
||||
// 配置段落,避免生成多余的空段落
|
||||
// Configure paragraphs to avoid generating extra empty paragraphs
|
||||
HTMLAttributes: {
|
||||
class: 'text-knowledge-tiptap-editor-paragraph',
|
||||
},
|
||||
@@ -88,30 +88,30 @@ export const useInitEditor = ({
|
||||
}
|
||||
const text = event.clipboardData?.getData('text/plain');
|
||||
|
||||
// 如果粘贴的纯文本中包含换行符
|
||||
// If the pasted plain text contains a newline character
|
||||
if (text?.includes('\n')) {
|
||||
event.preventDefault(); // 阻止默认粘贴行为
|
||||
event.preventDefault(); // Block default paste behavior
|
||||
|
||||
const html = getRenderHtmlContent(text);
|
||||
|
||||
// 将转换后的 HTML 插入编辑器
|
||||
// Insert the converted HTML into the editor
|
||||
editor.chain().focus().insertContent(html).run();
|
||||
|
||||
return true; // 表示我们已处理
|
||||
return true; // It means we have dealt with it.
|
||||
}
|
||||
|
||||
return false; // 使用默认行为
|
||||
return false; // Use default behavior
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 当激活的分片改变时,更新编辑器内容
|
||||
// Update editor content when active sharding changes
|
||||
useEffect(() => {
|
||||
if (!editor || !chunk) {
|
||||
return;
|
||||
}
|
||||
const htmlContent = getRenderHtmlContent(chunk.content || '');
|
||||
// 设置内容,保留换行符
|
||||
// Set content, keep newlines
|
||||
editor.commands.setContent(htmlContent || '', false, {
|
||||
preserveWhitespace: 'full',
|
||||
});
|
||||
|
||||
@@ -37,16 +37,16 @@ export const usePreviewContextMenu = ({
|
||||
onActiveChunkChange,
|
||||
onAddChunk,
|
||||
}: UsePreviewContextMenuProps) => {
|
||||
// 使用ref保存最新的chunks引用
|
||||
// Use ref to save the latest chunks reference
|
||||
const chunksRef = useRef(chunks);
|
||||
const { deleteSlice } = useDeleteChunk();
|
||||
|
||||
// 每次props.chunks更新时,更新ref
|
||||
// Every time props.chunks is updated, update the ref.
|
||||
useEffect(() => {
|
||||
chunksRef.current = chunks;
|
||||
}, [chunks]);
|
||||
|
||||
// 激活特定分片的编辑模式
|
||||
// Activate edit mode for specific shardings
|
||||
const handleActivateEditMode = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
onActiveChunkChange?.(chunk);
|
||||
@@ -54,10 +54,10 @@ export const usePreviewContextMenu = ({
|
||||
[onActiveChunkChange],
|
||||
);
|
||||
|
||||
// 在特定分片前添加新分片
|
||||
// Add new shardings before specific shardings
|
||||
const handleAddChunkBefore = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
// Get the latest chunks from the ref
|
||||
const currentChunks = chunksRef.current;
|
||||
const index = currentChunks.findIndex(
|
||||
c =>
|
||||
@@ -86,17 +86,17 @@ export const usePreviewContextMenu = ({
|
||||
|
||||
onChunksChange?.(updatedChunks);
|
||||
|
||||
// 自动激活新分片的编辑模式
|
||||
// Automatically activate editing mode for new shardings
|
||||
onActiveChunkChange?.(newChunk);
|
||||
onAddChunk?.(newChunk);
|
||||
},
|
||||
[onChunksChange, onActiveChunkChange, documentId, onAddChunk],
|
||||
);
|
||||
|
||||
// 在特定分片后添加新分片
|
||||
// Add new shardings after specific shardings
|
||||
const handleAddChunkAfter = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
// Get the latest chunks from the ref
|
||||
const currentChunks = chunksRef.current;
|
||||
const index = currentChunks.findIndex(
|
||||
c =>
|
||||
@@ -123,7 +123,7 @@ export const usePreviewContextMenu = ({
|
||||
...currentChunks.slice(index + 1),
|
||||
];
|
||||
|
||||
// 自动激活新分片的编辑模式
|
||||
// Automatically activate editing mode for new shardings
|
||||
onActiveChunkChange?.(newChunk);
|
||||
onChunksChange?.(updatedChunks);
|
||||
onAddChunk?.(newChunk);
|
||||
@@ -131,10 +131,10 @@ export const usePreviewContextMenu = ({
|
||||
[onChunksChange, onActiveChunkChange, documentId, onAddChunk],
|
||||
);
|
||||
|
||||
// 删除特定分片
|
||||
// Remove specific shardings
|
||||
const handleDeleteChunk = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// 从ref中获取最新的chunks
|
||||
// Get the latest chunks from the ref
|
||||
const currentChunks = chunksRef.current;
|
||||
const updatedChunks = currentChunks.filter(
|
||||
c =>
|
||||
|
||||
@@ -63,7 +63,7 @@ export const useSaveChunk = ({
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理远程分片的保存逻辑
|
||||
* Handling save logic for remote shardings
|
||||
*/
|
||||
const saveRemoteChunk = async (chunk: Chunk) => {
|
||||
if (chunk.content === '') {
|
||||
@@ -74,7 +74,7 @@ export const useSaveChunk = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理本地分片的保存逻辑
|
||||
* Save logic for handling local shardings
|
||||
*/
|
||||
const saveLocalChunk = async (chunk: Chunk) => {
|
||||
if (chunk.content === '') {
|
||||
@@ -85,7 +85,7 @@ export const useSaveChunk = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存分片的主函数
|
||||
* Save the main function of sharding
|
||||
*/
|
||||
const saveChunk = async (chunk: Chunk) => {
|
||||
if (!chunk.local_slice_id) {
|
||||
|
||||
@@ -35,7 +35,7 @@ export const useUpdateRemoteChunk = ({
|
||||
const { updateSlice } = useUpdateChunk();
|
||||
|
||||
/**
|
||||
* 处理远程分片的更新操作
|
||||
* Handling updates for remote sharding
|
||||
*/
|
||||
const updateRemoteChunk = async (chunk: Chunk) => {
|
||||
if (!chunk.slice_id) {
|
||||
|
||||
@@ -28,7 +28,7 @@ export {
|
||||
export { BaseTextKnowledgeEditor } from './scenes/base';
|
||||
export type { Editor } from '@tiptap/react';
|
||||
|
||||
// 新增组件导出
|
||||
// Add component export
|
||||
export { HoverEditBar } from './features/hover-edit-bar/hover-edit-bar';
|
||||
export {
|
||||
EditAction,
|
||||
@@ -37,7 +37,7 @@ export {
|
||||
DeleteAction,
|
||||
} from './features/hover-edit-bar-actions';
|
||||
|
||||
// 事件总线相关导出
|
||||
// Event Bus Dependent Export
|
||||
export {
|
||||
eventBus,
|
||||
createEventBus,
|
||||
|
||||
@@ -46,12 +46,12 @@ export const BaseTextKnowledgeEditor = ({
|
||||
const [chunks, setChunks] = useState<DocumentChunk[]>(initialChunks);
|
||||
const [activeChunk, setActiveChunk] = useState<DocumentChunk | null>(null);
|
||||
|
||||
// 使用编辑器核心功能
|
||||
// Using the core editor functions
|
||||
const { editor } = useInitEditor({
|
||||
chunk: activeChunk,
|
||||
});
|
||||
|
||||
// 退出新增分片功能
|
||||
// Exit the new sharding feature
|
||||
const { saveChunk } = useSaveChunk({
|
||||
chunks,
|
||||
documentId,
|
||||
@@ -63,7 +63,7 @@ export const BaseTextKnowledgeEditor = ({
|
||||
onDeleteChunk,
|
||||
});
|
||||
|
||||
// 监听右键菜单事件
|
||||
// Monitor right-click menu events
|
||||
useEventListener(
|
||||
'previewContextMenuItemAction',
|
||||
useCallback(
|
||||
@@ -88,7 +88,7 @@ export const BaseTextKnowledgeEditor = ({
|
||||
),
|
||||
);
|
||||
|
||||
// 监听悬浮编辑栏事件
|
||||
// Monitor floating edit bar events
|
||||
useEventListener(
|
||||
'hoverEditBarAction',
|
||||
useCallback(
|
||||
|
||||
@@ -24,18 +24,18 @@ export interface ActiveChunkInfo {
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理文档中活动的chunk
|
||||
* 使用renderLevel字段来唯一标识chunk的渲染位置
|
||||
* Manage active chunks in documents
|
||||
* Use the renderLevel field to uniquely identify the render location of the chunk
|
||||
*/
|
||||
export const useActiveChunk = () => {
|
||||
// 存储活动的chunk和它的renderLevel
|
||||
// Store the active chunk and its renderLevel
|
||||
const [activeChunkInfo, setActiveChunkInfo] = useState<ActiveChunkInfo>({
|
||||
chunk: null,
|
||||
renderLevel: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* 清除活动chunk信息
|
||||
* Clear active chunks
|
||||
*/
|
||||
const clearActiveChunk = () => {
|
||||
setActiveChunkInfo({
|
||||
@@ -45,8 +45,8 @@ export const useActiveChunk = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置活动chunk和它的renderLevel
|
||||
* 在用户交互(如双击)时使用
|
||||
* Set the active chunk and its renderLevel
|
||||
* Use during user interaction (e.g. double-clicking)
|
||||
*/
|
||||
const setActiveChunkWithLevel = (chunk: LevelDocumentTreeNode) => {
|
||||
if (!chunk.renderLevel) {
|
||||
@@ -61,7 +61,7 @@ export const useActiveChunk = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查给定的chunk是否是当前活动的chunk
|
||||
* Checks whether the given chunk is the currently active chunk
|
||||
*/
|
||||
const isActiveChunk = (renderLevel: string | undefined) => {
|
||||
if (!renderLevel) {
|
||||
|
||||
@@ -24,19 +24,19 @@ export interface ActiveChunkInfo {
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理文档中具有相同ID的chunk的渲染路径
|
||||
* 通过为每个chunk实例分配唯一的渲染路径,解决重复ID的问题
|
||||
* Manage the rendering path for chunks with the same ID in a document
|
||||
* Solve the problem of duplicate IDs by assigning a unique render path to each chunk instance
|
||||
*/
|
||||
export const useChunkRenderPath = () => {
|
||||
// 存储活动的chunk和它的渲染路径
|
||||
// Store the active chunk and its render path
|
||||
const [activeChunkInfo, setActiveChunkInfo] = useState<ActiveChunkInfo>({
|
||||
chunk: null,
|
||||
renderPath: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* 设置活动chunk,但不设置渲染路径
|
||||
* 通常在外部逻辑中使用,如usePreviewContextMenu
|
||||
* Set the active chunk, but not the render path
|
||||
* Usually used in external logic, such as usePreviewContextMenu
|
||||
*/
|
||||
const setActiveChunk = (chunk: LevelDocumentChunk | null) => {
|
||||
setActiveChunkInfo(prev => ({
|
||||
@@ -46,7 +46,7 @@ export const useChunkRenderPath = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除活动chunk信息
|
||||
* Clear active chunks
|
||||
*/
|
||||
const clearActiveChunk = () => {
|
||||
setActiveChunkInfo({
|
||||
@@ -56,8 +56,8 @@ export const useChunkRenderPath = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置活动chunk和它的渲染路径
|
||||
* 在用户交互(如双击)时使用
|
||||
* Set the active chunk and its rendering path
|
||||
* Use during user interaction (e.g. double-clicking)
|
||||
*/
|
||||
const setActiveChunkWithPath = (
|
||||
chunk: LevelDocumentChunk,
|
||||
@@ -70,14 +70,14 @@ export const useChunkRenderPath = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查给定的chunk和渲染路径是否匹配当前活动的chunk
|
||||
* Checks whether the given chunk and rendering path match the currently active chunk
|
||||
*/
|
||||
const isActiveChunk = (chunkId: string, renderPath: string) =>
|
||||
chunkId === activeChunkInfo.chunk?.text_knowledge_editor_chunk_uuid &&
|
||||
renderPath === activeChunkInfo.renderPath;
|
||||
|
||||
/**
|
||||
* 为chunk生成唯一的渲染路径
|
||||
* Generate a unique render path for chunks
|
||||
*/
|
||||
const generateRenderPath = (basePath: string, chunkId: string) =>
|
||||
`${basePath}-${chunkId}`;
|
||||
|
||||
@@ -19,8 +19,8 @@ import { useEffect } from 'react';
|
||||
import { createLocateChunkId } from '../../services/locate-segment';
|
||||
|
||||
/**
|
||||
* 滚动到选中的元素
|
||||
* @param selectionIDs 选中的元素ID数组
|
||||
* Scroll to the selected element
|
||||
* @Param selectionIDs array of selected element IDs
|
||||
*/
|
||||
export const useScrollToSelection = (selectionIDs?: string[]) => {
|
||||
useEffect(() => {
|
||||
|
||||
@@ -66,7 +66,7 @@ export const LevelTextKnowledgeEditor: React.FC<
|
||||
[chunks],
|
||||
);
|
||||
|
||||
// 使用活动chunk hook
|
||||
// Using active chunk hooks
|
||||
const {
|
||||
activeChunkInfo,
|
||||
clearActiveChunk,
|
||||
@@ -74,12 +74,12 @@ export const LevelTextKnowledgeEditor: React.FC<
|
||||
isActiveChunk,
|
||||
} = useActiveChunk();
|
||||
|
||||
// 使用编辑器核心功能
|
||||
// Using the core editor functions
|
||||
const { editor } = useInitEditor({
|
||||
chunk: activeChunkInfo.chunk,
|
||||
});
|
||||
|
||||
// 分片功能
|
||||
// Sharding function
|
||||
const { saveChunk } = useSaveChunk({
|
||||
chunks: initialChunks,
|
||||
documentId,
|
||||
@@ -92,10 +92,10 @@ export const LevelTextKnowledgeEditor: React.FC<
|
||||
},
|
||||
});
|
||||
|
||||
// 使用滚动到选中元素的hook
|
||||
// Use the hook that scrolls to the selected element
|
||||
useScrollToSelection(selectionIDs);
|
||||
|
||||
// 监听右键菜单事件
|
||||
// Monitor right-click menu events
|
||||
useEventListener(
|
||||
'previewContextMenuItemAction',
|
||||
useCallback(({ type, targetChunk, chunks: newChunks }) => {
|
||||
@@ -109,7 +109,7 @@ export const LevelTextKnowledgeEditor: React.FC<
|
||||
}, []),
|
||||
);
|
||||
|
||||
// 监听悬浮编辑栏事件
|
||||
// Monitor floating edit bar events
|
||||
useEventListener(
|
||||
'hoverEditBarAction',
|
||||
useCallback(({ type, targetChunk, chunks: newChunks }) => {
|
||||
@@ -231,7 +231,7 @@ const RenderContent = ({
|
||||
].includes(levelDocumentTree.type) ? (
|
||||
<div key={levelDocumentTree.text_knowledge_editor_chunk_uuid}>
|
||||
{(() => {
|
||||
// 检查这个chunk是否是当前活动的chunk,使用renderLevel字段
|
||||
// Check whether this chunk is the currently active chunk, using the renderLevel field
|
||||
const chunkIsActive = isActiveChunk(levelDocumentTree.renderLevel);
|
||||
|
||||
if (chunkIsActive) {
|
||||
@@ -261,7 +261,7 @@ const RenderContent = ({
|
||||
previewContextMenuItemsContributes
|
||||
}
|
||||
onActivateEditMode={activedChunk => {
|
||||
// 设置活动chunk,使用chunk自身的renderLevel
|
||||
// Set the active chunk, using the renderLevel of the chunk itself
|
||||
setActiveChunkWithLevel(
|
||||
activedChunk as LevelDocumentTreeNode,
|
||||
);
|
||||
|
||||
@@ -71,7 +71,7 @@ const getChildren = (
|
||||
}
|
||||
}, []);
|
||||
|
||||
/** 兜底情况,如果 idp 的结果没有标题,则补上标题 */
|
||||
/** Bottom line, if the result of idp has no title, add the title */
|
||||
export const withTitle = (
|
||||
segments: LevelDocumentChunk[],
|
||||
title?: string,
|
||||
|
||||
@@ -24,7 +24,7 @@ export type LevelDocumentTreeNode = Omit<ILevelSegment, 'children' | 'parent'> &
|
||||
Chunk & {
|
||||
parent?: string;
|
||||
children?: LevelDocumentTreeNode[];
|
||||
renderLevel?: string; // 用于唯一标识chunk的渲染路径
|
||||
renderLevel?: string; // Rendering path used to uniquely identify chunks
|
||||
};
|
||||
|
||||
export type LevelDocumentTree = LevelDocumentTreeNode[];
|
||||
|
||||
@@ -31,7 +31,7 @@ export const createLocalChunk = (props: { sequence: string }): Chunk => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新本地分片
|
||||
* Update local sharding
|
||||
*/
|
||||
export const updateLocalChunk = ({
|
||||
chunks,
|
||||
@@ -44,34 +44,34 @@ export const updateLocalChunk = ({
|
||||
}): Chunk[] =>
|
||||
chunks.map(c => (c.local_slice_id === localChunkSliceId ? newChunk : c));
|
||||
|
||||
// 删除本地分片
|
||||
// Delete local sharding
|
||||
export const deleteLocalChunk = (
|
||||
chunks: Chunk[],
|
||||
localChunkSliceId: string,
|
||||
): Chunk[] => chunks.filter(c => c.local_slice_id !== localChunkSliceId);
|
||||
|
||||
/**
|
||||
* 更新文档分片内容
|
||||
* Update document sharding content
|
||||
*/
|
||||
export const updateChunkContent = (chunk: Chunk, content: string): Chunk => ({
|
||||
...chunk,
|
||||
content,
|
||||
});
|
||||
|
||||
// 删除远程分片
|
||||
// Delete remote sharding
|
||||
export const deleteRemoteChunk = (
|
||||
chunks: Chunk[],
|
||||
remoteChunkSliceId: string,
|
||||
): Chunk[] => chunks.filter(c => c.slice_id !== remoteChunkSliceId);
|
||||
|
||||
/**
|
||||
* 更新chunks
|
||||
* Update chunks
|
||||
*/
|
||||
export const updateChunks = (chunks: Chunk[], chunk: Chunk): Chunk[] =>
|
||||
chunks.map(c => (c.slice_id === chunk.slice_id ? chunk : c));
|
||||
|
||||
/**
|
||||
* 创建远程分片
|
||||
* Create remote sharding
|
||||
*/
|
||||
export const createRemoteChunk = (props: {
|
||||
sequence: string;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
|
||||
/**
|
||||
* 更新文档分片内容
|
||||
* Update document sharding content
|
||||
*/
|
||||
export const updateChunkContent = (chunk: Chunk, content: string): Chunk => ({
|
||||
...chunk,
|
||||
@@ -25,13 +25,13 @@ export const updateChunkContent = (chunk: Chunk, content: string): Chunk => ({
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新chunks
|
||||
* Update chunks
|
||||
*/
|
||||
export const updateChunks = (chunks: Chunk[], chunk: Chunk): Chunk[] =>
|
||||
chunks.map(c => (c.slice_id === chunk.slice_id ? chunk : c));
|
||||
|
||||
/**
|
||||
* 获取激活的分片
|
||||
* Get active sharding
|
||||
*/
|
||||
export const getActiveChunk = (
|
||||
chunks: Chunk[],
|
||||
@@ -44,15 +44,15 @@ export const getActiveChunk = (
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理编辑器输出的HTML内容
|
||||
* 移除不必要的外层<p>标签,保持与原始内容格式一致
|
||||
* Process the HTML content output by the editor
|
||||
* Remove unnecessary outer < p > tags to maintain the original content format
|
||||
*/
|
||||
export const processEditorContent = (content: string): string => {
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 如果内容被<p>标签包裹,并且只有一个<p>标签
|
||||
// If the content is wrapped with < p > tags, and there is only one < p > tag
|
||||
const singleParagraphMatch = content.match(/^<p>(.*?)<\/p>$/s);
|
||||
if (singleParagraphMatch) {
|
||||
return singleParagraphMatch[1];
|
||||
|
||||
@@ -18,7 +18,7 @@ import classNames from 'classnames';
|
||||
|
||||
export const getEditorTableClassname = () =>
|
||||
classNames(
|
||||
// 表格样式
|
||||
// table style
|
||||
'[&_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',
|
||||
|
||||
@@ -18,12 +18,12 @@ import classNames from 'classnames';
|
||||
|
||||
export const getEditorWordsCls = () =>
|
||||
classNames(
|
||||
// 换行
|
||||
// line feed
|
||||
'[&_p]:break-words [&_p]:whitespace-pre-wrap',
|
||||
// 保留所有空格和换行符
|
||||
// Keep all spaces and line breaks
|
||||
'[&_.ProseMirror_*]:break-words [&_.ProseMirror_*]:whitespace-pre-wrap',
|
||||
// 段落
|
||||
// paragraph
|
||||
'[&_.editor-paragraph]:min-h-[1.5em] [&_.editor-paragraph]:leading-normal',
|
||||
// 空段落
|
||||
// Empty paragraph
|
||||
'[&_.editor-paragraph:empty]:min-h-[1.5em] [&_.editor-paragraph:empty]:block',
|
||||
);
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
import { type Editor } from '@tiptap/react';
|
||||
|
||||
/**
|
||||
* 获取编辑器内容
|
||||
* 返回用户真实编辑的内容,移除TipTap自动添加的外层<p>标签
|
||||
* Get editor content
|
||||
* Return the user's actual edited content and remove the outer < p > tag automatically added by TipTap
|
||||
*/
|
||||
export const getEditorContent = (editor: Editor | null) => {
|
||||
if (!editor) {
|
||||
@@ -29,38 +29,38 @@ export const getEditorContent = (editor: Editor | null) => {
|
||||
|
||||
const doc = removeEditorWrapperParagraph(content);
|
||||
|
||||
// 返回处理后的HTML
|
||||
// Returns the processed HTML.
|
||||
return doc;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理编辑器输出的HTML内容
|
||||
* 移除不必要的外层<p>标签,保持与原始内容格式一致
|
||||
* Process the HTML content output by the editor
|
||||
* Remove unnecessary outer < p > tags to maintain the original content format
|
||||
*/
|
||||
export const removeEditorWrapperParagraph = (content: string): string => {
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 使用DOM解析器来处理HTML
|
||||
// Using a DOM parser to process HTML
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(content, 'text/html');
|
||||
|
||||
// 找到所有编辑器生成的p标签
|
||||
// Find all editor-generated p tags
|
||||
const generatedParagraphs = doc.querySelectorAll(
|
||||
'p.text-knowledge-tiptap-editor-paragraph',
|
||||
);
|
||||
|
||||
// 替换这些p标签为它们的内容
|
||||
// Replace these p tags with their content
|
||||
generatedParagraphs.forEach(p => {
|
||||
const parent = p.parentNode;
|
||||
if (parent) {
|
||||
// 创建一个文档片段来存储p标签的内容
|
||||
// Create a document fragment to store the contents of the p tag
|
||||
const fragment = document.createDocumentFragment();
|
||||
while (p.firstChild) {
|
||||
fragment.appendChild(p.firstChild);
|
||||
}
|
||||
// 用内容替换p标签
|
||||
// Replace p tags with content
|
||||
parent.replaceChild(fragment, p);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 编辑器对/n不会换行,所以需要转换为<br />标签
|
||||
* The editor doesn't wrap/n, so it needs to be converted to a < br/> tag
|
||||
*/
|
||||
export const getInitEditorContent = (content: string) => {
|
||||
if (content === '') {
|
||||
|
||||
@@ -17,16 +17,16 @@
|
||||
import { escapeHtml } from '@/text-knowledge-editor/utils/escape-html';
|
||||
|
||||
/**
|
||||
* 获取渲染后的HTML内容
|
||||
* Get the rendered HTML content
|
||||
*/
|
||||
export const getRenderHtmlContent = (content: string) => {
|
||||
if (content === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 转义HTML,只允许白名单中的标签
|
||||
// Escape HTML, allowing only whitelisted tags
|
||||
const htmlContent = escapeHtml(content);
|
||||
|
||||
// 编辑器对/n不会换行,所以需要转换为<br />标签
|
||||
// The editor doesn't wrap/n, so it needs to be converted to a < br/> tag
|
||||
return htmlContent.replace(/\n/g, '<br />');
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ import { type Chunk } from '@/text-knowledge-editor/types/chunk';
|
||||
import { processEditorContent } from '../inner/document-editor.service';
|
||||
|
||||
/**
|
||||
* 判断内容是否改变
|
||||
* Determine whether the content has changed
|
||||
*/
|
||||
export const isEditorContentChange = (
|
||||
chunks: Chunk[],
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* html白名单,防止XSS攻击
|
||||
* HTML whitelist to prevent XSS attacks
|
||||
*/
|
||||
|
||||
// 默认允许的HTML标签白名单
|
||||
// Whitelist of HTML tags allowed by default
|
||||
const DEFAULT_ALLOWED_TAGS = [
|
||||
'img',
|
||||
'table',
|
||||
@@ -35,10 +35,10 @@ const DEFAULT_ALLOWED_TAGS = [
|
||||
];
|
||||
|
||||
/**
|
||||
* 转义HTML,只允许白名单中的标签
|
||||
* @param unsafe 不安全的HTML字符串
|
||||
* @param allowedTags 允许的HTML标签数组,默认为DEFAULT_ALLOWED_TAGS
|
||||
* @returns 转义后的HTML字符串
|
||||
* Escape HTML, allowing only whitelisted tags
|
||||
* @param unsafe HTML string
|
||||
* @Param allowedTags Array of HTML tags allowed, default to DEFAULT_ALLOWED_TAGS
|
||||
* @Returns the escaped HTML string
|
||||
*/
|
||||
export function escapeHtml(
|
||||
unsafe: string,
|
||||
@@ -48,7 +48,7 @@ export function escapeHtml(
|
||||
return '';
|
||||
}
|
||||
|
||||
// 构建正则表达式模式
|
||||
// Building regular expression patterns
|
||||
const allowedTagsPattern = allowedTags.join('|');
|
||||
const tagRegex = new RegExp(
|
||||
`<(?!(${allowedTagsPattern})\\b[^>]*>|\\/(?:${allowedTagsPattern})>)`,
|
||||
|
||||
Reference in New Issue
Block a user