feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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%)
|
||||
}
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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%);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}`;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -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%)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 });
|
||||
},
|
||||
})),
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user