feat: manually mirror opencoze's code from bytedance

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

View File

@@ -0,0 +1,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;

View File

@@ -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;

View File

@@ -0,0 +1,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,63 @@
# @coze-workflow/playground
workflow 画布页面
## Overview
This package is part of the Coze Studio monorepo and provides workflow functionality. It serves as a core component in the Coze ecosystem.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-workflow/playground": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-workflow/playground';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Core functionality for Coze Studio
- TypeScript support
- Modern ES modules
## API Reference
Please refer to the TypeScript definitions for detailed API documentation.
## Development
This package is built with:
- TypeScript
- Modern JavaScript
- Vitest for testing
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,5 @@
{
"dupCheck": {
"ignoreGlobPatterns": ["src/**/*"]
}
}

View File

@@ -0,0 +1,11 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {
'@coze-arch/max-line-per-function': 'off',
'import/no-duplicates': 'off',
},
ignores: ['dist', 'node_modules'],
});

View File

@@ -0,0 +1,216 @@
{
"name": "@coze-workflow/playground",
"version": "0.0.1",
"description": "workflow 画布页面",
"license": "Apache-2.0",
"author": "lvxinsheng@bytedance.com",
"maintainers": [],
"exports": {
".": "./src/index.tsx",
"./workflow-playground": "./src/workflow-playground.tsx",
"./services/*": "./src/services/*",
"./typing": "./src/typing/index.ts"
},
"main": "src/index.tsx",
"typesVersions": {
"*": {
"typing": [
"./src/typing/index.ts"
],
"workflow-playground": [
"./src/workflow-playground.tsx"
],
"services/*": [
"./src/services/*"
]
}
},
"scripts": {
"build": "exit 0",
"create:node": "plop --plopfile ./scripts/create-node/plopfile.js",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@codemirror/commands": "^6.3.3",
"@codemirror/view": "^6.34.1",
"@coze-agent-ide/bot-plugin": "workspace:*",
"@coze-agent-ide/bot-plugin-export": "workspace:*",
"@coze-agent-ide/bot-plugin-tools": "workspace:*",
"@coze-agent-ide/model-manager": "workspace:*",
"@coze-agent-ide/plugin-risk-warning": "workspace:*",
"@coze-agent-ide/plugin-shared": "workspace:*",
"@coze-agent-ide/space-bot": "workspace:*",
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-error": "workspace:*",
"@coze-arch/bot-flags": "workspace:*",
"@coze-arch/bot-hooks": "workspace:*",
"@coze-arch/bot-http": "workspace:*",
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/bot-md-box-adapter": "workspace:*",
"@coze-arch/bot-monaco-editor": "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/bot-utils": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/hooks": "workspace:*",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/idl": "workspace:*",
"@coze-arch/load-remote-worker": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@coze-arch/report-events": "workspace:*",
"@coze-arch/report-tti": "workspace:*",
"@coze-arch/responsive-kit": "workspace:*",
"@coze-arch/web-context": "workspace:*",
"@coze-common/biz-tooltip-ui": "workspace:*",
"@coze-common/chat-uikit": "workspace:*",
"@coze-common/editor-plugins": "workspace:*",
"@coze-common/json-viewer": "workspace:*",
"@coze-common/loading-button": "workspace:*",
"@coze-common/mouse-pad-selector": "workspace:*",
"@coze-common/prompt-kit": "workspace:*",
"@coze-common/prompt-kit-adapter": "workspace:*",
"@coze-common/prompt-kit-base": "workspace:*",
"@coze-common/resource-tree": "workspace:*",
"@coze-data/database": "workspace:*",
"@coze-data/database-v2": "workspace:*",
"@coze-data/knowledge-ide-adapter": "workspace:*",
"@coze-data/knowledge-ide-base": "workspace:*",
"@coze-data/knowledge-modal-adapter": "workspace:*",
"@coze-data/knowledge-modal-base": "workspace:*",
"@coze-data/knowledge-resource-processor-base": "workspace:*",
"@coze-devops/mockset-manage": "workspace:*",
"@coze-devops/testset-manage": "workspace:*",
"@coze-editor/editor": "0.1.0-alpha.d92d50",
"@coze-editor/extension-json-empty-string-value-completion": "0.1.0-alpha.d92d50",
"@coze-editor/extension-json-hover": "0.1.0-alpha.d92d50",
"@coze-editor/extension-json-unnecessary-properties": "0.1.0-alpha.d92d50",
"@coze-editor/extension-regexp-decorator": "0.1.0-alpha.d92d50",
"@coze-foundation/layout": "workspace:*",
"@coze-foundation/local-storage": "workspace:*",
"@coze-project-ide/framework": "workspace:*",
"@coze-studio/bot-detail-store": "workspace:*",
"@coze-studio/components": "workspace:*",
"@coze-studio/open-chat": "workspace:*",
"@coze-studio/premium-components-adapter": "workspace:*",
"@coze-studio/user-store": "workspace:*",
"@coze-workflow/base": "workspace:*",
"@coze-workflow/base-adapter": "workspace:*",
"@coze-workflow/code-editor-adapter": "workspace:*",
"@coze-workflow/components": "workspace:*",
"@coze-workflow/fabric-canvas": "workspace:*",
"@coze-workflow/feature-encapsulate": "workspace:*",
"@coze-workflow/history": "workspace:*",
"@coze-workflow/nodes": "workspace:*",
"@coze-workflow/nodes-adapter": "workspace:*",
"@coze-workflow/render": "workspace:*",
"@coze-workflow/resources-adapter": "workspace:*",
"@coze-workflow/setters": "workspace:*",
"@coze-workflow/test-run": "workspace:*",
"@coze-workflow/test-run-next": "workspace:*",
"@coze-workflow/variable": "workspace:*",
"@douyinfe/semi-icons": "^2.36.0",
"@douyinfe/semi-illustrations": "^2.36.0",
"@douyinfe/semi-ui": "~2.72.3",
"@flowgram-adapter/common": "workspace:*",
"@flowgram-adapter/free-layout-editor": "workspace:*",
"@radix-ui/react-scroll-area": "^1.1.0",
"@tanstack/react-query": "~5.13.4",
"@types/parse-multipart": "~1.0.2",
"ahooks": "^3.7.8",
"ajv": "~8.12.0",
"axios": "^1.4.0",
"classnames": "^2.3.2",
"colorthief": "~2.4.0",
"copy-to-clipboard": "^3.3.3",
"cron-string-generator": "^1.0.11",
"cropperjs": "^1.5.12",
"dayjs": "^1.11.7",
"dnd-core": "~16.0.1",
"eventemitter3": "^5.0.1",
"immer": "^10.0.3",
"immutability-helper": "^3.1.1",
"inversify": "^6.0.1",
"io-ts": "2.2.20",
"lodash-es": "^4.17.21",
"md5": "^2.3.0",
"mime-types": "2.1.35",
"nanoid": "^4.0.2",
"parse-multipart": "^1.0.4",
"re-resizable": "~6.9.11",
"react-beautiful-dnd": "13.1.1",
"react-click-away-listener": "^2.2.3",
"react-cropper": "^2.3.3",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-error-boundary": "^4.0.9",
"react-intersection-observer": "~9.5.3",
"reflect-metadata": "^0.1.13",
"semver": "^7.3.7",
"slate": "0.101.5",
"slate-history": "0.100.0",
"slate-react": "0.101.5",
"use-context-selector": "^1.4.1",
"yargs-parser": "~21.1.1",
"zod": "3.22.4",
"zustand": "^4.4.7"
},
"devDependencies": {
"@codemirror/language": "^6.10.1",
"@codemirror/state": "^6.4.1",
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/tailwind-config": "workspace:*",
"@coze-arch/tea": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@lezer/common": "^1.2.2",
"@monaco-editor/react": "^4.5.2",
"@rspack/core": "0.6.0",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/json-schema": "~7.0.15",
"@types/lodash-es": "^4.17.10",
"@types/md5": "^2.3.2",
"@types/node": "^18",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@types/react-helmet": "^6.1.11",
"@types/semver": "^7.3.4",
"@vitest/coverage-v8": "~3.0.5",
"debug": "^4.3.4",
"fp-ts": "^2.5.0",
"i18next": ">= 19.0.0",
"less": "^3.13.1",
"monaco-editor": "^0.45.0",
"plop": "~4.0.1",
"prop-types": "^15.5.7",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"react-helmet": "^6.1.0",
"react-is": ">= 16.8.0",
"react-router-dom": "^6.22.0",
"scheduler": ">=0.19.0",
"styled-components": ">=4",
"stylelint": "^15.11.0",
"tailwindcss": "~3.3.3",
"typescript": "~5.8.2",
"utility-types": "^3.10.0",
"vite": "^4.3.9",
"vite-plugin-svgr": "~3.3.0",
"vitest": "~3.0.5",
"webpack": "~5.91.0"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0",
"utility-types": "^3.10.0"
},
"// deps": "debug@^4.3.4 为脚本自动补齐,请勿改动"
}

View File

@@ -0,0 +1,150 @@
const path = require('path');
const fs = require('fs');
const ROOT_DIR = process.cwd();
// 工具函数 aa-bb-cc -> AaBbCc
const getPascalName = name =>
name
.split('-')
.map(s => s.slice(0, 1).toUpperCase() + s.slice(1))
.join('');
// 工具函数 aa-bb-cc -> aaBbCc
const getCamelName = name =>
name
.split('-')
.map((s, i) => (i === 0 ? s : s.slice(0, 1).toUpperCase() + s.slice(1)))
.join('');
// 工具函数 aa-bb-cc -> AA_BB_CC
const getConstantName = name =>
name
.split('-')
.map(s => s.toUpperCase())
.join('_');
module.exports = plop => {
// 注册一个新的动作,用于在导出文件和注册文件中添加新的节点注册信息
plop.setActionType('registryNode', async answers => {
const { name, pascalName, supportTest } = answers;
const constantName = getConstantName(name);
const registryName = `${constantName}_NODE_REGISTRY`;
// 修改导出文件
const nodeExportFilePath = './src/node-registries/index.ts';
const nodeContent = fs.readFileSync(nodeExportFilePath, 'utf8');
const nodeContentNew = nodeContent.replace(
'// cli 脚本插入标识registry请勿修改/删除此行注释',
`export { ${registryName} } from './${name}';
// cli 脚本插入标识registry请勿修改/删除此行注释`,
);
fs.writeFileSync(nodeExportFilePath, nodeContentNew, 'utf8');
// 修改注册文件
const nodeRegistryFilePath = './src/nodes-v2/constants.ts';
const nodeRegistryContent = fs.readFileSync(nodeRegistryFilePath, 'utf8');
const nodeRegistryContentNew = nodeRegistryContent
.replace(
'// cli 脚本插入标识import请勿修改/删除此行注释',
`${registryName},
// cli 脚本插入标识import请勿修改/删除此行注释`,
)
.replace(
'// cli 脚本插入标识registry请勿修改/删除此行注释',
`// cli 脚本插入标识registry请勿修改/删除此行注释
${registryName},`,
);
fs.writeFileSync(nodeRegistryFilePath, nodeRegistryContentNew, 'utf8');
// 修改 node-content 注册文件
const nodeContentRegistryFilePath =
'./src/components/node-render/node-render-new/content/index.tsx';
const nodeContentRegistryContent = fs.readFileSync(
nodeContentRegistryFilePath,
'utf8',
);
const nodeContentRegistryContentNew = nodeContentRegistryContent
.replace(
'// cli 脚本插入标识import请勿修改/删除此行注释',
`import { ${pascalName}Content } from '@/node-registries/${name}';
// cli 脚本插入标识import请勿修改/删除此行注释`,
)
.replace(
'// cli 脚本插入标识registry请勿修改/删除此行注释',
`[StandardNodeType.${pascalName}]: ${pascalName}Content,
// cli 脚本插入标识registry请勿修改/删除此行注释`,
);
fs.writeFileSync(
nodeContentRegistryFilePath,
nodeContentRegistryContentNew,
'utf8',
);
// 如果节点无需支持单节点测试,删除 node-test 文件
const testFilePath = path.resolve(
ROOT_DIR,
`./src/node-registries/${name}/node-test.ts`,
);
if (!supportTest && fs.existsSync(testFilePath)) {
fs.unlinkSync(testFilePath);
}
return `节点 ${name} 已注册`;
});
// 注册一个新的生成器,用于创建新的节点目录和文件
plop.setGenerator('create node', {
description: 'generate template',
prompts: [
{
type: 'input',
name: 'name',
message:
'请输入组件名称,以"-"(空格)分隔,用于生成目录名称, eg: "database-create"',
},
{
type: 'input',
name: 'pascalName',
message:
'请确认大写驼峰命名,用于类名,注意特殊命名: http -> HTTP ,而不是 http -> Http: ',
default: answers => getPascalName(answers.name),
},
{
type: 'input',
name: 'camelName',
message:
'请确认小写驼峰命名,用于变量前缀,注意特殊命名: my-ai -> myAI而不是 my-ai -> myAi: ',
default: answers => getCamelName(answers.name),
},
{
type: 'confirm',
name: 'supportTest',
message: '是否支持单节点测试?',
default: false,
},
],
actions: data => {
const { name, pascalName, camelName, supportTest } = data;
const constantName = getConstantName(data.name);
const actions = [
{
type: 'addMany',
destination: path.resolve(ROOT_DIR, `./src/node-registries/${name}`),
templateFiles: 'templates',
data: {
PASCAL_NAME_PLACE_HOLDER: pascalName,
CAMEL_NAME_PLACE_HOLDER: camelName,
CONSTANT_NAME_PLACE_HOLDER: constantName,
SUPPORT_TEST: supportTest,
},
},
{
type: 'registryNode',
},
];
return actions;
},
});
};

View File

@@ -0,0 +1,26 @@
import { nanoid } from 'nanoid';
import { ViewVariableType } from '@coze-workflow/variable';
// 入参路径,试运行等功能依赖该路径提取参数
export const INPUT_PATH = 'inputs.inputParameters';
// 定义固定出参
export const OUTPUTS = [
{
key: nanoid(),
name: 'outputList',
type: ViewVariableType.ArrayObject,
children: [
{
key: nanoid(),
name: 'id',
type: ViewVariableType.String,
},
{
key: nanoid(),
name: 'content',
type: ViewVariableType.String,
},
],
},
];

View File

@@ -0,0 +1,20 @@
import { type NodeDataDTO } from '@coze-workflow/base';
import { type FormData } from './types';
import { OUTPUTS } from './constants';
/**
* 节点后端数据 -> 前端表单数据
*/
export const transformOnInit = (value: NodeDataDTO) => ({
...(value ?? {}),
outputs: value?.outputs ?? OUTPUTS,
});
/**
* 前端表单数据 -> 节点后端数据
* @param value
* @returns
*/
export const transformOnSubmit = (value: FormData): NodeDataDTO =>
value as unknown as NodeDataDTO;

View File

@@ -0,0 +1,42 @@
import {
ValidateTrigger,
type FormMetaV2,
} from '@flowgram-adapter/free-layout-editor';
import { createValueExpressionInputValidate } from '@/node-registries/common/validators';
import {
fireNodeTitleChange,
provideNodeOutputVariablesEffect,
} from '@/node-registries/common/effects';
import { type FormData } from './types';
import { FormRender } from './form';
import { transformOnInit, transformOnSubmit } from './data-transformer';
export const {{CONSTANT_NAME_PLACE_HOLDER}}_FORM_META: FormMetaV2<FormData> = {
// 节点表单渲染
render: () => <FormRender />,
// 验证触发时机
validateTrigger: ValidateTrigger.onChange,
// 验证规则
validate: {
// 必填
'inputs.inputParameters.0.input': createValueExpressionInputValidate({
required: true,
}),
},
// 副作用管理
effect: {
nodeMeta: fireNodeTitleChange,
outputs: provideNodeOutputVariablesEffect,
},
// 节点后端数据 -> 前端表单数据
formatOnInit: transformOnInit,
// 前端表单数据 -> 节点后端数据
formatOnSubmit: transformOnSubmit,
};

View File

@@ -0,0 +1,26 @@
import { I18n } from '@coze-arch/i18n';
import { NodeConfigForm } from '@/node-registries/common/components';
import { OutputsField, InputsParametersField } from '../common/fields';
import { INPUT_PATH } from './constants';
export const FormRender = () => (
<NodeConfigForm>
<InputsParametersField
name={INPUT_PATH}
title={I18n.t('node_http_request_params')}
tooltip={I18n.t('node_http_request_params_desc')}
defaultValue={[]}
/>
<OutputsField
title={I18n.t('workflow_detail_node_output')}
tooltip={I18n.t('node_http_response_data')}
id="{{CAMEL_NAME_PLACE_HOLDER}}-node-outputs"
name="outputs"
topLevelReadonly={true}
customReadonly
/>
</NodeConfigForm>
);

View File

@@ -0,0 +1,2 @@
export { {{CONSTANT_NAME_PLACE_HOLDER}}_NODE_REGISTRY } from './node-registry';
export { {{PASCAL_NAME_PLACE_HOLDER}}Content } from './node-content';

View File

@@ -0,0 +1,10 @@
import { InputParameters, Outputs } from '../common/components';
export function {{PASCAL_NAME_PLACE_HOLDER}}Content() {
return (
<>
<InputParameters />
<Outputs />
</>
);
}

View File

@@ -0,0 +1,27 @@
import {
DEFAULT_NODE_META_PATH,
DEFAULT_OUTPUTS_PATH,
} from '@coze-workflow/nodes';
import {
StandardNodeType,
type WorkflowNodeRegistry,
} from '@coze-workflow/base';
import { {{CONSTANT_NAME_PLACE_HOLDER}}_FORM_META } from './form-meta';
import { INPUT_PATH } from './constants';
{{#if SUPPORT_TEST}}
import { test, type NodeTestMeta } from './node-test';
{{/if}}
export const {{CONSTANT_NAME_PLACE_HOLDER}}_NODE_REGISTRY: WorkflowNodeRegistry{{#if SUPPORT_TEST}}<NodeTestMeta>{{/if}} = {
type: StandardNodeType.{{PASCAL_NAME_PLACE_HOLDER}},
meta: {
nodeDTOType: StandardNodeType.{{PASCAL_NAME_PLACE_HOLDER}},
size: { width: 360, height: 130.7 },
nodeMetaPath: DEFAULT_NODE_META_PATH,
outputsPath: DEFAULT_OUTPUTS_PATH,
inputParametersPath: INPUT_PATH,
test{{#unless SUPPORT_TEST}}: false{{/unless}},
},
formMeta: {{CONSTANT_NAME_PLACE_HOLDER}}_FORM_META,
};

View File

@@ -0,0 +1,5 @@
import type { NodeTestMeta } from '@/test-run-kit';
const test: NodeTestMeta = true;
export { test, type NodeTestMeta };

View File

@@ -0,0 +1,5 @@
import { type InputValueVO } from '@coze-workflow/base';
export interface FormData {
inputs: { inputParameters: InputValueVO[] };
}

View File

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

View File

@@ -0,0 +1,92 @@
.related-entities-option {
cursor: pointer;
margin-bottom: 2px;
padding: 0;
font-size: 14px;
line-height: 20px;
border-radius: 4px;
&:last-child {
margin-bottom: 0;
}
&:hover{
background-color: var(--coz-mg-secondary-hovered, rgba(87, 104, 161, 8%));
}
}
.related-entities-option-disabled {
cursor: not-allowed;
color: var(--Fg-COZ-fg-dim, rgba(55, 67, 106, 38%));
opacity: 0.5;
}
.related-entities-option-selected {
background-color: var(--coz-mg-primary);
}
.bot-foot-loading {
display: flex;
align-items: center;
justify-content: center;
color: #4D53E8;
background-color: white;
}
.empty-block {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 100%;
height: 100%;
.text {
margin-top: 10px;
font-size: 12px;
line-height: 16px;
color: rgba(52, 60, 87, 72%);
text-align: center;
}
}
.bot-panel-option {
margin-bottom: 2px;
&:last-child {
margin-bottom: 0;
}
}
.variable-panel-option {
margin-bottom: 2px;
padding: 0;
font-size: 14px;
line-height: 20px;
&:last-child {
margin-bottom: 0;
}
&:global(.semi-select-option-selected) {
background-color: var(--coz-mg-primary);
}
&:global(.semi-select-option):hover {
background-color: var(--coz-mg-secondary-hovered, rgba(87, 104, 161, 8%));
}
}
.variable-option-checked {
font-weight: 500;
color: rgba(81, 71, 255, 100%);
}

View File

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

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozEmpty } from '@coze-arch/coze-design/icons';
import s from '../index.module.less';
export default function EmptyContent() {
return (
<div className={s['empty-block']}>
<IconCozEmpty
style={{ fontSize: '32px', color: 'rgba(52, 60, 87, 0.72)' }}
/>
<span className={s.text}>
{I18n.t(
'variable_binding_there_are_no_variables_in_this_project',
{},
'该智能体下暂时没有定义变量',
)}
</span>
</div>
);
}

View File

@@ -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 React from 'react';
import cls from 'classnames';
import { IntelligenceType } from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import {
IconCozArrowRight,
IconCozCheckMarkFill,
} from '@coze-arch/coze-design/icons';
import { Avatar, Tag, Typography } from '@coze-arch/coze-design';
import { useHover } from '@coze-arch/hooks';
import type { IBotSelectOption } from '@/components/bot-project-select/types';
interface OptionItemProps extends IBotSelectOption {
checked?: boolean;
disabled?: boolean;
}
export default function OptionItem({
disabled,
checked,
avatar,
name,
type,
}: OptionItemProps) {
const [ref, isHover] = useHover<HTMLDivElement>();
const renderOperate = () => {
if (isHover && !disabled) {
return (
<div className={'flex items-center coz-fg-secondary flex-shrink-0'}>
<span className={'text-[12px]'}>
{I18n.t('variable_binding_continue', {}, '继续')}
</span>
<IconCozArrowRight className="text-[12px] ml-2px" />
</div>
);
}
return type === IntelligenceType.Project ? (
<Tag size="mini" color="primary" className={'flex-shrink-0'}>
{I18n.t('wf_chatflow_106')}
</Tag>
) : (
<Tag size="mini" color="primary" className={'flex-shrink-0'}>
{I18n.t('wf_chatflow_107')}
</Tag>
);
};
return (
<div
ref={ref}
className={cls('flex w-full items-center pl-8px pr-8px pt-2px pb-2px')}
>
{checked ? (
<IconCozCheckMarkFill className="text-[16px] mr-8px coz-fg-hglt flex-shrink-0" />
) : (
<div className={'w-16px h-16px mr-8px flex-shrink-0'} />
)}
<Avatar
style={{ flexShrink: 0, marginRight: 8, width: 16, height: 16 }}
shape="square"
src={avatar}
/>
<div
className="flex"
style={{ flexGrow: 1, flexShrink: 1, overflow: 'hidden' }}
>
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{
fontSize: 12,
color: '#1D1C23',
fontWeight: 400,
}}
>
{name}
</Typography.Text>
</div>
{renderOperate()}
</div>
);
}

View File

@@ -0,0 +1,176 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 cls from 'classnames';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { GlobalVariableService } from '@coze-workflow/variable';
import { IntelligenceType } from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { Tooltip } from '@coze-arch/coze-design';
import { useRelatedBotService } from '@/hooks';
import { type RelatedEntitiesProps } from '../types';
import s from '../index.module.less';
import { isItemDisabled } from '../../utils';
import type { IBotSelectOption, DisableExtraOptions } from '../../types';
import { RenderFootLoading } from '../../bots';
import OptionItem from './option-item';
import EmptyContent from './empty-content';
interface Options extends DisableExtraOptions {
onClick: () => void;
checkedValue?: string;
}
const renderCustomOption = (
item: IBotSelectOption | undefined,
extraOptions: Options,
) => {
if (!item) {
return null;
}
const {
disableBot,
disableProject,
disableBotTooltip,
disableProjectTooltip,
onClick,
checkedValue,
} = extraOptions;
const isBot = item.type === IntelligenceType.Bot;
const disabled = isItemDisabled({ disableBot, disableProject }, item.type);
const disabledTooltip =
isBot && disableBot ? disableBotTooltip : disableProjectTooltip;
const handleClick = () => {
if (disabled) {
return;
}
onClick?.();
};
return (
<div
onClick={handleClick}
className={cls(s['related-entities-option'], {
[s['related-entities-option-disabled']]: disabled,
[s['related-entities-option-selected']]: checkedValue === item.value,
})}
>
{disabled ? (
<Tooltip
keepDOM={true}
content={disabledTooltip}
position="left"
className={'w-full'}
>
<div className={'w-full'}>
<OptionItem
{...item}
disabled={disabled}
checked={checkedValue === item.value}
/>
</div>
</Tooltip>
) : (
<OptionItem
{...item}
disabled={disabled}
checked={checkedValue === item.value}
/>
)}
</div>
);
};
export default function Panel({
relatedEntities = [],
relatedEntityValue,
disableProjectTooltip,
disableProject,
disableBotTooltip,
disableBot,
isLoadMore,
onLoadMore,
onRelatedSelect,
relatedBotPanelStyle,
}: RelatedEntitiesProps) {
const relatedBotService = useRelatedBotService();
const globalVariableService = useService<GlobalVariableService>(
GlobalVariableService,
);
return (
<>
{relatedEntities && relatedEntities?.length > 0 ? (
<div
className={
'coz-fg-secondary mt-8px mb-4px pl-28px text-[12px] font-medium leading-16px'
}
>
{I18n.t(
'variable_binding_please_bind_an_agent_or_app_first',
{},
'请先绑定智能体或应用',
)}
</div>
) : null}
<div className={'h-[292px] overflow-y-auto'} style={relatedBotPanelStyle}>
{relatedEntities?.map(item =>
renderCustomOption(item, {
disableProjectTooltip,
disableProject,
disableBotTooltip,
disableBot,
onClick: () => {
relatedBotService.updateRelatedBot({
id: item.value,
type: item.type === IntelligenceType.Bot ? 'bot' : 'project',
});
globalVariableService.loadGlobalVariables(
item.type === 1 ? 'bot' : 'project',
item.value,
);
onRelatedSelect?.(item);
},
checkedValue: relatedEntityValue?.id,
}),
)}
{isLoadMore ? (
<div className={s['bot-foot-loading']}>
<RenderFootLoading onObserver={onLoadMore} />
</div>
) : null}
{!relatedEntities || relatedEntities?.length <= 0 ? (
<EmptyContent />
) : null}
</div>
</>
);
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useQuery } from '@tanstack/react-query';
import {
IntelligenceStatus,
IntelligenceType,
} from '@coze-arch/idl/intelligence_api';
import { intelligenceApi } from '@coze-arch/bot-api';
interface QueryProps {
spaceId: string;
}
export default function useQueryBotList({ spaceId }: QueryProps) {
const { data } = useQuery({
queryKey: ['related-bot-panel', 'GetDraftIntelligenceList', spaceId],
queryFn: async () => {
const res = await intelligenceApi.GetDraftIntelligenceList({
space_id: spaceId,
name: '',
types: [IntelligenceType.Bot, IntelligenceType.Project],
size: 30,
order_by: 0,
cursor_id: undefined,
status: [
IntelligenceStatus.Using,
IntelligenceStatus.Banned,
IntelligenceStatus.MoveFailed,
],
});
return res?.data ?? {};
},
retry: false,
});
return data;
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { debounce } from 'lodash-es';
import { PUBLIC_SPACE_ID } from '@coze-workflow/base';
import {
type DraftIntelligenceListData,
IntelligenceStatus,
IntelligenceType,
} from '@coze-arch/idl/intelligence_api';
import { intelligenceApi } from '@coze-arch/bot-api';
import { useGlobalState } from '@/hooks';
import { type RelatedEntitiesHookProps } from '../types';
import { useExtraBotOption } from '../../use-extra-bot-option';
import { type IBotSelectOption, type IBotSelectOptions } from '../../types';
import useQueryBotList from './use-query-bot-list';
export default function useRelated({
relatedEntityValue,
}: RelatedEntitiesHookProps) {
const [baseRelatedEntities, setBaseRelatedEntities] = useState<
IBotSelectOption[]
>([]);
const [nextCursorId, setNextCursorId] = useState<string | undefined>();
const [search, setSearch] = useState<string>('');
const [isLoadMore, setIsLoadMore] = useState<boolean>(false);
const isLoadMoreDate = useRef(false);
const { spaceId, personalSpaceId } = useGlobalState();
const querySpaceId = spaceId === PUBLIC_SPACE_ID ? personalSpaceId : spaceId;
const defaultBotData = useQueryBotList({
spaceId: querySpaceId,
});
const fetchCallback = (
fetchBotData?: DraftIntelligenceListData,
isReset = false,
) => {
const { intelligences, total = 0, next_cursor_id } = fetchBotData ?? {};
const list: IBotSelectOptions = (intelligences ?? []).map(it => ({
name: it.basic_info?.name ?? '',
value: it.basic_info?.id ?? '',
avatar: it.basic_info?.icon_url ?? '',
type: it.type || IntelligenceType.Bot,
}));
const totalList = isReset ? list : [...baseRelatedEntities, ...list];
setNextCursorId(next_cursor_id);
setBaseRelatedEntities(totalList);
setIsLoadMore(totalList.length < total);
};
const fetchBotList = async (query?: string, isReset = false) => {
const res = await intelligenceApi.GetDraftIntelligenceList({
space_id: querySpaceId,
name: query ?? search,
types: [IntelligenceType.Bot, IntelligenceType.Project],
size: 30,
order_by: 0,
cursor_id: nextCursorId,
status: [
IntelligenceStatus.Using,
IntelligenceStatus.Banned,
IntelligenceStatus.MoveFailed,
],
});
fetchCallback(res?.data, isReset);
};
const onRelatedEntitiesSearch = useCallback((query: string) => {
setSearch(query);
setNextCursorId(undefined);
fetchBotList(query, true);
}, []);
const loadMore = async () => {
if (isLoadMoreDate.current) {
return;
}
isLoadMoreDate.current = true;
await fetchBotList();
isLoadMoreDate.current = false;
};
useEffect(() => {
fetchCallback(defaultBotData, true);
}, [defaultBotData]);
const isBot = relatedEntityValue?.type === IntelligenceType.Bot;
// 由于分页限制 选中的 botId 可能找不到对应的 option 需要额外添加
const extraBotOption = useExtraBotOption(
baseRelatedEntities,
relatedEntityValue?.id,
isBot,
);
const relatedEntities = useMemo<IBotSelectOption[]>(
() => [extraBotOption, ...baseRelatedEntities].filter(e => !!e),
[extraBotOption, baseRelatedEntities],
);
return {
relatedEntities,
onRelatedEntitiesSearch: debounce(onRelatedEntitiesSearch, 300),
loadMoreRelatedEntities: loadMore,
isLoadMore,
};
}

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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, { useCallback, useRef, useState } from 'react';
import cls from 'classnames';
import { IntelligenceType } from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { IconCozMagnifier } from '@coze-arch/coze-design/icons';
import { Avatar, Select } from '@coze-arch/coze-design';
import { type Select as SemiSelect } from '@coze-arch/bot-semi';
import { useRelatedBotService } from '@/hooks';
import VariablePanel from './variable-panel/variable-panel';
import { type BotProjectVariableSelectProps, type RelatedValue } from './types';
import useRelated from './related-bot-panel/use-related';
import RelatedBotPanel from './related-bot-panel/panel';
const formatRelatedBotValue = (value?: {
id: string;
type: 'bot' | 'project';
}): RelatedValue | undefined => {
if (!value) {
return value;
}
return {
id: value.id,
type:
value.type === 'bot' ? IntelligenceType.Bot : IntelligenceType.Project,
};
};
export default function BotProjectVariableSelect({
className,
variablesFormatter,
relatedEntityValue: relatedEntityValueFromProps,
disableBot,
disableProject,
disableBotTooltip,
disableProjectTooltip,
onVariableSelect,
variableValue,
variablePanelStyle,
relatedBotPanelStyle,
customVariablePanel,
}: BotProjectVariableSelectProps) {
const selectRef = useRef<SemiSelect | null>(null);
const relatedBotService = useRelatedBotService();
const getRelatedEntityValue = () =>
relatedEntityValueFromProps ||
formatRelatedBotValue(relatedBotService.getRelatedBotValue());
const relatedEntityValue = getRelatedEntityValue();
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const [showBotPanel, setShowBotPanel] = useState(!relatedEntityValue?.id);
const {
relatedEntities,
onRelatedEntitiesSearch,
isLoadMore,
loadMoreRelatedEntities,
} = useRelated({
relatedEntityValue,
});
const handleFocus = useCallback(() => {
setTimeout(() => {
setShowBotPanel(true);
}, 100);
}, []);
const handleBlur = useCallback(() => {
selectRef.current?.clearInput?.();
selectRef.current?.close?.();
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setShowBotPanel(!getRelatedEntityValue()?.id);
}, 300);
}, []);
return (
<div className={cls(className, 'w-full flex flex-col p-4px')}>
<Select
autoClearSearchValue={true}
ref={selectRef}
filter
inputProps={{
onBlur: handleBlur,
onFocusCapture: handleFocus,
}}
value={relatedEntityValue?.id}
className={'w-full'}
size={'small'}
placeholder={I18n.t(
'variable_binding_search_project',
{},
'搜索智能体/应用',
)}
optionList={[]}
onSearch={onRelatedEntitiesSearch}
emptyContent={null}
prefix={
<IconCozMagnifier className={'text-[16px] ml-8px coz-fg-secondary'} />
}
showArrow={false}
renderSelectedItem={() => {
const item = relatedEntities.find(
e => e.value === relatedEntityValue?.id,
);
if (item) {
return (
<div className={'flex items-center'}>
<Avatar
className={'w-16px h-16px mr-8px'}
shape="square"
src={item.avatar}
/>
<span className={'text-[12px]'}>{item.name}</span>
</div>
);
}
return null;
}}
/>
<div className={'w-full coz-fg-primary'}>
{showBotPanel ? (
<RelatedBotPanel
relatedBotPanelStyle={relatedBotPanelStyle}
relatedEntityValue={relatedEntityValue}
relatedEntities={relatedEntities}
onLoadMore={loadMoreRelatedEntities}
isLoadMore={isLoadMore}
disableBot={disableBot}
disableProject={disableProject}
disableBotTooltip={disableBotTooltip}
disableProjectTooltip={disableProjectTooltip}
onRelatedSelect={() => {
setShowBotPanel(false);
}}
/>
) : (
customVariablePanel || (
<VariablePanel
variablePanelStyle={variablePanelStyle}
variableValue={variableValue}
onVariableSelect={onVariableSelect}
variablesFormatter={variablesFormatter}
/>
)
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type CSSProperties, type ReactNode } from 'react';
import { type IntelligenceType } from '@coze-arch/idl/intelligence_api';
import { type VariableMetaWithNode } from '@/form-extensions/typings';
import type {
DisableExtraOptions,
IBotSelectOption,
} from '@/components/bot-project-select/types';
export interface Variable extends VariableMetaWithNode {
disabled?: boolean;
value?: string;
}
export interface RelatedVariablesHookProps {
variablesFormatter?: (variables: Variable[]) => Variable[];
}
export interface RelatedEntitiesHookProps {
relatedEntityValue?: RelatedValue;
}
export interface RelatedValue {
id: string;
type: IntelligenceType;
}
export interface RelatedEntitiesProps extends DisableExtraOptions {
onLoadMore: () => Promise<void>;
isLoadMore: boolean;
relatedEntities?: IBotSelectOption[];
relatedEntityValue?: RelatedValue;
onRelatedSelect?: (item: IBotSelectOption) => void;
relatedBotPanelStyle?: CSSProperties;
}
export interface VariablesPanelProps {
onVariableSelect?: (value?: string) => void;
variableValue?: string;
variablePanelStyle?: CSSProperties;
variablesFormatter?: (variables: Variable[]) => Variable[];
}
export interface BotProjectVariableSelectProps extends DisableExtraOptions {
className?: string;
onVariableSelect?: (value?: string) => void;
variablesFormatter?: (variables: Variable[]) => Variable[];
relatedEntityValue?: RelatedValue;
variableValue?: string;
variablePanelStyle?: CSSProperties;
relatedBotPanelStyle?: CSSProperties;
customVariablePanel?: ReactNode;
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo } from 'react';
import { isGlobalVariableKey } from '@coze-workflow/variable';
import { useNodeAvailableVariablesWithNode } from '@/form-extensions/hooks';
import { type RelatedVariablesHookProps } from '../types';
export default function useRelatedVariable({
variablesFormatter = v => v,
}: RelatedVariablesHookProps) {
const availableVariables = useNodeAvailableVariablesWithNode();
const globalVariables = useMemo(
() =>
variablesFormatter(
availableVariables.filter(
item => item.nodeId && isGlobalVariableKey(item.nodeId),
),
),
[availableVariables, variablesFormatter],
);
return {
globalVariables,
};
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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, { useMemo } from 'react';
import cls from 'classnames';
import { useGlobalVariableServiceState } from '@coze-workflow/variable';
import { VARIABLE_TYPE_ALIAS_MAP } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { IconCozEmpty } from '@coze-arch/coze-design/icons';
import { Select, Tag, Typography } from '@coze-arch/coze-design';
import { type Variable, type VariablesPanelProps } from '../types';
import styles from '../index.module.less';
import useRelatedVariable from './use-related-variable';
interface OptionProps extends Variable {
checked?: boolean;
onSelect?: (v?: string) => void;
}
const OptionItem = (option: OptionProps) => (
<Select.Option
selected={option.checked}
key={`${option.value}-${option.disabled}`}
value={option.value}
disabled={option.disabled}
className={styles['variable-panel-option']}
onSelect={v => option.onSelect?.(v?.value)}
>
<div
style={{
maxWidth: '100%',
}}
className={
'flex items-center justify-between pl-32px pr-8px pt-2px pb-2px'
}
>
<Typography.Text
className={cls('flex-1 leading-20px', {
[styles['variable-option-checked']]: option.checked,
})}
disabled={option.disabled}
ellipsis={{ showTooltip: true }}
style={{
fontSize: 12,
marginRight: 4,
}}
>
{option.name}
</Typography.Text>
<Tag disabled={option.disabled} size="mini" color="primary">
{VARIABLE_TYPE_ALIAS_MAP[option.type]}
</Tag>
</div>
</Select.Option>
);
interface EmptyProps {
className?: string;
}
export const EmptyVariableContent = ({ className }: EmptyProps) => {
const { type } = useGlobalVariableServiceState();
const isSelectedBotOrProject = Boolean(type);
return useMemo(() => {
const getEmptyMsg = () => {
if (isSelectedBotOrProject) {
return I18n.t(
'variable_binding_there_are_no_variables_in_this_project',
);
}
return I18n.t('variable_select_empty_library_tips');
};
return (
<div className={cls(className, styles['empty-block'])}>
<IconCozEmpty
style={{ fontSize: '32px', color: 'rgba(52, 60, 87, 0.72)' }}
/>
<span className={styles.text}>{getEmptyMsg()}</span>
</div>
);
}, [isSelectedBotOrProject]);
};
export default function VariablePanel({
onVariableSelect,
variableValue,
variablePanelStyle,
variablesFormatter,
}: VariablesPanelProps) {
const { globalVariables } = useRelatedVariable({ variablesFormatter });
return (
<>
{globalVariables.length > 0 && (
<div
className={
'coz-fg-secondary mt-8px mb-4px pl-28px text-[12px] font-medium leading-16px'
}
>
{I18n.t(
'variable_binding_please_select_a_variable',
{},
'请先选择变量',
)}
</div>
)}
<div className={'h-[292px] overflow-y-auto'} style={variablePanelStyle}>
{globalVariables.map(e => (
<OptionItem
{...e}
onSelect={onVariableSelect}
checked={variableValue === e.value}
/>
))}
{globalVariables.length <= 0 && <EmptyVariableContent />}
</div>
</>
);
}

View File

@@ -0,0 +1,65 @@
.select-wrapper {
position: relative;
:global {
// 修复bot-select在画布中因为缩放导致尺寸不正常
.semi-portal-inner {
width: 100%;
}
}
}
.dropdown {
width: 100%;
min-height: 50px;
:global {
.semi-select-option-list {
&::-webkit-scrollbar {
height: 0;
min-height: 42px;
background: transparent;
}
.semi-select-loading-wrapper {
min-height: 40px;
text-align: center;
}
.semi-select-option-selected {
.semi-icon-tick {
color: var(--light-usage-primary-color-primary, #4d53e8);
}
}
}
}
}
.bot-foot-loading {
display: flex;
align-items: center;
justify-content: center;
color: #4D53E8;
background-color: white;
}
.bot-option {
display: flex;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 8px;
}
.bot-option-disabled {
cursor: not-allowed;
opacity: 0.5;
}
.loading-tag {
display: flex;
align-items: end;
font-size: 12px;
}

View File

@@ -0,0 +1,362 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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, useState, useRef, useMemo } from 'react';
import { debounce } from 'lodash-es';
import classNames from 'classnames';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { IconSearch } from '@douyinfe/semi-icons';
import { PUBLIC_SPACE_ID } from '@coze-workflow/base/constants';
import { concatTestId } from '@coze-workflow/base';
import {
IntelligenceStatus,
IntelligenceType,
} from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { Typography, Spin, Avatar, Select } from '@coze-arch/bot-semi';
import { intelligenceApi } from '@coze-arch/bot-api';
import { Tag, Tooltip } from '@coze-arch/coze-design';
import { ChatflowService } from '@/services';
import { useGlobalState, useRelatedBotService } from '../../hooks';
import { isItemDisabled } from './utils';
import { useExtraBotOption } from './use-extra-bot-option';
import type {
IBotSelectOption,
IBotSelectOptions,
ValueType,
DisableExtraOptions,
} from './types';
import styles from './bots.module.less';
const RenderCustomOption = (
item: IBotSelectOption | undefined,
extraOptions: DisableExtraOptions,
) => {
if (!item) {
return null;
}
const {
disableBot,
disableProject,
disableBotTooltip,
disableProjectTooltip,
} = extraOptions;
const isBot = item.type === IntelligenceType.Bot;
const disabled = isItemDisabled({ disableBot, disableProject }, item.type);
const disabledTooltip =
isBot && disableBot ? disableBotTooltip : disableProjectTooltip;
const renderOptionItem = optionItem => (
<div className="flex" style={{ width: '100%', alignItems: 'center' }}>
<Avatar
size="extra-extra-small"
style={{ flexShrink: 0, marginRight: 8 }}
shape="square"
src={optionItem.avatar}
/>
<div className="flex" style={{ flexGrow: 1, flexShrink: 1, width: 0 }}>
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{
fontSize: 12,
color: '#1D1C23',
fontWeight: 400,
}}
>
{optionItem.name}
</Typography.Text>
</div>
{optionItem.type === IntelligenceType.Project ? (
<Tag size="mini" color="primary">
{I18n.t('wf_chatflow_106')}
</Tag>
) : (
<Tag size="mini" color="primary">
{I18n.t('wf_chatflow_107')}
</Tag>
)}
</div>
);
return (
<Select.Option
data-testid={concatTestId(
'workflow',
'playground',
'testrun',
'bot-select',
'option',
item.value,
)}
value={item.value}
showTick={true}
key={item.value}
disabled={disabled}
className={classNames(
styles['bot-option'],
disabled ? styles['bot-option-disabled'] : '',
)}
>
{disabled ? (
<Tooltip content={disabledTooltip} position="left">
{renderOptionItem(item)}
</Tooltip>
) : (
renderOptionItem(item)
)}
</Select.Option>
);
};
export const RenderFootLoading = ({
onObserver,
}: {
onObserver: () => Promise<void>;
}) => {
const indicatorRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const callback = entries => {
if (entries[0].isIntersecting) {
onObserver?.();
}
};
const loadingObserver = new IntersectionObserver(callback);
indicatorRef.current && loadingObserver.observe(indicatorRef.current);
return () => loadingObserver.disconnect();
}, []);
return (
<div className={styles['loading-tag']} ref={indicatorRef}>
<Spin style={{ marginRight: 10 }} size="small" />
<span>{I18n.t('workflow_add_common_loading')}</span>
</div>
);
};
interface BotsProps {
isBot: boolean;
value?: ValueType;
onChange?: (value?: ValueType) => void;
}
export const Bots: React.FC<BotsProps & DisableExtraOptions> = ({
isBot,
value,
onChange,
disableBot = false,
disableProject = false,
disableBotTooltip = '',
disableProjectTooltip = '',
...props
}) => {
const idValue = value?.id;
const globalState = useGlobalState();
const DebounceTime = 500;
const chatflowService = useService<ChatflowService>(ChatflowService);
const relatedBotService = useRelatedBotService();
const isLoadMoreDate = useRef(false);
const [selectList = [], setSelectList] = useState<IBotSelectOptions>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isShowFoot, setIsShowFoot] = useState<boolean>(false);
const [search, setSearch] = useState<string>('');
const [searchTotal, setTotal] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const nextCursorRef = useRef<string | undefined>();
const useNewGlobalVariableCache = !globalState.isInIDE;
const handleChange = (botInfo?: IBotSelectOption, _value?: ValueType) => {
chatflowService.setSelectItem(botInfo);
onChange?.(_value);
};
// 由于分页限制 选中的 botId 可能找不到对应的 option 需要额外添加
const extraBotOption = useExtraBotOption(
selectList,
idValue,
isBot,
handleChange,
);
// 接口得到的总数并非真实的总数,前端可能会拼接 options
const listMaxHeight = useMemo(() => {
const realTotal = extraBotOption ? searchTotal + 1 : searchTotal;
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
return realTotal < 7 ? realTotal * 32 : 208;
}, [searchTotal, extraBotOption]);
useEffect(() => {
fetchBotList();
}, []);
useEffect(() => {
const findItem = selectList?.find(item => item.value === value?.id);
const disabled = isItemDisabled(
{ disableBot, disableProject },
findItem?.type,
);
// 禁用状态下清空选中值
if (value && findItem?.type && disabled) {
onChange?.(undefined);
}
if (value?.id !== chatflowService.selectItem?.value) {
if (!value) {
chatflowService.setSelectItem(undefined);
return;
}
if (findItem) {
chatflowService.setSelectItem(findItem);
}
}
}, [value, selectList]);
const fetchBotList = async (query?: string, isReset = false) => {
if (query) {
setIsLoading(true);
}
if (isReset) {
// 如果是重新搜索,需要清空上一次的游标
nextCursorRef.current = undefined;
}
// project 内部使用新接口查询列表
const res = await intelligenceApi.GetDraftIntelligenceList({
space_id:
globalState.spaceId === PUBLIC_SPACE_ID
? globalState.personalSpaceId
: globalState.spaceId,
name: query ?? search,
types: [IntelligenceType.Bot, IntelligenceType.Project],
size: 30,
order_by: 0,
cursor_id: nextCursorRef.current,
status: [
IntelligenceStatus.Using,
IntelligenceStatus.Banned,
IntelligenceStatus.MoveFailed,
],
});
const { intelligences, total = 0, next_cursor_id } = res?.data ?? {};
const list: IBotSelectOptions = (intelligences ?? []).map(it => ({
name: it.basic_info?.name ?? '',
value: it.basic_info?.id ?? '',
avatar: it.basic_info?.icon_url ?? '',
type: it.type || IntelligenceType.Bot,
}));
const totalList = isReset ? list : [...selectList, ...list];
setTotal(total);
nextCursorRef.current = next_cursor_id;
setSelectList(totalList);
setIsShowFoot(totalList.length < total);
setIsLoading(false);
};
const loadMoreData = async () => {
if (isLoadMoreDate.current) {
return;
}
isLoadMoreDate.current = true;
await fetchBotList();
isLoadMoreDate.current = false;
};
const handleSearch = query => {
setSearch(query);
fetchBotList(query, true);
};
return (
<div className={styles['select-wrapper']} ref={containerRef}>
<Select
value={idValue}
data-testid={concatTestId(
'workflow',
'playground',
'testerun',
'bot-select',
)}
dropdownClassName={styles.dropdown}
filter
remote
placeholder={I18n.t('wf_chatflow_73')}
emptyContent={I18n.t('agentflow_addbot_select_empty_no_bot')}
onSearch={debounce(handleSearch, DebounceTime)}
prefix={<IconSearch />}
loading={isLoading}
style={{ width: '100%' }}
virtualize={{
height: listMaxHeight,
width: '100%',
itemSize: 32,
}}
onChange={newValue => {
// 设置选中项类型bot / project
const findItem = selectList.find(item => item.value === newValue);
if (useNewGlobalVariableCache && findItem?.type) {
relatedBotService.updateRelatedBot({
id: newValue as string,
type: findItem?.type === IntelligenceType.Bot ? 'bot' : 'project',
});
}
handleChange(
findItem,
findItem
? {
id: newValue as string,
type: findItem.type,
}
: undefined,
);
}}
{...props}
>
{[extraBotOption, ...selectList]
.filter(item => item)
.map(item =>
RenderCustomOption(item, {
disableBot,
disableProject,
disableBotTooltip,
disableProjectTooltip,
}),
)}
{isShowFoot ? (
<Select.Option
value={new Date().getTime()}
key={new Date().getTime()}
className={styles['bot-foot-loading']}
disabled
>
<RenderFootLoading onObserver={loadMoreData} />
</Select.Option>
) : null}
</Select>
</div>
);
};

View File

@@ -0,0 +1,31 @@
.json-viewer {
max-height: unset;
padding: 0 6px;
background-color: unset;
border: unset;
:global {
// .flow-test-run-line {
// width: 38px;
// // &::before {}
// &::after {
// width: 30px;
// border-bottom-left-radius: 2px;
// }
// }
.field-icon {
color: var(--Light-usage-text---color-text-1, rgba(29, 28, 35, 80%));
}
.field-key {
color: var(--Light-usage-text---color-text-1, rgba(28, 31, 35, 80%));
}
.field-value {
color: var(--Light-usage-text---color-text-1, rgba(28, 31, 35, 80%));
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import { JsonViewer } from '@coze-common/json-viewer';
import styles from './chat-history.module.less';
interface Props {
data: object | null;
}
export const ChatHistory: FC<Props> = ({ data }) => (
<JsonViewer data={data} className={styles['json-viewer']} />
);

View File

@@ -0,0 +1,6 @@
.container {
margin-top: 16px;
padding: 12px;
background: var(--Light-color-grey---grey-1, #F0F0F5);
border-radius: 8px;
}

View File

@@ -0,0 +1,216 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable complexity */
import React from 'react';
import { useMount } from 'ahooks';
import { IntelligenceType } from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { withField } from '@coze-arch/bot-semi';
import { Variables } from './variables';
import { useVariables } from './use-variables';
import { useTableInfo } from './use-table-info';
import { useProjectVariables } from './use-project-variables';
import { useLTMInfo } from './use-ltm-info';
import { useChatHistory } from './use-chat-history';
import { type ValueType } from './types';
import { TableInfo } from './table-info';
import { Item } from './item';
import { ChatHistory } from './chat-history';
import { Bots } from './bots';
import styles from './index.module.less';
interface BotSelectProps {
value?: ValueType;
onChange?: (value?: ValueType) => void;
hideLabel?: boolean;
hideVariblesForce?: boolean;
hasVariableNode?: boolean;
hasVariableAssignNode?: boolean;
hasDatabaseNode?: boolean;
hasLTMNode?: boolean;
hasChatHistoryEnabledLLM?: boolean;
disableBot?: boolean;
disableBotTooltip?: string;
disableProject?: boolean;
disableProjectTooltip?: string;
}
/**
* copy from bot-select
*/
export const BotProjectSelect: React.FC<BotSelectProps> = ({
value: originValue,
onChange,
hideLabel = false,
hideVariblesForce = false,
hasVariableNode = false,
hasVariableAssignNode = false,
hasDatabaseNode = false,
hasChatHistoryEnabledLLM = false,
hasLTMNode = false,
...props
}) => {
let value = originValue;
// 兼容历史数据
if (typeof originValue === 'string') {
value = {
id: originValue,
type: IntelligenceType.Bot,
};
}
const valueId = value?.id;
const isBot = value?.type === IntelligenceType.Bot;
const { tableInfo, isLoading: isTableInfoLoading } = useTableInfo(
isBot ? valueId : undefined,
);
const { variables, isLoading: isVariablesLoading } = useVariables(
isBot ? valueId : undefined,
);
const { variables: projectVariables, isLoading: isProjectVariablesLoading } =
useProjectVariables(!isBot ? valueId : undefined);
const {
chatHistory,
// conversationId,
// sectionId,
} = useChatHistory(isBot ? valueId : undefined);
const { ltmEnabled, isLoading: isLTMInfoLoading } = useLTMInfo(
isBot ? valueId : undefined,
);
const botSelected = !!valueId;
const hasVariables = variables && variables.length > 0;
const hasTableInfo = tableInfo && tableInfo.length > 0;
const hasProjectVariables = projectVariables && projectVariables?.length > 0;
const hasChatHistory = !!chatHistory;
const showTableInfo = botSelected && !isTableInfoLoading && hasDatabaseNode;
const showVariables =
botSelected &&
!isVariablesLoading &&
(hasVariableNode || hasVariableAssignNode) &&
!hideVariblesForce;
// 试运行都是临时搞了一个会话,这里把 bot 聊天历史展示可能会误导,先隐藏
const showChatHistory = false;
// const showChatHistory =
// botSelected && !isChatHistoryLoading && hasChatHistoryEnabledLLM;
const showLTMInfo = botSelected && hasLTMNode && !isLTMInfoLoading;
const showProjectVariables =
botSelected &&
(hasVariableNode || hasVariableAssignNode) &&
!isProjectVariablesLoading &&
!isBot;
useMount(() => {
const sourceBotId = new URLSearchParams(window.location.search).get(
'bot_id',
);
if (!valueId && sourceBotId) {
onChange?.({
id: sourceBotId,
type: IntelligenceType.Bot,
});
}
});
return (
<>
<Item hideLabel={hideLabel} label={I18n.t('workflow_240218_02')}>
<Bots isBot={isBot} value={value} onChange={onChange} {...props} />
</Item>
{showProjectVariables ? (
<div className={styles.container}>
<Item
label={I18n.t('wf_chatflow_126')}
defaultText={I18n.t('wf_chatflow_127')}
>
{hasProjectVariables ? (
<Variables variables={projectVariables} />
) : null}
</Item>
</div>
) : null}
{isBot ? (
<div
className={styles.container}
style={{
display:
showVariables || showTableInfo || showChatHistory || showLTMInfo
? 'block'
: 'none',
}}
>
{showVariables ? (
<Item
label={I18n.t('workflow_detail_testrun_variable_node_field')}
defaultText={I18n.t(
'workflow_detail_testrun_variable_node_nofield',
)}
>
{hasVariables ? <Variables variables={variables} /> : null}
</Item>
) : null}
{showTableInfo ? (
<Item
label={I18n.t('workflow_240218_05')}
defaultText={I18n.t('workflow_240218_04')}
>
{hasTableInfo ? <TableInfo data={tableInfo} /> : null}
</Item>
) : null}
{showChatHistory ? (
<Item
label={I18n.t('workflow_chathistory_testrun_title')}
defaultText={I18n.t('workflow_chathistory_testrun_nocontent')}
>
{hasChatHistory ? <ChatHistory data={chatHistory} /> : null}
</Item>
) : null}
{showLTMInfo ? (
<Item label={I18n.t('ltm_240617_02')}>
<div className="text-[12px]">
{I18n.t('timecapsule_1228_001')}:{' '}
{ltmEnabled
? I18n.t('timecapsule_0124_001')
: I18n.t('timecapsule_0124_002')}
</div>
</Item>
) : null}
</div>
) : null}
</>
);
};
export const BotProjectSelectWithField = withField(BotProjectSelect, {
valueKey: 'value',
onKeyChangeFnName: 'onChange',
});
BotProjectSelectWithField.defaultProps = {
fieldStyle: { overflow: 'visible' },
};

View File

@@ -0,0 +1,24 @@
.container {
margin-top: 24px;
&:first-child {
margin-top: 0;
}
}
.label,
.default-text {
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
}
.label {
margin-bottom: 8px;
color: var(--Light-color-grey---grey-3, #A7A7B0);
}
.default-text {
color: var(--Light-color-grey---grey-3, #A7A7B0);
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type PropsWithChildren } from 'react';
import styles from './item.module.less';
interface ItemProps {
label: string;
defaultText?: string;
hideLabel?: boolean;
}
export const Item: React.FC<PropsWithChildren<ItemProps>> = ({
children,
label,
defaultText,
hideLabel = false,
}) => {
const haveChildren = !!children;
const showDefaultText = !haveChildren && !!defaultText;
const showLabel = !hideLabel && !showDefaultText;
return (
<div className={styles.container}>
{showLabel ? <div className={styles.label}>{label}</div> : null}
{showDefaultText ? (
<div className={styles['default-text']}>{defaultText}</div>
) : null}
{children}
</div>
);
};

View File

@@ -0,0 +1,56 @@
// 修改后禁用 stylelint
/* stylelint-disable no-descending-specificity */
/* stylelint-disable declaration-no-important */
.table {
:global {
table {
border: 1px solid rgba(46, 46, 56, 8%);
border-radius: 8px;
thead {
th {
font-weight: 600 !important;
color: var(--Light-usage-text---color-text-1, rgba(29, 28, 35, 80%)) !important;
border-bottom: 1px solid rgba(46, 47, 56, 9%) !important;
}
}
th,
td {
overflow: hidden;
font-size: 12px;
font-weight: 600;
font-style: normal;
line-height: 16px;
color: var(--Light-usage-text---color-text-1, rgba(29, 28, 35, 80%));
text-overflow: ellipsis;
white-space: nowrap;
}
td {
border-bottom: 0 !important;
&:first-child {
width: 114px;
}
}
}
.semi-table-tbody>.semi-table-row>.semi-table-row-cell,
.semi-table-thead>.semi-table-row>.semi-table-row-head {
padding: 8px 0;
&:first-child {
padding-right: 20px;
padding-left: 20px;
}
&:last-child {
padding-right: 14px;
}
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { Table } from '@coze-arch/bot-semi';
import { type BotTable } from '@coze-arch/bot-api/memory';
import { TagList } from '../tag-list';
import styles from './table-info.module.less';
interface TableInfoProps {
data?: BotTable[];
}
export const TableInfo: React.FC<TableInfoProps> = ({
data = [{ table_name: 'none' }],
}) => {
const columns = [
{
title: I18n.t('db_add_table_name'),
dataIndex: 'table_name',
render: (text: string) => <span>{text}</span>,
},
{
title: I18n.t('db_add_table_field_name'),
dataIndex: 'field_list',
render: (fieldList: BotTable['field_list']) => {
if (fieldList && fieldList.length > 0) {
return (
<TagList
tags={(fieldList ?? []).map(({ name }) => name || '')}
max={3}
/>
);
}
return 'none';
},
},
];
return (
<Table
className={styles.table}
columns={columns}
dataSource={data}
pagination={false}
/>
);
};

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//后端无定义 根据 bot_info 中的 workflow_info.profile_memory 推导而来
import { type IntelligenceType } from '@coze-arch/idl/intelligence_api';
export interface Variable {
key: string;
description?: string;
default_value?: string;
}
export interface IBotSelectOption {
name: string;
avatar: string;
value: string;
type: IntelligenceType;
}
export interface ValueType {
id?: string;
type?: IntelligenceType;
}
export type IBotSelectOptions = IBotSelectOption[];
export interface DisableExtraOptions {
disableBot?: boolean;
disableProject?: boolean;
disableBotTooltip?: string;
disableProjectTooltip?: string;
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useQuery } from '@tanstack/react-query';
import { IntelligenceType } from '@coze-arch/idl/intelligence_api';
import {
type GetDraftBotInfoAgwData,
type ModelInfo,
} from '@coze-arch/bot-api/playground_api';
import { type BotTable } from '@coze-arch/bot-api/memory';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { type IBotSelectOption } from './types';
export const useBotInfo = (botId?: string) => {
const { isLoading, data: botInfo } = useQuery({
queryKey: ['bot_info', botId || ''],
queryFn: async () => {
if (!botId) {
return undefined;
}
const { data } = await PlaygroundApi.GetDraftBotInfoAgw({
bot_id: botId,
});
return data;
},
});
return { isLoading, botInfo };
};
// 为 wf 使用 bot 信息做数据转换
export const transformBotInfo = {
// 模型数据
model: (data?: GetDraftBotInfoAgwData): ModelInfo =>
data?.bot_info?.model_info ?? {},
// 基本信息数据
basicInfo: (
botInfo?: GetDraftBotInfoAgwData,
): IBotSelectOption | undefined => {
if (!botInfo) {
return undefined;
}
return {
name: botInfo?.bot_info?.name ?? '',
avatar: botInfo?.bot_info?.icon_url ?? '',
value: botInfo?.bot_info?.bot_id ?? '',
type: IntelligenceType.Bot,
};
},
// 数据库信息
database: (botInfo?: GetDraftBotInfoAgwData): BotTable[] | undefined =>
botInfo?.bot_info?.database_list,
// 变量信息
variable: (botInfo?: GetDraftBotInfoAgwData) =>
botInfo?.bot_info?.variable_list,
};

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useQuery } from '@tanstack/react-query';
import { Scene } from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import { transformBotInfo, useBotInfo } from './use-bot-info';
const MAX_ROUND_COUNT = 20;
const getMessageList = async ({
botId,
botCount,
}: {
botId: string;
botCount: number;
}) => {
const totalRound = Math.ceil(botCount / MAX_ROUND_COUNT);
let cursor = '0';
let round = 0;
let conversationId = '';
let sectionId = '';
let messageList: { role: string; content: string }[] = [];
while (round < totalRound) {
const res = await DeveloperApi.GetMessageList({
bot_id: botId,
draft_mode: true,
scene: Scene.Playground,
cursor,
count: Math.min(botCount - round * MAX_ROUND_COUNT, MAX_ROUND_COUNT),
});
messageList = [
...messageList,
...res.message_list
.filter(item => item.type === 'question' || item.type === 'answer')
.map(item => ({
role: item?.role as string,
content: item?.content as string,
})),
];
conversationId = res?.connector_conversation_id || '';
sectionId = res?.last_section_id || '';
cursor = res?.cursor;
round += 1;
if (!res.hasmore) {
break;
}
}
return { messageList, conversationId, sectionId };
};
export const useChatHistory = (botId?: string) => {
const { botInfo, isLoading: isBotInfoLoading } = useBotInfo(botId);
const botCount =
transformBotInfo.model(botInfo)?.short_memory_policy?.history_round ?? 0;
const { isLoading: isMessageLoading, data } = useQuery({
queryKey: ['bot_info', botId, botCount],
queryFn: () =>
getMessageList({
botId: botId as string,
botCount,
}),
enabled: botCount !== undefined,
});
const { messageList, conversationId, sectionId } = data || {};
return {
chatHistory: messageList?.length
? {
chatHistory: messageList,
}
: null,
isLoading: isBotInfoLoading || isMessageLoading,
conversationId,
sectionId,
};
};

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo } from 'react';
import { IntelligenceType } from '@coze-arch/idl/intelligence_api';
import { useProjectItemInfo } from './use-project-info';
import { transformBotInfo, useBotInfo } from './use-bot-info';
import type { IBotSelectOption, ValueType } from './types';
export const useExtraBotOption = (
botOptionList: IBotSelectOption[],
currentBotValue?: string,
isBot?: boolean,
handleChange?: (botInfo?: IBotSelectOption, _value?: ValueType) => void,
// eslint-disable-next-line max-params
): IBotSelectOption | undefined => {
const { botInfo } = useBotInfo(isBot ? currentBotValue : undefined);
const { projectItemInfo } = useProjectItemInfo(
!isBot ? currentBotValue : undefined,
);
const botValue = useMemo(() => {
const botFinded = botOptionList.find(
({ value }) => value === currentBotValue,
);
if (!botFinded) {
const botItem = transformBotInfo.basicInfo(botInfo);
if (botItem) {
handleChange?.(botItem, {
id: botItem.value,
type: botItem.type,
});
}
return botItem;
}
return undefined;
}, [botOptionList, botInfo, currentBotValue]);
const projectValue = useMemo(() => {
const projectFinded = botOptionList.find(
({ value }) => value === currentBotValue,
);
if (projectFinded) {
return undefined;
}
let projectItem;
if (projectItemInfo) {
projectItem = {
name: projectItemInfo?.basic_info?.name || '',
value: projectItemInfo?.basic_info?.id || '',
avatar: projectItemInfo?.basic_info?.icon_url || '',
type: IntelligenceType.Project,
};
handleChange?.(projectItem, {
id: projectItem.value,
type: projectItem.type,
});
}
return projectItem;
}, [projectItemInfo, botOptionList, currentBotValue]);
return isBot ? botValue : projectValue;
};

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import get from 'lodash-es/get';
import { TimeCapsuleMode } from '@coze-arch/idl/playground_api';
import { useBotInfo } from './use-bot-info';
export const useLTMInfo = (botId?: string) => {
const { isLoading, botInfo } = useBotInfo(botId);
const timeCapsuleMode = get(
botInfo,
['bot_info', 'bot_tag_info', 'time_capsule_info', 'time_capsule_mode'],
TimeCapsuleMode.Off,
);
return {
// 是否开启长期记忆
ltmEnabled: timeCapsuleMode === TimeCapsuleMode.On,
isLoading,
};
};

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useQuery } from '@tanstack/react-query';
import { IntelligenceType } from '@coze-arch/idl/intelligence_api';
import { intelligenceApi, MemoryApi } from '@coze-arch/bot-api';
export const useProjectInfo = (projectId?: string) => {
const { isLoading, data: variableList } = useQuery({
queryKey: ['project_info', projectId || ''],
queryFn: async () => {
if (!projectId) {
return undefined;
}
const { VariableList } = await MemoryApi.GetProjectVariableList({
ProjectID: projectId,
});
return (
VariableList?.filter?.(v => v.Enable)?.map(variable => ({
key: variable.Keyword,
})) || []
);
},
});
return { isLoading, variableList };
};
export const useProjectItemInfo = (projectId?: string) => {
const { isLoading, data: projectItemInfo } = useQuery({
queryKey: ['project_item_info', projectId || ''],
queryFn: async () => {
if (!projectId) {
return undefined;
}
const { data } = await intelligenceApi.GetDraftIntelligenceInfo({
intelligence_id: projectId,
intelligence_type: IntelligenceType.Project,
});
return data;
},
});
return { isLoading, projectItemInfo };
};

View File

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

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type BotTable, BotTableRWMode } from '@coze-arch/bot-api/memory';
import { transformBotInfo, useBotInfo } from './use-bot-info';
// 多人模式下产品希望前端展示uuid & id 目前这两个字段会被后端过滤 先由前端补充这两个字段 后端充分评估过再移除过滤逻辑
function addUidAndIdToBotFieldsIfIsUnlimitedReadWriteMode(
tableInfo: BotTable[],
): BotTable[] {
tableInfo.forEach(bot => {
if (
bot.rw_mode === BotTableRWMode.UnlimitedReadWrite &&
(bot?.field_list?.length as number) > 0
) {
['id', 'uuid'].forEach(name => {
const fieldExisted = !!bot.field_list?.find(
field => field.name === name,
);
if (!fieldExisted) {
bot.field_list?.unshift({ name });
}
});
}
});
return tableInfo;
}
export const useTableInfo = (botID?: string) => {
const { isLoading, botInfo } = useBotInfo(botID);
let tableInfo: BotTable[] | undefined;
tableInfo = transformBotInfo.database(botInfo);
if (tableInfo) {
tableInfo = addUidAndIdToBotFieldsIfIsUnlimitedReadWriteMode(tableInfo);
}
return { tableInfo, isLoading };
};

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type Variable } from '@coze-arch/bot-api/playground_api';
import { transformBotInfo, useBotInfo } from './use-bot-info';
export const useVariables = (botID?: string) => {
const { botInfo, isLoading } = useBotInfo(botID);
const variables: Variable[] | undefined = transformBotInfo.variable(botInfo);
return {
variables,
isLoading,
};
};

View File

@@ -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 { IntelligenceType } from '@coze-arch/idl/intelligence_api';
export const isItemDisabled = (
{ disableBot, disableProject },
type?: IntelligenceType,
) => {
const isBot = type === IntelligenceType.Bot;
const isProject = type === IntelligenceType.Project;
const disabled = (isBot && disableBot) || (isProject && disableProject);
return disabled || !type;
};

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type Variable } from '@coze-arch/bot-api/playground_api';
import { TagList } from '../tag-list';
interface VariablesProps {
variables: Variable[];
}
export const Variables: React.FC<VariablesProps> = ({ variables }) => {
const tagList = variables.map(({ key }) => key ?? '');
return <TagList tags={tagList} max={5} />;
};

View File

@@ -0,0 +1,58 @@
.select-wrapper {
position: relative;
:global {
// 修复bot-select在画布中因为缩放导致尺寸不正常
.semi-portal-inner {
width: 100%;
}
}
}
.dropdown {
min-height: 50px;
width: 100%;
:global {
.semi-select-option-list {
&::-webkit-scrollbar {
height: 0;
background: transparent;
min-height: 42px;
}
.semi-select-loading-wrapper {
text-align: center;
min-height: 40px;
}
.semi-select-option-selected {
.semi-icon-tick {
color: var(--light-usage-primary-color-primary, #4d53e8);
}
}
}
}
}
.bot-foot-loading {
color: #4D53E8;
display: flex;
justify-content: center;
align-items: center;
background-color: white;
}
.bot-option {
display: flex;
padding-left: 24px;
padding-top: 10px;
padding-bottom: 10px;
}
.loading-tag {
font-size: 12px;
display: flex;
align-items: end;
}

View File

@@ -0,0 +1,236 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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, useState, useRef, useMemo } from 'react';
import { debounce } from 'lodash-es';
import { PUBLIC_SPACE_ID } from '@coze-workflow/base/constants';
import { concatTestId } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { Typography, Spin, Avatar, Select } from '@coze-arch/bot-semi';
import {
ListBotDraftType,
PublishStatus,
} from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import { IconSearch } from '@douyinfe/semi-icons';
import { useGlobalState } from '../../hooks';
import { useExtraBotOption } from './use-extra-bot-option';
import type { IBotSelectOption } from './types';
import styles from './bots.module.less';
type IBotSelectOptions = IBotSelectOption[];
const RenderCustomOption = item => {
if (!item) {
return null;
}
return (
<Select.Option
data-testid={concatTestId(
'workflow',
'playground',
'testrun',
'bot-select',
'option',
item.value,
)}
value={item.value}
showTick={true}
key={item.value}
className={styles['bot-option']}
>
<div className="flex" style={{ width: '100%' }}>
<Avatar
size="extra-extra-small"
style={{ flexShrink: 0, marginRight: 8 }}
shape="square"
src={item.avatar}
/>
<div className="flex" style={{ flexGrow: 1, flexShrink: 1, width: 0 }}>
<Typography.Text
ellipsis={{ showTooltip: true }}
style={{
fontSize: 12,
color: '#1D1C23',
fontWeight: 400,
}}
>
{item.name}
</Typography.Text>
</div>
</div>
</Select.Option>
);
};
const RenderFootLoading = ({
onObserver,
}: {
onObserver: () => Promise<void>;
}) => {
const indicatorRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const callback = entries => {
if (entries[0].isIntersecting) {
onObserver?.();
}
};
const loadingObserver = new IntersectionObserver(callback);
indicatorRef.current && loadingObserver.observe(indicatorRef.current);
return () => loadingObserver.disconnect();
}, []);
return (
<div className={styles['loading-tag']} ref={indicatorRef}>
<Spin style={{ marginRight: 10 }} size="small" />
<span>{I18n.t('workflow_add_common_loading')}</span>
</div>
);
};
interface BotsProps {
value?: string;
onChange?: (value: string) => void;
}
export const Bots: React.FC<BotsProps> = ({ value, onChange, ...props }) => {
const globalState = useGlobalState();
const DebounceTime = 500;
const isLoadMoreDate = useRef(false);
const [selectList = [], setSelectList] = useState<IBotSelectOptions>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isShowFoot, setIsShowFoot] = useState<boolean>(false);
const [pageIndex, setPageIndex] = useState<number>(1);
const [search, setSearch] = useState<string>('');
const [searchTotal, setTotal] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
// 由于分页限制 选中的botId可能找不到对应的option 需要额外添加
const extraBotOption = useExtraBotOption(selectList, value);
// 接口得到的总数并非真实的总数,前端可能会拼接 options
const listMaxHeight = useMemo(() => {
const realTotal = extraBotOption ? searchTotal + 1 : searchTotal;
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
return realTotal < 7 ? realTotal * 32 : 208;
}, [searchTotal, extraBotOption]);
useEffect(() => {
fetchBotList();
}, []);
const fetchBotList = async (
index?: number,
query?: string,
isReset = false,
) => {
if (query) {
setIsLoading(true);
}
const res = await DeveloperApi.GetDraftBotList({
space_id:
globalState.spaceId === PUBLIC_SPACE_ID
? globalState.personalSpaceId
: globalState.spaceId,
bot_name: query ?? search,
order_by: 0,
team_bot_type: ListBotDraftType.TeamBots,
page_index: index ?? pageIndex,
page_size: 30,
is_publish: PublishStatus.All,
});
const { bot_draft_list, total = 0 } = res?.data ?? {};
const list: IBotSelectOptions = (bot_draft_list ?? []).map(it => ({
name: it.name ?? '',
value: it.id ?? '',
avatar: it.icon_url ?? '',
}));
const totalList = isReset ? list : [...selectList, ...list];
setTotal(total);
setSelectList(totalList);
setIsShowFoot(totalList.length < total);
setIsLoading(false);
};
const loadMoreData = async () => {
if (isLoadMoreDate.current) {
return;
}
isLoadMoreDate.current = true;
const newPageIndex = pageIndex + 1;
setPageIndex(newPageIndex);
await fetchBotList(newPageIndex);
isLoadMoreDate.current = false;
};
const handleSearch = query => {
setSearch(query);
setPageIndex(1);
fetchBotList(1, query, true);
};
return (
<div className={styles['select-wrapper']} ref={containerRef}>
<Select
value={value}
data-testid={concatTestId(
'workflow',
'playground',
'testerun',
'bot-select',
)}
dropdownClassName={styles.dropdown}
showClear
filter
remote
placeholder={I18n.t('workflow_detail_testrun_variable_node_select')}
emptyContent={I18n.t('agentflow_addbot_select_empty_no_bot')}
onSearch={debounce(handleSearch, DebounceTime)}
prefix={<IconSearch />}
loading={isLoading}
style={{ width: '100%' }}
virtualize={{
height: listMaxHeight,
width: '100%',
itemSize: 32,
}}
onChange={newValue => onChange?.(newValue as string)}
{...props}
>
{[extraBotOption, ...selectList]
.filter(item => item)
.map(item => RenderCustomOption(item))}
{isShowFoot ? (
<Select.Option
value={new Date().getTime()}
key={new Date().getTime()}
className={styles['bot-foot-loading']}
disabled
>
<RenderFootLoading onObserver={loadMoreData} />
</Select.Option>
) : null}
</Select>
</div>
);
};

View File

@@ -0,0 +1,31 @@
.json-viewer {
max-height: unset;
background-color: unset;
border: unset;
padding: 0 6px;
:global {
// .flow-test-run-line {
// width: 38px;
// // &::before {}
// &::after {
// width: 30px;
// border-bottom-left-radius: 2px;
// }
// }
.field-icon {
color: var(--Light-usage-text---color-text-1, rgba(29, 28, 35, 0.80));
}
.field-key {
color: var(--Light-usage-text---color-text-1, rgba(28, 31, 35, 0.80));
}
.field-value {
color: var(--Light-usage-text---color-text-1, rgba(28, 31, 35, 0.80));
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import { JsonViewer } from '@coze-common/json-viewer';
import styles from './chat-history.module.less';
interface Props {
data: object | null;
}
export const ChatHistory: FC<Props> = ({ data }) => (
<JsonViewer data={data} className={styles['json-viewer']} />
);

View File

@@ -0,0 +1,6 @@
.container {
border-radius: 8px;
background: var(--Light-color-grey---grey-1, #F0F0F5);
padding: 12px;
margin-top: 16px;
}

View File

@@ -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.
*/
/* eslint-disable complexity */
import React from 'react';
import { useMount } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import { withField } from '@coze-arch/bot-semi';
import { Variables } from './variables';
import { useVariables } from './use-variables';
import { useTableInfo } from './use-table-info';
import { useLTMInfo } from './use-ltm-info';
import { useChatHistory } from './use-chat-history';
import { TableInfo } from './table-info';
import { Item } from './item';
import { ChatHistory } from './chat-history';
import { Bots } from './bots';
import styles from './index.module.less';
interface BotSelectProps {
value?: string;
onChange?: (value: string) => void;
hideLabel?: boolean;
hideVariblesForce?: boolean;
hasVariableNode?: boolean;
hasVariableAssignNode?: boolean;
/** @deprecated 这个字段目前没有用了,后续可以清理掉 */
hasDatabaseNode?: boolean;
hasLTMNode?: boolean;
hasChatHistoryEnabledLLM?: boolean;
}
export const BotSelect: React.FC<BotSelectProps> = ({
value,
onChange,
hideLabel = false,
hideVariblesForce = false,
hasVariableNode = false,
hasVariableAssignNode = false,
hasDatabaseNode = false,
hasChatHistoryEnabledLLM = false,
hasLTMNode = false,
...props
}) => {
const { tableInfo, isLoading: isTableInfoLoading } = useTableInfo(value);
const { variables, isLoading: isVariablesLoading } = useVariables(value);
const { chatHistory } = useChatHistory(value);
const { ltmEnabled, isLoading: isLTMInfoLoading } = useLTMInfo(value);
const botSelected = !!value;
const hasVariables = variables && variables.length > 0;
const hasTableInfo = tableInfo && tableInfo.length > 0;
const hasChatHistory = !!chatHistory;
const showTableInfo = botSelected && !isTableInfoLoading && hasDatabaseNode;
const showVariables =
botSelected &&
!isVariablesLoading &&
(hasVariableNode || hasVariableAssignNode) &&
!hideVariblesForce;
// 试运行都是临时搞了一个会话,这里把 bot 聊天历史展示可能会误导,先隐藏
const showChatHistory = false;
// const showChatHistory =
// botSelected && !isChatHistoryLoading && hasChatHistoryEnabledLLM;
const showLTMInfo = botSelected && hasLTMNode && !isLTMInfoLoading;
useMount(() => {
const sourceBotId = new URLSearchParams(window.location.search).get(
'bot_id',
);
if (!value && sourceBotId) {
onChange?.(sourceBotId);
}
});
return (
<>
<Item hideLabel={hideLabel} label={I18n.t('workflow_240218_02')}>
<Bots value={value} onChange={onChange} {...props} />
</Item>
<div
className={styles.container}
style={{
display:
showVariables || showTableInfo || showChatHistory || showLTMInfo
? 'block'
: 'none',
}}
>
{showVariables ? (
<Item
label={I18n.t('workflow_detail_testrun_variable_node_field')}
defaultText={I18n.t(
'workflow_detail_testrun_variable_node_nofield',
)}
>
{hasVariables ? <Variables variables={variables} /> : null}
</Item>
) : null}
{showTableInfo ? (
<Item
label={I18n.t('workflow_240218_05')}
defaultText={I18n.t('workflow_240218_04')}
>
{hasTableInfo ? <TableInfo data={tableInfo} /> : null}
</Item>
) : null}
{showChatHistory ? (
<Item
label={I18n.t('workflow_chathistory_testrun_title')}
defaultText={I18n.t('workflow_chathistory_testrun_nocontent')}
>
{hasChatHistory ? <ChatHistory data={chatHistory} /> : null}
</Item>
) : null}
{showLTMInfo ? (
<Item label={I18n.t('ltm_240617_02')}>
<div>
{I18n.t('timecapsule_1228_001')}:{' '}
{ltmEnabled
? I18n.t('timecapsule_0124_001')
: I18n.t('timecapsule_0124_002')}
</div>
</Item>
) : null}
</div>
</>
);
};
export const BotSelectWithField = withField(BotSelect, {
valueKey: 'value',
onKeyChangeFnName: 'onChange',
});
BotSelectWithField.defaultProps = {
fieldStyle: { overflow: 'visible' },
};

View File

@@ -0,0 +1,24 @@
.container {
margin-top: 24px;
&:first-child {
margin-top: 0;
}
}
.label,
.default-text {
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
.label {
color: var(--Light-color-grey---grey-3, #A7A7B0);
margin-bottom: 8px;
}
.default-text {
color: var(--Light-color-grey---grey-3, #A7A7B0);
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type PropsWithChildren } from 'react';
import styles from './item.module.less';
interface ItemProps {
label: string;
defaultText?: string;
hideLabel?: boolean;
}
export const Item: React.FC<PropsWithChildren<ItemProps>> = ({
children,
label,
defaultText,
hideLabel = false,
}) => {
const haveChildren = !!children;
const showDefaultText = !haveChildren && !!defaultText;
const showLabel = !hideLabel && !showDefaultText;
return (
<div className={styles.container}>
{showLabel && <div className={styles.label}>{label}</div>}
{showDefaultText && (
<div className={styles['default-text']}>{defaultText}</div>
)}
{children}
</div>
);
};

View File

@@ -0,0 +1,52 @@
.table {
:global {
table {
border-radius: 8px;
border: 1px solid rgba(46, 46, 56, 0.08);
thead {
th {
border-bottom: 1px solid rgba(46, 47, 56, 0.09) !important;
color: var(--Light-usage-text---color-text-1, rgba(29, 28, 35, 0.80)) !important;
font-weight: 600 !important;
}
}
th,
td {
overflow: hidden;
color: var(--Light-usage-text---color-text-1, rgba(29, 28, 35, 0.80));
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 16px;
}
td {
border-bottom: 0 !important;
&:first-child {
width: 114px;
}
}
}
.semi-table-tbody>.semi-table-row>.semi-table-row-cell,
.semi-table-thead>.semi-table-row>.semi-table-row-head {
padding: 8px 0;
&:first-child {
padding-left: 20px;
padding-right: 20px;
}
&:last-child {
padding-right: 14px;
}
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Table } from '@coze-arch/bot-semi';
import { type BotTable } from '@coze-arch/bot-api/memory';
import { I18n } from '@coze-arch/i18n';
import { TagList } from '../tag-list';
import styles from './table-info.module.less';
interface TableInfoProps {
data?: BotTable[];
}
export const TableInfo: React.FC<TableInfoProps> = ({
data = [{ table_name: 'none' }],
}) => {
const columns = [
{
title: I18n.t('db_add_table_name'),
dataIndex: 'table_name',
render: (text: string) => <span>{text}</span>,
},
{
title: I18n.t('db_add_table_field_name'),
dataIndex: 'field_list',
render: (fieldList: BotTable['field_list']) => {
if (fieldList && fieldList.length > 0) {
return (
<TagList
tags={(fieldList ?? []).map(({ name }) => name || '')}
max={3}
/>
);
}
return 'none';
},
},
];
return (
<Table
className={styles.table}
columns={columns}
dataSource={data}
pagination={false}
/>
);
};

View File

@@ -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.
*/
//后端无定义 根据bot_info中的workflow_info.profile_memory推导而来
export interface Variable {
key: string;
description?: string;
default_value?: string;
}
export interface IBotSelectOption {
name: string;
avatar: string;
value: string;
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useQuery } from '@tanstack/react-query';
import {
type GetDraftBotInfoAgwData,
type ModelInfo,
} from '@coze-arch/bot-api/playground_api';
import { type BotTable } from '@coze-arch/bot-api/memory';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { type IBotSelectOption } from './types';
export const useBotInfo = (botId?: string) => {
const { isLoading, data: botInfo } = useQuery({
queryKey: ['bot_info', botId || ''],
queryFn: async () => {
if (!botId) {
return undefined;
}
const { data } = await PlaygroundApi.GetDraftBotInfoAgw({
bot_id: botId,
});
return data;
},
});
return { isLoading, botInfo };
};
// 为wf使用bot信息做数据转换
export const transformBotInfo = {
// 模型数据
model: (data?: GetDraftBotInfoAgwData): ModelInfo =>
data?.bot_info?.model_info ?? {},
// 基本信息数据
basicInfo: (
botInfo?: GetDraftBotInfoAgwData,
): IBotSelectOption | undefined => {
if (!botInfo) {
return undefined;
}
return {
name: botInfo?.bot_info?.name ?? '',
avatar: botInfo?.bot_info?.icon_url ?? '',
value: botInfo?.bot_info?.bot_id ?? '',
};
},
// 数据库信息
database: (botInfo?: GetDraftBotInfoAgwData): BotTable[] | undefined =>
botInfo?.bot_info?.database_list,
// 变量信息
variable: (botInfo?: GetDraftBotInfoAgwData) =>
botInfo?.bot_info?.variable_list,
};

View File

@@ -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 { useQuery } from '@tanstack/react-query';
import { Scene } from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import { transformBotInfo, useBotInfo } from './use-bot-info';
const MAX_ROUND_COUNT = 20;
const getMessageList = async ({
botId,
botCount,
}: {
botId: string;
botCount: number;
}) => {
const totalRound = Math.ceil(botCount / MAX_ROUND_COUNT);
let cursor = '0';
let round = 0;
let messageList: { role: string; content: string }[] = [];
while (round < totalRound) {
const res = await DeveloperApi.GetMessageList({
bot_id: botId,
draft_mode: true,
scene: Scene.Playground,
cursor,
count: Math.min(botCount - round * MAX_ROUND_COUNT, MAX_ROUND_COUNT),
});
messageList = [
...messageList,
...res.message_list
.filter(item => item.type === 'question' || item.type === 'answer')
.map(item => ({
role: item?.role as string,
content: item?.content as string,
})),
];
cursor = res.cursor;
round += 1;
if (!res.hasmore) {
break;
}
}
return messageList;
};
export const useChatHistory = (botId?: string) => {
const { botInfo, isLoading: isBotInfoLoading } = useBotInfo(botId);
const botCount =
transformBotInfo.model(botInfo)?.short_memory_policy?.history_round ?? 0;
const { isLoading: isMessageLoading, data: messageList } = useQuery({
queryKey: ['bot_info', botId, botCount],
queryFn: () =>
getMessageList({
botId: botId as string,
botCount,
}),
enabled: botCount !== undefined,
});
return {
chatHistory: messageList?.length
? {
chatHistory: messageList,
}
: null,
isLoading: isBotInfoLoading || isMessageLoading,
};
};

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo } from 'react';
import { transformBotInfo, useBotInfo } from './use-bot-info';
import type { IBotSelectOption } from './types';
export const useExtraBotOption = (
botOptionList: IBotSelectOption[],
currentBotValue?: string,
): IBotSelectOption | undefined => {
const { botInfo } = useBotInfo(currentBotValue);
return useMemo(() => {
const botFinded = botOptionList.find(
({ value }) => value === currentBotValue,
);
if (!botFinded) {
return transformBotInfo.basicInfo(botInfo);
}
return undefined;
}, [botOptionList, botInfo]);
};

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import get from 'lodash-es/get';
import { TimeCapsuleMode } from '@coze-arch/idl/playground_api';
import { useBotInfo } from './use-bot-info';
export const useLTMInfo = (botId?: string) => {
const { isLoading, botInfo } = useBotInfo(botId);
const timeCapsuleMode = get(
botInfo,
['bot_info', 'bot_tag_info', 'time_capsule_info', 'time_capsule_mode'],
TimeCapsuleMode.Off,
);
return {
// 是否开启长期记忆
ltmEnabled: timeCapsuleMode === TimeCapsuleMode.On,
isLoading,
};
};

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type BotTable, BotTableRWMode } from '@coze-arch/bot-api/memory';
import { transformBotInfo, useBotInfo } from './use-bot-info';
// 多人模式下产品希望前端展示uuid & id 目前这两个字段会被后端过滤 先由前端补充这两个字段 后端充分评估过再移除过滤逻辑
function addUidAndIdToBotFieldsIfIsUnlimitedReadWriteMode(
tableInfo: BotTable[],
): BotTable[] {
tableInfo.forEach(bot => {
if (
bot.rw_mode === BotTableRWMode.UnlimitedReadWrite &&
(bot?.field_list?.length as number) > 0
) {
['id', 'uuid'].forEach(name => {
const fieldExisted = !!bot.field_list?.find(
field => field.name === name,
);
if (!fieldExisted) {
bot.field_list?.unshift({ name });
}
});
}
});
return tableInfo;
}
export const useTableInfo = (botID?: string) => {
const { isLoading, botInfo } = useBotInfo(botID);
let tableInfo: BotTable[] | undefined;
tableInfo = transformBotInfo.database(botInfo);
if (tableInfo) {
tableInfo = addUidAndIdToBotFieldsIfIsUnlimitedReadWriteMode(tableInfo);
}
return { tableInfo, isLoading };
};

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type Variable } from '@coze-arch/bot-api/playground_api';
import { transformBotInfo, useBotInfo } from './use-bot-info';
export const useVariables = (botID?: string) => {
const { botInfo, isLoading } = useBotInfo(botID);
const variables: Variable[] | undefined = transformBotInfo.variable(botInfo);
return {
variables,
isLoading,
};
};

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type Variable } from '@coze-arch/bot-api/playground_api';
import { TagList } from '../tag-list';
interface VariablesProps {
variables: Variable[];
}
export const Variables: React.FC<VariablesProps> = ({ variables }) => {
const tagList = variables.map(({ key }) => key ?? '');
return <TagList tags={tagList} max={5} />;
};

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tooltip, CozInputNumber } from '@coze-arch/coze-design';
export interface ChatHistoryRoundProps {
value?: number;
onChange?: (value: number) => void;
readonly?: boolean;
}
const MIN_ROUND = 1;
const MAX_ROUND = 30;
export const ChatHistoryRound = ({
value,
onChange,
readonly,
}: ChatHistoryRoundProps) => (
<div className="absolute right-[0] top-[9px] flex items-center gap-[4px]">
<span className="text-xs">{I18n.t('wf_history_rounds')}</span>
<Tooltip content={I18n.t('model_config_history_round_explain')}>
<IconCozInfoCircle className="coz-fg-dim text-xs" />
</Tooltip>
<CozInputNumber
className="w-[60px]"
size="small"
min={MIN_ROUND}
max={MAX_ROUND}
disabled={readonly}
value={value}
onChange={w => {
if (isNaN(w as number)) {
return;
}
onChange?.(w as number);
}}
/>
</div>
);

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useEffect } from 'react';
// import { useMutation } from '@tanstack/react-query';
import { EventType, workflowApi } from '@coze-workflow/base/api';
import { I18n } from '@coze-arch/i18n';
import { SideSheet } from '@coze-arch/coze-design';
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
import { UIIconButton } from '@coze-arch/bot-semi';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { IconClose } from '@douyinfe/semi-icons';
import { useCancelTestRun } from '../test-run/hooks/use-cancel-test-run';
import { WorkflowRunService } from '../../services';
import { useExecStateEntity, useGlobalState } from '../../hooks';
// import { useGenerateMessageFormInitValue } from './use-generate-message-form-init-value';
import { type MessageFormValue } from './types';
import { Title } from './title';
import { MessageForm } from './message-form';
export const ChatTestRunPauseSideSheet = () => {
const [visible, setVisible] = useState(false);
const { workflowId, spaceId } = useGlobalState();
const { cancelTestRun } = useCancelTestRun();
const exeState = useExecStateEntity();
const testrunService = useService<WorkflowRunService>(WorkflowRunService);
const sceneChatNodeEvent = exeState.getNodeEvent(EventType.SceneChat);
// const generateMessageFormInitValue = useGenerateMessageFormInitValue();
// const { mutate } = useMutation({
// mutationFn: workflowApi.WorkFlowTestResume,
// onSuccess: () => {
// debugger;
// testrunService.continueTestRun();
// setVisible(false);
// },
// });
useEffect(() => {
if (sceneChatNodeEvent) {
testrunService.pauseTestRun();
setVisible(true);
} else {
setVisible(false);
}
}, [sceneChatNodeEvent]);
const handleClose = () => {
setVisible(false);
cancelTestRun();
};
const handleMessageFormSubmit = async (values: MessageFormValue) => {
await workflowApi.WorkFlowTestResume({
workflow_id: workflowId,
execute_id: exeState.config.executeId,
event_id: sceneChatNodeEvent?.id ?? '',
space_id: spaceId,
data: JSON.stringify(values),
});
testrunService.continueTestRun();
setVisible(false);
// debugger;
// mutate({
// workflow_id: workflowId,
// execute_id: exeState.config.executeId,
// event_id: sceneChatNodeEvent?.id ?? '',
// data: JSON.stringify(values),
// });
};
if (visible) {
return (
<SideSheet
visible={visible}
closable={false}
width={600}
title={
<div className="flex items-center">
<UIIconButton
type="secondary"
icon={<IconClose style={{ color: '#1C1D23' }} />}
iconSize="large"
onClick={handleClose}
/>
{I18n.t(
'scene_workflow_chat_node_test_run_title',
{},
'Test Q&A nodes',
)}
</div>
}
style={{
background: '#F7F7FA',
}}
>
<div className="h-full flex flex-col">
<Title icon={sceneChatNodeEvent?.node_icon ?? ''} />
<div className="flex-1">
<MessageForm
initValues={{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Messages: (typeSafeJSONParse(sceneChatNodeEvent?.data) as any)
?.messages,
// messages:
// generateMessageFormInitValue(
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// (typeSafeJSONParse(sceneChatNodeEvent?.data) as any)
// ?.messages,
// ) ?? [],
}}
onSubmit={handleMessageFormSubmit}
/>
</div>
</div>
</SideSheet>
);
} else {
return null;
}
};

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Tag } from '@coze-arch/coze-design';
import { Form, ArrayField, UIButton, Typography } from '@coze-arch/bot-semi';
import { IconPlay } from '@douyinfe/semi-icons';
import { type MessageFormValue } from './types';
const { Input } = Form;
interface MessageFormProps {
initValues: MessageFormValue;
onSubmit: (values: MessageFormValue) => void;
}
export const MessageForm: FC<MessageFormProps> = props => {
const { initValues, onSubmit } = props;
const formRef = useRef<Form<MessageFormValue>>(null);
const renderNickNameLabel = (nickName, roleName) => (
<>
{`${nickName}`} <Tag>{roleName ? roleName : nickName}</Tag>
</>
);
return (
<Form<MessageFormValue>
ref={formRef}
onSubmit={onSubmit}
className="relative h-full flex flex-col"
>
<div className="p-6 rounded-lg coz-bg-max coz-stroke-primary flex-1 mb-6 border border-solid">
<ArrayField field="Messages" initValue={initValues.Messages}>
{({ arrayFields }) => (
<>
{arrayFields.map(({ field, key, remove }, index) => {
if (initValues.Messages[index].content) {
return (
<div className="py-3">
<Form.Label>
{renderNickNameLabel(
initValues.Messages[index].nickname,
initValues.Messages[index].role,
)}
</Form.Label>
<div>
<Typography.Text>
{initValues.Messages[index].content}
</Typography.Text>
</div>
</div>
);
} else {
return (
<Input
label={renderNickNameLabel(
initValues.Messages[index].nickname,
initValues.Messages[index].role,
)}
field={`${field}[content]`}
rules={[
{
required: true,
message: 'Required',
},
]}
/>
);
}
})}
</>
)}
</ArrayField>
</div>
<div className="mb-6 text-right">
<UIButton
icon={<IconPlay />}
type="primary"
theme="solid"
htmlType="submit"
className="btn-margin-right "
>
{I18n.t(
'scene_workflow_chat_node_test_run_button',
{},
'Continue running',
)}
</UIButton>
</div>
</Form>
);
};

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Tag, Typography } from '@coze-arch/coze-design';
export const Title: FC<{
icon: string;
}> = props => {
const { icon } = props;
return (
<div className="flex items-center gap-2 mb-3">
<img src={icon} width={16} height={16} />
<Typography.Title heading={6}>
{I18n.t('scene_workflow_chat_node_name', {}, 'Role scheduling')}
</Typography.Title>
<Tag color="cyan" loading>
{I18n.t('scene_workflow_chat_node_test_run_running', {}, 'Running')}
</Tag>
</div>
);
};

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface MessageFormValue {
// eslint-disable-next-line @typescript-eslint/naming-convention
Messages: Array<MessageValue>;
}
export interface MessageValue {
role: string;
content: string;
nickname: string;
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useGetSceneFlowRoleList } from '../../hooks/use-get-scene-flow-params';
import { type SpeakerMessageSetValue } from '../../form-extensions/setters/speaker-message-set-array/types';
import { type MessageValue } from './types';
export const useGenerateMessageFormInitValue = () => {
const { data: roleList } = useGetSceneFlowRoleList();
return (messages: Array<SpeakerMessageSetValue> | undefined) => {
const result = messages?.reduce<Array<MessageValue>>((buf, message) => {
const role = roleList?.find(
_role => _role.biz_role_id === message.biz_role_id,
);
if (!role) {
return buf;
} else {
buf.push({
role: message.role,
content: message.content,
nickname: message.nickname ?? '',
});
return buf;
}
}, []);
return result;
};
};

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRenderer, option } from '@coze-editor/editor/react';
import preset, { languages, themes } from '@coze-editor/editor/preset-code';
import { shell } from '@coze-editor/editor/language-shell';
import { json } from '@coze-editor/editor/language-json';
import { mixLanguages } from '@coze-editor/editor';
import { EditorView } from '@codemirror/view';
import { cozeLight } from './themes/coze-light';
import { cozeDark } from './themes/coze-dark';
// 注册语言
languages.register('json', {
// mixLanguages 用于解决 「插值也使用了括号,导致无法正确高亮」的问题
language: mixLanguages({
outerLanguage: json.language,
}),
languageService: json.languageService,
});
languages.register('shell', shell);
// 注册主题
themes.register('coze-light', cozeLight);
themes.register('coze-dark', cozeDark);
const minHeightOption = (value?: string | number) =>
EditorView.theme({
'.cm-content, .cm-gutter, .cm-right-gutter': {
minHeight:
typeof value === 'number'
? `${value}px`
: typeof value === 'string'
? value
: 'unset',
},
'&.cm-editor': {
minHeight:
typeof value === 'number'
? `${value}px`
: typeof value === 'string'
? value
: 'unset',
},
});
const maxHeightOption = (value?: string | number) =>
EditorView.theme({
'&.cm-editor': {
maxHeight:
typeof value === 'number'
? `${value}px`
: typeof value === 'string'
? value
: 'unset',
},
});
const heightOption = (value?: string | number) =>
EditorView.theme({
'&.cm-editor': {
height:
typeof value === 'number'
? `${value}px`
: typeof value === 'string'
? value
: 'unset',
},
});
const paddingOption = (value?: string | number) =>
EditorView.theme({
'&.cm-editor': {
padding:
typeof value === 'number'
? `${value}px`
: typeof value === 'string'
? value
: 'unset',
},
});
const borderRadiusOption = (value?: string | number) =>
EditorView.theme({
'&.cm-editor, .cm-gutters': {
borderRadius:
typeof value === 'number'
? `${value}px`
: typeof value === 'string'
? value
: 'unset',
},
});
const lineHeightOption = (value?: string | number) =>
EditorView.theme({
'.cm-content, .cm-gutter, .cm-right-gutter': {
lineHeight:
typeof value === 'number'
? `${value}px`
: typeof value === 'string'
? value
: 'unset',
},
});
function createStyleOptions() {
return [
option('minHeight', minHeightOption),
option('maxHeight', maxHeightOption),
option('editerHeight', heightOption),
option('borderRadius', borderRadiusOption),
option('padding', paddingOption),
option('lineHeight', lineHeightOption),
];
}
const builtinExtensions = [
EditorView.theme({
'&.cm-focused': {
outline: 'none',
},
'& *': {
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
},
}),
EditorView.theme({
'&.cm-content': {
wordBreak: 'break-all',
},
}),
];
export const CodeEditor = createRenderer(
[...preset, ...createStyleOptions()],
builtinExtensions,
);

View File

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

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRenderer, option } from '@coze-editor/editor/react';
import universal from '@coze-editor/editor/preset-universal';
import { mixLanguages } from '@coze-editor/editor';
import { keymap, EditorView } from '@codemirror/view';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
const RawEditorTheme = EditorView.theme({
'&.cm-editor': {
outline: 'none',
},
'&.cm-content': {
wordBreak: 'break-all',
},
});
const minHeightOption = (value?: string | number) =>
EditorView.theme({
'.cm-content, .cm-gutter, .cm-right-gutter': {
minHeight:
typeof value === 'number'
? `${value}px`
: typeof value === 'string'
? value
: 'unset',
},
});
const lineHeightOption = (value?: string | number) =>
EditorView.theme({
'.cm-content, .cm-gutter, .cm-right-gutter': {
lineHeight:
typeof value === 'number'
? `${value}px`
: typeof value === 'string'
? value
: 'unset',
},
});
const extensions = [
mixLanguages({}),
RawEditorTheme,
// ...其他 extensions
history(),
keymap.of([...defaultKeymap, ...historyKeymap]),
];
export const TextEditor = createRenderer(
[
...universal,
option('minHeight', minHeightOption),
option('lineHeight', lineHeightOption),
],
extensions,
);

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createTheme, tags as t } from '@coze-editor/editor/preset-code';
import { EditorView } from '@codemirror/view';
const colors = {
background: '#151B27',
// syntax
comment: '#FFFFFF63',
key: '#39E5D7',
string: '#FF94D2',
number: '#FF9933',
boolean: '#78B0FF',
null: '#78B0FF',
separator: '#FFFFFFC9',
};
export const cozeDark = [
EditorView.theme({
'.cm-completionIcon-property': {
backgroundImage:
'url("' +
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMWVtIiBoZWlnaHQ9IjFlbSIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xMi4zNTc2IDguMTAzNTVDMTIuMTYyMyA3LjkwODI5IDExLjg0NTcgNy45MDgyOSAxMS42NTA1IDguMTAzNTVMOC4xMDM1NSAxMS42NTA1QzcuOTA4MjkgMTEuODQ1NyA3LjkwODI5IDEyLjE2MjMgOC4xMDM1NSAxMi4zNTc2TDExLjY1MDUgMTUuOTA0NUMxMS44NDU3IDE2LjA5OTggMTIuMTYyMyAxNi4wOTk4IDEyLjM1NzYgMTUuOTA0NUwxNS45MDQ1IDEyLjM1NzZDMTYuMDk5OCAxMi4xNjIzIDE2LjA5OTggMTEuODQ1NyAxNS45MDQ1IDExLjY1MDVMMTIuMzU3NiA4LjEwMzU1WiIgZmlsbD0iI0ZGRkZGRkM5Ii8+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMS4wMDI2IDEuNDU1NDVDMTEuNjIxNCAxLjA5ODE4IDEyLjM4MzggMS4wOTgxOCAxMy4wMDI2IDEuNDU1NDVMMjAuNjM4IDUuODYzNzRDMjEuMjU2OCA2LjIyMSAyMS42MzggNi44ODEyNiAyMS42MzggNy41OTU3OVYxNi40MTI0QzIxLjYzOCAxNy4xMjY5IDIxLjI1NjggMTcuNzg3MiAyMC42MzggMTguMTQ0NEwxMy4wMDI2IDIyLjU1MjdDMTIuMzgzOCAyMi45MSAxMS42MjE0IDIyLjkxIDExLjAwMjYgMjIuNTUyN0wzLjM2NzE5IDE4LjE0NDRDMi43NDgzOSAxNy43ODcyIDIuMzY3MTkgMTcuMTI2OSAyLjM2NzE5IDE2LjQxMjRWNy41OTU3OUMyLjM2NzE5IDYuODgxMjYgMi43NDgzOSA2LjIyMTAxIDMuMzY3MTkgNS44NjM3NEwxMS4wMDI2IDEuNDU1NDVaTTEyLjAwMjYgMy4xODc1TDE5LjYzOCA3LjU5NTc5VjE2LjQxMjRMMTIuMDAyNiAyMC44MjA3TDQuMzY3MTkgMTYuNDEyNEw0LjM2NzE5IDcuNTk1NzlMMTIuMDAyNiAzLjE4NzVaIiBmaWxsPSIjRkZGRkZGQzkiLz48L3N2Zz4=' +
'")',
backgroundSize: '11px 11px',
backgroundRepeat: 'no-repeat',
width: '11px',
height: '11px',
},
'.cm-completionIcon-property::after': {
content: '""',
},
}),
createTheme({
variant: 'dark',
settings: {
background: colors.background,
foreground: '#fff',
caret: '#AEAFAD',
selection: '#d9d9d942',
gutterBackground: colors.background,
gutterForeground: '#FFFFFF63',
gutterBorderColor: 'transparent',
gutterBorderWidth: 0,
lineHighlight: '#272e3d36',
bracketColors: ['#FFEF61', '#DD99FF', '#78B0FF'],
tooltip: {
backgroundColor: '#363D4D',
color: '#fff',
border: 'none',
},
completionItemHover: {
backgroundColor: '#FFFFFF0F',
},
completionItemSelected: {
backgroundColor: '#FFFFFF17',
},
completionItemIcon: {
color: '#FFFFFFC9',
},
completionItemLabel: {
color: '#FFFFFFC9',
},
completionItemDetail: {
color: '#FFFFFF63',
},
},
styles: [
// json
{
tag: t.comment,
color: colors.comment,
},
{
tag: [t.propertyName],
color: colors.key,
},
{
tag: [t.string],
color: colors.string,
},
{
tag: [t.number],
color: colors.number,
},
{
tag: [t.bool],
color: colors.boolean,
},
{
tag: [t.null],
color: colors.null,
},
{
tag: [t.separator],
color: colors.separator,
},
// shell
// curl
{
tag: [t.standard(t.variableName)],
color: '#3BEB84',
},
// -X
{
tag: [t.attributeName],
color: '#FF9933',
},
// url in string (includes quotes), e.g.
{
tag: [t.special(t.string)],
color: '#78B0FF',
},
],
}),
];

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createTheme, tags as t } from '@coze-editor/editor/preset-code';
import { EditorView } from '@codemirror/view';
const colors = {
background: '#F7F7FC',
// syntax
comment: '#000A298A',
key: '#00818C',
string: '#D1009D',
number: '#C74200',
boolean: '#2B57D9',
null: '#2B57D9',
separator: '#0F1529D1',
};
export const cozeLight = [
EditorView.theme({
'.cm-completionIcon-property': {
backgroundImage:
'url("' +
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMWVtIiBoZWlnaHQ9IjFlbSIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xMi4zNTc2IDguMTAzNTVDMTIuMTYyMyA3LjkwODI5IDExLjg0NTcgNy45MDgyOSAxMS42NTA1IDguMTAzNTVMOC4xMDM1NSAxMS42NTA1QzcuOTA4MjkgMTEuODQ1NyA3LjkwODI5IDEyLjE2MjMgOC4xMDM1NSAxMi4zNTc2TDExLjY1MDUgMTUuOTA0NUMxMS44NDU3IDE2LjA5OTggMTIuMTYyMyAxNi4wOTk4IDEyLjM1NzYgMTUuOTA0NUwxNS45MDQ1IDEyLjM1NzZDMTYuMDk5OCAxMi4xNjIzIDE2LjA5OTggMTEuODQ1NyAxNS45MDQ1IDExLjY1MDVMMTIuMzU3NiA4LjEwMzU1WiIgZmlsbD0iIzA2MDcwOUNDIi8+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMS4wMDI2IDEuNDU1NDVDMTEuNjIxNCAxLjA5ODE4IDEyLjM4MzggMS4wOTgxOCAxMy4wMDI2IDEuNDU1NDVMMjAuNjM4IDUuODYzNzRDMjEuMjU2OCA2LjIyMSAyMS42MzggNi44ODEyNiAyMS42MzggNy41OTU3OVYxNi40MTI0QzIxLjYzOCAxNy4xMjY5IDIxLjI1NjggMTcuNzg3MiAyMC42MzggMTguMTQ0NEwxMy4wMDI2IDIyLjU1MjdDMTIuMzgzOCAyMi45MSAxMS42MjE0IDIyLjkxIDExLjAwMjYgMjIuNTUyN0wzLjM2NzE5IDE4LjE0NDRDMi43NDgzOSAxNy43ODcyIDIuMzY3MTkgMTcuMTI2OSAyLjM2NzE5IDE2LjQxMjRWNy41OTU3OUMyLjM2NzE5IDYuODgxMjYgMi43NDgzOSA2LjIyMTAxIDMuMzY3MTkgNS44NjM3NEwxMS4wMDI2IDEuNDU1NDVaTTEyLjAwMjYgMy4xODc1TDE5LjYzOCA3LjU5NTc5VjE2LjQxMjRMMTIuMDAyNiAyMC44MjA3TDQuMzY3MTkgMTYuNDEyNEw0LjM2NzE5IDcuNTk1NzlMMTIuMDAyNiAzLjE4NzVaIiBmaWxsPSIjMDYwNzA5Q0MiLz48L3N2Zz4=' +
'")',
backgroundSize: '11px 11px',
backgroundRepeat: 'no-repeat',
width: '11px',
height: '11px',
},
'.cm-completionIcon-property::after': {
content: '""',
},
}),
createTheme({
variant: 'light',
settings: {
background: colors.background,
foreground: '#4D4D4C',
caret: '#AEAFAD',
selection: '#52649A21',
gutterBackground: colors.background,
gutterForeground: '#000A298A',
gutterBorderColor: 'transparent',
gutterBorderWidth: 0,
lineHighlight: '#efefef78',
bracketColors: ['#E4D129', '#AC05FF', '#2B57D9'],
tooltip: {
backgroundColor: 'var(--coz-bg-max)',
color: 'var(--coz-fg-primary)',
border: 'solid 1px var(--coz-stroke-plus)',
boxShadow: 'var(--coz-shadow-default)',
borderRadius: '8px',
},
tooltipCompletion: {
backgroundColor: '#FFFFFF',
color: '#060709CC',
},
completionItemHover: {
backgroundColor: '#5768A114',
},
completionItemSelected: {
backgroundColor: '#52649A21',
},
completionItemIcon: {
color: '#060709CC',
},
completionItemLabel: {
color: '#060709CC',
},
completionItemDetail: {
color: '#2029459E',
},
},
styles: [
// JSON
{
tag: t.comment,
color: colors.comment,
},
{
tag: [t.propertyName],
color: colors.key,
},
{
tag: [t.string],
color: colors.string,
},
{
tag: [t.number],
color: colors.number,
},
{
tag: [t.bool],
color: colors.boolean,
},
{
tag: [t.null],
color: colors.null,
},
{
tag: [t.separator],
color: colors.separator,
},
// shell
// curl
{
tag: [t.standard(t.variableName)],
color: '#00804A',
},
// -X
{
tag: [t.attributeName],
color: '#C74200',
},
// url in string (includes quotes), e.g.
{
tag: [t.special(t.string)],
color: '#2B57D9',
},
],
}),
];

View File

@@ -0,0 +1,322 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 { CommentEditorBlock } from '../../type';
import { getCozeCom, getCozeCn } from './util';
export const commentEditorMockBlocks = [
{
type: 'heading-one',
children: [
{
text: 'Workflow Comment',
bold: true,
underline: true,
type: 'text',
},
],
},
{
type: 'paragraph',
children: [
{
text: '[Format]',
bold: true,
type: 'text',
},
],
},
{
type: 'paragraph',
children: [
{
text: 'Bold',
bold: true,
type: 'text',
},
{
text: ' ',
type: 'text',
},
{
text: 'Italic',
italic: true,
type: 'text',
},
{
text: ' ',
type: 'text',
},
{
text: 'Underline',
underline: true,
type: 'text',
},
{
text: ' ',
type: 'text',
},
{
text: 'Strikethrough',
strikethrough: true,
type: 'text',
},
{
text: ' ',
type: 'text',
},
{
strikethrough: true,
text: 'Mixed',
bold: true,
italic: true,
underline: true,
type: 'text',
},
],
},
{
type: 'paragraph',
children: [
{
text: '[Quote]',
bold: true,
type: 'text',
},
],
},
{
type: 'block-quote',
children: [
{
type: 'text',
text: 'This line should be displayed as a quote.',
},
],
},
{
type: 'block-quote',
children: [
{
type: 'text',
text: 'Line 2: content.',
},
],
},
{
type: 'block-quote',
children: [
{
type: 'text',
text: 'Line 3: content.',
},
],
},
{
type: 'paragraph',
children: [
{
text: '[Bullet List]',
bold: true,
type: 'text',
},
],
},
{
type: 'bulleted-list',
children: [
{
type: 'list-item',
children: [
{
text: 'item order 1',
type: 'text',
},
],
},
{
type: 'list-item',
children: [
{
text: 'item order 2',
type: 'text',
},
],
},
{
type: 'bulleted-list',
children: [
{
type: 'list-item',
children: [
{
text: 'item order 2.1',
type: 'text',
},
],
},
{
type: 'list-item',
children: [
{
text: 'item order 2.2',
type: 'text',
},
],
},
],
},
{
type: 'list-item',
children: [
{
text: 'item order 3',
type: 'text',
},
],
},
],
},
{
type: 'paragraph',
children: [
{
text: '[Numbered List]',
bold: true,
type: 'text',
},
],
},
{
type: 'numbered-list',
children: [
{
type: 'list-item',
children: [
{
text: 'item order 1',
type: 'text',
},
],
},
{
type: 'list-item',
children: [
{
text: 'item order 2',
type: 'text',
},
],
},
{
type: 'list-item',
children: [
{
text: 'item order 3',
type: 'text',
},
],
},
],
},
{
type: 'paragraph',
children: [
{
bold: true,
text: '[Hyper Link]',
type: 'text',
},
],
},
{
type: 'paragraph',
children: [
{
text: 'Coze 👉🏻 ',
type: 'text',
},
{
link: getCozeCom(),
text: 'coze.com',
type: 'text',
},
],
},
{
type: 'paragraph',
children: [
{
text: 'Coze for CN 👉🏻 ',
type: 'text',
},
{
text: 'coze.cn',
link: getCozeCn(),
type: 'text',
},
],
},
{
type: 'paragraph',
children: [
{
bold: true,
text: '[Heading]',
type: 'text',
},
],
},
{
type: 'heading-one',
children: [
{
text: 'Heading 1',
type: 'text',
},
],
},
{
type: 'heading-two',
children: [
{
text: 'Heading 2',
type: 'text',
},
],
},
{
type: 'heading-three',
children: [
{
text: 'Heading 3',
type: 'text',
},
],
},
{
type: 'heading-three',
children: [
{
text: 'Heading Formatted',
bold: true,
italic: true,
underline: true,
type: 'text',
},
],
},
] as CommentEditorBlock[];

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getCozeCom, getCozeCn } from './util';
export const commentEditorMockHTML = `<h1><u><strong>Workflow Comment</strong></u></h1><p><strong>[Format]</strong></p><p><strong>Bold</strong> <em>Italic</em> <u>Underline</u> <del>Strikethrough</del> <del><u><em><strong>Mixed</strong></em></u></del></p><p><strong>[Quote]</strong></p><blockquote><p>This line should be displayed as a quote.</p></blockquote><blockquote><p>Line 2: content.</p></blockquote><blockquote><p>Line 3: content.</p></blockquote><p><strong>[Bullet List]</strong></p><ul><li>item order 1</li><li>item order 2</li><li>item order 3</li></ul><p><strong>[Numbered List]</strong></p><ol><li>item order 1</li><li>item order 2</li><li>item order 3</li></ol><p><strong>[Hyper Link]</strong></p><p>Coze 👉🏻 <a href="${getCozeCom()}">coze.com</a></p><p>Coze for CN 👉🏻 <a href="${getCozeCn()}">coze.cn</a></p><p><strong>[Heading]</strong></p><h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3><h3><u><em><strong>Heading Formatted</strong></em></u></h3>`;

View File

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

View File

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

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getCozeCom, getCozeCn } from './util';
export const commentEditorMockMarkdown = `# __**Workflow Comment**__
**[Format]**
**Bold** *Italic* __Underline__ ~~Strikethrough~~ ~~__***Mixed***__~~
**[Quote]**
> This line should be displayed as a quote.
> Line 2: content.
> Line 3: content.
**[Bullet List]**
- item order 1
- item order 2
- item order 3
**[Numbered List]**
1. item order 1
2. item order 2
3. item order 3
**[Hyper Link]**
Coze 👉🏻 [coze.com](${getCozeCom()})
Coze for CN 👉🏻 [coze.cn](${getCozeCn()})
**[Heading]**
# Heading 1
## Heading 2
### Heading 3
### __***Heading Formatted***__`;

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const commentEditorMockText = `Workflow Comment
[Format]
Bold Italic Underline Strikethrough Mixed
[Quote]
This line should be displayed as a quote.
Line 2: content.
Line 3: content.
[Bullet List]
item order 1
item order 2
item order 3
[Numbered List]
item order 1
item order 2
item order 3
[Hyper Link]
Coze 👉🏻 coze.com
Coze for CN 👉🏻 coze.cn
[Heading]
Heading 1
Heading 2
Heading 3
Heading Formatted`;

View File

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

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect } from 'vitest';
import { CommentEditorParser } from '../parsers';
import {
commentEditorMockBlocks,
commentEditorMockText,
commentEditorMockMarkdown,
commentEditorMockHTML,
commentEditorMockJSON,
} from './mock';
describe('CommentEditorParser', () => {
it('toText', () => {
expect(CommentEditorParser.toText(commentEditorMockBlocks)).toBe(
commentEditorMockText,
);
});
it('toMarkdown', () => {
expect(CommentEditorParser.toMarkdown(commentEditorMockBlocks)).toBe(
commentEditorMockMarkdown,
);
});
it('toHTML', () => {
expect(CommentEditorParser.toHTML(commentEditorMockBlocks)).toBe(
commentEditorMockHTML,
);
});
it('toJSON', () => {
expect(CommentEditorParser.toJSON(commentEditorMockBlocks)).toBe(
commentEditorMockJSON,
);
});
it('fromJSON', () => {
expect(CommentEditorParser.fromJSON(commentEditorMockJSON)).toEqual(
commentEditorMockBlocks,
);
});
});

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Editor } from 'slate';
import type { CommentEditorCommand } from '../type';
import type { CommentEditorModel } from '../model';
import { CommentEditorBlockFormat } from '../constant';
// 定义块前缀模式和对应的格式
const blockPrefixConfig: Array<[RegExp, CommentEditorBlockFormat]> = [
[/^#$/, CommentEditorBlockFormat.HeadingOne],
[/^##$/, CommentEditorBlockFormat.HeadingTwo],
[/^###$/, CommentEditorBlockFormat.HeadingThree],
[/^>$/, CommentEditorBlockFormat.Blockquote],
[/^-$/, CommentEditorBlockFormat.BulletedList],
[/^\*$/, CommentEditorBlockFormat.BulletedList],
[/^1\.$/, CommentEditorBlockFormat.NumberedList],
];
// 删除文本的函数
const deleteText = (model: CommentEditorModel, text: string): void => {
Array.from(text).forEach(() => {
Editor.deleteBackward(model.editor, { unit: 'character' });
});
};
// 处理块前缀的函数
const handleBlockPrefix = (
model: CommentEditorModel,
text: string,
): boolean => {
const matchedConfig = blockPrefixConfig.find(([pattern]) =>
pattern.test(text),
);
if (matchedConfig) {
const [, format] = matchedConfig;
deleteText(model, text);
model.markBlock(format);
return true;
}
return false;
};
export const blockPrefixCommand: CommentEditorCommand = {
key: ' ',
exec: ({ model, event }) => {
// 检查是否正在输入拼音
if (event.nativeEvent.isComposing) {
return;
}
const { before: beforeText } = model.getBlockText();
if (!beforeText) {
return;
}
if (handleBlockPrefix(model, beforeText)) {
event.preventDefault();
}
},
};

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CommentEditorCommand } from '../type';
import { CommentEditorLeafFormat } from '../constant';
export const boldCommand: CommentEditorCommand = {
key: 'b',
modifier: true,
exec: ({ model, event }) => {
event.preventDefault();
model.markLeaf(CommentEditorLeafFormat.Bold);
},
};

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 { CommentEditorCommand } from '../type';
import { CommentEditorBlockFormat } from '../constant';
export const clearFormatEnterCommand: CommentEditorCommand = {
key: 'Enter',
shift: false,
exec: ({ model, event }) => {
// 检查是否正在输入拼音
if (event.nativeEvent.isComposing) {
return;
}
const isEmptyBlock = !model.getBlockText().text;
const hasBlockFormat = !model.isBlockMarked(
CommentEditorBlockFormat.Paragraph,
);
if (!isEmptyBlock || !hasBlockFormat) {
return;
}
event.preventDefault();
model.clearFormat();
},
};
export const clearFormatBackspaceCommand: CommentEditorCommand = {
key: 'Backspace',
shift: false,
exec: ({ model, event }) => {
const isAtBlockStart = !model.getBlockText().before;
const hasBlockFormat = !model.isBlockMarked(
CommentEditorBlockFormat.Paragraph,
);
if (!isAtBlockStart || !hasBlockFormat) {
return;
}
event.preventDefault();
model.clearFormat();
},
};

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CommentEditorCommand } from '../type';
import { CommentEditorBlockFormat } from '../constant';
export const headingOneCommand: CommentEditorCommand = {
key: '1',
modifier: true,
exec: ({ model, event }) => {
event.preventDefault();
model.markBlock(CommentEditorBlockFormat.HeadingOne);
},
};
export const headingTwoCommand: CommentEditorCommand = {
key: '2',
modifier: true,
exec: ({ model, event }) => {
event.preventDefault();
model.markBlock(CommentEditorBlockFormat.HeadingTwo);
},
};
export const headingThreeCommand: CommentEditorCommand = {
key: '3',
modifier: true,
exec: ({ model, event }) => {
event.preventDefault();
model.markBlock(CommentEditorBlockFormat.HeadingThree);
},
};

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { selectAllCommand } from './select-all';
export { boldCommand } from './bold';
export { italicCommand } from './italic';
export { underlineCommand } from './underline';
export { strikethroughCommand } from './strikethrough';
export { paragraphCommand } from './paragraph';
export {
headingOneCommand,
headingTwoCommand,
headingThreeCommand,
} from './heading';
export { quoteCommand } from './quote';
export {
clearFormatEnterCommand,
clearFormatBackspaceCommand,
} from './clear-format';
export { blockPrefixCommand } from './block-prefix';

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CommentEditorCommand } from '../type';
import { CommentEditorLeafFormat } from '../constant';
export const italicCommand: CommentEditorCommand = {
key: 'i',
modifier: true,
exec: ({ model, event }) => {
event.preventDefault();
model.markLeaf(CommentEditorLeafFormat.Italic);
},
};

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CommentEditorCommand } from '../type';
import { CommentEditorBlockFormat } from '../constant';
export const paragraphCommand: CommentEditorCommand = {
key: 'o',
modifier: true,
exec: ({ model, event }) => {
event.preventDefault();
model.markBlock(CommentEditorBlockFormat.Paragraph);
},
};

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CommentEditorCommand } from '../type';
import { CommentEditorBlockFormat } from '../constant';
export const quoteCommand: CommentEditorCommand = {
key: 'q',
modifier: true,
exec: ({ model, event }) => {
event.preventDefault();
model.markBlock(CommentEditorBlockFormat.Blockquote);
},
};

View File

@@ -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 { Editor, Transforms } from 'slate';
import type { CommentEditorCommand } from '../type';
export const selectAllCommand: CommentEditorCommand = {
key: 'a',
modifier: true,
exec: ({ model, event }) => {
event.preventDefault();
Transforms.select(model.editor, {
anchor: Editor.start(model.editor, []),
focus: Editor.end(model.editor, []),
});
},
};

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CommentEditorCommand } from '../type';
import { CommentEditorLeafFormat } from '../constant';
export const strikethroughCommand: CommentEditorCommand = {
key: 's',
modifier: true,
exec: ({ model, event }) => {
event.preventDefault();
model.markLeaf(CommentEditorLeafFormat.Strikethrough);
},
};

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