feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
/** @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',
},
};
export default config;

View File

@@ -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;

View File

@@ -0,0 +1,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,16 @@
# data-base
> Project template for react component with storybook and supports publish independently.
## Features
- [x] eslint & ts
- [x] esm bundle
- [x] umd bundle
- [x] storybook
## Commands
- init: `rush update`
- dev: `npm run dev`
- build: `npm run build`

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { useShallow } from 'zustand/react/shallow';
import { describe, expect, it, vi } from 'vitest';
import { render } from '@testing-library/react';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { DatabaseDebug } from '../../src/components/database-debug';
vi.mock('@coze-studio/bot-detail-store/bot-skill', () => ({
useBotSkillStore: vi.fn(selector => {
if (typeof selector === 'function') {
return selector({
databaseList: [
{
id: 'db1',
name: 'Test Database',
tableId: 'table1',
tables: [
{
id: 'table1',
name: 'Test Table',
fields: [
{
id: 'field1',
name: 'Test Field',
type: 'string',
},
],
},
],
},
],
});
}
return {
databaseList: [],
};
}),
}));
vi.mock('../../src/components/database-debug/multi-table', () => ({
default: vi.fn(() => <div>MultiTable</div>),
}));
vi.mock('@coze-studio/bot-detail-store/bot-info', () => ({
useBotInfoStore: vi.fn(() => 'test-bot-id'),
}));
// Mock zustand stores
vi.mock('@coze-studio/bot-detail-store/bot-skill', () => ({
useBotSkillStore: vi.fn(selector => {
if (typeof selector === 'function') {
return selector({
databaseList: [
{
id: 'db1',
name: 'Test Database',
tableId: 'table1',
tables: [
{
id: 'table1',
name: 'Test Table',
fields: [
{
id: 'field1',
name: 'Test Field',
type: 'string',
},
],
},
],
},
],
});
}
return {
databaseList: [],
};
}),
}));
vi.mock('@coze-studio/bot-detail-store/bot-info', () => ({
useBotInfoStore: vi.fn(() => 'test-bot-id'),
}));
vi.mock('zustand/react/shallow', () => ({
useShallow: vi.fn(fn => fn),
}));
describe('DatabaseDebug', () => {
it('should use correct store selectors', () => {
render(<DatabaseDebug />);
expect(useBotInfoStore).toHaveBeenCalled();
expect(useBotSkillStore).toHaveBeenCalled();
expect(useShallow).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { describe, expect, it } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { type DatabaseList } from '@coze-studio/bot-detail-store';
import { BotTableRWMode, FieldItemType } from '@coze-arch/bot-api/memory';
import MultiTable from '../../src/components/database-debug/multi-table';
import s from '../../src/components/database-debug/index.module.less';
const mockDatabaseList: DatabaseList = [
{
tableId: 'table1',
name: 'Database 1',
desc: 'Test database 1',
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
tableMemoryList: [
{
nanoid: 'field1',
name: 'Field 1',
desc: 'Test field 1',
must_required: true,
type: FieldItemType.Text,
},
],
},
{
tableId: 'table2',
name: 'Database 2',
desc: 'Test database 2',
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
tableMemoryList: [
{
nanoid: 'field2',
name: 'Field 2',
desc: 'Test field 2',
must_required: true,
type: FieldItemType.Number,
},
],
},
];
vi.mock('@coze-arch/coze-design', () => ({
TabPane: vi.fn(({ children }) => <div>{children}</div>),
Tabs: vi.fn(({ children, renderTabBar }) => (
<div>
{renderTabBar?.({
list: mockDatabaseList.map(item => ({
tab: item.name,
itemKey: item.tableId,
})),
activeKey: 'table1',
onTabClick: vi.fn(),
})}
{children}
</div>
)),
Typography: {
Text: vi.fn(({ children, className, onClick }) => (
<div className={className} onClick={onClick}>
{children}
</div>
)),
},
Divider: vi.fn(({ key }) => <div key={key}>|</div>),
}));
vi.mock('@coze-data/e2e', () => ({
BotE2e: {
BotDatabaseDebugModalTableNameTab: 'BotDatabaseDebugModalTableNameTab',
BotDatabaseDebugModalResetBtn: 'BotDatabaseDebugModalResetBtn',
},
}));
vi.mock('../../src/components/database-debug/table/reset-btn', () => ({
default: vi.fn(() => <div>ResetBtn</div>),
}));
vi.mock('../../src/components/database-debug/table/index', () => ({
DataTable: vi.fn(() => <div>DataTable</div>),
}));
// Mock CSS Modules
vi.mock('../../src/components/database-debug/index.module.less', () => ({
default: {
'modal-content-tabs': 'modal-content-tabs',
'tab-bar-box': 'tab-bar-box',
'tab-bar-item': 'tab-bar-item',
'tab-bar-item-active': 'tab-bar-item-active',
},
}));
describe('MultiTable', () => {
it('should render database tabs', () => {
render(<MultiTable botID="test-bot" databaseList={mockDatabaseList} />);
expect(screen.getByText('Database 1')).toBeInTheDocument();
expect(screen.getByText('Database 2')).toBeInTheDocument();
});
it('should render empty state when no databases', () => {
render(<MultiTable botID="test-bot" databaseList={[]} />);
expect(screen.queryByText('Database 1')).not.toBeInTheDocument();
});
it('should use provided activeDatabaseID', () => {
render(
<MultiTable
botID="test-bot"
databaseList={mockDatabaseList}
activeDatabaseID="table2"
/>,
);
// 第二个数据库应该被激活
const tab = screen.getByText('Database 2').closest('div');
expect(tab).toHaveClass(s['tab-bar-item']);
});
it('should handle database switching', () => {
render(<MultiTable botID="test-bot" databaseList={mockDatabaseList} />);
// 点击第二个数据库标签
fireEvent.click(screen.getByText('Database 2'));
// 验证切换是否成功
const tab = screen.getByText('Database 2').closest('div');
expect(tab).toHaveClass(s['tab-bar-item']);
});
});

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

View File

@@ -0,0 +1,109 @@
{
"name": "@coze-data/database",
"version": "1.0.0",
"description": "coze database",
"license": "Apache-2.0",
"author": "rosefang.123@bytedance.com",
"maintainers": [],
"exports": {
".": "./src/index.ts",
"./filebox-list": "./src/components/filebox-list/index.tsx",
"./multi-table": "./src/components/database-debug/multi-table.tsx",
"./table": "./src/components/database-debug/table/index.tsx"
},
"main": "src/index.ts",
"unpkg": "./dist/umd/index.js",
"module": "./dist/esm/index.js",
"types": "./src/index.ts",
"typesVersions": {
"*": {
"filebox-list": [
"./src/components/filebox-list/index.tsx"
],
"multi-table": [
"./src/components/database-debug/multi-table.tsx"
],
"table": [
"./src/components/database-debug/table/index.tsx"
]
}
},
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/bot-error": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-tea": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/i18n": "workspace:*",
"@coze-common/chat-area-plugin-message-grab": "workspace:*",
"@coze-data/e2e": "workspace:*",
"@coze-data/knowledge-resource-processor-base": "workspace:*",
"@coze-data/knowledge-resource-processor-core": "workspace:*",
"@coze-studio/user-store": "workspace:*",
"@douyinfe/semi-icons": "^2.36.0",
"ahooks": "^3.7.8",
"classnames": "^2.3.2",
"dayjs": "^1.11.7",
"immer": "^10.0.3",
"lodash-es": "^4.17.21",
"zustand": "^4.4.7"
},
"devDependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-flags": "workspace:*",
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/report-events": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@coze-common/chat-area": "workspace:*",
"@coze-common/chat-core": "workspace:*",
"@coze-common/table-view": "workspace:*",
"@coze-data/reporter": "workspace:*",
"@coze-studio/bot-detail-store": "workspace:*",
"@douyinfe/semi-illustrations": "^2.36.0",
"@rollup/plugin-commonjs": "^24.0.0",
"@rollup/plugin-json": "~6.0.0",
"@rollup/plugin-node-resolve": "~15.0.1",
"@rollup/plugin-replace": "^4.0.0",
"@swc/core": "^1.3.35",
"@swc/helpers": "^0.4.12",
"@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",
"autoprefixer": "^10.4.16",
"less-loader": "~11.1.3",
"postcss": "^8.4.32",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"react-router-dom": "^6.22.0",
"rollup": "^4.9.0",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-node-externals": "^6.1.2",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-ts": "^3.1.1",
"tailwindcss": "~3.3.3",
"vitest": "~3.0.5"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
},
"// deps": "immer@^10.0.3 为脚本自动补齐,请勿改动"
}

View File

@@ -0,0 +1,97 @@
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import fs from 'fs';
import tailwindcss from 'tailwindcss';
import ts from 'rollup-plugin-ts';
import postcss from 'rollup-plugin-postcss';
import { nodeExternals } from 'rollup-plugin-node-externals';
import cleanup from 'rollup-plugin-cleanup';
import autoprefixer from 'autoprefixer';
import replace from '@rollup/plugin-replace';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import json from '@rollup/plugin-json';
import commonjs from '@rollup/plugin-commonjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
// CI 环境下不需要构建这么多版本
const isInCIEnv = process.env.CI === 'true';
const banner =
'/*!\n' +
` * ${packageJson.name} v${packageJson.version}\n` +
` * (c) 2023-${new Date().getFullYear()} Flow team\n` +
' */';
const outputConfigs = {
esm: {
banner,
format: 'es',
file: path.resolve(__dirname, 'dist/esm/index.js'),
},
umd: {
banner,
format: 'umd',
file: path.resolve(__dirname, 'dist/umd/index.js'),
},
};
const createReplacePlugin = () =>
replace({
preventAssignment: true,
values: {
__TEST__: false,
__VERSION__: `'${packageJson.version}'`,
__DEV__: process.env.NODE_ENV === 'development',
},
});
const createConfig = (format, output) => {
const isUmdBuild = /^umd/.test(format);
const isEsmBuild = /^esm/.test(format);
const input = path.resolve(__dirname, './src/index.ts');
// TODO: 这里替换成真实的名称
if (isUmdBuild) {
output.name = 'FlowFoo';
}
return {
input,
output,
plugins: [
commonjs(),
createReplacePlugin(),
nodeResolve(),
cleanup(),
postcss({
plugins: [tailwindcss(), autoprefixer()],
autoModules: true,
modules: {
generateScopedName: '[name][local]_[hash:base64:5]',
},
extensions: ['.css', '.less'],
}),
json({
namedExports: false,
}),
// ESM 仅打包必要内容
// UMD 由于需要放到 page 上直接运行,因此需要将所有依赖都打包进来
isEsmBuild
? nodeExternals({ devDeps: true, peerDeps: true, deps: true })
: null,
ts({
transpiler: 'swc',
tsconfig: path.resolve(__dirname, './tsconfig.build.json'),
}),
].filter(Boolean),
};
};
export default Object.keys(outputConfigs)
.filter(k => (isInCIEnv ? k === 'esm' : true))
.map(format => createConfig(format, outputConfigs[format]));

View File

@@ -0,0 +1,50 @@
/* stylelint-disable declaration-no-important */
.modal-content-tabs {
:global {
.semi-tabs {
display: flex;
flex-direction: column;
height: 100%;
}
.semi-tabs-content {
height: calc(100% - 40px);
padding: 0 !important;
padding-bottom: 0;
}
.semi-tabs-pane {
height: 100%;
}
.semi-tabs-pane-motion-overlay {
height: 100%;
}
}
}
.tab-bar-box {
display: flex;
gap: 16px;
align-items: center;
justify-content: flex-start;
padding: 8px 0 8px 16px;
}
.tab-bar-item {
cursor: pointer;
font-size: 18px;
font-weight: 600;
font-style: normal;
line-height: 24px;
color: var(--Light-usage-text---color-text-2, rgba(29, 28, 35, 60%));
text-overflow: ellipsis;
}
.tab-bar-item-active {
color: #4D53E8;
}

View File

@@ -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 { useShallow } from 'zustand/react/shallow';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import MultiTable from './multi-table';
export const DatabaseDebug = () => {
const botID = useBotInfoStore(state => state.botId);
const { databaseList } = useBotSkillStore(
useShallow(detail => ({
databaseList: detail.databaseList,
})),
);
return <MultiTable botID={botID} databaseList={databaseList} />;
};

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { TabPane, Tabs, Typography, Divider } from '@coze-arch/coze-design';
const { Text } = Typography;
import { BotE2e } from '@coze-data/e2e';
import ResetBtn from './table/reset-btn';
import { DataTable, type DataTableRef } from './table';
import s from './index.module.less';
import type { DatabaseInfo, DatabaseList } from '@coze-studio/bot-detail-store';
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
const TablePaneContent = forwardRef<
DataTableRef,
{
info: DatabaseInfo;
botID?: string;
workflowID?: string;
projectID?: string;
}
>(({ info, botID, workflowID, projectID }, ref) => {
const _ref = useRef<DataTableRef>(null);
useImperativeHandle(ref, () => ({
refetch: _ref.current?.refetch,
}));
return (
<>
<DataTable
projectID={projectID}
database={info}
botID={botID}
workflowID={workflowID}
ref={_ref}
/>
<div
className="absolute top-[6px] right-0"
data-testid={BotE2e.BotDatabaseDebugModalResetBtn}
>
<ResetBtn
database={info}
botID={botID}
workflowID={workflowID}
projectID={projectID}
afterReset={() => {
_ref.current?.refetch?.();
}}
/>
</div>
</>
);
});
export interface MultiTableProps {
botID?: string;
workflowID?: string;
databaseList: DatabaseList;
projectID?: string;
activeDatabaseID?: string;
}
const MultiTable = forwardRef<DataTableRef, MultiTableProps>(
({ botID, workflowID, databaseList, projectID, activeDatabaseID }, ref) => {
const [activeKeyInner, setActiveKeyInner] = useState(
activeDatabaseID ?? databaseList?.[0]?.tableId,
);
useEffect(() => {
if (typeof activeDatabaseID !== 'undefined') {
setActiveKeyInner(activeDatabaseID);
}
}, [activeDatabaseID]);
const tableRefMap = useRef<Record<string, () => Promise<void>>>({});
useImperativeHandle(ref, () => ({
refetch: tableRefMap.current[activeKeyInner],
}));
return (
<div
className={classNames({
[s['modal-content-tabs']]: true,
['h-full']: true,
})}
>
{databaseList.length ? (
<Tabs
type="line"
keepDOM={false}
renderTabBar={tabBarProps => {
const { list, activeKey, onTabClick } = tabBarProps;
return (
<div className={classNames([s['tab-bar-box'], 'mr-[108px]'])}>
{list?.map((item, index) => (
<>
<Text
data-dtestid={`${BotE2e.BotDatabaseDebugModalTableNameTab}.${item.tab}`}
className={classNames({
[s['tab-bar-item']]: true,
[s['tab-bar-item-active']]:
activeKey === item.itemKey,
})}
onClick={e => {
onTabClick?.(item.itemKey, e);
}}
ellipsis={{
showTooltip: true,
}}
>
{item.tab}
</Text>
{index === list.length - 1 ? null : (
<Divider layout="vertical" />
)}
</>
))}
</div>
);
}}
activeKey={activeKeyInner}
onChange={setActiveKeyInner}
>
{databaseList.map(item => (
<TabPane tab={item.name} itemKey={item.tableId}>
<TablePaneContent
projectID={projectID}
info={item}
botID={botID}
workflowID={workflowID}
ref={tableRef => {
if (tableRef?.refetch) {
tableRefMap.current[item.tableId] = tableRef.refetch;
}
}}
/>
</TabPane>
))}
</Tabs>
) : null}
</div>
);
},
);
export default MultiTable;

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { get } from 'lodash-es';
import { type TableMemoryItem } from '@coze-studio/bot-detail-store';
import { colWidthCacheService } from '@coze-common/table-view';
import { type ColumnProps } from '@coze-arch/bot-semi/Table';
import { Typography } from '@coze-arch/bot-semi';
import { FieldItemType } from '@coze-arch/bot-api/memory';
import styles from './index.module.less';
const { Text } = Typography;
const DEFAULT_WIDTH = 120;
export const MAX_WIDTH = 855;
const getTitle = (name: string, mustRequired: boolean) => (
<div className="flex items-center">
<Text
ellipsis={{
showTooltip: {
opts: { content: name },
},
}}
>
{name}
</Text>
{mustRequired ? (
<span style={{ color: 'red', height: '16px' }}>*</span>
) : null}
</div>
);
/* eslint-disable complexity */
export const getColumns = (
_list: TableMemoryItem[],
tableId: string,
): { list: ColumnProps[]; width: number } => {
const cacheWidthMap = colWidthCacheService?.getTableWidthMap(tableId) ?? {};
const initWidth =
MAX_WIDTH / _list.length > DEFAULT_WIDTH
? MAX_WIDTH / _list.length
: DEFAULT_WIDTH;
const list: ColumnProps[] = _list.map((i, index) => {
let res: ColumnProps = {};
const width = get(cacheWidthMap, i.name || '');
const dataWidth = width ? width : initWidth;
const isLast = index === _list.length - 1;
switch (i.type) {
// 文本
case FieldItemType.Text:
res = {
// @ts-expect-error -- linter-disable-autofix
className:
isLast && `${styles['last-column-text']} not-resize-handle`,
title: getTitle(i.name as string, i.must_required || false),
dataIndex: i.name,
width: isLast ? undefined : dataWidth,
};
break;
// 整数
case FieldItemType.Number:
res = {
// @ts-expect-error -- linter-disable-autofix
className:
isLast && `${styles['last-column-min-width']} not-resize-handle`,
title: getTitle(i.name as string, i.must_required || false),
dataIndex: i.name,
width: isLast ? undefined : dataWidth,
};
break;
// 数字
case FieldItemType.Float:
res = {
// @ts-expect-error -- linter-disable-autofix
className:
isLast && `${styles['last-column-min-width']} not-resize-handle`,
title: getTitle(i.name as string, i.must_required || false),
dataIndex: i.name,
width: isLast ? undefined : dataWidth,
};
break;
// 时间
case FieldItemType.Date:
res = {
// @ts-expect-error -- linter-disable-autofix
className:
isLast && `${styles['last-column-date']} not-resize-handle`,
title: getTitle(i.name as string, i.must_required || false),
dataIndex: i.name,
width: isLast ? undefined : dataWidth,
};
break;
// 布尔
case FieldItemType.Boolean:
res = {
// @ts-expect-error -- linter-disable-autofix
className:
isLast && `${styles['last-column-min-width']} not-resize-handle`,
title: getTitle(i.name as string, i.must_required || false),
dataIndex: i.name,
width: isLast ? undefined : dataWidth,
};
break;
default:
break;
}
return res;
});
const defaultWidth = 120;
return {
list,
width: list.reduce(
(prev: number, cur: ColumnProps) =>
prev + (Number(cur.width) || defaultWidth),
0,
) as number,
};
};

View File

@@ -0,0 +1,94 @@
.empty-wrapper-database {
:global {
.semi-empty-image {
align-items: center;
width: 140px;
height: 140px;
}
.semi-empty-vertical .semi-empty-content {
margin-top: 16px;
}
}
}
.data-table {
:global {
.table-wrapper {
overflow-y: auto;
}
.semi-table-wrapper {
margin-top: 0;
}
.semi-table-header-sticky .semi-table-thead>.semi-table-row>.semi-table-row-head {
background-color: transparent;
border-width: 1px;
}
.semi-table-tbody>.semi-table-row:hover>.semi-table-row-cell {
background-color: transparent;
background-image: none;
}
.semi-table-tbody>.semi-table-row>.semi-table-row-cell-ellipsis {
overflow: inherit;
text-overflow: inherit;
white-space: normal;
}
// 继承高度
.semi-spin {
position: static;
width: 100%;
height: 100%;
}
.semi-spin-children {
height: 100%;
}
.semi-table-small {
height: 100%;
}
.semi-table-container {
height: 100%;
}
}
}
.reset-confirm-modal {
:global {
.semi-modal-title {
color: #1C1D23;
}
.semi-modal-body {
color: #1C1D23;
}
.semi-modal-footer .semi-button {
margin-left: 16px;
}
.semi-button-tertiary {
color: #1C1D23
}
}
}
.last-column-min-width {
min-width: 120px;
}
.last-column-date {
min-width: 160px;
}
.last-column-text {
min-width: 300px;
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import classNames from 'classnames';
import { IllustrationConstruction } from '@douyinfe/semi-illustrations';
import { type DatabaseInfo } from '@coze-studio/bot-detail-store';
import { DataNamespace, dataReporter } from '@coze-data/reporter';
import {
TableView,
type TableViewRecord,
colWidthCacheService,
} from '@coze-common/table-view';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { type ColumnProps } from '@coze-arch/bot-semi/Table';
import { UIEmpty } from '@coze-arch/bot-semi';
import { CustomError } from '@coze-arch/bot-error';
import {
type SearchBotTableInfoResponse,
TableType,
} from '@coze-arch/bot-api/memory';
import { MemoryApi } from '@coze-arch/bot-api';
import { MAX_WIDTH, getColumns } from './get-columns';
import s from './index.module.less';
export interface DatabaseTable {
database: DatabaseInfo;
botID?: string;
workflowID?: string;
projectID?: string;
}
export interface DataTableRef {
refetch?: () => Promise<void>;
}
export const DataTable = forwardRef<DataTableRef, DatabaseTable>(
(props, ref) => {
const { database, botID, workflowID, projectID } = props;
const { tableId } = database;
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Record<string, unknown>[]>([]);
colWidthCacheService.initWidthMap();
const columns: { list: ColumnProps[]; width: number } = useMemo(
() => getColumns(database.tableMemoryList, tableId),
[database.tableMemoryList],
);
const fetchTableData = async () => {
setLoading(true);
let resp: SearchBotTableInfoResponse | undefined;
try {
resp = await MemoryApi.ListDatabaseRecords({
project_id: projectID,
workflow_id: workflowID,
bot_id: botID,
database_id: tableId,
offset: 0,
limit: 0,
table_type: TableType.DraftTable,
});
} catch (error) {
dataReporter.errorEvent(DataNamespace.DATABASE, {
eventName: REPORT_EVENTS.DatabaseQueryTable,
error:
error instanceof Error
? error
: new CustomError(
REPORT_EVENTS.DatabaseQueryTable,
`${REPORT_EVENTS.DatabaseQueryTable}: operation fail`,
),
});
}
if (resp?.data) {
setData(resp?.data || []);
}
setLoading(false);
};
useEffect(() => {
fetchTableData();
}, []);
useImperativeHandle(ref, () => ({
refetch: fetchTableData,
}));
const handleResize = col => {
const resizeList = columns.list.filter(
item => item.dataIndex !== col.dataIndex,
);
// 计算拖拽列能拖拽的最小宽度,小于最小宽度则返回最小宽度
const widthCount = resizeList.reduce(
(prev, cur) => Number(prev) + Number(cur.width),
0,
);
const minWidth = MAX_WIDTH - widthCount;
if (widthCount + col.width < MAX_WIDTH) {
return {
...col,
width: col.width < minWidth ? minWidth : col.width,
};
}
return col;
};
if (!data?.length && !loading) {
return (
<UIEmpty
className={classNames([s['empty-wrapper-database'], 'pb-0'])}
empty={{
icon: <IllustrationConstruction />,
title: I18n.t('timecapsule_0108_003'),
}}
></UIEmpty>
);
}
return (
<TableView
tableKey={tableId}
columns={columns.list}
dataSource={data as TableViewRecord[]}
loading={loading}
className={s['data-table']}
resizable
onResize={handleResize}
/>
);
},
);

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 DatabaseInfo } from '@coze-studio/bot-detail-store';
import { DataNamespace, dataReporter } from '@coze-data/reporter';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { Button, useUIModal } from '@coze-arch/bot-semi';
import { IconWarningSize24 } from '@coze-arch/bot-icons';
import { TableType } from '@coze-arch/bot-api/memory';
import { MemoryApi } from '@coze-arch/bot-api';
import s from './index.module.less';
export interface DatabaseTable {
database: DatabaseInfo;
botID?: string;
workflowID?: string;
projectID?: string;
afterReset?: () => Promise<void> | void;
}
const ResetBtn: React.FC<DatabaseTable> = props => {
const { database, botID, workflowID, afterReset, projectID } = props;
const { tableId, name } = database;
const {
open,
close,
modal: clearModal,
} = useUIModal({
type: 'info',
title: I18n.t('dialog_240305_01'),
content: I18n.t('dialog_240305_02'),
okButtonProps: {
type: 'warning',
},
icon: <IconWarningSize24 />,
onOk: async () => {
try {
await MemoryApi.ResetBotTable({
...(workflowID ? { workflow_id: workflowID } : {}),
...(botID ? { bot_id: botID } : {}),
...(projectID ? { project_id: projectID } : {}),
table_id: tableId,
table_type: TableType.DraftTable,
database_info_id: tableId,
});
} catch (error) {
dataReporter.errorEvent(DataNamespace.DATABASE, {
error: error as Error,
eventName: REPORT_EVENTS.DatabaseResetTableRecords,
});
return;
}
close();
afterReset?.();
},
onCancel: () => {
close();
},
className: s['reset-confirm-modal'],
// ToolPane的 z-index 是1000所以此处需要加 1001 的z-index避免被 database 数据面板遮住
zIndex: 1001,
});
return (
<>
<Button
type="tertiary"
onClick={() => {
sendTeaEvent(EVENT_NAMES.memory_click_front, {
bot_id: botID ?? '',
resource_type: 'database',
resource_id: tableId,
resource_name: name,
action: 'reset',
source: 'bot_detail_page',
source_detail: 'memory_preview',
});
open();
}}
className={s['button-reset']}
>
{I18n.t('database_240227_01')}
</Button>
{clearModal(<>{I18n.t('dialog_240305_02')}</>)}
</>
);
};
export default ResetBtn;

View File

@@ -0,0 +1,17 @@
/* stylelint-disable declaration-no-important */
.action-button {
color: rgba(29, 28, 35, 60%) !important
}
.rename-form{
:global{
// 不需要 Form 默认 的padding 间距
.semi-form-field{
padding: 0;
}
// 不需要 Form 默认的校验 icon
.semi-form-field-validate-status-icon{
display: none;
}
}
}

View File

@@ -0,0 +1,278 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { useRef, type FC } from 'react';
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
import { DataNamespace, dataReporter } from '@coze-data/reporter';
import {
GrabElementType,
PublicEventNames,
publicEventCenter,
} from '@coze-common/chat-area-plugin-message-grab';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { type SpaceProps } from '@coze-arch/bot-semi/Space';
import {
UIIconButton,
UIToast,
Space,
Tooltip,
UIModal,
Form,
UIFormTextArea,
UIDropdown,
UIDropdownMenu,
UIDropdownItem,
} from '@coze-arch/bot-semi';
import {
IconCopy,
IconMore,
IconQuotation,
IconWaringRed,
} from '@coze-arch/bot-icons';
import { type FileVO } from '@coze-arch/bot-api/filebox';
import { fileboxApi } from '@coze-arch/bot-api';
import { FileBoxListType, type UseBotStore } from '../types';
import wrapperStyle from '../index.module.less';
import { type Result } from '../hooks/use-file-list';
import { COZE_CONNECTOR_ID } from '../const';
import s from './index.module.less';
export interface ActionButtonsProps {
botId: string;
record: FileVO;
type: FileBoxListType;
reloadAsync: () => Promise<Result>;
setIsFrozenCurrentHoverCardId?: (v: boolean) => void;
spaceProps?: SpaceProps;
useBotStore?: UseBotStore;
isStore?: boolean;
onCancel?: () => void;
}
export const ActionButtons: FC<ActionButtonsProps> = props => {
const {
record,
reloadAsync,
setIsFrozenCurrentHoverCardId,
spaceProps = {},
botId,
isStore = false,
useBotStore,
onCancel,
} = props;
const {
FileName: name = '',
Uri: uri = '',
MainURL: url = '',
Type: type,
} = record;
const grabPluginIdForDebug = usePageRuntimeStore(state => state.grabPluginId);
const grabPluginIdForStore = useBotStore?.(state => state.grabPluginId) || '';
const grabPluginId = isStore ? grabPluginIdForStore : grabPluginIdForDebug;
const isImage = type === FileBoxListType.Image;
const renameRef = useRef<HTMLTextAreaElement>(null);
const handleCopy = async (value: string) => {
await navigator.clipboard.writeText(value);
UIToast.success(I18n.t(isImage ? 'filebox_0008' : 'filebox_0023'));
};
const handleDelete = () => {
UIModal.error({
title: I18n.t(isImage ? 'filebox_0013' : 'filebox_0022'),
className: wrapperStyle['confirm-modal'],
okButtonProps: {
theme: 'solid',
type: 'danger',
},
okText: I18n.t('Delete'),
onOk: async () => {
try {
await fileboxApi.PublicBatchDeleteFiles({
uris: [uri],
detail_page_id: '',
bot_id: botId,
connector_id: COZE_CONNECTOR_ID,
});
UIToast.success(I18n.t(isImage ? 'filebox_0016' : 'filebox_0024'));
reloadAsync();
} catch (error) {
dataReporter.errorEvent(DataNamespace.FILEBOX, {
error: error as Error,
eventName: REPORT_EVENTS.FileBoxDeleteFile,
});
}
},
icon: <IconWaringRed />,
});
};
const handleRename = () => {
const modal = UIModal.info({
title: I18n.t('chatflow_agent_menu_rename'),
className: wrapperStyle['confirm-modal'],
content: (
<Form<{ renamedValue: string }>
initValues={{
renamedValue: name,
}}
className={s['rename-form']}
>
<UIFormTextArea
field="renamedValue"
validate={(v: string) => {
if (!v) {
return I18n.t('file_name_cannot_be_empty');
}
}}
noLabel
ref={renameRef}
maxCount={100}
maxLength={100}
rows={3}
onChange={(v: string) => {
modal.update({
okButtonProps: {
disabled: !v,
},
});
}}
/>
</Form>
),
okButtonProps: {
theme: 'solid',
type: 'primary',
},
okText: I18n.t('Confirm'),
onOk: async () => {
try {
await fileboxApi.PublicUpdateFile({
update_items: {
file_name: renameRef.current?.value,
uri,
},
detail_page_id: '',
bot_id: botId,
connector_id: COZE_CONNECTOR_ID,
});
UIToast.success(I18n.t('Update_success'));
reloadAsync();
} catch (error) {
dataReporter.errorEvent(DataNamespace.FILEBOX, {
error: error as Error,
eventName: REPORT_EVENTS.FileBoxUpdateFile,
});
}
},
icon: null,
});
};
const handleQuote = () => {
publicEventCenter.emit(PublicEventNames.UpdateQuote, {
grabPluginId,
quote: [
{
type: isImage ? GrabElementType.IMAGE : GrabElementType.LINK,
...(isImage
? {
src: url,
}
: { url }),
children: [
{
text: name,
},
],
},
],
});
onCancel?.();
};
return (
<Space spacing={0} {...spaceProps}>
<Tooltip content={I18n.t('ask_quote')}>
<UIIconButton
icon={<IconQuotation />}
className={s['action-button']}
onClick={e => {
e.stopPropagation();
handleQuote();
}}
/>
</Tooltip>
<Tooltip content={I18n.t('filebox_0007')}>
<UIIconButton
icon={<IconCopy />}
className={s['action-button']}
onClick={e => {
e.stopPropagation();
handleCopy(name);
}}
/>
</Tooltip>
<UIDropdown
render={
<UIDropdownMenu>
<UIDropdownItem
onClick={e => {
e.stopPropagation();
handleRename();
}}
>
{I18n.t('filebox_0010')}
</UIDropdownItem>
<UIDropdownItem
onClick={e => {
e.stopPropagation();
handleDelete();
}}
>
{I18n.t('Delete')}
</UIDropdownItem>
</UIDropdownMenu>
}
onVisibleChange={v => {
if (v) {
setIsFrozenCurrentHoverCardId?.(true);
} else {
setIsFrozenCurrentHoverCardId?.(false);
}
}}
>
<UIIconButton
icon={<IconMore />}
className={s['action-button']}
onClick={e => {
e.stopPropagation();
}}
/>
</UIDropdown>
</Space>
);
};

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Coze的渠道id在某些场景下需要写死传递给后端
export const COZE_CONNECTOR_ID = '10000010';

View File

@@ -0,0 +1,47 @@
/* stylelint-disable max-nesting-depth */
/* stylelint-disable declaration-no-important */
/* stylelint-disable no-descending-specificity */
/* stylelint-disable selector-class-pattern */
.table {
:global {
// 去除顶部 margin 防止 header 出现意外的滚动
.semi-table-wrapper {
margin-top: 0 !important;
}
// 去除 table 自身的滚动,使用外层容器的滚动加载,配合 useInfiniteScroll 使用
.semi-table-body {
overflow: visible !important;
max-height: none !important;
}
}
}
// 文档名称列
.column-document-name {
display: flex;
gap: 8px;
align-items: center;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: rgba(29, 28, 35, 100%)
}
// 文件大小列
.column-document-size {
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: rgba(29, 28, 35, 60%)
}
// 上传时间列
.column-document-update-time {
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: rgba(29, 28, 35, 60%)
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import dayjs from 'dayjs';
import { getTypeIcon } from '@coze-data/knowledge-resource-processor-base';
import { I18n } from '@coze-arch/i18n';
import { Typography, UITable } from '@coze-arch/bot-semi';
import { type FileVO } from '@coze-arch/bot-api/filebox';
import { type ColumnProps } from '@coze-arch/coze-design';
import { type FileBoxListProps, FileBoxListType } from '../types';
import { type Result } from '../hooks/use-file-list';
import { formatSize } from '../helpers/format-size';
import { ActionButtons } from '../action-buttons';
import s from './index.module.less';
export interface DocumentListProps extends FileBoxListProps {
botId: string;
documents: FileVO[];
reloadAsync: () => Promise<Result>;
}
export const DocumentList: FC<DocumentListProps> = props => {
const { documents, reloadAsync, botId, useBotStore, isStore, onCancel } =
props;
const columns: ColumnProps<FileVO>[] = [
{
title: I18n.t('filebox_0018'),
dataIndex: 'name',
render: (_, record) => {
const { Format: format, MainURL: url, FileName: name } = record;
return (
<div className={s['column-document-name']}>
{getTypeIcon({
type: format,
url,
inModal: true,
})}
<Typography.Text
ellipsis={{
showTooltip: true,
}}
>
{name || I18n.t('filebox_0047')}
</Typography.Text>
</div>
);
},
},
{
title: I18n.t('datasets_unit_upload_field_size'),
dataIndex: 'FileSize',
render: text => (
<div className={s['column-document-size']}>
{formatSize(Number(text))}
</div>
),
},
{
title: I18n.t('filebox_0020'),
dataIndex: 'UpdateTime',
render: text => (
<div className={s['column-document-update-time']}>
{dayjs.unix(Number(text)).format('YYYY-MM-DD HH:mm')}
</div>
),
},
{
title: I18n.t('Actions'),
dataIndex: 'action',
width: 120,
render: (_, record) => (
<ActionButtons
record={record}
reloadAsync={reloadAsync}
type={FileBoxListType.Document}
spaceProps={{
spacing: 8,
}}
botId={botId}
useBotStore={useBotStore}
isStore={isStore}
onCancel={onCancel}
/>
),
},
];
return (
<UITable
tableProps={{
dataSource: documents,
sticky: true,
columns,
rowKey: 'id',
onRow: (record, index) => ({
onClick: () => {
window.open(record.MainURL);
},
}),
className: s.table,
}}
/>
);
};

View File

@@ -0,0 +1,17 @@
.filter {
cursor: pointer;
display: flex;
gap: 12px;
align-items: center;
padding: 6px 0;
font-weight: 600;
line-height: 20px;
color: rgba(29, 28, 35, 60%);
}
.filter-item-active {
color: rgba(77, 83, 232, 100%);
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Divider } from '@coze-arch/bot-semi';
import { FileBoxListType } from '../types';
import { useFileBoxListStore } from '../store';
import s from './index.module.less';
export const FileBoxFilter: FC = () => {
const fileListType = useFileBoxListStore(state => state.fileListType);
const setFileListType = useFileBoxListStore(state => state.setFileListType);
return (
<div className={s.filter}>
<div
className={classNames({
[s['filter-item-active']]: fileListType === FileBoxListType.Image,
})}
onClick={() => setFileListType(FileBoxListType.Image)}
>
{I18n.t('filebox_0002')}
</div>
<Divider layout="vertical" />
<div
className={classNames({
[s['filter-item-active']]: fileListType === FileBoxListType.Document,
})}
onClick={() => setFileListType(FileBoxListType.Document)}
>
{I18n.t('filebox_0003')}
</div>
</div>
);
};

View File

@@ -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.
*/
export enum Size {
B = 'B',
KB = 'KB',
MB = 'MB',
GB = 'GB',
}
const sizeB = 1024;
const sizeKB = 1024 * sizeB;
const sizeMB = 1024 * sizeKB;
const sizeGB = 1024 * sizeMB;
export const formatFixed = (v: number) => v.toFixed(2);
export const formatSize = (v: number): string => {
if (v > 0 && v < sizeB) {
return `${formatFixed(v)}${Size.B}`;
} else if (v < sizeKB) {
return `${formatFixed(v / sizeB)}${Size.KB}`;
} else if (v < sizeMB) {
return `${formatFixed(v / sizeKB)}${Size.MB}`;
} else if (v < sizeGB) {
return `${formatFixed(v / sizeMB)}${Size.MB}`;
}
return `${formatFixed(v / sizeMB)}${Size.MB}`;
};

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const prefixUri = (uri: string, url: string) => {
const [filePrefix] = uri.split('/');
const urlArray = url.split('/');
const filePrefixIndex = urlArray.findIndex(text => text === filePrefix);
const tosRegion = urlArray[filePrefixIndex - 1];
const processedUri = `${tosRegion}/${uri}`;
return processedUri;
};

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type InfiniteScrollOptions } from 'ahooks/lib/useInfiniteScroll/types';
import { useInfiniteScroll } from 'ahooks';
import { DataNamespace, dataReporter } from '@coze-data/reporter';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { type FileVO } from '@coze-arch/bot-api/filebox';
import { fileboxApi } from '@coze-arch/bot-api';
import { type FileBoxListType } from '../types';
import { COZE_CONNECTOR_ID } from '../const';
export interface UseFileListParams {
botId: string;
searchValue?: string;
type: FileBoxListType;
}
export interface Result {
list: FileVO[];
total: number;
}
const PAGE_SIZE = 20;
export const useFileList = (
params: UseFileListParams,
options: InfiniteScrollOptions<Result>,
) => {
const { botId, searchValue, type } = params;
const fetchFileList = async (
page: number,
pageSize: number,
): Promise<Result> => {
let result: Result = {
list: [],
total: 0,
};
try {
const res = await fileboxApi.FileList({
// 前端从 1 开始计数,方便 Math.ceil 计算,传给后端时手动减 1
page_num: page - 1,
page_size: pageSize,
bid: botId,
file_name: searchValue,
file_type: type,
connector_id: COZE_CONNECTOR_ID,
});
result = {
list: res.list || [],
total: res.total_count || 0,
};
} catch (error) {
dataReporter.errorEvent(DataNamespace.FILEBOX, {
eventName: REPORT_EVENTS.FileBoxListFile,
error: error as Error,
});
}
return result;
};
return useInfiniteScroll<Result>(
async d => {
const p = d ? Math.ceil(d.list.length / PAGE_SIZE) + 1 : 1;
return fetchFileList(p, PAGE_SIZE);
},
{
manual: true,
...options,
},
);
};

View File

@@ -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 { DataNamespace, dataReporter } from '@coze-data/reporter';
import { type UnitItem } from '@coze-data/knowledge-resource-processor-core';
import {
transformUnitList,
getFileExtension,
getBase64,
} from '@coze-data/knowledge-resource-processor-base';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { FileBizType } from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
export const useRetry = (params: {
unitList: UnitItem[];
setUnitList: (unitList: UnitItem[]) => void;
}) => {
const { unitList, setUnitList } = params;
const onRetry = async (record: UnitItem, index: number) => {
try {
const { fileInstance } = record;
if (fileInstance) {
const { name } = 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,
});
setUnitList(
transformUnitList({
unitList,
data: result?.data,
fileInstance,
index,
}),
);
}
} catch (e) {
const error = e as Error;
dataReporter.errorEvent(DataNamespace.KNOWLEDGE, {
eventName: REPORT_EVENTS.KnowledgeUploadFile,
error,
});
}
};
return onRetry;
};

View File

@@ -0,0 +1,218 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { useEffect, useState } from 'react';
import { dataReporter, DataNamespace } from '@coze-data/reporter';
import {
type UnitItem,
UnitType,
UploadStatus,
} from '@coze-data/knowledge-resource-processor-core';
import {
UploadUnitFile,
UploadUnitTable,
} from '@coze-data/knowledge-resource-processor-base';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { UIModal, UIToast } from '@coze-arch/bot-semi';
import { fileboxApi } from '@coze-arch/bot-api';
import { FileBoxListType } from '../types';
import s from '../index.module.less';
import { prefixUri } from '../helpers/prefix-uri';
import { COZE_CONNECTOR_ID } from '../const';
import { useRetry } from './use-retry';
import { type Result } from './use-file-list';
export interface UseUploadModalParams {
botId: string;
fileListType: FileBoxListType;
reloadAsync: () => Promise<Result>;
}
export const useUploadModal = (params: UseUploadModalParams) => {
const { botId, fileListType, reloadAsync } = params;
const [visible, setVisible] = useState(false);
const [unitList, setUnitList] = useState<UnitItem[]>([]);
const hideUploadFile = false;
const AddUnitMaxLimit = 10;
const onRetry = useRetry({ unitList, setUnitList });
const submitButtonDisabled =
unitList.length === 0 ||
unitList.some(
i =>
/**
* 1. 未上传成功的
* 2. 校验失败的
* 3. 名字为空的(名字为空暂不影响 validateMessage所以需要单独判断
*/
i.status !== UploadStatus.SUCCESS || i.validateMessage || !i.name,
);
const handleUnitListUpdate = (data: UnitItem[]) => {
// 防止重命名后再上传被覆盖
const newData = data.map(i => {
let resultName = i.name;
unitList.forEach(u => {
if (
u.uid === i.uid &&
u.status === i.status &&
u.status === UploadStatus.SUCCESS
) {
resultName = u.name;
}
});
return {
...i,
name: resultName,
};
});
setUnitList(newData);
};
const handleUploadSubmit = async () => {
try {
const {
DestFiles = [],
SuccessNum,
FailNum,
} = await fileboxApi.UploadFiles({
source_files: unitList.map(i => {
const { uri, url, name } = i;
return {
file_uri: prefixUri(uri, url),
file_name: name,
};
}),
bid: botId,
cid: COZE_CONNECTOR_ID,
biz_type:
fileListType === FileBoxListType.Image ? 'coze-img' : 'coze-file',
});
const failedDestFiles = DestFiles.filter(i => i.status !== 0).map(i => ({
...i,
errorMessage:
i.status === 708252039
? I18n.t('file_name_exist')
: I18n.t('Upload_failed'),
}));
UIToast.success(
I18n.t('upload_success_failed_count', {
successNum: SuccessNum,
failedNum: FailNum,
}),
);
if (failedDestFiles.length === 0) {
await reloadAsync();
setVisible(false);
} else {
const newUnitList = failedDestFiles.map(i => {
const unit = unitList.find(
u => prefixUri(u.uri, u.url) === i.file_uri,
);
return {
...unit,
dynamicErrorMessage: i.errorMessage,
};
});
setUnitList(newUnitList as UnitItem[]);
}
} catch (error) {
dataReporter.errorEvent(DataNamespace.FILEBOX, {
error: error as Error,
eventName: REPORT_EVENTS.FileBoxUploadFile,
});
}
};
// reset
useEffect(() => {
if (!visible) {
setUnitList([]);
}
}, [visible]);
return {
open: () => setVisible(true),
close: () => setVisible(false),
node: (
<UIModal
visible={visible}
onCancel={() => setVisible(false)}
title={I18n.t('datasets_createFileModel_step2')}
width={792}
onOk={handleUploadSubmit}
keepDOM={false}
okButtonProps={{
disabled: submitButtonDisabled,
}}
className={s['upload-modal']}
>
<UploadUnitFile
action=""
maxSizeMB={20}
accept={
fileListType === FileBoxListType.Image
? '.png,.jpg,.jpeg'
: '.pdf,.txt,.doc,.docx'
}
dragMainText={I18n.t(
fileListType === FileBoxListType.Image
? 'knowledge_photo_004'
: 'datasets_createFileModel_step2_UploadDoc',
)}
dragSubText={
fileListType === FileBoxListType.Image
? I18n.t('knowledge_photo_005')
: I18n.t('datasets_createFileModel_step2_UploadDoc_description', {
fileFormat: 'PDF、TXT、DOC、DOCX',
maxDocNum: 300,
filesize: '20MB',
pdfPageNum: 250,
})
}
limit={AddUnitMaxLimit}
unitList={unitList}
multiple={AddUnitMaxLimit > 1}
style={
hideUploadFile ? { visibility: 'hidden', height: 0 } : undefined
}
setUnitList={handleUnitListUpdate}
onFinish={handleUnitListUpdate}
/>
{unitList.length > 0 ? (
<div className="overflow-y-auto my-[25px]">
<UploadUnitTable
type={UnitType.IMAGE_FILE}
edit={true}
unitList={unitList}
onChange={setUnitList}
onRetry={onRetry}
inModal
/>
</div>
) : null}
</UIModal>
),
};
};

View File

@@ -0,0 +1,67 @@
.card-group {
margin-bottom: 20px;
}
// Card 整体
.card {
cursor: auto;
width: 209px;
height: 214px;
border-radius: 8px;
&:hover {
box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 8%);
}
:global {
// 固定高度142超出高度的图片截取居中部分展示
.semi-card-cover {
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
height: 142px;
}
}
}
// Card 封面
.card-cover {
cursor: pointer;
border-radius: 8px 8px 0 0;
// 设置最小高度142保证填满封面
img {
min-height: 142px;
}
}
// Card 内容区 (title + description)
.card-content {
display: flex;
flex-direction: column;
gap: 4px;
.photo-name {
font-weight: 600;
line-height: 20px;
color: rgba(29, 28, 35, 100%);
}
}
// Card 底部栏(时间 + 操作按钮)
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
height: 24px;
.create-time {
font-size: 12px;
line-height: 16px;
color: rgba(28, 29, 35, 35%)
}
}

View File

@@ -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 { type FC, useState } from 'react';
import dayjs from 'dayjs';
import { I18n } from '@coze-arch/i18n';
import { Card, CardGroup, Typography, Image } from '@coze-arch/bot-semi';
import { type FileVO } from '@coze-arch/bot-api/filebox';
import { type FileBoxListProps, FileBoxListType } from '../types';
import { type Result } from '../hooks/use-file-list';
import { ActionButtons } from '../action-buttons';
import s from './index.module.less';
export interface ImageListProps extends FileBoxListProps {
images: FileVO[];
reloadAsync: () => Promise<Result>;
}
export const ImageList: FC<ImageListProps> = props => {
const { images, reloadAsync, botId, useBotStore, isStore, onCancel } = props;
const [currentHoverCardId, setCurrentHoverCardId] = useState<string>('');
const [isFrozenCurrentHoverCardId, setIsFrozenCurrentHoverCardId] =
useState<boolean>(false);
return (
<CardGroup spacing={12} className={s['card-group']}>
{images?.map(i => {
const {
// MainURL 加载太慢了,列表中使用 ThumbnailURL 进行缩略图展示
ThumbnailURL: url,
MainURL: previewUrl,
FileID: id,
FileName: name,
UpdateTime: updateTime,
} = i || {};
const isHover = currentHoverCardId === id;
const onMouseEnter = () => {
setCurrentHoverCardId(id || '');
};
const onMouseLeave = () => {
if (isFrozenCurrentHoverCardId) {
return;
}
setCurrentHoverCardId('');
};
return (
<Card
key={id}
cover={
<Image
src={url}
// 仅设置宽度,高度会按图片原比例自动缩放
width={209}
className={s['card-cover']}
preview={{
src: previewUrl,
}}
/>
}
headerLine={false}
bodyStyle={{
padding: '12px',
}}
className={s.card}
>
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={s['card-content']}
>
<Typography.Text
className={s['photo-name']}
ellipsis={{
showTooltip: true,
}}
>
{name || I18n.t('filebox_0047')}
</Typography.Text>
<div className={s['card-footer']}>
<Typography.Text className={s['create-time']}>
{dayjs.unix(Number(updateTime)).format('YYYY-MM-DD HH:mm')}
</Typography.Text>
{isHover ? (
<ActionButtons
record={i}
reloadAsync={reloadAsync}
type={FileBoxListType.Document}
setIsFrozenCurrentHoverCardId={
setIsFrozenCurrentHoverCardId
}
botId={botId}
useBotStore={useBotStore}
isStore={isStore}
onCancel={onCancel}
/>
) : null}
</div>
</div>
</Card>
);
})}
</CardGroup>
);
};

View File

@@ -0,0 +1,101 @@
/* stylelint-disable selector-class-pattern */
/* stylelint-disable max-nesting-depth */
/* stylelint-disable declaration-no-important */
.filebox-list {
width: 100%;
height: 100%;
// 移除全局样式
:global {
.semi-spin-block.semi-spin {
height: auto;
}
.semi-spin-children {
height: auto
}
}
}
.file-list {
overflow: auto;
display: flex;
flex-direction: column;
width: 100%;
height: calc(100% - 52px);
.file-list-spin {
flex: 1;
}
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.footer {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
.spin {
width: 100%;
height: 60px;
font-size: 14px;
}
}
// UI稿的 confirm modal 和 semi 默认的不一样,需要手动调整样式
.confirm-modal {
:global {
.semi-modal-header {
margin-bottom: 16px;
// icon 和 title 的间距
.semi-modal-icon-wrapper {
margin-right: 8px;
}
// title 颜色
.semi-modal-confirm-title-text {
color: rgba(29, 28, 35, 100%);
}
// 关闭 icon 的 hover 颜色
.semi-button:hover {
background-color: rgba(46, 46, 56, 8%);
}
}
.semi-modal-body {
margin: 0;
padding: 16px 0;
.semi-modal-confirm-content {
color: rgba(29, 28, 35, 100%)
}
}
.semi-modal-footer button {
min-width: 96px;
margin-left: 16px;
}
}
}
// UIModal 的背景颜色不符,需要调整
.upload-modal{
:global{
.semi-modal-content{
background: #f7f7fa !important
}
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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, type FC } from 'react';
import { debounce } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { Space, UIButton, UISearch, Spin, UIEmpty } from '@coze-arch/bot-semi';
import { IconSegmentEmpty } from '@coze-arch/bot-icons';
import { type FileBoxListProps, FileBoxListType } from './types';
import { useFileBoxListStore } from './store';
import { ImageList } from './image-list';
import { useUploadModal } from './hooks/use-upload-modal';
import { useFileList } from './hooks/use-file-list';
import { FileBoxFilter } from './filebox-filter';
import { DocumentList } from './document-list';
import s from './index.module.less';
export const FileBoxList: FC<FileBoxListProps> = props => {
const { botId } = props;
const searchValue = useFileBoxListStore(state => state.searchValue);
const setSearchValue = useFileBoxListStore(state => state.setSearchValue);
const fileListType = useFileBoxListStore(state => state.fileListType);
const ref = useRef<HTMLDivElement>(null);
const { data, loading, loadingMore, reloadAsync, noMore } = useFileList(
{
botId,
searchValue,
type: fileListType,
},
{
isNoMore: d => !!(d && d.list.length >= d.total),
target: ref,
},
);
// 手动控制 data 加载时机
useEffect(() => {
if (botId) {
reloadAsync();
// 重新加载时,回到最顶部
ref.current?.scrollTo?.({
top: 0,
behavior: 'smooth',
});
}
}, [searchValue, botId, fileListType]);
const items = data?.list || [];
const { open, node } = useUploadModal({ botId, fileListType, reloadAsync });
const debounceSearch = debounce((v: string) => {
setSearchValue(v);
}, 300);
const isImage = fileListType === FileBoxListType.Image;
const getEmptyTitle = () => {
if (searchValue) {
return I18n.t(isImage ? 'filebox_010' : 'filebox_011');
}
return I18n.t(isImage ? 'filebox_0017' : 'filebox_0025');
};
return (
<div className={s['filebox-list']}>
<div className={s.header}>
{/* 切换图片/文档 */}
<FileBoxFilter />
<Space spacing={12}>
{/* 搜索框 */}
<UISearch
placeholder={I18n.t(
'card_builder_dataEditor_get_errormsg_please_enter',
)}
onChange={debounceSearch}
/>
{/* 上传按钮 */}
<UIButton type="primary" theme="solid" onClick={open}>
{I18n.t('datasets_createFileModel_step2')}
</UIButton>
</Space>
</div>
<div className={s['file-list']} ref={ref}>
<Spin
spinning={loading}
wrapperClassName={s['file-list-spin']}
childStyle={{
height: '100%',
width: '100%',
// 防止切换 fileListType 时 items 数量不一致,导致 loading 闪烁
display: loading ? 'none' : 'block',
}}
>
{items.length <= 0 ? (
<UIEmpty
empty={{
icon: <IconSegmentEmpty />,
title: getEmptyTitle(),
}}
/>
) : isImage ? (
<ImageList images={items} reloadAsync={reloadAsync} {...props} />
) : (
<DocumentList
documents={items}
reloadAsync={reloadAsync}
{...props}
/>
)}
</Spin>
<div className={s.footer}>
{!noMore && (
<Spin
spinning={loadingMore}
tip={I18n.t('loading')}
wrapperClassName={s.spin}
/>
)}
</div>
</div>
{node}
</div>
);
};

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { devtools } from 'zustand/middleware';
import { create } from 'zustand';
import { FileBoxListType } from './types';
interface FileBoxListState {
fileListType: FileBoxListType;
searchValue: string;
}
interface FileBoxListAction {
setFileListType: (v: FileBoxListType) => void;
setSearchValue: (v: string) => void;
}
export const useFileBoxListStore = create<
FileBoxListState & FileBoxListAction
>()(
devtools((set, get) => ({
fileListType: FileBoxListType.Image,
searchValue: '',
setFileListType: (v: FileBoxListType) => {
set({ fileListType: v });
},
setSearchValue: (v: string) => {
set({ searchValue: v });
},
})),
);

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type StoreApi, type UseBoundStore } from 'zustand';
export enum FileBoxListType {
Image = 1,
Document = 2,
}
export type UseBotStore = UseBoundStore<
StoreApi<{
grabPluginId: string;
}>
>;
export interface FileBoxListProps {
botId: string;
useBotStore?: UseBotStore;
isStore?: boolean;
onCancel?: () => void;
}

View File

@@ -0,0 +1,28 @@
/* stylelint-disable max-nesting-depth */
/* stylelint-disable declaration-no-important */
.memory-debug-dropdown {
min-width: 120px;
padding: 4px;
}
.memory-debug-dropdown-item {
height: 32px !important;
padding: 8px !important;
line-height: 20px;
color: var(--coz-fg-primary);
border-radius: 4px;
&:not(.semi-dropdown-item-active):hover{
background-color:var(--coz-mg-secondary-hovered) !important
}
:global {
.semi-dropdown-item-icon {
font-size: 16px;
color: var(--coz-fg-secondary);
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import { BotE2e } from '@coze-data/e2e';
import { UIDropdownItem, UIDropdownMenu } from '@coze-arch/bot-semi';
import {
type MemoryModule,
type MemoryDebugDropdownMenuItem,
} from '../../types';
import { useSendTeaEventForMemoryDebug } from '../../hooks/use-send-tea-event-for-memory-debug';
import styles from './index.module.less';
export interface MemoryDebugDropdownProps {
menuList: MemoryDebugDropdownMenuItem[];
onClickItem: (memoryModule: MemoryModule) => void;
isStore?: boolean;
}
export const MemoryDebugDropdown: FC<MemoryDebugDropdownProps> = props => {
const { menuList, isStore = false, onClickItem } = props;
const sendTeaEventForMemoryDebug = useSendTeaEventForMemoryDebug({ isStore });
const handleClickMenu = (memoryModule: MemoryModule) => {
sendTeaEventForMemoryDebug(memoryModule);
onClickItem(memoryModule);
};
return (
<UIDropdownMenu className={styles['memory-debug-dropdown']}>
{menuList?.map(item => (
<UIDropdownItem
data-dtestid={`${BotE2e.BotMemoryDebugDropdownItem}.${item.name}`}
icon={item.icon}
onClick={() => handleClickMenu(item.name)}
className={styles['memory-debug-dropdown-item']}
>
{item.label}
</UIDropdownItem>
))}
</UIDropdownMenu>
);
};

View File

@@ -0,0 +1,87 @@
/* stylelint-disable no-descending-specificity */
/* stylelint-disable declaration-no-important */
.tabs_memory {
height: 100%;
:global {
.semi-tabs-tab-line.semi-tabs-tab-left.semi-tabs-tab-active {
font-size: 14px;
font-weight: 600;
color: #1D1C23;
background: rgba(28, 28, 35, 5%);
border-left: none;
border-radius: 8px;
}
.semi-tabs-bar-left {
box-sizing: border-box;
width: 216px;
padding: 24px 12px 0;
background: #F0F0F5;
border-right: none;
}
.semi-tabs-bar-line.semi-tabs-bar-left .semi-tabs-tab {
border-left: none;
}
.semi-tabs-tab {
display: flex;
align-items: center;
height: 40px;
margin-bottom: 4px;
padding: 0 12px;
font-size: 14px;
font-weight: 400;
color: #1D1C23;
&:hover {
border-left: none;
border-radius: 8px;
}
}
.semi-tabs-content {
flex: 1;
padding: 12px 12px 0;
.semi-tabs-pane-active.semi-tabs-pane {
overflow: auto;
}
}
.semi-tabs-pane,
.semi-tabs-pane-motion-overlay {
height: 100%;
}
}
}
.memory-debug-modal {
:global {
.semi-modal-content {
padding: 0;
background-color: #F7F7FA !important;
.semi-modal-header {
margin: 0;
padding: 24px;
border-bottom: 1px solid rgba(29, 28, 35, 8%);
}
}
}
}
.memory-debug-modal-tabs-tab{
svg{
margin-right: 8px;
font-size: 16px;
vertical-align: text-top;
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type Attributes } from 'react';
import { BotE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { TabPane, Tabs, useUIModal } from '@coze-arch/bot-semi';
import {
type MemoryModule,
type MemoryDebugDropdownMenuItem,
} from '../../types';
import { useSendTeaEventForMemoryDebug } from '../../hooks/use-send-tea-event-for-memory-debug';
import styles from './index.module.less';
export interface MemoryDebugModalProps {
memoryModule: MemoryModule | undefined;
menuList: MemoryDebugDropdownMenuItem[];
isStore: boolean;
setMemoryModule: (type: MemoryModule) => void;
}
export const useMemoryDebugModal = ({
memoryModule,
menuList,
setMemoryModule,
isStore,
}: MemoryDebugModalProps) => {
const sendTeaEventForMemoryDebug = useSendTeaEventForMemoryDebug({ isStore });
const defaultModule = menuList[0]?.name;
const curMemoryModule = memoryModule || defaultModule;
const { modal, open, close } = useUIModal({
type: 'info',
width: 1138,
height: 665,
className: styles['memory-debug-modal'],
bodyStyle: {
padding: 0,
},
title: I18n.t('database_memory_menu'),
centered: true,
footer: null,
onCancel: () => {
sendTeaEventForMemoryDebug(curMemoryModule, { action: 'turn_off' });
setMemoryModule(defaultModule);
close();
},
});
const onChange = (key: MemoryModule) => {
setMemoryModule(key);
sendTeaEventForMemoryDebug(key);
};
return {
node: modal(
<Tabs
className={styles.tabs_memory}
tabPosition="left"
activeKey={curMemoryModule}
onChange={onChange as (k: string) => void}
lazyRender
>
{menuList.map(item => (
<TabPane
itemKey={item.name}
key={item.name}
tab={
<span
data-dtestid={`${BotE2e.BotMemoryDebugModalTab}.${item.name}`}
className={styles['memory-debug-modal-tabs-tab']}
>
{item.icon}
{item.label}
</span>
}
>
{/* 给 children 传递 onCancel 参数,用于从内部关闭弹窗 */}
{React.isValidElement(item.component)
? React.cloneElement(item.component, {
onCancel: close,
} as unknown as Attributes)
: item.component}
</TabPane>
))}
</Tabs>,
),
open,
close,
};
};

View File

@@ -0,0 +1,118 @@
/* stylelint-disable declaration-no-important */
// @import '../../../../../../../assets/styles/common.less';
.variable-debug-container {
overflow-y: auto;
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
height: 100%;
.keyword {
flex-shrink: 0;
width: 140px;
margin-right: 16px;
}
.value {
flex: 1;
width: 0;
}
.update_time {
flex-shrink: 0;
width: 110px;
margin-left: 16px;
}
.modal-container-title {
display: flex;
align-items: center;
margin-bottom: 6px;
.keyword,
.value,
.update_time {
font-size: 12px;
line-height: 16px;
color: var(--Light-usage-text---color-text-0, #1D1C23);
}
}
.modal-container-row {
display: flex;
align-items: center;
&.system_row {
font-size: 12px;
color: var(--Light-usage-text---color-text-1, rgba(29, 28, 35, 80%));
:global {
.semi-typography {
flex: 1;
font-size: 12px;
color: var(--Light-usage-text---color-text-1, rgba(29, 28, 35, 80%));
}
}
}
&:not(:last-child) {
margin-bottom: 16px;
}
.keyword {
font-size: 14px;
font-weight: 600;
line-height: 22px;
}
.update_time {
font-size: 12px;
line-height: 16px;
color: var(--light-usage-text-color-text-2, rgb(29 28 35 / 60%));
text-overflow: ellipsis;
}
}
}
.operate-area {
flex-shrink: 0;
padding: 24px 0;
border-top: 1px solid var(--light-usage-border-color-border, rgb(29 28 35 / 8%));
}
.hover-tip {
max-width: 410px !important;
}
.variable-debug-footer {
display: flex;
justify-content: flex-end;
margin: 24px 0;
}
.debug-header-deal-button {
height: 26px !important;
margin-left: 8px !important;
padding: 4px !important;
border-radius: 4px !important;
&:hover {
background: var(--light-usage-fill-color-fill-0,
rgb(46 46 56 / 4%)) !important;
}
&:active {
background: var(--light-usage-fill-color-fill-1,
rgb(46 46 56 / 8%)) !important;
}
&.click {
background: var(--light-usage-fill-color-fill-2,
rgb(46 46 56 / 12%)) !important;
}
}

View File

@@ -0,0 +1,353 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import classNames from 'classnames';
import { useReactive } from 'ahooks';
import { IconAlertCircle } from '@douyinfe/semi-icons';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { dataReporter, DataNamespace } from '@coze-data/reporter';
import { BotE2e } from '@coze-data/e2e';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import {
Modal,
Spin,
Toast,
Tooltip,
Typography,
UIButton,
UIInput,
} from '@coze-arch/bot-semi';
import { CustomError } from '@coze-arch/bot-error';
import type { KVItem } from '@coze-arch/bot-api/memory';
import { MemoryApi } from '@coze-arch/bot-api';
import { formatDate } from '../../utils';
import s from './index.module.less';
const { Paragraph } = Typography;
/* eslint-disable */
const ProfileInput = ({
className,
value,
botId,
keyword,
onClear,
afterUpdate,
}: {
className?: string;
value?: string;
botId: string;
keyword: string;
onClear: () => void;
afterUpdate?: () => void;
}) => {
const [inputV, setInputV] = useState(value);
useEffect(() => setInputV(value), [value]);
const onUpdate = async () => {
try {
if (inputV === value) {
return;
}
const resp = (await MemoryApi.SetKvMemory({
bot_id: botId,
data: [{ keyword, value: inputV }],
})) as { code: number };
if (resp.code === 0) {
Toast.success({
content: I18n.t('Update_success'),
showClose: false,
});
afterUpdate?.();
} else {
Toast.warning({
content: I18n.t('Update_failed'),
showClose: false,
});
}
} catch (error) {
dataReporter.errorEvent(DataNamespace.VARIABLE, {
eventName: REPORT_EVENTS.VariableSetValue,
error:
error instanceof Error
? error
: new CustomError(
REPORT_EVENTS.VariableSetValue,
`${REPORT_EVENTS.VariableSetValue}: operation fail`,
),
meta: {
bot_id: botId,
},
});
}
};
return (
<div
className={className}
data-dtestid={`${BotE2e.BotVariableDebugModalValueInput}.${keyword}`}
>
<UIInput
showClear
value={inputV}
onChange={v => setInputV(v)}
onClear={onClear}
onBlur={onUpdate}
/>
</div>
);
};
export const VariableDebug = () => {
const botId = useBotInfoStore(store => store.botId);
const variables = useBotSkillStore(store => store.variables);
const [loading, setLoading] = useState(false);
const [showResetModal, setShowResetModal] = useState(false);
const $list = useReactive({
current: [] as (KVItem & { loading?: boolean })[],
});
const getKvList = async () => {
try {
setLoading(true);
const resp = await MemoryApi.GetPlayGroundMemory({
bot_id: botId,
});
if (resp?.memories) {
const data = variables.map(i => {
const item = resp.memories?.find(j => j.keyword === i.key) || {};
return {
...item,
keyword: i.key,
loading: false,
};
});
$list.current = data as KVItem[];
}
} catch (error) {
dataReporter.errorEvent(DataNamespace.VARIABLE, {
eventName: REPORT_EVENTS.VariableGetValue,
error:
error instanceof Error
? error
: new CustomError(
REPORT_EVENTS.VariableSetValue,
`${REPORT_EVENTS.VariableSetValue}: get list fail`,
),
meta: {
bot_id: botId,
},
});
} finally {
setLoading(false);
}
};
useEffect(() => {
getKvList();
}, []);
const onDelete = async (keyword?: string) => {
try {
const resp = (await MemoryApi.DelProfileMemory({
bot_id: botId,
keywords: keyword ? [keyword] : undefined,
})) as unknown as { code: number };
if (resp.code === 0) {
Toast.success({
content: I18n.t('variable_reset_succ_tips'),
showClose: false,
});
getKvList();
} else {
Toast.warning({
content: I18n.t('variable_reset_fail_tips'),
showClose: false,
});
}
} catch (error) {
dataReporter.errorEvent(DataNamespace.VARIABLE, {
eventName: REPORT_EVENTS.VariableDeleteValue,
error:
error instanceof Error
? error
: new CustomError(
REPORT_EVENTS.VariableSetValue,
`${REPORT_EVENTS.VariableSetValue}: operation fail`,
),
meta: {
bot_id: botId,
},
});
}
};
return (
<div className={s['variable-debug-container']}>
<Spin spinning={loading}>
<div className={s['modal-container-title']}>
<div
className={s.keyword}
data-testid={BotE2e.BotVariableDebugModalNameTitleText}
>
{I18n.t('variable_field_name')}
</div>
<div
className={s.value}
data-testid={BotE2e.BotVariableDebugModalValueTitleText}
>
{I18n.t('variable_field_value')}
</div>
<div
className={s.update_time}
data-testid={BotE2e.BotVariableDebugModalEditDateTitleText}
>
{I18n.t('variable_edit_time')}
</div>
</div>
{$list.current.map(i => {
if (!i.keyword) {
return null;
}
return (
<div
key={i.keyword}
className={classNames(s['modal-container-row'], {
[s.system_row]: i.is_system,
})}
>
<div
className={s.keyword}
data-dtestid={`${BotE2e.BotVariableDebugModalNameText}.${i.keyword}`}
>
<Paragraph
ellipsis={{
rows: 1,
showTooltip: {
opts: {
style: {
maxWidth: 234,
wordBreak: 'break-word',
},
},
},
}}
>
{i.keyword}
</Paragraph>
</div>
{/* 是否为系统字段 */}
{i.is_system ? (
<Paragraph
data-dtestid={`${BotE2e.BotVariableDebugModalValueInput}.${i.keyword}`}
ellipsis={{
rows: 1,
showTooltip: {
opts: {
style: {
maxWidth: 234,
wordBreak: 'break-word',
},
},
},
}}
>
{i.value}
</Paragraph>
) : (
<ProfileInput
className={s.value}
value={
i.value ||
variables?.find(item => item.key === i.keyword)
?.default_value
}
keyword={i.keyword || ''}
botId={botId}
onClear={async () => {
await onDelete(i.keyword || '');
}}
afterUpdate={getKvList}
/>
)}
<div
className={s.update_time}
data-dtestid={`${BotE2e.BotVariableDebugModalEditDateText}.${i.keyword}`}
>
{i.update_time
? formatDate(Number(i.update_time), 'YYYY-MM-DD HH:mm')
: ''}
</div>
</div>
);
})}
</Spin>
<div
className={s['variable-debug-footer']}
data-testid={BotE2e.BotVariableDebugModalResetBtn}
>
<Tooltip
className={s['hover-tip']}
showArrow
content={I18n.t('variable_reset_tips')}
>
<UIButton
type="tertiary"
onClick={() => {
sendTeaEvent(EVENT_NAMES.memory_click_front, {
bot_id: botId,
resource_type: 'variable',
action: 'reset',
source: 'bot_detail_page',
source_detail: 'memory_preview',
});
setShowResetModal(true);
}}
>
{I18n.t('variable_reset')}
</UIButton>
</Tooltip>
</div>
<Modal
zIndex={9999}
centered
okType="danger"
visible={showResetModal}
onCancel={() => {
setShowResetModal(false);
}}
title={I18n.t('variable_reset_confirm')}
okText={I18n.t('variable_reset_yes')}
cancelText={I18n.t('variable_reset_no')}
keepDOM={false}
maskClosable={false}
icon={
<IconAlertCircle size="extra-large" style={{ color: '#FF2710' }} />
}
onOk={async () => {
await onDelete();
setShowResetModal(false);
}}
>
{I18n.t('variable_reset_tips')}
</Modal>
</div>
);
};

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { useParams } from 'react-router-dom';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { sendTeaEvent, EVENT_NAMES } from '@coze-arch/bot-tea';
export const useSendTeaEventForMemoryDebug = (p: { isStore: boolean }) => {
const { isStore = false } = p;
// TODO@XML 看起来在商店也用到了,先不改
const params = useParams<DynamicParams>();
const { bot_id = '', product_id = '' } = params;
const resourceTypeMaps = {
longTimeMemory: 'long_term_memory',
database: 'database',
variable: 'variable',
filebox: 'filebox',
};
return (type: string, extraParams: Record<string, unknown> = {}) => {
sendTeaEvent(EVENT_NAMES.memory_click_front, {
bot_id: isStore ? product_id : bot_id,
product_id: isStore ? product_id : '',
resource_type: resourceTypeMaps[type || ''],
action: 'turn_on',
source: isStore ? 'store_detail_page' : 'bot_detail_page',
source_detail: 'memory_preview',
...extraParams,
});
};
};

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { useMemoryDebugModal } from './components/memory-debug-modal';
export { VariableDebug } from './components/variable-debug';
export { DatabaseDebug } from './components/database-debug';
// export { FileBoxList } from './components/filebox-list';
export { MemoryDebugDropdown } from './components/memory-debug-dropdown';
export { MemoryModule, MemoryDebugDropdownMenuItem } from './types';
export { useSendTeaEventForMemoryDebug } from './hooks/use-send-tea-event-for-memory-debug';
export { default as MultiDataTable } from './components/database-debug/multi-table';
export { type DataTableRef } from './components/database-debug/table';
export { type UseBotStore } from './components/filebox-list/types';

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
export enum MemoryModule {
Variable = 'variable',
Database = 'database',
LongTermMemory = 'longTermMemory',
Filebox = 'filebox',
}
export interface MemoryDebugDropdownMenuItem {
label: string;
name: MemoryModule;
icon: ReactNode;
component: ReactNode;
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module '*.less' {
const resource: { [key: string]: string };
export = resource;
}
declare const IS_BOE: boolean;
declare const IS_DEV_MODE: boolean;
declare const IS_OVERSEA: boolean;
declare const IS_OVERSEA_RELEASE: boolean;
declare const IS_PPE: boolean;
declare const IS_PROD: boolean;
declare const IS_RELEASE_VERSION: boolean;

View File

@@ -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.
*/
import dayjs from 'dayjs';
export const formatDate = (v: number, template = 'YYYY/MM/DD HH:mm:ss') =>
dayjs.unix(v).format(template);

View File

@@ -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 { Config } from 'tailwindcss';
export default {
content: ['./src/**/*.{ts,tsx}', '../../packages/**/src/**/*.{ts,tsx}'],
} satisfies Config;

View File

@@ -0,0 +1,82 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"types": [],
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"references": [
{
"path": "../../../arch/bot-api/tsconfig.build.json"
},
{
"path": "../../../arch/bot-error/tsconfig.build.json"
},
{
"path": "../../../arch/bot-flags/tsconfig.build.json"
},
{
"path": "../../../arch/bot-tea/tsconfig.build.json"
},
{
"path": "../../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../../arch/i18n/tsconfig.build.json"
},
{
"path": "../../../arch/report-events/tsconfig.build.json"
},
{
"path": "../../../common/chat-area/chat-area/tsconfig.build.json"
},
{
"path": "../../../common/chat-area/chat-core/tsconfig.build.json"
},
{
"path": "../../../common/chat-area/plugin-message-grab/tsconfig.build.json"
},
{
"path": "../../common/e2e/tsconfig.build.json"
},
{
"path": "../../common/reporter/tsconfig.build.json"
},
{
"path": "../../../components/bot-icons/tsconfig.build.json"
},
{
"path": "../../../components/bot-semi/tsconfig.build.json"
},
{
"path": "../../../components/table-view/tsconfig.build.json"
},
{
"path": "../../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../../knowledge/knowledge-resource-processor-base/tsconfig.build.json"
},
{
"path": "../../knowledge/knowledge-resource-processor-core/tsconfig.build.json"
},
{
"path": "../../../studio/stores/bot-detail/tsconfig.build.json"
},
{
"path": "../../../studio/user-store/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View File

@@ -0,0 +1,16 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "stories", "vitest.config.ts", "tailwind.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"types": ["vitest/globals"]
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'web',
test: {
environment: 'happy-dom',
},
});