feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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}}}`),
|
||||
) ?? []
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
.icon {
|
||||
margin-right: 4px;
|
||||
|
||||
> svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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'];
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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 <></>;
|
||||
};
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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(',');
|
||||
};
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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 />;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
.upload-content {
|
||||
:global {
|
||||
.semi-checkbox-inner {
|
||||
.semi-checkbox-inner-display {
|
||||
}
|
||||
}
|
||||
|
||||
.semi-checkbox-content {
|
||||
flex: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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列表中对应组件的hide:true
|
||||
*/
|
||||
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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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_component:false && 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;
|
||||
},
|
||||
{},
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'),
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user