Files
coze-studio/frontend/packages/agent-ide/tool/src/components/tool-content-block/index.tsx

314 lines
8.8 KiB
TypeScript

/*
* 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 does not need to be passed in after plugin modification (if you keep old-fashioned events, you need to pass in)
*/
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 for long-term use */
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,
})),
);
// The placement of the container on the page is different in different position styles
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);
// Record user usage status
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 the user manually expands/retracts the initialization, the initialization is completed immediately
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(() => {
// Initialization can only be done after passing in the default value
if (isBoolean(defaultExpand)) {
// Ignore defaultExpand changes after initialization is complete
if (!initialized.current) {
setIsOpen(defaultExpand);
initialized.current = true;
}
} else {
setIsOpen(false);
initialized.current = false;
}
}, [defaultExpand]);
const content = useMemo(() => {
// Only after successful initialization can we start rendering the Collapsible component
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>
);
};