feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
5
frontend/packages/studio/components/.stylelintrc.js
Normal file
5
frontend/packages/studio/components/.stylelintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/studio/components/README.md
Normal file
16
frontend/packages/studio/components/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-studio/components
|
||||
|
||||
> 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,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { AvatarName } from '../src/avatar-name';
|
||||
|
||||
describe('AvatarName', () => {
|
||||
it('should one image and @username', () => {
|
||||
const wrapper = render(
|
||||
<AvatarName
|
||||
name="BotNickName"
|
||||
username="BotUserName"
|
||||
avatar="https://sf-coze-web-cdn.coze.com/obj/coze-web-sg/obric/coze/favicon.1970.png"
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.getAllByRole('img').length).toBe(1);
|
||||
expect(wrapper.getByText(/^@BotUserName/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should two image', () => {
|
||||
const wrapper = render(
|
||||
<AvatarName
|
||||
name="BotNickName"
|
||||
username="BotUserName"
|
||||
avatar="https://sf-coze-web-cdn.coze.com/obj/coze-web-sg/obric/coze/favicon.1970.png"
|
||||
label={{
|
||||
icon: 'https://sf-coze-web-cdn.coze.com/obj/coze-web-sg/obric/coze/favicon.1970.png',
|
||||
name: 'test',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.getAllByRole('img').length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 { render, fireEvent, act } from '@testing-library/react';
|
||||
|
||||
import { SelectSpaceModal } from '../src/select-space-modal';
|
||||
|
||||
const spaces = [
|
||||
{
|
||||
id: 'space0',
|
||||
name: 'space0',
|
||||
hide_operation: false,
|
||||
},
|
||||
{
|
||||
id: 'space1',
|
||||
name: 'space1',
|
||||
hide_operation: true,
|
||||
},
|
||||
{
|
||||
id: 'space2',
|
||||
name: 'space2',
|
||||
hide_operation: false,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mock('@coze-arch/bot-studio-store', () => ({
|
||||
useSpaceStore: () => ({
|
||||
space: { ...spaces[0], id: spaces[0].id },
|
||||
spaces: {
|
||||
bot_space_list: spaces,
|
||||
},
|
||||
getState: () => ({
|
||||
getPersonalSpaceID: () => 'personal-space-id',
|
||||
}),
|
||||
}),
|
||||
useSpaceList: () => ({ spaces, loading: false }),
|
||||
}));
|
||||
|
||||
vi.mock('@coze-studio/bot-detail-store/page-runtime', () => ({
|
||||
usePageRuntimeStore: () => ({
|
||||
pageFrom: 'test',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@coze-studio/bot-detail-store/bot-skill', () => ({
|
||||
useBotSkillStore: () => ({
|
||||
hasWorkflow: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('SelectSpaceModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should render botName and spaces', () => {
|
||||
const wrapper = render(<SelectSpaceModal visible botName="mockBot" />);
|
||||
expect(wrapper.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(
|
||||
wrapper.getByDisplayValue('mockBot(duplicate_rename_copy)'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// 检查表单是否存在
|
||||
expect(wrapper.getByRole('form')).toBeInTheDocument();
|
||||
|
||||
// 检查确定和取消按钮
|
||||
expect(
|
||||
wrapper.getByRole('button', { name: 'confirm' }),
|
||||
).toBeInTheDocument();
|
||||
expect(wrapper.getByRole('button', { name: 'cancel' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should fire events', async () => {
|
||||
const mockCancel = vi.fn();
|
||||
const mockConfirm = vi.fn();
|
||||
render(
|
||||
<SelectSpaceModal
|
||||
visible
|
||||
botName="mockBot"
|
||||
onCancel={mockCancel}
|
||||
onConfirm={mockConfirm}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(document.body.querySelector('[aria-label="cancel"]')!);
|
||||
expect(mockCancel).toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
await fireEvent.click(
|
||||
document.body.querySelector('[aria-label="confirm"]')!,
|
||||
);
|
||||
});
|
||||
expect(mockConfirm).toHaveBeenCalledWith(
|
||||
'space0',
|
||||
'mockBot(duplicate_rename_copy)',
|
||||
);
|
||||
});
|
||||
});
|
||||
BIN
frontend/packages/studio/components/assets/avatar_default.png
Normal file
BIN
frontend/packages/studio/components/assets/avatar_default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M6.76033 11.9922L3.91012 11.9854C3.79967 11.9851 3.71034 11.8954 3.7106 11.7849C3.71073 11.732 3.73179 11.6814 3.76918 11.644L6.52438 8.88878C6.62852 8.78464 6.79737 8.78464 6.90151 8.88878L8.17289 10.1602L11.5444 6.78861C11.6486 6.68447 11.8174 6.68447 11.9216 6.78861C11.9716 6.83862 11.9997 6.90645 11.9997 6.97717V11.8C11.9997 11.9105 11.9101 12 11.7997 12H6.81585C6.79659 12 6.77796 11.9973 6.76033 11.9922ZM2.66634 14.6667C1.93301 14.6667 1.33301 14.0667 1.33301 13.3334V2.66671C1.33301 1.93337 1.93301 1.33337 2.66634 1.33337H13.333C14.0663 1.33337 14.6663 1.93337 14.6663 2.66671V13.3334C14.6663 14.0667 14.0663 14.6667 13.333 14.6667H2.66634ZM2.66634 13.3334H13.333V2.66671H2.66634V13.3334ZM3.99967 4.00004H5.99967V6.00004H3.99967V4.00004Z"
|
||||
fill="currentColor" fill-opacity="0.96" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 917 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M6 8.66671H10C11.841 8.66671 14 9.95241 14 12.2667V13.3334C14 14.0667 13.4 14.6667 12.6667 14.6667H3.33333C2.6 14.6667 2 14.0667 2 13.3334V12.2667C2 9.95436 4.15905 8.66671 6 8.66671ZM12.6667 13.3334V12.2223C12.6667 10.8213 11.2473 10 10 10H6C4.77998 10 3.33333 10.7812 3.33333 12.2223V13.3334H12.6667ZM8 8.00004C6.15905 8.00004 4.66667 6.50766 4.66667 4.66671C4.66667 2.82576 6.15905 1.33337 8 1.33337C9.84095 1.33337 11.3333 2.82576 11.3333 4.66671C11.3333 6.50766 9.84095 8.00004 8 8.00004ZM8 6.66671C9.10457 6.66671 10 5.77128 10 4.66671C10 3.56214 9.10457 2.66671 8 2.66671C6.89543 2.66671 6 3.56214 6 4.66671C6 5.77128 6.89543 6.66671 8 6.66671Z"
|
||||
fill="currentColor" fill-opacity="0.96" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 820 B |
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
7
frontend/packages/studio/components/eslint.config.js
Normal file
7
frontend/packages/studio/components/eslint.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
141
frontend/packages/studio/components/package.json
Normal file
141
frontend/packages/studio/components/package.json
Normal file
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"name": "@coze-studio/components",
|
||||
"version": "0.0.1",
|
||||
"description": "biz components extract from apps/bot/src/components",
|
||||
"license": "Apache-2.0",
|
||||
"author": "fanwenjie.fe@bytedance.com",
|
||||
"maintainers": [],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./coze-brand": "./src/coze-brand/index.tsx",
|
||||
"./search-no-result": "./src/search-no-result/index.tsx",
|
||||
"./sortable-list": "./src/sortable-list/index.tsx",
|
||||
"./dnd-provider": "./src/dnd-provider/index.tsx",
|
||||
"./sortable-list-hooks": "./src/sortable-list/hooks.ts",
|
||||
"./generate-gif": "./src/generate-gif/index.tsx",
|
||||
"./markdown-editor": "./src/markdown-editor/index.tsx",
|
||||
"./parameters-popover": "./src/plugins/parameters-popover/index.tsx",
|
||||
"./collapsible-role-list": "./src/social-scene/collapsible-role-list/index.tsx",
|
||||
"./monetize": "./src/monetize/index.ts",
|
||||
"./collapsible-icon-button": "./src/collapsible-icon-button/index.tsx",
|
||||
"./table-select-all-popover": "./src/table-select-all-popover/index.tsx"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"coze-brand": [
|
||||
"./src/coze-brand/index.tsx"
|
||||
],
|
||||
"search-no-result": [
|
||||
"./src/search-no-result/index.tsx"
|
||||
],
|
||||
"sortable-list": [
|
||||
"./src/sortable-list/index.tsx"
|
||||
],
|
||||
"dnd-provider": [
|
||||
"./src/dnd-provider/index.tsx"
|
||||
],
|
||||
"sortable-list-hooks": [
|
||||
"./src/sortable-list/hooks.ts"
|
||||
],
|
||||
"generate-gif": [
|
||||
"./src/generate-gif/index.tsx"
|
||||
],
|
||||
"markdown-editor": [
|
||||
"./src/markdown-editor/index.tsx"
|
||||
],
|
||||
"parameters-popover": [
|
||||
"./src/plugins/parameters-popover/index.tsx"
|
||||
],
|
||||
"collapsible-role-list": [
|
||||
"./src/social-scene/collapsible-role-list/index.tsx"
|
||||
],
|
||||
"monetize": [
|
||||
"./src/monetize/index.ts"
|
||||
],
|
||||
"collapsible-icon-button": [
|
||||
"./src/collapsible-icon-button/index.tsx"
|
||||
],
|
||||
"table-select-all-popover": [
|
||||
"./src/table-select-all-popover/index.tsx"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^5.1.5",
|
||||
"@coze-agent-ide/bot-input-length-limit": "workspace:*",
|
||||
"@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-md-box-adapter": "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/web-context": "workspace:*",
|
||||
"@coze-common/assets": "workspace:*",
|
||||
"@coze-common/biz-components": "workspace:*",
|
||||
"@coze-common/websocket-manager-adapter": "workspace:*",
|
||||
"@coze-data/e2e": "workspace:*",
|
||||
"@coze-foundation/space-store": "workspace:*",
|
||||
"@coze-studio/bot-utils": "workspace:*",
|
||||
"@douyinfe/semi-icons": "^2.36.0",
|
||||
"ahooks": "^3.7.8",
|
||||
"axios": "^1.4.0",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"immer": "^10.0.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-markdown": "^8.0.3",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-space-api": "workspace:*",
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@coze-studio/bot-detail-store": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"debug": "^4.3.4",
|
||||
"nanoid": "^4.0.2",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"use-event-callback": "~0.1.0",
|
||||
"utility-types": "^3.10.0",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5",
|
||||
"webpack": "~5.91.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0",
|
||||
"utility-types": "^3.10.0"
|
||||
},
|
||||
"// deps": "immer@^10.0.3 为脚本自动补齐,请勿改动"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
.ctn {
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
|
||||
box-sizing: content-box;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
background-color: var(--coz-fg-white, #fff);
|
||||
border: 1.5px solid var(--coz-fg-white, #fff);
|
||||
border-radius: 50%;
|
||||
|
||||
&.loading {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
padding: 2px;
|
||||
|
||||
font-size: 8px;
|
||||
|
||||
background-color: var(--coz-mg-hglt-plus-green, #00B83E);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
|
||||
&.icon-generating {
|
||||
color: var(--coz-fg-white, #fff);
|
||||
}
|
||||
|
||||
&.icon-success {
|
||||
color: var(--coz-mg-color-plus-emerald, #00B83E);
|
||||
}
|
||||
|
||||
&.icon-fail {
|
||||
color: var(--coz-mg-color-plus-orange, #FF811A);
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.icon-icon-coz_loading.icon-icon-loading {
|
||||
animation: semi-animation-rotate .6s linear infinite;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 classNames from 'classnames';
|
||||
import { DotStatus } from '@coze-studio/bot-detail-store';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozCheckMarkCircleFillPalette,
|
||||
IconCozLoading,
|
||||
IconCozWarningCircleFillPalette,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface AvatarBackgroundNoticeDotProps {
|
||||
status: DotStatus;
|
||||
}
|
||||
|
||||
export const AvatarBackgroundNoticeDot: React.FC<
|
||||
AvatarBackgroundNoticeDotProps
|
||||
> = ({ status }) => {
|
||||
if (status === DotStatus.None || status === DotStatus.Cancel) {
|
||||
return null;
|
||||
}
|
||||
const dot = {
|
||||
[DotStatus.Generating]: (
|
||||
<Tooltip content={I18n.t('profilepicture_hover_generating')}>
|
||||
<IconCozLoading
|
||||
className={classNames(s.icon, s['icon-generating'])}
|
||||
spin={true}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
[DotStatus.Success]: (
|
||||
<Tooltip content={I18n.t('profilepicture_hover_generated')}>
|
||||
<IconCozCheckMarkCircleFillPalette
|
||||
className={classNames(s.icon, s['icon-success'])}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
[DotStatus.Fail]: (
|
||||
<Tooltip content={I18n.t('profilepicture_hover_failed')}>
|
||||
<IconCozWarningCircleFillPalette
|
||||
className={classNames(s.icon, s['icon-fail'])}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
s.ctn,
|
||||
status === DotStatus.Generating ? s.loading : undefined,
|
||||
)}
|
||||
>
|
||||
{dot[status]}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
.container {
|
||||
flex-shrink: 0;
|
||||
max-width: 100%;
|
||||
height: 18px;
|
||||
|
||||
.avatar {
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
border-radius: 12px;
|
||||
|
||||
img {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.label-icon {
|
||||
cursor: pointer;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
|
||||
.txt {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
|
||||
@apply coz-fg-dim;
|
||||
|
||||
&.name {
|
||||
@apply coz-fg-secondary;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
&.light {
|
||||
.txt {
|
||||
color: rgba(255, 255, 255, 39%);
|
||||
|
||||
&.name {
|
||||
color: rgba(255, 255, 255, 79%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.white {
|
||||
.txt {
|
||||
color: #FFF;
|
||||
|
||||
&.name {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.middle {
|
||||
height: 20px;
|
||||
|
||||
.txt {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
&.username {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
height: 20px;
|
||||
|
||||
.label-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.txt {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
117
frontend/packages/studio/components/src/avatar-name/index.tsx
Normal file
117
frontend/packages/studio/components/src/avatar-name/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 { Space, Typography, Tooltip } from '@coze-arch/coze-design';
|
||||
import { Image } from '@coze-arch/bot-semi';
|
||||
|
||||
import AvatarDefault from '../../assets/avatar_default.png';
|
||||
|
||||
import s from './index.module.less';
|
||||
const { Text } = Typography;
|
||||
interface AvatarNameProps {
|
||||
avatar?: string;
|
||||
username?: string;
|
||||
name?: string;
|
||||
label?: {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
href?: string;
|
||||
};
|
||||
theme?: 'default' | 'light' | 'white';
|
||||
className?: string;
|
||||
nameMaxWidth?: number;
|
||||
size?: 'default' | 'large' | 'small';
|
||||
renderCenterSlot?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AvatarSizeMap = {
|
||||
small: 12,
|
||||
default: 14,
|
||||
large: 16,
|
||||
};
|
||||
|
||||
export const AvatarName = ({
|
||||
avatar,
|
||||
username,
|
||||
name,
|
||||
label,
|
||||
theme,
|
||||
className,
|
||||
nameMaxWidth,
|
||||
size = 'default',
|
||||
renderCenterSlot = null,
|
||||
}: AvatarNameProps) => (
|
||||
<Space
|
||||
spacing={4}
|
||||
className={classNames(
|
||||
s.container,
|
||||
theme && s[theme],
|
||||
{ [s.large]: size === 'large' },
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
width={AvatarSizeMap[size]}
|
||||
height={AvatarSizeMap[size]}
|
||||
src={avatar || AvatarDefault}
|
||||
fallback={<img src={AvatarDefault} width={'100%'} height={'100%'} />}
|
||||
preview={false}
|
||||
className={s.avatar}
|
||||
/>
|
||||
<Space spacing={2}>
|
||||
<Text
|
||||
className={classNames(s.txt, s.name)}
|
||||
ellipsis={{ showTooltip: false, rows: 1 }}
|
||||
style={
|
||||
typeof nameMaxWidth === 'number' ? { maxWidth: nameMaxWidth } : {}
|
||||
}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
{label?.icon ? (
|
||||
<Tooltip
|
||||
showArrow
|
||||
content={label?.name}
|
||||
position={'top'}
|
||||
trigger={label?.name ? 'hover' : 'custom'}
|
||||
>
|
||||
<img
|
||||
src={label?.icon}
|
||||
className={s['label-icon']}
|
||||
tabIndex={-1}
|
||||
onMouseDown={event => {
|
||||
if (label?.href) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
window.open(label.href, '_blank');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
{renderCenterSlot}
|
||||
{username ? (
|
||||
<Text
|
||||
className={classNames(s.txt, s.username)}
|
||||
ellipsis={{ showTooltip: false, rows: 1 }}
|
||||
>
|
||||
@{username}
|
||||
</Text>
|
||||
) : null}
|
||||
</Space>
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
.popover-content {
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.popover-card-title {
|
||||
margin-bottom: 24px;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
color: #1C1D24;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.popover-card-img {
|
||||
max-width: 360px;
|
||||
border: 1px solid #E5E5E5;
|
||||
border-radius: 8px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-card-icon {
|
||||
color: rgba(29, 28, 35, 35%)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren, useRef, useCallback } from 'react';
|
||||
|
||||
import { get } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
|
||||
import { type ImageProps } from '@coze-arch/bot-semi/Image';
|
||||
import { Popover, Image } from '@coze-arch/bot-semi';
|
||||
import { IconGroupCardOutlined } from '@coze-arch/bot-icons';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface CardThumbnailPopoverProps extends PopoverProps {
|
||||
title?: string;
|
||||
url?: string;
|
||||
className?: string;
|
||||
imgProps?: ImageProps;
|
||||
}
|
||||
|
||||
export const CardThumbnailPopover: React.FC<
|
||||
PropsWithChildren<CardThumbnailPopoverProps>
|
||||
> = ({ children, url, title = '卡片预览', className, imgProps, ...props }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const popoverRef = useRef<any>();
|
||||
|
||||
const onImageLoad = useCallback(() => {
|
||||
const calcPosition = get(
|
||||
popoverRef.current,
|
||||
'tooltipRef.current.foundation.calcPosition',
|
||||
);
|
||||
if (typeof calcPosition === 'function') {
|
||||
calcPosition?.();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
position="top"
|
||||
showArrow
|
||||
ref={popoverRef}
|
||||
content={
|
||||
<div className={s['popover-content']}>
|
||||
<div className={s['popover-card-title']}>{title}</div>
|
||||
{url && (
|
||||
<div className={s['popover-card-img']}>
|
||||
<Image src={url} {...imgProps} onLoad={onImageLoad} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children || (
|
||||
<IconGroupCardOutlined
|
||||
className={cls(className, s['popover-card-icon'])}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
.carousel-item {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface CarouselItemProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CarouselItem: React.FC<CarouselItemProps> = props => {
|
||||
const { children, className } = props;
|
||||
return (
|
||||
<div className={cls(styles['carousel-item'], className, 'carousel-item')}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
.carousel {
|
||||
position: relative;
|
||||
|
||||
.arrow-container {
|
||||
&::before {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
|
||||
width: 52px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.left {
|
||||
&::before {
|
||||
left: 0;
|
||||
background: linear-gradient(90deg,
|
||||
var(--coz-bg-max) 7.46%,
|
||||
rgba(255, 255, 255, 0%) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.right {
|
||||
&::before {
|
||||
right: 0;
|
||||
background: linear-gradient(270deg,
|
||||
var(--coz-bg-max) 7.46%,
|
||||
rgba(255, 255, 255, 0%) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
font-size: 20px;
|
||||
|
||||
background: var(--coz-mg-primary);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 16px 0 #0000001a;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.no-border {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.left-arrow {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right-arrow {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&-content {
|
||||
overflow: auto;
|
||||
flex: 1 0 auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
243
frontend/packages/studio/components/src/carousel/index.tsx
Normal file
243
frontend/packages/studio/components/src/carousel/index.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { chunk } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import {
|
||||
IconCozArrowRight,
|
||||
IconCozArrowLeft,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
|
||||
import { CarouselItem } from './carousel-item';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface CarouselProps {
|
||||
/** 元素布局行数默认为1 */
|
||||
rows?: number;
|
||||
/** 元素布局列数,默认为均分数组 */
|
||||
column?: number;
|
||||
/** 每次点击箭头滚动的百分比,0~1. 默认值为0.5 */
|
||||
scrollStep?: number;
|
||||
/** 滚动回调 */
|
||||
onScroll?: () => void;
|
||||
/** 箭头是否显示边框 */
|
||||
enableArrowBorder?: boolean;
|
||||
/** 箭头是否显示阴影渐变 */
|
||||
enableArrowShalldow?: boolean;
|
||||
/** 子元素样式 */
|
||||
itemClassName?: string;
|
||||
/** 左箭头样式 */
|
||||
leftArrowClassName?: string;
|
||||
/** 右箭头样式 */
|
||||
rightArrowClassName?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ArrowProps {
|
||||
className?: string;
|
||||
enableArrowBorder?: boolean;
|
||||
enableArrowShalldow?: boolean;
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const LeftArrow = ({
|
||||
enableArrowBorder,
|
||||
enableArrowShalldow,
|
||||
className,
|
||||
onClick,
|
||||
}: ArrowProps) => (
|
||||
<div
|
||||
className={cls(
|
||||
styles['arrow-container'],
|
||||
{ [styles.left]: enableArrowShalldow },
|
||||
'arrow-container-left',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cls(
|
||||
className,
|
||||
styles['left-arrow'],
|
||||
styles.arrow,
|
||||
'left-arrow',
|
||||
{
|
||||
[styles['no-border']]: !enableArrowBorder,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<IconCozArrowLeft />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const RightArrow = ({
|
||||
enableArrowBorder,
|
||||
enableArrowShalldow,
|
||||
className,
|
||||
onClick,
|
||||
}: ArrowProps) => (
|
||||
<div
|
||||
className={cls(
|
||||
styles['arrow-container'],
|
||||
{ [styles.right]: enableArrowShalldow },
|
||||
'arrow-container-right',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cls(
|
||||
className,
|
||||
styles['right-arrow'],
|
||||
styles.arrow,
|
||||
'right-arrow',
|
||||
{
|
||||
[styles['no-border']]: !enableArrowBorder,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<IconCozArrowRight />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Carousel: React.FC<CarouselProps> = ({
|
||||
rows = 1,
|
||||
column,
|
||||
itemClassName = '',
|
||||
leftArrowClassName = '',
|
||||
rightArrowClassName = '',
|
||||
children,
|
||||
enableArrowShalldow = true,
|
||||
scrollStep = 0.5,
|
||||
enableArrowBorder = true,
|
||||
onScroll,
|
||||
}) => {
|
||||
const itemsContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [leftArrowVisible, setLeftArrowVisible] = useState(false);
|
||||
const [rightArrowVisible, setRightArrowVisible] = useState(false);
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
const carouselItems = React.Children.map(
|
||||
children,
|
||||
(child: React.ReactNode, idx: number) => (
|
||||
<CarouselItem className={itemClassName} key={idx}>
|
||||
{child}
|
||||
</CarouselItem>
|
||||
),
|
||||
);
|
||||
const chunkedCarouselItems: React.ReactNode[][] = chunk(
|
||||
carouselItems,
|
||||
column ?? Math.ceil((carouselItems?.length || 0) / rows),
|
||||
);
|
||||
const rowItems = Array.from(Array(rows).fill(null)).map((_row, idx) => (
|
||||
<div className={cls(styles['carousel-row'], 'carousel-row')} key={idx}>
|
||||
{chunkedCarouselItems[idx]}
|
||||
</div>
|
||||
));
|
||||
const handleScrollLeft = () => {
|
||||
if (
|
||||
itemsContainerRef?.current?.scrollLeft !== undefined &&
|
||||
itemsContainerRef?.current?.clientWidth
|
||||
) {
|
||||
// 部分浏览器不支持 scrollTo 方法
|
||||
itemsContainerRef.current.scrollTo?.({
|
||||
left: Math.max(
|
||||
itemsContainerRef.current.scrollLeft -
|
||||
itemsContainerRef.current.clientWidth * scrollStep,
|
||||
0,
|
||||
),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleScrollRight = () => {
|
||||
const containWidth = itemsContainerRef?.current?.clientWidth ?? 0;
|
||||
if (itemsContainerRef?.current?.scrollLeft !== undefined && containWidth) {
|
||||
const scrollLeftMax =
|
||||
(itemsContainerRef?.current?.scrollWidth ?? 0) -
|
||||
(itemsContainerRef?.current?.clientWidth ?? 0);
|
||||
itemsContainerRef.current.scrollTo?.({
|
||||
left: Math.min(
|
||||
itemsContainerRef.current.scrollLeft + containWidth * scrollStep,
|
||||
scrollLeftMax,
|
||||
),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
|
||||
useEffect(() => {
|
||||
const updateArrowVisible = () => {
|
||||
const scrollLeft = Math.ceil(itemsContainerRef?.current?.scrollLeft ?? 0);
|
||||
const scrollRight =
|
||||
(itemsContainerRef?.current?.scrollWidth ?? 0) -
|
||||
(itemsContainerRef?.current?.clientWidth ?? 0) -
|
||||
scrollLeft;
|
||||
|
||||
const shouldShowArrowLeft = scrollLeft > 0;
|
||||
// 极端场景下存在 1px 偏差
|
||||
const shouldShowArrowRight = Math.abs(scrollRight) > 2;
|
||||
|
||||
setLeftArrowVisible(shouldShowArrowLeft);
|
||||
setRightArrowVisible(shouldShowArrowRight);
|
||||
};
|
||||
const scrollEvent = () => {
|
||||
onScroll?.();
|
||||
updateArrowVisible();
|
||||
};
|
||||
|
||||
// 初始化时判读一次是否显示箭头
|
||||
updateArrowVisible();
|
||||
itemsContainerRef?.current?.addEventListener('scroll', scrollEvent);
|
||||
window?.addEventListener('resize', updateArrowVisible);
|
||||
return () => {
|
||||
itemsContainerRef?.current?.removeEventListener('scroll', scrollEvent);
|
||||
window?.removeEventListener('resize', updateArrowVisible);
|
||||
};
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<div className={cls(styles.carousel, 'carousel')}>
|
||||
{leftArrowVisible ? (
|
||||
<LeftArrow
|
||||
onClick={handleScrollLeft}
|
||||
enableArrowBorder={enableArrowBorder}
|
||||
enableArrowShalldow={enableArrowShalldow}
|
||||
className={leftArrowClassName}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={cls(styles['carousel-content'], 'carousel-content')}
|
||||
ref={itemsContainerRef}
|
||||
>
|
||||
{rowItems}
|
||||
</div>
|
||||
{rightArrowVisible ? (
|
||||
<RightArrow
|
||||
onClick={handleScrollRight}
|
||||
enableArrowBorder={enableArrowBorder}
|
||||
enableArrowShalldow={enableArrowShalldow}
|
||||
className={rightArrowClassName}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type RefObject,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
type SetStateAction,
|
||||
type Dispatch,
|
||||
useState,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { noop, omit } from 'lodash-es';
|
||||
import { useSize } from 'ahooks';
|
||||
|
||||
interface TConfigItem {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
type ContextItems = Record<symbol, TConfigItem>;
|
||||
|
||||
interface CollapsibleIconButtonContextValue {
|
||||
showText: boolean;
|
||||
setItems: Dispatch<SetStateAction<ContextItems | undefined>>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 这是 context
|
||||
export const CollapsibleIconButtonContext =
|
||||
createContext<CollapsibleIconButtonContextValue>({
|
||||
showText: true,
|
||||
setItems: noop,
|
||||
});
|
||||
|
||||
export const useItem = (key: symbol, ref: RefObject<HTMLElement>) => {
|
||||
const { showText, setItems } = useContext(CollapsibleIconButtonContext);
|
||||
const size = useSize(ref);
|
||||
useEffect(() => {
|
||||
setItems(items => ({
|
||||
...items,
|
||||
[key]: { width: size?.width ?? 0 },
|
||||
}));
|
||||
}, [size?.width]);
|
||||
|
||||
// 组件销毁后移除
|
||||
useEffect(
|
||||
() => () => {
|
||||
setItems(items => omit(items, key));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return showText;
|
||||
};
|
||||
|
||||
export const useWrapper = (ref: RefObject<HTMLElement>, gap = 0) => {
|
||||
const [items, setItems] = useState<ContextItems>();
|
||||
const size = useSize(ref);
|
||||
|
||||
const totalWidth = Object.getOwnPropertySymbols(items || {}).reduce<number>(
|
||||
(res, key, index) =>
|
||||
res + (items?.[key]?.width ?? 0) + (index > 0 ? gap : 0),
|
||||
0,
|
||||
);
|
||||
const showText = !!size?.width && size.width >= totalWidth;
|
||||
|
||||
const contextValue = useMemo<CollapsibleIconButtonContextValue>(
|
||||
() => ({
|
||||
showText,
|
||||
setItems,
|
||||
}),
|
||||
[showText],
|
||||
);
|
||||
|
||||
return contextValue;
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
type FC,
|
||||
useRef,
|
||||
forwardRef,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { omit } from 'lodash-es';
|
||||
import {
|
||||
Button,
|
||||
type ButtonProps,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import { CollapsibleIconButtonContext, useWrapper, useItem } from './context';
|
||||
|
||||
/** 能让 Group 内的所有 CollapsibleIconButton 根据空余宽度自动展开(露出文案)收起(隐藏文案只剩图标) */
|
||||
export const CollapsibleIconButtonGroup: FC<
|
||||
PropsWithChildren<{
|
||||
/** @default 12 */
|
||||
gap?: number;
|
||||
}>
|
||||
> = ({ children, gap = 12 }) => {
|
||||
const wrapperDomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const contextValue = useWrapper(wrapperDomRef, gap);
|
||||
return (
|
||||
<div
|
||||
ref={wrapperDomRef}
|
||||
className="flex items-center justify-end flex-1 overflow-hidden"
|
||||
style={{ gap }}
|
||||
>
|
||||
<CollapsibleIconButtonContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</CollapsibleIconButtonContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollapsibleIconButton = forwardRef<
|
||||
HTMLSpanElement,
|
||||
{
|
||||
itemKey: symbol;
|
||||
text: string;
|
||||
} & ButtonProps
|
||||
>(({ itemKey, text, ...rest }, ref) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const showText = useItem(itemKey, contentRef);
|
||||
|
||||
return (
|
||||
<span ref={ref}>
|
||||
{/* 不可见时渲染到屏幕外侧,用于获取宽度 */}
|
||||
<div className={showText ? '' : 'fixed left-[-999px]'} ref={contentRef}>
|
||||
<Button
|
||||
size="default"
|
||||
color="secondary"
|
||||
// 不可见时不附带 testid,避免对 E2E 产生影响
|
||||
{...(showText ? rest : omit(rest, 'data-testid'))}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
</div>
|
||||
{!showText && (
|
||||
<Tooltip content={text}>
|
||||
<IconButton size="default" color="secondary" {...rest} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
/** 更为通用的版本 */
|
||||
export const Collapsible = forwardRef<
|
||||
HTMLSpanElement,
|
||||
{
|
||||
itemKey: symbol;
|
||||
collapsedContent: ReactNode;
|
||||
fullContent: ReactNode;
|
||||
collapsedTooltip?: ReactNode;
|
||||
}
|
||||
>(({ itemKey, fullContent, collapsedContent, collapsedTooltip }, ref) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const showFull = useItem(itemKey, contentRef);
|
||||
|
||||
return (
|
||||
<span ref={ref}>
|
||||
{/* 不可见时渲染到屏幕外侧,用于获取宽度 */}
|
||||
<div className={showFull ? '' : 'fixed left-[-999px]'} ref={contentRef}>
|
||||
{fullContent}
|
||||
</div>
|
||||
{!showFull && (
|
||||
<Tooltip
|
||||
trigger={collapsedTooltip ? 'hover' : 'custom'}
|
||||
content={collapsedTooltip}
|
||||
>
|
||||
<span>{collapsedContent}</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
/** 不会折叠,但参与宽度计算的元素 */
|
||||
export function PlaceholderContainer({
|
||||
itemKey,
|
||||
children,
|
||||
}: PropsWithChildren<{ itemKey: symbol }>) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
useItem(itemKey, ref);
|
||||
|
||||
return <span ref={ref}>{children}</span>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.coze-brand {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
& > svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
71
frontend/packages/studio/components/src/coze-brand/index.tsx
Normal file
71
frontend/packages/studio/components/src/coze-brand/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
IconBrandCnWhiteRow,
|
||||
IconBrandCnBlackRow,
|
||||
IconBrandEnBlackRow,
|
||||
} from '@coze-arch/bot-icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface CozeBrandProps {
|
||||
isOversea: boolean;
|
||||
isWhite?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function CozeBrand({
|
||||
isOversea,
|
||||
isWhite,
|
||||
className,
|
||||
style,
|
||||
}: CozeBrandProps) {
|
||||
const navigate = useNavigate();
|
||||
const navBack = () => {
|
||||
navigate('/');
|
||||
};
|
||||
if (isOversea) {
|
||||
return (
|
||||
<IconBrandEnBlackRow
|
||||
onClick={navBack}
|
||||
className={classNames(styles['coze-brand'], className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isWhite) {
|
||||
return (
|
||||
<IconBrandCnWhiteRow
|
||||
onClick={navBack}
|
||||
className={classNames(styles['coze-brand'], className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<IconBrandCnBlackRow
|
||||
onClick={navBack}
|
||||
className={classNames(styles['coze-brand'], className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { DndProvider as Provider } from 'react-dnd';
|
||||
import { type ReactNode, createContext, useContext } from 'react';
|
||||
const DnDContext = createContext<{
|
||||
isInProvider: boolean;
|
||||
}>({
|
||||
isInProvider: false,
|
||||
});
|
||||
export const DndProvider = ({ children }: { children: ReactNode }) => {
|
||||
const context = useContext(DnDContext);
|
||||
return (
|
||||
<DnDContext.Provider
|
||||
value={{
|
||||
isInProvider: true,
|
||||
}}
|
||||
>
|
||||
{context.isInProvider ? (
|
||||
children
|
||||
) : (
|
||||
<Provider backend={HTML5Backend} context={window}>
|
||||
{children}
|
||||
</Provider>
|
||||
)}
|
||||
</DnDContext.Provider>
|
||||
);
|
||||
};
|
||||
338
frontend/packages/studio/components/src/duplicate-bot/index.tsx
Normal file
338
frontend/packages/studio/components/src/duplicate-bot/index.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, useState } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
|
||||
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
|
||||
import {
|
||||
REPORT_EVENTS as ReportEventNames,
|
||||
createReportEvent,
|
||||
} from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { openNewWindow, getParamsFromQuery } from '@coze-arch/bot-utils';
|
||||
import { BotPageFromEnum } from '@coze-arch/bot-typings/common';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import { useSpaceList, useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
import { SpaceApi } from '@coze-arch/bot-space-api';
|
||||
import { type Size } from '@coze-arch/bot-semi/Button';
|
||||
import { UIButton, Toast } from '@coze-arch/bot-semi';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import {
|
||||
ProductEntityType,
|
||||
type ProductMetaInfo,
|
||||
} from '@coze-arch/bot-api/product_api';
|
||||
import { DeveloperApi, PlaygroundApi, ProductApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { SelectSpaceModal } from '../select-space-modal';
|
||||
|
||||
const botDuplicateEvent = createReportEvent({
|
||||
eventName: ReportEventNames.botDuplicate,
|
||||
});
|
||||
|
||||
interface DuplicateBotProps {
|
||||
storeCategory?: ProductMetaInfo['category'];
|
||||
botName?: string;
|
||||
botID?: string;
|
||||
isDisabled?: boolean;
|
||||
btnTxt?: string;
|
||||
pageFrom?: BotPageFromEnum;
|
||||
version?: string;
|
||||
buttonSize?: Size;
|
||||
enableCozeDesign?: boolean;
|
||||
/**
|
||||
* cozeDesign 的情况下才生效
|
||||
*/
|
||||
isBlock?: boolean;
|
||||
eventCallbacks?: Partial<{
|
||||
clickButton: () => void;
|
||||
duplicateFinished: ({ newBotId }: { newBotId: string }) => void;
|
||||
}>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line -- Needs to be refactored
|
||||
export const DuplicateBot: FC<DuplicateBotProps> = ({
|
||||
storeCategory,
|
||||
botName,
|
||||
botID,
|
||||
isDisabled,
|
||||
btnTxt,
|
||||
pageFrom,
|
||||
version,
|
||||
buttonSize,
|
||||
enableCozeDesign,
|
||||
isBlock,
|
||||
eventCallbacks,
|
||||
}) => {
|
||||
const {
|
||||
space: { hide_operation, id: spaceID },
|
||||
getPersonalSpaceID,
|
||||
} = useSpaceStore();
|
||||
const { spaces: list = [] } = useSpaceList();
|
||||
|
||||
const { pageFromFromStore } = usePageRuntimeStore(
|
||||
useShallow(state => ({
|
||||
pageFromFromStore: state.pageFrom,
|
||||
})),
|
||||
);
|
||||
const { botIdFromStore, botNameFromStore } = useBotInfoStore(
|
||||
useShallow(state => ({
|
||||
botIdFromStore: state.botId,
|
||||
botNameFromStore: state.name,
|
||||
})),
|
||||
);
|
||||
const [showSpaceModal, setShowSpaceModal] = useState(false);
|
||||
|
||||
const { runAsync: copyAndOpenBot } = useRequest(
|
||||
// eslint-disable-next-line complexity
|
||||
async (targetSpaceId?: string, name?: string): Promise<string> => {
|
||||
botDuplicateEvent.start();
|
||||
|
||||
let resp: {
|
||||
code?: string | number;
|
||||
msg?: string;
|
||||
data?: { bot_id?: string };
|
||||
};
|
||||
if (
|
||||
(pageFrom === BotPageFromEnum.Store ||
|
||||
pageFrom === BotPageFromEnum.Template) &&
|
||||
botID &&
|
||||
version &&
|
||||
targetSpaceId
|
||||
) {
|
||||
if (pageFrom === BotPageFromEnum.Template) {
|
||||
const {
|
||||
code,
|
||||
message,
|
||||
data: { new_entity_id: newBotId } = {},
|
||||
} = await ProductApi.PublicDuplicateProduct({
|
||||
product_id: botID,
|
||||
entity_type: ProductEntityType.BotTemplate,
|
||||
space_id: targetSpaceId,
|
||||
name: name ?? '',
|
||||
});
|
||||
resp = {
|
||||
code,
|
||||
msg: message,
|
||||
data: {
|
||||
bot_id: newBotId,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
resp = await PlaygroundApi.DuplicateBotVersionToSpace({
|
||||
bot_id: botID,
|
||||
version,
|
||||
target_space_id: targetSpaceId,
|
||||
name: name ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
//复制完成,关闭空间弹窗
|
||||
setShowSpaceModal(false);
|
||||
} else if (pageFromFromStore === BotPageFromEnum.Explore) {
|
||||
//explore时可以复制到某个空间下
|
||||
resp = await DeveloperApi.DuplicateBotToSpace({
|
||||
draft_bot_id: botIdFromStore,
|
||||
target_space_id: targetSpaceId || '',
|
||||
name,
|
||||
});
|
||||
|
||||
//复制完成,关闭空间弹窗
|
||||
setShowSpaceModal(false);
|
||||
} else {
|
||||
resp = await SpaceApi.DuplicateDraftBot({
|
||||
bot_id: botIdFromStore,
|
||||
});
|
||||
}
|
||||
|
||||
eventCallbacks?.duplicateFinished?.({
|
||||
newBotId: resp.data?.bot_id ?? '',
|
||||
});
|
||||
|
||||
const botTeaparams = {
|
||||
bot_type:
|
||||
pageFromFromStore === BotPageFromEnum.Explore ||
|
||||
pageFromFromStore === BotPageFromEnum.Store
|
||||
? 'store_bot'
|
||||
: 'team_bot',
|
||||
bot_id: botID ?? botIdFromStore,
|
||||
workspace_type:
|
||||
pageFromFromStore === BotPageFromEnum.Store
|
||||
? 'store_workspace'
|
||||
: getPersonalSpaceID() === targetSpaceId
|
||||
? 'personal_workspace'
|
||||
: 'team_workspace',
|
||||
bot_name: botName ?? botNameFromStore ?? '',
|
||||
};
|
||||
if (resp.code === 0) {
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_result, {
|
||||
...botTeaparams,
|
||||
result: 'success',
|
||||
});
|
||||
} else {
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_result, {
|
||||
...botTeaparams,
|
||||
result: 'failed',
|
||||
error_code: resp.code,
|
||||
error_message: resp.msg,
|
||||
});
|
||||
}
|
||||
|
||||
const respData = resp.data;
|
||||
|
||||
if (!respData) {
|
||||
throw new CustomError(
|
||||
ReportEventNames.botDuplicate,
|
||||
I18n.t('bot_copy_info_error'),
|
||||
);
|
||||
}
|
||||
const { bot_id: botId } = respData;
|
||||
if (!botID && !botIdFromStore) {
|
||||
throw new CustomError(
|
||||
ReportEventNames.botDuplicate,
|
||||
I18n.t('bot_copy_id_error'),
|
||||
);
|
||||
}
|
||||
|
||||
const url = `${location.origin}/space/${
|
||||
targetSpaceId || spaceID
|
||||
}/bot/${botId}?from=copy`;
|
||||
|
||||
return url;
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
botDuplicateEvent.success();
|
||||
},
|
||||
onError: e => {
|
||||
botDuplicateEvent.error({ error: e, reason: e.message });
|
||||
setShowSpaceModal(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const beforeCopyClick = () => {
|
||||
eventCallbacks?.clickButton?.();
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_click, {
|
||||
bot_type:
|
||||
pageFromFromStore === BotPageFromEnum.Bot ? 'team_bot' : 'store_bot',
|
||||
});
|
||||
|
||||
if (pageFrom === BotPageFromEnum.Store) {
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_click_front, {
|
||||
bot_type: 'store_bot',
|
||||
bot_id: botID,
|
||||
bot_name: botName,
|
||||
category_id: storeCategory?.id,
|
||||
category_name: storeCategory?.name,
|
||||
source: 'bots_store',
|
||||
from: getParamsFromQuery({ key: 'from' }),
|
||||
});
|
||||
setShowSpaceModal(true);
|
||||
} else if (pageFromFromStore === BotPageFromEnum.Explore) {
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_click_front, {
|
||||
bot_type: 'store_bot',
|
||||
bot_id: botNameFromStore,
|
||||
bot_name: botNameFromStore,
|
||||
source: 'explore_bot_detailpage',
|
||||
from: 'explore_card',
|
||||
});
|
||||
sendTeaEvent(EVENT_NAMES.click_bot_duplicate, {
|
||||
bot_id: botIdFromStore,
|
||||
bot_name: botNameFromStore,
|
||||
from: 'explore_card',
|
||||
source: 'explore_bot_detailpage',
|
||||
});
|
||||
//探索页面来源:team>1时选择copy 空间,否则copy到个人空间
|
||||
if (list.length === 1) {
|
||||
openNewWindow(() => copyAndOpenBot(list?.[0].id));
|
||||
} else {
|
||||
setShowSpaceModal(true);
|
||||
}
|
||||
} else if (pageFrom === BotPageFromEnum.Template) {
|
||||
//探索页面来源:team>1时选择copy 空间,否则copy到个人空间
|
||||
if (list.length === 1) {
|
||||
openNewWindow(() => copyAndOpenBot(list?.[0].id));
|
||||
} else {
|
||||
setShowSpaceModal(true);
|
||||
}
|
||||
} else {
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_click_front, {
|
||||
bot_type: 'team_bot',
|
||||
bot_id: botIdFromStore,
|
||||
bot_name: botNameFromStore,
|
||||
source: 'bots_detailpage',
|
||||
from: 'bots_card',
|
||||
});
|
||||
// bot页面来源:若有操作权限直接copy到当前空间下
|
||||
if (hide_operation) {
|
||||
Toast.warning('Bot in public space cannot duplicate');
|
||||
return;
|
||||
} else {
|
||||
openNewWindow(copyAndOpenBot);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{enableCozeDesign ? (
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
size={buttonSize}
|
||||
onClick={beforeCopyClick}
|
||||
disabled={isDisabled}
|
||||
block={isBlock}
|
||||
>
|
||||
{btnTxt || I18n.t('duplicate')}
|
||||
</Button>
|
||||
) : (
|
||||
<UIButton
|
||||
type="primary"
|
||||
theme="solid"
|
||||
size={buttonSize}
|
||||
onClick={beforeCopyClick}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{btnTxt || I18n.t('duplicate')}
|
||||
</UIButton>
|
||||
)}
|
||||
|
||||
{/* 选择空间弹窗 */}
|
||||
<SelectSpaceModal
|
||||
botName={botName ?? botNameFromStore}
|
||||
visible={showSpaceModal}
|
||||
onCancel={() => {
|
||||
setShowSpaceModal(false);
|
||||
}}
|
||||
onConfirm={(id, name) => {
|
||||
sendTeaEvent(EVENT_NAMES.click_create_bot_confirm, {
|
||||
click: 'success',
|
||||
create_type: 'duplicate',
|
||||
from: 'explore_card',
|
||||
source: 'explore_bot_detailpage',
|
||||
});
|
||||
openNewWindow(() => copyAndOpenBot(id, name));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
const isMacOS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(
|
||||
navigator.userAgent,
|
||||
);
|
||||
|
||||
export const SHORTCUTS = {
|
||||
CTRL: isMacOS ? '⌘' : 'Ctrl',
|
||||
SHIFT: isMacOS ? '⇧' : '⇧',
|
||||
ALT: isMacOS ? '⌥' : 'Alt',
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
.item {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: #1D1C23;
|
||||
}
|
||||
|
||||
.itemTitle {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.itemContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type ReactNode } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { getIsIPad } from '@coze-arch/bot-utils';
|
||||
import { Divider, Typography, Tag } from '@coze-arch/bot-semi';
|
||||
import { IconCloseNoCycle } from '@coze-arch/bot-icons';
|
||||
|
||||
import { SHORTCUTS } from './constants';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface ShortcutItemProps {
|
||||
title: ReactNode;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
function ShortcutItem({ title, children }: ShortcutItemProps) {
|
||||
return (
|
||||
<div className={s.item}>
|
||||
<div className={s.itemTitle}>{title}</div>
|
||||
<div className={s.itemContent}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DividerWithMargin() {
|
||||
return <Divider style={{ margin: '12px 0' }} />;
|
||||
}
|
||||
|
||||
function ShortcutTag({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Tag
|
||||
style={{
|
||||
margin: '0 4px',
|
||||
height: 24,
|
||||
padding: '0 8px',
|
||||
fontSize: 14,
|
||||
backgroundColor: '#f0f0f5',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
interface FlowShortcutsHelpProps {
|
||||
closable?: boolean;
|
||||
onClose?: () => void;
|
||||
isAgentFlow?: boolean;
|
||||
}
|
||||
|
||||
const isIPad = getIsIPad();
|
||||
|
||||
function FlowShortcutsHelp(props: FlowShortcutsHelpProps) {
|
||||
const { closable = false, onClose, isAgentFlow = false } = props;
|
||||
return (
|
||||
<>
|
||||
{closable ? (
|
||||
<div className={s.close} onClick={() => onClose?.()}>
|
||||
<IconCloseNoCycle />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Typography.Title heading={5} style={{ marginBottom: 16 }}>
|
||||
{I18n.t('flowcanvas_shortcuts_shortcuts')}
|
||||
</Typography.Title>
|
||||
|
||||
<ShortcutItem title={I18n.t('flowcanvas_shortcuts_move_canvas')}>
|
||||
<ShortcutTag>{I18n.t('flowcanvas_shortcuts_space')}</ShortcutTag>
|
||||
<ShortcutTag>{I18n.t('flowcanvas_shortcuts_drag')}</ShortcutTag>
|
||||
</ShortcutItem>
|
||||
|
||||
<DividerWithMargin />
|
||||
|
||||
<ShortcutItem
|
||||
title={
|
||||
<>
|
||||
{I18n.t('flowcanvas_shortcuts_multiple_select')}/
|
||||
{I18n.t('flowcanvas_shortcuts_multiple_deselect')}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ShortcutTag>
|
||||
{SHORTCUTS.CTRL}/{SHORTCUTS.SHIFT}
|
||||
</ShortcutTag>
|
||||
<ShortcutTag>{I18n.t('flowcanvas_shortcuts_click')}</ShortcutTag>
|
||||
</ShortcutItem>
|
||||
|
||||
<DividerWithMargin />
|
||||
|
||||
<ShortcutItem title={I18n.t('flowcanvas_shortcuts_zoom_in')}>
|
||||
<ShortcutTag>{SHORTCUTS.CTRL}</ShortcutTag>
|
||||
<ShortcutTag>+</ShortcutTag>
|
||||
<span style={{ margin: '0 6px' }}>
|
||||
{I18n.t('flowcanvas_shortcuts_or')}
|
||||
</span>
|
||||
<ShortcutTag>{SHORTCUTS.CTRL}</ShortcutTag>
|
||||
<ShortcutTag>{I18n.t('flowcanvas_shortcuts_scroll')}</ShortcutTag>
|
||||
</ShortcutItem>
|
||||
|
||||
<DividerWithMargin />
|
||||
|
||||
<ShortcutItem title={I18n.t('flowcanvas_shortcuts_zoom_out')}>
|
||||
<ShortcutTag>{SHORTCUTS.CTRL}</ShortcutTag>
|
||||
<ShortcutTag>-</ShortcutTag>
|
||||
<span style={{ margin: '0 6px' }}>
|
||||
{I18n.t('flowcanvas_shortcuts_or')}
|
||||
</span>
|
||||
<ShortcutTag>{SHORTCUTS.CTRL}</ShortcutTag>
|
||||
<ShortcutTag>{I18n.t('flowcanvas_shortcuts_scroll')}</ShortcutTag>
|
||||
</ShortcutItem>
|
||||
|
||||
<DividerWithMargin />
|
||||
|
||||
{isIPad || isAgentFlow ? null : (
|
||||
<>
|
||||
<ShortcutItem title={I18n.t('flowcanvas_shortcuts_duplicate')}>
|
||||
<ShortcutTag>{SHORTCUTS.ALT}</ShortcutTag>
|
||||
<ShortcutTag>{I18n.t('flowcanvas_shortcuts_drag')}</ShortcutTag>
|
||||
</ShortcutItem>
|
||||
<DividerWithMargin />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ShortcutItem title={I18n.t('flowcanvas_shortcuts_copy')}>
|
||||
<ShortcutTag>{SHORTCUTS.CTRL}</ShortcutTag>
|
||||
<ShortcutTag>C</ShortcutTag>
|
||||
</ShortcutItem>
|
||||
|
||||
<DividerWithMargin />
|
||||
|
||||
<ShortcutItem title={I18n.t('flowcanvas_shortcuts_paste')}>
|
||||
<ShortcutTag>{SHORTCUTS.CTRL}</ShortcutTag>
|
||||
<ShortcutTag>V</ShortcutTag>
|
||||
</ShortcutItem>
|
||||
|
||||
<DividerWithMargin />
|
||||
|
||||
<UndoRedoShortcuts />
|
||||
|
||||
<ShortcutItem title={I18n.t('flowcanvas_shortcuts_delete')}>
|
||||
<ShortcutTag>{I18n.t('flowcanvas_shortcuts_backspace')}</ShortcutTag>
|
||||
</ShortcutItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UndoRedoShortcuts() {
|
||||
return (
|
||||
<>
|
||||
<ShortcutItem title={I18n.t('workflow_detail_undo_tooltip')}>
|
||||
<ShortcutTag>{SHORTCUTS.CTRL}</ShortcutTag>
|
||||
<ShortcutTag>Z</ShortcutTag>
|
||||
</ShortcutItem>
|
||||
|
||||
<DividerWithMargin />
|
||||
|
||||
<ShortcutItem title={I18n.t('workflow_detail_redo_tooltip')}>
|
||||
<ShortcutTag>{SHORTCUTS.CTRL}</ShortcutTag>
|
||||
<ShortcutTag>{SHORTCUTS.SHIFT}</ShortcutTag>
|
||||
<ShortcutTag>Z</ShortcutTag>
|
||||
</ShortcutItem>
|
||||
|
||||
<DividerWithMargin />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { FlowShortcutsHelp };
|
||||
@@ -0,0 +1,35 @@
|
||||
.btn {
|
||||
min-width: 80px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
&.grey {
|
||||
background: linear-gradient(90deg, rgba(var(--coze-brand-1), var(--coze-brand-1-alpha)) 0%, rgba(var(--coze-purple-1), var(--coze-purple-1-alpha)) 100%) !important;
|
||||
|
||||
:global {
|
||||
.coz-ai-button-icon {
|
||||
color: rgba(var(--coze-brand-3), var(--coze-brand-3-alpha)) !important;
|
||||
}
|
||||
|
||||
.coz-ai-button-text {
|
||||
background: linear-gradient(90deg, rgba(var(--coze-brand-3), var(--coze-brand-3-alpha)) 0%, rgba(var(--coze-purple-3), var(--coze-purple-3-alpha)) 100%) !important;
|
||||
background-clip: text !important;
|
||||
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.generate-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.icon-icon-coz_loading {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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, useState, useEffect } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { AIButton, Tooltip } from '@coze-arch/coze-design';
|
||||
import { PicType } from '@coze-arch/bot-api/playground_api';
|
||||
import { PlaygroundApi } from '@coze-arch/bot-api';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface GenerateButtonProps {
|
||||
scene?: 'gif' | 'static_image';
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: 'small' | 'default' | 'large';
|
||||
text?: string;
|
||||
cancelText?: string;
|
||||
tooltipText?: string;
|
||||
className?: string;
|
||||
transparent?: boolean;
|
||||
style?: CSSProperties;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onCancel?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export const GenerateButton: React.FC<GenerateButtonProps> = ({
|
||||
scene,
|
||||
loading = false,
|
||||
disabled: outerDisabled = false,
|
||||
tooltipText: outerTooltipText,
|
||||
className,
|
||||
transparent = false,
|
||||
style,
|
||||
onCancel,
|
||||
onClick,
|
||||
size,
|
||||
text,
|
||||
cancelText,
|
||||
}) => {
|
||||
const [exceedImageGenCountLimit, setExceedImageGenCountLimit] =
|
||||
useState(false);
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const handleClick = loading ? onCancel : onClick;
|
||||
const innerLoading = hovering ? false : loading;
|
||||
const disabled = exceedImageGenCountLimit || outerDisabled;
|
||||
const exceedLimitTooltipText = {
|
||||
gif: I18n.t('profilepicture_popup_toast_daymax_gif'),
|
||||
static_image: I18n.t('profilepicture_popup_toast_daymax_image'),
|
||||
};
|
||||
const tooltipText =
|
||||
exceedImageGenCountLimit && scene
|
||||
? exceedLimitTooltipText[scene]
|
||||
: outerTooltipText;
|
||||
const getGenPicTimes = async () => {
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { data } = await PlaygroundApi.GetGenPicTimes();
|
||||
if (data?.infos) {
|
||||
let gifCount = 0;
|
||||
let staticImageCount = 0;
|
||||
data.infos.forEach(({ type, times }) => {
|
||||
if (
|
||||
[PicType.IconGif, PicType.BackgroundGif].includes(type as PicType)
|
||||
) {
|
||||
gifCount += times || 0;
|
||||
} else if (
|
||||
[PicType.IconStatic, PicType.BackgroundStatic].includes(
|
||||
type as PicType,
|
||||
)
|
||||
) {
|
||||
staticImageCount += times || 0;
|
||||
}
|
||||
});
|
||||
if (
|
||||
(scene === 'gif' && gifCount >= 10) ||
|
||||
(scene === 'static_image' && staticImageCount >= 20)
|
||||
) {
|
||||
// 达到上限,禁用按钮
|
||||
setExceedImageGenCountLimit(true);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @coze-arch/no-empty-catch
|
||||
} catch (error) {
|
||||
// empty
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
// 获取图片限制,每天限制10个gif,20个静态图,根据scene来判断是否达到上限
|
||||
if (!loading) {
|
||||
getGenPicTimes();
|
||||
}
|
||||
}, [loading]);
|
||||
const button = (
|
||||
<AIButton
|
||||
color="aihglt"
|
||||
className={classNames(s.btn, {
|
||||
[s.grey]: disabled || (loading && !hovering),
|
||||
})}
|
||||
style={
|
||||
disabled
|
||||
? { cursor: 'not-allowed', ...style }
|
||||
: { cursor: 'pointer', ...style }
|
||||
}
|
||||
loading={innerLoading}
|
||||
size={size}
|
||||
onClick={disabled ? undefined : handleClick}
|
||||
>
|
||||
{hovering && loading
|
||||
? cancelText || I18n.t('profilepicture_popup_cancel')
|
||||
: text || I18n.t('profilepicture_popup_generate')}
|
||||
</AIButton>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'pointer-events-auto inline-block leading-none rounded-lg',
|
||||
{
|
||||
'coz-bg-max': !transparent,
|
||||
},
|
||||
)}
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
>
|
||||
{tooltipText ? <Tooltip content={tooltipText}>{button}</Tooltip> : button}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 classNames from 'classnames';
|
||||
import { withSlardarIdButton } from '@coze-studio/bot-utils';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozImage, IconCozUpload } from '@coze-arch/coze-design/icons';
|
||||
import { Button, Image, Popover, Toast, Upload } from '@coze-arch/coze-design';
|
||||
import { FileBizType } from '@coze-arch/bot-api/developer_api';
|
||||
import { customUploadRequest } from '@coze-common/biz-components/picture-upload';
|
||||
|
||||
import { type ImageItem, ImageList } from '../image-list';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface ImagePickerProps {
|
||||
setImage: (image: ImageItem) => void;
|
||||
imageList: ImageItem[];
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export default function ImagePicker(props: ImagePickerProps) {
|
||||
const { setImage, imageList, url } = props;
|
||||
const [uploadBtnLoading, setUploadBtnLoading] = useState(false);
|
||||
return (
|
||||
<div className={s['image-ctn']}>
|
||||
<Popover
|
||||
position="right"
|
||||
trigger="hover"
|
||||
keepDOM
|
||||
content={
|
||||
<div className={s['upload-panel']}>
|
||||
<ImageList
|
||||
data={imageList || []}
|
||||
className={s['image-list']}
|
||||
imageItemClassName={s['image-item']}
|
||||
showDeleteIcon={false}
|
||||
showSelectedIcon={false}
|
||||
onClick={({ item }) => {
|
||||
setImage(item);
|
||||
}}
|
||||
/>
|
||||
<Upload
|
||||
action=""
|
||||
limit={1}
|
||||
customRequest={options => {
|
||||
customUploadRequest({
|
||||
...options,
|
||||
fileBizType: FileBizType.BIZ_BOT_ICON,
|
||||
onSuccess(data) {
|
||||
setImage({
|
||||
img_info: {
|
||||
tar_uri: data?.upload_uri || '',
|
||||
tar_url: data?.upload_url || '',
|
||||
},
|
||||
});
|
||||
},
|
||||
beforeUploadCustom() {
|
||||
setUploadBtnLoading(true);
|
||||
},
|
||||
afterUploadCustom() {
|
||||
setUploadBtnLoading(false);
|
||||
},
|
||||
});
|
||||
}}
|
||||
fileList={[]}
|
||||
accept=".jpeg,.jpg,.png"
|
||||
showReplace={false}
|
||||
showUploadList={false}
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
maxSize={2 * 1024}
|
||||
onSizeError={() => {
|
||||
Toast.error({
|
||||
// starling 切换
|
||||
content: withSlardarIdButton(
|
||||
I18n.t(
|
||||
'dataset_upload_image_warning',
|
||||
{},
|
||||
'Please upload an image less than 2MB',
|
||||
),
|
||||
),
|
||||
showClose: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
loading={uploadBtnLoading}
|
||||
icon={<IconCozUpload />}
|
||||
>
|
||||
{I18n.t('creat_popup_profilepicture_upload')}
|
||||
</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{url ? (
|
||||
<div>
|
||||
<Image src={url} className={s['show-image']} preview={false} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={classNames(s['empty-image'])}>
|
||||
<IconCozImage />
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
.ctn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.image-ctn {
|
||||
line-height: 0;
|
||||
|
||||
.show-image img,
|
||||
.empty-image {
|
||||
width: 108px;
|
||||
height: 108px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.empty-image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 20px;
|
||||
color: var(--coz-fg-secondary, #06070980);
|
||||
|
||||
background-color: var(--coz-mg-primary, #0607090A);
|
||||
}
|
||||
}
|
||||
|
||||
.text-ctn {
|
||||
position: relative;
|
||||
flex: 1 0 0;
|
||||
height: 108px;
|
||||
margin-left: 16px;
|
||||
|
||||
:global {
|
||||
.semi-input-textarea-wrapper {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
.semi-input-textarea {
|
||||
height: 108px;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.semi-input-textarea::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-panel {
|
||||
padding: 16px;
|
||||
|
||||
background: var(--coz-bg-max, #FFF);
|
||||
border: 0.5px solid var(--coz-stroke-primary, rgba(6, 7, 9, 10%));
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 25%);
|
||||
|
||||
.image-list {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.image-item {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-button-content-right {
|
||||
margin-left: 4px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
153
frontend/packages/studio/components/src/generate-gif/index.tsx
Normal file
153
frontend/packages/studio/components/src/generate-gif/index.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 { useParams } from 'react-router-dom';
|
||||
import { type CSSProperties, useMemo } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { avatarBackgroundWebSocket } from '@coze-studio/bot-detail-store';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import { PicType } from '@coze-arch/bot-api/playground_api';
|
||||
import { PlaygroundApi } from '@coze-arch/bot-api';
|
||||
import webSocketManager from '@coze-common/websocket-manager-adapter';
|
||||
import { TextArea } from '@coze-arch/coze-design';
|
||||
|
||||
import { type ImageItem } from '../image-list';
|
||||
import { GenerateButton } from '../generate-button';
|
||||
import ImagePicker from './image-picker';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface GenerateGifProps {
|
||||
scene: 'background' | 'avatar';
|
||||
image: ImageItem; // 默认图片
|
||||
text: string; // 默认文本
|
||||
loading: boolean; // 生成时,socket全局监听服务端响应,因此loading需要受控
|
||||
imageList?: ImageItem[]; // 图片候选列表(只包含静态图)
|
||||
generatingTaskId: string; // 生成中的任务id
|
||||
exceedMaxImageCount?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
setImage: (image: ImageItem) => void;
|
||||
setText: (text: string) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setGeneratingTaskId: (id: string) => void;
|
||||
}
|
||||
|
||||
export const GenerateGif: React.FC<GenerateGifProps> = ({
|
||||
image = {
|
||||
id: '',
|
||||
img_info: {
|
||||
tar_uri: '',
|
||||
tar_url: '',
|
||||
},
|
||||
},
|
||||
text = '',
|
||||
loading,
|
||||
imageList = [],
|
||||
exceedMaxImageCount = false,
|
||||
className,
|
||||
generatingTaskId,
|
||||
style,
|
||||
setLoading,
|
||||
setImage,
|
||||
setText,
|
||||
setGeneratingTaskId,
|
||||
scene,
|
||||
}) => {
|
||||
const { bot_id = '0' } = useParams<DynamicParams>();
|
||||
|
||||
const { tar_url: url, tar_uri: uri } = image.img_info ?? {};
|
||||
const hasEmptyValue = !image?.img_info?.tar_url || !text?.trim();
|
||||
const filterImageList = useMemo(
|
||||
() => imageList.filter(item => item?.img_info?.tar_url !== url),
|
||||
[imageList, url],
|
||||
);
|
||||
let tooltipText = '';
|
||||
if (exceedMaxImageCount) {
|
||||
tooltipText = I18n.t('profilepicture_popup_toast_picturemax');
|
||||
}
|
||||
return (
|
||||
<div className={classNames(s.ctn, className)} style={style}>
|
||||
<ImagePicker setImage={setImage} url={url} imageList={filterImageList} />
|
||||
<div className={s['text-ctn']}>
|
||||
<TextArea
|
||||
rows={5}
|
||||
value={text}
|
||||
maxLength={400}
|
||||
className={s['text-area']}
|
||||
placeholder={I18n.t('profilepicture_popup_generategif_default')}
|
||||
onChange={value => {
|
||||
setText(value);
|
||||
}}
|
||||
/>
|
||||
<GenerateButton
|
||||
scene="gif"
|
||||
className={s['generate-btn']}
|
||||
disabled={exceedMaxImageCount || hasEmptyValue}
|
||||
tooltipText={tooltipText}
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
setLoading?.(true);
|
||||
try {
|
||||
avatarBackgroundWebSocket.createConnection();
|
||||
// 只负责发送请求,响应在WebSocket中接收
|
||||
const { data } = await PlaygroundApi.GeneratePic({
|
||||
gen_prompt: {
|
||||
ori_prompt: text.trim(),
|
||||
},
|
||||
image_uri: uri,
|
||||
image_url: url,
|
||||
pic_type:
|
||||
scene === 'avatar' ? PicType.IconGif : PicType.BackgroundGif,
|
||||
bot_id,
|
||||
device_id: webSocketManager.deviceId,
|
||||
});
|
||||
if (data?.task_id) {
|
||||
setGeneratingTaskId?.(data.task_id);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
eventName: 'fail_to_generate_gif',
|
||||
error: error as Error,
|
||||
});
|
||||
setLoading?.(false);
|
||||
setGeneratingTaskId?.('');
|
||||
}
|
||||
}}
|
||||
onCancel={async () => {
|
||||
if (generatingTaskId) {
|
||||
try {
|
||||
const { code } = await PlaygroundApi.CancelGenerateGif({
|
||||
task_id: generatingTaskId,
|
||||
});
|
||||
if (code === 0) {
|
||||
setLoading?.(false);
|
||||
}
|
||||
} catch (error) {
|
||||
const e =
|
||||
error instanceof Error ? error : new Error(error as string);
|
||||
logger.error({ error: e });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
.segment-tab {
|
||||
:global {
|
||||
.semi-radio-content {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozArrowLeft } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Collapsible,
|
||||
IconButton,
|
||||
SegmentTab,
|
||||
Space,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import { type TabItem } from './type';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface GenerateImageTabProps {
|
||||
// tab列表
|
||||
tabs: TabItem[];
|
||||
// 是否可折叠
|
||||
enableCollapsible?: boolean;
|
||||
// 当前激活的tab
|
||||
activeKey?: string;
|
||||
// 当前激活的tab变化回调
|
||||
onTabChange?: (tabKey: string) => void;
|
||||
// 是否展示wait文案
|
||||
showWaitTip?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export enum GenerateType {
|
||||
Static = 'static',
|
||||
Gif = 'gif',
|
||||
}
|
||||
|
||||
export const GenerateImageTab: React.FC<GenerateImageTabProps> = ({
|
||||
tabs = [],
|
||||
enableCollapsible = false,
|
||||
activeKey,
|
||||
onTabChange,
|
||||
showWaitTip = true,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [isOpen, setOpen] = useState(true);
|
||||
const toggle = () => {
|
||||
setOpen(!isOpen);
|
||||
};
|
||||
|
||||
// tabPane 不卸载
|
||||
const component = (
|
||||
<div>
|
||||
{tabs.map(item => (
|
||||
<div
|
||||
key={item.value}
|
||||
className={classNames({
|
||||
hidden: activeKey !== item.value,
|
||||
'border-0 border-t border-solid mt-2 coz-stroke-primary': isOpen,
|
||||
})}
|
||||
>
|
||||
{item.component}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'border border-solid coz-stroke-plus coz-bg-max rounded-md w-full coz-fg-plus mt-3 pt-2 pb-4 px-4',
|
||||
{
|
||||
'coz-bg-primary pointer-events-none': disabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className=" flex items-center gap-2 justify-between ">
|
||||
<Space>
|
||||
{enableCollapsible ? (
|
||||
<IconButton
|
||||
className="!bg-transparent hover:!coz-mg-primary-hovered"
|
||||
icon={
|
||||
<IconCozArrowLeft
|
||||
className={classNames(
|
||||
isOpen ? 'rotate-90' : '-rotate-90',
|
||||
'coz-fg-secondary',
|
||||
)}
|
||||
onClick={toggle}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<SegmentTab
|
||||
className={s['segment-tab']}
|
||||
onChange={e => {
|
||||
onTabChange?.(e.target.value);
|
||||
}}
|
||||
options={tabs.map(item => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}))}
|
||||
defaultValue={activeKey ?? tabs[0]?.value}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
{showWaitTip ? (
|
||||
<div className="coz-fg-dim text-xs flex-1 truncate">
|
||||
{I18n.t('profilepicture_popup_async')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{enableCollapsible ? (
|
||||
// keepDOM 异常失效使用collapseHeight 不销毁dom保留状态
|
||||
<Collapsible isOpen={isOpen} keepDOM collapseHeight={1}>
|
||||
<div> {component} </div>
|
||||
</Collapsible>
|
||||
) : (
|
||||
<div> {component} </div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { type ReactElement } from 'react';
|
||||
|
||||
export interface TabItem {
|
||||
label: string;
|
||||
value: string;
|
||||
component: ReactElement;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
.ctn {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 7px;
|
||||
justify-content: center;
|
||||
|
||||
.image-item {
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
object-fit: cover;
|
||||
border: 1.5px solid transparent;
|
||||
border-radius: 8px;
|
||||
|
||||
&.selected {
|
||||
border: 1.5px solid var(--coz-stroke-hglt, #4E40E5);
|
||||
}
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
|
||||
display: none;
|
||||
|
||||
font-size: 16px;
|
||||
color: var(--coz-fg-hglt-red, #F54A45);
|
||||
}
|
||||
|
||||
&:hover .delete-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.check-icon {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
background-color: var(--coz-mg-hglt-plus, #4E40E5);
|
||||
border-radius: 4px;
|
||||
|
||||
svg {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
119
frontend/packages/studio/components/src/image-list/index.tsx
Normal file
119
frontend/packages/studio/components/src/image-list/index.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CSSProperties } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
IconCozCheckMarkFill,
|
||||
IconCozMinusCircleFillPalette,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { type PicTask } from '@coze-arch/bot-api/playground_api';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export type ImageItem = PicTask;
|
||||
|
||||
export interface ImageListProps {
|
||||
selectedKey?: string; // 选中的key
|
||||
data: ImageItem[]; // 列表数据
|
||||
className?: string;
|
||||
imageItemClassName?: string;
|
||||
showDeleteIcon?: boolean;
|
||||
showSelectedIcon?: boolean;
|
||||
style?: CSSProperties;
|
||||
onRemove?: (params: {
|
||||
index?: number;
|
||||
item?: ImageItem;
|
||||
data: ImageItem[];
|
||||
}) => void; // 删除图片,data是此次删除之后的数据
|
||||
onSelect?: (params: {
|
||||
index?: number;
|
||||
item: ImageItem;
|
||||
data: ImageItem[];
|
||||
selected: boolean;
|
||||
}) => void; // 选中图片,其中item和data都是此次选中之前的数据,selected表示在本次选中之前此图片是否已是选中状态
|
||||
onClick?: (params: {
|
||||
index: number;
|
||||
item: ImageItem;
|
||||
data: ImageItem[];
|
||||
}) => void; // 点击图片
|
||||
}
|
||||
|
||||
export const ImageList: React.FC<ImageListProps> = ({
|
||||
data,
|
||||
showDeleteIcon = true,
|
||||
showSelectedIcon = true,
|
||||
className,
|
||||
imageItemClassName,
|
||||
style,
|
||||
onSelect,
|
||||
onRemove,
|
||||
onClick,
|
||||
selectedKey,
|
||||
}) => {
|
||||
if (!data || data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={classNames(className, s.ctn)} style={style}>
|
||||
{data.map((item, index) => {
|
||||
const { img_info } = item;
|
||||
const { tar_uri, tar_url } = img_info ?? {};
|
||||
return (
|
||||
<div
|
||||
key={tar_uri}
|
||||
className={classNames(s['image-item'], imageItemClassName)}
|
||||
>
|
||||
<img
|
||||
src={tar_url}
|
||||
alt="图片"
|
||||
className={classNames({
|
||||
[s.selected]: showSelectedIcon && selectedKey === tar_uri,
|
||||
})}
|
||||
onClick={() => {
|
||||
onClick?.({ index, item, data });
|
||||
onSelect?.({
|
||||
index,
|
||||
item,
|
||||
data,
|
||||
selected: selectedKey === tar_uri,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{showDeleteIcon ? (
|
||||
<IconCozMinusCircleFillPalette
|
||||
className={s['delete-icon']}
|
||||
onClick={() => {
|
||||
onRemove?.({
|
||||
index,
|
||||
item,
|
||||
data: data.filter(i => i !== item),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{showSelectedIcon && selectedKey === tar_uri ? (
|
||||
<div className={s['check-icon']}>
|
||||
<IconCozCheckMarkFill />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
72
frontend/packages/studio/components/src/index.ts
Normal file
72
frontend/packages/studio/components/src/index.ts
Normal file
@@ -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.
|
||||
*/
|
||||
|
||||
export { AvatarBackgroundNoticeDot } from './avatar-background-notice-dot';
|
||||
|
||||
export { ImageList, type ImageItem, type ImageListProps } from './image-list';
|
||||
export { GenerateButton } from './generate-button';
|
||||
|
||||
export {
|
||||
InputWithCountField,
|
||||
InputWithCount,
|
||||
type InputWithCountProps,
|
||||
} from './input-with-count';
|
||||
export { UIBreadcrumb, type BreadCrumbProps } from './ui-breadcrumb';
|
||||
export { type UISearchProps, UISearch } from './ui-search';
|
||||
export { PopoverContent } from './popover-content';
|
||||
|
||||
export { SelectSpaceModal } from './select-space-modal';
|
||||
export { DuplicateBot } from './duplicate-bot';
|
||||
export { CozeBrand, type CozeBrandProps } from './coze-brand';
|
||||
|
||||
export { CardThumbnailPopover } from './card-thumbnail-popover';
|
||||
|
||||
export { LinkList, type LinkListItem } from './link-list';
|
||||
export { AvatarName } from './avatar-name';
|
||||
export { TopBar as PersonalHeader } from './personal-header';
|
||||
|
||||
export { Carousel } from './carousel';
|
||||
export {
|
||||
GenerateImageTab,
|
||||
GenerateType,
|
||||
type GenerateImageTabProps,
|
||||
} from './generate-img-tab';
|
||||
export { FlowShortcutsHelp } from './flow-shortcuts-help';
|
||||
export { LoadingButton } from './loading-button';
|
||||
export { Search, SearchProps } from './search';
|
||||
|
||||
export { ResizableLayout } from './resizable-layout';
|
||||
|
||||
export { ModelOptionItem } from './model-option/option-item';
|
||||
export { InputSlider, InputSliderProps } from './input-controls/input-slider';
|
||||
export { UploadGenerateButton } from './upload-generate-button';
|
||||
|
||||
export { usePluginLimitModal, transPricingRules } from './plugin-limit-info';
|
||||
|
||||
// 曝光埋点上报组件,进入视图上报
|
||||
export { TeaExposure } from './tea-exposure';
|
||||
export { Sticky } from './sticky';
|
||||
|
||||
export {
|
||||
ProjectTemplateCopyModal,
|
||||
type ProjectTemplateCopyValue,
|
||||
useProjectTemplateCopyModal,
|
||||
appendCopySuffix,
|
||||
} from './project-duplicate-modal';
|
||||
export { SpaceFormSelect } from './space-form-select';
|
||||
// !Notice 以下模块只允许导出类型,避免首屏加载 react-dnd,@blueprintjs/core 等相关代码
|
||||
export { type TItemRender, type ITemRenderProps } from './sortable-list';
|
||||
export { type ConnectDnd, type OnMove } from './sortable-list/hooks';
|
||||
@@ -0,0 +1,21 @@
|
||||
.input-slider {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
|
||||
:global {
|
||||
.semi-slider {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-number {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { isInteger, isUndefined } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { useHover } from 'ahooks';
|
||||
import { InputNumber } from '@coze-arch/coze-design';
|
||||
import { type SliderProps } from '@coze-arch/bot-semi/Slider';
|
||||
import { Slider } from '@coze-arch/bot-semi';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface InputSliderProps {
|
||||
value?: number;
|
||||
onChange?: (v: number) => void;
|
||||
max?: number;
|
||||
min?: number;
|
||||
step?: number;
|
||||
disabled?: boolean;
|
||||
decimalPlaces?: number;
|
||||
marks?: SliderProps['marks'];
|
||||
className?: string;
|
||||
}
|
||||
const POWVAL = 10;
|
||||
const formateDecimalPlacesString = (
|
||||
value: string | number,
|
||||
prevValue?: number,
|
||||
decimalPlaces?: number,
|
||||
) => {
|
||||
if (isUndefined(decimalPlaces)) {
|
||||
return value.toString();
|
||||
}
|
||||
const numberValue = Number(value);
|
||||
const stringValue = value.toString();
|
||||
if (Number.isNaN(numberValue)) {
|
||||
return `${value}`;
|
||||
}
|
||||
if (decimalPlaces === 0 && !isInteger(Number(value)) && prevValue) {
|
||||
return `${prevValue}`;
|
||||
}
|
||||
const decimalPointIndex = stringValue.indexOf('.');
|
||||
|
||||
if (decimalPointIndex < 0) {
|
||||
return stringValue;
|
||||
}
|
||||
const formattedValue = stringValue.substring(
|
||||
0,
|
||||
decimalPointIndex + 1 + decimalPlaces,
|
||||
);
|
||||
|
||||
if (formattedValue.endsWith('.') && decimalPlaces === 0) {
|
||||
return formattedValue.substring(0, formattedValue.length - 1);
|
||||
}
|
||||
return formattedValue;
|
||||
};
|
||||
|
||||
const formateDecimalPlacesNumber = (
|
||||
value: number,
|
||||
prevValue?: number,
|
||||
decimalPlaces?: number,
|
||||
) => {
|
||||
if (isUndefined(decimalPlaces)) {
|
||||
return value;
|
||||
}
|
||||
if (decimalPlaces === 0 && !isInteger(value) && prevValue) {
|
||||
return prevValue;
|
||||
}
|
||||
const pow = Math.pow(POWVAL, decimalPlaces);
|
||||
return Math.round(value * pow) / pow;
|
||||
};
|
||||
|
||||
export const InputSlider: React.FC<InputSliderProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
max = 1,
|
||||
min = 0,
|
||||
step = 1,
|
||||
disabled,
|
||||
decimalPlaces = 0,
|
||||
className,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const hover = useHover(ref);
|
||||
const sliderRenderId = useMemo(() => nanoid(), [max, min, hover]);
|
||||
const [isFocus, setFocus] = useState(false);
|
||||
const [inputRenderId, setInputRenderId] = useState(nanoid());
|
||||
const updateInputNumber = () => {
|
||||
if (isFocus) {
|
||||
return;
|
||||
}
|
||||
setInputRenderId(nanoid());
|
||||
};
|
||||
const onNumberChange = (numberValue: number) => {
|
||||
updateInputNumber();
|
||||
|
||||
// 防止 -0
|
||||
if (numberValue === 0) {
|
||||
onChange?.(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedFormattedValue = formateDecimalPlacesNumber(
|
||||
numberValue,
|
||||
value,
|
||||
decimalPlaces,
|
||||
);
|
||||
|
||||
onChange?.(expectedFormattedValue);
|
||||
};
|
||||
|
||||
// 防止 -0 导致 InputNumber 无限循环更新
|
||||
const fixedValue = Object.is(value, -0) ? 0 : value;
|
||||
|
||||
useEffect(() => {
|
||||
updateInputNumber();
|
||||
}, [isFocus]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={classNames(styles['input-slider'], className)}>
|
||||
<Slider
|
||||
key={sliderRenderId}
|
||||
className={styles.slider}
|
||||
disabled={disabled}
|
||||
value={fixedValue}
|
||||
max={max}
|
||||
min={min}
|
||||
step={step}
|
||||
showBoundary
|
||||
onChange={v => {
|
||||
if (typeof v === 'number') {
|
||||
onChange?.(v);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<InputNumber
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
key={inputRenderId}
|
||||
className={styles['input-number']}
|
||||
value={fixedValue}
|
||||
disabled={disabled}
|
||||
formatter={inputValue => formateDecimalPlacesString(inputValue, value)}
|
||||
onNumberChange={onNumberChange}
|
||||
max={max}
|
||||
min={min}
|
||||
step={step}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
.limit-count {
|
||||
overflow: hidden;
|
||||
|
||||
padding-right: 12px;
|
||||
padding-left: 8px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: rgb(29 28 35 / 60%);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { type InputProps } from '@coze-arch/bot-semi/Input';
|
||||
import { UIInput, withField } from '@coze-arch/bot-semi';
|
||||
import 'utility-types';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface LimitCountProps {
|
||||
maxLen: number;
|
||||
len: number;
|
||||
}
|
||||
|
||||
const LimitCount: React.FC<LimitCountProps> = ({ maxLen, len }) => (
|
||||
<span className={s['limit-count']}>
|
||||
<span>{len}</span>
|
||||
<span>/</span>
|
||||
<span>{maxLen}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
export interface InputWithCountProps extends InputProps {
|
||||
// 设置字数限制并显示字数统计
|
||||
getValueLength?: (value?: InputProps['value'] | string) => number;
|
||||
}
|
||||
|
||||
export const InputWithCount: React.FC<InputWithCountProps> = props => {
|
||||
const { value, maxLength, getValueLength } = props;
|
||||
|
||||
const len = useMemo(() => {
|
||||
if (getValueLength) {
|
||||
return getValueLength(value);
|
||||
} else if (value) {
|
||||
return value.toString().length;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}, [value, getValueLength]);
|
||||
|
||||
return (
|
||||
<UIInput
|
||||
{...props}
|
||||
suffix={
|
||||
Boolean(maxLength) && <LimitCount maxLen={maxLength ?? 0} len={len} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InputWithCountField = withField(InputWithCount);
|
||||
@@ -0,0 +1,29 @@
|
||||
.link-list {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.link-list-item {
|
||||
padding: 0 18px;
|
||||
color: rgb(255 255 255 / 60%);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.click-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.pointer {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
frontend/packages/studio/components/src/link-list/index.tsx
Normal file
74
frontend/packages/studio/components/src/link-list/index.tsx
Normal file
@@ -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 React, { type ReactNode, type CSSProperties } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Space } from '@coze-arch/bot-semi';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface LinkListItem {
|
||||
extra?: string;
|
||||
icon?: ReactNode;
|
||||
label: string;
|
||||
link?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const LinkList = ({
|
||||
className,
|
||||
style,
|
||||
data,
|
||||
pointerClassName,
|
||||
itemClassName,
|
||||
}: {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
data: LinkListItem[];
|
||||
pointerClassName?: string;
|
||||
itemClassName?: string;
|
||||
}) => (
|
||||
<div className={classNames(s['link-list'], className)} style={style}>
|
||||
{data?.map(item => (
|
||||
<div
|
||||
className={classNames(s['link-list-item'], itemClassName)}
|
||||
key={`link-list-${item.label}`}
|
||||
>
|
||||
{!!item.extra && <span style={{ marginRight: 4 }}>{item.extra}</span>}
|
||||
<div
|
||||
className={classNames(
|
||||
s['click-area'],
|
||||
(item.link || item.onClick) && s.pointer,
|
||||
(item.link || item.onClick) && pointerClassName,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (item.link) {
|
||||
window.open(item.link);
|
||||
} else {
|
||||
item.onClick?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Space spacing={4}>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background-color: #f7f7fa;
|
||||
padding: 6px 36px;
|
||||
}
|
||||
|
||||
.tool-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
17
frontend/packages/studio/components/src/list-tab/index.ts
Normal file
17
frontend/packages/studio/components/src/list-tab/index.ts
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.
|
||||
*/
|
||||
|
||||
export { ListTab, type BotListHeaderProps } from './list-tab';
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren, type ReactNode } from 'react';
|
||||
|
||||
import cs from 'classnames';
|
||||
import { type TabsProps } from '@coze-arch/bot-semi/Tabs';
|
||||
import { Tabs } from '@coze-arch/bot-semi';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface BotListHeaderProps extends TabsProps {
|
||||
toolbar?: ReactNode;
|
||||
containerClass?: string;
|
||||
}
|
||||
|
||||
export const ListTab: React.FC<PropsWithChildren<BotListHeaderProps>> = ({
|
||||
children,
|
||||
toolbar,
|
||||
containerClass,
|
||||
...props
|
||||
}) => (
|
||||
<Tabs
|
||||
{...props}
|
||||
tabPaneMotion={false}
|
||||
type="button"
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- react 组件
|
||||
renderTabBar={(innerProps, Node) => (
|
||||
<div className={cs(s.header, containerClass)}>
|
||||
<Node {...innerProps} />
|
||||
<div className={s['tool-bar']}>{toolbar}</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Tabs>
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { forwardRef, useState } from 'react';
|
||||
|
||||
import { isString } from 'lodash-es';
|
||||
import { type ToastReactProps } from '@coze-arch/bot-semi/Toast';
|
||||
import { type ButtonProps } from '@coze-arch/bot-semi/Button';
|
||||
import { UIButton, Toast, Spin } from '@coze-arch/bot-semi';
|
||||
|
||||
export type LoadingButtonProps = ButtonProps & {
|
||||
/** 加载中的 toast 文案 */
|
||||
loadingToast?: string | Omit<ToastReactProps, 'type'>;
|
||||
};
|
||||
|
||||
export const LoadingButton: React.ForwardRefExoticComponent<LoadingButtonProps> =
|
||||
forwardRef<UIButton, LoadingButtonProps>(
|
||||
({ loadingToast, ...buttonProps }, ref) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const onClick: React.MouseEventHandler<
|
||||
HTMLButtonElement
|
||||
> = async event => {
|
||||
let toastId = '';
|
||||
try {
|
||||
if (loadingToast) {
|
||||
toastId = Toast.info({
|
||||
icon: <Spin />,
|
||||
showClose: false,
|
||||
duration: 0,
|
||||
...(isString(loadingToast)
|
||||
? { content: loadingToast }
|
||||
: loadingToast),
|
||||
});
|
||||
}
|
||||
setLoading(true);
|
||||
if (buttonProps.onClick) {
|
||||
await buttonProps.onClick(event);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (toastId) {
|
||||
Toast.close(toastId);
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<UIButton
|
||||
ref={ref}
|
||||
loading={loading}
|
||||
{...buttonProps}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
.action-bar {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.icon-button {
|
||||
font-size: 16px;
|
||||
// rgb mark
|
||||
color: #060709;
|
||||
|
||||
&.icon-button-active {
|
||||
// rgb mark
|
||||
background-color: var(--semi-color-fill-1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 -- svg */
|
||||
import { useState, type ComponentProps, type CSSProperties } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Toast, Tooltip, UIButton, Upload } from '@coze-arch/bot-semi';
|
||||
import { IconLinkStroked } from '@coze-arch/bot-icons';
|
||||
|
||||
import {
|
||||
InsertLinkPopover,
|
||||
type InsertLinkPopoverProps,
|
||||
} from '../insert-link-popover';
|
||||
import { getFixedVariableTemplate } from '../../utils/onboarding-variable';
|
||||
import { type TriggerAction } from '../../type';
|
||||
import { getIsFileFormatValid } from '../../helpers/get-is-file-format-valid';
|
||||
import { OnboardingVariable } from '../../constant/onboarding-variable';
|
||||
import { getFileSizeReachLimitI18n, MAX_FILE_SIZE } from '../../constant/file';
|
||||
import { ReactComponent as IconMemberOutlined } from '../../../../assets/icon_member_outlined.svg';
|
||||
import { ReactComponent as IconImageOutlined } from '../../../../assets/icon_image_outlined.svg';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const SHOW_ADD_NAME_BTN = false;
|
||||
|
||||
export interface ActionBarProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
onTriggerAction?: (action: TriggerAction) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const iconButtonProps: ComponentProps<typeof UIButton> = {
|
||||
size: 'small',
|
||||
type: 'tertiary',
|
||||
theme: 'borderless',
|
||||
className: styles['icon-button'],
|
||||
};
|
||||
export const ActionBar: React.FC<ActionBarProps> = ({
|
||||
className,
|
||||
style,
|
||||
onTriggerAction,
|
||||
disabled,
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const togglePopoverVisible = () => {
|
||||
setVisible(e => !e);
|
||||
};
|
||||
|
||||
const closePopover = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const onConfirmInsertLink: InsertLinkPopoverProps['onConfirm'] = param => {
|
||||
closePopover();
|
||||
onTriggerAction?.({ type: 'link', sync: true, payload: param });
|
||||
};
|
||||
|
||||
const onInsertImage = (file: File) => {
|
||||
onTriggerAction?.({ type: 'image', payload: { file }, sync: false });
|
||||
};
|
||||
|
||||
const onInsertVariable = () => {
|
||||
onTriggerAction?.({
|
||||
type: 'variable',
|
||||
payload: {
|
||||
variableTemplate: getFixedVariableTemplate(
|
||||
OnboardingVariable.USER_NAME,
|
||||
),
|
||||
},
|
||||
sync: true,
|
||||
});
|
||||
};
|
||||
|
||||
const showFileTypeInvalidToast = () =>
|
||||
Toast.warning({
|
||||
showClose: false,
|
||||
content: I18n.t('file_format_not_supported'),
|
||||
});
|
||||
|
||||
const showFileSizeInvalidToast = () =>
|
||||
Toast.warning({
|
||||
showClose: false,
|
||||
content: getFileSizeReachLimitI18n(),
|
||||
});
|
||||
const onFileChange = (files: File[]) => {
|
||||
const file = files.at(0);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!getIsFileFormatValid(file)) {
|
||||
showFileTypeInvalidToast();
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
showFileSizeInvalidToast();
|
||||
return;
|
||||
}
|
||||
onInsertImage(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(className, styles['action-bar'])} style={style}>
|
||||
<Upload
|
||||
accept="image/*"
|
||||
limit={1}
|
||||
onFileChange={onFileChange}
|
||||
action="/"
|
||||
fileList={[]}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Tooltip disableFocusListener content={I18n.t('add_image')}>
|
||||
<UIButton
|
||||
{...iconButtonProps}
|
||||
icon={<IconImageOutlined />}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
|
||||
<InsertLinkPopover
|
||||
visible={visible}
|
||||
onClickOutSide={closePopover}
|
||||
onConfirm={onConfirmInsertLink}
|
||||
>
|
||||
<span>
|
||||
<Tooltip disableFocusListener content={I18n.t('add_link')}>
|
||||
<UIButton
|
||||
onClick={togglePopoverVisible}
|
||||
{...iconButtonProps}
|
||||
className={classNames(
|
||||
visible && styles['icon-button-active'],
|
||||
styles['icon-button'],
|
||||
)}
|
||||
icon={<IconLinkStroked />}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</InsertLinkPopover>
|
||||
|
||||
{/* 暂时禁用,后续放开 */}
|
||||
{SHOW_ADD_NAME_BTN && (
|
||||
<Tooltip content={I18n.t('add_nickname')}>
|
||||
<UIButton
|
||||
{...iconButtonProps}
|
||||
icon={<IconMemberOutlined />}
|
||||
disabled={disabled}
|
||||
onClick={onInsertVariable}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
.popover-content {
|
||||
display: flex;
|
||||
column-gap: 12px;
|
||||
justify-content: space-between;
|
||||
|
||||
box-sizing: content-box;
|
||||
width: 398px;
|
||||
height: 128px;
|
||||
padding: 24px;
|
||||
|
||||
// rgb mark
|
||||
background: #F4F4F6;
|
||||
|
||||
|
||||
.input-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.confirm-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, type ComponentProps, type PropsWithChildren } from 'react';
|
||||
|
||||
import { Form, Popover, UIButton, UIInput } from '@coze-arch/bot-semi';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface InsertLinkPopoverProps
|
||||
extends Pick<ComponentProps<typeof Popover>, 'onClickOutSide' | 'visible'> {
|
||||
onConfirm?: (param: { link: string; text: string }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全受控
|
||||
*/
|
||||
export const InsertLinkPopover: React.FC<
|
||||
PropsWithChildren<InsertLinkPopoverProps>
|
||||
> = ({ children, visible, onClickOutSide, onConfirm }) => (
|
||||
<Popover
|
||||
trigger="custom"
|
||||
visible={visible}
|
||||
onClickOutSide={onClickOutSide}
|
||||
showArrow={false}
|
||||
position="topRight"
|
||||
content={<Content onConfirm={onConfirm} />}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
|
||||
const Content: React.FC<Pick<InsertLinkPopoverProps, 'onConfirm'>> = ({
|
||||
onConfirm: inputOnConfirm,
|
||||
}) => {
|
||||
const [text, setText] = useState('');
|
||||
const [link, setLink] = useState('');
|
||||
const onConfirm = () => {
|
||||
clearInput();
|
||||
inputOnConfirm?.({ text, link });
|
||||
};
|
||||
const clearInput = () => {
|
||||
setLink('');
|
||||
setText('');
|
||||
};
|
||||
return (
|
||||
<div className={styles['popover-content']}>
|
||||
<div className={styles['input-content']}>
|
||||
<div className={styles['input-row']}>
|
||||
<Form.Label required text="Text" />
|
||||
<UIInput value={text} onChange={setText} />
|
||||
</div>
|
||||
<div className={styles['input-row']}>
|
||||
<Form.Label required text="Link" />
|
||||
<UIInput value={link} onChange={setLink} />
|
||||
</div>
|
||||
</div>
|
||||
<UIButton
|
||||
onClick={onConfirm}
|
||||
disabled={!text || !link}
|
||||
theme="solid"
|
||||
className={styles['confirm-button']}
|
||||
>
|
||||
Confirm
|
||||
</UIButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: rgb(255 255 255 / 90%);
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
color: rgba(6, 7, 9, 50%);
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 250px;
|
||||
height: 6px;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 { Progress } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type UploadState } from '../../type';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const UploadProgressMask: React.FC<UploadState> = ({
|
||||
fileName,
|
||||
percent,
|
||||
}) => (
|
||||
<div className={styles.mask}>
|
||||
<div className={styles.text}>
|
||||
{I18n.t('uploading_filename', { filename: fileName })}
|
||||
</div>
|
||||
<Progress className={styles.progress} percent={percent} />
|
||||
</div>
|
||||
);
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export const MAX_FILE_SIZE = 20 * 1024 * 1024;
|
||||
export const getFileSizeReachLimitI18n = () =>
|
||||
I18n.t('file_too_large', {
|
||||
max_size: '20MB',
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum OnboardingVariable {
|
||||
USER_NAME = 'user_name',
|
||||
}
|
||||
|
||||
export type OnboardingVariableMap = Record<OnboardingVariable, string>;
|
||||
@@ -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 const getInsertTextAtPosition = ({
|
||||
text,
|
||||
insertText,
|
||||
position,
|
||||
}: {
|
||||
text: string;
|
||||
insertText: string;
|
||||
position: number;
|
||||
}): string => `${text.slice(0, position)}${insertText}${text.slice(position)}`;
|
||||
@@ -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 { primitiveExhaustiveCheck } from '../utils/exhaustive-check';
|
||||
import { type SyncAction } from '../type';
|
||||
import { getMarkdownLink } from './get-markdown-link';
|
||||
|
||||
export const getSyncInsertText = (action: SyncAction): string => {
|
||||
const { type, payload } = action;
|
||||
if (type === 'link') {
|
||||
const { text, link } = payload;
|
||||
return getMarkdownLink({ text, link });
|
||||
}
|
||||
if (type === 'variable') {
|
||||
return payload.variableTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 不应该走到这里
|
||||
*/
|
||||
primitiveExhaustiveCheck(type);
|
||||
return '';
|
||||
};
|
||||
@@ -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 const getIsFileFormatValid = (file: File) =>
|
||||
file.type.startsWith('image/') && file.type !== 'image/svg+xml';
|
||||
@@ -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 const getMarkdownImageLink = ({
|
||||
fileName,
|
||||
link,
|
||||
}: {
|
||||
fileName: string;
|
||||
link: string;
|
||||
}) => ``;
|
||||
@@ -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 const getMarkdownLink = ({
|
||||
text,
|
||||
link,
|
||||
}: {
|
||||
text: string;
|
||||
link: string;
|
||||
}) => `[${text}](${link})`;
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 {
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ChangeEventHandler,
|
||||
} from 'react';
|
||||
|
||||
import useEventCallback from 'use-event-callback';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { useDragAndPasteUpload } from '@coze-arch/bot-hooks';
|
||||
|
||||
import { primitiveExhaustiveCheck } from '../utils/exhaustive-check';
|
||||
import { type AsyncAction, type SyncAction } from '../type';
|
||||
import { getMarkdownImageLink } from '../helpers/get-markdown-image-link';
|
||||
import { getIsFileFormatValid } from '../helpers/get-is-file-format-valid';
|
||||
import { getInsertTextAtPosition } from '../helpers/get-insert-text-at-position';
|
||||
import { getSyncInsertText } from '../helpers/get-insert-text';
|
||||
import { MAX_FILE_SIZE, getFileSizeReachLimitI18n } from '../constant/file';
|
||||
import { type ActionBarProps } from '../components/action-bar';
|
||||
import { type MarkdownEditorProps } from '..';
|
||||
import { useUpload } from './use-upload-file';
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export const useMarkdownEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
getUserId = () => '',
|
||||
customUpload,
|
||||
}: MarkdownEditorProps) => {
|
||||
const onTriggerSyncAction = (action: SyncAction) => {
|
||||
handleInsertText(getSyncInsertText(action));
|
||||
};
|
||||
|
||||
const onTriggerAsyncAction = (action: AsyncAction) => {
|
||||
const { type, payload } = action;
|
||||
if (type === 'image') {
|
||||
const { file } = payload;
|
||||
return uploadFileList([file]);
|
||||
}
|
||||
|
||||
primitiveExhaustiveCheck(type);
|
||||
};
|
||||
|
||||
const onTriggerAction: ActionBarProps['onTriggerAction'] = props => {
|
||||
if (props.sync) {
|
||||
return onTriggerSyncAction(props);
|
||||
}
|
||||
return onTriggerAsyncAction(props);
|
||||
};
|
||||
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
const dragTargetRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onUploadAllSuccess = useEventCallback(
|
||||
({ url, fileName }: { url: string; fileName: string }) => {
|
||||
handleInsertText(
|
||||
getMarkdownImageLink({
|
||||
fileName,
|
||||
link: url,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// 判断使用内置上传方法 or 自定义
|
||||
const selectUploadMethod = () => {
|
||||
if (customUpload) {
|
||||
return customUpload({
|
||||
onUploadAllSuccess,
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
|
||||
return useUpload({
|
||||
getUserId,
|
||||
onUploadAllSuccess,
|
||||
});
|
||||
}
|
||||
};
|
||||
const { uploadFileList, uploadState } = selectUploadMethod();
|
||||
|
||||
const { isDragOver } = useDragAndPasteUpload({
|
||||
ref: dragTargetRef,
|
||||
onUpload: fileList => {
|
||||
const file = fileList.at(0);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
onTriggerAction({ type: 'image', sync: false, payload: { file } });
|
||||
},
|
||||
disableDrag: Boolean(uploadState),
|
||||
disablePaste: Boolean(uploadState),
|
||||
fileLimit: 1,
|
||||
maxFileSize: MAX_FILE_SIZE,
|
||||
isFileFormatValid: getIsFileFormatValid,
|
||||
getExistingFileCount: () => 0,
|
||||
closeDelay: undefined,
|
||||
invalidFormatMessage: I18n.t('file_format_not_supported'),
|
||||
invalidSizeMessage: getFileSizeReachLimitI18n(),
|
||||
fileExceedsMessage: I18n.t('files_exceeds_limit'),
|
||||
});
|
||||
|
||||
const [wrapInsertionIndex, setWrapInsertionIndex] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (wrapInsertionIndex === null || !ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current.selectionStart = wrapInsertionIndex;
|
||||
ref.current.selectionEnd = wrapInsertionIndex;
|
||||
setWrapInsertionIndex(null);
|
||||
}, [ref.current, wrapInsertionIndex, value]);
|
||||
console.log('outter value', { value });
|
||||
|
||||
const onTextareaChange: ChangeEventHandler<HTMLTextAreaElement> = e => {
|
||||
console.log('onTextareaChange', { value: e.target.value });
|
||||
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
||||
const handleInsertText = (insertText: string) => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
ref.current.focus();
|
||||
const { selectionEnd } = ref.current;
|
||||
/**
|
||||
* 选中文字时点击 action bar, 将内容插入到文字的末尾
|
||||
*/
|
||||
console.log('handleInsertText', { value, insertText, selectionEnd });
|
||||
|
||||
const insertTextAtPosition = getInsertTextAtPosition({
|
||||
text: value,
|
||||
insertText,
|
||||
position: selectionEnd,
|
||||
});
|
||||
onChange(insertTextAtPosition);
|
||||
setWrapInsertionIndex(selectionEnd + insertText.length);
|
||||
};
|
||||
|
||||
return {
|
||||
textAreaRef: ref,
|
||||
onTextareaChange,
|
||||
onTriggerAction,
|
||||
dragTargetRef,
|
||||
uploadState,
|
||||
isDragOver,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { withSlardarIdButton } from '@coze-studio/bot-utils';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Toast } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type UploadState } from '../type';
|
||||
import { UploadController } from '../service/upload-controller';
|
||||
|
||||
/**
|
||||
* 暂时没有场景,所以这里将多实例、一次行上传多文件的能力屏蔽了
|
||||
*/
|
||||
export const useUpload = ({
|
||||
getUserId,
|
||||
onUploadAllSuccess,
|
||||
}: {
|
||||
getUserId: () => string;
|
||||
onUploadAllSuccess: (param: { url: string; fileName: string }) => void;
|
||||
}) => {
|
||||
const [uploadState, setUploadState] = useState<UploadState | null>(null);
|
||||
|
||||
const uploadControllerMap = useRef<Record<string, UploadController>>({});
|
||||
|
||||
const clearState = () => setUploadState(null);
|
||||
|
||||
const deleteUploadControllerById = (id: string) => {
|
||||
delete uploadControllerMap.current[id];
|
||||
};
|
||||
const cancelUploadById = (id: string) => {
|
||||
const controller = uploadControllerMap.current[id];
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
controller.cancel();
|
||||
deleteUploadControllerById(id);
|
||||
};
|
||||
|
||||
const handleError = (_e: unknown, controllerId: string) => {
|
||||
clearState();
|
||||
cancelUploadById(controllerId);
|
||||
Toast.error({
|
||||
content: withSlardarIdButton(I18n.t('Upload_failed')),
|
||||
showClose: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
setUploadState(null);
|
||||
};
|
||||
|
||||
const handleProgress = (percent: number) => {
|
||||
setUploadState(state => {
|
||||
if (!state) {
|
||||
return state;
|
||||
}
|
||||
return { ...state, percent };
|
||||
});
|
||||
};
|
||||
|
||||
const handleStartUpload = (fileName: string) =>
|
||||
setUploadState({ fileName, percent: 0 });
|
||||
|
||||
const uploadFileList = (fileList: File[]) => {
|
||||
if (uploadState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const controllerId = nanoid();
|
||||
|
||||
const file = fileList.at(0);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
handleStartUpload(file.name);
|
||||
|
||||
uploadControllerMap.current[controllerId] = new UploadController({
|
||||
fileList,
|
||||
controllerId,
|
||||
userId: getUserId(),
|
||||
onProgress: event => {
|
||||
handleProgress(event.percent);
|
||||
},
|
||||
onComplete: event => {
|
||||
handleUploadSuccess();
|
||||
onUploadAllSuccess(event);
|
||||
},
|
||||
onUploadError: handleError,
|
||||
onGetTokenError: handleError,
|
||||
onGetUploadInstanceError: handleError,
|
||||
});
|
||||
};
|
||||
|
||||
const clearAllSideEffect = () => {
|
||||
Object.entries(uploadControllerMap.current).forEach(([, controller]) =>
|
||||
controller.cancel(),
|
||||
);
|
||||
uploadControllerMap.current = {};
|
||||
};
|
||||
|
||||
useEffect(() => clearAllSideEffect, []);
|
||||
|
||||
return {
|
||||
uploadState,
|
||||
uploadFileList,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
.markdown-editor {
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: 6px 12px;
|
||||
|
||||
background-color: #fff;
|
||||
// rgb mark
|
||||
border: 1px solid;
|
||||
border-color: rgb(6 7 9 / 10%);
|
||||
border-radius: 8px;
|
||||
|
||||
&.markdown-editor-drag {
|
||||
box-shadow: 0 0 0 2px #DADCFB;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #34F;
|
||||
}
|
||||
|
||||
.markdown-action-bar {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 0;
|
||||
|
||||
border-bottom: 1px solid rgb(6 7 9 / 10%);
|
||||
}
|
||||
|
||||
.markdown-editor-content {
|
||||
resize: none;
|
||||
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
padding: 0;
|
||||
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
|
||||
// rgb mark
|
||||
color: #383743;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
&::selection {
|
||||
background: rgb(77 83 232 / 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CSSProperties } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { type CustomUploadParams, type CustomUploadRes } from './type';
|
||||
import { useMarkdownEditor } from './hooks/use-markdown-editor';
|
||||
import { UploadProgressMask } from './components/upload-progress-mask';
|
||||
import { ActionBar } from './components/action-bar';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface MarkdownEditorProps {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
onChange: (value: string) => void;
|
||||
getUserId?: () => string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
style?: CSSProperties;
|
||||
customUpload?: (params: CustomUploadParams) => CustomUploadRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全受控组件
|
||||
*/
|
||||
export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
|
||||
value = '',
|
||||
placeholder = '',
|
||||
className,
|
||||
disabled,
|
||||
style,
|
||||
...props
|
||||
}) => {
|
||||
const {
|
||||
textAreaRef,
|
||||
dragTargetRef,
|
||||
onTextareaChange,
|
||||
onTriggerAction,
|
||||
isDragOver,
|
||||
uploadState,
|
||||
} = useMarkdownEditor({
|
||||
value,
|
||||
...props,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles['markdown-editor'],
|
||||
isDragOver && styles['markdown-editor-drag'],
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
ref={dragTargetRef}
|
||||
>
|
||||
<ActionBar
|
||||
className={styles['markdown-action-bar']}
|
||||
onTriggerAction={onTriggerAction}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onTextareaChange}
|
||||
className={styles['markdown-editor-content']}
|
||||
/>
|
||||
{uploadState && <UploadProgressMask {...uploadState} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 {
|
||||
uploadFileV2,
|
||||
type EventPayloadMaps as BaseEventPayloadMap,
|
||||
type UploaderInstance,
|
||||
type UploadFileV2Param,
|
||||
type FileItem,
|
||||
} from '@coze-arch/bot-utils/upload-file-v2';
|
||||
import { PlaygroundApi } from '@coze-arch/bot-api';
|
||||
|
||||
export type EventPayloadMap = BaseEventPayloadMap & {
|
||||
ready: boolean;
|
||||
};
|
||||
|
||||
export interface UploadControllerProps {
|
||||
controllerId: string;
|
||||
fileList: File[];
|
||||
userId: string;
|
||||
onProgress?: (
|
||||
event: EventPayloadMap['progress'],
|
||||
controllerId: string,
|
||||
) => void;
|
||||
onComplete?: (
|
||||
event: {
|
||||
url: string;
|
||||
fileName: string;
|
||||
},
|
||||
controllerId: string,
|
||||
) => void;
|
||||
onUploadError?: (event: Error, controllerId: string) => void;
|
||||
onUploaderReady?: (
|
||||
event: EventPayloadMap['ready'],
|
||||
controllerId: string,
|
||||
) => void;
|
||||
onStartUpload?: (
|
||||
param: Parameters<Required<UploadFileV2Param>['onStartUpload']>[number],
|
||||
controllerId: string,
|
||||
) => void;
|
||||
onGetUploadInstanceError?: (error: Error, controllerId: string) => void;
|
||||
onGetTokenError?: (error: Error, controllerId: string) => void;
|
||||
}
|
||||
|
||||
const isImage = (file: File) => file.type.startsWith('image/');
|
||||
|
||||
export class UploadController {
|
||||
controllerId: string;
|
||||
abortController: AbortController;
|
||||
uploader: UploaderInstance | null;
|
||||
fileItemList: FileItem[];
|
||||
|
||||
constructor({
|
||||
controllerId,
|
||||
fileList,
|
||||
userId,
|
||||
onProgress,
|
||||
onComplete,
|
||||
onUploadError,
|
||||
onUploaderReady,
|
||||
onStartUpload,
|
||||
onGetTokenError,
|
||||
onGetUploadInstanceError,
|
||||
}: UploadControllerProps) {
|
||||
this.fileItemList = fileList.map(file => ({
|
||||
file,
|
||||
fileType: isImage(file) ? 'image' : 'object',
|
||||
}));
|
||||
this.controllerId = controllerId;
|
||||
this.abortController = new AbortController();
|
||||
this.uploader = null;
|
||||
uploadFileV2({
|
||||
fileItemList: this.fileItemList,
|
||||
userId,
|
||||
signal: this.abortController.signal,
|
||||
timeout: undefined,
|
||||
onUploaderReady: uploader => {
|
||||
this.uploader = uploader;
|
||||
onUploaderReady?.(true, controllerId);
|
||||
},
|
||||
onProgress: event => onProgress?.(event, controllerId),
|
||||
onSuccess: async event => {
|
||||
const uri = event.uploadResult.Uri;
|
||||
try {
|
||||
if (!uri) {
|
||||
throw new Error(
|
||||
`upload success without uri, uploadID ${event.uploadID}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await PlaygroundApi.GetImagexShortUrl({
|
||||
uris: [uri],
|
||||
});
|
||||
|
||||
const url = result.data?.url_info?.[uri]?.url;
|
||||
|
||||
if (!url) {
|
||||
throw new Error(`failed to get url, uri: ${uri}`);
|
||||
}
|
||||
|
||||
onComplete?.(
|
||||
{ url, fileName: event.uploadResult.FileName ?? '' },
|
||||
controllerId,
|
||||
);
|
||||
} catch (e) {
|
||||
onUploadError?.(
|
||||
e instanceof Error ? e : new Error(String(e)),
|
||||
controllerId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onUploadError: event => onUploadError?.(event.extra.error, controllerId),
|
||||
onStartUpload: event => onStartUpload?.(event, controllerId),
|
||||
onGetUploadInstanceError: error =>
|
||||
onGetUploadInstanceError?.(error, controllerId),
|
||||
onGetTokenError: error => onGetTokenError?.(error, controllerId),
|
||||
});
|
||||
}
|
||||
|
||||
cancel = () => {
|
||||
this.abortController.abort();
|
||||
};
|
||||
|
||||
pause = () => {
|
||||
this.uploader?.pause();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 InsertImageAction {
|
||||
type: 'image';
|
||||
sync: false;
|
||||
payload: {
|
||||
file: File;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InsertLinkAction {
|
||||
type: 'link';
|
||||
sync: true;
|
||||
payload: {
|
||||
text: string;
|
||||
link: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InsertVariableAction {
|
||||
type: 'variable';
|
||||
sync: true;
|
||||
payload: {
|
||||
variableTemplate: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type SyncAction = InsertLinkAction | InsertVariableAction;
|
||||
|
||||
export type AsyncAction = InsertImageAction;
|
||||
|
||||
export type TriggerAction = SyncAction | AsyncAction;
|
||||
|
||||
export interface UploadState {
|
||||
percent: number;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
// 自定义上传方法入参
|
||||
export interface CustomUploadParams {
|
||||
onUploadAllSuccess: (param: { url: string; fileName: string }) => void;
|
||||
}
|
||||
// 自定义上传方法出参
|
||||
export interface CustomUploadRes {
|
||||
uploadFileList: (fileList: File[]) => void;
|
||||
//null表示已完成
|
||||
uploadState: UploadState | null;
|
||||
}
|
||||
@@ -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 const primitiveExhaustiveCheck = (_: never) => 0;
|
||||
export const recordExhaustiveCheck = (_: Record<string, never>) => 0;
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type OnboardingVariable,
|
||||
type OnboardingVariableMap,
|
||||
} from '../constant/onboarding-variable';
|
||||
import { typedKeys } from './typed-keys';
|
||||
|
||||
export interface VariableWithRange {
|
||||
range: [number, number];
|
||||
variable: OnboardingVariable;
|
||||
}
|
||||
|
||||
export const getFixedVariableTemplate = (template: string) => `{{${template}}}`;
|
||||
|
||||
export const matchAllTemplateRanges = (
|
||||
text: string,
|
||||
template: string,
|
||||
): { start: number; end: number }[] => {
|
||||
// 正则表达式,用于匹配双花括号内的内容
|
||||
const templateRegex = new RegExp(getFixedVariableTemplate(template), 'g');
|
||||
const matches: { start: number; end: number }[] = [];
|
||||
|
||||
// 循环查找所有匹配项
|
||||
while (true) {
|
||||
const match = templateRegex.exec(text);
|
||||
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
const templateString = match[0];
|
||||
const start = match.index;
|
||||
const end = templateString.length + start;
|
||||
|
||||
matches.push({ start, end });
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
|
||||
export const getVariableRangeList = (
|
||||
content: string,
|
||||
variableMap: OnboardingVariableMap,
|
||||
) => {
|
||||
const result: VariableWithRange[] = [];
|
||||
typedKeys(variableMap).forEach(variable => {
|
||||
const allMatchedRanges = matchAllTemplateRanges(content, variable);
|
||||
const variableWithRangeList: VariableWithRange[] = allMatchedRanges.map(
|
||||
({ start, end }) => ({
|
||||
variable,
|
||||
range: [start, end],
|
||||
}),
|
||||
);
|
||||
result.push(...variableWithRangeList);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const typedKeys = <T extends Parameters<typeof Object.keys>[number]>(
|
||||
o: T,
|
||||
): Array<keyof T> => Object.keys(o) as Array<keyof T>;
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MdBoxLazy } from '@coze-arch/bot-md-box-adapter/lazy';
|
||||
import { type ModelDescGroup } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
export const ModelDescription: React.FC<{
|
||||
descriptionGroupList: ModelDescGroup[];
|
||||
}> = ({ descriptionGroupList }) => (
|
||||
<MdBoxLazy
|
||||
autoFixSyntax={{ autoFixEnding: false }}
|
||||
markDown={descriptionGroupList
|
||||
.map(({ group_name, desc }) => `${group_name}\n${desc?.join('\n')}`)
|
||||
.join('\n\n')}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 { lazy, Suspense } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Tag,
|
||||
Highlight,
|
||||
Typography,
|
||||
Avatar,
|
||||
type TagProps,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { Popover } from '@coze-arch/bot-semi';
|
||||
import { IconInfo } from '@coze-arch/bot-icons';
|
||||
import { useFlags } from '@coze-arch/bot-flags';
|
||||
import { type ModelDescGroup } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
const LazyModelDescription = lazy(async () => {
|
||||
const { ModelDescription } = await import('./model-description');
|
||||
return {
|
||||
default: ModelDescription,
|
||||
};
|
||||
});
|
||||
const ModelDescription = (props: {
|
||||
descriptionGroupList: ModelDescGroup[];
|
||||
}) => (
|
||||
<Suspense>
|
||||
<LazyModelDescription {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
export interface OptionItemTag {
|
||||
label: string;
|
||||
color?: TagProps['color'];
|
||||
}
|
||||
export interface OptionItemProps {
|
||||
tokenLimit: number | undefined;
|
||||
descriptionGroupList: ModelDescGroup[] | undefined;
|
||||
avatar: string | undefined;
|
||||
name: string | undefined;
|
||||
searchWords?: string[];
|
||||
endPointName?: string; // 接入点名称(专业版有)
|
||||
showEndPointName?: boolean;
|
||||
className?: string;
|
||||
/**
|
||||
* @deprecated
|
||||
* 原先只会有「限额」标签,M-5395720900 后会有大量新标签,避免兼容问题产品同意先简单隐藏掉标签展示
|
||||
*/
|
||||
tags?: OptionItemTag[];
|
||||
}
|
||||
|
||||
export const ModelOptionItem: React.FC<OptionItemProps> = ({
|
||||
avatar,
|
||||
descriptionGroupList,
|
||||
tokenLimit = 0,
|
||||
name,
|
||||
searchWords = [],
|
||||
endPointName,
|
||||
showEndPointName = false,
|
||||
className,
|
||||
}) => {
|
||||
const [FLAGS] = useFlags();
|
||||
const tags: OptionItemTag[] = [];
|
||||
|
||||
const shouldShowEndPoint = showEndPointName && endPointName;
|
||||
// 社区版暂不支持该功能
|
||||
const displayName = FLAGS['bot.studio.model_select_switch_end_point_name_pos']
|
||||
? endPointName || name
|
||||
: name;
|
||||
|
||||
// 社区版暂不支持该功能
|
||||
const displayEndPointName = FLAGS[
|
||||
'bot.studio.model_select_switch_end_point_name_pos'
|
||||
]
|
||||
? name
|
||||
: endPointName;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'w-full px-[8px] flex justify-between overflow-hidden gap-[16px]',
|
||||
{
|
||||
'py-2': showEndPointName,
|
||||
},
|
||||
className,
|
||||
endPointName ? 'items-start' : 'items-center',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="flex-1 flex items-center gap-[8px] overflow-hidden"
|
||||
data-testid="bot.ide.bot_creator.select_model_formitem"
|
||||
>
|
||||
<Avatar
|
||||
shape="square"
|
||||
src={avatar}
|
||||
className={classNames('shrink-0', {
|
||||
'!h-4 !w-4': !showEndPointName,
|
||||
'!h-8 !w-8': showEndPointName,
|
||||
})}
|
||||
data-testid="bot-detail.model-config-modal.model-avatar"
|
||||
/>
|
||||
<div
|
||||
className={classNames('flex-1', {
|
||||
'items-center': showEndPointName && !endPointName,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className="inline-block truncate leading-[20px]"
|
||||
data-testid="bot-detail.model-config-modal.model-name"
|
||||
>
|
||||
<Highlight
|
||||
sourceString={displayName}
|
||||
searchWords={searchWords}
|
||||
highlightClassName="coz-fg-hglt-yellow bg-transparent"
|
||||
/>
|
||||
</span>
|
||||
{descriptionGroupList?.length ? (
|
||||
<Popover
|
||||
trigger="hover"
|
||||
className="max-w-[224px] py-[8px] px-[12px]"
|
||||
content={
|
||||
<ModelDescription
|
||||
descriptionGroupList={descriptionGroupList}
|
||||
data-testid="bot-detail.model-config-modal-model.description-popover"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IconInfo
|
||||
data-testid="bot-detail.model-config-modal.model-info-button"
|
||||
className="ml-[4px]"
|
||||
/>
|
||||
</Popover>
|
||||
) : null}
|
||||
<Tag
|
||||
prefixIcon={null}
|
||||
color="primary"
|
||||
className="shrink-0 !ml-[8px]"
|
||||
data-testid="bot-detail.model-config-modal.model-token-tag"
|
||||
size="mini"
|
||||
>
|
||||
{(tokenLimit / 1024).toFixed(0)}K
|
||||
</Tag>
|
||||
</div>
|
||||
{shouldShowEndPoint ? (
|
||||
<Typography.Text
|
||||
className="coz-fg-secondary text-[12px] leading-[16px]"
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
content: displayEndPointName,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Highlight
|
||||
sourceString={displayEndPointName}
|
||||
searchWords={searchWords}
|
||||
highlightClassName="coz-fg-hglt-yellow bg-transparent"
|
||||
component="span"
|
||||
/>
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{tags?.length ? (
|
||||
<div
|
||||
className={classNames('flex shrink-0', {
|
||||
'pt-[2px]': shouldShowEndPoint,
|
||||
})}
|
||||
>
|
||||
{tags.map(tag => (
|
||||
<Tag color={tag.color} key={tag.label} size="mini">
|
||||
{tag.label}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
25
frontend/packages/studio/components/src/monetize/index.ts
Normal file
25
frontend/packages/studio/components/src/monetize/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 {
|
||||
MonetizeConfigPanel,
|
||||
MonetizeConfigValue,
|
||||
} from './monetize-config-panel';
|
||||
export { MonetizeCreditRefreshCycle } from './monetize-credit-refresh-cycle';
|
||||
export { MonetizeDescription } from './monetize-description';
|
||||
export { MonetizeFreeChatCount } from './monetize-free-chat-count';
|
||||
export { MonetizeSwitch } from './monetize-switch';
|
||||
export { MonetizePublishInfo } from './monetize-publish-info';
|
||||
@@ -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 { useDebounceFn } from 'ahooks';
|
||||
import { type BotMonetizationRefreshPeriod } from '@coze-arch/bot-api/benefit';
|
||||
|
||||
import { MonetizeSwitch } from '../monetize-switch';
|
||||
import { MonetizeFreeChatCount } from '../monetize-free-chat-count';
|
||||
import { MonetizeDescription } from '../monetize-description';
|
||||
import { MonetizeCreditRefreshCycle } from '../monetize-credit-refresh-cycle';
|
||||
|
||||
export interface MonetizeConfigValue {
|
||||
/** 是否开启付费 */
|
||||
isOn: boolean;
|
||||
/** 开启付费后,用户免费体验的次数 */
|
||||
freeCount: number;
|
||||
/** 刷新周期 */
|
||||
refreshCycle: BotMonetizationRefreshPeriod;
|
||||
}
|
||||
|
||||
interface MonetizeConfigPanelProps {
|
||||
disabled?: boolean;
|
||||
value: MonetizeConfigValue;
|
||||
onChange: (value: MonetizeConfigValue) => void;
|
||||
/**
|
||||
* 内置防抖后的 onChange 事件,业务侧可选择性使用,正常只传 onChange 即可
|
||||
* (由于该组件是完全受控组件,因此不能只传 onDebouncedChange,必须传 onChange 实时更新视图)
|
||||
*/
|
||||
onDebouncedChange?: (value: MonetizeConfigValue) => void;
|
||||
}
|
||||
|
||||
export function MonetizeConfigPanel({
|
||||
disabled = false,
|
||||
value,
|
||||
onChange,
|
||||
onDebouncedChange,
|
||||
}: MonetizeConfigPanelProps) {
|
||||
const { run: debouncedChange } = useDebounceFn(
|
||||
({ isOn, freeCount, refreshCycle }: MonetizeConfigValue) => {
|
||||
onDebouncedChange?.({
|
||||
isOn,
|
||||
freeCount,
|
||||
refreshCycle,
|
||||
});
|
||||
},
|
||||
{ wait: 300 },
|
||||
);
|
||||
|
||||
const refreshCycleDisabled = !value.isOn || disabled || value.freeCount <= 0;
|
||||
|
||||
return (
|
||||
<div className="w-[480px] p-[24px] flex flex-col gap-[24px]">
|
||||
<MonetizeSwitch
|
||||
disabled={disabled}
|
||||
isOn={value.isOn}
|
||||
onChange={v => {
|
||||
onChange({ ...value, isOn: v });
|
||||
debouncedChange({
|
||||
...value,
|
||||
isOn: v,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<MonetizeDescription isOn={value.isOn} />
|
||||
<MonetizeFreeChatCount
|
||||
isOn={value.isOn}
|
||||
disabled={disabled}
|
||||
freeCount={value.freeCount}
|
||||
onFreeCountChange={v => {
|
||||
onChange({ ...value, freeCount: v });
|
||||
debouncedChange({
|
||||
...value,
|
||||
freeCount: v,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<MonetizeCreditRefreshCycle
|
||||
freeCount={value.freeCount}
|
||||
disabled={refreshCycleDisabled}
|
||||
refreshCycle={value.refreshCycle}
|
||||
onRefreshCycleChange={v => {
|
||||
onChange({ ...value, refreshCycle: v });
|
||||
debouncedChange({ ...value, refreshCycle: v });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Tooltip, Select } from '@coze-arch/coze-design';
|
||||
import { BotMonetizationRefreshPeriod } from '@coze-arch/bot-api/benefit';
|
||||
|
||||
const refreshCycleTextMap: Record<BotMonetizationRefreshPeriod, string> = {
|
||||
[BotMonetizationRefreshPeriod.Unknown]: I18n.t(
|
||||
'coze_premium_credits_cycle_4',
|
||||
),
|
||||
[BotMonetizationRefreshPeriod.Never]: I18n.t('coze_premium_credits_cycle_4'),
|
||||
[BotMonetizationRefreshPeriod.Day]: I18n.t('coze_premium_credits_cycle_1'),
|
||||
[BotMonetizationRefreshPeriod.Week]: I18n.t('coze_premium_credits_cycle_2'),
|
||||
[BotMonetizationRefreshPeriod.Month]: I18n.t('coze_premium_credits_cycle_3'),
|
||||
};
|
||||
|
||||
const getOptionList = () => [
|
||||
{
|
||||
value: BotMonetizationRefreshPeriod.Never,
|
||||
text: refreshCycleTextMap[BotMonetizationRefreshPeriod.Never],
|
||||
},
|
||||
{
|
||||
value: BotMonetizationRefreshPeriod.Day,
|
||||
text: refreshCycleTextMap[BotMonetizationRefreshPeriod.Day],
|
||||
tooltip: I18n.t('coze_premium_credits_cycle_tip6'),
|
||||
},
|
||||
{
|
||||
value: BotMonetizationRefreshPeriod.Week,
|
||||
text: refreshCycleTextMap[BotMonetizationRefreshPeriod.Week],
|
||||
tooltip: I18n.t('coze_premium_credits_cycle_tip7'),
|
||||
},
|
||||
{
|
||||
value: BotMonetizationRefreshPeriod.Month,
|
||||
text: refreshCycleTextMap[BotMonetizationRefreshPeriod.Month],
|
||||
tooltip: I18n.t('coze_premium_credits_cycle_tip8'),
|
||||
},
|
||||
];
|
||||
|
||||
export function MonetizeCreditRefreshCycle({
|
||||
refreshCycle,
|
||||
onRefreshCycleChange,
|
||||
disabled,
|
||||
freeCount,
|
||||
}: {
|
||||
freeCount: number;
|
||||
disabled: boolean;
|
||||
refreshCycle: number;
|
||||
onRefreshCycleChange: (value: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4px">
|
||||
<div className="coz-fg-primary text-lg font-medium">
|
||||
{I18n.t('coze_premium_credits_cycle_5')}
|
||||
</div>
|
||||
<Tooltip
|
||||
theme="dark"
|
||||
content={I18n.t('coze_premium_credits_cycle_tip1')}
|
||||
>
|
||||
<IconCozInfoCircle className="text-base coz-fg-secondary" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
key={freeCount}
|
||||
trigger={freeCount <= 0 ? 'hover' : 'custom'}
|
||||
content={I18n.t('coze_premium_credits_cycle_tip4')}
|
||||
>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
onChange={value => {
|
||||
onRefreshCycleChange(Number(value));
|
||||
}}
|
||||
value={refreshCycle}
|
||||
position="bottomRight"
|
||||
className="w-[140px]"
|
||||
renderSelectedItem={(item: Record<string, unknown>) =>
|
||||
refreshCycleTextMap[item.value as BotMonetizationRefreshPeriod]
|
||||
}
|
||||
>
|
||||
{getOptionList().map(item => (
|
||||
<Select.Option key={item.value} value={item.value}>
|
||||
<div className="mx-8px w-[100px]">{item.text}</div>
|
||||
{item.tooltip ? (
|
||||
<Tooltip theme="dark" position="right" content={item.tooltip}>
|
||||
<IconCozInfoCircle className="mr-8px coz-fg-secondary text-base" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</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 { I18n } from '@coze-arch/i18n';
|
||||
import { Popover } from '@coze-arch/coze-design';
|
||||
|
||||
import previewCard from './preview-card.png';
|
||||
|
||||
export function MonetizeDescription({ isOn }: { isOn: boolean }) {
|
||||
return (
|
||||
<div className="coz-fg-primary">
|
||||
<span>
|
||||
{isOn ? I18n.t('monetization_on_des') : I18n.t('monetization_off_des')}
|
||||
</span>
|
||||
{isOn ? (
|
||||
<Popover
|
||||
content={
|
||||
<div className="p-[12px] coz-bg-max rounded-[10px]">
|
||||
<img width={320} src={previewCard} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span className="coz-fg-hglt cursor-pointer">
|
||||
{I18n.t('monetization_on_viewbill')}
|
||||
</span>
|
||||
</Popover>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import { InputNumber, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
export function MonetizeFreeChatCount({
|
||||
isOn,
|
||||
disabled,
|
||||
freeCount,
|
||||
onFreeCountChange,
|
||||
}: {
|
||||
isOn: boolean;
|
||||
disabled: boolean;
|
||||
freeCount: number;
|
||||
onFreeCountChange: (value: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center font-semibold leading-[20px]">
|
||||
<span className="coz-fg-primary">
|
||||
{I18n.t('free_chat_allowance')}
|
||||
</span>
|
||||
<Tooltip theme="dark" content={I18n.t('free_chat_allowance_tips')}>
|
||||
<span className="ml-[4px] h-[12px] w-[12px] text-[12px] leading-[12px] coz-fg-secondary">
|
||||
<IconCozInfoCircle />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="coz-fg-secondary text-base leading-[16px]">
|
||||
{freeCount > 5
|
||||
? I18n.t('coze_premium_credits_cycle_tip2')
|
||||
: I18n.t('coze_premium_credits_cycle_tip3')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InputNumber
|
||||
keepFocus
|
||||
className="w-[140px]"
|
||||
disabled={!isOn || disabled}
|
||||
precision={0}
|
||||
min={0}
|
||||
max={100}
|
||||
value={freeCount}
|
||||
onNumberChange={onFreeCountChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Avatar, Tooltip } from '@coze-arch/coze-design';
|
||||
import { type PublishConnectorInfo } from '@coze-arch/bot-api/developer_api';
|
||||
import { type BotMonetizationConfigData } from '@coze-arch/bot-api/benefit';
|
||||
|
||||
export function MonetizePublishInfo({
|
||||
className,
|
||||
monetizeConfig,
|
||||
supportPlatforms,
|
||||
}: {
|
||||
className?: string;
|
||||
monetizeConfig: BotMonetizationConfigData;
|
||||
supportPlatforms: Array<Pick<PublishConnectorInfo, 'id' | 'name' | 'icon'>>;
|
||||
}) {
|
||||
const supportPlatformsText = supportPlatforms.map(p => p.name).join(', ');
|
||||
|
||||
return (
|
||||
<div className={cls('flex justify-end items-center gap-[12px]', className)}>
|
||||
<div className="flex items-center gap-[4px]">
|
||||
<span className="font-medium coz-fg-plus">
|
||||
{`${I18n.t('monetization')}: ${
|
||||
monetizeConfig.is_enable
|
||||
? I18n.t('monetization_publish_on')
|
||||
: I18n.t('monetization_publish_off')
|
||||
}`}
|
||||
</span>
|
||||
<Tooltip
|
||||
content={
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
{monetizeConfig.is_enable
|
||||
? I18n.t('monetization_on_des')
|
||||
: I18n.t('monetization_off_des')}
|
||||
</div>
|
||||
{monetizeConfig.is_enable ? (
|
||||
<div className="mt-[8px] pt-[8px] border-0 border-t border-solid coz-stroke-primary">
|
||||
{`${I18n.t('free_chat_allowance')} : ${
|
||||
monetizeConfig.free_chat_allowance_count
|
||||
}`}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<IconCozInfoCircle className="w-[16px] h-[16px] coz-fg-secondary" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-[4px]">
|
||||
<span className="font-normal coz-fg-tertiary">
|
||||
{I18n.t('monetization_support')}:
|
||||
</span>
|
||||
<span className="flex items-center gap-[4px]">
|
||||
{supportPlatforms.map(p => (
|
||||
<Avatar
|
||||
key={p.id}
|
||||
className="h-[16px] w-[16px] rounded-[4px]"
|
||||
size="extra-extra-small"
|
||||
shape="square"
|
||||
src={p.icon}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
<Tooltip
|
||||
content={`${I18n.t(
|
||||
'monetization_support_tips',
|
||||
)}: ${supportPlatformsText}`}
|
||||
>
|
||||
<IconCozInfoCircle className="w-[16px] h-[16px] coz-fg-secondary" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</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 { I18n } from '@coze-arch/i18n';
|
||||
import { Switch } from '@coze-arch/coze-design';
|
||||
|
||||
export function MonetizeSwitch({
|
||||
disabled,
|
||||
isOn,
|
||||
onChange,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
isOn: boolean;
|
||||
onChange: (isOn: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<h3 className="m-0 text-[20px] font-medium coz-fg-plus">
|
||||
{I18n.t('premium_monetization_config')}
|
||||
</h3>
|
||||
<Switch
|
||||
disabled={disabled}
|
||||
className="ml-[5px]"
|
||||
size="small"
|
||||
checked={isOn}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { TopBar } from './top-bar';
|
||||
export { SpaceAppList } from './space-app-list';
|
||||
@@ -0,0 +1,49 @@
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
padding: 6px 14px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
@apply coz-fg-secondary;
|
||||
|
||||
&:hover {
|
||||
@apply coz-mg-primary coz-fg-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
@apply coz-mg-primary coz-fg-primary;
|
||||
}
|
||||
|
||||
.item-link {
|
||||
margin-bottom: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tag-container {
|
||||
@apply flex items-center gap-4px;
|
||||
|
||||
.label {
|
||||
@apply px-4px text-foreground-2 font-semibold text-lg;
|
||||
}
|
||||
|
||||
.tag {
|
||||
@apply rounded-mini;
|
||||
|
||||
padding: 1px 6px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.label {
|
||||
@apply coz-fg-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 { NavLink } from 'react-router-dom';
|
||||
import React, { type ReactNode } from 'react';
|
||||
|
||||
import { isString } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { SpaceAppEnum } from '@coze-arch/web-context';
|
||||
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
|
||||
import { Space, Badge } from '@coze-arch/coze-design';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import { useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
import { getFlags } from '@coze-arch/bot-flags';
|
||||
import { KnowledgeE2e, BotE2e } from '@coze-data/e2e';
|
||||
import { useSpaceApp } from '@coze-foundation/space-store';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface MenuItem {
|
||||
/**
|
||||
* 如果是string,需传入starling key,并且会由div包一层
|
||||
* 如果是function,则自定义label的实现,active表示是否是选中态
|
||||
*/
|
||||
label: string | ((active: boolean) => React.ReactNode);
|
||||
/** label 外的 badge,未来再扩展配置项 */
|
||||
badge?: string;
|
||||
app: SpaceAppEnum;
|
||||
/**
|
||||
* Q:为什么不叫 visible?FG 要取反,filter() 也要取反,很麻烦
|
||||
* A:为了兼容旧配置,缺省时认定为 visible。避免合码时无冲突 导致忽略掉新增配置的问题。
|
||||
*/
|
||||
invisible?: boolean;
|
||||
/** 目前(24.05.21)没发现用处,怀疑是以前的功能迭代掉了,@huangjian 说先留着 */
|
||||
icon?: ReactNode;
|
||||
/** 目前(24.05.21)没发现用处,怀疑是以前的功能迭代掉了,@huangjian 说先留着 */
|
||||
selectedIcon?: ReactNode;
|
||||
/** 自动化打标 */
|
||||
e2e?: string;
|
||||
}
|
||||
|
||||
const GET_MENU_SPACE_APP = (): Array<MenuItem> => [
|
||||
{
|
||||
label: 'menu_bots',
|
||||
app: SpaceAppEnum.BOT,
|
||||
e2e: BotE2e.BotTab,
|
||||
},
|
||||
{
|
||||
label: 'menu_plugins',
|
||||
app: SpaceAppEnum.PLUGIN,
|
||||
},
|
||||
{
|
||||
label: 'menu_workflows',
|
||||
app: SpaceAppEnum.WORKFLOW,
|
||||
},
|
||||
{
|
||||
label: 'imageflow_title',
|
||||
app: SpaceAppEnum.IMAGEFLOW,
|
||||
invisible: false,
|
||||
},
|
||||
{
|
||||
label: 'menu_datasets',
|
||||
app: SpaceAppEnum.KNOWLEDGE,
|
||||
e2e: KnowledgeE2e.KnowledgeTab,
|
||||
},
|
||||
{
|
||||
label: 'menu_widgets',
|
||||
app: SpaceAppEnum.WIDGET,
|
||||
invisible: !getFlags()['bot.builder.bot.builder.widget'],
|
||||
},
|
||||
{
|
||||
label: 'scene_resource_name',
|
||||
badge: 'scene_beta_sign',
|
||||
app: SpaceAppEnum.SOCIAL_SCENE,
|
||||
invisible: !getFlags()['bot.studio.social'],
|
||||
},
|
||||
];
|
||||
export const SpaceAppList = () => {
|
||||
const spaceApp = useSpaceApp();
|
||||
|
||||
const { id: spaceId } = useSpaceStore(store => store.space);
|
||||
|
||||
return (
|
||||
<Space spacing={4}>
|
||||
{GET_MENU_SPACE_APP()
|
||||
.filter(item => !item.invisible)
|
||||
.map(item => {
|
||||
const active = item.app === spaceApp;
|
||||
const tabContent = (
|
||||
<NavLink
|
||||
key={item.app}
|
||||
data-testid={item.e2e}
|
||||
to={`/space/${spaceId}/${item.app}`}
|
||||
className={s['item-link']}
|
||||
onClick={() => {
|
||||
sendTeaEvent(EVENT_NAMES.workspace_tab_expose, {
|
||||
tab_name: item.app,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isString(item.label) ? (
|
||||
<div
|
||||
className={classNames({
|
||||
[s.item]: true,
|
||||
[s.active]: active,
|
||||
})}
|
||||
>
|
||||
{I18n.t(item.label as I18nKeysNoOptionsType)}
|
||||
</div>
|
||||
) : (
|
||||
item.label(active)
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
return item.badge ? (
|
||||
<Badge
|
||||
type="alt"
|
||||
key={item.app}
|
||||
count={I18n.t(item.badge as I18nKeysNoOptionsType)}
|
||||
countStyle={{
|
||||
backgroundColor: 'var(--coz-mg-color-plus-emerald)',
|
||||
}}
|
||||
>
|
||||
{tabContent}
|
||||
</Badge>
|
||||
) : (
|
||||
tabContent
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
.topBar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
.des {
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.name {
|
||||
max-width: calc(100% - 28px);
|
||||
word-break: break-word;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
height: 28px;
|
||||
@apply coz-fg-plus;
|
||||
}
|
||||
|
||||
.split {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 4px;
|
||||
border-bottom: none;
|
||||
background-color: var(--coz-stroke-primary);
|
||||
}
|
||||
}
|
||||
@@ -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 React, { type JSX } from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozSetting } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Typography,
|
||||
Space,
|
||||
IconButton,
|
||||
Divider,
|
||||
Avatar,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import { useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { SpaceAppList } from '../space-app-list';
|
||||
|
||||
import s from './index.module.less';
|
||||
interface TopBarProps {
|
||||
showActions?: boolean;
|
||||
showFilter?: boolean;
|
||||
isPersonal?: boolean;
|
||||
actions: JSX.Element;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
titleExtend?: JSX.Element;
|
||||
}
|
||||
|
||||
export const TopBar = (props: TopBarProps) => {
|
||||
const { Text } = Typography;
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
space: { name: spaceName, id: spaceId, icon_url: spaceIconUrl },
|
||||
} = useSpaceStore();
|
||||
const {
|
||||
showActions,
|
||||
showFilter,
|
||||
isPersonal,
|
||||
actions,
|
||||
children,
|
||||
className,
|
||||
titleExtend,
|
||||
} = props;
|
||||
const settingLabel = I18n.t('basic_setting');
|
||||
|
||||
return (
|
||||
<div className={classnames(s.topBar, className)}>
|
||||
<Space className="w-full flex justify-between mb-24px">
|
||||
<Space>
|
||||
<div className={s.des}>
|
||||
<Avatar
|
||||
src={spaceIconUrl}
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: { content: spaceName },
|
||||
},
|
||||
}}
|
||||
className={classnames(s.name, '!max-w-[320px]')}
|
||||
>
|
||||
{spaceName}
|
||||
</Text>
|
||||
{titleExtend}
|
||||
</div>
|
||||
</Space>
|
||||
<Space spacing={8} className="flex items-center align-right">
|
||||
{showActions ? actions : null}
|
||||
{!isPersonal && (
|
||||
<>
|
||||
<Divider layout="horizontal" className={s.split} />
|
||||
<IconButton
|
||||
color="primary"
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
sendTeaEvent(EVENT_NAMES.workspace_tab_expose, {
|
||||
tab_name: 'team_manage',
|
||||
});
|
||||
|
||||
navigate(`/space/${spaceId}/team`);
|
||||
}}
|
||||
icon={<IconCozSetting />}
|
||||
aria-label={settingLabel}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
<div className={s.tabs}>
|
||||
<Space className="w-full flex justify-between">
|
||||
<Space spacing={8} className="flex items-center align-left shrink-0">
|
||||
<SpaceAppList />
|
||||
</Space>
|
||||
<Space
|
||||
className="!flex items-center !overflow-hidden shrink-1"
|
||||
spacing={8}
|
||||
>
|
||||
{showFilter ? children : null}
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
.plugin-limit-table {
|
||||
:global {
|
||||
.semi-table-thead>.semi-table-row>.semi-table-row-head {
|
||||
height: 16px;
|
||||
padding: 4px 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.semi-table-row{
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.semi-table-tbody .semi-table-row .semi-table-row-cell {
|
||||
padding: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Avatar, Typography, UITable, useUIModal } from '@coze-arch/bot-semi';
|
||||
import {
|
||||
type PluginPricingRule,
|
||||
PluginPricingStrategy,
|
||||
} from '@coze-arch/bot-api/plugin_develop';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const PluginLimitName: FC<{
|
||||
name: string;
|
||||
url: string;
|
||||
}> = ({ name, url }) => (
|
||||
<span className="flex items-center">
|
||||
<Avatar
|
||||
size="small"
|
||||
className="h-6 flex-none flex-shrink-0 mr-2 w-6"
|
||||
shape="square"
|
||||
src={url}
|
||||
></Avatar>
|
||||
<Typography.Text ellipsis={{ showTooltip: true }}>{name}</Typography.Text>
|
||||
</span>
|
||||
);
|
||||
|
||||
export interface UsePluginLimitModalProps {
|
||||
content: React.ReactNode;
|
||||
dataSource: Array<{
|
||||
info: {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
price: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const transPricingRules = (
|
||||
pluginPricingRules?: Array<PluginPricingRule>,
|
||||
) =>
|
||||
Array.isArray(pluginPricingRules)
|
||||
? pluginPricingRules
|
||||
.filter(item => item.PricingStrategy !== PluginPricingStrategy.Free)
|
||||
.map(item => ({
|
||||
info: {
|
||||
name: item?.PluginInfo?.name,
|
||||
url: item?.PluginInfo?.plugin_icon,
|
||||
},
|
||||
price: parseInt(item?.PriceResult?.TokensForOnce ?? '0'),
|
||||
}))
|
||||
: [];
|
||||
|
||||
export const usePluginLimitModal = ({
|
||||
content,
|
||||
dataSource,
|
||||
}: UsePluginLimitModalProps) => {
|
||||
const { modal, open, close } = useUIModal({
|
||||
okText: I18n.t('plugin_usage_limits_modal_got_it_button'),
|
||||
onOk: () => {
|
||||
close();
|
||||
},
|
||||
title: I18n.t('plugin_usage_limits_modal_title'),
|
||||
hasCancel: false,
|
||||
onCancel: () => {
|
||||
close();
|
||||
},
|
||||
});
|
||||
return {
|
||||
node: modal(
|
||||
<>
|
||||
{content ? content : null}
|
||||
<UITable
|
||||
tableProps={{
|
||||
className: styles['plugin-limit-table'],
|
||||
columns: [
|
||||
{
|
||||
width: 192,
|
||||
title: I18n.t('plugin_usage_limits_modal_table_header_plugin'),
|
||||
dataIndex: 'info',
|
||||
render: info => (
|
||||
<PluginLimitName name={info.name} url={info.url} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: I18n.t('plugin_usage_limits_modal_table_header_price'),
|
||||
dataIndex: 'price',
|
||||
render: text => <span>{text} Coze tokens</span>,
|
||||
},
|
||||
],
|
||||
dataSource,
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</>,
|
||||
),
|
||||
open: () => {
|
||||
open();
|
||||
},
|
||||
close,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,859 @@
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
/* stylelint-disable declaration-no-important */
|
||||
/* stylelint-disable max-nesting-depth */
|
||||
@import '@coze-common/assets/style/common.less';
|
||||
@import '@coze-common/assets/style/mixins.less';
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.plugin-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - 68px);
|
||||
height: 98px;
|
||||
margin: 14px 0 0 68px;
|
||||
padding-right: 12px;
|
||||
padding-bottom: 14px;
|
||||
|
||||
&:not(:last-child) {
|
||||
position: relative;
|
||||
border-bottom: 1px solid rgba(28, 31, 35, 8%);
|
||||
}
|
||||
|
||||
.plugin-api-main {
|
||||
flex: 1;
|
||||
|
||||
.plugin-api-name {
|
||||
width: 100%;
|
||||
|
||||
:global {
|
||||
.semi-typography {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
color: #1c1d23 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-api-desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
|
||||
:global {
|
||||
.semi-typography {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color:
|
||||
rgba(28, 31, 35, 60%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.api-params {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
|
||||
.params-tags {
|
||||
max-width: 500px;
|
||||
|
||||
.tag-item {
|
||||
width: fit-content;
|
||||
min-width: fit-content;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
|
||||
/* 133.333% */
|
||||
color: rgba(28, 29, 35, 60%);
|
||||
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&> :not(:first-child) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.params-desc {
|
||||
cursor: pointer;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: #4d53e8;
|
||||
letter-spacing: 0.12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-api-method {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tools-content {
|
||||
padding: 0 20px 24px;
|
||||
|
||||
.tools-table-thead {
|
||||
padding-bottom: 6px;
|
||||
|
||||
th {
|
||||
padding: 6px 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-table {
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* 133.333% */
|
||||
color: rgba(28, 31, 35, 60%);
|
||||
word-wrap: break-word;
|
||||
|
||||
thead {
|
||||
th {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
/* 133.333% */
|
||||
color: #888d92;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.api-row {
|
||||
&.border-top {
|
||||
position: relative;
|
||||
|
||||
td {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #f9f9f9;
|
||||
}
|
||||
|
||||
td {
|
||||
overflow: hidden;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-tag-grey-light {
|
||||
height: 20px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.semi-tag-content {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-plugin {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding-right: 10px;
|
||||
|
||||
&-image {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
|
||||
>img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-image-status {
|
||||
background-color: rgba(#fff, 0) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: rgba(28, 29, 35, 80%);
|
||||
}
|
||||
}
|
||||
|
||||
.api-method {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
color: rgba(28, 31, 35, 35%);
|
||||
}
|
||||
|
||||
.icon-config {
|
||||
cursor: pointer;
|
||||
color: rgba(107, 109, 117, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.api-method-read {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.api-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
padding-right: 10px;
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
color: rgba(29, 28, 35, 80%);
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-tag-grey-light {
|
||||
background: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.api-divider {
|
||||
height: 12px;
|
||||
border-color: #b3c4ff;
|
||||
}
|
||||
|
||||
.copy {
|
||||
cursor: copy;
|
||||
}
|
||||
|
||||
.icon-tips:hover {
|
||||
background-color: var(--semi-color-fill-0);
|
||||
}
|
||||
|
||||
.icon-tips {
|
||||
cursor: pointer;
|
||||
.common-svg-icon(14px, rgba(107, 109, 117, 1));
|
||||
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&-publish {
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
box-sizing: border-box;
|
||||
max-width: 264px;
|
||||
}
|
||||
|
||||
.popover-api-name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
color: #1c1f23;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.popover-api-desc {
|
||||
padding: 4px 0;
|
||||
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: rgba(28, 31, 35, 60%);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.plugin-panel-header {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
height: 80px;
|
||||
|
||||
font-weight: 400;
|
||||
|
||||
.creator-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
|
||||
img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.creator-time {
|
||||
/* Paragraph/small/EN-Regular */
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
|
||||
/* 133.333% */
|
||||
color: rgba(28, 29, 35, 35%);
|
||||
text-align: right;
|
||||
letter-spacing: 0.12px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.header-main {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
margin-right: 16px;
|
||||
|
||||
// margin-top: -12px;
|
||||
.header-name {
|
||||
width: 100%;
|
||||
|
||||
:global {
|
||||
.semi-typography {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
color: #1c1d23 !important;
|
||||
word-wrap: break-word !important;
|
||||
}
|
||||
|
||||
.semi-highlight-tag {
|
||||
color: #fda633;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-desc {
|
||||
width: 100%;
|
||||
|
||||
:global {
|
||||
.semi-typography {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color:
|
||||
rgba(28, 29, 35, 80%) !important;
|
||||
letter-spacing: 0.12px;
|
||||
word-wrap: break-word !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-info {
|
||||
/* Paragraph/small/EN-Regular */
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
|
||||
/* 133.333% */
|
||||
color: rgba(28, 29, 35, 35%);
|
||||
text-align: right;
|
||||
|
||||
:global {
|
||||
.semi-divider-vertical {
|
||||
height: 10px;
|
||||
color: rgba(28, 29, 35, 12%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-tags {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.header-tag {
|
||||
letter-spacing: 0.12px !important;
|
||||
border-radius: 6px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
padding-right: 24px;
|
||||
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
color: #1c1f23;
|
||||
|
||||
&-title {
|
||||
width: 218px;
|
||||
padding: 24px;
|
||||
line-height: 16px;
|
||||
background: #ebedf0;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-modal-filter {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.composition-modal-layout {
|
||||
background: #ebedf0;
|
||||
}
|
||||
|
||||
.plugin-modal {
|
||||
height: 100%;
|
||||
|
||||
.iconSearch {
|
||||
.common-svg-icon(16px, rgba(28, 29, 35, 0.35));
|
||||
|
||||
margin-right: 8px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.tool-tag-list {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
padding-top: 16px;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
&-label {
|
||||
height: 40px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0 12px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 40px;
|
||||
color: rgba(28, 29, 35, 35%);
|
||||
}
|
||||
|
||||
&-cell {
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 44px;
|
||||
margin-bottom: 4px;
|
||||
padding: 0 10px 0 12px;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 44px;
|
||||
color: #1d1c23;
|
||||
|
||||
border-radius: 3px;
|
||||
|
||||
&-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
.common-svg-icon(24px, #1d1c23);
|
||||
|
||||
>img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&-divider {
|
||||
width: calc(100% - 24px);
|
||||
margin: 12px;
|
||||
background: rgba(28, 29, 35, 12%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #1c1f23;
|
||||
background:
|
||||
rgba(46, 50, 56, 5%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--light-usage-text-color-text-0, #1c1d23);
|
||||
|
||||
background:
|
||||
rgba(46, 47, 56, 5%);
|
||||
border-radius: 8px;
|
||||
|
||||
.tool-tag-list-cell-icon {
|
||||
.common-svg-icon(24px, #4d53e8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.addbtn {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.plugin-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 218px;
|
||||
|
||||
background: #ebedf0;
|
||||
}
|
||||
|
||||
.tool-content-area {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.plugin-modal-filter {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.plugin-content {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 12px;
|
||||
|
||||
.loading-more {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.plugin-content-filter {
|
||||
display: flex;
|
||||
padding: 0 36px;
|
||||
padding-left: 22px;
|
||||
|
||||
.plugin-content-sort {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.bot-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-tabs-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.semi-tabs-tab-button.semi-tabs-tab-active {
|
||||
color: rgba(28, 29, 35, 80%);
|
||||
background-color: transparent;
|
||||
|
||||
.semi-icon {
|
||||
.common-svg-icon(20px, rgba(28, 29, 35, 0.8));
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tabs-tab-single.semi-tabs-tab-active .semi-icon:not(.semi-icon-checkbox_tick,
|
||||
.semi-icon-radio,
|
||||
.semi-icon-checkbox_indeterminate) {
|
||||
top: 0;
|
||||
color: rgba(28, 29, 35, 80%);
|
||||
}
|
||||
|
||||
.semi-tabs-tab-single.semi-tabs-tab .semi-icon:not(.semi-icon-checkbox_tick,
|
||||
.semi-icon-radio,
|
||||
.semi-icon-checkbox_indeterminate) {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.semi-tabs-tab:last-child::before {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: -4px;
|
||||
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
|
||||
background-color:
|
||||
rgba(28, 29, 35, 12%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-collapse {
|
||||
:global {
|
||||
.semi-collapse-item {
|
||||
position: relative;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.semi-collapse-header:hover::before {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
|
||||
background-color: #f7f7fa;
|
||||
}
|
||||
|
||||
.semi-collapse-header {
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #dfdfdf;
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
background:
|
||||
rgba(46, 47, 56, 5%);
|
||||
border-bottom: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background:
|
||||
rgba(46, 47, 56, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.semi-collapse-header-icon {
|
||||
width: auto;
|
||||
height: 24px;
|
||||
|
||||
&:hover {
|
||||
background:
|
||||
rgba(46, 47, 56, 9%);
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
.common-svg-icon(16px, rgba(28, 29, 35, 0.35));
|
||||
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.activePanel {
|
||||
margin-bottom: 8px;
|
||||
background:
|
||||
rgba(46, 47, 56, 5%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
|
||||
:global {
|
||||
.semi-collapse-header {
|
||||
border-bottom: 1px solid #dfdfdf;
|
||||
}
|
||||
|
||||
.semi-collapse-header:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-modal-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.semi-spin-children {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-content,
|
||||
.plugin-collapse {
|
||||
:global {
|
||||
.semi-collapse-header {
|
||||
height: 120px !important;
|
||||
margin: 0 !important;
|
||||
padding: 14px 16px;
|
||||
|
||||
&[aria-expanded='true'] {
|
||||
border-radius: 8px 8px 0 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-collapse {
|
||||
padding: 16px 0 12px;
|
||||
}
|
||||
|
||||
.semi-collapse-content {
|
||||
// background-color: #fff;
|
||||
padding: 0;
|
||||
border-radius: 0 0 8px 8px;
|
||||
// border-color: var(
|
||||
// --light-usage-border-color-border,
|
||||
// rgba(28, 31, 35, 0.08)
|
||||
// );
|
||||
// border-width: 0 1px 1px 1px;
|
||||
// border-style: solid;
|
||||
}
|
||||
|
||||
.semi-collapse-item {
|
||||
// border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.parameter-item {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
.parameter-text {
|
||||
max-width: 100%;
|
||||
margin-bottom: 4px;
|
||||
color: #1c1f23;
|
||||
|
||||
.parameter-name {
|
||||
font-weight: 700;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.parameter-type {
|
||||
color: rgba(28, 31, 35, 80%);
|
||||
}
|
||||
|
||||
.parameter-required {
|
||||
color: #fc8800;
|
||||
}
|
||||
}
|
||||
|
||||
.parameter-desc {
|
||||
color: rgba(28, 31, 35, 60%);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.apis-add-icon {
|
||||
.common-svg-icon;
|
||||
}
|
||||
|
||||
.default-text {
|
||||
.text;
|
||||
|
||||
color: rgba(28, 29, 35, 60%);
|
||||
}
|
||||
|
||||
.operator-btn {
|
||||
width: 98px;
|
||||
|
||||
&.added {
|
||||
color: #b4baf6;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f5;
|
||||
}
|
||||
|
||||
&.addedMouseIn {
|
||||
color: #ff441e;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(29, 28, 35, 12%);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-select {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.hide-button-model-wrap {
|
||||
.ml20 {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.h56 {
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
top: 20px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-func-collapse {
|
||||
.plugin-api-desc {
|
||||
cursor: pointer;
|
||||
width: 200px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren } from 'react';
|
||||
|
||||
import { sortBy } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
|
||||
import { Popover, Space } from '@coze-arch/bot-semi';
|
||||
import {
|
||||
PluginParamTypeFormat,
|
||||
type PluginApi,
|
||||
type PluginParameter,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface ParametersPopoverProps extends PopoverProps {
|
||||
pluginApi: PluginApi;
|
||||
callback?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onVisibleChange?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
enum AssistType {
|
||||
File = 1,
|
||||
Image = 2,
|
||||
Doc = 3,
|
||||
Code = 4,
|
||||
Ppt = 5,
|
||||
Txt = 6,
|
||||
Excel = 7,
|
||||
Audio = 8,
|
||||
Zip = 9,
|
||||
Video = 10,
|
||||
Svg = 11,
|
||||
}
|
||||
|
||||
const assistTypeToDisplayMap = {
|
||||
[AssistType.File]: 'File',
|
||||
[AssistType.Image]: 'Image',
|
||||
[AssistType.Doc]: 'Doc',
|
||||
[AssistType.Code]: 'Code',
|
||||
[AssistType.Ppt]: 'PPT',
|
||||
[AssistType.Txt]: 'Txt',
|
||||
[AssistType.Excel]: 'Excel',
|
||||
[AssistType.Audio]: 'Audio',
|
||||
[AssistType.Zip]: 'Zip',
|
||||
[AssistType.Video]: 'Video',
|
||||
[AssistType.Svg]: 'Svg',
|
||||
};
|
||||
|
||||
const getDisplayType = (parameter: Readonly<PluginParameter>) => {
|
||||
const { type, format } = parameter;
|
||||
|
||||
const { assist_type } = parameter as { assist_type: AssistType };
|
||||
|
||||
let displayType = type;
|
||||
if (type === 'string' && format === PluginParamTypeFormat.ImageUrl) {
|
||||
displayType = 'image';
|
||||
} else if (type === 'string' && assist_type) {
|
||||
displayType = assistTypeToDisplayMap[assist_type];
|
||||
}
|
||||
return displayType;
|
||||
};
|
||||
|
||||
const ParameterItem: React.FC<{ parameter: Readonly<PluginParameter> }> = ({
|
||||
parameter,
|
||||
}) => {
|
||||
const { name, desc, required } = parameter;
|
||||
|
||||
return (
|
||||
<div className={s['parameter-item']}>
|
||||
<Space className={s['parameter-text']} wrap>
|
||||
<span className={s['parameter-name']}>{name}</span>
|
||||
<span className={s['parameter-type']}>{getDisplayType(parameter)}</span>
|
||||
{required ? (
|
||||
<span className={s['parameter-required']}>
|
||||
{I18n.t('tool_para_required')}
|
||||
</span>
|
||||
) : null}
|
||||
</Space>
|
||||
<div className={s['parameter-desc']}>{desc}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ParametersPopover: React.FC<
|
||||
PropsWithChildren<ParametersPopoverProps>
|
||||
> = ({ children, pluginApi, callback, onVisibleChange, ...props }) => (
|
||||
<Popover
|
||||
trigger={props?.trigger || 'hover'}
|
||||
position="right"
|
||||
showArrow
|
||||
onVisibleChange={onVisibleChange}
|
||||
content={
|
||||
<div
|
||||
className={classNames(
|
||||
'max-h-[400px] overflow-x-hidden overflow-y-auto',
|
||||
s['popover-content'],
|
||||
)}
|
||||
onClick={e => {
|
||||
callback?.(e);
|
||||
}}
|
||||
>
|
||||
{pluginApi.name ? (
|
||||
<div className={s['popover-api-name']}>{pluginApi.name}</div>
|
||||
) : null}
|
||||
{pluginApi.desc ? (
|
||||
<div className={s['popover-api-desc']}>{pluginApi.desc}</div>
|
||||
) : null}
|
||||
{sortBy(pluginApi.parameters || [], item => item.name?.length)?.map(
|
||||
p => {
|
||||
if (!p) {
|
||||
return null;
|
||||
}
|
||||
return <ParameterItem parameter={p} key={p.name} />;
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<div>{children}</div>
|
||||
</Popover>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
.tip-content {
|
||||
width: 180px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 18px;
|
||||
color: var(--light-color-grey-grey-8, #2e3238);
|
||||
|
||||
li {
|
||||
list-style-type: disc;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
Suspense,
|
||||
lazy,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import s from './index.module.less';
|
||||
// react-markdown 20ms 左右的 longtask
|
||||
const LazyReactMarkdown = lazy(() => import('react-markdown'));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ReactMarkdown = (props: any) => (
|
||||
<Suspense fallback={null}>
|
||||
<LazyReactMarkdown {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
export const PopoverContent: React.FC<
|
||||
PropsWithChildren & {
|
||||
text?: string;
|
||||
node?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
> = ({ children, className, style }) => (
|
||||
<div className={classNames(s['tip-content'], className)} style={style}>
|
||||
{typeof children === 'string' ? (
|
||||
<ReactMarkdown skipHtml={true} className={s.markdown}>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { extractTemplateActionCommonParams } from '@coze-arch/bot-tea/utils';
|
||||
import {
|
||||
EVENT_NAMES,
|
||||
type ParamsTypeDefine,
|
||||
sendTeaEvent,
|
||||
} from '@coze-arch/bot-tea';
|
||||
import {
|
||||
ProductEntityType,
|
||||
type ProductInfo,
|
||||
} from '@coze-arch/bot-api/product_api';
|
||||
import { ProductApi } from '@coze-arch/bot-api';
|
||||
import { botInputLengthService } from '@coze-agent-ide/bot-input-length-limit';
|
||||
import {
|
||||
type BaseFormProps,
|
||||
Form,
|
||||
FormInput,
|
||||
Modal,
|
||||
type ModalProps,
|
||||
type FormApi,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import { SpaceFormSelect } from '../space-form-select';
|
||||
import { appendCopySuffix } from './utils';
|
||||
|
||||
export interface ProjectTemplateCopyValue {
|
||||
productId: string;
|
||||
name: string;
|
||||
spaceId?: string;
|
||||
}
|
||||
|
||||
const filedKeyMap: Record<
|
||||
keyof ProjectTemplateCopyValue,
|
||||
keyof ProjectTemplateCopyValue
|
||||
> = {
|
||||
name: 'name',
|
||||
spaceId: 'spaceId',
|
||||
productId: 'productId',
|
||||
} as const;
|
||||
|
||||
interface ProjectTemplateCopyModalProps
|
||||
extends Omit<ModalProps, 'size' | 'okText' | 'cancelText'> {
|
||||
isSelectSpace: boolean;
|
||||
formProps: BaseFormProps<ProjectTemplateCopyValue>;
|
||||
}
|
||||
|
||||
export const ProjectTemplateCopyModal: React.FC<
|
||||
ProjectTemplateCopyModalProps
|
||||
> = ({ isSelectSpace, formProps, ...modalProps }) => (
|
||||
<Modal
|
||||
size="default"
|
||||
okText={I18n.t('Confirm')}
|
||||
cancelText={I18n.t('Cancel')}
|
||||
{...modalProps}
|
||||
>
|
||||
<Form<ProjectTemplateCopyValue> {...formProps}>
|
||||
<FormInput
|
||||
label={I18n.t('creat_project_project_name')}
|
||||
rules={[{ required: true }]}
|
||||
field={filedKeyMap.name}
|
||||
maxLength={botInputLengthService.getInputLengthLimit('projectName')}
|
||||
getValueLength={botInputLengthService.getValueLength}
|
||||
noErrorMessage
|
||||
/>
|
||||
{isSelectSpace ? <SpaceFormSelect field={filedKeyMap.spaceId} /> : null}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
export type ProjectTemplateCopySuccessCallback = (param: {
|
||||
originProductId: string;
|
||||
newEntityId: string;
|
||||
spaceId: string;
|
||||
}) => void;
|
||||
|
||||
export const useProjectTemplateCopyModal = (props: {
|
||||
modalTitle: string;
|
||||
/** 是否需要选择 space */
|
||||
isSelectSpace: boolean;
|
||||
onSuccess?: ProjectTemplateCopySuccessCallback;
|
||||
/** 埋点参数 - 当前页面/来源 */
|
||||
source: NonNullable<
|
||||
ParamsTypeDefine[EVENT_NAMES.template_action_front]['source']
|
||||
>;
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [initValues, setInitValues] = useState<ProjectTemplateCopyValue>();
|
||||
const [sourceProduct, setSourceProduct] = useState<ProductInfo>();
|
||||
const [isFormValid, setIsFormValid] = useState(true);
|
||||
const formApi = useRef<FormApi<ProjectTemplateCopyValue>>();
|
||||
|
||||
const onModalClose = () => {
|
||||
setVisible(false);
|
||||
setInitValues(undefined);
|
||||
formApi.current = undefined;
|
||||
setIsFormValid(true);
|
||||
};
|
||||
|
||||
const { run, loading } = useRequest(
|
||||
async (copyRequestParam: ProjectTemplateCopyValue | undefined) => {
|
||||
if (!copyRequestParam) {
|
||||
throw new Error('duplicate project template values not provided');
|
||||
}
|
||||
const { productId, spaceId, name } = copyRequestParam;
|
||||
return ProductApi.PublicDuplicateProduct({
|
||||
product_id: productId,
|
||||
space_id: spaceId,
|
||||
name,
|
||||
entity_type: ProductEntityType.ProjectTemplate,
|
||||
});
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: (data, [inputParam]) => {
|
||||
onModalClose();
|
||||
sendTeaEvent(EVENT_NAMES.template_action_front, {
|
||||
action: 'duplicate',
|
||||
after_id: data.data?.new_entity_id,
|
||||
source: props.source,
|
||||
...extractTemplateActionCommonParams(sourceProduct),
|
||||
});
|
||||
props?.onSuccess?.({
|
||||
originProductId: inputParam?.productId ?? '',
|
||||
newEntityId: data.data?.new_entity_id ?? '',
|
||||
spaceId: inputParam?.spaceId ?? '',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
modalContextHolder: (
|
||||
<ProjectTemplateCopyModal
|
||||
title={props.modalTitle}
|
||||
isSelectSpace={props.isSelectSpace}
|
||||
visible={visible}
|
||||
okButtonProps={{
|
||||
disabled: !isFormValid,
|
||||
loading,
|
||||
}}
|
||||
onOk={async () => {
|
||||
const val = await formApi.current?.validate();
|
||||
if (val) {
|
||||
run(val);
|
||||
}
|
||||
}}
|
||||
onCancel={onModalClose}
|
||||
formProps={{
|
||||
initValues,
|
||||
onValueChange: val => {
|
||||
// 当用户删除 input 中所有字符时,val.name 字段会消失,而不是空字符串,神秘
|
||||
setIsFormValid(!!val.name?.trim());
|
||||
},
|
||||
getFormApi: api => {
|
||||
formApi.current = api;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
),
|
||||
copyProject: ({
|
||||
initValue,
|
||||
sourceProduct: inputSourceProduct,
|
||||
}: {
|
||||
initValue: ProjectTemplateCopyValue;
|
||||
/** 用于提取埋点参数 */
|
||||
sourceProduct: ProductInfo;
|
||||
}) => {
|
||||
setInitValues({
|
||||
...initValue,
|
||||
name: botInputLengthService.sliceStringByMaxLength({
|
||||
value: appendCopySuffix(initValue.name),
|
||||
field: 'projectName',
|
||||
}),
|
||||
});
|
||||
setSourceProduct(inputSourceProduct);
|
||||
setVisible(true);
|
||||
setIsFormValid(!!initValue?.name?.trim());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { appendCopySuffix };
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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';
|
||||
|
||||
export const appendCopySuffix = (name: string) =>
|
||||
`${name}(${I18n.t('duplicate_rename_copy')})`;
|
||||
@@ -0,0 +1,9 @@
|
||||
.handle {
|
||||
width: 1px;
|
||||
background-color: var(--coz-stroke-primary);
|
||||
}
|
||||
|
||||
.hot-zone:hover .handle, .handle-moving {
|
||||
width: 4px;
|
||||
background-color: var(--coz-stroke-hglt);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT 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 MouseEventHandler,
|
||||
type FC,
|
||||
useRef,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import s from './handle.module.less';
|
||||
|
||||
// 目前只支持水平方向,按需扩展吧
|
||||
export interface ResizableLayoutHandleProps {
|
||||
className?: string;
|
||||
hotZoneClassName?: string;
|
||||
onMove: (offset: number) => void;
|
||||
onMoveStart: () => void;
|
||||
onMoveEnd: () => void;
|
||||
}
|
||||
|
||||
interface HandleState {
|
||||
startX: number;
|
||||
moving: boolean;
|
||||
}
|
||||
|
||||
const hotZoneStyle = classnames(
|
||||
s['hot-zone'],
|
||||
'flex items-stretch justify-center',
|
||||
'cursor-col-resize',
|
||||
'z-10',
|
||||
'w-[8px] mx-[-3.5px]',
|
||||
'bg-transparent',
|
||||
);
|
||||
|
||||
const handleStyle = classnames('transition-width duration-300 ease-in-out');
|
||||
|
||||
export const ResizableLayoutHandle: FC<ResizableLayoutHandleProps> = ({
|
||||
className,
|
||||
hotZoneClassName,
|
||||
onMove,
|
||||
onMoveStart,
|
||||
onMoveEnd,
|
||||
}) => {
|
||||
const [moving, setMoving] = useState(false);
|
||||
const stateRef = useRef<HandleState>({
|
||||
startX: 0,
|
||||
moving: false,
|
||||
});
|
||||
|
||||
const callbackRef = useRef({
|
||||
onMove,
|
||||
onMoveStart,
|
||||
onMoveEnd,
|
||||
});
|
||||
|
||||
callbackRef.current = {
|
||||
onMove,
|
||||
onMoveStart,
|
||||
onMoveEnd,
|
||||
};
|
||||
|
||||
const moveEnd = useCallback(() => {
|
||||
setMoving(false);
|
||||
stateRef.current = {
|
||||
startX: 0,
|
||||
moving: false,
|
||||
};
|
||||
offEvents();
|
||||
callbackRef.current.onMoveEnd();
|
||||
}, []);
|
||||
|
||||
const move = useCallback((e: PointerEvent) => {
|
||||
if (stateRef.current.moving) {
|
||||
callbackRef.current.onMove(e.clientX - stateRef.current.startX);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const offEvents = () => {
|
||||
window.removeEventListener('pointermove', move, false);
|
||||
// 适配移动端出现多点触控的情况
|
||||
window.removeEventListener('pointerdown', moveEnd, false);
|
||||
window.removeEventListener('pointerup', moveEnd, false);
|
||||
window.removeEventListener('pointercancel', moveEnd, false);
|
||||
};
|
||||
|
||||
const onMouseDown: MouseEventHandler<HTMLDivElement> = e => {
|
||||
stateRef.current = {
|
||||
moving: true,
|
||||
startX: e.pageX,
|
||||
};
|
||||
setMoving(true);
|
||||
callbackRef.current.onMoveStart();
|
||||
window.addEventListener('pointermove', move, false);
|
||||
// 适配移动端出现多点触控的情况
|
||||
window.addEventListener('pointerdown', moveEnd, false);
|
||||
window.addEventListener('pointerup', moveEnd, false);
|
||||
window.addEventListener('pointercancel', moveEnd, false);
|
||||
};
|
||||
// TODO hover 样式 & 热区宽度需要和 UI 对齐
|
||||
return (
|
||||
<div
|
||||
className={classnames(hotZoneStyle, hotZoneClassName)}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
<div
|
||||
className={classnames(
|
||||
className,
|
||||
s.handle,
|
||||
moving && s['handle-moving'],
|
||||
handleStyle,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResizableLayoutHandle.displayName = 'ResizableLayoutHandle';
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Children,
|
||||
type PropsWithChildren,
|
||||
useRef,
|
||||
type FC,
|
||||
isValidElement,
|
||||
cloneElement,
|
||||
type ReactNode,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { sum } from 'lodash-es';
|
||||
import classnames from 'classnames';
|
||||
import { useDebounceEffect, useSize } from 'ahooks';
|
||||
|
||||
import { type ResizableLayoutProps } from './types';
|
||||
import { ResizableLayoutHandle } from './handle';
|
||||
|
||||
interface LayoutState {
|
||||
moving: boolean;
|
||||
itemWidth: number[];
|
||||
}
|
||||
|
||||
const getDefaultState = () => ({
|
||||
moving: false,
|
||||
itemWidth: [],
|
||||
});
|
||||
|
||||
export const ResizableLayout: FC<PropsWithChildren<ResizableLayoutProps>> = ({
|
||||
className,
|
||||
children,
|
||||
handleClassName,
|
||||
hotZoneClassName,
|
||||
}) => {
|
||||
const [state, setState] = useState<LayoutState>(getDefaultState());
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const childRef = useRef<HTMLElement[]>([]);
|
||||
|
||||
const size = useSize(containerRef);
|
||||
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
if (!size?.width) {
|
||||
return;
|
||||
}
|
||||
const totalSize = sum(state.itemWidth);
|
||||
// 排除还没有进行过拖拽的情况,此时本地 state 中没有记录上次分配的宽度
|
||||
if (totalSize <= 0) {
|
||||
return;
|
||||
}
|
||||
const ratio = size.width / totalSize;
|
||||
const newItemWidth = state.itemWidth.map(w => w * ratio);
|
||||
childRef.current.forEach(
|
||||
(item, index) => (item.style.width = `${newItemWidth[index]}px`),
|
||||
);
|
||||
setState({
|
||||
...state,
|
||||
itemWidth: newItemWidth,
|
||||
});
|
||||
},
|
||||
[size?.width],
|
||||
{
|
||||
wait: 20,
|
||||
maxWait: 100,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'flex w-full items-stretch',
|
||||
className,
|
||||
state.moving && 'cursor-col-resize select-none',
|
||||
)}
|
||||
ref={containerRef}
|
||||
>
|
||||
{Children.map(children, (child, index) => {
|
||||
let node: ReactNode;
|
||||
if (isValidElement(child)) {
|
||||
node = cloneElement(
|
||||
child,
|
||||
Object.assign({}, child.props, {
|
||||
ref: (target: React.ReactNode) => {
|
||||
if (target instanceof HTMLElement) {
|
||||
childRef.current[index] = target;
|
||||
} else {
|
||||
if (!IS_PROD && target) {
|
||||
throw Error(
|
||||
'children of ResizableLayout need a ref of HTMLElement',
|
||||
);
|
||||
}
|
||||
}
|
||||
// @ts-expect-error -- 跳过类型体操
|
||||
const { ref } = child;
|
||||
if (typeof ref === 'function') {
|
||||
ref(target);
|
||||
} else if (ref && typeof ref === 'object') {
|
||||
ref.current = target;
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
node = (
|
||||
<div
|
||||
ref={elm => {
|
||||
if (elm) {
|
||||
childRef.current[index] = elm;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{index > 0 && (
|
||||
<ResizableLayoutHandle
|
||||
className={handleClassName}
|
||||
hotZoneClassName={hotZoneClassName}
|
||||
onMoveStart={() => {
|
||||
setState({
|
||||
moving: true,
|
||||
itemWidth: childRef.current.map(
|
||||
item => item.clientWidth ?? 0,
|
||||
),
|
||||
});
|
||||
}}
|
||||
// 相对于初始位置的偏移量
|
||||
onMove={offset => {
|
||||
const pre = index - 1;
|
||||
childRef.current[pre].style.width = `${
|
||||
state.itemWidth[pre] + offset
|
||||
}px`;
|
||||
childRef.current[index].style.width = `${
|
||||
state.itemWidth[index] - offset
|
||||
}px`;
|
||||
}}
|
||||
onMoveEnd={() => {
|
||||
setState({
|
||||
// 拖拽结束后,记录真实宽度
|
||||
itemWidth: childRef.current.map(
|
||||
item => item.clientWidth ?? 0,
|
||||
),
|
||||
moving: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{node}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface ResizableLayoutProps {
|
||||
className?: string;
|
||||
handleClassName?: string;
|
||||
hotZoneClassName?: string;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user