feat: manually mirror opencoze's code from bytedance

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

View File

@@ -0,0 +1,31 @@
import { mergeConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
viteFinal: config =>
mergeConfig(config, {
plugins: [
svgr({
svgrOptions: {
native: false,
},
}),
],
}),
};
export default config;

View File

@@ -0,0 +1,14 @@
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

View File

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

View File

@@ -0,0 +1,16 @@
# @coze-agent-ide/bot-config-area
> 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`

View File

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

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

View File

@@ -0,0 +1,58 @@
{
"name": "@coze-agent-ide/bot-config-area",
"version": "0.0.1",
"description": "@coze-agent-ide/bot-config-area",
"license": "Apache-2.0",
"author": "haozhenfei@bytedance.com",
"maintainers": [],
"main": "src/index.ts",
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-agent-ide/bot-creator-context": "workspace:*",
"@coze-agent-ide/bot-editor-context-store": "workspace:*",
"@coze-agent-ide/model-manager": "workspace:*",
"@coze-agent-ide/space-bot": "workspace:*",
"@coze-agent-ide/tool": "workspace:*",
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-studio-store": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/idl": "workspace:*",
"@coze-studio/bot-detail-store": "workspace:*",
"@coze-studio/components": "workspace:*",
"ahooks": "^3.7.8",
"classnames": "^2.3.2",
"lodash-es": "^4.17.21",
"zustand": "^4.4.7"
},
"devDependencies": {
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/lodash-es": "^4.17.10",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"stylelint": "^15.11.0",
"vite-plugin-svgr": "~3.3.0",
"vitest": "~3.0.5"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
}
}

View File

@@ -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 { DialogueConfigView } from './model-config-view/dialogue-config-view';
export { SingleAgentModelView } from './model-config-view/single-agent-model-view';
export { MonetizeConfigButton } from './monetize-config/button';
export { MonetizeConfigPanel } from './monetize-config/panel';
export { QueryCollect } from './query-collect';
export { ModelConfigView } from './model-config-view';
export type { SingleAgentModelViewProps } from './model-config-view/single-agent-model-view';

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type ReactNode } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { I18n } from '@coze-arch/i18n';
import { Popover } from '@coze-arch/bot-semi';
import { CollapsibleIconButton } from '@coze-studio/components/collapsible-icon-button';
import { InputSlider } from '@coze-studio/components';
import { useModelStore } from '@coze-studio/bot-detail-store/model';
import { ModelFormItem } from '@coze-agent-ide/model-manager';
import { IconCozChatSetting } from '@coze-arch/coze-design/icons';
const DialogueConfig: FC<{ tips: ReactNode }> = ({ tips }) => {
const { model, setModelByImmer } = useModelStore(
useShallow(state => ({
model: state,
setModelByImmer: state.setModelByImmer,
})),
);
const handleChange = (value: number) => {
setModelByImmer(draft => {
if (!draft.config.ShortMemPolicy) {
draft.config.ShortMemPolicy = { HistoryRound: value };
return;
}
draft.config.ShortMemPolicy.HistoryRound = value;
});
};
return (
<div className="p-[24px]">
<div className="leading-[32px] coz-fg-plus text-[20px] font-[500]">
{I18n.t('workflow_agent_dialog_set')}
</div>
{tips ? (
<div className="mt-[16px] coz-fg-secondary text-[14px] leading-[20px]">
{tips}
</div>
) : null}
<div className="mt-[16px] coz-fg-plus text-[14px] leading-[20px] font-[500]">
{I18n.t('workflow_agent_dialog_set_chathistory')}
</div>
<ModelFormItem
popoverContent={I18n.t('model_config_history_round_explain')}
label={I18n.t('model_config_history_round')}
>
<InputSlider
step={1}
min={0}
max={100}
decimalPlaces={0}
value={model.config.ShortMemPolicy?.HistoryRound}
onChange={handleChange}
/>
</ModelFormItem>
</div>
);
};
const itemKey = Symbol.for('DialogueConfigView');
export const DialogueConfigView: FC<{
tips: ReactNode;
}> = ({ tips }) => (
<Popover
className="overflow-hidden rounded-[12px] w-[600px]"
trigger="click"
autoAdjustOverflow={true}
content={<DialogueConfig tips={tips} />}
>
<CollapsibleIconButton
itemKey={itemKey}
data-testid="bot.ide.bot_creator.set_model_view_button"
icon={<IconCozChatSetting className="text-[16px]" />}
text={I18n.t('workflow_agent_dialog_set')}
/>
</Popover>
);

View File

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

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { BotMode } from '@coze-arch/bot-api/playground_api';
import { useGetSingleAgentCurrentModel } from '@coze-agent-ide/model-manager';
import { SingleAgentModelView } from './single-agent-model-view';
import { DialogueConfigView } from './dialogue-config-view';
export const ModelConfigView: React.FC<{
mode: BotMode;
modelListExtraHeaderSlot?: React.ReactNode;
}> = ({ mode, modelListExtraHeaderSlot }) => {
const currentModel = useGetSingleAgentCurrentModel();
if (mode === BotMode.SingleMode) {
return currentModel?.model_type ? (
<SingleAgentModelView
modelListExtraHeaderSlot={modelListExtraHeaderSlot}
/>
) : null;
}
if (mode === BotMode.MultiMode || mode === BotMode.WorkflowMode) {
return (
<DialogueConfigView
tips={
mode === BotMode.WorkflowMode
? I18n.t('workflow_agent_dialog_set_desc')
: null
}
/>
);
}
return null;
};

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useModelStore } from '@coze-studio/bot-detail-store/model';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { type Model } from '@coze-arch/bot-api/developer_api';
import { ModelSelect } from '@coze-agent-ide/model-manager/model-select-v2';
import {
useModelCapabilityCheckModal,
useGetSingleAgentCurrentModel,
getModelOptionList,
} from '@coze-agent-ide/model-manager';
import { useBotEditor } from '@coze-agent-ide/bot-editor-context-store';
import {
useBotCreatorContext,
BotCreatorScene,
} from '@coze-agent-ide/bot-creator-context';
export interface SingleAgentModelViewProps {
modelListExtraHeaderSlot?: React.ReactNode;
triggerRender?: (model?: Model, popoverVisible?: boolean) => React.ReactNode;
}
export function SingleAgentModelView(props: SingleAgentModelViewProps) {
const { modelListExtraHeaderSlot, triggerRender } = props;
const spaceId = useSpaceStore(store => store.space.id);
const { scene } = useBotCreatorContext();
const currentModel = useGetSingleAgentCurrentModel();
const currentModelId = currentModel?.model_type
? String(currentModel.model_type)
: undefined;
const { storeSet } = useBotEditor();
const modelStore = storeSet.useModelStore(
useShallow(state => ({
onlineModelList: state.onlineModelList,
offlineModelMap: state.offlineModelMap,
getModelPreset: state.getModelPreset,
})),
);
const [currentModelIdState, setCurrentModelIdState] = useState<
string | undefined
>(currentModelId);
const { modelConfig, setModelByImmer } = useModelStore(
useShallow(state => ({
modelConfig: state.config,
setModelByImmer: state.setModelByImmer,
})),
);
const { modalNode, checkAndOpenModal } = useModelCapabilityCheckModal({
onOk: modelId => {
setCurrentModelIdState(modelId);
},
});
const isReadonly = useBotDetailIsReadonly();
const modelList = getModelOptionList({
onlineModelList: modelStore.onlineModelList,
offlineModelMap: modelStore.offlineModelMap,
currentModelId: String(currentModel?.model_type),
});
useEffect(() => {
setCurrentModelIdState(currentModelId);
}, [currentModelId]);
return currentModelIdState ? (
<>
<ModelSelect
popoverClassName="h-auto !max-h-[70vh]"
disabled={isReadonly}
enableJumpDetail={
scene === BotCreatorScene.Bot && spaceId && !IS_OPEN_SOURCE
? { spaceId }
: undefined
}
modelListExtraHeaderSlot={modelListExtraHeaderSlot}
selectedModelId={currentModelIdState}
modelList={modelList}
onModelChange={m => {
const modelId = String(m.model_type);
const checkPassed = checkAndOpenModal(modelId);
if (checkPassed) {
setCurrentModelIdState(modelId);
}
return checkPassed;
}}
modelConfigProps={{
hideDiversityCollapseButton: true,
agentType: 'single',
currentConfig: modelConfig,
onConfigChange: v => {
setModelByImmer(draft => {
draft.config = {
model: currentModelIdState,
...v,
};
});
},
modelStore,
}}
triggerRender={triggerRender}
modalSlot={modalNode}
/>
</>
) : null;
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CollapsibleIconButton } from '@coze-studio/components/collapsible-icon-button';
import { useMonetizeConfigStore } from '@coze-studio/bot-detail-store';
import { I18n } from '@coze-arch/i18n';
import { IconCozWallet } from '@coze-arch/coze-design/icons';
import { Popover } from '@coze-arch/coze-design';
import { MonetizeConfigPanel } from '../panel';
const itemKey = Symbol.for('MonetizeConfigButton');
export function MonetizeConfigButton() {
const isOn = useMonetizeConfigStore(store => store.isOn);
return (
<Popover
trigger="click"
autoAdjustOverflow={true}
content={<MonetizeConfigPanel />}
>
<CollapsibleIconButton
itemKey={itemKey}
icon={<IconCozWallet className="text-[16px]" />}
text={isOn ? I18n.t('monetization_on') : I18n.t('monetization_off')}
color={isOn ? 'highlight' : 'secondary'}
/>
</Popover>
);
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useDebounceFn } from 'ahooks';
import { useMonetizeConfigReadonly } from '@coze-agent-ide/space-bot/hook';
import {
MonetizeCreditRefreshCycle,
MonetizeDescription,
MonetizeFreeChatCount,
MonetizeSwitch,
} from '@coze-studio/components/monetize';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { useMonetizeConfigStore } from '@coze-studio/bot-detail-store';
import {
MonetizationEntityType,
type BotMonetizationRefreshPeriod,
} from '@coze-arch/idl/benefit';
import { benefitApi } from '@coze-arch/bot-api';
export function MonetizeConfigPanel() {
const botId = useBotInfoStore(store => store.botId);
const {
isOn,
freeCount,
refreshCycle,
setIsOn,
setFreeCount,
setRefreshCycle,
} = useMonetizeConfigStore();
const isReadonly = useMonetizeConfigReadonly();
const { run: debouncedSaveBotConfig } = useDebounceFn(
({
isEnable,
freeChats,
}: {
isEnable: boolean;
freeChats: number;
refreshCycle: BotMonetizationRefreshPeriod;
}) => {
benefitApi.PublicSaveBotDraftMonetizationConfig({
entity_id: botId,
entity_type: MonetizationEntityType.Bot,
is_enable: isEnable,
free_chat_allowance_count: freeChats,
refresh_period: refreshCycle,
});
},
{ wait: 300 },
);
const refreshCycleDisabled = !isOn || isReadonly || freeCount <= 0;
return (
<div className="w-[480px] p-[24px] flex flex-col gap-[24px]">
<MonetizeSwitch
disabled={isReadonly}
isOn={isOn}
onChange={value => {
setIsOn(value);
debouncedSaveBotConfig({
isEnable: value,
freeChats: freeCount,
refreshCycle,
});
}}
/>
<MonetizeDescription isOn={isOn} />
<MonetizeFreeChatCount
isOn={isOn}
disabled={isReadonly}
freeCount={freeCount}
onFreeCountChange={value => {
setFreeCount(value);
debouncedSaveBotConfig({
isEnable: isOn,
freeChats: value,
refreshCycle,
});
}}
/>
<MonetizeCreditRefreshCycle
freeCount={freeCount}
disabled={refreshCycleDisabled}
refreshCycle={refreshCycle}
onRefreshCycleChange={value => {
setRefreshCycle(value);
debouncedSaveBotConfig({
isEnable: isOn,
freeChats: freeCount,
refreshCycle: value,
});
}}
/>
</div>
);
}

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type MouseEvent, useEffect, useRef, useState } from 'react';
import { get } from 'lodash-es';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { I18n } from '@coze-arch/i18n';
import { IconCozTamplate } from '@coze-arch/coze-design/icons';
import {
Button,
Form,
type FormApi,
IconButton,
Popover,
Typography,
} from '@coze-arch/coze-design';
import { type GenerateUserQueryCollectPolicyRequest } from '@coze-arch/bot-api/playground_api';
import { Tips } from './tips';
import s from './index.module.less';
const options = [
{
label: I18n.t('bot_dev_privacy_setting_conversation'),
value: I18n.t('bot_dev_privacy_setting_conversation'),
},
];
const defaultOptionsValue = [I18n.t('bot_dev_privacy_setting_conversation')];
interface GenerateByTemplateProps {
handleGenerate: (v: GenerateUserQueryCollectPolicyRequest) => void;
loading: boolean;
templateLink: string;
link: string;
}
// eslint-disable-next-line @coze-arch/max-line-per-function
export const GenerateByTemplate: FC<GenerateByTemplateProps> = ({
handleGenerate,
loading,
templateLink,
link,
}) => {
const { botId } = useBotInfoStore($store => ({
botId: $store.botId,
}));
const [visible, setVisible] = useState(false);
const [configInfo, setConfigInfo] =
useState<GenerateUserQueryCollectPolicyRequest>();
const [isFailToValid, setIsFailToValid] = useState(true);
const formApi = useRef<FormApi<GenerateUserQueryCollectPolicyRequest>>();
const onFormValueChange = (values: GenerateUserQueryCollectPolicyRequest) => {
const developerName = get(values, 'developer_name');
const contactInformation = get(values, 'contact_information');
setIsFailToValid(!(developerName && contactInformation));
setConfigInfo({
...values,
});
};
const onVisibleChange = (isVisble: boolean) => {
if (isVisble) {
setDefaultValue();
}
};
const setDefaultValue = () => {
if (configInfo) {
formApi.current?.setValue('developer_name', configInfo.developer_name);
formApi.current?.setValue(
'contact_information',
configInfo.contact_information,
);
}
};
useEffect(() => {
if (link) {
setConfigInfo({ developer_name: '', contact_information: '' });
setVisible(false);
}
}, [link]);
const onClickGenerate = () => {
handleGenerate({ ...configInfo, bot_id: botId });
};
const onOpen = (e: MouseEvent) => {
e.stopPropagation();
setVisible(true);
};
return (
<Popover
position="right"
trigger="custom"
stopPropagation={true}
onVisibleChange={onVisibleChange}
visible={visible}
onClickOutSide={() => setVisible(false)}
content={
<div className="p-[16px] w-[320px]">
<div className="coz-fg-plus text-[20px] font-medium leading-[32px]">
{I18n.t('bot_dev_privacy_setting_privacy_template_1')}
</div>
<div className="coz-fg-primary text-[14px] font-normal leading-[20px] pb-[12px]">
{I18n.t('bot_dev_privacy_setting_privacy_template_2', {
privacy_template: (
<Typography.Text link onClick={() => window.open(templateLink)}>
{I18n.t('bot_dev_privacy_setting_privacy_template_3')}
</Typography.Text>
),
})}
</div>
<div>
<Form<GenerateUserQueryCollectPolicyRequest>
getFormApi={api => (formApi.current = api)}
labelPosition="top"
showValidateIcon={false}
className={s['form-wrap']}
onValueChange={values =>
onFormValueChange(
values as GenerateUserQueryCollectPolicyRequest,
)
}
autoComplete="off"
disabled={loading}
>
<Form.Input
field="developer_name"
label={I18n.t('bot_dev_privacy_setting_developer_name')}
style={{ width: '100%' }}
trigger="blur"
maxLength={50}
placeholder={I18n.t(
'bot_dev_privacy_setting_developer_collect3',
)}
rules={[
{
required: true,
message: I18n.t(
'bot_dev_privacy_setting_developer_collect3',
),
},
]}
/>
<Form.Select
field="collect_detail"
label={{
text: I18n.t('bot_dev_privacy_setting_developer_collect1'),
extra: (
<Tips
content={I18n.t(
'bot_dev_privacy_setting_developer_collect7',
)}
size="small"
/>
),
}}
optionList={options}
disabled
initValue={defaultOptionsValue}
style={{ width: '100%' }}
placeholder={I18n.t(
'bot_dev_privacy_setting_developer_collect4',
)}
rules={[
{
required: true,
message: I18n.t(
'bot_dev_privacy_setting_developer_collect4',
),
},
]}
/>
<Form.Input
field="contact_information"
label={I18n.t('bot_dev_privacy_setting_developer_collect2')}
style={{ width: '100%' }}
trigger="blur"
maxLength={50}
placeholder={I18n.t(
'bot_dev_privacy_setting_developer_collect5',
)}
rules={[
{
required: true,
message: I18n.t(
'bot_dev_privacy_setting_developer_collect5',
),
},
]}
/>
</Form>
</div>
<div className="flex justify-end mt-[12px]">
<Button
loading={loading}
onClick={onClickGenerate}
disabled={isFailToValid}
>
{I18n.t(
loading
? 'bot_dev_privacy_setting_generate_link2'
: 'bot_dev_privacy_setting_generate_link1',
)}
</Button>
</div>
</div>
}
>
<IconButton
icon={<IconCozTamplate />}
iconPosition="left"
color="secondary"
size="small"
onClick={onOpen}
>
{I18n.t('bot_dev_privacy_setting_privacy_template')}
</IconButton>
</Popover>
);
};

View File

@@ -0,0 +1,96 @@
/* stylelint-disable max-nesting-depth */
/* stylelint-disable selector-class-pattern */
.query-collect-modal {
:global {
.semi-modal-header {
align-items: center;
.semi-button-borderless {
width: 40px;
height: 40px;
padding: 11px;
&:hover {
background-color: rgba(var(--coze-bg-6), var(--coze-bg-6-alpha));
}
&:active {
background-color: rgba(var(--coze-bg-8), var(--coze-bg-8-alpha));
}
.semi-button-content {
color: rgba(var(--coze-fg-2), var(--coze-fg-2-alpha));
.semi-icon {
font-size: 18px;
}
}
}
}
.semi-modal-footer {
margin: 0;
}
.semi-switch:not(.semi-switch-checked){
background-color: var(--semi-color-fill-0);
}
.semi-switch:not(.semi-switch-checked):hover {
background-color: var(--semi-color-fill-1);
}
}
}
.form-wrap {
:global {
.semi-form-field {
padding: 0;
padding-bottom: 16px;
}
.semi-form-field-label {
margin-bottom: 6px;
padding: 0 8px;
font-size: 12px;
font-weight: 500;
font-style: normal;
line-height: 16px;
color: var(--coz-fg-secondary);
}
.semi-form-field-error-message {
padding-left: 8px;
}
.semi-input-prefix-text {
font-size: 12px;
font-weight: 400;
color: var(--coz-fg-secondary);
}
.semi-input-wrapper {
background-color: transparent;
}
.semi-input-suffix {
.coz-icon-button {
display: flex;
padding-right: 4px;
.coz-button.coz-btn-small{
border-radius: 6px;
}
}
}
.semi-form-field-label-required .semi-form-field-label-text::after{
margin-left: 0;
}
.semi-input-wrapper__with-suffix .semi-input{
padding-right: 4px;
}
}
}

View File

@@ -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 { type FC, useEffect, useRef, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { CollapsibleIconButton } from '@coze-studio/components/collapsible-icon-button';
import { updateQueryCollect } from '@coze-studio/bot-detail-store/save-manager';
import { useQueryCollectStore } from '@coze-studio/bot-detail-store/query-collect';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { I18n } from '@coze-arch/i18n';
import {
useGenerateLink,
useGetUserQueryCollectOption,
} from '@coze-agent-ide/space-bot/hook';
import { IconCozEye } from '@coze-arch/coze-design/icons';
import { Modal, Switch, Form, type FormApi } from '@coze-arch/coze-design';
import { getUrlValue, isValidUrl } from './utils';
import { Tips } from './tips';
import { GenerateByTemplate } from './generate-by-template';
import s from './index.module.less';
const itemKey = Symbol.for('QueryCollect');
// eslint-disable-next-line @coze-arch/max-line-per-function
export const QueryCollect: FC = () => {
const { privatePolicy, isCollect, setQueryCollect } = useQueryCollectStore(
useShallow($store => ({
isCollect: $store.is_collected,
privatePolicy: $store.private_policy,
setQueryCollect: $store.setQueryCollect,
})),
);
const { queryCollectOption, supportText } = useGetUserQueryCollectOption();
const isReadonly = useBotDetailIsReadonly();
const { link, loading, runGenerate } = useGenerateLink();
const formApi = useRef<FormApi<{ policyLink: string }>>();
const [visible, setVisible] = useState(false);
const [checked, setChecked] = useState(false);
const [privacyUrl, setPrivacyUrl] = useState(privatePolicy);
const privacyErrMsg = useRef('');
const onClose = () => {
setVisible(false);
};
const onOk = async () => {
const policyLink = formApi.current?.getValue('policyLink');
const queryCollectConf = {
is_collected: checked,
// cp-disable-next-line
private_policy: checked ? `https://${policyLink}` : '',
};
const {
data: { check_not_pass_msg, check_not_pass },
} = await updateQueryCollect(queryCollectConf);
privacyErrMsg.current = check_not_pass ? (check_not_pass_msg ?? '') : '';
await formApi.current?.validate();
setQueryCollect(queryCollectConf);
setVisible(false);
};
const openModal = () => {
setVisible(true);
};
useEffect(() => {
if (link) {
formApi.current?.setValue('policyLink', getUrlValue(link));
formApi.current?.validate();
}
}, [link]);
useEffect(() => {
setChecked(isCollect);
formApi.current?.setValue('policyLink', getUrlValue(privatePolicy ?? ''));
}, [visible, privatePolicy, isCollect]);
useEffect(() => {
privacyErrMsg.current = '';
}, [privacyUrl]);
return (
<>
<CollapsibleIconButton
itemKey={itemKey}
text={I18n.t('bot_dev_privacy_title')}
icon={<IconCozEye className="text-[16px]" />}
onClick={openModal}
/>
<Modal
width={480}
visible={visible}
onCancel={onClose}
maskClosable={false}
title={
<span className="text-[20px]">{I18n.t('bot_dev_privacy_title')}</span>
}
cancelText={I18n.t('cancel')}
okText={I18n.t('confirm')}
className={s['query-collect-modal']}
onOk={onOk}
okButtonProps={{
disabled: loading || isReadonly,
style: { marginLeft: '8px' },
}}
>
<div className="py-[16px]">
<div className="flex items-center justify-between py-[16px] pl-[12px] pr-[24px] rounded-[8px] border border-solid border-[var(--coz-stroke-plus)]">
<div className="flex items-center justify-center gap-[3px] ">
<span className="coz-fg-plus text-[14px] font-normal leading-[20px] ">
{I18n.t('bot_dev_privacy_setting_title')}
</span>
<Tips
content={
I18n.t('bot_dev_privacy_setting_channel') + supportText
}
size="medium"
/>
</div>
<Switch
checked={checked}
size="small"
onChange={v => setChecked(v)}
disabled={loading || isReadonly}
/>
</div>
<div className="coz-fg-secondary text-[12px] font-normal leading-[16px] px-[8px] pt-[2px] pb-[18px]">
{I18n.t('bot_dev_privacy_setting_desc')}
</div>
<div style={{ display: checked ? 'block' : 'none' }}>
<Form<{ policyLink: string }>
getFormApi={api => (formApi.current = api)}
labelPosition="top"
showValidateIcon={false}
className={s['form-wrap']}
autoComplete="off"
disabled={loading || isReadonly}
>
<Form.Input
field="policyLink"
label={I18n.t('bot_dev_privacy_setting_link1')}
style={{ width: '100%' }}
trigger="blur"
// cp-disable-next-line
prefix="https://"
stopValidateWithError
maxLength={50}
disabled={loading || isReadonly}
placeholder={I18n.t('privacy_link_placeholder')}
onChange={setPrivacyUrl}
suffix={
IS_OVERSEA || isReadonly ? null : (
<GenerateByTemplate
handleGenerate={runGenerate}
loading={loading}
templateLink={
queryCollectOption?.private_policy_template || ''
}
link={link}
/>
)
}
rules={[
{
validator: (_, value) => !checked || isValidUrl(value),
message: I18n.t('bot_dev_privacy_setting_invalid_link'),
},
{
validator: () => !checked || !privacyErrMsg.current,
message: () => privacyErrMsg.current,
},
]}
helpText={
<div className="coz-fg-secondary text-[12px] font-normal leading-[16px] px-[8px] pt-[2px]">
{I18n.t('bot_dev_privacy_setting_link2')}
</div>
}
/>
</Form>
</div>
</div>
</Modal>
</>
);
};

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from 'classnames';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tooltip } from '@coze-arch/coze-design';
export const Tips = ({
content,
size = 'medium',
}: {
content: string;
size: 'small' | 'medium';
}) => (
<Tooltip content={content}>
<div
className={classNames(
size === 'small'
? 'w-[16px] h-[16px] rounded-[4px]'
: 'w-[24px] h-[24px] rounded-[8px]',
'flex items-center justify-center hover:coz-mg-secondary-hovered cursor-pointer',
)}
>
<IconCozInfoCircle className="coz-fg-secondary" />
</div>
</Tooltip>
);

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const DOMAIN_REGEXP = /^([0-9a-zA-Z-]{1,}\.)+([a-zA-Z]{2,})$/;
export function isValidUrl(url: string): boolean {
try {
// cp-disable-next-line
const urlObject = new URL(`https://${url}`);
return DOMAIN_REGEXP.test(urlObject.hostname);
// eslint-disable-next-line @coze-arch/use-error-in-catch -- 根据函数功能无需 throw error
} catch {
return false;
}
}
// cp-disable-next-line
export const getUrlValue = (url: string) => url?.replace(/^https:\/\//, '');

View File

@@ -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.
*/
/// <reference types='@coze-arch/bot-typings' />
declare const IS_OVERSEA: boolean;

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DemoComponent } from '../src';
export default {
title: 'Example/Demo',
component: DemoComponent,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {},
};
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Base = {
args: {
name: 'tecvan',
},
};

View File

@@ -0,0 +1,34 @@
import { Meta } from "@storybook/blocks";
<Meta title="Hello world" />
<div className="sb-container">
<div className='sb-section-title'>
# Hello world
Hello world
</div>
</div>
<style>
{`
.sb-container {
margin-bottom: 48px;
}
.sb-section {
width: 100%;
display: flex;
flex-direction: row;
gap: 20px;
}
img {
object-fit: cover;
}
.sb-section-title {
margin-bottom: 32px;
}
`}
</style>

View File

@@ -0,0 +1,69 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"jsx": "react-jsx",
"lib": ["DOM", "ESNext"],
"module": "ESNext",
"target": "ES2020",
"moduleResolution": "bundler",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"exclude": ["node_modules", "dist"],
"references": [
{
"path": "../../arch/bot-api/tsconfig.build.json"
},
{
"path": "../../arch/bot-store/tsconfig.build.json"
},
{
"path": "../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../arch/i18n/tsconfig.build.json"
},
{
"path": "../../arch/idl/tsconfig.build.json"
},
{
"path": "../bot-editor-context-store/tsconfig.build.json"
},
{
"path": "../../components/bot-semi/tsconfig.build.json"
},
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../context/tsconfig.build.json"
},
{
"path": "../model-manager/tsconfig.build.json"
},
{
"path": "../space-bot/tsconfig.build.json"
},
{
"path": "../../studio/components/tsconfig.build.json"
},
{
"path": "../../studio/stores/bot-detail/tsconfig.build.json"
},
{
"path": "../tool/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"exclude": ["**/*"],
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
]
}

View File

@@ -0,0 +1,20 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"jsx": "react-jsx",
"lib": ["DOM", "ESNext"],
"module": "ESNext",
"target": "ES2020",
"moduleResolution": "bundler"
},
"include": ["__tests__", "vitest.config.ts", "stories"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
]
}

View File

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