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,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 CSSProperties, type PropsWithChildren } from 'react';
import classNames from 'classnames';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { type ButtonProps, type Theme } from '@coze-arch/bot-semi/Button';
import { UIButton } from '@coze-arch/bot-semi';
import { BotE2e } from '@coze-data/e2e';
import s from './index.module.less';
interface AddButtonProps {
onClick?: () => void;
className?: string;
style?: CSSProperties;
theme?: Theme;
icon?: React.ReactNode;
disabled?: boolean;
}
export const AddButton: React.FC<
PropsWithChildren<AddButtonProps & ButtonProps>
> = ({
onClick,
className,
style,
children,
theme,
icon,
disabled,
type,
...props
}) => {
const isReadonly = useBotDetailIsReadonly();
if (isReadonly) {
return null;
}
return (
<UIButton
data-testid={BotE2e.BotVariableAddModalAddBtn}
disabled={disabled}
style={style}
className={classNames(s.add, className)}
type={type || 'tertiary'}
theme={theme || 'light'}
icon={icon}
onClick={onClick}
{...props}
>
{children}
</UIButton>
);
};

View File

@@ -0,0 +1,3 @@
.add {
min-width: 96px;
}

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 { AddButton } from './add-button';

View File

@@ -0,0 +1,172 @@
/*
* 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 ConnectorConfigStatus } from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { Button, type ButtonProps } from '@coze-arch/coze-design';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { useUIModal, UIButton, Typography } from '@coze-arch/bot-semi';
import {
AuthStatus,
type AuthLoginInfo,
ConfigStatus,
} from '@coze-arch/bot-api/developer_api';
import { IconAlertCircle } from '@douyinfe/semi-icons';
import {
checkAuthInfoValid,
executeAuthRedirect,
logAndToastAuthInfoError,
useRevokeAuth,
} from '../../util/auth';
export interface AuthorizeButtonProps {
origin: 'setting' | 'publish';
id: string;
agentType?: 'bot' | 'project';
channelName: string;
status: ConfigStatus | AuthStatus | ConnectorConfigStatus;
revokeSuccess: (id: string) => void;
authInfo: AuthLoginInfo;
isMouseIn?: boolean;
/** 是否使用 Coze 2.0 的 Button 组件,默认 false */
isV2?: boolean;
/** 自定义 Coze 2.0 Button 的 props */
v2ButtonProps?: ButtonProps;
onBeforeAuthRedirect?: (
parameters: Pick<AuthorizeButtonProps, 'id' | 'authInfo' | 'origin'>,
) => void;
}
export const AuthorizeButton = ({
status,
id,
agentType = 'bot',
channelName,
revokeSuccess,
origin,
authInfo,
isMouseIn = true,
isV2 = false,
v2ButtonProps = {
color: 'highlight',
size: 'small',
},
onBeforeAuthRedirect,
}: AuthorizeButtonProps) => {
const isConfiguredOrConfiguring = [
ConfigStatus.Configured,
ConfigStatus.Configuring,
].includes(status as ConfigStatus);
const handleAuth = () => {
if (!checkAuthInfoValid(authInfo)) {
logAndToastAuthInfoError();
return;
}
if (
(origin === 'publish' && status === ConfigStatus.NotConfigured) ||
(origin === 'setting' && status === AuthStatus.Unauthorized)
) {
sendTeaEvent(
origin === 'publish'
? EVENT_NAMES.publish_oauth_button_click
: EVENT_NAMES.settings_oauth_button_click,
{ action: '授权', channel_name: channelName },
);
onBeforeAuthRedirect?.({ id, authInfo, origin });
executeAuthRedirect({ id, authInfo, origin });
}
if (
(origin === 'publish' && isConfiguredOrConfiguring) ||
(origin === 'setting' && status === AuthStatus.Authorized)
) {
sendTeaEvent(
origin === 'publish'
? EVENT_NAMES.publish_oauth_button_click
: EVENT_NAMES.settings_oauth_button_click,
{ action: '解除授权', channel_name: channelName },
);
openRevokeAuthModal();
}
};
const { revokeLoading, runRevoke } = useRevokeAuth({
id,
onRevokeSuccess: revokeSuccess,
onRevokeFinally: () => closeRevokeAuthModal(),
});
const {
open: openRevokeAuthModal,
close: closeRevokeAuthModal,
modal: revokeModal,
visible: revokeModalVisible,
} = useUIModal({
confirmLoading: revokeLoading,
type: 'info',
title: I18n.t('user_revoke_authorization_title'),
onOk: runRevoke,
okText: I18n.t('Confirm'),
cancelText: I18n.t('Cancel'),
icon: (
<IconAlertCircle
style={{ color: 'var(--semi-color-danger)' }}
size="extra-large"
/>
),
onCancel: () => {
closeRevokeAuthModal();
},
okButtonProps: {
type: 'danger',
},
});
const buttonText = I18n.t(
isConfiguredOrConfiguring
? 'bot_publish_columns_action_revoke_authorize'
: 'bot_publish_columns_action_authorize',
);
const authButton = isV2 ? (
<Button onClick={handleAuth} {...v2ButtonProps}>
{buttonText}
</Button>
) : (
<UIButton onClick={handleAuth} theme="borderless">
{buttonText}
</UIButton>
);
return status === ConfigStatus.Configured ? (
<>
{/* 在 hover 渠道表单对应行,或“撤销授权”弹窗显示中时,显示“撤销授权”按钮 */}
{isMouseIn || revokeModalVisible ? authButton : null}
{revokeModal(
agentType === 'project' ? (
<Typography.Text type="secondary">
{I18n.t('project_release_cancel1_desc')}
</Typography.Text>
) : null,
)}
</>
) : (
authButton
);
};

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { I18n } from '@coze-arch/i18n';
import { Tooltip } from '@coze-arch/coze-design';
import { Popconfirm, UIIconButton } from '@coze-arch/bot-semi';
import { IconStopOutlined, IconAuto } from '@coze-arch/bot-icons';
import commonStyles from '../../assets/styles/index.module.less';
interface AutoGenerateProps {
needConfirmAgain: boolean;
confirmAgainTexts: {
title: string;
content: string;
};
autoTrigger: boolean;
loading: boolean;
setLoading?: (autoLoading: boolean) => void;
generate: () => void;
cancel: () => void;
}
export const AutoGenerateButton: React.FC<AutoGenerateProps> = ({
needConfirmAgain,
confirmAgainTexts,
loading,
autoTrigger = false,
setLoading,
generate,
cancel,
}) => {
const isReadonly = useBotDetailIsReadonly();
useEffect(() => {
setLoading?.(loading);
}, [loading]);
const handleClick = () => {
// loading时 触发stop生成
if (loading) {
cancel();
return;
}
// 有开场白时 点击触发二次确认弹窗
if (needConfirmAgain) {
return;
}
// 其余触发自动生成开场白逻辑
generate();
};
const btn = (
<span>
<Tooltip
content={
loading
? I18n.t('stop_generating')
: I18n.t('bot_edit_opening_tooltip')
}
>
<UIIconButton
className={commonStyles['icon-button-16']}
iconSize="small"
icon={loading ? <IconStopOutlined /> : <IconAuto />}
onClick={handleClick}
>
{autoTrigger
? loading
? I18n.t('stop_generating')
: I18n.t('bot_edit_opening_tooltip')
: null}
</UIIconButton>
</Tooltip>
</span>
);
return needConfirmAgain && !loading ? (
<Popconfirm
disabled={isReadonly}
trigger="click"
okType="danger"
okText={I18n.t('bot_opening_remarks_replace_confirm_button')}
cancelText={I18n.t('bot_opening_remarks_replace_cancel_button')}
onConfirm={generate}
{...confirmAgainTexts}
>
{btn}
</Popconfirm>
) : (
<span style={{ display: 'inline-block' }}>{btn}</span>
);
};

View File

@@ -0,0 +1,16 @@
.error-container {
.error-link {
.error-link-underline {
text-decoration: underline;
color: var(--semi-color-danger);
font-size: 14px;
margin-left: 2px;
max-width: 200px;
a {
color: var(--semi-color-danger);
}
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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 BindConnectorResponse,
type GetBindConnectorConfigResponse,
type SaveBindConnectorConfigResponse,
} from '@coze-arch/idl/developer_api';
import { I18n } from '@coze-arch/i18n';
import { Form, Typography } from '@coze-arch/bot-semi';
import { type ApiError } from '@coze-arch/bot-http';
import styles from './index.module.less';
type ErrorResponse =
| GetBindConnectorConfigResponse
| SaveBindConnectorConfigResponse
| BindConnectorResponse;
function isBindConnectorResponse(
res: ErrorResponse,
): res is BindConnectorResponse {
return ['bind_bot_id', 'bind_bot_name', 'bind_space_id'].every(
key => key in res,
);
}
export interface ConnectorErrorProps {
errorMessage: ApiError;
}
export const ConnectorError = ({ errorMessage }: ConnectorErrorProps) => {
const res = (errorMessage?.raw ?? {}) as ErrorResponse;
return (
<Form.ErrorMessage
error={
isBindConnectorResponse(res) ? (
<div className={styles['error-link']}>
{I18n.t('bot_publish_bind_error', {
bot_name: (
<Typography.Text
className={styles['error-link-underline']}
link={{
href: `/space/${res.bind_space_id}/${res.bind_agent_type === 1 ? 'project-ide' : 'bot'}/${res.bind_bot_id}`,
}}
ellipsis={{
showTooltip: {
opts: {
content: res.bind_bot_name,
},
},
}}
>
{res.bind_bot_name}
</Typography.Text>
),
key_name: 'token',
})}
</div>
) : (
errorMessage?.msg
)
}
className={styles['error-container']}
/>
);
};

View File

@@ -0,0 +1,28 @@
.disable-field {
padding: 12px 0 24px;
.title {
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
}
}
.input {
&& {
padding-top: 4px;
}
:global {
.semi-input-suffix {
cursor: pointer;
padding: 8px;
}
}
}
.link-button {
&&& {
background-color: transparent;
}
}

View File

@@ -0,0 +1,198 @@
/*
* 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 { logger } from '@coze-arch/logger';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import { IconCozTrashCan, IconCozPlus } from '@coze-arch/coze-design/icons';
import { TagGroup, ArrayField, Button } from '@coze-arch/coze-design';
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
import { type RuleItem } from '@coze-arch/bot-semi/Form';
import { UIFormInput, Form, Typography } from '@coze-arch/bot-semi';
import {
type Options,
type FormSchemaItem,
} from '@coze-arch/bot-api/developer_api';
import { type TFormData } from '../types';
import styles from './index.module.less';
function formatMultiSelectValue(rawValue: string, enums?: Options[]) {
const arrayValue = typeSafeJSONParse(rawValue) as string[] | undefined;
if (!arrayValue) {
return [];
}
return arrayValue.map(value => ({
children: enums?.find(option => option.value === value)?.label ?? value,
}));
}
export interface ConnectorFieldProps {
formItemSchema: FormSchemaItem;
isReadOnly: boolean;
initValue?: TFormData;
}
export const ConnectorField = (props: ConnectorFieldProps) => {
const { formItemSchema, isReadOnly, initValue } = props;
const rawInitValue = initValue?.[formItemSchema.name];
if (isReadOnly) {
return (
<div className={styles['disable-field']}>
<div className={styles.title}>{formItemSchema.title}</div>
{formItemSchema.type === 'array' ? (
<TagGroup
tagList={formatMultiSelectValue(rawInitValue, formItemSchema.enums)}
/>
) : (
<Typography.Text
style={{ width: '100%' }}
ellipsis={{
showTooltip: {
opts: {
content: rawInitValue,
style: { wordBreak: 'break-word' },
},
},
}}
>
{rawInitValue}
</Typography.Text>
)}
</div>
);
}
function createRules(fieldSchema: FormSchemaItem): RuleItem[] {
// 确保 formItemSchema.rules 是一个数组
const itemRules = fieldSchema.rules ?? [];
const rules = itemRules.map(rule => {
const ruleMessage = rule.message
? I18n.t(rule.message as I18nKeysNoOptionsType, {
field: fieldSchema.name,
})
: undefined;
return { ...rule, ...(ruleMessage && { message: ruleMessage }) };
});
// 添加 'required' 规则
rules.push({
required: fieldSchema.required,
message: I18n.t('bot_publish_field_placeholder', {
field: fieldSchema.title ?? '',
}),
});
return rules as RuleItem[];
}
if (!formItemSchema.name) {
return null;
}
switch (formItemSchema.component) {
case 'Input':
if (formItemSchema.type === 'array') {
let values: string[] = [];
try {
values = JSON.parse(rawInitValue);
} catch (e) {
logger.error({ error: e as Error });
values = [];
}
// 添加一个默认空值
if (!values.length) {
values.push('');
}
return (
<ArrayField field={formItemSchema.name} initValue={values}>
{({ arrayFields, add }) => (
<>
{arrayFields.map(({ key, field, remove }, i) => (
<UIFormInput
key={key}
placeholder={I18n.t('bot_publish_field_placeholder', {
field: formItemSchema.title ?? '',
})}
field={field}
label={formItemSchema.title}
noLabel={i > 0}
required={formItemSchema.required}
rules={createRules(formItemSchema)}
fieldClassName={styles.input}
suffix={
arrayFields.length <= 1 ? null : (
<IconCozTrashCan onClick={remove} />
)
}
/>
))}
<Button
className={styles['link-button']}
color="highlight"
size="small"
icon={<IconCozPlus />}
onClick={add}
>
{I18n.t('binding_add_card')}
</Button>
</>
)}
</ArrayField>
);
}
return (
<UIFormInput
key={formItemSchema.name}
placeholder={I18n.t('bot_publish_field_placeholder', {
field: formItemSchema.title ?? '',
})}
field={formItemSchema.name}
label={formItemSchema.title}
required={formItemSchema.required}
showClear
rules={createRules(formItemSchema)}
initValue={rawInitValue}
/>
);
case 'Select': {
const isMultiple = formItemSchema.type === 'array';
const selectInitValue = isMultiple
? (typeSafeJSONParse(rawInitValue) as string[] | undefined)
: rawInitValue;
return (
<Form.Select
key={formItemSchema.name}
placeholder={`Enter ${formItemSchema.title}`}
field={formItemSchema.name}
label={formItemSchema.title}
optionList={formItemSchema.enums}
multiple={isMultiple}
rules={createRules(formItemSchema)}
initValue={selectInitValue}
/>
);
}
default:
return null;
}
};

View File

@@ -0,0 +1,29 @@
.step-order {
margin-top: 2px;
display: flex;
width: 16px;
height: 16px;
justify-content: center;
align-items: center;
border-radius: 50%;
background: var(--light-color-brand-brand-5, #4d53e8);
color: var(--light-color-white-white, #fff);
font-size: 10px;
font-weight: 600;
}
.step-title {
color: #000;
font-size: 14px;
font-weight: 600;
line-height: 22px;
margin-bottom: 8px;
}
.markdown {
color: var(--light-usage-text-color-text-0, #1D1C23);
font-size: 14px;
line-height: 22px;
}

View File

@@ -0,0 +1,145 @@
/*
* 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 -- ignore */
import ReactMarkdown from 'react-markdown';
import {
forwardRef,
type Ref,
useRef,
useImperativeHandle,
useEffect,
} from 'react';
import { useUpdate } from 'ahooks';
import type { FormApi } from '@coze-arch/bot-semi/Form';
import { Space, Form } from '@coze-arch/bot-semi';
import { type ApiError } from '@coze-arch/bot-http';
import { type SchemaAreaInfo } from '@coze-arch/bot-api/developer_api';
import { type FormActions, type TFormData } from '../types';
import { ConnectorField } from '../connector-field';
import styles from './index.module.less';
export interface ConnectorFormProps {
schemaAreaInfo?: SchemaAreaInfo;
initValue?: TFormData;
getFormDisable: (disable: boolean) => void;
isReadOnly: boolean;
setErrorMessage: (error?: ApiError) => void;
}
const DEFAULT_FORM_STEP = 2;
// 多选 Select 在 Form 中的 value 是 string[],但提交到后端需要转换成 JSON string
type FormValues = Record<string, string | string[]>;
export const ConnectorForm = forwardRef(
(props: ConnectorFormProps, ref: Ref<FormActions>) => {
const {
schemaAreaInfo,
initValue,
getFormDisable,
isReadOnly,
setErrorMessage,
} = props;
const formApiRef = useRef<FormApi<FormValues>>();
const update = useUpdate();
useImperativeHandle<FormActions, FormActions>(ref, () => ({
submit: async () => {
const values = await formApiRef.current?.validate();
return Object.fromEntries(
Object.entries(values ?? {}).map(([key, value]) => [
key,
Array.isArray(value) ? JSON.stringify(value) : value,
]),
);
},
reset: () => formApiRef.current?.reset(),
}));
useEffect(() => {
// 解决formApiRef.current取值不实时问题
update();
// eslint-disable-next-line react-hooks/exhaustive-deps -- ignore
}, [schemaAreaInfo]);
const formDisabled =
schemaAreaInfo?.schema_list
?.filter(item => item.required)
.some(field => {
const value = formApiRef.current?.getValue(field.name);
if (Array.isArray(value)) {
return !value.length || (value.length === 1 && !value[0]);
}
return !value;
}) || !schemaAreaInfo?.schema_list?.length;
useEffect(() => {
getFormDisable(formDisabled);
// eslint-disable-next-line react-hooks/exhaustive-deps -- ignore
}, [formDisabled]);
return (
<div>
{schemaAreaInfo?.title_text ? (
<Space spacing={12} align="start">
<span className={styles['step-order']}>
{schemaAreaInfo.step_order || DEFAULT_FORM_STEP}
</span>
<div className={styles['step-content']}>
<div className={styles['step-title']}>
{schemaAreaInfo.title_text}
</div>
</div>
</Space>
) : null}
{schemaAreaInfo?.description ? (
<ReactMarkdown skipHtml={true} className={styles.markdown}>
{schemaAreaInfo?.description}
</ReactMarkdown>
) : null}
{schemaAreaInfo?.schema_list?.length ? (
<Form<FormValues>
initValues={initValue}
className={styles['config-form']}
onValueChange={() => {
update();
setErrorMessage(undefined);
}}
getFormApi={formApi => (formApiRef.current = formApi)}
autoScrollToError
allowEmpty
>
{schemaAreaInfo?.schema_list?.map(item => (
<ConnectorField
initValue={initValue}
formItemSchema={item}
isReadOnly={isReadOnly}
key={item.name}
/>
))}
</Form>
) : null}
</div>
);
},
);

View File

@@ -0,0 +1,22 @@
.start-text {
color: var(--light-usage-text-color-text-0, #1D1C23);
font-size: 14px;
line-height: 22px;
}
.config-link {
color: var(--light-color-brand-brand-5, #4D53E8);
font-size: 12px;
line-height: 16px;
}
.markdown {
color: var(--light-usage-text-color-text-0, #1D1C23);
font-size: 14px;
line-height: 22px;
}
.guide {
margin-bottom: 32px;
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ReactMarkdown from 'react-markdown';
import { Typography } from '@coze-arch/bot-semi';
import { type QuerySchemaConfig } from '@coze-arch/bot-api/developer_api';
import styles from './index.module.less';
export const ConnectorGuide = ({
connectorConfigInfo = {},
}: {
connectorConfigInfo?: QuerySchemaConfig;
}) => (
<div className={styles.guide}>
{connectorConfigInfo?.start_text ? (
<ReactMarkdown
skipHtml={true}
linkTarget="_blank"
className={styles.markdown}
>
{connectorConfigInfo?.start_text}
</ReactMarkdown>
) : null}
{connectorConfigInfo?.guide_link_url &&
connectorConfigInfo?.guide_link_text ? (
<div>
<Typography.Text
link={{
href: connectorConfigInfo?.guide_link_url,
}}
className={styles['config-link']}
>
{connectorConfigInfo?.guide_link_text}
</Typography.Text>
</div>
) : null}
</div>
);

View File

@@ -0,0 +1,49 @@
.step-order {
margin-top: 2px;
display: flex;
width: 16px;
height: 16px;
justify-content: center;
align-items: center;
border-radius: 50%;
background: var(--light-color-brand-brand-5, #4d53e8);
color: var(--light-color-white-white, #fff);
font-size: 10px;
font-weight: 600;
}
.step-title {
color: #000;
font-size: 14px;
font-weight: 600;
line-height: 22px;
margin-bottom: 8px;
}
.markdown {
color: var(--light-usage-text-color-text-0, #1D1C23);
font-size: 14px;
line-height: 22px;
}
.link-area .link-list {
margin-top: 16px;
.title {
color: var(--light-usage-text-color-text-0, #1D1C23);
font-size: 14px;
font-weight: 600;
line-height: 22px;
}
.link {
word-break: break-word;
}
.semi-form-field-error-message {
position: absolute;
}
}

View File

@@ -0,0 +1,91 @@
/*
* 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 ReactMarkdown from 'react-markdown';
import { Space, Typography } from '@coze-arch/bot-semi';
import { type CopyLinkAreaInfo } from '@coze-arch/bot-api/developer_api';
import { type TFormData } from '../types';
import styles from './index.module.less';
export const ConnectorLink = ({
copyLinkAreaInfo = {},
agentType = 'bot',
botId = '',
initValue = {},
}: {
copyLinkAreaInfo?: CopyLinkAreaInfo;
agentType?: 'bot' | 'project';
botId: string;
initValue?: TFormData;
}) => {
//支持通配URL
const formatUrl = (url?: string) => {
let newUrl = url ?? '';
if (newUrl) {
if (agentType === 'project') {
newUrl = newUrl.replace(/{project_id}/g, botId);
} else {
newUrl = newUrl.replace(/{bot_id}/g, botId);
}
newUrl = newUrl
.replace(/{hostname}/g, window.location.hostname)
.replace(/{corp_id}/g, initValue.corp_id);
}
return newUrl;
};
return (
<div className={styles['link-area']}>
{copyLinkAreaInfo?.title_text ? (
<Space spacing={12} align="start">
<span className={styles['step-order']}>
{copyLinkAreaInfo.step_order || 1}
</span>
<div className={styles['step-content']}>
<div className={styles['step-title']}>
{copyLinkAreaInfo.title_text}
</div>
</div>
</Space>
) : null}
{copyLinkAreaInfo?.description ? (
<ReactMarkdown skipHtml={true} className={styles.markdown}>
{copyLinkAreaInfo.description}
</ReactMarkdown>
) : null}
{copyLinkAreaInfo?.link_list?.length ? (
<div className={styles['link-list']}>
{copyLinkAreaInfo?.link_list.map(item => (
<div key={item.link} style={{ marginBottom: 32 }}>
<Typography.Title className={styles.title}>
{item.title}
</Typography.Title>
<Typography.Text className={styles.link} copyable>
{formatUrl(item.link)}
</Typography.Text>
</div>
))}
</div>
) : null}
</div>
);
};

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import { useRequest } from 'ahooks';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import {
type SchemaAreaPage,
SchemaAreaPageApi,
type GetBindConnectorConfigResponse,
type SaveBindConnectorConfigResponse,
type BindConnectorResponse,
} from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import { useParams } from 'react-router-dom';
export type ActionResponse =
| {
action: SchemaAreaPageApi.BindConnector;
data: BindConnectorResponse;
}
| {
action: SchemaAreaPageApi.GetBindConnectorConfig;
data: GetBindConnectorConfigResponse;
}
| {
action: SchemaAreaPageApi.SaveBindConnectorConfig;
data: SaveBindConnectorConfigResponse;
}
| {
action: SchemaAreaPageApi.NotQuery;
data: undefined;
};
interface StepActionProps {
botId: string;
origin?: 'bot' | 'project';
schemaPages: SchemaAreaPage[];
onNextStepSuccess: (resp: ActionResponse) => void;
onNextStepError: (error: Error) => void;
}
interface StepRunParams {
connectorId: string;
assignFormValue: Record<string, string>;
}
export const useStepAction = ({
botId,
origin = 'bot',
schemaPages,
onNextStepSuccess,
onNextStepError,
}: StepActionProps) => {
const [step, setStep] = useState(0);
const { space_id = '' } = useParams<DynamicParams>();
const agentType = origin === 'bot' ? 0 : 1;
const currentAction =
schemaPages?.[step]?.api_action ?? SchemaAreaPageApi.BindConnector;
const SERVICE_MAP = {
[SchemaAreaPageApi.NotQuery]: async () => await Promise.resolve(),
[SchemaAreaPageApi.GetBindConnectorConfig]: async (
params?: StepRunParams,
) => {
const data = await DeveloperApi.GetBindConnectorConfig({
connector_id: params?.connectorId ?? '',
detail: params?.assignFormValue ?? {},
agent_type: agentType,
bot_id: botId,
space_id,
});
return data;
},
[SchemaAreaPageApi.SaveBindConnectorConfig]: async (
params?: StepRunParams,
) => {
const data = await DeveloperApi.SaveBindConnectorConfig({
connector_id: params?.connectorId ?? '',
detail: params?.assignFormValue ?? {},
agent_type: agentType,
bot_id: botId,
space_id,
});
return data;
},
[SchemaAreaPageApi.BindConnector]: async (params?: StepRunParams) => {
const res = await DeveloperApi.BindConnector(
{
connector_id: params?.connectorId ?? '',
connector_info: params?.assignFormValue ?? {},
agent_type: agentType,
bot_id: botId,
space_id,
},
{ __disableErrorToast: true },
);
return res;
},
};
const { run, loading } = useRequest(
async (params?: StepRunParams) => await SERVICE_MAP[currentAction](params),
{
manual: true,
ready: Object.keys(SERVICE_MAP).includes(String(currentAction)),
onSuccess: data => {
const action = currentAction as
| SchemaAreaPageApi.BindConnector
| SchemaAreaPageApi.GetBindConnectorConfig;
onNextStepSuccess?.({ data, action });
},
onError: error => {
onNextStepError(error);
},
},
);
return {
run,
loading,
step,
setStep,
};
};

View File

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

View File

@@ -0,0 +1,350 @@
/*
* 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, useState } from 'react';
import { useRequest } from 'ahooks';
import { type PublishConnectorInfo } from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { UIButton, useUIModal, UIToast, Spin } from '@coze-arch/bot-semi';
import { isApiError, type ApiError } from '@coze-arch/bot-http';
import {
type PublishConnectorInfo as BotPublishConnectorInfo,
type QuerySchemaConfig,
BindType,
SchemaAreaPageApi,
type BindConnectorResponse,
type SchemaAreaInfo,
type CopyLinkAreaInfo,
} from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import { connector2Redirect } from '@coze-foundation/account-adapter';
import styles from '../../pages/publish/index.module.less';
import { useUnbindPlatformModal } from '../../hook/use-unbind-platform';
import { type FormActions, type TSubmitValue } from './types';
import { type ActionResponse, useStepAction } from './hooks/use-step-action';
import { ConnectorLink } from './connector-link';
import { ConnectorGuide } from './connector-guide';
import { ConnectorForm } from './connector-form';
import { ConnectorError } from './connector-error';
interface ConnectorConfigureProps {
botId: string;
origin?: 'project' | 'bot';
onSuccess: (
val: BotPublishConnectorInfo | PublishConnectorInfo | undefined,
) => void;
onUnbind?: () => void;
}
interface ConnectorConfigureValueType {
initValue: BotPublishConnectorInfo | PublishConnectorInfo;
}
// eslint-disable-next-line complexity
export const useConnectorFormModal = ({
botId,
origin = 'bot',
onSuccess,
onUnbind,
}: ConnectorConfigureProps) => {
const formRef = useRef<FormActions>(null);
const [propsValue, setPropsValue] = useState<ConnectorConfigureValueType>();
const { initValue } = propsValue ?? {};
const [errorMessage, setErrorMessage] = useState<ApiError>();
const [formDisabled, setFormDisabled] = useState(false);
const [assignValue, setAssignValue] = useState<TSubmitValue>();
const bindId = useRef('');
const handleClose = () => {
setErrorMessage(undefined);
setStep(0);
setAssignValue(undefined);
formRef.current?.reset();
close();
};
const handleUnbind = () => {
handleClose();
if (onUnbind) {
onUnbind();
} else {
// 兼容历史逻辑,未传入 onUnbind 时,解绑后也调用 onSuccess
onSuccess({
...(initValue as BotPublishConnectorInfo),
bind_info: {},
bind_id: '',
});
}
UIToast.success(I18n.t('bot_publish_disconnect_success'));
};
const [connectorConfigInfo, setConnectorConfigInfo] =
useState<QuerySchemaConfig>();
const lastConnectId = useRef<string>();
const { loading: formSchemaLoading } = useRequest(
async () => {
const data = await DeveloperApi.QuerySchemaList({
connector_id: initValue?.id ?? '',
scene: origin,
});
return data;
},
{
ready: Boolean(initValue?.id),
refreshDeps: [initValue?.id],
onBefore: () => {
if (initValue?.id !== lastConnectId.current) {
lastConnectId.current = initValue?.id;
setConnectorConfigInfo({});
}
},
onSuccess: data => {
if (!data.schema_area_pages?.length) {
data.schema_area_pages = [
{
schema_area: data.schema_area,
copy_link_area: data.copy_link_area,
},
];
}
setConnectorConfigInfo(data);
},
onError: () => {
setConnectorConfigInfo({});
},
},
);
const { schema_area_pages: schemaPages = [] } = connectorConfigInfo ?? {};
const bindCb = (data: BindConnectorResponse) => {
/** 适用Kv+Auth授权场景KvAuthBind = 4
* reddit渠道若成功返回client_id则覆盖auth_login_info中的client_id并附带加密state跳转授权页面
* 其余渠道若成功返回auth_params则合并auth_login_info作为授权链接参数跳转
* */
if (
initValue?.bind_type === BindType.KvAuthBind &&
(data?.client_id || data?.auth_params)
) {
connector2Redirect(
{
navigatePath: `${location.pathname}${location.search}`,
type: 'oauth',
extra: {
origin: 'publish',
encrypt_state: data?.encrypt_state,
},
},
initValue?.id || '',
{
...initValue?.auth_login_info,
client_id: data?.client_id,
...data.auth_params,
},
);
} else {
bindId.current = data?.bind_id ?? '';
}
};
const stepCallback = () => {
const isLastStep = step === schemaPages?.length - 1;
if (isLastStep) {
if (initValue) {
onSuccess({
...initValue,
bind_info: { ...assignValue },
bind_id: bindId.current,
});
}
handleClose();
} else {
setStep(step + 1);
}
};
const {
loading,
run: nextStepRun,
step,
setStep,
} = useStepAction({
botId,
origin,
schemaPages,
onNextStepSuccess: (resp: ActionResponse) => {
if (resp.action === SchemaAreaPageApi.BindConnector) {
bindCb(resp.data);
}
if (resp.action === SchemaAreaPageApi.GetBindConnectorConfig) {
setAssignValue({
...assignValue,
...resp.data.config?.detail,
});
}
stepCallback();
},
onNextStepError: error => {
if (isApiError(error)) {
setErrorMessage(error);
}
},
});
const { node: unbindPlatformModal, open: openUnbindPlatformModal } =
useUnbindPlatformModal({
botId,
origin,
platformInfo: initValue as BotPublishConnectorInfo,
onUnbind: () => {
handleUnbind();
},
});
const nextBtnClick = async () => {
const value = await formRef.current?.submit();
setAssignValue({ ...assignValue, ...value });
nextStepRun({
connectorId: initValue?.id ?? '',
assignFormValue: { ...assignValue, ...value },
});
};
const renderFooter = () =>
initValue?.bind_id ? (
<>
<UIButton
theme="light"
type="tertiary"
onClick={() => {
close();
setStep(0);
}}
>
{I18n.t('Cancel')}
</UIButton>
<UIButton theme="solid" type="danger" onClick={openUnbindPlatformModal}>
{I18n.t('bot_publish_disconnect', {
platform: initValue?.name ?? '',
})}
</UIButton>
</>
) : (
<>
{schemaPages?.length &&
step !== 0 &&
schemaPages[step]?.api_action !== SchemaAreaPageApi.NotQuery ? (
// 页面按钮不执行任何操作时 不展示上一步
<UIButton
theme="solid"
onClick={() => {
setErrorMessage(undefined);
setStep(step - 1);
}}
>
{I18n.t('Previous_1')}
</UIButton>
) : null}
<UIButton
theme="solid"
onClick={nextBtnClick}
disabled={formDisabled}
loading={loading}
>
{step === (schemaPages?.length ?? 0) - 1
? schemaPages[step]?.api_action !== SchemaAreaPageApi.NotQuery
? I18n.t('Save')
: I18n.t('Complete')
: I18n.t('Next_1')}
</UIButton>
</>
);
const { modal, open, close } = useUIModal({
type: 'action-small',
footer: renderFooter(),
onCancel: handleClose,
title: connectorConfigInfo?.title_text,
});
const renderConnectorArea = (
copyArea?: CopyLinkAreaInfo,
schemaArea?: SchemaAreaInfo,
) => (
<>
{copyArea ? (
<ConnectorLink
copyLinkAreaInfo={copyArea}
agentType={origin}
botId={botId}
initValue={{ ...initValue?.bind_info, ...assignValue }}
/>
) : null}
{schemaArea ? (
<ConnectorForm
schemaAreaInfo={schemaArea}
initValue={{ ...initValue?.bind_info, ...assignValue }}
ref={formRef}
getFormDisable={disable => setFormDisabled(disable)}
isReadOnly={Boolean(initValue?.bind_id)}
setErrorMessage={setErrorMessage}
/>
) : null}
{errorMessage ? <ConnectorError errorMessage={errorMessage} /> : null}
</>
);
return {
node: modal(
<Spin
wrapperClassName={styles['config-area']}
spinning={formSchemaLoading}
>
<ConnectorGuide connectorConfigInfo={connectorConfigInfo} />
{schemaPages?.length && !initValue?.bind_id ? (
<div>
{renderConnectorArea(
schemaPages[step]?.copy_link_area,
schemaPages[step]?.schema_area,
)}
</div>
) : null}
{initValue?.bind_id && schemaPages?.length ? (
<>
{schemaPages?.map((item, i) => (
<div key={i}>
{renderConnectorArea(item.copy_link_area, item.schema_area)}
</div>
))}
</>
) : null}
{unbindPlatformModal}
</Spin>,
),
open: (props: ConnectorConfigureValueType) => {
setPropsValue(props);
open();
},
close,
};
};

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type Ref, forwardRef, type FC } from 'react';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { type IconButtonProps } from '@coze-arch/coze-design/types';
import { Button, IconButton } from '@coze-arch/coze-design';
import { type UIButton } from '@coze-arch/bot-semi';
import s from './index.module.less';
export const BotDebugButton: FC<IconButtonProps> = forwardRef(
(props: IconButtonProps, ref: Ref<UIButton>) => {
const isReadonly = useBotDetailIsReadonly();
const className = props.theme || '';
if (isReadonly) {
return null;
}
if (props.icon && !props.children) {
return <IconButton {...props} className={s[className]} ref={ref} />;
}
return <Button {...props} className={s[className]} ref={ref} />;
},
);

View File

@@ -0,0 +1,9 @@
.borderless {
// padding: 0 !important;
}
// .solid {
// font-size: 12px;
// }
// .primary {
// padding: 6px 12px;
// }

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 { BotDebugButton } from './bot-debug-button';

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { IconCozDebug } from '@coze-arch/coze-design/icons';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { OperateTypeEnum, ToolPane } from '@coze-agent-ide/debug-tool-list';
import { useEvaluationPanelStore } from '@/store/evaluation-panel';
import { useDebugStore } from '../../store/debug-panel';
export const BotDebugToolPane: React.FC = () => {
const { isDebugPanelShow, setIsDebugPanelShow, setCurrentDebugQueryId } =
useDebugStore();
const { setIsEvaluationPanelVisible } = useEvaluationPanelStore();
return (
<ToolPane
visible={true}
itemKey={'key_debug'}
title={I18n.t('debug_btn')}
operateType={OperateTypeEnum.CUSTOM}
icon={(<IconCozDebug />) as React.ReactNode}
customShowOperateArea={isDebugPanelShow}
beforeVisible={async () => {
await sendTeaEvent(EVENT_NAMES.open_debug_panel, {
path: 'preview_debug',
});
setCurrentDebugQueryId('');
if (!isDebugPanelShow) {
setIsEvaluationPanelVisible(false);
}
setIsDebugPanelShow(!isDebugPanelShow);
}}
/>
);
};

View File

@@ -0,0 +1,18 @@
.container {
height: 100%;
width: 400px;
background-color: #fff;
display: flex;
justify-content: flex-end;
flex-shrink: 0;
z-index: 101;
.debug-panel-lazy-loading {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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 { useHotkeys } from 'react-hotkeys-hook';
import { Suspense, lazy, useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { userStoreService } from '@coze-studio/user-store';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { setPCBody } from '@coze-arch/bot-utils';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { Spin } from '@coze-arch/bot-semi';
import { setPCBodyWithDebugPanel } from '../../util';
import { useDebugStore } from '../../store/debug-panel';
import s from './index.module.less';
const DebugPanel = lazy(() => import('@coze-devops/debug-panel'));
export const BotDebugPanel = () => {
const {
isDebugPanelShow,
currentDebugQueryId,
setIsDebugPanelShow,
setCurrentDebugQueryId,
} = useDebugStore();
const { botId } = useBotInfoStore(
useShallow(state => ({
botId: state.botId,
})),
);
const userID = userStoreService.useUserInfo()?.user_id_str ?? '';
const { id: spaceID } = useSpaceStore(state => state.space);
useHotkeys('ctrl+k, meta+k', () => {
if (!isDebugPanelShow) {
sendTeaEvent(EVENT_NAMES.open_debug_panel, {
path: 'shortcut_debug',
});
}
setCurrentDebugQueryId('');
setIsDebugPanelShow(!isDebugPanelShow);
});
useEffect(() => {
if (isDebugPanelShow) {
setPCBodyWithDebugPanel();
window.scrollTo(document.body.scrollWidth, 0);
} else {
setPCBody();
}
return () => {
setPCBody();
};
}, [isDebugPanelShow]);
useEffect(
() => () => {
setCurrentDebugQueryId('');
},
[],
);
return isDebugPanelShow ? (
<div className={s.container}>
<Suspense
fallback={
<div className={s['debug-panel-lazy-loading']}>
<Spin />
</div>
}
>
<DebugPanel
isShow={isDebugPanelShow}
botId={botId}
userID={userID}
spaceID={spaceID}
placement="left"
currentQueryLogId={currentDebugQueryId}
onClose={() => {
setIsDebugPanelShow(false);
setCurrentDebugQueryId('');
}}
/>
</Suspense>
</div>
) : null;
};

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useRequest } from 'ahooks';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { Spin } from '@coze-arch/bot-semi';
import { useFlags } from '@coze-arch/bot-flags';
import { Branch } from '@coze-arch/bot-api/dp_manage_api';
import { dpManageApi } from '@coze-arch/bot-api';
import { useParams } from 'react-router-dom';
import { NewBotDiffView } from './new-diff-view';
import { BotDiffView } from '.';
import styles from './index.module.less';
export const BotSubmitModalDiffView: React.FC<{ visible: boolean }> = props => {
const params = useParams<DynamicParams>();
const [Flags] = useFlags();
const isUseNewTemplate = !!Flags?.['bot.devops.merge_prompt_diff'];
const {
data: botDiffData,
loading,
error,
} = useRequest(
async () => {
const { bot_id = '', space_id = '' } = params;
const resp = await dpManageApi.BotDiff({
space_id,
bot_id,
left: {
branch: Branch.Base,
},
template_key: isUseNewTemplate ? 'diff_template_v2' : '',
right: { branch: Branch.PersonalDraft },
});
return resp.data;
},
{ refreshDeps: [] },
);
return (
<div
className={styles['modal-diff-container']}
style={{ display: props.visible ? 'block' : 'none' }}
>
{loading ? (
<Spin spinning={loading} style={{ height: '100%', width: '100%' }} />
) : isUseNewTemplate ? (
<NewBotDiffView
diffData={botDiffData?.diff_display_node || []}
hasError={error !== undefined}
/>
) : (
<BotDiffView
diffData={botDiffData?.diff_display_node || []}
hasError={error !== undefined}
/>
)}
</div>
);
};

View File

@@ -0,0 +1,129 @@
/* stylelint-disable */
.info-title {
margin-bottom: 8px;
font-size: 16px;
font-weight: 600;
}
.info-subtitle {
margin-top: 12px;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
}
.container {
width: 100%;
height: 100%;
}
.diff-table {
margin-bottom: 24px;
:global {
.semi-table-row-head {
padding: 4px 8px !important;
font-size: 12px;
background-color: #2e2e380a !important;
border-bottom: 1px solid var(--semi-color-border);
}
.semi-table-row-cell {
padding: 10px 8px !important;
font-size: 12px;
}
}
}
.cell-span {
font-size: 12px !important;
font-weight: 400;
word-break: break-word;
}
.property-tooltip {
word-break: break-word;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 200px;
}
.empty-info {
margin-top: 4px;
}
// .leftNode
.list {
background-color: white !important;
border: 1px solid var(--Stroke-COZ-stroke-plus, rgba(6, 7, 9, 15%)) !important;
border-radius: 8px;
:global {
.semi-list-item {
border-bottom: 1px solid
var(--Stroke-COZ-stroke-plus, rgba(6, 7, 9, 15%)) !important;
}
.semi-list-item:last-child {
border-bottom: none !important;
}
}
}
.list-item {
display: grid;
grid-template-columns: 280px 120px 1fr;
align-items: center;
}
.tag-1 {
color: #3ec254;
background-color: #d2f3d5;
}
.tag-2 {
color: #ff441e;
background-color: #ffe0d2;
}
.tag-4 {
color: #ff441e;
background-color: #ffe0d2;
}
.tag-3 {
color: #ff9600;
background-color: #fff1cc;
}
.property-title {
font-size: 12px;
font-weight: 500;
color: var(--Fg-COZ-fg-primary, rgba(6, 7, 9, 80%));
}
.info-block&:not(:first-child){
margin-top: 24px;
}
.mask{
pointer-events: none;
position: absolute;
bottom: 80px;
width: 100%;
height: 32px;
background: linear-gradient(to top, rgba(var(--coze-bg-2), 1) 0, rgba(var(--coze-bg-2), 0) 100%);
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { Table, Typography, UITag } from '@coze-arch/bot-semi';
import {
type DiffDisplayNode,
DiffActionType,
} from '@coze-arch/bot-api/dp_manage_api';
import {
DIFF_TABLE_INDENT_BASE,
DIFF_TABLE_INDENT_LENGTH,
DiffNodeRender,
} from '@coze-agent-ide/agent-ide-commons';
import { flatDataSource } from '../../util';
import EmptyIcon from '../../assets/image/diff-empty.svg';
import styles from './index.module.less';
const ActionTypeEnum = {
[DiffActionType.Add]: 'devops_publish_multibranch_changeset_add',
[DiffActionType.Delete]: 'devops_publish_multibranch_changeset_delete',
[DiffActionType.Modify]: 'devops_publish_multibranch_changeset_modify',
[DiffActionType.Remove]: 'devops_publish_multibranch_changeset_remove',
};
export const BotDiffView: React.FC<{
diffData: DiffDisplayNode[];
hasError: boolean;
}> = ({ diffData, hasError }) => (
<div className={styles.container}>
{diffData?.length > 0 ? (
diffData.map(item => (
<div className={styles['info-block']} key={item.display_name}>
<div className={styles['info-title']}>{item.display_name}</div>
{item?.sub_nodes?.length ? (
<BotDiffBlockTable blockDiffData={item.sub_nodes} />
) : null}
</div>
))
) : (
<div className={styles['empty-container']}>
<img src={EmptyIcon} />
<Typography.Text className={styles['empty-info']}>
{I18n.t(
hasError
? 'devops_publish_multibranch_NetworkError'
: 'devops_publish_multibranch_nodiff',
)}
</Typography.Text>
</div>
)}
</div>
);
export const BotDiffBlockTable: React.FC<{
blockDiffData: DiffDisplayNode[];
}> = ({ blockDiffData }) => {
const columns = [
{
title: I18n.t('devops_publish_multibranch_property'),
width: 280,
render: node => (
<Typography.Text
ellipsis={{
showTooltip: {
opts: {
content: node.display_name,
className: styles['property-tooltip'],
},
},
}}
className={styles['cell-span']}
>
{node.level > 0 ? (
<Typography.Text
style={{
marginLeft:
DIFF_TABLE_INDENT_BASE +
DIFF_TABLE_INDENT_LENGTH * (node.level - 1),
marginRight: 8,
}}
>
-
</Typography.Text>
) : null}
{node.display_name}
</Typography.Text>
),
},
{
title: I18n.t('devops_publish_multibranch_changetype'),
render: (node: DiffDisplayNode) => {
if (
!node.diff_res ||
node.diff_res?.action === DiffActionType.Unknown
) {
return '';
}
return (
<UITag className={styles[`tag-${node.diff_res.action}`]}>
{I18n.t(ActionTypeEnum[node.diff_res.action])}
</UITag>
);
},
width: 120,
},
{
title: I18n.t('devops_publish_multibranch_changes'),
render: (node: DiffDisplayNode) =>
node?.diff_res?.action === DiffActionType.Modify ? (
<DiffNodeRender
node={node}
left={node?.diff_res?.display_left || ''}
right={node?.diff_res?.display_right || ''}
/>
) : (
''
),
ellipsis: true,
},
];
if (!blockDiffData) {
return null;
}
return (
<Table
dataSource={flatDataSource(blockDiffData)}
columns={columns}
pagination={false}
onRow={() => ({
className: styles['table-row'],
})}
className={styles['diff-table']}
/>
);
};

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { List, Typography, UITag } from '@coze-arch/bot-semi';
import {
type DiffDisplayNode,
DiffActionType,
} from '@coze-arch/bot-api/dp_manage_api';
import {
DIFF_TABLE_INDENT_BASE,
DIFF_TABLE_INDENT_LENGTH,
DiffNodeRender,
} from '@coze-agent-ide/agent-ide-commons';
import { flatDataSource } from '../../util';
import EmptyIcon from '../../assets/image/diff-empty.svg';
import { type FlatDiffDisplayNode } from './type';
import styles from './index.module.less';
const ActionTypeEnum = {
[DiffActionType.Add]: 'devops_publish_multibranch_changeset_add',
[DiffActionType.Delete]: 'devops_publish_multibranch_changeset_delete',
[DiffActionType.Modify]: 'devops_publish_multibranch_changeset_modify',
[DiffActionType.Remove]: 'devops_publish_multibranch_changeset_remove',
};
export const NewBotDiffView: React.FC<{
diffData: DiffDisplayNode[];
hasError: boolean;
type?: 'diff' | 'publish';
}> = ({ diffData, hasError, type = 'diff' }) => (
<div className={styles.container}>
{diffData?.length > 0 ? (
diffData.map(item => (
<div className={styles['info-block']} key={item.display_name}>
<div className={styles['info-title']}>{item.display_name}</div>
{item?.sub_nodes?.length
? item?.sub_nodes?.map((node, index) => (
<BotSubNode node={node} key={index} type={type} />
))
: null}
</div>
))
) : (
<div className={styles['empty-container']}>
<img src={EmptyIcon} />
<Typography.Text className={styles['empty-info']}>
{I18n.t(
hasError
? 'devops_publish_multibranch_NetworkError'
: 'devops_publish_multibranch_nodiff',
)}
</Typography.Text>
</div>
)}
<div className="h-[32px]"></div>
<div className={styles.mask}></div>
</div>
);
export const BotSubNode: React.FC<{
node: DiffDisplayNode;
type?: 'diff' | 'publish';
}> = ({ node, type = 'diff' }) => {
const { display_name } = node;
return (
<div>
{display_name ? (
<div className={styles['info-subtitle']}>{display_name}</div>
) : (
<></>
)}
{node?.sub_nodes?.length ? (
<BotDiffBlockTable blockDiffData={node?.sub_nodes} type={type} />
) : null}
</div>
);
};
export const BotDiffBlockTable: React.FC<{
blockDiffData: DiffDisplayNode[];
type: 'diff' | 'publish';
}> = ({ blockDiffData, type = 'diff' }) => {
if (!blockDiffData) {
return null;
}
const renderTitle = (node: FlatDiffDisplayNode) => (
<Typography.Text
ellipsis={{
showTooltip: {
opts: {
content: node.display_name,
className: styles['property-tooltip'],
},
},
}}
className={styles['property-title']}
>
{node.level > 0 ? (
<Typography.Text
style={{
marginLeft:
DIFF_TABLE_INDENT_BASE +
DIFF_TABLE_INDENT_LENGTH * (node.level - 1),
marginRight: 8,
}}
>
-
</Typography.Text>
) : null}
{node.display_name}
</Typography.Text>
);
const renderModify = (node: DiffDisplayNode) => {
if (!node.diff_res || node.diff_res?.action === DiffActionType.Unknown) {
return '';
}
return (
<UITag className={styles[`tag-${node.diff_res.action}`]}>
{I18n.t(ActionTypeEnum[node.diff_res.action])}
</UITag>
);
};
const renderView = (node: DiffDisplayNode) =>
node?.diff_res?.action === DiffActionType.Modify ? (
<DiffNodeRender
left={node?.diff_res?.display_left || ''}
right={node?.diff_res?.display_right || ''}
node={node}
type={type}
/>
) : (
''
);
return (
<List
dataSource={flatDataSource(blockDiffData)}
bordered
className={styles.list}
renderItem={item => (
<List.Item>
<div className={styles['list-item']}>
{renderTitle(item)}
<div> {renderModify(item)}</div>
{renderView(item)}
</div>
</List.Item>
)}
/>
);
};

View File

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

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { size } from 'lodash-es';
import classNames from 'classnames';
import {
IconCozCheckMarkCircleFill,
IconCozInfoCircleFill,
} from '@coze-arch/coze-design/icons';
import { Typography } from '@coze-arch/bot-semi';
import { type TransferResourceInfo } from '@coze-arch/bot-api/playground_api';
interface IResource extends TransferResourceInfo {
spaceID: string;
}
interface IItemGridView {
title: string;
resources: Array<IResource>;
onResourceClick?: (id: string, spaceID: string) => void;
showStatus?: boolean;
}
export function ItemGridView(props: IItemGridView) {
const { title, resources, showStatus = false, onResourceClick } = props;
// HACK: 由于 grid 布局下边界线是透出的背景色,所以 resource 数量为单数的时候需要补齐一个
const isEven = size(resources) % 2 === 0;
const finalResources = isEven
? resources
: [...resources, { name: '', id: '', icon: '', spaceID: '' }];
return (
<>
<p className="text-[12px] leading-[16px] font-[500] coz-fg-secondary text-left align-top w-full mb-[6px]">
{title}
</p>
<div className="mb-[12px]">
<div className="grid grid-cols-2 rounded-[6px] overflow-hidden border border-solid coz-stroke-primary gap-[1px] bg-[var(--coz-stroke-primary)] rounded-[4px]">
{finalResources.map(item => (
<div
key={item.id}
className={classNames(
'flex justify-center items-center gap-x-[4px] p-[8px] w-full coz-bg-plus',
item.id ? 'hover:cursor-pointer' : '',
)}
onClick={() => {
if (item.id) {
onResourceClick?.(item.id, item.spaceID);
}
}}
>
<img
src={item.icon}
className="w-[16px] h-[16px] rounded-[2px]"
/>
<Typography.Text
ellipsis={{ showTooltip: true }}
className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top grow"
>
{item.name}
</Typography.Text>
{showStatus && item.status === 1 ? (
<div className="coz-fg-hglt-green flex justify-center items-center">
<IconCozCheckMarkCircleFill />
</div>
) : null}
{showStatus && item.status === 0 ? (
<div className="coz-fg-hglt-red flex justify-center items-center">
<IconCozInfoCircleFill />
</div>
) : null}
</div>
))}
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { size } from 'lodash-es';
import { useRequest, useUnmount } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import { MoveAction } from '@coze-arch/bot-api/playground_api';
import { type BotSpace } from '@coze-arch/bot-api/developer_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { SelectorItem } from '../selector-item';
import { ItemGridView } from '../item-grid-view';
interface IMoveDetailPaneProps {
targetSpace: BotSpace | null;
botID: string;
fromSpaceID: string;
onUnmount?: () => void;
onDetailLoaded?: () => void;
}
export function MoveDetailPane(props: IMoveDetailPaneProps) {
const { targetSpace, botID, fromSpaceID, onUnmount, onDetailLoaded } = props;
const { data: moveDetails } = useRequest(
async () => {
const data = await PlaygroundApi.MoveDraftBot({
bot_id: botID,
target_spaceId: targetSpace.id,
from_spaceId: fromSpaceID,
move_action: MoveAction.Preview,
});
return {
...data?.async_task,
cannotMove: data?.forbid_move,
};
},
{
onSuccess: data => {
if (data && !data.cannotMove) {
onDetailLoaded?.();
}
},
},
);
useUnmount(() => {
onUnmount?.();
});
return (
<div className="flex flex-col">
<div className="w-full border-[0.5px] border-solid coz-stroke-primary mb-[12px]"></div>
<div className="flex flex-col max-h-[406px] overflow-y-auto">
<div className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top w-full mb-[6px]">
{I18n.t('resource_move_target_team')}
</div>
<div>
<div className="flex flex-col rounded-[6px] overflow-hidden mb-[16px]">
<SelectorItem space={targetSpace} selected disabled />
</div>
</div>
{moveDetails?.cannotMove ? (
<div className="flex items-center gap-x-[8px] p-[12px] w-full coz-mg-hglt-red rounded-[4px] mb-[12px]">
<p className="text-[12px] leading-[16px] font-[400] coz-fg-hglt-red text-left align-top grow">
{I18n.t('move_not_allowed_contain_bot_nodes')}
</p>
</div>
) : null}
{!moveDetails?.cannotMove &&
(size(moveDetails?.transfer_resource_plugin_list) ||
size(moveDetails?.transfer_resource_workflow_list) ||
size(moveDetails?.transfer_resource_knowledge_list)) ? (
<>
<div className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top w-full mb-[6px]">
{I18n.t('resource_move_together')}
</div>
<div className="flex items-center gap-x-[8px] p-[8px] w-full coz-mg-hglt-red rounded-[4px] mb-[12px]">
<p className="text-[12px] leading-[16px] font-[400] coz-fg-hglt-red text-left align-top grow">
{I18n.t('resource_move_together_desc')}
</p>
</div>
{size(moveDetails?.transfer_resource_plugin_list) > 0 ? (
<ItemGridView
title={I18n.t('store_search_recommend_result2')}
resources={moveDetails.transfer_resource_plugin_list.map(
item => ({
...item,
spaceID: fromSpaceID,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(`/space/${spaceID}/plugin/${id}`);
}}
/>
) : null}
{size(moveDetails?.transfer_resource_workflow_list) > 0 ? (
<ItemGridView
title={I18n.t('store_search_recommend_result3')}
resources={moveDetails.transfer_resource_workflow_list.map(
item => ({
...item,
spaceID: fromSpaceID,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(
`/work_flow?space_id=${spaceID}&workflow_id=${id}`,
);
}}
/>
) : null}
{size(moveDetails?.transfer_resource_knowledge_list) > 0 ? (
<ItemGridView
title={I18n.t('performance_knowledge')}
resources={moveDetails.transfer_resource_knowledge_list.map(
item => ({
...item,
spaceID: fromSpaceID,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(`/space/${spaceID}/knowledge/${id}`);
}}
/>
) : null}
</>
) : null}
</div>
</div>
);
}

View File

@@ -0,0 +1,85 @@
/*
* 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, { useState } from 'react';
import { size } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { useSpaceList } from '@coze-arch/bot-studio-store';
import { type BotSpace, SpaceType } from '@coze-arch/bot-api/developer_api';
import { SelectorItem } from '../selector-item';
export function useSelectSpacePane() {
const { spaces } = useSpaceList();
const [targetSpace, setTargetSpace] = useState<BotSpace | null>(null);
const personalSpace = spaces.find(
item => item.space_type === SpaceType.Personal,
);
const teamSpaces = spaces.filter(item => item.space_type === SpaceType.Team);
const selectSpacePane = (
<div className="flex flex-col">
<div className="w-full border-[0.5px] border-solid coz-stroke-primary mb-[12px]"></div>
<div className="flex flex-col max-h-[406px] overflow-y-auto">
<div className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top w-full mb-[6px]">
{I18n.t('menu_title_personal_space')}
</div>
<div>
<div className="flex flex-col rounded-[6px] overflow-hidden mb-[16px]">
<SelectorItem space={personalSpace} disabled />
</div>
</div>
<div className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top w-full mb-[6px]">
{I18n.t('resource_move_target_team')}
</div>
<div>
<div className="flex flex-col rounded-[6px] overflow-hidden">
{size(teamSpaces) > 0 ? (
spaces
.filter(item => item.space_type !== SpaceType.Personal)
.map(item => (
<SelectorItem
key={item.id}
space={item}
selected={item.id === targetSpace?.id}
onSelect={space => {
setTargetSpace(space);
}}
/>
))
) : (
<SelectorItem
space={{
// MOCK: 用于展示未加入任何空间的兜底情况
name: I18n.t('resource_move_no_team_joined'),
}}
disabled
/>
)}
</div>
</div>
</div>
</div>
);
return {
targetSpace,
setTargetSpace,
selectSpacePane,
};
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classnames from 'classnames';
import { IconCozCheckMarkFill } from '@coze-arch/coze-design/icons';
import { type BotSpace } from '@coze-arch/bot-api/developer_api';
interface ISelectorItemProps {
space: BotSpace;
disabled?: boolean;
selected?: boolean;
onSelect?: (space: BotSpace) => void;
}
export function SelectorItem(props: ISelectorItemProps) {
const { space, disabled = false, selected = false, onSelect } = props;
return (
<div
className={classnames(
'flex justify-between items-center gap-x-[8px] p-[8px] w-full coz-mg-primary',
disabled ? '' : 'hover:coz-mg-primary-hovered cursor-pointer',
)}
onClick={() => {
if (!disabled) {
onSelect?.(space);
}
}}
>
<div className="flex items-center">
{space.icon_url ? (
<img
src={space.icon_url}
className="w-[24px] h-[24px] rounded-full mr-[8px]"
/>
) : null}
<p
className={classnames(
'text-[14px] leading-[20px] font-[400] text-left align-middle whitespace-normal -webkit-box line-clamp-1 overflow-hidden grow',
disabled ? 'coz-fg-secondary' : 'coz-fg-primary',
)}
>
{space.name}
</p>
</div>
{selected ? (
<div className="w-[24px] h-[24px] flex justify-center items-center">
<IconCozCheckMarkFill className="coz-fg-secondary" />
</div>
) : null}
</div>
);
}

View File

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

View File

@@ -0,0 +1,361 @@
/*
* 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 React, { useCallback, useState } from 'react';
import { size } from 'lodash-es';
import classNames from 'classnames';
import { useBoolean, useRequest } from 'ahooks';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { IconCozCross } from '@coze-arch/coze-design/icons';
import { Button, Modal, Toast } from '@coze-arch/coze-design';
import { useSpaceList } from '@coze-arch/bot-studio-store';
import { MoveAction } from '@coze-arch/bot-api/playground_api';
import {
DraftBotStatus,
type DraftBot,
SpaceType,
} from '@coze-arch/bot-api/developer_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { ItemGridView } from './components/item-grid-view';
interface BotMoveFailedModalOptions {
/**
* botInfo
*/
botInfo: Pick<DraftBot, 'id' | 'name'> | null;
/**
* 更新 bot 状态
*/
onUpdateBotStatus?: (status: DraftBotStatus) => void;
/**
* 迁移成功等效于删除 bot
*/
onMoveSuccess?: () => void;
}
interface UseBotMoveFailedModalValue {
/**
* 打开弹窗的方法
* @param {BotMoveModalOptions} [options] - 此次打开Modal的配置项
*/
open: (options?: BotMoveFailedModalOptions) => void;
/**
* 关闭弹窗的方法
*/
close: () => void;
/**
* 弹窗组件实例,需要手动挂载一下
*/
modalNode: React.ReactNode;
}
const DefaultOptions: BotMoveFailedModalOptions = { botInfo: null };
// eslint-disable-next-line complexity
export function useBotMoveFailedModal(): UseBotMoveFailedModalValue {
const [options, setOptions] =
useState<BotMoveFailedModalOptions>(DefaultOptions);
const [visible, { setTrue: setVisibleTrue, setFalse: setVisibleFalse }] =
useBoolean(false);
const [paneType, setPaneType] = useState<
'detail' | 'confirm_cancel' | 'confirm_force'
>('detail');
const title = (
<span className="mb-[20px] coz-fg-plus text-[16px] font-medium leading-[22px]">
{paneType === 'detail'
? I18n.t('move_failed')
: paneType === 'confirm_cancel'
? I18n.t('move_failed_cancel_confirm_title')
: paneType === 'confirm_force'
? I18n.t('move_failed_force_confirm_title')
: ''}
</span>
);
const open = useCallback((opts?: BotMoveFailedModalOptions) => {
setOptions(opts || DefaultOptions);
setPaneType('detail');
setVisibleTrue();
}, []);
const close = useCallback(() => {
setVisibleFalse();
}, []);
const { spaces } = useSpaceList();
const fromSpaceID =
spaces?.find(s => s.space_type === SpaceType.Personal)?.id ?? '';
const { data: moveDetails } = useRequest(
async () => {
if (!options.botInfo) {
return;
}
const data = await PlaygroundApi.MoveDraftBot({
bot_id: options.botInfo.id,
from_spaceId: fromSpaceID,
move_action: MoveAction.ViewTask,
});
return data.async_task;
},
{ refreshDeps: [options.botInfo] },
);
const { loading, run } = useRequest(
async (moveAction: MoveAction) => {
const data = await PlaygroundApi.MoveDraftBot({
bot_id: options.botInfo.id,
from_spaceId: fromSpaceID,
move_action: moveAction,
});
return { ...data, moveAction };
},
{
manual: true,
onSuccess: data => {
if (data.bot_status === DraftBotStatus.Using) {
if (data.moveAction === MoveAction.CancelTask) {
options.onUpdateBotStatus?.(data.bot_status);
} else {
Toast.success(I18n.t('resource_move_bot_success_toast'));
options.onMoveSuccess?.();
}
close();
} else if (data.bot_status === DraftBotStatus.MoveFail) {
options.onUpdateBotStatus?.(data.bot_status);
Toast.error({
content: withSlardarIdButton(I18n.t('move_failed_toast')),
});
close();
}
},
onError: error => {
Toast.error({
content: withSlardarIdButton(
error?.message || I18n.t('move_failed_toast'),
),
showClose: false,
});
close();
},
},
);
const retry = async () => {
await run(MoveAction.RetryMove);
};
const forceMove = async () => {
await run(MoveAction.ForcedMove);
};
const cancelMove = async () => {
await run(MoveAction.CancelTask);
};
const footer = (
<div
className={classNames(
'coz-modal-footer flex gap-2 justify-end',
'w-full',
)}
>
{paneType === 'detail' ? (
<>
<Button
className="flex-1 !ml-0"
size="large"
color="primary"
disabled={!moveDetails || loading}
onClick={() => {
setPaneType('confirm_cancel');
}}
>
{I18n.t('move_failed_btn_cancel')}
</Button>
<Button
className="flex-1 !ml-0"
size="large"
color="primary"
disabled={!moveDetails || loading}
onClick={() => {
setPaneType('confirm_force');
}}
>
{I18n.t('move_failed_btn_force')}
</Button>
<Button
className="flex-1 !ml-0"
color="brand"
size="large"
loading={loading}
disabled={!moveDetails || loading}
onClick={() => {
retry();
}}
>
{I18n.t('Retry')}
</Button>
</>
) : null}
{paneType === 'confirm_cancel' ? (
<>
<Button
className="!ml-0"
color="primary"
onClick={() => {
setPaneType('detail');
}}
>
{I18n.t('back')}
</Button>
<Button
className="!ml-0"
color="brand"
loading={loading}
onClick={() => {
cancelMove();
}}
>
{I18n.t('confirm')}
</Button>
</>
) : null}
{paneType === 'confirm_force' ? (
<>
<Button
className="!ml-0"
color="primary"
onClick={() => {
setPaneType('detail');
}}
>
{I18n.t('back')}
</Button>
<Button
className="!ml-0"
color="brand"
loading={loading}
onClick={() => {
forceMove();
}}
>
{I18n.t('confirm')}
</Button>
</>
) : null}
</div>
);
const modalNode = (
<Modal
visible={visible}
title={title}
footer={footer}
width={paneType !== 'detail' ? '448px' : '480px'}
footerFill
onCancel={close}
closable={!['confirm_cancel', 'confirm_force'].includes(paneType)}
maskClosable={false}
keepDOM={false}
closeIcon={<IconCozCross className="coz-fg-secondary" />}
>
{paneType === 'detail' ? (
<div className="flex flex-col">
<div className="w-full border-[0.5px] border-solid coz-stroke-primary mb-[12px]"></div>
<div className="flex flex-col max-h-[406px] overflow-y-auto">
<div className="flex items-center gap-x-[8px] p-[8px] w-full coz-mg-primary rounded-[6px] mb-[12px]">
<p className="text-[12px] leading-[16px] font-[400] coz-fg-secondary text-left align-top grow">
{I18n.t('move_failed_desc')}
</p>
</div>
{size(moveDetails?.transfer_resource_plugin_list) > 0 ? (
<ItemGridView
title={I18n.t('store_search_recommend_result2')}
resources={moveDetails?.transfer_resource_plugin_list.map(
item => ({
...item,
spaceID: item.status
? moveDetails?.task_info.TargetSpaceId
: moveDetails?.task_info.OriSpaceId,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(`/space/${spaceID}/plugin/${id}`);
}}
showStatus
/>
) : null}
{size(moveDetails?.transfer_resource_workflow_list) > 0 ? (
<ItemGridView
title={I18n.t('store_search_recommend_result3')}
resources={moveDetails?.transfer_resource_workflow_list.map(
item => ({
...item,
spaceID: item.status
? moveDetails?.task_info.TargetSpaceId
: moveDetails?.task_info.OriSpaceId,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(
`/work_flow?space_id=${spaceID}&workflow_id=${id}`,
);
}}
showStatus
/>
) : null}
{size(moveDetails?.transfer_resource_knowledge_list) > 0 ? (
<ItemGridView
title={I18n.t('performance_knowledge')}
resources={moveDetails?.transfer_resource_knowledge_list.map(
item => ({
...item,
spaceID: item.status
? moveDetails?.task_info.TargetSpaceId
: moveDetails?.task_info.OriSpaceId,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(`/space/${spaceID}/knowledge/${id}`);
}}
showStatus
/>
) : null}
</div>
</div>
) : null}
{paneType === 'confirm_force' ? (
<div className="mt-[20px]">
{I18n.t('move_failed_force_confirm_content')}
</div>
) : null}
{paneType === 'confirm_cancel' ? (
<div className="mt-[20px]">
{I18n.t('move_failed_cancel_confirm_content')}
</div>
) : null}
</Modal>
);
return {
modalNode,
open,
close,
};
}

View File

@@ -0,0 +1,293 @@
/*
* 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 React, { useCallback, useState } from 'react';
import classNames from 'classnames';
import { useBoolean, useRequest } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import { useSpaceList } from '@coze-arch/bot-studio-store';
import { MoveAction } from '@coze-arch/bot-api/playground_api';
import {
DraftBotStatus,
type DraftBot,
SpaceType,
} from '@coze-arch/bot-api/developer_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { cozeMitt } from '@coze-common/coze-mitt';
import { IconInfo } from '@coze-arch/bot-icons';
import { IconCozCross } from '@coze-arch/coze-design/icons';
import {
Button,
IconButton,
Modal,
Toast,
Tooltip,
Typography,
} from '@coze-arch/coze-design';
import { useSelectSpacePane } from './components/select-space-pane';
import { MoveDetailPane } from './components/move-detail-pane';
interface BotMoveModalOptions {
/**
* botInfo
*/
botInfo: Pick<DraftBot, 'name' | 'id'> | null;
/**
* 更新 bot 状态
*/
onUpdateBotStatus?: (status: DraftBotStatus) => void;
/**
* 迁移成功等效于删除 bot
*/
onMoveSuccess?: () => void;
/**
* 关闭 modal
*/
onClose?: () => void;
}
interface UseBotMoveModalValue {
/**
* 打开弹窗的方法
* @param {BotMoveModalOptions} [options] - 此次打开Modal的配置项
*/
open: (options?: BotMoveModalOptions) => void;
/**
* 关闭弹窗的方法
*/
close: () => void;
/**
* 弹窗组件实例,需要手动挂载一下
*/
modalNode: React.ReactNode;
}
const DefaultOptions: BotMoveModalOptions = { botInfo: null };
export function useBotMoveModal(): UseBotMoveModalValue {
const [options, setOptions] = useState<BotMoveModalOptions>(DefaultOptions);
const [visible, { setTrue: setVisibleTrue, setFalse: setVisibleFalse }] =
useBoolean(false);
const [paneType, setPaneType] = useState<'select' | 'move' | 'confirm'>(
'select',
);
const { targetSpace, selectSpacePane, setTargetSpace } = useSelectSpacePane();
const title =
paneType !== 'confirm' ? (
<div className="flex justify-start items-center mb-[24px] w-[380px]">
<div className="coz-fg-plus text-[16px] font-medium leading-[22px] max-w-full">
<Typography.Text
ellipsis={{
showTooltip: true,
}}
className="text-[16px]"
>
{I18n.t('resource_move_title', {
bot_name: options.botInfo?.name ?? '',
})}
</Typography.Text>
</div>
<Tooltip content={I18n.t('resource_move_notice')}>
<IconButton
size="small"
color="secondary"
icon={<IconInfo className="coz-fg-secondary" />}
/>
</Tooltip>
</div>
) : (
I18n.t('resource_move_confirm_title')
);
const open = useCallback((opts?: BotMoveModalOptions) => {
setOptions(opts || DefaultOptions);
setPaneType('select');
setTargetSpace(null);
setVisibleTrue();
}, []);
const close = useCallback(() => {
setVisibleFalse();
}, []);
const { spaces } = useSpaceList();
const fromSpaceID = spaces.find(s => s.space_type === SpaceType.Personal)?.id;
const { loading: moveLoading, run: moveBot } = useRequest(
async () => {
const data = await PlaygroundApi.MoveDraftBot({
bot_id: options.botInfo.id,
target_spaceId: targetSpace.id,
from_spaceId: fromSpaceID,
move_action: MoveAction.Move,
});
return data;
},
{
manual: true,
onSuccess: data => {
if (data.bot_status === DraftBotStatus.Using) {
Toast.success(I18n.t('resource_move_bot_success_toast'));
options.onMoveSuccess?.();
close();
cozeMitt.emit('refreshFavList', {
numDelta: -1,
});
} else if (data.bot_status === DraftBotStatus.MoveFail) {
options.onUpdateBotStatus?.(data.bot_status);
Toast.error({
content: withSlardarIdButton(I18n.t('move_failed_toast')),
});
close();
}
},
onError: error => {
Toast.error({
content: withSlardarIdButton(
error?.message || I18n.t('move_failed_toast'),
),
showClose: false,
});
close();
},
},
);
const onConfirm = async () => {
await moveBot();
};
const [moveDisabled, setMoveDisabled] = useState(true);
const footer = (
<div
className={classNames(
'coz-modal-footer flex gap-2 justify-end',
paneType !== 'confirm' && 'w-full',
)}
>
{paneType === 'select' ? (
<Button
className="flex-1 !ml-0"
color="brand"
size="large"
disabled={!targetSpace}
onClick={() => {
setPaneType('move');
}}
>
{I18n.t('next')}
</Button>
) : null}
{paneType === 'move' ? (
<>
<Button
className="flex-1 !ml-0"
size="large"
color="primary"
onClick={() => {
setPaneType('select');
}}
>
{I18n.t('back')}
</Button>
<Button
className="flex-1 !ml-0"
color="brand"
size="large"
disabled={moveDisabled}
onClick={() => {
setPaneType('confirm');
}}
>
{I18n.t('resource_move')}
</Button>
</>
) : null}
{paneType === 'confirm' ? (
<>
<Button
className="!ml-0"
color="primary"
onClick={() => {
setPaneType('move');
}}
>
{I18n.t('back')}
</Button>
<Button
className="!ml-0"
color="brand"
loading={moveLoading}
onClick={() => {
onConfirm();
}}
>
{I18n.t('confirm')}
</Button>
</>
) : null}
</div>
);
const modalNode = (
<Modal
visible={visible}
title={title}
footer={footer}
width={paneType === 'confirm' ? '448px' : '480px'}
footerFill
onCancel={() => {
close?.();
options.onClose?.();
}}
closable={paneType !== 'confirm'}
maskClosable={false}
keepDOM={false}
closeIcon={<IconCozCross className="coz-fg-secondary" />}
>
{paneType === 'select' ? selectSpacePane : null}
{paneType === 'move' ? (
<>
<MoveDetailPane
targetSpace={targetSpace}
botID={options.botInfo.id}
fromSpaceID={fromSpaceID}
onUnmount={() => setMoveDisabled(true)}
onDetailLoaded={() => setMoveDisabled(false)}
/>
{IS_CN_REGION ? (
<div className="coz-fg-hglt-red">{I18n.t('move_desc1')}</div>
) : null}
</>
) : null}
{paneType === 'confirm' ? (
<div className="mt-[20px]">
{I18n.t('resource_move_confirm_content')}
</div>
) : null}
</Modal>
);
return {
modalNode,
open,
close,
};
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { userStoreService } from '@coze-studio/user-store';
import { SkillKeyEnum } from '@coze-agent-ide/tool-config';
import {
AddButton,
ToolContentBlock,
useToolValidData,
type ToolEntryCommonProps,
} from '@coze-agent-ide/tool';
import { I18n } from '@coze-arch/i18n';
import { OpenBlockEvent, emitEvent } from '@coze-arch/bot-utils';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { useDefaultExPandCheck } from '@coze-arch/bot-hooks';
import { useBackgroundContent } from '@coze-agent-ide/chat-background-shared';
import { type UseChatBackgroundUploaderProps } from '@coze-agent-ide/chat-background';
import {
useChatBackgroundUploader,
ChatBackGroundContent,
} from '@coze-agent-ide/chat-background';
type ITextToSpeechProps = ToolEntryCommonProps;
export const ChatBackground: React.FC<ITextToSpeechProps> = ({ title }) => {
const setToolValidData = useToolValidData();
const { backgroundImageInfoList, setBackgroundImageInfoList } =
useBotSkillStore(
useShallow($store => ({
backgroundImageInfoList: $store.backgroundImageInfoList,
setBackgroundImageInfoList: $store.setBackgroundImageInfoList,
})),
);
const isReadonly = useBotDetailIsReadonly();
const { showDot } = useBackgroundContent();
const hasBackGroundImage = Boolean(
backgroundImageInfoList?.[0]?.web_background_image?.origin_image_url,
);
const defaultExpand = useDefaultExPandCheck({
blockKey: SkillKeyEnum.BACKGROUND_IMAGE_BLOCK,
configured: hasBackGroundImage || showDot, // 无图 有进行中的状态也展示背景图模块不允许被隐藏
});
const userInfo = userStoreService.useUserInfo();
const getUserId: UseChatBackgroundUploaderProps['getUserId'] = () => ({
userId: userInfo?.user_id_str ?? '',
});
const { node, open } = useChatBackgroundUploader({
getUserId,
onSuccess: value => {
setBackgroundImageInfoList(value);
emitEvent(OpenBlockEvent.BACKGROUND_IMAGE_BLOCK);
},
backgroundValue: backgroundImageInfoList,
});
useEffect(() => {
setToolValidData(hasBackGroundImage);
}, [hasBackGroundImage]);
return (
<>
<ToolContentBlock
showBottomBorder
tooltipType={'tooltip'}
header={title}
defaultExpand={defaultExpand}
actionButton={
<>
<AddButton
tooltips={
hasBackGroundImage ? I18n.t('bgi_already_set') : undefined
}
onClick={() => {
open();
}}
disabled={hasBackGroundImage}
enableAutoHidden={true}
data-testid="bot.editor.tool.background.add-button"
/>
</>
}
>
<ChatBackGroundContent
isReadOnly={isReadonly}
backgroundImageInfoList={backgroundImageInfoList}
openConfig={open}
setBackgroundImageInfoList={setBackgroundImageInfoList}
/>
</ToolContentBlock>
{node}
</>
);
};

View File

@@ -0,0 +1,29 @@
.container {
width: 100%;
span {
width: 100%;
}
.ellipse {
&>textarea {
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: inherit;
}
}
.auto-size {
background-color: var(--semi-color-white);
&>textarea {
border-radius: 8px;
overflow-y: var(--chatflow-custom-textarea-overflow-y, hidden);
max-height: var(--chatflow-custom-textarea-focused-max-height, unset);
color: var(--semi-color-text-0, rgb(56, 55, 67));
}
}
}

View File

@@ -0,0 +1,170 @@
/*
* 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 CSSProperties,
useEffect,
useMemo,
useRef,
forwardRef,
type ForwardedRef,
useImperativeHandle,
} from 'react';
import { nanoid } from 'nanoid';
import classNames from 'classnames';
import { useBoolean } from 'ahooks';
import { type TextAreaProps } from '@coze-arch/bot-semi/Input';
import { TextArea } from '@coze-arch/bot-semi';
import styles from './index.module.less';
interface CommonTextareaType {
textAreaClassName?: string;
textAreaProps?: Partial<TextAreaProps>;
// 一种特殊的针对placeholder处理方式::placeholder达不到预期
emptyClassName?: string;
}
interface ChatflowCustomTextareaProps extends TextAreaProps {
value: string;
onChange: (
value: string,
e: React.MouseEvent<HTMLTextAreaElement, MouseEvent>,
) => void;
/** 展示模式(即需要省略时)的配置 */
ellipse?: {
rows?: number;
} & CommonTextareaType;
/** 编辑模式(即需要自动适应)的配置 */
autoSize?: {
maxHeight?: number;
} & CommonTextareaType;
readonly?: boolean;
className?: string;
style?: React.CSSProperties;
}
export const CollapsibleTextarea = forwardRef(
(
{
value,
onChange,
onBlur,
ellipse = { rows: 4 },
autoSize = { maxHeight: 340 },
readonly,
className,
style,
maxCount,
maxLength,
onFocus,
...restCommonTextAreaProps
}: ChatflowCustomTextareaProps,
ref: ForwardedRef<HTMLTextAreaElement>,
) => {
const textAreaId = useMemo(() => nanoid(), []);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const [focused, { setTrue: setFocusedTrue, setFalse: setFocusedFalse }] =
useBoolean(false);
useImperativeHandle(ref, () => ({
...(textAreaRef.current as HTMLTextAreaElement),
focus: () => setFocusedTrue(),
}));
useEffect(() => {
if (focused) {
// 加timeout可以实现focus的时候滚动到最底并光标在最后
setTimeout(() => {
if (textAreaRef.current) {
// 默认光标在最后
textAreaRef.current.setSelectionRange(
Number.MAX_SAFE_INTEGER,
Number.MAX_SAFE_INTEGER,
);
textAreaRef.current.focus();
textAreaRef.current.scroll({ top: textAreaRef.current.scrollTop });
}
});
}
}, [focused]);
const renderTextArea = () => {
if (focused) {
return (
<TextArea
autosize
// key是保证readonly变化后重新渲染
key="not-readonly"
style={
autoSize?.maxHeight
? // 这里的 style 会应用到 wrapper 上,不限定高度时会意外出现滚动条,只能通过变量修改 textarea 的 overflow
// 此外max-height 会导致预期外的 blur 事件,也只能通过 css 变量将 max-height 动态传给 textarea
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- 传递 css 变量
({
'--chatflow-custom-textarea-overflow-y': 'auto',
'--chatflow-custom-textarea-focused-max-height': `${autoSize.maxHeight}px`,
} as CSSProperties)
: undefined
}
id={textAreaId}
ref={textAreaRef}
value={value}
onBlur={e => {
setFocusedFalse();
onBlur?.(e);
}}
onChange={onChange}
readonly={readonly}
className={classNames(
styles['auto-size'],
autoSize?.textAreaClassName,
{ [autoSize?.emptyClassName || '']: !value },
)}
maxCount={maxCount}
maxLength={maxLength}
{...restCommonTextAreaProps}
{...autoSize?.textAreaProps}
/>
);
}
return (
<TextArea
// key是保证readonly变化后重新渲染
key="readonly"
style={{ WebkitLineClamp: ellipse?.rows }}
value={value}
rows={ellipse?.rows}
onFocus={e => {
onFocus?.(e);
setFocusedTrue();
}}
className={classNames(styles.ellipse, ellipse?.textAreaClassName, {
[ellipse?.emptyClassName || '']: !value,
})}
{...restCommonTextAreaProps}
{...ellipse?.textAreaProps}
/>
);
};
return (
<div className={classNames(styles.container, className)} style={style}>
{renderTextArea()}
</div>
);
},
);

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { EVENT_NAMES } from '@coze-arch/bot-tea';
import { Tooltip, UIIconButton } from '@coze-arch/bot-semi';
import { IconViewDiff } from '@coze-arch/bot-icons';
import { type PublishConnectorInfo } from '@coze-arch/bot-api/developer_api';
import { sendTeaEventInBot } from '@coze-agent-ide/agent-ide-commons';
import { useBotModeStore } from '../../store/bot-mode';
import { useConnectorDiffModal } from '../../hook/use-connector-diff-modal';
export const DiffViewButton: React.FC<{
record: PublishConnectorInfo;
isMouseIn: boolean;
}> = ({ record, isMouseIn }) => {
const { open: connectorDiffModalOpen, node: connectorDiffModalNode } =
useConnectorDiffModal();
const isCollaboration = useBotModeStore(s => s.isCollaboration);
const openConnectorDiffModal = (info: PublishConnectorInfo) => {
sendTeaEventInBot(EVENT_NAMES.bot_publish_difference, {
platform_type: info.name,
});
connectorDiffModalOpen(info);
};
return (
<>
{isMouseIn && isCollaboration ? (
<Tooltip content={I18n.t('devops_publish_multibranch_viewdiff')}>
<UIIconButton
onClick={() => {
openConnectorDiffModal(record);
}}
icon={<IconViewDiff color="#4D53E8" />}
></UIIconButton>
</Tooltip>
) : null}
{connectorDiffModalNode}
</>
);
};

View File

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

View File

@@ -0,0 +1,112 @@
/*
* 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 SetStateAction } from 'react';
import { type PublishConnectorInfo } from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { UIButton } from '@coze-arch/bot-semi';
import {
type PublishConnectorInfo as BotPublishConnectorInfo,
ConfigStatus,
} from '@coze-arch/bot-api/developer_api';
import { useParams } from 'react-router-dom';
import { useConnectorFormModal } from '../bind-connector-modal/use-connector-form-modal';
import { OLD_WX_FWH_ID } from '../../util';
interface KvBindButtonProps {
setDataSource?: (value: SetStateAction<BotPublishConnectorInfo[]>) => void;
setSelectedPlatforms?: (id: SetStateAction<string[]>) => void;
record: BotPublishConnectorInfo | PublishConnectorInfo;
/** 渠道配置成功的回调。若不传入 `unbindCallback`,解绑渠道也会调用该回调,且 bind_id 为空字符串 `''` */
bindSuccessCallback?: (value: PublishConnectorInfo | undefined) => void;
/** 解绑渠道的回调 */
unbindCallback?: () => void;
/** 绑定的 agent_type 。默认为 bot */
origin?: 'project' | 'bot';
/** 绑定的 bot_id/project_id 。不传则根据 origin 从路由参数中获取 */
originId?: string;
}
export const KvBindButton = ({
setDataSource,
setSelectedPlatforms,
record,
bindSuccessCallback,
unbindCallback,
origin = 'bot',
originId,
}: KvBindButtonProps) => {
const { bot_id = '', project_id = '' } = useParams<DynamicParams>();
// 传给后端的参数名字是 bot_id另外使用参数 agent_type 来区分 0-bot 1-project
const botId = originId ?? (origin === 'bot' ? bot_id : project_id);
const bindSuccessCb = (
value: BotPublishConnectorInfo | PublishConnectorInfo | undefined,
) => {
if (bindSuccessCallback) {
bindSuccessCallback(value as PublishConnectorInfo);
return;
}
setDataSource?.((list: BotPublishConnectorInfo[]) => {
const target = list.find(l => l.id === value?.id);
if (target) {
// 解绑旧的服务号后,需要隐藏掉旧的服务号渠道,不允许再绑定
if (target.id === OLD_WX_FWH_ID && !value?.bind_id) {
return list.filter(item => item.id !== OLD_WX_FWH_ID);
}
target.bind_id = value?.bind_id;
target.bind_info = value?.bind_info ?? {};
target.config_status = value?.bind_id
? ConfigStatus.Configured
: ConfigStatus.NotConfigured;
}
return [...list];
});
if (!value?.bind_id) {
setSelectedPlatforms?.(list => list.filter(item => item !== value?.id));
}
};
const { node: connectorFormModal, open: openConnectorsForm } =
useConnectorFormModal({
botId,
origin,
onSuccess: bindSuccessCb,
onUnbind: unbindCallback,
});
const handleConfigure = () => openConnectorsForm({ initValue: record });
const buttonText = I18n.t('bot_publish_action_configure');
return (
<>
{origin === 'project' ? (
<Button onClick={handleConfigure} size="small" color="primary">
{buttonText}
</Button>
) : (
<UIButton onClick={handleConfigure} theme="borderless">
{buttonText}
</UIButton>
)}
{connectorFormModal}
</>
);
};

View File

@@ -0,0 +1,15 @@
.wrapper-multi {
position: relative; // sheet按钮定位
:global {
.semi-sidesheet.semi-sidesheet-popup {
z-index: 100;
}
}
}
.wrapper-single {
display: grid;
grid-template-columns: 26fr 14fr;
flex: 1 1;
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRef, type PropsWithChildren } from 'react';
import classNames from 'classnames';
import { BotMode } from '@coze-arch/bot-api/developer_api';
import s from './index.module.less';
interface ContentViewProps {
mode: number;
className?: string;
style?: React.CSSProperties;
}
export const ContentView: React.FC<PropsWithChildren<ContentViewProps>> = ({
mode = 1,
className,
style,
children,
}) => {
const wrapperRef = useRef<HTMLDivElement>(null);
const isSingle = mode === BotMode.SingleMode;
const isMulti = mode === BotMode.MultiMode;
return (
<div
className={classNames(
'w-full h-full overflow-hidden',
isSingle && s['wrapper-single'],
isMulti && s['wrapper-multi'],
className,
)}
style={style}
ref={wrapperRef}
>
{children}
</div>
);
};
export default ContentView;

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { IconButton } from '@coze-arch/coze-design';
import { IconAdd } from '@coze-arch/bot-icons';
import { type ISysConfigItemGroup } from '../../hooks';
const DEFAULT_VARIABLE_LENGTH = 10;
export const AddVariable = (props: {
groupConfig?: ISysConfigItemGroup;
isReadonly: boolean;
hideAddButton?: boolean;
forceShow?: boolean;
handleInputedClick: () => void;
}) => {
const {
groupConfig,
isReadonly,
hideAddButton = false,
forceShow = false,
handleInputedClick,
} = props;
const enableVariables = groupConfig?.var_info_list ?? [];
return (enableVariables.length < DEFAULT_VARIABLE_LENGTH &&
!isReadonly &&
!hideAddButton) ||
forceShow ? (
<div className="my-3 px-[22px] text-left">
<IconButton
className="!m-0"
onClick={() => handleInputedClick()}
icon={<IconAdd />}
>
{I18n.t('bot_userProfile_add')}
</IconButton>
</div>
) : null;
};

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Spin, IconButton } from '@coze-arch/coze-design';
import { IconAdd } from '@coze-arch/bot-icons';
import { VariableTree } from '../variable-tree';
import { VariableGroupWrapper } from '../group-wrapper';
import s from '../../index.module.less';
import { type ISysConfigItemGroup, type ISysConfigItem } from '../../hooks';
const DEFAULT_VARIABLE_LENGTH = 10;
export const GroupTable = (props: {
isReadonly?: boolean;
loading?: boolean;
highLight?: boolean;
activeId?: string;
subGroupConfig?: ISysConfigItemGroup[];
variablesConfig?: ISysConfigItem[];
handleInputedClick: () => void;
hideAddButton?: boolean;
header?: React.ReactNode;
}) => {
const {
isReadonly,
loading,
highLight,
activeId,
subGroupConfig,
variablesConfig,
handleInputedClick,
hideAddButton,
header,
} = props;
const showAddButton = !isReadonly && !hideAddButton;
return (
<table className={cls(s['memory-edit-table'], 'pl-6')}>
{header}
{loading ? (
<Spin
spinning={loading}
style={{ width: '100%', height: '100%' }}
></Spin>
) : (
<>
{subGroupConfig?.map(subGroup => (
<VariableGroupWrapper variableGroup={subGroup} level={1}>
<VariableTree
isReadonly={isReadonly}
highLight={highLight}
activeId={activeId}
configList={subGroup.var_info_list}
/>
{showAddButton &&
subGroup.var_info_list?.length < DEFAULT_VARIABLE_LENGTH ? (
<div className="my-3 text-left">
<IconButton
className="!m-0"
onClick={() => handleInputedClick()}
icon={<IconAdd />}
>
{I18n.t('bot_userProfile_add')}
</IconButton>
</div>
) : null}
</VariableGroupWrapper>
))}
<VariableTree
isReadonly={isReadonly}
highLight={highLight}
activeId={activeId}
configList={variablesConfig}
/>
{showAddButton &&
variablesConfig?.length < DEFAULT_VARIABLE_LENGTH ? (
<div className="my-3 text-left">
<IconButton
className="!m-0"
onClick={() => handleInputedClick()}
icon={<IconAdd />}
>
{I18n.t('bot_userProfile_add')}
</IconButton>
</div>
) : null}
</>
)}
</table>
);
};

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type PropsWithChildren, type ReactNode, useState } from 'react';
import cls from 'classnames';
import { IconCozArrowRight } from '@coze-arch/coze-design/icons';
import { Collapsible } from '@coze-arch/coze-design';
export const VariableGroupWrapper = (
props: PropsWithChildren<{
variableGroup: {
key: string | ReactNode;
description: string | ReactNode;
};
defaultOpen?: boolean; // 添加默认展开属性
level?: number;
}>,
) => {
const { variableGroup, children, defaultOpen = true, level = 0 } = props;
const [isOpen, setIsOpen] = useState(defaultOpen);
const isTopLevel = level === 0;
return (
<>
<div
className={cls(
'flex w-full cursor-pointer flex-col px-1 py-2',
isTopLevel && 'hover:coz-mg-secondary-hovered hover:rounded-lg ',
)}
onClick={() => setIsOpen(!isOpen)}
>
<div className="flex w-full items-center">
<div className="w-6 flex items-center">
<IconCozArrowRight
className={cls(
'w-[14px] h-[14px] transition-all',
isOpen ? 'rotate-90' : '',
)}
/>
</div>
<div className="flex items-center">
<div
className={cls(
'coz-stroke-primary text-xxl font-medium coz-fg-plus',
{
'!text-sm my-[10px]': !isTopLevel,
},
)}
>
{variableGroup.key}
</div>
</div>
</div>
{isTopLevel ? (
<div className="text-sm coz-fg-secondary pl-6">
{variableGroup.description}
</div>
) : null}
</div>
<Collapsible keepDOM isOpen={isOpen}>
<div
className={cls({
'pl-3': !isTopLevel,
})}
>
{children}
</div>
</Collapsible>
</>
);
};

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.
*/
export {
SysParamHeader,
getSysItemConfig,
type ISysHeaderItem,
} from './sys-header';
export {
UserParamHeader,
getUserItemConfig,
type IUserHeaderItem,
} from './user-header';
export { type IHeaderItemProps, type ItemType } from './types';

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { exhaustiveCheckSimple } from '../../utils/exhaustive-check';
import { type IHeaderItemProps, type ItemType } from './types';
export type SysItemType = ItemType;
export type ISysHeaderItem = IHeaderItemProps;
export const SysParamHeader = (props: { isReadonly: boolean }) => {
const { isReadonly } = props;
const sysHeaderItems = [
getSysItemConfig('filed', isReadonly),
getSysItemConfig('description', isReadonly),
getSysItemConfig('default', isReadonly),
getSysItemConfig('channel', isReadonly),
getSysItemConfig('action', isReadonly),
];
return (
<thead>
<tr className="flex gap-x-4 flex-nowrap">
{sysHeaderItems.map(item =>
item ? <th className={item.className}>{item.title}</th> : null,
)}
</tr>
</thead>
);
};
export const getSysItemConfig = (
item: SysItemType,
isReadonly: boolean,
): ISysHeaderItem => {
if (item === 'filed') {
return {
type: 'filed',
className: 'w-[140px] flex-none basis-[140px] coz-fg-secondary',
title: (
<>
{I18n.t('bot_edit_memory_title_filed')}
<span className="coz-fg-hglt-red">*</span>
</>
),
};
}
if (item === 'description') {
return {
type: 'description',
className: 'w-[128px] flex-none basis-[128px] coz-fg-secondary',
title: I18n.t('bot_edit_memory_title_description'),
};
}
if (item === 'default') {
return {
type: 'default',
className: 'w-[128px] flex-none basis-[128px] coz-fg-secondary',
title: I18n.t('bot_edit_memory_title_default'),
};
}
if (item === 'channel') {
return {
type: 'channel',
className: 'w-[128px] flex-none basis-[128px] coz-fg-secondary',
title: I18n.t('variable_Table_Title_support_channels'),
};
}
if (item === 'action') {
if (isReadonly) {
return null;
}
return {
type: 'action',
className: 'w-[122px] flex-none basis-[122px] coz-fg-secondary',
title: I18n.t('bot_edit_memory_title_action'),
};
}
exhaustiveCheckSimple(item);
};

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
export type ItemType =
| 'filed'
| 'description'
| 'default'
| 'channel'
| 'action';
export interface IHeaderItemProps {
type: ItemType;
className: string;
title: string | ReactNode;
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { exhaustiveCheckSimple } from '../../utils/exhaustive-check';
import { type IHeaderItemProps, type ItemType } from './types';
export type UserItemType = Exclude<ItemType, 'channel'>;
export type IUserHeaderItem = IHeaderItemProps;
export const UserParamHeader = (props: { isReadonly: boolean }) => {
const { isReadonly } = props;
const userHeaderItems = [
getUserItemConfig('filed', isReadonly),
getUserItemConfig('description', isReadonly),
getUserItemConfig('default', isReadonly),
getUserItemConfig('action', isReadonly),
];
return (
<thead>
<tr className="flex gap-x-4 flex-nowrap">
{userHeaderItems.map(item =>
item ? <th className={item.className}>{item.title}</th> : null,
)}
</tr>
</thead>
);
};
export const getUserItemConfig = (
item: UserItemType,
isReadonly: boolean,
): IUserHeaderItem => {
if (item === 'filed') {
return {
type: 'filed',
className: 'flex-1 coz-fg-secondary',
title: (
<>
{I18n.t('bot_edit_memory_title_filed')}
<span className="coz-fg-hglt-red">*</span>
</>
),
};
}
if (item === 'description') {
return {
type: 'description',
className: 'flex-1 coz-fg-secondary',
title: I18n.t('bot_edit_memory_title_description'),
};
}
if (item === 'default') {
return {
type: 'default',
className: 'w-[164px] flex-none basis-[164px] coz-fg-secondary',
title: I18n.t('bot_edit_memory_title_default'),
};
}
if (item === 'action') {
if (isReadonly) {
return null;
}
return {
type: 'action',
className: 'w-[122px] flex-none basis-[122px] coz-fg-secondary',
title: I18n.t('bot_edit_memory_title_action'),
};
}
exhaustiveCheckSimple(item);
};

View File

@@ -0,0 +1,51 @@
/*
* 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 s from '../../index.module.less';
import { type ISysConfigItem } from '../../hooks';
export const VariableTree = (props: {
isReadonly?: boolean;
highLight?: boolean;
activeId?: string;
configList: ISysConfigItem[];
}) => {
const { isReadonly, highLight, activeId, configList } = props;
return (
<tbody className="overflow-visible flex-1 h-0">
{configList.map((item: ISysConfigItem, index: number) => (
<tr
key={`memory-row-list_${index}`}
className={classNames(
s['memory-row'],
activeId === item.id && highLight && s['active-row'],
activeId === item.id && highLight && 'active-row',
'flex gap-x-4 flex-nowrap',
)}
>
{item.key ? <td>{item.key}</td> : null}
{item.description ? <td>{item.description}</td> : null}
{item.default_value ? <td>{item.default_value}</td> : null}
{item.channel ? <td>{item.channel}</td> : null}
{item.method && !isReadonly ? <td>{item.method}</td> : null}
</tr>
))}
</tbody>
);
};

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useParams } from 'react-router-dom';
import React, { type FC, useState, useEffect } from 'react';
import { SkillKeyEnum } from '@coze-agent-ide/tool-config';
import {
AddButton,
ToolContentBlock,
useToolValidData,
type ToolEntryCommonProps,
} from '@coze-agent-ide/tool';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { I18n } from '@coze-arch/i18n';
import { OpenBlockEvent, emitEvent } from '@coze-arch/bot-utils';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { useDefaultExPandCheck } from '@coze-arch/bot-hooks';
import { DataErrorBoundary, DataNamespace } from '@coze-data/reporter';
import { MemoryList } from './memory-list';
import { MemoryAddModal } from './memory-add-modal';
import s from './index.module.less';
const MAX_SIZE = 10;
type IDataMemoryProps = ToolEntryCommonProps;
const BaseDataMemory: FC<IDataMemoryProps> = ({ title }) => {
const setToolValidData = useToolValidData();
const variables = useBotSkillStore($store => $store.variables);
const [visible, setVisible] = useState(false);
const isReadonly = useBotDetailIsReadonly();
const [activeId, setActiveId] = useState<undefined | string>();
const params = useParams<DynamicParams>();
const onOpenMemoryAdd = ($activeId?: string) => {
if (isReadonly) {
return;
}
sendTeaEvent(EVENT_NAMES.memory_click_front, {
bot_id: params?.bot_id || '',
resource_type: 'variable',
action: 'turn_on',
source: 'bot_detail_page',
source_detail: 'memory_manage',
});
setVisible(true);
setActiveId($activeId);
};
const defaultExpand = useDefaultExPandCheck({
blockKey: SkillKeyEnum.DATA_MEMORY_BLOCK,
configured: variables.length > 0,
});
useEffect(() => {
setToolValidData(Boolean(variables?.length));
}, [variables?.length]);
return (
<>
<ToolContentBlock
blockEventName={OpenBlockEvent.DATA_MEMORY_BLOCK_OPEN}
showBottomBorder
header={title}
defaultExpand={defaultExpand}
// icon={userInfo}
actionButton={
<>
<AddButton
tooltips={
variables.length < MAX_SIZE
? I18n.t('bot_edit_variable_add_tooltip')
: I18n.t('bot_edit_variable_add_tooltip_edit')
}
onClick={() => onOpenMemoryAdd()}
enableAutoHidden={true}
data-testid="bot.editor.tool.data-memory.add-button"
/>
</>
}
>
<div className={s['memory-content']}>
<MemoryList onOpenMemoryAdd={onOpenMemoryAdd} />
</div>
</ToolContentBlock>
<MemoryAddModal
visible={visible}
activeId={activeId}
onCancel={() => {
setVisible(false);
sendTeaEvent(EVENT_NAMES.memory_click_front, {
bot_id: params?.bot_id || '',
resource_type: 'variable',
action: 'turn_off',
source: 'bot_detail_page',
source_detail: 'memory_manage',
});
}}
onOk={() => {
setVisible(false);
emitEvent(OpenBlockEvent.DATA_MEMORY_BLOCK_OPEN);
}}
/>
</>
);
};
export const DataMemory: FC<IDataMemoryProps> = props => (
<DataErrorBoundary namespace={DataNamespace.VARIABLE}>
<BaseDataMemory {...props} />
</DataErrorBoundary>
);

View File

@@ -0,0 +1,369 @@
/*
* 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 React, { useEffect, useMemo, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { nanoid } from 'nanoid';
import classNames from 'classnames';
import { useRequest } from 'ahooks';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { type VariableItem } from '@coze-studio/bot-detail-store';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import {
Checkbox,
Space,
Switch,
Tooltip,
Typography,
} from '@coze-arch/coze-design';
import { IconInfo } from '@coze-arch/bot-icons';
import { type GetSysVariableConfResponse } from '@coze-arch/bot-api/memory';
import { MemoryApi } from '@coze-arch/bot-api';
import { BotE2e } from '@coze-data/e2e';
const { Text } = Typography;
import s from './index.module.less';
export type TVariable = VariableItem & {
enable?: boolean;
must_not_use_in_prompt?: string; // 服务端类型已上线无法改boolean。""、"false"、"true"
ext_desc?: string;
prompt_disabled?: boolean;
// eslint-disable-next-line @typescript-eslint/naming-convention
EffectiveChannelList?: string[];
};
export interface ISysConfigItem {
id: string;
key: React.ReactNode;
default_value: React.ReactNode;
description: React.ReactNode;
channel?: React.ReactNode;
method?: React.ReactNode;
}
export interface ISysConfigItemGroup {
id: string;
key: React.ReactNode;
default_value: React.ReactNode;
description: React.ReactNode;
method?: React.ReactNode;
channel?: React.ReactNode;
var_info_list?: ISysConfigItem[];
}
export interface SystemConfig {
sysConfigList: ISysConfigItemGroup[];
sysVariables: TVariable[];
enableVariables: VariableItem[];
loading: boolean;
}
export interface SysConfigData {
conf: TVariable[];
groupConf: GetSysVariableConfResponse['group_conf'];
}
export const useSystemVariables = (
variables: VariableItem[],
visible: boolean,
): SystemConfig => {
const { run, loading } = useRequest(async () => {
const res = await MemoryApi.GetSysVariableConf();
const resData = res?.group_conf?.reduce(
(prev, cur) => {
cur.group_name
? prev.group_conf.push(cur)
: (prev.conf = prev.conf?.concat(cur.var_info_list || []));
return prev;
},
{
conf: [],
group_conf: [],
},
);
// 分组新逻辑
const configInfo = initSysVarStatus(resData);
setConfig(configInfo);
});
const { variables: values } = useBotSkillStore(
useShallow(state => ({
variables: state.variables,
})),
);
const [sysConfig, setConfig] = useState<SysConfigData>({
conf: [],
groupConf: [],
});
// 这里需要根据config来设置sysVariables
const sysVariables = useMemo(() => {
const group = sysConfig.groupConf?.reduce(
(prev, cur) => prev.concat(cur?.var_info_list),
[],
);
return [...sysConfig.conf, ...group];
}, [sysConfig]);
// 拼接已启用的系统变量和自定义变量
const enableVariables = useMemo(() => {
const enableSysVariables =
sysVariables
.filter(v => v.enable)
?.map(sys => ({ ...sys, is_system: true })) || [];
const customVariables =
variables.filter(variable => !variable.is_system) || [];
return [...enableSysVariables, ...customVariables];
}, [variables, sysVariables]);
const initSysVarStatus = data => {
const { conf = [], group_conf = [] } = data || {};
const setItem = varItem => {
const enableItem: TVariable | undefined = values?.find(
item => item.key === varItem.key && item.is_system,
);
return {
...varItem,
is_system: enableItem?.is_system,
enable: !!enableItem,
prompt_disabled: enableItem?.prompt_disabled ?? true,
};
};
const confLIst = conf?.map(setItem);
const groupConfList = group_conf?.map(group => ({
...group,
var_info_list: group.var_info_list?.map(groupItem => ({
...setItem(groupItem),
prompt_disabled: true,
channel: groupItem?.EffectiveChannelList?.join(','),
})),
}));
return {
conf: confLIst || [],
groupConf: groupConfList || [],
};
};
useEffect(() => {
if (visible) {
run();
}
}, [visible]);
const setSysConfigStatus = (key, prop, checked) => {
const { conf = [], groupConf = [] } = sysConfig;
const configIndex = conf.findIndex(confItem => confItem.key === key);
if (configIndex !== -1) {
conf[configIndex][prop] = checked;
if (prop === 'enable') {
conf[configIndex].prompt_disabled = !checked;
}
}
groupConf.forEach(groupItem => {
const index = groupItem?.var_info_list.findIndex(
item => item.key === key,
);
if (index !== -1) {
groupItem.var_info_list[index][prop] = checked;
}
setConfig({ conf, groupConf });
});
};
const changeEnable = (checked: boolean, key: string) => {
setSysConfigStatus(key, 'enable', checked);
};
const changeCheckbox = (checked: boolean, key: string) => {
setSysConfigStatus(key, 'prompt_disabled', checked);
};
const SysVarConfigRender = ({
value,
enable,
e2e,
extDesc,
className,
}: {
value: string;
enable: boolean | undefined;
e2e?: string;
extDesc?: string;
className?: string;
}): JSX.Element => (
<div
className={classNames(
[s.sys_item_box, !enable && s.disabled, className],
'flex items-center',
)}
data-dtestid={e2e}
>
<Text ellipsis={{ showTooltip: true }}>{value}</Text>
{!!extDesc && (
<Tooltip content={I18n.t(extDesc as I18nKeysNoOptionsType)}>
<IconInfo
style={{
color: '#C6CACD',
marginLeft: 4,
}}
/>
</Tooltip>
)}
</div>
);
const SysVarGroupConfigRender = ({
value,
e2e,
enable = true,
extDesc,
className,
}: {
value: string;
e2e?: string;
enable?: boolean;
extDesc?: string;
className?: string;
}): JSX.Element => (
<div
className={classNames([
s.sys_item_group,
!enable && s.disabled,
className,
])}
data-dtestid={e2e}
>
<div>{value}</div>
{!!extDesc && (
<Tooltip content={I18n.t(extDesc as I18nKeysNoOptionsType)}>
<IconInfo
style={{
color: '#C6CACD',
marginLeft: 4,
}}
/>
</Tooltip>
)}
</div>
);
const configItem = (
item: TVariable,
promptDisabled = false,
): ISysConfigItem => ({
id: item.key,
key: SysVarConfigRender({
value: item.key ?? '',
enable: item.enable,
e2e: `${BotE2e.BotVariableAddModalNameText}.${item.key}`,
extDesc: item.ext_desc,
className: 'w-[140px] flex-none basis-[140px]',
}),
description: SysVarConfigRender({
value: item.description ?? '',
enable: item.enable,
e2e: `${BotE2e.BotVariableAddModalDescText}.${item.key}`,
className: 'w-[128px] flex-none basis-[128px]',
}),
default_value: SysVarConfigRender({
value: item.default_value || '--',
enable: item.enable,
e2e: `${BotE2e.BotVariableAddModalDefaultValueText}.${item.key}`,
className: 'w-[128px] flex-none basis-[128px]',
}),
channel: SysVarConfigRender({
value: item.channel || '--',
enable: item.enable,
className: 'w-[128px] flex-none basis-[128px]',
}),
method: (
<Space className={s['memory-method']} spacing={24}>
<Tooltip content={I18n.t('variable_240520_03')} theme="dark">
<div className={s['memory-method-checkbox']}>
<Checkbox
disabled={
promptDisabled ||
!item.enable ||
item.must_not_use_in_prompt === 'true'
}
checked={item?.prompt_disabled ? false : true}
onChange={v => {
changeCheckbox(!v.target.checked, item.key);
}}
></Checkbox>
</div>
</Tooltip>
<Tooltip
showArrow
position="top"
theme="dark"
zIndex={1031}
style={{
backgroundColor: '#41464c',
color: '#fff',
maxWidth: '276px',
}}
content={I18n.t('variable_240407_01')}
>
<Switch
data-dtestid={`${BotE2e.BotVariableAddModalSwitch}.${item.key}`}
size="small"
checked={item?.enable ?? false}
onChange={checked => changeEnable(checked, item.key)}
/>
</Tooltip>
</Space>
),
});
const groupList: ISysConfigItemGroup[] = sysConfig?.groupConf?.map(item => ({
id: nanoid(),
key: SysVarGroupConfigRender({
value: item.group_name ?? '--',
e2e: `${BotE2e.BotVariableAddModalNameText}.${item.group_name}`,
extDesc: item?.group_ext_desc,
className: 'w-[140px] flex-none basis-[140px]',
}),
description: SysVarConfigRender({
value: item.group_desc || '--',
enable: true,
e2e: `${BotE2e.BotVariableAddModalDescText}.${item.group_name}`,
className: 'w-[128px] flex-none basis-[128px]',
}),
default_value: SysVarConfigRender({
value: '--',
enable: true,
e2e: `${BotE2e.BotVariableAddModalDefaultValueText}.${item.group_name}`,
className: 'w-[128px] flex-none basis-[128px]',
}),
channel: SysVarConfigRender({
value: '--',
enable: true,
className: 'w-[128px] flex-none basis-[128px]',
}),
var_info_list: item?.var_info_list?.length
? item?.var_info_list.map(childItem => configItem(childItem, true))
: undefined,
}));
// 系统变量
const sysConfigList: ISysConfigItem[] = sysConfig?.conf?.map(item =>
configItem(item),
);
return {
sysConfigList: [...sysConfigList, ...groupList],
sysVariables,
enableVariables,
loading,
};
};

View File

@@ -0,0 +1,446 @@
/* stylelint-disable declaration-no-important */
/* stylelint-disable max-nesting-depth */
/* stylelint-disable selector-class-pattern */
/* stylelint-disable no-descending-specificity */
@import '../../assets/styles/common.less';
@import '../../assets/styles/mixins.less';
@import '../../assets/styles/index.module.less';
.memory-content {
user-select: none !important;
}
.memory-list {
display: flex;
flex-wrap: wrap;
align-items: center;
:global {
.semi-tag-grey-light {
cursor: pointer;
margin: 0 10px 12px 0;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--light-color-grey-grey-5, #6B6B75);
background: var(--light-usage-fill-color-fill-1, rgba(46, 46, 56, 8%));
border-radius: 6px;
&:hover {
background: var(--light-usage-fill-color-fill-2, rgba(46, 46, 56, 12%));
}
}
}
}
.template-footer {
display: flex;
align-items: center;
justify-content: flex-end;
.template-cancel-button {
min-width: 98px;
background-color: #fff;
&:hover {
background-color: rgba(46, 46, 56, 8%) !important;
}
>span {
font-size: 14px;
font-weight: 600;
line-height: 22px;
color: var(--light-usage-text-color-text-0, #1C1D23);
}
}
}
.template-demo {
.desc {
font-size: 12px;
line-height: 16px;
color: #000;
}
.image {
width: 100%;
margin: 16px 0 8px;
background: #FFF;
border: 1px solid #ededee;
border-radius: 10px;
.image-template {
display: block;
width: 100%;
>img {
width: 100%;
}
}
}
.tip {
margin-bottom: 8px;
font-size: 10px;
line-height: 16px;
color: var(--light-usage-text-color-text-2, rgba(29, 28, 35, 60%));
}
}
.template-variable-list {
display: block;
width: 100%;
border-radius: 8px;
>img {
width: 100%;
}
}
.use-template-pop-confirm {
:global {
.semi-button.semi-button-with-icon-only.semi-button-size-small {
display: none;
}
}
}
.tip-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 560px;
padding: 12px;
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 18px;
color: var(--light-color-grey-grey-8, #2e3238);
.tip-top {
padding: 12px 8px;
background: var(--light-color-grey-grey-0, #f9f9f9);
border-radius: 8px;
}
.tip-bottom {
display: flex;
align-items: center;
justify-content: center;
margin-top: 4px;
}
}
.default-text {
.tip-text;
}
.view-examples {
display: flex;
align-items: center;
margin-bottom: 12px;
font-size: 14px;
line-height: 22px;
color: var(--light-color-brand-brand-5, #4D53E8);
.view-examples-text,
.view-examples-icon {
cursor: pointer;
}
}
.memory-add-modal {
background-color: #F7F7FA;
:global {
.semi-modal-body {
display: flex;
flex-direction: column;
padding-bottom: 0 !important;
}
.semi-modal-content {
height: calc(100vh - 140px);
background-color: #F7F7FA;
}
.semi-modal-footer {
margin-top: 12px;
}
}
.add-button-row-fix {
margin-bottom: 38px;
padding: 0 16px 12px 32px;
text-align: left;
.add-button {
width: 217px;
margin: 0 !important;
padding: 0 48px;
}
}
.modal-add-container {
display: flex;
flex: 1;
flex-direction: column;
.memory-add-empty {
margin-top: -8.5%;
:global {
.semi-empty-content {
margin-top: 16px;
}
.semi-empty-title {
font-size: 16px;
font-weight: 600;
line-height: 22px;
color: var(--light-usage-text-color-text-0, #1D1C23);
}
}
}
&.center {
justify-content: center;
}
.use-template {
display: flex;
flex-shrink: 0;
justify-content: flex-end;
margin-bottom: 16px;
padding-top: 8px;
}
.memory-edit-table {
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
font-size: 12px;
font-weight: 400;
line-height: 16px;
color: var(--light-usage-text-color-text-2, rgba(28, 31, 35, 60%));
thead {
flex-shrink: 0;
tr {
height: 28px;
padding: 6px 16px 6px 0;
border-bottom: 1px solid var(--light-usage-border-color-border-1, rgba(29, 28, 35, 12%));
}
th {
font-size: 12px;
font-weight: 600;
font-style: normal;
line-height: 16px;
color: var(--Fg-COZ-fg-secondary, rgba(27, 41, 73, 62%));
text-align: start;
// padding: 0 12px;
&:last-child {
padding: 0;
}
}
}
.memory-row {
position: relative;
align-items: flex-start;
padding: 12px 16px 12px 0;
border-radius: 8px;
transition: background linear 300ms;
&.active-row {
background: var(--light-color-brand-brand-1, #D9DCFA);
}
td {
padding: 0;
}
}
.add-button-row {
margin: 12px 0;
padding: 0 22px;
text-align: left;
.add-button {
width: 217px;
margin: 0 !important;
padding: 0 48px;
}
}
}
.memory-key-err {
position: relative;
color: var(--light-color-red-red-5, #f93920);
.key-error-tip {
position: absolute;
bottom: -20px;
left: 0;
width: 100%;
}
:global {
.semi-input-wrapper {
border: 1px solid var(--light-color-red-red-5, #f93920);
}
}
}
.memory-key-readonly {
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: var(--light-color-grey-grey-8, #2e3238);
}
.memory-description-readonly {
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: var(--light-color-grey-grey-8, #2e3238);
}
.readonly-none {
cursor: not-allowed;
width: 100%;
padding: 6px;
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: var(--semi-color-disabled-text);
background: var(--light-color-grey-grey-1, #edeff2);
border: 1px solid var(--semi-color-border);
border-radius: 8px;
}
.memory-description-readonly,
.memory-key-readonly {
width: 100%;
height: 32px;
padding: 6px;
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px;
color: var(--light-usage-text-color-text-1, rgba(28, 29, 35, 80%));
background: var(--light-color-grey-grey-1, #edeff2);
border-radius: 8px;
}
.memory-method {
display: flex;
align-items: center;
justify-content: flex-start;
height: 32px;
text-align: center;
:global {
.semi-space {
height: 32px;
}
}
&-checkbox {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
}
}
}
.sys_item_box {
min-height: 32px;
padding-left: 12px;
font-size: 14px;
font-weight: 400;
line-height: 32px;
color: var(--Light-usage-text---color-text-0, #1D1C23);
// &.disabled {
// color: var(--Light-usage-text---color-text-3, rgba(29, 28, 35, 35%));
// }
}
.sys_item_group {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 600;
line-height: 20px;
color: var(--Light-usage-text---color-text-0, #1D1C23);
&.disabled {
color: var(--Light-usage-text---color-text-3, rgba(29, 28, 35, 35%));
}
}
.group-collapsible {
display: flex;
width: 100%;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
color: var(--Light-usage-text---color-text-0, #1D1C23);
&-key {
cursor: pointer;
display: flex;
align-items: center;
}
&-value {
div {
padding-left: 16px;
}
}
&-desc {
div {
padding-left:8px;
}
}
}

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 { DataMemory } from './data-memory';

View File

@@ -0,0 +1,489 @@
/*
* 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 */
/* eslint-disable max-lines-per-function */
import { type ComponentProps, useState, useRef, useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { nanoid } from 'nanoid';
import { uniqBy } from 'lodash-es';
import classNames from 'classnames';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import {
useBotDetailIsReadonly,
type VariableItem,
uniqMemoryList,
VariableKeyErrType,
} from '@coze-studio/bot-detail-store';
import { useBotInfoAuditor } from '@coze-studio/bot-audit-adapter';
import { BotE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozTrashCan, IconCozPlus } from '@coze-arch/coze-design/icons';
import {
IconButton,
Modal,
Input,
Typography,
Tooltip,
Space,
Form,
Checkbox,
Switch,
Button,
} from '@coze-arch/coze-design';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { AddButton } from '../add-button';
import { MemoryTemplateModal } from './memory-template-modal';
import { useSystemVariables } from './hooks';
import { SysParamHeader, UserParamHeader } from './components/parma-header';
import { VariableGroupWrapper } from './components/group-wrapper';
import { GroupTable } from './components/group-table';
import s from './index.module.less';
const DEFAULT_VARIABLE_LENGTH = 10;
const ACTIVE_ID_TIMER_INTERVAL = 1000;
const INPUT_TIMER_INTERVAL = 100;
export type MemoryAddModalProps = ComponentProps<typeof Modal> & {
activeId?: string;
onOk?: () => void;
onCancel?: () => void;
};
export const MemoryAddModal: React.FC<MemoryAddModalProps> = props => {
const isReadonly = useBotDetailIsReadonly();
const botInfoAuditor = useBotInfoAuditor();
const { variables: variablesInStore, setBotSkillByImmer } = useBotSkillStore(
useShallow(state => ({
variables: state.variables,
setBotSkillByImmer: state.setBotSkillByImmer,
})),
);
const { botId } = useBotInfoStore(
useShallow(state => ({
botId: state.botId,
})),
);
const [variables, setVariables] = useState<VariableItem[]>([]);
const [visible, setVisible] = useState(false);
const [highLight, setHighLight] = useState(false);
const [timer, setTimer] = useState<undefined | NodeJS.Timeout>();
const inputingRef = useRef<HTMLInputElement>(null);
const tbodyRef = useRef<HTMLTableSectionElement>(null);
const [addButtonFix, setAddButtonFix] = useState(false);
const { sysConfigList, sysVariables, enableVariables, loading } =
useSystemVariables(variables, !!props.visible);
const onBlur = () => {
setVariables(uniqMemoryList(variables, sysVariables));
};
useEffect(() => {
if (props.visible) {
setVariables(
uniqMemoryList(
variablesInStore?.filter(varItem => !varItem.is_system),
sysVariables,
),
);
if (!variablesInStore.length) {
handleInputedClick('init');
}
if (props.activeId) {
clearTimeout(timer);
setHighLight(true);
setTimer(
setTimeout(() => {
setHighLight(false);
}, ACTIVE_ID_TIMER_INTERVAL),
);
}
}
}, [props.activeId, props.visible]);
useEffect(() => {
// 控制高亮的元素滚至视区内
if (highLight) {
document.getElementsByClassName('active-row')?.[0]?.scrollIntoView();
}
}, [highLight]);
useEffect(() => {
if (tbodyRef.current) {
const tbodyScrollHeight = tbodyRef.current.scrollHeight;
const tbodyClientHeight = tbodyRef.current.clientHeight;
setAddButtonFix(tbodyScrollHeight > tbodyClientHeight);
}
}, [tbodyRef.current, variables.length]);
const handleInputedClick = (type?: 'init') => {
sendTeaEvent(EVENT_NAMES.memory_click_front, {
bot_id: botId,
resource_type: 'variable',
action: 'add',
source: 'bot_detail_page',
source_detail: 'memory_manage',
});
setVariables([
...(type === 'init' ? [] : variables),
{
id: nanoid(),
key: '',
description: '',
default_value: '',
prompt_disabled: false,
},
]);
setTimeout(() => {
inputingRef?.current?.focus();
}, INPUT_TIMER_INTERVAL);
};
const mutateItemByKey = (
key: string,
value: string | boolean | undefined,
index: number,
) => {
const tempArr = [...variables];
tempArr[index] = { ...tempArr[index], [key]: value };
setVariables(uniqMemoryList([...tempArr], sysVariables));
botInfoAuditor.reset();
};
const onCancel = () => {
botInfoAuditor.reset();
props?.onCancel?.();
};
const configList = variables.map((item: VariableItem, index: number) => {
const sendTeaEventEdit = () => {
sendTeaEvent(EVENT_NAMES.memory_click_front, {
bot_id: botId,
resource_id: item.id,
resource_name: item.key,
resource_type: 'variable',
action: 'edit',
source: 'bot_detail_page',
source_detail: 'memory_manage',
});
};
return {
id: item.key,
key: !isReadonly ? (
<div
className={classNames(s['memory-key'], {
[s['memory-key-err']]: item.errType,
})}
>
<Input
data-testid={`${BotE2e.BotVariableAddModalNameInput}.${item.key}`}
data-dtestid={`${BotE2e.BotVariableAddModalNameInput}.${item.key}`}
disabled={isReadonly}
placeholder={I18n.t('variable_name_placeholder')}
className="flex-1"
value={item.key}
ref={inputingRef}
onChange={v => {
mutateItemByKey('key', v, index);
}}
autoFocus={!item.key}
maxLength={50}
onBlur={() => {
sendTeaEventEdit();
onBlur();
}}
/>
{item.errType === VariableKeyErrType.KEY_NAME_USED && (
<span className={s['key-error-tip']}>
{I18n.t('bot_edit_variable_field_occupied_error')}
</span>
)}
{item.errType === VariableKeyErrType.KEY_IS_NULL && (
<span className={s['key-error-tip']}>
{I18n.t('bot_edit_variable_field_required_error')}
</span>
)}
</div>
) : (
<Typography.Text
data-testid={`${BotE2e.BotVariableAddModalNameInput}.${item.key}`}
className={classNames(
s['memory-key-readonly'],
!item.key && s['readonly-none'],
'flex-1',
)}
ellipsis={{ showTooltip: true }}
>
{item.key || I18n.t('bot_element_unset')}
</Typography.Text>
),
description: !isReadonly ? (
<Input
data-testid={`${BotE2e.BotVariableAddModalDescInput}.${item.key}`}
disabled={isReadonly}
className={classNames(s['memory-description'], 'flex-1')}
placeholder={I18n.t('bot_edit_variable_description_placeholder')}
value={item.description}
onChange={v => {
mutateItemByKey('description', v, index);
}}
maxLength={200}
onBlur={() => {
sendTeaEventEdit();
onBlur();
}}
/>
) : (
<Typography.Text
data-testid={`${BotE2e.BotVariableAddModalDescInput}.${item.key}`}
className={classNames(
s['memory-description-readonly'],
!item.description && s['readonly-none'],
'flex-1',
)}
ellipsis={{ showTooltip: true }}
>
{item.description || I18n.t('bot_element_unset')}
</Typography.Text>
),
default_value: !isReadonly ? (
<Input
data-testid={`${BotE2e.BotVariableAddModalDefaultValueInput}.${item.key}`}
disabled={isReadonly}
className={classNames(
s['memory-description'],
'w-[164px] basis-[164px] flex-none',
)}
placeholder={I18n.t('bot_edit_variable_default_value_placeholder')}
value={item.default_value}
onChange={v => {
mutateItemByKey('default_value', v, index);
}}
maxLength={1000}
onBlur={() => {
sendTeaEventEdit();
onBlur();
}}
/>
) : (
<Typography.Text
data-testid={`${BotE2e.BotVariableAddModalDefaultValueInput}.${item.key}`}
className={classNames(
s['memory-description-readonly'],
!item.default_value && s['readonly-none'],
'w-[164px] basis-[164px] flex-none',
)}
ellipsis={{ showTooltip: true }}
>
{item.default_value || I18n.t('bot_element_unset')}
</Typography.Text>
),
method: (
<Space className={s['memory-method']} spacing={14}>
<Tooltip content={I18n.t('variable_240520_03')} theme="dark">
<div className={s['memory-method-checkbox']}>
<Checkbox
checked={item?.prompt_disabled ? false : true}
onChange={v => {
mutateItemByKey('prompt_disabled', !v.target.checked, index);
}}
></Checkbox>
</div>
</Tooltip>
<Switch
data-testid={`${BotE2e.BotVariableAddModalSwitch}.${item.key}`}
size="small"
checked={!item?.is_disabled}
onChange={checked => {
mutateItemByKey('is_disabled', !checked, index);
}}
/>
<Tooltip content={I18n.t('bot_datamemory_remove_field')} theme="dark">
<IconButton
data-dtestid={`${BotE2e.BotVariableAddModalDelBtn}.${item.key}`}
icon={<IconCozTrashCan />}
color="secondary"
onClick={() => {
if (isReadonly) {
return;
}
sendTeaEvent(EVENT_NAMES.memory_click_front, {
bot_id: botId,
resource_id: item.id,
resource_name: item.key,
resource_type: 'variable',
action: 'delete',
source: 'bot_detail_page',
source_detail: 'memory_manage',
});
variables.splice(index, 1);
onBlur();
}}
/>
</Tooltip>
</Space>
),
};
});
return (
<Modal
{...props}
centered
onCancel={onCancel}
footer={
<>
{enableVariables.length < DEFAULT_VARIABLE_LENGTH && addButtonFix ? (
<div className={s['add-button-row-fix']}>
<AddButton
className={s['add-button']}
type="tertiary"
onClick={() => handleInputedClick()}
icon={<IconCozPlus />}
>
{I18n.t('bot_userProfile_add')}
</AddButton>
</div>
) : null}
<div className={s['template-footer']}>
<Button
data-testid={BotE2e.BotVariableAddModalCancelBtn}
color="primary"
onClick={onCancel}
>
{I18n.t('edit_variables_modal_cancel_text')}
</Button>
<Button
data-testid={BotE2e.BotVariableAddModalSaveBtn}
disabled={variables.some(
item =>
item.errType === VariableKeyErrType.KEY_NAME_USED ||
item.errType === VariableKeyErrType.KEY_IS_NULL,
)}
onClick={async () => {
const checkPass = await botInfoAuditor.check({
variable_list: variables.map(i => ({
key: i.key,
description: i.description,
default_value: i.default_value,
})),
});
if (checkPass.check_not_pass) {
return;
}
setBotSkillByImmer(botSkill => {
botSkill.variables = [...enableVariables];
});
props?.onOk?.();
}}
>
{I18n.t('edit_variables_modal_ok_text')}
</Button>
</div>
</>
}
width={800}
title={I18n.t('edit_variables_modal_title')}
className={classNames(s['memory-add-modal'], props.className)}
>
<div
className={classNames(
s['modal-add-container'],
!variables.length && s.center,
'gap-y-2',
)}
>
{/* 用户变量 */}
<VariableGroupWrapper
variableGroup={{
key: I18n.t('variable_user_name'),
description: I18n.t('variable_user_description'),
}}
>
<GroupTable
isReadonly={isReadonly}
loading={loading}
highLight={highLight}
activeId={props.activeId}
variablesConfig={configList}
handleInputedClick={handleInputedClick}
header={<UserParamHeader isReadonly={isReadonly} />}
/>
</VariableGroupWrapper>
{/* 系统变量 */}
<VariableGroupWrapper
variableGroup={{
key: I18n.t('variable_system_name'),
description: I18n.t('variable_system_describtion'),
}}
>
<GroupTable
isReadonly={isReadonly}
loading={loading}
highLight={highLight}
activeId={props.activeId}
subGroupConfig={sysConfigList.filter(item => item.var_info_list)}
variablesConfig={sysConfigList.filter(item => !item.var_info_list)}
handleInputedClick={handleInputedClick}
header={<SysParamHeader isReadonly={isReadonly} />}
hideAddButton={true}
/>
</VariableGroupWrapper>
{!botInfoAuditor.pass && (
<Form.ErrorMessage
error={I18n.t('variable_edit_not_pass')}
></Form.ErrorMessage>
)}
<MemoryTemplateModal
visible={visible}
needSecondConfirm={!!variables.length}
showType="variableList"
addTemplate={(arr: VariableItem[]) => {
const result = [
// 使用模版时覆盖历史变量
// ...variables,
...arr.map(q => ({
id: nanoid(),
...q,
key: q.key,
description: q.description,
default_value: q.default_value,
})),
];
setVariables(uniqBy(result, 'key').filter(i => i.key));
setVisible(false);
}}
onCancel={() => {
setVisible(false);
}}
onOk={() => {
setVisible(false);
}}
/>
</div>
</Modal>
);
};

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { I18n } from '@coze-arch/i18n';
import { Tag, Tooltip } from '@coze-arch/bot-semi';
import { IconChevronRight } from '@douyinfe/semi-icons';
import { MemoryTemplateModal } from './memory-template-modal';
import s from './index.module.less';
export const MemoryList = ({
onOpenMemoryAdd,
}: {
onOpenMemoryAdd: (activeKey?: string) => void;
}) => {
const variables = useBotSkillStore(innerS => innerS.variables);
const [visible, setVisible] = useState(false);
const ELLIPSIS_SIZE = 13;
return (
<div>
{variables.some(item => item.key) ? (
<div className={s['memory-list']}>
{variables.map(item => {
if (!item.key) {
return;
}
return item.key.length > ELLIPSIS_SIZE ? (
<Tooltip content={item.key}>
<Tag
color="grey"
key={`config-item_${item.key}`}
onClick={() => onOpenMemoryAdd(item.key)}
>
{item.key.slice(0, ELLIPSIS_SIZE)}...
</Tag>
</Tooltip>
) : (
<Tag
color="grey"
key={`config-item_${item.key}`}
onClick={() => onOpenMemoryAdd(item.key)}
>
{item.key}
</Tag>
);
})}
</div>
) : (
<>
<div className={s['default-text']}>
{I18n.t('user_profile_intro')}
</div>
{FEATURE_ENABLE_VARIABLE ? (
<div className={s['view-examples']}>
<div
className={s['view-examples-text']}
onClick={() => setVisible(true)}
>
View examples
</div>
<IconChevronRight
className={s['view-examples-icon']}
size="small"
style={{ marginLeft: 4 }}
onClick={() => setVisible(true)}
/>
</div>
) : null}
<MemoryTemplateModal
visible={visible}
onCancel={() => {
setVisible(false);
}}
onOk={() => {
setVisible(false);
}}
/>
</>
)}
</div>
);
};

View File

@@ -0,0 +1,142 @@
/*
* 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 ComponentProps } from 'react';
import { type VariableItem } from '@coze-studio/bot-detail-store';
import { UIModal, Image, type Modal } from '@coze-arch/bot-semi';
import { Button, Popconfirm } from '@coze-arch/bot-semi';
import { I18n } from '@coze-arch/i18n';
import { IconAlertCircle } from '@douyinfe/semi-icons';
import { BotDebugButton } from '../bot-debug-button';
import IMG_TEMPLATE_USE_I18N from '../../assets/image/template_i18n.png';
import templateSample from '../../assets/image/sample3_i18n.png';
import s from './index.module.less';
export type MemoryTemplateModalProps = ComponentProps<typeof Modal> & {
addTemplate?: (arr: VariableItem[]) => void;
needSecondConfirm?: boolean;
showType?: 'variableList';
};
const list: VariableItem[] = [
{
key: 'Name',
description: I18n.t('profile_memory_sample_description_name'),
},
{
key: 'Address',
description: I18n.t('profile_memory_sample_description_address'),
},
{
key: 'PhoneNumber',
description: I18n.t('profile_memory_sample_description_mobile'),
},
{
key: 'Height',
description: I18n.t('profile_memory_sample_description_height'),
},
{
key: 'Weight',
description: I18n.t('profile_memory_sample_description_weight'),
},
];
export const MemoryTemplateModal: React.FC<
MemoryTemplateModalProps
> = props => (
<UIModal
{...props}
type="action"
centered
footer={
props.showType === 'variableList' ? (
<div className={s['template-footer']}>
<Button
theme="solid"
className={s['template-cancel-button']}
onClick={props.onCancel}
>
{I18n.t('cancel_template')}
</Button>
{props.needSecondConfirm ? (
<Popconfirm
className={s['use-template-pop-confirm']}
position="top"
icon={
<IconAlertCircle
size="extra-large"
style={{ color: '#ff9600' }}
/>
}
title={I18n.t('use_template_confirm_title')}
content={I18n.t('use_template_confirm_info')}
okText={I18n.t('use_template_confirm_ok_text')}
cancelText={I18n.t('use_template_confirm_ cancel_text')}
okButtonProps={{ type: 'warning' }}
onConfirm={() => props.addTemplate?.(list)}
>
<BotDebugButton
theme="solid"
type="primary"
style={{ padding: '8px 12px' }}
>
{I18n.t('Use_template')}
</BotDebugButton>
</Popconfirm>
) : (
<BotDebugButton
theme="solid"
type="primary"
style={{ padding: '8px 12px' }}
onClick={() => props.addTemplate?.(list)}
>
{I18n.t('Use_template')}
</BotDebugButton>
)}
</div>
) : null
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
width={props.showType === 'variableList' ? 562 : 448}
title={I18n.t('variable_template_title')}
className={props.className}
>
<div className={s['modal-container']}>
{props.showType === 'variableList' ? (
<Image
className={s['template-variable-list']}
src={IMG_TEMPLATE_USE_I18N}
preview={false}
/>
) : (
<div className={s['template-demo']}>
<div className={s.desc}>{I18n.t('variable_template_demo_desc')}</div>
<div className={s.image}>
<Image
className={s['image-template']}
src={templateSample}
preview={false}
/>
</div>
<div className={s.tip}>{I18n.t('variable_template_demo_text')}</div>
</div>
)}
</div>
</UIModal>
);

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 exhaustiveCheckSimple = (_: never) => undefined;

View File

@@ -0,0 +1,435 @@
/*
* 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 { useNavigate, useParams } from 'react-router-dom';
import React, { type FC, useEffect, useRef, useState, useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import copy from 'copy-to-clipboard';
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { FilterKnowledgeType } from '@coze-data/utils';
import { type UnitType } from '@coze-data/knowledge-resource-processor-core';
import { RagModeConfiguration } from '@coze-data/knowledge-modal-base';
import { useKnowledgeListModal } from '@coze-data/knowledge-modal-adapter';
import { ActionType } from '@coze-data/knowledge-ide-base/types';
import { useDatasetStore } from '@coze-data/knowledge-data-set-for-agent';
import { BotE2e } from '@coze-data/e2e';
import { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { IconCozCopy, IconCozMinusCircle } from '@coze-arch/coze-design/icons';
import { Tooltip, Popover } from '@coze-arch/coze-design';
import { OpenBlockEvent, emitEvent } from '@coze-arch/bot-utils';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { UIButton, UITag, Toast } from '@coze-arch/bot-semi';
import { IconRobot, IconStyleSet, IconDownArrow } from '@coze-arch/bot-icons';
import { useDefaultExPandCheck } from '@coze-arch/bot-hooks';
import { CustomError } from '@coze-arch/bot-error';
import { DatasetSource, FormatType } from '@coze-arch/bot-api/knowledge';
import { KnowledgeApi } from '@coze-arch/bot-api';
import { SkillKeyEnum } from '@coze-agent-ide/tool-config';
import {
ToolContentBlock,
useToolValidData,
type ToolEntryCommonProps,
ToolItemList,
ToolItem,
ToolItemAction,
AddButton,
} from '@coze-agent-ide/tool';
import { useBotEditor } from '@coze-agent-ide/bot-editor-context-store';
import { usePopoverLock } from '../../hook/use-popover-lock';
import { useDatasetAutoChangeConfirm } from '../../hook/use-dataset-auto-change-confirm';
import s from './index.module.less';
const E2E_NAME_MAP = {
[FormatType.Image]: 'image',
[FormatType.Table]: 'table',
[FormatType.Text]: 'text',
};
export const Setting: React.FC<{ modelId: string }> = ({ modelId }) => {
const { knowledge, updateSkillKnowledgeDatasetInfo } = useBotSkillStore(
useShallow(state => ({
knowledge: state.knowledge,
updateSkillKnowledgeDatasetInfo: state.updateSkillKnowledgeDatasetInfo,
})),
);
const isReadonly = useBotDetailIsReadonly();
const { props, setLocked, visible, setVisible } = usePopoverLock();
const confirm = useDatasetAutoChangeConfirm();
const hasTableDataSet = useDatasetStore(state =>
state.dataSetList.some(dataSet => dataSet.format_type === FormatType.Table),
);
return (
<Popover
className={s['setting-content-popover']}
content={
<RagModeConfiguration
showNL2SQLConfig={hasTableDataSet}
dataSetInfo={knowledge.dataSetInfo}
onDataSetInfoChange={async newVal => {
const { auto } = newVal;
// 修改调用模式时做前置检查
if (auto !== knowledge.dataSetInfo.auto) {
try {
setLocked(true);
const res = await confirm(auto, modelId);
if (res) {
updateSkillKnowledgeDatasetInfo(newVal);
}
} finally {
setLocked(false);
}
} else {
updateSkillKnowledgeDatasetInfo(newVal);
}
}}
isReadonly={isReadonly}
/>
}
position="bottomLeft"
trigger="click"
zIndex={1031}
{...props}
>
<UIButton
data-testid={BotE2e.BotKnowledgeAutoMaticBtn}
theme="borderless"
size="small"
icon={knowledge.dataSetInfo.auto ? <IconRobot /> : <IconStyleSet />}
className={s['setting-trigger']}
onClick={() => {
setVisible(!visible);
}}
>
{knowledge.dataSetInfo.auto
? I18n.t('dataset_automatic_call')
: I18n.t('dataset_on_demand_call')}
<IconDownArrow className={s['setting-trigger-icon']} />
</UIButton>
</Popover>
);
};
type IDataSetAreaProps = ToolEntryCommonProps & {
formatType?: FormatType;
tooltip?: string;
initRef: React.MutableRefObject<boolean>;
desc?: string;
};
const renderTableToolNode = (title: string) => (
<div className={s['tip-content']}>{title}</div>
);
export const DataSetAreaItem: FC<IDataSetAreaProps> = ({
title,
desc,
formatType,
initRef,
tooltip,
}) => {
const params = useParams();
const navigate = useNavigate();
const [removedIds, setRemovedIds] = useState<string[]>([]);
const dataSetList = useDatasetStore(state => state.dataSetList);
const setDataSetList = useDatasetStore(state => state.setDataSetList);
const setToolValidData = useToolValidData();
const defaultKnowledgeType = useMemo(() => {
switch (formatType) {
case FormatType.Table:
return FilterKnowledgeType.TABLE;
case FormatType.Text:
return FilterKnowledgeType.TEXT;
case FormatType.Image:
return FilterKnowledgeType.IMAGE;
default:
return undefined;
}
}, [formatType]);
const { knowledge, updateSkillKnowledgeDatasetList } = useBotSkillStore(
useShallow(state => ({
knowledge: state.knowledge,
updateSkillKnowledgeDatasetList: state.updateSkillKnowledgeDatasetList,
})),
);
const isReadonly = useBotDetailIsReadonly();
const jumpToDetail = (datasetID: string) => {
const actionType = dataSetList.find(
dataset => dataset.dataset_id === datasetID,
)
? ActionType.REMOVE
: ActionType.ADD;
const queryParams = {
biz: 'agentIDE',
bot_id: params.bot_id,
page_mode: 'modal',
action_type: actionType,
};
navigate(
`/space/${params.space_id}/knowledge/${datasetID}?${new URLSearchParams(queryParams).toString()}`,
);
};
const jumpToAdd = (datasetID: string, type: UnitType) => {
const queryParams = {
biz: 'agentIDE',
type,
bot_id: params.bot_id,
action_type: ActionType.ADD,
page_mode: 'modal',
};
navigate(
`/space/${params.space_id}/knowledge/${datasetID}/upload?${new URLSearchParams(queryParams).toString()}`,
);
};
const { node: addModal, open: openAddModal } = useKnowledgeListModal({
datasetList: dataSetList,
defaultType: defaultKnowledgeType,
onDatasetListChange: list => {
emitEvent(OpenBlockEvent.DATA_SET_BLOCK_OPEN);
setDataSetList(list);
},
onClickAddKnowledge: jumpToAdd,
onClickKnowledgeDetail: jumpToDetail,
});
useEffect(() => {
// 排除首次初始化和删除更新,原因:
// 因为删除会快速操作useEffect 追踪到数据可能是最终结果,无法保证每次删除都能监听到
if (initRef.current && removedIds.length === 0) {
updateSkillKnowledgeDatasetList(
dataSetList.map(d => ({
dataset_id: d.dataset_id ?? '',
name: d.name,
})),
);
}
}, [dataSetList]);
useEffect(() => {
if (removedIds.length > 0) {
const updatedDataSetList = dataSetList.filter(
d => !removedIds.includes(d?.dataset_id ?? ''),
);
const updateParam = updatedDataSetList.map(d => ({
dataset_id: d.dataset_id ?? '',
name: d.name,
}));
updateSkillKnowledgeDatasetList(updateParam);
setRemovedIds([]);
}
}, [removedIds]);
const onCopy = (text: string) => {
const res = copy(text);
if (!res) {
throw new CustomError(ReportEventNames.parmasValidation, 'empty copy');
}
Toast.success({
content: I18n.t('copy_success'),
showClose: false,
id: 'dataset_copy_id',
});
};
const defaultExpand = useDefaultExPandCheck({
blockKey: SkillKeyEnum.DATA_SET_BLOCK,
configured: knowledge.dataSetList.length > 0,
});
const currentDatasetList = useMemo(
() =>
dataSetList.filter(
item => formatType === undefined || item.format_type === formatType,
),
[dataSetList],
);
useEffect(() => {
setToolValidData(Boolean(currentDatasetList.length));
}, [currentDatasetList.length]);
return (
<>
{addModal}
<ToolContentBlock
className={s['data-set-container']}
blockEventName={OpenBlockEvent.DATA_SET_BLOCK_OPEN}
header={title}
setting={null}
tooltipType={tooltip ? 'tooltip' : undefined}
tooltip={tooltip ? renderTableToolNode(tooltip) : null}
defaultExpand={defaultExpand}
actionButton={
<AddButton
tooltips={I18n.t('bot_edit_dataset_add_tooltip')}
onClick={openAddModal}
enableAutoHidden={true}
data-testid={`bot.editor.tool.data-set-${
E2E_NAME_MAP[formatType as keyof typeof E2E_NAME_MAP]
}.add-button`}
/>
}
>
<div className={s['data-set-content']}>
{currentDatasetList.length ? (
<>
{currentDatasetList.length && !knowledge.dataSetInfo.auto ? (
<div className={s['dataset-setting-tip']}>
{I18n.t('bot_edit_dataset_on_demand_prompt1')}
<Tooltip content={I18n.t('bot_edit_datasets_copyName')}>
<UITag
onClick={() =>
onCopy(I18n.t('dataset_recall_copy_value'))
}
type="light"
className={s['copy-trigger']}
>
<IconCozCopy className={s['icon-copy']} />
{I18n.t('dataset_recall_copy_label')}
</UITag>
</Tooltip>
{I18n.t('bot_edit_dataset_on_demand_prompt2')}
</div>
) : null}
<ToolItemList>
{currentDatasetList.map((item, index) => (
<ToolItem
key={item.dataset_id}
title={item?.name ?? ''}
description={item?.description ?? ''}
avatar={item?.icon_url ?? ''}
onClick={() =>
item?.dataset_id && jumpToDetail(item?.dataset_id)
}
actions={
<>
{!isReadonly && (
<ToolItemAction
tooltips={I18n.t('Copy_name')}
onClick={() => onCopy(item?.name ?? '')}
data-testid="bot.editor.tool.plugin.copy-button"
>
<IconCozCopy className="text-sm coz-fg-secondary" />
</ToolItemAction>
)}
{!isReadonly && (
<ToolItemAction
tooltips={I18n.t('remove_dataset')}
onClick={() => {
setDataSetList(
dataSetList.filter(
d => d.dataset_id !== item.dataset_id,
),
);
if (item?.dataset_id) {
setRemovedIds([
...removedIds,
item?.dataset_id,
]);
}
}}
>
<IconCozMinusCircle className="text-sm coz-fg-secondary" />
</ToolItemAction>
)}
</>
}
/>
))}
</ToolItemList>
</>
) : (
<div className={s['default-text']}>
{desc ?? I18n.t('bot_edit_dataset_explain')}
</div>
)}
</div>
</ToolContentBlock>
</>
);
};
export const useDataSetArea = () => {
const spaceId = useSpaceStore(v => v.space.id);
const {
storeSet: { useDraftBotDataSetStore },
} = useBotEditor();
const initRef = useRef(false);
const setDataSetList = useDatasetStore(state => state.setDataSetList);
const { knowledge } = useBotSkillStore(
useShallow(state => ({
knowledge: state.knowledge,
})),
);
const { pageFrom, init } = usePageRuntimeStore(
useShallow(state => ({
pageFrom: state.pageFrom,
init: state.init,
})),
);
const getDataSetList = async () => {
if (knowledge.dataSetList.length) {
const resp = await KnowledgeApi.ListDataset({
space_id: spaceId,
filter: {
dataset_ids: knowledge.dataSetList.map(i => i.dataset_id ?? ''),
source_type:
pageFrom === 'explore' ? DatasetSource.SourceExplore : undefined,
},
});
const validDatasetList = (resp?.dataset_list ?? []).filter(item =>
knowledge.dataSetList.some(i => i.dataset_id === item.dataset_id),
);
// 方便数据复用
useDraftBotDataSetStore.getState().batchUpdate(validDatasetList);
setDataSetList(validDatasetList);
}
initRef.current = true;
};
useEffect(() => {
if (init) {
getDataSetList();
}
}, [init]);
useEffect(
() => () => {
setDataSetList([]);
},
[],
);
return {
node: DataSetAreaItem,
initRef,
};
};

View File

@@ -0,0 +1,94 @@
@import '../../assets/styles/common.less';
@import '../../assets/styles/index.module.less';
.icon-copy {
.common-svg-icon(14px, rgba(107, 109, 117, 1));
&:hover {
background-color: var(--semi-color-fill-0);
}
}
.data-set-content {
.dataset-setting-tip {
margin-bottom: 4px;
padding: 12px;
color: rgba(6, 7, 9, 80%);
font-size: 12px;
line-height: 16px;
background: rgba(186, 192, 255, 20%);
border-radius: 8px;
.copy-trigger {
cursor: pointer;
margin: 0 4px;
color: rgba(6, 7, 9, 80%);
background: rgba(6, 7, 9, 4%);
font-size: 10px;
font-style: normal;
font-weight: 400;
line-height: 14px;
.icon-copy {
.common-svg-icon(14px, rgba(6, 7, 9, 0.04));
/* stylelint-disable-next-line declaration-no-important */
margin-right: 2px !important;
}
}
:global {
.semi-tag-grey-light {
/* stylelint-disable-next-line declaration-no-important */
background: rgba(6, 7, 9, 4%) !important;
}
}
}
}
.default-text {
.tip-text;
}
.setting-trigger {
cursor: pointer;
display: flex;
column-gap: 4px;
align-items: center;
margin-left: 8px;
font-size: 12px;
font-weight: 600;
font-style: normal;
line-height: 16px;
&-icon {
svg {
width: 10px;
height: 10px;
}
}
:global {
.semi-button-content-right {
display: flex;
align-items: center;
@apply coz-fg-secondary;
}
}
}
.setting-content-popover {
background: #f7f7fa;
border-radius: 12px;
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useParams } from 'react-router-dom';
import React, { type FC, type PropsWithChildren } from 'react';
import { useCreation } from 'ahooks';
import { logger as rawLogger, LoggerContext } from '@coze-arch/logger';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
const botDebugLogger = rawLogger.createLoggerWith({
ctx: {
meta: {},
namespace: 'bot_debug',
},
});
const BotEditorLoggerContextProvider: FC<PropsWithChildren> = ({
children,
}) => {
const params = useParams<DynamicParams>();
const loggerWithId = useCreation(
() =>
botDebugLogger.createLoggerWith({
ctx: {
meta: {
bot_id: params.bot_id,
},
},
}),
[],
);
return (
<LoggerContext.Provider value={loggerWithId}>
{children}
</LoggerContext.Provider>
);
};
export { BotEditorLoggerContextProvider };

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.
*/
/**comp */
export {
TableMemory,
reloadDatabaseList,
useExpertModeConfig,
} from './table-memory';
export { SuggestionBlock } from './suggestion/suggestion-block';
export { SheetView, SingleSheet, MultipleSheet } from './sheet-view';
export {
OnboardingMessage,
settingAreaScrollId,
EditorExpendModal,
SuggestionList,
type OnboardingEditorAction,
} from './onboarding-message';
export { ModeSelect, type ModeSelectProps } from './mode-select';
export {
ModeLabel,
type ModeLabelProps,
type ModeOption,
} from './mode-select/mode-change-view';
export { DataMemory } from './data-memory';
export { ContentView } from './content-view';
export { ChatBackground } from './chat-background';
export { BotDebugToolPane } from './bot-debug-panel/button';
export { BotDebugPanel } from './bot-debug-panel';
export { BotEditorLoggerContextProvider } from './error-boundary-with-logger';
export { AutoGenerateButton } from './auto-generate-btn';
export { BotDebugButton } from './bot-debug-button';
export { CollapsibleTextarea } from './collapsible-textarea';
export { SuggestionContent } from './suggestion/suggestion-content/suggestion-content';
export { BotSubmitModalDiffView } from './bot-diff-view/bot-submit-modal';
export { InputSlider } from './input-slider';
export { Setting } from './data-set/data-set-area';
export { AuthorizeButton } from './authorize-button';
export {
NavModal,
NAV_MODAL_MAIN_CONTENT_HEIGHT,
NavModalItem,
NavModalProps,
} from './nav-modal';
export { KvBindButton, DiffViewButton } from './connector-action';
export { MemoryToolPane, type MemoryToolPaneProps } from './memory-tool-pane';
export {
PluginPermissionManageList,
PermissionManageTitle,
} from './plugin-permission-manage-list';
export { PublishPlatformSetting } from './publish-platform-setting';
import PublishPlatformDescription from './publish-platform-description';
export { PublishPlatformDescription };

View File

@@ -0,0 +1,67 @@
.input-slider {
display: flex;
align-items: flex-start;
:global {
.semi-slider {
padding: 0;
}
}
.slider {
width: 174px;
height: 52px;
:global {
.semi-slider-marks {
top: 32px;
font-size: 12px;
color: var(--light-usage-text-color-text-2, rgba(28, 31, 35, 0.6));
}
.semi-slider-mark {
transform: unset;
}
.semi-slider-mark:last-child {
left: unset;
right: 0;
transform: translateX(-100%);
width: fit-content;
white-space: nowrap;
}
.semi-slider-dot.semi-slider-dot-active {
background-color: transparent;
}
}
}
.input-number {
flex: 1;
:global {
.semi-input-wrapper {
border: 1px solid
var(--light-usage-border-color-border, rgba(28, 31, 35, 0.08));
background-color: #fff;
&:focus-within {
border-color: var(--semi-color-focus-border);
}
}
input {
text-align: center;
}
}
}
.input-btn {
position: absolute;
padding: 10px 8px;
font-size: 12px;
cursor: pointer;
color: rgba(28, 29, 35, 0.8);
top: 0;
z-index: 1;
&:first-child {
left: 0;
}
&:last-child {
right: 0;
}
&-disabled {
cursor: not-allowed;
}
}
}

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 { InputSlider } from './input-slider';

View File

@@ -0,0 +1,194 @@
/*
* 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 FC } from 'react';
import { isInteger, isNumber, isUndefined } from 'lodash-es';
import classNames from 'classnames';
import { type SliderProps } from '@coze-arch/bot-semi/Slider';
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
import { withField, InputNumber, Slider } from '@coze-arch/bot-semi';
import { IconMinus, IconPlus } from '@douyinfe/semi-icons';
import { RCSliderWrapper, type RCSliderProps } from '../rc-slider-wrapper';
import s from './index.module.less';
interface InputSliderProps {
value?: number;
onChange?: (v: number) => void;
max?: number;
min?: number;
step?: number;
disabled?: boolean;
decimalPlaces?: number;
marks?: SliderProps['marks'];
className?: string;
/** 是否使用 rc-slider 替换 semi-slider目前 semi-slider 存在一个比较明显的 bug在缩放场景下拖拽定位存在问题已经反馈等待修复 */
useRcSlider?: boolean;
}
const POWVAL = 10;
const formateDecimalPlacesString = (
value: string | number,
prevValue?: number,
decimalPlaces?: number,
) => {
if (isUndefined(decimalPlaces)) {
return value.toString();
}
const numberValue = Number(value);
const stringValue = value.toString();
if (Number.isNaN(numberValue)) {
return `${value}`;
}
if (decimalPlaces === 0 && !isInteger(Number(value)) && prevValue) {
return `${prevValue}`;
}
const decimalPointIndex = stringValue.indexOf('.');
if (decimalPointIndex < 0) {
return stringValue;
}
const formattedValue = stringValue.substring(
0,
decimalPointIndex + 1 + decimalPlaces,
);
if (formattedValue.endsWith('.') && decimalPlaces === 0) {
return formattedValue.substring(0, formattedValue.length - 1);
}
return formattedValue;
};
const formateDecimalPlacesNumber = (
value: number,
prevValue?: number,
decimalPlaces?: number,
) => {
if (isUndefined(decimalPlaces)) {
return value;
}
if (decimalPlaces === 0 && !isInteger(value) && prevValue) {
return prevValue;
}
const pow = Math.pow(POWVAL, decimalPlaces);
return Math.round(value * pow) / pow;
};
const BaseInputSlider: React.FC<InputSliderProps> = ({
value,
onChange,
max = 1,
min = 0,
step = 1,
disabled,
decimalPlaces,
marks,
className,
useRcSlider = false,
}) => {
const onNumberChange = (numberValue: number) => {
const formattedValue = formateDecimalPlacesNumber(
numberValue,
value,
decimalPlaces,
);
onChange?.(formattedValue);
};
return (
<div className={classNames(s['input-slider'], className)}>
{useRcSlider ? (
<RCSliderWrapper
disabled={disabled}
value={value}
max={max}
min={min}
step={step}
marks={marks as RCSliderProps['marks']}
onChange={v => {
if (typeof v === 'number') {
onChange?.(v);
}
}}
/>
) : (
<Slider
className={s.slider}
disabled={disabled}
value={value}
max={max}
min={min}
step={step}
marks={marks}
onChange={v => {
if (typeof v === 'number') {
onChange?.(v);
}
}}
/>
)}
<div style={{ position: 'relative', marginLeft: 24 }}>
<IconMinus
className={classNames(
s['input-btn'],
disabled && s['input-btn-disabled'],
)}
onClick={e => {
e.stopPropagation();
if (isNumber(value) && value <= min) {
return;
}
if (!disabled && value !== undefined) {
onNumberChange(value - step);
}
}}
/>
<InputNumber
className={s['input-number']}
value={value}
disabled={disabled}
formatter={inputValue =>
formateDecimalPlacesString(inputValue, value)
}
hideButtons
onNumberChange={onNumberChange}
max={max}
min={min}
/>
<IconPlus
className={classNames(
s['input-btn'],
disabled && s['input-btn-disabled'],
)}
onClick={e => {
if (isNumber(value) && value >= max) {
return;
}
e.stopPropagation();
if (!disabled && value !== undefined) {
onNumberChange(value + step);
}
}}
/>
</div>
</div>
);
};
export const InputSlider: FC<CommonFieldProps & InputSliderProps> =
withField(BaseInputSlider);

View File

@@ -0,0 +1,95 @@
/*
* 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 FC } from 'react';
import { type ReactElement } from 'react-markdown/lib/react-markdown';
import { I18n } from '@coze-arch/i18n';
import { type ButtonProps } from '@coze-arch/coze-design';
import { IconMemoryDownMenu } from '@coze-arch/bot-icons';
import { DataErrorBoundary, DataNamespace } from '@coze-data/reporter';
import { BotE2e } from '@coze-data/e2e';
import {
MemoryDebugDropdown,
useMemoryDebugModal,
type MemoryDebugDropdownMenuItem,
type MemoryModule,
useSendTeaEventForMemoryDebug,
} from '@coze-data/database';
import { OperateTypeEnum, ToolPane } from '@coze-agent-ide/debug-tool-list';
export interface MemoryToolPaneProps {
menuList: MemoryDebugDropdownMenuItem[];
}
export const MemoryToolPane: FC<MemoryToolPaneProps> = ({ menuList }) => {
const isStore = false;
const sendTeaEventForMemoryDebug = useSendTeaEventForMemoryDebug({
isStore,
});
const [curMemoryModule, setCurMemoryModule] = useState<MemoryModule>();
const defaultModule = menuList[0]?.name;
const { open, node: memoryModal } = useMemoryDebugModal({
memoryModule: curMemoryModule || defaultModule,
menuList,
setMemoryModule: setCurMemoryModule,
isStore,
});
return (
<DataErrorBoundary namespace={DataNamespace.MEMORY}>
{memoryModal}
{
(
<ToolPane
visible={menuList.length > 0}
itemKey={`key_${I18n.t('database_memory_menu')}`}
operateType={OperateTypeEnum.DROPDOWN}
title={I18n.t('database_memory_menu')}
icon={<IconMemoryDownMenu />}
onEntryButtonClick={() => {
sendTeaEventForMemoryDebug(defaultModule);
setCurMemoryModule(defaultModule);
open();
}}
dropdownProps={{
showTick: true,
clickToHide: true,
render: (
<MemoryDebugDropdown
menuList={menuList}
onClickItem={memoryModule => {
setCurMemoryModule(memoryModule);
open();
}}
/>
),
}}
buttonProps={
{
'data-testid': BotE2e.BotMemoryDebugBtn,
} as unknown as ButtonProps
}
/>
) as ReactElement
}
</DataErrorBoundary>
);
};

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { IconCozArrowDown } from '@coze-arch/coze-design/icons';
import { Tooltip } from '@coze-arch/coze-design';
import { UIButton } from '@coze-arch/bot-semi';
import { useFlags } from '@coze-arch/bot-flags';
import { type ModeOption } from './mode-change-view';
import s from './index.module.less';
export interface ChangeButtonProps {
disabled: boolean;
tooltip?: string;
modeInfo: ModeOption | undefined;
}
export function ChangeButton({
modeInfo,
disabled,
tooltip,
}: ChangeButtonProps) {
const [FLAGS] = useFlags();
// 社区版暂不支持该功能
const showText = modeInfo?.showText || FLAGS['bot.studio.prompt_diff'];
const ToolTipFragment = tooltip ? Tooltip : React.Fragment;
const content = (
<ToolTipFragment content={tooltip}>
<UIButton
theme="outline"
size="small"
className={classNames(s['mode-change-title-space'], {
'!coz-mg-primary': disabled,
})}
icon={
<div className="coz-fg-primary text-[16px] flex items-center">
{modeInfo?.icon}
</div>
}
disabled={disabled}
data-testid="bot-edit-agent-mode-open-button"
>
<div
className={classNames(s['mode-change-title'], 'flex items-center')}
>
{showText ? modeInfo?.title : null}
<IconCozArrowDown className="w-4 h-5 coz-fg-secondary" />
</div>
</UIButton>
</ToolTipFragment>
);
return showText ? (
content
) : (
<Tooltip content={modeInfo?.title}>{content}</Tooltip>
);
}

View File

@@ -0,0 +1,81 @@
.font-normal {
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px; /* 133.333% */
@apply text-foreground-3;
}
.mode-change-title-space {
margin-left: 4px !important;
padding: 2px 8px !important;
.mode-change-title {
.font-normal();
}
.mode-change-icon {
@apply text-foreground-3;
margin-left: 4px;
svg {
width: 10px;
height: 10px;
}
}
}
.mode-change-title-space:active {
background: var(--light-usage-fill-color-fill-1, rgba(46, 46, 56, 8%));
}
.mode-change-title-space:focus {
background: var(--light-usage-fill-color-fill-2, rgba(46, 46, 56, 12%));
}
.mode-change-popover {
width: 455px;
background: #f7f7fa;
border: 1px solid
var(--light-usage-border-color-border, rgba(28, 31, 35, 8%));
border-radius: 12px;
/* --shadow-elevated */
box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 10%),
0 0 1px 0 rgba(0, 0, 0, 30%);
.mode-change-popover-content {
padding: 16px;
:global {
.semi-radio {
border: 1px solid
var(--light-usage-border-color-border, rgba(29, 28, 35, 8%));
}
.semi-radio-cardRadioGroup_checked {
border: 1px solid var(--light-color-brand-brand-5, #4d53e8);
}
}
}
}
.mode-change-disabled {
display: flex;
align-items: center;
margin-left: 4px;
padding: 2px 8px;
.icon {
width: 16px;
height: 16px;
margin-right: 4px;
}
.label {
.font-normal();
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import {
autosaveManager,
getBotDetailDtoInfo,
initBotDetailStore,
multiAgentSaveManager,
updateBotRequest,
updateHeaderStatus,
useBotDetailIsReadonly,
} from '@coze-studio/bot-detail-store';
import {
AgentVersionCompat,
BotMode,
} from '@coze-arch/bot-api/playground_api';
import { useBotPageStore } from '../../store/bot-page/store';
import { ModeChangeView, type ModeChangeViewProps } from './mode-change-view';
export interface ModeSelectProps
extends Pick<ModeChangeViewProps, 'optionList'> {
readonly?: boolean;
tooltip?: string;
}
export const ModeSelect: React.FC<ModeSelectProps> = ({
readonly,
tooltip,
optionList,
}) => {
const { mode } = useBotInfoStore(useShallow(store => ({ mode: store.mode })));
const { modeSwitching, setBotState } = useBotPageStore(
useShallow(state => ({
modeSwitching: state.bot.modeSwitching,
setBotState: state.setBotState,
})),
);
const isReadonly = useBotDetailIsReadonly() || readonly;
const handleModeChange = async (value: BotMode) => {
try {
setBotState({ modeSwitching: true });
// bot信息全量保存
const { botSkillInfo } = getBotDetailDtoInfo();
await updateBotRequest(botSkillInfo);
// 服务端约定 切换模式需要单独调一次只传 bot_mode 的 update
const switchModeParams = {
bot_mode: value,
...(value === BotMode.MultiMode
? { version_compat: AgentVersionCompat.NewVersion }
: {}),
};
const { data } = await updateBotRequest(switchModeParams);
updateHeaderStatus(data);
autosaveManager.close();
multiAgentSaveManager.close();
await initBotDetailStore();
multiAgentSaveManager.start();
autosaveManager.start();
} finally {
setBotState({ modeSwitching: false });
}
};
return (
<ModeChangeView
modeSelectLoading={modeSwitching}
modeValue={mode}
onModeChange={handleModeChange}
isReadOnly={isReadonly}
tooltip={tooltip}
optionList={optionList}
/>
);
};

View File

@@ -0,0 +1,152 @@
/*
* 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 classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Typography, Popover, Radio } from '@coze-arch/bot-semi';
import { BotMode } from '@coze-arch/bot-api/developer_api';
import { ChangeButton } from './change-button';
import s from './index.module.less';
export interface ModeLabelProps {
icon: ReactNode;
isDisabled: boolean;
isSelected: boolean;
title: ReactNode;
desc: ReactNode;
}
export const ModeLabel: React.FC<ModeLabelProps> = ({
icon,
isDisabled,
isSelected,
title,
desc,
}) => (
<div className={classNames('flex items-center gap-[12px]')}>
<div
className={
(classNames('text-[16px]'),
isDisabled ? 'coz-fg-dim' : 'coz-fg-primary')
}
>
{icon}
</div>
<div data-testid={`bot-edit-agent-select-mode-button-${title}`}>
<div
className={classNames(
'text-[16px] leading-[22px]',
isSelected ? 'font-[500]' : 'font-[400]',
isDisabled ? 'coz-fg-dim' : 'coz-fg-primary',
)}
>
{title}
</div>
<Typography.Text
className={classNames(
'mt-[4px]',
'text-[14px] font-[400] leading-[20px]',
isDisabled ? 'coz-fg-dim' : 'coz-fg-secondary',
)}
>
{desc}
</Typography.Text>
</div>
</div>
);
export interface ModeOption
extends Omit<ModeLabelProps, 'isSelected' | 'isDisabled'> {
value: BotMode;
showText: boolean;
getIsDisabled: (params: { currentMode: BotMode }) => boolean;
}
export interface ModeChangeViewProps {
modeSelectLoading: boolean;
modeValue: BotMode;
onModeChange: (value: BotMode) => Promise<void>;
isReadOnly: boolean;
optionList: ModeOption[];
tooltip?: string;
}
export const ModeChangeView = (props: ModeChangeViewProps) => {
const {
modeValue = BotMode.SingleMode,
onModeChange,
modeSelectLoading,
isReadOnly,
tooltip,
optionList,
} = props;
const disabled = isReadOnly || modeSelectLoading;
const modeInfo = optionList.find(option => option.value === modeValue);
if (disabled) {
return (
<ChangeButton disabled={disabled} tooltip={tooltip} modeInfo={modeInfo} />
);
}
return (
<Popover
className={s['mode-change-popover']}
data-testid="bot-detail.mode-chage-view.popover"
trigger="click"
position="bottomLeft"
autoAdjustOverflow={false}
content={
<div className={s['mode-change-popover-content']}>
<div className="coz-fg-plus text-[14px] font-[500] leading-[20px] mb-[12px]">
{I18n.t('chatflow_switch_mode_title')}
</div>
<Radio.Group
type="pureCard"
direction="vertical"
value={modeValue}
defaultValue={modeValue}
disabled={disabled}
options={optionList.map(option => {
const isSelected = modeValue === option.value;
const isDisabled = option.getIsDisabled({
currentMode: modeValue,
});
return {
value: option.value,
disabled: isDisabled,
label: (
<ModeLabel
{...option}
key={option.value}
isDisabled={isDisabled}
isSelected={isSelected}
/>
),
};
})}
onChange={e => onModeChange(e.target.value)}
/>
</div>
}
>
<div>
<ChangeButton disabled={false} modeInfo={modeInfo} />
</div>
</Popover>
);
};

View File

@@ -0,0 +1,14 @@
.coz-nav-modal.coz-modal.as-modal .semi-modal-content {
padding: 0;
}
.coz-nav-modal {
.semi-modal-body {
display: flex;
height: var(--nav-modal-body-height);
}
.semi-modal-footer {
display: none;
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type ReactNode, type HtmlHTMLAttributes } from 'react';
import { merge } from 'lodash-es';
import { IconCozCross } from '@coze-arch/coze-design/icons';
import { Button, Modal, type ModalProps } from '@coze-arch/coze-design';
import './index.less';
export type NavModalProps = Omit<ModalProps, 'children' | 'icon'> & {
navigation: ReactNode;
mainContent: ReactNode;
mainContentTitle?: ReactNode | string;
};
const NAV_MODAL_BODY_HEIGHT = 604;
const NAV_MODAL_PADDING_TOP = 24;
const NAV_MODAL_CLOSE_BUTTON_SIDE_LENGTH = 40;
export const NAV_MODAL_MAIN_CONTENT_HEIGHT =
NAV_MODAL_BODY_HEIGHT -
NAV_MODAL_PADDING_TOP -
NAV_MODAL_CLOSE_BUTTON_SIDE_LENGTH;
export const NavModal: FC<NavModalProps> = props => {
const {
title,
navigation,
mainContent,
mainContentTitle,
className,
onCancel,
closeIcon,
style,
...restProps
} = props;
return (
<Modal
header={null}
footer={null}
className={`coz-nav-modal ${className || ''}`}
style={merge(style, {
'--nav-modal-body-height': `${NAV_MODAL_BODY_HEIGHT}px`,
})}
{...restProps}
>
<div className="flex w-full h-full">
<div className="flex pt-[30px] px-[8px] coz-bg-max w-[200px] shrink-0 flex-col">
<div className="text-[20px] coz-fg-plus mx-[8px] leading-[28px] font-medium mb-[16px]">
{title}
</div>
{navigation}
</div>
<div
className="flex flex-col coz-bg-plus overflow-auto px-[24px] w-full"
style={{
paddingTop: NAV_MODAL_PADDING_TOP,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- .
// @ts-expect-error
'--nav-modal-main-content-height': `${NAV_MODAL_MAIN_CONTENT_HEIGHT}px`,
}}
>
<div className="flex justify-end">
{mainContentTitle ? (
<div className="mr-auto content-center text-[20px] coz-fg-plus mx-[8px] leading-[28px] font-medium">
{mainContentTitle}
</div>
) : null}
{closeIcon || (
<Button
style={{
height: NAV_MODAL_CLOSE_BUTTON_SIDE_LENGTH,
width: NAV_MODAL_CLOSE_BUTTON_SIDE_LENGTH,
}}
size="large"
color="secondary"
onClick={onCancel}
icon={<IconCozCross />}
></Button>
)}
</div>
{mainContent}
</div>
</div>
</Modal>
);
};
export interface NavModalItemProps extends HtmlHTMLAttributes<HTMLDivElement> {
selectedIcon?: ReactNode;
unselectedIcon?: ReactNode;
text: string;
selected?: boolean;
onClick?: () => void;
suffix?: ReactNode;
}
export const NavModalItem: FC<NavModalItemProps> = props => {
const {
text,
selected = false,
selectedIcon = <></>,
unselectedIcon = <></>,
suffix,
onClick,
className,
} = props;
return (
<div
onClick={onClick}
className={[
'flex',
'flex-row',
'cursor-pointer',
'items-center',
'justify-between',
'rounded-normal',
'px-[8px]',
'py-[6px]',
'mb-[6px]',
'text-lg',
'text-foreground-4',
'w-full',
'hover:bg-background-5',
'active:bg-background-6',
selected ? 'bg-background-4' : '',
className,
].join(' ')}
>
<div className="flex flex-row gap-[8px] items-center flex-1 overflow-hidden">
{selected ? selectedIcon : unselectedIcon}
<div className="flex-1 overflow-hidden">
<div className="font-medium">{text}</div>
</div>
</div>
{typeof suffix === 'string' ? (
<div className="font-base text-foreground-2">{suffix}</div>
) : (
suffix ?? <></>
)}
</div>
);
};
NavModal.displayName = 'NavModal';
NavModalItem.displayName = 'NavModalItem';

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type PropsWithChildren, useRef } from 'react';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { AIButton, type ButtonProps } from '@coze-arch/coze-design';
import { usePromptEditor } from '../../context/editor-kit';
import { useBotEditorService } from '../../context/bot-editor-service';
export const NLPromptButton: React.FC<PropsWithChildren<ButtonProps>> = ({
children,
...buttonProps
}) => {
const ref = useRef<HTMLDivElement>(null);
const { nLPromptModalVisibilityService } = useBotEditorService();
const { promptEditor } = usePromptEditor();
const isReadonly = useBotDetailIsReadonly();
const isDisabled = !promptEditor || isReadonly;
const onClick = () => {
if (!ref.current) {
return;
}
const { offsetHeight, offsetTop } = ref.current;
const { top, left } = ref.current.getBoundingClientRect();
nLPromptModalVisibilityService.open(
{
top: top + offsetHeight,
left: left + offsetTop,
},
'ai-button',
);
};
return (
<div ref={ref}>
<AIButton
color="aihglt"
iconPosition="left"
size="small"
disabled={isDisabled}
onClick={onClick}
{...buttonProps}
>
{children}
</AIButton>
</div>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type PropsWithChildren, type ReactNode } from 'react';
import classNames from 'classnames';
import { Tooltip } from '@coze-arch/coze-design';
import { IconInfo } from '@coze-arch/bot-icons';
import s from './index.module.less';
export const ToolTipNode: FC<
PropsWithChildren<{
content: ReactNode;
className?: string;
tipContentClassName?: string;
}>
> = ({ content, children, className, tipContentClassName }) => (
<Tooltip
className={tipContentClassName}
content={<div className={classNames(s['tip-content'])}>{content}</div>}
>
<div className={classNames(className, 'flex items-center')}>
<IconInfo
className={classNames(
s['icon-info'],
'cursor-pointer coz-fg-secondary',
)}
/>
{children}
</div>
</Tooltip>
);

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 settingAreaScrollId = 'setting_area_scroll';

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type PropsWithChildren } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Tooltip } from '@coze-arch/coze-design';
import { UIModal, type UIModalProps } from '@coze-arch/bot-semi';
import { IconMinimizeOutlined } from '@coze-arch/bot-icons';
import styles from '../index.module.less';
export const EditorExpendModal: FC<PropsWithChildren<UIModalProps>> = ({
children,
...modalProps
}) => (
<UIModal
{...modalProps}
title={
<div className="coz-fg-plus text-[20px] leading-8">
{I18n.t('bot_edit_opening_text_title')}
</div>
}
centered
style={{
maxWidth: 640,
aspectRatio: 640 / 668,
height: 'auto',
}}
bodyStyle={{
padding: 0,
}}
className={styles['editor-expend-modal']}
footer={null}
type="base-composition"
closeIcon={
<Tooltip content={I18n.t('collapse')}>
<IconMinimizeOutlined
size="extra-large"
className="cursor-pointer"
onClick={modalProps.onCancel}
/>
</Tooltip>
}
>
{children}
</UIModal>
);

View File

@@ -0,0 +1,138 @@
@import '../../assets/styles/common.less';
@import '../../assets/styles/mixins.less';
.text {
margin-bottom: 8px;
font-size: 12px;
font-weight: 600;
line-height: 16px;
color: var(--light-usage-text-color-text-1, rgba(28, 29, 35, 80%));
}
.onboarding-message-blur {
textarea {
display: -webkit-box;
max-height: 98px;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
}
}
.onboarding-message-title {
.text;
position: relative;
display: flex;
align-items: center;
&.mt-20 {
margin-top: 20px;
}
}
.suggestion-message-item {
position: relative;
display: flex;
align-items: center;
width: 100%;
margin-bottom: 8px;
.apis-no-icon {
position: absolute;
right: 16px;
@apply coz-fg-hglt-purple;
}
:global {
.semi-input-textarea-wrapper {
padding-right: 48px;
}
}
}
.add-button-row {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.add-icon {
display: flex;
>img {
width: 14px;
height: 14px;
}
}
.onboarding-add-icon,
.msg-replace-icon {
// cursor: pointer;
}
.msg-replace-icon:hover {
background-color: var(--semi-color-fill-0);
}
.text-readonly {
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: var(--light-color-grey-grey-8, #2e3238);
white-space: pre-wrap;
&.mb-8 {
margin-bottom: 8px;
}
}
.text-none {
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: var(--light-color-grey-grey-3, #a7abb0);
}
@keyframes suggestion-highlight {
0% {
background-color: rgb(255, 248, 234);
}
100% {
background-color: rgb(244, 244, 245);
}
}
.suggestion-item-highlight {
animation: suggestion-highlight 0.8s infinite alternate;
animation-timing-function: ease;
}
.markdown-editor-btn {
margin-right: 8px;
padding: 0;
.markdown-editor-btn-text {
font-weight: 400;
@apply coz-fg-secondary;
}
}
.editor-expend-modal {
:global {
.semi-modal-header .semi-button-with-icon-only {
width: 32px;
height: 32px;
}
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {
useEffect,
useMemo,
lazy,
Suspense,
type ReactNode,
forwardRef,
} from 'react';
import { useShallow } from 'zustand/react/shallow';
import { debounce, isFunction } from 'lodash-es';
import { produce } from 'immer';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import {
botSkillSaveManager,
useBotDetailIsReadonly,
} from '@coze-studio/bot-detail-store';
import { OpenBlockEvent } from '@coze-arch/bot-utils';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { Spin } from '@coze-arch/bot-semi';
import { useDefaultExPandCheck } from '@coze-arch/bot-hooks';
import { ItemType } from '@coze-arch/bot-api/developer_api';
import { SkillKeyEnum } from '@coze-agent-ide/tool-config';
import {
ToolContentBlock,
useToolValidData,
type ToolEntryCommonProps,
} from '@coze-agent-ide/tool';
import {
BotCreatorScene,
useBotCreatorContext,
} from '@coze-agent-ide/bot-creator-context';
import { SuggestionList } from './suggestion-list';
import { useSubmitEditor } from './onboarding-editor/hooks/use-submit-editor';
import { type OnboardingEditorAction } from './onboarding-editor';
import { EditorExpendModal } from './editor-expend-modal';
import { settingAreaScrollId } from './const';
const OnboardingEditor = lazy(() => import('./onboarding-editor'));
export {
SuggestionList,
EditorExpendModal,
settingAreaScrollId,
type OnboardingEditorAction,
};
type IOnboardingMessageProps = ToolEntryCommonProps & {
actionButton?: ReactNode;
isLoading?: boolean;
};
const eventWaitTime = 5000;
export const OnboardingMessage = forwardRef<
OnboardingEditorAction,
IOnboardingMessageProps
>(({ title, actionButton, isLoading }, ref) => {
const { botId } = useBotInfoStore(
useShallow(state => ({
botId: state.botId,
})),
);
const { scene } = useBotCreatorContext();
const { onboardingContent, updateSkillOnboarding } = useBotSkillStore(
useShallow(state => ({
onboardingContent: state.onboardingContent,
updateSkillOnboarding: state.updateSkillOnboarding,
})),
);
const setToolValidData = useToolValidData();
const isReadonly = useBotDetailIsReadonly();
const defaultExpand = useDefaultExPandCheck({
blockKey: SkillKeyEnum.ONBORDING_MESSAGE_BLOCK,
configured:
onboardingContent.prologue.length > 0 ||
onboardingContent.suggested_questions.length > 1,
});
const [submitEditor] = useSubmitEditor();
const sendEvent = useMemo(
() =>
debounce((type: 'welcome_message' | 'suggestion') => {
sendTeaEvent(EVENT_NAMES.click_welcome_message_edit, {
type,
bot_id: botId,
});
}, eventWaitTime),
[botId],
);
useEffect(() => {
setToolValidData(
Boolean(
onboardingContent.prologue ||
onboardingContent.suggested_questions?.some?.(q => q.content),
),
);
}, [onboardingContent]);
return (
<>
<ToolContentBlock
blockEventName={OpenBlockEvent.ONBORDING_MESSAGE_BLOCK_OPEN}
header={title}
showBottomBorder
defaultExpand={defaultExpand}
actionButton={actionButton}
>
<Suspense fallback={<Spin />}>
<OnboardingEditor
ref={ref}
initValues={onboardingContent}
isReadonly={isReadonly}
isGenerating={isLoading}
// 社区版暂不支持该功能
plainText={scene === BotCreatorScene.DouyinBot}
onChange={submitEditor}
onBlur={() => {
botSkillSaveManager.saveFlush(ItemType.ONBOARDING);
}}
/>
</Suspense>
<SuggestionList
isReadonly={isReadonly}
initValues={onboardingContent}
onBlur={() => {
botSkillSaveManager.saveFlush(ItemType.ONBOARDING);
}}
onChange={update => {
updateSkillOnboarding(pre => {
sendEvent('suggestion');
return produce(pre, isFunction(update) ? update : () => update);
});
}}
/>
</ToolContentBlock>
</>
);
});

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useRef } from 'react';
import { initEditorByPrologue } from '../method/init-editor';
import { type OnboardingEditorContext } from '../index';
export const useInitEditor = ({
props,
editorRef,
}: OnboardingEditorContext) => {
const { initValues } = props;
const { prologue } = initValues || {};
const hasInit = useRef(false);
useEffect(() => {
if (hasInit.current) {
return;
}
if (!prologue) {
return;
}
if (!editorRef.current) {
return;
}
hasInit.current = true;
if (props.plainText) {
editorRef.current.setText(prologue);
} else {
initEditorByPrologue({
prologue,
editorRef,
});
}
// 滚动到顶部
editorRef.current?.scrollModule?.scrollTo({
top: 0,
});
}, [prologue, editorRef.current]);
};

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, type MutableRefObject, type RefObject } from 'react';
import { reporter } from '@coze-arch/logger';
import type { OnboardingEditorAction } from '../index';
export const useModalEditorSubmit = (
modalEditor: MutableRefObject<OnboardingEditorAction | null>,
ref: RefObject<OnboardingEditorAction>,
) => {
const [isModalEditorSubmitting, setIsModalEditorSubmitting] = useState(false);
const [editorImageUploadNum, setEditorImageUploadNum] = useState(0);
const [editorImageTotalNum, setEditorImageTotalNum] = useState(0);
const submitEditor = async () => {
try {
setIsModalEditorSubmitting(true);
const { checkAndGetMarkdown } = await import(
'@coze-common/md-editor-adapter'
);
const obj = await checkAndGetMarkdown({
editor: modalEditor.current.getEditor(),
validate: false,
onImageUploadProgress: (total, count) => {
setEditorImageUploadNum(count);
setEditorImageTotalNum(total);
},
});
if (!obj) {
return;
}
const content = modalEditor.current?.getEditor()?.getContent();
(ref as RefObject<OnboardingEditorAction>)?.current
?.getEditor()
?.setContent(content);
setIsModalEditorSubmitting(false);
} catch (error) {
setIsModalEditorSubmitting(false);
reporter.error({
message: 'onboarding-editor-modal-checkAndGetMarkdown-error',
error,
});
}
};
return {
submitEditor,
isModalEditorSubmitting,
editorImageUploadNum,
editorImageTotalNum,
};
};

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import { type OnboardingEditorContext } from '../index';
export type UseOnEditorProps = OnboardingEditorContext & {
onEditorFocus: (e: FocusEvent) => void;
onEditorBlur: (e: FocusEvent) => void;
};
export const useOnEditor = ({
editorRef,
onEditorFocus,
onEditorBlur,
}: UseOnEditorProps) => {
useEffect(() => {
if (!editorRef.current) {
return;
}
editorRef.current
?.getRootContainer()
?.addEventListener('focus', onEditorFocus, {
capture: true,
});
editorRef.current
?.getRootContainer()
?.addEventListener('blur', onEditorBlur, {
capture: true,
});
}, [editorRef.current]);
};

View File

@@ -0,0 +1,78 @@
/*
* 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 { trim } from 'lodash-es';
import { produce } from 'immer';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import {
getEditorLines,
removeLastLineMarkerOnChange,
} from '@/component/onboarding-message/onboarding-editor/method/editor-content-helper';
import type { OnboardingEditorContext } from '../index';
export const useSubmitEditor = () => {
const { updateSkillOnboarding } = useBotSkillStore(
useShallow(state => ({
updateSkillOnboarding: state.updateSkillOnboarding,
})),
);
const onSubmit = (context: OnboardingEditorContext) => {
const { api, editorRef } = context;
if (!api.current || !editorRef.current) {
return;
}
if (context.props.plainText) {
const content = editorRef.current.getText();
updateSkillOnboarding(pre =>
produce(pre, draft => {
draft.prologue = trim(String(content));
}),
);
return;
}
api.current.validate().then(async () => {
const { checkAndGetMarkdown } = await import(
'@coze-common/md-editor-adapter'
);
const obj = await checkAndGetMarkdown({
editor: editorRef.current,
validate: false,
});
if (!obj) {
return;
}
const { content } = obj;
const editorLines = getEditorLines(editorRef.current);
const handledContent = removeLastLineMarkerOnChange({
editorLines,
text: content,
});
updateSkillOnboarding(pre =>
produce(pre, draft => {
draft.prologue = handledContent;
}),
);
});
};
return [onSubmit];
};

View File

@@ -0,0 +1,13 @@
.onboarding-editor {
:global {
.icon-expand {
cursor: pointer;
color: rgba(6,7,8, 50%);
}
.editor-kit-container {
padding-top: 6px !important;
}
}
}

View File

@@ -0,0 +1,258 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {
useRef,
type RefObject,
useState,
forwardRef,
useImperativeHandle,
type CSSProperties,
} from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { type FormApi } from '@coze-arch/bot-semi/Form';
import { Spin, Form, Typography } from '@coze-arch/bot-semi';
import { LazyEditorFullInput } from '@coze-common/md-editor-adapter';
import type { Editor } from '@coze-common/md-editor-adapter';
import { botInputLengthService } from '@coze-agent-ide/bot-input-length-limit';
import { IconCozPeople } from '@coze-arch/coze-design/icons';
import { getSchema } from '@/component/onboarding-message/onboarding-editor/method/get-schema';
import { useOnEditor } from '@/component/onboarding-message/onboarding-editor/hooks/use-on-editor';
import s from '../index.module.less';
import { EditorExpendModal } from '../editor-expend-modal';
import { InsertTemplateToolItem } from './plugins/insert-template/tool-item';
import { InsertTemplate } from './plugins/insert-template';
import { sliceEditor } from './method/slice-editor';
import { initEditorByPrologue } from './method/init-editor';
import { getUploadToken } from './method/get-upload-token';
import { getImageUrl } from './method/get-image-url';
import { useModalEditorSubmit } from './hooks/use-modal-editor-submit';
import { useInitEditor } from './hooks/use-init-editor';
import styles from './index.module.less';
const EDITOR_HEIGHT = 132;
const MODAL_EDITOR_HEIGHT = 572;
export interface OnboardingEditorProps {
initValues?: {
prologue: string;
};
isReadonly?: boolean;
// 生成中
isGenerating?: boolean;
// 聚焦展开
focusExpand?: boolean;
onChange?: (context: OnboardingEditorContext) => void;
onBlur?: (context: OnboardingEditorContext) => void;
noExpand?: boolean; // 右下角展开icon
onExpand?: () => void; // 点击展开icon
style?: CSSProperties;
businessKey?: string; // 用于注册toolbar
noLabel?: boolean;
/**
* 开启 plainText 模式后,输入内容与原生 textarea 不会有区别,会取消 markdown 支持,并隐藏 toolbar会覆盖 noToolbar 属性)
* @default false
*/
plainText?: boolean;
}
export interface OnboardingEditorAction {
reInit: (initValues: { prologue: string }) => void;
getEditor: () => Editor | null;
}
export interface OnboardingEditorContext {
props: OnboardingEditorProps;
editorRef: RefObject<Editor>;
api: RefObject<FormApi | null>;
}
const InnerEditor = forwardRef<OnboardingEditorAction, OnboardingEditorProps>(
(props, ref) => {
const editorRef = useRef<Editor>(null);
const api = useRef<FormApi | null>(null);
const context: OnboardingEditorContext = {
props,
editorRef,
api,
};
const [isEditorFocus, setIsEditorFocus] = useState(false);
useInitEditor(context);
useOnEditor({
...context,
onEditorBlur: () => {
setIsEditorFocus(false);
props?.onBlur?.(context);
},
onEditorFocus: () => {
setIsEditorFocus(true);
},
});
useImperativeHandle(ref, () => ({
reInit: (initValues: { prologue: string }) => {
if (props.plainText) {
return editorRef.current.setText(initValues.prologue);
}
initEditorByPrologue({
prologue: initValues.prologue,
editorRef,
});
},
getEditor: () => editorRef.current,
}));
return (
<>
{!props?.noLabel ? (
<div className={classNames(s['onboarding-message-title'], s.text)}>
<span className="coz-fg-secondary">
{I18n.t('bot_edit_opening_text_title')}
</span>
</div>
) : null}
<Form<Record<string, unknown>>
getFormApi={formApi => (api.current = formApi)}
>
<Spin
data-testid="bot-editor.onboarding-editor"
spinning={props?.isGenerating ?? false}
tip={I18n.t('generating')}
>
<LazyEditorFullInput
businessKey={props.businessKey ?? 'onboarding-editor'}
fieldStyle={{ padding: '0' }}
disabled={props.isReadonly}
field="prologue"
onChange={() => {
sliceEditor(
editorRef,
botInputLengthService.getInputLengthLimit('onboarding'),
);
props?.onChange?.(context);
}}
noExpand={props?.noExpand ?? false}
schema={getSchema()}
style={{
height: isEditorFocus ? 'unset' : EDITOR_HEIGHT,
minHeight: EDITOR_HEIGHT,
...props?.style,
}}
getEditor={editor => {
editorRef.current = editor;
}}
noToolbar={props.isReadonly}
onExpand={props?.onExpand}
plainText={props.plainText}
className={styles['onboarding-editor']}
getUploadToken={getUploadToken}
getImgURL={getImageUrl}
registerPlugins={(plugins, { editor }) =>
plugins.concat([
[InsertTemplate, { editor, template: '{{user_name}}' }],
])
}
registerToolItem={items =>
items.concat([
() => (
<InsertTemplateToolItem
tooltipText={I18n.t('add_nickname')}
style={{ color: 'rgba(6,7,8,0.5)', height: '22px' }}
pluginValue="{{user_name}}"
>
<IconCozPeople />
</InsertTemplateToolItem>
),
])
}
noLabel
label={I18n.t('community_Group_Title_content')}
maxCount={botInputLengthService.getInputLengthLimit('onboarding')}
placeholder={I18n.t(
'community_Please_enter_please_enter_your_post',
)}
/>
</Spin>
</Form>
</>
);
},
);
export const OnboardingEditor = forwardRef<
OnboardingEditorAction,
OnboardingEditorProps
>((props, ref) => {
const [editorModalVisible, setEditorModalVisible] = useState(false);
const modalEditor = useRef<OnboardingEditorAction | null>(null);
const {
isModalEditorSubmitting,
editorImageUploadNum,
editorImageTotalNum,
submitEditor,
} = useModalEditorSubmit(
modalEditor,
ref as RefObject<OnboardingEditorAction>,
);
return (
<>
<EditorExpendModal
visible={editorModalVisible}
onCancel={() => {
if (isModalEditorSubmitting) {
return;
}
setEditorModalVisible(false);
}}
>
<InnerEditor
{...props}
ref={modalEditor}
noExpand={true}
businessKey="onboarding-editor-modal"
noLabel={true}
style={{
height: MODAL_EDITOR_HEIGHT,
}}
onChange={submitEditor}
/>
{isModalEditorSubmitting ? (
<Typography.Text size="small" className="coz-fg-secondary">
{I18n.t('community_Image_uploading', {
upload_num: editorImageUploadNum,
total_num: editorImageTotalNum,
})}
</Typography.Text>
) : null}
</EditorExpendModal>
<InnerEditor
ref={ref}
{...props}
onExpand={() => setEditorModalVisible(true)}
/>
</>
);
});
export default OnboardingEditor;

View File

@@ -0,0 +1,36 @@
/*
* 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 { DEFAULT_ZONE } from '@coze-common/md-editor-adapter';
import type { Editor } from '@coze-common/md-editor-adapter';
const countTextLines = (text: string) => text.split('\n').length;
export const getEditorLines = (editor: Editor) =>
editor.getContentState().getZoneState(DEFAULT_ZONE)?.length() ?? 0;
export const removeLastLineMarkerOnChange = ({
text,
editorLines,
}: {
text: string;
editorLines: number;
}) => {
if (countTextLines(text) > editorLines && text.endsWith('\n')) {
return text.slice(0, -1);
}
return text;
};

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 { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/bot-semi';
import {
type GetImgURLRequest,
type GetImgURLResponse,
} from '@coze-arch/bot-api/market_interaction_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
export const getImageUrl: (
req?: GetImgURLRequest,
) => Promise<GetImgURLResponse> = async req => {
const { Key: uri } = req;
const result = await PlaygroundApi.GetImagexShortUrl({
uris: [uri],
});
const { code, msg, data } = result;
const urlAndAudit = data?.url_info?.[uri];
const audit = urlAndAudit?.review_status;
const url = urlAndAudit?.url;
if (!audit) {
Toast.error({
content: I18n.t('inappropriate_contents'),
showClose: false,
});
throw new Error('inappropriate_contents');
}
if (!url) {
throw new Error('inappropriate_contents');
}
return {
code: Number(code),
message: msg,
data: {
url,
},
};
};

View File

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

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type GetUploadTokenResponse } from '@coze-arch/bot-api/market_interaction_api';
import { DeveloperApi } from '@coze-arch/bot-api';
const TIMEOUT = 60000;
export const getUploadToken: () => Promise<GetUploadTokenResponse> =
async () => {
const dataAuth = await DeveloperApi.GetUploadAuthToken(
{
scene: 'bot_task',
},
{ timeout: TIMEOUT },
);
const { code, msg, data } = dataAuth;
return {
code: Number(code),
message: msg,
data: {
...data,
...data.auth,
},
};
};

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { RefObject } from 'react';
import type { Editor } from '@coze-common/md-editor-adapter';
import { md2html } from '@coze-common/md-editor-adapter';
export interface InitEditorByPrologueProps {
prologue: string;
editorRef: RefObject<Editor>;
}
export const initEditorByPrologue = (props: InitEditorByPrologueProps) => {
const { prologue, editorRef } = props;
const htmlContent = md2html(prologue);
editorRef.current?.setHTML(htmlContent);
};

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { RefObject } from 'react';
import { ZoneDelta } from '@coze-common/md-editor-adapter';
import { type Editor } from '@coze-common/md-editor-adapter';
export const sliceEditor = (editorRef: RefObject<Editor>, maxCount: number) => {
if (!editorRef.current) {
return;
}
const editor = editorRef.current;
const range = editor.selection.getSelection();
const { start } = range;
const zone = start.zoneId;
const contentState = editor.getContentState();
const zoneState = contentState.getZoneState(zone);
if (!zoneState) {
return;
}
const currentCount = zoneState.totalWidth() - 1;
const sliceCount = currentCount - maxCount;
if (sliceCount > 0) {
const delta = new ZoneDelta({ zoneId: zone });
// 保留maxCount, 删除之后的内容
delta.retain(maxCount).delete(sliceCount);
editor.getContentState().apply(delta);
}
};

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ZoneDelta } from '@coze-common/md-editor-adapter';
import {
Plugin,
type Editor,
type IRenderContext,
Text,
} from '@coze-common/md-editor-adapter';
export class InsertTemplate extends Plugin {
private readonly template: string;
private editor: Editor;
static KEY = 'insertTemplate';
constructor(props: { editor: Editor; template: string }) {
const { editor, template } = props;
super();
this.editor = editor;
this.template = template;
this.editor.registerCommand(
InsertTemplate.KEY,
this.insertTemplate.bind(this),
);
}
match(attributeKey: string): boolean {
return attributeKey === InsertTemplate.KEY;
}
render(props: IRenderContext): JSX.Element {
return <Text className="font-medium">{props.children}</Text>;
}
insertTemplate() {
const range = this.editor.selection.getSelection();
const { start, end } = range;
const zone = start.zoneId;
const contentState = this.editor.getContentState();
const zoneState = contentState.getZoneState(zone);
if (!zoneState) {
return;
}
const lineState = zoneState.getLine(start.line);
if (!lineState) {
return;
}
const startPos = zoneState.pointToOffset(start);
const endPos = zoneState.pointToOffset(end);
if (startPos === null || endPos === null) {
return;
}
const delta = new ZoneDelta({ zoneId: zone });
delta.retain(startPos).delete(endPos - startPos);
delta.insert(this.template);
this.editor.getContentState().apply(delta);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type PropsWithChildren } from 'react';
import { ToolbarButton } from '@coze-common/md-editor-adapter';
const PLUGIN_KEY = 'insertTemplate';
export interface InsertTemplateToolItemProps {
style?: React.CSSProperties;
tooltipText?: string;
pluginValue: string;
}
export const InsertTemplateToolItem: FC<
PropsWithChildren<InsertTemplateToolItemProps>
> = ({ children, tooltipText, pluginValue }) => (
<ToolbarButton
extra={{
size: 'small',
}}
icon={children}
tooltipText={tooltipText}
pluginKey={PLUGIN_KEY}
pluginValue={pluginValue}
></ToolbarButton>
);

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import { nanoid } from 'nanoid';
import { type SuggestionListContext } from '../index';
const maxItemLength = 100;
export const useAddEmptySuggestion = (context: SuggestionListContext) => {
const {
isReadonly,
onChange,
initValues: { suggested_questions },
} = context.props;
useEffect(() => {
const addItemIfLastIsNotEmpty = () => {
// 如果列表全部有值,且不是只读状态,添加一条空项
const canAddItem =
suggested_questions.length < maxItemLength &&
suggested_questions.every(sug => sug.content);
if (canAddItem && !isReadonly) {
onChange?.(prev => ({
...prev,
suggested_questions: [
...prev.suggested_questions,
{ id: nanoid(), content: '' },
],
}));
}
};
addItemIfLastIsNotEmpty();
}, [suggested_questions]);
};

View File

@@ -0,0 +1,128 @@
/*
* 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 Dispatch,
type FC,
type SetStateAction,
useMemo,
} from 'react';
import { SortableList } from '@coze-studio/components/sortable-list';
import { type TItemRender } from '@coze-studio/components';
import { type SuggestQuestionMessage } from '@coze-studio/bot-detail-store';
import { I18n } from '@coze-arch/i18n';
import { type SuggestedQuestionsShowMode } from '@coze-arch/bot-api/developer_api';
import s from '../index.module.less';
import { SuggestQuestionItemContent } from './suggestion-item';
import { SuggestionHeader } from './suggestion-header';
import { useAddEmptySuggestion } from './hooks/use-add-empty-suggestion';
const SortableListSymbol = Symbol('onboarding-suggestion-list');
export interface SuggestionListContext {
props: SuggestionListProps;
}
interface SuggestionListInitValues {
suggested_questions: SuggestQuestionMessage[];
suggested_questions_show_mode: SuggestedQuestionsShowMode;
}
export interface SuggestionListProps {
initValues?: SuggestionListInitValues;
isReadonly?: boolean;
onBlur?: () => void;
onChange?: Dispatch<SetStateAction<SuggestionListInitValues>>;
}
export const SuggestionList: FC<SuggestionListProps> = props => {
const {
initValues: { suggested_questions },
isReadonly,
onBlur,
onChange,
} = props;
const context: SuggestionListContext = {
props,
};
useAddEmptySuggestion(context);
const itemRender = useMemo<TItemRender<SuggestQuestionMessage>>(
() =>
({ data, connect, isDragging, isHovered }) => (
<SuggestQuestionItemContent
key={data.id}
message={data}
isDragging={Boolean(isDragging)}
isHovered={Boolean(isHovered)}
connect={connect}
value={suggested_questions}
handleOnBlur={onBlur}
disabled={!data.content}
onMessageChange={value => {
onChange?.(prev => {
const _suggestions = [...prev.suggested_questions];
const index = _suggestions.findIndex(item => item.id === data.id);
_suggestions.splice(index, 1, value);
return {
...prev,
suggested_questions: _suggestions,
};
});
}}
handleRemoveSuggestion={id => {
onChange?.(prev => ({
...prev,
suggested_questions: prev.suggested_questions.filter(
sug => sug.id !== id,
),
}));
}}
/>
),
[isReadonly],
);
return (
<>
<SuggestionHeader
context={context}
onSwitchShowMode={mode => {
onChange?.(prev => ({
...prev,
suggested_questions_show_mode: mode,
}));
}}
/>
<SortableList
type={SortableListSymbol}
list={suggested_questions}
getId={suggestion => suggestion.id}
enabled={suggested_questions.length > 1}
onChange={(newList: SuggestQuestionMessage[]) => {
onChange?.(prev => ({
...prev,
suggested_questions: newList,
}));
}}
itemRender={itemRender}
/>
{isReadonly && !suggested_questions.length ? (
<div className={s['text-none']}>{I18n.t('bot_element_unset')}</div>
) : null}
</>
);
};

Some files were not shown because too many files have changed in this diff Show More