feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
@@ -0,0 +1,31 @@
|
||||
import { mergeConfig } from 'vite';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
viteFinal: config =>
|
||||
mergeConfig(config, {
|
||||
plugins: [
|
||||
svgr({
|
||||
svgrOptions: {
|
||||
native: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,14 @@
|
||||
/** @type { import('@storybook/react').Preview } */
|
||||
const preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
# @coze-common/chat-area-plugins-chat-shortcuts
|
||||
|
||||
chat-area插件,用于配置bot相关的快捷指令
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
|
||||
import {
|
||||
useLoadMore,
|
||||
getNextActiveItem,
|
||||
getPreviousItem,
|
||||
} from '../../../src/hooks/shortcut-bar/use-load-more';
|
||||
|
||||
describe('useLoadMore', () => {
|
||||
it('should initialize with default values', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useLoadMore<ShortCutCommand>({
|
||||
getId: item => item.command_id,
|
||||
listRef: { current: null },
|
||||
getMoreListService: async () =>
|
||||
Promise.resolve({ list: [], hasMore: false }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.activeId).toBe('');
|
||||
expect(result.current.loadingMore).toBe(false);
|
||||
expect(result.current.data).toEqual({ list: [], hasMore: false });
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
await act(() => {});
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('should load more when reaching limit', async () => {
|
||||
const getMoreListService = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ list: [{ id: '2' }], hasMore: false });
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useLoadMore({
|
||||
getId: item => item.id,
|
||||
listRef: { current: null },
|
||||
getMoreListService,
|
||||
defaultList: [{ id: '1' }],
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.goNext();
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(getMoreListService).toHaveBeenCalled();
|
||||
expect(result.current.data.list).toEqual([{ id: '1' }, { id: '2' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextActiveItem', () => {
|
||||
it('should return next item and reach limit flag', () => {
|
||||
const result = getNextActiveItem({
|
||||
curItem: { id: '1' },
|
||||
list: [{ id: '1' }, { id: '2' }, { id: '3' }],
|
||||
getId: item => item.id,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ reachLimit: true, item: { id: '2' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPreviousItem', () => {
|
||||
it('should return previous item and reach limit flag', () => {
|
||||
const result = getPreviousItem({
|
||||
curItem: { id: '2' },
|
||||
list: [{ id: '1' }, { id: '2' }, { id: '3' }],
|
||||
getId: item => item.id,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ reachLimit: false, item: { id: '1' } });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContentType, useSendTextMessage } from '@coze-common/chat-area';
|
||||
import { sendTeaEvent, EVENT_NAMES } from '@coze-arch/bot-tea';
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
|
||||
import {
|
||||
useSendTextQueryMessage,
|
||||
useSendUseToolMessage,
|
||||
getTemplateQuery,
|
||||
getImageAndFileList,
|
||||
} from '../../src/hooks/shortcut';
|
||||
|
||||
const sendTextMessageMock = vi.fn();
|
||||
const sendMultimodalMessage = vi.fn();
|
||||
vi.mock('@coze-arch/bot-tea', () => ({
|
||||
sendTeaEvent: vi.fn(),
|
||||
EVENT_NAMES: {
|
||||
page_view: 'page_view',
|
||||
},
|
||||
}));
|
||||
vi.mock('../../src/shortcut-tool/shortcut-edit/method', () => ({
|
||||
enableSendTypePanelHideTemplate: vi.fn(),
|
||||
}));
|
||||
vi.mock('@coze-common/chat-area', () => ({
|
||||
useSendTextMessage: () => sendTextMessageMock,
|
||||
useSendMultimodalMessage: () => sendMultimodalMessage,
|
||||
ContentType: {
|
||||
Image: 'image',
|
||||
File: 'file',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@coze-common/chat-core', () => ({
|
||||
default: () => vi.fn(),
|
||||
getFileInfo: vi.fn().mockImplementation(file => {
|
||||
if (file.type === 'image/png') {
|
||||
return {
|
||||
fileType: 'image',
|
||||
};
|
||||
}
|
||||
return {
|
||||
fileType: 'file',
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockShortcut: ShortCutCommand = {
|
||||
command_id: '7374755905893793836',
|
||||
command_name: 'muti',
|
||||
components_list: [
|
||||
{
|
||||
name: 'news',
|
||||
description: 'Keywords to search for news, must in English',
|
||||
input_type: 0,
|
||||
parameter: 'q',
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
description: '',
|
||||
object_id: '7374633552917479468',
|
||||
plugin_api_name: 'getNews',
|
||||
plugin_id: '7373521805258014764',
|
||||
send_type: 1,
|
||||
shortcut_command: '/muti',
|
||||
template_query: '查询{{news}}',
|
||||
tool_type: 2,
|
||||
tool_info: {
|
||||
tool_name: 'News',
|
||||
tool_params_list: [
|
||||
{
|
||||
default_value: '',
|
||||
desc: 'Keywords to search for news, must in English',
|
||||
name: 'q',
|
||||
refer_component: true,
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
// @ts-expect-error -- test ignore
|
||||
tool_type: 2,
|
||||
},
|
||||
work_flow_id: '',
|
||||
};
|
||||
|
||||
describe('useSendTextQueryMessage', () => {
|
||||
it('should send text message with query template', () => {
|
||||
const sendTextMessage = useSendTextMessage();
|
||||
const sendTextQueryMessage = useSendTextQueryMessage();
|
||||
mockShortcut.tool_type = undefined;
|
||||
sendTextQueryMessage({
|
||||
queryTemplate: 'test',
|
||||
shortcut: mockShortcut,
|
||||
});
|
||||
expect(sendTextMessage).toHaveBeenCalledWith(
|
||||
{ text: 'test', mentionList: [] },
|
||||
'shortcut',
|
||||
{
|
||||
extendFiled: {
|
||||
device_id: expect.any(String),
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(sendTeaEvent).toHaveBeenCalledWith(EVENT_NAMES.shortcut_use, {
|
||||
show_panel: undefined,
|
||||
tool_type: undefined,
|
||||
use_components: true,
|
||||
});
|
||||
});
|
||||
it('should send modify message with onBeforeSend', () => {
|
||||
const sendTextQueryMessage = useSendTextQueryMessage();
|
||||
const defaultOptions = {
|
||||
extendFiled: {
|
||||
device_id: expect.any(String),
|
||||
},
|
||||
};
|
||||
const onBeforeSendMock = vi.fn().mockReturnValue({
|
||||
message: {
|
||||
payload: {
|
||||
text: 'modified query template',
|
||||
mention_list: [{ id: 123 }],
|
||||
},
|
||||
},
|
||||
options: {
|
||||
...defaultOptions,
|
||||
test: '123',
|
||||
},
|
||||
});
|
||||
sendTextQueryMessage({
|
||||
queryTemplate: 'test',
|
||||
onBeforeSend: onBeforeSendMock,
|
||||
shortcut: mockShortcut,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSendUseToolMessage', () => {
|
||||
it('should send multimodal message with shortcut command', () => {
|
||||
const sendUseToolMessage = useSendUseToolMessage();
|
||||
const shortcut = {
|
||||
command_id: '7374755905893793836',
|
||||
command_name: 'muti',
|
||||
components_list: [
|
||||
{
|
||||
name: 'news',
|
||||
description: 'Keywords to search for news, must in English',
|
||||
input_type: 0,
|
||||
parameter: 'q',
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
description: '',
|
||||
object_id: '7374633552917479468',
|
||||
plugin_api_name: 'getNews',
|
||||
plugin_id: '7373521805258014764',
|
||||
send_type: 1,
|
||||
tool_type: 2,
|
||||
shortcut_command: '/muti',
|
||||
template_query: '查询{{news}}',
|
||||
tool_info: {
|
||||
tool_name: 'News',
|
||||
tool_params_list: [
|
||||
{
|
||||
default_value: '',
|
||||
desc: 'Keywords to search for news, must in English',
|
||||
name: 'q',
|
||||
refer_component: true,
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
tool_type: 2,
|
||||
},
|
||||
work_flow_id: '',
|
||||
};
|
||||
const componentsFormValues = { news: '查询北京news' };
|
||||
// @ts-expect-error --单测忽略
|
||||
sendUseToolMessage({ shortcut, componentsFormValues });
|
||||
expect(sendMultimodalMessage).toHaveBeenCalled();
|
||||
expect(sendTeaEvent).toHaveBeenCalledWith(EVENT_NAMES.shortcut_use, {
|
||||
show_panel: true,
|
||||
tool_type: 2,
|
||||
use_components: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTemplateQuery', () => {
|
||||
it('should return query from template', () => {
|
||||
const shortcut = {
|
||||
command_id: '7374755905893793836',
|
||||
command_name: 'muti',
|
||||
components_list: [
|
||||
{
|
||||
name: 'news',
|
||||
description: 'Keywords to search for news, must in English',
|
||||
input_type: 0,
|
||||
parameter: 'q',
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
description: '',
|
||||
object_id: '7374633552917479468',
|
||||
plugin_api_name: 'getNews',
|
||||
plugin_id: '7373521805258014764',
|
||||
send_type: 1,
|
||||
shortcut_command: '/muti',
|
||||
template_query: '查询{{news}}',
|
||||
tool_info: {
|
||||
tool_name: 'News',
|
||||
tool_params_list: [
|
||||
{
|
||||
default_value: '',
|
||||
desc: 'Keywords to search for news, must in English',
|
||||
name: 'q',
|
||||
refer_component: true,
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
tool_type: 2,
|
||||
},
|
||||
work_flow_id: '',
|
||||
};
|
||||
const componentsFormValues = { news: '北京新闻' };
|
||||
// @ts-expect-error --单测忽略
|
||||
const result = getTemplateQuery(shortcut, componentsFormValues);
|
||||
expect(result).toBe('查询北京新闻');
|
||||
});
|
||||
|
||||
it('should throw error when template_query is not defined', () => {
|
||||
const shortcut = {
|
||||
command_id: '7374755905893793836',
|
||||
command_name: 'muti',
|
||||
components_list: [
|
||||
{
|
||||
name: 'news',
|
||||
description: 'Keywords to search for news, must in English',
|
||||
input_type: 0,
|
||||
parameter: 'q',
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
description: '',
|
||||
object_id: '7374633552917479468',
|
||||
plugin_api_name: 'getNews',
|
||||
plugin_id: '7373521805258014764',
|
||||
send_type: 1,
|
||||
shortcut_command: '/muti',
|
||||
tool_info: {
|
||||
tool_name: 'News',
|
||||
tool_params_list: [
|
||||
{
|
||||
default_value: '',
|
||||
desc: 'Keywords to search for news, must in English',
|
||||
name: 'q',
|
||||
refer_component: true,
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
tool_type: 2,
|
||||
},
|
||||
work_flow_id: '',
|
||||
};
|
||||
const componentsFormValues = { news: '北京新闻' };
|
||||
// @ts-expect-error --单测忽略
|
||||
expect(() => getTemplateQuery(shortcut, componentsFormValues)).toThrowError(
|
||||
'template_query is not defined',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getImageAndFileList', () => {
|
||||
it('should return list of images and files', () => {
|
||||
const componentsFormValues = {
|
||||
image: {
|
||||
fileInstance: new File([''], 'filename', { type: 'image/png' }),
|
||||
url: 'http://example.com/image.png',
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
file: {
|
||||
fileInstance: new File([''], 'filename', { type: 'text/plain' }),
|
||||
url: 'http://example.com/file.txt',
|
||||
},
|
||||
};
|
||||
const result = getImageAndFileList(componentsFormValues);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: ContentType.Image,
|
||||
file: componentsFormValues.image.fileInstance,
|
||||
uri: componentsFormValues.image.url,
|
||||
width: componentsFormValues.image.width,
|
||||
height: componentsFormValues.image.height,
|
||||
},
|
||||
{
|
||||
type: ContentType.File,
|
||||
file: componentsFormValues.file.fileInstance,
|
||||
uri: componentsFormValues.file.url,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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 WorkFlowItemType } from '@coze-studio/bot-detail-store';
|
||||
import { type PluginApi, ToolType } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import {
|
||||
initToolInfoByToolApi,
|
||||
initToolInfoByWorkFlow,
|
||||
initToolInfoByPlugin,
|
||||
MAX_TOOL_PARAMS_COUNT,
|
||||
} from '../../../../../../src/shortcut-tool/shortcut-edit/action-switch-area/skill-switch/method';
|
||||
|
||||
describe('initToolInfoByToolApi', () => {
|
||||
it('returns null when no toolApi is provided', () => {
|
||||
expect(initToolInfoByToolApi()).toBeNull();
|
||||
});
|
||||
|
||||
it('initializes tool info by workflow when workflow_id is present', () => {
|
||||
// @ts-expect-error -- workflow_id is not required
|
||||
const workflow: WorkFlowItemType = {
|
||||
workflow_id: '1',
|
||||
name: 'Test Workflow',
|
||||
parameters: [],
|
||||
};
|
||||
|
||||
const result = initToolInfoByToolApi(workflow);
|
||||
expect(result?.tool_type).toBe(ToolType.ToolTypeWorkFlow);
|
||||
});
|
||||
|
||||
it('initializes tool info by plugin when workflow_id is not present', () => {
|
||||
const plugin: PluginApi = {
|
||||
name: 'Test Plugin',
|
||||
plugin_name: 'Test Plugin',
|
||||
parameters: [],
|
||||
};
|
||||
|
||||
const result = initToolInfoByToolApi(plugin);
|
||||
expect(result?.tool_type).toBe(ToolType.ToolTypePlugin);
|
||||
});
|
||||
|
||||
it('sorts parameters by required field and limits to MAX_TOOL_PARAMS_COUNT', () => {
|
||||
const parameters = Array(MAX_TOOL_PARAMS_COUNT + 2)
|
||||
.fill(null)
|
||||
.map((_, index) => ({
|
||||
name: `param${index}`,
|
||||
desc: `desc${index}`,
|
||||
required: index < MAX_TOOL_PARAMS_COUNT,
|
||||
type: 'string',
|
||||
}));
|
||||
|
||||
const plugin: PluginApi = {
|
||||
name: 'Test Plugin',
|
||||
plugin_name: 'Test Plugin',
|
||||
parameters,
|
||||
};
|
||||
|
||||
const result = initToolInfoByToolApi(plugin);
|
||||
expect(result?.tool_params_list.length).toBe(MAX_TOOL_PARAMS_COUNT + 2);
|
||||
// 前10个是required=true的参数
|
||||
expect(
|
||||
result?.tool_params_list
|
||||
.slice(0, MAX_TOOL_PARAMS_COUNT)
|
||||
.every(param => param.required),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('initToolInfoByWorkFlow', () => {
|
||||
it('initializes tool info from a workflow item', () => {
|
||||
// @ts-expect-error -- workflow_id is not required
|
||||
const workflow: WorkFlowItemType = {
|
||||
workflow_id: '1',
|
||||
name: 'Test Workflow',
|
||||
parameters: [],
|
||||
};
|
||||
|
||||
const result = initToolInfoByWorkFlow(workflow);
|
||||
expect(result.tool_type).toBe(ToolType.ToolTypeWorkFlow);
|
||||
expect(result.tool_name).toBe(workflow.name);
|
||||
expect(result.work_flow_id).toBe(workflow.workflow_id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initToolInfoByPlugin', () => {
|
||||
it('initializes tool info from a plugin item', () => {
|
||||
const plugin: PluginApi = {
|
||||
name: 'Test Plugin',
|
||||
plugin_name: 'Test Plugin',
|
||||
parameters: [],
|
||||
};
|
||||
|
||||
const result = initToolInfoByPlugin(plugin);
|
||||
expect(result.tool_type).toBe(ToolType.ToolTypePlugin);
|
||||
expect(result.tool_name).toBe(plugin.plugin_name);
|
||||
expect(result.plugin_api_name).toBe(plugin.name);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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 } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { type ShortcutEditFormValues } from '../../../../../src/shortcut-tool/types';
|
||||
import {
|
||||
initComponentsByToolParams,
|
||||
getUnusedComponents,
|
||||
} from '../../../../../src/shortcut-tool/shortcut-edit/action-switch-area/method';
|
||||
|
||||
describe('initComponentsByToolParams', () => {
|
||||
it('should initialize components correctly', () => {
|
||||
const params = [
|
||||
{ name: 'param1', desc: 'description1', refer_component: true },
|
||||
{ name: 'param2', desc: 'description2', refer_component: false },
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
name: 'param1',
|
||||
parameter: 'param1',
|
||||
description: 'description1',
|
||||
input_type: InputType.TextInput,
|
||||
default_value: { value: '' },
|
||||
hide: false,
|
||||
},
|
||||
{
|
||||
name: 'param2',
|
||||
parameter: 'param2',
|
||||
description: 'description2',
|
||||
input_type: InputType.TextInput,
|
||||
default_value: { value: '' },
|
||||
hide: true,
|
||||
},
|
||||
];
|
||||
expect(initComponentsByToolParams(params)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle empty params', () => {
|
||||
expect(initComponentsByToolParams([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUnusedComponents', () => {
|
||||
it('should return unused components', () => {
|
||||
// @ts-expect-error -- hide is missing
|
||||
const shortcut: ShortcutEditFormValues = {
|
||||
components_list: [
|
||||
{ name: 'comp1', hide: false },
|
||||
{ name: 'comp2', hide: false },
|
||||
],
|
||||
template_query: '{{comp1}}',
|
||||
};
|
||||
const expected = [{ name: 'comp2', hide: false }];
|
||||
expect(getUnusedComponents(shortcut)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle empty components_list', () => {
|
||||
// @ts-expect-error -- hide is missing
|
||||
const shortcut: ShortcutEditFormValues = {
|
||||
components_list: [],
|
||||
template_query: '',
|
||||
};
|
||||
expect(getUnusedComponents(shortcut)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle no unused components', () => {
|
||||
// @ts-expect-error -- hide is missing
|
||||
const shortcut: ShortcutEditFormValues = {
|
||||
components_list: [{ name: 'comp1', hide: false }],
|
||||
template_query: '{{comp1}}',
|
||||
};
|
||||
expect(getUnusedComponents(shortcut)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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,
|
||||
// eslint-disable-next-line camelcase
|
||||
type shortcut_command,
|
||||
} from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import {
|
||||
type ComponentsWithId,
|
||||
type ComponentTypeItem,
|
||||
} from '../../../../../src/shortcut-tool/shortcut-edit/components-table/types';
|
||||
import {
|
||||
attachIdToComponents,
|
||||
checkDuplicateName,
|
||||
formatSubmitValues,
|
||||
getComponentTypeFormBySubmitField,
|
||||
getComponentTypeSelectFormInitValues,
|
||||
getSubmitFieldFromComponentTypeForm,
|
||||
isUploadType,
|
||||
modifyComponentWhenSwitchChange,
|
||||
type SubmitComponentTypeFields,
|
||||
} from '../../../../../src/shortcut-tool/shortcut-edit/components-table/method';
|
||||
|
||||
describe('attachIdToComponents', () => {
|
||||
it('should attach unique id to each component', () => {
|
||||
// eslint-disable-next-line camelcase
|
||||
const components: shortcut_command.Components[] = [
|
||||
{ input_type: InputType.TextInput },
|
||||
{ input_type: InputType.Select },
|
||||
];
|
||||
const result = attachIdToComponents(components);
|
||||
expect(result[0]?.id).toBeDefined();
|
||||
expect(result[1]?.id).toBeDefined();
|
||||
expect(result[0]?.id).not.toEqual(result[1]?.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatSubmitValues', () => {
|
||||
it('should format values correctly', () => {
|
||||
const values: ComponentsWithId[] = [
|
||||
{ id: '1', input_type: InputType.TextInput, options: ['option1'] },
|
||||
{
|
||||
id: '2',
|
||||
input_type: InputType.Select,
|
||||
options: ['option1', 'option2'],
|
||||
},
|
||||
];
|
||||
const result = formatSubmitValues(values);
|
||||
expect(result[0]?.options).toEqual([]);
|
||||
expect(result[1]?.options).toEqual(['option1', 'option2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkDuplicateName', () => {
|
||||
it('should return true if duplicate names exist', () => {
|
||||
const values: ComponentsWithId[] = [
|
||||
{ id: '1', name: 'component1' },
|
||||
{ id: '2', name: 'component1' },
|
||||
];
|
||||
const formApi = { setError: vi.fn() };
|
||||
const result = checkDuplicateName(values, formApi as any);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if no duplicate names exist', () => {
|
||||
const values: ComponentsWithId[] = [
|
||||
{ id: '1', name: 'component1' },
|
||||
{ id: '2', name: 'component2' },
|
||||
];
|
||||
const formApi = { setError: vi.fn() };
|
||||
const result = checkDuplicateName(values, formApi as any);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getComponentTypeSelectFormInitValues', () => {
|
||||
it('should return initial values', () => {
|
||||
const result = getComponentTypeSelectFormInitValues();
|
||||
expect(result).toEqual({ type: 'text' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSubmitFieldFromComponentTypeForm', () => {
|
||||
it('returns TextInput type for text', () => {
|
||||
const values: ComponentTypeItem = { type: 'text' };
|
||||
const result = getSubmitFieldFromComponentTypeForm(values);
|
||||
expect(result).toEqual({ input_type: InputType.TextInput });
|
||||
});
|
||||
|
||||
it('returns Select type with options for select', () => {
|
||||
const values: ComponentTypeItem = {
|
||||
type: 'select',
|
||||
options: ['option1', 'option2'],
|
||||
};
|
||||
const result = getSubmitFieldFromComponentTypeForm(values);
|
||||
expect(result).toEqual({
|
||||
input_type: InputType.Select,
|
||||
options: ['option1', 'option2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns MixUpload type with upload options for multiple upload types', () => {
|
||||
const values: ComponentTypeItem = {
|
||||
type: 'upload',
|
||||
uploadTypes: [InputType.UploadImage, InputType.UploadDoc],
|
||||
};
|
||||
const result = getSubmitFieldFromComponentTypeForm(values);
|
||||
expect(result).toEqual({
|
||||
input_type: InputType.MixUpload,
|
||||
upload_options: [InputType.UploadImage, InputType.UploadDoc],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns specific Upload type for single upload type', () => {
|
||||
const values: ComponentTypeItem = {
|
||||
type: 'upload',
|
||||
uploadTypes: [InputType.UploadImage],
|
||||
};
|
||||
const result = getSubmitFieldFromComponentTypeForm(values);
|
||||
expect(result).toEqual({ input_type: InputType.UploadImage });
|
||||
});
|
||||
|
||||
it('returns TextInput type for unrecognized type', () => {
|
||||
// @ts-expect-error -- 无视
|
||||
const values: ComponentTypeItem = { type: 'unknown' };
|
||||
const result = getSubmitFieldFromComponentTypeForm(values);
|
||||
expect(result).toEqual({ input_type: InputType.TextInput });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUploadType', () => {
|
||||
it('should return true for upload types', () => {
|
||||
const result = isUploadType(InputType.UploadImage);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-upload types', () => {
|
||||
const result = isUploadType(InputType.TextInput);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getComponentTypeFormBySubmitField', () => {
|
||||
it('returns initial values when input_type is not provided', () => {
|
||||
const values: SubmitComponentTypeFields = {};
|
||||
const result = getComponentTypeFormBySubmitField(values);
|
||||
expect(result).toEqual({ type: 'text' });
|
||||
});
|
||||
|
||||
it('returns correct form for TextInput type', () => {
|
||||
const values: SubmitComponentTypeFields = {
|
||||
input_type: InputType.TextInput,
|
||||
};
|
||||
const result = getComponentTypeFormBySubmitField(values);
|
||||
expect(result).toEqual({ type: 'text' });
|
||||
});
|
||||
|
||||
it('returns correct form for Select type with options', () => {
|
||||
const values: SubmitComponentTypeFields = {
|
||||
input_type: InputType.Select,
|
||||
options: ['option1', 'option2'],
|
||||
};
|
||||
const result = getComponentTypeFormBySubmitField(values);
|
||||
expect(result).toEqual({ type: 'select', options: ['option1', 'option2'] });
|
||||
});
|
||||
|
||||
it('returns correct form for Upload type with upload options', () => {
|
||||
const values: SubmitComponentTypeFields = {
|
||||
input_type: InputType.UploadImage,
|
||||
upload_options: [InputType.UploadAudio, InputType.VIDEO],
|
||||
};
|
||||
const result = getComponentTypeFormBySubmitField(values);
|
||||
expect(result).toEqual({
|
||||
type: 'upload',
|
||||
uploadTypes: [InputType.UploadAudio, InputType.VIDEO],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns initial values when input_type is not recognized', () => {
|
||||
const values: SubmitComponentTypeFields = {
|
||||
input_type: 'unknown' as unknown as InputType,
|
||||
};
|
||||
const result = getComponentTypeFormBySubmitField(values);
|
||||
expect(result).toEqual({ type: 'text' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUploadType', () => {
|
||||
it('returns true for upload types', () => {
|
||||
expect(isUploadType(InputType.UploadImage)).toBeTruthy();
|
||||
expect(isUploadType(InputType.UploadDoc)).toBeTruthy();
|
||||
expect(isUploadType(InputType.UploadTable)).toBeTruthy();
|
||||
expect(isUploadType(InputType.UploadAudio)).toBeTruthy();
|
||||
expect(isUploadType(InputType.CODE)).toBeTruthy();
|
||||
expect(isUploadType(InputType.ARCHIVE)).toBeTruthy();
|
||||
expect(isUploadType(InputType.PPT)).toBeTruthy();
|
||||
expect(isUploadType(InputType.VIDEO)).toBeTruthy();
|
||||
expect(isUploadType(InputType.TXT)).toBeTruthy();
|
||||
expect(isUploadType(InputType.MixUpload)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false for non-upload types', () => {
|
||||
expect(isUploadType(InputType.TextInput)).toBeFalsy();
|
||||
expect(isUploadType(InputType.Select)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
describe('modifyComponentWhenSwitchChange', () => {
|
||||
it('should modify component hide property correctly', () => {
|
||||
const components: ComponentsWithId[] = [
|
||||
{ id: '1', hide: false },
|
||||
{ id: '2', hide: false },
|
||||
];
|
||||
const record: ComponentsWithId = { id: '1', hide: false };
|
||||
const result = modifyComponentWhenSwitchChange({
|
||||
components,
|
||||
record,
|
||||
checked: false,
|
||||
});
|
||||
expect(result[0]?.hide).toBe(true);
|
||||
expect(result[1]?.hide).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
vi.stubGlobal('IS_OVERSEA', false);
|
||||
vi.stubGlobal('IS_RELEASE_VERSION', false);
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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 ItemType } from '../../src/utils/data-helper';
|
||||
|
||||
describe('ItemType', () => {
|
||||
it('returns array item type for array input', () => {
|
||||
type Result = ItemType<string[]>;
|
||||
const result: Result = 'test';
|
||||
expect(typeof result).to.equal('string');
|
||||
});
|
||||
|
||||
it('returns same type for non-array input', () => {
|
||||
type Result = ItemType<number>;
|
||||
const result: Result = 123;
|
||||
expect(typeof result).to.equal('number');
|
||||
});
|
||||
});
|
||||
@@ -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 { getUIModeByBizScene } from '../../src/utils/get-ui-mode-by-biz-scene';
|
||||
|
||||
describe('ItemType', () => {
|
||||
it('returns UIMode correctly', () => {
|
||||
const res1 = getUIModeByBizScene({
|
||||
bizScene: 'agentApp',
|
||||
showBackground: false,
|
||||
});
|
||||
|
||||
const res2 = getUIModeByBizScene({
|
||||
bizScene: 'home',
|
||||
showBackground: false,
|
||||
});
|
||||
|
||||
const res3 = getUIModeByBizScene({
|
||||
bizScene: 'agentApp',
|
||||
showBackground: false,
|
||||
});
|
||||
|
||||
expect(res1).toBe('grey');
|
||||
expect(res2).toBe('white');
|
||||
expect(res3).toBe('grey');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isApiError } from '../../src/utils/handle-error';
|
||||
|
||||
describe('isApiError', () => {
|
||||
it('identifies ApiError correctly', () => {
|
||||
const error = { name: 'ApiError' };
|
||||
const result = isApiError(error);
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false for non-ApiError', () => {
|
||||
const error = { name: 'OtherError' };
|
||||
const result = isApiError(error);
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false for error without name', () => {
|
||||
const error = { message: 'An error occurred' };
|
||||
const result = isApiError(error);
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('handles null and undefined', () => {
|
||||
expect(isApiError(null)).to.be.false;
|
||||
expect(isApiError(undefined)).to.be.false;
|
||||
});
|
||||
});
|
||||
@@ -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 { getQueryFromTemplate } from '../../src/utils/shortcut-query';
|
||||
|
||||
describe('getQueryFromTemplate', () => {
|
||||
it('should replace placeholders with corresponding values', () => {
|
||||
const templateQuery = 'Hello, {{name}}!';
|
||||
const values = { name: 'John' };
|
||||
const result = getQueryFromTemplate(templateQuery, values);
|
||||
expect(result).to.equal('Hello, John!');
|
||||
});
|
||||
|
||||
it('should handle multiple placeholders', () => {
|
||||
const templateQuery = '{{greeting}}, {{name}}!';
|
||||
const values = { greeting: 'Hi', name: 'John' };
|
||||
const result = getQueryFromTemplate(templateQuery, values);
|
||||
expect(result).to.equal('Hi, John!');
|
||||
});
|
||||
|
||||
it('should leave unreplaced placeholders intact', () => {
|
||||
const templateQuery = 'Hello, {{name}}!';
|
||||
const values = { greeting: 'Hi' };
|
||||
const result = getQueryFromTemplate(templateQuery, values);
|
||||
expect(result).to.equal('Hello, {{name}}!');
|
||||
});
|
||||
|
||||
it('should handle empty values object', () => {
|
||||
const templateQuery = 'Hello, {{name}}!';
|
||||
const values = {};
|
||||
const result = getQueryFromTemplate(templateQuery, values);
|
||||
expect(result).to.equal('Hello, {{name}}!');
|
||||
});
|
||||
|
||||
it('should handle empty template string', () => {
|
||||
const templateQuery = '';
|
||||
const values = { name: 'John' };
|
||||
const result = getQueryFromTemplate(templateQuery, values);
|
||||
expect(result).to.equal('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {
|
||||
'@coze-arch/no-deep-relative-import': 'off',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "@coze-common/chat-area-plugins-chat-shortcuts",
|
||||
"version": "0.0.1",
|
||||
"description": "作为chat-input的插件slot默认实现,维护快捷键相关功能",
|
||||
"license": "Apache-2.0",
|
||||
"author": "haozhenfei@bytedance.com",
|
||||
"maintainers": [],
|
||||
"exports": {
|
||||
".": "./src/index.tsx",
|
||||
"./shortcut-tool": "./src/shortcut-tool/index.tsx"
|
||||
},
|
||||
"main": "src/index.tsx",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"shortcut-tool": [
|
||||
"./src/shortcut-tool/index.tsx"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-agent-ide/tool": "workspace:*",
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-error": "workspace:*",
|
||||
"@coze-arch/bot-flags": "workspace:*",
|
||||
"@coze-arch/bot-icons": "workspace:*",
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/bot-space-api": "workspace:*",
|
||||
"@coze-arch/bot-studio-store": "workspace:*",
|
||||
"@coze-arch/bot-tea": "workspace:*",
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-arch/logger": "workspace:*",
|
||||
"@coze-common/chat-area": "workspace:*",
|
||||
"@coze-common/chat-area-utils": "workspace:*",
|
||||
"@coze-common/chat-core": "workspace:*",
|
||||
"@coze-common/chat-uikit": "workspace:*",
|
||||
"@coze-common/chat-uikit-shared": "workspace:*",
|
||||
"@coze-common/websocket-manager-adapter": "workspace:*",
|
||||
"@coze-studio/bot-detail-store": "workspace:*",
|
||||
"@coze-studio/components": "workspace:*",
|
||||
"@coze-workflow/sdk": "workspace:*",
|
||||
"@douyinfe/semi-icons": "^2.36.0",
|
||||
"@douyinfe/semi-ui": "~2.72.3",
|
||||
"ahooks": "^3.7.8",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"classnames": "^2.3.2",
|
||||
"immer": "^10.0.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^4.0.2",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-agent-ide/tool-config": "workspace:*",
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"tailwindcss": "~3.3.3",
|
||||
"utility-types": "^3.10.0",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0"
|
||||
},
|
||||
"// deps": "immer@^10.0.3 为脚本自动补齐,请勿改动"
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon_cross_fill">
|
||||
<path id="Union" d="M1.75753 2.11084C1.56227 2.3061 1.56227 2.62268 1.75753 2.81795L4.93951 5.99993L1.75753 9.18192C1.56227 9.37718 1.56227 9.69376 1.75753 9.88902L2.11108 10.2426C2.30635 10.4378 2.62293 10.4378 2.81819 10.2426L6.00017 7.06059L9.18215 10.2426C9.37741 10.4378 9.694 10.4378 9.88926 10.2426L10.2428 9.88901C10.4381 9.69375 10.4381 9.37717 10.2428 9.18191L7.06084 5.99993L10.2428 2.81795C10.4381 2.62269 10.4381 2.30611 10.2428 2.11085L9.88926 1.75729C9.694 1.56203 9.37741 1.56203 9.18215 1.75729L6.00017 4.93927L2.81819 1.75729C2.62293 1.56202 2.30635 1.56202 2.11108 1.75729L1.75753 2.11084Z" fill="#060709" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 779 B |
@@ -0,0 +1,73 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5257_104690)">
|
||||
<g filter="url(#filter0_d_5257_104690)">
|
||||
<path
|
||||
d="M3.6 2.68c0-1.008 0-1.512.196-1.897a1.8 1.8 0 0 1 .786-.787C4.967-.2 5.472-.2 6.48-.2H16.8l6 7.2v13.92c0 1.008 0 1.512-.196 1.897a1.8 1.8 0 0 1-.787.787c-.384.196-.889.196-1.897.196H6.48c-1.008 0-1.512 0-1.897-.196a1.8 1.8 0 0 1-.786-.787c-.197-.385-.197-.89-.197-1.897V2.68z"
|
||||
fill="#FF54C5" />
|
||||
<path
|
||||
d="M3.66 2.68c0-.505 0-.88.024-1.178.024-.296.072-.51.165-.692a1.74 1.74 0 0 1 .76-.76c.183-.094.397-.142.693-.166.297-.024.672-.024 1.177-.024h10.292l5.968 7.162V20.92c0 .505 0 .88-.024 1.177-.024.297-.072.51-.165.693a1.74 1.74 0 0 1-.76.76c-.183.093-.397.141-.693.165-.297.025-.673.025-1.178.025H6.48c-.505 0-.88 0-1.177-.024-.296-.025-.51-.073-.693-.166a1.74 1.74 0 0 1-.76-.76c-.093-.183-.141-.396-.165-.693a15.907 15.907 0 0 1-.025-1.177V2.68z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path d="M3.6 1.6A1.8 1.8 0 0 1 5.4-.2h11.4l6 7.2v15a1.8 1.8 0 0 1-1.8 1.8H5.4A1.8 1.8 0 0 1 3.6 22V1.6z"
|
||||
fill="url(#paint0_radial_5257_104690)" fill-opacity=".6" />
|
||||
<g filter="url(#filter1_dd_5257_104690)" shape-rendering="crispEdges">
|
||||
<path
|
||||
d="M16.8-.2l6 7.2h-2.16c-1.345 0-2.017 0-2.53-.262A2.4 2.4 0 0 1 17.06 5.69c-.262-.513-.262-1.185-.262-2.53V-.2z"
|
||||
fill="#fff" />
|
||||
<path
|
||||
d="M16.846-.239l-.107-.127v3.529c0 .67 0 1.175.033 1.578.033.404.1.71.236.976.235.463.612.839 1.075 1.075.266.136.571.202.975.235.403.033.909.033 1.579.033h2.29l-.081-.099-6-7.2z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path
|
||||
d="M16.8-.2l6 7.2h-2.16c-1.345 0-2.017 0-2.53-.262A2.4 2.4 0 0 1 17.06 5.69c-.262-.513-.262-1.185-.262-2.53V-.2z"
|
||||
fill="#FF7BD2" />
|
||||
<path d="M16.8-.2l6 7.2h-3.6a2.4 2.4 0 0 1-2.4-2.4V-.2z" fill="url(#paint1_linear_5257_104690)" fill-opacity=".6" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_5257_104690" x="-1.543" y="-3.629" width="29.486" height="34.286" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.714" />
|
||||
<feGaussianBlur stdDeviation="2.571" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104690" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_5257_104690" result="shape" />
|
||||
</filter>
|
||||
<filter id="filter1_dd_5257_104690" x="9.479" y="-5.331" width="20.776" height="22.051" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="2.4" />
|
||||
<feGaussianBlur stdDeviation="3.6" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104690" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.2" />
|
||||
<feGaussianBlur stdDeviation="1.2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
|
||||
<feBlend in2="effect1_dropShadow_5257_104690" result="effect2_dropShadow_5257_104690" />
|
||||
<feBlend in="SourceGraphic" in2="effect2_dropShadow_5257_104690" result="shape" />
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_5257_104690" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(23.09998 27.59995 -27.8692 23.32533 4.2 .4)">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".357" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".783" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_5257_104690" x1="15.299" y1="4.6" x2="25.75" y2="-.606"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".5" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".723" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_5257_104690">
|
||||
<path fill="#fff" transform="translate(.9)" d="M0 0h24v24H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,80 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5257_104663)">
|
||||
<g filter="url(#filter0_d_5257_104663)">
|
||||
<path
|
||||
d="M3.3 2.88c0-1.008 0-1.512.197-1.897a1.8 1.8 0 0 1 .786-.787C4.668 0 5.173 0 6.18 0H16.5l6 7.2v13.92c0 1.008 0 1.512-.196 1.897a1.8 1.8 0 0 1-.786.787c-.385.196-.89.196-1.898.196H6.18c-1.008 0-1.512 0-1.897-.196a1.8 1.8 0 0 1-.786-.787c-.197-.385-.197-.889-.197-1.897V2.88z"
|
||||
fill="#00A8F3" />
|
||||
<path
|
||||
d="M3.36 2.88c0-.505 0-.88.025-1.177.024-.297.072-.51.165-.693a1.74 1.74 0 0 1 .76-.76c.183-.093.397-.141.693-.166C5.3.06 5.675.06 6.18.06h10.292l5.968 7.162V21.12c0 .505 0 .88-.024 1.177-.024.297-.072.51-.165.693a1.741 1.741 0 0 1-.76.76c-.183.093-.397.142-.693.166-.297.024-.672.024-1.178.024H6.18c-.505 0-.88 0-1.177-.024-.296-.024-.51-.073-.693-.166a1.74 1.74 0 0 1-.76-.76c-.093-.183-.141-.396-.165-.693a15.907 15.907 0 0 1-.025-1.177V2.88z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path d="M3.3 1.8A1.8 1.8 0 0 1 5.1 0h11.4l6 7.2v15a1.8 1.8 0 0 1-1.8 1.8H5.1a1.8 1.8 0 0 1-1.8-1.8V1.8z"
|
||||
fill="url(#paint0_radial_5257_104663)" fill-opacity=".6" />
|
||||
<g clip-path="url(#clip1_5257_104663)" fill="#fff">
|
||||
<path
|
||||
d="M14.1 20.7h-1.8a.601.601 0 0 1-.6-.6v-3a.6.6 0 0 1 .6-.6h1.8v.6h-1.8v3h1.8v.6zM10.5 20.7H9.3a.601.601 0 0 1-.6-.6v-3a.6.6 0 0 1 .6-.6h1.2a.6.6 0 0 1 .6.6v3a.6.6 0 0 1-.6.6zm-1.2-3.6v3h1.2v-3H9.3zM6.9 20.7H5.7v-4.2h1.2a1.201 1.201 0 0 1 1.2 1.2v1.8a1.201 1.201 0 0 1-1.2 1.2zm-.6-.6h.6a.6.6 0 0 0 .6-.6v-1.8a.6.6 0 0 0-.6-.6h-.6v3z" />
|
||||
</g>
|
||||
<g filter="url(#filter1_dd_5257_104663)" shape-rendering="crispEdges">
|
||||
<path
|
||||
d="M16.5 0l6 7.2h-2.16c-1.344 0-2.016 0-2.53-.262a2.4 2.4 0 0 1-1.048-1.048c-.262-.514-.262-1.186-.262-2.53V0z"
|
||||
fill="#fff" />
|
||||
<path
|
||||
d="M16.547-.038l-.107-.128v3.529c0 .67 0 1.176.033 1.578.033.404.1.71.236.976.236.463.612.839 1.075 1.075.266.136.571.202.975.235.403.033.909.033 1.579.033h2.291l-.082-.098-6-7.2z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path
|
||||
d="M16.5 0l6 7.2h-2.16c-1.344 0-2.016 0-2.53-.262a2.4 2.4 0 0 1-1.048-1.048c-.262-.514-.262-1.186-.262-2.53V0z"
|
||||
fill="#00A8F3" />
|
||||
<path d="M16.5 0l6 7.2h-3.6a2.4 2.4 0 0 1-2.4-2.4V0z" fill="url(#paint1_linear_5257_104663)" fill-opacity=".6" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_5257_104663" x="-1.842" y="-3.429" width="29.486" height="34.286" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.714" />
|
||||
<feGaussianBlur stdDeviation="2.571" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104663" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_5257_104663" result="shape" />
|
||||
</filter>
|
||||
<filter id="filter1_dd_5257_104663" x="9.18" y="-5.131" width="20.776" height="22.051" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="2.4" />
|
||||
<feGaussianBlur stdDeviation="3.6" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104663" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.2" />
|
||||
<feGaussianBlur stdDeviation="1.2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
|
||||
<feBlend in2="effect1_dropShadow_5257_104663" result="effect2_dropShadow_5257_104663" />
|
||||
<feBlend in="SourceGraphic" in2="effect2_dropShadow_5257_104663" result="shape" />
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_5257_104663" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(23.09998 27.59995 -27.8692 23.32533 3.9 .6)">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".357" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".783" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_5257_104663" x1="15.001" y1="4.8" x2="25.451" y2="-.405"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".5" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".723" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_5257_104663">
|
||||
<path fill="#fff" transform="translate(.9)" d="M0 0h24v24H0z" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_5257_104663">
|
||||
<path fill="#fff" transform="translate(5.1 13.8)" d="M0 0h9.6v9.6H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,80 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5257_104664)">
|
||||
<g filter="url(#filter0_d_5257_104664)">
|
||||
<path
|
||||
d="M3.4 2.88c0-1.008 0-1.512.196-1.897a1.8 1.8 0 0 1 .787-.787C4.768 0 5.273 0 6.28 0H16.6l6 7.2v13.92c0 1.008 0 1.512-.196 1.897a1.8 1.8 0 0 1-.787.787c-.385.196-.889.196-1.897.196H6.28c-1.008 0-1.512 0-1.897-.196a1.8 1.8 0 0 1-.787-.787c-.196-.385-.196-.889-.196-1.897V2.88z"
|
||||
fill="#00C979" />
|
||||
<path
|
||||
d="M3.46 2.88c0-.505 0-.88.024-1.177.025-.297.073-.51.166-.693a1.74 1.74 0 0 1 .76-.76c.183-.093.396-.141.693-.166C5.4.06 5.775.06 6.28.06h10.292l5.968 7.162V21.12c0 .505 0 .88-.024 1.177-.024.297-.072.51-.165.693a1.74 1.74 0 0 1-.76.76c-.184.093-.397.142-.694.166-.297.024-.672.024-1.177.024H6.28c-.505 0-.88 0-1.177-.024-.297-.024-.51-.073-.693-.166a1.74 1.74 0 0 1-.76-.76c-.093-.183-.141-.396-.166-.693a15.907 15.907 0 0 1-.024-1.177V2.88z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path d="M3.4 1.8A1.8 1.8 0 0 1 5.2 0h11.4l6 7.2v15a1.8 1.8 0 0 1-1.8 1.8H5.2a1.8 1.8 0 0 1-1.8-1.8V1.8z"
|
||||
fill="url(#paint0_radial_5257_104664)" fill-opacity=".6" />
|
||||
<g clip-path="url(#clip1_5257_104664)" fill="#fff">
|
||||
<path
|
||||
d="M13.6 20.7h-1.8v-.6h1.8v-1.2h-1.2a.6.6 0 0 1-.6-.6v-1.2a.6.6 0 0 1 .6-.6h1.8v.6h-1.8v1.2h1.2a.6.6 0 0 1 .6.6v1.2a.6.6 0 0 1-.6.6zM9.4 20.1v-3.6h-.6v4.2h2.4v-.6H9.4zM8.2 16.5h-.6L7 18.3l-.6-1.8h-.6l.826 2.1-.826 2.1h.6l.6-1.8.6 1.8h.6l-.826-2.1.826-2.1z" />
|
||||
</g>
|
||||
<g filter="url(#filter1_dd_5257_104664)" shape-rendering="crispEdges">
|
||||
<path
|
||||
d="M16.6 0l6 7.2h-2.16c-1.344 0-2.016 0-2.53-.262a2.4 2.4 0 0 1-1.048-1.048c-.262-.514-.262-1.186-.262-2.53V0z"
|
||||
fill="#fff" />
|
||||
<path
|
||||
d="M16.646-.038l-.106-.128v3.529c0 .67 0 1.176.033 1.578.033.404.1.71.235.976.236.463.613.839 1.075 1.075.267.136.572.202.976.235.403.033.909.033 1.578.033H22.728l-.082-.098-6-7.2z"
|
||||
stroke="#000" stroke-opacity=".08" stroke-width=".12" />
|
||||
</g>
|
||||
<path
|
||||
d="M16.6 0l6 7.2h-2.16c-1.344 0-2.016 0-2.53-.262a2.4 2.4 0 0 1-1.048-1.048c-.262-.514-.262-1.186-.262-2.53V0z"
|
||||
fill="#00C979" />
|
||||
<path d="M16.6 0l6 7.2H19a2.4 2.4 0 0 1-2.4-2.4V0z" fill="url(#paint1_linear_5257_104664)" fill-opacity=".6" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_5257_104664" x="-1.743" y="-3.429" width="29.486" height="34.286" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.714" />
|
||||
<feGaussianBlur stdDeviation="2.571" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104664" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_5257_104664" result="shape" />
|
||||
</filter>
|
||||
<filter id="filter1_dd_5257_104664" x="9.28" y="-5.131" width="20.776" height="22.051" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="2.4" />
|
||||
<feGaussianBlur stdDeviation="3.6" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5257_104664" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.2" />
|
||||
<feGaussianBlur stdDeviation="1.2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
|
||||
<feBlend in2="effect1_dropShadow_5257_104664" result="effect2_dropShadow_5257_104664" />
|
||||
<feBlend in="SourceGraphic" in2="effect2_dropShadow_5257_104664" result="shape" />
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_5257_104664" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(23.09998 27.59995 -27.8692 23.32533 4 .6)">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".357" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".783" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_5257_104664" x1="15.1" y1="4.8" x2="25.551" y2="-.405"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FCF3CE" />
|
||||
<stop offset=".5" stop-color="#FCF3CD" stop-opacity="0" />
|
||||
<stop offset=".723" stop-color="#FCF3CD" stop-opacity=".095" />
|
||||
<stop offset="1" stop-color="#FCF3CD" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_5257_104664">
|
||||
<path fill="#fff" transform="translate(.9)" d="M0 0h24v24H0z" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_5257_104664">
|
||||
<path fill="#fff" transform="translate(5.2 13.8)" d="M0 0h9.6v9.6H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="coz_arrow_down_fill">
|
||||
<path id="Union" d="M6.00241 7.17466L9.53794 3.63912C9.7332 3.44386 10.0498 3.44386 10.245 3.63912L10.5986 3.99268C10.7939 4.18794 10.7939 4.50452 10.5986 4.69978L6.70951 8.58887C6.31899 8.97939 5.68582 8.97939 5.2953 8.58887L1.40621 4.69978C1.21095 4.50452 1.21095 4.18794 1.40621 3.99268L1.75977 3.63912C1.95503 3.44386 2.27161 3.44386 2.46687 3.63912L6.00241 7.17466Z" fill="currentColor" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 550 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="coz_arrow_down_fill">
|
||||
<path id="Union" d="M6.00241 4.82534L9.53794 8.36088C9.7332 8.55614 10.0498 8.55614 10.245 8.36088L10.5986 8.00732C10.7939 7.81206 10.7939 7.49548 10.5986 7.30022L6.70951 3.41113C6.31899 3.02061 5.68582 3.02061 5.2953 3.41113L1.40621 7.30022C1.21095 7.49548 1.21095 7.81206 1.40621 8.00732L1.75977 8.36088C1.95503 8.55614 2.27161 8.55614 2.46687 8.36088L6.00241 4.82534Z" fill="currentColor" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 550 B |
|
After Width: | Height: | Size: 384 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="coz_ban">
|
||||
<path id="Union" d="M0.666992 8.00008C0.666992 12.0502 3.95024 15.3334 8.00033 15.3334C12.0504 15.3334 15.3337 12.0502 15.3337 8.00008C15.3337 3.94999 12.0504 0.666748 8.00033 0.666748C3.95024 0.666748 0.666992 3.94999 0.666992 8.00008ZM11.7453 12.6881C10.7189 13.5091 9.41694 14.0001 8.00033 14.0001C4.68662 14.0001 2.00033 11.3138 2.00033 8.00008C2.00033 6.58346 2.49127 5.28151 3.31229 4.25508L11.7453 12.6881ZM12.6882 11.7453L4.25508 3.31225C5.28154 2.4911 6.58359 2.00008 8.00033 2.00008C11.314 2.00008 14.0003 4.68637 14.0003 8.00008C14.0003 9.41681 13.5093 10.7189 12.6882 11.7453Z" fill="#060709" fill-opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 751 B |
@@ -0,0 +1,9 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="coz_warning_circle_fill_palette">
|
||||
<path id="Union" d="M15.9997 30.6666C24.0998 30.6666 30.6663 24.1001 30.6663 15.9999C30.6663 7.89974 24.0998 1.33325 15.9997 1.33325C7.8995 1.33325 1.33301 7.89974 1.33301 15.9999C1.33301 24.1001 7.8995 30.6666 15.9997 30.6666Z" fill="#F22435"/>
|
||||
<g id="Union_2">
|
||||
<path d="M16.0003 9.33325C15.2639 9.33325 14.667 9.93021 14.667 10.6666V17.3333C14.667 18.0696 15.2639 18.6666 16.0003 18.6666C16.7367 18.6666 17.3337 18.0696 17.3337 17.3333V10.6666C17.3337 9.93021 16.7367 9.33325 16.0003 9.33325Z" fill="white"/>
|
||||
<path d="M16.0003 19.9999C15.2639 19.9999 14.667 20.5969 14.667 21.3333C14.667 22.0696 15.2639 22.6666 16.0003 22.6666C16.7367 22.6666 17.3337 22.0696 17.3337 21.3333C17.3337 20.5969 16.7367 19.9999 16.0003 19.9999Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 897 B |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="2" height="12" viewBox="0 0 2 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector 2594" d="M1 0V12" stroke="#1D1C23" stroke-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 177 B |
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { useUpdateEffect } from 'ahooks';
|
||||
import { IconSpin } from '@douyinfe/semi-icons';
|
||||
|
||||
import { useLoadMore } from '../../hooks/shortcut-bar/use-load-more';
|
||||
const TIME_TO_CANCEL_MOUSE_MOVE = 50;
|
||||
|
||||
export interface LoadMoreListData<TData extends object> {
|
||||
list: TData[];
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export type LoadMoreListProps<TData extends object> = {
|
||||
className?: string;
|
||||
getId: (data: TData) => string;
|
||||
defaultId?: string;
|
||||
itemRender: (data: TData) => React.ReactNode;
|
||||
defaultList?: TData[];
|
||||
listTopSlot?: React.ReactNode;
|
||||
getMoreListService: (
|
||||
currentData: LoadMoreListData<TData> | undefined,
|
||||
) => Promise<LoadMoreListData<TData>>;
|
||||
onSelect?: (data: TData) => void;
|
||||
onActiveId?: (id: string) => void;
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'className' | 'onSelect'>;
|
||||
|
||||
export const LoadMoreList = <TData extends object>(
|
||||
props: LoadMoreListProps<TData>,
|
||||
) => {
|
||||
const {
|
||||
className,
|
||||
onSelect,
|
||||
getId,
|
||||
itemRender,
|
||||
onActiveId,
|
||||
getMoreListService,
|
||||
defaultId,
|
||||
listTopSlot,
|
||||
defaultList,
|
||||
...restProps
|
||||
} = props;
|
||||
const mouseMovingCancelIdRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const mouseMovingRef = useRef(false);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const activeItemRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const {
|
||||
data,
|
||||
scrollIntoView,
|
||||
activeId,
|
||||
focusTo,
|
||||
goNext,
|
||||
goPrev,
|
||||
loadingMore,
|
||||
loading,
|
||||
} = useLoadMore<TData>({
|
||||
getMoreListService,
|
||||
getId: (item: TData) => getId(item),
|
||||
listRef,
|
||||
defaultList,
|
||||
});
|
||||
|
||||
const list = data?.list ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
onActiveId?.(activeId);
|
||||
}, [activeId]);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
const defaultItem = list.find(item => getId(item) === defaultId);
|
||||
if (defaultItem) {
|
||||
focusTo(defaultItem);
|
||||
scrollIntoView(defaultItem);
|
||||
onActiveId?.(defaultId || getId(defaultItem));
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={listRef}
|
||||
tabIndex={1}
|
||||
className={className}
|
||||
onMouseLeave={() => {
|
||||
focusTo(null);
|
||||
}}
|
||||
onMouseMove={() => {
|
||||
clearTimeout(mouseMovingCancelIdRef.current);
|
||||
mouseMovingRef.current = true;
|
||||
mouseMovingCancelIdRef.current = setTimeout(() => {
|
||||
mouseMovingRef.current = false;
|
||||
}, TIME_TO_CANCEL_MOUSE_MOVE);
|
||||
}}
|
||||
onKeyDown={event => {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
goNext();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
goPrev();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
const selectItem = list.find(item => getId(item) === activeId);
|
||||
selectItem && onSelect?.(selectItem);
|
||||
}
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
{listTopSlot}
|
||||
{list.map(item => (
|
||||
<div
|
||||
key={getId(item)}
|
||||
data-id={getId(item)}
|
||||
ref={getId(item) === activeId ? activeItemRef : null}
|
||||
onClick={() => {
|
||||
onSelect?.(item);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
// 鼠标位于滚动条中,会触发该事件,设置仅在移动鼠标过程中进行更新
|
||||
if (mouseMovingRef.current) {
|
||||
focusTo(item);
|
||||
listRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{itemRender(item)}
|
||||
</div>
|
||||
))}
|
||||
{loadingMore || loading ? (
|
||||
<div className="flex justify-center items-center">
|
||||
<IconSpin style={{ color: '#4D53E8' }} spin />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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 { useIsSendMessageLock } from '@coze-common/chat-area';
|
||||
|
||||
import { type DSL } from '../../types';
|
||||
import { ChatAreaStateContext } from '../../context/chat-area-state/context';
|
||||
import { type DSLContext } from './widgets/types';
|
||||
import { DSLWidgetsMap } from './widgets';
|
||||
|
||||
const getChildrenIds = (item: DSL['elements'][string]): string[] =>
|
||||
item.children ??
|
||||
((item.props?.Columns ?? []) as { children: string[] }[])?.reduce<string[]>(
|
||||
(res, column) => {
|
||||
if (column.children) {
|
||||
res.push(...column.children);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const DSLRender: FC<
|
||||
{
|
||||
elementId: string;
|
||||
} & IShortCutPanelProps
|
||||
> = ({ elementId, ...context }) => {
|
||||
const { dsl } = context;
|
||||
const item = dsl?.elements[elementId];
|
||||
const itemType = item?.type || '';
|
||||
const Component = itemType in DSLWidgetsMap ? DSLWidgetsMap[itemType] : null;
|
||||
const childrenIds = item && getChildrenIds(item);
|
||||
|
||||
if (!Component) {
|
||||
// TODO slardar report
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Component context={context} props={item?.props}>
|
||||
{childrenIds?.map(childrenId => (
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<DSLRender key={childrenId} elementId={childrenId} {...context} />
|
||||
</div>
|
||||
))}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
export type IShortCutPanelProps = DSLContext;
|
||||
|
||||
export const ShortCutPanel: FC<IShortCutPanelProps> = ({ dsl, ...context }) => {
|
||||
const isSendMessageLock = useIsSendMessageLock();
|
||||
|
||||
return (
|
||||
<ChatAreaStateContext.Provider value={{ isSendMessageLock }}>
|
||||
<DSLRender elementId={dsl.rootID} dsl={dsl} {...context} />
|
||||
</ChatAreaStateContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -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 { useRef } from 'react';
|
||||
|
||||
import { type FormApi } from '@coze-arch/bot-semi/Form';
|
||||
import { Form } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type DSLComponent, type TValue } from '../types';
|
||||
import { findInputElementsWithDefault } from '../../../../utils/dsl-template';
|
||||
|
||||
type FormValue = Record<string, TValue>;
|
||||
export const DSLForm: DSLComponent = ({
|
||||
context: { onChange, onSubmit, dsl },
|
||||
children,
|
||||
}) => {
|
||||
const formRef = useRef<FormApi>();
|
||||
|
||||
/**
|
||||
* text类型组件交互 支持 placeholder 表示默认值
|
||||
* @param formValues
|
||||
*/
|
||||
const onSubmitWrap = (formValues: FormValue) => {
|
||||
if (!onSubmit) {
|
||||
return;
|
||||
}
|
||||
const inputElementsWithDefault = findInputElementsWithDefault(dsl);
|
||||
|
||||
const newValues = Object.entries(formValues).reduce(
|
||||
(prev: Record<string, TValue>, curr) => {
|
||||
const [field, value] = curr;
|
||||
const input = inputElementsWithDefault.find(i => i.id === field);
|
||||
|
||||
if (input && !value) {
|
||||
prev[field] = input.defaultValue;
|
||||
} else {
|
||||
prev[field] = value;
|
||||
}
|
||||
|
||||
return prev;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
inputElementsWithDefault.forEach(input => {
|
||||
const { id, defaultValue } = input;
|
||||
|
||||
if (id && !(id in newValues)) {
|
||||
newValues[id] = defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
onSubmit(newValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form<FormValue>
|
||||
className="w-full"
|
||||
autoComplete="off"
|
||||
getFormApi={api => (formRef.current = api)}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
onChange={formState => onChange?.(formState.values!)}
|
||||
onSubmit={onSubmitWrap}
|
||||
>
|
||||
{children}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DSLFormUpload } from './upload';
|
||||
import { type DSLComponent } from './types';
|
||||
import { DSLFormInput } from './text-input';
|
||||
import { DSLSubmitButton } from './submit-button';
|
||||
import { DSLFormSelect } from './select';
|
||||
import { DSLRoot } from './root';
|
||||
import { DSLPlaceholer } from './placeholder';
|
||||
import { DSLColumnLayout } from './layout';
|
||||
import { DSLForm } from './form';
|
||||
|
||||
// 组件参数是在运行时决定,无法具体做类型约束
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const DSLWidgetsMap: Record<string, DSLComponent<any>> = {
|
||||
'@flowpd/cici-components/Input': DSLFormInput,
|
||||
'@flowpd/cici-components/Select': DSLFormSelect,
|
||||
'@flowpd/cici-components/Upload': DSLFormUpload,
|
||||
'@flowpd/cici-components/Placeholder': DSLPlaceholer,
|
||||
'@flowpd/cici-components/ColumnLayout': DSLColumnLayout,
|
||||
'@flowpd/cici-components/Form': DSLForm,
|
||||
'@flowpd/cici-components/PageContainer': DSLRoot,
|
||||
'@flowpd/cici-components/Button': DSLSubmitButton,
|
||||
} as const;
|
||||
@@ -0,0 +1,31 @@
|
||||
.label {
|
||||
overflow-x: hidden;
|
||||
|
||||
width: fit-content;
|
||||
max-width: calc(100 - 16px);
|
||||
margin-bottom: 0;
|
||||
padding-right: 0;
|
||||
|
||||
:global(.semi-form-field-label-text) {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 2px;
|
||||
padding: 2px;
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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 { Form, Tooltip, Typography } from '@coze-arch/bot-semi';
|
||||
import { IconInfo } from '@coze-arch/bot-icons';
|
||||
|
||||
import style from './index.module.less';
|
||||
|
||||
export const LabelWithDescription: FC<{
|
||||
name: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}> = ({ name, description, required = true }) => (
|
||||
<div className="w-full flex items-center px-2 mb-[2px]">
|
||||
<Form.Label
|
||||
text={
|
||||
<Typography.Text
|
||||
ellipsis={{ showTooltip: true }}
|
||||
className={style.text}
|
||||
>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
}
|
||||
required={required}
|
||||
className={style.label}
|
||||
/>
|
||||
{!!description && (
|
||||
<Tooltip content={description}>
|
||||
<IconInfo className={style.icon} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -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 type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
// 本期不需要不支持复布局解析
|
||||
export const DSLColumnLayout: FC<PropsWithChildren> = ({ children }) => (
|
||||
<div className="flex items-center justify-between w-full mb-3 gap-2">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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';
|
||||
|
||||
export const DSLPlaceholer: FC = () => (
|
||||
<div
|
||||
className="flex items-center justify-center rounded-lg coz-bg-plus text-center text-xs font-medium coz-fg-secondary "
|
||||
style={{ height: 58 }}
|
||||
>
|
||||
{I18n.t('shortcut_modal_components')}
|
||||
</div>
|
||||
);
|
||||
@@ -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 type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
export const DSLRoot: FC<PropsWithChildren> = ({ children }) => <>{children}</>;
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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 SelectProps } from '@coze-arch/bot-semi/Select';
|
||||
import { UIFormSelect } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type DSLFormFieldCommonProps, type DSLComponent } from '../types';
|
||||
import { LabelWithDescription } from '../label-with-desc';
|
||||
|
||||
export const DSLFormSelect: DSLComponent<
|
||||
DSLFormFieldCommonProps & Pick<SelectProps, 'optionList'>
|
||||
> = ({
|
||||
context: { readonly },
|
||||
props: { name, description, defaultValue, ...props },
|
||||
}) => {
|
||||
const required = !defaultValue?.value;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LabelWithDescription
|
||||
name={name}
|
||||
description={description}
|
||||
required={required}
|
||||
/>
|
||||
<UIFormSelect
|
||||
disabled={readonly}
|
||||
fieldStyle={{ padding: 0 }}
|
||||
className="w-full"
|
||||
field={name}
|
||||
initValue={defaultValue?.value}
|
||||
noLabel
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
.button {
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IconButton, useFormState } from '@coze-arch/bot-semi';
|
||||
import { IconSend } from '@coze-arch/bot-icons';
|
||||
|
||||
import {
|
||||
type DSLContext,
|
||||
type DSLComponent,
|
||||
type DSLFormFieldCommonProps,
|
||||
} from '../types';
|
||||
import { findInputElementById } from '../../../../utils/dsl-template';
|
||||
import { useChatAreaState } from '../../../../context/chat-area-state';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface DSLSubmitButtonProps {
|
||||
formFields?: string[];
|
||||
}
|
||||
|
||||
const useIsSubmitButtonDisable = ({
|
||||
context: { readonly, dsl },
|
||||
props: { formFields = [] },
|
||||
}: {
|
||||
context: DSLContext;
|
||||
props: Pick<DSLSubmitButtonProps, 'formFields'>;
|
||||
}): boolean => {
|
||||
const formState = useFormState();
|
||||
const disabled = formFields.some(field => {
|
||||
const isEmpty = !formState.values[field];
|
||||
const isError = !!formState.errors?.[field];
|
||||
const inputDefaultValue = findInputElementById(dsl, field)?.props
|
||||
?.defaultValue as DSLFormFieldCommonProps['defaultValue'];
|
||||
|
||||
if (inputDefaultValue?.value) {
|
||||
return isError;
|
||||
}
|
||||
|
||||
return isError || isEmpty;
|
||||
});
|
||||
const { isSendMessageLock } = useChatAreaState();
|
||||
|
||||
return readonly || disabled || isSendMessageLock;
|
||||
};
|
||||
|
||||
export const DSLSubmitButton: DSLComponent<DSLSubmitButtonProps> = ({
|
||||
context,
|
||||
props,
|
||||
}) => {
|
||||
const isDisabled = useIsSubmitButtonDisable({ context, props });
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<IconButton
|
||||
theme="borderless"
|
||||
className={styles.button}
|
||||
htmlType="submit"
|
||||
size="small"
|
||||
disabled={isDisabled}
|
||||
icon={<IconSend />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type RuleItem } from '@coze-arch/bot-semi/Form';
|
||||
import { Form } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type DSLFormFieldCommonProps, type DSLComponent } from '../types';
|
||||
import { LabelWithDescription } from '../label-with-desc';
|
||||
|
||||
const parseRules = (rules: RuleItem[]): RuleItem[] =>
|
||||
rules.map(rule => {
|
||||
if (rule.required) {
|
||||
return {
|
||||
...rule,
|
||||
// required 情况下,禁止输入空格
|
||||
validator: (r, v) => !!v?.trim(),
|
||||
};
|
||||
}
|
||||
return rule;
|
||||
});
|
||||
|
||||
export const DSLFormInput: DSLComponent<DSLFormFieldCommonProps> = ({
|
||||
context: { readonly },
|
||||
props: { name, description, rules, defaultValue, ...props },
|
||||
}) => {
|
||||
const required = !defaultValue?.value;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LabelWithDescription
|
||||
required={required}
|
||||
name={name}
|
||||
description={description}
|
||||
/>
|
||||
<Form.Input
|
||||
disabled={readonly}
|
||||
fieldStyle={{ padding: 0 }}
|
||||
placeholder={defaultValue?.value}
|
||||
className="w-full"
|
||||
field={name}
|
||||
noLabel
|
||||
rules={parseRules(rules)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren, type FC } from 'react';
|
||||
|
||||
import { type RuleItem } from '@coze-arch/bot-semi/Form';
|
||||
import { type InputType } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import { type DSL } from '../../../types';
|
||||
|
||||
export interface FileValue {
|
||||
fileInstance?: File;
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
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 interface DSLContext {
|
||||
dsl: DSL;
|
||||
uploadFile?: TCustomUpload;
|
||||
onChange?: (value: Record<string, TValue>) => void; // 需要兼容 file
|
||||
onSubmit?: (value: Record<string, TValue>) => void;
|
||||
readonly?: boolean; // 支持搭建时的预览模式
|
||||
}
|
||||
|
||||
export interface DSLFormFieldCommonProps {
|
||||
name: string;
|
||||
description?: string;
|
||||
rules: RuleItem[];
|
||||
defaultValue?: {
|
||||
type: InputType;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type DSLComponent<TProps = unknown> = FC<
|
||||
PropsWithChildren<{ context: DSLContext; props: TProps }>
|
||||
>;
|
||||
@@ -0,0 +1,85 @@
|
||||
.upload-button {
|
||||
min-width: 0;
|
||||
padding: 8px;
|
||||
border-style: dashed
|
||||
}
|
||||
|
||||
button.delete-btn {
|
||||
height: 20px;
|
||||
line-height: 1;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.file {
|
||||
position: relative;
|
||||
padding: 3px;
|
||||
|
||||
* {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #4E40E5;
|
||||
}
|
||||
}
|
||||
|
||||
.file-uploading::after {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: block;
|
||||
|
||||
width: var(--var-percent);
|
||||
min-width: 15%;
|
||||
height: 100%;
|
||||
|
||||
background-color: #e6e8ff;
|
||||
}
|
||||
|
||||
.container.container-error {
|
||||
.upload-button {
|
||||
border-color: #F22435;
|
||||
}
|
||||
|
||||
.input {
|
||||
border-color: #F22435;
|
||||
|
||||
}
|
||||
|
||||
.file {
|
||||
border-color: #F22435;
|
||||
}
|
||||
}
|
||||
|
||||
.retry {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
margin-right: 12px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
color: #4E40E5;
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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, useRef, useEffect } from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import { getFileInfo } from '@coze-common/chat-core';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type FileItem } from '@coze-arch/bot-semi/Upload';
|
||||
import {
|
||||
IconButton,
|
||||
Toast,
|
||||
Typography,
|
||||
UIButton,
|
||||
UIInput,
|
||||
Upload,
|
||||
useFieldApi,
|
||||
withField,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import {
|
||||
IconAdd,
|
||||
IconClose,
|
||||
IconCloseNoCycle,
|
||||
IconCopyLink,
|
||||
} from '@coze-arch/bot-icons';
|
||||
import { type shortcut_command } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import {
|
||||
type DSLFormFieldCommonProps,
|
||||
type DSLComponent,
|
||||
type TValue,
|
||||
type TCustomUpload,
|
||||
} from '../types';
|
||||
import { LabelWithDescription } from '../label-with-desc';
|
||||
import { getFileInfoByFileType } from '../../../../utils/file-const';
|
||||
|
||||
import style from './index.module.less';
|
||||
|
||||
const UploadContent: FC<{
|
||||
file: FileItem;
|
||||
disabled?: boolean;
|
||||
inputType: shortcut_command.InputType;
|
||||
onRemove: () => void;
|
||||
onRetry: () => void;
|
||||
}> = ({ file, disabled, inputType, onRemove, onRetry }) => {
|
||||
const isFailed = file.status === 'uploadFail';
|
||||
const isUploading = file.status === 'uploading';
|
||||
const fileType =
|
||||
file.fileInstance && getFileInfo(file.fileInstance)?.fileType;
|
||||
const fileIcon = fileType && getFileInfoByFileType(fileType)?.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
style.file,
|
||||
'flex border border-solid rounded-lg items-center w-full coz-stroke-primary',
|
||||
{
|
||||
[style['file-uploading'] || '']: isUploading,
|
||||
},
|
||||
)}
|
||||
style={{
|
||||
// @ts-expect-error ts 无法识别自定义变量
|
||||
'--var-percent': `${file.percent}%`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={fileIcon ?? file.url}
|
||||
className={classnames(
|
||||
'w-6 h-6',
|
||||
fileType === 'image' &&
|
||||
'rounded border border-solid coz-stroke-primary',
|
||||
)}
|
||||
/>
|
||||
<Typography.Text ellipsis className="mx-2 flex-1 text-sm">
|
||||
{file.name}
|
||||
</Typography.Text>
|
||||
{isFailed ? (
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (!disabled) {
|
||||
onRetry();
|
||||
}
|
||||
}}
|
||||
className={style.retry}
|
||||
>
|
||||
<IconClose className="coz-fg-hglt-red" />
|
||||
<div>{I18n.t('Retry')}</div>
|
||||
</div>
|
||||
) : null}
|
||||
<IconButton
|
||||
className={classnames('close-btn w-5 h-5', style['delete-btn'])}
|
||||
disabled={disabled}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
theme="borderless"
|
||||
size="small"
|
||||
icon={<IconCloseNoCycle className={style['delete-icon']} />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface UploadProps {
|
||||
value?: unknown;
|
||||
name: string;
|
||||
onChange?: (value: TValue) => void;
|
||||
uploadFile?: TCustomUpload;
|
||||
maxSize?: number;
|
||||
accept?: string;
|
||||
disabled?: boolean;
|
||||
validateStatus?: 'error' | 'success';
|
||||
inputType: shortcut_command.InputType;
|
||||
}
|
||||
|
||||
const FileUpload: FC<
|
||||
UploadProps & {
|
||||
toggle: () => void;
|
||||
}
|
||||
> = ({
|
||||
value,
|
||||
name,
|
||||
uploadFile,
|
||||
onChange,
|
||||
inputType,
|
||||
disabled,
|
||||
toggle,
|
||||
...props
|
||||
}) => {
|
||||
const [file, setFile] = useState<FileItem | undefined>();
|
||||
const fieldApi = useFieldApi(name);
|
||||
const uidRef = useRef<string | undefined>(file?.uid);
|
||||
const onUpload = (newFile: FileItem) => {
|
||||
if (newFile.fileInstance) {
|
||||
setFile({
|
||||
...newFile,
|
||||
percent: 0,
|
||||
status: 'uploading',
|
||||
});
|
||||
// 立即清理错误状态
|
||||
fieldApi.setError(true);
|
||||
uidRef.current = newFile?.uid;
|
||||
uploadFile?.({
|
||||
file: newFile.fileInstance,
|
||||
onProgress: percent => {
|
||||
if (uidRef.current !== newFile.uid) {
|
||||
return;
|
||||
}
|
||||
setFile({
|
||||
...newFile,
|
||||
percent,
|
||||
status: 'uploading',
|
||||
});
|
||||
},
|
||||
onSuccess: (url, width = 0, height = 0) => {
|
||||
if (uidRef.current !== newFile.uid) {
|
||||
return;
|
||||
}
|
||||
onChange?.({
|
||||
fileInstance: newFile.fileInstance,
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
setFile({
|
||||
...newFile,
|
||||
response: url,
|
||||
percent: 100,
|
||||
status: 'success',
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
if (uidRef.current !== newFile.uid) {
|
||||
return;
|
||||
}
|
||||
// 上传失败,触发错误状态
|
||||
fieldApi.setError(false);
|
||||
setFile({
|
||||
...newFile,
|
||||
status: 'uploadFail',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Upload
|
||||
action=""
|
||||
className="w-full"
|
||||
draggable
|
||||
limit={1}
|
||||
{...props}
|
||||
disabled={disabled}
|
||||
onAcceptInvalid={() => {
|
||||
Toast.error(I18n.t('shortcut_Illegal_file_format'));
|
||||
}}
|
||||
onSizeError={() => {
|
||||
if (props.maxSize) {
|
||||
Toast.error(
|
||||
I18n.t('file_too_large', {
|
||||
max_size: `${props.maxSize / 1024}MB`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}}
|
||||
customRequest={({ onSuccess }) => {
|
||||
// 即使 action="" ,在不传 customRequest 仍然会触发一次向当前 URL 上传文件的请求
|
||||
// 这里传一个 mock customRequest 来阻止 semi 默认的上传行为
|
||||
onSuccess('');
|
||||
}}
|
||||
showUploadList={false}
|
||||
onChange={({ currentFile }) => {
|
||||
// semi 同一个文件会触发多次 onChange,这里只响应首个
|
||||
if (
|
||||
uidRef.current !== currentFile.uid &&
|
||||
(!props.maxSize ||
|
||||
(currentFile.fileInstance?.size &&
|
||||
currentFile.fileInstance.size <= props.maxSize * 1024))
|
||||
) {
|
||||
onUpload(currentFile);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{file ? (
|
||||
<UploadContent
|
||||
file={file}
|
||||
inputType={inputType}
|
||||
onRemove={() => {
|
||||
uidRef.current = undefined;
|
||||
setFile(undefined);
|
||||
onChange?.('');
|
||||
setTimeout(() => {
|
||||
// 删除文件,清理错误状态避免立刻飘红
|
||||
fieldApi.setError(true);
|
||||
});
|
||||
}}
|
||||
onRetry={() => {
|
||||
if (file) {
|
||||
onUpload(file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<UIButton
|
||||
icon={<IconAdd />}
|
||||
disabled={disabled}
|
||||
className={classnames(style['upload-button'], 'w-full')}
|
||||
>
|
||||
<span className={style['upload-button-text-short']}>
|
||||
{I18n.t('shortcut_component_upload_component_placeholder')}
|
||||
</span>
|
||||
</UIButton>
|
||||
)}
|
||||
</Upload>
|
||||
{!file && (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon={<IconCopyLink />}
|
||||
onClick={toggle}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FileInput: FC<
|
||||
UploadProps & {
|
||||
toggle: () => void;
|
||||
}
|
||||
> = ({ disabled, onChange, toggle }) => (
|
||||
<>
|
||||
<UIInput disabled={disabled} onChange={onChange} className={style.input} />
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon={<IconCloseNoCycle />}
|
||||
onClick={toggle}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
// 为了方便控制向外传递的 value
|
||||
const UploadInner = withField((props: UploadProps) => {
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const hasError = props.validateStatus === 'error';
|
||||
const fieldApi = useFieldApi(props.name);
|
||||
|
||||
// 避免清空输入导致的飘红
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
props.onChange?.('');
|
||||
// 避免 onchange 触发校验导致立刻飘红
|
||||
setTimeout(() => {
|
||||
fieldApi.setError(true);
|
||||
});
|
||||
});
|
||||
}, [showInput]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'flex items-center justify-start gap-2',
|
||||
style.container,
|
||||
hasError && style['container-error'],
|
||||
)}
|
||||
>
|
||||
{showInput ? (
|
||||
<FileInput
|
||||
toggle={() => {
|
||||
setShowInput(false);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<FileUpload
|
||||
toggle={() => {
|
||||
setShowInput(true);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const DSLFormUpload: DSLComponent<
|
||||
DSLFormFieldCommonProps & {
|
||||
maxSize?: number;
|
||||
accept?: string;
|
||||
inputType: shortcut_command.InputType;
|
||||
}
|
||||
> = ({
|
||||
context: { uploadFile, readonly },
|
||||
props: { name, description, rules, ...props },
|
||||
}) => (
|
||||
<div>
|
||||
<LabelWithDescription name={name} description={description} />
|
||||
<UploadInner
|
||||
field={name}
|
||||
noLabel
|
||||
name={name}
|
||||
fieldStyle={{ padding: 0 }}
|
||||
uploadFile={uploadFile}
|
||||
disabled={readonly}
|
||||
rules={readonly ? undefined : rules}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const ChatAreaStateContext = createContext<{
|
||||
isSendMessageLock: boolean;
|
||||
}>({ isSendMessageLock: false });
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { ChatAreaStateContext } from './context';
|
||||
|
||||
export const useChatAreaState = () => useContext(ChatAreaStateContext);
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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, useMemo, useState } from 'react';
|
||||
|
||||
import { useInfiniteScroll } from 'ahooks';
|
||||
|
||||
import { useImperativeLayoutEffect } from '../use-imperative-layout-effect';
|
||||
import { type LoadMoreListData } from '../../components/load-more-list';
|
||||
|
||||
export interface LoadMoreHookProps<TData extends object> {
|
||||
getId: (item: TData) => string;
|
||||
listRef: RefObject<HTMLDivElement>;
|
||||
defaultList?: TData[];
|
||||
getMoreListService: (
|
||||
currentData: LoadMoreListData<TData> | undefined,
|
||||
) => Promise<LoadMoreListData<TData>>;
|
||||
}
|
||||
export const useLoadMore = <TData extends object>(
|
||||
props: LoadMoreHookProps<TData>,
|
||||
) => {
|
||||
const { getId, listRef, getMoreListService, defaultList } = props;
|
||||
const [activeId, setActiveId] = useState('');
|
||||
|
||||
const { data, loadingMore, loading, loadMore } = useInfiniteScroll<
|
||||
LoadMoreListData<TData>
|
||||
>(currentData => getMoreListService(currentData), {
|
||||
target: listRef,
|
||||
isNoMore: d => !d?.hasMore,
|
||||
});
|
||||
|
||||
const resultData = useMemo(() => {
|
||||
if (defaultList) {
|
||||
return {
|
||||
list: defaultList.concat(data?.list ?? []),
|
||||
hasMore: !!data?.hasMore,
|
||||
};
|
||||
}
|
||||
return {
|
||||
list: data?.list ?? [],
|
||||
hasMore: !!data?.hasMore,
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
const { list } = resultData;
|
||||
|
||||
const focusTo = (toItem: TData | null) => {
|
||||
if (!toItem) {
|
||||
setActiveId('');
|
||||
return;
|
||||
}
|
||||
if (!listRef.current) {
|
||||
return;
|
||||
}
|
||||
const findItem = list.find(item => getId(toItem) === getId(item));
|
||||
if (!findItem) {
|
||||
return;
|
||||
}
|
||||
const itemId = getId(findItem);
|
||||
setActiveId(itemId);
|
||||
};
|
||||
|
||||
const focusFirst = () => {
|
||||
const firstItem = list[0];
|
||||
firstItem && focusTo(firstItem);
|
||||
};
|
||||
|
||||
const scrollToFirst = () => {
|
||||
if (!listRef.current) {
|
||||
return;
|
||||
}
|
||||
listRef.current.scrollTop = 0;
|
||||
};
|
||||
|
||||
const scrollIntoView = useImperativeLayoutEffect((toItem: TData) => {
|
||||
const itemId = getId(toItem);
|
||||
const itemRef = listRef.current?.querySelector(`[data-id="${itemId}"]`);
|
||||
if (!itemRef) {
|
||||
return;
|
||||
}
|
||||
itemRef.scrollIntoView({
|
||||
behavior: 'instant' as ScrollBehavior,
|
||||
block: 'nearest',
|
||||
});
|
||||
});
|
||||
|
||||
const goNext = () => {
|
||||
const curItem = list.find(item => getId(item) === activeId);
|
||||
if (!curItem) {
|
||||
return;
|
||||
}
|
||||
const { item: nextItem, reachLimit } = getNextActiveItem<TData>({
|
||||
getId,
|
||||
list,
|
||||
curItem,
|
||||
});
|
||||
if (reachLimit) {
|
||||
loadMore();
|
||||
}
|
||||
if (!loadingMore) {
|
||||
focusTo(nextItem);
|
||||
scrollIntoView(nextItem);
|
||||
}
|
||||
};
|
||||
|
||||
const goPrev = () => {
|
||||
const curItem = list.find(item => getId(item) === activeId);
|
||||
if (!curItem) {
|
||||
return;
|
||||
}
|
||||
const { item: prevItem, reachLimit } = getPreviousItem<TData>({
|
||||
getId,
|
||||
list,
|
||||
curItem,
|
||||
});
|
||||
if (reachLimit) {
|
||||
loadMore();
|
||||
}
|
||||
focusTo(prevItem);
|
||||
scrollIntoView(prevItem);
|
||||
};
|
||||
|
||||
return {
|
||||
activeId,
|
||||
focusFirst,
|
||||
focusTo,
|
||||
scrollToFirst,
|
||||
scrollIntoView,
|
||||
goNext,
|
||||
goPrev,
|
||||
loadingMore,
|
||||
data: resultData,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
const getTargetItemAndIndex = <TData extends object>({
|
||||
getId,
|
||||
list,
|
||||
target,
|
||||
}: {
|
||||
getId: (item: TData) => string;
|
||||
list: TData[];
|
||||
target: TData;
|
||||
}) => {
|
||||
let targetIndex = -1;
|
||||
const targetItem = list.find((item, index) => {
|
||||
if (getId(item) === getId(target)) {
|
||||
targetIndex = index;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return {
|
||||
targetItem,
|
||||
targetIndex,
|
||||
};
|
||||
};
|
||||
|
||||
export const getNextActiveItem = <TData extends object>({
|
||||
curItem,
|
||||
list,
|
||||
getId,
|
||||
}: {
|
||||
curItem: TData;
|
||||
list: TData[];
|
||||
getId: (item: TData) => string;
|
||||
}): {
|
||||
reachLimit: boolean;
|
||||
item: TData;
|
||||
} => {
|
||||
const { targetIndex } = getTargetItemAndIndex({
|
||||
getId,
|
||||
list,
|
||||
target: curItem,
|
||||
});
|
||||
if (targetIndex < 0) {
|
||||
return {
|
||||
reachLimit: false,
|
||||
item: curItem,
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
const reachLimit = targetIndex >= list.length - 3;
|
||||
const nextIndex = (targetIndex + 1) % list.length;
|
||||
const item = list.at(nextIndex) || curItem;
|
||||
return {
|
||||
reachLimit,
|
||||
item,
|
||||
};
|
||||
};
|
||||
|
||||
export const getPreviousItem = <TData extends object>({
|
||||
curItem,
|
||||
list,
|
||||
getId,
|
||||
}: {
|
||||
curItem: TData;
|
||||
list: TData[];
|
||||
getId: (item: TData) => string;
|
||||
}): {
|
||||
reachLimit: boolean;
|
||||
item: TData;
|
||||
} => {
|
||||
const { targetIndex } = getTargetItemAndIndex({
|
||||
getId,
|
||||
list,
|
||||
target: curItem,
|
||||
});
|
||||
if (targetIndex < 0) {
|
||||
return {
|
||||
reachLimit: false,
|
||||
item: curItem,
|
||||
};
|
||||
}
|
||||
const reachLimit = targetIndex === 0;
|
||||
const nextIdx = (targetIndex - 1) % list.length;
|
||||
const item = list.at(nextIdx) || curItem;
|
||||
return {
|
||||
reachLimit,
|
||||
item,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep, merge } from 'lodash-es';
|
||||
import websocketManager from '@coze-common/websocket-manager-adapter';
|
||||
import {
|
||||
getFileInfo,
|
||||
type TextAndFileMixMessageProps,
|
||||
} from '@coze-common/chat-core';
|
||||
import {
|
||||
ContentType,
|
||||
type SendMessageOptions,
|
||||
useSendMultimodalMessage,
|
||||
useSendTextMessage,
|
||||
} from '@coze-common/chat-area';
|
||||
import { type PartialRequired } from '@coze-arch/bot-typings/common';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import { ToolType } from '@coze-arch/bot-api/playground_api';
|
||||
import type { ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
|
||||
import { getQueryFromTemplate } from '../utils/shortcut-query';
|
||||
import { enableSendTypePanelHideTemplate } from '../shortcut-tool/shortcut-edit/method';
|
||||
import {
|
||||
type OnBeforeSendQueryShortcutParams,
|
||||
type OnBeforeSendTemplateShortcutParams,
|
||||
} from '../shortcut-bar/types';
|
||||
import {
|
||||
type FileValue,
|
||||
type TValue,
|
||||
} from '../components/short-cut-panel/widgets/types';
|
||||
|
||||
export const useSendTextQueryMessage = () => {
|
||||
const sendTextMessage = useSendTextMessage();
|
||||
return (params: {
|
||||
queryTemplate: string;
|
||||
options?: SendMessageOptions;
|
||||
onBeforeSend?: (
|
||||
sendParams: OnBeforeSendQueryShortcutParams,
|
||||
) => OnBeforeSendQueryShortcutParams;
|
||||
shortcut: ShortCutCommand;
|
||||
}) => {
|
||||
const {
|
||||
queryTemplate,
|
||||
onBeforeSend,
|
||||
options: inputOptions,
|
||||
shortcut,
|
||||
} = params;
|
||||
const { tool_type } = shortcut;
|
||||
const useTool =
|
||||
tool_type !== undefined &&
|
||||
[ToolType.ToolTypeWorkFlow, ToolType.ToolTypePlugin].includes(tool_type);
|
||||
|
||||
const message = {
|
||||
payload: {
|
||||
text: queryTemplate,
|
||||
mention_list: [],
|
||||
},
|
||||
};
|
||||
|
||||
const pluginParams = useTool ? getPluginDefaultParams(shortcut) : {};
|
||||
|
||||
const options = merge(
|
||||
{
|
||||
extendFiled: {
|
||||
...pluginParams,
|
||||
device_id: String(websocketManager.deviceId),
|
||||
},
|
||||
},
|
||||
inputOptions,
|
||||
);
|
||||
const { message: newMessage, options: newOptions } = onBeforeSend?.({
|
||||
message,
|
||||
options,
|
||||
}) || {
|
||||
message,
|
||||
options,
|
||||
};
|
||||
sendTextMessage(
|
||||
{
|
||||
text: newMessage.payload.text,
|
||||
mentionList: newMessage.payload.mention_list,
|
||||
},
|
||||
'shortcut',
|
||||
newOptions,
|
||||
);
|
||||
sendTeaEvent(EVENT_NAMES.shortcut_use, {
|
||||
tool_type,
|
||||
use_components: !!shortcut.components_list?.length,
|
||||
show_panel: enableSendTypePanelHideTemplate(shortcut),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const useSendUseToolMessage = () => {
|
||||
const sendMultimodalMessage = useSendMultimodalMessage();
|
||||
return ({
|
||||
shortcut,
|
||||
options: inputOptions,
|
||||
componentsFormValues,
|
||||
onBeforeSendTemplateShortcut,
|
||||
withoutComponentsList = false,
|
||||
}: {
|
||||
shortcut: ShortCutCommand;
|
||||
componentsFormValues: Record<string, TValue>;
|
||||
options?: SendMessageOptions;
|
||||
onBeforeSendTemplateShortcut?: (
|
||||
params: OnBeforeSendTemplateShortcutParams,
|
||||
) => OnBeforeSendTemplateShortcutParams;
|
||||
withoutComponentsList?: boolean;
|
||||
}) => {
|
||||
const { tool_type } = shortcut;
|
||||
const sendQuery = getTemplateQuery(
|
||||
shortcut,
|
||||
componentsFormValues,
|
||||
/**
|
||||
* 无参数调用 store 场景下没有 componentList
|
||||
*/
|
||||
withoutComponentsList,
|
||||
);
|
||||
const useTool =
|
||||
tool_type !== undefined &&
|
||||
[ToolType.ToolTypeWorkFlow, ToolType.ToolTypePlugin].includes(tool_type);
|
||||
|
||||
const pluginParams = useTool
|
||||
? getPluginParams(shortcut, componentsFormValues)
|
||||
: {};
|
||||
|
||||
const imageAndFileList = getImageAndFileList(componentsFormValues);
|
||||
|
||||
const message: TextAndFileMixMessageProps = {
|
||||
payload: {
|
||||
mixList: [
|
||||
{
|
||||
type: ContentType.Text,
|
||||
// TODO 需要看下是否能够优化
|
||||
/**
|
||||
* 防止发送空消息(没有对话的气泡框) => 使用空格占位
|
||||
*/
|
||||
text: sendQuery || ' ',
|
||||
},
|
||||
...imageAndFileList,
|
||||
],
|
||||
mention_list: [],
|
||||
},
|
||||
};
|
||||
|
||||
const options = merge(
|
||||
{
|
||||
extendFiled: {
|
||||
...pluginParams,
|
||||
device_id: String(websocketManager.deviceId),
|
||||
},
|
||||
},
|
||||
inputOptions,
|
||||
);
|
||||
|
||||
const handledParams = onBeforeSendTemplateShortcut?.({
|
||||
message: cloneDeep(message),
|
||||
options: cloneDeep(options),
|
||||
}) || {
|
||||
message,
|
||||
options,
|
||||
};
|
||||
sendMultimodalMessage(
|
||||
handledParams.message ?? message,
|
||||
'shortcut',
|
||||
handledParams.options,
|
||||
);
|
||||
sendTeaEvent(EVENT_NAMES.shortcut_use, {
|
||||
tool_type,
|
||||
use_components: !!shortcut.components_list?.length,
|
||||
show_panel: !enableSendTypePanelHideTemplate(shortcut),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
interface ToolParamValue {
|
||||
value: string;
|
||||
resource_type: 'uri' | '';
|
||||
}
|
||||
|
||||
const getPluginParams = (
|
||||
shortcut: ShortCutCommand,
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
) => {
|
||||
const {
|
||||
plugin_id,
|
||||
plugin_api_name,
|
||||
components_list,
|
||||
tool_info: { tool_params_list } = {},
|
||||
} = shortcut;
|
||||
|
||||
const filterImagesValues = filterComponentFormValues(
|
||||
componentsFormValues,
|
||||
value => {
|
||||
const { fileInstance, url } = value;
|
||||
const resourceType = fileInstance ? 'uri' : '';
|
||||
return {
|
||||
value: url,
|
||||
resource_type: resourceType,
|
||||
};
|
||||
},
|
||||
value => ({
|
||||
value,
|
||||
resource_type: '',
|
||||
}),
|
||||
);
|
||||
|
||||
// key: components_list中的parameter属性 value:values中对应的值 | default_value
|
||||
const runPluginVariables = (tool_params_list ?? []).reduce<
|
||||
Record<string, ToolParamValue>
|
||||
>((acc, cur) => {
|
||||
const { default_value, name, refer_component } = cur;
|
||||
if (!name) {
|
||||
return acc;
|
||||
}
|
||||
if (!refer_component) {
|
||||
acc[name] = {
|
||||
value: default_value ?? '',
|
||||
resource_type: '',
|
||||
};
|
||||
return acc;
|
||||
}
|
||||
const targetComponentName = components_list?.find(
|
||||
com => com.parameter === name,
|
||||
)?.name;
|
||||
const componentValue =
|
||||
targetComponentName && filterImagesValues[targetComponentName];
|
||||
if (componentValue) {
|
||||
acc[name] = componentValue as ToolParamValue;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (!Object.keys(runPluginVariables).length) {
|
||||
return {
|
||||
shortcut_cmd_id: shortcut.command_id,
|
||||
toolList: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
shortcut_cmd_id: shortcut.command_id,
|
||||
toolList: [
|
||||
{
|
||||
plugin_id,
|
||||
api_name: plugin_api_name ?? '',
|
||||
parameters: runPluginVariables,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const getPluginDefaultParams = (shortcut: ShortCutCommand) => {
|
||||
const {
|
||||
plugin_id,
|
||||
plugin_api_name,
|
||||
tool_info: { tool_params_list } = {},
|
||||
} = shortcut;
|
||||
|
||||
// key: components_list中的parameter属性 value:values中对应的值 | default_value
|
||||
const runPluginVariables = (tool_params_list ?? []).reduce<
|
||||
Record<string, ToolParamValue>
|
||||
>((acc, cur) => {
|
||||
const { default_value, name } = cur;
|
||||
if (!name) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[name] = {
|
||||
value: default_value ?? '',
|
||||
resource_type: '',
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
shortcut_cmd_id: shortcut.command_id,
|
||||
toolList: [
|
||||
{
|
||||
plugin_id,
|
||||
api_name: plugin_api_name ?? '',
|
||||
parameters: runPluginVariables,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const getTemplateQuery = (
|
||||
shortcut: ShortCutCommand,
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
withoutComponentsList = false,
|
||||
) => {
|
||||
const { template_query, components_list } = shortcut;
|
||||
if (!template_query) {
|
||||
throw new Error('template_query is not defined');
|
||||
}
|
||||
// 处理图片文件
|
||||
const componentListValue = getComponentListValue(
|
||||
components_list,
|
||||
componentsFormValues,
|
||||
);
|
||||
|
||||
if (withoutComponentsList) {
|
||||
return getQueryFromTemplate(template_query, componentsFormValues ?? {});
|
||||
}
|
||||
|
||||
return getQueryFromTemplate(template_query, componentListValue);
|
||||
};
|
||||
|
||||
const filterComponentFormValues = (
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
setImageAndFileValue: (value: FileValue) => unknown,
|
||||
setTextValue: (value: string) => unknown,
|
||||
) =>
|
||||
Object.keys(componentsFormValues).reduce<Record<string, unknown>>(
|
||||
(acc, cur) => {
|
||||
const value = componentsFormValues[cur];
|
||||
// 文件类型
|
||||
if (typeof value === 'object' && value.fileInstance) {
|
||||
acc[cur] = setImageAndFileValue(value);
|
||||
return acc;
|
||||
}
|
||||
// 普通文本类型
|
||||
acc[cur] = setTextValue(value as string);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
export const getImageAndFileList = (
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
): TextAndFileMixMessageProps['payload']['mixList'] =>
|
||||
Object.keys(componentsFormValues).reduce<
|
||||
TextAndFileMixMessageProps['payload']['mixList']
|
||||
>((acc, cur) => {
|
||||
const value = componentsFormValues[cur];
|
||||
if (isComponentFile(value)) {
|
||||
acc.push({
|
||||
type: ContentType.File,
|
||||
file: value.fileInstance,
|
||||
uri: value.url,
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
if (isComponentImage(value)) {
|
||||
acc.push({
|
||||
type: ContentType.Image,
|
||||
file: value.fileInstance,
|
||||
uri: value.url,
|
||||
width: value.width || 0,
|
||||
height: value.height || 0,
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const isComponentFile = (
|
||||
value: TValue,
|
||||
): value is PartialRequired<FileValue, 'fileInstance' | 'url'> =>
|
||||
Boolean(
|
||||
typeof value === 'object' &&
|
||||
value.fileInstance &&
|
||||
getFileInfo(value.fileInstance)?.fileType !== 'image',
|
||||
);
|
||||
|
||||
const isComponentImage = (
|
||||
value: TValue,
|
||||
): value is PartialRequired<FileValue, 'fileInstance' | 'url'> =>
|
||||
Boolean(
|
||||
typeof value === 'object' &&
|
||||
value.fileInstance &&
|
||||
getFileInfo(value.fileInstance)?.fileType === 'image',
|
||||
);
|
||||
|
||||
// 获取component_list的value, 带上默认值
|
||||
export const getComponentListValue = (
|
||||
componentsList: ShortCutCommand['components_list'],
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
): Record<string, string> => {
|
||||
const filterValues = filterComponentFormValues(
|
||||
componentsFormValues,
|
||||
value => value?.fileInstance?.name,
|
||||
value => value,
|
||||
);
|
||||
|
||||
// key: components_list中的parameter属性 value:values中对应的值 | default_value
|
||||
return (componentsList ?? []).reduce<Record<string, string>>((acc, cur) => {
|
||||
const { default_value, name, hide } = cur;
|
||||
if (!name) {
|
||||
return acc;
|
||||
}
|
||||
if (hide) {
|
||||
acc[name] = default_value?.value ?? '';
|
||||
return acc;
|
||||
}
|
||||
const componentValue = filterValues[name];
|
||||
if (componentValue) {
|
||||
acc[name] = componentValue as string;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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, useRef, useLayoutEffect } from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- x
|
||||
type Destructor = (() => void) | void;
|
||||
type Fn<ARGS extends unknown[]> = (...args: ARGS) => Destructor;
|
||||
|
||||
export const useImperativeLayoutEffect = <Params extends unknown[]>(
|
||||
effect: Fn<Params>,
|
||||
deps: unknown[] = [],
|
||||
) => {
|
||||
const [effectValue, setEffectValue] = useState(0);
|
||||
const paramRef = useRef<Params>();
|
||||
const effectRef = useRef<Fn<Params>>(() => undefined);
|
||||
effectRef.current = effect;
|
||||
useLayoutEffect(() => {
|
||||
if (!effectValue) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- 体操不动, 凑活用吧
|
||||
// @ts-expect-error
|
||||
const params = paramRef.current || ([] as Params);
|
||||
return effectRef.current(...params);
|
||||
}, [effectValue, ...deps]);
|
||||
return (...args: Params) => {
|
||||
paramRef.current = args;
|
||||
setEffectValue(pre => pre + 1);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getFileInfo,
|
||||
type UploadPluginConstructor,
|
||||
} from '@coze-common/chat-core';
|
||||
import { useGetRegisteredPlugin } from '@coze-common/chat-area';
|
||||
|
||||
// 延迟1.5s后开始模拟上传进度
|
||||
const FAKE_PROGRESS_START_DELAY = 1500;
|
||||
// fake progress 初始进度
|
||||
const FAKE_PROGRESS_START = 50;
|
||||
// 最大进度
|
||||
const FAKE_PROGRESS_MAX = 85;
|
||||
// 每次步进值
|
||||
const FAKE_PROGRESS_STEP = 5;
|
||||
// 循环间隔
|
||||
const FAKE_PROGRESS_INTERVAL = 100;
|
||||
|
||||
export const useGetUploadPluginInstance = () => {
|
||||
const getRegisteredPlugin = useGetRegisteredPlugin();
|
||||
|
||||
return ({
|
||||
file,
|
||||
onProgress,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: {
|
||||
file: File;
|
||||
onProgress?: (percent: number) => void;
|
||||
onError?: (error: { status: number | undefined }) => void;
|
||||
onSuccess?: (url: string, width: number, height: number) => void;
|
||||
}) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const UploadPlugin: UploadPluginConstructor | null | undefined =
|
||||
getRegisteredPlugin('upload-plugin');
|
||||
if (!UploadPlugin) {
|
||||
return;
|
||||
}
|
||||
const uploader = new UploadPlugin({
|
||||
file,
|
||||
type: getFileInfo(file)?.fileType === 'image' ? 'image' : 'object',
|
||||
});
|
||||
// 如果1s内上传进度没有变化,主动触发fake progress, 500ms内从50%上升到80%,忽略后续的真实进度
|
||||
let isStartFakeProgress = false;
|
||||
let fakeProgressTimer: number | undefined;
|
||||
let fakeProgress = FAKE_PROGRESS_START;
|
||||
const fakeProgressHandler = () => {
|
||||
if (fakeProgress < FAKE_PROGRESS_MAX) {
|
||||
fakeProgress += FAKE_PROGRESS_STEP;
|
||||
onProgress?.(fakeProgress);
|
||||
}
|
||||
};
|
||||
|
||||
const startFakeProgressTimer = setTimeout(() => {
|
||||
isStartFakeProgress = true;
|
||||
fakeProgressTimer = window.setInterval(
|
||||
fakeProgressHandler,
|
||||
FAKE_PROGRESS_INTERVAL,
|
||||
);
|
||||
}, FAKE_PROGRESS_START_DELAY);
|
||||
|
||||
function clearFakeProgress() {
|
||||
clearTimeout(startFakeProgressTimer);
|
||||
clearInterval(fakeProgressTimer);
|
||||
fakeProgressTimer = undefined;
|
||||
fakeProgressTimer = undefined;
|
||||
isStartFakeProgress = false;
|
||||
}
|
||||
uploader.on('progress', ({ percent }) => {
|
||||
// 有假进度,忽略后续的真实进度
|
||||
if (isStartFakeProgress) {
|
||||
return;
|
||||
}
|
||||
startFakeProgressTimer && clearFakeProgress();
|
||||
onProgress?.(percent);
|
||||
});
|
||||
uploader.on('error', e => {
|
||||
onError?.({ status: e.extra.errorCode });
|
||||
clearFakeProgress();
|
||||
});
|
||||
|
||||
uploader.on(
|
||||
'complete',
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
({ uploadResult: { Url, Uri, ImageHeight = 0, ImageWidth = 0 } }) => {
|
||||
{
|
||||
onSuccess?.(Url || Uri || '', ImageWidth, ImageHeight);
|
||||
clearFakeProgress();
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
// !Notice 禁止直接导出 shortcut-tool,会导致下游依赖不需要的 knowledge-upload
|
||||
// export { ShortcutToolConfig } from './shortcut-tool';
|
||||
export { ShortcutBar } from './shortcut-bar';
|
||||
|
||||
export { ComponentsTable } from './shortcut-tool/shortcut-edit/components-table';
|
||||
|
||||
export {
|
||||
ShortCutCommand,
|
||||
getStrictShortcuts,
|
||||
} from '@coze-agent-ide/tool-config';
|
||||
|
||||
export type {
|
||||
OnBeforeSendTemplateShortcutParams,
|
||||
OnBeforeSendQueryShortcutParams,
|
||||
} from './shortcut-bar/types';
|
||||
|
||||
export { getUIModeByBizScene } from './utils/get-ui-mode-by-biz-scene';
|
||||
@@ -0,0 +1,13 @@
|
||||
.shortcut-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
:global {
|
||||
.semi-portal-inner {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// 快捷指令操作bar
|
||||
import { type CSSProperties, type FC, useRef, useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
import { useMessageWidth } from '@coze-common/chat-area';
|
||||
import { OverflowList, Popover } from '@coze-arch/bot-semi';
|
||||
import { SendType } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import {
|
||||
enableSendTypePanelHideTemplate,
|
||||
getFormValueFromShortcut,
|
||||
} from '../shortcut-tool/shortcut-edit/method';
|
||||
import { ShortcutTemplate } from '../shortcut-template';
|
||||
import { ShortcutsLoadMoreList } from '../shortcut/load-more/shortcuts-load-more-list';
|
||||
import { TemplateShortcut, LoadMore, QueryShortcut } from '../shortcut';
|
||||
import { useSendUseToolMessage } from '../hooks/shortcut';
|
||||
import { type TValue } from '../components/short-cut-panel/widgets/types';
|
||||
import {
|
||||
type OnBeforeSendQueryShortcutParams,
|
||||
type OnBeforeSendTemplateShortcutParams,
|
||||
type UIMode,
|
||||
} from './types';
|
||||
|
||||
import style from './index.module.less';
|
||||
|
||||
interface ChatShortCutBarProps {
|
||||
shortcuts: ShortCutCommand[];
|
||||
onActiveShortcutChange?: (
|
||||
shortcutInfo?: ShortCutCommand,
|
||||
isTemplateShortcutActive?: boolean,
|
||||
) => void;
|
||||
className?: string;
|
||||
wrapperClassName?: string;
|
||||
uiMode?: UIMode; // 默认为白色,有背景的时候为模糊
|
||||
defaultId?: string;
|
||||
wrapperStyle?: CSSProperties;
|
||||
toolTipFooterSlot?: React.ReactNode;
|
||||
onBeforeSendTemplateShortcut?: (
|
||||
params: OnBeforeSendTemplateShortcutParams,
|
||||
) => OnBeforeSendTemplateShortcutParams;
|
||||
onBeforeSendTextMessage?: (
|
||||
params: OnBeforeSendQueryShortcutParams,
|
||||
) => OnBeforeSendQueryShortcutParams;
|
||||
popoverTipShowBotInfo?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export const ShortcutBar: FC<ChatShortCutBarProps> = props => {
|
||||
const {
|
||||
shortcuts,
|
||||
onActiveShortcutChange,
|
||||
className,
|
||||
wrapperClassName,
|
||||
defaultId,
|
||||
uiMode = 'white',
|
||||
wrapperStyle,
|
||||
toolTipFooterSlot,
|
||||
onBeforeSendTemplateShortcut,
|
||||
onBeforeSendTextMessage,
|
||||
popoverTipShowBotInfo = false,
|
||||
} = props;
|
||||
const overflowListRef = useRef<HTMLDivElement>(null);
|
||||
const [isShowLoadMoreList, setIsShowLoadMoreList] = useState(false);
|
||||
const [activeShortcut, setActiveShortcut] = useState<
|
||||
ShortCutCommand | undefined
|
||||
>(undefined);
|
||||
const [shortcutTemplateVisible, setShortcutTemplateVisible] = useState(false);
|
||||
const sendUseToolMessage = useSendUseToolMessage();
|
||||
const messageWidth = useMessageWidth();
|
||||
|
||||
const handleActiveShortcutChange = (
|
||||
shortcut: ShortCutCommand | undefined,
|
||||
hideTemplate = false,
|
||||
) => {
|
||||
setActiveShortcut(shortcut);
|
||||
|
||||
const isTemplateShortcutActive =
|
||||
shortcut?.send_type === SendType.SendTypePanel && !hideTemplate;
|
||||
|
||||
onActiveShortcutChange?.(shortcut, isTemplateShortcutActive);
|
||||
setShortcutTemplateVisible(isTemplateShortcutActive);
|
||||
};
|
||||
|
||||
const shortcutClick = (shortcut: ShortCutCommand) => {
|
||||
/**
|
||||
* send_type=SendTypePanel 且 components_list hide均为true
|
||||
* 直接发送
|
||||
*/
|
||||
const hideTemplate = enableSendTypePanelHideTemplate(shortcut);
|
||||
|
||||
if (hideTemplate) {
|
||||
onShortcutTemplateNoParamsSubmit(
|
||||
getFormValueFromShortcut(shortcut),
|
||||
shortcut,
|
||||
);
|
||||
}
|
||||
|
||||
handleActiveShortcutChange(shortcut, hideTemplate);
|
||||
|
||||
setIsShowLoadMoreList(false);
|
||||
};
|
||||
|
||||
const closeShortcutTemplate = () => {
|
||||
setShortcutTemplateVisible(false);
|
||||
handleActiveShortcutChange(undefined);
|
||||
};
|
||||
|
||||
const renderShortcut = (shortcut: ShortCutCommand) => (
|
||||
<>
|
||||
{shortcut.send_type === SendType.SendTypeQuery && (
|
||||
<QueryShortcut
|
||||
uiMode={uiMode}
|
||||
key={shortcut.command_id}
|
||||
shortcut={shortcut}
|
||||
onBeforeSend={onBeforeSendTextMessage}
|
||||
toolTipFooterSlot={toolTipFooterSlot}
|
||||
popoverTipShowBotInfo={popoverTipShowBotInfo}
|
||||
onClick={() => shortcutClick(shortcut)}
|
||||
/>
|
||||
)}
|
||||
{shortcut.send_type === SendType.SendTypePanel && (
|
||||
<TemplateShortcut
|
||||
uiMode={uiMode}
|
||||
key={shortcut.command_id}
|
||||
shortcut={shortcut}
|
||||
toolTipFooterSlot={toolTipFooterSlot}
|
||||
popoverTipShowBotInfo={popoverTipShowBotInfo}
|
||||
onClick={() => shortcutClick(shortcut)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const onShortcutTemplateSubmit = (
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
) => {
|
||||
if (!activeShortcut) {
|
||||
return;
|
||||
}
|
||||
const { agent_id, object_id } = activeShortcut;
|
||||
sendUseToolMessage({
|
||||
shortcut: activeShortcut,
|
||||
options: {
|
||||
extendFiled: {
|
||||
extra: {
|
||||
bot_state: JSON.stringify({
|
||||
agent_id,
|
||||
bot_id: object_id,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
componentsFormValues,
|
||||
onBeforeSendTemplateShortcut,
|
||||
});
|
||||
closeShortcutTemplate();
|
||||
};
|
||||
|
||||
/**
|
||||
* sendType=panel 支持不展示组件直接发送
|
||||
*/
|
||||
const onShortcutTemplateNoParamsSubmit = (
|
||||
componentsFormValues: Record<string, TValue>,
|
||||
shortcut?: ShortCutCommand,
|
||||
) => {
|
||||
if (!shortcut) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { agent_id, object_id, components_list, tool_info } = shortcut;
|
||||
/**
|
||||
* sendType=panel,useTool=true 无参数直接发送
|
||||
*/
|
||||
const withoutComponentsList =
|
||||
!!tool_info?.tool_name && !components_list?.length;
|
||||
|
||||
sendUseToolMessage({
|
||||
shortcut,
|
||||
options: {
|
||||
extendFiled: {
|
||||
extra: {
|
||||
bot_state: JSON.stringify({
|
||||
agent_id,
|
||||
bot_id: object_id,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
componentsFormValues,
|
||||
onBeforeSendTemplateShortcut,
|
||||
withoutComponentsList,
|
||||
});
|
||||
closeShortcutTemplate();
|
||||
};
|
||||
|
||||
if (!shortcuts?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (shortcutTemplateVisible && activeShortcut) {
|
||||
return (
|
||||
<ShortcutTemplate
|
||||
shortcut={activeShortcut}
|
||||
onSubmit={onShortcutTemplateSubmit}
|
||||
visible={shortcutTemplateVisible}
|
||||
onClose={() => {
|
||||
handleActiveShortcutChange(undefined);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
style['shortcut-bar'],
|
||||
className,
|
||||
'flex justify-center items-center w-full',
|
||||
)}
|
||||
>
|
||||
<Popover
|
||||
content={
|
||||
<ShortcutsLoadMoreList
|
||||
defaultId={defaultId}
|
||||
shortcuts={shortcuts}
|
||||
onBeforeSendTextMessage={onBeforeSendTextMessage}
|
||||
onSelect={shortcutClick}
|
||||
/>
|
||||
}
|
||||
onVisibleChange={setIsShowLoadMoreList}
|
||||
position={'topLeft'}
|
||||
trigger="custom"
|
||||
visible={isShowLoadMoreList}
|
||||
spacing={{
|
||||
x: 0,
|
||||
y: 9,
|
||||
}}
|
||||
getPopupContainer={() => overflowListRef.current || document.body}
|
||||
onClickOutSide={() => setIsShowLoadMoreList(false)}
|
||||
onEscKeyDown={() => setIsShowLoadMoreList(false)}
|
||||
>
|
||||
<div
|
||||
ref={overflowListRef}
|
||||
className={cls(wrapperClassName, 'relative flex justify-start pb-4')}
|
||||
style={{
|
||||
maxWidth: messageWidth,
|
||||
...wrapperStyle,
|
||||
}}
|
||||
>
|
||||
<OverflowList
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
minVisibleItems={1}
|
||||
items={shortcuts}
|
||||
// @ts-expect-error visibleItemRenderer 有问题
|
||||
visibleItemRenderer={renderShortcut}
|
||||
overflowRenderer={overflowItems => {
|
||||
if (!overflowItems.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<LoadMore
|
||||
uiMode={uiMode}
|
||||
isLoadMoreActive={isShowLoadMoreList}
|
||||
shortcuts={shortcuts}
|
||||
onOpen={() => setIsShowLoadMoreList(true)}
|
||||
onClose={() => setIsShowLoadMoreList(false)}
|
||||
getPopupContainer={() => overflowListRef.current}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
import {
|
||||
type SendMessageOptions,
|
||||
type TextAndFileMixMessageProps,
|
||||
type TextMessageProps,
|
||||
} from '@coze-common/chat-core';
|
||||
|
||||
export interface ChatShortCutBarProps {
|
||||
shortcuts: ShortCutCommand[]; // 目前支持两种快捷键
|
||||
onClickShortCut: (shortcutInfo: ShortCutCommand) => void;
|
||||
}
|
||||
// 更新后 home 为 white 调试区、商店为 grey
|
||||
export type UIMode = 'grey' | 'white' | 'blur'; // 默认为白色,有背景的时候为模糊
|
||||
|
||||
export interface OnBeforeSendTemplateShortcutParams {
|
||||
message: TextAndFileMixMessageProps;
|
||||
options?: SendMessageOptions;
|
||||
}
|
||||
|
||||
export interface OnBeforeSendQueryShortcutParams {
|
||||
message: TextMessageProps;
|
||||
options?: SendMessageOptions;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.shortcut-template {
|
||||
margin: 0 24px 36px;
|
||||
background: #FFF;
|
||||
border: 1px solid #4E40E5;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.template-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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, useMemo, useRef } from 'react';
|
||||
|
||||
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
|
||||
import { useMessageWidth } from '@coze-common/chat-area';
|
||||
import { UIIconButton } from '@coze-arch/bot-semi';
|
||||
import { IconShortcutTemplateClose } from '@coze-arch/bot-icons';
|
||||
|
||||
import { getDSLFromComponents } from '../utils/dsl-template';
|
||||
import { useGetUploadPluginInstance } from '../hooks/use-upload-plugin';
|
||||
import { type TValue } from '../components/short-cut-panel/widgets/types';
|
||||
import { ShortCutPanel } from '../components/short-cut-panel';
|
||||
|
||||
import style from './index.module.less';
|
||||
|
||||
interface ShortcutTemplateProps {
|
||||
shortcut: Partial<ShortCutCommand>;
|
||||
visible?: boolean;
|
||||
readonly?: boolean;
|
||||
onClose?: () => void;
|
||||
onSubmit?: (componentsFormValues: Record<string, TValue>) => void;
|
||||
}
|
||||
export const ShortcutTemplate: FC<ShortcutTemplateProps> = props => {
|
||||
const { shortcut, onClose, visible, readonly, onSubmit } = props;
|
||||
const shortcutTemplateRef = useRef<HTMLDivElement>(null);
|
||||
const getRegisteredPluginInstance = useGetUploadPluginInstance();
|
||||
const messageWidth = useMessageWidth();
|
||||
|
||||
const dsl = useMemo(() => {
|
||||
const showComponents =
|
||||
shortcut.components_list?.filter(com => !com.hide) ?? [];
|
||||
return getDSLFromComponents(showComponents);
|
||||
}, [shortcut.components_list]);
|
||||
|
||||
const onShortcutPanelSubmit = (values: Record<string, TValue>) => {
|
||||
onSubmit?.(values);
|
||||
};
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={shortcutTemplateRef}
|
||||
className={style['shortcut-template']}
|
||||
style={{
|
||||
width: `calc(${messageWidth} - 48px)`,
|
||||
}}
|
||||
>
|
||||
{/*header*/}
|
||||
<div className="flex items-center text-sm coz-fg-primary px-4 py-[6px] coz-bg-primary rounded-t-3xl h-8">
|
||||
{shortcut.shortcut_icon?.url ? (
|
||||
<img
|
||||
src={shortcut.shortcut_icon.url}
|
||||
alt="icon"
|
||||
className="mr-1 h-[14px]"
|
||||
/>
|
||||
) : null}
|
||||
<div>{shortcut.command_name}</div>
|
||||
<UIIconButton
|
||||
icon={<IconShortcutTemplateClose />}
|
||||
onClick={onClose}
|
||||
wrapperClass="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
{/*content*/}
|
||||
<div className="p-3">
|
||||
<ShortCutPanel
|
||||
uploadFile={({ file, onError, onProgress, onSuccess }) => {
|
||||
getRegisteredPluginInstance?.({
|
||||
file,
|
||||
onProgress,
|
||||
onError,
|
||||
onSuccess,
|
||||
});
|
||||
}}
|
||||
readonly={readonly}
|
||||
onSubmit={onShortcutPanelSubmit}
|
||||
dsl={dsl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// 快捷指令在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;
|
||||