feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
31
frontend/packages/data/memory/variables/.storybook/main.js
Normal file
31
frontend/packages/data/memory/variables/.storybook/main.js
Normal 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;
|
||||
@@ -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;
|
||||
5
frontend/packages/data/memory/variables/.stylelintrc.js
Normal file
5
frontend/packages/data/memory/variables/.stylelintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/data/memory/variables/README.md
Normal file
16
frontend/packages/data/memory/variables/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-data/variable
|
||||
|
||||
> Project template for react component with storybook.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
7
frontend/packages/data/memory/variables/eslint.config.js
Normal file
7
frontend/packages/data/memory/variables/eslint.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
81
frontend/packages/data/memory/variables/package.json
Normal file
81
frontend/packages/data/memory/variables/package.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "@coze-data/variable",
|
||||
"version": "0.0.1",
|
||||
"description": "memory-variable",
|
||||
"license": "Apache-2.0",
|
||||
"author": "haozhenfei@bytedance.com",
|
||||
"maintainers": [],
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-error": "workspace:*",
|
||||
"@coze-arch/bot-flags": "workspace:*",
|
||||
"@coze-arch/bot-hooks": "workspace:*",
|
||||
"@coze-arch/bot-icons": "workspace:*",
|
||||
"@coze-arch/bot-monaco-editor": "workspace:*",
|
||||
"@coze-arch/bot-semi": "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/i18n": "workspace:*",
|
||||
"@coze-arch/logger": "workspace:*",
|
||||
"@coze-arch/report-events": "workspace:*",
|
||||
"@coze-arch/report-tti": "workspace:*",
|
||||
"@coze-common/chat-area-utils": "workspace:*",
|
||||
"@coze-data/e2e": "workspace:*",
|
||||
"@coze-data/knowledge-common-components": "workspace:*",
|
||||
"@coze-data/knowledge-ide-base": "workspace:*",
|
||||
"@coze-data/knowledge-modal-adapter": "workspace:*",
|
||||
"@coze-data/knowledge-modal-base": "workspace:*",
|
||||
"@coze-data/knowledge-resource-processor-adapter": "workspace:*",
|
||||
"@coze-data/knowledge-resource-processor-base": "workspace:*",
|
||||
"@coze-data/knowledge-resource-processor-core": "workspace:*",
|
||||
"@coze-data/knowledge-stores": "workspace:*",
|
||||
"@coze-data/reporter": "workspace:*",
|
||||
"@coze-data/utils": "workspace:*",
|
||||
"@coze-foundation/local-storage": "workspace:*",
|
||||
"@coze-studio/components": "workspace:*",
|
||||
"@coze-studio/premium-components-adapter": "workspace:*",
|
||||
"@coze-studio/premium-store-adapter": "workspace:*",
|
||||
"@douyinfe/semi-illustrations": "^2.36.0",
|
||||
"ahooks": "^3.7.8",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"dompurify": "3.0.8",
|
||||
"immer": "^10.0.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^4.0.2",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, type PropsWithChildren, useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { IconCozArrowRight } from '@coze-arch/coze-design/icons';
|
||||
import { Collapsible } from '@coze-arch/coze-design';
|
||||
|
||||
import { type VariableGroup } from '@/store';
|
||||
|
||||
export const GroupCollapsibleWrapper: FC<
|
||||
PropsWithChildren<{
|
||||
groupInfo: VariableGroup;
|
||||
level?: number;
|
||||
}>
|
||||
> = props => {
|
||||
const { groupInfo, children, level = 0 } = props;
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const isTopLevel = level === 0;
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cls(
|
||||
'flex w-full flex-col cursor-pointer px-1 py-2',
|
||||
isTopLevel ? 'hover:coz-mg-secondary-hovered hover:rounded-lg' : '',
|
||||
)}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="w-[22px] h-full flex items-center">
|
||||
<IconCozArrowRight
|
||||
className={cls('w-[14px] h-[14px]', isOpen ? 'rotate-90' : '')}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[370px] h-full flex items-center">
|
||||
<div
|
||||
className={cls(
|
||||
'coz-stroke-primary text-xxl font-medium',
|
||||
!isTopLevel ? '!text-sm my-[10px]' : '',
|
||||
)}
|
||||
>
|
||||
{groupInfo.groupName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isTopLevel ? (
|
||||
<div className="text-sm coz-fg-secondary pl-[22px]">
|
||||
{groupInfo.groupDesc}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Collapsible keepDOM isOpen={isOpen}>
|
||||
<div className={cls('w-full h-full', !isTopLevel ? 'pl-[18px]' : '')}>
|
||||
{children}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { VariableChannel } from '@coze-arch/bot-api/memory';
|
||||
|
||||
import { type VariableGroup } from '@/store';
|
||||
|
||||
import { flatGroupVariableMeta } from '../../../variable-tree/utils';
|
||||
|
||||
export const useGetHideKeys = (variableGroup: VariableGroup) => {
|
||||
const hideKeys: string[] = [];
|
||||
|
||||
const hideChannel =
|
||||
flatGroupVariableMeta([variableGroup]).filter(
|
||||
item => (item?.effectiveChannelList?.length ?? 0) > 0,
|
||||
).length <= 0;
|
||||
|
||||
const hideTypeChange = variableGroup.channel === VariableChannel.Custom;
|
||||
|
||||
if (hideChannel) {
|
||||
hideKeys.push('channel');
|
||||
}
|
||||
|
||||
if (hideTypeChange) {
|
||||
hideKeys.push('type');
|
||||
}
|
||||
return hideKeys;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { VariableGroupParamHeader } from './render';
|
||||
export { useGetHideKeys } from './hooks/use-get-hide-keys';
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
export const VariableGroupParamHeader = ({
|
||||
hideHeaderKeys,
|
||||
}: {
|
||||
hideHeaderKeys?: string[];
|
||||
}) => (
|
||||
<div
|
||||
className={cls(
|
||||
'flex w-full h-[28px] py-[6px] pl-8 items-center gap-x-4 justify-start',
|
||||
'border border-solid coz-stroke-primary border-t-0 border-x-0',
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 h-full flex items-center">
|
||||
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
|
||||
{I18n.t('bot_edit_memory_title_filed')}
|
||||
<span className="coz-fg-hglt-red">*</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 h-full flex items-center">
|
||||
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
|
||||
{I18n.t('bot_edit_memory_title_description')}
|
||||
</div>
|
||||
</div>
|
||||
{!hideHeaderKeys?.includes('type') ? (
|
||||
<div className="flex-none w-[166px] basis-[166px] h-full flex items-center box-content">
|
||||
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
|
||||
{I18n.t('variable_Table_Title_type')}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex-none w-[164px] basis-[164px] h-full flex items-center box-content">
|
||||
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
|
||||
{I18n.t('bot_edit_memory_title_default')}
|
||||
</div>
|
||||
</div>
|
||||
{!hideHeaderKeys?.includes('channel') ? (
|
||||
<div className="flex-none w-[164px] basis-[164px] h-full flex items-center box-content">
|
||||
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
|
||||
{I18n.t('variable_Table_Title_support_channels')}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex-none w-[130px] basis-[130px] h-full flex items-center box-content">
|
||||
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
|
||||
{I18n.t('bot_edit_memory_title_action')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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 VariableGroup as VariableGroupType } from '@/store';
|
||||
|
||||
import { type TreeNodeCustomData } from '../variable-tree/type';
|
||||
import { VariableTree } from '../variable-tree';
|
||||
import { VariableGroupParamHeader, useGetHideKeys } from './group-header';
|
||||
import { GroupCollapsibleWrapper } from './group-collapsible-wraper';
|
||||
|
||||
interface IVariableGroupProps {
|
||||
groupInfo: VariableGroupType;
|
||||
readonly?: boolean;
|
||||
validateExistKeyword?: boolean;
|
||||
onVariableChange: (changeValue: TreeNodeCustomData) => void;
|
||||
}
|
||||
|
||||
export const VariableGroup = (props: IVariableGroupProps) => {
|
||||
const {
|
||||
groupInfo,
|
||||
readonly = true,
|
||||
validateExistKeyword = false,
|
||||
onVariableChange,
|
||||
} = props;
|
||||
const hideHeaderKeys = useGetHideKeys(groupInfo);
|
||||
return (
|
||||
<>
|
||||
<GroupCollapsibleWrapper groupInfo={groupInfo}>
|
||||
<VariableGroupParamHeader hideHeaderKeys={hideHeaderKeys} />
|
||||
<div className="pl-6">
|
||||
{groupInfo.subGroupList?.map(subGroup => (
|
||||
<GroupCollapsibleWrapper groupInfo={subGroup} level={1}>
|
||||
<VariableTree
|
||||
hideHeaderKeys={hideHeaderKeys}
|
||||
groupId={groupInfo.groupId}
|
||||
value={subGroup.varInfoList ?? []}
|
||||
readonly={readonly}
|
||||
channel={subGroup.channel}
|
||||
validateExistKeyword={validateExistKeyword}
|
||||
onChange={onVariableChange}
|
||||
/>
|
||||
</GroupCollapsibleWrapper>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col pl-6">
|
||||
<VariableTree
|
||||
hideHeaderKeys={hideHeaderKeys}
|
||||
groupId={groupInfo.groupId}
|
||||
value={groupInfo.varInfoList ?? []}
|
||||
readonly={readonly}
|
||||
channel={groupInfo.channel}
|
||||
validateExistKeyword={validateExistKeyword}
|
||||
onChange={onVariableChange}
|
||||
/>
|
||||
</div>
|
||||
</GroupCollapsibleWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { IconAdd } from '@coze-arch/bot-icons';
|
||||
import { IconCozAddNode } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, type ButtonProps } from '@coze-arch/coze-design';
|
||||
|
||||
type AddOperationProps = React.PropsWithChildren<{
|
||||
readonly?: boolean;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
subitem?: boolean;
|
||||
size?: ButtonProps['size'];
|
||||
color?: ButtonProps['color'];
|
||||
}>;
|
||||
|
||||
export default function AddOperation({
|
||||
readonly,
|
||||
onClick,
|
||||
className,
|
||||
style,
|
||||
disabled,
|
||||
subitem = false,
|
||||
size,
|
||||
color,
|
||||
...restProps
|
||||
}: AddOperationProps) {
|
||||
if (readonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
data-testid={restProps['data-testid']}
|
||||
onClick={onClick}
|
||||
className={`${disabled ? 'disabled:text-[rgb(28,31,35,0.35)]' : 'text-[#4d53e8]'} ${className}`}
|
||||
style={style}
|
||||
icon={
|
||||
subitem ? (
|
||||
<IconCozAddNode />
|
||||
) : (
|
||||
<IconAdd className="text-[#4d53e8] disabled:text-[rgb(28,31,35,0.35)]" />
|
||||
)
|
||||
}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 { type TreeNodeCustomData } from '@/components/variable-tree/type';
|
||||
|
||||
export const ParamChannel = (props: { value: TreeNodeCustomData }) => {
|
||||
const { value } = props;
|
||||
return value.effectiveChannelList?.length ? (
|
||||
<div className="coz-stroke-primary text-[14px] font-[500] leading-[20px]">
|
||||
{value.effectiveChannelList?.join(',') ?? '--'}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozEdit } from '@coze-arch/coze-design/icons';
|
||||
import { CozInputNumber, Switch, Input } from '@coze-arch/coze-design';
|
||||
|
||||
import { ViewVariableType } from '@/store';
|
||||
import { type TreeNodeCustomData } from '@/components/variable-tree/type';
|
||||
|
||||
import { ReadonlyText } from '../readonly-text';
|
||||
import { JSONLikeTypes } from '../../constants';
|
||||
import { JSONImport } from '../../../json-import';
|
||||
|
||||
interface ParamDefaultProps {
|
||||
data: TreeNodeCustomData;
|
||||
onDefaultChange: (value: string | number | boolean) => void;
|
||||
onImportChange: (value: TreeNodeCustomData) => void;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export const ParamDefault = (props: ParamDefaultProps) => {
|
||||
const { data, onDefaultChange, onImportChange, readonly } = props;
|
||||
const [jsonModalVisible, setJsonModalVisible] = useState(false);
|
||||
|
||||
const isRoot = data.meta.level === 0;
|
||||
const isString = isRoot && data.type === ViewVariableType.String;
|
||||
const isNumber =
|
||||
isRoot &&
|
||||
(data.type === ViewVariableType.Number ||
|
||||
data.type === ViewVariableType.Integer);
|
||||
const isBoolean = isRoot && data.type === ViewVariableType.Boolean;
|
||||
const isShowJsonImport = JSONLikeTypes.includes(data.type) && isRoot;
|
||||
|
||||
return (
|
||||
<div className="w-[144px] h-full flex items-center [&_.semi-input-number-suffix-btns]:!h-auto">
|
||||
<div className="flex flex-col w-full relative">
|
||||
{readonly && !isShowJsonImport ? (
|
||||
<ReadonlyText
|
||||
className="w-[144px]"
|
||||
value={data.defaultValue || '-'}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{isString ? (
|
||||
<Input
|
||||
value={data.defaultValue}
|
||||
onChange={value => onDefaultChange(value)}
|
||||
placeholder={I18n.t(
|
||||
'workflow_detail_title_testrun_error_input',
|
||||
{
|
||||
a: data.name,
|
||||
},
|
||||
)}
|
||||
maxLength={1000}
|
||||
disabled={readonly}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isNumber ? (
|
||||
<CozInputNumber
|
||||
className={cls('h-full', {
|
||||
'[&_.semi-input-wrapper]:!coz-stroke-plus': true,
|
||||
})}
|
||||
value={data.defaultValue}
|
||||
onChange={value => onDefaultChange(value)}
|
||||
placeholder={I18n.t(
|
||||
'workflow_detail_title_testrun_error_input',
|
||||
{
|
||||
a: data.name,
|
||||
},
|
||||
)}
|
||||
disabled={readonly}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isBoolean ? (
|
||||
<Switch
|
||||
checked={Boolean(data.defaultValue === 'true')}
|
||||
size="small"
|
||||
onChange={value => onDefaultChange(value)}
|
||||
disabled={readonly}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isShowJsonImport ? (
|
||||
<>
|
||||
<div
|
||||
onClick={() => setJsonModalVisible(true)}
|
||||
className={cls(
|
||||
'coz-mg-primary rounded cursor-pointer flex items-center justify-center h-[32px] gap-x-1',
|
||||
{
|
||||
'coz-fg-primary': !readonly,
|
||||
'coz-fg-dim': readonly,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<IconCozEdit />
|
||||
<span className="text-sm font-medium">
|
||||
{readonly
|
||||
? I18n.t('variables_json_input_readonly_button')
|
||||
: I18n.t('variable_button_input_json')}
|
||||
</span>
|
||||
</div>
|
||||
<JSONImport
|
||||
visible={jsonModalVisible}
|
||||
treeData={data}
|
||||
rules={{
|
||||
jsonImport: true,
|
||||
readonly: Boolean(readonly),
|
||||
}}
|
||||
onOk={value => {
|
||||
onImportChange(value);
|
||||
setJsonModalVisible(false);
|
||||
}}
|
||||
onCancel={() => setJsonModalVisible(false)}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { I18n } from '@coze-arch/i18n';
|
||||
import { Input } from '@coze-arch/coze-design';
|
||||
|
||||
import { type Variable } from '@/store';
|
||||
|
||||
import { ReadonlyText } from '../readonly-text';
|
||||
|
||||
export const ParamDescription = (props: {
|
||||
data: Variable;
|
||||
onChange: (value: string) => void;
|
||||
readonly: boolean;
|
||||
}) => {
|
||||
const { data, onChange, readonly } = props;
|
||||
return !readonly ? (
|
||||
<div className="flex flex-col w-full relative overflow-hidden">
|
||||
<Input
|
||||
value={data.description}
|
||||
placeholder={I18n.t('workflow_detail_llm_output_decription')}
|
||||
maxLength={200}
|
||||
onChange={value => {
|
||||
onChange(value);
|
||||
}}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ReadonlyText value={data.description ?? ''} />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { useFormApi } from '@coze-arch/coze-design';
|
||||
|
||||
import { type Variable } from '@/store';
|
||||
|
||||
export const useCacheField = (data: Variable) => {
|
||||
const formApi = useFormApi();
|
||||
|
||||
const lastValidValueRef = useRef(data.name);
|
||||
|
||||
useEffect(() => {
|
||||
const currentValue = formApi.getValue(`${data.variableId}.name`);
|
||||
if (currentValue) {
|
||||
lastValidValueRef.current = currentValue;
|
||||
} else if (lastValidValueRef.current) {
|
||||
formApi.setValue(`${data.variableId}.name`, lastValidValueRef.current);
|
||||
}
|
||||
}, [data.variableId]);
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { FormInput, useFormApi } from '@coze-arch/coze-design';
|
||||
|
||||
import { type Variable } from '@/store';
|
||||
import { useVariableContext } from '@/context';
|
||||
|
||||
import { ReadonlyText } from '../readonly-text';
|
||||
import {
|
||||
requiredRules,
|
||||
duplicateRules,
|
||||
existKeywordRules,
|
||||
} from './services/check-rules';
|
||||
import { useCacheField } from './hooks/use-cache-field';
|
||||
|
||||
export const ParamName = (props: {
|
||||
data: Variable;
|
||||
readonly: boolean;
|
||||
onChange: (value: string) => void;
|
||||
validateExistKeyword?: boolean;
|
||||
}) => {
|
||||
const { data, onChange, readonly, validateExistKeyword = false } = props;
|
||||
const { groups } = useVariableContext();
|
||||
const formApi = useFormApi();
|
||||
|
||||
// 使用 ref 缓存最后一次的有效值, Tree组件隐藏的时候会销毁组件,Form表单的Field字段会删除,所以需要缓存
|
||||
useCacheField(data);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
'w-full overflow-hidden',
|
||||
'[&_.semi-form-field-error-message]:absolute',
|
||||
'[&_.semi-form-field-error-message]:text-[12px]',
|
||||
'[&_.semi-form-field-error-message]:font-[400]',
|
||||
'[&_.semi-form-field-error-message]:leading-[16px]',
|
||||
)}
|
||||
>
|
||||
{!readonly ? (
|
||||
<>
|
||||
<FormInput
|
||||
field={`${data.variableId}.name`}
|
||||
placeholder={I18n.t('variable_name_placeholder')}
|
||||
maxLength={50}
|
||||
autoFocus={!data.name}
|
||||
noLabel
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) =>
|
||||
requiredRules.validate({
|
||||
...data,
|
||||
name: value,
|
||||
}),
|
||||
message: requiredRules.message,
|
||||
},
|
||||
{
|
||||
validator: (_, value) =>
|
||||
validateExistKeyword
|
||||
? existKeywordRules.validate({
|
||||
...data,
|
||||
name: value,
|
||||
})
|
||||
: true,
|
||||
message: existKeywordRules.message,
|
||||
},
|
||||
{
|
||||
validator: (_, value) =>
|
||||
duplicateRules.validate(
|
||||
{
|
||||
...data,
|
||||
name: value,
|
||||
},
|
||||
groups,
|
||||
),
|
||||
message: duplicateRules.message,
|
||||
},
|
||||
]}
|
||||
onChange={value => {
|
||||
onChange(value);
|
||||
formApi.setValue(`${data.variableId}.name`, value);
|
||||
}}
|
||||
className="w-full truncate"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ReadonlyText value={data.name} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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 { VariableChannel } from '@coze-arch/bot-api/memory';
|
||||
|
||||
import { type Variable, type VariableGroup } from '@/store';
|
||||
|
||||
export const requiredRules = {
|
||||
validate: (value: Variable) => !!value.name,
|
||||
message: I18n.t('bot_edit_variable_field_required_error'),
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查变量名称是否重复
|
||||
* 1、检查变量名称在同组&同层级是否重复
|
||||
* 2、检查变量名称在不同组的Root节点名称是否重复
|
||||
*/
|
||||
export const duplicateRules = {
|
||||
validate: (value: Variable, groups: VariableGroup[]): boolean => {
|
||||
if (!value.name) {
|
||||
return true;
|
||||
} // 如果名称为空则跳过检查
|
||||
|
||||
// 1. 检查同组同层级是否重复
|
||||
const currentGroup = groups.find(group => group.groupId === value.groupId);
|
||||
|
||||
if (!currentGroup) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取当前节点所在的所有同层级节点(包括嵌套在其他节点children中的)
|
||||
const findSiblings = (
|
||||
variables: Variable[],
|
||||
targetParentId: string | null,
|
||||
): Variable[] => {
|
||||
let result: Variable[] = [];
|
||||
|
||||
for (const variable of variables) {
|
||||
// 如果当前变量的parentId与目标parentId相同,且不是自身,则添加到结果中
|
||||
if (
|
||||
variable.parentId === targetParentId &&
|
||||
variable.variableId !== value.variableId
|
||||
) {
|
||||
result.push(variable);
|
||||
}
|
||||
// 递归检查children
|
||||
if (variable.children?.length) {
|
||||
result = result.concat(
|
||||
findSiblings(variable.children, targetParentId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const siblings = findSiblings(currentGroup.varInfoList, value.parentId);
|
||||
|
||||
if (siblings.some(sibling => sibling.name === value.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 检查是否与其他组的根节点重名
|
||||
// 只有当前节点是根节点时才需要检查
|
||||
if (!value.parentId) {
|
||||
const otherGroupsRootNodes = groups
|
||||
.filter(group => group.groupId !== value.groupId)
|
||||
.flatMap(group => {
|
||||
const rootVariableList = group.varInfoList;
|
||||
const subGroupVarInfoList = group.subGroupList.flatMap(
|
||||
subGroup => subGroup.varInfoList,
|
||||
);
|
||||
return rootVariableList.concat(subGroupVarInfoList);
|
||||
});
|
||||
|
||||
if (otherGroupsRootNodes.some(node => node.name === value.name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
message: I18n.t('workflow_detail_node_error_variablename_duplicated'),
|
||||
};
|
||||
|
||||
export const existKeywordRules = {
|
||||
validate: (value: Variable) =>
|
||||
/^(?!.*\b(true|false|and|AND|or|OR|not|NOT|null|nil|If|Switch)\b)[a-zA-Z_][a-zA-Z_$0-9]*$/.test(
|
||||
value.name,
|
||||
),
|
||||
message: I18n.t('variables_app_name_limit'),
|
||||
};
|
||||
|
||||
export const checkParamNameRules = (
|
||||
value: Variable,
|
||||
groups: VariableGroup[],
|
||||
validateExistKeyword: boolean,
|
||||
):
|
||||
| {
|
||||
valid: boolean;
|
||||
message: string;
|
||||
}
|
||||
| undefined => {
|
||||
if (!requiredRules.validate(value)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: requiredRules.message,
|
||||
};
|
||||
}
|
||||
if (!duplicateRules.validate(value, groups)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: duplicateRules.message,
|
||||
};
|
||||
}
|
||||
if (
|
||||
validateExistKeyword &&
|
||||
!existKeywordRules.validate(value) &&
|
||||
value.channel === VariableChannel.APP
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
message: existKeywordRules.message,
|
||||
};
|
||||
}
|
||||
return {
|
||||
valid: true,
|
||||
message: '',
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/no-deep-relative-import */
|
||||
import { VariableE2e } from '@coze-data/e2e';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozTrashCan } from '@coze-arch/coze-design/icons';
|
||||
import { Tooltip, IconButton, Switch } from '@coze-arch/coze-design';
|
||||
|
||||
import { ObjectLikeTypes } from '@/store/variable-groups/types';
|
||||
import { useVariableContext } from '@/context';
|
||||
|
||||
import AddOperation from '../add-operation';
|
||||
import { type TreeNodeCustomData } from '../../../../type';
|
||||
|
||||
interface ParamOperatorProps {
|
||||
data: TreeNodeCustomData;
|
||||
level: number;
|
||||
onAppend: () => void;
|
||||
onDelete: () => void;
|
||||
onEnabledChange: (enabled: boolean) => void;
|
||||
hasObjectLike?: boolean;
|
||||
needRenderAppendChild?: boolean;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export default function ParamOperator({
|
||||
level,
|
||||
data,
|
||||
hasObjectLike,
|
||||
readonly,
|
||||
needRenderAppendChild = true,
|
||||
onEnabledChange,
|
||||
onDelete,
|
||||
onAppend,
|
||||
}: ParamOperatorProps) {
|
||||
const isLimited = level >= 3;
|
||||
|
||||
// 是否可以添加子项
|
||||
const canAddChild = !readonly && ObjectLikeTypes.includes(data.type);
|
||||
// 子项按钮是否可用
|
||||
const enableAddChildButton =
|
||||
!readonly && hasObjectLike && canAddChild && needRenderAppendChild;
|
||||
// 是否显示删除按钮
|
||||
const showDeleteButton = !readonly;
|
||||
// 是否显示开启/关闭按钮
|
||||
const enabledSwitch = level === 0;
|
||||
|
||||
const { variablePageCanEdit } = useVariableContext();
|
||||
|
||||
return (
|
||||
<div className="flex items-center h-[24px] flex-shrink-0 justify-start gap-x-2 w-[130px]">
|
||||
{/* 开启/关闭 */}
|
||||
<Switch
|
||||
size="small"
|
||||
disabled={!variablePageCanEdit || !enabledSwitch}
|
||||
checked={data.enabled}
|
||||
onChange={onEnabledChange}
|
||||
/>
|
||||
{/* 添加子项 */}
|
||||
{needRenderAppendChild ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<Tooltip
|
||||
content={I18n.t('workflow_detail_node_output_add_subitem')}
|
||||
theme="dark"
|
||||
>
|
||||
<div>
|
||||
<AddOperation
|
||||
color="secondary"
|
||||
disabled={isLimited || !enableAddChildButton}
|
||||
className="cursor-pointer"
|
||||
onClick={onAppend}
|
||||
subitem={true}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
{/* 删除 */}
|
||||
<IconButton
|
||||
data-testid={VariableE2e.VariableTreeDeleteBtn}
|
||||
color="secondary"
|
||||
onClick={onDelete}
|
||||
disabled={!showDeleteButton}
|
||||
icon={<IconCozTrashCan />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 { type ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
IconCozNumber,
|
||||
IconCozNumberBracket,
|
||||
IconCozString,
|
||||
IconCozStringBracket,
|
||||
IconCozBoolean,
|
||||
IconCozBooleanBracket,
|
||||
IconCozBrace,
|
||||
IconCozBraceBracket,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
|
||||
import { ViewVariableType } from '@/store';
|
||||
|
||||
export const VARIABLE_TYPE_ICONS_MAP: Record<ViewVariableType, ReactNode> = {
|
||||
[ViewVariableType.String]: <IconCozString />,
|
||||
[ViewVariableType.Integer]: <IconCozNumber />,
|
||||
[ViewVariableType.Boolean]: <IconCozBoolean />,
|
||||
[ViewVariableType.Number]: <IconCozNumber />,
|
||||
[ViewVariableType.Object]: <IconCozBrace />,
|
||||
[ViewVariableType.ArrayString]: <IconCozStringBracket />,
|
||||
[ViewVariableType.ArrayInteger]: <IconCozNumberBracket />,
|
||||
[ViewVariableType.ArrayBoolean]: <IconCozBooleanBracket />,
|
||||
[ViewVariableType.ArrayNumber]: <IconCozNumberBracket />,
|
||||
[ViewVariableType.ArrayObject]: <IconCozBraceBracket />,
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/no-deep-relative-import */
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type SelectProps } from '@coze-arch/bot-semi/Select';
|
||||
import { Cascader } from '@coze-arch/coze-design';
|
||||
|
||||
import { VARIABLE_TYPE_ALIAS_MAP } from '@/types/view-variable-tree';
|
||||
|
||||
import { ReadonlyText } from '../readonly-text';
|
||||
import { type TreeNodeCustomData } from '../../../../type';
|
||||
import {
|
||||
getVariableTypeList,
|
||||
getCascaderVal,
|
||||
allVariableTypeList,
|
||||
} from './utils';
|
||||
import { VARIABLE_TYPE_ICONS_MAP } from './constants';
|
||||
|
||||
interface ParamTypeProps {
|
||||
data: TreeNodeCustomData;
|
||||
level: number;
|
||||
onSelectChange?: SelectProps['onChange'];
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export default function ParamType({
|
||||
data,
|
||||
onSelectChange,
|
||||
level,
|
||||
readonly,
|
||||
}: ParamTypeProps) {
|
||||
const optionList = useMemo(() => getVariableTypeList({ level }), [level]);
|
||||
|
||||
const cascaderVal = useMemo(
|
||||
() => getCascaderVal(data.type, allVariableTypeList),
|
||||
[data.type],
|
||||
);
|
||||
|
||||
return readonly ? (
|
||||
<ReadonlyText
|
||||
className="w-full"
|
||||
value={VARIABLE_TYPE_ALIAS_MAP[data.type]}
|
||||
/>
|
||||
) : (
|
||||
<Cascader
|
||||
placeholder={I18n.t('workflow_detail_start_variable_type')}
|
||||
disabled={readonly}
|
||||
onChange={val => {
|
||||
let newVal = val;
|
||||
if (Array.isArray(val)) {
|
||||
newVal = val[val.length - 1];
|
||||
}
|
||||
onSelectChange?.(newVal);
|
||||
}}
|
||||
className="w-full coz-stroke-plus"
|
||||
displayProp="value"
|
||||
displayRender={selected => {
|
||||
if (!Array.isArray(selected)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{VARIABLE_TYPE_ICONS_MAP[selected[selected.length - 1]]}
|
||||
<div className="truncate">
|
||||
{VARIABLE_TYPE_ALIAS_MAP[selected[selected.length - 1]]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
treeData={optionList}
|
||||
value={cascaderVal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { VARIABLE_TYPE_ALIAS_MAP } from '@/types/view-variable-tree';
|
||||
import { ObjectLikeTypes } from '@/store/variable-groups/types';
|
||||
import { ViewVariableType } from '@/store';
|
||||
|
||||
const LEVEL_LIMIT = 3;
|
||||
|
||||
export const generateVariableOption = (
|
||||
type: ViewVariableType,
|
||||
label?: string,
|
||||
display?: string,
|
||||
) => ({
|
||||
value: Number(type),
|
||||
label: label || VARIABLE_TYPE_ALIAS_MAP[type],
|
||||
display: display || label || VARIABLE_TYPE_ALIAS_MAP[type],
|
||||
});
|
||||
|
||||
export interface VariableTypeOption {
|
||||
// 类型的值, 非叶子节点时可能为空
|
||||
value: number | string;
|
||||
// 选项的展示名称
|
||||
label: ReactNode;
|
||||
// 回显的展示名称
|
||||
display?: string;
|
||||
// 类型是否禁用
|
||||
disabled?: boolean;
|
||||
// 子类型
|
||||
children?: VariableTypeOption[];
|
||||
}
|
||||
|
||||
export const allVariableTypeList: Array<VariableTypeOption> = [
|
||||
generateVariableOption(ViewVariableType.String),
|
||||
generateVariableOption(ViewVariableType.Integer),
|
||||
generateVariableOption(ViewVariableType.Boolean),
|
||||
generateVariableOption(ViewVariableType.Number),
|
||||
generateVariableOption(ViewVariableType.Object),
|
||||
generateVariableOption(ViewVariableType.ArrayString),
|
||||
generateVariableOption(ViewVariableType.ArrayInteger),
|
||||
generateVariableOption(ViewVariableType.ArrayBoolean),
|
||||
generateVariableOption(ViewVariableType.ArrayNumber),
|
||||
generateVariableOption(ViewVariableType.ArrayObject),
|
||||
];
|
||||
|
||||
const filterTypes = (
|
||||
list: Array<VariableTypeOption>,
|
||||
options?: VariableListOptions,
|
||||
): Array<VariableTypeOption> => {
|
||||
const { level } = options || {};
|
||||
|
||||
return list.reduce((pre, cur) => {
|
||||
const newOption = { ...cur };
|
||||
|
||||
if (newOption.children) {
|
||||
newOption.children = filterTypes(newOption.children, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 到达层级限制时禁用 ObjectLike 类型,避免嵌套过深
|
||||
*/
|
||||
const disabled = Boolean(
|
||||
level &&
|
||||
level >= LEVEL_LIMIT &&
|
||||
ObjectLikeTypes.includes(Number(newOption.value)),
|
||||
);
|
||||
|
||||
return [
|
||||
...pre,
|
||||
{
|
||||
...newOption,
|
||||
disabled,
|
||||
},
|
||||
];
|
||||
}, [] as Array<VariableTypeOption>);
|
||||
};
|
||||
|
||||
interface VariableListOptions {
|
||||
level?: number;
|
||||
}
|
||||
|
||||
export const getVariableTypeList = options =>
|
||||
filterTypes(allVariableTypeList, options);
|
||||
|
||||
/**
|
||||
* 获取类型在选项列表中的路径,作为 cascader 的 value
|
||||
*/
|
||||
export const getCascaderVal = (
|
||||
originalVal: ViewVariableType,
|
||||
list: Array<VariableTypeOption>,
|
||||
path: Array<string | number> = [],
|
||||
) => {
|
||||
let valuePath = [...path];
|
||||
list.forEach(item => {
|
||||
if (item.children) {
|
||||
const childPath = getCascaderVal(originalVal, item.children, [
|
||||
...valuePath,
|
||||
item.value,
|
||||
]);
|
||||
if (childPath[childPath.length - 1] === originalVal) {
|
||||
valuePath = childPath;
|
||||
return;
|
||||
}
|
||||
} else if (item.value === originalVal) {
|
||||
valuePath.push(originalVal);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return valuePath;
|
||||
};
|
||||
@@ -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 classNames from 'classnames';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const ReadonlyText = (props: { value: string; className?: string }) => {
|
||||
const { value, className } = props;
|
||||
return (
|
||||
<Text
|
||||
className={classNames(
|
||||
'w-full coz-fg-primary text-sm !font-medium',
|
||||
className,
|
||||
)}
|
||||
ellipsis
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ViewVariableType } from '@/store';
|
||||
|
||||
export enum ChangeMode {
|
||||
Update,
|
||||
Delete,
|
||||
Append,
|
||||
UpdateEnabled,
|
||||
Replace,
|
||||
}
|
||||
|
||||
// JSON类型
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const JSONLikeTypes = [
|
||||
ViewVariableType.Object,
|
||||
ViewVariableType.ArrayObject,
|
||||
ViewVariableType.ArrayBoolean,
|
||||
ViewVariableType.ArrayNumber,
|
||||
ViewVariableType.ArrayString,
|
||||
ViewVariableType.ArrayInteger,
|
||||
];
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
|
||||
import isNumber from 'lodash-es/isNumber';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { type RenderFullLabelProps } from '@coze-arch/bot-semi/Tree';
|
||||
import { IconCozArrowRight } from '@coze-arch/coze-design/icons';
|
||||
|
||||
import { type Variable, type ViewVariableType } from '@/store';
|
||||
|
||||
import { type TreeNodeCustomData } from '../../type';
|
||||
import { TreeIndentWidth } from '../../constants';
|
||||
import { ChangeMode } from './constants';
|
||||
import ParamType from './components/param-type';
|
||||
import ParamOperator from './components/param-operator';
|
||||
import { ParamName } from './components/param-name';
|
||||
import { ParamDescription } from './components/param-description';
|
||||
import { ParamDefault } from './components/param-default';
|
||||
import { ParamChannel } from './components/param-channel';
|
||||
export interface CustomTreeNodeProps extends RenderFullLabelProps {
|
||||
level: number;
|
||||
readonly?: boolean;
|
||||
variablePageCanEdit?: boolean;
|
||||
needRenderAppendChild?: boolean;
|
||||
onChange: (mode: ChangeMode, param: TreeNodeCustomData) => void;
|
||||
hasObjectLike?: boolean;
|
||||
disableDelete?: boolean;
|
||||
couldCollapse?: boolean;
|
||||
hideHeaderKeys?: string[];
|
||||
collapsed?: boolean;
|
||||
validateExistKeyword?: boolean;
|
||||
onCollapse?: (collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
export default function CustomTreeNode(props: CustomTreeNodeProps) {
|
||||
const {
|
||||
data,
|
||||
className,
|
||||
level,
|
||||
readonly = false,
|
||||
onChange,
|
||||
hasObjectLike,
|
||||
couldCollapse = true,
|
||||
hideHeaderKeys,
|
||||
collapsed = false,
|
||||
onCollapse,
|
||||
validateExistKeyword = false,
|
||||
} = props;
|
||||
// 当前值
|
||||
const value = cloneDeep(data) as Variable;
|
||||
const treeNodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 删除时
|
||||
const onDelete = () => {
|
||||
onChange(ChangeMode.Delete, value);
|
||||
};
|
||||
|
||||
// 新增子项时
|
||||
const onAppend = () => {
|
||||
onChange(ChangeMode.Append, value);
|
||||
};
|
||||
// 类型切换时
|
||||
const onSelectChange = (
|
||||
val?: string | number | Array<unknown> | Record<string, unknown>,
|
||||
) => {
|
||||
if (val === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!isNumber(val)) {
|
||||
return;
|
||||
}
|
||||
// 清除默认值
|
||||
value.defaultValue = '';
|
||||
value.children = [];
|
||||
onChange(ChangeMode.Update, { ...value, type: val as ViewVariableType });
|
||||
};
|
||||
const onDefaultChange = (
|
||||
val: string | number | boolean | TreeNodeCustomData,
|
||||
) => {
|
||||
onChange(ChangeMode.Update, { ...value, defaultValue: val.toString() });
|
||||
};
|
||||
|
||||
const onImportChange = (val: TreeNodeCustomData) => {
|
||||
onChange(ChangeMode.Replace, val);
|
||||
};
|
||||
|
||||
const onNameChange = (name: string) => {
|
||||
if (value.name === name) {
|
||||
return;
|
||||
}
|
||||
onChange(ChangeMode.Update, { ...value, name });
|
||||
};
|
||||
|
||||
const onDescriptionChange = useCallback(
|
||||
(description: string) => {
|
||||
if (value.description === description) {
|
||||
return;
|
||||
}
|
||||
onChange(ChangeMode.Update, { ...value, description });
|
||||
},
|
||||
[onChange, value],
|
||||
);
|
||||
|
||||
const onEnabledChange = useCallback(
|
||||
(enabled: boolean) => {
|
||||
onChange(ChangeMode.UpdateEnabled, { ...value, enabled });
|
||||
},
|
||||
[onChange, value],
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={classNames('flex items-center', {
|
||||
[className]: Boolean(className),
|
||||
})}
|
||||
ref={treeNodeRef}
|
||||
>
|
||||
<div className="flex flex-1 my-3 gap-x-4 items-center w-full relative h-[32px]">
|
||||
<div className="flex flex-1 items-center flex-nowrap overflow-x-hidden overflow-y-visible">
|
||||
<div
|
||||
className="flex items-center justify-end"
|
||||
style={{ width: level * TreeIndentWidth }}
|
||||
></div>
|
||||
<IconCozArrowRight
|
||||
className={classNames(
|
||||
'flex-none mr-2 w-[16px] h-[16px]',
|
||||
collapsed ? 'rotate-90' : '',
|
||||
couldCollapse ? '' : 'invisible',
|
||||
'cursor-pointer',
|
||||
level === 0 && !couldCollapse ? 'hidden' : '',
|
||||
)}
|
||||
onClick={() => {
|
||||
onCollapse?.(!collapsed);
|
||||
}}
|
||||
/>
|
||||
<ParamName
|
||||
readonly={readonly}
|
||||
data={value}
|
||||
onChange={onNameChange}
|
||||
validateExistKeyword={validateExistKeyword}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ParamDescription
|
||||
data={value}
|
||||
onChange={onDescriptionChange}
|
||||
readonly={readonly}
|
||||
/>
|
||||
</div>
|
||||
{!hideHeaderKeys?.includes('type') ? (
|
||||
<div className="flex-none w-[166px] basis-[166px]">
|
||||
<ParamType
|
||||
level={level}
|
||||
readonly={readonly}
|
||||
data={value}
|
||||
onSelectChange={onSelectChange}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex-none w-[164px] basis-[164px]">
|
||||
<ParamDefault
|
||||
readonly={readonly}
|
||||
data={value}
|
||||
onDefaultChange={onDefaultChange}
|
||||
onImportChange={onImportChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-none w-[164px] basis-[164px] empty:hidden">
|
||||
<ParamChannel value={value} />
|
||||
</div>
|
||||
<div className="flex-none w-[130px] basis-[130px]">
|
||||
<ParamOperator
|
||||
data={value}
|
||||
readonly={readonly}
|
||||
level={level}
|
||||
onDelete={onDelete}
|
||||
onAppend={onAppend}
|
||||
hasObjectLike={hasObjectLike}
|
||||
needRenderAppendChild={!hideHeaderKeys?.includes('type')}
|
||||
onEnabledChange={onEnabledChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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,
|
||||
useState,
|
||||
Suspense,
|
||||
lazy,
|
||||
type FC,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozBroom } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Tooltip, Modal } from '@coze-arch/coze-design';
|
||||
|
||||
import { MAX_JSON_LENGTH } from '../../constants';
|
||||
import { formatJson } from './utils/format-json';
|
||||
import {
|
||||
convertSchemaService,
|
||||
type SchemaNode,
|
||||
} from './service/convert-schema-service';
|
||||
|
||||
import lightStyles from './light.module.less';
|
||||
|
||||
const LazyBizIDEMonacoEditor = lazy(async () => {
|
||||
const { Editor } = await import('@coze-arch/bot-monaco-editor');
|
||||
return { default: Editor };
|
||||
});
|
||||
|
||||
const BizIDEMonacoEditor = props => (
|
||||
<Suspense>
|
||||
<LazyBizIDEMonacoEditor {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
interface JSONEditorProps {
|
||||
id: string;
|
||||
value: string;
|
||||
groupId: string;
|
||||
setValue: (value: string) => void;
|
||||
visible: boolean;
|
||||
readonly?: boolean;
|
||||
onCancel: () => void;
|
||||
onOk: (value: SchemaNode[]) => void;
|
||||
}
|
||||
|
||||
const ValidateRules = {
|
||||
jsonValid: {
|
||||
message: I18n.t('variables_json_input_error'),
|
||||
validator: (value: string) => {
|
||||
try {
|
||||
const rs = JSON.parse(value);
|
||||
const isJson = typeof rs === 'object';
|
||||
return isJson;
|
||||
// eslint-disable-next-line @coze-arch/use-error-in-catch
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
jsonLength: {
|
||||
message: I18n.t('variables_json_input_limit'),
|
||||
validator: (value: string) => {
|
||||
if (value.length > MAX_JSON_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const JSONEditor: FC<JSONEditorProps> = props => {
|
||||
const { id, value, setValue, visible, onCancel, onOk, readonly } = props;
|
||||
const [schema, setSchema] = useState<SchemaNode[] | undefined>();
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const change = useCallback(async () => {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
setError(undefined);
|
||||
return new Promise(resolve => {
|
||||
Modal.warning({
|
||||
title: I18n.t('workflow_json_node_update_tips_title'),
|
||||
content: I18n.t('workflow_json_node_update_tips_content'),
|
||||
okType: 'warning',
|
||||
okText: I18n.t('Confirm'),
|
||||
cancelText: I18n.t('Cancel'),
|
||||
onOk: () => {
|
||||
const outputValue = convert(value) || [];
|
||||
onOk(outputValue);
|
||||
resolve(true);
|
||||
},
|
||||
onCancel: () => resolve(false),
|
||||
});
|
||||
});
|
||||
}, [schema]);
|
||||
|
||||
const convert = (jsonString: string): SchemaNode[] | undefined => {
|
||||
if (!jsonString) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const json = JSON.parse(jsonString);
|
||||
const outputValue = convertSchemaService(json);
|
||||
if (
|
||||
!outputValue ||
|
||||
!Array.isArray(outputValue) ||
|
||||
outputValue.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return outputValue;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const validate = (newValue: string) => {
|
||||
const rules = Object.values(ValidateRules);
|
||||
for (const rule of rules) {
|
||||
if (!rule.validator(newValue)) {
|
||||
setError(rule.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setError(undefined);
|
||||
return true;
|
||||
};
|
||||
|
||||
const isValid = useMemo(() => validate(value), [value]);
|
||||
|
||||
// 同步 value 和 schema
|
||||
useEffect(() => {
|
||||
const _schema = convert(value);
|
||||
setSchema(_schema);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={
|
||||
readonly
|
||||
? I18n.t('variables_json_input_readonly_title')
|
||||
: I18n.t('workflow_json_windows_title')
|
||||
}
|
||||
okText={I18n.t('Confirm')}
|
||||
cancelText={I18n.t('Cancel')}
|
||||
onOk={change}
|
||||
onCancel={onCancel}
|
||||
height={530}
|
||||
okButtonProps={{
|
||||
disabled: !isValid || readonly,
|
||||
}}
|
||||
>
|
||||
<div key={id} className="w-full relative">
|
||||
<div className="w-full h-[48px] coz-bg-primary rounded-t-lg coz-fg-primary font-medium text-sm flex items-center justify-between px-4">
|
||||
<div className="coz-fg-primary">JSON</div>
|
||||
<Tooltip content={I18n.t('workflow_exception_ignore_format')}>
|
||||
<IconButton
|
||||
className="bg-transparent"
|
||||
disabled={readonly}
|
||||
icon={<IconCozBroom />}
|
||||
onClick={() => {
|
||||
setValue(formatJson(value));
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="w-full h-[320px]">
|
||||
<BizIDEMonacoEditor
|
||||
key={id}
|
||||
value={value}
|
||||
defaultLanguage="json"
|
||||
/** 通过 css 样式覆盖 icube-dark 主题 */
|
||||
className={lightStyles.light}
|
||||
options={{
|
||||
fontSize: 13,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
contextmenu: false,
|
||||
scrollbar: {
|
||||
verticalScrollbarSize: 10,
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
lineNumbers: 'on',
|
||||
lineNumbersMinChars: 3,
|
||||
folding: false,
|
||||
lineDecorationsWidth: 2,
|
||||
renderLineHighlight: 'none',
|
||||
glyphMargin: false,
|
||||
scrollBeyondLastLine: false,
|
||||
overviewRulerBorder: false,
|
||||
wordWrap: 'on',
|
||||
fixedOverflowWidgets: true,
|
||||
readOnly: readonly,
|
||||
}}
|
||||
onChange={stringValue => {
|
||||
setValue(stringValue || '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<div className="absolute top-full">
|
||||
<span className="coz-fg-hglt-red text-[12px] font-[400] leading-[16px] whitespace-nowrap">
|
||||
{error}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,399 @@
|
||||
// fork from biz-ide-component vs-light-theme.module.less
|
||||
// 样式与 ide 保持一致
|
||||
/* stylelint-disable declaration-no-important */
|
||||
.light {
|
||||
:global {
|
||||
.monaco-editor {
|
||||
--vscode-foreground: #616161;
|
||||
--vscode-disabledForeground: rgba(97, 97, 97, 50%);
|
||||
--vscode-errorForeground: #a1260d;
|
||||
--vscode-descriptionForeground: #717171;
|
||||
--vscode-icon-foreground: #424242;
|
||||
--vscode-focusBorder: #0090f1;
|
||||
--vscode-textSeparator-foreground: rgba(0, 0, 0, 18%);
|
||||
--vscode-textLink-foreground: #006ab1;
|
||||
--vscode-textLink-activeForeground: #006ab1;
|
||||
--vscode-textPreformat-foreground: #a31515;
|
||||
--vscode-textBlockQuote-background: rgba(127, 127, 127, 10%);
|
||||
--vscode-textBlockQuote-border: rgba(0, 122, 204, 50%);
|
||||
--vscode-textCodeBlock-background: rgba(220, 220, 220, 40%);
|
||||
--vscode-widget-shadow: rgba(0, 0, 0, 16%);
|
||||
--vscode-input-background: #fff;
|
||||
--vscode-input-foreground: #616161;
|
||||
--vscode-inputOption-activeBorder: #007acc;
|
||||
--vscode-inputOption-hoverBackground: rgba(184, 184, 184, 31%);
|
||||
--vscode-inputOption-activeBackground: rgba(0, 144, 241, 20%);
|
||||
--vscode-inputOption-activeForeground: #000;
|
||||
--vscode-input-placeholderForeground: rgba(97, 97, 97, 50%);
|
||||
--vscode-inputValidation-infoBackground: #d6ecf2;
|
||||
--vscode-inputValidation-infoBorder: #007acc;
|
||||
--vscode-inputValidation-warningBackground: #f6f5d2;
|
||||
--vscode-inputValidation-warningBorder: #b89500;
|
||||
--vscode-inputValidation-errorBackground: #f2dede;
|
||||
--vscode-inputValidation-errorBorder: #be1100;
|
||||
--vscode-dropdown-background: #fff;
|
||||
--vscode-dropdown-foreground: #616161;
|
||||
--vscode-dropdown-border: #cecece;
|
||||
--vscode-button-foreground: #fff;
|
||||
--vscode-button-separator: rgba(255, 255, 255, 40%);
|
||||
--vscode-button-background: #007acc;
|
||||
--vscode-button-hoverBackground: #0062a3;
|
||||
--vscode-button-secondaryForeground: #fff;
|
||||
--vscode-button-secondaryBackground: #5f6a79;
|
||||
--vscode-button-secondaryHoverBackground: #4c5561;
|
||||
--vscode-badge-background: #c4c4c4;
|
||||
--vscode-badge-foreground: #333;
|
||||
--vscode-scrollbar-shadow: #ddd;
|
||||
--vscode-scrollbarSlider-background: rgba(100, 100, 100, 40%);
|
||||
--vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 70%);
|
||||
--vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 60%);
|
||||
--vscode-progressBar-background: #0e70c0;
|
||||
--vscode-editorError-foreground: #e51400;
|
||||
--vscode-editorWarning-foreground: #bf8803;
|
||||
--vscode-editorInfo-foreground: #1a85ff;
|
||||
--vscode-editorHint-foreground: #6c6c6c;
|
||||
--vscode-sash-hoverBorder: #0090f1;
|
||||
--vscode-editor-background: #fffffe;
|
||||
--vscode-editor-foreground: #000;
|
||||
--vscode-editorStickyScroll-background: #fffffe;
|
||||
--vscode-editorStickyScrollHover-background: #f0f0f0;
|
||||
--vscode-editorWidget-background: #f3f3f3;
|
||||
--vscode-editorWidget-foreground: #616161;
|
||||
--vscode-editorWidget-border: #c8c8c8;
|
||||
--vscode-quickInput-background: #f3f3f3;
|
||||
--vscode-quickInput-foreground: #616161;
|
||||
--vscode-quickInputTitle-background: rgba(0, 0, 0, 6%);
|
||||
--vscode-pickerGroup-foreground: #0066bf;
|
||||
--vscode-pickerGroup-border: #cccedb;
|
||||
--vscode-keybindingLabel-background: rgba(221, 221, 221, 40%);
|
||||
--vscode-keybindingLabel-foreground: #555;
|
||||
--vscode-keybindingLabel-border: rgba(204, 204, 204, 40%);
|
||||
--vscode-keybindingLabel-bottomBorder: rgba(187, 187, 187, 40%);
|
||||
--vscode-editor-selectionBackground: #add6ff;
|
||||
--vscode-editor-inactiveSelectionBackground: #e5ebf1;
|
||||
--vscode-editor-selectionHighlightBackground: rgba(173, 214, 255, 30%);
|
||||
--vscode-editor-findMatchBackground: #a8ac94;
|
||||
--vscode-editor-findMatchHighlightBackground: rgba(234, 92, 0, 33%);
|
||||
--vscode-editor-findRangeHighlightBackground: rgba(180, 180, 180, 30%);
|
||||
--vscode-searchEditor-findMatchBackground: rgba(234, 92, 0, 22%);
|
||||
--vscode-search-resultsInfoForeground: #616161;
|
||||
--vscode-editor-hoverHighlightBackground: rgba(173, 214, 255, 15%);
|
||||
--vscode-editorHoverWidget-background: #f3f3f3;
|
||||
--vscode-editorHoverWidget-foreground: #616161;
|
||||
--vscode-editorHoverWidget-border: #c8c8c8;
|
||||
--vscode-editorHoverWidget-statusBarBackground: #e7e7e7;
|
||||
--vscode-editorLink-activeForeground: #00f;
|
||||
--vscode-editorInlayHint-foreground: #969696;
|
||||
--vscode-editorInlayHint-background: rgba(196, 196, 196, 10%);
|
||||
--vscode-editorInlayHint-typeForeground: #969696;
|
||||
--vscode-editorInlayHint-typeBackground: rgba(196, 196, 196, 10%);
|
||||
--vscode-editorInlayHint-parameterForeground: #969696;
|
||||
--vscode-editorInlayHint-parameterBackground: rgba(196, 196, 196, 10%);
|
||||
--vscode-editorLightBulb-foreground: #ddb100;
|
||||
--vscode-editorLightBulbAutoFix-foreground: #007acc;
|
||||
--vscode-diffEditor-insertedTextBackground: rgba(156, 204, 44, 25%);
|
||||
--vscode-diffEditor-removedTextBackground: rgba(255, 0, 0, 20%);
|
||||
--vscode-diffEditor-insertedLineBackground: rgba(155, 185, 85, 20%);
|
||||
--vscode-diffEditor-removedLineBackground: rgba(255, 0, 0, 20%);
|
||||
--vscode-diffEditor-diagonalFill: rgba(34, 34, 34, 20%);
|
||||
--vscode-diffEditor-unchangedRegionBackground: #e4e4e4;
|
||||
--vscode-diffEditor-unchangedRegionForeground: #4d4c4c;
|
||||
--vscode-diffEditor-unchangedCodeBackground: rgba(184, 184, 184, 16%);
|
||||
--vscode-list-focusOutline: #0090f1;
|
||||
--vscode-list-activeSelectionBackground: #0060c0;
|
||||
--vscode-list-activeSelectionForeground: #fff;
|
||||
--vscode-list-inactiveSelectionBackground: #e4e6f1;
|
||||
--vscode-list-hoverBackground: #f0f0f0;
|
||||
--vscode-list-dropBackground: #d6ebff;
|
||||
--vscode-list-highlightForeground: #0066bf;
|
||||
--vscode-list-focusHighlightForeground: #bbe7ff;
|
||||
--vscode-list-invalidItemForeground: #b89500;
|
||||
--vscode-list-errorForeground: #b01011;
|
||||
--vscode-list-warningForeground: #855f00;
|
||||
--vscode-listFilterWidget-background: #f3f3f3;
|
||||
--vscode-listFilterWidget-outline: rgba(0, 0, 0, 0%);
|
||||
--vscode-listFilterWidget-noMatchesOutline: #be1100;
|
||||
--vscode-listFilterWidget-shadow: rgba(0, 0, 0, 16%);
|
||||
--vscode-list-filterMatchBackground: rgba(234, 92, 0, 33%);
|
||||
--vscode-tree-indentGuidesStroke: #a9a9a9;
|
||||
--vscode-tree-inactiveIndentGuidesStroke: rgba(169, 169, 169, 40%);
|
||||
--vscode-tree-tableColumnsBorder: rgba(97, 97, 97, 13%);
|
||||
--vscode-tree-tableOddRowsBackground: rgba(97, 97, 97, 4%);
|
||||
--vscode-list-deemphasizedForeground: #8e8e90;
|
||||
--vscode-checkbox-background: #fff;
|
||||
--vscode-checkbox-selectBackground: #f3f3f3;
|
||||
--vscode-checkbox-foreground: #616161;
|
||||
--vscode-checkbox-border: #cecece;
|
||||
--vscode-checkbox-selectBorder: #424242;
|
||||
--vscode-quickInputList-focusForeground: #fff;
|
||||
--vscode-quickInputList-focusBackground: #0060c0;
|
||||
--vscode-menu-foreground: #616161;
|
||||
--vscode-menu-background: #fff;
|
||||
--vscode-menu-selectionForeground: #fff;
|
||||
--vscode-menu-selectionBackground: #0060c0;
|
||||
--vscode-menu-separatorBackground: #d4d4d4;
|
||||
--vscode-toolbar-hoverBackground: rgba(184, 184, 184, 31%);
|
||||
--vscode-toolbar-activeBackground: rgba(166, 166, 166, 31%);
|
||||
--vscode-editor-snippetTabstopHighlightBackground: rgba(10, 50, 100, 20%);
|
||||
--vscode-editor-snippetFinalTabstopHighlightBorder: rgba(10,
|
||||
50,
|
||||
100,
|
||||
50%);
|
||||
--vscode-breadcrumb-foreground: rgba(97, 97, 97, 80%);
|
||||
--vscode-breadcrumb-background: #fffffe;
|
||||
--vscode-breadcrumb-focusForeground: #4e4e4e;
|
||||
--vscode-breadcrumb-activeSelectionForeground: #4e4e4e;
|
||||
--vscode-breadcrumbPicker-background: #f3f3f3;
|
||||
--vscode-merge-currentHeaderBackground: rgba(64, 200, 174, 50%);
|
||||
--vscode-merge-currentContentBackground: rgba(64, 200, 174, 20%);
|
||||
--vscode-merge-incomingHeaderBackground: rgba(64, 166, 255, 50%);
|
||||
--vscode-merge-incomingContentBackground: rgba(64, 166, 255, 20%);
|
||||
--vscode-merge-commonHeaderBackground: rgba(96, 96, 96, 40%);
|
||||
--vscode-merge-commonContentBackground: rgba(96, 96, 96, 16%);
|
||||
--vscode-editorOverviewRuler-currentContentForeground: rgba(64,
|
||||
200,
|
||||
174,
|
||||
50%);
|
||||
--vscode-editorOverviewRuler-incomingContentForeground: rgba(64,
|
||||
166,
|
||||
255,
|
||||
50%);
|
||||
--vscode-editorOverviewRuler-commonContentForeground: rgba(96,
|
||||
96,
|
||||
96,
|
||||
40%);
|
||||
--vscode-editorOverviewRuler-findMatchForeground: rgba(209,
|
||||
134,
|
||||
22,
|
||||
49%);
|
||||
--vscode-editorOverviewRuler-selectionHighlightForeground: rgba(160,
|
||||
160,
|
||||
160,
|
||||
80%);
|
||||
--vscode-minimap-findMatchHighlight: #d18616;
|
||||
--vscode-minimap-selectionOccurrenceHighlight: #c9c9c9;
|
||||
--vscode-minimap-selectionHighlight: #add6ff;
|
||||
--vscode-minimap-errorHighlight: rgba(255, 18, 18, 70%);
|
||||
--vscode-minimap-warningHighlight: #bf8803;
|
||||
--vscode-minimap-foregroundOpacity: #000;
|
||||
--vscode-minimapSlider-background: rgba(100, 100, 100, 20%);
|
||||
--vscode-minimapSlider-hoverBackground: rgba(100, 100, 100, 35%);
|
||||
--vscode-minimapSlider-activeBackground: rgba(0, 0, 0, 30%);
|
||||
--vscode-problemsErrorIcon-foreground: #e51400;
|
||||
--vscode-problemsWarningIcon-foreground: #bf8803;
|
||||
--vscode-problemsInfoIcon-foreground: #1a85ff;
|
||||
--vscode-charts-foreground: #616161;
|
||||
--vscode-charts-lines: rgba(97, 97, 97, 50%);
|
||||
--vscode-charts-red: #e51400;
|
||||
--vscode-charts-blue: #1a85ff;
|
||||
--vscode-charts-yellow: #bf8803;
|
||||
--vscode-charts-orange: #d18616;
|
||||
--vscode-charts-green: #388a34;
|
||||
--vscode-charts-purple: #652d90;
|
||||
--vscode-diffEditor-move-border: rgba(139, 139, 139, 61%);
|
||||
--vscode-diffEditor-moveActive-border: #ffa500;
|
||||
--vscode-symbolIcon-arrayForeground: #616161;
|
||||
--vscode-symbolIcon-booleanForeground: #616161;
|
||||
--vscode-symbolIcon-classForeground: #d67e00;
|
||||
--vscode-symbolIcon-colorForeground: #616161;
|
||||
--vscode-symbolIcon-constantForeground: #616161;
|
||||
--vscode-symbolIcon-constructorForeground: #652d90;
|
||||
--vscode-symbolIcon-enumeratorForeground: #d67e00;
|
||||
--vscode-symbolIcon-enumeratorMemberForeground: #007acc;
|
||||
--vscode-symbolIcon-eventForeground: #d67e00;
|
||||
--vscode-symbolIcon-fieldForeground: #007acc;
|
||||
--vscode-symbolIcon-fileForeground: #616161;
|
||||
--vscode-symbolIcon-folderForeground: #616161;
|
||||
--vscode-symbolIcon-functionForeground: #652d90;
|
||||
--vscode-symbolIcon-interfaceForeground: #007acc;
|
||||
--vscode-symbolIcon-keyForeground: #616161;
|
||||
--vscode-symbolIcon-keywordForeground: #616161;
|
||||
--vscode-symbolIcon-methodForeground: #652d90;
|
||||
--vscode-symbolIcon-moduleForeground: #616161;
|
||||
--vscode-symbolIcon-namespaceForeground: #616161;
|
||||
--vscode-symbolIcon-nullForeground: #616161;
|
||||
--vscode-symbolIcon-numberForeground: #616161;
|
||||
--vscode-symbolIcon-objectForeground: #616161;
|
||||
--vscode-symbolIcon-operatorForeground: #616161;
|
||||
--vscode-symbolIcon-packageForeground: #616161;
|
||||
--vscode-symbolIcon-propertyForeground: #616161;
|
||||
--vscode-symbolIcon-referenceForeground: #616161;
|
||||
--vscode-symbolIcon-snippetForeground: #616161;
|
||||
--vscode-symbolIcon-stringForeground: #616161;
|
||||
--vscode-symbolIcon-structForeground: #616161;
|
||||
--vscode-symbolIcon-textForeground: #616161;
|
||||
--vscode-symbolIcon-typeParameterForeground: #616161;
|
||||
--vscode-symbolIcon-unitForeground: #616161;
|
||||
--vscode-symbolIcon-variableForeground: #007acc;
|
||||
--vscode-actionBar-toggledBackground: rgba(0, 144, 241, 20%);
|
||||
--vscode-editor-lineHighlightBorder: #eee;
|
||||
--vscode-editor-rangeHighlightBackground: rgba(253, 255, 0, 20%);
|
||||
--vscode-editor-symbolHighlightBackground: rgba(234, 92, 0, 33%);
|
||||
--vscode-editorCursor-foreground: #000;
|
||||
--vscode-editorWhitespace-foreground: rgba(51, 51, 51, 20%);
|
||||
--vscode-editorLineNumber-foreground: #237893;
|
||||
--vscode-editorIndentGuide-background: rgba(51, 51, 51, 20%);
|
||||
--vscode-editorIndentGuide-activeBackground: rgba(51, 51, 51, 20%);
|
||||
--vscode-editorIndentGuide-background1: #d3d3d3;
|
||||
--vscode-editorIndentGuide-background2: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorIndentGuide-background3: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorIndentGuide-background4: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorIndentGuide-background5: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorIndentGuide-background6: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorIndentGuide-activeBackground1: #939393;
|
||||
--vscode-editorIndentGuide-activeBackground2: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorIndentGuide-activeBackground3: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorIndentGuide-activeBackground4: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorIndentGuide-activeBackground5: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorIndentGuide-activeBackground6: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorActiveLineNumber-foreground: #0b216f;
|
||||
--vscode-editorLineNumber-activeForeground: #0b216f;
|
||||
--vscode-editorRuler-foreground: #d3d3d3;
|
||||
--vscode-editorCodeLens-foreground: #919191;
|
||||
--vscode-editorBracketMatch-background: rgba(0, 100, 0, 10%);
|
||||
--vscode-editorBracketMatch-border: #b9b9b9;
|
||||
--vscode-editorOverviewRuler-border: rgba(127, 127, 127, 30%);
|
||||
--vscode-editorGutter-background: #fffffe;
|
||||
--vscode-editorUnnecessaryCode-opacity: rgba(0, 0, 0, 47%);
|
||||
--vscode-editorGhostText-foreground: rgba(0, 0, 0, 47%);
|
||||
--vscode-editorOverviewRuler-rangeHighlightForeground: rgba(0,
|
||||
122,
|
||||
204,
|
||||
60%);
|
||||
--vscode-editorOverviewRuler-errorForeground: rgba(255, 18, 18, 70%);
|
||||
--vscode-editorOverviewRuler-warningForeground: #bf8803;
|
||||
--vscode-editorOverviewRuler-infoForeground: #1a85ff;
|
||||
--vscode-editorBracketHighlight-foreground1: #0431fa;
|
||||
--vscode-editorBracketHighlight-foreground2: #319331;
|
||||
--vscode-editorBracketHighlight-foreground3: #7b3814;
|
||||
--vscode-editorBracketHighlight-foreground4: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketHighlight-foreground5: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketHighlight-foreground6: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketHighlight-unexpectedBracket-foreground: rgba(255,
|
||||
18,
|
||||
18,
|
||||
80%);
|
||||
--vscode-editorBracketPairGuide-background1: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketPairGuide-background2: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketPairGuide-background3: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketPairGuide-background4: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketPairGuide-background5: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketPairGuide-background6: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketPairGuide-activeBackground1: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketPairGuide-activeBackground2: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketPairGuide-activeBackground3: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketPairGuide-activeBackground4: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketPairGuide-activeBackground5: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorBracketPairGuide-activeBackground6: rgba(0, 0, 0, 0%);
|
||||
--vscode-editorUnicodeHighlight-border: #cea33d;
|
||||
--vscode-editorUnicodeHighlight-background: rgba(206, 163, 61, 8%);
|
||||
--vscode-editorOverviewRuler-bracketMatchForeground: #a0a0a0;
|
||||
--vscode-editor-linkedEditingBackground: rgba(255, 0, 0, 30%);
|
||||
--vscode-editor-wordHighlightBackground: rgba(87, 87, 87, 25%);
|
||||
--vscode-editor-wordHighlightStrongBackground: rgba(14, 99, 156, 25%);
|
||||
--vscode-editor-wordHighlightTextBackground: rgba(87, 87, 87, 25%);
|
||||
--vscode-editorOverviewRuler-wordHighlightForeground: rgba(160,
|
||||
160,
|
||||
160,
|
||||
80%);
|
||||
--vscode-editorOverviewRuler-wordHighlightStrongForeground: rgba(192,
|
||||
160,
|
||||
192,
|
||||
80%);
|
||||
--vscode-editorOverviewRuler-wordHighlightTextForeground: rgba(160,
|
||||
160,
|
||||
160,
|
||||
80%);
|
||||
--vscode-peekViewTitle-background: #f3f3f3;
|
||||
--vscode-peekViewTitleLabel-foreground: #000;
|
||||
--vscode-peekViewTitleDescription-foreground: #616161;
|
||||
--vscode-peekView-border: #1a85ff;
|
||||
--vscode-peekViewResult-background: #f3f3f3;
|
||||
--vscode-peekViewResult-lineForeground: #646465;
|
||||
--vscode-peekViewResult-fileForeground: #1e1e1e;
|
||||
--vscode-peekViewResult-selectionBackground: rgba(51, 153, 255, 20%);
|
||||
--vscode-peekViewResult-selectionForeground: #6c6c6c;
|
||||
--vscode-peekViewEditor-background: #f2f8fc;
|
||||
--vscode-peekViewEditorGutter-background: #f2f8fc;
|
||||
--vscode-peekViewEditorStickyScroll-background: #f2f8fc;
|
||||
--vscode-peekViewResult-matchHighlightBackground: rgba(234, 92, 0, 30%);
|
||||
--vscode-peekViewEditor-matchHighlightBackground: rgba(245, 216, 2, 87%);
|
||||
--vscode-editorMarkerNavigationError-background: #e51400;
|
||||
--vscode-editorMarkerNavigationError-headerBackground: rgba(229,
|
||||
20,
|
||||
0,
|
||||
10%);
|
||||
--vscode-editorMarkerNavigationWarning-background: #bf8803;
|
||||
--vscode-editorMarkerNavigationWarning-headerBackground: rgba(191,
|
||||
136,
|
||||
3,
|
||||
10%);
|
||||
--vscode-editorMarkerNavigationInfo-background: #1a85ff;
|
||||
--vscode-editorMarkerNavigationInfo-headerBackground: rgba(26,
|
||||
133,
|
||||
255,
|
||||
10%);
|
||||
--vscode-editorMarkerNavigation-background: #fffffe;
|
||||
--vscode-editorHoverWidget-highlightForeground: #0066bf;
|
||||
--vscode-editorSuggestWidget-background: #f3f3f3;
|
||||
--vscode-editorSuggestWidget-border: #c8c8c8;
|
||||
--vscode-editorSuggestWidget-foreground: #000;
|
||||
--vscode-editorSuggestWidget-selectedForeground: #fff;
|
||||
--vscode-editorSuggestWidget-selectedBackground: #0060c0;
|
||||
--vscode-editorSuggestWidget-highlightForeground: #0066bf;
|
||||
--vscode-editorSuggestWidget-focusHighlightForeground: #bbe7ff;
|
||||
--vscode-editorSuggestWidgetStatus-foreground: rgba(0, 0, 0, 50%);
|
||||
--vscode-editor-foldBackground: rgba(173, 214, 255, 30%);
|
||||
--vscode-editorGutter-foldingControlForeground: #424242;
|
||||
|
||||
background-color: #fff;
|
||||
|
||||
.bracket-highlighting-0 {
|
||||
color: #d5a00d; // { }
|
||||
}
|
||||
|
||||
.bracket-highlighting-1 {
|
||||
color: #8140e3; // [ ]
|
||||
}
|
||||
|
||||
.mtk1 {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.lines-content .core-guide-indent {
|
||||
box-shadow: 1px 0 0 0 transparent !important;
|
||||
}
|
||||
|
||||
.lines-content .core-guide-indent.indent-active {
|
||||
box-shadow: 1px 0 0 0 transparent !important;
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
color: #A7A7B0;
|
||||
}
|
||||
|
||||
.marker-widget {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
.find-widget {
|
||||
// 搜索框
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
|
||||
.monaco-sash {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.button:not(.disabled):hover,
|
||||
.monaco-editor .find-widget .codicon-find-selection:hover {
|
||||
background-color: #f8f8f8 !important;
|
||||
}
|
||||
|
||||
.monaco-inputbox {
|
||||
border: 1px solid #e4e3e4 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 SchemaNode {
|
||||
name: string;
|
||||
type: number;
|
||||
children?: SchemaNode[];
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
// modify from @byted/biz-ide-component
|
||||
export const convertSchemaService = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
object: any,
|
||||
maxDepth = 20,
|
||||
currentDepth = 1,
|
||||
): SchemaNode[] => {
|
||||
if (currentDepth > maxDepth) {
|
||||
return [];
|
||||
}
|
||||
const paramSchema: SchemaNode[] = [];
|
||||
Object.keys(object).forEach(key => {
|
||||
const value = object[key];
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 1 /* String */,
|
||||
});
|
||||
break;
|
||||
case 'number':
|
||||
if (Number.isInteger(value)) {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 2 /* Integer */,
|
||||
});
|
||||
} else {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 4 /* Number */,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 3 /* Boolean */,
|
||||
});
|
||||
break;
|
||||
case 'object':
|
||||
if (value === null) {
|
||||
break;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
switch (typeof value[0]) {
|
||||
case 'string':
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 99 /* ArrayString */,
|
||||
});
|
||||
break;
|
||||
case 'number':
|
||||
if (Number.isInteger(value[0])) {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 100 /* ArrayInteger */,
|
||||
});
|
||||
} else {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 102 /* ArrayNumber */,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 101 /* ArrayBoolean */,
|
||||
});
|
||||
break;
|
||||
case 'object':
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 103 /* ArrayObject */,
|
||||
children: convertSchemaService(
|
||||
value[0],
|
||||
maxDepth,
|
||||
currentDepth + 1,
|
||||
),
|
||||
});
|
||||
break;
|
||||
default:
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 99 /* ArrayString */,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 99 /* ArrayString */,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
paramSchema.push({
|
||||
name: key,
|
||||
defaultValue: JSON.stringify(value),
|
||||
type: 6 /* Object */,
|
||||
children: convertSchemaService(value, maxDepth, currentDepth + 1),
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('ContainsInvalidValue');
|
||||
}
|
||||
});
|
||||
return paramSchema;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const INDENT = 4;
|
||||
export const formatJson = (json: string) => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(json), null, INDENT);
|
||||
} catch (e) {
|
||||
return json;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
|
||||
import { merge, cloneDeep } from 'lodash-es';
|
||||
|
||||
import { type SchemaNode } from '../json-editor/service/convert-schema-service';
|
||||
import { JSONEditor } from '../json-editor';
|
||||
import type { TreeNodeCustomData } from '../../type';
|
||||
import {
|
||||
MAX_LEVEL,
|
||||
MAX_NAME_LENGTH,
|
||||
MAX_JSON_VARIABLE_COUNT,
|
||||
} from '../../constants';
|
||||
import { cutOffInvalidData } from './utils/cut-off';
|
||||
import { exportVariableService } from './services/use-case-service/export-variable-service';
|
||||
import { getEditorViewVariableJson } from './services/life-cycle-service/init-service';
|
||||
|
||||
interface JSONImportProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
treeData: TreeNodeCustomData;
|
||||
rules: {
|
||||
jsonImport: boolean;
|
||||
readonly: boolean;
|
||||
};
|
||||
onOk: (value: TreeNodeCustomData) => void;
|
||||
}
|
||||
|
||||
export const JSONImport: FC<JSONImportProps> = props => {
|
||||
const { treeData, rules, visible, onCancel, onOk } = props;
|
||||
const { jsonImport, readonly } = rules;
|
||||
const [jsonString, setJsonString] = useState('');
|
||||
|
||||
const handleImport = (data: SchemaNode[]) => {
|
||||
const allowDepth = MAX_LEVEL; // 最大深度限制
|
||||
const allowNameLength = MAX_NAME_LENGTH; // 名称长度限制
|
||||
const maxVariableCount = MAX_JSON_VARIABLE_COUNT; // 最大变量数量限制
|
||||
const variables = exportVariableService(
|
||||
data,
|
||||
{
|
||||
groupId: treeData.groupId,
|
||||
channel: treeData.channel,
|
||||
},
|
||||
treeData, // 传入原始变量以保持variableId
|
||||
);
|
||||
|
||||
// 裁切非法数据
|
||||
const dataCutoff = cutOffInvalidData({
|
||||
data: variables,
|
||||
allowDepth,
|
||||
allowNameLength,
|
||||
maxVariableCount,
|
||||
});
|
||||
|
||||
// 先深拷贝原始数据
|
||||
const clonedTreeData = cloneDeep(treeData);
|
||||
// 合并新旧数据
|
||||
const mergedData = merge(clonedTreeData, dataCutoff[0]);
|
||||
|
||||
// 更新数据
|
||||
return onOk(mergedData);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setJsonString(getEditorViewVariableJson(treeData));
|
||||
}, [treeData]);
|
||||
|
||||
if (!jsonImport) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<JSONEditor
|
||||
id={treeData.variableId}
|
||||
groupId={treeData.groupId}
|
||||
value={jsonString}
|
||||
readonly={readonly}
|
||||
setValue={(value: string) => {
|
||||
setJsonString(value);
|
||||
}}
|
||||
visible={visible}
|
||||
onOk={handleImport}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ViewVariableType } from '@/store/variable-groups/types';
|
||||
import { type TreeNodeCustomData } from '@/components/variable-tree/type';
|
||||
import { formatJson } from '@/components/variable-tree/components/json-editor/utils/format-json';
|
||||
|
||||
const getDefaultValueByType = (type: ViewVariableType) => {
|
||||
switch (type) {
|
||||
case ViewVariableType.String:
|
||||
return '';
|
||||
case ViewVariableType.Integer:
|
||||
case ViewVariableType.Number:
|
||||
return 0;
|
||||
case ViewVariableType.Boolean:
|
||||
return false;
|
||||
case ViewVariableType.Object:
|
||||
return {};
|
||||
case ViewVariableType.ArrayString:
|
||||
return [''];
|
||||
case ViewVariableType.ArrayInteger:
|
||||
return [0];
|
||||
case ViewVariableType.ArrayBoolean:
|
||||
return [true];
|
||||
case ViewVariableType.ArrayNumber:
|
||||
return [0];
|
||||
case ViewVariableType.ArrayObject:
|
||||
return [{}];
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const isArrayType = (type: ViewVariableType) =>
|
||||
[
|
||||
ViewVariableType.ArrayString,
|
||||
ViewVariableType.ArrayInteger,
|
||||
ViewVariableType.ArrayBoolean,
|
||||
ViewVariableType.ArrayNumber,
|
||||
ViewVariableType.ArrayObject,
|
||||
].includes(type);
|
||||
|
||||
export const getEditorViewVariableJson = (treeData: TreeNodeCustomData) => {
|
||||
const { defaultValue, type, name, children } = treeData;
|
||||
|
||||
if (defaultValue) {
|
||||
const json = JSON.parse(defaultValue);
|
||||
return formatJson(
|
||||
JSON.stringify({
|
||||
[name]: json,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 如果没有name,返回空对象
|
||||
if (!name) {
|
||||
return '{}';
|
||||
}
|
||||
|
||||
const isArray = isArrayType(type);
|
||||
|
||||
// 递归处理children
|
||||
const processChildren = (
|
||||
nodes?: TreeNodeCustomData[],
|
||||
parentType?: ViewVariableType,
|
||||
) => {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return getDefaultValueByType(parentType || type);
|
||||
}
|
||||
|
||||
if (isArray && !parentType) {
|
||||
const firstChild = nodes[0];
|
||||
if (!firstChild) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 如果是数组类型,根据第一个子元素的类型生成默认值
|
||||
const result = {};
|
||||
if (firstChild.children && firstChild.children.length > 0) {
|
||||
result[firstChild.name] = processChildren(
|
||||
firstChild.children,
|
||||
firstChild.type,
|
||||
);
|
||||
} else {
|
||||
result[firstChild.name] = getDefaultValueByType(firstChild.type);
|
||||
}
|
||||
return [result];
|
||||
}
|
||||
|
||||
return nodes.reduce(
|
||||
(acc, node) => {
|
||||
if (!node.name) {
|
||||
return acc;
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
const value = processChildren(node.children, node.type);
|
||||
acc[node.name] = isArrayType(node.type) ? [value] : value;
|
||||
} else {
|
||||
acc[node.name] = getDefaultValueByType(node.type);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} satisfies Record<string, unknown>,
|
||||
);
|
||||
};
|
||||
|
||||
// 生成最终的JSON结构
|
||||
const result = {
|
||||
[name]: processChildren(children),
|
||||
};
|
||||
|
||||
return formatJson(JSON.stringify(result));
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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 VariableChannel } from '@coze-arch/bot-api/memory';
|
||||
|
||||
import { type ViewVariableType } from '@/store/variable-groups/types';
|
||||
import { useVariableGroupsStore } from '@/store/variable-groups/store';
|
||||
import { type Variable } from '@/store';
|
||||
|
||||
import { type SchemaNode } from '../../../json-editor/service/convert-schema-service';
|
||||
|
||||
/**
|
||||
* 将转换后的数据转换为Variable
|
||||
* @param data 转换后的数据
|
||||
* @param baseInfo 基础信息
|
||||
* @param originalVariable 原始变量,用于保持variableId
|
||||
* @returns Variable[]
|
||||
*/
|
||||
export const exportVariableService = (
|
||||
data: SchemaNode[],
|
||||
baseInfo: {
|
||||
groupId: string;
|
||||
channel: VariableChannel;
|
||||
},
|
||||
originalVariable?: Variable,
|
||||
): Variable[] => {
|
||||
const store = useVariableGroupsStore.getState();
|
||||
|
||||
const convertNode = (
|
||||
node: SchemaNode,
|
||||
parentId = '',
|
||||
originalNode?: Variable,
|
||||
): Variable => {
|
||||
// 使用store中的createVariable方法创建基础变量
|
||||
const baseVariable = store.createVariable({
|
||||
variableType: node.type as ViewVariableType,
|
||||
groupId: baseInfo.groupId,
|
||||
parentId,
|
||||
channel: baseInfo.channel,
|
||||
});
|
||||
|
||||
// 如果存在原始节点,保持其variableId
|
||||
if (originalNode) {
|
||||
baseVariable.variableId = originalNode.variableId;
|
||||
baseVariable.description = originalNode.description;
|
||||
}
|
||||
|
||||
// 更新变量的基本信息
|
||||
baseVariable.name = node.name;
|
||||
baseVariable.defaultValue = node.defaultValue;
|
||||
|
||||
// 递归处理子节点,尝试匹配原始子节点
|
||||
if (node.children?.length) {
|
||||
baseVariable.children = node.children.map((child, index) => {
|
||||
const originalChild = originalNode?.children?.[index];
|
||||
return convertNode(child, baseVariable.variableId, originalChild);
|
||||
});
|
||||
}
|
||||
|
||||
return baseVariable;
|
||||
};
|
||||
|
||||
const variables = data.map(node => convertNode(node, '', originalVariable));
|
||||
|
||||
// 使用store中的updateMeta方法更新meta信息
|
||||
store.updateMeta({ variables });
|
||||
|
||||
return variables;
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Variable, ViewVariableType } from '@/store';
|
||||
|
||||
import {
|
||||
traverse,
|
||||
type TraverseContext,
|
||||
type TraverseHandler,
|
||||
} from './traverse';
|
||||
|
||||
const isOutputValueContext = (context: TraverseContext): boolean => {
|
||||
if (
|
||||
typeof context.node.value !== 'object' ||
|
||||
typeof context.node.value.type === 'undefined'
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const cutOffNameLength =
|
||||
(length: number): TraverseHandler =>
|
||||
(context: TraverseContext): void => {
|
||||
if (!isOutputValueContext(context)) {
|
||||
return;
|
||||
}
|
||||
if (context.node.value.name.length > length) {
|
||||
context.node.value.name = context.node.value.name.slice(0, length);
|
||||
}
|
||||
};
|
||||
|
||||
const cutOffDepth =
|
||||
(depth: number): TraverseHandler =>
|
||||
(context: TraverseContext): void => {
|
||||
if (
|
||||
!isOutputValueContext(context) ||
|
||||
context.node.value.level !== depth ||
|
||||
![ViewVariableType.Object, ViewVariableType.ArrayObject].includes(
|
||||
context.node.value.type,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
context.deleteSelf();
|
||||
};
|
||||
|
||||
export const cutOffInvalidData = (params: {
|
||||
data: Variable[];
|
||||
allowDepth: number;
|
||||
allowNameLength: number;
|
||||
maxVariableCount: number;
|
||||
}): Variable[] => {
|
||||
const { data, allowDepth, allowNameLength, maxVariableCount } = params;
|
||||
const cutOffVariableCountData = data.slice(0, maxVariableCount);
|
||||
return traverse<Variable[]>(cutOffVariableCountData, [
|
||||
cutOffNameLength(allowNameLength),
|
||||
cutOffDepth(allowDepth),
|
||||
]);
|
||||
};
|
||||
@@ -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 { nanoid } from 'nanoid';
|
||||
|
||||
import type { TreeNodeCustomData } from '../../../type';
|
||||
import { traverse, type TraverseContext } from './traverse';
|
||||
|
||||
/** 计算路径 */
|
||||
const getTreePath = (context: TraverseContext): string => {
|
||||
const parents = context
|
||||
.getParents()
|
||||
.filter(
|
||||
node =>
|
||||
typeof node.value === 'object' &&
|
||||
typeof node.value.name !== 'undefined' &&
|
||||
typeof node.value.type !== 'undefined',
|
||||
);
|
||||
return parents.map(node => node.value.name).join('/');
|
||||
};
|
||||
|
||||
/** 新旧数据保留 key 防止变量系统引用失效 */
|
||||
export const mergeData = (params: {
|
||||
newData: TreeNodeCustomData;
|
||||
oldData: TreeNodeCustomData;
|
||||
}): TreeNodeCustomData => {
|
||||
const { newData, oldData } = params;
|
||||
|
||||
// 计算旧数据中路径与key的映射
|
||||
const treeDataPathKeyMap = new Map<
|
||||
string,
|
||||
{
|
||||
key: string;
|
||||
}
|
||||
>();
|
||||
traverse(oldData, context => {
|
||||
if (
|
||||
typeof context.node.value !== 'object' ||
|
||||
typeof context.node.value.key === 'undefined' ||
|
||||
typeof context.node.value.type === 'undefined'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const stringifyPath = getTreePath(context);
|
||||
treeDataPathKeyMap.set(stringifyPath, {
|
||||
key: context.node.value.key,
|
||||
});
|
||||
});
|
||||
|
||||
// 新数据复用旧数据的key,失败则重新生成
|
||||
const newDataWithKey = traverse(newData, context => {
|
||||
if (
|
||||
typeof context.node.value !== 'object' ||
|
||||
typeof context.node.value.type === 'undefined'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const stringifyPath = getTreePath(context);
|
||||
const { key } = treeDataPathKeyMap.get(stringifyPath) || {
|
||||
key: nanoid(),
|
||||
};
|
||||
context.node.value.key = key;
|
||||
});
|
||||
|
||||
return newDataWithKey;
|
||||
};
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 @typescript-eslint/no-namespace */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type TraverseValue = any;
|
||||
export interface TraverseNode {
|
||||
value: TraverseValue;
|
||||
container?: TraverseValue;
|
||||
parent?: TraverseNode;
|
||||
key?: string;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export interface TraverseContext {
|
||||
node: TraverseNode;
|
||||
setValue: (value: TraverseValue) => void;
|
||||
getParents: () => TraverseNode[];
|
||||
getPath: () => Array<string | number>;
|
||||
getStringifyPath: () => string;
|
||||
deleteSelf: () => void;
|
||||
}
|
||||
|
||||
export type TraverseHandler = (context: TraverseContext) => void;
|
||||
|
||||
/**
|
||||
* 深度遍历对象,对每个值做处理
|
||||
* @param value 遍历对象
|
||||
* @param handle 处理函数
|
||||
*/
|
||||
export const traverse = <T extends TraverseValue = TraverseValue>(
|
||||
value: T,
|
||||
handler: TraverseHandler | TraverseHandler[],
|
||||
): T => {
|
||||
const traverseHandler: TraverseHandler = Array.isArray(handler)
|
||||
? (context: TraverseContext) => {
|
||||
handler.forEach(handlerFn => handlerFn(context));
|
||||
}
|
||||
: handler;
|
||||
TraverseUtils.traverseNodes({ value }, traverseHandler);
|
||||
return value;
|
||||
};
|
||||
|
||||
namespace TraverseUtils {
|
||||
/**
|
||||
* 深度遍历对象,对每个值做处理
|
||||
* @param node 遍历节点
|
||||
* @param handle 处理函数
|
||||
*/
|
||||
export const traverseNodes = (
|
||||
node: TraverseNode,
|
||||
handle: TraverseHandler,
|
||||
): void => {
|
||||
const { value } = node;
|
||||
if (!value) {
|
||||
// 异常处理
|
||||
return;
|
||||
}
|
||||
if (Object.prototype.toString.call(value) === '[object Object]') {
|
||||
// 对象,遍历对象的每个属性
|
||||
Object.entries(value).forEach(([key, item]) =>
|
||||
traverseNodes(
|
||||
{
|
||||
value: item,
|
||||
container: value,
|
||||
key,
|
||||
parent: node,
|
||||
},
|
||||
handle,
|
||||
),
|
||||
);
|
||||
} else if (Array.isArray(value)) {
|
||||
// 数组,遍历数组的每个元素
|
||||
// 从数组的末尾开始遍历,这样即使中途移除了某个元素,也不会影响到未处理的元素的索引
|
||||
for (let index = value.length - 1; index >= 0; index--) {
|
||||
const item: string = value[index];
|
||||
traverseNodes(
|
||||
{
|
||||
value: item,
|
||||
container: value,
|
||||
index,
|
||||
parent: node,
|
||||
},
|
||||
handle,
|
||||
);
|
||||
}
|
||||
}
|
||||
const context: TraverseContext = createContext({ node });
|
||||
handle(context);
|
||||
};
|
||||
|
||||
const createContext = ({
|
||||
node,
|
||||
}: {
|
||||
node: TraverseNode;
|
||||
}): TraverseContext => ({
|
||||
node,
|
||||
setValue: (value: unknown) => setValue(node, value),
|
||||
getParents: () => getParents(node),
|
||||
getPath: () => getPath(node),
|
||||
getStringifyPath: () => getStringifyPath(node),
|
||||
deleteSelf: () => deleteSelf(node),
|
||||
});
|
||||
|
||||
const setValue = (node: TraverseNode, value: unknown) => {
|
||||
// 设置值函数
|
||||
// 引用类型,需要借助父元素修改值
|
||||
// 由于是递归遍历,所以需要根据node来判断是给对象的哪个属性赋值,还是给数组的哪个元素赋值
|
||||
if (!value || !node) {
|
||||
return;
|
||||
}
|
||||
node.value = value;
|
||||
// 从上级作用域node中取出container,key,index
|
||||
const { container, key, index } = node;
|
||||
if (key && container) {
|
||||
container[key] = value;
|
||||
} else if (typeof index === 'number') {
|
||||
container[index] = value;
|
||||
}
|
||||
};
|
||||
|
||||
const getParents = (node: TraverseNode): TraverseNode[] => {
|
||||
const parents: TraverseNode[] = [];
|
||||
let currentNode: TraverseNode | undefined = node;
|
||||
while (currentNode) {
|
||||
parents.unshift(currentNode);
|
||||
currentNode = currentNode.parent;
|
||||
}
|
||||
return parents;
|
||||
};
|
||||
|
||||
const getPath = (node: TraverseNode): Array<string | number> => {
|
||||
const path: Array<string | number> = [];
|
||||
const parents = getParents(node);
|
||||
parents.forEach(parent => {
|
||||
if (parent.key) {
|
||||
path.unshift(parent.key);
|
||||
} else if (parent.index) {
|
||||
path.unshift(parent.index);
|
||||
}
|
||||
});
|
||||
return path;
|
||||
};
|
||||
|
||||
const getStringifyPath = (node: TraverseNode): string => {
|
||||
const path = getPath(node);
|
||||
return path.reduce((stringifyPath: string, pathItem: string | number) => {
|
||||
if (typeof pathItem === 'string') {
|
||||
const re = /\W/g;
|
||||
if (re.test(pathItem)) {
|
||||
// 包含特殊字符
|
||||
return `${stringifyPath}["${pathItem}"]`;
|
||||
}
|
||||
return `${stringifyPath}.${pathItem}`;
|
||||
} else {
|
||||
return `${stringifyPath}[${pathItem}]`;
|
||||
}
|
||||
}, '');
|
||||
};
|
||||
|
||||
const deleteSelf = (node: TraverseNode): void => {
|
||||
const { container, key, index } = node;
|
||||
if (key && container) {
|
||||
delete container[key];
|
||||
} else if (typeof index === 'number') {
|
||||
container.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
/** 每一级树缩进宽度 */
|
||||
export const TreeIndentWidth = 30;
|
||||
/** 树节点展开收起按钮宽度 */
|
||||
export const TreeCollapseWidth = 24;
|
||||
|
||||
// 名称最长50字符
|
||||
export const MAX_NAME_LENGTH = 50;
|
||||
|
||||
// 最大深度限制
|
||||
export const MAX_LEVEL = 3;
|
||||
// 最大变量数量限制
|
||||
export const MAX_JSON_VARIABLE_COUNT = 1;
|
||||
// 最大JSON长度限制30kb
|
||||
export const MAX_JSON_LENGTH = 30 * 1024;
|
||||
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import { useParams } from 'react-router-dom';
|
||||
import React, {
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import { type TreeProps } from '@coze-arch/bot-semi/Tree';
|
||||
import { type VariableChannel } from '@coze-arch/bot-api/memory';
|
||||
import { IconCozPlus } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Toast, Tree, useFormApi } from '@coze-arch/coze-design';
|
||||
|
||||
import { traverse } from '@/utils/traverse';
|
||||
import { useVariableGroupsStore, ViewVariableType } from '@/store';
|
||||
import { VariableTreeContext } from '@/context/variable-tree-context';
|
||||
|
||||
import { flatVariableTreeData } from './utils';
|
||||
import { type TreeNodeCustomData } from './type';
|
||||
import { ChangeMode } from './components/custom-tree-node/constants';
|
||||
import CustomTreeNode from './components/custom-tree-node';
|
||||
|
||||
export interface VariableTreeProps {
|
||||
groupId: string;
|
||||
value: Array<TreeNodeCustomData>;
|
||||
treeProps?: TreeProps;
|
||||
readonly?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
showAddButton?: boolean;
|
||||
/** 默认变量类型 */
|
||||
defaultVariableType?: ViewVariableType;
|
||||
defaultCollapse?: boolean;
|
||||
children?: React.ReactNode;
|
||||
maxLimit?: number;
|
||||
hideHeaderKeys?: string[];
|
||||
channel: VariableChannel;
|
||||
validateExistKeyword?: boolean;
|
||||
onChange?: (changeValue: TreeNodeCustomData) => void;
|
||||
}
|
||||
|
||||
export interface VariableTreeRef {
|
||||
validate: () => void;
|
||||
}
|
||||
|
||||
function useExpandedKeys(keys: string[], defaultCollapse: boolean) {
|
||||
const [expandedKeys, setExpandedKeys] = useState(defaultCollapse ? [] : keys);
|
||||
|
||||
const expandTreeNode = useCallback((key: string) => {
|
||||
setExpandedKeys(prev => [...new Set([...prev, key])]);
|
||||
}, []);
|
||||
|
||||
const collapseTreeNode = useCallback((key: string) => {
|
||||
setExpandedKeys(prev => prev.filter(expandedKey => expandedKey !== key));
|
||||
}, []);
|
||||
|
||||
return { expandedKeys, expandTreeNode, collapseTreeNode };
|
||||
}
|
||||
|
||||
export function Index(
|
||||
props: VariableTreeProps,
|
||||
ref: React.Ref<VariableTreeRef>,
|
||||
) {
|
||||
const {
|
||||
readonly = false,
|
||||
treeProps,
|
||||
className,
|
||||
style,
|
||||
value,
|
||||
defaultVariableType = ViewVariableType.String,
|
||||
defaultCollapse = false,
|
||||
maxLimit,
|
||||
groupId,
|
||||
channel,
|
||||
hideHeaderKeys,
|
||||
validateExistKeyword = false,
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
createVariable,
|
||||
addRootVariable,
|
||||
addChildVariable,
|
||||
updateVariable,
|
||||
deleteVariable,
|
||||
findAndModifyVariable,
|
||||
} = useVariableGroupsStore(
|
||||
useShallow(state => ({
|
||||
createVariable: state.createVariable,
|
||||
addRootVariable: state.addRootVariable,
|
||||
addChildVariable: state.addChildVariable,
|
||||
updateVariable: state.updateVariable,
|
||||
deleteVariable: state.deleteVariable,
|
||||
findAndModifyVariable: state.findAndModifyVariable,
|
||||
})),
|
||||
);
|
||||
|
||||
const formApi = useFormApi();
|
||||
|
||||
const isValueEmpty = !value || value.length === 0;
|
||||
|
||||
const itemKeysWithChildren = useMemo(() => {
|
||||
const keys: string[] = [];
|
||||
traverse(value, item => {
|
||||
if (item.children?.length > 0) {
|
||||
keys.push(item.variableId);
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}, [value]);
|
||||
|
||||
const flatTreeData = useMemo(() => flatVariableTreeData(value), [value]);
|
||||
|
||||
const { expandedKeys, expandTreeNode, collapseTreeNode } = useExpandedKeys(
|
||||
itemKeysWithChildren,
|
||||
defaultCollapse,
|
||||
);
|
||||
const params = useParams<DynamicParams>();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
validate: () => formApi.validate(),
|
||||
}));
|
||||
|
||||
const disableAdd = useMemo(() => {
|
||||
if (maxLimit === undefined) {
|
||||
return false;
|
||||
}
|
||||
return (value?.length ?? 0) >= maxLimit;
|
||||
}, [value, maxLimit]);
|
||||
|
||||
const showAddButton = !readonly && !disableAdd;
|
||||
|
||||
const onAdd = () => {
|
||||
const newVariable = createVariable({
|
||||
groupId,
|
||||
parentId: '',
|
||||
variableType: defaultVariableType,
|
||||
channel,
|
||||
});
|
||||
|
||||
addRootVariable(newVariable);
|
||||
onChange?.(newVariable);
|
||||
sendTeaEvent(EVENT_NAMES.memory_click_front, {
|
||||
project_id: params?.project_id || '',
|
||||
resource_type: 'variable',
|
||||
action: 'add',
|
||||
source: 'app_detail_page',
|
||||
source_detail: 'memory_manage',
|
||||
});
|
||||
};
|
||||
|
||||
// 树节点的 change 方法
|
||||
const onTreeNodeChange = (mode: ChangeMode, param: TreeNodeCustomData) => {
|
||||
const findResult = findAndModifyVariable(
|
||||
groupId,
|
||||
item => item.variableId === param.variableId,
|
||||
);
|
||||
if (!findResult) {
|
||||
Toast.error(I18n.t('workflow_detail_node_output_parsingfailed'));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case ChangeMode.Append: {
|
||||
const { variableId: parentId, channel: parentChannel } = findResult;
|
||||
const childVariable = createVariable({
|
||||
groupId,
|
||||
parentId,
|
||||
variableType: defaultVariableType,
|
||||
channel: parentChannel,
|
||||
});
|
||||
addChildVariable(childVariable);
|
||||
|
||||
// 当前节点下新增节点 展开当前节点
|
||||
if (findResult?.variableId) {
|
||||
expandTreeNode(findResult.variableId);
|
||||
}
|
||||
sendTeaEvent(EVENT_NAMES.memory_click_front, {
|
||||
project_id: params?.project_id || '',
|
||||
resource_type: 'variable',
|
||||
action: 'add',
|
||||
source: 'app_detail_page',
|
||||
source_detail: 'memory_manage',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ChangeMode.Update: {
|
||||
updateVariable(param);
|
||||
sendTeaEvent(EVENT_NAMES.memory_click_front, {
|
||||
project_id: params?.project_id || '',
|
||||
resource_type: 'variable',
|
||||
action: 'edit',
|
||||
source: 'app_detail_page',
|
||||
source_detail: 'memory_manage',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ChangeMode.Delete: {
|
||||
deleteVariable(param);
|
||||
sendTeaEvent(EVENT_NAMES.memory_click_front, {
|
||||
project_id: params?.project_id || '',
|
||||
resource_type: 'variable',
|
||||
action: 'delete',
|
||||
source: 'app_detail_page',
|
||||
source_detail: 'memory_manage',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ChangeMode.UpdateEnabled: {
|
||||
findResult.enabled = param.enabled;
|
||||
// 一键关闭所有子节点
|
||||
traverse<TreeNodeCustomData>(findResult, node => {
|
||||
if (!param.enabled) {
|
||||
node.enabled = param.enabled;
|
||||
}
|
||||
});
|
||||
// 子点开启,父节点也开启
|
||||
if (findResult.parentId && findResult.enabled) {
|
||||
const parentData = findAndModifyVariable(
|
||||
groupId,
|
||||
item => item.variableId === findResult.parentId,
|
||||
);
|
||||
if (parentData) {
|
||||
parentData.enabled = findResult.enabled;
|
||||
updateVariable(parentData);
|
||||
}
|
||||
}
|
||||
updateVariable(findResult);
|
||||
sendTeaEvent(EVENT_NAMES.memory_click_front, {
|
||||
project_id: params?.project_id || '',
|
||||
resource_type: 'variable',
|
||||
action: param.enabled ? 'turn_on' : 'turn_off',
|
||||
source: 'app_detail_page',
|
||||
source_detail: 'memory_manage',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ChangeMode.Replace: {
|
||||
updateVariable(param);
|
||||
expandTreeNode(findResult.variableId);
|
||||
sendTeaEvent(EVENT_NAMES.memory_click_front, {
|
||||
project_id: params?.project_id || '',
|
||||
resource_type: 'variable',
|
||||
action: 'edit',
|
||||
source: 'app_detail_page',
|
||||
source_detail: 'memory_manage',
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
onChange?.(param);
|
||||
};
|
||||
|
||||
if (readonly && isValueEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VariableTreeContext.Provider value={{ groupId, variables: flatTreeData }}>
|
||||
<div
|
||||
className={classNames(
|
||||
// 基础容器样式
|
||||
'relative h-full',
|
||||
// 交互状态
|
||||
!readonly && 'cursor-default',
|
||||
// 自定义类名
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<Tree
|
||||
style={readonly ? {} : { overflow: 'inherit' }}
|
||||
motion={false}
|
||||
keyMaps={{
|
||||
key: 'variableId',
|
||||
}}
|
||||
disabled={readonly}
|
||||
className={classNames(
|
||||
// 基础滚动行为
|
||||
'overflow-x-auto',
|
||||
|
||||
// Tree 列表基础样式
|
||||
[
|
||||
// 列表容器样式
|
||||
'[&_.semi-tree-option-list]:overflow-visible',
|
||||
'[&_.semi-tree-option-list]:p-0',
|
||||
'[&_.semi-tree-option-list>div:first-child]:mt-0',
|
||||
// 选项样式
|
||||
'[&_.semi-tree-option]:!pl-2',
|
||||
].join(' '),
|
||||
|
||||
// 交互状态样式
|
||||
readonly
|
||||
? '[&_.semi-tree-option-list-block_.semi-tree-option:hover]:bg-inherit'
|
||||
: [
|
||||
'[&_.semi-tree-option-list-block_.semi-tree-option:hover]:bg-transparent',
|
||||
'[&_.semi-tree-option-list-block_.semi-tree-option:active]:bg-transparent',
|
||||
].join(' '),
|
||||
)}
|
||||
renderFullLabel={renderFullLabelProps => {
|
||||
const { data } = renderFullLabelProps;
|
||||
const currentLevelReadOnly = readonly || data.IsReadOnly;
|
||||
|
||||
const onCollapse = (collapsed: boolean) => {
|
||||
const { variableId } = renderFullLabelProps.data;
|
||||
|
||||
if (!variableId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (collapsed) {
|
||||
expandTreeNode(variableId);
|
||||
} else {
|
||||
collapseTreeNode(variableId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CustomTreeNode
|
||||
{...renderFullLabelProps}
|
||||
hideHeaderKeys={hideHeaderKeys}
|
||||
validateExistKeyword={validateExistKeyword}
|
||||
onChange={onTreeNodeChange}
|
||||
hasObjectLike={data.meta.hasObjectLike}
|
||||
readonly={currentLevelReadOnly}
|
||||
couldCollapse={(data.children?.length ?? 0) > 0}
|
||||
collapsed={renderFullLabelProps.expandStatus.expanded}
|
||||
onCollapse={onCollapse}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
emptyContent={<></>}
|
||||
expandedKeys={[...expandedKeys, nanoid()]}
|
||||
treeData={value}
|
||||
{...treeProps}
|
||||
/>
|
||||
{showAddButton ? (
|
||||
<div className="flex items-center my-3">
|
||||
<IconButton icon={<IconCozPlus />} onClick={onAdd}>
|
||||
{I18n.t('workflow_detail_node_output_add_subitem')}
|
||||
</IconButton>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</VariableTreeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// 导出可调用ref方法的组件
|
||||
export const VariableTree = React.forwardRef(Index);
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CSSProperties } from 'react';
|
||||
|
||||
import { type Variable, type ViewVariableType } from '@/store';
|
||||
|
||||
import { type ChangeMode } from './components/custom-tree-node/constants';
|
||||
|
||||
export interface RecursedParamDefinition {
|
||||
name?: string;
|
||||
/** Tree 组件要求每一个节点都有 key,而 key 不适合用名称(前后缀)等任何方式赋值,最终确定由接口转换层一次性提供随机 key */
|
||||
fieldRandomKey?: string;
|
||||
desc?: string;
|
||||
type: ViewVariableType;
|
||||
children?: RecursedParamDefinition[];
|
||||
}
|
||||
|
||||
export type TreeNodeCustomData = Variable;
|
||||
|
||||
export interface CustomTreeNodeFuncRef {
|
||||
data: TreeNodeCustomData;
|
||||
level: number;
|
||||
readonly: boolean;
|
||||
// 通用change方法
|
||||
onChange: (mode: ChangeMode, param: TreeNodeCustomData) => void;
|
||||
// 定制的类型改变的change方法,主要用于自定义render使用
|
||||
// 添加子项
|
||||
onAppend: () => void;
|
||||
// 删除该项
|
||||
onDelete: () => void;
|
||||
// 类型改变时内部的调用方法,主要用于从类Object类型转为其他类型时需要删除所有子项
|
||||
onSelectChange: (
|
||||
val?: string | number | Array<unknown> | Record<string, unknown>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type WithCustomStyle<T = object> = {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
} & T;
|
||||
@@ -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 { traverse } from '@/utils/traverse';
|
||||
import { type Variable, type VariableGroup } from '@/store';
|
||||
|
||||
import { type TreeNodeCustomData } from './type';
|
||||
|
||||
interface RootFindResult {
|
||||
isRoot: true;
|
||||
data: TreeNodeCustomData;
|
||||
parentData: null;
|
||||
}
|
||||
interface ChildrenFindResult {
|
||||
isRoot: false;
|
||||
parentData: TreeNodeCustomData;
|
||||
data: TreeNodeCustomData;
|
||||
}
|
||||
|
||||
export type FindDataResult = RootFindResult | ChildrenFindResult | null;
|
||||
/**
|
||||
* 根据target数组,找到key在该项的值和位置,主要是获取位置,方便操作parent的children
|
||||
*/
|
||||
export function findCustomTreeNodeDataResult(
|
||||
target: Array<TreeNodeCustomData>,
|
||||
variableId: string,
|
||||
): FindDataResult {
|
||||
const dataInRoot = target.find(item => item.variableId === variableId);
|
||||
if (dataInRoot) {
|
||||
// 如果是根节点
|
||||
return {
|
||||
isRoot: true,
|
||||
parentData: null,
|
||||
data: dataInRoot,
|
||||
};
|
||||
}
|
||||
function findDataInChildrenLoop(
|
||||
customChildren: Array<TreeNodeCustomData>,
|
||||
parentData?: TreeNodeCustomData,
|
||||
): FindDataResult {
|
||||
function findDataLoop(
|
||||
customData: TreeNodeCustomData,
|
||||
_parentData: TreeNodeCustomData,
|
||||
): FindDataResult {
|
||||
if (customData.variableId === variableId) {
|
||||
return {
|
||||
isRoot: false,
|
||||
parentData: _parentData,
|
||||
data: customData,
|
||||
};
|
||||
}
|
||||
if (customData.children && customData.children.length > 0) {
|
||||
return findDataInChildrenLoop(
|
||||
customData.children as Array<TreeNodeCustomData>,
|
||||
customData,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
for (const child of customChildren) {
|
||||
const childResult = findDataLoop(child, parentData || child);
|
||||
if (childResult) {
|
||||
return childResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return findDataInChildrenLoop(target);
|
||||
}
|
||||
|
||||
// 将groupVariableMeta打平为viewVariableTreeNode[]
|
||||
export function flatGroupVariableMeta(
|
||||
groupVariableMeta: VariableGroup[],
|
||||
maxDepth = Infinity,
|
||||
) {
|
||||
const res: Variable[] = [];
|
||||
traverse(
|
||||
groupVariableMeta,
|
||||
item => {
|
||||
res.push(...item.varInfoList);
|
||||
},
|
||||
'subGroupList',
|
||||
maxDepth,
|
||||
);
|
||||
return res;
|
||||
}
|
||||
export const flatVariableTreeData = (treeData: Variable[]) => {
|
||||
const res: Variable[] = [];
|
||||
traverse(
|
||||
treeData,
|
||||
item => {
|
||||
res.push(item);
|
||||
},
|
||||
'children',
|
||||
);
|
||||
return res;
|
||||
};
|
||||
31
frontend/packages/data/memory/variables/src/context/index.ts
Normal file
31
frontend/packages/data/memory/variables/src/context/index.ts
Normal 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 { createContext, useContext } from 'react';
|
||||
|
||||
import { type VariableGroup } from '../store';
|
||||
|
||||
interface VariableContextType {
|
||||
variablePageCanEdit?: boolean;
|
||||
groups: VariableGroup[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const VariableContext = createContext<VariableContextType>({
|
||||
groups: [],
|
||||
});
|
||||
|
||||
export const useVariableContext = () => useContext(VariableContext);
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import { type Variable } from '../store';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const VariableTreeContext = createContext<{
|
||||
groupId: string;
|
||||
variables: Variable[];
|
||||
}>({
|
||||
groupId: '',
|
||||
variables: [],
|
||||
});
|
||||
|
||||
export const useVariableTreeContext = () => useContext(VariableTreeContext);
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useVariableGroupsStore } from '../../store';
|
||||
|
||||
export const useDestory = () => {
|
||||
const { clear } = useVariableGroupsStore();
|
||||
useEffect(
|
||||
() => () => {
|
||||
clear();
|
||||
},
|
||||
[clear],
|
||||
);
|
||||
return {
|
||||
clear,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { type project_memory as ProjectMemory } from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { useVariableGroupsStore } from '../../store';
|
||||
|
||||
export const useInit = (projectID?: string, version?: string) => {
|
||||
const { data: reqData, loading } = useGetVariableList(projectID, version);
|
||||
const { initStore } = useVariableGroupsStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { variableGroups, canEdit } = reqData;
|
||||
|
||||
initStore({
|
||||
variableGroups,
|
||||
canEdit: canEdit && !version,
|
||||
});
|
||||
}, [loading]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
const useGetVariableList = (
|
||||
projectID?: string,
|
||||
version?: string,
|
||||
): {
|
||||
data: {
|
||||
variableGroups: ProjectMemory.GroupVariableInfo[];
|
||||
canEdit: boolean;
|
||||
};
|
||||
loading: boolean;
|
||||
error: string;
|
||||
} => {
|
||||
const {
|
||||
data: reqData,
|
||||
loading,
|
||||
error,
|
||||
} = useRequest(
|
||||
async () => {
|
||||
if (!projectID) {
|
||||
throw new CustomError(
|
||||
'useListDataSetReq_error',
|
||||
'projectID cannot be empty',
|
||||
);
|
||||
}
|
||||
const res = await MemoryApi.GetProjectVariableList({
|
||||
ProjectID: projectID,
|
||||
version: version || undefined,
|
||||
});
|
||||
|
||||
const { GroupConf, code, CanEdit: canEdit, msg } = res;
|
||||
|
||||
if (code !== 0) {
|
||||
return {
|
||||
error: msg,
|
||||
data: {
|
||||
variableGroups: [],
|
||||
canEdit: false,
|
||||
},
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (!GroupConf) {
|
||||
return {
|
||||
data: {
|
||||
variableGroups: [],
|
||||
canEdit,
|
||||
},
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
variableGroups: GroupConf,
|
||||
canEdit,
|
||||
};
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
onError: () => {
|
||||
Toast.error({
|
||||
content: I18n.t('Network_error'),
|
||||
showClose: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
return {
|
||||
data: {
|
||||
variableGroups: reqData?.variableGroups ?? [],
|
||||
canEdit: reqData?.canEdit ?? false,
|
||||
},
|
||||
loading,
|
||||
error: error?.message ?? '',
|
||||
};
|
||||
};
|
||||
@@ -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 { useState } from 'react';
|
||||
|
||||
import { useHiddenSession } from '@/hooks/use-case/use-hidden-session';
|
||||
|
||||
export const useChangeWarning = () => {
|
||||
const [isShowBanner, setIsShowBanner] = useState(false);
|
||||
const { isSessionHidden, hideSession } = useHiddenSession(
|
||||
'variable_config_change_banner_remind',
|
||||
);
|
||||
|
||||
const showBanner = () => {
|
||||
setIsShowBanner(true);
|
||||
};
|
||||
|
||||
const hideBanner = () => {
|
||||
setIsShowBanner(false);
|
||||
};
|
||||
|
||||
const hideBannerForever = () => {
|
||||
hideSession();
|
||||
setIsShowBanner(false);
|
||||
};
|
||||
|
||||
return {
|
||||
isShowBanner: isShowBanner && !isSessionHidden,
|
||||
showBanner,
|
||||
hideBanner,
|
||||
hideBannerForever,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { localStorageService } from '@coze-foundation/local-storage';
|
||||
|
||||
const SESSION_HIDDEN_KEY = 'coze-home-session-area-hidden-key';
|
||||
|
||||
export const useHiddenSession = (key: string) => {
|
||||
const [isSessionHidden, setIsSessionHidden] = useState(isKeyExist(key));
|
||||
return {
|
||||
isSessionHidden,
|
||||
hideSession: () => {
|
||||
if (isKeyExist(key)) {
|
||||
return;
|
||||
}
|
||||
const oldValue = localStorageService.getValue(SESSION_HIDDEN_KEY) || '';
|
||||
localStorageService.setValue(
|
||||
SESSION_HIDDEN_KEY,
|
||||
oldValue ? `${oldValue},${key}` : key,
|
||||
);
|
||||
setIsSessionHidden(true);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const isKeyExist = (key: string) => {
|
||||
const oldValue = localStorageService.getValue(SESSION_HIDDEN_KEY);
|
||||
return oldValue?.includes(key);
|
||||
};
|
||||
@@ -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 { useLocation } from 'react-router-dom';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useDataNavigate } from '@coze-data/knowledge-stores';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button, Toast } from '@coze-arch/coze-design';
|
||||
|
||||
export const useLeaveWarning = () => {
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
const location = useLocation();
|
||||
const prevPathRef = useRef(location.pathname);
|
||||
const resourceNavigate = useDataNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const currentPath = location.pathname;
|
||||
const wasInVariablePage = prevPathRef.current.includes('/variables');
|
||||
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (hasUnsavedChanges) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
wasInVariablePage &&
|
||||
!currentPath.includes('/variables') &&
|
||||
hasUnsavedChanges
|
||||
) {
|
||||
Toast.warning({
|
||||
content: (
|
||||
<div>
|
||||
<span className="text-sm font-medium coz-fg-plus mr-2">
|
||||
{I18n.t('variable_config_toast_savetips')}
|
||||
</span>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
resourceNavigate.navigateTo?.('/variables');
|
||||
}}
|
||||
>
|
||||
{I18n.t('variable_config_toast_return_button')}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (currentPath.includes('/variables') && hasUnsavedChanges) {
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
}
|
||||
|
||||
prevPathRef.current = currentPath;
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [location, hasUnsavedChanges]);
|
||||
|
||||
return {
|
||||
hasUnsavedChanges,
|
||||
setHasUnsavedChanges,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
padding-bottom: 16px; // 给底部留出间距
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex-shrink: 0;
|
||||
|
||||
padding: 16px 24px;
|
||||
|
||||
background: #fff;
|
||||
border-top: 1px solid var(--semi-color-border);
|
||||
|
||||
// 如果需要阴影效果
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 6%);
|
||||
}
|
||||
}
|
||||
56
frontend/packages/data/memory/variables/src/index.tsx
Normal file
56
frontend/packages/data/memory/variables/src/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useKnowledgeParams } from '@coze-data/knowledge-stores';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { TabBar, TabPane } from '@coze-arch/coze-design';
|
||||
|
||||
import { VariablesValue } from './variables-value';
|
||||
import { VariablesConfig } from './variables-config';
|
||||
|
||||
export const VariablesPage = () => {
|
||||
const params = useKnowledgeParams();
|
||||
const { projectID = '', version } = params;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'h-full w-full overflow-hidden',
|
||||
'border border-solid coz-stroke-primary coz-bg-max',
|
||||
)}
|
||||
>
|
||||
<TabBar
|
||||
lazyRender
|
||||
type="text"
|
||||
className={classNames(
|
||||
'h-full flex flex-col',
|
||||
// 滚动条位置调整到 tab 内容中
|
||||
'[&_.semi-tabs-content]:p-0 [&_.semi-tabs-content]:grow [&_.semi-tabs-content]:overflow-hidden',
|
||||
'[&_.semi-tabs-pane-active]:h-full',
|
||||
'[&_.semi-tabs-pane-motion-overlay]:h-full [&_.semi-tabs-pane-motion-overlay]:overflow-auto',
|
||||
)}
|
||||
tabBarClassName="flex items-center h-[56px] mx-[16px]"
|
||||
>
|
||||
<TabPane tab={I18n.t('db_optimize_033')} itemKey="config">
|
||||
<VariablesConfig projectID={projectID} version={version} />
|
||||
</TabPane>
|
||||
<TabPane tab={I18n.t('variable_Tabname_test_data')} itemKey="values">
|
||||
<VariablesValue projectID={projectID} version={version} />
|
||||
</TabPane>
|
||||
</TabBar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { I18n } from '@coze-arch/i18n';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { useVariableGroupsStore } from '../../store';
|
||||
/**
|
||||
* 提交变量
|
||||
* @param projectID
|
||||
* @returns
|
||||
*/
|
||||
export async function submit(projectID: string) {
|
||||
const { getAllRootVariables, getDtoVariable } =
|
||||
useVariableGroupsStore.getState();
|
||||
const res = await MemoryApi.UpdateProjectVariable({
|
||||
ProjectID: projectID,
|
||||
VariableList: getAllRootVariables().map(item => getDtoVariable(item)),
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
Toast.success(I18n.t('Update_success'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并确保 projectID 是非空字符串
|
||||
* @param projectID 可能为空的项目ID
|
||||
* @returns projectID 是否为非空字符串
|
||||
*/
|
||||
export const checkProjectID = (projectID: unknown): projectID is string =>
|
||||
typeof projectID === 'string' && projectID.length > 0;
|
||||
25
frontend/packages/data/memory/variables/src/store/index.ts
Normal file
25
frontend/packages/data/memory/variables/src/store/index.ts
Normal 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 {
|
||||
useVariableGroupsStore,
|
||||
type VariableGroup,
|
||||
type Variable,
|
||||
type VariableMeta,
|
||||
VariableTypeDTO,
|
||||
VariableSchemaDTO,
|
||||
ViewVariableType,
|
||||
} from './variable-groups';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 {
|
||||
useVariableGroupsStore,
|
||||
type VariableGroup,
|
||||
type Variable,
|
||||
type VariableMeta,
|
||||
} from './store';
|
||||
export { ViewVariableType, VariableTypeDTO, VariableSchemaDTO } from './types';
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import { devtools, subscribeWithSelector } from 'zustand/middleware';
|
||||
import { create } from 'zustand';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { produce } from 'immer';
|
||||
import {
|
||||
type project_memory as ProjectMemory,
|
||||
type VariableChannel,
|
||||
VariableType,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
|
||||
import { traverse } from '../../utils/traverse';
|
||||
import { type ViewVariableType, ObjectLikeTypes } from './types';
|
||||
import { getDtoVariable } from './transform/vo2dto';
|
||||
import { getGroupListByDto } from './transform/dto2vo';
|
||||
|
||||
export interface Variable {
|
||||
variableId: string;
|
||||
type: ViewVariableType;
|
||||
name: string;
|
||||
children: Variable[];
|
||||
defaultValue: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
channel: VariableChannel;
|
||||
effectiveChannelList: string[];
|
||||
variableType: VariableType;
|
||||
readonly: boolean;
|
||||
groupId: string;
|
||||
parentId: string;
|
||||
meta: VariableMeta;
|
||||
}
|
||||
|
||||
export interface VariableMeta {
|
||||
isHistory: boolean;
|
||||
level?: number;
|
||||
hasObjectLike?: boolean;
|
||||
field?: string;
|
||||
}
|
||||
|
||||
export interface VariableGroup {
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
groupDesc: string;
|
||||
groupExtDesc: string;
|
||||
isReadOnly: boolean;
|
||||
channel: VariableChannel;
|
||||
subGroupList: VariableGroup[];
|
||||
varInfoList: Variable[];
|
||||
raw: ProjectMemory.GroupVariableInfo;
|
||||
}
|
||||
|
||||
export interface VariableGroupsStore {
|
||||
variableGroups: VariableGroup[];
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
export const getDefaultVariableGroupStore = (): VariableGroupsStore => ({
|
||||
canEdit: false,
|
||||
variableGroups: [],
|
||||
});
|
||||
|
||||
export interface VariableGroupsAction {
|
||||
setVariableGroups: (variableGroups: VariableGroup[]) => void;
|
||||
createVariable: (variableInfo: {
|
||||
variableType: ViewVariableType;
|
||||
groupId: string;
|
||||
parentId: string;
|
||||
channel: VariableChannel;
|
||||
}) => Variable;
|
||||
// 更新变量, 根据groupId和variableId更新
|
||||
updateVariable: (newVariable: Variable) => void;
|
||||
// 更新变量的meta信息
|
||||
updateMeta: (params: {
|
||||
variables: Variable[];
|
||||
level?: number;
|
||||
parentId?: string;
|
||||
}) => void;
|
||||
// 新增根节点变量
|
||||
addRootVariable: (variable: Omit<Variable, 'channel'>) => void;
|
||||
// 新增子节点变量
|
||||
addChildVariable: (variable: Variable) => void;
|
||||
// 删除变量
|
||||
deleteVariable: (variable: Variable) => void;
|
||||
// 保存后作为历史变量对待
|
||||
saveHistory: () => void;
|
||||
// 获取DTO variable
|
||||
getDtoVariable: (variable: Variable) => ProjectMemory.Variable;
|
||||
// 获取groups下所有的变量
|
||||
getAllRootVariables: () => Variable[];
|
||||
// 获取groups下所有的变量
|
||||
getAllVariables: () => Variable[];
|
||||
transformDto2Vo: (data: ProjectMemory.GroupVariableInfo[]) => VariableGroup[];
|
||||
initStore: (data: {
|
||||
variableGroups: ProjectMemory.GroupVariableInfo[];
|
||||
canEdit: boolean;
|
||||
}) => void;
|
||||
clear: () => void;
|
||||
// 在变量树中查找变量,并可选地修改或删除
|
||||
findAndModifyVariable: (
|
||||
groupId: string,
|
||||
predicate: (variable: Variable) => boolean,
|
||||
options?: {
|
||||
modifyVariable?: (variable: Variable) => void;
|
||||
removeVariable?: boolean;
|
||||
mark?: string;
|
||||
},
|
||||
) => Variable | null;
|
||||
}
|
||||
|
||||
export const useVariableGroupsStore = create<
|
||||
VariableGroupsStore & VariableGroupsAction
|
||||
>()(
|
||||
devtools(
|
||||
subscribeWithSelector((set, get) => ({
|
||||
...getDefaultVariableGroupStore(),
|
||||
setVariableGroups: variableGroups =>
|
||||
set({ variableGroups }, false, 'setVariableGroups'),
|
||||
createVariable: baseInfo => ({
|
||||
variableId: nanoid(),
|
||||
type: baseInfo.variableType,
|
||||
name: '',
|
||||
enabled: true,
|
||||
children: [],
|
||||
defaultValue: '',
|
||||
description: '',
|
||||
channel: baseInfo.channel,
|
||||
effectiveChannelList: [],
|
||||
variableType: VariableType.KVVariable,
|
||||
readonly: false,
|
||||
groupId: baseInfo.groupId,
|
||||
parentId: baseInfo.parentId,
|
||||
meta: {
|
||||
isHistory: false,
|
||||
},
|
||||
}),
|
||||
addRootVariable: variable => {
|
||||
set(
|
||||
produce<VariableGroupsStore>(state => {
|
||||
const findGroup = state.variableGroups.find(
|
||||
item => item.groupId === variable.groupId,
|
||||
);
|
||||
if (!findGroup) {
|
||||
return;
|
||||
}
|
||||
findGroup.varInfoList.push({
|
||||
...variable,
|
||||
channel: findGroup.channel,
|
||||
});
|
||||
get().updateMeta({
|
||||
variables: findGroup.varInfoList,
|
||||
level: 0,
|
||||
parentId: '',
|
||||
});
|
||||
}),
|
||||
false,
|
||||
'addRootVariable',
|
||||
);
|
||||
},
|
||||
addChildVariable: variable => {
|
||||
get().findAndModifyVariable(
|
||||
variable.groupId,
|
||||
item => item.variableId === variable.parentId,
|
||||
{
|
||||
modifyVariable: parentNode => {
|
||||
parentNode.children.push(variable);
|
||||
get().updateMeta({
|
||||
variables: parentNode.children,
|
||||
level: (parentNode.meta.level || 0) + 1,
|
||||
parentId: parentNode.variableId,
|
||||
});
|
||||
},
|
||||
mark: 'addChildVariable',
|
||||
},
|
||||
);
|
||||
},
|
||||
deleteVariable: variable => {
|
||||
get().findAndModifyVariable(
|
||||
variable.groupId,
|
||||
item => item.variableId === variable.variableId,
|
||||
{ removeVariable: true, mark: 'deleteVariable' },
|
||||
);
|
||||
},
|
||||
findAndModifyVariable: (groupId, predicate, options) => {
|
||||
let foundVariable: Variable | null = null;
|
||||
|
||||
set(
|
||||
produce<VariableGroupsStore>(state => {
|
||||
const findInGroups = (groups: VariableGroup[]): boolean => {
|
||||
for (const group of groups) {
|
||||
if (group.groupId === groupId) {
|
||||
if (findInTree(group.varInfoList, predicate, options)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (group.subGroupList?.length) {
|
||||
if (findInGroups(group.subGroupList)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const findInTree = (
|
||||
variables: Variable[],
|
||||
predicateIn: (variable: Variable) => boolean,
|
||||
optionsIn?: {
|
||||
modifyVariable?: (variable: Variable) => void;
|
||||
removeVariable?: boolean;
|
||||
},
|
||||
): boolean => {
|
||||
for (let i = 0; i < variables.length; i++) {
|
||||
if (predicateIn(variables[i])) {
|
||||
foundVariable = cloneDeep(variables[i]);
|
||||
if (optionsIn?.removeVariable) {
|
||||
variables.splice(i, 1);
|
||||
}
|
||||
if (optionsIn?.modifyVariable) {
|
||||
optionsIn.modifyVariable(variables[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (variables[i].children?.length) {
|
||||
if (
|
||||
findInTree(variables[i].children, predicateIn, optionsIn)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
findInGroups(state.variableGroups);
|
||||
}),
|
||||
false,
|
||||
options?.mark || 'findVariableInTree',
|
||||
);
|
||||
|
||||
return foundVariable;
|
||||
},
|
||||
updateVariable: newVariable => {
|
||||
get().findAndModifyVariable(
|
||||
newVariable.groupId,
|
||||
variable => variable.variableId === newVariable.variableId,
|
||||
{
|
||||
mark: 'updateVariable',
|
||||
modifyVariable: variable => {
|
||||
Object.assign(variable, newVariable);
|
||||
get().updateMeta({
|
||||
variables: [variable],
|
||||
level: variable.meta.level,
|
||||
parentId: variable.parentId,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
updateMeta: ({ variables, level = 0, parentId = '' }) => {
|
||||
variables.forEach(item => {
|
||||
item.meta.level = level;
|
||||
item.meta.hasObjectLike = ObjectLikeTypes.includes(item.type);
|
||||
item.parentId = parentId;
|
||||
if (item.children?.length) {
|
||||
get().updateMeta({
|
||||
variables: item.children,
|
||||
level: level + 1,
|
||||
parentId: item.variableId,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
saveHistory: () => {
|
||||
set(
|
||||
produce<VariableGroupsStore>(state => {
|
||||
state.variableGroups.forEach(item => {
|
||||
traverse(item.varInfoList, itemIn => {
|
||||
itemIn.meta.isHistory = true;
|
||||
});
|
||||
});
|
||||
}),
|
||||
false,
|
||||
'saveHistory',
|
||||
);
|
||||
},
|
||||
getAllRootVariables: () => {
|
||||
const { variableGroups } = get();
|
||||
const res: Variable[] = [];
|
||||
traverse(
|
||||
variableGroups,
|
||||
item => {
|
||||
res.push(...item.varInfoList);
|
||||
},
|
||||
'subGroupList',
|
||||
);
|
||||
return res;
|
||||
},
|
||||
getAllVariables: () => {
|
||||
const { variableGroups } = get();
|
||||
const variables = variableGroups.map(item => item.varInfoList).flat();
|
||||
const res: Variable[] = [];
|
||||
traverse(
|
||||
variables,
|
||||
item => {
|
||||
res.push(item);
|
||||
},
|
||||
'children',
|
||||
);
|
||||
return res;
|
||||
},
|
||||
transformDto2Vo: data => {
|
||||
const transformedData = getGroupListByDto(data);
|
||||
// 在数据转换完成后,立即更新meta信息
|
||||
transformedData.forEach(group => {
|
||||
get().updateMeta({ variables: group.varInfoList });
|
||||
});
|
||||
return transformedData;
|
||||
},
|
||||
getDtoVariable: (variable: Variable) => getDtoVariable(variable),
|
||||
initStore: data => {
|
||||
const { transformDto2Vo } = get();
|
||||
const transformedData = transformDto2Vo(data.variableGroups);
|
||||
set(
|
||||
{
|
||||
variableGroups: transformedData,
|
||||
canEdit: data.canEdit,
|
||||
},
|
||||
false,
|
||||
'initStore',
|
||||
);
|
||||
},
|
||||
clear: () => {
|
||||
set({ ...getDefaultVariableGroupStore() }, false, 'clear');
|
||||
},
|
||||
})),
|
||||
{
|
||||
enabled: IS_DEV_MODE,
|
||||
name: 'knowledge.variableGroups',
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import {
|
||||
exhaustiveCheckSimple,
|
||||
safeAsyncThrow,
|
||||
} from '@coze-common/chat-area-utils';
|
||||
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
|
||||
import {
|
||||
type project_memory as ProjectMemory,
|
||||
VariableChannel,
|
||||
VariableType,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
|
||||
import {
|
||||
VariableTypeDTO,
|
||||
type VariableSchemaDTO,
|
||||
ViewVariableType,
|
||||
} from '../types';
|
||||
import { type VariableGroup, type Variable } from '../store';
|
||||
|
||||
export const getGroupListByDto = (
|
||||
dtoGroups: ProjectMemory.GroupVariableInfo[],
|
||||
): VariableGroup[] => {
|
||||
const groups = dtoGroups?.map(group => {
|
||||
const baseGroupInfo = getBaseGroupInfoByDto(group);
|
||||
const { groupId } = baseGroupInfo;
|
||||
const varInfoList = getGroupVariableListByDto({
|
||||
group,
|
||||
groupId,
|
||||
});
|
||||
return {
|
||||
...baseGroupInfo,
|
||||
varInfoList,
|
||||
subGroupList: getSubGroupListByDto({
|
||||
group,
|
||||
groupId,
|
||||
}),
|
||||
};
|
||||
});
|
||||
return groups || [];
|
||||
};
|
||||
|
||||
const getBaseGroupInfoByDto = (
|
||||
group: Partial<ProjectMemory.GroupVariableInfo>,
|
||||
): Omit<VariableGroup, 'subGroupList' | 'varInfoList'> => {
|
||||
const {
|
||||
GroupName: groupName = '',
|
||||
GroupDesc: groupDesc = '',
|
||||
GroupExtDesc: groupExtDesc = '',
|
||||
IsReadOnly: isReadOnly = false,
|
||||
DefaultChannel: channel = VariableChannel.Custom,
|
||||
} = group;
|
||||
const groupId = nanoid();
|
||||
return {
|
||||
groupId,
|
||||
groupName,
|
||||
groupDesc,
|
||||
groupExtDesc,
|
||||
channel,
|
||||
isReadOnly,
|
||||
raw: group,
|
||||
};
|
||||
};
|
||||
|
||||
const getGroupVariableListByDto = ({
|
||||
group,
|
||||
groupId,
|
||||
}: {
|
||||
group: Partial<ProjectMemory.GroupVariableInfo>;
|
||||
groupId: string;
|
||||
}): Variable[] => {
|
||||
const { VarInfoList: varInfoList = [] } = group;
|
||||
return (
|
||||
varInfoList?.map(dtoVariable =>
|
||||
getViewVariableByDto(dtoVariable, groupId),
|
||||
) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
const getSubGroupListByDto = ({
|
||||
group,
|
||||
groupId,
|
||||
}: {
|
||||
group: Partial<ProjectMemory.GroupVariableInfo>;
|
||||
groupId: string;
|
||||
}): VariableGroup[] => {
|
||||
const { SubGroupList: subGroupList = [] } = group;
|
||||
return (
|
||||
subGroupList?.map(subGroup => ({
|
||||
...getBaseGroupInfoByDto({
|
||||
...subGroup,
|
||||
DefaultChannel: group.DefaultChannel, // 服务端返回的 subGroup 没有 DefaultChannel需要手动设置
|
||||
}),
|
||||
groupId,
|
||||
varInfoList: getGroupVariableListByDto({
|
||||
group: subGroup,
|
||||
groupId,
|
||||
}),
|
||||
subGroupList: [],
|
||||
})) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
export function getViewVariableByDto(
|
||||
dtoVariable: ProjectMemory.Variable,
|
||||
groupId: string,
|
||||
): Variable {
|
||||
const variableSchema = typeSafeJSONParse(
|
||||
dtoVariable.Schema || '{}',
|
||||
) as VariableSchemaDTO;
|
||||
|
||||
const { type } = variableSchema;
|
||||
|
||||
const baseVariable = createBaseVariable({
|
||||
dtoVariable,
|
||||
groupId,
|
||||
});
|
||||
|
||||
if (type === VariableTypeDTO.List) {
|
||||
return convertListVariable(baseVariable, variableSchema);
|
||||
}
|
||||
|
||||
if (type === VariableTypeDTO.Object) {
|
||||
return convertObjectVariable(baseVariable, variableSchema);
|
||||
}
|
||||
|
||||
return {
|
||||
...baseVariable,
|
||||
type: dTOTypeToViewType(variableSchema.type),
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function dTOTypeToViewType(
|
||||
type: VariableTypeDTO,
|
||||
{
|
||||
arrayItemType,
|
||||
}: {
|
||||
arrayItemType?: VariableTypeDTO;
|
||||
} = {},
|
||||
): ViewVariableType {
|
||||
switch (type) {
|
||||
case VariableTypeDTO.Boolean:
|
||||
return ViewVariableType.Boolean;
|
||||
case VariableTypeDTO.Integer:
|
||||
return ViewVariableType.Integer;
|
||||
case VariableTypeDTO.Float:
|
||||
return ViewVariableType.Number;
|
||||
case VariableTypeDTO.String:
|
||||
return ViewVariableType.String;
|
||||
case VariableTypeDTO.Object:
|
||||
return ViewVariableType.Object;
|
||||
case VariableTypeDTO.List:
|
||||
if (!arrayItemType) {
|
||||
throw new Error(
|
||||
`Unkown variable DTO list need sub type but get ${arrayItemType}`,
|
||||
);
|
||||
}
|
||||
|
||||
switch (arrayItemType) {
|
||||
case VariableTypeDTO.Boolean:
|
||||
return ViewVariableType.ArrayBoolean;
|
||||
case VariableTypeDTO.Integer:
|
||||
return ViewVariableType.ArrayInteger;
|
||||
case VariableTypeDTO.Float:
|
||||
return ViewVariableType.ArrayNumber;
|
||||
case VariableTypeDTO.String:
|
||||
return ViewVariableType.ArrayString;
|
||||
case VariableTypeDTO.Object:
|
||||
return ViewVariableType.ArrayObject;
|
||||
case VariableTypeDTO.List:
|
||||
safeAsyncThrow(
|
||||
`List type variable can't have sub list type: ${type}:${arrayItemType}`,
|
||||
);
|
||||
return ViewVariableType.String;
|
||||
default:
|
||||
exhaustiveCheckSimple(arrayItemType);
|
||||
safeAsyncThrow(`Unknown variable DTO Type: ${type}:${arrayItemType}`);
|
||||
return ViewVariableType.String;
|
||||
}
|
||||
default:
|
||||
exhaustiveCheckSimple(type);
|
||||
safeAsyncThrow(`Unknown variable DTO Type: ${type}:${arrayItemType}`);
|
||||
return ViewVariableType.String;
|
||||
}
|
||||
}
|
||||
|
||||
function createBaseVariable({
|
||||
dtoVariable,
|
||||
groupId,
|
||||
}: {
|
||||
dtoVariable: ProjectMemory.Variable;
|
||||
groupId: string;
|
||||
}): Omit<Variable, 'type' | 'children'> {
|
||||
return {
|
||||
variableId: nanoid(),
|
||||
name: dtoVariable.Keyword ?? '',
|
||||
description: dtoVariable.Description ?? '',
|
||||
enabled: dtoVariable.Enable ?? true,
|
||||
defaultValue: dtoVariable.DefaultValue ?? '',
|
||||
channel: dtoVariable.Channel ?? VariableChannel.Custom,
|
||||
effectiveChannelList: dtoVariable.EffectiveChannelList ?? [],
|
||||
variableType: dtoVariable.VariableType ?? VariableType.KVVariable,
|
||||
readonly: dtoVariable.IsReadOnly ?? false,
|
||||
groupId,
|
||||
parentId: '',
|
||||
meta: {
|
||||
isHistory: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function convertListVariable(
|
||||
baseVariable: Omit<Variable, 'type' | 'children'>,
|
||||
variableSchema: VariableSchemaDTO,
|
||||
): Variable {
|
||||
const subVariableSchema = variableSchema.schema as VariableSchemaDTO;
|
||||
|
||||
const { type: subVariableType } = subVariableSchema;
|
||||
|
||||
if (subVariableType === VariableTypeDTO.Object) {
|
||||
return convertListObjectVariable(baseVariable, variableSchema);
|
||||
}
|
||||
|
||||
return {
|
||||
...baseVariable,
|
||||
type: dTOTypeToViewType(variableSchema.type, {
|
||||
arrayItemType: subVariableType,
|
||||
}),
|
||||
children: [],
|
||||
} as unknown as Variable;
|
||||
}
|
||||
|
||||
/**
|
||||
*@example schema: array<object>
|
||||
{
|
||||
"type": "list",
|
||||
"name": "arr_obj",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"schema": [{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"required": false
|
||||
}, {
|
||||
"type": "integer",
|
||||
"name": "age",
|
||||
"required": false
|
||||
}]
|
||||
},
|
||||
}
|
||||
*/
|
||||
function convertListObjectVariable(
|
||||
baseVariable: Omit<Variable, 'type' | 'children'>,
|
||||
variableSchema: VariableSchemaDTO,
|
||||
): Variable {
|
||||
const subVariableSchema = variableSchema.schema;
|
||||
|
||||
if (!subVariableSchema) {
|
||||
throw new Error('List object variable schema is invalid');
|
||||
}
|
||||
|
||||
const { type: subVariableType } = subVariableSchema;
|
||||
|
||||
return {
|
||||
...baseVariable,
|
||||
type: dTOTypeToViewType(VariableTypeDTO.List, {
|
||||
arrayItemType: subVariableType,
|
||||
}),
|
||||
children: Array.isArray(subVariableSchema.schema)
|
||||
? subVariableSchema.schema.map(schema =>
|
||||
createVariableBySchema(schema, {
|
||||
groupId: baseVariable.groupId,
|
||||
parentId: baseVariable.variableId,
|
||||
}),
|
||||
)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @example schema: object
|
||||
* object
|
||||
{
|
||||
"type": "object",
|
||||
"name": "obj",
|
||||
"schema": [{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"required": false
|
||||
}, {
|
||||
"type": "integer",
|
||||
"name": "age",
|
||||
"required": false
|
||||
}],
|
||||
}
|
||||
* @returns
|
||||
*/
|
||||
function convertObjectVariable(
|
||||
baseVariable: Omit<Variable, 'type' | 'children'>,
|
||||
variableSchema: VariableSchemaDTO,
|
||||
): Variable {
|
||||
const schema = variableSchema.schema || [];
|
||||
|
||||
return {
|
||||
...baseVariable,
|
||||
type: dTOTypeToViewType(variableSchema.type),
|
||||
children: Array.isArray(schema)
|
||||
? schema.map(subMeta =>
|
||||
createVariableBySchema(subMeta, {
|
||||
groupId: baseVariable.groupId,
|
||||
parentId: baseVariable.variableId,
|
||||
}),
|
||||
)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
function createVariableBySchema(
|
||||
subMeta: VariableSchemaDTO,
|
||||
{
|
||||
groupId,
|
||||
parentId,
|
||||
}: {
|
||||
groupId: string;
|
||||
parentId: string;
|
||||
},
|
||||
): Variable {
|
||||
return getViewVariableByDto(
|
||||
{
|
||||
Keyword: subMeta.name,
|
||||
Description: subMeta.description,
|
||||
Schema: JSON.stringify(subMeta),
|
||||
Enable: true,
|
||||
IsReadOnly: subMeta.readonly,
|
||||
},
|
||||
groupId,
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
exhaustiveCheckSimple,
|
||||
safeAsyncThrow,
|
||||
} from '@coze-common/chat-area-utils';
|
||||
import { type project_memory as ProjectMemory } from '@coze-arch/bot-api/memory';
|
||||
|
||||
import { type VariableSchemaDTO, VariableTypeDTO } from '../types';
|
||||
import { type Variable } from '../store';
|
||||
|
||||
/**
|
||||
* 前端变量类型
|
||||
*/
|
||||
export enum ViewVariableType {
|
||||
String = 1,
|
||||
Integer,
|
||||
Boolean,
|
||||
Number,
|
||||
Object = 6,
|
||||
// 上面是 api 中定义的 InputType。下面是整合后的。从 99 开始,避免和后端定义撞车
|
||||
ArrayString = 99,
|
||||
ArrayInteger,
|
||||
ArrayBoolean,
|
||||
ArrayNumber,
|
||||
ArrayObject,
|
||||
}
|
||||
|
||||
export function viewTypeToDTO(type: ViewVariableType): {
|
||||
type: VariableTypeDTO;
|
||||
arrayItemType?: VariableTypeDTO;
|
||||
} {
|
||||
switch (type) {
|
||||
case ViewVariableType.Boolean:
|
||||
return { type: VariableTypeDTO.Boolean };
|
||||
case ViewVariableType.Integer:
|
||||
return { type: VariableTypeDTO.Integer };
|
||||
case ViewVariableType.Number:
|
||||
return { type: VariableTypeDTO.Float };
|
||||
case ViewVariableType.String:
|
||||
return { type: VariableTypeDTO.String };
|
||||
case ViewVariableType.Object:
|
||||
return { type: VariableTypeDTO.Object };
|
||||
case ViewVariableType.ArrayBoolean:
|
||||
return {
|
||||
type: VariableTypeDTO.List,
|
||||
arrayItemType: VariableTypeDTO.Boolean,
|
||||
};
|
||||
case ViewVariableType.ArrayInteger:
|
||||
return {
|
||||
type: VariableTypeDTO.List,
|
||||
arrayItemType: VariableTypeDTO.Integer,
|
||||
};
|
||||
case ViewVariableType.ArrayNumber:
|
||||
return {
|
||||
type: VariableTypeDTO.List,
|
||||
arrayItemType: VariableTypeDTO.Float,
|
||||
};
|
||||
case ViewVariableType.ArrayString:
|
||||
return {
|
||||
type: VariableTypeDTO.List,
|
||||
arrayItemType: VariableTypeDTO.String,
|
||||
};
|
||||
case ViewVariableType.ArrayObject:
|
||||
return {
|
||||
type: VariableTypeDTO.List,
|
||||
arrayItemType: VariableTypeDTO.Object,
|
||||
};
|
||||
default:
|
||||
exhaustiveCheckSimple(type);
|
||||
safeAsyncThrow(`Unknown view variable type: ${type}`);
|
||||
return { type: VariableTypeDTO.String };
|
||||
}
|
||||
}
|
||||
|
||||
export const getDtoVariable = (
|
||||
viewVariable: Variable,
|
||||
): ProjectMemory.Variable => {
|
||||
const { type, arrayItemType } = viewTypeToDTO(viewVariable.type);
|
||||
|
||||
const schema: VariableSchemaDTO = {
|
||||
name: viewVariable.name,
|
||||
enable: viewVariable.enabled,
|
||||
description: viewVariable.description || '',
|
||||
type,
|
||||
readonly: Boolean(viewVariable.readonly),
|
||||
schema: '',
|
||||
};
|
||||
|
||||
// 处理数组类型
|
||||
if (type === VariableTypeDTO.List && arrayItemType) {
|
||||
if (arrayItemType === VariableTypeDTO.Object) {
|
||||
schema.schema = {
|
||||
type: VariableTypeDTO.Object,
|
||||
schema: viewVariable.children?.map(child => {
|
||||
const childDTO = getDtoVariable(child);
|
||||
return JSON.parse(childDTO.Schema || '{}');
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
schema.schema = {
|
||||
type: arrayItemType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 处理对象类型
|
||||
if (type === VariableTypeDTO.Object) {
|
||||
schema.schema = viewVariable.children?.map(child => {
|
||||
const childDTO = getDtoVariable(child);
|
||||
return JSON.parse(childDTO.Schema || '{}');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
Keyword: viewVariable.name,
|
||||
Channel: viewVariable.channel,
|
||||
VariableType: viewVariable.variableType ?? 1,
|
||||
DefaultValue: viewVariable.defaultValue,
|
||||
Description: viewVariable.description,
|
||||
EffectiveChannelList: viewVariable.effectiveChannelList,
|
||||
Enable: Boolean(viewVariable.enabled),
|
||||
IsReadOnly: Boolean(viewVariable.readonly),
|
||||
Schema: JSON.stringify(schema, null, 0),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 enum VariableTypeDTO {
|
||||
Object = 'object',
|
||||
List = 'list',
|
||||
String = 'string',
|
||||
Integer = 'integer',
|
||||
Boolean = 'boolean',
|
||||
Float = 'float',
|
||||
}
|
||||
|
||||
export interface VariableSchemaDTO {
|
||||
type: VariableTypeDTO;
|
||||
name: string;
|
||||
enable: boolean;
|
||||
description: string;
|
||||
readonly: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
schema?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端变量类型
|
||||
*/
|
||||
export enum ViewVariableType {
|
||||
String = 1,
|
||||
Integer,
|
||||
Boolean,
|
||||
Number,
|
||||
Object = 6,
|
||||
// 上面是 api 中定义的 InputType。下面是整合后的。从 99 开始,避免和后端定义撞车
|
||||
ArrayString = 99,
|
||||
ArrayInteger,
|
||||
ArrayBoolean,
|
||||
ArrayNumber,
|
||||
ArrayObject,
|
||||
}
|
||||
|
||||
export const BASE_ARRAY_PAIR: [ViewVariableType, ViewVariableType][] = [
|
||||
[ViewVariableType.String, ViewVariableType.ArrayString],
|
||||
[ViewVariableType.Integer, ViewVariableType.ArrayInteger],
|
||||
[ViewVariableType.Boolean, ViewVariableType.ArrayBoolean],
|
||||
[ViewVariableType.Number, ViewVariableType.ArrayNumber],
|
||||
[ViewVariableType.Object, ViewVariableType.ArrayObject],
|
||||
];
|
||||
|
||||
export const VARIABLE_TYPE_ALIAS_MAP: Record<ViewVariableType, string> = {
|
||||
[ViewVariableType.String]: 'String',
|
||||
[ViewVariableType.Integer]: 'Integer',
|
||||
[ViewVariableType.Boolean]: 'Boolean',
|
||||
[ViewVariableType.Number]: 'Number',
|
||||
[ViewVariableType.Object]: 'Object',
|
||||
[ViewVariableType.ArrayString]: 'Array<String>',
|
||||
[ViewVariableType.ArrayInteger]: 'Array<Integer>',
|
||||
[ViewVariableType.ArrayBoolean]: 'Array<Boolean>',
|
||||
[ViewVariableType.ArrayNumber]: 'Array<Number>',
|
||||
[ViewVariableType.ArrayObject]: 'Array<Object>',
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace ViewVariableType {
|
||||
/**
|
||||
* 获取所有变量类型的补集
|
||||
* @param inputTypes
|
||||
*/
|
||||
export function getComplement(inputTypes: ViewVariableType[]) {
|
||||
const allTypes: ViewVariableType[] = [
|
||||
...BASE_ARRAY_PAIR.map(_pair => _pair[0]),
|
||||
...BASE_ARRAY_PAIR.map(_pair => _pair[1]),
|
||||
];
|
||||
|
||||
return allTypes.filter(type => !inputTypes.includes(type));
|
||||
}
|
||||
|
||||
export function isArrayType(type: ViewVariableType): boolean {
|
||||
const arrayTypes = BASE_ARRAY_PAIR.map(_pair => _pair[1]);
|
||||
return arrayTypes.includes(type);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const ObjectLikeTypes = [
|
||||
ViewVariableType.Object,
|
||||
ViewVariableType.ArrayObject,
|
||||
];
|
||||
@@ -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 CSSProperties } from 'react';
|
||||
|
||||
import { ViewVariableType } from '../store';
|
||||
|
||||
export type WithCustomStyle<T = object> = {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
} & T;
|
||||
|
||||
export const VARIABLE_TYPE_ALIAS_MAP: Record<ViewVariableType, string> = {
|
||||
[ViewVariableType.String]: 'String',
|
||||
[ViewVariableType.Integer]: 'Integer',
|
||||
[ViewVariableType.Boolean]: 'Boolean',
|
||||
[ViewVariableType.Number]: 'Number',
|
||||
[ViewVariableType.Object]: 'Object',
|
||||
[ViewVariableType.ArrayString]: 'Array<String>',
|
||||
[ViewVariableType.ArrayInteger]: 'Array<Integer>',
|
||||
[ViewVariableType.ArrayBoolean]: 'Array<Boolean>',
|
||||
[ViewVariableType.ArrayNumber]: 'Array<Number>',
|
||||
[ViewVariableType.ArrayObject]: 'Array<Object>',
|
||||
};
|
||||
17
frontend/packages/data/memory/variables/src/typings.d.ts
vendored
Normal file
17
frontend/packages/data/memory/variables/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
export function traverse<
|
||||
T extends { [key in K]?: T[] },
|
||||
K extends string = 'children',
|
||||
>(
|
||||
nodeOrNodes: T | T[],
|
||||
action: (node: T) => void,
|
||||
traverseKey: K = 'children' as K,
|
||||
maxDepth = Infinity,
|
||||
currentDepth = 0,
|
||||
) {
|
||||
const nodes = Array.isArray(nodeOrNodes) ? nodeOrNodes : [nodeOrNodes];
|
||||
nodes.forEach(node => {
|
||||
action(node);
|
||||
if (currentDepth < maxDepth) {
|
||||
const children = node[traverseKey] ?? [];
|
||||
if (children?.length > 0) {
|
||||
traverse(children, action, traverseKey, maxDepth, currentDepth + 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
191
frontend/packages/data/memory/variables/src/variables-config.tsx
Normal file
191
frontend/packages/data/memory/variables/src/variables-config.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import cls from 'classnames';
|
||||
import { IllustrationNoContent } from '@douyinfe/semi-illustrations';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { VariableChannel } from '@coze-arch/bot-api/memory';
|
||||
import { IconCozCross } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Empty,
|
||||
Form,
|
||||
type FormApi,
|
||||
IconButton,
|
||||
Spin,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import { useVariableGroupsStore, type Variable } from '@/store';
|
||||
import { useLeaveWarning } from '@/hooks/use-case/use-leave-waring';
|
||||
import { useInit } from '@/hooks/life-cycle/use-init';
|
||||
import { useDestory } from '@/hooks/life-cycle/use-destory';
|
||||
import { VariableContext } from '@/context';
|
||||
import { VariableGroup as VariableGroupComponent } from '@/components/variable-group';
|
||||
|
||||
import {
|
||||
checkProjectID,
|
||||
submit,
|
||||
} from './service/use-case-service/submit-service';
|
||||
import { useChangeWarning } from './hooks/use-case/use-change-warning';
|
||||
|
||||
export interface VariableConfigProps {
|
||||
projectID: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export const VariablesConfig = ({
|
||||
projectID,
|
||||
version,
|
||||
}: VariableConfigProps) => {
|
||||
const formApiRef = useRef<FormApi | null>(null);
|
||||
|
||||
const { loading } = useInit(projectID, version);
|
||||
useDestory();
|
||||
const { setHasUnsavedChanges, hasUnsavedChanges } = useLeaveWarning();
|
||||
const { variableGroups, canEdit, saveHistory, getAllVariables } =
|
||||
useVariableGroupsStore(
|
||||
useShallow(state => ({
|
||||
variableGroups: state.variableGroups,
|
||||
canEdit: state.canEdit,
|
||||
saveHistory: state.saveHistory,
|
||||
getAllVariables: state.getAllVariables,
|
||||
})),
|
||||
);
|
||||
|
||||
const { isShowBanner, showBanner, hideBanner, hideBannerForever } =
|
||||
useChangeWarning();
|
||||
|
||||
const isEmpty = !variableGroups.length;
|
||||
|
||||
const onVariableChange = (changeValue: Variable) => {
|
||||
setHasUnsavedChanges(true);
|
||||
if (changeValue.meta?.isHistory) {
|
||||
showBanner();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!checkProjectID(projectID)) {
|
||||
return;
|
||||
}
|
||||
const formApi = formApiRef.current;
|
||||
if (!formApi) {
|
||||
return;
|
||||
}
|
||||
const isValid = await formApi.validate();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
saveHistory();
|
||||
await submit(projectID);
|
||||
setHasUnsavedChanges(false);
|
||||
};
|
||||
|
||||
const initValues = getAllVariables().reduce((acc, curr) => {
|
||||
acc[curr.variableId] = { name: curr.name };
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
formApiRef.current?.setValues(initValues);
|
||||
}, [loading, initValues]);
|
||||
|
||||
return (
|
||||
<VariableContext.Provider
|
||||
value={{
|
||||
variablePageCanEdit: canEdit,
|
||||
groups: variableGroups,
|
||||
}}
|
||||
>
|
||||
<div className="p-4 pb-[72px]">
|
||||
{loading ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spin />
|
||||
</div>
|
||||
) : isEmpty ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Empty
|
||||
image={<IllustrationNoContent className="w-[140px] h-[140px]" />}
|
||||
title={I18n.t('card_builder_varpanel_var_empty')}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{isShowBanner ? (
|
||||
<div className="h-[36px] flex items-center justify-center coz-mg-hglt coz-fg-primary text-sm mb-4 mt-[-16px] mx-[-16px]">
|
||||
<div className="flex items-center ml-auto">
|
||||
{I18n.t('variable_config_change_banner')}
|
||||
</div>
|
||||
<div className="flex items-center ml-auto cursor-pointer">
|
||||
<div
|
||||
className="coz-fg-secondary text-xs"
|
||||
onClick={hideBannerForever}
|
||||
>
|
||||
{I18n.t('do_not_remind_again')}
|
||||
</div>
|
||||
<IconButton
|
||||
className="ml-2 !bg-transparent"
|
||||
onClick={hideBanner}
|
||||
icon={<IconCozCross />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<Form<typeof initValues>
|
||||
getFormApi={formApi => {
|
||||
formApiRef.current = formApi;
|
||||
}}
|
||||
showValidateIcon={false}
|
||||
autoScrollToError
|
||||
initValues={initValues}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{variableGroups.map(item => (
|
||||
<VariableGroupComponent
|
||||
readonly={!canEdit || item.isReadOnly}
|
||||
groupInfo={item}
|
||||
onVariableChange={onVariableChange}
|
||||
validateExistKeyword={item.channel === VariableChannel.APP}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Form>
|
||||
<div
|
||||
className={cls(
|
||||
'flex items-center justify-end',
|
||||
'fixed bottom-[1px] right-[1px] left-[1px] pb-4 pt-6',
|
||||
'bg-white mr-4 px-4',
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!canEdit || !hasUnsavedChanges}
|
||||
>
|
||||
{I18n.t('edit_variables_modal_ok_text')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</VariableContext.Provider>
|
||||
);
|
||||
};
|
||||
169
frontend/packages/data/memory/variables/src/variables-value.tsx
Normal file
169
frontend/packages/data/memory/variables/src/variables-value.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import classNames from 'classnames';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { IllustrationNoContent } from '@douyinfe/semi-illustrations';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import { type KVItem } from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
import {
|
||||
IconCozRefresh,
|
||||
IconCozCrossCircleFill,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Table, Select, IconButton, Tooltip, Empty } from '@coze-arch/coze-design';
|
||||
|
||||
export interface VariablesValueProps {
|
||||
projectID: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export function VariablesValue({ projectID, version }: VariablesValueProps) {
|
||||
const { loading, data, refresh } = useRequest(async () => {
|
||||
const res = await MemoryApi.GetPlayGroundMemory({
|
||||
project_id: projectID,
|
||||
...(version ? { version } : {}),
|
||||
});
|
||||
return res.memories ?? [];
|
||||
});
|
||||
|
||||
const handleClear = async (item: KVItem) => {
|
||||
if (!item.keyword) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendTeaEvent(EVENT_NAMES.memory_click_front, {
|
||||
project_id: projectID,
|
||||
resource_type: 'variable',
|
||||
action: 'reset',
|
||||
source: 'app_detail_page',
|
||||
source_detail: 'memory_preview',
|
||||
});
|
||||
|
||||
await MemoryApi.DelProfileMemory({
|
||||
project_id: projectID,
|
||||
keywords: [item.keyword],
|
||||
});
|
||||
|
||||
refresh();
|
||||
};
|
||||
|
||||
const handleReset = async () => {
|
||||
sendTeaEvent(EVENT_NAMES.memory_click_front, {
|
||||
project_id: projectID,
|
||||
resource_type: 'variable',
|
||||
action: 'reset',
|
||||
source: 'app_detail_page',
|
||||
source_detail: 'memory_preview',
|
||||
});
|
||||
|
||||
await MemoryApi.DelProfileMemory({ project_id: projectID });
|
||||
|
||||
refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'h-full p-4',
|
||||
'[&_.semi-table-row]:!bg-transparent',
|
||||
'[&_.semi-table-row-head]:!bg-transparent',
|
||||
'[&_.semi-table-row-cell]:text-[14px]',
|
||||
)}
|
||||
>
|
||||
<Table
|
||||
useHoverStyle={false}
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoContent className="w-[140px] h-[140px]" />}
|
||||
title={I18n.t('variables_user_data_empty')}
|
||||
/>
|
||||
}
|
||||
tableProps={{
|
||||
loading,
|
||||
dataSource: data,
|
||||
columns: [
|
||||
{
|
||||
title: I18n.t('variable_Table_Title_name'),
|
||||
dataIndex: 'keyword',
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className={'flex items-center'}>
|
||||
<span className={'mr-4px'}>
|
||||
{I18n.t('variable_Table_Title_value')}
|
||||
</span>
|
||||
<Tooltip
|
||||
theme={'dark'}
|
||||
content={I18n.t('variable_Button_reset_variable')}
|
||||
>
|
||||
<IconButton
|
||||
color={'primary'}
|
||||
icon={<IconCozRefresh />}
|
||||
size={'small'}
|
||||
onClick={handleReset}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'value',
|
||||
render: (value: string, item: KVItem) => {
|
||||
const schema = typeSafeJSONParse(item?.schema) as
|
||||
| { readonly?: boolean }
|
||||
| undefined;
|
||||
|
||||
if (schema?.readonly) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
className="w-full truncate"
|
||||
value={value}
|
||||
showArrow={false}
|
||||
showClear={true}
|
||||
emptyContent={null}
|
||||
onClear={() => handleClear(item)}
|
||||
clearIcon={
|
||||
<IconButton
|
||||
theme={'borderless'}
|
||||
color={'secondary'}
|
||||
icon={<IconCozCrossCircleFill />}
|
||||
size={'large'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: I18n.t('variable_Table_Title_edit_time'),
|
||||
align: 'left',
|
||||
dataIndex: 'update_time',
|
||||
width: 150,
|
||||
render: (time: number, item: KVItem) =>
|
||||
item.value && dayjs.unix(time).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 { DemoComponent } from '../src';
|
||||
|
||||
export default {
|
||||
title: 'Example/Demo',
|
||||
component: DemoComponent,
|
||||
parameters: {
|
||||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'centered',
|
||||
},
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||
export const Base = {
|
||||
args: {
|
||||
name: 'tecvan',
|
||||
},
|
||||
};
|
||||
34
frontend/packages/data/memory/variables/stories/hello.mdx
Normal file
34
frontend/packages/data/memory/variables/stories/hello.mdx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Meta } from "@storybook/blocks";
|
||||
|
||||
<Meta title="Hello world" />
|
||||
|
||||
<div className="sb-container">
|
||||
<div className='sb-section-title'>
|
||||
# Hello world
|
||||
|
||||
Hello world
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
.sb-container {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.sb-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.sb-section-title {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
126
frontend/packages/data/memory/variables/tsconfig.build.json
Normal file
126
frontend/packages/data/memory/variables/tsconfig.build.json
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../arch/bot-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/bot-error/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/bot-flags/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/bot-hooks/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/bot-monaco-editor/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/bot-store/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/bot-tea/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/bot-utils/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/logger/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/report-events/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/report-tti/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../common/chat-area/utils/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../common/e2e/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../common/reporter/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../common/utils/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../components/bot-icons/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../components/bot-semi/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../../config/stylelint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../../config/vitest-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../foundation/local-storage/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../knowledge/common/components/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../knowledge/common/stores/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../knowledge/knowledge-ide-base/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../knowledge/knowledge-modal-adapter/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../knowledge/knowledge-modal-base/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../knowledge/knowledge-resource-processor-adapter/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../knowledge/knowledge-resource-processor-base/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../knowledge/knowledge-resource-processor-core/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../studio/components/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../studio/premium/premium-components-adapter/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../studio/premium/premium-store-adapter/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/data/memory/variables/tsconfig.json
Normal file
15
frontend/packages/data/memory/variables/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"exclude": ["**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
frontend/packages/data/memory/variables/tsconfig.misc.json
Normal file
23
frontend/packages/data/memory/variables/tsconfig.misc.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["__tests__", "vitest.config.ts", "stories"],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
frontend/packages/data/memory/variables/vitest.config.ts
Normal file
22
frontend/packages/data/memory/variables/vitest.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig } from '@coze-arch/vitest-config';
|
||||
|
||||
export default defineConfig({
|
||||
dirname: __dirname,
|
||||
preset: 'web',
|
||||
});
|
||||
Reference in New Issue
Block a user