feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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:\/\//, '');
|
||||
Reference in New Issue
Block a user