feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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,181 @@
|
||||
/*
|
||||
* 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 {
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useHover } from 'ahooks';
|
||||
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 { type TriggerAction } from '../../type';
|
||||
import { getIsFileFormatValid } from '../../helpers/get-is-file-format-valid';
|
||||
import {
|
||||
FILE_EXTENSION_LIST,
|
||||
getFileSizeReachLimitI18n,
|
||||
MAX_FILE_SIZE,
|
||||
} from '../../constant/file';
|
||||
import { getFixedVariableTemplate } from '../../../../utils/onboarding-variable';
|
||||
import { OnboardingVariable } from '../../../../constant/onboarding-variable';
|
||||
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';
|
||||
|
||||
export interface ActionBarProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
onTriggerAction?: (action: TriggerAction) => void;
|
||||
}
|
||||
|
||||
const iconButtonProps: ComponentProps<typeof UIButton> = {
|
||||
size: 'small',
|
||||
type: 'tertiary',
|
||||
theme: 'borderless',
|
||||
className: styles['icon-button'],
|
||||
};
|
||||
export const ActionBar: React.FC<ActionBarProps> = ({
|
||||
className,
|
||||
style,
|
||||
onTriggerAction,
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const uploadButtonRef = useRef(null);
|
||||
const isHover = useHover(uploadButtonRef);
|
||||
|
||||
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={FILE_EXTENSION_LIST.join(',')}
|
||||
onAcceptInvalid={showFileTypeInvalidToast}
|
||||
limit={1}
|
||||
onFileChange={onFileChange}
|
||||
action="/"
|
||||
fileList={[]}
|
||||
>
|
||||
<Tooltip
|
||||
visible={isHover}
|
||||
trigger="custom"
|
||||
content={I18n.t('add_image')}
|
||||
>
|
||||
<div ref={uploadButtonRef}>
|
||||
<UIButton
|
||||
ref={uploadButtonRef}
|
||||
{...iconButtonProps}
|
||||
icon={<IconImageOutlined />}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
|
||||
<InsertLinkPopover
|
||||
visible={visible}
|
||||
onClickOutSide={closePopover}
|
||||
onConfirm={onConfirmInsertLink}
|
||||
>
|
||||
<span>
|
||||
<Tooltip content={I18n.t('add_link')}>
|
||||
<UIButton
|
||||
onClick={togglePopoverVisible}
|
||||
{...iconButtonProps}
|
||||
className={classNames(
|
||||
visible && styles['icon-button-active'],
|
||||
styles['icon-button'],
|
||||
)}
|
||||
icon={<IconLinkStroked />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</InsertLinkPopover>
|
||||
|
||||
<Tooltip content={I18n.t('add_nickname')}>
|
||||
<UIButton
|
||||
{...iconButtonProps}
|
||||
icon={<IconMemberOutlined />}
|
||||
onClick={onInsertVariable}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
.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;
|
||||
border-radius: 12px;
|
||||
|
||||
|
||||
.input-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.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,25 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export const MAX_FILE_SIZE = 20 * 1024 * 1024;
|
||||
export const getFileSizeReachLimitI18n = () =>
|
||||
I18n.t('file_too_large', {
|
||||
max_size: '20MB',
|
||||
});
|
||||
|
||||
export const FILE_EXTENSION_LIST = ['.jpeg', '.jpg', '.png'];
|
||||
@@ -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 { type SyncAction } from '../type';
|
||||
import { primitiveExhaustiveCheck } from '../../../utils/exhaustive-check';
|
||||
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,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 { FILE_EXTENSION_LIST } from '../constant/file';
|
||||
|
||||
export const getIsFileFormatValid = (file: File) =>
|
||||
FILE_EXTENSION_LIST.some(extension => file.name.endsWith(extension));
|
||||
@@ -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,157 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import useEventCallback from 'use-event-callback';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { useDragAndPasteUpload } from '@coze-arch/bot-hooks';
|
||||
|
||||
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 { primitiveExhaustiveCheck } from '../../../utils/exhaustive-check';
|
||||
import { type MarkdownEditorProps } from '..';
|
||||
import { useUpload } from './use-upload-file';
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export const useMarkdownEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
getUserId,
|
||||
maxLength,
|
||||
getValueLength,
|
||||
getSlicedTextOnExceed,
|
||||
}: 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,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const { uploadFileList, uploadState } = useUpload({
|
||||
getUserId,
|
||||
onUploadAllSuccess,
|
||||
});
|
||||
|
||||
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]);
|
||||
|
||||
const onTextareaChange = (text: string) => {
|
||||
onChange(text);
|
||||
};
|
||||
|
||||
const handleInsertText = (insertText: string) => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
ref.current.focus();
|
||||
const { selectionEnd } = ref.current;
|
||||
/**
|
||||
* 选中文字时点击 action bar, 将内容插入到文字的末尾
|
||||
*/
|
||||
const insertTextAtPosition = getInsertTextAtPosition({
|
||||
text: value,
|
||||
insertText,
|
||||
position: selectionEnd,
|
||||
});
|
||||
const valueLength =
|
||||
getValueLength?.(insertTextAtPosition) ?? insertTextAtPosition.length;
|
||||
if (maxLength && valueLength > maxLength) {
|
||||
const slicedText =
|
||||
getSlicedTextOnExceed?.(insertTextAtPosition) ??
|
||||
insertTextAtPosition.slice(0, maxLength);
|
||||
onChange(slicedText);
|
||||
} else {
|
||||
onChange(insertTextAtPosition);
|
||||
}
|
||||
setWrapInsertionIndex(selectionEnd + insertText.length);
|
||||
};
|
||||
|
||||
return {
|
||||
textAreaRef: ref,
|
||||
onTextareaChange,
|
||||
onTriggerAction,
|
||||
dragTargetRef,
|
||||
uploadState,
|
||||
isDragOver,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { 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 handleAuditFailed = (controllerId: string) => {
|
||||
clearState();
|
||||
cancelUploadById(controllerId);
|
||||
Toast.error({
|
||||
content: withSlardarIdButton(I18n.t('inappropriate_contents')),
|
||||
showClose: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
clearState();
|
||||
};
|
||||
|
||||
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,
|
||||
onAuditFailed: handleAuditFailed,
|
||||
});
|
||||
};
|
||||
|
||||
const clearAllSideEffect = () => {
|
||||
Object.entries(uploadControllerMap.current).forEach(([, controller]) =>
|
||||
controller.cancel(),
|
||||
);
|
||||
uploadControllerMap.current = {};
|
||||
};
|
||||
|
||||
useEffect(() => clearAllSideEffect, []);
|
||||
|
||||
return {
|
||||
uploadState,
|
||||
uploadFileList,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
.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 {
|
||||
flex-shrink: 0;
|
||||
|
||||
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-wrapper {
|
||||
flex: 1;
|
||||
|
||||
}
|
||||
|
||||
.markdown-editor-content {
|
||||
resize: none;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
||||
// 这里不需要背景色 强行覆盖
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background-color: inherit !important;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
&::selection {
|
||||
background: rgb(77 83 232 / 20%);
|
||||
}
|
||||
|
||||
>textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
padding: 0 !important;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
|
||||
// rgb mark
|
||||
color: #383743;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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 { TextArea } from '@coze-arch/coze-design';
|
||||
|
||||
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;
|
||||
onChange: (value: string) => void;
|
||||
getUserId: () => string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
maxLength?: number | undefined;
|
||||
getValueLength?: (value: string) => number;
|
||||
/** 超过最大长度时截断函数 */
|
||||
getSlicedTextOnExceed?: (value: string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全受控组件
|
||||
*/
|
||||
export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
|
||||
value,
|
||||
className,
|
||||
style,
|
||||
maxLength,
|
||||
getValueLength,
|
||||
getSlicedTextOnExceed,
|
||||
getUserId,
|
||||
onChange,
|
||||
}) => {
|
||||
const {
|
||||
textAreaRef,
|
||||
dragTargetRef,
|
||||
onTextareaChange,
|
||||
onTriggerAction,
|
||||
isDragOver,
|
||||
uploadState,
|
||||
} = useMarkdownEditor({
|
||||
value,
|
||||
maxLength,
|
||||
getValueLength,
|
||||
getSlicedTextOnExceed,
|
||||
getUserId,
|
||||
onChange,
|
||||
});
|
||||
|
||||
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}
|
||||
/>
|
||||
<TextArea
|
||||
ref={textAreaRef}
|
||||
value={value}
|
||||
onChange={onTextareaChange}
|
||||
className={styles['markdown-editor-content']}
|
||||
wrapperClassName={styles['markdown-editor-wrapper']}
|
||||
maxLength={maxLength}
|
||||
getValueLength={getValueLength}
|
||||
/>
|
||||
{uploadState ? <UploadProgressMask {...uploadState} /> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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;
|
||||
onAuditFailed?: (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,
|
||||
onAuditFailed,
|
||||
}: 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 urlAndAudit = result.data?.url_info?.[uri];
|
||||
|
||||
const audit = urlAndAudit?.review_status;
|
||||
|
||||
const url = urlAndAudit?.url;
|
||||
|
||||
if (!audit) {
|
||||
onAuditFailed?.(controllerId);
|
||||
return;
|
||||
}
|
||||
|
||||
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,51 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
.content {
|
||||
overflow: hidden;
|
||||
width: 276px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
color: rgb(6 7 9 / 80%);
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-bottom: 16px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
color: rgb(6 7 9 / 50%);
|
||||
}
|
||||
|
||||
.table {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
color: rgb(6 7 9 / 80%);
|
||||
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
color: #060709;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.row {
|
||||
&:last-child {
|
||||
* {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.icon-cell {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.cell-column {
|
||||
width: 24px !important;
|
||||
min-width: none !important;
|
||||
}
|
||||
|
||||
.mark-column {
|
||||
width: 68px;
|
||||
min-width: none !important;
|
||||
padding-right: 0 !important;
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
padding-left: 0 !important
|
||||
}
|
||||
|
||||
.example-column {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
padding-right: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* 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 -- 纯 ui 渲染 */
|
||||
import { type PropsWithChildren, type ComponentProps } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconButton, Popover, Table } from '@coze-arch/bot-semi';
|
||||
import { IconCloseNoCycle } from '@coze-arch/bot-icons';
|
||||
|
||||
import { ReactComponent as Strikethrough } from '../../../../assets/markdown-icon/strikethrough.svg';
|
||||
import { ReactComponent as Quote } from '../../../../assets/markdown-icon/quote.svg';
|
||||
import { ReactComponent as NumberedList } from '../../../../assets/markdown-icon/numbered-list.svg';
|
||||
import { ReactComponent as Italic } from '../../../../assets/markdown-icon/italic.svg';
|
||||
import { ReactComponent as H3 } from '../../../../assets/markdown-icon/h3.svg';
|
||||
import { ReactComponent as H2 } from '../../../../assets/markdown-icon/h2.svg';
|
||||
import { ReactComponent as H1 } from '../../../../assets/markdown-icon/h1.svg';
|
||||
import { ReactComponent as Code } from '../../../../assets/markdown-icon/code.svg';
|
||||
import { ReactComponent as CodeBlock } from '../../../../assets/markdown-icon/code-block.svg';
|
||||
import { ReactComponent as BulletedList } from '../../../../assets/markdown-icon/bulleted-list.svg';
|
||||
import { ReactComponent as Bold } from '../../../../assets/markdown-icon/bold.svg';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export type MarkdownDescriptionPopoverProps = Pick<
|
||||
ComponentProps<typeof Popover>,
|
||||
'visible' | 'onVisibleChange'
|
||||
>;
|
||||
|
||||
interface MarkdownDescription {
|
||||
mark: string;
|
||||
example: string;
|
||||
iconKey: string;
|
||||
}
|
||||
|
||||
type TableProps = ComponentProps<typeof Table>;
|
||||
|
||||
const IconMap: Record<
|
||||
string,
|
||||
React.FunctionComponent<React.SVGProps<SVGSVGElement>>
|
||||
> = {
|
||||
h1: props => <H1 {...props} />,
|
||||
h2: props => <H2 {...props} />,
|
||||
h3: props => <H3 {...props} />,
|
||||
bold: props => <Bold {...props} />,
|
||||
italic: props => <Italic {...props} />,
|
||||
strikethrough: props => <Strikethrough {...props} />,
|
||||
quote: props => <Quote {...props} />,
|
||||
code: props => <Code {...props} />,
|
||||
codeBlock: props => <CodeBlock {...props} />,
|
||||
numberedList: props => <NumberedList {...props} />,
|
||||
bulletedList: props => <BulletedList {...props} />,
|
||||
};
|
||||
|
||||
const columns: Required<TableProps>['columns'] = [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'icon',
|
||||
className: styles['cell-column'],
|
||||
onCell: () => ({
|
||||
className: styles['icon-cell'],
|
||||
}),
|
||||
render: (_, record: MarkdownDescription) => {
|
||||
const IconComponent = IconMap[record.iconKey];
|
||||
if (!IconComponent) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles['icon-wrapper']}>
|
||||
<IconComponent className={styles.icon} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'mark',
|
||||
className: styles['mark-column'],
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'example',
|
||||
align: 'right',
|
||||
className: styles['example-column'],
|
||||
},
|
||||
];
|
||||
const getData: () => MarkdownDescription[] = () => [
|
||||
{
|
||||
mark: I18n.t('markdown_heading1'),
|
||||
example: I18n.t('markdown_heading1_syntax', { space: I18n.t('space') }),
|
||||
iconKey: 'h1',
|
||||
},
|
||||
{
|
||||
mark: I18n.t('markdown_heading2'),
|
||||
example: I18n.t('markdown_heading2_syntax', { space: I18n.t('space') }),
|
||||
iconKey: 'h2',
|
||||
},
|
||||
{
|
||||
mark: I18n.t('markdown_heading3'),
|
||||
example: I18n.t('markdown_heading3_syntax', { space: I18n.t('space') }),
|
||||
iconKey: 'h3',
|
||||
},
|
||||
{
|
||||
mark: I18n.t('markdown_bold'),
|
||||
example: I18n.t('markdown_bold_syntax', {
|
||||
space: I18n.t('space'),
|
||||
text: I18n.t('text'),
|
||||
}),
|
||||
iconKey: 'bold',
|
||||
},
|
||||
{
|
||||
mark: I18n.t('markdown_italic'),
|
||||
example: I18n.t('markdown_italic_syntax', {
|
||||
space: I18n.t('space'),
|
||||
text: I18n.t('text'),
|
||||
}),
|
||||
iconKey: 'italic',
|
||||
},
|
||||
{
|
||||
mark: I18n.t('markdown_strickthrough'),
|
||||
example: I18n.t('markdown_strickthrough_syntax', {
|
||||
space: I18n.t('space'),
|
||||
text: I18n.t('text'),
|
||||
}),
|
||||
iconKey: 'strikethrough',
|
||||
},
|
||||
{
|
||||
mark: I18n.t('markdown_quote'),
|
||||
example: I18n.t('markdown_quote_syntax', { space: I18n.t('space') }),
|
||||
iconKey: 'quote',
|
||||
},
|
||||
{
|
||||
mark: I18n.t('markdown_code'),
|
||||
example: I18n.t('markdown_code_syntax', {
|
||||
space: I18n.t('space'),
|
||||
code: I18n.t('code'),
|
||||
}),
|
||||
iconKey: 'code',
|
||||
},
|
||||
{
|
||||
mark: I18n.t('markdown_codeblock'),
|
||||
example: I18n.t('markdown_codeblock_syntax', {
|
||||
space: I18n.t('space'),
|
||||
}),
|
||||
iconKey: 'codeBlock',
|
||||
},
|
||||
{
|
||||
mark: I18n.t('markdown_numberedlist'),
|
||||
example: I18n.t('markdown_numberedlist_syntax', { space: I18n.t('space') }),
|
||||
iconKey: 'numberedList',
|
||||
},
|
||||
{
|
||||
mark: I18n.t('markdown_bulletedlist'),
|
||||
example: I18n.t('markdown_bulletedlist_syntax', { space: I18n.t('space') }),
|
||||
iconKey: 'bulletedList',
|
||||
},
|
||||
];
|
||||
|
||||
export const MarkdownDescriptionPopover: React.FC<
|
||||
PropsWithChildren<MarkdownDescriptionPopoverProps>
|
||||
> = ({ children, visible, onVisibleChange }) => (
|
||||
<Popover
|
||||
trigger="custom"
|
||||
visible={visible}
|
||||
showArrow
|
||||
position="rightTop"
|
||||
content={
|
||||
<div className={styles.content}>
|
||||
<div className={styles.title}>
|
||||
<div>Markdown</div>
|
||||
<IconButton
|
||||
size="small"
|
||||
theme="borderless"
|
||||
icon={<IconCloseNoCycle />}
|
||||
onClick={() => onVisibleChange?.(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.description}>{I18n.t('markdown_intro')}</div>
|
||||
<Table
|
||||
className={styles.table}
|
||||
showHeader={false}
|
||||
columns={columns}
|
||||
dataSource={getData()}
|
||||
pagination={false}
|
||||
size="small"
|
||||
onRow={() => ({
|
||||
className: styles.row,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type SuggestQuestionMessage } from '@coze-studio/bot-detail-store';
|
||||
import { type BotEditorOnboardingSuggestion } from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
import { OnboardingSuggestion } from '../../../onboarding-suggestion';
|
||||
|
||||
export interface OnboardingSuggestionContentProps {
|
||||
onSuggestionChange: (param: SuggestQuestionMessage) => void;
|
||||
onDeleteSuggestion: (id: string) => void;
|
||||
onboardingSuggestions: BotEditorOnboardingSuggestion[];
|
||||
}
|
||||
export const OnboardingSuggestionContent: React.FC<
|
||||
OnboardingSuggestionContentProps
|
||||
> = ({ onDeleteSuggestion, onSuggestionChange, onboardingSuggestions }) => (
|
||||
<>
|
||||
{onboardingSuggestions.map(({ id, content, highlight }) => (
|
||||
<OnboardingSuggestion
|
||||
key={id}
|
||||
id={id}
|
||||
value={content}
|
||||
onChange={(changedId, value) => {
|
||||
onSuggestionChange({ id: changedId, content: value, highlight });
|
||||
}}
|
||||
onDelete={onDeleteSuggestion}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
@@ -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 const ONBOARDING_PREVIEW_DELAY = 500;
|
||||
@@ -0,0 +1,88 @@
|
||||
.onboarding-markdown {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.edit-content {
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
|
||||
padding: 24px;
|
||||
|
||||
background-color: #FFF;
|
||||
border-left: 1px solid rgb(6 7 9 / 10%);
|
||||
}
|
||||
|
||||
.preview-content-scroll {
|
||||
align-items: flex-start;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.opening-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.opening-question {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.strong-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: rgb(6 7 9 / 96%);
|
||||
}
|
||||
|
||||
.markdown-editor {
|
||||
box-sizing: border-box;
|
||||
width: 590px;
|
||||
height: 50%;
|
||||
min-height: 200px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
:global(.semi-modal-content) {
|
||||
padding: 0;
|
||||
|
||||
:global(.semi-modal-header) {
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid rgb(6 7 9 / 10%);
|
||||
}
|
||||
|
||||
:global(.semi-modal-body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-tag {
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
color: #4E40E5;
|
||||
background: rgba(128, 138, 255, 20%);
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
@@ -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 { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDebounce } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tag, Tooltip, UIModal, type UIModalProps } from '@coze-arch/bot-semi';
|
||||
import { IconInfo } from '@coze-arch/bot-icons';
|
||||
import { type BotEditorOnboardingSuggestion } from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
import {
|
||||
OnboardingPreview,
|
||||
type OnboardingPreviewProps,
|
||||
} from '../onboarding-preview';
|
||||
import { MarkdownEditor, type MarkdownEditorProps } from '../markdown-editor';
|
||||
import { ReactComponent as IconMinimizeOutlined } from '../../assets/icon_minimize_outlined.svg';
|
||||
import { ONBOARDING_PREVIEW_DELAY } from './constant';
|
||||
import {
|
||||
OnboardingSuggestionContent,
|
||||
type OnboardingSuggestionContentProps,
|
||||
} from './components/onboarding-suggestion-content';
|
||||
import { MarkdownDescriptionPopover } from './components/markdown-description-popover';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface OnboardingMarkdownModalProps
|
||||
extends UIModalProps,
|
||||
OnboardingSuggestionContentProps,
|
||||
Pick<
|
||||
MarkdownEditorProps,
|
||||
'getValueLength' | 'maxLength' | 'getSlicedTextOnExceed'
|
||||
> {
|
||||
getBotInfo: OnboardingPreviewProps['getBotInfo'];
|
||||
getUserInfo: () => {
|
||||
userName: string;
|
||||
userId: string;
|
||||
};
|
||||
prologue: string;
|
||||
onPrologueChange: (prologue: string) => void;
|
||||
/**
|
||||
* 要预览的和能编辑的可能不一样
|
||||
*/
|
||||
previewSuggestions?: BotEditorOnboardingSuggestion[];
|
||||
}
|
||||
|
||||
export const OnboardingMarkdownModal: React.FC<
|
||||
OnboardingMarkdownModalProps
|
||||
> = ({
|
||||
getBotInfo,
|
||||
getUserInfo,
|
||||
onDeleteSuggestion,
|
||||
onPrologueChange,
|
||||
onSuggestionChange,
|
||||
onboardingSuggestions,
|
||||
previewSuggestions = onboardingSuggestions,
|
||||
prologue,
|
||||
getValueLength,
|
||||
maxLength,
|
||||
getSlicedTextOnExceed,
|
||||
...modalProps
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [previewScrollable, setScrollable] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const onShowPopover = () => setVisible(true);
|
||||
const debouncedPrologue = useDebounce(prologue, {
|
||||
wait: ONBOARDING_PREVIEW_DELAY,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
const { scrollHeight, clientHeight } = ref.current;
|
||||
|
||||
setScrollable(scrollHeight > clientHeight);
|
||||
}, [debouncedPrologue, onboardingSuggestions, modalProps.visible]);
|
||||
|
||||
return (
|
||||
<UIModal
|
||||
{...modalProps}
|
||||
title={I18n.t('bot_preview_opening_remarks')}
|
||||
centered
|
||||
footer={null}
|
||||
type="base-composition"
|
||||
className={styles.modal}
|
||||
closeIcon={
|
||||
<Tooltip content={I18n.t('collapse')}>
|
||||
<span className={styles['modal-icon']}>
|
||||
<IconMinimizeOutlined className={styles['modal-icon']} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div className={styles['onboarding-markdown']}>
|
||||
<div className={styles['edit-content']}>
|
||||
<div className={styles['opening-text']}>
|
||||
<div className={styles['strong-text']}>
|
||||
{I18n.t('bot_edit_opening_text_title')}
|
||||
</div>
|
||||
<MarkdownDescriptionPopover
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
>
|
||||
<Tag
|
||||
onClick={onShowPopover}
|
||||
className={styles['markdown-tag']}
|
||||
color="indigo"
|
||||
>
|
||||
<IconInfo style={{ marginRight: 4 }} />
|
||||
{I18n.t('markdown_is_supported')}
|
||||
</Tag>
|
||||
</MarkdownDescriptionPopover>
|
||||
</div>
|
||||
|
||||
<MarkdownEditor
|
||||
className={styles['markdown-editor']}
|
||||
getUserId={() => getUserInfo().userId}
|
||||
value={prologue}
|
||||
onChange={onPrologueChange}
|
||||
getValueLength={getValueLength}
|
||||
maxLength={maxLength}
|
||||
getSlicedTextOnExceed={getSlicedTextOnExceed}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
styles['opening-question'],
|
||||
styles['strong-text'],
|
||||
)}
|
||||
>
|
||||
{I18n.t('bot_edit_opening_question_title')}
|
||||
<Tooltip content={I18n.t('bot_edit_opening_questions_tooltip')}>
|
||||
<IconInfo />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<OnboardingSuggestionContent
|
||||
onDeleteSuggestion={onDeleteSuggestion}
|
||||
onSuggestionChange={onSuggestionChange}
|
||||
onboardingSuggestions={onboardingSuggestions}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
styles['preview-content'],
|
||||
previewScrollable && styles['preview-content-scroll'],
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<OnboardingPreview
|
||||
content={debouncedPrologue}
|
||||
suggestions={previewSuggestions}
|
||||
getBotInfo={getBotInfo}
|
||||
getUserName={() => getUserInfo().userName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UIModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
.onboarding {
|
||||
height: auto;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 { OnBoarding } from '@coze-common/chat-uikit';
|
||||
import { type BotEditorOnboardingSuggestion } from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
import { useRenderVariable } from '../../hooks/onboarding/use-render-variable-element';
|
||||
import { OnboardingVariable } from '../../constant/onboarding-variable';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface OnboardingPreviewProps {
|
||||
content: string;
|
||||
suggestions: BotEditorOnboardingSuggestion[];
|
||||
getBotInfo: () => {
|
||||
avatarUrl: string;
|
||||
botName: string;
|
||||
};
|
||||
getUserName: () => string;
|
||||
}
|
||||
|
||||
export const OnboardingPreview: React.FC<OnboardingPreviewProps> = ({
|
||||
content,
|
||||
suggestions,
|
||||
getBotInfo,
|
||||
getUserName,
|
||||
}) => {
|
||||
const username = getUserName();
|
||||
const { botName, avatarUrl } = getBotInfo();
|
||||
|
||||
const renderVariable = useRenderVariable({
|
||||
[OnboardingVariable.USER_NAME]: username,
|
||||
});
|
||||
|
||||
return (
|
||||
<OnBoarding
|
||||
className={styles.onboarding}
|
||||
name={botName}
|
||||
avatar={avatarUrl}
|
||||
suggestionListWithString={suggestions.map(item => item.content)}
|
||||
prologue={content}
|
||||
mdBoxProps={{
|
||||
insertedElements: renderVariable(content),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
.suggestion-message-item {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.no-icon {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-input-textarea-wrapper {
|
||||
padding-right: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-button-16 {
|
||||
&:hover {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-button {
|
||||
&.semi-button-size-small {
|
||||
height: 16px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-no-disabled {
|
||||
color: rgb(29 28 35 / 20%);
|
||||
}
|
||||
@@ -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 ComponentProps, forwardRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { TextArea, Tooltip, UIIconButton } from '@coze-arch/bot-semi';
|
||||
import { IconNo } from '@coze-arch/bot-icons';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface OnboardingSuggestionProps
|
||||
extends Omit<
|
||||
ComponentProps<typeof TextArea>,
|
||||
'autosize' | 'rows' | 'placeholder' | 'onChange'
|
||||
> {
|
||||
id: string;
|
||||
onDelete?: (id: string) => void;
|
||||
onChange?: (id: string, value: string) => void;
|
||||
}
|
||||
|
||||
export const OnboardingSuggestion = forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
OnboardingSuggestionProps
|
||||
>(({ value, onChange, id, onDelete, className, ...restProps }, ref) => (
|
||||
<div className={styles['suggestion-message-item']}>
|
||||
<TextArea
|
||||
autosize
|
||||
rows={1}
|
||||
ref={ref}
|
||||
className={className}
|
||||
placeholder={I18n.t('opening_question_placeholder')}
|
||||
value={value}
|
||||
onChange={v => {
|
||||
onChange?.(id, v);
|
||||
}}
|
||||
{...restProps}
|
||||
/>
|
||||
<Tooltip content={I18n.t('bot_edit_plugin_delete_tooltip')}>
|
||||
<UIIconButton
|
||||
wrapperClass={classNames(styles['icon-button-16'], styles['no-icon'])}
|
||||
iconSize="small"
|
||||
icon={
|
||||
<IconNo
|
||||
className={classNames(!value && styles['icon-no-disabled'])}
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
onDelete?.(id);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
));
|
||||
Reference in New Issue
Block a user