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,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.
*/
// 快捷指令在IDE中的配置tool
export { ShortcutToolConfig } from './shortcut-config';

View File

@@ -0,0 +1,37 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import { Image } from '@coze-arch/bot-semi';
import style from '../index.module.less';
import shortcutTipEn from '../../../assets/shortcut-tip_en.png';
import shortcutTipCn from '../../../assets/shortcut-tip_cn.png';
export const ShortcutTips = () => (
<div className={style['tip-content']}>
<div style={{ marginBottom: '8px' }}>
{I18n.t('bot_ide_shortcut_intro')}
</div>
<Image
preview={false}
width={416}
src={IS_OVERSEA ? shortcutTipEn : shortcutTipCn}
/>
</div>
);

View File

@@ -0,0 +1,197 @@
@ide-tool-prefix: chat-studio-tool-content-block;
.shortcut-tool-config {
padding-right: 0;
padding-left: 0;
:global {
.@{ide-tool-prefix}-content {
/* stylelint-disable declaration-no-important */
padding-right: 0 !important;
padding-left: 0 !important;
}
}
}
.shortcut-list {
display: flex;
flex-direction: column;
}
.shortcut-item {
display: flex;
place-content: center space-between;
height: 52px;
margin-bottom: 4px;
padding: 8px;
background: rgba(6, 7, 9, 4%);
border-radius: 8px;
&.shortcut-item-mouse_hover {
background: rgba(6, 7, 9, 12%);
border: 1px solid rgba(6, 7, 9, 10%);
}
&.shortcut-item_hovered {
background: rgba(6, 7, 9, 4%);
}
}
.shortcut-item_title {
overflow: hidden;
display: flex;
align-items: center;
font-size: 12px;
font-weight: 500;
font-style: normal;
line-height: 16px;
color: rgba(6, 7, 9, 80%);
text-overflow: ellipsis;
}
.shortcut-item_header {
display: flex;
flex-direction: column;
justify-content: center;
width: calc(100% - 80px);
}
.operation {
display: flex;
align-items: center;
justify-content: space-between;
}
.operation-item-icon {
cursor: pointer;
:global {
.semi-icon {
svg {
width: 14px;
height: 14px;
}
}
}
}
.operation-item-icon_drag {
cursor: grab;
background: unset !important;
&.operation-dragging {
cursor: grabbing;
}
}
.operation-item-icon_hover {
&:hover {
background: rgba(6, 7, 9, 16%);
border-radius: 6px;
}
}
.delete-modal {
:global {
.semi-modal {
border-radius: 8px;
.semi-modal-content {
padding: 16px;
border-radius: 8px;
.semi-modal-header {
margin: 0;
}
.semi-modal-footer {
margin: 24px 0 0;
}
}
}
}
}
.delete-common-modal-button-style {
min-width: 56px;
height: 32px;
padding: 6px 16px;
font-size: 14px;
font-weight: 500;
font-style: normal;
line-height: 20px;
background: rgba(6, 7, 9, 8%);
border-radius: 8px;
}
.delete-modal-cancel-button {
.delete-common-modal-button-style;
color: rgba(6, 7, 9, 80%);
}
.delete-modal-ok-button {
.delete-common-modal-button-style;
color: #fff;
background-color: #f22435;
border-radius: 8px;
&:hover {
background-color: #ba0010 !important;
}
&:active {
background-color: #b0000f !important;
}
}
.icon-button-16 {
cursor: pointer;
&:hover {
border-radius: 4px;
}
:global {
.semi-button {
&.semi-button-size-small {
height: 16px;
padding: 1px !important;
svg {
@apply text-foreground-2;
}
}
}
}
}
.tip-content {
display: flex;
flex-direction: column;
width: 416px;
font-size: 14px;
font-weight: 500;
font-style: normal;
line-height: 20px;
}
.hidden {
visibility: hidden;
}
.shortcut-config-empty {
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px;
color: var(--coz-fg-secondary);
}

View File

@@ -0,0 +1,319 @@
/*
* 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.
*/
// 快捷指令在IDE中的配置tool
import React, { type FC, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useDebounceFn } from 'ahooks';
import {
type ShortCutStruct,
getStrictShortcuts,
type ShortCutCommand,
ToolKey,
} from '@coze-agent-ide/tool-config';
import type { IToggleContentBlockEventParams } from '@coze-agent-ide/tool';
import {
AddButton,
EventCenterEventName,
ToolContentBlock,
useEvent,
useToolContentBlockDefaultExpand,
useToolDispatch,
useToolValidData,
} from '@coze-agent-ide/tool';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import {
getBotDetailIsReadonly,
updateShortcutSort,
} from '@coze-studio/bot-detail-store';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { Toast } from '@coze-arch/bot-semi';
import { BotMode } from '@coze-arch/bot-api/playground_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { type SkillsModalProps } from '../types';
import { useShortcutEditModal } from '../shortcut-edit';
import { isApiError } from '../../utils/handle-error';
import { EmptyShortcuts } from './shortcut-list/empty-shortcuts';
import { ShortcutList } from './shortcut-list';
import { ShortcutTips } from './config-action';
import style from './index.module.less';
const MAX_SHORTCUTS = 10;
export interface ShortcutToolConfigProps {
title: string;
toolKey: 'shortcut';
skillModal: FC<SkillsModalProps>;
botMode: BotMode;
}
// eslint-disable-next-line @coze-arch/max-line-per-function
export const ShortcutToolConfig: FC<ShortcutToolConfigProps> = props => {
const [apiErrorMessage, setApiErrorMessage] = useState('');
const { title, skillModal: SkillModal, botMode } = props;
const { isReadonly, botId } = useBotInfoStore(
useShallow(state => ({
isReadonly: getBotDetailIsReadonly(),
botId: state.botId,
})),
);
const { shortcuts: initShortcuts = [] } = useBotSkillStore(
useShallow(state => ({
shortcuts: state.shortcut.shortcut_list,
})),
);
const getSpaceId = useSpaceStore(state => state.getSpaceId);
const setHasValidData = useToolValidData();
// single不展示指定agent的快捷指令
const singleShortcuts = initShortcuts?.filter(shortcut => !shortcut.agent_id);
const shortcuts =
botMode === BotMode.SingleMode ? singleShortcuts : initShortcuts;
const hasConfiguredShortcuts = Boolean(shortcuts && shortcuts.length > 0);
setHasValidData(hasConfiguredShortcuts);
const isReachLimit = shortcuts.length >= MAX_SHORTCUTS;
const defaultExpand = useToolContentBlockDefaultExpand({
configured: hasConfiguredShortcuts,
});
const dispatch = useToolDispatch<ShortCutStruct>();
const { emit } = useEvent();
const [selectedShortcut, setSelectedShortcut] = useState<
ShortCutCommand | undefined
>(undefined);
const { run: updateShortcutSortDebounce } = useDebounceFn(
async (newShortcuts: string[]) => {
await updateShortcutSort(newShortcuts);
},
{
wait: 500,
},
);
const onDisorder = async (orderList: ShortCutCommand[]) => {
try {
const newSortList = orderList.map(item => item.command_id);
dispatch({ shortcut_list: orderList, shortcut_sort: newSortList });
await updateShortcutSortDebounce(newSortList);
} catch (e) {
logger.error({
error: e as Error,
eventName: 'shortcut-disorder-service-fail',
});
}
};
const onEditClick = (shortcut: ShortCutCommand) => {
setSelectedShortcut(shortcut);
openShortcutModal();
};
const onRemoveClick = async (shortcut: ShortCutCommand) => {
try {
const newSorts = shortcuts
?.filter(item => item.command_id !== shortcut.command_id)
.map(item => item.command_id);
await updateShortcutSort(newSorts);
const newShortcuts = shortcuts?.filter(
item => item.command_id !== shortcut.command_id,
);
newShortcuts && dispatch({ shortcut_list: newShortcuts });
} catch (error) {
if (!isApiError(error)) {
Toast.error(I18n.t('shortcut_modal_fail_to_delete_shortcut_error'));
}
logger.error({
error: error as Error,
eventName: 'shortcut-removeShortcut-fail',
});
}
};
const closeModal = () => {
closeShortcutModal();
setApiErrorMessage('');
};
const editShortcut = async (
shortcut: ShortCutCommand,
onFail: () => void,
) => {
try {
await PlaygroundApi.CreateUpdateShortcutCommand(
{
object_id: botId,
space_id: getSpaceId(),
shortcuts: shortcut,
},
{ __disableErrorToast: true },
);
// TODO: hzf 得加上
// if (res && res.data?.check_not_pass) {
// Toast.error(I18n.t('shortcut_modal_illegal_keyword_detected_error'));
// onFail();
// return;
// }
const newShortcuts = shortcuts?.map(item =>
item.command_id === shortcut.command_id ? shortcut : item,
);
newShortcuts && dispatch({ shortcut_list: newShortcuts });
closeModal();
onFail();
} catch (e) {
onFail();
if (!isApiError(e)) {
Toast.error(I18n.t('shortcut_modal_fail_to_update_shortcut_error'));
}
if (isApiError(e)) {
const error = e as { message?: string; msg?: string };
setApiErrorMessage(error.message || error.msg || '');
}
logger.error({
error: e as Error,
eventName: 'shortcut-editShortcut-fail',
});
}
};
const addShortcut = async (shortcut: ShortCutCommand, onFail: () => void) => {
try {
const { shortcuts: newShortcut } =
await PlaygroundApi.CreateUpdateShortcutCommand(
{
object_id: botId,
space_id: getSpaceId(),
shortcuts: shortcut,
},
{ __disableErrorToast: true },
);
const strictShortcuts = newShortcut && getStrictShortcuts([newShortcut]);
// 一次只能添加一个快捷指令
const strictShortcut = strictShortcuts?.[0];
if (!strictShortcut) {
Toast.error('Please fill in the required fields');
return;
}
const newShortcuts = [
...(shortcuts?.map(item => item.command_id) || []),
strictShortcut.command_id,
];
await updateShortcutSort(newShortcuts);
dispatch({ shortcut_list: [...(shortcuts || []), ...strictShortcuts] });
emit<IToggleContentBlockEventParams>(
EventCenterEventName.ToggleContentBlock,
{
abilityKey: ToolKey.SHORTCUT,
isExpand: true,
},
);
closeModal();
} catch (error) {
onFail();
if (!isApiError(error)) {
Toast.error(I18n.t('shortcut_modal_fail_to_add_shortcut_error'));
}
if (isApiError(error)) {
const e = error as { message?: string; msg?: string };
setApiErrorMessage(e.message || e.msg || '');
}
logger.error({
error: error as Error,
eventName: 'shortcut-addShortcut-fail',
});
}
};
const {
node: ShortcutModal,
open: openShortcutModal,
close: closeShortcutModal,
} = useShortcutEditModal({
skillModal: SkillModal,
shortcut: selectedShortcut,
errorMessage: apiErrorMessage,
setErrorMessage: setApiErrorMessage,
onAdd: addShortcut,
onEdit: editShortcut,
botMode,
});
const renderShortcutConfig = () => {
if (!hasConfiguredShortcuts) {
return <EmptyShortcuts />;
}
return (
<ShortcutList
shortcuts={shortcuts}
isReadonly={isReadonly}
onDisorder={onDisorder}
onRemove={onRemoveClick}
onEdit={onEditClick}
/>
);
};
return (
<>
{ShortcutModal}
<ToolContentBlock
className={style['shortcut-tool-config']}
showBottomBorder={!hasConfiguredShortcuts}
header={title}
defaultExpand={defaultExpand}
tooltip={<ShortcutTips />}
actionButton={
!isReadonly && (
<>
<AddButton
tooltips={
isReachLimit
? I18n.t('bot_ide_shortcut_max_limit', {
maxCount: MAX_SHORTCUTS,
})
: I18n.t('bot_ide_shortcut_add_button')
}
onClick={() => {
if (isReachLimit) {
return;
}
setSelectedShortcut(undefined);
openShortcutModal();
}}
enableAutoHidden={true}
data-testid="bot.editor.tool.shortcut.add-button"
/>
</>
)
}
>
{renderShortcutConfig()}
</ToolContentBlock>
</>
);
};

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.
*/
import { type FC } from 'react';
import { I18n } from '@coze-arch/i18n';
import style from '../index.module.less';
export const EmptyShortcuts: FC = () => (
<div className={style['shortcut-config-empty']}>
{I18n.t('bot_ide_shortcut_intro')}
</div>
);

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 { type FC } from 'react';
import cls from 'classnames';
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
import { ToolItemList } from '@coze-agent-ide/tool';
import { SortableList } from '@coze-studio/components/sortable-list';
import style from '../index.module.less';
import { ShortcutItem } from './shortcut-item';
interface ShortcutsListProps {
shortcuts: ShortCutCommand[];
isReadonly: boolean;
onRemove?: (shortcut: ShortCutCommand) => void;
onDisorder?: (orderList: ShortCutCommand[]) => void;
onEdit?: (shortcut: ShortCutCommand) => void;
}
const SortableListSymbol = Symbol('Shortcut-config-list-sortlist');
export const ShortcutList: FC<ShortcutsListProps> = props => {
const { shortcuts, onDisorder, onEdit, onRemove, isReadonly } = props;
const handleRemove = (shortcut: ShortCutCommand) => {
onRemove?.(shortcut);
};
const handleDisorder = (orderList: ShortCutCommand[]) => {
onDisorder?.(orderList);
};
const handleEdit = (shortcut: ShortCutCommand) => {
onEdit?.(shortcut);
};
return (
<>
<div className={cls(style['shortcut-list'])}>
<ToolItemList>
<SortableList
type={SortableListSymbol}
list={shortcuts}
getId={shortcut => shortcut.command_id}
enabled={shortcuts.length > 1 && !isReadonly}
onChange={handleDisorder}
itemRender={({ data: shortcut, connect, isDragging }) => (
<ShortcutItem
isDragging={Boolean(isDragging)}
connect={connect}
key={shortcut.command_id}
shortcut={shortcut}
isReadonly={isReadonly}
onRemove={() => handleRemove(shortcut)}
onEdit={() => handleEdit(shortcut)}
/>
)}
/>
</ToolItemList>
</div>
</>
);
};

View File

@@ -0,0 +1,111 @@
/*
* 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, useEffect, useRef } from 'react';
import type { ShortCutCommand } from '@coze-agent-ide/tool-config';
import {
ToolItem,
ToolItemActionEdit,
ToolItemActionDelete,
ToolItemActionDrag,
} from '@coze-agent-ide/tool';
import { type ConnectDnd } from '@coze-studio/components';
import { I18n } from '@coze-arch/i18n';
import { UIModal } from '@coze-arch/bot-semi';
import style from '../index.module.less';
import DefaultShortcutIcon from '../../../assets/shortcut-icon-default.svg';
interface ShortcutItemProps {
shortcut: ShortCutCommand;
isReadonly: boolean;
connect: ConnectDnd;
isDragging: boolean;
onRemove?: (shortcut: ShortCutCommand) => void;
onEdit?: (shortcut: ShortCutCommand) => void;
onDisorder?: (order: number) => void;
}
export const ShortcutItem: FC<ShortcutItemProps> = ({
shortcut,
onEdit,
onRemove,
connect,
isReadonly,
isDragging,
}) => {
const dropRef = useRef<HTMLDivElement>(null);
const dragRef = useRef<HTMLDivElement>(null);
connect(dropRef, dragRef);
useEffect(() => {
connect(dropRef, dragRef);
}, [dragRef, dropRef]);
// 点击删除,弹出二次确认弹窗
const openConfirmRemoveModal = () => {
UIModal.info({
title: I18n.t('bot_ide_shortcut_removal_confirm'),
width: 320,
icon: null,
closeIcon: <></>,
className: style['delete-modal'],
cancelText: I18n.t('Cancel'),
okText: I18n.t('Remove'),
cancelButtonProps: { className: style['delete-modal-cancel-button'] },
okButtonProps: {
className: style['delete-modal-ok-button'],
},
onOk: () => onRemove?.(shortcut),
});
};
return (
<div ref={dropRef}>
<ToolItem
title={shortcut.command_name ?? ''}
description={shortcut.description ?? ''}
avatar={shortcut.shortcut_icon?.url || DefaultShortcutIcon}
avatarStyle={{
padding: '10px',
background: '#fff',
}}
actions={
<>
<div ref={dragRef}>
<ToolItemActionDrag
data-testid="chat-area.shortcut.drag-button"
isDragging={isDragging}
disabled={isReadonly}
/>
</div>
<ToolItemActionEdit
tooltips={I18n.t('bot_ide_shortcut_item_edit')}
onClick={() => onEdit?.(shortcut)}
data-testid="chat-area.shortcut.edit-button"
disabled={isReadonly}
/>
<ToolItemActionDelete
tooltips={I18n.t('bot_ide_shortcut_item_trash')}
onClick={() => openConfirmRemoveModal()}
disabled={isReadonly}
data-testid="chat-area.shortcut.delete-button"
/>
</>
}
/>
</div>
);
};

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 { type FC, useRef, useState } from 'react';
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
import { Deferred } from '@coze-common/chat-area-utils';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { UIModal } from '@coze-arch/bot-semi';
export interface HasUnusedComponentsConfirmModalProps {
onConfirm?: () => void;
onCancel?: () => void;
components: ShortCutCommand['components_list'];
}
export const HasUnusedComponentsConfirmModal: FC<
HasUnusedComponentsConfirmModalProps
> = ({ onConfirm, components, onCancel }) => {
const unUsedComponentsNames = components
?.map(component => component.name)
.join(', ');
return (
<UIModal
visible
footer={null}
onCancel={onCancel}
bodyStyle={{
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
padding: '0 0 16px 0',
}}
title={I18n.t(
'shortcut_modal_save_shortcut_with_components_unused_modal_title',
)}
>
<div className="pb-6">
{I18n.t(
'shortcut_modal_save_shortcut_with_components_unused_modal_desc',
{
unUsedComponentsNames,
},
)}
</div>
<div className="flex gap-2 justify-end">
<Button
onClick={onCancel}
color="highlight"
className="!coz-mg-hglt !coz-fg-hglt"
>
{I18n.t('Cancel')}
</Button>
<Button onClick={onConfirm}>{I18n.t('Confirm')}</Button>
</div>
</UIModal>
);
};
export const useHasUnusedComponentsConfirmModal = () => {
const [visible, setVisible] = useState(false);
const [components, setComponents] = useState<
ShortCutCommand['components_list']
>([]);
const openDeferred = useRef<Deferred<boolean> | null>(null);
const close = () => {
openDeferred.current?.resolve(false);
setVisible(false);
};
const onConfirm = () => {
openDeferred.current?.resolve(true);
setVisible(false);
};
const open = (unUsedComponents: ShortCutCommand['components_list']) => {
openDeferred.current = new Deferred<boolean>();
setComponents(unUsedComponents);
setVisible(true);
return openDeferred.current.promise;
};
return {
node: visible ? (
<HasUnusedComponentsConfirmModal
components={components}
onCancel={close}
onConfirm={onConfirm}
/>
) : null,
close,
open,
};
};

View File

@@ -0,0 +1,185 @@
/*
* 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,
forwardRef,
type RefObject,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import { type Form } from '@coze-arch/bot-semi';
import {
type shortcut_command,
ToolType,
} from '@coze-arch/bot-api/playground_api';
import VarQueryTextareaWrapperWithField from '../var-query-textarea/field';
import { ComponentsTable } from '../components-table';
import type { ComponentsTableActions } from '../components-table';
import type {
ShortcutEditFormValues,
SkillsModalProps,
ToolInfo,
} from '../../types';
import { getDSLFromComponents } from '../../../utils/dsl-template';
import { SkillSwitch } from './skill-switch';
import { getUnusedComponents, initComponentsByToolParams } from './method';
import { useHasUnusedComponentsConfirmModal } from './confirm-modal';
export interface IActionSwitchAreaProps {
skillModal: FC<SkillsModalProps>;
editedShortcut: ShortcutEditFormValues;
formRef: RefObject<Form>;
modalRef?: RefObject<HTMLDivElement>;
isBanned: boolean;
}
export interface IActionSwitchAreaRef {
getValues: () => ShortcutEditFormValues;
validate: () => Promise<boolean>;
}
export const ActionSwitchArea = forwardRef<
IActionSwitchAreaRef,
IActionSwitchAreaProps
>((props, ref) => {
const {
editedShortcut,
skillModal: SkillModal,
formRef,
isBanned,
modalRef,
} = props;
const useTool = editedShortcut?.use_tool ?? false;
const initialComponents = editedShortcut?.components_list?.length
? editedShortcut.components_list
: [];
const [components, setComponents] =
useState<shortcut_command.Components[]>(initialComponents);
const componentsRef = useRef<{
formApi?: ComponentsTableActions;
}>(null);
const { open: openConfirmModal, node: ConfirmModal } =
useHasUnusedComponentsConfirmModal();
useImperativeHandle(ref, () => ({
getValues: () => {
const values = formRef.current?.formApi.getValues();
return {
...values,
components_list: components,
use_tool: useTool,
card_schema: getDSLFromComponents(components),
};
},
validate: async () => {
if (!formRef.current) {
return false;
}
return await checkComponentsValid();
},
}));
const onToolParamsChange = (toolInfo: ToolInfo | null) => {
const {
tool_type,
plugin_id,
plugin_api_name,
api_id,
tool_name,
work_flow_id,
tool_params_list = [],
} = toolInfo || {};
const newComponents = initComponentsByToolParams(tool_params_list);
// TODO: hzf, 有点复杂,看看可以initValue么
formRef.current?.formApi.setValue('components_list', newComponents);
setComponents(newComponents);
// 只有这种情况需要手动更新数据
componentsRef.current?.formApi?.setValues(newComponents);
formRef.current?.formApi.setValue('tool_type', tool_type);
formRef.current?.formApi.setValue('plugin_id', plugin_id);
tool_type === ToolType.ToolTypeWorkFlow &&
formRef.current?.formApi.setValue('work_flow_id', work_flow_id);
formRef.current?.formApi.setValue('plugin_api_name', plugin_api_name);
formRef.current?.formApi.setValue('plugin_api_id', api_id);
formRef.current?.formApi.setValue('tool_info', {
tool_name,
tool_params_list,
});
};
const checkComponentsValid = async (): Promise<boolean> => {
if (!formRef.current) {
return false;
}
try {
await componentsRef.current?.formApi?.validate();
// eslint-disable-next-line @coze-arch/use-error-in-catch -- form validate
} catch (err) {
return false;
}
const componentNotUsed = getUnusedComponents(editedShortcut);
if (componentNotUsed.length) {
return await openConfirmModal(componentNotUsed);
}
return true;
};
useEffect(() => {
formRef.current?.formApi.setValue('components_list', components);
}, [components]);
return (
<>
<SkillSwitch
skillModal={SkillModal}
isBanned={isBanned}
onToolChange={onToolParamsChange}
editedShortcut={editedShortcut}
/>
<ComponentsTable
toolType={useTool ? ToolType.ToolTypePlugin : undefined}
toolInfo={editedShortcut?.tool_info ?? {}}
ref={componentsRef}
disabled={isBanned}
components={components}
onChange={newComponents => {
setComponents(newComponents);
}}
/>
<VarQueryTextareaWrapperWithField
field="template_query"
value={editedShortcut?.template_query || ''}
components={components}
modalRef={modalRef}
/>
{ConfirmModal}
</>
);
});

View File

@@ -0,0 +1,52 @@
/*
* 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 {
InputType,
type shortcut_command,
type ToolParams,
} from '@coze-arch/bot-api/playground_api';
import { type ShortcutEditFormValues } from '../../types';
export const initComponentsByToolParams = (
params: ToolParams[],
): shortcut_command.Components[] =>
params?.map(param => {
const { name, desc, refer_component } = param;
return {
name,
parameter: name,
description: desc,
input_type: InputType.TextInput,
default_value: {
value: '',
},
hide: !refer_component,
};
});
// 获取没有被使用的组件
export const getUnusedComponents = (
shortcut: ShortcutEditFormValues,
): shortcut_command.Components[] => {
const { components_list, template_query } = shortcut;
return (
components_list?.filter(
component => !template_query?.includes(`{{${component.name}}}`),
) ?? []
);
};

View File

@@ -0,0 +1,8 @@
.icon {
margin-right: 4px;
> svg {
width: 18px;
height: 18px;
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC } from 'react';
import cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Form } from '@coze-arch/bot-semi';
import style from '../../index.module.less';
import FieldLabel from '../../components/field-label';
import {
type ShortcutEditFormValues,
type SkillsModalProps,
type ToolInfo,
} from '../../../types';
import { getToolInfoByShortcut } from '../../../../utils/tool-params';
import { useToolAction } from './tool-action';
export interface ChooseSendTypeRadioProps {
editedShortcut?: ShortcutEditFormValues;
skillModal: FC<SkillsModalProps>;
isBanned: boolean;
onToolChange?: (tooInfo: ToolInfo | null) => void;
}
const { Checkbox } = Form;
export const SkillSwitch: FC<ChooseSendTypeRadioProps> = props => {
const { editedShortcut, skillModal, isBanned, onToolChange } = props;
const { action, open, cancel } = useToolAction({
initTool: getToolInfoByShortcut(editedShortcut),
onSelect: onToolChange,
skillModal,
isBanned,
});
return (
<div
className={cls(
style['form-item'],
style['shortcut-action-item'],
'pb-[16px]',
)}
>
<FieldLabel>{I18n.t('shortcut_modal_skill')}</FieldLabel>
<div className="flex items-center justify-between h-[32px]">
<Checkbox
field="use_tool"
onChange={e => {
const { checked } = e.target;
checked ? open() : cancel();
}}
noLabel
fieldClassName="!pb-0"
>
{I18n.t('shortcut_modal_shortcut_action_use_plugin_wf')}
</Checkbox>
<div className="flex items-center">
{editedShortcut?.use_tool ? action : null}
</div>
</div>
</div>
);
};

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.
*/
// format选择的插件参数列表
import { type WorkFlowItemType } from '@coze-studio/bot-detail-store';
import { type PluginApi, ToolType } from '@coze-arch/bot-api/playground_api';
import { type ToolInfo } from '../../../types';
// 最多勾选10个如果入参数量超过10个仅勾选其中10个优先勾选required参数勾满10个时其他checkbox置灰不可继续勾选。
export const MAX_TOOL_PARAMS_COUNT = 10;
// 初始化工具列表参数
export const initToolInfoByToolApi = (
toolApi?: WorkFlowItemType | PluginApi,
): ToolInfo | null => {
if (!toolApi) {
return null;
}
const isWorkflow = 'workflow_id' in toolApi;
const workflowPluginProcessedToolInfo = isWorkflow
? initToolInfoByWorkFlow(toolApi as WorkFlowItemType)
: initToolInfoByPlugin(toolApi);
const { tool_params_list } = workflowPluginProcessedToolInfo;
// 对params进行排序将required=true的字段排在前面
const sortedParams = tool_params_list?.sort(
(a, b) => (b.required ? 1 : -1) - (a.required ? 1 : -1),
);
return {
...workflowPluginProcessedToolInfo,
tool_params_list:
sortedParams?.map((param, index) => {
const { name, desc, required, type } = param;
return {
name,
type,
desc,
required,
default_value: '',
refer_component: index < MAX_TOOL_PARAMS_COUNT,
};
}) || [],
};
};
// workflow参数转化为toolParams
export const initToolInfoByWorkFlow = (
workFlow: WorkFlowItemType,
): ToolInfo => {
const { name, parameters, workflow_id, ...rest } = workFlow;
return {
...rest,
tool_type: ToolType.ToolTypeWorkFlow,
tool_name: name,
plugin_api_name: name,
tool_params_list: parameters || [],
work_flow_id: workflow_id,
};
};
export const initToolInfoByPlugin = (plugin: PluginApi): ToolInfo => {
const { name, plugin_name, parameters, ...rest } = plugin;
return {
...rest,
tool_type: ToolType.ToolTypePlugin,
tool_name: plugin_name ?? '',
plugin_api_name: name,
tool_params_list: parameters || [],
};
};
// 获取skillModal开启的tab
export const getSkillModalTab = (): (
| 'plugin'
| 'workflow'
| 'imageFlow'
| 'datasets'
)[] => ['plugin', 'workflow'];

View File

@@ -0,0 +1,206 @@
/*
* 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, useState } from 'react';
import cs from 'classnames';
import { type WorkFlowItemType } from '@coze-studio/bot-detail-store';
import { I18n } from '@coze-arch/i18n';
import {
Typography,
Toast,
Popover,
UIButton,
Tooltip,
} from '@coze-arch/bot-semi';
import {
IconAdd,
IconInfo,
IconPluginsSelected,
IconWorkflowsSelected,
} from '@coze-arch/bot-icons';
import {
type PluginApi,
type PluginParameter,
ToolType,
} from '@coze-arch/bot-api/developer_api';
import style from '../../index.module.less';
import ActionButton from '../../components/action-button';
import {
OpenModeType,
type SkillsModalProps,
type ToolInfo,
} from '../../../types';
import { validatePluginAndWorkflowParams } from '../../../../utils/tool-params';
import CloseToolIcon from '../../../../assets/close-tool.svg';
import { getSkillModalTab, initToolInfoByToolApi } from './method';
import styles from './index.module.less';
interface ToolActionProps {
initTool?: ToolInfo;
skillModal: FC<SkillsModalProps>;
onSelect?: (tooInfo: ToolInfo | null) => void;
isBanned: boolean;
}
const ToolButton = (props: {
toolInfo: ToolInfo;
onCancel: () => void;
isBanned: boolean;
}) => {
const {
toolInfo: { tool_type, tool_name },
onCancel,
isBanned,
} = props;
const [removePopoverVisible, setRemovePopoverVisible] = useState(false);
const removePopoverContent = (
<div className={style['remove-popover-content']}>
<Typography.Text className={style.title}>
{I18n.t('shortcut_modal_remove_plugin_wf_double_confirm')}
</Typography.Text>
<Typography.Text className={style.desc}>
{I18n.t('shortcut_modal_remove_plugin_wf_double_tip')}
</Typography.Text>
<UIButton className={style['delete-btn']} onClick={() => onCancel()}>
{I18n.t('shortcut_modal_remove_plugin_wf_button')}
</UIButton>
</div>
);
return (
<Popover
trigger="custom"
position="bottomRight"
content={removePopoverContent}
onClickOutSide={() => setRemovePopoverVisible(false)}
visible={removePopoverVisible}
>
<div
className={cs(
'flex ml-2 rounded-[6px] coz-mg-primary items-center px-[10px] py-[3px] text-xs coz-fg-primary',
)}
>
{tool_type === ToolType.ToolTypePlugin && (
<IconPluginsSelected className={styles.icon} />
)}
{tool_type === ToolType.ToolTypeWorkFlow && (
<IconWorkflowsSelected className={styles.icon} />
)}
<Typography.Text
ellipsis={{
showTooltip: {
opts: { content: tool_name },
},
}}
size="small"
>
{tool_name}
</Typography.Text>
{isBanned ? (
<Tooltip content={I18n.t('Plugin_delisted')}>
<IconInfo className="ml-1" />
</Tooltip>
) : null}
<img
className="ml-[8px] cursor-pointer"
alt="close"
src={CloseToolIcon}
onClick={() => {
setRemovePopoverVisible(true);
}}
/>
</div>
</Popover>
);
};
export const useToolAction = (props: ToolActionProps) => {
const { skillModal: SkillModal, onSelect, initTool, isBanned } = props;
const [skillModalVisible, setSkillModalVisible] = useState(false);
const [selectedTool, setSelectedTool] = useState<ToolInfo | null>(
initTool || null,
);
const onToolChange = (toolApi: WorkFlowItemType | PluginApi | undefined) => {
const tooInfo = initToolInfoByToolApi(toolApi);
const { tool_params_list } = tooInfo || {};
if (!checkParams(tool_params_list ?? [])) {
return;
}
onSelect?.(tooInfo);
setSelectedTool(tooInfo);
setSkillModalVisible(false);
};
const checkParams = (parameters: Array<PluginParameter>) => {
const { isSuccess, inValidType } = validatePluginAndWorkflowParams(
parameters ?? [],
true,
);
if (isSuccess) {
return true;
}
if (inValidType === 'empty') {
Toast.error(I18n.t('shortcut_modal_add_plugin_wf_no_input_error'));
} else {
Toast.error(I18n.t('shortcut_modal_add_plugin_wf_complex_input_error'));
}
return false;
};
const open = () => {
onSelect?.(null);
setSkillModalVisible(true);
};
const cancel = () => {
onSelect?.(null);
setSelectedTool(null);
};
const action = (
<div className="mr-2 mt-[-2px]">
{selectedTool?.tool_type ? (
<ToolButton
toolInfo={selectedTool}
onCancel={cancel}
isBanned={isBanned}
/>
) : (
<ActionButton icon={<IconAdd />} onClick={open}>
{I18n.t('shortcut_modal_use_tool_select_button')}
</ActionButton>
)}
{skillModalVisible ? (
<SkillModal
tabs={getSkillModalTab()}
onCancel={() => setSkillModalVisible(false)}
openMode={OpenModeType.OnlyOnceAdd}
openModeCallback={onToolChange}
/>
) : null}
</div>
);
return {
action,
open,
cancel,
};
};

View File

@@ -0,0 +1,90 @@
/*
* 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, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useToolStore } from '@coze-agent-ide/tool';
import { I18n } from '@coze-arch/i18n';
import { type ShortcutFileInfo } from '@coze-arch/bot-api/playground_api';
import { FormInputWithMaxCount } from '../components';
import { type ShortcutEditFormValues } from '../../types';
import { validateButtonNameRepeat } from '../../../utils/tool-params';
import { ShortcutIconField } from './shortcut-icon';
export interface ButtonNameProps {
editedShortcut: ShortcutEditFormValues;
}
export const ButtonName: FC<ButtonNameProps> = props => {
const { existedShortcuts } = useToolStore(
useShallow(state => ({
existedShortcuts: state.shortcut.shortcut_list,
})),
);
const { editedShortcut } = props;
const [selectIcon, setSelectIcon] = useState<ShortcutFileInfo | undefined>(
editedShortcut.shortcut_icon,
);
return (
<FormInputWithMaxCount
className="p-1"
field="command_name"
placeholder={I18n.t('shortcut_modal_button_name_input_placeholder')}
prefix={
<ShortcutIconField
iconInfo={selectIcon}
field="shortcut_icon"
noLabel
fieldClassName="!pb-0"
onLoadList={list => {
// 如果是编辑状态不设置默认icon, 新增下默认选中列表第一个icon
const isEdit = !!editedShortcut.command_id;
if (isEdit) {
return;
}
const defaultIcon = list.at(0);
defaultIcon && setSelectIcon(defaultIcon);
}}
/>
}
suffix={<></>}
maxCount={20}
maxLength={20}
rules={[
{
required: true,
message: I18n.t('shortcut_modal_button_name_is_required'),
},
{
validator: (rule, value) =>
validateButtonNameRepeat(
{
...editedShortcut,
command_name: value,
},
existedShortcuts ?? [],
),
message: I18n.t('shortcut_modal_button_name_conflict_error'),
},
]}
noLabel
required
/>
);
};

View File

@@ -0,0 +1,114 @@
/*
* 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, useState } from 'react';
import cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozWarningCircle } from '@coze-arch/coze-design/icons';
import { Skeleton } from '@coze-arch/bot-semi';
import { type FileInfo } from '@coze-arch/bot-api/playground_api';
import { Icon } from './icon';
const SINGLE_LINE_LOADING_COUNT = 10;
export interface IconListProps {
list: FileInfo[];
initValue?: FileInfo;
onSelect: (item: FileInfo) => void;
onClear: (item: FileInfo) => void;
}
export const IconList: FC<IconListProps> = props => {
const { list, onSelect, onClear, initValue } = props;
const [selectIcon, setSelectIcon] = useState<FileInfo | undefined>(initValue);
const onIconClick = (item: FileInfo) => {
const { url } = item;
if (!url) {
return;
}
if (url === selectIcon?.url) {
setSelectIcon(undefined);
onClear(item);
return;
}
setSelectIcon(item);
onSelect(item);
};
return (
<div className="flex flex-wrap gap-1 p-4">
{list.map((item, index) => (
<div onClick={() => onIconClick?.(item)}>
<Icon
key={index}
icon={item}
className={cls({
'coz-mg-secondary-pressed': item.uri === selectIcon?.uri,
})}
/>
</div>
))}
</div>
);
};
export const AnimateLoading = () => (
<>
<SingleLoading />
<SingleLoading />
<SingleLoading />
</>
);
const SingleLoading = () => (
<div>
<Skeleton
active
loading
placeholder={
<div
style={{
display: 'flex',
gap: 12,
padding: 8,
}}
>
{Array.from({ length: SINGLE_LINE_LOADING_COUNT }).map((_, index) => (
<Skeleton.Image
key={index}
style={{
height: 28,
width: 28,
borderRadius: 6,
}}
/>
))}
</div>
}
/>
</div>
);
export const IconListField = () => (
<div className="flex justify-center items-center flex-col w-[420px] h-[148px]">
<IconCozWarningCircle className="mb-4 w-8 h-8 coz-fg-hglt-red" />
<div className="coz-fg-secondary text-xs">
{/*@ts-expect-error --替换*/}
{I18n.t('Connection failed')}
</div>
</div>
);

View File

@@ -0,0 +1,54 @@
/*
* 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 } from 'react';
import cls from 'classnames';
import { type FileInfo } from '@coze-arch/bot-api/playground_api';
import DefaultIcon from '../../../../assets/shortcut-icon-default.svg';
export interface ShortcutIconProps {
icon?: FileInfo;
className?: string;
width?: number;
height?: number;
}
const DEFAULT_ICON_SIZE = 28;
const DefaultIconInfo = {
url: DefaultIcon,
};
export const Icon: FC<ShortcutIconProps> = props => {
const { icon, width, height, className } = props;
return (
<div className="flex items-center">
<img
className={cls(
'rounded-[6px] p-1 coz-mg-primary hover:coz-mg-secondary-hovered mr-1 cursor-pointer',
className,
)}
style={{
width: width ?? DEFAULT_ICON_SIZE,
height: height ?? DEFAULT_ICON_SIZE,
}}
alt="icon"
src={icon?.url || DefaultIconInfo.url}
/>
</div>
);
};

View File

@@ -0,0 +1,119 @@
/*
* 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, useEffect, useState } from 'react';
import cls from 'classnames';
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
import { Popover, withField } from '@coze-arch/bot-semi';
import { type FileInfo } from '@coze-arch/bot-api/playground_api';
import DefaultIcon from '../../../../assets/shortcut-icon-default.svg';
import { useGetIconList } from './use-get-icon-list';
import { IconList, AnimateLoading, IconListField } from './icon-list';
import { Icon } from './icon';
export interface ShortcutIconProps {
iconInfo?: FileInfo;
onChange?: (iconInfo: FileInfo | undefined) => void;
onLoadList?: (list: FileInfo[]) => void;
}
const DefaultIconInfo = {
url: DefaultIcon,
};
export const ShortcutIcon: FC<ShortcutIconProps> = props => {
const { iconInfo: initIconInfo, onChange, onLoadList } = props;
const [iconListVisible, setIconListVisible] = useState(false);
const { iconList, loading, error } = useGetIconList();
const [selectIcon, setSelectIcon] = useState(
initIconInfo?.url ? initIconInfo : DefaultIconInfo,
);
const onSelectIcon = (item: FileInfo) => {
const { url } = item;
if (!url) {
return;
}
setSelectIcon(item);
setIconListVisible(false);
onChange?.(item);
};
const onClearIcon = () => {
setSelectIcon(DefaultIcon);
setIconListVisible(false);
onChange?.(undefined);
};
const IconListRender = () => {
if (error) {
return <IconListField />;
}
if (loading) {
return <AnimateLoading />;
}
return (
<IconList
initValue={selectIcon}
list={iconList}
onSelect={onSelectIcon}
onClear={onClearIcon}
/>
);
};
useEffect(() => {
if (loading) {
return;
}
onLoadList?.(iconList);
}, [loading]);
useEffect(() => {
initIconInfo && onSelectIcon(initIconInfo);
}, [initIconInfo]);
return (
<Popover
trigger="custom"
visible={iconListVisible}
onClickOutSide={() => setIconListVisible(false)}
position="bottomLeft"
spacing={{
x: 0,
y: 10,
}}
content={IconListRender()}
>
<div
className="flex items-center"
onClick={() => setIconListVisible(true)}
>
<Icon
icon={selectIcon}
width={22}
height={24}
className={cls({
'coz-mg-secondary-pressed': iconListVisible,
})}
/>
</div>
</Popover>
);
};
export const ShortcutIconField: FC<ShortcutIconProps & CommonFieldProps> =
withField(ShortcutIcon);

View File

@@ -0,0 +1,32 @@
/*
* 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 { useRequest } from 'ahooks';
import { GetFileUrlsScene } from '@coze-arch/bot-api/playground_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
export const useGetIconList = () => {
const { data, loading, error } = useRequest(
async () =>
await PlaygroundApi.GetFileUrls({
scene: GetFileUrlsScene.shorcutIcon,
}),
);
return {
iconList: data?.file_list ?? [],
loading,
error,
};
};

View File

@@ -0,0 +1,135 @@
/*
* 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 } from 'react';
import { I18n } from '@coze-arch/i18n';
import { InputType } from '@coze-arch/bot-api/playground_api';
import {
type SelectComponentTypeItem,
type TextComponentTypeItem,
type UploadComponentTypeItem,
} from '../types';
import { type UploadItemType } from '../../../../utils/file-const';
import { UploadField } from './upload-field';
import { SelectWithInputTypeField } from './select-field';
import { InputWithInputTypeField } from './input-field';
export interface ComponentDefaultChangeValue {
type: InputType.TextInput | UploadItemType;
value: string;
}
export interface ComponentDefaultValueProps {
field: string;
componentType:
| TextComponentTypeItem
| SelectComponentTypeItem
| UploadComponentTypeItem;
disabled?: boolean;
}
export const ComponentDefaultValue: FC<ComponentDefaultValueProps> = props => {
const { componentType, field, disabled = false } = props;
const { type } = componentType;
if (type === 'text') {
return (
<InputWithInputTypeField
noLabel
value={{
type: InputType.TextInput,
value: '',
}}
field={field}
noErrorMessage
placeholder={I18n.t(
'shortcut_modal_use_tool_parameter_default_value_placeholder',
)}
disabled={disabled}
/>
);
}
if (type === 'select') {
return (
<SelectWithInputTypeField
value={{
type: InputType.TextInput,
value: '',
}}
noLabel
style={{
width: '100%',
}}
field={field}
noErrorMessage
optionList={componentType.options.map(option => ({
label: option,
value: option,
}))}
disabled={disabled}
/>
);
}
if (type === 'upload') {
// 先置灰,后续放开上传默认值
return <UploadField />;
// return (
// <UploadDefaultValue
// noLabel
// field={field}
// acceptUploadItemTypes={componentType.uploadTypes}
// uploadItemConfig={{
// [InputType.UploadImage]: {
// maxSize: IMAGE_MAX_SIZE,
// },
// [InputType.UploadDoc]: {
// maxSize: FILE_MAX_SIZE,
// },
// [InputType.UploadTable]: {
// maxSize: FILE_MAX_SIZE,
// },
// [InputType.UploadAudio]: {
// maxSize: FILE_MAX_SIZE,
// },
// }}
// onChange={res => {
// const { default_value, default_value_type } = res
// ? convertComponentDefaultValueToFormValues(res)
// : {
// default_value: '',
// default_value_type: undefined,
// };
// return {
// value: default_value,
// type: default_value_type,
// };
// }}
// uploadFile={({ file, onError, onProgress, onSuccess }) => {
// getRegisteredPluginInstance?.({
// file,
// onProgress,
// onError,
// onSuccess,
// });
// }}
// />
// );
}
return <></>;
};

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 React, { type FC } from 'react';
import type { InputProps } from '@coze-arch/bot-semi/Input';
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
import { UIInput, withField } from '@coze-arch/bot-semi';
import { InputType } from '@coze-arch/bot-api/playground_api';
type InputWithInputTypeProps = {
value?: { type: InputType; value: string };
onChange?: (value: { type: InputType; value: string }) => void;
} & Omit<InputProps, 'value'>;
const MaxLength = 100;
const InputWithInputType: FC<InputWithInputTypeProps> = props => {
const { value, onChange, ...rest } = props;
return (
<UIInput
value={value?.value}
{...rest}
maxLength={MaxLength}
onChange={inputValue => {
const newValue = {
type: value?.type || InputType.TextInput,
value: inputValue,
};
onChange?.(newValue);
return newValue;
}}
/>
);
};
export const InputWithInputTypeField: FC<
InputWithInputTypeProps & CommonFieldProps
> = withField(InputWithInputType, {
valueKey: 'value',
onKeyChangeFnName: 'onChange',
});

View File

@@ -0,0 +1,123 @@
/*
* 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 { FileTypeEnum, getFileInfo } from '@coze-common/chat-core';
import { shortcut_command } from '@coze-arch/bot-api/playground_api';
import { type UploadItemConfig } from '../types';
import { acceptMap, type UploadItemType } from '../../../../utils/file-const';
type FileTypeEnumWithoutDefault = Exclude<
FileTypeEnum,
FileTypeEnum.DEFAULT_UNKNOWN
>;
const fileTypeToInputTypeMap: {
[key in FileTypeEnumWithoutDefault]: UploadItemType;
} = {
[FileTypeEnum.IMAGE]: shortcut_command.InputType.UploadImage,
[FileTypeEnum.AUDIO]: shortcut_command.InputType.UploadAudio,
[FileTypeEnum.PDF]: shortcut_command.InputType.UploadDoc,
[FileTypeEnum.DOCX]: shortcut_command.InputType.UploadDoc,
[FileTypeEnum.EXCEL]: shortcut_command.InputType.UploadTable,
[FileTypeEnum.CSV]: shortcut_command.InputType.UploadTable,
[FileTypeEnum.VIDEO]: shortcut_command.InputType.VIDEO,
[FileTypeEnum.PPT]: shortcut_command.InputType.PPT,
[FileTypeEnum.TXT]: shortcut_command.InputType.TXT,
[FileTypeEnum.ARCHIVE]: shortcut_command.InputType.ARCHIVE,
[FileTypeEnum.CODE]: shortcut_command.InputType.CODE,
};
export const getFileTypeFromInputType = (
inputType: shortcut_command.InputType,
) => {
for (const [fileType, type] of Object.entries(fileTypeToInputTypeMap)) {
if (type === inputType) {
return fileType;
}
}
return null;
};
export const getInputTypeFromFileType = (
fileType: FileTypeEnumWithoutDefault,
) => fileTypeToInputTypeMap[fileType];
export const getInputTypeFromFile = (file: File): UploadItemType | '' => {
const fileInfo = getFileInfo(file);
const fileType = fileInfo?.fileType;
if (!fileInfo) {
return '';
}
if (!fileType || fileType === FileTypeEnum.DEFAULT_UNKNOWN) {
return '';
}
return getInputTypeFromFileType(fileType);
};
// 判断文件是否超过最大限制
export const isOverMaxSizeByUploadItemConfig = (
file: File | undefined,
config: UploadItemConfig | undefined,
): {
isOverSize: boolean;
// 单位 MB
maxSize?: number;
} => {
if (!file) {
return {
isOverSize: false,
};
}
if (!config) {
return {
isOverSize: false,
};
}
const inputType = getInputTypeFromFile(file);
if (!inputType) {
return {
isOverSize: false,
};
}
const { maxSize } = config[inputType];
if (!maxSize) {
return {
isOverSize: false,
};
}
return {
isOverSize: file.size > maxSize * 1024,
maxSize,
};
};
// 根据acceptUploadItemTypes获取accept
export const getAcceptByUploadItemTypes = (
acceptUploadItemTypes: UploadItemType[],
) => {
const accept: string[] = [];
for (const type of acceptUploadItemTypes) {
if (!type) {
continue;
}
const acceptStr = acceptMap[type];
if (!acceptStr) {
continue;
}
accept.push(...acceptStr.split(','));
}
return accept.join(',');
};

View File

@@ -0,0 +1,54 @@
/*
* 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 { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
import { Select, withField } from '@coze-arch/bot-semi';
import { InputType } from '@coze-arch/bot-api/playground_api';
type InputWithInputTypeProps = {
value?: { type: InputType; value: string };
onSelect?: (value: { type: InputType; value: string }) => void;
} & Omit<React.ComponentProps<typeof Select>, 'value' | 'onSelect'>;
const SelectWithInputType: FC<InputWithInputTypeProps> = props => {
const { value, onSelect, ...rest } = props;
return (
<Select
{...rest}
showClear={!!value?.value}
onClear={() => {
onSelect?.({ type: InputType.TextInput, value: '' });
}}
value={value?.value}
onSelect={selectValue => {
const newValue = {
type: value?.type || InputType.TextInput,
value: selectValue as string,
};
onSelect?.(newValue);
return newValue;
}}
/>
);
};
export const SelectWithInputTypeField: FC<
InputWithInputTypeProps & CommonFieldProps
> = withField(SelectWithInputType, {
valueKey: 'value',
onKeyChangeFnName: 'onSelect',
});

View File

@@ -0,0 +1,19 @@
/*
* 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 { UIInput } from '@coze-arch/bot-semi';
export const UploadField = () => <UIInput disabled />;

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 React, { type FC } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Tooltip } from '@coze-arch/coze-design';
import { Typography } from '@coze-arch/bot-semi';
import { IconInfo } from '@coze-arch/bot-icons';
import { type ToolInfo } from '@coze-arch/bot-api/playground_api';
const { Text } = Typography;
export interface ComponentParameterProps {
toolInfo: ToolInfo;
parameter: string;
}
export const ComponentParameter: FC<ComponentParameterProps> = ({
toolInfo,
parameter,
}) => {
const { tool_params_list = [] } = toolInfo;
const { name, type, required, desc } =
tool_params_list.find(item => item.name === parameter) || {};
return (
<div className="px-2 flex items-center justify-center coz-fg-secondary max-w-[86px]">
<Text className="mr-1" ellipsis>
{name}
</Text>
<Tooltip
className="max-w-[226px]"
content={
<div className="flex flex-col justify-center" key={name}>
<div className="flex items-center">
<Text
ellipsis={{
showTooltip: {
opts: {
content: name || '',
position: 'top',
},
},
}}
>
<span className="text-sm font-medium mr-[9px]">
{name || '-'}
</span>
</Text>
<span className="rounded coz-mg-primary px-[6px] py-[1px] mr-[3px]">
{type}
</span>
{Boolean(required) && (
<span className="rounded coz-mg-primary px-[6px] py-[1px]">
{I18n.t('workflow_add_parameter_required')}
</span>
)}
</div>
<span className="mt-[3px] coze-fg-primary text-sm">
{desc || '-'}
</span>
</div>
}
>
<IconInfo />
</Tooltip>
</div>
);
};

View File

@@ -0,0 +1,12 @@
.upload-content {
:global {
.semi-checkbox-inner {
.semi-checkbox-inner-display {
}
}
.semi-checkbox-content {
flex: 0 auto;
}
}
}

View File

@@ -0,0 +1,255 @@
/*
* 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, {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { Toast, UIInput, Popover, Form } from '@coze-arch/bot-semi';
import { IconChevronDown } from '@douyinfe/semi-icons';
import {
type ComponentTypeItem,
type ComponentTypeSelectContentRadioValueType,
type SelectComponentTypeItem,
type UploadComponentTypeItem,
} from '../types';
import { UploadContent } from './upload-contnet';
import { SelectContentField } from './select-content';
import { formatComponentTypeForm } from './method';
const { RadioGroup, Radio } = Form;
const SelectTypeAndLableMap: Record<
ComponentTypeSelectContentRadioValueType,
string
> = {
text: I18n.t('shortcut_component_type_text'),
select: I18n.t('shortcut_component_type_selector'),
upload: I18n.t('shortcut_modal_components_modal_upload_component'),
};
export const ComponentTypeSelectRecordItem = (props: {
value: ComponentTypeItem;
onSubmit?: (value: ComponentTypeItem) => void;
disabled?: boolean;
}) => {
const { value: defaultValue, onSubmit, disabled = false } = props;
const [submitValue, setSubmitValue] =
useState<ComponentTypeItem>(defaultValue);
const [componentType, setComponentType] =
useState<ComponentTypeItem>(defaultValue);
const [selectPopoverVisible, setSelectPopoverVisible] = useState(false);
const componentTypeSelectFormRef = useRef<{
formApi: ComponentTypeSelectFormMethods;
} | null>(null);
const onComponentTypeSelectFormSubmit = async () => {
if (await componentTypeSelectFormRef.current?.formApi.validate()) {
if (!componentType) {
return;
}
onSubmit?.(componentType);
setSubmitValue(componentType);
setSelectPopoverVisible(false);
}
};
useEffect(() => {
setSubmitValue(defaultValue);
}, [defaultValue]);
return (
<div className="flex items-center">
<div className="w-full">
<>
<Popover
trigger="custom"
footer={null}
visible={selectPopoverVisible}
position="topRight"
onClickOutSide={() => setSelectPopoverVisible(false)}
content={() => (
<div className="p-6 w-[288px]">
<ComponentTypeSelectForm
ref={componentTypeSelectFormRef}
value={submitValue}
onChange={setComponentType}
/>
<div className="flex justify-end gap-2">
<Button
color="highlight"
onClick={() => setSelectPopoverVisible(false)}
>
{I18n.t('cancel')}
</Button>
<Button onClick={onComponentTypeSelectFormSubmit}>
{I18n.t('Confirm')}
</Button>
</div>
</div>
)}
>
<UIInput
className={cls('w-full', disabled && '!pointer-events-auto')}
suffix={
<IconChevronDown
onClick={() => setSelectPopoverVisible(!selectPopoverVisible)}
/>
}
placeholder={I18n.t(
'shortcut_modal_selector_component_default_text',
)}
value={SelectTypeAndLableMap[submitValue?.type]}
onClick={() => setSelectPopoverVisible(!selectPopoverVisible)}
disabled={disabled}
readonly
/>
</Popover>
</>
</div>
</div>
);
};
export interface ComponentTypeSelectFormProps {
value: ComponentTypeItem;
onChange?: (values: ComponentTypeItem) => void;
}
export interface ComponentTypeSelectFormMethods {
validate: () => Promise<boolean>;
}
export const ComponentTypeSelectForm = forwardRef<
{ formApi?: ComponentTypeSelectFormMethods },
ComponentTypeSelectFormProps
>((props, ref) => {
const { value, onChange } = props;
const [selectOption, setSelectOption] =
useState<ComponentTypeSelectContentRadioValueType>(value.type);
const optionsMap = getComponentTypeOptionMap(value);
const formRef = useRef<Form>(null);
useImperativeHandle(ref, () => ({
formApi: {
validate: async () => {
try {
if (selectOption === 'select') {
return Boolean(
await formRef.current?.formApi.validate(['values.options']),
);
}
if (selectOption === 'upload') {
return Boolean(
await formRef.current?.formApi.validate(['values.uploadTypes']),
);
}
return true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (errors: any) {
if (selectOption === 'select') {
const message = errors?.values?.options;
message && Toast.error(message);
}
if (selectOption === 'upload') {
const message = errors?.values?.uploadTypes;
message && Toast.error(message);
}
return false;
}
},
},
}));
return (
<Form<{ values: ComponentTypeItem }>
autoComplete="off"
ref={formRef}
initValues={{ values: value }}
className="flex flex-col gap-6"
onValueChange={({ values }) => {
onChange?.(formatComponentTypeForm(values));
}}
>
<div className="coz-fg-plus text-[16px] font-medium">
{I18n.t('shortcut_modal_components_modal_component_type')}
</div>
<RadioGroup
fieldStyle={{
padding: 0,
}}
className="flex flex-col !p-0 gap-3"
defaultValue={selectOption}
field="values.type"
noLabel
onChange={e => {
setSelectOption(e.target.value);
}}
>
{Object.entries(optionsMap).map(([key, { label }]) => (
<Radio value={key}>{label}</Radio>
))}
</RadioGroup>
{Object.entries(optionsMap).map(([key, { render }]) => (
<div
key={key}
className={cls({
hidden: key !== selectOption,
})}
>
{render?.()}
</div>
))}
</Form>
);
});
const getComponentTypeOptionMap = (
initValue?: ComponentTypeItem,
): {
[key in ComponentTypeSelectContentRadioValueType]: {
label: string;
render?: () => React.ReactNode;
};
} => ({
text: {
label: I18n.t('shortcut_component_type_text'),
},
select: {
label: I18n.t('shortcut_component_type_selector'),
render: () => (
<SelectContentField
field="values.options"
value={(initValue as SelectComponentTypeItem)?.options}
/>
),
},
upload: {
label: I18n.t('shortcut_modal_components_modal_upload_component'),
render: () => (
<UploadContent
value={(initValue as UploadComponentTypeItem)?.uploadTypes}
/>
),
},
});

View File

@@ -0,0 +1,33 @@
/*
* 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 ComponentTypeItem } from '../types';
export const formatComponentTypeForm = (
values: ComponentTypeItem,
): ComponentTypeItem => {
const { type } = values;
if (type === 'text') {
return { type };
}
if (type === 'select') {
return { type, options: values.options };
}
if (type === 'upload') {
return { type, uploadTypes: values.uploadTypes };
}
return values;
};

View File

@@ -0,0 +1,220 @@
/*
* 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 Dispatch,
type SetStateAction,
useEffect,
useMemo,
useRef,
useState,
type FC,
} from 'react';
import { SortableList } from '@coze-studio/components/sortable-list';
import { type TItemRender } from '@coze-studio/components';
import { I18n } from '@coze-arch/i18n';
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
import {
IconButton,
Tooltip,
UIButton,
UIInput,
useFieldApi,
useFieldState,
withField,
} from '@coze-arch/bot-semi';
import {
IconAdd,
IconShortcutDisorder,
IconShortcutTrash,
} from '@coze-arch/bot-icons';
import { shortid } from '../../../../utils/uuid';
export interface OptionData {
value?: string;
id: string;
}
export interface OptionListProps {
options: OptionData[];
onChange: Dispatch<SetStateAction<OptionData[]>>;
}
const dndType = Symbol.for(
'chat-area-plugins-chat-shortcuts-component-options-dnd-list',
);
export const OptionsList: FC<OptionListProps> = ({ options, onChange }) => {
const sortable = options.length > 1;
const itemRender = useMemo<TItemRender<OptionData>>(
() =>
({ data, connect }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const dropRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line react-hooks/rules-of-hooks
const handleRef = useRef<HTMLDivElement>(null);
connect(dropRef, handleRef);
return (
<div ref={dropRef} className="flex items-center mb-6 last:mb-0">
<UIInput
className="flex-1"
value={data.value}
maxLength={20}
onChange={value => {
onChange(_options => {
const index = _options.findIndex(item => item.id === data.id);
_options.splice(index, 1, {
value,
id: data.id,
});
return [..._options];
});
}}
/>
<div className="ml-2" ref={handleRef}>
<IconButton
size="small"
className={sortable ? 'cursor-grab' : ''}
icon={<IconShortcutDisorder />}
disabled={!sortable}
theme="borderless"
type="tertiary"
/>
</div>
<div className="ml-2">
<Tooltip content={I18n.t('Remove')}>
<IconButton
size="small"
icon={<IconShortcutTrash />}
type="tertiary"
theme="borderless"
disabled={options.length <= 1}
onClick={() => {
onChange(_options =>
_options.filter(item => item.id !== data.id),
);
}}
/>
</Tooltip>
</div>
</div>
);
},
[sortable],
);
return (
<SortableList
type={dndType}
list={options}
itemRender={itemRender}
onChange={onChange}
enabled={sortable}
/>
);
};
const MAX_OPTIONS = 20;
export interface SelectContentProps {
value?: string[];
onChange?: (newOptions: string[]) => void;
hasError?: boolean;
}
export const SelectContent: FC<SelectContentProps> = ({
value: initialValue,
onChange,
hasError,
}) => {
const [options, setOptions] = useState<OptionData[]>([]);
useEffect(() => {
setOptions(
(initialValue?.length ? initialValue : ['']).map<OptionData>(item => ({
value: item,
id: shortid(),
})),
);
}, []);
useEffect(() => {
const values = options
.map(option => option.value?.trim())
.filter(value => !!value);
onChange?.(values as string[]);
}, [options]);
return (
<div className="flex flex-col items-start">
<div className="coz-fg-plus mb-[14px] font-medium">
{I18n.t('shortcut_modal_selector_component_options')}
</div>
<div className="flex justify-between">
<UIButton
size="small"
type="tertiary"
theme="borderless"
disabled={options.length >= MAX_OPTIONS}
icon={<IconAdd />}
className="!coz-fg-hglt text-sm font-medium"
onClick={() => {
setOptions([
...options,
{
value: '',
id: shortid(),
},
]);
}}
>
{I18n.t('shortcut_modal_selector_component_options')}
</UIButton>
</div>
<div className="max-h-40 my-6 overflow-y-auto">
<OptionsList options={options} onChange={setOptions} />
</div>
</div>
);
};
const SelectContentFieldInner = withField(SelectContent);
export const SelectContentField: FC<
CommonFieldProps & SelectContentProps
> = props => {
const state = useFieldState(props.field);
const api = useFieldApi(props.field);
return (
<div onMouseEnter={() => api.setError('')}>
<SelectContentFieldInner
{...props}
pure
hasError={!!state.error?.length}
trigger="custom"
rules={[
{
validator: (rules, value) => !!value?.length,
message: I18n.t(
'shortcut_modal_selector_component_no_options_error',
),
},
]}
/>
</div>
);
};

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 cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Form } from '@coze-arch/bot-semi';
import { InputType } from '@coze-arch/bot-api/playground_api';
import { type UploadComponentTypeItem } from '../types';
import { ACCEPT_UPLOAD_TYPES } from '../../../../utils/file-const';
import styles from './index.module.less';
export interface UploadContentProps {
value: UploadComponentTypeItem['uploadTypes'] | undefined;
onChange?: (value: UploadComponentTypeItem['uploadTypes']) => void;
}
const { Checkbox, CheckboxGroup } = Form;
const DefaultValue = [
InputType.UploadImage,
InputType.UploadAudio,
InputType.UploadDoc,
InputType.UploadTable,
InputType.CODE,
InputType.ARCHIVE,
InputType.PPT,
InputType.VIDEO,
InputType.TXT,
];
export const UploadContent = (props: UploadContentProps) => {
const { value = DefaultValue, onChange } = props;
return (
<>
<div className="coz-fg-plus text-[16px] font-medium">
{I18n.t('shortcut_modal_upload_component_supported_file_formats')}
</div>
<CheckboxGroup
field="values.uploadTypes"
onChange={checkedValues => {
onChange?.(checkedValues);
}}
initValue={value}
className={cls('flex flex-wrap flex-row', styles['upload-content'])}
noLabel
noErrorMessage
rules={[
{
validator: (rules, newValue) => !!newValue?.length,
message: I18n.t(
'shortcut_modal_please_select_file_formats_for_upload_component_tip',
),
},
]}
>
{ACCEPT_UPLOAD_TYPES.map(({ type, label, icon }) => (
<div key={type} className="flex-1 basis-1/2">
<Checkbox
className="flex-row-reverse justify-end"
noLabel
defaultChecked={value?.includes(type)}
value={type}
>
<div className="flex gap-1">
<img src={icon} alt={label} className="w-5 h-[25px] mr-2" />
{label}
</div>
</Checkbox>
</div>
))}
</CheckboxGroup>
</>
);
};

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import styles from './index.module.less';
export const tableEmpty = (useTool: boolean, selected: boolean) => (
<div className={styles.empty}>
{useTool
? selected
? I18n.t('shortcut_modal_skill_has_no_param_tip')
: I18n.t('shortcut_modal_skill_select_button')
: I18n.t('shortcut_modal_form_to_be_filled_up_tip')}
</div>
);

View File

@@ -0,0 +1,30 @@
.table {
padding: 0 12px;
border: 1px solid var(--coz-stroke-primary);
border-radius: 8px;
:global {
.semi-table-placeholder {
border-bottom: 0;
}
.semi-form-field {
padding: 4px 0;
}
.semi-table-tbody {
tr:first-child td {
padding-top: 8px;
}
tr:last-child td {
padding-bottom: 12px;
}
}
}
}
.empty {
color: var(--coz-fg-dim);
text-align: left;
}

View File

@@ -0,0 +1,242 @@
/*
* 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 { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { DndProvider } from '@coze-studio/components/dnd-provider';
import { type OnMove } from '@coze-studio/components';
import { I18n } from '@coze-arch/i18n';
import { type FormApi } from '@coze-arch/bot-semi/Form';
import { Form, Table, Toast, Tooltip } from '@coze-arch/bot-semi';
import { IconAdd } from '@coze-arch/bot-icons';
import {
InputType,
ToolType,
type ToolInfo,
type shortcut_command,
} from '@coze-arch/bot-api/playground_api';
import { compTip } from '../components/tip';
import FieldLabel from '../components/field-label';
import ActionButton from '../components/action-button';
import { shortid } from '../../../utils/uuid';
import { type ComponentsWithId } from './types';
import { getColumns, tableComponents } from './table-components';
import {
attachIdToComponents,
checkDuplicateName,
formatSubmitValues,
} from './method';
import { tableEmpty } from './empty';
import styles from './index.module.less';
export interface ComponentsTableProps {
components: shortcut_command.Components[]; // 变量列表
onChange?: (components: shortcut_command.Components[]) => void;
toolType?: shortcut_command.ToolType;
toolInfo: ToolInfo;
disabled: boolean;
}
export interface ComponentsTableActions {
validate: () => Promise<shortcut_command.Components[]>;
setValues: (values: shortcut_command.Components[]) => void;
}
const MAX_COMPONENTS = 10;
// 半受控组件,使用初始值 + 暴露 API 的方式,方便内部维护本地 id 用于拖拽排序标识数据
export const ComponentsTable = forwardRef<
{ formApi?: ComponentsTableActions },
ComponentsTableProps
// eslint-disable-next-line @coze-arch/max-line-per-function
>(({ components, onChange, toolType, disabled, toolInfo }, ref) => {
const [values, setValues] = useState<ComponentsWithId[]>(
attachIdToComponents(components),
);
const formRef = useRef<FormApi<{ values: ComponentsWithId[] }>>();
const onChangeInner = (newValues: ComponentsWithId[]) => {
setValues(newValues);
formRef.current?.setValues({ values: newValues }, { isOverride: true });
onChange?.(formatSubmitValues(newValues));
};
useImperativeHandle(ref, () => ({
formApi: formRef.current
? {
validate: async (...props) => {
// 在这里统一处理,避免多个相同字段触发多次 toast
if (
values.some(
component =>
component.input_type === InputType.Select &&
!component.options?.length,
)
) {
Toast.error(
I18n.t('shortcut_modal_selector_component_no_options_error'),
);
throw Error('shortcut_modal_selector_component_no_options_error');
}
if (
formRef.current &&
checkDuplicateName(values, formRef.current)
) {
throw Error('duplicated names');
}
const submitValues = await formRef.current?.validate(...props);
return formatSubmitValues(submitValues?.values ?? []);
},
setValues: newComponents => {
const newValues = attachIdToComponents(newComponents);
setValues(newValues);
formRef.current?.setValues(
{ values: newValues },
{ isOverride: true },
);
formRef.current?.setTouched('values', false);
formRef.current?.setError('values', '');
},
}
: undefined,
}));
const onMove: OnMove<string> = (sourceId, targetId, isBefore) => {
const newValues = [...values];
const sourceIndex = newValues.findIndex(source => source.id === sourceId);
const errors = formRef.current?.getError('values') || [];
const sourceError = errors.splice(sourceIndex, 1)[0];
const sourceItem = newValues.splice(sourceIndex, 1)[0];
const targetIndex =
newValues.findIndex(target => target.id === targetId) +
(isBefore ? 0 : 1);
sourceItem && newValues.splice(targetIndex, 0, sourceItem);
errors.splice(targetIndex, 0, sourceError);
// 前后 index 相同的情况不触发 onChange 避免频繁 rerender
if (sourceIndex !== targetIndex) {
onChangeInner(newValues);
// 只在拖拽排序后,需要手动更新 form value
formRef.current?.setValues(
{
values: newValues,
},
{ isOverride: true },
);
formRef.current?.setError('values', errors);
}
};
const showAdd =
toolType === undefined ||
![ToolType.ToolTypeWorkFlow, ToolType.ToolTypePlugin].includes(toolType);
const selected = !!toolInfo?.tool_name;
const oversize = values.length >= MAX_COMPONENTS;
const addBtn = (
<ActionButton
icon={<IconAdd />}
disabled={oversize || disabled}
onClick={() => {
onChangeInner([
...values,
{
id: shortid(),
input_type: InputType.TextInput,
},
]);
}}
>
{I18n.t('add')}
</ActionButton>
);
const tipBtn = oversize ? (
<Tooltip
content={I18n.t('shortcut_modal_max_component_tip', {
maxCount: MAX_COMPONENTS,
})}
>
{addBtn}
</Tooltip>
) : (
addBtn
);
return (
<div className="pb-6">
<div className="flex items-center justify-between pb-1.5">
<FieldLabel tip={compTip()}>
{I18n.t('shortcut_modal_components')}
</FieldLabel>
{showAdd ? tipBtn : null}
</div>
<DndProvider>
<Form<{ values: ComponentsWithId[] }>
initValues={{ values }}
// 手动触发校验,避免受增删和拖拽排序影响
trigger="custom"
autoComplete="off"
disabled={disabled}
getFormApi={api => (formRef.current = api)}
onValueChange={(newValues, changedValues) => {
const changedKeys = Object.keys(changedValues);
if (
changedKeys.length === 1 &&
// 只在表单修改场景下触发 onChange 避免无限循环
changedKeys[0]?.startsWith('values.[')
) {
onChangeInner([...newValues.values]);
// 只在编辑表单场景下对具体字段触发校验,其它场景(整行的增删排序)不触发校验
setTimeout(() => {
if (formRef.current) {
checkDuplicateName(newValues.values, formRef.current);
}
// @ts-expect-error semi 的类型定义无法支持多段 path
formRef.current?.validate([changedKeys[0]]);
});
}
}}
>
<div className={styles.table}>
<Table<ComponentsWithId>
dataSource={values}
size="small"
columns={getColumns({
components: values,
onChange: onChangeInner,
toolInfo,
toolType,
disabled,
})}
components={tableComponents}
pagination={false}
onRow={item => ({
id: item?.id ?? '',
sortable: (values?.length ?? 0) > 1 && !disabled,
onMove,
})}
empty={tableEmpty(!showAdd, selected)}
/>
</div>
</Form>
</DndProvider>
</div>
);
});

View File

@@ -0,0 +1,250 @@
/*
* 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 FormApi } from '@coze-arch/bot-semi/Form';
import {
InputType,
type shortcut_command,
type ToolInfo,
} from '@coze-arch/bot-api/playground_api';
import { shortid } from '../../../utils/uuid';
import { type UploadItemType } from '../../../utils/file-const';
import { type ComponentsWithId, type ComponentTypeItem } from './types';
const MAX_COMPONENTS = 10;
export const attachIdToComponents = (
components: shortcut_command.Components[],
): ComponentsWithId[] =>
components.map(item => ({
...item,
id: shortid(),
}));
export const formatSubmitValues = (
values: ComponentsWithId[],
): shortcut_command.Components[] =>
values.map(({ id, options, ...value }) => ({
...value,
options: value.input_type === InputType.Select ? options : [],
}));
export const checkDuplicateName = (
values: ComponentsWithId[],
formApi: FormApi,
) => {
const fieldMap: Record<string, number[]> = {};
values.forEach((item, index) => {
if (item.name) {
if (fieldMap[item.name]) {
fieldMap[item.name]?.push(index);
} else {
fieldMap[item.name] = [index];
}
}
});
setTimeout(() => {
// 避免修改后立刻被 field 自己的校验状态覆盖
Object.entries(fieldMap).forEach(([name, indexArray]) => {
const isDuplicated = indexArray.length > 1;
indexArray.forEach(index => {
formApi.setError(`values.${index}.name`, !isDuplicated);
});
});
});
return Object.entries(fieldMap).some(
([name, indexArr]) => indexArr.length > 1,
);
};
export interface SubmitComponentTypeFields {
input_type?: InputType;
options?: string[];
upload_options?: UploadItemType[];
}
export const getComponentTypeSelectFormInitValues = (): ComponentTypeItem => ({
type: 'text',
});
// 定义一个映射对象将ComponentTypeItem的type映射到对应的input_type和其他字段
const componentTypeHandlers = {
text: () => ({ input_type: InputType.TextInput }),
select: (value: ComponentTypeItem) => {
const { type } = value;
if (type !== 'select') {
return;
}
return {
input_type: InputType.Select,
options: value.options,
};
},
upload: (value: ComponentTypeItem) => {
if (value.type !== 'upload') {
return;
}
const { uploadTypes } = value;
if (uploadTypes.length > 1) {
return {
input_type: InputType.MixUpload,
upload_options: uploadTypes,
};
}
return {
input_type: uploadTypes.at(0) as InputType,
upload_options: undefined,
};
},
};
export const getSubmitFieldFromComponentTypeForm = (
values: ComponentTypeItem,
): SubmitComponentTypeFields => {
const { type } = values;
const handler = componentTypeHandlers[type];
const result = handler && handler(values);
if (result) {
return result;
}
// 如果没有找到处理函数,就返回默认值
return { input_type: InputType.TextInput };
};
// 是否是上传类型
export const isUploadType = (
type: InputType,
): type is
| InputType.UploadImage
| InputType.UploadDoc
| InputType.UploadTable
| InputType.UploadAudio
| InputType.CODE
| InputType.ARCHIVE
| InputType.PPT
| InputType.VIDEO
| InputType.TXT
| InputType.MixUpload =>
[
InputType.UploadImage,
InputType.UploadDoc,
InputType.UploadTable,
InputType.UploadAudio,
InputType.CODE,
InputType.ARCHIVE,
InputType.PPT,
InputType.VIDEO,
InputType.TXT,
InputType.MixUpload,
].includes(type);
// 将input_type映射到对应的处理函数
const inputTypeHandlers = {
[InputType.TextInput]: () => ({ type: 'text' }),
[InputType.Select]: (options: string[] = []) => ({
type: 'select' as const,
options,
}),
upload: (uploadTypes: UploadItemType[] = []) => ({
type: 'upload' as const,
uploadTypes,
}),
};
export const getComponentTypeFormBySubmitField = (
values: SubmitComponentTypeFields,
): ComponentTypeItem => {
const { input_type, options, upload_options } = values;
if (!input_type) {
return getComponentTypeSelectFormInitValues();
}
if (isUploadType(input_type)) {
const handler = inputTypeHandlers.upload;
return handler(upload_options);
}
const handler = inputTypeHandlers[input_type];
if (handler) {
return handler(options);
}
return getComponentTypeSelectFormInitValues();
};
/**
* 1. 修改components列表中对应组件的hidetrue
*/
export const modifyComponentWhenSwitchChange = ({
components,
record,
checked,
}: {
components: ComponentsWithId[];
record: ComponentsWithId;
checked: boolean;
}) =>
components.map(item => {
if (item.id === record.id) {
return {
...item,
hide: !checked,
};
}
return item;
});
// components switch是否disable
export const isSwitchDisabled = ({
components,
record,
toolInfo,
}: {
components: ComponentsWithId[];
record: ComponentsWithId;
toolInfo: ToolInfo;
}) => {
const { default_value } = record ?? {};
const isWithDefaultValue = !!default_value?.value;
const isRequired = (() => {
if (!toolInfo?.tool_name) {
return true;
}
/**
* 使用工具&为必填参数
*/
return !!toolInfo?.tool_params_list?.find(t => t.name === record.parameter)
?.required;
})();
// 组件超过最大数量, 不允许开启
const isMaxCount =
record.hide && components.filter(com => !com.hide).length >= MAX_COMPONENTS;
/** 必填且没有默认值不允许关闭 */
const isFinalRequired = isRequired && !isWithDefaultValue;
return isFinalRequired || isMaxCount;
};

View File

@@ -0,0 +1,335 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type RefObject,
useEffect,
useRef,
type FC,
type PropsWithChildren,
} from 'react';
import cs from 'classnames';
import { useDnDSortableItem } from '@coze-studio/components/sortable-list-hooks';
import { type OnMove } from '@coze-studio/components';
import { I18n } from '@coze-arch/i18n';
import { Switch } from '@coze-arch/coze-design';
import { type TooltipProps } from '@coze-arch/bot-semi/Tooltip';
import {
type ColumnProps,
type TableComponents,
} from '@coze-arch/bot-semi/Table';
import { Form, IconButton, Tooltip } from '@coze-arch/bot-semi';
import { IconShortcutTrash, IconSvgShortcutDrag } from '@coze-arch/bot-icons';
import {
shortcut_command,
type ToolInfo,
} from '@coze-arch/bot-api/playground_api';
import { type UploadItemType } from '../../../utils/file-const';
import { type ComponentsWithId } from './types';
import {
getComponentTypeFormBySubmitField,
getSubmitFieldFromComponentTypeForm,
isSwitchDisabled,
modifyComponentWhenSwitchChange,
} from './method';
import { ComponentTypeSelectRecordItem } from './component-type-select';
import { ComponentParameter } from './component-parameter';
import { ComponentDefaultValue } from './component-default-value';
type ColumnPropType = ColumnProps<ComponentsWithId>;
const TooltipWithDisabled: FC<TooltipProps & { disabled?: boolean }> = ({
disabled,
children,
...props
}) => (disabled ? <>{children}</> : <Tooltip {...props}>{children}</Tooltip>);
const getOperationColumns = ({
components,
onChange,
toolType,
disabled,
toolInfo,
}: GetColumnsParams): ColumnPropType => {
const deleteable = !disabled;
const showDelete = toolType === undefined;
return {
key: 'operation',
title: null,
width: showDelete ? '80px' : '40px',
render: (_, record) => (
<div className="flex items-center pl-[12px]">
<Switch
checked={!record.hide}
disabled={isSwitchDisabled({
components,
record,
toolInfo,
})}
size="mini"
onChange={checked =>
onChange?.(
modifyComponentWhenSwitchChange({
components,
record,
checked,
}),
)
}
/>
{showDelete ? (
<div className="px-2">
<TooltipWithDisabled
content={I18n.t('Remove')}
disabled={!deleteable}
>
<IconButton
size="small"
theme="borderless"
type="tertiary"
disabled={!deleteable}
icon={<IconShortcutTrash />}
onClick={() => {
onChange?.(components.filter(item => item.id !== record.id));
}}
/>
</TooltipWithDisabled>
</div>
) : null}
</div>
),
};
};
const getColumnsMap = (params: GetColumnsParams) => {
const { components, disabled } = params;
const sortable = components.length > 1 && !disabled;
return {
name: {
key: 'name',
title: (
<Form.Label
className="leading-5 p-0 m-0"
text={I18n.t('shortcut_modal_component_name')}
required
/>
),
width: 1,
render: (_, record, index) => (
<div className="flex items-center">
<div
id={handleId}
className={cs(
'px-[2px]',
sortable ? 'cursor-grab' : 'cursor-not-allowed',
)}
>
<IconSvgShortcutDrag />
</div>
<Form.Input
noLabel
maxLength={20}
field={`values.[${index}].name`}
noErrorMessage
placeholder={I18n.t('shortcut_modal_component_name')}
rules={[
{
required: true,
},
]}
disabled={disabled || record.hide}
/>
</div>
),
},
description: {
key: 'description',
title: (
<Form.Label
className="leading-5 p-0 m-0"
text={I18n.t('Description')}
/>
),
width: '190px',
render: (_, record, index) => (
<div className="pl-[2px]">
<Form.Input
noLabel
maxLength={100}
field={`values.[${index}].description`}
noErrorMessage
placeholder={I18n.t('Description')}
disabled={disabled || record.hide}
/>
</div>
),
},
inputType: {
key: 'input_type',
title: (
<Form.Label
className="leading-5 p-0 m-0"
text={I18n.t('shortcut_modal_component_type')}
required
/>
),
render: (_, record, index) => (
<div className="pl-[2px]">
<ComponentTypeSelectRecordItem
value={getComponentTypeFormBySubmitField({
input_type: record.input_type,
options: record.options,
upload_options: record.upload_options as UploadItemType[],
})}
disabled={disabled || record.hide}
onSubmit={value => {
const { input_type, options, upload_options } =
getSubmitFieldFromComponentTypeForm(value);
params?.onChange?.(
params.components.map((item, i) =>
i === index
? {
...item,
input_type,
options,
default_value: {
value: '',
},
upload_options,
}
: item,
),
);
}}
/>
</div>
),
},
defaultValue: {
key: 'default_value',
title: (
<Form.Label
className="leading-5 p-0 m-0"
text={I18n.t('shortcut_modal_use_tool_parameter_default_value')}
/>
),
render: (_, record, index) => (
<div className="pl-[2px] max-w-[136px]">
<ComponentDefaultValue
componentType={getComponentTypeFormBySubmitField({
input_type: record.input_type,
options: record.options,
upload_options: record.upload_options as UploadItemType[],
})}
field={`values.[${index}].default_value`}
disabled={disabled || record.hide}
/>
</div>
),
},
parameter: {
key: 'parameter',
title: (
<Form.Label
className="leading-5 p-0 m-0"
text={I18n.t('shortcut_modal_component_plugin_wf_parameter')}
/>
),
dataIndex: 'parameter',
render: text => (
<ComponentParameter toolInfo={params.toolInfo} parameter={text} />
),
},
operations: getOperationColumns(params),
} satisfies Record<string, ColumnPropType>;
};
interface GetColumnsParams {
components: ComponentsWithId[];
onChange?: (values: ComponentsWithId[]) => void;
toolType?: shortcut_command.ToolType;
disabled: boolean;
toolInfo: ToolInfo;
}
const assignWidth = (base: ColumnPropType, width: string | number) =>
Object.assign({}, base, { width });
export const getColumns = (params: GetColumnsParams): ColumnPropType[] => {
const { toolType } = params;
const columnsMap = getColumnsMap(params);
if (
toolType === shortcut_command.ToolType.ToolTypePlugin ||
toolType === shortcut_command.ToolType.ToolTypeWorkFlow
) {
return [
assignWidth(columnsMap.name, '103px'),
assignWidth(columnsMap.description, '103px'),
assignWidth(columnsMap.inputType, '103px'),
assignWidth(columnsMap.defaultValue, '126px'),
assignWidth(columnsMap.parameter, '86px'),
columnsMap.operations,
];
}
return [
assignWidth(columnsMap.name, '125px'),
assignWidth(columnsMap.description, '125px'),
assignWidth(columnsMap.inputType, '125px'),
assignWidth(columnsMap.defaultValue, '136px'),
columnsMap.operations,
];
};
const type = Symbol.for(
'chat-area-plugins-chat-shortcuts-components-table-item',
);
const handleId = 'chat-area-plugins-chat-shortcuts-components-drag-handle';
const DraggableBodyRow: FC<
PropsWithChildren<{
id: string;
sortable: boolean;
onMove: OnMove<string>;
}>
> = ({ id, onMove, children, sortable }) => {
// 因为 name 可能为空,这里拿 shortid 做一个兜底
const dropRef = useRef<HTMLElement>(null);
const { connect } = useDnDSortableItem<string>({
type,
id,
onMove,
enabled: sortable,
});
useEffect(() => {
// 为了避免复杂的跨组件传值,这里稍微直接操作一下 DOM ,非常抱歉
const handleRef = {
current: (dropRef.current?.querySelector(`#${handleId}`) ??
null) as HTMLElement | null,
};
connect(dropRef, handleRef);
}, []);
return <tr ref={dropRef as RefObject<HTMLTableRowElement>}>{children}</tr>;
};
export const tableComponents = {
body: {
// semi-ui 导出的类型定义非常不负责任
row: DraggableBodyRow,
},
} as unknown as TableComponents;

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 { type shortcut_command } from '@coze-arch/bot-api/playground_api';
import { type UploadItemType } from '../../../utils/file-const';
import { type FileValue } from '../../../components/short-cut-panel/widgets/types';
export type ComponentsWithId = shortcut_command.Components & { id: string };
export type ComponentTypeSelectContentRadioValueType =
| 'text'
| 'select'
| 'upload';
export interface BaseComponentTypeItem {
type: ComponentTypeSelectContentRadioValueType;
}
export interface TextComponentTypeItem extends BaseComponentTypeItem {
type: 'text';
}
export interface SelectComponentTypeItem extends BaseComponentTypeItem {
type: 'select';
options: string[];
}
export interface UploadComponentTypeItem extends BaseComponentTypeItem {
type: 'upload';
uploadTypes: UploadItemType[];
}
export type ComponentTypeItem =
| TextComponentTypeItem
| SelectComponentTypeItem
| UploadComponentTypeItem;
export type TValue = string | FileValue | undefined;
export type TCustomUpload = (uploadParams: {
file: File;
onProgress?: (percent: number) => void;
onSuccess?: (url: string, width?: number, height?: number) => void;
onError?: (e: { status?: number }) => void;
}) => void;
export type UploadItemConfig = {
[key in UploadItemType]: {
maxSize?: number;
};
};

View File

@@ -0,0 +1,45 @@
/*
* 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 { Form } from '@coze-arch/bot-semi';
import style from './index.module.less';
// TODO: hzf, 取名component有点奇怪
export type FormInputWithMaxCountProps = {
maxCount: number;
} & React.ComponentProps<typeof Form.Input>;
// input后带上suffix表示能够输入的最大字数
export const FormInputWithMaxCount = (props: FormInputWithMaxCountProps) => {
const [count, setCount] = React.useState(0);
const handleChange = (v: string) => {
setCount(v.length);
};
const countSuffix = (
<div
className={style['form-input-with-count']}
>{`${count}/${props.maxCount}`}</div>
);
return (
<Form.Input
{...props}
onChange={value => handleChange(value)}
suffix={countSuffix}
/>
);
};

View File

@@ -0,0 +1,17 @@
.btn {
:global {
.semi-icon {
color: var(--coz-fg-secondary);
}
button {
padding: 2px 8px;
}
.semi-button-content-right {
margin-left: 4px;
font-weight: 500;
color: var(--coz-fg-secondary);
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type ReactNode, type PropsWithChildren } from 'react';
import { UIIconButton } from '@coze-arch/bot-semi';
import styles from './index.module.less';
const ActionButton: FC<
PropsWithChildren<{
icon: ReactNode;
onClick?: () => void;
disabled?: boolean;
}>
> = ({ onClick, icon, children, disabled }) => (
<UIIconButton
icon={icon}
wrapperClass={styles.btn}
onClick={onClick}
disabled={disabled}
>
{children}
</UIIconButton>
);
export default ActionButton;

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 React, { type FC, type PropsWithChildren, type ReactNode } from 'react';
import cs from 'classnames';
import { Tooltip, type TooltipProps } from '@coze-arch/coze-design';
import { Form } from '@coze-arch/bot-semi';
import { IconInfo } from '@coze-arch/bot-icons';
export const FieldLabel: FC<
PropsWithChildren<{
className?: string;
tooltip?: TooltipProps;
tip?: ReactNode;
required?: boolean;
}>
> = ({ children, className, tooltip, tip, required = false }) => (
<div className="flex items-center mb-[6px]">
<Form.Label
text={children}
className="!coz-fg-primary !text-[14px] !leading-[20px] !m-0"
required={required}
/>
{!!tip && (
<Tooltip content={tip} {...tooltip}>
<IconInfo className={cs('coz-fg-secondary ml-[-12px]', className)} />
</Tooltip>
)}
</div>
);
export default FieldLabel;

View File

@@ -0,0 +1,67 @@
/*
* 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';
const style = {
color: 'var(--semi-color-primary-hover)',
};
const getVar = (text: string) => (
<span style={style}>
{'{{'}
{text}
{'}}'}
</span>
);
const var1 = getVar(
I18n.t('shortcut_modal_query_message_hover_tip_component_mode_var1'),
);
const var2 = getVar(
I18n.t('shortcut_modal_query_message_hover_tip_component_mode_var2'),
);
export const queryTip = () => (
<div className="p[16px] leading-[16px] text-[12px] font-normal coz-fg-secondary">
<h2 className="m-0 mb-[12px] text-[14px] font-medium leading-[20px] coz-fg-plus">
{I18n.t('shortcut_modal_query_message_hover_tip_title')}
</h2>
<ul className="pl-[12px]">
<li>
{I18n.t('shortcut_modal_query_message_hover_tip_send_query_mode')}
</li>
<li>
{I18n.t('shortcut_modal_query_message_hover_tip_component_mode', {
var1,
var2,
})}
</li>
</ul>
<p>
<span className="coz-fg-hglt-red w-[12px] inline-block">*</span>
{I18n.t(
'shortcut_modal_query_message_hover_tip_how_to_insert_components',
)}
</p>
</div>
);
export const compTip = () =>
I18n.t('shortcut_modal_components_hover_tip', {
var1,
var2,
});

View File

@@ -0,0 +1,479 @@
@ide-tool-prefix: chat-studio-tool-content-block;
.shortcut-tool-config {
padding-right: 0;
padding-left: 0;
:global {
.@{ide-tool-prefix}-content {
/* stylelint-disable declaration-no-important */
padding-right: 0 !important;
padding-left: 0 !important;
}
.semi-modal-body {
padding: 24px 0;
}
}
}
.shortcut-list {
display: flex;
flex-direction: column;
}
.shortcut-item {
display: flex;
place-content: center space-between;
height: 48px;
margin-bottom: 4px;
padding: 8px;
background: rgba(6, 7, 9, 8%);
border-radius: 8px;
&:hover {
background: rgba(6, 7, 9, 16%);
border: 1px solid rgba(6, 7, 9, 10%);
border-radius: 8px;
}
}
.shortcut-item_title {
overflow: hidden;
font-size: 12px;
font-weight: 500;
font-style: normal;
line-height: 16px;
color: rgba(6, 7, 9, 80%);
text-overflow: ellipsis;
}
.shortcut-item_content {
overflow: hidden;
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: rgba(6, 7, 9, 50%);
text-overflow: ellipsis;
}
.operation {
display: flex;
align-items: center;
justify-content: space-between;
}
.operation-item-icon {
cursor: pointer;
:global {
.semi-icon {
svg {
width: 14px;
height: 14px;
}
}
}
}
.operation-item-icon_hover {
&:hover {
background: rgba(6, 7, 9, 16%);
border-radius: 6px;
}
}
.delete-modal {
:global {
.semi-modal {
border-radius: 8px;
.semi-modal-content {
padding: 16px;
border-radius: 8px;
.semi-modal-header {
margin: 0;
}
.semi-modal-footer {
margin: 24px 0 0;
}
}
}
}
}
.delete-common-modal-button-style {
min-width: 56px;
height: 32px;
padding: 6px 16px;
font-size: 14px;
font-weight: 500;
font-style: normal;
line-height: 20px;
background: rgba(6, 7, 9, 8%);
border-radius: 8px;
}
.delete-modal-cancel-button {
.delete-common-modal-button-style;
color: rgba(6, 7, 9, 80%);
}
.delete-modal-ok-button {
.delete-common-modal-button-style;
color: #cc1424;
background-color: rgba(255, 115, 127, 20%);
border-radius: 8px;
&:hover {
background-color: rgba(255, 115, 127, 80%) !important;
}
}
.edit-form-wrapper {
overflow-y: auto;
flex: 1 1 auto;
&::-webkit-scrollbar-thumb {
background-color: var(--coz-fg-dim);
border-radius: 3px;
}
&::-webkit-scrollbar {
width: 4px;
background: transparent;
}
.shortcut-action-item {
overflow: hidden;
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px;
color: rgba(6, 7, 9, 80%);
text-overflow: ellipsis;
white-space: nowrap;
.use-tool-checkbox {
margin-top: 4px;
:global {
.semi-form-field {
padding: 0;
}
.semi-modal-body {
padding-top: 24px;
padding-bottom: 0;
}
}
}
}
.shortcut-action-radio-group {
:global {
/* stylelint-disable */
.semi-radio-cardRadioGroup .semi-radio-addon,
.semi-checkbox-addon {
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px;
color: rgba(6, 7, 9, 80%);
}
}
}
:global {
.semi-form-field-label {
margin-bottom: 6px;
font-size: 12px;
font-weight: 500;
font-style: normal;
line-height: 16px;
color: rgba(6, 7, 9, 50%);
}
.semi-input::placeholder,
.semi-input-textarea::placeholder {
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px;
color: rgba(6, 7, 9, 30%);
}
}
}
.tool-params-table {
padding-bottom: 24px;
:global {
.semi-table-small
.semi-table-tbody
> .semi-table-row
> .semi-table-row-cell {
max-width: 200px;
padding: 12px 8px;
}
}
.table-title {
display: flex;
align-items: center;
justify-content: space-between;
}
.params-value-component_name {
margin-left: 8px;
}
.params-value {
display: flex;
align-items: center;
}
.params-name-content {
.params-name {
display: flex;
gap: 8px;
align-items: center;
justify-content: flex-start;
height: 16px;
margin-bottom: 6px;
}
.name {
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px;
color: rgba(6, 7, 9, 80%);
}
.params-field {
flex: 0 0 auto;
padding: 1px 6px;
background: rgba(6, 7, 9, 4%);
border-radius: 4px;
}
}
.footer {
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: rgba(6, 7, 9, 50%);
}
}
.shortcut-edit-modal {
:global {
.semi-modal-content {
background-color: var(--coz-bg-plus);
.semi-modal-header {
margin-bottom: 0;
}
.semi-modal-body {
padding: 24px 0;
}
}
// 使 query字段(VarQueryTextarea) 的 popover 能够展示全
.semi-modal-wrap,
.semi-modal-content,
.semi-modal-body {
overflow: visible !important;
}
}
}
.edit-modal-wrapper {
position: relative;
display: flex;
flex: 1;
align-items: stretch;
box-sizing: content-box;
min-height: 0;
// 12px + 4px
margin: 0 -16px 24px 0;
> form {
padding-right: 12px;
}
&.wrapper-border {
margin-right: 0;
padding-left: 24px;
border: 1px solid var(--coz-stroke-plus);
border-radius: 8px;
> form {
padding-top: 18px;
}
}
.preview-component {
overflow-y: auto;
display: flex;
width: 400px;
margin-left: 8px;
background-color: var(--coz-bg-primary);
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
animation: ease-in;
.shortcut-panel {
width: 100%;
margin-top: auto;
margin-bottom: 0;
}
}
:global {
.semi-form {
flex: 1;
}
}
}
.hidden {
display: none;
}
.form-item {
:global {
.semi-input-textarea-counter {
padding-right: 8px;
}
.semi-radioGroup-vertical {
row-gap: unset;
}
.semi-radio-cardRadioGroup,
.semi-radio-cardRadioGroup_checked,
.semi-radio-cardRadioGroup_hover {
margin-bottom: 4px;
padding: 0;
background: unset;
border: 0;
&:hover {
margin-bottom: 4px;
padding: 0;
background: unset;
border: 0;
}
}
.semi-form-field {
padding-top: 0;
padding-bottom: 16px;
}
}
}
.form-input-with-count {
overflow: hidden;
padding-right: 8px;
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: rgba(6, 7, 9, 50%);
text-overflow: ellipsis;
}
.remove-popover-content {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 320px;
padding: 16px;
.title {
margin-bottom: 4px;
font-size: 16px;
font-weight: 500;
line-height: 22px;
color: rgba(6, 7, 9, 96%);
}
.desc {
margin-bottom: 24px;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: rgba(6, 7, 9, 50%);
}
.delete-btn {
display: flex;
gap: 6px;
align-items: center;
align-self: flex-end;
justify-content: center;
min-width: 56px;
height: 32px;
padding: 6px 16px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: #fff;
background: #f22435;
border-radius: 8px;
}
:global {
.semi-button-light:not(.semi-button-disabled):hover {
background-color: #ba0010;
}
.semi-button-light:not(.semi-button-disabled):active {
background-color: #b0000f;
}
}
}
.switch-agent-input-wrapper {
:global {
.semi-portal-inner {
width: 100%;
}
.semi-form-vertical .semi-form-field {
padding-top: 0;
}
}
}

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 { useState } from 'react';
import { type ShortcutEditModalProps, ShortcutEditModal } from './modal';
export { ShortcutEditModal, ShortcutEditModalProps };
export const useShortcutEditModal = (
props: Omit<ShortcutEditModalProps, 'onClose'>,
) => {
const [visible, setVisible] = useState(false);
const close = () => {
setVisible(false);
props.setErrorMessage('');
};
const open = () => {
setVisible(true);
};
return {
node: visible ? <ShortcutEditModal {...props} onClose={close} /> : null,
close,
open,
};
};

View File

@@ -0,0 +1,291 @@
/*
* 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 ShortCutCommand } from '@coze-agent-ide/tool-config';
import {
type Components,
InputType,
SendType,
type ToolParams,
} from '@coze-arch/bot-api/playground_api';
import type { ShortcutEditFormValues } from '../types';
import { initToolEnabledByToolTYpe } from '../../utils/tool-params';
import { getDSLFromComponents } from '../../utils/dsl-template';
export const getSubmitValue = (
values: ShortcutEditFormValues,
): ShortCutCommand => {
const newValues = { ...values };
/**
* 最先执行 根据是否包含 components_list 设置 send_type
*/
mutableSendType(newValues);
const { send_type, use_tool = false } = newValues;
mutableFormatCommandName(newValues);
mutableSetCardSchemaForForm(newValues);
if (send_type === SendType.SendTypeQuery && !use_tool) {
mutableInitQueryFormValues(newValues);
} else {
mutableModifyToolParamsWhenComponentChange(newValues);
}
if (!use_tool) {
mutableInitNotUseToolFormValues(newValues);
}
// TODO: hzf干掉不合理
return newValues as ShortCutCommand;
};
const mutableSendType = (value: ShortcutEditFormValues) => {
if (value?.components_list?.length) {
value.send_type = SendType.SendTypePanel;
} else {
value.send_type = SendType.SendTypeQuery;
}
};
/**
* 为了兼容,需要在修改components_list的default_value,hide的时候同步修改toolParams
* 1.components_list.hide => !toolParams.refer_component
* 2.components_list.default_value => refer_componentfalse && toolParams.default_value
*/
const mutableModifyToolParamsWhenComponentChange = (
value: ShortcutEditFormValues,
): void => {
const { components_list, tool_info: { tool_params_list } = {} } = value;
if (!components_list || !tool_params_list) {
return;
}
components_list.forEach(com => {
const { default_value, hide } = com;
const targetToolParams = findToolParamsByComponent(tool_params_list, com);
if (!targetToolParams) {
return;
}
targetToolParams.refer_component = !hide;
targetToolParams.default_value = hide ? default_value?.value : '';
});
};
export const findToolParamsByComponent = (
params: Array<ToolParams>,
component: Components,
) => params?.find(param => param.name === component.parameter);
// 初始化query类型的表单参数
const mutableInitQueryFormValues = (values: ShortcutEditFormValues): void => {
values.tool_type = undefined;
values.plugin_id = '';
values.work_flow_id = '';
values.plugin_api_name = '';
values.tool_info = {
tool_name: '',
tool_params_list: [],
};
values.components_list = [];
values.card_schema = '';
};
// 初始化使用插件组件的时候表单参数
const mutableInitNotUseToolFormValues = (
values: ShortcutEditFormValues,
): void => {
values.tool_type = undefined;
values.plugin_id = '';
values.work_flow_id = '';
values.plugin_api_name = '';
values.tool_info = {
tool_name: '',
tool_params_list: [],
};
values.components_list?.forEach(com => {
com.parameter = '';
});
};
const mutableSetCardSchemaForForm = (values: ShortcutEditFormValues): void => {
const { components_list } = values;
const templateDsl = components_list
? getDSLFromComponents(components_list)
: '';
values.card_schema = JSON.stringify(templateDsl);
};
const mutableFormatCommandName = (values: ShortcutEditFormValues): void => {
const { shortcut_command } = values;
if (shortcut_command) {
values.shortcut_command = `/${shortcut_command.trim()}`;
}
};
/**
* 筛选toolParams存在,components中不存在的变量
* 并且refer_component=false,
* 转化为components_list
* 用于向前兼容,旧指令中tool的默认参数放在toolParams中
*/
export const initComponentsListFromToolParams = (
components: Components[],
toolParams: Array<ToolParams>,
): Array<Components> => {
const newComponents = components.slice();
toolParams.forEach(param => {
const { name, default_value, desc, refer_component } = param;
if (!components.find(com => com.parameter === name)) {
newComponents.push({
name,
description: desc,
parameter: name,
input_type: InputType.TextInput,
hide: !refer_component,
default_value: {
type: InputType.TextInput,
value: default_value,
},
});
}
});
return newComponents;
};
/**
* 兼容旧指令
* 如果InputType为 UploadImage, UploadDoc, UploadTable, UploadAudio,
* 判断upload_options是否为空
* 为空,加上对应的upload_options
*/
export const initComponentsUploadOptions = (
components: Components[],
): Components[] =>
components.map(com => {
const { input_type, upload_options } = com;
if (
!upload_options?.length &&
input_type &&
[
InputType.UploadImage,
InputType.UploadDoc,
InputType.UploadTable,
InputType.UploadAudio,
].includes(input_type)
) {
return {
...com,
upload_options: [input_type],
};
}
return com;
});
export const getInitialValues = (
initShortcut?: ShortCutCommand,
): ShortcutEditFormValues => {
// 初始化
if (!initShortcut) {
return {
send_type: SendType.SendTypeQuery,
use_tool: false,
};
}
// 回显
const {
shortcut_command,
tool_type,
components_list,
tool_info: { tool_params_list = [] } = {},
} = initShortcut;
const modifyComponentsListByToolParams = initComponentsListFromToolParams(
components_list ?? [],
tool_params_list,
);
const modifyComponentsListByUploadOptions = initComponentsUploadOptions(
modifyComponentsListByToolParams,
);
return {
...initShortcut,
shortcut_command: shortcut_command?.replace(/^\//, ''),
use_tool: initToolEnabledByToolTYpe(tool_type),
components_list: modifyComponentsListByUploadOptions,
};
};
export const enableSendTypePanelHideTemplate = (shortcut?: ShortCutCommand) => {
if (shortcut?.send_type !== SendType.SendTypePanel) {
return false;
}
const { tool_params_list, tool_name } = shortcut?.tool_info ?? {};
if (tool_name) {
return (
!!tool_params_list?.length &&
tool_params_list.every(c => !c.refer_component)
);
}
return (
!!shortcut?.components_list?.length &&
shortcut.components_list.every(c => c.hide)
);
};
export const getFormValueFromShortcut = (shortcut?: ShortCutCommand) => {
const { tool_params_list, tool_name } = shortcut?.tool_info ?? {};
if (tool_name) {
if (!tool_params_list?.length) {
return {};
}
return tool_params_list.reduce((prev: Record<string, string>, curr) => {
const key = curr.name;
const defaultValue = curr?.default_value;
if (!key || !defaultValue) {
return prev;
}
prev[key] = defaultValue;
return prev;
}, {});
}
if (!shortcut?.components_list?.length) {
return {};
}
return shortcut.components_list.reduce(
(prev: Record<string, string>, curr) => {
const key = curr.name;
const { value } = curr?.default_value ?? {};
if (!key || !value) {
return prev;
}
prev[key] = value;
return prev;
},
{},
);
};

View File

@@ -0,0 +1,348 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {
type Dispatch,
type FC,
type SetStateAction,
useEffect,
useRef,
useState,
} from 'react';
import { useShallow } from 'zustand/react/shallow';
import cls from 'classnames';
import { useRequest } from 'ahooks';
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
import { useMultiAgentStore } from '@coze-studio/bot-detail-store/multi-agent';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { Form, UIModal, type UIModalProps } from '@coze-arch/bot-semi';
import { PluginStatus } from '@coze-arch/bot-api/plugin_develop';
import { ToolType, BotMode } from '@coze-arch/bot-api/playground_api';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { type ShortcutEditFormValues, type SkillsModalProps } from '../types';
import {
validateCmdString,
validateCommandNameRepeat,
} from '../../utils/tool-params';
import { ShortcutTemplate } from '../../shortcut-template';
import { SwitchAgent } from './switch-agent';
import { getInitialValues, getSubmitValue } from './method';
import { FieldLabel } from './components/field-label';
import { FormInputWithMaxCount } from './components';
import { ButtonName } from './button-name';
import {
ActionSwitchArea,
type IActionSwitchAreaRef,
} from './action-switch-area';
import style from './index.module.less';
export interface ShortcutEditModalProps
extends Omit<UIModalProps, 'onOk' | 'onCancel'> {
errorMessage: string;
setErrorMessage: Dispatch<SetStateAction<string>>;
shortcut?: ShortCutCommand;
skillModal: FC<SkillsModalProps>;
onAdd?: (shortcuts: ShortCutCommand, onFail: () => void) => void;
onEdit?: (shortcuts: ShortCutCommand, onFail: () => void) => void;
botMode: BotMode;
onClose: () => void;
}
// eslint-disable-next-line @coze-arch/max-line-per-function
export const ShortcutEditModal: FC<ShortcutEditModalProps> = props => {
const {
errorMessage,
setErrorMessage,
shortcut,
onAdd,
onEdit,
skillModal: SkillModal,
onClose,
botMode,
} = props;
const { botId, spaceId } = useBotInfoStore(
useShallow(state => ({
botId: state.botId,
spaceId: state.space_id,
})),
);
const { agents } = useMultiAgentStore(
useShallow(state => ({
agents: state.agents,
})),
);
const { existedShortcuts } = useBotSkillStore(
useShallow(state => ({
existedShortcuts: state.shortcut.shortcut_list,
})),
);
const formRef = useRef<Form>(null);
const modalRef = useRef<HTMLDivElement>(null);
const actionSwitchAreaRef = useRef<IActionSwitchAreaRef>(null);
const { TextArea } = Form;
const [confirmLoading, setConfirmLoading] = useState(false);
const [editedShortcut, setEditedShortcut] = useState<ShortcutEditFormValues>(
getInitialValues(shortcut),
);
// 使用技能 & 未选择工具 => 禁止提交
const disableSubmit =
editedShortcut?.use_tool && !editedShortcut?.tool_info?.tool_name;
const showPanel = !!editedShortcut?.components_list?.filter(
comp => !comp.hide,
).length;
const mode = shortcut ? 'edit' : 'create';
const onConfirm = async () => {
setConfirmLoading(true);
if (!(await checkFormValid())) {
setConfirmLoading(false);
return;
}
const values = formRef.current?.formApi.getValues();
const formattedValues = getSubmitValue(values);
console.log('onConfirm', formattedValues);
if (mode === 'create') {
// TODO: hzf add的类型应该没有command_id
onAdd?.(formattedValues, () => {
setConfirmLoading(false);
});
return;
}
if (mode === 'edit') {
onEdit?.(formattedValues, () => {
setConfirmLoading(false);
});
}
};
const checkFormValid = async () => {
try {
await formRef.current?.formApi.validate();
return actionSwitchAreaRef.current?.validate();
// eslint-disable-next-line @coze-arch/use-error-in-catch -- 正常表单校验不需要处理e
} catch (e) {
return false;
}
};
const onFormValueChange = (values?: ShortcutEditFormValues) => {
setErrorMessage('');
if (!values) {
return;
}
setEditedShortcut({ ...values });
};
useEffect(() => {
formRef.current?.formApi.setValue('object_id', botId);
}, []);
const { data: pluginData } = useRequest(
async () => {
// 方便类型推断
if (shortcut?.plugin_id && spaceId) {
const res = await PluginDevelopApi.GetPlaygroundPluginList({
page: 1,
size: 1,
plugin_ids: [shortcut.plugin_id],
space_id: spaceId,
is_get_offline: true,
});
return res.data?.plugin_list?.[0];
}
},
{
ready: !!(shortcut?.plugin_id && spaceId),
},
);
const isBanned =
shortcut?.tool_type === ToolType.ToolTypePlugin &&
shortcut?.plugin_id === pluginData?.id &&
pluginData?.status === PluginStatus.BANNED;
return (
<>
<UIModal
{...props}
visible
footer={null}
onCancel={onClose}
className={style['shortcut-edit-modal']}
bodyStyle={{
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
minHeight: 0,
}}
width={showPanel ? 1120 : 670}
title={
mode === 'create'
? I18n.t('shortcut_modal_title')
: I18n.t('shortcut_modal_title_edit_shortcut')
}
>
<div
className={cls(
style['edit-modal-wrapper'],
showPanel && style.wrapperBorder,
)}
ref={modalRef}
contentEditable={false}
>
<Form<ShortcutEditFormValues>
ref={formRef}
trigger="blur"
initValues={editedShortcut}
autoComplete={'off'}
autoScrollToError
className={cls(style['edit-form-wrapper'], {
'pr-6': showPanel,
})}
onValueChange={values => onFormValueChange(values)}
>
<div className={style['form-item']}>
<FieldLabel required>
{I18n.t('shortcut_modal_button_name')}
</FieldLabel>
<ButtonName editedShortcut={editedShortcut} />
</div>
<div className={style['form-item']}>
<FieldLabel
tip={I18n.t('shortcut_modal_shortcut_name_input_placeholder')}
required
>
{I18n.t('shortcut_modal_shortcut_name')}
</FieldLabel>
<FormInputWithMaxCount
required
noLabel
prefix="/"
maxCount={20}
maxLength={20}
field="shortcut_command"
placeholder={I18n.t(
'shortcut_modal_shortcut_name_input_placeholder',
)}
rules={[
{
required: true,
message: I18n.t('shortcut_modal_shortcut_name_is_required'),
},
{
validator: (rule, value) => validateCmdString(value),
message: I18n.t(
'shortcut_modal_use_at_least_one_letter_error',
),
},
{
validator: (rule, value) =>
validateCommandNameRepeat(
{
...editedShortcut,
shortcut_command: `/${value}`,
},
existedShortcuts ?? [],
),
message: I18n.t(
'shortcut_modal_shortcut_name_conflict_error',
),
},
]}
/>
</div>
<div className={style['form-item']}>
<FieldLabel>
{I18n.t('shortcut_modal_shortcut_description')}
</FieldLabel>
<TextArea
maxCount={100}
maxLength={100}
rows={3}
placeholder={I18n.t(
'shortcut_modal_shortcut_description_input_placeholder',
)}
field="description"
noLabel
/>
</div>
<ActionSwitchArea
ref={actionSwitchAreaRef}
editedShortcut={editedShortcut}
skillModal={SkillModal}
formRef={formRef}
modalRef={modalRef}
isBanned={isBanned}
/>
{botMode === BotMode.MultiMode && (
<SwitchAgent
editedShortcut={editedShortcut}
showPanel={showPanel}
agents={agents}
formRef={formRef}
/>
)}
</Form>
{showPanel && editedShortcut ? (
<div className={style['preview-component']}>
<div className={style['shortcut-panel']}>
<ShortcutTemplate
visible={true}
shortcut={editedShortcut}
readonly
/>
</div>
</div>
) : null}
</div>
<div className="flex gap-2 justify-end">
<Form.ErrorMessage
className="flex-1 text-left mt-0"
error={errorMessage || ''}
/>
<Button
onClick={onClose}
color="highlight"
className="!coz-mg-hglt !coz-fg-hglt"
>
{I18n.t('Cancel')}
</Button>
<Button
onClick={onConfirm}
loading={confirmLoading}
disabled={disableSubmit}
>
{I18n.t('Confirm')}
</Button>
</div>
</UIModal>
</>
);
};

View File

@@ -0,0 +1,252 @@
/*
* 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 RefObject, useMemo, useRef, useState } from 'react';
import cls from 'classnames';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { type Agent } from '@coze-studio/bot-detail-store';
import { I18n } from '@coze-arch/i18n';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { type Form, Popover, Typography, Input } from '@coze-arch/bot-semi';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { IconChevronDown } from '@douyinfe/semi-icons';
import styles from '../index.module.less';
import FieldLabel from '../components/field-label';
import type { ShortcutEditFormValues } from '../../types';
import { type ItemType } from '../../../utils/data-helper';
import { LoadMoreList } from '../../../components/load-more-list';
import SelectCheck from '../../../assets/select-check.png';
import AgentIcon from '../../../assets/agent-icon.png';
const { Text } = Typography;
const PAGE_SIZE = 10;
interface ResultData {
list: {
agentName: string;
agentId: string;
}[];
hasMore: boolean;
}
export interface SwitchAgentProps {
formRef: RefObject<Form>;
showPanel: boolean;
agents: Agent[];
editedShortcut: ShortcutEditFormValues;
}
export const SwitchAgent = (props: SwitchAgentProps) => {
const { formRef, showPanel, editedShortcut, agents } = props;
const inputRef = useRef<HTMLInputElement>(null);
const popRef = useRef<Popover>(null);
const [isShowLoadMoreList, setIsShowLoadMoreList] = useState(false);
const { defaultAgentId, defaultName } = useMemo(() => {
if (!editedShortcut.agent_id) {
return {
defaultAgentId: '',
// @ts-expect-error --后面替换
defaultName: I18n.t('Do not specify'),
};
}
return {
defaultAgentId: editedShortcut.agent_id,
defaultName:
agents.find(agent => agent.id === editedShortcut.agent_id)?.name ?? '',
};
}, [editedShortcut.agent_id]);
const [inputValue, setInputValue] = useState(defaultName);
return (
<>
<Popover
ref={popRef}
content={
<AgentLoadMoreList
defaultSelectedId={defaultAgentId}
showPanel={showPanel}
onSelect={item => {
formRef.current?.formApi.setValue('agent_id', item.agentId);
setInputValue(item.agentName);
setIsShowLoadMoreList(false);
}}
/>
}
keepDOM
onVisibleChange={setIsShowLoadMoreList}
autoAdjustOverflow={false}
position={'bottomLeft'}
trigger="custom"
visible={isShowLoadMoreList}
onClickOutSide={() => setIsShowLoadMoreList(false)}
onEscKeyDown={() => setIsShowLoadMoreList(false)}
>
<div
className={cls(
'w-full pb-[32px]',
styles['switch-agent-input-wrapper'],
)}
onClick={() => {
setIsShowLoadMoreList(!isShowLoadMoreList);
}}
>
<FieldLabel>
{I18n.t('multiagent_shortcut_modal_specify_node')}
</FieldLabel>
<Input
ref={inputRef}
suffix={<IconChevronDown />}
className="w-full hover:!coz-mg-secondary-hovered active:!coz-mg-secondary-pressed"
readonly={true}
value={inputValue}
/>
</div>
</Popover>
</>
);
};
interface AgentLoadMoreListProps {
onSelect: (item: ItemType<ResultData['list']>) => void;
defaultSelectedId?: string;
showPanel: boolean;
}
const AgentLoadMoreList = (props: AgentLoadMoreListProps) => {
const { onSelect, showPanel, defaultSelectedId } = props;
const [activeId, setActiveId] = useState('');
const [selectedId, setSelectedId] = useState('');
const botId = useBotInfoStore(state => state.botId);
const getSpaceId = () => useSpaceStore.getState().getSpaceId();
return (
<LoadMoreList<ItemType<ResultData['list']>>
defaultId={defaultSelectedId}
className={cls(
'max-h-[122px] p-1 overflow-y-auto cursor-pointer overflow-x-hidden',
styles['load-more-list'],
)}
style={{
width: showPanel ? '687px' : '590px',
}}
getId={item => item.agentId}
defaultList={[
{
agentId: '',
agentName: I18n.t(
'multiagent_shortcut_modal_specify_node_option_do_not_specify',
),
},
]}
getMoreListService={currentData => {
const page = currentData
? Math.ceil(currentData.list.length / PAGE_SIZE) + 1
: 1;
return getAgentList({
page,
pageSize: PAGE_SIZE,
botId,
spaceId: getSpaceId(),
});
}}
onActiveId={id => setActiveId(id)}
onSelect={item => {
setSelectedId(item.agentId);
}}
itemRender={item => (
<AgentItem
onClick={onSelect}
activeId={activeId}
selectedId={selectedId}
data={item}
/>
)}
/>
);
};
interface AgentItemProps {
activeId: string;
selectedId: string;
data: ItemType<ResultData['list']>;
onClick?: (item: ItemType<ResultData['list']>) => void;
}
const AgentItem = (renderProps: AgentItemProps) => {
const { onClick, activeId, data, selectedId } = renderProps;
return (
<div
className={cls(
'flex justify-start p-2 items-center h-8 cursor-pointer w-full',
{
'rounded border coz-stroke-plus coz-mg-secondary-hovered':
activeId === data.agentId,
},
)}
onClick={() => onClick?.(data)}
>
<img
alt="checked"
src={SelectCheck}
className="w-4 h-4 ml-2"
style={{
visibility: [activeId, selectedId].includes(data.agentId)
? 'visible'
: 'hidden',
}}
/>
<img alt="icon" src={AgentIcon} className="mr-2 w-4 h-4 ml-2" />
<Text ellipsis className="w-full coz-fg-primary text-sm">
{data.agentName}
</Text>
</div>
);
};
const getAgentList = async (props: {
botId: string;
spaceId: string;
page: number;
pageSize: number;
}): Promise<ResultData> => {
try {
const { botId, spaceId, page, pageSize } = props;
const res = await PlaygroundApi.GetShortcutAvailNodes({
bot_id: botId,
space_id: spaceId,
page_num: page,
page_size: pageSize,
});
const { nodes, has_more } = res.data;
return {
list: nodes.map(item => ({
agentName: item.agent_name,
agentId: item.agent_id,
})),
hasMore: has_more,
};
} catch (e) {
console.error('getAgentListError', e);
return {
list: [],
hasMore: false,
};
}
};

View File

@@ -0,0 +1,159 @@
/*
* 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,
useEffect,
useState,
type Dispatch,
type SetStateAction,
forwardRef,
useImperativeHandle,
useRef,
} from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Popover, Typography } from '@coze-arch/bot-semi';
import { type shortcut_command } from '@coze-arch/bot-api/playground_api';
import { InputTypeTag } from './var-list';
import { componentTypeOptionMap } from './util';
export interface ComponentsSelectPopoverProps {
autoFocusFirst?: boolean;
visible: boolean;
components?: ValidComponents[];
onClose: () => void;
onChange: (component: ValidComponents) => void;
}
export interface ComponentsSelectPopoverActions {
setHover: Dispatch<SetStateAction<number>>;
select: () => void;
}
export type ValidComponents = shortcut_command.Components &
Required<Pick<shortcut_command.Components, 'input_type' | 'name'>>;
export const ComponentsSelectPopover = forwardRef<
ComponentsSelectPopoverActions,
PropsWithChildren<ComponentsSelectPopoverProps>
>(
(
{ components = [], visible, onChange, children, onClose, autoFocusFirst },
ref,
) => {
useImperativeHandle(ref, () => ({
setHover: param => {
const newIndex = typeof param === 'number' ? param : param(hoverIndex);
const targetDom = optionsDomRef.current[newIndex];
setHoverIndex(newIndex);
if (targetDom) {
targetDom.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
},
select: () => {
const hoverComponent = components[hoverIndex];
hoverComponent && onChange(hoverComponent);
onClose();
},
}));
const optionsDomRef = useRef<(HTMLDivElement | null)[]>([]);
const [hoverIndex, setHoverIndex] = useState(-1);
useEffect(() => {
if (visible) {
setHoverIndex(autoFocusFirst ? 0 : -1);
}
}, [visible]);
return (
<Popover
visible={visible}
trigger="custom"
onEscKeyDown={onClose}
onClickOutSide={onClose}
onVisibleChange={newVisible => {
if (!newVisible) {
onClose();
}
}}
position="bottom"
className="!rounded-[8px]"
content={
<div
onMouseLeave={() => {
setHoverIndex(-1);
}}
className="p-1 max-h-44 overflow-y-auto box-border"
>
{components
.filter(item => item.input_type !== undefined && item.name)
.map((item, index) => {
const type = componentTypeOptionMap[item.input_type]?.label;
return (
<div
key={item.name}
ref={el => {
optionsDomRef.current[index] = el;
}}
onMouseEnter={() => setHoverIndex(index)}
className={classNames(
'flex items-center px-2 h-8 gap-2 cursor-pointer rounded-[4px]',
{
'coz-mg-secondary-hovered': index === hoverIndex,
},
)}
onClick={() => {
onChange(item);
onClose();
}}
>
<Typography.Text
ellipsis={{
showTooltip: true,
}}
className="flex-1"
>
{item.name}
</Typography.Text>
{type ? <InputTypeTag>{type}</InputTypeTag> : null}
</div>
);
})}
{!components.length && (
<Typography.Text
ellipsis={{
showTooltip: true,
}}
style={{ color: 'rgba(29, 28, 35, 0.35)' }}
className="flex-1 p-2.5 coz-fg-secondary text-xs"
>
{I18n.t('shortcut_modal_query_message_insert_component_button')}
</Typography.Text>
)}
</div>
}
>
{children}
</Popover>
);
},
);

View File

@@ -0,0 +1,127 @@
/*
* 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,
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import cs from 'classnames';
import {
ExpressionEditorEvent,
ExpressionEditorRender,
type ExpressionEditorTreeNode,
} from '@coze-workflow/sdk';
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
import { VarExpressionEditorSuggestion } from './suggestion';
import { VarExpressionEditorModel } from './model';
import styles from './index.module.less';
export interface ExpressionEditorContainerProps {
value: string;
getPopupContainer?: PopoverProps['getPopupContainer'];
variableTree: ExpressionEditorTreeNode[];
onChange?: (value: string) => void;
placeholder?: string;
readonly?: boolean;
style?: CSSProperties;
className?: string;
}
export interface ExpressionEditorContainerRef {
model: VarExpressionEditorModel;
}
const ExpressionEditorContainer = forwardRef<
ExpressionEditorContainerRef,
ExpressionEditorContainerProps
>((props, ref) => {
const {
variableTree,
placeholder,
onChange,
readonly = false,
style,
className,
getPopupContainer,
} = props;
const [focus, _setFocus] = useState<boolean>(false);
const containerRef = useRef<HTMLDivElement>(null);
const formValue: string = props.value || '';
const [model] = useState<VarExpressionEditorModel>(
() => new VarExpressionEditorModel(formValue),
);
useImperativeHandle(ref, () => ({ model }));
useEffect(() => model.setVariableTree(variableTree), [variableTree]);
useEffect(() => model.setFocus(focus), [focus]);
// 同步表单值变化
useEffect(() => {
if (model.value === formValue) {
// 无需同步
return;
}
model.setValue(formValue);
}, [formValue]);
useEffect(() => {
const disposer = model.on<ExpressionEditorEvent.Change>(
ExpressionEditorEvent.Change,
(params: { value: string }) => onChange?.(params.value),
);
return () => {
disposer();
};
}, []);
if (!model?.variableTree) {
return null;
}
return (
<div
className={cs(className, styles.container)}
style={style}
ref={containerRef}
>
<ExpressionEditorRender
model={model}
className={styles.editorRender}
readonly={readonly}
placeholder={placeholder}
/>
{readonly ? null : (
<VarExpressionEditorSuggestion
model={model}
containerRef={containerRef}
getPopupContainer={getPopupContainer}
/>
)}
</div>
);
});
export default ExpressionEditorContainer;

View File

@@ -0,0 +1,156 @@
/*
* 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,
type RefObject,
useMemo,
useRef,
useState,
} from 'react';
import { type ExpressionEditorTreeNode } from '@coze-workflow/sdk';
import { I18n } from '@coze-arch/i18n';
import { type CommonFieldProps } from '@coze-arch/coze-design';
import { withField, UIIconButton, useFormState } from '@coze-arch/bot-semi';
import { IconCopyLink } from '@coze-arch/bot-icons';
import { type shortcut_command } from '@coze-arch/bot-api/playground_api';
import { queryTip } from '../components/tip';
import FieldLabel from '../components/field-label';
import btnStyles from '../components/action-button/index.module.less';
import { type VarTreeNode } from './type';
import type { ExpressionEditorContainerRef } from './container';
import {
ComponentsSelectPopover,
type ValidComponents,
} from './components-select';
import VarQueryTextarea, { type UsageWithVarTextAreaProps } from '.';
const VarQueryTextareaWithField: FC<
CommonFieldProps & UsageWithVarTextAreaProps
> = withField(VarQueryTextarea);
type VProps = CommonFieldProps & Pick<UsageWithVarTextAreaProps, 'value'>;
interface VarQueryTextareaWrapper extends VProps {
components?: shortcut_command.Components[];
modalRef?: RefObject<HTMLDivElement>;
}
const maxCount = 3000;
const VarQueryTextareaWrapperWithField: FC<VarQueryTextareaWrapper> = props => {
const { components, modalRef, ...innerProps } = props;
const { value, field } = props;
const [showLinBtnPopup, setShowLinkBtnPopup] = useState(false);
const editorRef = useRef<ExpressionEditorContainerRef>(null);
const { errors } = useFormState();
const isErrorStatus = !!(field && errors && field in errors);
const validComponents = useMemo(() => {
if (!components?.length) {
return [];
}
return components.filter(
(item): item is ValidComponents =>
item.input_type !== undefined && !!item.name,
);
}, [components]);
const variableList = validComponents.map(
({ name, input_type }) =>
({
label: name,
value: name,
key: name,
varInputType: input_type,
} satisfies VarTreeNode),
);
const hasComponents = !!validComponents?.length;
const placeholder = hasComponents
? I18n.t('shortcut_modal_query_message_placeholder')
: I18n.t('shortcut_modal_query_content_input_placeholder');
const onComponentsSelectChange = (component: ValidComponents) => {
const newValue = `${value ?? ''}{{${component.name}}}`;
editorRef.current?.model.insertText(newValue);
};
return (
<>
<div className="flex items-center justify-between">
<FieldLabel
tooltip={{ className: '!max-w-[370px]' }}
tip={queryTip()}
required
>
{I18n.t('shortcut_modal_query_content')}
</FieldLabel>
{hasComponents ? (
<ComponentsSelectPopover
visible={showLinBtnPopup}
components={validComponents}
onClose={() => {
setShowLinkBtnPopup(false);
}}
onChange={onComponentsSelectChange}
>
<UIIconButton
icon={<IconCopyLink />}
wrapperClass={btnStyles.btn}
onClick={() => {
setShowLinkBtnPopup(!showLinBtnPopup);
}}
>
{I18n.t('shortcut_modal_query_insert_component_tip')}
</UIIconButton>
</ComponentsSelectPopover>
) : null}
</div>
<VarQueryTextareaWithField
{...innerProps}
variableProps={{
variableList: variableList as ExpressionEditorTreeNode[],
getPopupContainer: () => modalRef?.current ?? document.body,
editorRef,
isErrorStatus,
}}
trigger={['blur', 'change']}
rules={[
{
required: true,
message: I18n.t('shortcut_modal_query_content_is_required'),
},
{
max: maxCount,
message: I18n.t(
'shortcut_modal_query_message_max_length_reached_error',
),
},
]}
placeholder={placeholder}
maxCount={maxCount}
rows={3}
fieldClassName="!pt-0 !pb-[16px]"
noLabel
/>
</>
);
};
export default VarQueryTextareaWrapperWithField;

View File

@@ -0,0 +1,78 @@
@height: calc(var(--studio-var-textarea-line-height) * 1px);
.editor-render {
cursor: text;
resize: none;
position: relative;
box-sizing: border-box;
width: 100%;
font-size: 14px;
line-height: 22px;
vertical-align: bottom;
background-color: transparent;
border: 0 solid transparent;
outline: none;
box-shadow: none;
}
.container {
position: relative;
}
.textarea {
box-sizing: border-box;
padding: 5px 12px;
line-height: 22px;
color: var(--semi-color-text-0);
background-color: var(--semi-color-white);
border: 1px var(--semi-color-border) solid;
border-radius: 8px;
&:hover {
background-color: var(--semi-color-fill-0);
}
&.focus {
background-color: var(--semi-color-fill-0);
border: 1px var(--semi-color-primary) solid;
}
&.error {
border: 1px var(--semi-color-danger) solid;
&:hover,
&.focus {
background-color: var(--semi-color-danger-light-default);
}
}
}
.scroller {
overflow-y: auto;
height: @height;
:global {
div > div > div[data-slate-editor='true'] {
min-height: @height !important;
}
}
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
box-sizing: border-box;
height: 24px;
padding: 3px 0 5px;
font-size: 12px;
color: var(--semi-color-text-2);
}

View File

@@ -0,0 +1,111 @@
/*
* 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,
type RefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { debounce } from 'lodash-es';
import cs from 'classnames';
import { type ExpressionEditorTreeNode } from '@coze-workflow/sdk';
import { type TextAreaProps } from '@coze-arch/coze-design';
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
import { getCssVarStyle } from './util';
import ExpressionEditorContainer, {
type ExpressionEditorContainerRef,
} from './container';
import styles from './index.module.less';
export interface UsageWithVarTextAreaProps
extends Pick<
TextAreaProps,
'maxCount' | 'rows' | 'value' | 'style' | 'placeholder'
> {
onChange?: (value: string) => void;
variableProps?: {
variableList: ExpressionEditorTreeNode[];
getPopupContainer?: PopoverProps['getPopupContainer'];
editorRef?: RefObject<ExpressionEditorContainerRef>;
isErrorStatus?: boolean;
};
}
const debounceMs = 100;
const VarQueryTextarea: FC<UsageWithVarTextAreaProps> = props => {
const { maxCount, rows, style, value, onChange, placeholder, variableProps } =
props;
const {
variableList = [],
getPopupContainer,
editorRef: propEditorRef,
isErrorStatus = false,
} = variableProps ?? {};
const editorRef = useRef<ExpressionEditorContainerRef>(null);
const [focus, _setFocus] = useState<boolean>(false);
const showMaxCount = typeof maxCount === 'number';
const scroll = typeof rows === 'number';
const cssVarsStyle = getCssVarStyle({ rows, style });
const count = value ? value.length : 0;
useEffect(() => editorRef.current?.model.setFocus(focus), [focus]);
// 设置防抖防止 onFocus / onBlur 在点击时出现抖动
const setFocus = useCallback(
debounce((newFocusValue: boolean) => {
_setFocus(newFocusValue);
}, debounceMs),
[],
);
return (
<div
className={cs(
styles.textarea,
focus && styles.focus,
isErrorStatus && styles.error,
)}
style={cssVarsStyle}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
>
<div className={scroll ? styles.scroller : undefined}>
<ExpressionEditorContainer
ref={propEditorRef ?? editorRef}
value={value ?? ''}
onChange={onChange}
variableTree={variableList}
placeholder={placeholder}
getPopupContainer={getPopupContainer}
/>
</div>
{showMaxCount ? (
<div className={styles.footer}>
{count}/{maxCount}
</div>
) : null}
</div>
);
};
export default VarQueryTextarea;

View File

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

View File

@@ -0,0 +1,152 @@
/* stylelint-disable no-descending-specificity */
/* stylelint-disable declaration-no-important */
/* stylelint-disable max-nesting-depth */
.expression-editor-suggestion-pin {
position: absolute;
transform: translateY(-0.5rem);
width: 0;
height: 1.5rem;
}
.expression-editor-suggestion {
z-index: 1000;
overflow: auto;
width: 272px;
max-height: 236px;
background: var(--light-usage-bg-color-bg-3, #fff);
border: 0.5px solid rgba(153, 182, 255, 12%);
border-radius: 8px;
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 25%);
}
.expression-editor-suggestion-empty {
z-index: 1000;
background: #fff;
border: 0.5px solid rgba(153, 182, 255, 12%);
border-radius: 8px;
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 25%);
p {
margin: 4px 6px;
font-size: 12px;
font-weight: 600;
font-style: normal;
line-height: 16px;
color: var(--light-usage-text-color-text-2, rgba(29, 28, 35, 60%));
}
}
.expression-editor-suggestion-tree {
:global {
.semi-tree-search-wrapper {
display: none;
}
.semi-tree-option-list {
width: fit-content;
min-width: 100%;
padding: 6px 6px 6px 0;
li {
height: 32px;
margin-top: 4px;
&:first-child {
margin-top: 0;
}
}
.semi-tree-option {
pointer-events: none;
margin-left: 0;
background-color: transparent;
.semi-tree-option-indent,
.semi-tree-option-empty-icon {
display: none;
}
}
.semi-tree-option-label {
pointer-events: auto;
height: 32px;
padding: 0 4px;
border-radius: 4px;
&:hover,
&:active {
background-color: var(--coz-mg-secondary-hovered);
}
.semi-tree-option-label-text {
display: inline-block;
width: fit-content;
white-space: nowrap;
& span {
display: inline-block;
width: fit-content;
white-space: nowrap;
}
.semi-tree-option-highlight {
color: var(--light-usage-warning-color-warning, #ff9600);
}
}
}
.semi-tree-option-selected {
font-weight: 600;
color: var(--light-usage-primary-color-primary, #4d53e8);
}
.semi-tree-option-disabled {
.semi-tree-option-label {
cursor: not-allowed;
background: transparent;
}
.semi-icon + .semi-tree-option-label {
color: var(--light-usage-text-color-text-0, #1d1c23);
}
}
}
.semi-tree-option-empty-icon {
width: 16px;
}
.semi-tree-option-expand-icon {
pointer-events: auto;
width: 16px;
height: 16px;
margin-right: 0;
padding: 4px;
border-radius: 4px;
&:hover {
background: var(--light-usage-fill-color-fill-1, rgb(46 46 56 / 8%));
}
&:active {
background: var(--light-usage-fill-color-fill-2, rgb(46 46 56 / 12%));
}
svg {
width: 16px;
height: 16px;
}
}
}
}
.highlight-label {
color: var(--coz-fg-hglt-yellow);
}

View File

@@ -0,0 +1,196 @@
/*
* 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 RefObject,
type ReactNode,
useState,
useRef,
} from 'react';
import classNames from 'classnames';
import {
useListeners,
useSelectNode,
useKeyboardSelect,
useRenderEffect,
useSuggestionReducer,
type SelectorBoxConfigEntity,
type PlaygroundConfigEntity,
type ExpressionEditorModel,
type ExpressionEditorTreeNode,
} from '@coze-workflow/sdk';
import { type TreeProps } from '@coze-arch/bot-semi/Tree';
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
import { Popover, Tree } from '@coze-arch/bot-semi';
import { VarListItem, InputTypeTag } from '../var-list';
import { componentTypeOptionMap } from '../util';
import { type VarTreeNode } from '../type';
import styles from './index.module.less';
interface ExpressionEditorSuggestionProps {
className?: string;
model: ExpressionEditorModel;
containerRef: RefObject<HTMLDivElement>;
getPopupContainer?: PopoverProps['getPopupContainer'];
playgroundConfig?: PlaygroundConfigEntity;
selectorBoxConfig?: SelectorBoxConfigEntity;
treeProps?: Partial<TreeProps>;
}
/**
* 自动提示
*/
export const VarExpressionEditorSuggestion: FC<
ExpressionEditorSuggestionProps
> = props => {
const {
model,
containerRef,
className,
playgroundConfig,
selectorBoxConfig,
getPopupContainer = () => containerRef.current ?? document.body,
treeProps = {},
} = props;
const suggestionRef = useRef<HTMLDivElement>(null);
const [searchValue, setSearchValue] = useState('');
const treeRef = useRef<Tree>(null);
const suggestionReducer = useSuggestionReducer({
model,
entities: {
playgroundConfig,
selectorBoxConfig,
},
ref: {
container: containerRef,
suggestion: suggestionRef,
tree: treeRef,
},
});
const [state] = suggestionReducer;
const selectNode = useSelectNode(suggestionReducer);
useRenderEffect(suggestionReducer);
useListeners(suggestionReducer);
useKeyboardSelect(suggestionReducer, selectNode);
const renderLabel = (label?: ReactNode, data?: VarTreeNode) => {
if (typeof label !== 'string') {
return null;
}
const idx = label.indexOf(searchValue);
if (idx === -1) {
return label;
}
let tag: string | null = null;
if (typeof data?.varInputType === 'number') {
const text = componentTypeOptionMap[data.varInputType]?.label;
if (text) {
tag = text;
}
}
return (
<VarListItem>
<div>
{label.substring(0, idx)}
<span className={styles.highlightLabel}>{searchValue}</span>
{label.substring(idx + searchValue.length)}
</div>
{tag ? <InputTypeTag>{tag}</InputTypeTag> : null}
</VarListItem>
);
};
return (
<Popover
trigger="custom"
visible={state.visible}
keepDOM={true}
getPopupContainer={getPopupContainer}
content={
<>
<div
className={styles['expression-editor-suggestion-empty']}
style={{
display:
!state.visible || !state.emptyContent ? 'none' : 'inherit',
}}
>
<p>{state.emptyContent}</p>
</div>
<div
className={classNames(
className,
styles['expression-editor-suggestion'],
)}
ref={suggestionRef}
style={{
display:
!state.visible || state.emptyContent || state.hiddenDOM
? 'none'
: 'inherit',
}}
onClick={e => {
e.stopPropagation();
e.preventDefault();
}}
>
<Tree
{...treeProps}
key={state.key}
className={classNames(
styles['expression-editor-suggestion-tree'],
treeProps.className,
)}
showFilteredOnly
filterTreeNode
onChangeWithObject
ref={treeRef}
treeData={state.variableTree}
searchRender={false}
value={state.selected}
emptyContent={<></>}
onSelect={(key, selected, node) => {
selectNode(node as ExpressionEditorTreeNode);
}}
renderLabel={renderLabel}
onSearch={inputValue => {
setSearchValue(inputValue);
}}
/>
</div>
</>
}
>
<div
className={styles['expression-editor-suggestion-pin']}
style={{
top: state.rect?.top,
left: state.rect?.left,
}}
/>
</Popover>
);
};

View File

@@ -0,0 +1,22 @@
/*
* 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 TreeNodeData } from '@coze-arch/bot-semi/Tree';
import { type InputType } from '@coze-arch/bot-api/playground_api';
export interface VarTreeNode extends TreeNodeData {
varInputType?: InputType;
}

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 CSSProperties } from 'react';
import { I18n } from '@coze-arch/i18n';
import { InputType } from '@coze-arch/bot-api/playground_api';
export const studioVarTextareaLineHeightKey =
'--studio-var-textarea-line-height';
export const studioVarTextareaLineHeight = 22;
export const getCssVarStyle = (options?: {
rows?: number;
style?: CSSProperties;
}): CSSProperties | undefined => {
const { rows, style } = options ?? {};
if (typeof rows !== 'number') {
return style;
}
const vars = {
[studioVarTextareaLineHeightKey]: studioVarTextareaLineHeight * rows,
};
return {
...style,
...vars,
};
};
export const componentTypeOptionMap: Partial<
Record<
InputType,
{
label: string;
}
>
> = {
[InputType.TextInput]: {
label: I18n.t('shortcut_component_type_text'),
},
[InputType.Select]: {
label: I18n.t('shortcut_component_type_selector'),
},
[InputType.MixUpload]: {
label: I18n.t('shortcut_modal_components_modal_upload_component'),
},
};

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type PropsWithChildren, type FC } from 'react';
export const InputTypeTag: FC<PropsWithChildren> = ({ children }) => (
<span className="coz-mg-secondary-hovered rounded-[4px] h-[16px] text-[12px] coz-fg-primary px-[5px] leading-[16px]">
{children}
</span>
);
export const VarListItem: FC<PropsWithChildren> = ({ children }) => (
<div className="flex justify-between items-center px-[4px] text-[14px] font-normal coz-fg-primary">
{children}
</div>
);

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type WorkFlowItemType } from '@coze-studio/bot-detail-store';
import { type ToolParams } from '@coze-arch/bot-api/playground_api';
import { type Dataset } from '@coze-arch/bot-api/knowledge';
import type { PluginApi } from '@coze-arch/bot-api/developer_api';
import type { ShortCutCommand } from '@coze-agent-ide/tool-config';
export enum OpenModeType {
OnlyOnceAdd = 'only_once_add',
}
// TODO: hzf 两份定义?
export interface SkillsModalProps {
tabsConfig?: {
plugin?: {
list: PluginApi[];
onChange: (list: PluginApi[]) => void;
};
workflow?: {
list: WorkFlowItemType[];
onChange: (list: WorkFlowItemType[]) => void;
};
datasets?: {
list: Dataset[];
onChange: (list: Dataset[]) => void;
};
imageFlow?: {
list: WorkFlowItemType[];
onChange: (list: WorkFlowItemType[]) => void;
};
};
tabs: ('plugin' | 'workflow' | 'datasets' | 'imageFlow')[];
/** 打开弹窗模式:
* 默认不传
* only_once_add仅可添加一次后关闭并返回callback函数
*/
openMode?: OpenModeType;
openModeCallback?: (val?: PluginApi | WorkFlowItemType) => void;
onCancel?: () => void;
}
export interface ToolInfo {
tool_type: ShortCutCommand['tool_type'] | '';
tool_params_list: ToolParams[];
tool_name: string;
plugin_api_name?: string;
api_id?: string;
plugin_id?: string;
work_flow_id?: string;
}
export type ShortcutEditFormValues = Partial<ShortCutCommand> & {
use_tool: boolean;
};