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,247 @@
/* stylelint-disable block-no-empty */
/* stylelint-disable declaration-no-important */
/* stylelint-disable no-descending-specificity */
.modal-container {
min-width: 100%;
}
.modal-table-btn {
display: flex;
justify-content: flex-end;
}
.modal-temp {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.modal-temp-right {
position: relative;
display: flex;
flex-direction: column;
height: 409px;
margin-bottom: 24px;
padding: 15px 24px;
background: rgba(255, 255, 255, 100%);
border: 1px solid rgba(29, 28, 35, 8%);
border-radius: 12px;
.modal-temp-title {
font-size: 14px;
font-weight: 600;
line-height: 20px;
color: rgba(29, 28, 35, 100%);
}
.modal-temp-image {
margin-top: 16px;
margin-bottom: 12px;
border-radius: 8px;
}
.modal-temp-description {
height: 64px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
color: rgba(29, 28, 35, 80%);
}
.modal-temp-btn-group {
display: flex;
gap: 12px;
align-items: center;
justify-content: flex-end;
margin-top: 16px;
padding: 8px 0;
.modal-temp-btn {
width: 120px;
}
}
.modal-temp-preview {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 329px;
padding: 16px 21px 24px;
background: rgba(241, 242, 253, 100%);
border-radius: 11px 11px 0 0;
.title {
padding-bottom: 12px;
font-size: 14px;
font-weight: 600;
line-height: 18px;
color: rgba(77, 83, 232, 100%);
}
}
}
.modal-modify-tips {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
padding: 12px;
background: var(--light-usage-warning-light-color-warning-light-default,
#fff8ea);
.tip-icon {
margin: 0 20px 0 17px;
color: rgba(252, 136, 0, 100%);
>svg {
width: 20px;
height: 20px;
}
}
.description {
display: flex;
align-items: center;
margin-right: 65px;
}
.link {
cursor: pointer;
margin-left: 20px;
color: #3370ff;
white-space: nowrap;
}
}
// 新增样式 @zhangyuanzhou.zyz
.entry {
display: flex;
gap: 64px;
align-items: center;
justify-content: center;
height: 409px;
margin-bottom: 24px;
background: rgba(255, 255, 255, 100%);
border: 1px solid rgba(29, 28, 35, 8%);
border-radius: 12px;
.entry-method {
cursor: pointer;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
color: var(--Light-usage-text---color-text-0, #1d1c23);
.entry-method-icon {
width: 24px;
height: 24px;
}
.entry-method-title {
font-size: 12px;
font-weight: 600;
font-style: normal;
line-height: 16px;
}
}
.entry-method:hover {
color: var(--Light-color-brand---brand-5, #4d53e8);
}
}
// 当窗口足够大时高度固定为641px
// 当窗口太小时高度随vh变化333px为内容区距离视窗边缘的距离
.database-table-structure-container {
overflow: auto;
width: 100%;
height: min(641px, calc(100vh - 333px));
}
.title-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.right {
display: flex;
gap: 32px;
align-items: center;
}
}
.generate-ai-popover-wrapper {
display: flex;
flex-direction: column;
gap: 16px;
width: 560px;
padding: 24px;
background-color: rgba(247, 247, 250, 100%);
border-radius: 12px;
.title {
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
line-height: 24px;
}
.input {}
.button-wrapper {
display: flex;
justify-content: flex-end;
width: 100%;
}
.text-area {
:global {
.semi-input-textarea {
overflow: auto;
max-height: 191px;
}
}
}
}
.popover {
:global {
.semi-popover {
border-radius: 12px;
}
.semi-popover-content {
border-radius: 12px;
}
}
}
.modal-close-button {
height: 24px !important;
padding: 4px !important;
border-radius: 3px !important;
&:hover {
background-color: rgba(46, 50, 56, 5%) !important;
}
}

View File

@@ -0,0 +1,655 @@
/*
* 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 max-lines */
/* eslint-disable @coze-arch/max-line-per-function */
/* eslint-disable max-lines-per-function */
import { useRef, useMemo, useEffect, useState } from 'react';
import { nanoid } from 'nanoid';
import { useLocalStorageState } from 'ahooks';
import { type DatabaseInfo } from '@coze-studio/bot-detail-store';
import { DataNamespace, dataReporter } from '@coze-data/reporter';
import { BotE2e } from '@coze-data/e2e';
import {
CreateType,
type NL2DBInfo,
type OnSave,
} from '@coze-data/database-v2-base/types';
import { TEMPLATE_INFO } from '@coze-data/database-v2-base/constants';
import {
DatabaseTableStructure,
type DatabaseTableStructureRef,
} from '@coze-data/database-v2-base/components/database-table-structure';
import { FormDatabaseModeSelect } from '@coze-data/database-v2-adapter/components/database-mode-select';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { I18n, getUnReactiveLanguage } from '@coze-arch/i18n';
import { sendTeaEvent, EVENT_NAMES } from '@coze-arch/bot-tea';
import {
Button,
Image,
Popconfirm,
Icon,
TextArea,
Popover,
UIButton,
Modal,
Form,
Toast,
} from '@coze-arch/bot-semi';
import { IconWarningSize24 } from '@coze-arch/bot-icons';
import {
BotTableRWMode,
type RecommendDataModelResponse,
SceneType,
FieldItemType,
} from '@coze-arch/bot-api/memory';
import { MemoryApi } from '@coze-arch/bot-api';
import {
IconCozWarning as IconAlertTriangle,
IconCozCross as IconClose,
} from '@coze-arch/coze-design/icons';
import { BotDebugButton } from '../bot-debug-button';
import tableTempEN from '../../assets/table-template-en.png';
import tableTempCN from '../../assets/table-template-cn.png';
import tablePreviewEN from '../../assets/table-preview-en.png';
import tablePreviewCN from '../../assets/table-preview-cn.png';
import { ReactComponent as UpArrowSVG } from '../../assets/icon_up-arrow.svg';
import { ReactComponent as DownArrowSvg } from '../../assets/icon_down-arrow.svg';
import { ReactComponent as AddSVG } from '../../assets/icon_add_outlined.svg';
import { ReactComponent as GenerateSVG } from '../../assets/generate.svg';
import s from './index.module.less';
export interface ExpertModeConfig {
isExpertMode: boolean;
maxTableNum: number;
maxColumnNum: number;
readAndWriteModes: BotTableRWMode[];
}
export interface DatabaseModalProps {
visible: boolean;
onCancel: () => void;
database: DatabaseInfo;
botId: string;
spaceId: string;
readonly: boolean;
// eslint-disable-next-line @typescript-eslint/naming-convention -- 历史逻辑
NL2DBInfo: NL2DBInfo | null;
expertModeConfig?: ExpertModeConfig;
onSave?: OnSave;
}
export const DatabaseModal: React.FC<DatabaseModalProps> = props => {
const {
database,
botId,
// spaceId,
readonly,
onCancel,
onSave,
NL2DBInfo,
expertModeConfig,
visible,
} = props;
const [generateTableLoading, setGenerateTableLoading] = useState(false);
const [contentCheckErrorMsg, setContentCheckErrorMsg] = useState<string>('');
const [isEntry, setIsEntry] = useState<boolean>(
!database.tableId && !NL2DBInfo,
);
const [isPreview, setIsPreview] = useState<boolean>(false);
const [isDeletedField, setIsDeletedField] = useState<boolean>(false);
const [
shouldHideDatabaseTableStructureTipsForCurrent,
setShouldHideDatabaseTableStructureTipsForCurrent,
] = useState<boolean>(false);
const [createType, setCreateType] = useState<CreateType>(
NL2DBInfo ? CreateType.recommend : CreateType.custom,
);
const [data, setData] = useState<DatabaseInfo>({
tableId: '',
name: '',
desc: '',
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
tableMemoryList: [],
});
const [AIPopoverVisible, setAIPopoverVisible] = useState(false);
const nlTextAreaRef = useRef<HTMLTextAreaElement>();
const tableStructureRef = useRef<DatabaseTableStructureRef>();
const [
mapOfShouldHidingDatabaseTableStructureTips,
setMapOfShouldHidingDatabaseTableStructureTips,
] = useLocalStorageState<string | undefined>(
// FIXME: 此属性名意义不明确,处为了兼容,暂不修改此属性名,但后续需要使用更明确的命名
'use-local-storage-state-modify-tips',
{
defaultValue: '',
},
);
const language = getUnReactiveLanguage();
const isEdit = Boolean(data.tableId);
const [isReadonly, setIsReadonly] = useState(false);
const handleSave = async () => {
try {
setIsReadonly(true);
// @ts-expect-error -- linter-disable-autofix
await tableStructureRef.current.submit();
} finally {
setIsReadonly(false);
}
};
const hideTableStructureTips = useMemo(() => {
const lsMap = JSON.parse(
mapOfShouldHidingDatabaseTableStructureTips || '{}',
);
return (
!isEdit ||
lsMap?.[botId] ||
shouldHideDatabaseTableStructureTipsForCurrent
);
}, [
isEdit,
shouldHideDatabaseTableStructureTipsForCurrent,
mapOfShouldHidingDatabaseTableStructureTips,
]);
const title = useMemo(() => {
if (isEdit) {
return I18n.t('db_edit_title');
}
if (createType === CreateType.excel) {
return I18n.t('db_table_0126_011');
}
return I18n.t('db_add_table_title');
}, [isEdit, createType]);
const showEntry = isEntry && !isEdit && !NL2DBInfo;
const shouldShowAIGenerate =
/**
* 1. 入口不展示
* 2. 编辑态不展示
* 3. Excel导入时不展示
*/
!showEntry && !isEdit && createType !== CreateType.excel;
const setDataToDefault = () => {
setData({
name: '',
desc: '',
tableId: '',
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
tableMemoryList: [
{
nanoid: nanoid(),
name: '',
desc: '',
type: FieldItemType.Text,
must_required: false,
},
],
});
};
const setTableFieldsListToDefault = () => {
tableStructureRef.current?.setTableFieldsList([
{
nanoid: nanoid(),
name: '',
desc: '',
type: FieldItemType.Text,
must_required: false,
},
]);
};
const onUseTemplate = () => {
setCreateType(CreateType.template);
setIsEntry(false);
setData({
...TEMPLATE_INFO,
});
};
const onUseCustom = () => {
setCreateType(CreateType.custom);
setIsEntry(false);
setDataToDefault();
};
const generateTableByNL = async (text: string, type: SceneType) => {
setGenerateTableLoading(true);
let res: RecommendDataModelResponse | undefined;
try {
res = await MemoryApi.RecommendDataModel({
bot_id: botId,
scene_type: type,
text,
});
} catch (error) {
setGenerateTableLoading(false);
dataReporter.errorEvent(DataNamespace.DATABASE, {
eventName: REPORT_EVENTS.DatabaseNL2DB,
error: error as Error,
});
setDataToDefault();
setTableFieldsListToDefault();
}
if (res?.bot_table_list?.[0]) {
if (type === SceneType.BotPersona) {
setCreateType(CreateType.recommend);
}
if (type === SceneType.ModelDesc) {
setCreateType(CreateType.naturalLanguage);
}
setData({
tableId: '',
// @ts-expect-error -- linter-disable-autofix
name: res.bot_table_list[0].table_name,
// @ts-expect-error -- linter-disable-autofix
desc: res.bot_table_list[0].table_desc,
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
// @ts-expect-error -- linter-disable-autofix
tableMemoryList: res.bot_table_list[0].field_list.map(i => ({
name: i.name,
desc: i.desc,
must_required: i.must_required,
type: i.type,
nanoid: nanoid(),
id: Number(i.id),
})),
});
// data 是初始值,此处需要手动 setState 更新子组件状态
// 若 Modal 已提前关闭,子组件卸载,则 ref 为空,需要加上可选链判断一下
tableStructureRef.current?.setTableFieldsList(
// @ts-expect-error -- linter-disable-autofix
res.bot_table_list[0].field_list.map(i => ({
name: i.name,
desc: i.desc,
must_required: i.must_required,
type: i.type,
nanoid: nanoid(),
id: Number(i.id),
})),
);
} else {
if (type === SceneType.BotPersona) {
Toast.info(I18n.t('recommended_failed'));
setDataToDefault();
setTableFieldsListToDefault();
}
if (type === SceneType.ModelDesc) {
Toast.warning(I18n.t('generate_failed'));
setAIPopoverVisible(true);
}
}
setGenerateTableLoading(false);
};
const handleGenerate = () => {
const generate = () => {
const { value } = nlTextAreaRef.current || {};
if (value) {
generateTableByNL(value, SceneType.ModelDesc);
}
};
sendTeaEvent(EVENT_NAMES.generate_with_ai_click, {
bot_id: botId,
need_login: true,
have_access: true,
});
setAIPopoverVisible(false);
if (
// @ts-expect-error -- linter-disable-autofix
tableStructureRef.current.tableFieldsList.filter(i => i.name).length > 0
) {
Modal.warning({
title: I18n.t('bot_database_ai_replace'),
content: I18n.t('bot_database_ai_replace_detailed'),
okButtonProps: {
type: 'warning',
},
onOk: () => {
generate();
},
maskClosable: false,
icon: <IconWarningSize24 />,
});
} else {
generate();
}
};
useEffect(() => {
setAIPopoverVisible(false);
setIsPreview(false);
setShouldHideDatabaseTableStructureTipsForCurrent(false);
setIsDeletedField(false);
setContentCheckErrorMsg('');
setIsEntry(true);
}, [visible]);
useEffect(() => {
setData(database);
}, [database]);
useEffect(() => {
setCreateType(NL2DBInfo ? CreateType.recommend : CreateType.custom);
}, [NL2DBInfo]);
useEffect(() => {
if (NL2DBInfo && visible) {
generateTableByNL(NL2DBInfo.prompt, SceneType.BotPersona);
}
}, [NL2DBInfo, visible]);
const DefaultFooter = (
<>
{contentCheckErrorMsg ? (
<Form.ErrorMessage error={contentCheckErrorMsg} />
) : null}
{hideTableStructureTips ? null : (
<div className={s['modal-modify-tips']}>
<div className={s.description}>
<IconAlertTriangle className={s['tip-icon']} />
<span style={{ textAlign: 'left' }}>{I18n.t('db_edit_tips1')}</span>
<span
className={s.link}
onClick={() => {
const lsMap = JSON.parse(
mapOfShouldHidingDatabaseTableStructureTips || '{}',
);
lsMap[botId] = true;
setMapOfShouldHidingDatabaseTableStructureTips(
JSON.stringify(lsMap),
);
}}
>
{I18n.t('db_edit_tips2')}
</span>
</div>
<IconClose
onClick={() =>
setShouldHideDatabaseTableStructureTipsForCurrent(true)
}
style={{ cursor: 'pointer' }}
/>
</div>
)}
<div className={s['modal-table-btn']}>
{isDeletedField ? (
<Popconfirm
title={I18n.t('db_del_field_confirm_title')}
content={I18n.t('db_del_field_confirm_info')}
okText={I18n.t('db_del_field_confirm_yes')}
cancelText={I18n.t('db_del_field_confirm_no')}
okType="danger"
onConfirm={handleSave}
>
<BotDebugButton
loading={isReadonly}
theme="solid"
type="primary"
readonly={readonly}
>
{I18n.t('db_edit_save')}
</BotDebugButton>
</Popconfirm>
) : (
<BotDebugButton
readonly={readonly}
loading={isReadonly}
theme="solid"
type="primary"
onClick={handleSave}
>
{I18n.t('db_edit_save')}
</BotDebugButton>
)}
</div>
</>
);
const Entry = (
<div className={s['modal-temp']}>
<div className={s.entry}>
<div
className={s['entry-method']}
onClick={onUseCustom}
data-testid={BotE2e.BotDatabaseAddModalAddCustomBtn}
>
<Icon svg={<AddSVG />} className={s['entry-method-icon']} />
<span className={s['entry-method-title']}>
{I18n.t('db_add_table_cust')}
</span>
</div>
</div>
<div className={s['modal-temp-right']}>
<div
className={s['modal-temp-title']}
data-testid={BotE2e.BotDatabaseAddModalTemplateTitle}
>
{I18n.t('db_add_table_temp_title')}
</div>
<Image
className={s['modal-temp-image']}
height={201}
src={language === 'zh-CN' ? tableTempCN : tableTempEN}
/>
<div className={s['modal-temp-description']}>
💡{I18n.t('db_add_table_temp_tips')}
</div>
{isPreview ? (
<div className={s['modal-temp-preview']}>
<div className={s.title}>
{I18n.t('db_add_table_temp_preview_tips')}
</div>
<Image
height={239}
src={language === 'zh-CN' ? tablePreviewCN : tablePreviewEN}
/>
</div>
) : null}
<div className={s['modal-temp-btn-group']}>
<Button
data-testid={BotE2e.BotDatabaseAddModalPreviewTemplateBtn}
theme="light"
type="tertiary"
onClick={() => setIsPreview(state => !state)}
className={s['modal-temp-btn']}
>
{I18n.t('db_add_table_temp_preview')}
</Button>
<Button
data-testid={BotE2e.BotDatabaseAddModalUseTemplateBtn}
theme="solid"
type="primary"
onClick={onUseTemplate}
className={s['modal-temp-btn']}
>
{I18n.t('db_add_table_temp_use')}
</Button>
</div>
</div>
</div>
);
const getFooter = () => {
if (showEntry) {
return null;
}
if (createType === CreateType.excel) {
return null;
}
return DefaultFooter;
};
const getContent = () => {
if (showEntry) {
return Entry;
}
if (createType === CreateType.excel) {
return null;
}
return (
<div className={s['database-table-structure-container']}>
<DatabaseTableStructure
data={data}
// @ts-expect-error -- linter-disable-autofix
ref={tableStructureRef}
loading={generateTableLoading}
loadingTips={I18n.t('bot_database_ai_waiting')}
botId={botId}
readAndWriteModeOptions={
// @ts-expect-error -- linter-disable-autofix
expertModeConfig.isExpertMode ? 'expert' : 'normal'
}
// @ts-expect-error -- linter-disable-autofix
maxColumnNum={expertModeConfig.maxColumnNum}
onSave={onSave}
onCancel={onCancel}
onDeleteField={list => {
setIsDeletedField(
!database.tableMemoryList.every(i =>
// TODO: 当前field id生成规则有问题故暂时使用 nanoid 替换
list.find(j => j.nanoid === i.nanoid),
),
);
}}
createType={createType}
setContentCheckErrorMsg={setContentCheckErrorMsg}
renderModeSelect={params => <FormDatabaseModeSelect {...params} />}
/>
</div>
);
};
return (
<Modal
visible={visible}
onCancel={onCancel}
closable={false}
width={1138}
centered
footer={getFooter()}
title={
<div className={s['title-wrapper']}>
<div data-testid={BotE2e.BotDatabaseAddModalTitle}>{title}</div>
<div className={s.right}>
{shouldShowAIGenerate ? (
<Popover
trigger="custom"
position="bottomRight"
content={
<div className={s['generate-ai-popover-wrapper']}>
<div
className={s.title}
data-testid={
BotE2e.BotDatabaseAddModalTitleCreateAiModalTitle
}
>
{I18n.t('bot_database_ai_create')}
</div>
<TextArea
data-testid={
BotE2e.BotDatabaseAddModalTitleCreateAiModalDesc
}
autosize
// @ts-expect-error -- linter-disable-autofix
ref={nlTextAreaRef}
rows={1}
placeholder={I18n.t('bot_database_ai_create_tip')}
className={s['text-area']}
/>
<div className={s['button-wrapper']}>
<UIButton
data-testid={
BotE2e.BotDatabaseAddModalTitleCreateAiModalCreateBtn
}
theme="borderless"
onClick={handleGenerate}
icon={<Icon svg={<GenerateSVG />} />}
>
{I18n.t('bot_database_ai_generate')}
</UIButton>
</div>
</div>
}
keepDOM
visible={AIPopoverVisible}
onVisibleChange={_v => {
setAIPopoverVisible(_v);
}}
onClickOutSide={() => {
setAIPopoverVisible(false);
}}
className={s.popover}
>
<UIButton
data-testid={BotE2e.BotDatabaseAddModalTitleCreateAiBtn}
theme="borderless"
icon={
AIPopoverVisible ? (
<Icon svg={<UpArrowSVG />} />
) : (
<Icon svg={<DownArrowSvg />} />
)
}
iconPosition="right"
onClick={() => {
sendTeaEvent(EVENT_NAMES.nl2table_create_table_click, {
bot_id: botId,
need_login: true,
have_access: true,
});
setAIPopoverVisible(true);
}}
>
{I18n.t('bot_database_ai_create')}
</UIButton>
</Popover>
) : null}
<UIButton
data-testid={BotE2e.BotDatabaseAddModalTitleCloseIcon}
icon={<IconClose />}
type="tertiary"
theme="borderless"
onClick={onCancel}
className={s['modal-close-button']}
/>
</div>
</div>
}
maskClosable={false}
>
<div className={s['modal-container']}>{getContent()}</div>
</Modal>
);
};