feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_3530_1794" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M0 0H16V16H0V0Z" fill="black" />
</mask>
<g mask="url(#mask0_3530_1794)">
<path
d="M5.00781 11.1694L7.22266 4.82812H8.78271L10.9932 11.1694H9.6001L9.12109 9.63135H6.87988L6.40088 11.1694H5.00781ZM7.9873 6.05859L7.1875 8.63818H8.81348L8.01367 6.05859H7.9873Z"
fill="currentColor" />
<rect x="1.5" y="1.5" width="13" height="13" rx="6.5" stroke="currentColor" stroke-width="1.33" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,4 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" fill="#FFBF00"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.1167 12.8149C28.1167 15.6088 23.5869 17.8737 17.9991 17.8737C12.4113 17.8737 7.88151 15.6088 7.88151 12.8149C7.88151 10.021 12.4113 7.7561 17.9991 7.7561C23.5869 7.7561 28.1167 10.021 28.1167 12.8149ZM28.1083 15.3452C28.1053 15.3452 28.1028 15.3474 28.1025 15.3504C27.834 18.0245 23.4127 20.1514 17.9978 20.1514C12.5838 20.1514 8.16296 18.0252 7.89325 15.3516C7.89287 15.348 7.88978 15.3452 7.88609 15.3452C7.88212 15.3452 7.87891 15.3484 7.87891 15.3523V18.1279V18.3739C7.87891 18.3775 7.88182 18.3804 7.88541 18.3804C7.88875 18.3804 7.89155 18.383 7.89188 18.3863C8.16098 21.0601 12.5821 23.1867 17.9965 23.1867C23.411 23.1867 27.832 21.0601 28.1011 18.3863C28.1015 18.383 28.1043 18.3804 28.1076 18.3804C28.1112 18.3804 28.1141 18.3775 28.1141 18.3739V18.1279V15.351C28.1141 15.3478 28.1115 15.3452 28.1083 15.3452ZM28.101 20.4093C28.1013 20.4059 28.1042 20.4033 28.1075 20.4033C28.1112 20.4033 28.1141 20.4063 28.1141 20.4099V23.186V23.4321C28.1141 23.4357 28.1112 23.4386 28.1076 23.4386C28.1043 23.4386 28.1015 23.4411 28.1011 23.4445C27.832 26.1183 23.411 28.2448 17.9965 28.2448C12.5821 28.2448 8.16098 26.1183 7.89188 23.4445C7.89155 23.4411 7.88875 23.4386 7.88541 23.4386C7.88182 23.4386 7.87891 23.4357 7.87891 23.4321V23.186V20.4099C7.87891 20.4063 7.88185 20.4033 7.88549 20.4033C7.88887 20.4033 7.8917 20.4059 7.89204 20.4093C8.16274 23.0823 12.5831 25.208 17.9965 25.208C23.4099 25.208 27.8303 23.0823 28.101 20.4093Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.33398 2.99992C8.33398 1.52716 9.52789 0.333252 11.0007 0.333252H27.0006C28.4734 0.333252 29.6673 1.52716 29.6673 2.99992V18.9999C29.6673 20.4727 28.4734 21.6666 27.0006 21.6666H21.6673V26.9999C21.6673 28.4727 20.4734 29.6666 19.0007 29.6666H3.00065C1.52789 29.6666 0.333984 28.4727 0.333984 26.9999V10.9999C0.333984 9.52716 1.52789 8.33325 3.00065 8.33325H8.33398V2.99992ZM8.33398 10.9999H3.00065V26.9999H19.0007V21.6666H11.0007C9.52789 21.6666 8.33398 20.4727 8.33398 18.9999V10.9999ZM19.0007 18.9999H11.0007V10.9999H19.0007V18.9999ZM21.6673 18.9999V10.9999C21.6673 9.52716 20.4734 8.33325 19.0007 8.33325H11.0007V2.99992H27.0006V18.9999H21.6673Z" fill="#1D1C23" fill-opacity="0.35"/>
</svg>

After

Width:  |  Height:  |  Size: 841 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,13 @@
@common-box-shadow: 0px 2px 8px 0px rgba(31, 35, 41, 0.02),
0px 2px 4px 0px rgba(31, 35, 41, 0.02), 0px 2px 2px 0px rgba(31, 35, 41, 0.02);
.common-svg-icon(@size:14px, @color:#3370ff) {
> svg {
width: @size;
height: @size;
> path {
fill: @color;
}
}
}

View File

@@ -0,0 +1,182 @@
/* stylelint-disable declaration-no-important */
@import './common.less';
.wrapper {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: #f7f7fa;
}
.text {
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
.container {
display: flex;
flex-direction: row;
width: 100%;
height: calc(100% - 80px);
flex: 1;
}
.spin {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 100;
background-color: rgb(255 255 255 / 50%);
}
.playground-neat {
.message-area {
min-width: 258px;
}
}
.develop-area {
overflow: hidden;
display: flex;
flex-direction: column;
}
.develop-area-scroll {
overflow: auto;
flex: 1;
}
.setting-area {
overflow: hidden;
display: flex;
flex: 1 1;
flex-direction: column;
border-left: 1px solid rgb(28 29 35 / 12%);
.setting-title-block {
width: 100%;
display: flex;
justify-content: flex-end;
margin: 12px 0;
}
.setting-area-scroll {
overflow: auto;
flex: 1;
}
:global {
.semi-collapse-item {
border-bottom: none;
}
.semi-collapse-header {
margin-left: 0;
margin-right: 0;
}
.semi-collapse-content {
padding-left: 0;
padding-right: 0;
}
.semi-select {
width: 100%;
}
}
:global {
.semi-collapsible-wrapper {
padding-left: 16px;
}
}
}
.message-area {
position: relative;
width: 100%;
height: 100%;
flex-direction: column;
display: flex;
justify-content: space-between;
overflow: hidden;
min-width: 404px;
transition: min-width 0.2s ease;
}
.playground-neat {
.message-area {
min-width: 258px;
}
}
.title {
margin: 8px 0 0 4px !important;
}
.sheet-title-node-cover {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
}
.bj-cover {
background-color: #fff;
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 4%),
0 0 1px 0 rgb(0 0 0 / 8%);
}
.border-cover {
border-bottom: none;
}
.spin-wrapper.top-level {
width: 100%;
height: 100% !important;
:global {
.semi-spin-children {
.wrapper();
}
}
}
.sheet-view-left-header {
padding: 16px 28px !important;
}
.icon-button-16 {
cursor: pointer;
&:hover {
border-radius: 4px;
}
:global {
.semi-button {
&.semi-button-size-small {
padding: 1px !important;
height: 16px;
svg {
@apply text-foreground-2;
}
}
}
}
}
// 能力模块默认说明文案样式
.tip-text {
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
color: var(--light-usage-text-color-text-2, rgb(28 29 35 / 60%));
}

View File

@@ -0,0 +1,67 @@
.border-line(@radius: 8px, @color: #eceef0) {
&::after {
content: '';
position: absolute;
width: 200%;
height: 200%;
top: 0;
left: 0;
transform-origin: 0 0;
border-width: 1px;
border-style: solid;
transform: scale(0.5, 0.5);
box-sizing: border-box;
pointer-events: none;
color: @color;
border-color: @color;
border-radius: @radius;
}
}
.base-border-line(@color: #eceef0) {
content: '';
display: inline-block;
position: absolute;
background: @color;
}
.border-left-line(@color: #eceef0) {
&::after {
top: 0;
left: 0;
bottom: 0;
width: 1px;
transform: scaleX(0.5);
.base-border-line(@color);
}
}
.border-right-line(@color: #eceef0) {
&::after {
top: 0;
right: 0;
bottom: 0;
width: 1px;
transform: scaleX(0.5);
.base-border-line(@color);
}
}
.border-top-line(@color: #eceef0) {
&::after {
top: 0;
right: 0;
left: 0;
height: 1px;
transform: scaleY(0.5);
.base-border-line(@color);
}
}
.border-bottom-line(@color: #eceef0) {
&::after {
left: 0;
right: 0;
bottom: 0;
height: 1px;
transform: scaleY(0.5);
.base-border-line(@color);
}
}

View File

@@ -0,0 +1,23 @@
.semi-modal-body {
overflow: auto;
}
// 统一button样式
.semi-button {
font-size: 12px;
.semi-button-content-right {
margin-left: 4px;
}
}
// borderless取消hover样式
.semi-button-borderless:not(.semi-button-disabled):hover {
background-color: inherit;
border: none;
}
.semi-button.semi-button-primary:focus-visible,
.semi-button.semi-button-secondary:focus-visible,
.semi-button.semi-button-tertiary:focus-visible,
.semi-button.semi-button-warning:focus-visible,
.semi-button.semi-button-danger:focus-visible {
outline: none;
}

View File

@@ -0,0 +1,13 @@
@bg-gray-blue: rgba(
240,
245,
255,
0.81
); // highlight the area of the background
@text-gray-blue: #536eb1; // highlight the area of the text
@bg-white-smoke: #f5f5f5; // background of input/table/btn TODOrgba(46, 50, 56, 0.05)) #2e3238
@border-light-gray: rgba(28, 29, 35, 0.12);
@error-red: #f93920;
@text-title-black: #2e3238;
@text-highlight-blue: #3370ff;

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type CSSProperties, type PropsWithChildren } from 'react';
import classNames from 'classnames';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { type ButtonProps, type Theme } from '@coze-arch/bot-semi/Button';
import { UIButton } from '@coze-arch/bot-semi';
import { BotE2e } from '@coze-data/e2e';
import s from './index.module.less';
interface AddButtonProps {
onClick?: () => void;
className?: string;
style?: CSSProperties;
theme?: Theme;
icon?: React.ReactNode;
disabled?: boolean;
}
export const AddButton: React.FC<
PropsWithChildren<AddButtonProps & ButtonProps>
> = ({
onClick,
className,
style,
children,
theme,
icon,
disabled,
type,
...props
}) => {
const isReadonly = useBotDetailIsReadonly();
if (isReadonly) {
return null;
}
return (
<UIButton
data-testid={BotE2e.BotVariableAddModalAddBtn}
disabled={disabled}
style={style}
className={classNames(s.add, className)}
type={type || 'tertiary'}
theme={theme || 'light'}
icon={icon}
onClick={onClick}
{...props}
>
{children}
</UIButton>
);
};

View File

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

View File

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

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ConnectorConfigStatus } from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { Button, type ButtonProps } from '@coze-arch/coze-design';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { useUIModal, UIButton, Typography } from '@coze-arch/bot-semi';
import {
AuthStatus,
type AuthLoginInfo,
ConfigStatus,
} from '@coze-arch/bot-api/developer_api';
import { IconAlertCircle } from '@douyinfe/semi-icons';
import {
checkAuthInfoValid,
executeAuthRedirect,
logAndToastAuthInfoError,
useRevokeAuth,
} from '../../util/auth';
export interface AuthorizeButtonProps {
origin: 'setting' | 'publish';
id: string;
agentType?: 'bot' | 'project';
channelName: string;
status: ConfigStatus | AuthStatus | ConnectorConfigStatus;
revokeSuccess: (id: string) => void;
authInfo: AuthLoginInfo;
isMouseIn?: boolean;
/** 是否使用 Coze 2.0 的 Button 组件,默认 false */
isV2?: boolean;
/** 自定义 Coze 2.0 Button 的 props */
v2ButtonProps?: ButtonProps;
onBeforeAuthRedirect?: (
parameters: Pick<AuthorizeButtonProps, 'id' | 'authInfo' | 'origin'>,
) => void;
}
export const AuthorizeButton = ({
status,
id,
agentType = 'bot',
channelName,
revokeSuccess,
origin,
authInfo,
isMouseIn = true,
isV2 = false,
v2ButtonProps = {
color: 'highlight',
size: 'small',
},
onBeforeAuthRedirect,
}: AuthorizeButtonProps) => {
const isConfiguredOrConfiguring = [
ConfigStatus.Configured,
ConfigStatus.Configuring,
].includes(status as ConfigStatus);
const handleAuth = () => {
if (!checkAuthInfoValid(authInfo)) {
logAndToastAuthInfoError();
return;
}
if (
(origin === 'publish' && status === ConfigStatus.NotConfigured) ||
(origin === 'setting' && status === AuthStatus.Unauthorized)
) {
sendTeaEvent(
origin === 'publish'
? EVENT_NAMES.publish_oauth_button_click
: EVENT_NAMES.settings_oauth_button_click,
{ action: '授权', channel_name: channelName },
);
onBeforeAuthRedirect?.({ id, authInfo, origin });
executeAuthRedirect({ id, authInfo, origin });
}
if (
(origin === 'publish' && isConfiguredOrConfiguring) ||
(origin === 'setting' && status === AuthStatus.Authorized)
) {
sendTeaEvent(
origin === 'publish'
? EVENT_NAMES.publish_oauth_button_click
: EVENT_NAMES.settings_oauth_button_click,
{ action: '解除授权', channel_name: channelName },
);
openRevokeAuthModal();
}
};
const { revokeLoading, runRevoke } = useRevokeAuth({
id,
onRevokeSuccess: revokeSuccess,
onRevokeFinally: () => closeRevokeAuthModal(),
});
const {
open: openRevokeAuthModal,
close: closeRevokeAuthModal,
modal: revokeModal,
visible: revokeModalVisible,
} = useUIModal({
confirmLoading: revokeLoading,
type: 'info',
title: I18n.t('user_revoke_authorization_title'),
onOk: runRevoke,
okText: I18n.t('Confirm'),
cancelText: I18n.t('Cancel'),
icon: (
<IconAlertCircle
style={{ color: 'var(--semi-color-danger)' }}
size="extra-large"
/>
),
onCancel: () => {
closeRevokeAuthModal();
},
okButtonProps: {
type: 'danger',
},
});
const buttonText = I18n.t(
isConfiguredOrConfiguring
? 'bot_publish_columns_action_revoke_authorize'
: 'bot_publish_columns_action_authorize',
);
const authButton = isV2 ? (
<Button onClick={handleAuth} {...v2ButtonProps}>
{buttonText}
</Button>
) : (
<UIButton onClick={handleAuth} theme="borderless">
{buttonText}
</UIButton>
);
return status === ConfigStatus.Configured ? (
<>
{/* 在 hover 渠道表单对应行,或“撤销授权”弹窗显示中时,显示“撤销授权”按钮 */}
{isMouseIn || revokeModalVisible ? authButton : null}
{revokeModal(
agentType === 'project' ? (
<Typography.Text type="secondary">
{I18n.t('project_release_cancel1_desc')}
</Typography.Text>
) : null,
)}
</>
) : (
authButton
);
};

View File

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

View File

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

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type BindConnectorResponse,
type GetBindConnectorConfigResponse,
type SaveBindConnectorConfigResponse,
} from '@coze-arch/idl/developer_api';
import { I18n } from '@coze-arch/i18n';
import { Form, Typography } from '@coze-arch/bot-semi';
import { type ApiError } from '@coze-arch/bot-http';
import styles from './index.module.less';
type ErrorResponse =
| GetBindConnectorConfigResponse
| SaveBindConnectorConfigResponse
| BindConnectorResponse;
function isBindConnectorResponse(
res: ErrorResponse,
): res is BindConnectorResponse {
return ['bind_bot_id', 'bind_bot_name', 'bind_space_id'].every(
key => key in res,
);
}
export interface ConnectorErrorProps {
errorMessage: ApiError;
}
export const ConnectorError = ({ errorMessage }: ConnectorErrorProps) => {
const res = (errorMessage?.raw ?? {}) as ErrorResponse;
return (
<Form.ErrorMessage
error={
isBindConnectorResponse(res) ? (
<div className={styles['error-link']}>
{I18n.t('bot_publish_bind_error', {
bot_name: (
<Typography.Text
className={styles['error-link-underline']}
link={{
href: `/space/${res.bind_space_id}/${res.bind_agent_type === 1 ? 'project-ide' : 'bot'}/${res.bind_bot_id}`,
}}
ellipsis={{
showTooltip: {
opts: {
content: res.bind_bot_name,
},
},
}}
>
{res.bind_bot_name}
</Typography.Text>
),
key_name: 'token',
})}
</div>
) : (
errorMessage?.msg
)
}
className={styles['error-container']}
/>
);
};

View File

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

View File

@@ -0,0 +1,198 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { logger } from '@coze-arch/logger';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import { IconCozTrashCan, IconCozPlus } from '@coze-arch/coze-design/icons';
import { TagGroup, ArrayField, Button } from '@coze-arch/coze-design';
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
import { type RuleItem } from '@coze-arch/bot-semi/Form';
import { UIFormInput, Form, Typography } from '@coze-arch/bot-semi';
import {
type Options,
type FormSchemaItem,
} from '@coze-arch/bot-api/developer_api';
import { type TFormData } from '../types';
import styles from './index.module.less';
function formatMultiSelectValue(rawValue: string, enums?: Options[]) {
const arrayValue = typeSafeJSONParse(rawValue) as string[] | undefined;
if (!arrayValue) {
return [];
}
return arrayValue.map(value => ({
children: enums?.find(option => option.value === value)?.label ?? value,
}));
}
export interface ConnectorFieldProps {
formItemSchema: FormSchemaItem;
isReadOnly: boolean;
initValue?: TFormData;
}
export const ConnectorField = (props: ConnectorFieldProps) => {
const { formItemSchema, isReadOnly, initValue } = props;
const rawInitValue = initValue?.[formItemSchema.name];
if (isReadOnly) {
return (
<div className={styles['disable-field']}>
<div className={styles.title}>{formItemSchema.title}</div>
{formItemSchema.type === 'array' ? (
<TagGroup
tagList={formatMultiSelectValue(rawInitValue, formItemSchema.enums)}
/>
) : (
<Typography.Text
style={{ width: '100%' }}
ellipsis={{
showTooltip: {
opts: {
content: rawInitValue,
style: { wordBreak: 'break-word' },
},
},
}}
>
{rawInitValue}
</Typography.Text>
)}
</div>
);
}
function createRules(fieldSchema: FormSchemaItem): RuleItem[] {
// 确保 formItemSchema.rules 是一个数组
const itemRules = fieldSchema.rules ?? [];
const rules = itemRules.map(rule => {
const ruleMessage = rule.message
? I18n.t(rule.message as I18nKeysNoOptionsType, {
field: fieldSchema.name,
})
: undefined;
return { ...rule, ...(ruleMessage && { message: ruleMessage }) };
});
// 添加 'required' 规则
rules.push({
required: fieldSchema.required,
message: I18n.t('bot_publish_field_placeholder', {
field: fieldSchema.title ?? '',
}),
});
return rules as RuleItem[];
}
if (!formItemSchema.name) {
return null;
}
switch (formItemSchema.component) {
case 'Input':
if (formItemSchema.type === 'array') {
let values: string[] = [];
try {
values = JSON.parse(rawInitValue);
} catch (e) {
logger.error({ error: e as Error });
values = [];
}
// 添加一个默认空值
if (!values.length) {
values.push('');
}
return (
<ArrayField field={formItemSchema.name} initValue={values}>
{({ arrayFields, add }) => (
<>
{arrayFields.map(({ key, field, remove }, i) => (
<UIFormInput
key={key}
placeholder={I18n.t('bot_publish_field_placeholder', {
field: formItemSchema.title ?? '',
})}
field={field}
label={formItemSchema.title}
noLabel={i > 0}
required={formItemSchema.required}
rules={createRules(formItemSchema)}
fieldClassName={styles.input}
suffix={
arrayFields.length <= 1 ? null : (
<IconCozTrashCan onClick={remove} />
)
}
/>
))}
<Button
className={styles['link-button']}
color="highlight"
size="small"
icon={<IconCozPlus />}
onClick={add}
>
{I18n.t('binding_add_card')}
</Button>
</>
)}
</ArrayField>
);
}
return (
<UIFormInput
key={formItemSchema.name}
placeholder={I18n.t('bot_publish_field_placeholder', {
field: formItemSchema.title ?? '',
})}
field={formItemSchema.name}
label={formItemSchema.title}
required={formItemSchema.required}
showClear
rules={createRules(formItemSchema)}
initValue={rawInitValue}
/>
);
case 'Select': {
const isMultiple = formItemSchema.type === 'array';
const selectInitValue = isMultiple
? (typeSafeJSONParse(rawInitValue) as string[] | undefined)
: rawInitValue;
return (
<Form.Select
key={formItemSchema.name}
placeholder={`Enter ${formItemSchema.title}`}
field={formItemSchema.name}
label={formItemSchema.title}
optionList={formItemSchema.enums}
multiple={isMultiple}
rules={createRules(formItemSchema)}
initValue={selectInitValue}
/>
);
}
default:
return null;
}
};

View File

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

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable complexity -- ignore */
import ReactMarkdown from 'react-markdown';
import {
forwardRef,
type Ref,
useRef,
useImperativeHandle,
useEffect,
} from 'react';
import { useUpdate } from 'ahooks';
import type { FormApi } from '@coze-arch/bot-semi/Form';
import { Space, Form } from '@coze-arch/bot-semi';
import { type ApiError } from '@coze-arch/bot-http';
import { type SchemaAreaInfo } from '@coze-arch/bot-api/developer_api';
import { type FormActions, type TFormData } from '../types';
import { ConnectorField } from '../connector-field';
import styles from './index.module.less';
export interface ConnectorFormProps {
schemaAreaInfo?: SchemaAreaInfo;
initValue?: TFormData;
getFormDisable: (disable: boolean) => void;
isReadOnly: boolean;
setErrorMessage: (error?: ApiError) => void;
}
const DEFAULT_FORM_STEP = 2;
// 多选 Select 在 Form 中的 value 是 string[],但提交到后端需要转换成 JSON string
type FormValues = Record<string, string | string[]>;
export const ConnectorForm = forwardRef(
(props: ConnectorFormProps, ref: Ref<FormActions>) => {
const {
schemaAreaInfo,
initValue,
getFormDisable,
isReadOnly,
setErrorMessage,
} = props;
const formApiRef = useRef<FormApi<FormValues>>();
const update = useUpdate();
useImperativeHandle<FormActions, FormActions>(ref, () => ({
submit: async () => {
const values = await formApiRef.current?.validate();
return Object.fromEntries(
Object.entries(values ?? {}).map(([key, value]) => [
key,
Array.isArray(value) ? JSON.stringify(value) : value,
]),
);
},
reset: () => formApiRef.current?.reset(),
}));
useEffect(() => {
// 解决formApiRef.current取值不实时问题
update();
// eslint-disable-next-line react-hooks/exhaustive-deps -- ignore
}, [schemaAreaInfo]);
const formDisabled =
schemaAreaInfo?.schema_list
?.filter(item => item.required)
.some(field => {
const value = formApiRef.current?.getValue(field.name);
if (Array.isArray(value)) {
return !value.length || (value.length === 1 && !value[0]);
}
return !value;
}) || !schemaAreaInfo?.schema_list?.length;
useEffect(() => {
getFormDisable(formDisabled);
// eslint-disable-next-line react-hooks/exhaustive-deps -- ignore
}, [formDisabled]);
return (
<div>
{schemaAreaInfo?.title_text ? (
<Space spacing={12} align="start">
<span className={styles['step-order']}>
{schemaAreaInfo.step_order || DEFAULT_FORM_STEP}
</span>
<div className={styles['step-content']}>
<div className={styles['step-title']}>
{schemaAreaInfo.title_text}
</div>
</div>
</Space>
) : null}
{schemaAreaInfo?.description ? (
<ReactMarkdown skipHtml={true} className={styles.markdown}>
{schemaAreaInfo?.description}
</ReactMarkdown>
) : null}
{schemaAreaInfo?.schema_list?.length ? (
<Form<FormValues>
initValues={initValue}
className={styles['config-form']}
onValueChange={() => {
update();
setErrorMessage(undefined);
}}
getFormApi={formApi => (formApiRef.current = formApi)}
autoScrollToError
allowEmpty
>
{schemaAreaInfo?.schema_list?.map(item => (
<ConnectorField
initValue={initValue}
formItemSchema={item}
isReadOnly={isReadOnly}
key={item.name}
/>
))}
</Form>
) : null}
</div>
);
},
);

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ReactMarkdown from 'react-markdown';
import { Space, Typography } from '@coze-arch/bot-semi';
import { type CopyLinkAreaInfo } from '@coze-arch/bot-api/developer_api';
import { type TFormData } from '../types';
import styles from './index.module.less';
export const ConnectorLink = ({
copyLinkAreaInfo = {},
agentType = 'bot',
botId = '',
initValue = {},
}: {
copyLinkAreaInfo?: CopyLinkAreaInfo;
agentType?: 'bot' | 'project';
botId: string;
initValue?: TFormData;
}) => {
//支持通配URL
const formatUrl = (url?: string) => {
let newUrl = url ?? '';
if (newUrl) {
if (agentType === 'project') {
newUrl = newUrl.replace(/{project_id}/g, botId);
} else {
newUrl = newUrl.replace(/{bot_id}/g, botId);
}
newUrl = newUrl
.replace(/{hostname}/g, window.location.hostname)
.replace(/{corp_id}/g, initValue.corp_id);
}
return newUrl;
};
return (
<div className={styles['link-area']}>
{copyLinkAreaInfo?.title_text ? (
<Space spacing={12} align="start">
<span className={styles['step-order']}>
{copyLinkAreaInfo.step_order || 1}
</span>
<div className={styles['step-content']}>
<div className={styles['step-title']}>
{copyLinkAreaInfo.title_text}
</div>
</div>
</Space>
) : null}
{copyLinkAreaInfo?.description ? (
<ReactMarkdown skipHtml={true} className={styles.markdown}>
{copyLinkAreaInfo.description}
</ReactMarkdown>
) : null}
{copyLinkAreaInfo?.link_list?.length ? (
<div className={styles['link-list']}>
{copyLinkAreaInfo?.link_list.map(item => (
<div key={item.link} style={{ marginBottom: 32 }}>
<Typography.Title className={styles.title}>
{item.title}
</Typography.Title>
<Typography.Text className={styles.link} copyable>
{formatUrl(item.link)}
</Typography.Text>
</div>
))}
</div>
) : null}
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useHotkeys } from 'react-hotkeys-hook';
import { Suspense, lazy, useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { userStoreService } from '@coze-studio/user-store';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { setPCBody } from '@coze-arch/bot-utils';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { Spin } from '@coze-arch/bot-semi';
import { setPCBodyWithDebugPanel } from '../../util';
import { useDebugStore } from '../../store/debug-panel';
import s from './index.module.less';
const DebugPanel = lazy(() => import('@coze-devops/debug-panel'));
export const BotDebugPanel = () => {
const {
isDebugPanelShow,
currentDebugQueryId,
setIsDebugPanelShow,
setCurrentDebugQueryId,
} = useDebugStore();
const { botId } = useBotInfoStore(
useShallow(state => ({
botId: state.botId,
})),
);
const userID = userStoreService.useUserInfo()?.user_id_str ?? '';
const { id: spaceID } = useSpaceStore(state => state.space);
useHotkeys('ctrl+k, meta+k', () => {
if (!isDebugPanelShow) {
sendTeaEvent(EVENT_NAMES.open_debug_panel, {
path: 'shortcut_debug',
});
}
setCurrentDebugQueryId('');
setIsDebugPanelShow(!isDebugPanelShow);
});
useEffect(() => {
if (isDebugPanelShow) {
setPCBodyWithDebugPanel();
window.scrollTo(document.body.scrollWidth, 0);
} else {
setPCBody();
}
return () => {
setPCBody();
};
}, [isDebugPanelShow]);
useEffect(
() => () => {
setCurrentDebugQueryId('');
},
[],
);
return isDebugPanelShow ? (
<div className={s.container}>
<Suspense
fallback={
<div className={s['debug-panel-lazy-loading']}>
<Spin />
</div>
}
>
<DebugPanel
isShow={isDebugPanelShow}
botId={botId}
userID={userID}
spaceID={spaceID}
placement="left"
currentQueryLogId={currentDebugQueryId}
onClose={() => {
setIsDebugPanelShow(false);
setCurrentDebugQueryId('');
}}
/>
</Suspense>
</div>
) : null;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react';
import { size } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { useSpaceList } from '@coze-arch/bot-studio-store';
import { type BotSpace, SpaceType } from '@coze-arch/bot-api/developer_api';
import { SelectorItem } from '../selector-item';
export function useSelectSpacePane() {
const { spaces } = useSpaceList();
const [targetSpace, setTargetSpace] = useState<BotSpace | null>(null);
const personalSpace = spaces.find(
item => item.space_type === SpaceType.Personal,
);
const teamSpaces = spaces.filter(item => item.space_type === SpaceType.Team);
const selectSpacePane = (
<div className="flex flex-col">
<div className="w-full border-[0.5px] border-solid coz-stroke-primary mb-[12px]"></div>
<div className="flex flex-col max-h-[406px] overflow-y-auto">
<div className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top w-full mb-[6px]">
{I18n.t('menu_title_personal_space')}
</div>
<div>
<div className="flex flex-col rounded-[6px] overflow-hidden mb-[16px]">
<SelectorItem space={personalSpace} disabled />
</div>
</div>
<div className="text-[12px] leading-[16px] font-[500] coz-fg-primary text-left align-top w-full mb-[6px]">
{I18n.t('resource_move_target_team')}
</div>
<div>
<div className="flex flex-col rounded-[6px] overflow-hidden">
{size(teamSpaces) > 0 ? (
spaces
.filter(item => item.space_type !== SpaceType.Personal)
.map(item => (
<SelectorItem
key={item.id}
space={item}
selected={item.id === targetSpace?.id}
onSelect={space => {
setTargetSpace(space);
}}
/>
))
) : (
<SelectorItem
space={{
// MOCK: 用于展示未加入任何空间的兜底情况
name: I18n.t('resource_move_no_team_joined'),
}}
disabled
/>
)}
</div>
</div>
</div>
</div>
);
return {
targetSpace,
setTargetSpace,
selectSpacePane,
};
}

View File

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

View File

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

View File

@@ -0,0 +1,361 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function -- 不好拆 */
import React, { useCallback, useState } from 'react';
import { size } from 'lodash-es';
import classNames from 'classnames';
import { useBoolean, useRequest } from 'ahooks';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { IconCozCross } from '@coze-arch/coze-design/icons';
import { Button, Modal, Toast } from '@coze-arch/coze-design';
import { useSpaceList } from '@coze-arch/bot-studio-store';
import { MoveAction } from '@coze-arch/bot-api/playground_api';
import {
DraftBotStatus,
type DraftBot,
SpaceType,
} from '@coze-arch/bot-api/developer_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { ItemGridView } from './components/item-grid-view';
interface BotMoveFailedModalOptions {
/**
* botInfo
*/
botInfo: Pick<DraftBot, 'id' | 'name'> | null;
/**
* 更新 bot 状态
*/
onUpdateBotStatus?: (status: DraftBotStatus) => void;
/**
* 迁移成功等效于删除 bot
*/
onMoveSuccess?: () => void;
}
interface UseBotMoveFailedModalValue {
/**
* 打开弹窗的方法
* @param {BotMoveModalOptions} [options] - 此次打开Modal的配置项
*/
open: (options?: BotMoveFailedModalOptions) => void;
/**
* 关闭弹窗的方法
*/
close: () => void;
/**
* 弹窗组件实例,需要手动挂载一下
*/
modalNode: React.ReactNode;
}
const DefaultOptions: BotMoveFailedModalOptions = { botInfo: null };
// eslint-disable-next-line complexity
export function useBotMoveFailedModal(): UseBotMoveFailedModalValue {
const [options, setOptions] =
useState<BotMoveFailedModalOptions>(DefaultOptions);
const [visible, { setTrue: setVisibleTrue, setFalse: setVisibleFalse }] =
useBoolean(false);
const [paneType, setPaneType] = useState<
'detail' | 'confirm_cancel' | 'confirm_force'
>('detail');
const title = (
<span className="mb-[20px] coz-fg-plus text-[16px] font-medium leading-[22px]">
{paneType === 'detail'
? I18n.t('move_failed')
: paneType === 'confirm_cancel'
? I18n.t('move_failed_cancel_confirm_title')
: paneType === 'confirm_force'
? I18n.t('move_failed_force_confirm_title')
: ''}
</span>
);
const open = useCallback((opts?: BotMoveFailedModalOptions) => {
setOptions(opts || DefaultOptions);
setPaneType('detail');
setVisibleTrue();
}, []);
const close = useCallback(() => {
setVisibleFalse();
}, []);
const { spaces } = useSpaceList();
const fromSpaceID =
spaces?.find(s => s.space_type === SpaceType.Personal)?.id ?? '';
const { data: moveDetails } = useRequest(
async () => {
if (!options.botInfo) {
return;
}
const data = await PlaygroundApi.MoveDraftBot({
bot_id: options.botInfo.id,
from_spaceId: fromSpaceID,
move_action: MoveAction.ViewTask,
});
return data.async_task;
},
{ refreshDeps: [options.botInfo] },
);
const { loading, run } = useRequest(
async (moveAction: MoveAction) => {
const data = await PlaygroundApi.MoveDraftBot({
bot_id: options.botInfo.id,
from_spaceId: fromSpaceID,
move_action: moveAction,
});
return { ...data, moveAction };
},
{
manual: true,
onSuccess: data => {
if (data.bot_status === DraftBotStatus.Using) {
if (data.moveAction === MoveAction.CancelTask) {
options.onUpdateBotStatus?.(data.bot_status);
} else {
Toast.success(I18n.t('resource_move_bot_success_toast'));
options.onMoveSuccess?.();
}
close();
} else if (data.bot_status === DraftBotStatus.MoveFail) {
options.onUpdateBotStatus?.(data.bot_status);
Toast.error({
content: withSlardarIdButton(I18n.t('move_failed_toast')),
});
close();
}
},
onError: error => {
Toast.error({
content: withSlardarIdButton(
error?.message || I18n.t('move_failed_toast'),
),
showClose: false,
});
close();
},
},
);
const retry = async () => {
await run(MoveAction.RetryMove);
};
const forceMove = async () => {
await run(MoveAction.ForcedMove);
};
const cancelMove = async () => {
await run(MoveAction.CancelTask);
};
const footer = (
<div
className={classNames(
'coz-modal-footer flex gap-2 justify-end',
'w-full',
)}
>
{paneType === 'detail' ? (
<>
<Button
className="flex-1 !ml-0"
size="large"
color="primary"
disabled={!moveDetails || loading}
onClick={() => {
setPaneType('confirm_cancel');
}}
>
{I18n.t('move_failed_btn_cancel')}
</Button>
<Button
className="flex-1 !ml-0"
size="large"
color="primary"
disabled={!moveDetails || loading}
onClick={() => {
setPaneType('confirm_force');
}}
>
{I18n.t('move_failed_btn_force')}
</Button>
<Button
className="flex-1 !ml-0"
color="brand"
size="large"
loading={loading}
disabled={!moveDetails || loading}
onClick={() => {
retry();
}}
>
{I18n.t('Retry')}
</Button>
</>
) : null}
{paneType === 'confirm_cancel' ? (
<>
<Button
className="!ml-0"
color="primary"
onClick={() => {
setPaneType('detail');
}}
>
{I18n.t('back')}
</Button>
<Button
className="!ml-0"
color="brand"
loading={loading}
onClick={() => {
cancelMove();
}}
>
{I18n.t('confirm')}
</Button>
</>
) : null}
{paneType === 'confirm_force' ? (
<>
<Button
className="!ml-0"
color="primary"
onClick={() => {
setPaneType('detail');
}}
>
{I18n.t('back')}
</Button>
<Button
className="!ml-0"
color="brand"
loading={loading}
onClick={() => {
forceMove();
}}
>
{I18n.t('confirm')}
</Button>
</>
) : null}
</div>
);
const modalNode = (
<Modal
visible={visible}
title={title}
footer={footer}
width={paneType !== 'detail' ? '448px' : '480px'}
footerFill
onCancel={close}
closable={!['confirm_cancel', 'confirm_force'].includes(paneType)}
maskClosable={false}
keepDOM={false}
closeIcon={<IconCozCross className="coz-fg-secondary" />}
>
{paneType === 'detail' ? (
<div className="flex flex-col">
<div className="w-full border-[0.5px] border-solid coz-stroke-primary mb-[12px]"></div>
<div className="flex flex-col max-h-[406px] overflow-y-auto">
<div className="flex items-center gap-x-[8px] p-[8px] w-full coz-mg-primary rounded-[6px] mb-[12px]">
<p className="text-[12px] leading-[16px] font-[400] coz-fg-secondary text-left align-top grow">
{I18n.t('move_failed_desc')}
</p>
</div>
{size(moveDetails?.transfer_resource_plugin_list) > 0 ? (
<ItemGridView
title={I18n.t('store_search_recommend_result2')}
resources={moveDetails?.transfer_resource_plugin_list.map(
item => ({
...item,
spaceID: item.status
? moveDetails?.task_info.TargetSpaceId
: moveDetails?.task_info.OriSpaceId,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(`/space/${spaceID}/plugin/${id}`);
}}
showStatus
/>
) : null}
{size(moveDetails?.transfer_resource_workflow_list) > 0 ? (
<ItemGridView
title={I18n.t('store_search_recommend_result3')}
resources={moveDetails?.transfer_resource_workflow_list.map(
item => ({
...item,
spaceID: item.status
? moveDetails?.task_info.TargetSpaceId
: moveDetails?.task_info.OriSpaceId,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(
`/work_flow?space_id=${spaceID}&workflow_id=${id}`,
);
}}
showStatus
/>
) : null}
{size(moveDetails?.transfer_resource_knowledge_list) > 0 ? (
<ItemGridView
title={I18n.t('performance_knowledge')}
resources={moveDetails?.transfer_resource_knowledge_list.map(
item => ({
...item,
spaceID: item.status
? moveDetails?.task_info.TargetSpaceId
: moveDetails?.task_info.OriSpaceId,
}),
)}
onResourceClick={(id, spaceID) => {
window.open(`/space/${spaceID}/knowledge/${id}`);
}}
showStatus
/>
) : null}
</div>
</div>
) : null}
{paneType === 'confirm_force' ? (
<div className="mt-[20px]">
{I18n.t('move_failed_force_confirm_content')}
</div>
) : null}
{paneType === 'confirm_cancel' ? (
<div className="mt-[20px]">
{I18n.t('move_failed_cancel_confirm_content')}
</div>
) : null}
</Modal>
);
return {
modalNode,
open,
close,
};
}

View File

@@ -0,0 +1,293 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function -- 难拆*/
import React, { useCallback, useState } from 'react';
import classNames from 'classnames';
import { useBoolean, useRequest } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import { useSpaceList } from '@coze-arch/bot-studio-store';
import { MoveAction } from '@coze-arch/bot-api/playground_api';
import {
DraftBotStatus,
type DraftBot,
SpaceType,
} from '@coze-arch/bot-api/developer_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { cozeMitt } from '@coze-common/coze-mitt';
import { IconInfo } from '@coze-arch/bot-icons';
import { IconCozCross } from '@coze-arch/coze-design/icons';
import {
Button,
IconButton,
Modal,
Toast,
Tooltip,
Typography,
} from '@coze-arch/coze-design';
import { useSelectSpacePane } from './components/select-space-pane';
import { MoveDetailPane } from './components/move-detail-pane';
interface BotMoveModalOptions {
/**
* botInfo
*/
botInfo: Pick<DraftBot, 'name' | 'id'> | null;
/**
* 更新 bot 状态
*/
onUpdateBotStatus?: (status: DraftBotStatus) => void;
/**
* 迁移成功等效于删除 bot
*/
onMoveSuccess?: () => void;
/**
* 关闭 modal
*/
onClose?: () => void;
}
interface UseBotMoveModalValue {
/**
* 打开弹窗的方法
* @param {BotMoveModalOptions} [options] - 此次打开Modal的配置项
*/
open: (options?: BotMoveModalOptions) => void;
/**
* 关闭弹窗的方法
*/
close: () => void;
/**
* 弹窗组件实例,需要手动挂载一下
*/
modalNode: React.ReactNode;
}
const DefaultOptions: BotMoveModalOptions = { botInfo: null };
export function useBotMoveModal(): UseBotMoveModalValue {
const [options, setOptions] = useState<BotMoveModalOptions>(DefaultOptions);
const [visible, { setTrue: setVisibleTrue, setFalse: setVisibleFalse }] =
useBoolean(false);
const [paneType, setPaneType] = useState<'select' | 'move' | 'confirm'>(
'select',
);
const { targetSpace, selectSpacePane, setTargetSpace } = useSelectSpacePane();
const title =
paneType !== 'confirm' ? (
<div className="flex justify-start items-center mb-[24px] w-[380px]">
<div className="coz-fg-plus text-[16px] font-medium leading-[22px] max-w-full">
<Typography.Text
ellipsis={{
showTooltip: true,
}}
className="text-[16px]"
>
{I18n.t('resource_move_title', {
bot_name: options.botInfo?.name ?? '',
})}
</Typography.Text>
</div>
<Tooltip content={I18n.t('resource_move_notice')}>
<IconButton
size="small"
color="secondary"
icon={<IconInfo className="coz-fg-secondary" />}
/>
</Tooltip>
</div>
) : (
I18n.t('resource_move_confirm_title')
);
const open = useCallback((opts?: BotMoveModalOptions) => {
setOptions(opts || DefaultOptions);
setPaneType('select');
setTargetSpace(null);
setVisibleTrue();
}, []);
const close = useCallback(() => {
setVisibleFalse();
}, []);
const { spaces } = useSpaceList();
const fromSpaceID = spaces.find(s => s.space_type === SpaceType.Personal)?.id;
const { loading: moveLoading, run: moveBot } = useRequest(
async () => {
const data = await PlaygroundApi.MoveDraftBot({
bot_id: options.botInfo.id,
target_spaceId: targetSpace.id,
from_spaceId: fromSpaceID,
move_action: MoveAction.Move,
});
return data;
},
{
manual: true,
onSuccess: data => {
if (data.bot_status === DraftBotStatus.Using) {
Toast.success(I18n.t('resource_move_bot_success_toast'));
options.onMoveSuccess?.();
close();
cozeMitt.emit('refreshFavList', {
numDelta: -1,
});
} else if (data.bot_status === DraftBotStatus.MoveFail) {
options.onUpdateBotStatus?.(data.bot_status);
Toast.error({
content: withSlardarIdButton(I18n.t('move_failed_toast')),
});
close();
}
},
onError: error => {
Toast.error({
content: withSlardarIdButton(
error?.message || I18n.t('move_failed_toast'),
),
showClose: false,
});
close();
},
},
);
const onConfirm = async () => {
await moveBot();
};
const [moveDisabled, setMoveDisabled] = useState(true);
const footer = (
<div
className={classNames(
'coz-modal-footer flex gap-2 justify-end',
paneType !== 'confirm' && 'w-full',
)}
>
{paneType === 'select' ? (
<Button
className="flex-1 !ml-0"
color="brand"
size="large"
disabled={!targetSpace}
onClick={() => {
setPaneType('move');
}}
>
{I18n.t('next')}
</Button>
) : null}
{paneType === 'move' ? (
<>
<Button
className="flex-1 !ml-0"
size="large"
color="primary"
onClick={() => {
setPaneType('select');
}}
>
{I18n.t('back')}
</Button>
<Button
className="flex-1 !ml-0"
color="brand"
size="large"
disabled={moveDisabled}
onClick={() => {
setPaneType('confirm');
}}
>
{I18n.t('resource_move')}
</Button>
</>
) : null}
{paneType === 'confirm' ? (
<>
<Button
className="!ml-0"
color="primary"
onClick={() => {
setPaneType('move');
}}
>
{I18n.t('back')}
</Button>
<Button
className="!ml-0"
color="brand"
loading={moveLoading}
onClick={() => {
onConfirm();
}}
>
{I18n.t('confirm')}
</Button>
</>
) : null}
</div>
);
const modalNode = (
<Modal
visible={visible}
title={title}
footer={footer}
width={paneType === 'confirm' ? '448px' : '480px'}
footerFill
onCancel={() => {
close?.();
options.onClose?.();
}}
closable={paneType !== 'confirm'}
maskClosable={false}
keepDOM={false}
closeIcon={<IconCozCross className="coz-fg-secondary" />}
>
{paneType === 'select' ? selectSpacePane : null}
{paneType === 'move' ? (
<>
<MoveDetailPane
targetSpace={targetSpace}
botID={options.botInfo.id}
fromSpaceID={fromSpaceID}
onUnmount={() => setMoveDisabled(true)}
onDetailLoaded={() => setMoveDisabled(false)}
/>
{IS_CN_REGION ? (
<div className="coz-fg-hglt-red">{I18n.t('move_desc1')}</div>
) : null}
</>
) : null}
{paneType === 'confirm' ? (
<div className="mt-[20px]">
{I18n.t('resource_move_confirm_content')}
</div>
) : null}
</Modal>
);
return {
modalNode,
open,
close,
};
}

View File

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

View File

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

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {
type CSSProperties,
useEffect,
useMemo,
useRef,
forwardRef,
type ForwardedRef,
useImperativeHandle,
} from 'react';
import { nanoid } from 'nanoid';
import classNames from 'classnames';
import { useBoolean } from 'ahooks';
import { type TextAreaProps } from '@coze-arch/bot-semi/Input';
import { TextArea } from '@coze-arch/bot-semi';
import styles from './index.module.less';
interface CommonTextareaType {
textAreaClassName?: string;
textAreaProps?: Partial<TextAreaProps>;
// 一种特殊的针对placeholder处理方式::placeholder达不到预期
emptyClassName?: string;
}
interface ChatflowCustomTextareaProps extends TextAreaProps {
value: string;
onChange: (
value: string,
e: React.MouseEvent<HTMLTextAreaElement, MouseEvent>,
) => void;
/** 展示模式(即需要省略时)的配置 */
ellipse?: {
rows?: number;
} & CommonTextareaType;
/** 编辑模式(即需要自动适应)的配置 */
autoSize?: {
maxHeight?: number;
} & CommonTextareaType;
readonly?: boolean;
className?: string;
style?: React.CSSProperties;
}
export const CollapsibleTextarea = forwardRef(
(
{
value,
onChange,
onBlur,
ellipse = { rows: 4 },
autoSize = { maxHeight: 340 },
readonly,
className,
style,
maxCount,
maxLength,
onFocus,
...restCommonTextAreaProps
}: ChatflowCustomTextareaProps,
ref: ForwardedRef<HTMLTextAreaElement>,
) => {
const textAreaId = useMemo(() => nanoid(), []);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const [focused, { setTrue: setFocusedTrue, setFalse: setFocusedFalse }] =
useBoolean(false);
useImperativeHandle(ref, () => ({
...(textAreaRef.current as HTMLTextAreaElement),
focus: () => setFocusedTrue(),
}));
useEffect(() => {
if (focused) {
// 加timeout可以实现focus的时候滚动到最底并光标在最后
setTimeout(() => {
if (textAreaRef.current) {
// 默认光标在最后
textAreaRef.current.setSelectionRange(
Number.MAX_SAFE_INTEGER,
Number.MAX_SAFE_INTEGER,
);
textAreaRef.current.focus();
textAreaRef.current.scroll({ top: textAreaRef.current.scrollTop });
}
});
}
}, [focused]);
const renderTextArea = () => {
if (focused) {
return (
<TextArea
autosize
// key是保证readonly变化后重新渲染
key="not-readonly"
style={
autoSize?.maxHeight
? // 这里的 style 会应用到 wrapper 上,不限定高度时会意外出现滚动条,只能通过变量修改 textarea 的 overflow
// 此外max-height 会导致预期外的 blur 事件,也只能通过 css 变量将 max-height 动态传给 textarea
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- 传递 css 变量
({
'--chatflow-custom-textarea-overflow-y': 'auto',
'--chatflow-custom-textarea-focused-max-height': `${autoSize.maxHeight}px`,
} as CSSProperties)
: undefined
}
id={textAreaId}
ref={textAreaRef}
value={value}
onBlur={e => {
setFocusedFalse();
onBlur?.(e);
}}
onChange={onChange}
readonly={readonly}
className={classNames(
styles['auto-size'],
autoSize?.textAreaClassName,
{ [autoSize?.emptyClassName || '']: !value },
)}
maxCount={maxCount}
maxLength={maxLength}
{...restCommonTextAreaProps}
{...autoSize?.textAreaProps}
/>
);
}
return (
<TextArea
// key是保证readonly变化后重新渲染
key="readonly"
style={{ WebkitLineClamp: ellipse?.rows }}
value={value}
rows={ellipse?.rows}
onFocus={e => {
onFocus?.(e);
setFocusedTrue();
}}
className={classNames(styles.ellipse, ellipse?.textAreaClassName, {
[ellipse?.emptyClassName || '']: !value,
})}
{...restCommonTextAreaProps}
{...ellipse?.textAreaProps}
/>
);
};
return (
<div className={classNames(styles.container, className)} style={style}>
{renderTextArea()}
</div>
);
},
);

View File

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

View File

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

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type SetStateAction } from 'react';
import { type PublishConnectorInfo } from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { UIButton } from '@coze-arch/bot-semi';
import {
type PublishConnectorInfo as BotPublishConnectorInfo,
ConfigStatus,
} from '@coze-arch/bot-api/developer_api';
import { useParams } from 'react-router-dom';
import { useConnectorFormModal } from '../bind-connector-modal/use-connector-form-modal';
import { OLD_WX_FWH_ID } from '../../util';
interface KvBindButtonProps {
setDataSource?: (value: SetStateAction<BotPublishConnectorInfo[]>) => void;
setSelectedPlatforms?: (id: SetStateAction<string[]>) => void;
record: BotPublishConnectorInfo | PublishConnectorInfo;
/** 渠道配置成功的回调。若不传入 `unbindCallback`,解绑渠道也会调用该回调,且 bind_id 为空字符串 `''` */
bindSuccessCallback?: (value: PublishConnectorInfo | undefined) => void;
/** 解绑渠道的回调 */
unbindCallback?: () => void;
/** 绑定的 agent_type 。默认为 bot */
origin?: 'project' | 'bot';
/** 绑定的 bot_id/project_id 。不传则根据 origin 从路由参数中获取 */
originId?: string;
}
export const KvBindButton = ({
setDataSource,
setSelectedPlatforms,
record,
bindSuccessCallback,
unbindCallback,
origin = 'bot',
originId,
}: KvBindButtonProps) => {
const { bot_id = '', project_id = '' } = useParams<DynamicParams>();
// 传给后端的参数名字是 bot_id另外使用参数 agent_type 来区分 0-bot 1-project
const botId = originId ?? (origin === 'bot' ? bot_id : project_id);
const bindSuccessCb = (
value: BotPublishConnectorInfo | PublishConnectorInfo | undefined,
) => {
if (bindSuccessCallback) {
bindSuccessCallback(value as PublishConnectorInfo);
return;
}
setDataSource?.((list: BotPublishConnectorInfo[]) => {
const target = list.find(l => l.id === value?.id);
if (target) {
// 解绑旧的服务号后,需要隐藏掉旧的服务号渠道,不允许再绑定
if (target.id === OLD_WX_FWH_ID && !value?.bind_id) {
return list.filter(item => item.id !== OLD_WX_FWH_ID);
}
target.bind_id = value?.bind_id;
target.bind_info = value?.bind_info ?? {};
target.config_status = value?.bind_id
? ConfigStatus.Configured
: ConfigStatus.NotConfigured;
}
return [...list];
});
if (!value?.bind_id) {
setSelectedPlatforms?.(list => list.filter(item => item !== value?.id));
}
};
const { node: connectorFormModal, open: openConnectorsForm } =
useConnectorFormModal({
botId,
origin,
onSuccess: bindSuccessCb,
onUnbind: unbindCallback,
});
const handleConfigure = () => openConnectorsForm({ initValue: record });
const buttonText = I18n.t('bot_publish_action_configure');
return (
<>
{origin === 'project' ? (
<Button onClick={handleConfigure} size="small" color="primary">
{buttonText}
</Button>
) : (
<UIButton onClick={handleConfigure} theme="borderless">
{buttonText}
</UIButton>
)}
{connectorFormModal}
</>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from 'classnames';
import s from '../../index.module.less';
import { type ISysConfigItem } from '../../hooks';
export const VariableTree = (props: {
isReadonly?: boolean;
highLight?: boolean;
activeId?: string;
configList: ISysConfigItem[];
}) => {
const { isReadonly, highLight, activeId, configList } = props;
return (
<tbody className="overflow-visible flex-1 h-0">
{configList.map((item: ISysConfigItem, index: number) => (
<tr
key={`memory-row-list_${index}`}
className={classNames(
s['memory-row'],
activeId === item.id && highLight && s['active-row'],
activeId === item.id && highLight && 'active-row',
'flex gap-x-4 flex-nowrap',
)}
>
{item.key ? <td>{item.key}</td> : null}
{item.description ? <td>{item.description}</td> : null}
{item.default_value ? <td>{item.default_value}</td> : null}
{item.channel ? <td>{item.channel}</td> : null}
{item.method && !isReadonly ? <td>{item.method}</td> : null}
</tr>
))}
</tbody>
);
};

View File

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

View File

@@ -0,0 +1,369 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import React, { useEffect, useMemo, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { nanoid } from 'nanoid';
import classNames from 'classnames';
import { useRequest } from 'ahooks';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { type VariableItem } from '@coze-studio/bot-detail-store';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import {
Checkbox,
Space,
Switch,
Tooltip,
Typography,
} from '@coze-arch/coze-design';
import { IconInfo } from '@coze-arch/bot-icons';
import { type GetSysVariableConfResponse } from '@coze-arch/bot-api/memory';
import { MemoryApi } from '@coze-arch/bot-api';
import { BotE2e } from '@coze-data/e2e';
const { Text } = Typography;
import s from './index.module.less';
export type TVariable = VariableItem & {
enable?: boolean;
must_not_use_in_prompt?: string; // 服务端类型已上线无法改boolean。""、"false"、"true"
ext_desc?: string;
prompt_disabled?: boolean;
// eslint-disable-next-line @typescript-eslint/naming-convention
EffectiveChannelList?: string[];
};
export interface ISysConfigItem {
id: string;
key: React.ReactNode;
default_value: React.ReactNode;
description: React.ReactNode;
channel?: React.ReactNode;
method?: React.ReactNode;
}
export interface ISysConfigItemGroup {
id: string;
key: React.ReactNode;
default_value: React.ReactNode;
description: React.ReactNode;
method?: React.ReactNode;
channel?: React.ReactNode;
var_info_list?: ISysConfigItem[];
}
export interface SystemConfig {
sysConfigList: ISysConfigItemGroup[];
sysVariables: TVariable[];
enableVariables: VariableItem[];
loading: boolean;
}
export interface SysConfigData {
conf: TVariable[];
groupConf: GetSysVariableConfResponse['group_conf'];
}
export const useSystemVariables = (
variables: VariableItem[],
visible: boolean,
): SystemConfig => {
const { run, loading } = useRequest(async () => {
const res = await MemoryApi.GetSysVariableConf();
const resData = res?.group_conf?.reduce(
(prev, cur) => {
cur.group_name
? prev.group_conf.push(cur)
: (prev.conf = prev.conf?.concat(cur.var_info_list || []));
return prev;
},
{
conf: [],
group_conf: [],
},
);
// 分组新逻辑
const configInfo = initSysVarStatus(resData);
setConfig(configInfo);
});
const { variables: values } = useBotSkillStore(
useShallow(state => ({
variables: state.variables,
})),
);
const [sysConfig, setConfig] = useState<SysConfigData>({
conf: [],
groupConf: [],
});
// 这里需要根据config来设置sysVariables
const sysVariables = useMemo(() => {
const group = sysConfig.groupConf?.reduce(
(prev, cur) => prev.concat(cur?.var_info_list),
[],
);
return [...sysConfig.conf, ...group];
}, [sysConfig]);
// 拼接已启用的系统变量和自定义变量
const enableVariables = useMemo(() => {
const enableSysVariables =
sysVariables
.filter(v => v.enable)
?.map(sys => ({ ...sys, is_system: true })) || [];
const customVariables =
variables.filter(variable => !variable.is_system) || [];
return [...enableSysVariables, ...customVariables];
}, [variables, sysVariables]);
const initSysVarStatus = data => {
const { conf = [], group_conf = [] } = data || {};
const setItem = varItem => {
const enableItem: TVariable | undefined = values?.find(
item => item.key === varItem.key && item.is_system,
);
return {
...varItem,
is_system: enableItem?.is_system,
enable: !!enableItem,
prompt_disabled: enableItem?.prompt_disabled ?? true,
};
};
const confLIst = conf?.map(setItem);
const groupConfList = group_conf?.map(group => ({
...group,
var_info_list: group.var_info_list?.map(groupItem => ({
...setItem(groupItem),
prompt_disabled: true,
channel: groupItem?.EffectiveChannelList?.join(','),
})),
}));
return {
conf: confLIst || [],
groupConf: groupConfList || [],
};
};
useEffect(() => {
if (visible) {
run();
}
}, [visible]);
const setSysConfigStatus = (key, prop, checked) => {
const { conf = [], groupConf = [] } = sysConfig;
const configIndex = conf.findIndex(confItem => confItem.key === key);
if (configIndex !== -1) {
conf[configIndex][prop] = checked;
if (prop === 'enable') {
conf[configIndex].prompt_disabled = !checked;
}
}
groupConf.forEach(groupItem => {
const index = groupItem?.var_info_list.findIndex(
item => item.key === key,
);
if (index !== -1) {
groupItem.var_info_list[index][prop] = checked;
}
setConfig({ conf, groupConf });
});
};
const changeEnable = (checked: boolean, key: string) => {
setSysConfigStatus(key, 'enable', checked);
};
const changeCheckbox = (checked: boolean, key: string) => {
setSysConfigStatus(key, 'prompt_disabled', checked);
};
const SysVarConfigRender = ({
value,
enable,
e2e,
extDesc,
className,
}: {
value: string;
enable: boolean | undefined;
e2e?: string;
extDesc?: string;
className?: string;
}): JSX.Element => (
<div
className={classNames(
[s.sys_item_box, !enable && s.disabled, className],
'flex items-center',
)}
data-dtestid={e2e}
>
<Text ellipsis={{ showTooltip: true }}>{value}</Text>
{!!extDesc && (
<Tooltip content={I18n.t(extDesc as I18nKeysNoOptionsType)}>
<IconInfo
style={{
color: '#C6CACD',
marginLeft: 4,
}}
/>
</Tooltip>
)}
</div>
);
const SysVarGroupConfigRender = ({
value,
e2e,
enable = true,
extDesc,
className,
}: {
value: string;
e2e?: string;
enable?: boolean;
extDesc?: string;
className?: string;
}): JSX.Element => (
<div
className={classNames([
s.sys_item_group,
!enable && s.disabled,
className,
])}
data-dtestid={e2e}
>
<div>{value}</div>
{!!extDesc && (
<Tooltip content={I18n.t(extDesc as I18nKeysNoOptionsType)}>
<IconInfo
style={{
color: '#C6CACD',
marginLeft: 4,
}}
/>
</Tooltip>
)}
</div>
);
const configItem = (
item: TVariable,
promptDisabled = false,
): ISysConfigItem => ({
id: item.key,
key: SysVarConfigRender({
value: item.key ?? '',
enable: item.enable,
e2e: `${BotE2e.BotVariableAddModalNameText}.${item.key}`,
extDesc: item.ext_desc,
className: 'w-[140px] flex-none basis-[140px]',
}),
description: SysVarConfigRender({
value: item.description ?? '',
enable: item.enable,
e2e: `${BotE2e.BotVariableAddModalDescText}.${item.key}`,
className: 'w-[128px] flex-none basis-[128px]',
}),
default_value: SysVarConfigRender({
value: item.default_value || '--',
enable: item.enable,
e2e: `${BotE2e.BotVariableAddModalDefaultValueText}.${item.key}`,
className: 'w-[128px] flex-none basis-[128px]',
}),
channel: SysVarConfigRender({
value: item.channel || '--',
enable: item.enable,
className: 'w-[128px] flex-none basis-[128px]',
}),
method: (
<Space className={s['memory-method']} spacing={24}>
<Tooltip content={I18n.t('variable_240520_03')} theme="dark">
<div className={s['memory-method-checkbox']}>
<Checkbox
disabled={
promptDisabled ||
!item.enable ||
item.must_not_use_in_prompt === 'true'
}
checked={item?.prompt_disabled ? false : true}
onChange={v => {
changeCheckbox(!v.target.checked, item.key);
}}
></Checkbox>
</div>
</Tooltip>
<Tooltip
showArrow
position="top"
theme="dark"
zIndex={1031}
style={{
backgroundColor: '#41464c',
color: '#fff',
maxWidth: '276px',
}}
content={I18n.t('variable_240407_01')}
>
<Switch
data-dtestid={`${BotE2e.BotVariableAddModalSwitch}.${item.key}`}
size="small"
checked={item?.enable ?? false}
onChange={checked => changeEnable(checked, item.key)}
/>
</Tooltip>
</Space>
),
});
const groupList: ISysConfigItemGroup[] = sysConfig?.groupConf?.map(item => ({
id: nanoid(),
key: SysVarGroupConfigRender({
value: item.group_name ?? '--',
e2e: `${BotE2e.BotVariableAddModalNameText}.${item.group_name}`,
extDesc: item?.group_ext_desc,
className: 'w-[140px] flex-none basis-[140px]',
}),
description: SysVarConfigRender({
value: item.group_desc || '--',
enable: true,
e2e: `${BotE2e.BotVariableAddModalDescText}.${item.group_name}`,
className: 'w-[128px] flex-none basis-[128px]',
}),
default_value: SysVarConfigRender({
value: '--',
enable: true,
e2e: `${BotE2e.BotVariableAddModalDefaultValueText}.${item.group_name}`,
className: 'w-[128px] flex-none basis-[128px]',
}),
channel: SysVarConfigRender({
value: '--',
enable: true,
className: 'w-[128px] flex-none basis-[128px]',
}),
var_info_list: item?.var_info_list?.length
? item?.var_info_list.map(childItem => configItem(childItem, true))
: undefined,
}));
// 系统变量
const sysConfigList: ISysConfigItem[] = sysConfig?.conf?.map(item =>
configItem(item),
);
return {
sysConfigList: [...sysConfigList, ...groupList],
sysVariables,
enableVariables,
loading,
};
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ComponentProps } from 'react';
import { type VariableItem } from '@coze-studio/bot-detail-store';
import { UIModal, Image, type Modal } from '@coze-arch/bot-semi';
import { Button, Popconfirm } from '@coze-arch/bot-semi';
import { I18n } from '@coze-arch/i18n';
import { IconAlertCircle } from '@douyinfe/semi-icons';
import { BotDebugButton } from '../bot-debug-button';
import IMG_TEMPLATE_USE_I18N from '../../assets/image/template_i18n.png';
import templateSample from '../../assets/image/sample3_i18n.png';
import s from './index.module.less';
export type MemoryTemplateModalProps = ComponentProps<typeof Modal> & {
addTemplate?: (arr: VariableItem[]) => void;
needSecondConfirm?: boolean;
showType?: 'variableList';
};
const list: VariableItem[] = [
{
key: 'Name',
description: I18n.t('profile_memory_sample_description_name'),
},
{
key: 'Address',
description: I18n.t('profile_memory_sample_description_address'),
},
{
key: 'PhoneNumber',
description: I18n.t('profile_memory_sample_description_mobile'),
},
{
key: 'Height',
description: I18n.t('profile_memory_sample_description_height'),
},
{
key: 'Weight',
description: I18n.t('profile_memory_sample_description_weight'),
},
];
export const MemoryTemplateModal: React.FC<
MemoryTemplateModalProps
> = props => (
<UIModal
{...props}
type="action"
centered
footer={
props.showType === 'variableList' ? (
<div className={s['template-footer']}>
<Button
theme="solid"
className={s['template-cancel-button']}
onClick={props.onCancel}
>
{I18n.t('cancel_template')}
</Button>
{props.needSecondConfirm ? (
<Popconfirm
className={s['use-template-pop-confirm']}
position="top"
icon={
<IconAlertCircle
size="extra-large"
style={{ color: '#ff9600' }}
/>
}
title={I18n.t('use_template_confirm_title')}
content={I18n.t('use_template_confirm_info')}
okText={I18n.t('use_template_confirm_ok_text')}
cancelText={I18n.t('use_template_confirm_ cancel_text')}
okButtonProps={{ type: 'warning' }}
onConfirm={() => props.addTemplate?.(list)}
>
<BotDebugButton
theme="solid"
type="primary"
style={{ padding: '8px 12px' }}
>
{I18n.t('Use_template')}
</BotDebugButton>
</Popconfirm>
) : (
<BotDebugButton
theme="solid"
type="primary"
style={{ padding: '8px 12px' }}
onClick={() => props.addTemplate?.(list)}
>
{I18n.t('Use_template')}
</BotDebugButton>
)}
</div>
) : null
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
width={props.showType === 'variableList' ? 562 : 448}
title={I18n.t('variable_template_title')}
className={props.className}
>
<div className={s['modal-container']}>
{props.showType === 'variableList' ? (
<Image
className={s['template-variable-list']}
src={IMG_TEMPLATE_USE_I18N}
preview={false}
/>
) : (
<div className={s['template-demo']}>
<div className={s.desc}>{I18n.t('variable_template_demo_desc')}</div>
<div className={s.image}>
<Image
className={s['image-template']}
src={templateSample}
preview={false}
/>
</div>
<div className={s.tip}>{I18n.t('variable_template_demo_text')}</div>
</div>
)}
</div>
</UIModal>
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**comp */
export {
TableMemory,
reloadDatabaseList,
useExpertModeConfig,
} from './table-memory';
export { SuggestionBlock } from './suggestion/suggestion-block';
export { SheetView, SingleSheet, MultipleSheet } from './sheet-view';
export {
OnboardingMessage,
settingAreaScrollId,
EditorExpendModal,
SuggestionList,
type OnboardingEditorAction,
} from './onboarding-message';
export { ModeSelect, type ModeSelectProps } from './mode-select';
export {
ModeLabel,
type ModeLabelProps,
type ModeOption,
} from './mode-select/mode-change-view';
export { DataMemory } from './data-memory';
export { ContentView } from './content-view';
export { ChatBackground } from './chat-background';
export { BotDebugToolPane } from './bot-debug-panel/button';
export { BotDebugPanel } from './bot-debug-panel';
export { BotEditorLoggerContextProvider } from './error-boundary-with-logger';
export { AutoGenerateButton } from './auto-generate-btn';
export { BotDebugButton } from './bot-debug-button';
export { CollapsibleTextarea } from './collapsible-textarea';
export { SuggestionContent } from './suggestion/suggestion-content/suggestion-content';
export { BotSubmitModalDiffView } from './bot-diff-view/bot-submit-modal';
export { InputSlider } from './input-slider';
export { Setting } from './data-set/data-set-area';
export { AuthorizeButton } from './authorize-button';
export {
NavModal,
NAV_MODAL_MAIN_CONTENT_HEIGHT,
NavModalItem,
NavModalProps,
} from './nav-modal';
export { KvBindButton, DiffViewButton } from './connector-action';
export { MemoryToolPane, type MemoryToolPaneProps } from './memory-tool-pane';
export {
PluginPermissionManageList,
PermissionManageTitle,
} from './plugin-permission-manage-list';
export { PublishPlatformSetting } from './publish-platform-setting';
import PublishPlatformDescription from './publish-platform-description';
export { PublishPlatformDescription };

View File

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

View File

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

View File

@@ -0,0 +1,194 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC } from 'react';
import { isInteger, isNumber, isUndefined } from 'lodash-es';
import classNames from 'classnames';
import { type SliderProps } from '@coze-arch/bot-semi/Slider';
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
import { withField, InputNumber, Slider } from '@coze-arch/bot-semi';
import { IconMinus, IconPlus } from '@douyinfe/semi-icons';
import { RCSliderWrapper, type RCSliderProps } from '../rc-slider-wrapper';
import s from './index.module.less';
interface InputSliderProps {
value?: number;
onChange?: (v: number) => void;
max?: number;
min?: number;
step?: number;
disabled?: boolean;
decimalPlaces?: number;
marks?: SliderProps['marks'];
className?: string;
/** 是否使用 rc-slider 替换 semi-slider目前 semi-slider 存在一个比较明显的 bug在缩放场景下拖拽定位存在问题已经反馈等待修复 */
useRcSlider?: boolean;
}
const POWVAL = 10;
const formateDecimalPlacesString = (
value: string | number,
prevValue?: number,
decimalPlaces?: number,
) => {
if (isUndefined(decimalPlaces)) {
return value.toString();
}
const numberValue = Number(value);
const stringValue = value.toString();
if (Number.isNaN(numberValue)) {
return `${value}`;
}
if (decimalPlaces === 0 && !isInteger(Number(value)) && prevValue) {
return `${prevValue}`;
}
const decimalPointIndex = stringValue.indexOf('.');
if (decimalPointIndex < 0) {
return stringValue;
}
const formattedValue = stringValue.substring(
0,
decimalPointIndex + 1 + decimalPlaces,
);
if (formattedValue.endsWith('.') && decimalPlaces === 0) {
return formattedValue.substring(0, formattedValue.length - 1);
}
return formattedValue;
};
const formateDecimalPlacesNumber = (
value: number,
prevValue?: number,
decimalPlaces?: number,
) => {
if (isUndefined(decimalPlaces)) {
return value;
}
if (decimalPlaces === 0 && !isInteger(value) && prevValue) {
return prevValue;
}
const pow = Math.pow(POWVAL, decimalPlaces);
return Math.round(value * pow) / pow;
};
const BaseInputSlider: React.FC<InputSliderProps> = ({
value,
onChange,
max = 1,
min = 0,
step = 1,
disabled,
decimalPlaces,
marks,
className,
useRcSlider = false,
}) => {
const onNumberChange = (numberValue: number) => {
const formattedValue = formateDecimalPlacesNumber(
numberValue,
value,
decimalPlaces,
);
onChange?.(formattedValue);
};
return (
<div className={classNames(s['input-slider'], className)}>
{useRcSlider ? (
<RCSliderWrapper
disabled={disabled}
value={value}
max={max}
min={min}
step={step}
marks={marks as RCSliderProps['marks']}
onChange={v => {
if (typeof v === 'number') {
onChange?.(v);
}
}}
/>
) : (
<Slider
className={s.slider}
disabled={disabled}
value={value}
max={max}
min={min}
step={step}
marks={marks}
onChange={v => {
if (typeof v === 'number') {
onChange?.(v);
}
}}
/>
)}
<div style={{ position: 'relative', marginLeft: 24 }}>
<IconMinus
className={classNames(
s['input-btn'],
disabled && s['input-btn-disabled'],
)}
onClick={e => {
e.stopPropagation();
if (isNumber(value) && value <= min) {
return;
}
if (!disabled && value !== undefined) {
onNumberChange(value - step);
}
}}
/>
<InputNumber
className={s['input-number']}
value={value}
disabled={disabled}
formatter={inputValue =>
formateDecimalPlacesString(inputValue, value)
}
hideButtons
onNumberChange={onNumberChange}
max={max}
min={min}
/>
<IconPlus
className={classNames(
s['input-btn'],
disabled && s['input-btn-disabled'],
)}
onClick={e => {
if (isNumber(value) && value >= max) {
return;
}
e.stopPropagation();
if (!disabled && value !== undefined) {
onNumberChange(value + step);
}
}}
/>
</div>
</div>
);
};
export const InputSlider: FC<CommonFieldProps & InputSliderProps> =
withField(BaseInputSlider);

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, type FC } from 'react';
import { type ReactElement } from 'react-markdown/lib/react-markdown';
import { I18n } from '@coze-arch/i18n';
import { type ButtonProps } from '@coze-arch/coze-design';
import { IconMemoryDownMenu } from '@coze-arch/bot-icons';
import { DataErrorBoundary, DataNamespace } from '@coze-data/reporter';
import { BotE2e } from '@coze-data/e2e';
import {
MemoryDebugDropdown,
useMemoryDebugModal,
type MemoryDebugDropdownMenuItem,
type MemoryModule,
useSendTeaEventForMemoryDebug,
} from '@coze-data/database';
import { OperateTypeEnum, ToolPane } from '@coze-agent-ide/debug-tool-list';
export interface MemoryToolPaneProps {
menuList: MemoryDebugDropdownMenuItem[];
}
export const MemoryToolPane: FC<MemoryToolPaneProps> = ({ menuList }) => {
const isStore = false;
const sendTeaEventForMemoryDebug = useSendTeaEventForMemoryDebug({
isStore,
});
const [curMemoryModule, setCurMemoryModule] = useState<MemoryModule>();
const defaultModule = menuList[0]?.name;
const { open, node: memoryModal } = useMemoryDebugModal({
memoryModule: curMemoryModule || defaultModule,
menuList,
setMemoryModule: setCurMemoryModule,
isStore,
});
return (
<DataErrorBoundary namespace={DataNamespace.MEMORY}>
{memoryModal}
{
(
<ToolPane
visible={menuList.length > 0}
itemKey={`key_${I18n.t('database_memory_menu')}`}
operateType={OperateTypeEnum.DROPDOWN}
title={I18n.t('database_memory_menu')}
icon={<IconMemoryDownMenu />}
onEntryButtonClick={() => {
sendTeaEventForMemoryDebug(defaultModule);
setCurMemoryModule(defaultModule);
open();
}}
dropdownProps={{
showTick: true,
clickToHide: true,
render: (
<MemoryDebugDropdown
menuList={menuList}
onClickItem={memoryModule => {
setCurMemoryModule(memoryModule);
open();
}}
/>
),
}}
buttonProps={
{
'data-testid': BotE2e.BotMemoryDebugBtn,
} as unknown as ButtonProps
}
/>
) as ReactElement
}
</DataErrorBoundary>
);
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Typography, Popover, Radio } from '@coze-arch/bot-semi';
import { BotMode } from '@coze-arch/bot-api/developer_api';
import { ChangeButton } from './change-button';
import s from './index.module.less';
export interface ModeLabelProps {
icon: ReactNode;
isDisabled: boolean;
isSelected: boolean;
title: ReactNode;
desc: ReactNode;
}
export const ModeLabel: React.FC<ModeLabelProps> = ({
icon,
isDisabled,
isSelected,
title,
desc,
}) => (
<div className={classNames('flex items-center gap-[12px]')}>
<div
className={
(classNames('text-[16px]'),
isDisabled ? 'coz-fg-dim' : 'coz-fg-primary')
}
>
{icon}
</div>
<div data-testid={`bot-edit-agent-select-mode-button-${title}`}>
<div
className={classNames(
'text-[16px] leading-[22px]',
isSelected ? 'font-[500]' : 'font-[400]',
isDisabled ? 'coz-fg-dim' : 'coz-fg-primary',
)}
>
{title}
</div>
<Typography.Text
className={classNames(
'mt-[4px]',
'text-[14px] font-[400] leading-[20px]',
isDisabled ? 'coz-fg-dim' : 'coz-fg-secondary',
)}
>
{desc}
</Typography.Text>
</div>
</div>
);
export interface ModeOption
extends Omit<ModeLabelProps, 'isSelected' | 'isDisabled'> {
value: BotMode;
showText: boolean;
getIsDisabled: (params: { currentMode: BotMode }) => boolean;
}
export interface ModeChangeViewProps {
modeSelectLoading: boolean;
modeValue: BotMode;
onModeChange: (value: BotMode) => Promise<void>;
isReadOnly: boolean;
optionList: ModeOption[];
tooltip?: string;
}
export const ModeChangeView = (props: ModeChangeViewProps) => {
const {
modeValue = BotMode.SingleMode,
onModeChange,
modeSelectLoading,
isReadOnly,
tooltip,
optionList,
} = props;
const disabled = isReadOnly || modeSelectLoading;
const modeInfo = optionList.find(option => option.value === modeValue);
if (disabled) {
return (
<ChangeButton disabled={disabled} tooltip={tooltip} modeInfo={modeInfo} />
);
}
return (
<Popover
className={s['mode-change-popover']}
data-testid="bot-detail.mode-chage-view.popover"
trigger="click"
position="bottomLeft"
autoAdjustOverflow={false}
content={
<div className={s['mode-change-popover-content']}>
<div className="coz-fg-plus text-[14px] font-[500] leading-[20px] mb-[12px]">
{I18n.t('chatflow_switch_mode_title')}
</div>
<Radio.Group
type="pureCard"
direction="vertical"
value={modeValue}
defaultValue={modeValue}
disabled={disabled}
options={optionList.map(option => {
const isSelected = modeValue === option.value;
const isDisabled = option.getIsDisabled({
currentMode: modeValue,
});
return {
value: option.value,
disabled: isDisabled,
label: (
<ModeLabel
{...option}
key={option.value}
isDisabled={isDisabled}
isSelected={isSelected}
/>
),
};
})}
onChange={e => onModeChange(e.target.value)}
/>
</div>
}
>
<div>
<ChangeButton disabled={false} modeInfo={modeInfo} />
</div>
</Popover>
);
};

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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