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,9 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="64" height="64" fill="url(#pattern0_6088_81588)" style=""/>
<defs>
<pattern id="pattern0_6088_81588" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_6088_81588" transform="scale(0.0104167)"/>
</pattern>
<image id="image0_6088_81588" width="96" height="96" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAACXBIWXMAACE4AAAhOAFFljFgAAABaWlDQ1BEaXNwbGF5IFAzAAB4nHWQvUvDUBTFT6tS0DqIDh0cMolD1NIKdnFoKxRFMFQFq1OafgltfCQpUnETVyn4H1jBWXCwiFRwcXAQRAcR3Zw6KbhoeN6XVNoi3sfl/Ticc7lcwBtQGSv2AijplpFMxKS11Lrke4OHnlOqZrKooiwK/v276/PR9d5PiFlNu3YQ2U9cl84ul3aeAlN//V3Vn8maGv3f1EGNGRbgkYmVbYsJ3iUeMWgp4qrgvMvHgtMunzuelWSc+JZY0gpqhrhJLKc79HwHl4plrbWD2N6f1VeXxRzqUcxhEyYYilBRgQQF4X/8044/ji1yV2BQLo8CLMpESRETssTz0KFhEjJxCEHqkLhz634PrfvJbW3vFZhtcM4v2tpCAzidoZPV29p4BBgaAG7qTDVUR+qh9uZywPsJMJgChu8os2HmwiF3e38M6Hvh/GMM8B0CdpXzryPO7RqFn4Er/QcXKWq8MSlPPgAAA8VJREFUeAHtnD9uE0EUxp/BSlwg4SIFBVKClIICidwg5gTACWJOADcgnAA4AeYEwAXAdJRBoqCgMBIFBYURFEmUKOy3u09eb2wwlv2+AX8/abL5v+P5jd/MvBm78eP15pkJGk18uGiHWTkyEceprWel5QKObK0xNBHH8Vk7F3DBBBUJICMBZCSAjASQkQAyEkBGAshIABkJICMBZCSAjASQkQAyEkBGAshIABkJICMBZCSAjASQkQAyEkBGAsg0LWWabbNLO2W5adbaKgrwq3MyNDscjK4/32floCgn6R46S09Au5OV3fLamf3vXNYkIGHYN/v6vPg8IRo4nLtm3/lHE690s7L3d40+DxDw5Wkmo2dMcDTx2C4nIAANfv3Z+ZCybBCmBo9oIlwAbxBGyEDD77yJb3yAe+L+KKgLCY4APGA0PMIOG9QBdSFJ4Ai48WL6gMkAdUGdCMQLQI9b9kA7D6gT4RlJELBnyUKoW7yAFHu/QwiL8QISXpUyBuJ4AYmtRMfAajmYeAFIB6QKoW4EAT1KT/sjea6oZ9Fw1gEf7qYVilAX1IkARwAG4oNbaYQj1AF1IU0OeOsAPOCP3azcKxJj0eCeaHjUwRv/6n2LJl7A9hOzjTujrxF3310rRESEJcR63Av3rI5FWJ9s7Vs08enoTvneIIP9Ih1cB4uhPF2xu7iFERp6+La89s//HD0fHQP0GxYBbz+gU3lzFoQB9MZps6JJW5L4nl+reBjDddYtybzXPxxfna+UAMe3CyOmgRCHBkevn5QWCRaQxp6w7/9uPzb79jIrr4qeu6jBGY2OcQdhDVfiBkydeAEIB9MaAN/P94a7xdcuwcNJ9eRDPazgb70gZOUnKDYLsbPuuBGmovEC0JCzZkQ9/ldnTcuEsDiMn4ZOmvmkwkrkgnzATQ3SURVOKgKrz5SeCWj8Tw+MAS8VgYVYfTUaDe6NdIQ3/sqlIjwfE5mYw0wHU12/r3cApSJsNF/fuD2aSi4Cb3SkI3CtTzeViuhP/l2X4KmIahqivpbwtQFKvn74XPx//O9pCzqlIkqUiiCjVEQgy05F+Is4lIqYglIRYygVUUWpCCJKRRBZqVSEk2IqggB3GuqpCD+bH3E8HDMdNDx6fQIn9NJ5lSRgpSIIpPMqyd8xTyrCT0XMkoog8m8I+I/hv0xV5EgAGQkgIwFkJICMBJCRADISQEYCyEgAGQkgIwFkJICMBJCRADISQEYCyEgAGQkgIwFkJICMBJCRADISQCY/G3pq6/lBIREH2hyUAlp5EfH8AnA31N4fXmhDAAAAAElFTkSuQmCC"/>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,16 @@
.database-model-content {
:global {
.semi-form-field {
padding-top: 0;
padding-bottom: 16px;
}
.semi-input-wrapper {
@apply coz-bg-plus;
}
.semi-input-textarea-wrapper {
@apply coz-bg-plus;
}
}
}

View File

@@ -0,0 +1,290 @@
/*
* 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 { useState, type FC, useRef, useEffect, useCallback } from 'react';
import { CozeFormTextArea, CozeInputWithCountField } from '@coze-data/utils';
import {
PictureUpload,
type RenderAutoGenerateParams,
} from '@coze-common/biz-components/picture-upload';
import { I18n } from '@coze-arch/i18n';
import { Form, type FormApi, Modal } from '@coze-arch/coze-design';
import { FormatType } from '@coze-arch/bot-api/memory';
import { FileBizType, IconType } from '@coze-arch/bot-api/developer_api';
import { KnowledgeApi } from '@coze-arch/bot-api';
import styles from './index.module.less';
export enum ModalMode {
CREATE = 'create',
EDIT = 'edit',
}
export interface UseDatabaseBaseInfoModalProps {
onSubmit: (formData: FormData) => void;
onClose?: () => void;
initValues: FormData | undefined;
mode: ModalMode;
renderAutoGenerate?: (params: RenderAutoGenerateParams) => React.ReactNode;
}
export interface DatabaseBaseInfoModalProps
extends UseDatabaseBaseInfoModalProps {
visible: boolean;
}
export const useDatabaseInfoModal = ({
onSubmit,
onClose,
initValues,
mode = ModalMode.CREATE,
renderAutoGenerate,
}: UseDatabaseBaseInfoModalProps) => {
const [visible, setVisible] = useState(false);
const open = () => {
setVisible(true);
};
const close = () => {
setVisible(false);
onClose?.();
};
return {
visible,
open,
close,
modal: (
<DatabaseBaseInfoModal
visible={visible}
onClose={close}
onSubmit={onSubmit}
initValues={initValues}
mode={mode}
renderAutoGenerate={renderAutoGenerate}
/>
),
};
};
export interface FormData {
name: string;
description: string;
icon_uri?: Array<{
url: string;
uri: string;
uid?: string;
isDefault?: boolean;
}>;
}
export const DatabaseBaseInfoModal: FC<DatabaseBaseInfoModalProps> = ({
visible,
initValues,
onClose,
onSubmit,
mode,
renderAutoGenerate,
}) => {
const formRef = useRef<FormApi<FormData> | null>(null);
const handleSubmit = async () => {
if (!formRef.current) {
return;
}
const formData = await formRef.current.validate();
onSubmit({
...formData,
icon_uri: [
{
url: formData?.icon_uri?.[0]?.url ?? '',
uri: formData?.icon_uri?.[0]?.uid ?? '',
},
],
});
};
const [coverIcon, setCoverIcon] = useState<{
uri: string;
url: string;
}>({
uri: initValues?.icon_uri?.[0]?.uri ?? '',
url: initValues?.icon_uri?.[0]?.url ?? '',
});
const [iconInfoGenerate, setIconInfoGenerate] = useState<{
name: string;
desc: string;
}>({
name: initValues?.name ?? '',
desc: initValues?.description ?? '',
});
const setDefaultIcon = async () => {
const { icon } = await KnowledgeApi.GetIcon({
format_type: FormatType.Database,
});
setCoverIcon({
uri: icon?.uri ?? '',
url: icon?.url ?? '',
});
formRef.current?.setValue('icon_uri', [
{
url: icon?.url ?? '',
uri: icon?.uri ?? '',
uid: icon?.uri ?? '',
isDefault: true,
},
]);
};
const initForm = useCallback(
({ name, description, icon_uri }: FormData) => {
if (!formRef.current) {
return;
}
formRef.current.setValue('name', name);
formRef.current.setValue('description', description);
setIconInfoGenerate({
name: name ?? '',
desc: description ?? '',
});
if (!icon_uri || !icon_uri[0].url) {
setDefaultIcon();
return;
}
formRef.current.setValue('icon_uri', [
{
url: icon_uri[0].url,
uri: icon_uri[0].uri,
uid: icon_uri[0].uri,
isDefault: true,
},
]);
},
[formRef],
);
useEffect(() => {
if (!visible) {
return;
}
if (!initValues) {
return;
}
initForm(initValues);
}, [visible, initValues, initForm]);
const handleClose = () => {
if (formRef.current) {
formRef.current?.reset(['name', 'description', 'icon_uri']);
}
onClose?.();
};
return (
<Modal
title={
mode === ModalMode.CREATE
? I18n.t('db_add_table_title')
: I18n.t('db_edit_title')
}
closable
visible={visible}
onCancel={handleClose}
className="w-[480px]"
okText={I18n.t('db2_004')}
okButtonProps={{
// @ts-expect-error -- for e2e
'data-testid': 'database.info_modal.button.confirm',
}}
cancelText={I18n.t('db_del_field_confirm_no')}
onOk={handleSubmit}
maskClosable={false}
>
<Form<FormData>
className={styles['database-model-content']}
getFormApi={formApi => {
formRef.current = formApi;
}}
initValues={initValues}
>
{({ formState }) => (
<>
<CozeInputWithCountField
data-testid="database.info_modal.input.name"
field="name"
label={I18n.t('db_add_table_name')}
placeholder={I18n.t('db_add_table_name_tips')}
required
maxLength={50}
disabled={mode === ModalMode.EDIT}
onChange={(value: string) => {
setIconInfoGenerate(prev => ({
...prev,
name: value?.trim() || '',
}));
}}
rules={[
{
required: true,
message: I18n.t('db2_005'),
},
{
pattern: /^[a-z][a-z0-9_]{0,63}$/,
message: I18n.t('db_new_0004'),
},
]}
/>
<CozeFormTextArea
data-testid="database.info_modal.input.description"
field="description"
label={I18n.t('db_add_table_desc')}
placeholder={I18n.t('db_add_table_desc_tips')}
maxCount={100}
maxlength={100}
onChange={(value: string) => {
setIconInfoGenerate(prev => ({
...prev,
desc: value?.trim() || '',
}));
}}
/>
<PictureUpload
label={I18n.t('datasets_model_create_avatar')}
field="icon_uri"
fileBizType={FileBizType.BIZ_DATASET_ICON}
iconType={IconType.Dataset}
uploadClassName="w-auto"
generateInfo={iconInfoGenerate}
withAutoGenerate={!!renderAutoGenerate}
renderAutoGenerate={renderAutoGenerate}
initValue={[
{
url: coverIcon?.url,
uid: coverIcon?.uri,
isDefault: true,
},
]}
/>
</>
)}
</Form>
</Modal>
);
};

View File

@@ -0,0 +1,6 @@
.table-header-label-tooltip-icon {
cursor: pointer;
margin-left: 8px;
color: rgba(198, 202, 205, 100%);
}

View File

@@ -0,0 +1,326 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useRef, useMemo, useEffect } from 'react';
import type { ReactNode, RefObject } from 'react';
import {
type DatabaseInfo,
type TableMemoryItem,
} from '@coze-studio/bot-detail-store';
import { BotE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozCross } from '@coze-arch/coze-design/icons';
import { IconButton, Modal, Button } from '@coze-arch/coze-design';
import {
type BotTableRWMode,
type AlterBotTableResponse,
type InsertBotTableResponse,
} from '@coze-arch/bot-api/memory';
import { DismissibleBanner } from '../dismissible-banner';
import {
DatabaseTableStructure,
type DatabaseTableStructureRef,
} from '../database-table-structure';
import {
CreateType,
type TableFieldsInfo,
type OnSave,
} from '../../types/database-field';
// import { useExpertModeConfig } from '../../hooks/use-expert-mode-config';
const MAX_COLUMNS = 20;
interface CreateTableModalExtraParams {
botId?: string;
spaceId?: string;
creatorId?: string;
}
// RenderGenerate属性类型定义
export interface RenderGenerateProps {
tableStructureRef: RefObject<DatabaseTableStructureRef>;
onGenerateChange: (tableMemoryList: TableMemoryItem[]) => void;
onGenerating: (loading: boolean) => void;
botId: string;
}
export interface RenderModeSelectProps {
dataTestId: string;
field: string;
label: string;
type: 'select';
options: BotTableRWMode[];
}
export interface DatabaseCreateTableModalProps {
visible: boolean;
onClose: () => void;
onReturn?: () => void;
onSubmit?: (response: InsertBotTableResponse | AlterBotTableResponse) => void;
initValue: DatabaseInfo;
showDatabaseBaseInfo: boolean;
extraParams?: CreateTableModalExtraParams;
onlyShowDatabaseInfoRWMode: boolean;
projectID?: string;
renderGenerate?: (props: RenderGenerateProps) => ReactNode;
renderModeSelect?: (props: RenderModeSelectProps) => ReactNode;
}
interface UseDatabaseCreateTableModalProps {
onClose?: () => void;
onReturn?: () => void;
initValue: DatabaseInfo;
onSubmit?: (response: InsertBotTableResponse | AlterBotTableResponse) => void;
showDatabaseBaseInfo: boolean;
extraParams?: CreateTableModalExtraParams;
onlyShowDatabaseInfoRWMode: boolean;
projectID?: string;
renderGenerate?: (props: RenderGenerateProps) => ReactNode;
renderModeSelect?: (props: RenderModeSelectProps) => ReactNode;
}
export const useDatabaseCreateTableModal = ({
onReturn,
onSubmit,
initValue,
showDatabaseBaseInfo,
extraParams,
onlyShowDatabaseInfoRWMode,
projectID,
renderGenerate,
renderModeSelect,
}: UseDatabaseCreateTableModalProps) => {
const [visible, setVisible] = useState(false);
const open = () => {
setVisible(true);
};
const close = () => {
setVisible(false);
// onClose?.();
};
return {
visible,
open,
close,
modal: (
<DatabaseCreateTableModal
visible={visible}
onClose={close}
onReturn={onReturn}
onSubmit={onSubmit}
initValue={initValue}
showDatabaseBaseInfo={showDatabaseBaseInfo}
extraParams={extraParams}
onlyShowDatabaseInfoRWMode={onlyShowDatabaseInfoRWMode}
projectID={projectID}
renderGenerate={renderGenerate}
renderModeSelect={renderModeSelect}
/>
),
};
};
// eslint-disable-next-line @coze-arch/max-line-per-function
export function DatabaseCreateTableModal({
visible,
onClose,
onReturn,
onSubmit,
initValue,
showDatabaseBaseInfo,
onlyShowDatabaseInfoRWMode,
extraParams: { botId = '', spaceId = '', creatorId = '' } = {},
projectID,
renderGenerate,
renderModeSelect,
}: DatabaseCreateTableModalProps) {
// AI generate loading
const [generateTableLoading, setGenerateTableLoading] = useState(false);
// save button loading
const [saveBtnLoading, setSaveBtnLoading] = useState<boolean>(false);
// save button disabled
const [saveBtnDisabled, setSaveBtnDisabled] = useState<boolean>(false);
// database structure
const [databaseInitValue, setDatabaseInitValue] =
useState<DatabaseInfo>(initValue);
// export mode's some config(actually nobody knows why this is here...)
// const expertModeConfig = useExpertModeConfig({ botId });
// DataBase Table ref
const tableStructureRef = useRef<DatabaseTableStructureRef>(null);
/**
* modal mode
* @has tableId: Edit Mode;
* @no tableId: Create Mode
*/
const isModify = useMemo(() => Boolean(initValue.tableId), [initValue]);
const handleValidateTable = (list: TableFieldsInfo, isEmptyList: boolean) => {
if (isEmptyList) {
setSaveBtnDisabled(true);
return;
}
// 系统字段不计入字段数量限制
if (list.filter(i => !i.isSystemField).length > MAX_COLUMNS) {
setSaveBtnDisabled(true);
return;
}
const validateRes = list.every(ele => {
if (!ele?.errorMapper) {
return true;
} else {
if (
ele.errorMapper.name?.length > 0 ||
ele.errorMapper.type?.length > 0
) {
return false;
}
return true;
}
});
setSaveBtnDisabled(!validateRes);
};
const onSave: OnSave = async ({ response }) => {
/**
* 在 DatabaseTableStructure 这个组件中,提交已经区分了 edit 和 create 两种状态,
* 并且存在一个onSave的回调因此提交之后的逻辑全部收敛在这里
*/
await onSubmit?.(response);
};
const onCreateSubmit = async () => {
if (tableStructureRef.current) {
try {
setSaveBtnLoading(true);
await tableStructureRef.current.submit();
} finally {
setSaveBtnLoading(false);
}
}
};
useEffect(() => {
setDatabaseInitValue(initValue);
}, [initValue]);
return (
<Modal
closable
maskClosable={false}
visible={visible}
onCancel={undefined}
onOk={onCreateSubmit}
size="xxl"
header={
<>
<div className="flex flex-row items-center">
<div className="flex-1 text-[20px] font-medium coz-fg-plus">
{isModify
? I18n.t('db_edit_title')
: I18n.t('db_add_table_title')}
</div>
{!isModify
? renderGenerate?.({
tableStructureRef,
onGenerateChange: tableMemoryList => {
setDatabaseInitValue({
...databaseInitValue,
tableMemoryList,
});
},
onGenerating: setGenerateTableLoading,
botId,
})
: null}
<div>
<IconButton
color="secondary"
icon={<IconCozCross className="text-[20px] coz-fg-secondary" />}
onClick={onClose}
/>
</div>
</div>
{/* 编辑弹窗出现 Banner 提示 */}
{isModify ? (
<DismissibleBanner
type="warning"
persistentKey="_coze_database_edit_warning"
className="mx-[-24px]"
>
{I18n.t('db_edit_tips1')}
</DismissibleBanner>
) : null}
</>
}
footer={
<div className="coz-modal-footer">
<Button
color="primary"
onClick={() => {
if (onReturn) {
onReturn();
return;
}
}}
>
{isModify ? I18n.t('db_del_field_confirm_no') : I18n.t('db2_003')}
</Button>
<Button
data-testid={BotE2e.BotDatabaseAddModalSubmitBtn}
loading={saveBtnLoading}
onClick={onCreateSubmit}
disabled={saveBtnDisabled}
>
{I18n.t('db_edit_save')}
</Button>
</div>
}
>
<div>
<DatabaseTableStructure
data={databaseInitValue}
ref={tableStructureRef}
loading={generateTableLoading}
loadingTips={I18n.t('bot_database_ai_waiting')}
botId={botId}
spaceId={spaceId}
creatorId={creatorId}
// maxColumnNum={expertModeConfig.maxColumnNum}
maxColumnNum={MAX_COLUMNS}
useComputingEnableGoToNextStep={handleValidateTable}
createType={CreateType.custom}
hiddenTableBorder
readAndWriteModeOptions="expert"
showDatabaseBaseInfo={showDatabaseBaseInfo}
onlyShowDatabaseInfoRWMode={onlyShowDatabaseInfoRWMode}
onSave={onSave}
onCancel={onClose}
projectID={projectID}
renderModeSelect={renderModeSelect}
/>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
import { type FieldItemType } from '@coze-arch/bot-api/developer_api';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tag, Tooltip, Typography } from '@coze-arch/coze-design';
import { FIELD_TYPE_OPTIONS } from '../../constants/database-field';
export interface DatabaseFieldTitleProps {
field?: string;
textType?: 'primary' | 'secondary';
type?: FieldItemType;
tip?: ReactNode;
required?: boolean;
}
export function DatabaseFieldTitle({
field,
textType = 'secondary',
type,
tip,
required,
}: DatabaseFieldTitleProps) {
return (
<div className="flex flex-row items-center">
<Typography.Text type={textType} weight={500} fontSize="12px" ellipsis>
{field}
</Typography.Text>
{required ? (
<span className="coz-fg-hglt-red text-[12px] leading-[16px]">*</span>
) : null}
{tip ? (
<Tooltip content={tip} style={{ maxWidth: 'unset' }}>
<IconCozInfoCircle className="w-[12px] h-[12px] ml-[3px] coz-fg-secondary" />
</Tooltip>
) : null}
{typeof type === 'number' ? (
<Tag color="primary" size="mini" className="ml-[4px]">
{FIELD_TYPE_OPTIONS.find(i => i.value === type)?.label ?? type}
</Tag>
) : null}
</div>
);
}

View File

@@ -0,0 +1,8 @@
/* stylelint-disable declaration-no-important */
.modal-key-tip {
width: 200px !important;
ul {
padding-left: 16px;
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PopoverContent } from '@coze-studio/components';
import { I18n } from '@coze-arch/i18n';
import s from './index.module.less';
export const KeyTipsNode: React.FC = () => (
<PopoverContent className={s['modal-key-tip']}>{`- ${I18n.t(
'db_add_table_field_name_tips1',
)}
- ${I18n.t('db_add_table_field_name_tips2')}
- ${I18n.t('db_add_table_field_name_tips3')}
- ${I18n.t('db_add_table_field_name_tips4')}`}</PopoverContent>
);

View File

@@ -0,0 +1,189 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable complexity */
import { type TableMemoryItem } from '@coze-studio/bot-detail-store';
import { I18n } from '@coze-arch/i18n';
import {
type MapperItem,
type TriggerType,
VerifyType,
} from '../../../types/database-field';
// 校验 Table Name 和 Field Name
const namingRegexMapper = [
{
type: 1,
regex: /[^a-z0-9_]/,
errorMsg: I18n.t('db_add_table_field_name_tips2'),
},
{
type: 2,
regex: /^[^a-z]/,
errorMsg: I18n.t('db_add_table_field_name_tips3'),
},
{
type: 3,
regex: /[\s\S]{64,}/,
errorMsg: I18n.t('db_add_table_field_name_tips4'),
},
];
export const validateNaming = (str: string, errList: string[] = []) => {
let list = [...errList];
namingRegexMapper.forEach(i => {
list = list.filter(j => j !== i.errorMsg);
if (i.regex.test(str || '')) {
list.push(i.errorMsg);
}
});
return list;
};
// 校验 Table Fields
export const thMapper: MapperItem[] = [
{
label: I18n.t('db_add_table_field_name'),
key: 'name',
validator: [
{
type: VerifyType.Naming,
message: '',
},
{
type: VerifyType.Required,
message: I18n.t('db_table_save_exception_nofieldname'),
},
{
type: VerifyType.Unique,
message: I18n.t('db_table_save_exception_fieldname'),
},
],
defaultValue: '',
require: true,
},
{
label: I18n.t('db_add_table_field_desc'),
key: 'desc',
require: false,
validator: [],
defaultValue: '',
},
{
label: I18n.t('db_add_table_field_type'),
key: 'type',
require: true,
validator: [
{
type: VerifyType.Required,
message: I18n.t('db_table_save_exception_fieldtype'),
},
],
defaultValue: '',
},
{
label: I18n.t('db_add_table_field_necessary'),
key: 'must_required',
require: false,
validator: [],
defaultValue: true,
},
];
export const validateFields = (
list: TableMemoryItem[],
trigger: TriggerType,
) => {
const resList = list.map(_listItem => {
const listItem: TableMemoryItem = { ..._listItem };
thMapper.forEach(thItem => {
const thKey = thItem.key as keyof TableMemoryItem;
thItem.validator.forEach(verifyItem => {
if (!listItem?.errorMapper) {
listItem.errorMapper = {};
}
let errTarget = listItem?.errorMapper?.[thKey];
const value = listItem[thKey];
if (!errTarget) {
listItem.errorMapper[thKey] = [];
errTarget = [];
}
const msg = verifyItem.message;
switch (verifyItem.type) {
case VerifyType.Required: {
// 报错出现时机:点击保存按钮时,出现提示。表中某一行填写了数据,但是未填写必填字段时,需要报错
if (
trigger === 'save' &&
!value &&
thMapper.find(
i =>
!!listItem[i.key as keyof TableMemoryItem] && !i.defaultValue,
)
) {
listItem.errorMapper[thKey].push(msg);
}
// 报错消失时机:必填输入框输入了内容后,报错立刻消失
if (trigger === 'change' && value) {
listItem.errorMapper[thKey] = errTarget.filter(i => i !== msg);
}
break;
}
case VerifyType.Unique: {
// 报错出现时机:点击保存按钮时,出现提示。
if (
trigger === 'save' &&
value &&
list.filter(i => i[thKey] === listItem[thKey]).length !== 1
) {
listItem.errorMapper[thKey].push(msg);
}
// 报错消失时机:必填输入框输入了内容后,报错立刻消失
if (
trigger === 'change' &&
value &&
list.filter(i => i[thKey] === listItem[thKey]).length === 1
) {
listItem.errorMapper[thKey] = errTarget.filter(i => i !== msg);
}
break;
}
case VerifyType.Naming: {
// 报错出现时机:命名格式有问题,失去焦点时,立刻校验格式
if (
trigger === 'save' ||
trigger === 'blur' ||
(trigger === 'change' && errTarget.length)
) {
listItem.errorMapper[thKey] = validateNaming(
value as string,
errTarget,
);
}
break;
}
default:
break;
}
listItem.errorMapper[thKey] = Array.from(
new Set(listItem.errorMapper[thKey]),
);
});
});
return listItem;
});
return resList;
};

View File

@@ -0,0 +1,300 @@
/* stylelint-disable declaration-no-important */
/* stylelint-disable no-descending-specificity */
/* stylelint-disable selector-class-pattern */
/* stylelint-disable no-duplicate-selectors */
/* stylelint-disable font-family-no-missing-generic-family-keyword */
.th-tip-name {
display: flex;
flex-direction: column;
}
.th-tip-dot {
position: relative;
padding-left: 16px;
}
.th-tip-dot::before {
content: '';
position: absolute;
top: 7px;
left: 0;
width: 5px;
height: 5px;
background-color: #000;
border-radius: 50%;
}
// 新增样式 @zhangyuanzhou.zyz
.form-input-error {
:global {
.semi-input-wrapper {
border-color: #f93920
}
.semi-input-wrapper:hover {
border-color: var(--semi-color-border, rgba(29, 28, 35, 8%))
}
}
}
.table-structure-form {
padding-bottom: 4px;
:global {
// 去除 error message icon
.semi-form-field-validate-status-icon {
display: none;
}
.semi-form-field-validate-status-icon+span {
font-size: 12px;
}
.semi-col-12 {
.semi-form-field[x-field-id="prompt_disabled"] {
display: flex;
justify-content: flex-end;
.semi-form-field-main {
display: flex;
align-items: center;
width: auto !important;
}
}
}
.semi-switch-checked {
background-color: rgba(var(--coze-brand-5),1) !important;
}
.semi-switch-checked:hover {
background-color: rgba(var(--coze-brand-6),1) !important;
}
}
}
.max-row-banner {
margin-bottom: 24px;
}
.form-item-label-tooltip-icon {
cursor: pointer;
color: rgba(198, 202, 205, 100%);
}
.table-setting-select {
:global {
.semi-select:hover {
border: 1px solid var(--semi-color-border) !important;
}
}
}
.table-structure-table {
:global {
// table 圆角和边框
.semi-table-container {
padding: 0 8px;
border: 1px solid var(--Light-usage-border---color-border-1, rgba(29, 28, 35, 8%));
border-radius: 8px;
}
.semi-table-header {
.semi-table-row-head {
padding-right: 8px;
padding-left: 8px;
background: none !important;
}
}
// hover时去除背景色
.semi-table-body {
.semi-table-row {
background: none !important;
}
.semi-table-row:hover {
>.semi-table-row-cell {
background: none !important;
}
}
}
//
.semi-table-row {
cursor: default !important;
>.semi-table-row-cell {
padding: 12px 8px !important;
padding-left: 0;
border-bottom: 1px solid transparent !important;
}
}
.semi-table-body {
overflow: visible;
max-height: none !important;
padding: 12px 0;
border-radius: 8px;
}
// 移除 UITable 中的 before元素影响
.semi-table-row-cell::before {
display: none;
}
// 覆盖表结构编辑弹窗的只读字段样式,(在视觉上)由 disabled input 变为 plain text
.semi-input-wrapper-disabled {
background: none;
border: none;
.semi-input-disabled {
padding: 0;
font-weight: 500;
color: var(--coz-fg-primary);
-webkit-text-fill-color: var(--coz-fg-primary);
}
.semi-input-suffix {
display: none;
}
}
.semi-select-disabled {
cursor: default;
opacity: 1;
background: none;
border: none;
&:hover {
background-color: transparent !important;
}
.semi-select-selection {
cursor: default;
margin: 0;
.semi-select-selection-text {
font-weight: 500;
color: var(--coz-fg-primary);
}
}
.semi-select-arrow {
display: none;
}
}
}
}
.table-structure-table-wrapper {
height: auto;
}
.table-empty-tips {
padding: 8px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
color: @error-red;
}
.spin {
:global {
.semi-spin-wrapper div {
font-size: 16px;
color: rgba(29, 28, 35, 35%);
}
}
}
.table-setting-option {
:global {
.semi-select-option-selected {
.semi-select-option-icon {
color: #4D53E8
}
}
}
}
.table-name-form-field {
padding-bottom: 24px !important;
// semi 原有的 disable 样式不满足需求,需要更明显的文字颜色
:global {
.semi-input-wrapper-disabled {
-webkit-text-fill-color: rgba(29, 28, 35, 100%);
}
}
}
.table-desc-form-field {
padding-top: 0 !important;
}
.prompt_disabled_popover {
:global {
.semi-popover-icon-arrow {
right: 8px !important;
}
}
}
.read_mode_popover {
:global {
.semi-popover-icon-arrow {
left: 8px !important;
}
}
}
.hidden-form-border {
:global {
.semi-table-container {
padding: 0;
border: none;
border-radius: 0;
}
// .semi-table-row-head {
// padding-right: 0 !important;
// padding-left: 0 !important;
// }
// .semi-input-wrapper {
// @apply coz-bg-plus;
// }
// .semi-input-textarea-wrapper {
// @apply coz-bg-plus;
// }
// .semi-select {
// @apply coz-bg-plus;
// }
}
}

View File

@@ -0,0 +1,770 @@
/*
* 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 */
import {
useRef,
forwardRef,
useImperativeHandle,
useState,
type MutableRefObject,
useEffect,
type ReactNode,
} from 'react';
import { nanoid } from 'nanoid';
import { noop } from 'lodash-es';
import classNames from 'classnames';
import { useBoolean } 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 { REPORT_EVENTS } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { Button, Switch, Form } from '@coze-arch/coze-design';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import {
UITable,
UITableAction,
Tooltip,
Image,
Banner,
} from '@coze-arch/bot-semi';
import { IconAdd } from '@coze-arch/bot-icons';
import { isApiError } from '@coze-arch/bot-http';
import {
type UpdateDatabaseRequest,
type AddDatabaseRequest,
BotTableRWMode,
type FieldItemType,
} from '@coze-arch/bot-api/memory';
import { MemoryApi } from '@coze-arch/bot-api';
import { SLSelect } from '../singleline-select';
import { FormSLInput, SLInput } from '../singleline-input';
import { DatabaseFieldTitle } from '../database-field-title';
import {
type TriggerType,
type TableFieldsInfo,
type TableBasicInfo,
type ReadAndWriteModeOptions,
type CreateType,
type OnSave,
} from '../../types/database-field';
import {
DATABASE_CONTENT_CHECK_ERROR_CODE,
DATABASE_CONTENT_CHECK_ERROR_CODE_NEW,
FIELD_TYPE_OPTIONS,
RW_MODE_OPTIONS_MAP,
SYSTEM_FIELDS,
} from '../../constants/database-field';
import keyExample from '../../assets/key-example.png';
import { validateFields, validateNaming } from './helpers/validate';
import { KeyTipsNode } from './components/KeyTipsNode';
import s from './index.module.less';
const MAX_COLUMNS = 20;
export interface DatabaseTableStructureProps {
data: DatabaseInfo;
botId?: string;
spaceId?: string;
creatorId?: string;
forceEdit?: boolean;
loading?: boolean;
loadingTips?: string;
projectID?: string;
/**
* excel: 单用户模式|只读模式
* normal: 单用户模式|只读模式
* expert: 单用户模式|只读模式|多用户模式
* undefined: 不支持读写模式
*/
readAndWriteModeOptions?: ReadAndWriteModeOptions;
/** databaseInfo中只显示 Mode 的UI */
onlyShowDatabaseInfoRWMode?: boolean;
enableAdd?: boolean;
isReadonlyMode?: boolean;
maxColumnNum?: number;
/**
* 是否展示基本信息(表名、介绍)
*/
showDatabaseBaseInfo?: boolean;
hiddenTableBorder?: boolean;
useComputingEnableGoToNextStep?: (
list: TableFieldsInfo,
isEmptyList: boolean,
) => void;
onCancel?: () => void;
onSave?: OnSave;
onDeleteField?: (list: TableFieldsInfo) => void;
setContentCheckErrorMsg?: (s: string) => void;
createType: CreateType;
renderModeSelect?: (props: {
dataTestId: string;
field: string;
label: string;
type: 'select';
options: BotTableRWMode[];
}) => ReactNode;
}
export interface DatabaseTableStructureRef {
validate: () => Promise<boolean>;
submit: () => Promise<void>;
isReadonly: boolean;
setTableFieldsList: (list: TableFieldsInfo) => void;
tableFieldsList: TableFieldsInfo;
tableBasicInfoFormRef: MutableRefObject<Form<TableBasicInfo>>;
}
export const DatabaseTableStructure = forwardRef<
DatabaseTableStructureRef,
DatabaseTableStructureProps
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function, complexity -- 历史文件拷贝
>((props, ref) => {
const {
data: initialData,
botId = '',
spaceId = '',
creatorId = '',
onSave,
onCancel,
onDeleteField,
forceEdit = false,
maxColumnNum = MAX_COLUMNS,
useComputingEnableGoToNextStep,
readAndWriteModeOptions,
onlyShowDatabaseInfoRWMode,
enableAdd = true,
loading = false,
setContentCheckErrorMsg = noop,
// TODO 把 AI generate 的 loading tip 放到 table 里面
// loadingTips,
createType,
showDatabaseBaseInfo,
hiddenTableBorder,
isReadonlyMode,
projectID,
renderModeSelect,
} = props;
const [tableFieldsList, setTableFieldsList] = useState<TableFieldsInfo>([]);
const inputRef = useRef<{
triggerFocus?: () => void;
}>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const tableBasicInfoFormRef = useRef<Form<TableBasicInfo>>();
const isModify = Boolean(initialData.tableId);
const [isReadonly, { setTrue: enableReadonly, setFalse: disableReadonly }] =
useBoolean(false);
// 系统字段不计入字段数量限制
const userFields = tableFieldsList.filter(i => !i.isSystemField);
const isRowMaxLimit = userFields.length >= maxColumnNum;
const isExceedRowMaxLimit = userFields.length > maxColumnNum;
const isEmptyList =
userFields.filter(i => i.name || i.desc || i.type).length <= 0;
const databaseAuditErrorCodes = [
DATABASE_CONTENT_CHECK_ERROR_CODE,
DATABASE_CONTENT_CHECK_ERROR_CODE_NEW,
];
const handleContentCheckError = (error: Error) => {
if (
isApiError(error) &&
databaseAuditErrorCodes.includes(Number(error?.code))
) {
setContentCheckErrorMsg(
error?.msg || I18n.t('knowledge_bot_update_databse_tnserr_msg'),
);
}
};
const handleAdd = (triggerFocus = true) => {
if (isReadonly) {
return;
}
const newTableFieldsList = [
...tableFieldsList,
{
nanoid: nanoid(),
name: '',
desc: '',
type: undefined as unknown as FieldItemType,
must_required: false,
},
];
setTableFieldsList(newTableFieldsList);
if (triggerFocus) {
setTimeout(() => {
inputRef.current?.triggerFocus?.();
scrollRef.current?.scrollIntoView({
block: 'end',
behavior: 'smooth',
});
}, 100);
}
};
const verifyTableFields = (trigger: TriggerType) => {
setTableFieldsList(newTableFieldsList =>
validateFields(newTableFieldsList, trigger),
);
};
const verifyAllBeforeSave = async (): Promise<boolean> => {
// 触发 tableFields 校验
const validatedTableFieldsList = validateFields(tableFieldsList, 'save');
setTableFieldsList(validatedTableFieldsList);
// 触发并校验 tableBasicInfo
if (showDatabaseBaseInfo) {
try {
// @ts-expect-error -- linter-disable-autofix
await tableBasicInfoFormRef.current.formApi.validate(['name']);
} catch (error) {
return false;
}
}
// 校验 tableFields
if (
validatedTableFieldsList.find(i =>
Object.keys(i.errorMapper || {}).find(
j => !!i?.errorMapper?.[j]?.length,
),
)
) {
return false;
}
// 校验 tableFields 是否为空
if (isEmptyList) {
return false;
}
return true;
};
const save = async () => {
// @ts-expect-error -- linter-disable-autofix
let tableBasicInfo: TableBasicInfo = {};
if (tableBasicInfoFormRef.current) {
tableBasicInfo = tableBasicInfoFormRef.current.formApi.getValues();
} else {
tableBasicInfo = {
name: initialData.name,
desc: initialData.desc,
readAndWriteMode: initialData.readAndWriteMode,
prompt_disabled: initialData.extra_info?.prompt_disabled === 'true',
};
}
if (isModify) {
sendTeaEvent(EVENT_NAMES.edit_table_click, {
need_login: true,
have_access: true,
bot_id: botId,
table_name: tableBasicInfo.name,
});
} else {
sendTeaEvent(EVENT_NAMES.create_table_click, {
need_login: true,
have_access: true,
bot_id: botId,
table_name: tableBasicInfo.name,
database_create_type: createType,
});
}
let resp;
const params: AddDatabaseRequest | UpdateDatabaseRequest = {
table_name: onlyShowDatabaseInfoRWMode
? initialData.name
: tableBasicInfo.name,
table_desc: onlyShowDatabaseInfoRWMode
? initialData.desc
: tableBasicInfo.desc,
icon_uri: initialData.icon_uri,
prompt_disabled: !!tableBasicInfo.prompt_disabled,
field_list: tableFieldsList
.filter(i => !!i.name && !i.isSystemField)
.map(i => ({
name: i.name,
desc: i.desc || '',
type: i.type,
must_required: i.must_required,
id: i?.id,
alterId: i?.alterId,
})),
rw_mode: tableBasicInfo.readAndWriteMode,
};
if (!isModify) {
try {
resp = await MemoryApi.AddDatabase({
...params,
space_id: spaceId,
creator_id: creatorId,
project_id: projectID,
});
} catch (error) {
dataReporter.errorEvent(DataNamespace.DATABASE, {
eventName: REPORT_EVENTS.DatabaseAddTable,
error: error as Error,
});
handleContentCheckError(error as Error);
return;
}
} else {
try {
resp = await MemoryApi.UpdateDatabase({
...params,
id: initialData.tableId,
});
} catch (error) {
dataReporter.errorEvent(DataNamespace.DATABASE, {
eventName: REPORT_EVENTS.DatabaseAlterTable,
error: error as Error,
});
handleContentCheckError(error as Error);
return;
}
}
if (onSave) {
await onSave({
response: resp,
});
}
onCancel?.();
};
const handleSave = async () => {
try {
enableReadonly();
const validateRes = await verifyAllBeforeSave();
if (!validateRes) {
return;
}
await save();
} finally {
disableReadonly();
}
};
const getTableNameErrorMessage = (v: string) => {
if (!v) {
return I18n.t('db_add_table_name_tips');
}
const errList = validateNaming(v);
if (errList.length > 0) {
return errList.join('; ');
}
return '';
};
// 初始化 ref 属性
useImperativeHandle<DatabaseTableStructureRef, DatabaseTableStructureRef>(
ref,
() => ({
async submit() {
return await handleSave();
},
async validate() {
const res = await verifyAllBeforeSave();
return res;
},
setTableFieldsList,
isReadonly,
// @ts-expect-error -- linter-disable-autofix
tableBasicInfoFormRef,
tableFieldsList,
}),
[isReadonly, tableFieldsList, tableBasicInfoFormRef],
);
// 校验是否 disable 下一步按钮
useComputingEnableGoToNextStep?.(tableFieldsList, isEmptyList);
const dataSource = enableAdd
? [...tableFieldsList, { operate: 'add' }]
: tableFieldsList;
const resetContentCheckErrorMsg = () => {
setContentCheckErrorMsg('');
};
useEffect(() => {
setTableFieldsList([...SYSTEM_FIELDS, ...initialData.tableMemoryList]);
if (tableBasicInfoFormRef.current && !loading) {
tableBasicInfoFormRef.current.formApi.setValues({
name: initialData?.name || '',
desc: initialData?.desc || '',
prompt_disabled: isModify
? initialData.extra_info?.prompt_disabled === 'true'
: false,
readAndWriteMode:
initialData?.readAndWriteMode || BotTableRWMode.LimitedReadWrite,
});
}
}, [initialData, loading]);
return (
<div
className={classNames(s['table-structure-wrapper'], {
[s['hidden-form-border']]: hiddenTableBorder,
})}
>
{showDatabaseBaseInfo ? (
<Form<TableBasicInfo>
// @ts-expect-error -- linter-disable-autofix
ref={tableBasicInfoFormRef}
layout="vertical"
className={s['table-structure-form']}
onValueChange={(_values, changedV) => {
if ('name' in changedV || 'desc' in changedV) {
resetContentCheckErrorMsg();
}
}}
>
{onlyShowDatabaseInfoRWMode ? null : (
<>
<FormSLInput
field="name"
label={{
text: I18n.t('db_add_table_name'),
required: true,
}}
validate={v => getTableNameErrorMessage(v)}
trigger={['change', 'blur']}
fieldClassName={s['table-name-form-field']}
disabled={isReadonlyMode}
inputProps={{
'data-testid': BotE2e.BotDatabaseAddModalTableNameInput,
disabled: !forceEdit && (isReadonly || isModify),
placeholder: I18n.t('db_add_table_name_tips'),
}}
onFocusPopoverProps={{
style: { padding: '2px 12px' },
position: 'left',
content: <KeyTipsNode />,
}}
/>
<Form.TextArea
data-testid={BotE2e.BotDatabaseAddModalTableDescInput}
field="desc"
label={I18n.t('db_add_table_desc')}
disabled={isReadonly || isReadonlyMode}
rows={2}
placeholder={I18n.t('db_add_table_desc_tips')}
fieldClassName={s['table-desc-form-field']}
/>
</>
)}
{renderModeSelect && readAndWriteModeOptions
? renderModeSelect({
dataTestId: BotE2e.BotDatabaseAddModalTableQueryModeSelect,
field: 'readAndWriteMode',
label: I18n.t('db_table_0129_001'),
type: 'select',
options: RW_MODE_OPTIONS_MAP[readAndWriteModeOptions],
})
: null}
</Form>
) : null}
{isExceedRowMaxLimit ? (
<Banner
type="warning"
description={I18n.t('db_table_0126_027', {
ColumNum: maxColumnNum,
})}
className={s['max-row-banner']}
/>
) : null}
<UITable
tableProps={{
loading,
columns: [
{
dataIndex: 'name',
title: (
<DatabaseFieldTitle
field={I18n.t('db_add_table_field_name')}
required
tip={
<div className={s['th-tip-name']}>
<span style={{ width: 494, marginBottom: 8 }}>
{I18n.t('db_add_table_field_name_tips')}
</span>
<Image
preview={false}
width={494}
height={163}
src={keyExample}
/>
</div>
}
/>
),
render: (text, record, index) =>
record.operate !== 'add' ? (
<SLInput
style={{ position: 'static' }}
onRef={inputRef}
value={record.name}
inputProps={{
'data-testid': BotE2e.BotDatabaseAddModalFieldNameInput,
'data-dtestid': BotE2e.BotDatabaseAddModalFieldNameInput,
disabled:
isReadonly || record.isSystemField || isReadonlyMode,
placeholder: 'Enter Name',
}}
errorMsgFloat
onFocusPopoverProps={{
style: { padding: '2px 12px' },
position: 'left',
content: <KeyTipsNode />,
}}
errorMsg={tableFieldsList[index]?.errorMapper?.name?.join(
'; ',
)}
handleChange={v => {
const newTableMemoryList = [...tableFieldsList];
newTableMemoryList[index].name = v;
setTableFieldsList(newTableMemoryList);
verifyTableFields('change');
resetContentCheckErrorMsg();
}}
handleBlur={() => {
verifyTableFields('blur');
}}
/>
) : (
<div
ref={scrollRef}
data-testid={BotE2e.BotDatabaseAddModalAddBtn}
>
{isRowMaxLimit ? (
<div style={{ paddingRight: 10 }}>
<Tooltip
position="top"
content={I18n.t('bot_database_add_field', {
number: maxColumnNum,
})}
>
<Button color="secondary" disabled icon={<IconAdd />}>
{I18n.t('bot_userProfile_add')}
</Button>
</Tooltip>
</div>
) : (
<Button
color="primary"
disabled={isReadonly}
onClick={() => handleAdd(true)}
icon={<IconAdd />}
>
{I18n.t('bot_userProfile_add')}
</Button>
)}
</div>
),
width: 261,
},
{
dataIndex: 'desc',
title: (
<DatabaseFieldTitle
field={I18n.t('db_add_table_field_desc')}
tip={
<article style={{ width: 327 }}>
{I18n.t('db_add_table_field_desc_tips')}
</article>
}
/>
),
render: (text, record, index) =>
record.operate !== 'add' ? (
<SLInput
value={record.desc}
maxCount={300}
inputProps={{
'data-testid': `${BotE2e.BotDatabaseAddModalFieldDescInput}.${index}.${record.name}`,
'data-dtestid': `${BotE2e.BotDatabaseAddModalFieldDescInput}.${index}.${record.name}`,
maxLength: 300,
disabled:
isReadonly || record.isSystemField || isReadonlyMode,
placeholder: I18n.t(
'bot_edit_variable_description_placeholder',
),
}}
errorMsgFloat
handleChange={v => {
const newTableMemoryList = [...tableFieldsList];
newTableMemoryList[index].desc = v;
setTableFieldsList(newTableMemoryList);
resetContentCheckErrorMsg();
}}
/>
) : null,
width: 369,
},
{
dataIndex: 'type',
title: (
<DatabaseFieldTitle
field={I18n.t('db_add_table_field_type')}
required
tip={
<article style={{ width: 327 }}>
{I18n.t('db_add_table_field_type_tips')}
</article>
}
/>
),
render: (text, record, index) =>
record.operate !== 'add' ? (
<SLSelect
value={record.type}
selectProps={{
'data-testid': `${BotE2e.BotDatabaseAddModalFieldTypeSelect}.${index}.${record.name}`,
'data-dtestid': `${BotE2e.BotDatabaseAddModalFieldTypeSelect}.${index}.${record.name}`,
disabled:
isReadonly ||
(isModify && !!record.id) ||
record.isSystemField ||
isReadonlyMode,
placeholder: I18n.t('db_table_save_exception_fieldtype'),
optionList: FIELD_TYPE_OPTIONS,
}}
errorMsgFloat
errorMsg={tableFieldsList[index]?.errorMapper?.type?.join(
'; ',
)}
handleChange={v => {
const newTableMemoryList = [...tableFieldsList];
newTableMemoryList[index].type = v as FieldItemType;
setTableFieldsList(newTableMemoryList);
verifyTableFields('change');
}}
/>
) : null,
width: 214,
},
{
dataIndex: 'must_required',
title: (
<DatabaseFieldTitle
field={I18n.t('db_add_table_field_necessary')}
tip={
<article style={{ width: 327 }}>
<p className={s['th-tip-dot']}>
{I18n.t('db_add_table_field_necessary_tips1')}
</p>
<p className={s['th-tip-dot']}>
{I18n.t('db_add_table_field_necessary_tips2')}
</p>
</article>
}
/>
),
render: (text, record, index) =>
record.operate !== 'add' ? (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Switch
data-testid={`${BotE2e.BotDatabaseAddModalFieldRequiredSwitch}.${index}.${record.name}`}
data-dtestid={`${BotE2e.BotDatabaseAddModalFieldRequiredSwitch}.${index}.${record.name}`}
disabled={
isReadonly ||
record.disableMustRequired ||
record.isSystemField ||
isReadonlyMode
}
checked={record.must_required}
onChange={v => {
const newTableMemoryList = [...tableFieldsList];
newTableMemoryList[index].must_required = v;
setTableFieldsList(newTableMemoryList);
}}
size="small"
aria-label="a switch for semi demo"
/>
</div>
) : null,
width: 108,
},
{
dataIndex: 'operate',
title: <DatabaseFieldTitle field={I18n.t('db_table_0126_021')} />,
render: (text, record, index) =>
record.operate !== 'add' ? (
<UITableAction
deleteProps={{
handleClick: () => {
if (isReadonly) {
return;
}
const newTableMemoryList = [
...tableFieldsList.slice(0, index),
...tableFieldsList.slice(index + 1),
];
setTableFieldsList(newTableMemoryList);
verifyTableFields('change');
onDeleteField?.(newTableMemoryList);
},
popconfirm: {
defaultVisible: false,
visible: false,
},
tooltip: {
content: I18n.t('datasets_table_title_actions_delete'),
},
disabled: record.isSystemField || isReadonlyMode,
}}
editProps={{
hide: true,
}}
/>
) : null,
width: 85,
},
],
dataSource,
pagination: false,
className: s['table-structure-table'],
rowKey: 'nanoid',
}}
wrapperClassName={s['table-structure-table-wrapper']}
/>
{/* 表格为空时,底部的错误提示 */}
{isEmptyList && !loading ? (
<div className={s['table-empty-tips']}>
{I18n.t('db_table_save_exception_nofield')}
</div>
) : null}
</div>
);
});

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, type PropsWithChildren } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozCross } from '@coze-arch/coze-design/icons';
import { type BannerProps, IconButton, Typography } from '@coze-arch/coze-design';
export type BannerType = NonNullable<BannerProps['type']>;
const BannerClassNames: Record<BannerType, string> = {
info: 'bg-[rgba(var(--coze-brand-1),var(--coze-brand-1-alpha))]',
warning: 'bg-[rgba(var(--coze-yellow-1),var(--coze-yellow-1-alpha))]',
danger: 'bg-[rgba(var(--coze-red-1),var(--coze-red-1-alpha))]',
success: 'bg-[rgba(var(--coze-green-1),var(--coze-green-1-alpha))]',
};
export interface DismissibleBannerProps extends PropsWithChildren {
type?: BannerType;
persistentKey: string;
className?: string;
}
export function DismissibleBanner({
type,
persistentKey,
className,
children,
}: DismissibleBannerProps) {
const [dismissed, setDismissed] = useState(
Boolean(localStorage.getItem(persistentKey)),
);
const [closed, setClosed] = useState(false);
if (dismissed || closed) {
return null;
}
return (
<div
className={classNames(
'p-[8px] flex justify-center',
BannerClassNames[type ?? 'info'],
className,
)}
>
<div className="flex grow justify-center text-[14px] leading-[20px]">
{children}
</div>
<div className="flex items-center gap-[10px] leading-none">
<Typography.Text
type="secondary"
fontSize="12px"
className="cursor-pointer"
onClick={() => {
localStorage.setItem(persistentKey, '1');
setDismissed(true);
}}
>
{I18n.t('not_show_again')}
</Typography.Text>
<IconButton
color="secondary"
size="mini"
className="!h-[unset]"
icon={<IconCozCross className="w-[16px] h-[16px]" />}
onClick={() => setClosed(true)}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,64 @@
.input-wrapper {
position: relative;
width: 100%;
// semi 原有的 disable 样式不满足需求,需要更明显的文字颜色
:global {
// .semi-input-wrapper-disabled {
// -webkit-text-fill-color: rgba(29, 28, 35, 100%);
// }
// .semi-input-suffix {
// -webkit-text-fill-color: var(--semi-color-disabled-text, rgba(56, 55, 67, 20%))
// }
}
span {
width: 100%;
}
}
.error-wrapper {
:global {
.semi-input-wrapper {
border: 1px solid @error-red;
}
}
}
.error-content {
height: 20px;
}
.error-float {
position: absolute;
width: 100%;
white-space: nowrap;
}
.error-text {
position: absolute;
padding-top: 2px;
font-size: 12px;
color: @error-red;
}
.input {
input {
overflow: hidden;
text-overflow: ellipsis;
}
}
.limit-count {
overflow: hidden;
padding-right: 12px;
padding-left: 8px;
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: var(--light-usage-text-color-text-3, rgba(28, 31, 35, 35%));
}

View File

@@ -0,0 +1,215 @@
/*
* 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 ComponentProps,
useRef,
useImperativeHandle,
useMemo,
useEffect,
type ForwardedRef,
} from 'react';
import isNumber from 'lodash-es/isNumber';
import cs from 'classnames';
import { useReactive } from 'ahooks';
import { type CommonexcludeType } from '@coze-arch/bot-semi/Form';
import {
type CommonFieldProps,
withField,
type InputProps,
Input,
type PopoverProps,
Popover,
type TooltipProps,
Tooltip,
} from '@coze-arch/coze-design';
import s from './index.module.less';
export interface SLInputRefType {
triggerFocus?: () => void;
}
export type SLInputProps = ComponentProps<typeof Input> & {
value: string | undefined;
onRef?: ForwardedRef<SLInputRefType>;
ellipsis?: boolean;
handleChange?: (v: string) => void;
handleBlur?: (v: string) => void;
handleFocus?: (v: string) => void;
ellipsisPopoverProps?: PopoverProps;
onFocusPopoverProps?: PopoverProps;
tooltipProps?: TooltipProps;
// eslint-disable-next-line @typescript-eslint/naming-convention -- 历史逻辑
inputProps?: InputProps & { 'data-dtestid'?: string; 'data-testid'?: string };
errorMsg?: string;
errorMsgFloat?: boolean;
maxCount?: number;
className?: string;
style?: React.CSSProperties;
};
// eslint-disable-next-line complexity
export const SLInput: React.FC<SLInputProps> = props => {
const { ellipsis = true, maxCount, errorMsgFloat } = props;
const showCount = isNumber(maxCount) && maxCount > 0;
useImperativeHandle(props.onRef, () => ({
triggerFocus,
}));
const $state = useReactive({
value: props.value,
inputOnFocus: false,
inputEle: false,
});
const inputRef = useRef<HTMLInputElement>(null);
const triggerFocus = () => {
$state.inputEle = true;
inputRef?.current?.focus();
};
const onFocus = () => {
$state.inputOnFocus = true;
$state.inputEle = true;
props?.handleFocus?.($state.value || '');
};
const onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
$state.inputOnFocus = false;
props?.handleBlur?.($state.value || '');
props?.onBlur?.(e);
$state.inputEle = false;
};
const onChange = (v: string) => {
$state.value = v;
props?.handleChange?.(v);
};
const onclick = () => {
if (!$state.inputEle) {
setTimeout(() => {
inputRef?.current?.focus();
}, 10);
}
$state.inputEle = true;
};
const hasEllipsis = useMemo(() => {
const clientWidth = inputRef.current?.clientWidth || 0;
const scrollWidth = inputRef.current?.scrollWidth || 0;
return clientWidth < scrollWidth - 1;
}, [
ellipsis,
$state.inputOnFocus,
$state.value,
inputRef.current?.clientWidth,
inputRef.current?.scrollWidth,
$state.inputEle,
]);
useEffect(() => {
$state.value = props.value;
}, [props.value]);
const LimitCountNode = (
<span className={s['limit-count']}>
{$state.value?.length || 0}/{maxCount}
</span>
);
return (
<div
className={cs(s['input-wrapper'], props.className)}
style={props.style}
>
{!$state.inputEle && hasEllipsis ? (
<Tooltip
content={
<article
style={{
maxWidth: 200,
wordWrap: 'break-word',
wordBreak: 'normal',
}}
>
{$state.value}
</article>
}
position={'top'}
showArrow
mouseEnterDelay={300}
{...props.tooltipProps}
>
<div
className={cs(props?.errorMsg ? s['error-wrapper'] : null)}
onClick={onclick}
>
<Input
{...props.inputProps}
ref={inputRef}
value={$state.value}
className={ellipsis ? s.input : ''}
suffix={showCount ? LimitCountNode : undefined}
></Input>
</div>
</Tooltip>
) : (
<div className={cs(props?.errorMsg ? s['error-wrapper'] : null)}>
<Popover
{...props.onFocusPopoverProps}
trigger="custom"
visible={
Boolean(props.onFocusPopoverProps?.content) && $state.inputOnFocus
}
showArrow
>
<Input
{...props.inputProps}
ref={inputRef}
value={$state.value}
className={ellipsis ? s.input : ''}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
suffix={showCount ? LimitCountNode : undefined}
></Input>
</Popover>
</div>
)}
{props?.errorMsg ? (
<div
className={cs({
[s['error-content']]: true,
[s['error-float']]: Boolean(errorMsgFloat),
})}
>
<div className={s['error-text']}>{props?.errorMsg}</div>
</div>
) : null}
</div>
);
};
// Semi 不导出被 withField 包装的组件的 props 类型(甚至是 any ´_>`
// https://github.com/DouyinFE/semi-design/blob/v2.69.2/packages/semi-ui/form/hoc/withField.tsx#L528
export const FormSLInput: React.FunctionComponent<
CommonFieldProps & Omit<SLInputProps, keyof CommonexcludeType>
> = withField(SLInput, {
valueKey: 'value',
onKeyChangeFnName: 'handleChange',
});

View File

@@ -0,0 +1,54 @@
.select-wrapper {
width: 100%;
// semi 原有的 disable 样式不满足需求,需要更明显的文字颜色
:global {
// .semi-select-disabled .semi-select-selection {
// color: rgba(29, 28, 35, 100%);
// }
.semi-select-disabled .semi-select-arrow {
color: rgba(29, 28, 35, 60%);
}
}
span {
width: 100%;
}
}
.error-wrapper {
:global {
.semi-select {
border: 1px solid @error-red;
}
}
}
.error-content {
height: 20px;
}
.error-float {
position: absolute;
width: 100%;
}
.error-text {
position: absolute;
padding-top: 2px;
font-size: 12px;
color: @error-red;
}
.selected-option {
:global {
.semi-select-option-selected {
.semi-select-option-icon {
color: #4D53E8
}
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classnames from 'classnames';
import {
Select,
type SelectProps,
type InputProps,
} from '@coze-arch/coze-design';
import s from './index.module.less';
export interface SLSelectRefType {
triggerFocus?: () => void;
}
export type SLSelectProps = InputProps & {
value: SelectProps['value'];
handleChange?: (v: SelectProps['value']) => void;
errorMsg?: string;
errorMsgFloat?: boolean;
selectProps?: SelectProps & {
// eslint-disable-next-line @typescript-eslint/naming-convention
'data-dtestid'?: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
'data-testid'?: string;
};
};
export const SLSelect: React.FC<SLSelectProps> = props => {
const { errorMsg, errorMsgFloat } = props;
return (
<div
className={classnames({
[s['select-wrapper']]: true,
[s['error-wrapper']]: Boolean(errorMsg),
})}
>
<Select
{...props.selectProps}
style={{ width: '100%' }}
value={props.value}
onChange={v => {
props?.handleChange?.(v);
}}
dropdownClassName={s['selected-option']}
/>
{errorMsg ? (
<div
className={classnames({
[s['error-content']]: true,
[s['error-float']]: Boolean(errorMsgFloat),
})}
>
<div className={s['error-text']}>{errorMsg}</div>
</div>
) : null}
</div>
);
};

View File

@@ -0,0 +1,171 @@
/*
* 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 { nanoid } from 'nanoid';
import {
type DatabaseInfo,
type TableMemoryItem,
} from '@coze-studio/bot-detail-store';
import { I18n } from '@coze-arch/i18n';
import { FieldItemType, BotTableRWMode } from '@coze-arch/bot-api/memory';
import { type ReadAndWriteModeOptions } from '../types/database-field';
export const FIELD_TYPE_OPTIONS = [
{ value: FieldItemType.Text, label: I18n.t('db_add_table_field_type_txt') },
{ value: FieldItemType.Number, label: I18n.t('db_add_table_field_type_int') },
{ value: FieldItemType.Date, label: I18n.t('db_add_table_field_type_time') },
{
value: FieldItemType.Float,
label: I18n.t('db_add_table_field_type_number'),
},
{
value: FieldItemType.Boolean,
label: I18n.t('db_add_table_field_type_bool'),
},
];
export const TEMPLATE_INFO: DatabaseInfo = {
name: 'book_notes',
desc: I18n.t('db_add_table_temp_desc'),
tableId: '',
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
tableMemoryList: [
{
nanoid: nanoid(),
name: 'name',
desc: I18n.t('db_add_table_temp_field_desc1'),
type: FieldItemType.Text,
must_required: true,
},
{
name: 'section',
nanoid: nanoid(),
desc: I18n.t('db_add_table_temp_field_desc2'),
type: FieldItemType.Number,
must_required: false,
},
{
name: 'note',
nanoid: nanoid(),
desc: I18n.t('db_add_table_temp_field_desc3'),
type: FieldItemType.Text,
must_required: false,
},
],
};
export const RW_MODE_OPTIONS_CONFIG: Record<
BotTableRWMode,
{ tips: string; label: string }
> = {
[BotTableRWMode.LimitedReadWrite]: {
tips: I18n.t('db_table_0129_005'),
label: I18n.t('db_table_0129_002'),
},
[BotTableRWMode.ReadOnly]: {
tips: I18n.t('db_table_0129_006'),
label: I18n.t('db_table_0129_003'),
},
[BotTableRWMode.UnlimitedReadWrite]: {
tips: I18n.t('db_table_0129_007'),
label: I18n.t('db_table_0129_004'),
},
[BotTableRWMode.RWModeMax]: {
tips: '',
label: '',
},
};
export const RW_MODE_OPTIONS_MAP: Record<
ReadAndWriteModeOptions,
BotTableRWMode[]
> = {
excel: [BotTableRWMode.LimitedReadWrite],
normal: [BotTableRWMode.LimitedReadWrite],
expert: [BotTableRWMode.LimitedReadWrite, BotTableRWMode.UnlimitedReadWrite],
};
export const DATABASE_CONTENT_CHECK_ERROR_CODE = 708024072;
export const DATABASE_CONTENT_CHECK_ERROR_CODE_NEW = 708334072;
/**
* 内置字段: uuid
* bstudio_connector_uid
*/
export const USER_ID_FIELD: TableMemoryItem = {
name: 'uuid',
desc: I18n.t('workflow_240221_01'),
type: FieldItemType.Text,
must_required: true,
nanoid: nanoid(),
isSystemField: true,
};
/**
* 内置字段: id
*/
export const ID_FIELD: TableMemoryItem = {
name: 'id',
desc: I18n.t('database_240520_01'),
type: FieldItemType.Number,
must_required: true,
nanoid: nanoid(),
isSystemField: true,
};
/**
* 内置字段: sys_platform
* bstudio_connector_id
*/
export const PLATFORM_FIELD: TableMemoryItem = {
name: 'sys_platform',
desc: I18n.t('db_optimize_002'),
type: FieldItemType.Text,
must_required: true,
nanoid: nanoid(),
isSystemField: true,
};
/**
* 内置字段: connector_id
* bstudio_create_time
*/
export const CREATE_TIME_FIELD: TableMemoryItem = {
name: 'bstudio_create_time',
desc: I18n.t('db_optimize_003'),
type: FieldItemType.Date,
must_required: true,
nanoid: nanoid(),
isSystemField: true,
};
/**
* 内置系统字段
*/
export const SYSTEM_FIELDS = [
ID_FIELD,
PLATFORM_FIELD,
USER_ID_FIELD,
CREATE_TIME_FIELD,
];
export const SYSTEM_FIELD_ROW_INDEX: Record<string, string | undefined> = {
id: 'bstudio_id',
sys_platform: 'bstudio_connector_id',
uuid: 'bstudio_connector_uid',
bstudio_create_time: 'bstudio_create_time',
};

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { FIELD_TYPE_OPTIONS } from './database-field';
export { TEMPLATE_INFO } from './database-field';
export { SYSTEM_FIELDS, SYSTEM_FIELD_ROW_INDEX } from './database-field';
export { PLATFORM_FIELD } from './database-field';

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const DatabaseDetailWaring = () => <></>;

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type DatabaseInfo } from '@coze-studio/bot-detail-store';
import {
type AlterBotTableResponse,
type InsertBotTableResponse,
} from '@coze-arch/bot-api/memory';
export type OnSave = (params: {
response: InsertBotTableResponse | AlterBotTableResponse;
}) => Promise<void>;
/* eslint-disable @typescript-eslint/naming-convention -- 历史文件拷贝 */
export enum CreateType {
custom = 'custom',
template = 'template',
excel = 'excel',
// 推荐建表
recommend = 'recommend',
// 输入自然语言建表
naturalLanguage = 'naturalLanguage',
}
/* eslint-enable @typescript-eslint/naming-convention -- 历史文件拷贝 */
export interface MapperItem {
label: string;
key: string;
validator: {
type: VerifyType;
message: string;
}[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 历史文件拷贝
defaultValue: any;
require: boolean;
}
export type TableBasicInfo = Pick<
DatabaseInfo,
'name' | 'desc' | 'readAndWriteMode'
> & { prompt_disabled: boolean };
export type TableFieldsInfo = DatabaseInfo['tableMemoryList'];
export enum VerifyType {
Required = 1,
Unique = 2,
Naming = 3,
}
export type TriggerType = 'blur' | 'change' | 'save';
export interface NL2DBInfo {
prompt: string;
}
export type ReadAndWriteModeOptions = 'excel' | 'normal' | 'expert';

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 数据库详情页 tab
*/
export enum DatabaseTabs {
/** 表结构 */
Structure = 'structure',
/** 测试数据 */
Draft = 'draft',
/** 线上数据 */
Online = 'online',
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { DatabaseTabs } from './database-tabs';
export { CreateType } from './database-field';
export { type NL2DBInfo } from './database-field';
export { type OnSave } from './database-field';

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
import { CustomError } from '@coze-arch/bot-error';
export const getBase64 = (file: Blob): Promise<string> =>
new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = event => {
const result = event.target?.result;
if (!result || typeof result !== 'string') {
reject(
new CustomError(ReportEventNames.parmasValidation, 'file read fail'),
);
return;
}
resolve(result.replace(/^.*?,/, ''));
};
fileReader.readAsDataURL(file);
});

View File

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