feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
@@ -0,0 +1,31 @@
|
||||
import { mergeConfig } from 'vite';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
viteFinal: config =>
|
||||
mergeConfig(config, {
|
||||
plugins: [
|
||||
svgr({
|
||||
svgrOptions: {
|
||||
native: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,14 @@
|
||||
/** @type { import('@storybook/react').Preview } */
|
||||
const preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/data/knowledge/common/components/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-data/knowledge-common-components
|
||||
|
||||
> Project template for react component with storybook.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"name": "@coze-data/knowledge-common-components",
|
||||
"version": "0.0.1",
|
||||
"description": "@coze-data/knowledge-common-components",
|
||||
"license": "Apache-2.0",
|
||||
"author": "haozhenfei@bytedance.com",
|
||||
"maintainers": [],
|
||||
"exports": {
|
||||
".": "./src/index.tsx",
|
||||
"./file-picker": "./src/file-picker/index.tsx",
|
||||
"./text-knowledge-editor": "./src/text-knowledge-editor/index.tsx",
|
||||
"./text-knowledge-editor/*": "./src/text-knowledge-editor/*/index.tsx"
|
||||
},
|
||||
"main": "src/index.tsx",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"file-picker": [
|
||||
"./src/file-picker/index.tsx"
|
||||
],
|
||||
"text-knowledge-editor": [
|
||||
"./src/text-knowledge-editor/index.tsx"
|
||||
],
|
||||
"text-knowledge-editor/*": [
|
||||
"./src/text-knowledge-editor/*/index.tsx"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-error": "workspace:*",
|
||||
"@coze-arch/bot-md-box-adapter": "workspace:*",
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-arch/report-events": "workspace:*",
|
||||
"@coze-common/virtual-list": "workspace:*",
|
||||
"@coze-data/e2e": "workspace:*",
|
||||
"@coze-data/feature-register": "workspace:*",
|
||||
"@coze-data/knowledge-stores": "workspace:*",
|
||||
"@coze-data/reporter": "workspace:*",
|
||||
"@tiptap/core": "^2.12.0",
|
||||
"@tiptap/extension-hard-break": "^2.12.0",
|
||||
"@tiptap/extension-image": "^2.12.0",
|
||||
"@tiptap/extension-table": "^2.12.0",
|
||||
"@tiptap/extension-table-cell": "^2.12.0",
|
||||
"@tiptap/extension-table-header": "^2.12.0",
|
||||
"@tiptap/extension-table-row": "^2.12.0",
|
||||
"@tiptap/pm": "^2.12.0",
|
||||
"@tiptap/react": "^2.12.0",
|
||||
"@tiptap/starter-kit": "^2.12.0",
|
||||
"ahooks": "^3.7.8",
|
||||
"classnames": "^2.3.2",
|
||||
"dompurify": "3.0.8",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"nanoid": "^4.0.2",
|
||||
"react-arborist": "^3.4.0",
|
||||
"react-pdf": "9.1.1",
|
||||
"use-resize-observer": "^9.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 151 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M0.832031 5H19.1654V15.8333C19.1654 16.7538 18.4192 17.5 17.4987 17.5H2.4987C1.57822 17.5 0.832031 16.7538 0.832031 15.8333V5Z"
|
||||
fill="#FFC60A" />
|
||||
<path
|
||||
d="M0.832031 3.33329C0.832031 2.41282 1.57822 1.66663 2.4987 1.66663H6.89425C7.29396 1.66663 7.6904 1.73852 8.06466 1.87886L10.2661 2.70439C10.6403 2.84474 11.0368 2.91663 11.4365 2.91663H17.4987C18.4192 2.91663 19.1654 3.66282 19.1654 4.58329V4.99996H0.832031V3.33329Z"
|
||||
fill="#FF9D4C" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 573 B |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 191 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.5 2.50004C2.5 1.57957 3.24619 0.833374 4.16667 0.833374H12.1548C12.3758 0.833374 12.5878 0.921171 12.7441 1.07745L17.2559 5.5893C17.4122 5.74558 17.5 5.95754 17.5 6.17855V17.5C17.5 18.4205 16.7538 19.1667 15.8333 19.1667H4.16667C3.24619 19.1667 2.5 18.4205 2.5 17.5V2.50004Z"
|
||||
fill="#336DF4" />
|
||||
<path
|
||||
d="M2.5 2.50004C2.5 1.57957 3.24619 0.833374 4.16667 0.833374H12.1548C12.3758 0.833374 12.5878 0.921171 12.7441 1.07745L17.2559 5.5893C17.4122 5.74558 17.5 5.95754 17.5 6.17855V17.5C17.5 18.4205 16.7538 19.1667 15.8333 19.1667H4.16667C3.24619 19.1667 2.5 18.4205 2.5 17.5V2.50004Z"
|
||||
fill="#35BD4B" />
|
||||
<path
|
||||
d="M5.625 7.43201H12.3864C13.4847 7.43201 14.375 8.32235 14.375 9.42064V16.182H7.61364C6.51534 16.182 5.625 15.2917 5.625 14.1934V7.43201ZM10.5638 8.62519V11.2104H13.1818V9.42064C13.1818 8.98133 12.8257 8.62519 12.3864 8.62519H10.5638ZM9.37066 8.62519H6.81818V11.2104H9.37066V8.62519ZM6.81818 12.4036V14.1934C6.81818 14.6327 7.17432 14.9888 7.61364 14.9888H9.37066V12.4036H6.81818ZM10.5638 14.9888H13.1818V12.4036H10.5638V14.9888Z"
|
||||
fill="white" />
|
||||
<path opacity="0.7"
|
||||
d="M12.5 1.23574C12.5 1.08726 12.6795 1.0129 12.7845 1.11789L17.2155 5.54886C17.3205 5.65385 17.2461 5.83337 17.0976 5.83337H14.1667C13.2462 5.83337 12.5 5.08718 12.5 4.16671V1.23574Z"
|
||||
fill="#298C38" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.5 2.50004C2.5 1.57957 3.24619 0.833374 4.16667 0.833374H12.1548C12.3758 0.833374 12.5878 0.921171 12.7441 1.07745L17.2559 5.5893C17.4122 5.74558 17.5 5.95754 17.5 6.17855V17.5C17.5 18.4205 16.7538 19.1667 15.8333 19.1667H4.16667C3.24619 19.1667 2.5 18.4205 2.5 17.5V2.50004Z"
|
||||
fill="#336DF4" />
|
||||
<path
|
||||
d="M2.5 2.50004C2.5 1.57957 3.24619 0.833374 4.16667 0.833374H12.1548C12.3758 0.833374 12.5878 0.921171 12.7441 1.07745L17.2559 5.5893C17.4122 5.74558 17.5 5.95754 17.5 6.17855V17.5C17.5 18.4205 16.7538 19.1667 15.8333 19.1667H4.16667C3.24619 19.1667 2.5 18.4205 2.5 17.5V2.50004Z"
|
||||
fill="#336DF4" />
|
||||
<path
|
||||
d="M5.83203 13.5773C5.83203 13.2321 6.11185 12.9523 6.45703 12.9523H11.0404C11.3855 12.9523 11.6654 13.2321 11.6654 13.5773C11.6654 13.9224 11.3855 14.2023 11.0404 14.2023H6.45703C6.11185 14.2023 5.83203 13.9224 5.83203 13.5773Z"
|
||||
fill="white" />
|
||||
<path
|
||||
d="M6.45703 9.20227C6.11185 9.20227 5.83203 9.48209 5.83203 9.82727C5.83203 10.1724 6.11185 10.4523 6.45703 10.4523H13.5404C13.8855 10.4523 14.1654 10.1724 14.1654 9.82727C14.1654 9.48209 13.8855 9.20227 13.5404 9.20227H6.45703Z"
|
||||
fill="white" />
|
||||
<path opacity="0.7"
|
||||
d="M12.5 1.23574C12.5 1.08726 12.6795 1.0129 12.7845 1.11789L17.2155 5.54886C17.3205 5.65385 17.2461 5.83337 17.0976 5.83337H14.1667C13.2462 5.83337 12.5 5.08718 12.5 4.16671V1.23574Z"
|
||||
fill="#0442D2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
|
||||
interface IPreviewMdProps {
|
||||
fileUrl: string;
|
||||
}
|
||||
|
||||
function wait(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const PreviewMd = (props: IPreviewMdProps) => {
|
||||
const { fileUrl } = props;
|
||||
const [mdContent, setMdContent] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
fetch(fileUrl)
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
setLoading(false);
|
||||
setMdContent(text);
|
||||
});
|
||||
}, [fileUrl]);
|
||||
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function render() {
|
||||
if (ref.current) {
|
||||
for (
|
||||
let i = 0, len = mdContent.length;
|
||||
i < Math.ceil(len / 50_000);
|
||||
i++
|
||||
) {
|
||||
await wait(10);
|
||||
ref.current.textContent += mdContent.slice(
|
||||
i * 50_000,
|
||||
(i + 1) * 50_000,
|
||||
);
|
||||
}
|
||||
ref.current.textContent = mdContent;
|
||||
}
|
||||
}
|
||||
|
||||
render();
|
||||
}, [mdContent]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full h-full flex-1 py-2 px-4">
|
||||
<Spin
|
||||
wrapperClassName="w-full h-full grow"
|
||||
spinning={loading}
|
||||
childStyle={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{mdContent ? (
|
||||
<div>
|
||||
<pre
|
||||
className="max-w-full overflow-auto whitespace-pre-wrap break-all text-[14px] leading-[22px]"
|
||||
ref={ref}
|
||||
>
|
||||
{/* {mdContent} */}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
interface IPreviewTxtProps {
|
||||
fileUrl: string;
|
||||
}
|
||||
|
||||
function wait(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const PreviewTxt = (props: IPreviewTxtProps) => {
|
||||
const { fileUrl } = props;
|
||||
const [txtContent, setTxtContent] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
fetch(fileUrl)
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
setLoading(false);
|
||||
setTxtContent(text);
|
||||
});
|
||||
}, [fileUrl]);
|
||||
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function render() {
|
||||
if (ref.current) {
|
||||
for (
|
||||
let i = 0, len = txtContent.length;
|
||||
i < Math.ceil(len / 50_000);
|
||||
i++
|
||||
) {
|
||||
await wait(10);
|
||||
if (ref.current) {
|
||||
ref.current.textContent += txtContent.slice(
|
||||
i * 50_000,
|
||||
(i + 1) * 50_000,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (ref.current) {
|
||||
ref.current.textContent = txtContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render();
|
||||
}, [txtContent]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full h-full flex-1 py-2 px-4">
|
||||
<Spin
|
||||
wrapperClassName="w-full h-full grow"
|
||||
spinning={loading}
|
||||
childStyle={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<pre
|
||||
className="max-w-full overflow-auto whitespace-pre-wrap break-all text-[14px] leading-[22px]"
|
||||
ref={ref}
|
||||
>
|
||||
{/* {txtContent} */}
|
||||
</pre>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { FixedSizeList as List, AutoSizer } from '@coze-common/virtual-list';
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
import 'react-pdf/dist/Page/TextLayer.css';
|
||||
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||
|
||||
interface IUsePreviewPdfProps {
|
||||
fileUrl: string;
|
||||
}
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc =
|
||||
REGION === 'cn'
|
||||
? // cp-disable-next-line
|
||||
`//lf-cdn.coze.cn/obj/unpkg/pdfjs-dist/${pdfjs.version}/build/pdf.worker.min.mjs`
|
||||
: // cp-disable-next-line
|
||||
`//sf-cdn.coze.com/obj/unpkg-va/pdfjs-dist/${pdfjs.version}/build/pdf.worker.min.mjs`;
|
||||
|
||||
const options = {
|
||||
cMapUrl:
|
||||
REGION === 'cn'
|
||||
? // cp-disable-next-line
|
||||
`//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/`,
|
||||
// 提升性能
|
||||
cMapPacked: true,
|
||||
};
|
||||
|
||||
export const usePreviewPdf = (props: IUsePreviewPdfProps) => {
|
||||
const { fileUrl } = props;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [numPages, setNumPages] = useState<number>(0);
|
||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const listRef = useRef<List>(null);
|
||||
const [pageHeight, setPageHeight] = useState(
|
||||
containerRef.current?.clientHeight,
|
||||
);
|
||||
const itemSize = Math.floor((pageHeight ?? 500) + 20);
|
||||
|
||||
const onNext = () => {
|
||||
if (currentPage < numPages - 1) {
|
||||
const nextPage = currentPage + 1;
|
||||
setCurrentPage(nextPage);
|
||||
listRef.current?.scrollToItem(nextPage, 'start');
|
||||
}
|
||||
};
|
||||
|
||||
const onBack = () => {
|
||||
if (currentPage > 0) {
|
||||
const prevPage = currentPage - 1;
|
||||
setCurrentPage(prevPage);
|
||||
listRef.current?.scrollToItem(prevPage, 'start');
|
||||
}
|
||||
};
|
||||
|
||||
const onDocumentLoadSuccess = ({
|
||||
numPages: totalPages,
|
||||
}: {
|
||||
numPages: number;
|
||||
}) => {
|
||||
setLoading(false);
|
||||
setNumPages(totalPages);
|
||||
};
|
||||
|
||||
const handleScroll = ({ scrollOffset }: { scrollOffset: number }) => {
|
||||
const newIndex = Math.floor(scrollOffset / itemSize);
|
||||
setCurrentPage(newIndex);
|
||||
};
|
||||
|
||||
const [scale, setScale] = useState(1);
|
||||
const increaseScale = () => {
|
||||
setScale(prevScale => Math.min(prevScale + 0.1, 2));
|
||||
};
|
||||
const decreaseScale = () => {
|
||||
setScale(prevScale => Math.max(prevScale - 0.1, 0.5));
|
||||
};
|
||||
|
||||
const memoizedList = useMemo(
|
||||
() =>
|
||||
!loading ? (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
height={height ?? 0}
|
||||
itemCount={numPages}
|
||||
itemSize={itemSize}
|
||||
width={width ?? 0}
|
||||
onScroll={handleScroll}
|
||||
ref={listRef}
|
||||
>
|
||||
{({
|
||||
index,
|
||||
style,
|
||||
}: {
|
||||
index: number;
|
||||
style: React.CSSProperties;
|
||||
}) => (
|
||||
<div key={`page_${index + 1}`} style={style}>
|
||||
<Page
|
||||
pageNumber={index + 1}
|
||||
className={cls(
|
||||
'flex items-center justify-center !coz-bg-primary',
|
||||
)}
|
||||
width={(width ?? 100) - 32}
|
||||
scale={scale}
|
||||
onLoadSuccess={page => {
|
||||
setPageHeight(page.height);
|
||||
}}
|
||||
loading={
|
||||
<div
|
||||
style={{
|
||||
height: containerRef.current?.clientHeight,
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
</AutoSizer>
|
||||
) : null,
|
||||
[loading, numPages, pageHeight, scale, itemSize],
|
||||
);
|
||||
|
||||
const pdfNode = (
|
||||
<>
|
||||
<div className="flex flex-col items-center w-full h-full relative coz-bg-primary">
|
||||
<div
|
||||
className={cls(
|
||||
'absolute top-0 left-0 right-0 bot-0 flex items-center justify-center h-full',
|
||||
'z-10',
|
||||
!loading && 'invisible',
|
||||
)}
|
||||
>
|
||||
<Spin />
|
||||
</div>
|
||||
<div
|
||||
className={cls(
|
||||
'flex absolute top-0 left-0 right-0 overflow-auto px-5 w-full h-full justify-center',
|
||||
loading && 'invisible',
|
||||
)}
|
||||
ref={containerRef}
|
||||
>
|
||||
<Document
|
||||
file={fileUrl}
|
||||
// bug fix https://github.com/wojtekmaj/react-pdf/issues/974
|
||||
key={fileUrl}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
options={options}
|
||||
className={cls('flex w-full h-full')}
|
||||
loading={
|
||||
<Spin
|
||||
wrapperClassName="w-full h-full"
|
||||
childStyle={{
|
||||
width: containerRef.current?.clientWidth,
|
||||
height: '100%',
|
||||
}}
|
||||
></Spin>
|
||||
}
|
||||
>
|
||||
{containerRef.current ? memoizedList : null}
|
||||
</Document>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return {
|
||||
pdfNode,
|
||||
numPages,
|
||||
currentPage: currentPage + 1,
|
||||
onNext,
|
||||
onBack,
|
||||
scale,
|
||||
increaseScale,
|
||||
decreaseScale,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
/* stylelint-disable declaration-no-important */
|
||||
.expand-icon {
|
||||
transform: rotate(90deg) !important;
|
||||
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 0 !important;
|
||||
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.common-file-picker-wrapper {
|
||||
:global {
|
||||
.semi-tree-option-selected {
|
||||
background-color: var(--coz-mg-hglt-hovered) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tree-option-disable {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
.semi-tree-option-readonly {
|
||||
cursor: default;
|
||||
|
||||
&:hover{
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tree-option {
|
||||
height: 32px !important;
|
||||
margin: 1px 0 !important;
|
||||
line-height: 32px !important;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--coz-mg-secondary-hovered);
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tree-option-expand-icon {
|
||||
.expand-icon();
|
||||
}
|
||||
|
||||
.expand-placeholder {
|
||||
.expand-icon();
|
||||
}
|
||||
|
||||
.semi-tree-option-list .semi-tree-option-collapsed .semi-tree-option-expand-icon {
|
||||
transform: unset !important;
|
||||
}
|
||||
|
||||
.file-selector {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 12px;
|
||||
|
||||
.semi-checkbox-inner {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.semi-radio {
|
||||
height: 16px;
|
||||
min-height: 16px;
|
||||
line-height: 16px;
|
||||
vertical-align: top;
|
||||
|
||||
.semi-radio-inner {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-node-row-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
|
||||
height: 36px;
|
||||
margin: 2px 0;
|
||||
padding: 8px 13px;
|
||||
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 4px;
|
||||
|
||||
line-height: 20px;
|
||||
|
||||
.semi-spin-middle>.semi-spin-wrapper svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
|
||||
height: 20px;
|
||||
|
||||
line-height: 20px;
|
||||
|
||||
.file-name {
|
||||
overflow: hidden;
|
||||
|
||||
width: 80%;
|
||||
height: 20px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: var(--coz-fg-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-loading-info {
|
||||
height: 20px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
color: var(--coz-fg-hglt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* 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, { useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
import { Tree } from '@coze-arch/coze-design';
|
||||
import type {
|
||||
TreeProps,
|
||||
RenderFullLabelProps,
|
||||
} from '@coze-arch/bot-semi/Tree';
|
||||
import { CommonE2e } from '@coze-data/e2e';
|
||||
|
||||
import { distinctFileNodes, levelMapTreeNodesToMap } from '../utils';
|
||||
import {
|
||||
isFileNodeArray,
|
||||
type FileNode,
|
||||
type PickerRef,
|
||||
type TransSelectedFilesMiddleware,
|
||||
type FileId,
|
||||
type CalcCurrentSelectFilesMiddleware,
|
||||
} from '../types';
|
||||
import { useDefaultLabelRenderer } from '../hooks/useDefaultLabelRenderer';
|
||||
import {
|
||||
DEFAULT_FILENODE_LEVEL_INDENT,
|
||||
DEFAULT_VIRTUAL_CONTAINER_HEIGHT,
|
||||
DEFAULT_VIRTUAL_ITEM_HEIGHT,
|
||||
} from '../consts';
|
||||
|
||||
import styles from './common-file-picker.module.less';
|
||||
|
||||
export interface CommonFilePickerProps
|
||||
extends Omit<TreeProps, 'onChange' | 'onSelect'> {
|
||||
/** 渲染使用的树数据 */
|
||||
treeData: FileNode[];
|
||||
/** 提供一个定制化的 render tree node */
|
||||
customTreeDataRenderer?: (
|
||||
renderProps: RenderFullLabelProps,
|
||||
) => React.ReactNode;
|
||||
|
||||
/** 是否只能选中叶子结点 如果传入 customRenderTreeData 则失效 */
|
||||
onlySelectLeaf?: boolean;
|
||||
/** 是否多选 如果自定义 customTreeDataRenderer 一定要传入, 选项会影响 customTreeDataRenderer 的入参 */
|
||||
multiple?: boolean;
|
||||
|
||||
/**
|
||||
* 是否开启虚拟化
|
||||
*/
|
||||
enableVirtualize?: boolean;
|
||||
/** 虚拟化选项 */
|
||||
/** 虚拟化容器高度 */
|
||||
virtualizeHeight?: number;
|
||||
/** 每个 item 高度 */
|
||||
virtualizeItemSize?: number;
|
||||
|
||||
/** 默认已经选中的内容 可以作为 initValue 使用,也可以作为 value 的代替者使用 */
|
||||
defaultValue?: FileNode[] | FileId[];
|
||||
|
||||
/** 样式渲染特性 */
|
||||
normalLabelStyle?: React.CSSProperties;
|
||||
selectedLabelStyle?: React.CSSProperties;
|
||||
halfSelectedLabelStyle?: React.CSSProperties;
|
||||
|
||||
/** 缩进大小 默认大小 25px 如果 backgroundMode 为 position 将会反应为 left 如果为 padding 将会反应成 padding-left */
|
||||
indentSize?: number;
|
||||
|
||||
/** 树组件展开的 icon */
|
||||
expandIcon?: React.ReactNode;
|
||||
|
||||
/** onChange 业务层可以透传 */
|
||||
onChange?: (args?: FileNode[]) => void;
|
||||
|
||||
/** onSelect 业务层可以透传 */
|
||||
onSelect?: (key: string, selected: boolean, node: FileNode) => void;
|
||||
|
||||
/** 用来转换 selectedFiles, 发生在 设置选中态 到 提交给上层组件 之间 注意 处理有先后顺序,前一个中间件的返回将作为后一个的输入 */
|
||||
transSelectedFilesMiddlewares?: TransSelectedFilesMiddleware[];
|
||||
|
||||
/** 设置选择态的钩子 发生在 点击选中框 到 设置选择态 之间 处理有先后顺序 */
|
||||
selectFilesMiddlewares?: CalcCurrentSelectFilesMiddleware[];
|
||||
|
||||
/** disable select 禁止选择 */
|
||||
disableSelect?: boolean;
|
||||
|
||||
/** checkRelation: 父子节点选中态是否关联 */
|
||||
checkRelation?: 'related' | 'unRelated';
|
||||
|
||||
/** 默认展开的节点 key */
|
||||
defaultExpandKeys?: FileId[];
|
||||
}
|
||||
|
||||
function diffChangeNodes(
|
||||
prevChangeNodes: FileNode[],
|
||||
changeNodes: FileNode[],
|
||||
): [FileNode[], FileNode[], FileNode[]] {
|
||||
const addNodes: FileNode[] = [];
|
||||
const removeNodes: FileNode[] = [];
|
||||
const retainNodes: FileNode[] = [];
|
||||
const prevChangeKeysSet = new Set(prevChangeNodes.map(node => node.key));
|
||||
const changeKeysSet = new Set(changeNodes.map(node => node.key));
|
||||
|
||||
for (const changeNode of prevChangeNodes) {
|
||||
if (!changeKeysSet.has(changeNode.key)) {
|
||||
removeNodes.push(changeNode);
|
||||
}
|
||||
}
|
||||
|
||||
for (const changeNode of changeNodes) {
|
||||
if (prevChangeKeysSet.has(changeNode.key)) {
|
||||
retainNodes.push(changeNode);
|
||||
} else {
|
||||
addNodes.push(changeNode);
|
||||
}
|
||||
}
|
||||
|
||||
return [addNodes, removeNodes, retainNodes];
|
||||
}
|
||||
|
||||
function getFirstKeyOfDefaultValue(defaultValue?: FileId[] | FileNode[]) {
|
||||
if (!defaultValue || defaultValue.length === 0) {
|
||||
return '';
|
||||
}
|
||||
if (typeof defaultValue[0] === 'string') {
|
||||
return defaultValue[0];
|
||||
}
|
||||
return defaultValue[0].key;
|
||||
}
|
||||
|
||||
function transDefaultValueToFileNodes(
|
||||
treeData: FileNode[],
|
||||
defaultValue?: FileId[] | FileNode[],
|
||||
): FileNode[] {
|
||||
const treeDataMap = levelMapTreeNodesToMap(treeData);
|
||||
return (
|
||||
defaultValue?.map(element => {
|
||||
// 因为开了 onChangeWithObject 所以这里选中态要用 object 存储
|
||||
if (typeof element === 'string') {
|
||||
return (
|
||||
treeDataMap.get(element) ?? {
|
||||
key: element,
|
||||
}
|
||||
);
|
||||
}
|
||||
return element;
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
function transDefaultValueToRenderNode(defaultValue?: FileId[] | FileNode[]) {
|
||||
return (
|
||||
defaultValue?.map(element => {
|
||||
// 因为开了 onChangeWithObject 所以这里选中态要用 object 存储
|
||||
if (typeof element === 'string') {
|
||||
return {
|
||||
key: element,
|
||||
};
|
||||
}
|
||||
return element;
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ------------------
|
||||
* common file picker
|
||||
* 用于数据上传选择文件
|
||||
* ------------------
|
||||
* useImperativeHandle:
|
||||
* search: 提供树搜索能力
|
||||
* ------------------
|
||||
* props: FilePickerProps
|
||||
* ------------------
|
||||
*/
|
||||
export const CommonFilePicker = React.forwardRef(
|
||||
(props: CommonFilePickerProps, ref: React.ForwardedRef<PickerRef>) => {
|
||||
const {
|
||||
treeData,
|
||||
customTreeDataRenderer,
|
||||
onlySelectLeaf,
|
||||
multiple,
|
||||
virtualizeHeight,
|
||||
virtualizeItemSize,
|
||||
indentSize,
|
||||
expandIcon,
|
||||
onChange,
|
||||
transSelectedFilesMiddlewares,
|
||||
defaultValue,
|
||||
selectFilesMiddlewares = [],
|
||||
disableSelect = false,
|
||||
checkRelation = 'related',
|
||||
defaultExpandKeys = [],
|
||||
enableVirtualize = true,
|
||||
} = props;
|
||||
|
||||
const treeRef = useRef<Tree>(null);
|
||||
// 使用受控模式
|
||||
const [selectValue, setSelectValue] = useState<FileNode[]>(
|
||||
transDefaultValueToRenderNode(defaultValue),
|
||||
);
|
||||
const [expandedKeys, setExpandedKeys] =
|
||||
useState<string[]>(defaultExpandKeys);
|
||||
|
||||
const prevChangeNodes = useRef<FileNode[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const defaultFileNodes = transDefaultValueToFileNodes(
|
||||
treeData,
|
||||
defaultValue,
|
||||
);
|
||||
setSelectValue(transDefaultValueToRenderNode(defaultValue));
|
||||
prevChangeNodes.current = defaultFileNodes;
|
||||
}, [defaultValue]);
|
||||
|
||||
const renderTreeData = useDefaultLabelRenderer(
|
||||
!!multiple,
|
||||
!!onlySelectLeaf,
|
||||
{
|
||||
indentSize: indentSize ?? DEFAULT_FILENODE_LEVEL_INDENT,
|
||||
expandIcon,
|
||||
disableSelect,
|
||||
defaultSingleSelectKey: !multiple
|
||||
? getFirstKeyOfDefaultValue(defaultValue)
|
||||
: '',
|
||||
},
|
||||
);
|
||||
useImperativeHandle(ref, () => ({
|
||||
search: treeRef.current?.search,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={styles['common-file-picker-wrapper']}>
|
||||
<Tree
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{...(props as any)}
|
||||
data-testid={CommonE2e.CommonFilePicker}
|
||||
checkRelation={checkRelation}
|
||||
value={selectValue}
|
||||
treeData={treeData}
|
||||
ref={treeRef}
|
||||
renderFullLabel={
|
||||
customTreeDataRenderer ??
|
||||
(renderTreeData as (
|
||||
renderProps: RenderFullLabelProps,
|
||||
) => React.ReactNode)
|
||||
}
|
||||
multiple={multiple}
|
||||
virtualize={
|
||||
enableVirtualize
|
||||
? {
|
||||
height: virtualizeHeight ?? DEFAULT_VIRTUAL_CONTAINER_HEIGHT,
|
||||
itemSize: virtualizeItemSize ?? DEFAULT_VIRTUAL_ITEM_HEIGHT,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={(currentExpandedKeys, current) => {
|
||||
setExpandedKeys(currentExpandedKeys);
|
||||
}}
|
||||
onChangeWithObject
|
||||
onChange={changeNodes => {
|
||||
let transedChangeNodes: FileNode[];
|
||||
if (multiple) {
|
||||
if (
|
||||
!changeNodes ||
|
||||
!Array.isArray(changeNodes) ||
|
||||
!isFileNodeArray(changeNodes)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
transedChangeNodes = changeNodes;
|
||||
} else {
|
||||
if (!changeNodes) {
|
||||
return;
|
||||
}
|
||||
transedChangeNodes = [changeNodes as unknown as FileNode];
|
||||
}
|
||||
|
||||
// 计算 diff
|
||||
const [addNodes, removeNodes, retainNodes] = diffChangeNodes(
|
||||
prevChangeNodes.current,
|
||||
transedChangeNodes as FileNode[],
|
||||
);
|
||||
|
||||
// 这里的中间件更多用在定制化选中态的场景 比如想要反选所有子节点但是保持父亲节点选中
|
||||
transedChangeNodes = distinctFileNodes(
|
||||
selectFilesMiddlewares.reduce(
|
||||
(selectedElements, middleware) =>
|
||||
middleware(
|
||||
selectedElements,
|
||||
addNodes,
|
||||
removeNodes,
|
||||
retainNodes,
|
||||
),
|
||||
transedChangeNodes,
|
||||
),
|
||||
);
|
||||
prevChangeNodes.current = transedChangeNodes;
|
||||
|
||||
setSelectValue(
|
||||
transedChangeNodes.map(transedNode => ({
|
||||
key: transedNode.key,
|
||||
})),
|
||||
);
|
||||
// 虽然这里返回的是父节点带 children 但是因为都是后端一次接口的快照
|
||||
// 具体上报什么数据交给业务方
|
||||
// 这里的中间件主要用在定制化上报给上层组件的选中数据的场景,比如 checkRelation 'related' 模式下 上面返回的只有父亲节点的数据,但是父组件想要所有数据
|
||||
// 警告:这里如果使用 loadData 时拿不到没请求的子节点(换句话说拿到的最后一层的数据不一定是叶子结点, 同样的在这里选中之后交回给后端的其实也只是一个父节点 不能保证在一个快照里
|
||||
if (transSelectedFilesMiddlewares) {
|
||||
transedChangeNodes = transSelectedFilesMiddlewares.reduce(
|
||||
(selectedElements, middleware) => middleware(selectedElements),
|
||||
transedChangeNodes,
|
||||
);
|
||||
}
|
||||
onChange?.(transedChangeNodes);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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 const DEFAULT_FILENODE_LEVEL_INDENT = 24;
|
||||
|
||||
// 虚拟化配置相关数据
|
||||
export const DEFAULT_VIRTUAL_CONTAINER_HEIGHT = 540;
|
||||
export const DEFAULT_VIRTUAL_ITEM_HEIGHT = 28;
|
||||
17
frontend/packages/data/knowledge/common/components/src/file-picker/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,389 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import type { RenderFullLabelProps } from '@coze-arch/bot-semi/Tree';
|
||||
import { Checkbox, Radio, Spin, Typography } from '@coze-arch/bot-semi';
|
||||
|
||||
import { ReactComponent as Text } from '@/assets/text-file.svg';
|
||||
import { ReactComponent as Sheet } from '@/assets/sheet-file.svg';
|
||||
import { ReactComponent as Folder } from '@/assets/folder.svg';
|
||||
|
||||
import {
|
||||
TreeNodeType,
|
||||
type FileSelectCheckStatus,
|
||||
type FileNode,
|
||||
} from '../types';
|
||||
|
||||
const noRealExpandClassName = 'no-real-expand';
|
||||
|
||||
const ActionComponent = (props: {
|
||||
checkStatus: Partial<FileSelectCheckStatus>;
|
||||
isLeaf: boolean;
|
||||
renderExpandIcon: React.ReactElement;
|
||||
onlySelectLeaf: boolean;
|
||||
multiple: boolean;
|
||||
disableSelect: boolean;
|
||||
unCheckable: boolean;
|
||||
}) => {
|
||||
const {
|
||||
checkStatus,
|
||||
isLeaf,
|
||||
renderExpandIcon,
|
||||
onlySelectLeaf,
|
||||
multiple,
|
||||
disableSelect,
|
||||
unCheckable = false,
|
||||
} = props;
|
||||
const getSingalSelectAction = (className: string) => (
|
||||
<span
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
aria-checked={checkStatus.checked}
|
||||
className={`file-selector ${className}`}
|
||||
>
|
||||
<Radio checked={checkStatus.checked} disabled={disableSelect} />
|
||||
</span>
|
||||
);
|
||||
const getMultiSelectAction = (className: string) => (
|
||||
<span
|
||||
role="checkbox"
|
||||
tabIndex={0}
|
||||
aria-checked={checkStatus.checked}
|
||||
className={`file-selector ${className}`}
|
||||
>
|
||||
<Checkbox
|
||||
indeterminate={checkStatus.halfChecked}
|
||||
checked={checkStatus.checked}
|
||||
disabled={disableSelect}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
const expandPlaceHolder = <span className="expand-placeholder"></span>;
|
||||
const selectActionPlaceHolder = (
|
||||
<span className="action-placeholder file-selector"></span>
|
||||
);
|
||||
|
||||
// 当前整棵树是多选 并且 只能选中叶子结点
|
||||
if (multiple && onlySelectLeaf) {
|
||||
return isLeaf && !unCheckable ? (
|
||||
<>
|
||||
{expandPlaceHolder}
|
||||
{getMultiSelectAction(noRealExpandClassName)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{renderExpandIcon} {selectActionPlaceHolder}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 当前整棵树是多选 并且 能选中所有结点
|
||||
if (multiple && !onlySelectLeaf) {
|
||||
if (unCheckable) {
|
||||
return (
|
||||
<>
|
||||
{renderExpandIcon} {selectActionPlaceHolder}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isLeaf ? expandPlaceHolder : renderExpandIcon}
|
||||
{getMultiSelectAction(isLeaf ? noRealExpandClassName : '')}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 当前整棵树是单选 并且 只能选中叶子结点
|
||||
if (!multiple && onlySelectLeaf) {
|
||||
return isLeaf && !unCheckable ? (
|
||||
<>
|
||||
{expandPlaceHolder}
|
||||
{getSingalSelectAction(noRealExpandClassName)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{renderExpandIcon}
|
||||
{selectActionPlaceHolder}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 当前整棵树是单选 并且 能选中所有结点
|
||||
if (!multiple && !onlySelectLeaf) {
|
||||
if (unCheckable) {
|
||||
return (
|
||||
<>
|
||||
{renderExpandIcon}
|
||||
{selectActionPlaceHolder}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isLeaf ? expandPlaceHolder : renderExpandIcon}
|
||||
{getSingalSelectAction(isLeaf ? noRealExpandClassName : '')}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const LabelContent = (props: {
|
||||
iconUrl?: string;
|
||||
label: React.ReactNode;
|
||||
type?: TreeNodeType;
|
||||
isLoading?: boolean;
|
||||
loadingInfo?: string;
|
||||
}) => {
|
||||
const { iconUrl, label, type, isLoading, loadingInfo } = props;
|
||||
return (
|
||||
<>
|
||||
<span className="file-icon">
|
||||
{isLoading ? (
|
||||
<Spin spinning />
|
||||
) : iconUrl ? (
|
||||
<img src={iconUrl} />
|
||||
) : (
|
||||
{
|
||||
[TreeNodeType.FILE_TABLE]: <Sheet />,
|
||||
[TreeNodeType.FOLDER]: <Folder />,
|
||||
[TreeNodeType.FILE_TEXT]: <Text />,
|
||||
}[type ?? TreeNodeType.FILE_TEXT]
|
||||
)}
|
||||
</span>
|
||||
<span className="file-content">
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
type: 'tooltip',
|
||||
opts: {
|
||||
style: {
|
||||
maxWidth: '800px',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
className="file-name"
|
||||
>
|
||||
{label}
|
||||
</Typography.Text>
|
||||
{isLoading ? (
|
||||
<span className="file-loading-info">{loadingInfo}</span>
|
||||
) : null}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* -----------------------------
|
||||
* 获取默认的文件树 label renderer 这层不感知平台信息
|
||||
* -----------------------------
|
||||
* @param {boolean} multiple 是否多选
|
||||
* @param {boolean} onlySelectLeaf 是否只能选中叶子结点
|
||||
* @param {{indentSize: 树组件缩进尺寸, expandIcon: 树组件可展开节点 展开图标, disableSelect: 选择的 disable 状态}} renderOption 渲染相关的自定义选项
|
||||
* @returns label render 函数
|
||||
*/
|
||||
export function useDefaultLabelRenderer(
|
||||
multiple: boolean,
|
||||
onlySelectLeaf: boolean,
|
||||
renderOption: {
|
||||
indentSize: number;
|
||||
expandIcon?: React.ReactNode;
|
||||
disableSelect?: boolean;
|
||||
defaultSingleSelectKey?: string;
|
||||
},
|
||||
) {
|
||||
const [singleSelectedKey, setSingleSelectKey] = useState(
|
||||
renderOption.defaultSingleSelectKey ?? '',
|
||||
);
|
||||
|
||||
const getExpandIcon = (
|
||||
labelDefaultIcon: React.ReactElement,
|
||||
customIcon?: React.ReactNode,
|
||||
) => {
|
||||
if (!customIcon) {
|
||||
return labelDefaultIcon;
|
||||
}
|
||||
const {
|
||||
props: { onClick, className, role },
|
||||
} = labelDefaultIcon;
|
||||
return (
|
||||
<span
|
||||
role={role}
|
||||
className={`${className} semi-tree-option-expand-icon`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{customIcon}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const getFileContentClassName = (isChecked: boolean) =>
|
||||
`w-full file-node-row-content flex items-center ${
|
||||
isChecked ? 'file-node-selected' : 'fileNodeNormal'
|
||||
}`;
|
||||
|
||||
/**
|
||||
* 在整行点击的处理函数 不是点击 checkbox 或者 radio 或者 expandIcon 的处理函数
|
||||
* @param isLeaf: 是否是叶子结点
|
||||
* @param onExpand: 展开行 处理函数
|
||||
* @param onCheck: 选中状态的 toggle
|
||||
**/
|
||||
const getItemAction = (params: {
|
||||
isLeaf: boolean;
|
||||
onExpand: (e: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
onCheck: (e: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
unCheckable: boolean;
|
||||
}) => {
|
||||
const { onExpand, onCheck, isLeaf, unCheckable } = params;
|
||||
return function (e: React.MouseEvent<Element, MouseEvent>) {
|
||||
// 如果只能选中叶子结点 那么无论 多选 / 单选 父节点只能展开,叶子结点可以选中
|
||||
// 反之 父节点子节点 都是选中 无论多选单选,想要展开就点击 expand icon
|
||||
if (onlySelectLeaf) {
|
||||
if (!isLeaf) {
|
||||
onExpand(e);
|
||||
} else {
|
||||
if (!unCheckable) {
|
||||
onCheck(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!unCheckable) {
|
||||
onCheck(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const labelRenderer = (
|
||||
renderProps: RenderFullLabelProps & {
|
||||
data: FileNode;
|
||||
},
|
||||
) => {
|
||||
const {
|
||||
data,
|
||||
className,
|
||||
level,
|
||||
onCheck,
|
||||
onExpand,
|
||||
onClick,
|
||||
checkStatus,
|
||||
style,
|
||||
expandIcon,
|
||||
} = renderProps;
|
||||
const { indentSize, disableSelect } = renderOption;
|
||||
const {
|
||||
label,
|
||||
key,
|
||||
type,
|
||||
isLoading,
|
||||
loadingInfo,
|
||||
render,
|
||||
readonly,
|
||||
unCheckable = false,
|
||||
} = data;
|
||||
// 单选选中选项的 key
|
||||
const singleSelectCheckStatus = singleSelectedKey === key;
|
||||
const rowCheckStatus = multiple
|
||||
? checkStatus
|
||||
: {
|
||||
checked: singleSelectCheckStatus,
|
||||
};
|
||||
// 只要 data isLeaf 不是空 永远先看 data.isLeaf
|
||||
const isLeaf = data?.isLeaf ?? !(data.children && data.children.length);
|
||||
const checkItem = multiple
|
||||
? (e: React.MouseEvent<Element, MouseEvent>) => {
|
||||
if (disableSelect) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
onCheck(e);
|
||||
}
|
||||
: (e: React.MouseEvent<Element, MouseEvent>) => {
|
||||
if (disableSelect) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
onClick(e);
|
||||
setSingleSelectKey(key);
|
||||
};
|
||||
const indent = indentSize * level;
|
||||
const rowStyle = {
|
||||
...style,
|
||||
paddingLeft: indent,
|
||||
};
|
||||
const renderExpandIcon = getExpandIcon(
|
||||
expandIcon as React.ReactElement,
|
||||
renderOption.expandIcon,
|
||||
);
|
||||
return (
|
||||
<li
|
||||
style={{
|
||||
...rowStyle,
|
||||
}}
|
||||
className={`${className} ${
|
||||
checkStatus.checked ? 'semi-tree-option-selected' : ''
|
||||
} ${disableSelect ? 'semi-tree-option-disable' : ''} ${
|
||||
readonly ? 'semi-tree-option-readonly' : ''
|
||||
}`}
|
||||
role="treeitem"
|
||||
onClick={
|
||||
readonly
|
||||
? undefined
|
||||
: getItemAction({
|
||||
isLeaf,
|
||||
onExpand,
|
||||
onCheck: checkItem,
|
||||
unCheckable,
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className={getFileContentClassName(rowCheckStatus.checked)}>
|
||||
{render ? (
|
||||
render()
|
||||
) : (
|
||||
<>
|
||||
<ActionComponent
|
||||
checkStatus={rowCheckStatus}
|
||||
isLeaf={isLeaf}
|
||||
renderExpandIcon={renderExpandIcon}
|
||||
onlySelectLeaf={onlySelectLeaf}
|
||||
multiple={multiple}
|
||||
disableSelect={!!disableSelect}
|
||||
unCheckable={unCheckable}
|
||||
/>
|
||||
<LabelContent
|
||||
iconUrl={data.icon}
|
||||
label={label}
|
||||
type={type}
|
||||
isLoading={isLoading}
|
||||
loadingInfo={loadingInfo}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
return labelRenderer;
|
||||
}
|
||||
@@ -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 {
|
||||
CommonFilePicker,
|
||||
type CommonFilePickerProps,
|
||||
} from './components/common-file-picker';
|
||||
export { appendAllAddFiles, getLeafFiles } from './utils';
|
||||
export {
|
||||
type PickerRef,
|
||||
type FileNode,
|
||||
TreeNodeType,
|
||||
type FileId,
|
||||
} from './types';
|
||||
@@ -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 { type ReactNode } from 'react';
|
||||
|
||||
import { isObject } from 'lodash-es';
|
||||
import type { TreeNodeData } from '@coze-arch/bot-semi/Tree';
|
||||
|
||||
export enum TreeNodeType {
|
||||
FILE_TEXT = 2,
|
||||
FILE_TABLE = 3,
|
||||
FOLDER = 1,
|
||||
}
|
||||
|
||||
export interface PickerRef {
|
||||
search?: (searchText: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件选择树节点
|
||||
*/
|
||||
export interface FileNode extends TreeNodeData {
|
||||
/** 独一无二的 key 标识 可以用 文件 id */
|
||||
key: string;
|
||||
value?: string;
|
||||
label?: React.ReactNode;
|
||||
type?: TreeNodeType;
|
||||
// icon 的 URL
|
||||
icon?: string;
|
||||
children?: FileNode[];
|
||||
/** 标识当前节点是不是叶子结点 loadData 时必备 */
|
||||
isLeaf?: boolean;
|
||||
/** 该节点是否可以选中 */
|
||||
selectable?: boolean;
|
||||
/** 节点的 loading 状态,开启后 loading 默认替换 icon,展示 loadingInfo,
|
||||
* 注意这个和 semi 本身带的 loading 不一样,semi 的 loading 指的是 展开的 loading 状态 */
|
||||
isLoading?: boolean;
|
||||
/** 节点 loading 的提示,默认是 `获取中` */
|
||||
loadingInfo?: string;
|
||||
/** 具体的文档类型 比如 doc docx txt 等 */
|
||||
file_type?: string;
|
||||
/** 三方文档链接 */
|
||||
file_url?: string;
|
||||
/** 【飞书场景】wiki 空间id,*/
|
||||
space_id?: string;
|
||||
/** 【飞书场景】wiki 叶子id,*/
|
||||
obj_token?: string;
|
||||
/** 自定义渲染 Item */
|
||||
render?: () => ReactNode;
|
||||
/** 只读,不可交互 */
|
||||
readonly?: boolean;
|
||||
/** 节点是否不可选择,默认为 false */
|
||||
unCheckable?: boolean;
|
||||
}
|
||||
|
||||
export type FileId = string;
|
||||
|
||||
/**
|
||||
* 文件选择树 节点选择状态
|
||||
*/
|
||||
export interface FileSelectCheckStatus {
|
||||
checked: boolean;
|
||||
halfChecked: boolean;
|
||||
}
|
||||
|
||||
// 三部分 当前选中的 新增选中的 较上次不选中的 较上次不变的
|
||||
export type TransSelectedFilesMiddleware = (
|
||||
fileNodes: FileNode[],
|
||||
) => FileNode[];
|
||||
|
||||
export type CalcCurrentSelectFilesMiddleware = (
|
||||
fileNodes: FileNode[],
|
||||
addNodes?: FileNode[],
|
||||
removeNodes?: FileNode[],
|
||||
retainNodes?: FileNode[],
|
||||
) => FileNode[];
|
||||
|
||||
/** 类型断言 节点是不是 fileNode */
|
||||
export function isFileNode(fileNode: unknown): fileNode is FileNode {
|
||||
return !!fileNode && isObject(fileNode) && !!(fileNode as FileNode).key;
|
||||
}
|
||||
|
||||
/** 类型断言 数组是不是 fileNode 数组 */
|
||||
export function isFileNodeArray(fileNodes: unknown[]): fileNodes is FileNode[] {
|
||||
return fileNodes.every(fileNode => isFileNode(fileNode));
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { FileNode, TransSelectedFilesMiddleware } from './types';
|
||||
|
||||
export const getLeafFiles: TransSelectedFilesMiddleware = (
|
||||
files?: FileNode[],
|
||||
) => {
|
||||
if (!files || files.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const leafFiles: FileNode[] = [];
|
||||
const helpQueue = Array.from(files);
|
||||
while (helpQueue.length > 0) {
|
||||
const curFile = helpQueue.shift();
|
||||
if (
|
||||
curFile?.isLeaf ||
|
||||
!curFile?.children ||
|
||||
curFile.children.length === 0
|
||||
) {
|
||||
curFile && leafFiles.push(curFile);
|
||||
} else {
|
||||
helpQueue.push(...curFile.children);
|
||||
}
|
||||
}
|
||||
return leafFiles;
|
||||
};
|
||||
|
||||
function levelMapTreeNodes<
|
||||
T extends {
|
||||
children?: T[];
|
||||
isLeaf?: boolean;
|
||||
},
|
||||
>(treeNodes: T[]): T[] {
|
||||
const allNode: T[] = [];
|
||||
const helpRemoveQueue = Array.from(treeNodes);
|
||||
while (helpRemoveQueue.length > 0) {
|
||||
const curFile = helpRemoveQueue.shift();
|
||||
curFile && allNode.push(curFile);
|
||||
if (!curFile?.isLeaf && curFile?.children && curFile.children.length > 0) {
|
||||
helpRemoveQueue.push(...curFile.children);
|
||||
}
|
||||
}
|
||||
return allNode;
|
||||
}
|
||||
|
||||
export function levelMapTreeNodesToMap<
|
||||
T extends {
|
||||
key: string;
|
||||
children?: T[];
|
||||
isLeaf?: boolean;
|
||||
},
|
||||
>(treeNodes: T[]): Map<string, T> {
|
||||
const allNodeMap: Map<string, T> = new Map();
|
||||
const helpRemoveQueue = Array.from(treeNodes);
|
||||
while (helpRemoveQueue.length > 0) {
|
||||
const curFile = helpRemoveQueue.shift();
|
||||
curFile && allNodeMap.set(curFile.key, curFile);
|
||||
if (!curFile?.isLeaf && curFile?.children && curFile.children.length > 0) {
|
||||
helpRemoveQueue.push(...curFile.children);
|
||||
}
|
||||
}
|
||||
return allNodeMap;
|
||||
}
|
||||
|
||||
export const appendAllAddFiles: TransSelectedFilesMiddleware = (
|
||||
files?: FileNode[],
|
||||
addNodes?: FileNode[],
|
||||
removeNodes?: FileNode[],
|
||||
retainNodes: FileNode[] = [],
|
||||
// eslint-disable-next-line max-params
|
||||
) => {
|
||||
if (!files || files.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (!addNodes) {
|
||||
return files;
|
||||
}
|
||||
|
||||
const allRemoveFiles: FileNode[] = levelMapTreeNodes<FileNode>(
|
||||
removeNodes ?? [],
|
||||
);
|
||||
const removeKeys = new Set(allRemoveFiles.map(file => file.key));
|
||||
const allAddFiles: FileNode[] = levelMapTreeNodes<FileNode>(addNodes ?? []);
|
||||
|
||||
return [...allAddFiles, ...retainNodes].filter(
|
||||
file => !removeKeys.has(file.key),
|
||||
);
|
||||
};
|
||||
|
||||
export const distinctFileNodes: TransSelectedFilesMiddleware = (
|
||||
files?: FileNode[],
|
||||
) => {
|
||||
if (!files) {
|
||||
return [];
|
||||
}
|
||||
const distinctFiles: FileNode[] = [];
|
||||
const distinctFileKey: Set<string> = new Set();
|
||||
for (const file of files) {
|
||||
if (distinctFileKey.has(file.key)) {
|
||||
continue;
|
||||
}
|
||||
distinctFileKey.add(file.key);
|
||||
distinctFiles.push(file);
|
||||
}
|
||||
return distinctFiles;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { PreviewMd } from './doc-preview/preview-md';
|
||||
export { PreviewTxt } from './doc-preview/preview-txt';
|
||||
export { usePreviewPdf } from './doc-preview/use-preview-pdf';
|
||||
export { default as SegmentMenu } from './segment-menu';
|
||||
export { DocumentEditor } from './text-knowledge-editor/features/editor';
|
||||
export { DocumentPreview } from './text-knowledge-editor/features/preview';
|
||||
export { LevelTextKnowledgeEditor } from './text-knowledge-editor/scenes/level';
|
||||
export { BaseTextKnowledgeEditor } from './text-knowledge-editor/scenes/base';
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
export interface IDocumentItemProps {
|
||||
id: string;
|
||||
title: string;
|
||||
selected?: boolean;
|
||||
onClick?: (id: string) => void;
|
||||
label?: ReactNode;
|
||||
tag?: ReactNode;
|
||||
}
|
||||
|
||||
const DocumentItem: React.FC<IDocumentItemProps> = props => {
|
||||
const { id, onClick, title, selected, tag, label } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
'w-full h-8 px-2 py-[6px] rounded-[8px] hover:coz-mg-primary cursor-pointer',
|
||||
'flex items-center',
|
||||
selected && 'coz-mg-primary',
|
||||
)}
|
||||
onClick={() => onClick?.(id)}
|
||||
>
|
||||
{label ? (
|
||||
<div className="w-full">{label}</div>
|
||||
) : (
|
||||
<>
|
||||
<Typography.Text
|
||||
ellipsis={{ showTooltip: true }}
|
||||
className="w-full coz-fg-primary text-[14px] leading-[20px] grow truncate"
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
<div className="flex items-center shrink-0">{tag}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentItem;
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Search, Tooltip } from '@coze-arch/coze-design';
|
||||
import { type ILevelSegment } from '@coze-data/knowledge-stores';
|
||||
|
||||
import MergeOperation from '../assets/merge-operation.png';
|
||||
import MergeOperationEn from '../assets/merge-operation-en.png';
|
||||
import LevelOperation from '../assets/level-operation.png';
|
||||
import DeleteOperation from '../assets/delete-operation.png';
|
||||
import DeleteOperationEn from '../assets/delete-operation-en.png';
|
||||
import { SegmentTree } from './segment-tree';
|
||||
import DocumentItem from './document-item';
|
||||
interface IMenuItem {
|
||||
id: string;
|
||||
title: string;
|
||||
label?: ReactNode;
|
||||
tag?: ReactNode;
|
||||
tosUrl?: string;
|
||||
}
|
||||
|
||||
interface ISegmentMenuProps {
|
||||
list: IMenuItem[];
|
||||
onClick?: (id: string) => void;
|
||||
selectedID?: string;
|
||||
isSearchable?: boolean;
|
||||
treeVisible?: boolean;
|
||||
treeDisabled?: boolean;
|
||||
levelSegments?: ILevelSegment[];
|
||||
setLevelSegments?: (segments: ILevelSegment[]) => void;
|
||||
setSelectionIDs?: (ids: string[]) => void;
|
||||
}
|
||||
|
||||
const SegmentMenu: React.FC<ISegmentMenuProps> = props => {
|
||||
const {
|
||||
isSearchable,
|
||||
list,
|
||||
onClick,
|
||||
selectedID,
|
||||
levelSegments,
|
||||
setLevelSegments,
|
||||
setSelectionIDs,
|
||||
treeDisabled,
|
||||
treeVisible,
|
||||
} = props;
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col grow w-full h-full">
|
||||
{isSearchable ? (
|
||||
<Search
|
||||
value={searchValue}
|
||||
placeholder={I18n.t('datasets_placeholder_search')}
|
||||
onChange={setSearchValue}
|
||||
/>
|
||||
) : 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">
|
||||
{/**文档列表 */}
|
||||
{I18n.t('knowledge_level_012')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col grow w-full">
|
||||
<div className="flex flex-col gap-1 h-[150px] grow !overflow-auto shrink-0">
|
||||
{list
|
||||
.filter(item => item.title.includes(searchValue))
|
||||
.map(document => {
|
||||
if (document.id !== '') {
|
||||
return (
|
||||
<DocumentItem
|
||||
key={document.id}
|
||||
id={document.id}
|
||||
selected={document.id === selectedID}
|
||||
onClick={onClick}
|
||||
title={document.title}
|
||||
tag={document.tag}
|
||||
label={document.label}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
{levelSegments?.length && treeVisible ? (
|
||||
<>
|
||||
<div className="h-4 flex justify-center items-center px-[8px] mb-[8px]">
|
||||
<div
|
||||
className={cls(
|
||||
'border border-solid border-[0.5px] transition w-full',
|
||||
'coz-stroke-primary',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
{/**分段层级 */}
|
||||
{I18n.t('knowledge_level_adjust')}
|
||||
</div>
|
||||
{treeDisabled ? null : (
|
||||
<Tooltip
|
||||
style={{
|
||||
maxWidth: 602,
|
||||
}}
|
||||
position="left"
|
||||
content={
|
||||
<>
|
||||
<div className="coz-fg-plus text-[14px] font-[500] leading-[20px] mb-3">
|
||||
{I18n.t('knowledge_hierarchies_categories')}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-1 justify-between w-[182px]">
|
||||
<span className="coz-fg-primary text-[12px] leading-[16px] font-[400]">
|
||||
{I18n.t('level_999')}
|
||||
</span>
|
||||
<img src={LevelOperation} className="w-[182px]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 justify-between w-[182px]">
|
||||
<span className="coz-fg-primary text-[12px] leading-[16px] font-[400]">
|
||||
{I18n.t('level_998')}
|
||||
</span>
|
||||
<img
|
||||
src={
|
||||
IS_OVERSEA ? MergeOperationEn : MergeOperation
|
||||
}
|
||||
className="w-[182px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 justify-between w-[182px]">
|
||||
<span className="coz-fg-primary text-[12px] leading-[16px] font-[400]">
|
||||
{I18n.t('level_997')}
|
||||
</span>
|
||||
<img
|
||||
src={
|
||||
IS_OVERSEA ? DeleteOperationEn : DeleteOperation
|
||||
}
|
||||
className="w-[182px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<IconCozInfoCircle className="coz-fg-secondary" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-[360px]">
|
||||
<SegmentTree
|
||||
segments={levelSegments}
|
||||
setLevelSegments={setLevelSegments}
|
||||
setSelectionIDs={setSelectionIDs}
|
||||
disabled={treeDisabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SegmentMenu;
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* 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 { Tree, type NodeRendererProps, type CursorProps } from 'react-arborist';
|
||||
import { useState, type CSSProperties } from 'react';
|
||||
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import cls from 'classnames';
|
||||
import { type ILevelSegment } from '@coze-data/knowledge-stores';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozArrowRight } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import {
|
||||
findDescendantIDs,
|
||||
getTreeNodes,
|
||||
handleDeleteNode,
|
||||
handleMergeNodes,
|
||||
handleTreeNodeMove,
|
||||
} from './utils/level-tree-op';
|
||||
import { useSegmentContextMenu } from './use-context-menu';
|
||||
import { type LevelDocumentTree } from './types';
|
||||
|
||||
interface ISegmentTreeProps {
|
||||
segments: ILevelSegment[];
|
||||
setLevelSegments?: (segments: ILevelSegment[]) => void;
|
||||
setSelectionIDs?: (ids: string[]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const SegmentTree: React.FC<ISegmentTreeProps> = ({
|
||||
segments,
|
||||
setLevelSegments,
|
||||
setSelectionIDs,
|
||||
disabled,
|
||||
}) => {
|
||||
/**
|
||||
* 选中功能
|
||||
*/
|
||||
const [selected, setSelected] = useState(new Set<string>());
|
||||
// 分片 id
|
||||
const [selectedThroughParent, setSelectedThroughParent] = useState(
|
||||
new Set<string>(),
|
||||
);
|
||||
const { ref, width, height } = useResizeObserver<HTMLDivElement>();
|
||||
|
||||
const onSelect = (node: LevelDocumentTree) => {
|
||||
setSelected(new Set([node.id]));
|
||||
setSelectedThroughParent(findDescendantIDs(node));
|
||||
setSelectionIDs?.([node.id, ...findDescendantIDs(node)]);
|
||||
};
|
||||
|
||||
/**
|
||||
* render
|
||||
*/
|
||||
const Node = ({
|
||||
node,
|
||||
style,
|
||||
dragHandle,
|
||||
}: NodeRendererProps<LevelDocumentTree>) => {
|
||||
const { isOpen, data } = node;
|
||||
const isLeaf = !data.children?.length;
|
||||
const expandIcon = (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="secondary"
|
||||
icon={
|
||||
<IconCozArrowRight
|
||||
className={cls(
|
||||
isOpen && 'rotate-90',
|
||||
'transition duration-150 ease-in-out',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
node.toggle();
|
||||
}}
|
||||
className={cls('bg-transparent ml-[4px] shrink-0')}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
'flex items-center gap-[4px]',
|
||||
'h-[32px] py-[4px] pr-[8px] mb-[2px]',
|
||||
'hover:coz-mg-primary cursor-pointer',
|
||||
'transition duration-150 ease-in-out',
|
||||
'rounded-[8px]',
|
||||
(selected.has(data.id) || selectedThroughParent.has(data.id)) &&
|
||||
'coz-mg-primary',
|
||||
)}
|
||||
onClick={() => {
|
||||
onSelect(data);
|
||||
}}
|
||||
onContextMenu={e => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
onContextMenu(e, node);
|
||||
}}
|
||||
style={style}
|
||||
ref={dragHandle}
|
||||
>
|
||||
{isLeaf ? <span className="w-6 ml-[4px] shrink-0" /> : expandIcon}
|
||||
<span
|
||||
className={cls('text-[14px] leading-[20px] coz-fg-primary truncate')}
|
||||
>
|
||||
{data.type !== 'image'
|
||||
? data.text.slice(0, 50)
|
||||
: I18n.t('knowledge_level_110')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* context menu
|
||||
*/
|
||||
const { popoverNode, onContextMenu } = useSegmentContextMenu({
|
||||
onMerge: node => {
|
||||
const { segments: newSegments, errMsg } = handleMergeNodes(
|
||||
node.id,
|
||||
Array.from(findDescendantIDs(node)),
|
||||
segments,
|
||||
);
|
||||
if (errMsg) {
|
||||
Toast.error(errMsg);
|
||||
}
|
||||
if (newSegments?.length) {
|
||||
setLevelSegments?.(newSegments);
|
||||
}
|
||||
},
|
||||
onDelete: node => {
|
||||
const newSegments = handleDeleteNode(
|
||||
[node.id, ...findDescendantIDs(node)],
|
||||
segments,
|
||||
);
|
||||
setLevelSegments?.(newSegments);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref} className="w-full h-full relative translate-z-0">
|
||||
<Tree
|
||||
data={getTreeNodes(segments)}
|
||||
disableDrag={disabled}
|
||||
disableDrop={disabled}
|
||||
onMove={({ dragIds, parentId, index }) => {
|
||||
const { segments: newSegments, errMsg } = handleTreeNodeMove(
|
||||
{ dragIDs: dragIds, parentID: parentId, dropIndex: index },
|
||||
segments,
|
||||
);
|
||||
if (errMsg) {
|
||||
Toast.error(errMsg);
|
||||
}
|
||||
if (newSegments?.length) {
|
||||
setLevelSegments?.(newSegments);
|
||||
}
|
||||
}}
|
||||
rowHeight={34}
|
||||
paddingTop={4}
|
||||
paddingBottom={4}
|
||||
width={width}
|
||||
height={height}
|
||||
renderCursor={Cursor}
|
||||
>
|
||||
{Node}
|
||||
</Tree>
|
||||
{popoverNode}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Cursor = ({ top, left, indent }: CursorProps) => {
|
||||
const placeholderStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
zIndex: 1,
|
||||
};
|
||||
const style: CSSProperties = {
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
top: `${top - 2}px`,
|
||||
left: `${left}px`,
|
||||
right: `${indent}px`,
|
||||
};
|
||||
return (
|
||||
<div style={{ ...placeholderStyle, ...style }}>
|
||||
<div className={cls('flex-1 h-[2px] coz-mg-hglt-plus')}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 ILevelSegment } from '@coze-data/knowledge-stores';
|
||||
|
||||
export type LevelDocumentTree = Omit<
|
||||
ILevelSegment,
|
||||
'children' | 'id' | 'parent'
|
||||
> & {
|
||||
id: string;
|
||||
parent: string;
|
||||
children: LevelDocumentTree[];
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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 NodeApi } from 'react-arborist';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useKnowledgeParams } from '@coze-data/knowledge-stores';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Menu, MenuSubMenu } from '@coze-arch/coze-design';
|
||||
|
||||
import { type LevelDocumentTree } from './types';
|
||||
|
||||
interface IUseSegmentContextMenuProps {
|
||||
onDelete: (node: LevelDocumentTree) => void;
|
||||
onMerge: (node: LevelDocumentTree) => void;
|
||||
}
|
||||
|
||||
export function useSegmentContextMenu({
|
||||
onDelete,
|
||||
onMerge,
|
||||
}: IUseSegmentContextMenuProps): {
|
||||
popoverNode: React.ReactNode;
|
||||
onContainerScroll: () => void;
|
||||
onContextMenu: (
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
treeNode: NodeApi<LevelDocumentTree>,
|
||||
) => void;
|
||||
} {
|
||||
const [treeNode, setTreeNode] = useState<NodeApi<LevelDocumentTree> | null>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
const params = useKnowledgeParams();
|
||||
|
||||
return {
|
||||
popoverNode: (
|
||||
<Menu
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
onClickOutSide={() => {
|
||||
setVisible(false);
|
||||
setTreeNode(null);
|
||||
}}
|
||||
trigger="custom"
|
||||
position="bottomLeft"
|
||||
render={
|
||||
<MenuSubMenu mode="menu">
|
||||
{treeNode && !treeNode.children?.length ? (
|
||||
<>
|
||||
<Menu.Item
|
||||
isMenu
|
||||
onClick={() => {
|
||||
onDelete(treeNode.data);
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
{I18n.t('knowledge_level_028')}
|
||||
</Menu.Item>
|
||||
</>
|
||||
) : null}
|
||||
{treeNode && treeNode.children?.length ? (
|
||||
<>
|
||||
<Menu.Item
|
||||
isMenu
|
||||
onClick={() => {
|
||||
onMerge(treeNode.data);
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
{I18n.t('knowledge_level_029')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
isMenu
|
||||
onClick={() => {
|
||||
onDelete(treeNode.data);
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
{I18n.t('knowledge_level_028')}
|
||||
</Menu.Item>
|
||||
</>
|
||||
) : null}
|
||||
</MenuSubMenu>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 0,
|
||||
width: 0,
|
||||
position: 'fixed',
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
),
|
||||
onContainerScroll: () => {
|
||||
if (visible) {
|
||||
setVisible(false);
|
||||
}
|
||||
},
|
||||
onContextMenu: (e, node: NodeApi<LevelDocumentTree>) => {
|
||||
e.preventDefault();
|
||||
setTreeNode(node);
|
||||
/** 在 project ide 里面,ide 容器设置了 contain: strict, 会导致 fixed position
|
||||
* 的偏移基础不对,所以这里需要减去 ide 容器的 left 和 top 值
|
||||
*/
|
||||
let clickX = e.pageX;
|
||||
let clickY = e.pageY;
|
||||
const ideDom = document.getElementById(
|
||||
`coze-project:///knowledge/${params.datasetID}`,
|
||||
);
|
||||
|
||||
if (ideDom) {
|
||||
const { left, top } = ideDom.getBoundingClientRect();
|
||||
clickX = clickX - left;
|
||||
clickY = clickY - top;
|
||||
}
|
||||
|
||||
setPosition({ left: clickX, top: clickY });
|
||||
setVisible(true);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { type ILevelSegment } from '@coze-data/knowledge-stores';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { type LevelDocumentTree } from '../types';
|
||||
|
||||
export const getTreeNodes = (
|
||||
segments: ILevelSegment[],
|
||||
): LevelDocumentTree[] => {
|
||||
const root = segments.find(f => f.parent === -1 && f.type === 'title');
|
||||
|
||||
if (!root) {
|
||||
return segments.map(item => ({
|
||||
...item,
|
||||
id: item.id.toString(),
|
||||
parent: item.parent?.toString(),
|
||||
children: [],
|
||||
}));
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
...root,
|
||||
id: root.id?.toString(),
|
||||
parent: root.parent?.toString(),
|
||||
children: getChildren(root, segments),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/** Segments to TreeNodes */
|
||||
const getChildren = (
|
||||
target: ILevelSegment,
|
||||
list: ILevelSegment[],
|
||||
): LevelDocumentTree[] =>
|
||||
(target.children ?? []).reduce<LevelDocumentTree[]>((acc, cur) => {
|
||||
const found = list.find(f => f.id === cur);
|
||||
if (found) {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
...found,
|
||||
id: found.id?.toString(),
|
||||
parent: found.parent?.toString(),
|
||||
children: getChildren(found, list),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [...acc];
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**TreeNodes related */
|
||||
export const findDescendantIDs = (node: LevelDocumentTree) => {
|
||||
const ids = new Set<string>();
|
||||
const findChild = (item: LevelDocumentTree) => {
|
||||
if (!item || !item.id) {
|
||||
return;
|
||||
}
|
||||
const { children } = item;
|
||||
if (children && children.length) {
|
||||
children.forEach(child => {
|
||||
if (child && child.id) {
|
||||
ids.add(child.id);
|
||||
findChild(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
findChild(node);
|
||||
return ids;
|
||||
};
|
||||
|
||||
export const findTreeNodeByID = (
|
||||
nodes: LevelDocumentTree[],
|
||||
id: string,
|
||||
): LevelDocumentTree | null => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
const found = findTreeNodeByID(node.children, id);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const handleTreeNodeMove = (
|
||||
positions: { dragIDs: string[]; parentID: string | null; dropIndex: number },
|
||||
segments: ILevelSegment[],
|
||||
): {
|
||||
segments: ILevelSegment[] | null;
|
||||
errMsg: string | null;
|
||||
} => {
|
||||
if (positions.parentID === null) {
|
||||
return {
|
||||
segments: null,
|
||||
errMsg: I18n.t('knowledge_hierarchies_categories_01'),
|
||||
};
|
||||
}
|
||||
const resSegments = cloneDeep(segments);
|
||||
for (const id of positions.dragIDs) {
|
||||
const dragSegmentIdx = resSegments.findIndex(
|
||||
segment => segment.id.toString() === id,
|
||||
);
|
||||
|
||||
if (dragSegmentIdx === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dragSegment = resSegments[dragSegmentIdx];
|
||||
const parentSegment = resSegments.find(
|
||||
segment => segment.id.toString() === positions.parentID,
|
||||
);
|
||||
|
||||
if (!parentSegment) {
|
||||
return {
|
||||
segments: null,
|
||||
errMsg: I18n.t('knowledge_hierarchies_categories_02'),
|
||||
};
|
||||
}
|
||||
|
||||
// 如果是同一个 parent,且拖动的位置在当前位置之前,dropIndex 减 1
|
||||
const originalIndex = parentSegment.children.indexOf(dragSegment.id);
|
||||
const dropIndex =
|
||||
originalIndex < positions.dropIndex &&
|
||||
dragSegment.parent === parentSegment.id
|
||||
? positions.dropIndex - 1
|
||||
: positions.dropIndex;
|
||||
|
||||
if (dragSegment.parent !== parentSegment.id) {
|
||||
// Remove from old parent's children
|
||||
const oldParent = resSegments.find(s => s.id === dragSegment.parent);
|
||||
oldParent?.children.splice(oldParent.children.indexOf(dragSegment.id), 1);
|
||||
dragSegment.parent = parentSegment.id;
|
||||
}
|
||||
|
||||
// Reorder in parent's children
|
||||
parentSegment.children = parentSegment.children.filter(
|
||||
child => child !== dragSegment.id,
|
||||
);
|
||||
parentSegment.children.splice(dropIndex, 0, dragSegment.id);
|
||||
}
|
||||
|
||||
return {
|
||||
segments: resSegments,
|
||||
errMsg: null,
|
||||
};
|
||||
};
|
||||
|
||||
export const handleDeleteNode = (ids: string[], segments: ILevelSegment[]) => {
|
||||
const resSegments = cloneDeep(segments);
|
||||
for (const id of ids) {
|
||||
const index = resSegments.findIndex(item => item.id.toString() === id);
|
||||
const parentSegment = resSegments.find(
|
||||
item => item.id === resSegments[index].parent,
|
||||
);
|
||||
if (parentSegment) {
|
||||
parentSegment.children = parentSegment.children.filter(
|
||||
item => item !== resSegments[index].id,
|
||||
);
|
||||
}
|
||||
resSegments.splice(index, 1);
|
||||
}
|
||||
return resSegments;
|
||||
};
|
||||
|
||||
export const handleMergeNodes = (
|
||||
id: string,
|
||||
descendants: string[],
|
||||
segments: ILevelSegment[],
|
||||
): {
|
||||
segments: ILevelSegment[] | null;
|
||||
errMsg: string | null;
|
||||
} => {
|
||||
const resSegments = cloneDeep(segments);
|
||||
const mergedSegment = resSegments.find(item => item.id.toString() === id);
|
||||
|
||||
if (!mergedSegment) {
|
||||
return {
|
||||
segments: null,
|
||||
errMsg: I18n.t('knowledge_hierarchies_categories_03'),
|
||||
};
|
||||
}
|
||||
|
||||
if (mergedSegment.parent === -1 && mergedSegment.type === 'title') {
|
||||
return {
|
||||
segments: null,
|
||||
errMsg: I18n.t('knowledge_hierarchies_categories_03'),
|
||||
};
|
||||
}
|
||||
|
||||
mergedSegment.children = [];
|
||||
mergedSegment.type = 'section-text';
|
||||
|
||||
for (const descendant of descendants) {
|
||||
const segmentToMerge = resSegments.find(
|
||||
item => item.id.toString() === descendant,
|
||||
);
|
||||
if (!segmentToMerge) {
|
||||
return {
|
||||
segments: null,
|
||||
errMsg: I18n.t('knowledge_hierarchies_categories_04'),
|
||||
};
|
||||
}
|
||||
|
||||
// 从原父节点的 children 中移除该节点
|
||||
const parentSegment = resSegments.find(
|
||||
item => item.id === segmentToMerge.parent,
|
||||
);
|
||||
if (parentSegment) {
|
||||
parentSegment.children = parentSegment.children.filter(
|
||||
childId => childId !== segmentToMerge.id,
|
||||
);
|
||||
}
|
||||
|
||||
if (!['table', 'image', 'title'].includes(segmentToMerge?.type ?? '')) {
|
||||
// 合并文本内容并删除节点
|
||||
mergedSegment.text += segmentToMerge.text;
|
||||
const index = resSegments.findIndex(
|
||||
item => item.id === segmentToMerge.id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
resSegments.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
// 非section-text类型的节点,将其移动到合并后节点的children中
|
||||
segmentToMerge.parent = mergedSegment.id;
|
||||
mergedSegment.children.push(segmentToMerge.id);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
segments: resSegments,
|
||||
errMsg: null,
|
||||
};
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||