feat: manually mirror opencoze's code from bytedance

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

View File

@@ -0,0 +1,63 @@
/*
* 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 PropsWithChildren } from 'react';
import { type BotMode } from '@coze-arch/bot-api/developer_api';
import { InvisibleToolController } from '../invisible-tool-controller';
import { type IEventCallbacks } from '../../typings/event-callbacks';
import {
type IPreferenceContext,
PreferenceContextProvider,
} from '../../context/preference-context';
import { AbilityAreaContextProvider } from '../../context/ability-area-context';
type IProps = {
eventCallbacks?: Partial<IEventCallbacks>;
mode: BotMode;
modeSwitching: boolean;
isInit: boolean;
} & Partial<IPreferenceContext>;
export const AbilityAreaContainer: FC<PropsWithChildren<IProps>> = props => {
const {
children,
eventCallbacks,
enableToolHiddenMode,
isReadonly,
mode,
modeSwitching,
isInit,
} = props;
return (
<PreferenceContextProvider
enableToolHiddenMode={enableToolHiddenMode}
isReadonly={isReadonly}
>
<AbilityAreaContextProvider
eventCallbacks={eventCallbacks}
mode={mode}
modeSwitching={modeSwitching}
isInit={isInit}
>
<InvisibleToolController />
{children}
</AbilityAreaContextProvider>
</PreferenceContextProvider>
);
};

View File

@@ -0,0 +1,67 @@
/*
* 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 } from 'react';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { IconCozEdit, IconCozPlus } from '@coze-arch/coze-design/icons';
import { IconButton } from '@coze-arch/coze-design';
import { ToolTooltip } from '../tool-tooltip';
import { type ToolButtonCommonProps } from '../../typings/button';
interface AddButtonProps extends ToolButtonCommonProps {
iconName?: 'add' | 'edit';
enableAutoHidden?: boolean;
}
export const AddButton: FC<AddButtonProps> = ({
onClick,
tooltips,
disabled,
loading,
iconName = 'add',
enableAutoHidden,
...restProps
}) => {
const readonly = useBotDetailIsReadonly();
if (readonly && enableAutoHidden) {
return null;
}
return (
<ToolTooltip content={tooltips}>
<div>
<IconButton
icon={
iconName === 'add' ? (
<IconCozPlus className="text-base coz-fg-secondary" />
) : (
<IconCozEdit className="text-base coz-fg-secondary" />
)
}
loading={loading}
onClick={onClick}
size="small"
color="secondary"
disabled={!!disabled}
data-testid={restProps['data-testid']}
/>
</div>
</ToolTooltip>
);
};

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ErrorBoundary } from 'react-error-boundary';
import { type FC, type PropsWithChildren } from 'react';
import { AbilityScope, type AgentSkillKey } from '@coze-agent-ide/tool-config';
import { AbilityConfigContextProvider } from '../../context/ability-config-context';
interface IProps {
agentSkillKey?: AgentSkillKey;
}
export const AgentSkillContainer: FC<PropsWithChildren<IProps>> = ({
children,
agentSkillKey,
}) => (
<ErrorBoundary fallback={<div>error</div>}>
<AbilityConfigContextProvider
abilityKey={agentSkillKey}
scope={AbilityScope.AGENT_SKILL}
>
{children}
</AbilityConfigContextProvider>
</ErrorBoundary>
);

View File

@@ -0,0 +1,100 @@
.content-block {
width: 100%;
padding: 12px;
background: var(--light-usage-fill-color-fill-0, rgb(46 46 56 / 4%));
border-radius: 8px;
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.header-icon-arrow {
cursor: pointer;
transform: rotate3d(0, 0, 0, 0);
display: flex;
align-items: center;
justify-content: center;
margin-right: 3px;
transition: transform .2s linear 0s;
&.open {
transform: rotate3d(0, 0, 1, 90deg);
}
}
.header-icon {
display: flex;
margin-right: 8px;
>img {
width: 16px;
height: 16px;
}
}
.header {
display: flex;
align-items: center;
font-size: 14px;
font-weight: bold;
line-height: 20px;
.label {
margin-left: 4px;
font-size: 14px;
font-weight: 600;
font-style: normal;
line-height: 22px;
color: var(--light-usage-text-color-text-0, #1D1C23);
}
.popover {
margin-left: 4px;
}
.icon {
width: 12px;
height: 12px;
&>svg {
width: 12px;
height: 12px;
color: #A7A9B0;
}
}
}
}
.content {
margin-top: 8px;
}
:global {
.semi-collapsible-wrapper {
padding-left: 0 !important;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
}
}
.overflow-content {
padding-left: 0;
&.open {
:global {
.semi-collapsible-wrapper,
.semi-collapsible-wrapper [x-semi-prop] {
overflow: visible !important;
}
}
}
}
}

View File

@@ -0,0 +1,178 @@
/*
* 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,
forwardRef,
type PropsWithChildren,
type ReactNode,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import { Tooltip, Collapsible } from '@coze-arch/bot-semi';
import { IconInfo, IconArrowRight } from '@coze-arch/bot-icons';
import s from './index.module.less';
export type AgentContentBlockProps = PropsWithChildren<{
allowToggleCollapsible?: boolean;
title?: ReactNode;
actionButton?: ReactNode;
tooltip?: string | ReactNode;
maxContentHeight?: number;
className?: string;
contentClassName?: string;
style?: CSSProperties;
defaultExpand?: boolean;
autoExpandWhenDomChange?: boolean;
}>;
export interface ContentRef {
setOpen?: (isOpen: boolean) => void;
}
export const AgentSkillContentBlock = forwardRef<
ContentRef,
AgentContentBlockProps
>(
(
{
allowToggleCollapsible = true,
children,
title,
actionButton,
maxContentHeight,
tooltip,
className,
contentClassName,
style,
defaultExpand = true,
autoExpandWhenDomChange,
},
ref,
) => {
const [isOpen, setIsOpen] = useState(defaultExpand);
const containerRef = useRef<HTMLDivElement | null>(null);
const childNodeRef = useRef<HTMLDivElement>(null);
const actionDivRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const target = childNodeRef.current;
if (autoExpandWhenDomChange && target && allowToggleCollapsible) {
const config = { attributes: true, childList: true, subtree: true };
// 只有开启了dom改变自动展开功能才启动
const callback: MutationCallback = mutationList => {
if (mutationList.length > 0 && !isOpen) {
// 当dom改变并且没有开启时会自动开启
setIsOpen(!isOpen);
}
};
const observer = new MutationObserver(callback);
observer.observe(target, config);
return () => {
observer.disconnect();
};
}
}, [isOpen, allowToggleCollapsible]);
useImperativeHandle(ref, () => ({
setOpen,
}));
const setOpen = (innerIsOpen: boolean) => {
setIsOpen(innerIsOpen);
};
return (
<div
className={classNames(className, s['content-block'])}
style={style}
ref={containerRef}
>
<header
className={classNames(s['header-content'])}
onClick={e => {
if (!allowToggleCollapsible) {
return;
}
// @danger 不可以阻止内部节点的点击冒泡,不然无法设置节点的选中态
const el = e.target as HTMLElement;
// 这里需要多重判断,
// 第一次判断如果包含在container内才需要去切换open
// 第二次判断如果包含在action内则不能切换open其他都可以
// @TIP contains方法会判断自身节点即A.contains(A)也是true。但是这里就算是自身也没有影响
if (containerRef.current && containerRef.current.contains(el)) {
if (actionDivRef.current && actionDivRef.current.contains(el)) {
// 此时不切换open
return;
}
setIsOpen(!isOpen);
}
}}
>
<div className={s.header}>
{allowToggleCollapsible ? (
<div
className={classNames({
[s['header-icon-arrow'] || '']: true,
[s.open || '']: isOpen,
})}
>
<IconArrowRight />
</div>
) : null}
<div className={s.label}>{title}</div>
{tooltip ? (
<Tooltip
showArrow
position="top"
className={s.popover}
content={tooltip}
>
<IconInfo className={s.icon} />
</Tooltip>
) : null}
</div>
<div ref={actionDivRef}>{actionButton}</div>
</header>
<div
className={classNames({
[s['overflow-content'] || '']: true,
[contentClassName || '']: Boolean(contentClassName),
[s.open || '']: isOpen,
})}
>
<Collapsible keepDOM fade isOpen={isOpen}>
<div
className={s.content}
ref={childNodeRef}
style={{
maxHeight: maxContentHeight ? maxContentHeight : undefined,
}}
>
{children}
</div>
</Collapsible>
</div>
</div>
);
},
);

View File

@@ -0,0 +1,34 @@
.item {
display: flex;
width: 100%;
margin-top: 12px;
&:first-child {
margin-top: 0;
}
.icon {
position: sticky;
top: 0;
flex-shrink: 0;
width: 24px;
height: 24px;
}
.skills {
display: flex;
flex: 1;
flex-wrap: wrap;
width: 0;
}
}
.popover-content {
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px; // 142.857%
color: var(--light-usage-bg-color-bg-0, #FFF);
}

View File

@@ -0,0 +1,59 @@
/*
* 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 React from 'react';
import classnames from 'classnames';
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
import { Popover } from '@coze-arch/bot-semi';
import styles from './index.module.less';
const POPOVER_PROPS: Partial<PopoverProps> = {
style: {
backgroundColor: 'var(--light-color-grey-grey-7, #41464C)',
borderColor: 'var(--light-color-grey-grey-7, #41464C)',
padding: '8px 12px',
},
showArrow: true,
position: 'top',
};
interface IProps {
children: React.ReactNode;
tooltip: React.ReactNode;
icon: React.ReactElement;
}
export const AgentSkillContent = React.memo((props: IProps) => {
const { children, tooltip, icon } = props;
const iconNode = React.cloneElement(icon, {
className: classnames(icon?.props?.className, styles.icon),
});
return (
<div className={styles.item}>
<Popover
{...POPOVER_PROPS}
content={<span className={styles['popover-content']}>{tooltip}</span>}
>
{iconNode}
</Popover>
<div className={styles.skills}>{children}</div>
</div>
);
});

View File

@@ -0,0 +1,67 @@
/*
* 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 } from 'react';
// eslint-disable-next-line @coze-arch/no-pkg-dir-import
import { type AgentModalTabKey } from '@coze-agent-ide/tool-config/src/types';
import { AbilityScope } from '@coze-agent-ide/tool-config';
import { UITabsModal } from '@coze-arch/bot-semi';
import { type ModalProps } from '@douyinfe/semi-foundation/lib/es/modal/modalFoundation';
import { ToolContainer } from '../tool-container';
import { useAgentModalTriggerEvent } from '../../hooks/agent-skill-modal/use-agent-modal-trigger-event';
export interface IAgentSkillModalPane {
key: AgentModalTabKey;
tab: React.ReactNode;
pane: React.ReactNode;
}
interface AgentSkillModalProps extends Partial<ModalProps> {
tabPanes: IAgentSkillModalPane[];
}
export const AgentSkillModal: FC<AgentSkillModalProps> = ({
tabPanes,
...restModalProps
}) => {
const { emitTabChangeEvent } = useAgentModalTriggerEvent();
return (
<UITabsModal
visible
tabs={{
tabsProps: {
lazyRender: true,
onChange: activityKey =>
emitTabChangeEvent(activityKey as AgentModalTabKey),
},
tabPanes: tabPanes.map(tab => ({
tabPaneProps: {
tab: tab.tab,
itemKey: tab.key,
},
content: (
<ToolContainer scope={AbilityScope.AGENT_SKILL}>
<>{tab.pane}</>
</ToolContainer>
),
})),
}}
{...restModalProps}
/>
);
};

View File

@@ -0,0 +1,18 @@
.container {
margin-top: 8px;
.content {
position: relative;
overflow: hidden auto;
width: 100%;
max-height: 280px;
}
.empty {
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 20px; // 142.857%
color: var(--light-usage-text-color-text-3, rgb(29 28 35 / 35%));
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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 PropsWithChildren, Children, useEffect } from 'react';
import classNames from 'classnames';
import { AbilityScope } from '@coze-agent-ide/tool-config';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { AgentSkillContentBlock } from '../agent-skill-content-block';
import { AgentSkillContainer } from '../agent-skill-container';
import { hasValidAgentSkillKey } from '../../utils/has-valid-key';
import { useSubscribeToolStore } from '../../hooks/public/store/use-tool-store';
import { useHasAgentSkill } from '../../hooks/public/agent/use-has-agent-skill';
import { useRegisterAgentSkillKey } from '../../hooks/builtin/use-register-agent-skill-key';
import { useNoneAgentSkill } from '../../hooks/agent-skill/use-agent-skill';
import styles from './index.module.less';
interface IProps {
title: string;
agentId: string;
className?: string;
style?: React.CSSProperties;
actionButton?: React.ReactNode;
emptyText: string;
}
export const AgentSkillView: FC<PropsWithChildren<IProps>> = ({
children,
agentId,
title,
className,
style,
actionButton,
emptyText,
}) => {
const readonly = useBotDetailIsReadonly();
const registerAgentSkillKey = useRegisterAgentSkillKey();
const noneAgentSkill = useNoneAgentSkill();
const { getHasAgentSkill } = useHasAgentSkill();
useSubscribeToolStore(AbilityScope.AGENT_SKILL, agentId);
// 前置注册
useEffect(() => {
Children.map(children, child => {
if (!hasValidAgentSkillKey(child)) {
return child;
}
const agentSkillKey = child.key;
registerAgentSkillKey(agentSkillKey);
});
}, [children]);
return (
<AgentSkillContentBlock
title={title}
className={classNames(styles.container, className)}
style={style}
actionButton={!readonly && actionButton}
>
{noneAgentSkill ? (
<span className={styles.empty}>{emptyText}</span>
) : (
<div className={styles.content}>
{Children.map(children, child => {
if (
typeof child === 'string' ||
typeof child === 'number' ||
typeof child === 'boolean'
) {
return child;
}
if (!hasValidAgentSkillKey(child)) {
return child;
}
const agentSkillKey = child.key;
const hasAgentSkill = getHasAgentSkill(agentSkillKey);
return (
hasAgentSkill && (
<AgentSkillContainer agentSkillKey={agentSkillKey}>
<>{child}</>
</AgentSkillContainer>
)
);
})}
</div>
)}
</AgentSkillContentBlock>
);
};

View File

@@ -0,0 +1,57 @@
/*
* 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 } from 'react';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { IconButton } from '@coze-arch/coze-design';
import { IconAuto } from '@coze-arch/bot-icons';
import { ToolTooltip } from '../tool-tooltip';
import { type ToolButtonCommonProps } from '../../typings/button';
interface AutoGenerateButtonProps extends ToolButtonCommonProps {
enableAutoHidden?: boolean;
}
export const AutoGenerateButton: FC<AutoGenerateButtonProps> = ({
onClick,
tooltips,
loading,
disabled,
enableAutoHidden,
...restProps
}) => {
const readonly = useBotDetailIsReadonly();
if (readonly && enableAutoHidden) {
return null;
}
return (
<ToolTooltip content={tooltips}>
<IconButton
icon={<IconAuto />}
loading={loading}
disabled={!!disabled}
onClick={onClick}
size="small"
color="secondary"
data-testid={restProps['data-testid']}
/>
</ToolTooltip>
);
};

View File

@@ -0,0 +1,23 @@
.tool-container-fallback {
display: flex;
align-items: center;
width: 100%;
height: 40px;
margin-bottom: 4px;
padding: 0 3px;
color: #F93920;
border-bottom: 1px solid;
@apply coz-stroke-primary;
}
.text {
margin-left: 7px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
}

View File

@@ -0,0 +1,45 @@
/*
* 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, useState } from 'react';
import { getSlardarInstance } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { IconInfoCircle } from '@coze-arch/bot-icons';
import styles from './index.module.less';
interface IProps {
toolTitle?: string;
}
export const ToolContainerFallback: FC<IProps> = ({ toolTitle }) => {
const [sessionId] = useState(() => getSlardarInstance()?.config()?.sessionId);
return (
<div className={styles['tool-container-fallback']}>
<IconInfoCircle />
<span className={styles.text}>
{toolTitle}
{I18n.t('tool_load_error')}
</span>
{!!sessionId && (
<div className="leading-[12px] ml-[6px] text-[12px] text-gray-400">
{sessionId}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,54 @@
.wrapper {
padding-bottom: 24px;
&.left {
.header {
padding: 0 28px 6px;
}
}
&.center {
.header {
height: 22px;
padding: 0 8px 6px;
font-size: 12px;
font-weight: 500;
line-height: 22px;
}
}
.header {
display: flex;
align-items: center;
border: none;
.title {
flex: 1;
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
.action-nodes {}
}
:global {
.collapse-panel-hide-underline > div {
border-bottom: 1px solid transparent;
}
}
}
.display-none {
display: none;
}
// // 第一个容器不加顶边
:nth-child(1 of.wrapper) {
.header {
margin-top: 0;
border-top: none;
border-bottom: none;
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type ReactNode } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { isArray } from 'lodash-es';
import classnames from 'classnames';
import {
TOOL_KEY_TO_API_STATUS_KEY_MAP,
type ToolGroupKey,
} from '@coze-agent-ide/tool-config';
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
import { useLayoutContext } from '@coze-arch/bot-hooks';
import { TabStatus } from '@coze-arch/bot-api/developer_api';
import { useRegisteredToolKeyConfigList } from '../../hooks/builtin/use-register-tool-key';
import { usePreference } from '../../context/preference-context';
import styles from './index.module.less';
/**
* 分组容器
* @see
*/
interface IProps {
children?: ReactNode;
title: ReactNode;
toolGroupKey?: ToolGroupKey;
actionNodes?: ReactNode;
className?: string;
}
export const GroupingContainer: FC<IProps> = props => {
const { children, title, toolGroupKey, actionNodes, className } = props;
// 容器在页面中的展示位置,不同位置样式有区别
const { placement } = useLayoutContext();
const { isReadonly } = usePreference();
const registeredToolKeyConfigList = useRegisteredToolKeyConfigList();
const registeredToolKeyListInGroup = registeredToolKeyConfigList.filter(
toolConfig => toolConfig.toolGroupKey === toolGroupKey,
);
const statusKeys = registeredToolKeyListInGroup.map(
toolConfig => TOOL_KEY_TO_API_STATUS_KEY_MAP[toolConfig.toolKey],
);
const { enableToolHiddenMode } = usePreference();
const tabInvisible = usePageRuntimeStore(
useShallow(state =>
statusKeys
.map(_key => state.botSkillBlockCollapsibleState[_key])
.every(status => status === TabStatus.Hide),
),
);
const getInvisible = () => {
if (!enableToolHiddenMode) {
return false;
}
if (isReadonly) {
return !registeredToolKeyListInGroup.some(
toolConfig => toolConfig.hasValidData,
);
}
return tabInvisible;
};
const invisible = getInvisible();
if (!children || (isArray(children) && !children.length)) {
return null;
}
return (
<div
className={classnames(styles[placement], 'coz-bg-plus', className, {
hidden: invisible,
[styles.wrapper || '']: !invisible,
})}
>
<div className={styles.header}>
<div className={classnames(styles.title, 'coz-fg-secondary')}>
{title}
</div>
<div className={styles['action-nodes']}>{actionNodes}</div>
</div>
{children}
</div>
);
};

View File

@@ -0,0 +1,38 @@
/*
* 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, type FC } from 'react';
import { useIsAllToolHidden } from '../../hooks/public/container/use-tool-all-hidden';
import { useAbilityAreaContext } from '../../context/ability-area-context';
type IProps = Record<string, unknown>;
export const InvisibleToolController: FC<IProps> = () => {
const isAllToolHidden = useIsAllToolHidden();
const { eventCallbacks, store } = useAbilityAreaContext();
const { isInitialed } = store.useToolAreaStore();
useEffect(() => {
if (!isInitialed) {
return;
}
eventCallbacks?.onAllToolHiddenStatusChange?.(isAllToolHidden);
}, [isAllToolHidden, isInitialed]);
return null;
};

View File

@@ -0,0 +1,187 @@
/*
* 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 } from 'react';
import classNames from 'classnames';
import { ToolGroupKey } from '@coze-agent-ide/tool-config';
import { I18n } from '@coze-arch/i18n';
import {
ModelFuncConfigStatus,
ModelFuncConfigType,
} from '@coze-arch/bot-api/developer_api';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import {
mergeModelFuncConfigStatus,
useModelCapabilityConfig,
} from '@coze-agent-ide/bot-editor-context-store';
import { IconCozWarningCircleFillPalette } from '@coze-arch/coze-design/icons';
import { Tag, Tooltip } from '@coze-arch/coze-design';
import { abilityKey2ModelFunctionConfigType } from '../../utils/model-function-config-type-mapping';
import { useGetToolConfig } from '../../hooks/builtin/use-get-tool-config';
import { useAbilityConfig } from '../../hooks/builtin/use-ability-config';
export const TipsDisplay: FC<{
status?: ModelFuncConfigStatus;
modelName: string;
showTooltip?: boolean;
toolName?: string;
className?: string;
}> = ({
status = ModelFuncConfigStatus.FullSupport,
modelName,
toolName,
showTooltip = true,
className,
}) => {
if (status === ModelFuncConfigStatus.NotSupport) {
const content = (
<Tag
size="mini"
color="primary"
className={classNames('mx-2', className)}
prefixIcon={
<IconCozWarningCircleFillPalette className="coz-fg-hglt-red" />
}
>
{I18n.t('not_supported')}
</Tag>
);
if (!showTooltip) {
return content;
}
return (
<Tooltip
content={
toolName
? I18n.t('not_supported_explain_toolName', {
modelName,
toolName,
})
: I18n.t('not_supported_explain', { modelName })
}
>
{content}
</Tooltip>
);
}
if (status === ModelFuncConfigStatus.PoorSupport) {
const content = (
<Tag
size="mini"
color="primary"
className={classNames('mx-2', className)}
prefixIcon={
<IconCozWarningCircleFillPalette className="coz-fg-hglt-yellow" />
}
>
{I18n.t('support_poor')}
</Tag>
);
if (!showTooltip) {
return content;
}
return (
<Tooltip
content={
toolName
? I18n.t('poorly_supported_explain_toolName', {
modelName,
toolName,
})
: I18n.t('support_poor_explain', { modelName })
}
>
{content}
</Tooltip>
);
}
return null;
};
const TipsImpl: FC<{ configType: ModelFuncConfigType }> = ({ configType }) => {
const modelCapabilityConfig = useModelCapabilityConfig();
const [configStatus, modelName] = modelCapabilityConfig[configType];
return <TipsDisplay status={configStatus} modelName={modelName} />;
};
const TipsImplForKnowledge: FC<{
configType: ModelFuncConfigType;
toolName: string;
}> = ({ configType, toolName }) => {
const modelCapabilityConfig = useModelCapabilityConfig();
const auto = useBotSkillStore(state => state.knowledge.dataSetInfo.auto);
const [autoConfigStatus, autoModelName] =
modelCapabilityConfig[
auto
? ModelFuncConfigType.KnowledgeAutoCall
: ModelFuncConfigType.KnowledgeOnDemandCall
];
// 根据自动调用还是按需调用,获取另一个 status取合并
const [configStatus, modelName] = modelCapabilityConfig[configType];
const mergedStatus = mergeModelFuncConfigStatus(
autoConfigStatus,
configStatus,
);
const mergedToolTittle: string[] = [];
if (mergedStatus === configStatus) {
mergedToolTittle.push(toolName);
}
if (mergedStatus === autoConfigStatus) {
mergedToolTittle.push(
auto
? I18n.t('dataset_automatic_call')
: I18n.t('dataset_on_demand_call'),
);
}
return (
<TipsDisplay
status={mergedStatus}
modelName={mergedStatus === autoConfigStatus ? autoModelName : modelName}
// 因为按需调用的提示合并到了这里,知识库显示 tips 时,需要显示具体不支持的能力(按需调用 / 知识库)
toolName={mergedToolTittle.join(', ')}
/>
);
};
const ModelCapabilityTipsImpl = () => {
const { abilityKey } = useAbilityConfig();
const getToolConfig = useGetToolConfig();
const toolConfig = getToolConfig(abilityKey);
const configType = abilityKey
? abilityKey2ModelFunctionConfigType(abilityKey)
: undefined;
// 降低 useModelCapabilityConfig 调用频率
if (toolConfig && configType) {
// 知识库需要引入一个额外的判断是否是按需调用
if (toolConfig.toolGroupKey === ToolGroupKey.KNOWLEDGE) {
return (
<TipsImplForKnowledge
configType={configType}
toolName={toolConfig.toolTitle}
/>
);
}
return <TipsImpl configType={configType} />;
}
// 不需要渲染任何内容
return null;
};
export const ModelCapabilityTips = ModelCapabilityTipsImpl;

View File

@@ -0,0 +1,120 @@
/*
* 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 PropsWithChildren } from 'react';
import classNames from 'classnames';
import {
AbilityScope,
TOOL_KEY_TO_API_STATUS_KEY_MAP,
type ToolKey,
} from '@coze-agent-ide/tool-config';
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
import { ErrorBoundary } from '@coze-arch/logger';
import { TabStatus } from '@coze-arch/bot-api/developer_api';
import { ToolContainerFallback } from '../fallbacks';
import { useGetToolConfig } from '../../hooks/builtin/use-get-tool-config';
import { usePreference } from '../../context/preference-context';
import { AbilityConfigContextProvider } from '../../context/ability-config-context';
interface IProps {
scope: AbilityScope;
toolKey?: ToolKey;
onMouseOver?: (toolKey: string | undefined) => void;
onMouseLeave?: (toolKey: string | undefined) => void;
}
export const ToolContainer: FC<PropsWithChildren<IProps>> = ({
children,
toolKey,
onMouseOver,
onMouseLeave,
}) => {
const { enableToolHiddenMode, isReadonly } = usePreference();
const toolStatus = usePageRuntimeStore(state =>
toolKey
? state.botSkillBlockCollapsibleState[
TOOL_KEY_TO_API_STATUS_KEY_MAP[toolKey]
]
: null,
);
const getToolConfig = useGetToolConfig();
const toolConfig = getToolConfig(toolKey);
const getInvisible = () => {
if (!enableToolHiddenMode) {
return false;
}
if (isReadonly) {
return !toolConfig?.hasValidData;
}
return toolStatus === TabStatus.Hide;
};
const invisible = getInvisible();
const handleOnMouseEnter = (key: string) => {
const siblingClassList = document.querySelector(`.collapse-panel-${key}`)
?.previousElementSibling?.classList;
// 如果找到兄弟节点则隐藏下划线
if (siblingClassList?.contains('collapse-panel')) {
siblingClassList.add('collapse-panel-hide-underline');
}
};
const handleOnMouseLeave = () => {
const className = 'collapse-panel-hide-underline';
document
.querySelectorAll(`.${className}`)
.forEach(element => element.classList.remove(className));
};
return (
<div
className={classNames({
hidden: invisible,
'collapse-panel': true,
[`collapse-panel-${toolKey}`]: true,
})}
onMouseEnter={() => {
if (toolKey) {
handleOnMouseEnter(toolKey);
}
}}
onMouseLeave={handleOnMouseLeave}
>
<ErrorBoundary
errorBoundaryName={`botEditorTool${toolConfig?.toolKey}`}
FallbackComponent={() => (
<ToolContainerFallback toolTitle={toolConfig?.toolTitle} />
)}
>
<AbilityConfigContextProvider
abilityKey={toolKey}
scope={AbilityScope.TOOL}
>
{children}
</AbilityConfigContextProvider>
</ErrorBoundary>
</div>
);
};

View File

@@ -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;
}
}
}
}

View File

@@ -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>
);
};

View File

@@ -0,0 +1,39 @@
/*
* 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, type FC } from 'react';
import classNames from 'classnames';
import { IconCozCardPencil } from '@coze-arch/coze-design/icons';
import { ToolItemAction } from '..';
type ToolItemActionCardProps = ComponentProps<typeof ToolItemAction>;
export const ToolItemActionCard: FC<ToolItemActionCardProps> = props => {
const { disabled } = props;
return (
<ToolItemAction {...props}>
<IconCozCardPencil
className={classNames('text-sm', {
'coz-fg-secondary': !disabled,
'coz-fg-dim': disabled,
})}
/>
</ToolItemAction>
);
};

View File

@@ -0,0 +1,38 @@
/*
* 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, type FC } from 'react';
import classNames from 'classnames';
import { IconCozCopy } from '@coze-arch/coze-design/icons';
import { ToolItemAction } from '..';
type ToolItemActionCopyProps = ComponentProps<typeof ToolItemAction>;
export const ToolItemActionCopy: FC<ToolItemActionCopyProps> = props => {
const { disabled } = props;
return (
<ToolItemAction {...props}>
<IconCozCopy
className={classNames('text-sm', {
'coz-fg-secondary': !disabled,
'coz-fg-dim': disabled,
})}
/>
</ToolItemAction>
);
};

View File

@@ -0,0 +1,38 @@
/*
* 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, type FC } from 'react';
import classNames from 'classnames';
import { IconCozTrashCan } from '@coze-arch/coze-design/icons';
import { ToolItemAction } from '..';
type ToolItemActionDeleteProps = ComponentProps<typeof ToolItemAction>;
export const ToolItemActionDelete: FC<ToolItemActionDeleteProps> = props => {
const { disabled } = props;
return (
<ToolItemAction {...props}>
<IconCozTrashCan
className={classNames('text-sm', {
'coz-fg-secondary': !disabled,
'coz-fg-dim': disabled,
})}
/>
</ToolItemAction>
);
};

View File

@@ -0,0 +1,42 @@
/*
* 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, type FC } from 'react';
import classNames from 'classnames';
import { IconCozHamburger } from '@coze-arch/coze-design/icons';
import { ToolItemAction } from '..';
type ToolItemActionEditProps = ComponentProps<typeof ToolItemAction> & {
isDragging: boolean;
};
export const ToolItemActionDrag: FC<ToolItemActionEditProps> = props => {
const { disabled, isDragging } = props;
return (
<ToolItemAction hoverStyle={false} {...props}>
<IconCozHamburger
className={classNames('text-sm', {
'coz-fg-secondary': !disabled,
'coz-fg-dim': disabled,
'cursor-grab': !isDragging,
'cursor-grabbing': isDragging,
})}
/>
</ToolItemAction>
);
};

View File

@@ -0,0 +1,38 @@
/*
* 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, type FC } from 'react';
import classNames from 'classnames';
import { IconCozEdit } from '@coze-arch/coze-design/icons';
import { ToolItemAction } from '..';
type ToolItemActionEditProps = ComponentProps<typeof ToolItemAction>;
export const ToolItemActionEdit: FC<ToolItemActionEditProps> = props => {
const { disabled } = props;
return (
<ToolItemAction {...props}>
<IconCozEdit
className={classNames('text-sm', {
'coz-fg-secondary': !disabled,
'coz-fg-dim': disabled,
})}
/>
</ToolItemAction>
);
};

View File

@@ -0,0 +1,39 @@
/*
* 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, type FC } from 'react';
import classNames from 'classnames';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { ToolItemAction } from '..';
type ToolItemActionInfoProps = ComponentProps<typeof ToolItemAction>;
export const ToolItemActionInfo: FC<ToolItemActionInfoProps> = props => {
const { disabled } = props;
return (
<ToolItemAction {...props}>
<IconCozInfoCircle
className={classNames('text-sm', {
'coz-fg-secondary': !disabled,
'coz-fg-dim': disabled,
})}
/>
</ToolItemAction>
);
};

View File

@@ -0,0 +1,38 @@
/*
* 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, type FC } from 'react';
import classNames from 'classnames';
import { IconCozSetting } from '@coze-arch/coze-design/icons';
import { ToolItemAction } from '..';
type ToolItemActionSettingProps = ComponentProps<typeof ToolItemAction>;
export const ToolItemActionSetting: FC<ToolItemActionSettingProps> = props => {
const { disabled } = props;
return (
<ToolItemAction {...props}>
<IconCozSetting
className={classNames('text-sm', {
'coz-fg-secondary': !disabled,
'coz-fg-dim': disabled,
})}
/>
</ToolItemAction>
);
};

View File

@@ -0,0 +1,64 @@
/*
* 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 PropsWithChildren, type MouseEventHandler } from 'react';
import classNames from 'classnames';
import { ToolTooltip } from '../tool-tooltip';
import { type ToolButtonCommonProps } from '../../typings/button';
type ToolItemActionProps = ToolButtonCommonProps & {
/** 是否展示hover样式 **/
hoverStyle?: boolean;
};
export const ToolItemAction: FC<PropsWithChildren<ToolItemActionProps>> = ({
children,
disabled,
tooltips,
onClick,
hoverStyle = true,
...restProps
}) => {
const handleClick: MouseEventHandler<HTMLDivElement> = e => {
e.preventDefault();
e.stopPropagation();
onClick?.();
};
return (
<ToolTooltip content={tooltips} disableFocusListener={disabled}>
<div
className={classNames(
'w-[24px] h-[24px] flex justify-center items-center rounded-mini',
{
'hover:coz-mg-secondary-hovered active:coz-mg-secondary-pressed cursor-pointer':
!disabled && hoverStyle,
},
{
'coz-fg-dim hover:coz-fg-dim active:coz-fg-dim cursor-not-allowed':
disabled,
},
)}
onClick={disabled ? undefined : handleClick}
data-testid={restProps['data-testid']}
>
{children}
</div>
</ToolTooltip>
);
};

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import classNames from 'classnames';
import { IconCozCard } from '@coze-arch/coze-design/icons';
import { ToolItemIcon } from '..';
interface ToolItemIconCardProps {
isError?: boolean;
}
export const ToolItemIconCard: FC<ToolItemIconCardProps> = ({ isError }) => (
<ToolItemIcon>
<IconCozCard
className={classNames('text-base', {
'coz-fg-secondary': !isError,
'coz-fg-hglt-yellow': isError,
})}
/>
</ToolItemIcon>
);

View File

@@ -0,0 +1,27 @@
/*
* 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 } from 'react';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { ToolItemIcon } from '..';
export const ToolItemIconInfo: FC = () => (
<ToolItemIcon>
<IconCozInfoCircle className="text-sm coz-fg-secondary" />
</ToolItemIcon>
);

View File

@@ -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 { IconCozPeople } from '@coze-arch/coze-design/icons';
import { ToolItemIcon } from '..';
export const ToolItemIconPeople = () => (
<ToolItemIcon size="small">
<IconCozPeople className="text-base coz-fg-secondary" />
</ToolItemIcon>
);

View File

@@ -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 { type FC, type PropsWithChildren } from 'react';
import classNames from 'classnames';
export const ToolItemIcon: FC<PropsWithChildren & { size?: 'small' }> = ({
children,
size,
}) => (
<div
className={classNames('flex justify-center items-center cursor-pointer', {
'w-[24px] h-[24px]': size !== 'small',
'w-[16px] h-[16px]': size === 'small',
})}
onClick={e => e.stopPropagation()}
>
{children}
</div>
);

View File

@@ -0,0 +1,21 @@
/*
* 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 PropsWithChildren } from 'react';
export const ToolItemList: FC<PropsWithChildren> = ({ children }) => (
<div className="grid grid-flow-row gap-y-[4px]">{children}</div>
);

View File

@@ -0,0 +1,59 @@
/*
* 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 ReactNode, type FC, type ChangeEvent } from 'react';
import { Switch } from '@coze-arch/coze-design';
import { ToolTooltip } from '../tool-tooltip';
import { ToolItemIconInfo } from '../tool-item-icon/icons/tool-item-icon-info';
interface ToolItemSwitchProps {
title: string;
tooltips?: ReactNode;
checked?: boolean;
disabled?: boolean;
onChange?:
| ((checked: boolean, e: ChangeEvent<HTMLInputElement>) => void)
| undefined;
}
export const ToolItemSwitch: FC<ToolItemSwitchProps> = ({
title,
tooltips,
checked,
disabled,
onChange,
}) => (
<div className="w-full px-[12px] py-[10px] coz-bg-max flex flex-row items-center rounded-[8px]">
<div className="flex flex-row items-center flex-1 min-w-0">
<p className="coz-fg-primary text-[14px] leading-[20px] mr-[4px]">
{title}
</p>
<ToolTooltip content={tooltips}>
<div>
<ToolItemIconInfo />
</div>
</ToolTooltip>
</div>
<Switch
size="mini"
checked={checked}
onChange={onChange}
disabled={disabled}
/>
</div>
);

View File

@@ -0,0 +1,3 @@
.actions-large * svg {
font-size: 16px;
}

View File

@@ -0,0 +1,210 @@
/*
* 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 react-hooks/rules-of-hooks */
import { type ReactNode, useRef, type FC } from 'react';
import classNames from 'classnames';
import { useHover } from 'ahooks';
import { Divider } from '@coze-arch/coze-design';
import { ToolTooltip } from '../tool-tooltip';
import {
ToolItemContextProvider,
useToolItemContext,
} from '../../context/tool-item-context';
import s from './index.module.less';
interface ToolItemProps {
/**
* 标题
*/
title: string;
/**
* 描述
*/
description: string;
/**
* tags
*/
tags?: ReactNode;
/**
* avatar
*/
avatar: string;
/**
* Actions区域
*/
actions?: ReactNode;
/**
* Icon展示区域
*/
icons?: ReactNode;
/**
* 禁用状态
*/
disabled?: boolean;
/**
* tooltips
*/
tooltips?: ReactNode;
/**
* 点击卡片的回调
*/
onClick?: () => void;
// 尺寸 - 适配 workflow-as-agent 模式下的大号卡片
size?: 'default' | 'large';
avatarStyle?: React.CSSProperties;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export const _ToolItem: FC<ToolItemProps> = ({
title,
description,
avatar,
actions,
icons,
onClick,
tooltips,
tags,
disabled,
size = 'default',
avatarStyle,
}) => {
const containerRef = useRef(null);
const isHovering = useHover(containerRef);
const { isForceShowAction } = useToolItemContext();
const isShowAction = isHovering || isForceShowAction;
return (
<ToolTooltip content={tooltips} position="top">
<div
data-testid={'bot.editor.tool.added-tool'}
ref={containerRef}
className={classNames(
'w-full flex flex-row items-center coz-bg-max rounded-[8px]',
{
default: 'min-h-[56px] px-[8px] py-[10px]',
large: 'min-h-[102px] px-[24px] py-[16px]',
}[size],
{
'!coz-mg-secondary-hovered': isHovering,
'cursor-pointer': Boolean(onClick),
'cursor-default': !onClick,
'cursor-not-allowed': disabled,
},
)}
onClick={onClick}
>
<div
className={classNames(
'flex flex-row flex-1 min-w-[0px] justify-center items-center',
{
'opacity-30': disabled,
},
)}
>
{avatar ? (
<img
src={avatar}
style={avatarStyle}
className={classNames(
{
default: 'w-[36px] h-[36px] rounded-[5px]',
large: 'w-[48px] h-[48px] rounded-[6px]',
}[size],
'overflow-hidden',
)}
/>
) : null}
<div
className={classNames(
{
default: 'ml-[8px]',
large: 'ml-[12px]',
}[size],
'flex flex-col flex-1 min-w-[0px] w-0',
)}
>
<div className="flex flex-row items-center overflow-hidden">
<p
className={classNames(
{
default: 'text-[14px] leading-[20px]',
large: 'text-[20px] leading-[28px]',
}[size],
'coz-fg-primary truncate flex-1 font-medium',
)}
>
{title}
</p>
{!isShowAction || disabled ? (
<div className="justify-self-end grid grid-flow-col gap-x-[2px]">
{icons}
</div>
) : null}
</div>
<p
className={classNames(
{
default: 'text-[12px] leading-[16px] truncate',
large:
'text-[14px] leading-[20px] mt-[2px] text-clip line-clamp-2',
}[size],
'coz-fg-secondary',
)}
>
{tags ? (
<>
{tags}
<Divider layout="vertical" margin="4px" className="h-[9px]" />
</>
) : null}
{description}
</p>
</div>
</div>
<div
className={classNames(
{
default: 'grid grid-flow-col gap-x-[2px]',
large: 'flex gap-[4px] ml-[12px]',
}[size],
size === 'large' && s['actions-large'],
{
hidden: !isShowAction,
'opacity-30': disabled,
},
)}
onClick={e => e.stopPropagation()}
>
{actions}
</div>
</div>
</ToolTooltip>
);
};
export const ToolItem: FC<ToolItemProps> = props => (
<ToolItemContextProvider>
<_ToolItem {...props} />
</ToolItemContextProvider>
);

View File

@@ -0,0 +1,31 @@
.tool-menu-dropdown-menu {
width: 210px;
max-height: 590px;
}
@media screen and (max-height: 750px) {
.tool-menu-dropdown-menu {
max-height: 400px;
}
}
.dropdown-item {
display: block;
}
.dropdown-item-container {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
font-weight: 400;
}
.dropdown-item-text {
margin-left: 8px;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}

View File

@@ -0,0 +1,130 @@
/*
* 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 } from 'react';
import { useShallow } from 'zustand/react/shallow';
import {
TOOL_GROUP_CONFIG,
TOOL_KEY_TO_API_STATUS_KEY_MAP,
type ToolKey,
} from '@coze-agent-ide/tool-config';
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
import { I18n } from '@coze-arch/i18n';
import { Menu, Checkbox } from '@coze-arch/coze-design';
import { TabStatus } from '@coze-arch/bot-api/developer_api';
import { ToolTooltip } from '../tool-tooltip';
import { useRegisteredToolKeyConfigList } from '../../hooks/builtin/use-register-tool-key';
import { useRegisteredToolGroupList } from '../../hooks/builtin/use-register-tool-group';
import { usePreference } from '../../context/preference-context';
import styles from './index.module.less';
type IProps = Record<string, unknown>;
export const ToolMenuDropdownMenu: FC<IProps> = () => {
const registeredToolKeyConfigList = useRegisteredToolKeyConfigList();
const registeredToolGroupList = useRegisteredToolGroupList();
const { botSkillBlockCollapsible, setBotSkillBlockCollapsibleState } =
usePageRuntimeStore(
useShallow(state => ({
botSkillBlockCollapsible: state.botSkillBlockCollapsibleState,
setBotSkillBlockCollapsibleState:
state.setBotSkillBlockCollapsibleState,
})),
);
const { isReadonly } = usePreference();
if (!registeredToolKeyConfigList.length) {
return null;
}
const toolGroupKeyList = Object.keys(TOOL_GROUP_CONFIG);
const menuConfig = toolGroupKeyList
.map(toolGroupKey => ({
toolGroupKey,
toolGroupTitle: registeredToolGroupList.find(
toolGroupConfig => toolGroupConfig.toolGroupKey === toolGroupKey,
)?.groupTitle,
toolList: registeredToolKeyConfigList
.filter(toolConfig => toolConfig.toolGroupKey === toolGroupKey)
.map(toolConfig => toolConfig),
}))
.filter(toolGroup => toolGroup.toolList.length);
const getToolStatus = (toolKey: ToolKey) =>
botSkillBlockCollapsible[TOOL_KEY_TO_API_STATUS_KEY_MAP[toolKey]];
const handleClick = (toolKey: ToolKey, currentStatus?: TabStatus) => {
if (isReadonly) {
return;
}
setBotSkillBlockCollapsibleState({
[TOOL_KEY_TO_API_STATUS_KEY_MAP[toolKey]]:
currentStatus === TabStatus.Hide ? TabStatus.Default : TabStatus.Hide,
});
};
return (
<div className={styles['tool-menu-dropdown-menu']}>
<Menu.SubMenu mode="menu">
{menuConfig.map((toolGroup, groupIdx) => (
<div key={toolGroup.toolGroupKey}>
<Menu.Title style={{ paddingLeft: '32px' }}>
{toolGroup.toolGroupTitle}
</Menu.Title>
{toolGroup.toolList.map(tool => {
const toolStatus = getToolStatus(tool.toolKey);
return (
<ToolTooltip
content={
tool.hasValidData
? I18n.t('modules_menu_guide_warning')
: undefined
}
position="right"
key={`tooltips-${tool.toolKey}`}
>
<Menu.Item
style={{ display: 'block' }}
key={tool.toolKey}
disabled={tool.hasValidData}
onClick={() => handleClick(tool.toolKey, toolStatus)}
>
<div className={styles['dropdown-item-container']}>
<Checkbox
checked={toolStatus !== TabStatus.Hide}
disabled={tool.hasValidData}
/>
<span className={styles['dropdown-item-text']}>
{tool.toolTitle}
</span>
</div>
</Menu.Item>
</ToolTooltip>
);
})}
{groupIdx < menuConfig.length - 1 ? <Menu.Divider /> : null}
</div>
))}
</Menu.SubMenu>
</div>
);
};

View File

@@ -0,0 +1,70 @@
/*
* 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 FC } from 'react';
import classnames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { useCommonConfigStore } from '@coze-foundation/global-store';
import guideFallbackImage from './images/guide-fallback.png';
import styles from './index.module.less';
interface IProps {
onClose?: () => void;
}
export const GuidePopover: FC<IProps> = ({ onClose }) => {
const [fallbackUrl, setFallbackUrl] = useState('');
const botIdeGuideVideoUrl = useCommonConfigStore(
state => state.commonConfigs.botIdeGuideVideoUrl,
);
return (
<div className={styles.guide}>
<p className={classnames(styles['guide-text'], 'coz-fg-primary')}>
{I18n.t('modules_menu_guide')}
</p>
{fallbackUrl ? (
<img src={fallbackUrl} className={styles['guide-image']} />
) : (
<video
width={380}
height={238}
src={botIdeGuideVideoUrl}
poster={guideFallbackImage}
data-object-fit
muted
data-autoplay
loop={true}
autoPlay={true}
onError={() => setFallbackUrl(guideFallbackImage)}
className={styles['guide-video']}
/>
)}
<Button
className={styles['guide-button']}
type="primary"
theme="solid"
onClick={onClose}
>
{I18n.t('modules_menu_guide_gotit')}
</Button>
</div>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -0,0 +1,29 @@
.guide-popover {}
.guide {
width: 380px;
}
.guide-text {
margin-bottom: 8px;
font-size: 16px;
font-weight: 600;
line-height: 22px;
}
.guide-image {
width: 380px;
margin: 0 0 10px;
padding: 0;
}
.guide-video {
overflow: hidden;
width: 380px;
margin-bottom: 8px;
border-radius: 8px;
}
.guide-button {
width: 100%;
}

View File

@@ -0,0 +1,79 @@
/*
* 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 } from 'react';
import classNames from 'classnames';
import { Menu, Popover, IconButton } from '@coze-arch/coze-design';
import { IconMenu } from '@coze-arch/bot-icons';
import { ToolMenuDropdownMenu } from '../tool-menu-dropdown-menu';
import { GuidePopover } from './guide-popover';
import s from './index.module.less';
interface IProps {
visible?: boolean;
newbieGuideVisible?: boolean;
onNewbieGuidePopoverClose?: () => void;
rePosKey: number;
}
export const ToolMenu: FC<IProps> = ({
visible = true,
onNewbieGuidePopoverClose,
newbieGuideVisible,
rePosKey,
}) => {
const onButtonClick = () => {
if (!newbieGuideVisible) {
return;
}
onNewbieGuidePopoverClose?.();
};
return (
<div
className={classNames({
hidden: !visible,
[s['guide-popover'] || '']: true,
})}
>
<Popover
content={<GuidePopover onClose={onNewbieGuidePopoverClose} />}
trigger="custom"
visible={newbieGuideVisible && visible}
showArrow
onClickOutSide={onButtonClick}
>
<Menu
trigger="click"
position="bottomRight"
render={<ToolMenuDropdownMenu />}
rePosKey={rePosKey}
>
<IconButton
size="default"
color="secondary"
icon={<IconMenu className="text-[16px]" />}
onClick={onButtonClick}
/>
</Menu>
</Popover>
</div>
);
};

View File

@@ -0,0 +1,3 @@
.tool-popover {
color: rgba(255, 255, 255, 79%)!important;
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import { Popover, type PopoverProps } from '@coze-arch/coze-design';
import s from './index.module.less';
type ToolPopoverProps = {
children: JSX.Element;
hideToolTip?: boolean;
} & PopoverProps;
export const ToolPopover: FC<ToolPopoverProps> = props => {
const { content, children, hideToolTip, ...restProps } = props;
return (
<Popover
showArrow
position="top"
className={s['tool-popover']}
trigger={hideToolTip ? 'custom' : 'hover'}
visible={hideToolTip ? false : undefined}
content={content}
style={{ backgroundColor: '#363D4D', padding: 8 }}
{...restProps}
>
{children}
</Popover>
);
};

View File

@@ -0,0 +1,10 @@
.tool-tooltips {
color: rgba(255, 255, 255, 79%)!important;
background-color: #363D4D !important;
:global {
.semi-tooltip-icon-arrow {
color: #363D4D !important;
}
}
}

View File

@@ -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 FC } from 'react';
import { Tooltip, type TooltipProps } from '@coze-arch/coze-design';
import s from './index.module.less';
type ToolTooltipsProps = {
children: JSX.Element;
hideToolTip?: boolean;
} & TooltipProps;
export const ToolTooltip: FC<ToolTooltipsProps> = props => {
const { content, children, hideToolTip, ...restProps } = props;
return content ? (
<Tooltip
trigger={hideToolTip ? 'custom' : 'hover'}
visible={hideToolTip ? false : undefined}
content={content}
className={s['tool-tooltips']}
{...restProps}
>
{children}
</Tooltip>
) : (
<>{children}</>
);
};

View File

@@ -0,0 +1,107 @@
/*
* 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 { Children, useMemo, type FC, type PropsWithChildren } from 'react';
import classNames from 'classnames';
import { AbilityScope } from '@coze-agent-ide/tool-config';
import { Spin } from '@coze-arch/coze-design';
import { PlacementEnum, useLayoutContext } from '@coze-arch/bot-hooks';
import { ToolContainer } from '../tool-container';
import { useSubscribeToolStore } from '../../hooks/public/store/use-tool-store';
import { useRegisterToolKey } from '../../hooks/builtin/use-register-tool-key';
import { useRegisterToolGroup } from '../../hooks/builtin/use-register-tool-group';
import { useAbilityAreaContext } from '../../context/ability-area-context';
type IProps = Record<string, unknown>;
export const ToolView: FC<PropsWithChildren<IProps>> = ({ children }) => {
const {
store: { useToolAreaStore },
} = useAbilityAreaContext();
const registerToolKey = useRegisterToolKey();
const registerToolGroup = useRegisterToolGroup();
useSubscribeToolStore(AbilityScope.TOOL);
const { isInitialed, isModeSwitching } = useToolAreaStore(state => ({
isInitialed: state.isInitialed,
isModeSwitching: state.isModeSwitching,
}));
const { placement } = useLayoutContext();
const newChildren = useMemo(() => {
const allChildren = Array.isArray(children) ? children : [children];
if (!isInitialed) {
return isModeSwitching ? null : (
<div
className={classNames('w-full flex items-center justify-center', {
'h-auto': placement === PlacementEnum.LEFT,
'h-full': placement === PlacementEnum.CENTER,
})}
>
<Spin spinning />
</div>
);
}
// 遍历 GroupingContainer 的所有子元素
return Children.map(allChildren, childLevel1 => {
if (Children.count(childLevel1?.props?.children)) {
return {
...childLevel1,
props: {
...childLevel1.props,
// 子元素都套一层 ToolContainer
children: Children.map(childLevel1.props.children, childLevel2 => {
const { toolKey, title: toolTitle } = childLevel2?.props ?? {};
const { toolGroupKey, title: groupTitle } =
childLevel1?.props ?? {};
if (!toolKey || !toolTitle || !toolGroupKey || !groupTitle) {
return childLevel2;
}
registerToolGroup({
toolGroupKey,
groupTitle,
});
registerToolKey({
toolKey,
toolGroupKey,
toolTitle,
hasValidData: false,
});
return (
<ToolContainer scope={AbilityScope.TOOL} toolKey={toolKey}>
{childLevel2}
</ToolContainer>
);
}),
},
};
} else {
return childLevel1;
}
});
}, [children, isInitialed]);
return newChildren;
};