chore: import upstream snapshot with attribution
@@ -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: [],
|
||||
});
|
||||
@@ -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/`,
|
||||
// Boost performance
|
||||
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,329 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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'> {
|
||||
/** Tree data used for rendering */
|
||||
treeData: FileNode[];
|
||||
/** Provide a customized render tree node */
|
||||
customTreeDataRenderer?: (
|
||||
renderProps: RenderFullLabelProps,
|
||||
) => React.ReactNode;
|
||||
|
||||
/** Whether only leaf nodes can be selected, invalid if customRenderTreeData is passed in */
|
||||
onlySelectLeaf?: boolean;
|
||||
/** Whether to multi-select, if the custom customTreeDataRenderer must be passed in, the option will affect customTreeDataRenderer imported parameter */
|
||||
multiple?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to enable virtualization
|
||||
*/
|
||||
enableVirtualize?: boolean;
|
||||
/** virtualization options */
|
||||
/** virtualized container height */
|
||||
virtualizeHeight?: number;
|
||||
/** Height of each item */
|
||||
virtualizeItemSize?: number;
|
||||
|
||||
/** The content already selected by default can be used as initValue or as a substitute for value */
|
||||
defaultValue?: FileNode[] | FileId[];
|
||||
|
||||
/** style rendering feature */
|
||||
normalLabelStyle?: React.CSSProperties;
|
||||
selectedLabelStyle?: React.CSSProperties;
|
||||
halfSelectedLabelStyle?: React.CSSProperties;
|
||||
|
||||
/** Indent size, default size 25px If backgroundMode is position, it will react to left If it is padding, it will react to padding-left. */
|
||||
indentSize?: number;
|
||||
|
||||
/** Tree component expansion icon */
|
||||
expandIcon?: React.ReactNode;
|
||||
|
||||
/** onChange business layer can be passed through */
|
||||
onChange?: (args?: FileNode[]) => void;
|
||||
|
||||
/** onSelect business layer can pass through */
|
||||
onSelect?: (key: string, selected: boolean, node: FileNode) => void;
|
||||
|
||||
/** Used to convert selectedFiles, occurs in, sets the selected state, to submit to the upper component, between, note that there is a sequence of processing, the return of the previous middleware will be used as the input of the latter */
|
||||
transSelectedFilesMiddlewares?: TransSelectedFilesMiddleware[];
|
||||
|
||||
/** The hook for setting the selection state occurs between clicking the check box and setting the selection state, and the processing is in sequence */
|
||||
selectFilesMiddlewares?: CalcCurrentSelectFilesMiddleware[];
|
||||
|
||||
/** Disable select disable select */
|
||||
disableSelect?: boolean;
|
||||
|
||||
/** checkRelation: Whether the selected state of the parent and child nodes is related */
|
||||
checkRelation?: 'related' | 'unRelated';
|
||||
|
||||
/** Default expanded node 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 => {
|
||||
// Because onChangeWithObject is opened, the selected state here should be stored with object.
|
||||
if (typeof element === 'string') {
|
||||
return (
|
||||
treeDataMap.get(element) ?? {
|
||||
key: element,
|
||||
}
|
||||
);
|
||||
}
|
||||
return element;
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
function transDefaultValueToRenderNode(defaultValue?: FileId[] | FileNode[]) {
|
||||
return (
|
||||
defaultValue?.map(element => {
|
||||
// Because onChangeWithObject is opened, the selected state here should be stored with object.
|
||||
if (typeof element === 'string') {
|
||||
return {
|
||||
key: element,
|
||||
};
|
||||
}
|
||||
return element;
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ------------------
|
||||
* common file picker
|
||||
* Select file for data upload
|
||||
* ------------------
|
||||
* useImperativeHandle:
|
||||
* Search: Provides tree search capability
|
||||
* ------------------
|
||||
* 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);
|
||||
// Use controlled mode
|
||||
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];
|
||||
}
|
||||
|
||||
// Calculate diff
|
||||
const [addNodes, removeNodes, retainNodes] = diffChangeNodes(
|
||||
prevChangeNodes.current,
|
||||
transedChangeNodes as FileNode[],
|
||||
);
|
||||
|
||||
// The middleware here is more used in scenarios where the selected state is customized, such as wanting to unselect all sub-nodes but keep the parent node selected
|
||||
transedChangeNodes = distinctFileNodes(
|
||||
selectFilesMiddlewares.reduce(
|
||||
(selectedElements, middleware) =>
|
||||
middleware(
|
||||
selectedElements,
|
||||
addNodes,
|
||||
removeNodes,
|
||||
retainNodes,
|
||||
),
|
||||
transedChangeNodes,
|
||||
),
|
||||
);
|
||||
prevChangeNodes.current = transedChangeNodes;
|
||||
|
||||
setSelectValue(
|
||||
transedChangeNodes.map(transedNode => ({
|
||||
key: transedNode.key,
|
||||
})),
|
||||
);
|
||||
// Although the parent node with children is returned here, because they are all snapshots of the backend interface
|
||||
// What data to report to the business party?
|
||||
// The middleware here is mainly used in scenarios where the selected data is reported to the upper component, such as checkRelation'related 'mode, where only the data of the parent node is returned, but the parent component wants all the data
|
||||
// Warning: If you can't get the unrequested sub-node when using loadData here (in other words, the last layer of data you get is not necessarily a leaf node), the same is true here. After selecting it, it is only a parent node, and it cannot be guaranteed to be in a snapshot.
|
||||
if (transSelectedFilesMiddlewares) {
|
||||
transedChangeNodes = transSelectedFilesMiddlewares.reduce(
|
||||
(selectedElements, middleware) => middleware(selectedElements),
|
||||
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.
|
||||
*/
|
||||
|
||||
// Indent related data
|
||||
export const DEFAULT_FILENODE_LEVEL_INDENT = 24;
|
||||
|
||||
// Virtualization configuration related data
|
||||
export const DEFAULT_VIRTUAL_CONTAINER_HEIGHT = 540;
|
||||
export const DEFAULT_VIRTUAL_ITEM_HEIGHT = 28;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
// At present, the entire tree is multi-selected, and only leaf nodes can be selected
|
||||
if (multiple && onlySelectLeaf) {
|
||||
return isLeaf && !unCheckable ? (
|
||||
<>
|
||||
{expandPlaceHolder}
|
||||
{getMultiSelectAction(noRealExpandClassName)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{renderExpandIcon} {selectActionPlaceHolder}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// The current entire tree is multi-selected, and all nodes can be selected
|
||||
if (multiple && !onlySelectLeaf) {
|
||||
if (unCheckable) {
|
||||
return (
|
||||
<>
|
||||
{renderExpandIcon} {selectActionPlaceHolder}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isLeaf ? expandPlaceHolder : renderExpandIcon}
|
||||
{getMultiSelectAction(isLeaf ? noRealExpandClassName : '')}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// The current entire tree is radio-selected, and only leaf nodes can be selected
|
||||
if (!multiple && onlySelectLeaf) {
|
||||
return isLeaf && !unCheckable ? (
|
||||
<>
|
||||
{expandPlaceHolder}
|
||||
{getSingalSelectAction(noRealExpandClassName)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{renderExpandIcon}
|
||||
{selectActionPlaceHolder}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// The current entire tree is radio-selected, and all nodes can be selected
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* -----------------------------
|
||||
* Get the default file tree label renderer layer that is not platform aware
|
||||
* -----------------------------
|
||||
* @param {boolean} multiple or not
|
||||
* @param {boolean} onlySelectLeaf Whether only leaf nodes can be selected
|
||||
* @param {{indentSize: tree component indent size, expandIcon: tree component expandable node, expand icon, disableSelect: selected disabled state}} renderOption Render-related customization options
|
||||
* @Returns label rendering function
|
||||
*/
|
||||
export function useDefaultLabelRenderer(
|
||||
multiple: boolean,
|
||||
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'
|
||||
}`;
|
||||
|
||||
/**
|
||||
* The handler that clicks on the entire line, not the handler that clicks on checkbox or radio or expandIcon
|
||||
* @Param isLeaf: whether it is a leaf node
|
||||
* @Param onExpand: expand the line, handle function
|
||||
* @param onCheck: toggle in selected state
|
||||
**/
|
||||
const getItemAction = (params: {
|
||||
isLeaf: boolean;
|
||||
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>) {
|
||||
// If only the leaf node can be selected, then no matter whether it is multi-select/single-select, the parent node can only be expanded, and the leaf node can be selected
|
||||
// On the contrary, the parent node sub-node is selected, regardless of the multi-select radio, if you want to expand, click the expand icon.
|
||||
if (onlySelectLeaf) {
|
||||
if (!isLeaf) {
|
||||
onExpand(e);
|
||||
} 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;
|
||||
// Radio select the key of the selected option
|
||||
const singleSelectCheckStatus = singleSelectedKey === key;
|
||||
const rowCheckStatus = multiple
|
||||
? checkStatus
|
||||
: {
|
||||
checked: singleSelectCheckStatus,
|
||||
};
|
||||
// As long as data isLeaf is not empty, always look at data.isLeaf first
|
||||
const isLeaf = data?.isLeaf ?? !(data.children && data.children.length);
|
||||
const checkItem = multiple
|
||||
? (e: React.MouseEvent<Element, MouseEvent>) => {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* file selection tree node
|
||||
*/
|
||||
export interface FileNode extends TreeNodeData {
|
||||
/** Unique key identifier, can be used, file id */
|
||||
key: string;
|
||||
value?: string;
|
||||
label?: React.ReactNode;
|
||||
type?: TreeNodeType;
|
||||
// URL of the icon
|
||||
icon?: string;
|
||||
children?: FileNode[];
|
||||
/** Required when identifying whether the current node is a leaf node loadData */
|
||||
isLeaf?: boolean;
|
||||
/** Can the node be selected? */
|
||||
selectable?: boolean;
|
||||
/** The loading status of the node, after opening, the loading default replaces the icon, and displays the loadingInfo.
|
||||
* Note that this is different from the loading of the semi itself. The loading of the semi refers to the unfolded loading state. */
|
||||
isLoading?: boolean;
|
||||
/** Node loading prompt, the default is "getting" */
|
||||
loadingInfo?: string;
|
||||
/** Specific document types, such as doc docx txt, etc */
|
||||
file_type?: string;
|
||||
/** Three-way document link */
|
||||
file_url?: string;
|
||||
/** [Feishu scene] wiki space id,*/
|
||||
space_id?: string;
|
||||
/** [Feishu scene] wiki leaf id,*/
|
||||
obj_token?: string;
|
||||
/** Custom Rendering Items */
|
||||
render?: () => ReactNode;
|
||||
/** Read-only, not interactive */
|
||||
readonly?: boolean;
|
||||
/** Whether the node is not selectable, the default is false */
|
||||
unCheckable?: boolean;
|
||||
}
|
||||
|
||||
export type FileId = string;
|
||||
|
||||
/**
|
||||
* File selection tree, node selection status
|
||||
*/
|
||||
export interface FileSelectCheckStatus {
|
||||
checked: boolean;
|
||||
halfChecked: boolean;
|
||||
}
|
||||
|
||||
// Three parts, currently selected, newly selected, unselected from last time, unchanged from last time
|
||||
export type TransSelectedFilesMiddleware = (
|
||||
fileNodes: FileNode[],
|
||||
) => FileNode[];
|
||||
|
||||
export type CalcCurrentSelectFilesMiddleware = (
|
||||
fileNodes: FileNode[],
|
||||
addNodes?: FileNode[],
|
||||
removeNodes?: FileNode[],
|
||||
retainNodes?: FileNode[],
|
||||
) => FileNode[];
|
||||
|
||||
/** Type assertion, node is not fileNode */
|
||||
export function isFileNode(fileNode: unknown): fileNode is FileNode {
|
||||
return !!fileNode && isObject(fileNode) && !!(fileNode as FileNode).key;
|
||||
}
|
||||
|
||||
/** Type assertion, array is not a fileNode array */
|
||||
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">
|
||||
{/**document list */}
|
||||
{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">
|
||||
{/**segment hierarchy */}
|
||||
{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,
|
||||
}) => {
|
||||
/**
|
||||
* select function
|
||||
*/
|
||||
const [selected, setSelected] = useState(new Set<string>());
|
||||
// Sharding 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);
|
||||
/** In the project ide, the ide container is set to contain: strict, which will cause a fixed position.
|
||||
* The base of the offset is wrong, so you need to subtract the left and top values of the ide container here
|
||||
*/
|
||||
let clickX = e.pageX;
|
||||
let clickY = e.pageY;
|
||||
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'),
|
||||
};
|
||||
}
|
||||
|
||||
// If it is the same parent and the dragged position is before the current position, the dropIndex is reduced by 1.
|
||||
const originalIndex = parentSegment.children.indexOf(dragSegment.id);
|
||||
const dropIndex =
|
||||
originalIndex < positions.dropIndex &&
|
||||
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'),
|
||||
};
|
||||
}
|
||||
|
||||
// Remove the node from the children of the original parent
|
||||
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 ?? '')) {
|
||||
// Merge text content and delete nodes
|
||||
mergedSegment.text += segmentToMerge.text;
|
||||
const index = resSegments.findIndex(
|
||||
item => item.id === segmentToMerge.id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
resSegments.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
// Nodes of type non-section-text, move them to the children of the merged node
|
||||
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(
|
||||
// layout
|
||||
'relative',
|
||||
// spacing
|
||||
'mb-2 p-2',
|
||||
// Text Style
|
||||
'text-sm leading-5',
|
||||
// color
|
||||
'coz-fg-primary hover:coz-mg-hglt-secondary-hovered coz-mg-secondary',
|
||||
// border
|
||||
'border border-solid coz-stroke-primary rounded-lg',
|
||||
// table style
|
||||
getEditorTableClassname(),
|
||||
// image style
|
||||
getEditorImgClassname(),
|
||||
// line feed
|
||||
getEditorWordsCls(),
|
||||
)}
|
||||
>
|
||||
<p
|
||||
// Filtered xss with DOMPurify
|
||||
// eslint-disable-next-line risxss/catch-potential-xss-react
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
DOMPurify.sanitize(getRenderHtmlContent(chunk.content ?? ''), {
|
||||
/**
|
||||
* 1. Prevent CSS injection attacks
|
||||
* 2. Prevent users from writing the style tag by mistake, resulting in the global style being modified and the page display being abnormal
|
||||
*/
|
||||
FORBID_TAGS: ['style'],
|
||||
}) ?? '',
|
||||
}}
|
||||
/>
|
||||
</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';
|
||||
|
||||
// Define event name literal type
|
||||
export type EventTypeName =
|
||||
| 'previewContextMenuItemAction'
|
||||
| 'hoverEditBarAction';
|
||||
|
||||
/**
|
||||
* event type definition
|
||||
*/
|
||||
export interface EventTypes extends Record<EventType, unknown> {
|
||||
// right-click menu related events
|
||||
previewContextMenuItemAction: {
|
||||
type: 'add-after' | 'add-before' | 'delete' | 'edit';
|
||||
targetChunk: Chunk;
|
||||
newChunk?: Chunk;
|
||||
chunks?: Chunk[];
|
||||
};
|
||||
|
||||
// Floating edit bar related events
|
||||
hoverEditBarAction: {
|
||||
type: 'add-after' | 'add-before' | 'delete' | 'edit';
|
||||
targetChunk: Chunk;
|
||||
newChunk?: Chunk;
|
||||
chunks?: Chunk[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* event handler type
|
||||
*/
|
||||
export type EventHandler<T extends EventTypeName> = Handler<EventTypes[T]>;
|
||||
|
||||
/**
|
||||
* Create an event bus instance
|
||||
*/
|
||||
export const createEventBus = (): Emitter<EventTypes> => mitt<EventTypes>();
|
||||
|
||||
/**
|
||||
* Global Event Bus instance
|
||||
*/
|
||||
export const eventBus = createEventBus();
|
||||
|
||||
/**
|
||||
* event bus hook
|
||||
* Used to use the event bus in components
|
||||
*/
|
||||
export const useEventBus = () => eventBus;
|
||||
|
||||
/**
|
||||
* listen event hook
|
||||
* Used to listen for events in a component
|
||||
* @param eventName
|
||||
* @param handler event handler
|
||||
* @Param deps dependency arrays, rebind events when dependencies change
|
||||
*/
|
||||
export const useEventListener = <T extends EventTypeName>(
|
||||
eventName: T,
|
||||
handler: EventHandler<T>,
|
||||
deps: React.DependencyList = [],
|
||||
) => {
|
||||
useEffect(() => {
|
||||
// binding event
|
||||
eventBus.on(eventName, handler as Handler<unknown>);
|
||||
|
||||
// Unbind event when component is unmounted
|
||||
return () => {
|
||||
eventBus.off(eventName, handler as Handler<unknown>);
|
||||
};
|
||||
}, deps);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Image from '@tiptap/extension-image';
|
||||
|
||||
// Extend TipTap Image to support a custom data-tos-key HTML attribute
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
imageWithTosKey: {
|
||||
setImageWithTosKey: (options: {
|
||||
src: string;
|
||||
alt?: string;
|
||||
title?: string;
|
||||
dataTosKey?: string | null;
|
||||
}) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const ImageWithTosKey = Image.extend({
|
||||
addAttributes() {
|
||||
const parentAttributes = (this.parent?.() as Record<string, unknown>) || {};
|
||||
return {
|
||||
...parentAttributes,
|
||||
dataTosKey: {
|
||||
default: null,
|
||||
parseHTML: (element: HTMLElement) =>
|
||||
element.getAttribute('data-tos-key'),
|
||||
renderHTML: (attributes: { dataTosKey?: string | null }) => {
|
||||
if (!attributes.dataTosKey) {
|
||||
return {};
|
||||
}
|
||||
return { 'data-tos-key': attributes.dataTosKey };
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setImageWithTosKey:
|
||||
(options: {
|
||||
src: string;
|
||||
alt?: string;
|
||||
title?: string;
|
||||
dataTosKey?: string | null;
|
||||
}) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: {
|
||||
src: options.src,
|
||||
alt: options.alt,
|
||||
title: options.title,
|
||||
dataTosKey: options.dataTosKey ?? null,
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default ImageWithTosKey;
|
||||
@@ -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,83 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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) => {
|
||||
// Handle image upload
|
||||
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) {
|
||||
// Insert pictures into the editor and set the tosKey
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.setImageWithTosKey({
|
||||
src: result.url,
|
||||
dataTosKey: result.tosKey ?? null,
|
||||
})
|
||||
.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 {
|
||||
// business logic
|
||||
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);
|
||||
|
||||
/**
|
||||
* When right-clicking on the editor, the context menu is displayed
|
||||
*/
|
||||
const { contextMenuPosition, openContextMenu } = useControlContextMenu({
|
||||
contextMenuRef,
|
||||
});
|
||||
|
||||
/**
|
||||
* When clicking outside the editor
|
||||
*/
|
||||
useOutEditorMode({
|
||||
editorRef,
|
||||
exclude: [contextMenuRef],
|
||||
onExitEditMode: () => {
|
||||
const newContent = getEditorContent(editor);
|
||||
onBlur?.(newContent);
|
||||
},
|
||||
});
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={editorRef}
|
||||
className={classNames(
|
||||
// layout
|
||||
'relative',
|
||||
// spacing
|
||||
'mb-2 p-2',
|
||||
// Text Style
|
||||
'text-sm leading-5',
|
||||
// color
|
||||
'coz-fg-primary coz-bg-max',
|
||||
// border
|
||||
'border border-solid coz-stroke-hglt rounded-lg',
|
||||
)}
|
||||
onContextMenu={openContextMenu}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
// table style
|
||||
getEditorTableClassname(),
|
||||
// image style
|
||||
getEditorImgClassname(),
|
||||
// line feed
|
||||
getEditorWordsCls(),
|
||||
)}
|
||||
>
|
||||
<EditorContent editor={editor} placeholder={placeholder} />
|
||||
{editorBottomSlot}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* right-click menu */}
|
||||
{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,
|
||||
}) => {
|
||||
// Add new shardings after specific shardings
|
||||
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';
|
||||
|
||||
/**
|
||||
* Add the action component of a new sharding before a specific sharding
|
||||
*/
|
||||
export const AddBeforeAction: React.FC<HoverEditBarActionProps> = ({
|
||||
chunk,
|
||||
chunks = [],
|
||||
disabled,
|
||||
}) => {
|
||||
// Add new shardings before specific shardings
|
||||
const { addEmptyChunkBefore } = useAddEmptyChunkAction({
|
||||
chunks,
|
||||
onChunksChange: ({ newChunk, chunks: newChunks }) => {
|
||||
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';
|
||||
|
||||
/**
|
||||
* Remove the action component for specific shardings
|
||||
*
|
||||
* The logic to remove specific shardings is implemented internally
|
||||
* If an onDelete callback is passed, it will be called on click
|
||||
* If chunks, onChunksChange are provided, the deletion logic is handled internally.
|
||||
* No need to rely on external usePreviewContextMenu
|
||||
*/
|
||||
export const DeleteAction: React.FC<HoverEditBarActionProps> = ({
|
||||
chunk,
|
||||
chunks = [],
|
||||
disabled,
|
||||
}) => {
|
||||
// Remove specific shardings
|
||||
const { deleteChunk } = useDeleteAction({
|
||||
chunks,
|
||||
onChunksChange: ({ chunks: newChunks }) => {
|
||||
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';
|
||||
|
||||
/**
|
||||
* Edit action component
|
||||
*
|
||||
* The logic to activate the edit mode for specific shardings is implemented internally
|
||||
* If an onEdit callback is passed, it will be called on click
|
||||
*/
|
||||
export const EditAction: React.FC<HoverEditBarActionProps> = ({
|
||||
chunk,
|
||||
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';
|
||||
|
||||
/**
|
||||
* Add a new sharding's menu item component after a specific sharding
|
||||
*/
|
||||
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,
|
||||
});
|
||||
|
||||
// Add new shardings after specific shardings
|
||||
const { addEmptyChunkAfter } = useAddEmptyChunkAction({
|
||||
chunks,
|
||||
onChunksChange: ({ newChunk, chunks: newChunks }) => {
|
||||
// Issue an event to add a new sharding after a specific sharding
|
||||
eventBus.emit('previewContextMenuItemAction', {
|
||||
type: 'add-after',
|
||||
newChunk,
|
||||
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';
|
||||
|
||||
/**
|
||||
* Add a new sharding's menu item component before a specific sharding
|
||||
*/
|
||||
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,
|
||||
});
|
||||
|
||||
// Add new shardings before specific shardings
|
||||
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';
|
||||
|
||||
/**
|
||||
* Remove a specific sharding menu item component
|
||||
*/
|
||||
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,
|
||||
});
|
||||
|
||||
// Remove specific shardings
|
||||
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';
|
||||
|
||||
/**
|
||||
* Edit Action Menu Item Component
|
||||
*
|
||||
* The logic to activate the edit mode for specific shardings is implemented internally
|
||||
* If an onEdit callback is passed, it will be called on click
|
||||
*/
|
||||
export const EditAction: React.FC<PreviewContextMenuItemProps> = ({
|
||||
chunk,
|
||||
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(
|
||||
// layout
|
||||
'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)}
|
||||
>
|
||||
{/* The action bar displayed when hovering */}
|
||||
{hoveredChunk === chunk.text_knowledge_editor_chunk_uuid &&
|
||||
!readonly ? (
|
||||
<HoverEditBar
|
||||
chunk={chunk}
|
||||
chunks={chunks}
|
||||
hoverEditBarActionsRegistry={hoverEditBarActionsRegistry}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DocumentChunkPreview chunk={chunk} locateId={locateId || ''} />
|
||||
</div>
|
||||
|
||||
{/* right-click menu */}
|
||||
{contextMenuPosition ? (
|
||||
<PreviewContextMenu
|
||||
previewContextMenuItemsRegistry={previewContextMenuItemsRegistry}
|
||||
x={contextMenuPosition.x}
|
||||
y={contextMenuPosition.y}
|
||||
chunk={chunk}
|
||||
chunks={chunks}
|
||||
readonly={readonly}
|
||||
contextMenuRef={contextMenuRef}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Wrap components with React.memo to avoid unnecessary re-rendering
|
||||
export const DocumentPreview = React.memo(
|
||||
DocumentPreviewComponent,
|
||||
(prevProps, nextProps) => {
|
||||
// If the sharding content changes, it needs to be re-rendered
|
||||
if (prevProps.chunk.content !== nextProps.chunk.content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// In other cases, no re-rendering is required
|
||||
return true;
|
||||
},
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
// Handle right-click menus
|
||||
const openContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Calculate the position relative to the event target element
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const relativeX = e.clientX - rect.left;
|
||||
const relativeY = e.clientY - rect.top;
|
||||
|
||||
setContextMenuPosition({
|
||||
x: relativeX,
|
||||
y: relativeY,
|
||||
});
|
||||
};
|
||||
|
||||
// Close the right-click menu
|
||||
const closeContextMenu = () => {
|
||||
setContextMenuPosition(null);
|
||||
};
|
||||
|
||||
// Process Click Document Other Locations
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// If you click outside the right-click menu, close the menu
|
||||
if (
|
||||
contextMenuRef.current &&
|
||||
!contextMenuRef.current.contains(event.target as Node)
|
||||
) {
|
||||
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);
|
||||
|
||||
// Handle right-click menus
|
||||
const openContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setContextMenuPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
});
|
||||
};
|
||||
|
||||
// Close the right-click menu
|
||||
const closeContextMenu = () => {
|
||||
setContextMenuPosition(null);
|
||||
};
|
||||
|
||||
// Process Click Document Other Locations
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// If you click outside the right-click menu, close the menu
|
||||
if (
|
||||
contextMenuRef.current &&
|
||||
!contextMenuRef.current.contains(event.target as Node)
|
||||
) {
|
||||
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);
|
||||
|
||||
// Handling right-click events
|
||||
const openContextMenu = (e: React.MouseEvent, chunk: Chunk) => {
|
||||
e.preventDefault();
|
||||
setContextMenuInfo({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
chunk,
|
||||
});
|
||||
};
|
||||
|
||||
// Close the right-click menu
|
||||
const closeContextMenu = () => {
|
||||
setContextMenuInfo(null);
|
||||
};
|
||||
|
||||
// Click elsewhere in the document to close the right-click menu
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
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);
|
||||
|
||||
// Handling mouse hover events
|
||||
const handleMouseEnter = (chunkId: string) => {
|
||||
setHoveredChunk(chunkId);
|
||||
};
|
||||
|
||||
// Handling mouse away events
|
||||
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) => {
|
||||
// Process Click Document Other Locations
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// If you click outside the editor, exit editing mode
|
||||
if (
|
||||
editorRef.current &&
|
||||
!editorRef.current.contains(event.target as Node) &&
|
||||
!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;
|
||||
}
|
||||
/**
|
||||
* Add new sharding hook after specific sharding
|
||||
*
|
||||
* Provides the ability to add new shardings after specific shardings
|
||||
*/
|
||||
export const useAddEmptyChunkAction = ({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
}: UseAddEmptyChunkActionProps) => {
|
||||
// Use ref to save the latest chunks reference
|
||||
const chunksRef = useRef<Chunk[]>(chunks);
|
||||
|
||||
// Every time props.chunks is updated, update the ref.
|
||||
useEffect(() => {
|
||||
chunksRef.current = chunks;
|
||||
}, [chunks]);
|
||||
|
||||
/**
|
||||
* Add new shardings after specific shardings
|
||||
* @Returns the result object containing the new sharding and the updated sharding list
|
||||
*/
|
||||
const handleAddEmptyChunkAfter = (chunk: Chunk) => {
|
||||
// Get the latest chunks from the ref
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add new shardings before specific shardings
|
||||
*/
|
||||
const handleAddEmptyChunkBefore = (chunk: Chunk) => {
|
||||
// Get the latest chunks from the ref
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove sharding hook
|
||||
*
|
||||
* Provides the ability to remove specific shardings
|
||||
*/
|
||||
export const useDeleteAction = ({
|
||||
chunks,
|
||||
onChunksChange,
|
||||
}: UseDeleteActionProps) => {
|
||||
// Use ref to save the latest chunks reference
|
||||
const chunksRef = useRef<Chunk[]>(chunks);
|
||||
const { deleteSlice } = useDeleteChunk();
|
||||
|
||||
// Every time props.chunks is updated, update the ref.
|
||||
useEffect(() => {
|
||||
chunksRef.current = chunks;
|
||||
}, [chunks]);
|
||||
|
||||
/**
|
||||
* Remove specific shardings
|
||||
*/
|
||||
const handleDeleteChunk = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// Get the latest chunks from the ref
|
||||
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,
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle the creation of local shardings
|
||||
*/
|
||||
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) => {
|
||||
/**
|
||||
* Handle deletion of local shardings
|
||||
*/
|
||||
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();
|
||||
|
||||
/**
|
||||
* Handling remotely sharding deletion operations
|
||||
*/
|
||||
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 { 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';
|
||||
import ImageWithTosKey from '@/text-knowledge-editor/extensions/image-with-tos-key';
|
||||
|
||||
interface UseDocumentEditorProps {
|
||||
chunk: Chunk | null;
|
||||
editorProps?: EditorProps;
|
||||
onChange?: (chunk: Chunk) => void;
|
||||
}
|
||||
|
||||
export const useInitEditor = ({
|
||||
chunk,
|
||||
editorProps,
|
||||
onChange,
|
||||
}: UseDocumentEditorProps) => {
|
||||
// Create an editor instance
|
||||
const editor: Editor | null = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
hardBreak: {
|
||||
// force wrap
|
||||
keepMarks: false,
|
||||
},
|
||||
paragraph: {
|
||||
// Configure paragraphs to avoid generating extra empty paragraphs
|
||||
HTMLAttributes: {
|
||||
class: 'text-knowledge-tiptap-editor-paragraph',
|
||||
},
|
||||
},
|
||||
}),
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
}),
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
ImageWithTosKey.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 the pasted plain text contains a newline character
|
||||
if (text?.includes('\n')) {
|
||||
event.preventDefault(); // Block default paste behavior
|
||||
|
||||
const html = getRenderHtmlContent(text);
|
||||
|
||||
// Insert the converted HTML into the editor
|
||||
editor.chain().focus().insertContent(html).run();
|
||||
|
||||
return true; // It means we have dealt with it.
|
||||
}
|
||||
|
||||
return false; // Use default behavior
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Update editor content when active sharding changes
|
||||
useEffect(() => {
|
||||
if (!editor || !chunk) {
|
||||
return;
|
||||
}
|
||||
const htmlContent = getRenderHtmlContent(chunk.content || '');
|
||||
// Set content, keep newlines
|
||||
editor.commands.setContent(htmlContent || '', false, {
|
||||
preserveWhitespace: 'full',
|
||||
});
|
||||
}, [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) => {
|
||||
// Use ref to save the latest chunks reference
|
||||
const chunksRef = useRef(chunks);
|
||||
const { deleteSlice } = useDeleteChunk();
|
||||
|
||||
// Every time props.chunks is updated, update the ref.
|
||||
useEffect(() => {
|
||||
chunksRef.current = chunks;
|
||||
}, [chunks]);
|
||||
|
||||
// Activate edit mode for specific shardings
|
||||
const handleActivateEditMode = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
onActiveChunkChange?.(chunk);
|
||||
},
|
||||
[onActiveChunkChange],
|
||||
);
|
||||
|
||||
// Add new shardings before specific shardings
|
||||
const handleAddChunkBefore = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// Get the latest chunks from the ref
|
||||
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);
|
||||
|
||||
// Automatically activate editing mode for new shardings
|
||||
onActiveChunkChange?.(newChunk);
|
||||
onAddChunk?.(newChunk);
|
||||
},
|
||||
[onChunksChange, onActiveChunkChange, documentId, onAddChunk],
|
||||
);
|
||||
|
||||
// Add new shardings after specific shardings
|
||||
const handleAddChunkAfter = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// Get the latest chunks from the ref
|
||||
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),
|
||||
];
|
||||
|
||||
// Automatically activate editing mode for new shardings
|
||||
onActiveChunkChange?.(newChunk);
|
||||
onChunksChange?.(updatedChunks);
|
||||
onAddChunk?.(newChunk);
|
||||
},
|
||||
[onChunksChange, onActiveChunkChange, documentId, onAddChunk],
|
||||
);
|
||||
|
||||
// Remove specific shardings
|
||||
const handleDeleteChunk = useCallback(
|
||||
(chunk: Chunk) => {
|
||||
// Get the latest chunks from the ref
|
||||
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,
|
||||
});
|
||||
|
||||
/**
|
||||
* Handling save logic for remote shardings
|
||||
*/
|
||||
const saveRemoteChunk = async (chunk: Chunk) => {
|
||||
if (chunk.content === '') {
|
||||
await deleteRemoteChunk(chunk);
|
||||
return;
|
||||
}
|
||||
await updateRemoteChunk(chunk);
|
||||
};
|
||||
|
||||
/**
|
||||
* Save logic for handling local shardings
|
||||
*/
|
||||
const saveLocalChunk = async (chunk: Chunk) => {
|
||||
if (chunk.content === '') {
|
||||
deleteLocalChunk(chunk);
|
||||
} else {
|
||||
await createLocalChunk(chunk);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the main function of sharding
|
||||
*/
|
||||
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();
|
||||
|
||||
/**
|
||||
* Handling updates for remote sharding
|
||||
*/
|
||||
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';
|
||||
|
||||
// Add component export
|
||||
export { HoverEditBar } from './features/hover-edit-bar/hover-edit-bar';
|
||||
export {
|
||||
EditAction,
|
||||
AddBeforeAction,
|
||||
AddAfterAction,
|
||||
DeleteAction,
|
||||
} from './features/hover-edit-bar-actions';
|
||||
|
||||
// Event Bus Dependent Export
|
||||
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);
|
||||
|
||||
// Using the core editor functions
|
||||
const { editor } = useInitEditor({
|
||||
chunk: activeChunk,
|
||||
});
|
||||
|
||||
// Exit the new sharding feature
|
||||
const { saveChunk } = useSaveChunk({
|
||||
chunks,
|
||||
documentId,
|
||||
onChunksChange: newChunks => {
|
||||
onChange?.(newChunks);
|
||||
setActiveChunk(null);
|
||||
},
|
||||
onAddChunk,
|
||||
onDeleteChunk,
|
||||
});
|
||||
|
||||
// Monitor right-click menu events
|
||||
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],
|
||||
),
|
||||
);
|
||||
|
||||
// Monitor floating edit bar events
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage active chunks in documents
|
||||
* Use the renderLevel field to uniquely identify the render location of the chunk
|
||||
*/
|
||||
export const useActiveChunk = () => {
|
||||
// Store the active chunk and its renderLevel
|
||||
const [activeChunkInfo, setActiveChunkInfo] = useState<ActiveChunkInfo>({
|
||||
chunk: null,
|
||||
renderLevel: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* Clear active chunks
|
||||
*/
|
||||
const clearActiveChunk = () => {
|
||||
setActiveChunkInfo({
|
||||
chunk: null,
|
||||
renderLevel: null,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the active chunk and its renderLevel
|
||||
* Use during user interaction (e.g. double-clicking)
|
||||
*/
|
||||
const setActiveChunkWithLevel = (chunk: LevelDocumentTreeNode) => {
|
||||
if (!chunk.renderLevel) {
|
||||
console.warn('Chunk does not have renderLevel field', chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveChunkInfo({
|
||||
chunk,
|
||||
renderLevel: chunk.renderLevel,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether the given chunk is the currently active 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage the rendering path for chunks with the same ID in a document
|
||||
* Solve the problem of duplicate IDs by assigning a unique render path to each chunk instance
|
||||
*/
|
||||
export const useChunkRenderPath = () => {
|
||||
// Store the active chunk and its render path
|
||||
const [activeChunkInfo, setActiveChunkInfo] = useState<ActiveChunkInfo>({
|
||||
chunk: null,
|
||||
renderPath: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* Set the active chunk, but not the render path
|
||||
* Usually used in external logic, such as usePreviewContextMenu
|
||||
*/
|
||||
const setActiveChunk = (chunk: LevelDocumentChunk | null) => {
|
||||
setActiveChunkInfo(prev => ({
|
||||
...prev,
|
||||
chunk,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear active chunks
|
||||
*/
|
||||
const clearActiveChunk = () => {
|
||||
setActiveChunkInfo({
|
||||
chunk: null,
|
||||
renderPath: null,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the active chunk and its rendering path
|
||||
* Use during user interaction (e.g. double-clicking)
|
||||
*/
|
||||
const setActiveChunkWithPath = (
|
||||
chunk: LevelDocumentChunk,
|
||||
renderPath: string,
|
||||
) => {
|
||||
setActiveChunkInfo({
|
||||
chunk,
|
||||
renderPath,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether the given chunk and rendering path match the currently active chunk
|
||||
*/
|
||||
const isActiveChunk = (chunkId: string, renderPath: string) =>
|
||||
chunkId === activeChunkInfo.chunk?.text_knowledge_editor_chunk_uuid &&
|
||||
renderPath === activeChunkInfo.renderPath;
|
||||
|
||||
/**
|
||||
* Generate a unique render path for chunks
|
||||
*/
|
||||
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';
|
||||