feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
@common-box-shadow: 0px 2px 8px 0px rgba(31, 35, 41, 0.02),
|
||||
0px 2px 4px 0px rgba(31, 35, 41, 0.02), 0px 2px 2px 0px rgba(31, 35, 41, 0.02);
|
||||
|
||||
.common-svg-icon(@size: 14px) {
|
||||
>svg {
|
||||
width: @size;
|
||||
height: @size;
|
||||
|
||||
@apply coz-fg-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.content-block {
|
||||
width: 100%;
|
||||
// border-radius: 8px;
|
||||
// background-color: #fff;
|
||||
// box-shadow: (@common-box-shadow);
|
||||
// margin-bottom: 4px;
|
||||
border-bottom: 1px solid theme('colors.stroke.5');
|
||||
|
||||
&.isOpen {
|
||||
border-bottom: 1px solid theme('colors.stroke.5') !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
&.left {
|
||||
.header-content {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.content-old {
|
||||
padding: 4px 28px;
|
||||
}
|
||||
}
|
||||
|
||||
&.center {
|
||||
.header-content {
|
||||
padding: 0 7px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 4px 0 16px;
|
||||
}
|
||||
|
||||
.content-old {
|
||||
padding: 8px 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
|
||||
border-radius: 3px;
|
||||
|
||||
&.collapsible {
|
||||
&:hover {
|
||||
@apply coz-mg-secondary-hovered;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply coz-mg-secondary-pressed;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply coz-fg-primary;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.header-icon-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin-right: 8px;
|
||||
padding: 1px;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
:global {
|
||||
.semi-icon {
|
||||
svg {
|
||||
font-size: 14px;
|
||||
|
||||
@apply coz-fg-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
display: flex;
|
||||
margin-right: 8px;
|
||||
|
||||
>img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
align-items: center;
|
||||
|
||||
height: 100%;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
|
||||
.icon {
|
||||
margin-left: 8px;
|
||||
.common-svg-icon(16px)
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.setting {
|
||||
// margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
// border-top: 1px solid #efefef;
|
||||
overflow: auto;
|
||||
height: calc(100% - 48px);
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-collapsible-wrapper {
|
||||
padding-left: 0 !important;
|
||||
border-bottom-right-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.semi-button {
|
||||
svg {
|
||||
@apply coz-fg-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* 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,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useMemo,
|
||||
useCallback,
|
||||
type ForwardedRef,
|
||||
} from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { isBoolean } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
|
||||
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
|
||||
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
|
||||
import { Collapsible } from '@coze-arch/coze-design';
|
||||
import {
|
||||
type OpenBlockEvent,
|
||||
handleEvent,
|
||||
removeEvent,
|
||||
skillKeyToApiStatusKeyTransformer,
|
||||
} from '@coze-arch/bot-utils';
|
||||
import { BotPageFromEnum } from '@coze-arch/bot-typings/common';
|
||||
import { Image } from '@coze-arch/bot-semi';
|
||||
import {
|
||||
IconInfo,
|
||||
IconChevronRight,
|
||||
IconChevronDown,
|
||||
} from '@coze-arch/bot-icons';
|
||||
import { useLayoutContext } from '@coze-arch/bot-hooks';
|
||||
import { TabStatus } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { ToolTooltip } from '../tool-tooltip';
|
||||
import { ToolPopover } from '../tool-popover';
|
||||
import { ModelCapabilityTips } from '../model-capability-tips';
|
||||
import { toolKeyToApiStatusKeyTransformer } from '../../utils/tool-content-block';
|
||||
import { EventCenterEventName } from '../../typings/scoped-events';
|
||||
import { type IToggleContentBlockEventParams } from '../../typings/event';
|
||||
import { useRegisterCollapse } from '../../hooks/tool/use-tool-toggle-collapse';
|
||||
import { useEvent } from '../../hooks/event/use-event';
|
||||
import { useAbilityConfig } from '../../hooks/builtin/use-ability-config';
|
||||
import { openBlockEventToToolKey } from '../../constants/tool-content-block';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface ToolContentBlockProps {
|
||||
contentClassName?: string;
|
||||
header?: ReactNode;
|
||||
icon?: string;
|
||||
actionButton?: ReactNode;
|
||||
tooltip?: ReactNode;
|
||||
setting?: ReactNode;
|
||||
maxContentHeight?: number;
|
||||
showBottomBorder?: boolean;
|
||||
showBorderTopRadius?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
collapsible?: boolean;
|
||||
defaultExpand?: boolean;
|
||||
onRef?: ForwardedRef<ToolContentRef>;
|
||||
/**
|
||||
* @deprecated tool 插件化改造后无需传入 (如果保留老式event则需要传入)
|
||||
*/
|
||||
blockEventName?: OpenBlockEvent;
|
||||
tooltipType?: 'tooltip' | 'popOver';
|
||||
childNodeWrapClassName?: string;
|
||||
headerClassName?: string;
|
||||
}
|
||||
|
||||
interface ToolContentRef {
|
||||
setOpen?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
/* eslint @coze-arch/max-line-per-function: ["error", {"max": 250}] */
|
||||
export const ToolContentBlock: React.FC<
|
||||
PropsWithChildren<ToolContentBlockProps>
|
||||
> = ({
|
||||
children,
|
||||
icon,
|
||||
header,
|
||||
actionButton,
|
||||
maxContentHeight,
|
||||
tooltip,
|
||||
tooltipType = 'popOver',
|
||||
setting,
|
||||
className,
|
||||
style,
|
||||
collapsible = true,
|
||||
defaultExpand,
|
||||
onRef,
|
||||
blockEventName,
|
||||
childNodeWrapClassName,
|
||||
headerClassName,
|
||||
}) => {
|
||||
/** 后续长期使用的ToolKey */
|
||||
const { abilityKey } = useAbilityConfig();
|
||||
|
||||
const { registerCollapse } = useRegisterCollapse();
|
||||
|
||||
useEffect(() => {
|
||||
if (!abilityKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
return registerCollapse(isExpand => setIsOpen(isExpand), abilityKey);
|
||||
}, [abilityKey]);
|
||||
|
||||
const isReadonly = useBotDetailIsReadonly();
|
||||
const { botId } = useBotInfoStore(
|
||||
useShallow(store => ({
|
||||
botId: store.botId,
|
||||
})),
|
||||
);
|
||||
const { editable, setBotSkillBlockCollapsibleState } = usePageRuntimeStore(
|
||||
useShallow(store => ({
|
||||
editable: store.editable,
|
||||
setBotSkillBlockCollapsibleState: store.setBotSkillBlockCollapsibleState,
|
||||
})),
|
||||
);
|
||||
|
||||
// 容器在页面中的展示位置,不同位置样式有区别
|
||||
const { placement } = useLayoutContext();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const initialized = useRef<boolean>(false);
|
||||
const childNode = (
|
||||
<div
|
||||
className={classNames(s.content, childNodeWrapClassName)}
|
||||
style={{ maxHeight: maxContentHeight ? maxContentHeight : 'unset' }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
const setOpen = ($isOpen: boolean) => {
|
||||
setIsOpen($isOpen);
|
||||
// 记录用户使用状态
|
||||
if (editable && !isReadonly && (abilityKey || blockEventName)) {
|
||||
if (blockEventName) {
|
||||
const blockKey = openBlockEventToToolKey[blockEventName];
|
||||
|
||||
blockKey &&
|
||||
setBotSkillBlockCollapsibleState({
|
||||
[skillKeyToApiStatusKeyTransformer(blockKey)]: $isOpen
|
||||
? TabStatus.Open
|
||||
: TabStatus.Close,
|
||||
});
|
||||
} else if (abilityKey) {
|
||||
setBotSkillBlockCollapsibleState({
|
||||
[toolKeyToApiStatusKeyTransformer(abilityKey)]: $isOpen
|
||||
? TabStatus.Open
|
||||
: TabStatus.Close,
|
||||
});
|
||||
}
|
||||
}
|
||||
// 尚未完成初始化时如果用户手动展开/收起,则马上完成初始化
|
||||
if (!initialized.current) {
|
||||
initialized.current = true;
|
||||
}
|
||||
};
|
||||
useImperativeHandle(onRef, () => ({
|
||||
setOpen,
|
||||
}));
|
||||
|
||||
const onEvent = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, [
|
||||
blockEventName,
|
||||
botId,
|
||||
editable,
|
||||
isReadonly,
|
||||
openBlockEventToToolKey,
|
||||
abilityKey,
|
||||
]);
|
||||
|
||||
const onEventNew = useCallback(
|
||||
({ abilityKey: _abilityKey, isExpand }: IToggleContentBlockEventParams) => {
|
||||
if (_abilityKey === abilityKey) {
|
||||
setOpen(isExpand);
|
||||
}
|
||||
},
|
||||
[abilityKey, setOpen],
|
||||
);
|
||||
|
||||
const { on } = useEvent();
|
||||
|
||||
useEffect(() => {
|
||||
blockEventName && handleEvent(blockEventName, onEvent);
|
||||
const offEvent =
|
||||
abilityKey &&
|
||||
on<IToggleContentBlockEventParams>(
|
||||
EventCenterEventName.ToggleContentBlock,
|
||||
onEventNew,
|
||||
);
|
||||
return () => {
|
||||
blockEventName && removeEvent(blockEventName, onEvent);
|
||||
offEvent?.();
|
||||
};
|
||||
}, [onEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
// 传入默认值之后才能初始化
|
||||
if (isBoolean(defaultExpand)) {
|
||||
// 初始化完成后忽略 defaultExpand 变化
|
||||
if (!initialized.current) {
|
||||
setIsOpen(defaultExpand);
|
||||
initialized.current = true;
|
||||
}
|
||||
} else {
|
||||
setIsOpen(false);
|
||||
initialized.current = false;
|
||||
}
|
||||
}, [defaultExpand]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
// 初始化成功之后才能开始渲染 Collapsible 组件
|
||||
if (!initialized.current) {
|
||||
return null;
|
||||
}
|
||||
if (collapsible) {
|
||||
return (
|
||||
<Collapsible keepDOM isOpen={isOpen || !collapsible}>
|
||||
{childNode}
|
||||
</Collapsible>
|
||||
);
|
||||
} else {
|
||||
return childNode;
|
||||
}
|
||||
}, [collapsible, isOpen, childNode]);
|
||||
|
||||
const onToggle = () => {
|
||||
if (collapsible) {
|
||||
setOpen(!isOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const isFromStore = usePageRuntimeStore(
|
||||
state => state.pageFrom === BotPageFromEnum.Store,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
s['content-block'],
|
||||
s[placement],
|
||||
{
|
||||
[s.isOpen || '']: isOpen,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<header
|
||||
className={classNames(s['header-content'], headerClassName, {
|
||||
[s.collapsible || '']: collapsible,
|
||||
})}
|
||||
>
|
||||
<div className={s.header} onClick={onToggle}>
|
||||
{collapsible ? (
|
||||
<div className={s['header-icon-arrow']}>
|
||||
{isOpen ? <IconChevronDown /> : <IconChevronRight />}
|
||||
</div>
|
||||
) : null}
|
||||
{icon ? (
|
||||
<Image preview={false} className={s['header-icon']} src={icon} />
|
||||
) : null}
|
||||
<div className="shrink-0">{header}</div>
|
||||
{tooltip && tooltipType === 'popOver' ? (
|
||||
<ToolPopover
|
||||
content={<div onClick={e => e.stopPropagation()}>{tooltip}</div>}
|
||||
>
|
||||
<IconInfo className={s.icon} />
|
||||
</ToolPopover>
|
||||
) : null}
|
||||
{tooltip && tooltipType === 'tooltip' ? (
|
||||
<ToolTooltip content={<div>{tooltip}</div>}>
|
||||
<IconInfo className={s.icon} />
|
||||
</ToolTooltip>
|
||||
) : null}
|
||||
{!isFromStore ? <ModelCapabilityTips /> : null}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
s['action-button'],
|
||||
'grid grid-flow-row gap-x-[2px]',
|
||||
)}
|
||||
>
|
||||
{!!setting && <div className={s.setting}>{setting}</div>}
|
||||
{actionButton}
|
||||
</div>
|
||||
</header>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user