feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
.add {
|
||||
min-width: 96px;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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']}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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} />;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
.borderless {
|
||||
// padding: 0 !important;
|
||||
}
|
||||
// .solid {
|
||||
// font-size: 12px;
|
||||
// }
|
||||
// .primary {
|
||||
// padding: 6px 12px;
|
||||
// }
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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%);
|
||||
}
|
||||
@@ -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']}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
69
frontend/packages/agent-ide/space-bot/src/component/index.ts
Normal file
69
frontend/packages/agent-ide/space-bot/src/component/index.ts
Normal 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 };
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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 |
@@ -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>
|
||||
);
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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];
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
.onboarding-editor {
|
||||
:global {
|
||||
.icon-expand {
|
||||
cursor: pointer;
|
||||
color: rgba(6,7,8, 50%);
|
||||
}
|
||||
|
||||
.editor-kit-container {
|
||||
padding-top: 6px !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user