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,35 @@
/*
* 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 { Helmet } from 'react-helmet';
import React from 'react';
import { I18n } from '@coze-arch/i18n';
import { useProjectInfo } from '../../hooks';
export const BrowserTitle: React.FC = () => {
const { projectInfo } = useProjectInfo();
return (
<Helmet>
<title>
{I18n.t('project_ide_tab_title', {
project_name: projectInfo?.name,
})}
</title>
</Helmet>
);
};

View File

@@ -0,0 +1,55 @@
.config-container {
overflow: hidden;
width: 100%;
height: 100%;
background: var(--coz-bg-max);
border: 1px solid var(--coz-stroke-primary);
border-top: none;
border-radius: 0 0 8px 8px;
.primary-sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0 14px;
font-size: 14px;
line-height: 20px;
.title {
font-weight: 500;
color: var(--coz-fg-plus);
}
}
.item {
display: flex;
align-items: center;
margin: 0 8px;
padding: 4px 8px 4px 20px;
font-size: 14px;
line-height: 1.5;
color: var(--coz-fg-primary);
border-radius: 8px;
&:hover {
cursor: pointer;
color: var(--coz-fg-plus);
background-color: var(--coz-mg-primary);
}
&.activate {
cursor: pointer;
color: var(--coz-fg-plus);
background-color: var(--coz-mg-primary);
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* 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, { useState, useCallback } from 'react';
import classnames from 'classnames';
import {
useIDENavigate,
useCurrentWidget,
type SplitWidget,
URI_SCHEME,
compareURI,
SIDEBAR_CONFIG_URI,
useActivateWidgetContext,
URI,
} from '@coze-project-ide/framework';
import { I18n } from '@coze-arch/i18n';
import {
IconCozArrowDown,
IconCozArrowUp,
IconCozChatSetting,
IconCozVariables,
} from '@coze-arch/coze-design/icons';
import { IconButton } from '@coze-arch/coze-design';
import { HEADER_HEIGHT } from '../../constants/styles';
import styles from './index.module.less';
export const SESSION_CONFIG_STR = '/session';
const SESSION_CONFIG_URI = new URI(`${URI_SCHEME}:///session`);
const VARIABLE_CONFIG_URI = new URI(`${URI_SCHEME}:///variables`);
const VARIABLES_STR = '/variables';
export const Configuration = () => {
const navigate = useIDENavigate();
const widget = useCurrentWidget();
const context = useActivateWidgetContext();
const [expand, setExpand] = useState(true);
const handleOpenSession = useCallback(() => {
navigate(SESSION_CONFIG_STR);
}, []);
const handleOpenVariables = useCallback(() => {
navigate(VARIABLES_STR);
}, []);
const handleSwitchExpand = () => {
if (widget) {
(widget as SplitWidget).toggleSubWidget(SIDEBAR_CONFIG_URI);
}
setExpand(!expand);
};
return (
<div className={styles['config-container']}>
<div
className={classnames(
styles['primary-sidebar-header'],
`h-[${HEADER_HEIGHT}px]`,
)}
>
<div className={styles.title}>{I18n.t('wf_chatflow_143')}</div>
<IconButton
icon={
expand ? (
<IconCozArrowDown className="coz-fg-primary" />
) : (
<IconCozArrowUp className="coz-fg-primary" />
)
}
color="secondary"
size="small"
onClick={handleSwitchExpand}
/>
</div>
{/* The community version does not currently support conversation management in project, for future expansion */}
{IS_OPEN_SOURCE ? null : (
<div
className={classnames(
styles.item,
compareURI(context?.uri, SESSION_CONFIG_URI) && styles.activate,
)}
onClick={handleOpenSession}
>
<IconCozChatSetting
className="coz-fg-plus"
style={{ marginRight: 4 }}
/>
{I18n.t('wf_chatflow_101')}
</div>
)}
<div
className={classnames(
styles.item,
compareURI(context?.uri, VARIABLE_CONFIG_URI) && styles.activate,
)}
onClick={handleOpenVariables}
>
<IconCozVariables className="coz-fg-plus" style={{ marginRight: 4 }} />
{I18n.t('dataide002')}
</div>
</div>
);
};

View File

@@ -0,0 +1,29 @@
/*
* 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 { IconCozIllusError } from '@coze-arch/coze-design/illustrations';
import { EmptyState } from '@coze-arch/coze-design';
export const ErrorFallback = () => (
<EmptyState
size="full_screen"
icon={<IconCozIllusError />}
title="An error occurred"
description="Please try again later."
/>
);

View File

@@ -0,0 +1,159 @@
/*
* 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, useEffect } from 'react';
import { type WsMessageProps } from '@coze-project-ide/framework/src/types';
import {
useIDEService,
ErrorService,
LayoutRestorer,
useIDENavigate,
useCommitVersion,
useWsListener,
useGetUIWidgetFromId,
getURIPathByPathname,
} from '@coze-project-ide/framework';
import {
BizResourceTypeEnum,
ProjectResourceGroupType,
usePrimarySidebarStore,
} from '@coze-project-ide/biz-components';
import {
MessageBizType,
MessageOperateType,
} from '@coze-arch/idl/workflow_api';
import { useFlags } from '@coze-arch/bot-flags';
import { type LayoutRestoreService } from '../../plugins/create-app-plugin/layout-restore-service';
const leftPanelResourceType = [
MessageBizType.Database,
MessageBizType.Dataset,
MessageBizType.Plugin,
MessageBizType.Workflow,
];
/**
* IDE 全局逻辑处理
*/
export const GlobalHandler = ({
spaceId,
projectId,
}: {
spaceId: string;
projectId: string;
}) => {
const [error, setError] = useState(false);
const navigate = useIDENavigate();
const errorService = useIDEService<ErrorService>(ErrorService);
const restoreService = useIDEService<LayoutRestoreService>(LayoutRestorer);
const { version } = useCommitVersion();
const [FLAGS] = useFlags();
const path = getURIPathByPathname(window.location.pathname);
if (error) {
throw new Error('project ide global handler error');
}
const fetchResource = usePrimarySidebarStore(state => state.fetchResource);
const refetchResource = usePrimarySidebarStore(state => state.refetch);
const [workflowId, setWorkflowId] = useState('');
const [refreshKey, setRefreshKey] = useState('');
const widget = useGetUIWidgetFromId(
`/${BizResourceTypeEnum.Workflow}/${workflowId}`,
);
useEffect(() => {
if (!workflowId) {
return;
}
refetchResource(tree => {
const workflowResource =
tree?.find(
group => group.groupType === ProjectResourceGroupType.Workflow,
)?.resourceList || [];
const nextName = workflowResource?.find(
workflow => workflow.id === workflowId,
)?.name;
if (nextName && widget) {
widget.context.widget?.setTitle(nextName);
}
});
}, [workflowId, widget, refreshKey]);
useWsListener((props: WsMessageProps) => {
if (
// 社区版暂不支持该功能
!FLAGS['bot.automation.project_multi_tab'] ||
!leftPanelResourceType.includes(props.bizType)
) {
return;
}
const isUpdateOperate = props.operateType === MessageOperateType.Update;
const isRollbackProject = props?.extra?.Scene === 'RollbackProject';
if (isUpdateOperate && isRollbackProject) {
window.location.reload();
return;
}
const isCreateOperate = props.operateType === MessageOperateType.Create;
// 只是创建 workflow 则刷新资源列表
const isCreateWorkflow = props?.extra?.methodName === 'CreateWorkflow';
// 封装解封场景需要刷新资源列表
const isEncapsulateWorkflow =
props?.extra?.methodName === 'EncapsulateWorkflow';
if (isCreateOperate && (isCreateWorkflow || isEncapsulateWorkflow)) {
refetchResource();
return;
}
if (!props?.resId) {
return;
}
setWorkflowId(props?.resId);
setRefreshKey(new Date().getTime().toString());
});
useEffect(() => {
fetchResource(spaceId, projectId, version, tree => {
const workflowResource =
(tree || []).find(
group => group.groupType === ProjectResourceGroupType.Workflow,
)?.resourceList || [];
const firstWorkflow = workflowResource?.[0];
if (!path && restoreService.openFirstWorkflow && firstWorkflow) {
navigate(`/workflow/${firstWorkflow.id}`);
restoreService.openFirstWorkflow = false;
}
});
}, [spaceId, projectId]);
useEffect(() => {
const errorListener = errorService.onError(() => {
setError(true);
});
return () => {
errorListener?.dispose();
};
}, []);
return null;
};

View File

@@ -0,0 +1,14 @@
.global-loading {
position: fixed;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
background: var(--coz-bg-max);
}

View File

@@ -0,0 +1,46 @@
/*
* 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, { useEffect, useState } from 'react';
import { Spin } from '@coze-arch/coze-design';
import { useIDEService } from '@coze-project-ide/framework';
import { AppContribution } from '../../plugins/create-app-plugin/app-contribution';
import css from './index.module.less';
export const GlobalLoading = () => {
const [ready, setReady] = useState(false);
const app = useIDEService<AppContribution>(AppContribution);
useEffect(() => {
const disposable = app.onStarted(() => {
setReady(true);
});
return () => disposable.dispose();
}, [app]);
if (ready) {
return null;
}
return (
<div className={css['global-loading']}>
<Spin />
</div>
);
};

View File

@@ -0,0 +1,97 @@
/*
* 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, { useState, useCallback, useEffect, useRef } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Modal } from '@coze-arch/coze-design';
import {
useIDEService,
WindowService,
ViewService,
ModalService,
ModalType,
type CustomTitleType,
} from '@coze-project-ide/framework';
import styles from './styles.module.less';
export const CloseConfirmModal = () => {
const modalService = useIDEService<ModalService>(ModalService);
const windowService = useIDEService<WindowService>(WindowService);
const viewService = useIDEService<ViewService>(ViewService);
const currentTitlesRef = useRef<CustomTitleType[]>();
const [visible, setVisible] = useState(false);
const handleOk = useCallback(() => {
(currentTitlesRef.current || []).forEach(title => {
title?.owner?.close();
});
setVisible(false);
}, []);
const handleCancel = useCallback(() => {
setVisible(false);
}, []);
useEffect(() => {
// 浏览器维度的 dispose 监听
const chromeDispose = windowService.onBeforeUnload(e => {
// 路由判断
const titles = viewService.getOpenTitles();
const hasUnsaved = titles.some(title => title.saving);
// 当存在未保存的项的时候,需要阻止
if (hasUnsaved) {
// 每次浏览器关闭之前都打开阻止关闭的弹窗
// 兼容不同浏览器的行为
e.preventDefault();
e.stopPropagation();
e.returnValue = '';
return '';
}
});
// 资源维度的 dispose 监听
const resourceDisposable = modalService.onModalVisibleChange(opt => {
const { type, options, visible: vis = true } = opt;
if (type === ModalType.CLOSE_CONFIRM) {
setVisible(Boolean(vis));
currentTitlesRef.current = options;
}
});
return () => {
chromeDispose.dispose();
resourceDisposable.dispose();
};
}, []);
return (
<Modal
visible={visible}
type="dialog"
title={I18n.t('project_ide_unsaved_changes')}
okText={I18n.t('project_ide_quit')}
okButtonColor="red"
cancelText={I18n.t('project_ide_cancel')}
onOk={handleOk}
onCancel={handleCancel}
maskClosable={false}
>
<div className={styles.content}>
{I18n.t('project_ide_unsaved_describe')}
</div>
</Modal>
);
};

View File

@@ -0,0 +1,6 @@
.content {
margin-top: 4px;
font-size: 14px;
line-height: 20px;
color: var(--coz-fg-secondary);
}

View File

@@ -0,0 +1,30 @@
/*
* 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 { ResourceModal } from './resource-modal';
import { CloseConfirmModal } from './close-confirm-modal';
export const GlobalModals = () => (
// do something
<>
{/* 移动资源库全局弹窗 */}
<ResourceModal />
{/* 保存中资源关闭弹窗 */}
<CloseConfirmModal />
</>
);

View File

@@ -0,0 +1,162 @@
/*
* 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, { useCallback, useEffect, useMemo, useState } from 'react';
import {
ModalService,
ModalType,
useIDEService,
} from '@coze-project-ide/framework';
import { I18n } from '@coze-arch/i18n';
import { IconCozWarningCircleFillPalette } from '@coze-arch/coze-design/icons';
import { Modal } from '@coze-arch/coze-design';
import { ResourceCopyScene } from '@coze-arch/bot-api/plugin_develop';
import { LoopContent } from './loop-content';
import styles from './styles.module.less';
export const ResourceModal = () => {
const [visible, setVisible] = useState(false);
const [scene, setScene] = useState<ResourceCopyScene | undefined>(undefined);
const [error, setError] = useState<boolean | string>(false);
const [resourceName, setResourceName] = useState('');
const modalService = useIDEService<ModalService>(ModalService);
const handleCancel = useCallback(() => {
modalService.onCloseResourceModal();
}, []);
const handleOk = useCallback(() => {
if (error) {
modalService.retry();
}
}, [error]);
const title = useMemo(() => {
switch (scene) {
case ResourceCopyScene.CopyResourceFromLibrary:
return I18n.t(
'resource_process_modal_title_import_resource_from_library',
);
case ResourceCopyScene.MoveResourceToLibrary:
return I18n.t('resource_process_modal_title_move_resource_to_library');
case ResourceCopyScene.CopyResourceToLibrary:
return I18n.t('resource_process_modal_title_copy_resource_to_library');
case ResourceCopyScene.CopyProjectResource:
return I18n.t('workflow_add_list_copy');
default:
return '';
}
}, [scene]);
const errorContent = useMemo(() => {
if (typeof error === 'string' && error !== 'no_task_id') {
return error;
}
switch (scene) {
case ResourceCopyScene.CopyResourceFromLibrary:
return I18n.t('resource_toast_copy_to_project_fail');
case ResourceCopyScene.MoveResourceToLibrary:
return I18n.t('resource_toast_move_to_library_fail');
case ResourceCopyScene.CopyResourceToLibrary:
return I18n.t('resource_toast_copy_to_library_fail');
case ResourceCopyScene.CopyProjectResource:
return I18n.t('project_toast_copy_failed');
default:
return '';
}
}, [scene, error]);
const content = useMemo(() => {
if (error) {
return (
<div className={styles['error-container']}>
<IconCozWarningCircleFillPalette
className="coz-fg-hglt-red"
fontSize={22}
/>
{errorContent}
</div>
);
}
return <LoopContent scene={scene} resourceName={resourceName} />;
}, [error, errorContent, resourceName, scene]);
const okText = useMemo(() => {
// Retry is not supported in the open-source environment.
if (IS_OPEN_SOURCE) {
return '';
}
if (typeof error === 'string') {
return '';
} else if (error) {
return I18n.t('resource_process_modal_retry_button');
}
return '';
}, [error]);
useEffect(() => {
const resourceDisposable = modalService.onModalVisibleChange(
({
type,
visible: vis = true,
scene: _scene,
resourceName: _resourceName,
}) => {
if (type === ModalType.RESOURCE) {
setVisible(Boolean(vis));
setScene(_scene);
setResourceName(_resourceName || '');
setError(false);
}
},
);
const resourceErrorDisposable = modalService.onError(isError => {
setError(isError);
});
return () => {
resourceDisposable.dispose();
resourceErrorDisposable.dispose();
};
}, []);
const cancelText = useMemo(() => {
// Cancel logic changes in the open-source environment.
if (IS_OPEN_SOURCE) {
return error ? I18n.t('resource_process_modal_cancel_button') : undefined;
}
return I18n.t('resource_process_modal_cancel_button');
}, [error]);
return (
<Modal
visible={visible}
width={384}
type="dialog"
title={title}
okText={okText}
onOk={handleOk}
cancelText={cancelText}
onCancel={handleCancel}
maskClosable={false}
>
<div className={styles.content}>{content}</div>
</Modal>
);
};

View File

@@ -0,0 +1,80 @@
/*
* 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, { useMemo } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Loading } from '@coze-arch/coze-design';
import { ResourceCopyScene } from '@coze-arch/bot-api/plugin_develop';
import styles from './styles.module.less';
export const LoopContent = ({
scene,
resourceName,
}: {
scene?: ResourceCopyScene;
resourceName?: string;
}) => {
const loopMoveText = useMemo(() => {
switch (scene) {
case ResourceCopyScene.CopyResourceFromLibrary:
return I18n.t(
'resource_process_modal_text_copying_resource_to_project',
{
resourceName,
},
);
case ResourceCopyScene.MoveResourceToLibrary:
return I18n.t(
'resource_process_modal_text_moving_resource_to_library',
{
resourceName,
},
);
case ResourceCopyScene.CopyResourceToLibrary:
return I18n.t(
'resource_process_modal_text_copying_resource_to_library',
{
resourceName,
},
);
case ResourceCopyScene.CopyProjectResource:
return I18n.t('project_toast_copying_resource', { resourceName });
default:
return '';
}
}, [scene, resourceName]);
const loopSuggestionText = useMemo(() => {
if (scene === ResourceCopyScene.MoveResourceToLibrary) {
return I18n.t(
'resource_process_modal_text_moving_process_interrupt_warning',
);
}
return I18n.t(
'resource_process_modal_text_copying_process_interrupt_warning',
);
}, [scene]);
return (
<div className={styles['description-container']}>
<Loading loading={true} wrapperClassName={styles.spin} />
<div>{loopMoveText}</div>
<div>{loopSuggestionText}</div>
</div>
);
};

View File

@@ -0,0 +1,34 @@
.content {
margin-top: 4px;
font-size: 14px;
line-height: 20px;
color: var(--coz-fg-secondary);
.error-container {
display: flex;
flex: 1 0 0;
flex-direction: column;
gap: 22px;
align-items: center;
justify-content: center;
margin: 24px 0 22px;
white-space: pre-wrap;
}
.description-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 28px 0 12px;
text-align: center;
.spin {
margin-bottom: 12px;
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { TopBar } from './top-bar';
export { PrimarySidebar } from './primary-sidebar';
export { widgetTitleRender } from './widget-title';
export { WidgetDefaultRenderer } from './widget-default-renderer';
export { SidebarExpand } from './sidebar-expand';
export { FullScreenButton } from './toolbar/full-screen-button';
export { ToolBar } from './toolbar';
export { GlobalModals } from './global-modals';
export { Configuration } from './configuration';
export { ErrorFallback } from './error-fallback';
export { GlobalHandler } from './global-handler';
export { BrowserTitle } from './browser-title';
export { GlobalLoading } from './global-loading';
export { UIBuilder } from './ui-builder';

View File

@@ -0,0 +1,110 @@
/*
* 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, { useState, lazy, Suspense } from 'react';
import classnames from 'classnames';
import { withQueryClient } from '@coze-workflow/base';
import { useProjectIDEServices } from '@coze-project-ide/framework';
import { useResourceList } from '@coze-project-ide/biz-components';
import { I18n } from '@coze-arch/i18n';
import {
IconCozSideExpand,
IconCozBinding,
} from '@coze-arch/coze-design/icons';
import { IconButton, Button } from '@coze-arch/coze-design';
import { useFlags } from '@coze-arch/bot-flags';
import { ResourceList } from '../resource-list';
import { HEADER_HEIGHT } from '../../constants/styles';
import styles from './styles.module.less';
const ResourceTreeModal = lazy(() =>
import('../resource-tree-modal').then(exps => ({
default: exps.ResourceTreeModal,
})),
);
const PrimarySidebarCore = ({
hideExpand,
idPrefix,
}: {
hideExpand?: boolean;
idPrefix?: string;
}) => {
const projectIDEServices = useProjectIDEServices();
const { workflowResource } = useResourceList();
const [modalVisible, setModalVisible] = useState(false);
const [FLAGS] = useFlags();
const handleExpand = () => {
projectIDEServices.view.primarySidebar.changeVisible(false);
};
return (
<div className={styles['primary-sidebar']}>
<div
className={classnames(
styles['primary-sidebar-header'],
`h-[${HEADER_HEIGHT}px]`,
)}
>
<div className={styles.title}>
{I18n.t('project_resource_sidebar_title')}
{/* 社区版暂不支持该功能 */}
{FLAGS['bot.automation.dependency_tree'] ? (
<>
<Button
size="small"
icon={<IconCozBinding />}
color="primary"
disabled={!workflowResource?.length}
onClick={() => setModalVisible(true)}
>
{I18n.t('reference_graph_entry_button')}
</Button>
{modalVisible ? (
<Suspense fallback={null}>
<ResourceTreeModal
modalVisible={modalVisible}
setModalVisible={setModalVisible}
/>
</Suspense>
) : null}
</>
) : null}
</div>
{hideExpand ? null : (
<IconButton
data-testid="project-expand-button"
icon={<IconCozSideExpand className="coz-fg-primary" />}
color="secondary"
size="small"
onClick={handleExpand}
/>
)}
</div>
<div
className={styles['resource-list-wrapper']}
style={{ height: `calc(100% - ${HEADER_HEIGHT}px)` }}
>
<ResourceList idPrefix={idPrefix} />
</div>
</div>
);
};
export const PrimarySidebar = withQueryClient(PrimarySidebarCore);

View File

@@ -0,0 +1,50 @@
.primary-sidebar {
overflow: hidden;
width: 100%;
height: 100%;
background: var(--coz-bg-max);
border: 1px solid var(--coz-stroke-primary);
border-bottom: none;
border-radius: 8px 8px 0 0;
}
.primary-sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0 14px;
font-size: 14px;
line-height: 20px;
.title {
font-weight: 500;
color: var(--coz-fg-plus);
}
}
.resource-list-wrapper {
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
height: 10px;
background: transparent;
}
&::-webkit-scrollbar:hover {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: transparent;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
}

View File

@@ -0,0 +1,191 @@
/*
* 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, { useMemo } from 'react';
import {
useProjectId,
useSpaceId,
useCommitVersion,
} from '@coze-project-ide/framework';
import { useWorkflowResource } from '@coze-project-ide/biz-workflow';
import { usePluginResource } from '@coze-project-ide/biz-plugin';
import { useDataResource } from '@coze-project-ide/biz-data';
import {
BizResourceTypeEnum,
ProjectResourceGroupType,
ResourceFolderCoze,
useResourceList,
VARIABLE_RESOURCE_ID,
} from '@coze-project-ide/biz-components';
import {
EProjectPermission,
useProjectAuth,
useProjectRole,
} from '@coze-common/auth';
import { FormatType } from '@coze-arch/bot-api/knowledge';
import {
IconCozDatabase,
IconCozDocument,
IconCozImage,
IconCozPlugin,
IconCozTable,
IconCozVariables,
} from '@coze-arch/coze-design/icons';
const datasetIconMap = {
[FormatType.Text]: <IconCozDocument />,
[FormatType.Table]: <IconCozTable />,
[FormatType.Image]: <IconCozImage />,
};
export const ResourceList = ({
idPrefix = 'fixed-sidebar',
}: {
idPrefix?: string;
}) => {
const {
onCustomCreate: createWorkflow,
onDelete: deleteWorkflow,
onChangeName: changeNameWorkflow,
onAction: handleWorkflowAction,
createResourceConfig: workflowCreateConfig,
iconRender: workflowIconRender,
modals: workflowModals,
} = useWorkflowResource();
const {
onCustomCreate: createPlugin,
onDelete: deletePlugin,
onChangeName: changeNamePlugin,
onAction: handlePluginAction,
// createResourceConfig: workflowCreateConfig,
validateConfig: validatePluginConfig,
modals: pluginModals,
} = usePluginResource();
const {
onCustomCreate: createData,
onDelete: deleteData,
onChangeName: changeNameData,
onAction: handleDataAction,
createResourceConfig: dataCreateConfig,
modals: dataModals,
validateConfig,
} = useDataResource();
const spaceId = useSpaceId();
const projectId = useProjectId();
const { version: commitVersion } = useCommitVersion();
let canCreate = useProjectAuth(
EProjectPermission.CREATE_RESOURCE,
projectId,
spaceId,
);
// 存在版本信息,预览状态无法创建资源
if (commitVersion) {
canCreate = false;
}
const projectRoles = useProjectRole(projectId);
const hideMoreBtn = useMemo(
// 没有任何权限,或者存在版本信息,需要隐藏操作按钮
() => (projectRoles?.length ?? 0) === 0 || !!commitVersion,
[projectRoles, commitVersion],
);
const { workflowResource, pluginResource, dataResource, initLoaded } =
useResourceList();
return (
<div>
<ResourceFolderCoze
id={`${idPrefix}_${ProjectResourceGroupType.Workflow}`}
groupType={ProjectResourceGroupType.Workflow}
defaultResourceType={BizResourceTypeEnum.Workflow}
resourceTree={workflowResource}
canCreate={canCreate}
initLoaded={initLoaded}
onChangeName={changeNameWorkflow}
onCustomCreate={createWorkflow}
onDelete={deleteWorkflow}
onAction={handleWorkflowAction}
createResourceConfig={workflowCreateConfig}
iconRender={workflowIconRender}
hideMoreBtn={hideMoreBtn}
/>
<ResourceFolderCoze
id={`${idPrefix}_${ProjectResourceGroupType.Plugin}`}
groupType={ProjectResourceGroupType.Plugin}
defaultResourceType={BizResourceTypeEnum.Plugin}
resourceTree={pluginResource}
canCreate={canCreate}
initLoaded={initLoaded}
// 业务实现
onChangeName={changeNamePlugin}
onCustomCreate={createPlugin}
onDelete={deletePlugin}
onAction={handlePluginAction}
iconRender={() => <IconCozPlugin />}
hideMoreBtn={hideMoreBtn}
validateConfig={validatePluginConfig}
/>
<ResourceFolderCoze
id={`${idPrefix}_${ProjectResourceGroupType.Data}`}
groupType={ProjectResourceGroupType.Data}
resourceTree={dataResource}
canCreate={canCreate}
initLoaded={initLoaded}
createResourceConfig={dataCreateConfig}
onChangeName={changeNameData}
onDelete={deleteData}
onAction={handleDataAction}
onCustomCreate={createData}
hideMoreBtn={hideMoreBtn}
validateConfig={validateConfig}
iconRender={({ resource }) => {
console.log(resource);
if (resource.id === VARIABLE_RESOURCE_ID) {
return <IconCozVariables />;
}
if (resource.type === BizResourceTypeEnum.Database) {
return <IconCozDatabase />;
}
if (resource.type === BizResourceTypeEnum.Knowledge) {
return (
<div className="flex items-center">
{datasetIconMap[resource.biz_extend?.format_type]}
{/**
* 1: 启用
* 2: 删除,一般没有
* 3: 禁用
*/}
{resource.biz_res_status === 3 ? (
<span className="ml-[3px]"></span>
) : null}
</div>
);
}
return <></>;
}}
/>
{workflowModals}
{pluginModals}
{dataModals}
</div>
);
};

View File

@@ -0,0 +1,68 @@
/*
* 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 ResourceFolderProps,
type ResourceType,
} from '@coze-project-ide/framework';
import {
type BizResourceContextMenuBtnType,
type ResourceFolderCozeProps,
type BizResourceType,
} from '@coze-project-ide/biz-components';
export const useResourceActionsDemo = () => {
const onChangeName: ResourceFolderProps['onChangeName'] =
async changeNameEvent => {
await console.log('[ResourceFolder]on change name>>>', changeNameEvent);
};
const onDelete = async (resources: ResourceType[]) => {
await console.log('[ResourceFolder]on delete>>>', resources);
};
const onCreate: ResourceFolderCozeProps['onCreate'] = async (
createEvent,
subType,
) => {
await console.log('[ResourceFolder]on create>>>', createEvent, subType);
};
const onCustomCreate: ResourceFolderCozeProps['onCustomCreate'] = async (
resourceType,
subType,
) => {
await console.log(
'[ResourceFolder]on custom create>>>',
resourceType,
subType,
);
};
const onAction = (
action: BizResourceContextMenuBtnType,
resource?: BizResourceType,
) => {
console.log('on action>>>', action, resource);
};
return {
onChangeName,
onAction,
onDelete,
onCreate,
onCustomCreate,
};
};

View File

@@ -0,0 +1,196 @@
/*
* 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, { useState, useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import classnames from 'classnames';
import {
useSpaceId,
useProjectId,
useCommitVersion,
} from '@coze-project-ide/framework';
import {
useWorkflowResource,
ResourceRefTooltip,
} from '@coze-project-ide/biz-workflow';
import {
useResourceList,
usePrimarySidebarStore,
} from '@coze-project-ide/biz-components';
import { I18n } from '@coze-arch/i18n';
import {
WorkflowStorageType,
type DependencyTree,
} from '@coze-arch/bot-api/workflow_api';
import { workflowApi } from '@coze-arch/bot-api';
import { IconCozCross, IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Modal, Button, Tooltip, Loading, Typography } from '@coze-arch/coze-design';
import { ResourceContent } from './resource-content';
import s from './styles.module.less';
const { Text } = Typography;
const DEFAULT_DATA = {
node_list: [],
};
export const ResourceTreeModal = ({
modalVisible,
setModalVisible,
}: {
modalVisible: boolean;
setModalVisible: (v: boolean) => void;
}) => {
const { selectedResource } = usePrimarySidebarStore(
useShallow(store => ({
selectedResource: store.selectedResource,
})),
);
const { workflowResource } = useResourceList();
const [loading, setLoading] = useState(true);
const [data, setData] = useState<DependencyTree>(DEFAULT_DATA);
const [selectedWorkflowId, setSelectedWorkflowId] = useState(
selectedResource || workflowResource?.[0]?.id,
);
const spaceId = useSpaceId();
const projectId = useProjectId();
const { version } = useCommitVersion();
const { iconRender } = useWorkflowResource();
const handleClose = () => {
setModalVisible(false);
};
const handleSwitchWorkflow = (id: string) => {
setSelectedWorkflowId(id);
};
const getWorkflowDep = async (id: string) => {
setLoading(true);
const res = await workflowApi.DependencyTree({
type: WorkflowStorageType.Project,
project_info: {
workflow_id: id,
space_id: spaceId,
project_id: projectId,
draft: version ? false : true,
project_version: version ? version : undefined,
},
});
// 兼容先请求后返回场景
if (selectedWorkflowId === id) {
setData(res?.data || DEFAULT_DATA);
}
setLoading(false);
};
useEffect(() => {
if (selectedWorkflowId) {
getWorkflowDep(selectedWorkflowId);
}
}, [selectedWorkflowId]);
const handleRetry = () => {
getWorkflowDep(selectedWorkflowId);
};
return (
<Modal
className={s.modal}
visible={modalVisible}
type="dialog"
hasScroll={false}
>
<div className={s['modal-container']}>
<div className={s['workflow-list']}>
<div className={s['list-header-container']}>
{I18n.t('reference_graph_modal_title')}
<Tooltip theme="dark" content={<ResourceRefTooltip />}>
<IconCozInfoCircle color="secondary" fontSize={16} />
</Tooltip>
</div>
<div
style={{
height: 0,
flexGrow: 1,
}}
>
<div className={s['list-title']}>
{I18n.t(
'reference_graph_modal_subtitle_view_relationship_given_workflow',
)}
</div>
<div className={s.list}>
{workflowResource.map(workflow => (
<div
className={classnames(
s['list-item'],
selectedWorkflowId === workflow.id && s.selected,
)}
key={workflow.id}
onClick={() => handleSwitchWorkflow(workflow.id)}
>
{iconRender?.({ resource: workflow })}
<Text
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: {
position: 'right',
theme: 'dark',
},
},
}}
>
{workflow.name}
</Text>
</div>
))}
</div>
</div>
</div>
<div className={s['resource-tree-container']}>
{loading ? (
<div className={s['loading-container']}>
<Loading
loading
size="large"
color="default"
className={s.loading}
/>
</div>
) : (
<ResourceContent
data={data}
spaceId={spaceId}
projectId={projectId}
onRetry={handleRetry}
/>
)}
</div>
<Button
className={s['close-icon']}
color="secondary"
size="large"
onClick={handleClose}
>
<IconCozCross fontSize={20} />
</Button>
</div>
</Modal>
);
};

View File

@@ -0,0 +1,83 @@
/*
* 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 React, { useCallback } from 'react';
import { LinkNode } from '@coze-project-ide/biz-workflow';
import { ResourceTree, isDepEmpty } from '@coze-common/resource-tree';
import { I18n } from '@coze-arch/i18n';
import { type DependencyTree } from '@coze-arch/bot-api/workflow_api';
import {
IconCozIllusNone,
IconCozIllusError,
} from '@coze-arch/coze-design/illustrations';
import { IconCozRefresh } from '@coze-arch/coze-design/icons';
import { EmptyState } from '@coze-arch/coze-design';
import s from './styles.module.less';
export const ResourceContent = ({
data,
spaceId,
projectId,
onRetry,
}: {
data: DependencyTree;
spaceId: string;
projectId: string;
onRetry: () => void;
}) => {
const isEmpty = isDepEmpty(data);
const renderLinkNode = useCallback(
extraInfo => (
<LinkNode extraInfo={extraInfo} spaceId={spaceId} projectId={projectId} />
),
[spaceId, projectId],
);
if (isEmpty) {
return (
<EmptyState
size="full_screen"
icon={<IconCozIllusNone />}
title={I18n.t('reference_graph_tip_current_workflow_has_no_reference')}
/>
);
}
return (
<ErrorBoundary
fallback={
<EmptyState
size="full_screen"
icon={<IconCozIllusError />}
title={I18n.t('reference_graph_tip_fail_to_load')}
buttonProps={{
icon: <IconCozRefresh />,
color: 'primary',
}}
buttonText={I18n.t('reference_graph_tip_fail_to_load_retry_needed')}
onButtonClick={onRetry}
/>
}
>
<ResourceTree
className={s['resource-tree']}
data={data}
renderLinkNode={renderLinkNode}
/>
</ErrorBoundary>
);
};

View File

@@ -0,0 +1,153 @@
/* stylelint-disable declaration-no-important */
.modal {
:global .semi-modal{
width: calc(100vw - 80px) !important;
height: calc(100vh - 80px) !important;
margin: 40px !important;
}
:global .semi-modal-body-wrapper {
height: 100%;
margin: 0;
}
:global .semi-modal-content {
padding: 6px !important;
}
:global .semi-modal-body {
height: 100%;
}
:global .semi-modal-footer {
display: none;
}
}
.close-icon {
position: absolute;
z-index: 999;
top: 16px;
right: 16px;
width: 40px;
height: 40px;
background-color: var(--coz-bg-max) !important;
border: 1px solid var(--coz-stroke-primary) !important;
&:hover {
background-color: #F2F3F7 !important;
}
&:active {
background-color: #E9EBF2 !important;
}
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: var(--coz-bg-primary);
.loading {
color: var(--coz-fg-dim);
}
}
.workflow-list {
display: flex;
flex-direction: column;
width: 200px;
padding: 6px;
.list-header-container {
display: flex;
column-gap: 4px;
align-items: center;
margin: 8px 0 12px 12px;
font-size: 20px;
font-weight: 500;
line-height: 28px;
}
.list-title {
margin-bottom: 4px;
padding: 4.5px 12px;
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-secondary);
}
.list {
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 4px;
justify-content: flex-start;
height: 100%;
.list-item {
cursor: pointer;
display: flex;
column-gap: 4px;
align-items: center;
width: 100%;
height: 28px;
padding: 4px 8px 4px 12px;
border-radius: 4px;
:global svg {
flex-shrink: 0;
}
:global .semi-typography {
font-weight: 500;
}
&:hover {
background-color: var(--coz-mg-secondary-hovered);
}
&:active {
background-color: var(--coz-mg-primary);
}
}
.selected {
background-color: var(--coz-mg-primary);
}
}
}
.modal-container {
display: flex;
width: 100%;
height: 100%;
}
.resource-tree-container {
position: relative;
width: 100%;
border: 1px solid var(--coz-stroke-primary);
border-radius: 8px;
.resource-tree {
flex-grow: 1;
width: 100%;
border: 1px solid var(--coz-stroke-primary);
border-radius: 8px;
}
}

View File

@@ -0,0 +1,136 @@
/*
* 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 { useLocation } from 'react-router-dom';
import React, {
useCallback,
useEffect,
useRef,
useState,
useLayoutEffect,
} from 'react';
import {
type TabBarToolbar,
useCurrentWidget,
useProjectIDEServices,
useSplitScreenArea,
} from '@coze-project-ide/framework';
import { usePrimarySidebarStore } from '@coze-project-ide/biz-components';
import { IconCozSideExpand } from '@coze-arch/coze-design/icons';
import { IconButton, Popover } from '@coze-arch/coze-design';
import { PrimarySidebar } from '../primary-sidebar';
import styles from './styles.module.less';
export const SidebarExpand = () => {
const projectIDEServices = useProjectIDEServices();
const currentWidget = useCurrentWidget<TabBarToolbar>();
const direction = useSplitScreenArea(
currentWidget.currentURI,
currentWidget.tabBar,
);
const { pathname } = useLocation();
const [visible, setVisible] = useState(
projectIDEServices.view.primarySidebar.getVisible(),
);
const canClosePopover = usePrimarySidebarStore(
state => state.canClosePopover,
);
const [popoverVisible, setPopoverVisible] = useState(false);
const leaveTimer = useRef<ReturnType<typeof setTimeout>>();
const mouseLeaveRef = useRef<boolean>();
const handleMouseEnter = () => {
setPopoverVisible(true);
clearTimeout(leaveTimer.current);
mouseLeaveRef.current = false;
};
const handleMouseLeave = () => {
mouseLeaveRef.current = true;
if (!canClosePopover) {
return;
}
leaveTimer.current = setTimeout(() => {
setPopoverVisible(false);
}, 100);
};
useEffect(() => {
if (canClosePopover && mouseLeaveRef.current) {
setPopoverVisible(false);
}
}, [canClosePopover]);
useLayoutEffect(() => {
setVisible(projectIDEServices.view.primarySidebar.getVisible());
}, [pathname]);
useEffect(() => {
// 侧边栏显隐状态切换时,更新按钮状态
const disposable = projectIDEServices.view.onSidebarVisibleChange(vis => {
setVisible(vis);
});
return () => {
disposable.dispose();
};
}, []);
const handleExpand = useCallback(() => {
projectIDEServices.view.primarySidebar.changeVisible(true);
setPopoverVisible(false);
}, []);
// 右边分屏不展示 hover icon
if (direction === 'right') {
return null;
}
return visible ? null : (
<Popover
motion={false}
visible={popoverVisible}
trigger="custom"
zIndex={1000}
style={{
background: 'transparent',
border: 'none',
boxShadow: 'none',
padding: 0,
}}
content={
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={styles['sidebar-wrapper']}
>
<PrimarySidebar hideExpand idPrefix={'popover-sidebar'} />
</div>
}
>
<IconButton
className={styles['icon-button']}
icon={<IconCozSideExpand style={{ rotate: '180deg' }} />}
color="secondary"
onClick={handleExpand}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
</Popover>
);
};

View File

@@ -0,0 +1,21 @@
.icon-button {
margin-right: 6px;
&&& {
background-color: var(--coz-bg-max);
&:hover {
background-color: var(--coz-bg-6);
}
&:active {
background-color: var(--coz-bg-8);
}
}
}
.sidebar-wrapper {
width: 282px;
height: calc(100vh - 114px);
}

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 React, { useState, useEffect, useMemo } from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozExpand, IconCozMinimize } from '@coze-arch/coze-design/icons';
import { IconButton, Tooltip } from '@coze-arch/coze-design';
import {
type TabBarToolbar,
useCurrentWidget,
useProjectIDEServices,
useSplitScreenArea,
Command,
useShortcuts,
} from '@coze-project-ide/framework';
import s from './styles.module.less';
export const FullScreenButton = () => {
const projectIDEServices = useProjectIDEServices();
const currentWidget = useCurrentWidget<TabBarToolbar>();
const { keybinding } = useShortcuts(Command.Default.VIEW_FULL_SCREEN);
const direction = useSplitScreenArea(
currentWidget.currentURI,
currentWidget.tabBar,
);
const [tooltipVisible, setTooltipVisible] = useState(false);
const [fullScreen, setFullScreen] = useState(
projectIDEServices.view.isFullScreenMode,
);
useEffect(() => {
const disposable = projectIDEServices.view.onFullScreenModeChange(
isFullScreen => {
setFullScreen(isFullScreen);
},
);
return () => {
disposable.dispose();
};
}, []);
const icon = useMemo(() => {
if (fullScreen) {
return <IconCozMinimize />;
} else {
return <IconCozExpand />;
}
}, [fullScreen]);
const content = useMemo(
() => (
<div className={s.shortcut}>
<div className={s.label}>
{fullScreen
? I18n.t('project_ide_restore')
: I18n.t('project_ide_maximize')}
</div>
<div className={s.keybinding}>{keybinding}</div>
</div>
),
[fullScreen, keybinding],
);
// 左边分屏不展示全屏按钮
if (direction === 'left') {
return null;
}
const handleSwitchFullScreen = () => {
projectIDEServices.view.switchFullScreenMode();
setTooltipVisible(false);
};
return (
<Tooltip
content={content}
position="bottom"
// 点击后布局变化tooltip 需要手动控制消失
trigger="custom"
visible={tooltipVisible}
>
<IconButton
className={s['icon-button']}
icon={icon}
color="secondary"
onClick={handleSwitchFullScreen}
onMouseOver={() => setTooltipVisible(true)}
onMouseOut={() => setTooltipVisible(false)}
/>
</Tooltip>
);
};

View File

@@ -0,0 +1,22 @@
.full-screen-button {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--coz-stroke-primary);
}
.shortcut {
display: flex;
align-items: center;
justify-content: space-between;
.label {
font-size: 14px;
line-height: 20px;
}
.keybinding {
font-weight: 700;
color: var(--coz-fg-dim);
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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 { type ProjectIDEWidget } from '@coze-project-ide/framework';
import { ReloadButton } from './reload-button';
import { FullScreenButton } from './full-screen-button';
export const ToolBar = ({ widget }: { widget: ProjectIDEWidget }) => (
<div style={{ display: 'flex' }}>
<ReloadButton widget={widget} />
<FullScreenButton />
</div>
);

View File

@@ -0,0 +1,68 @@
/*
* 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, { useState, useMemo } from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozRefresh } from '@coze-arch/coze-design/icons';
import { IconButton, Tooltip } from '@coze-arch/coze-design';
import {
CustomCommand,
useShortcuts,
type ProjectIDEWidget,
} from '@coze-project-ide/framework';
import s from '../full-screen-button/styles.module.less';
export const ReloadButton = ({ widget }: { widget: ProjectIDEWidget }) => {
const { keybinding } = useShortcuts(CustomCommand.RELOAD);
const [tooltipVisible, setTooltipVisible] = useState(false);
const content = useMemo(
() => (
<div className={s.shortcut}>
<div className={s.label}>{I18n.t('refresh_project_tags')}</div>
<div className={s.keybinding}>{keybinding}</div>
</div>
),
[keybinding],
);
const handleReload = () => {
widget.refresh();
widget.context.widget.setUIState('loading');
};
return (
<Tooltip
content={content}
position="bottom"
// 点击后布局变化tooltip 需要手动控制消失
trigger="custom"
visible={tooltipVisible}
>
<IconButton
className={s['icon-button']}
icon={<IconCozRefresh />}
color="secondary"
onClick={handleReload}
onMouseOver={() => setTooltipVisible(true)}
onMouseOut={() => setTooltipVisible(false)}
/>
</Tooltip>
);
};

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 { useNavigate } from 'react-router-dom';
import React, { useCallback } from 'react';
import { IconCozArrowLeft } from '@coze-arch/coze-design/icons';
import { IconButton } from '@coze-arch/coze-design';
import { useSpaceId } from '@coze-project-ide/framework';
export const GoBackButton: React.FC = () => {
const navigate = useNavigate();
const spaceId = useSpaceId();
const handleGoBack = useCallback(() => {
navigate(`/space/${spaceId}/develop`);
}, [spaceId, navigate]);
return (
<IconButton
color="secondary"
icon={<IconCozArrowLeft />}
onClick={handleGoBack}
/>
);
};

View File

@@ -0,0 +1,46 @@
/*
* 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 { Row, Col } from '@coze-arch/coze-design';
import { ModeTab } from '@coze-project-ide/ui-adapter';
import { ProjectInfo } from './project-info';
import { Operators } from './operators';
import { GoBackButton } from './go-back-button';
import styles from './styles.module.less';
export const TopBar = () => (
<div className={styles.container}>
<Row className={styles['top-bar']}>
<Col span={8} className={styles['left-col']}>
{/* 返回按钮 */}
<GoBackButton />
{/* 项目标题 */}
<ProjectInfo />
</Col>
{/* 海外版暂时不上 uibuilder 切换功能 */}
<Col span={8} className={styles['middle-col']}>
{IS_OVERSEA ? null : <ModeTab />}
</Col>
<Col span={8} className={styles['right-col']}>
<Operators />
</Col>
</Row>
</div>
);

View File

@@ -0,0 +1,172 @@
/*
* 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 { useNavigate } from 'react-router-dom';
import React, { type PropsWithChildren, useCallback } from 'react';
import { PublishButton } from '@coze-studio/project-publish';
import {
useCopyProjectModal,
useDeleteIntelligence,
} from '@coze-studio/project-entity-adapter';
import { CollapsibleIconButtonGroup } from '@coze-studio/components/collapsible-icon-button';
import { LeftContentButtons } from '@coze-project-ide/ui-adapter';
import {
useProjectId,
useSpaceId,
useCommitVersion,
} from '@coze-project-ide/framework';
import {
useProjectAuth,
EProjectPermission,
useProjectRole,
} from '@coze-common/auth';
import { I18n } from '@coze-arch/i18n';
import { IconCozEye, IconCozMore } from '@coze-arch/coze-design/icons';
import {
Button,
IconButton,
Divider,
Popover,
Menu,
Toast,
Tooltip,
Tag,
} from '@coze-arch/coze-design';
import { useProjectInfo } from '../../../hooks';
import { MonetizeConfig } from './monetize';
export const Operators = () => {
const navigate = useNavigate();
const spaceId = useSpaceId();
const projectId = useProjectId();
const { version } = useCommitVersion();
const { modalContextHolder: modalDelete, deleteIntelligence } =
useDeleteIntelligence({
onDeleteProjectSuccess: () => {
Toast.success(I18n.t('project_ide_toast_delete_success'));
navigate(`/space/${spaceId}/develop`);
},
});
const { modalContextHolder, openModal } = useCopyProjectModal({
onSuccess: () => navigate(`/space/${spaceId}/develop`),
});
const { projectInfo, initialValue } = useProjectInfo();
const projectRoles = useProjectRole(projectId);
const handleCopy = useCallback(() => {
openModal({
initialValue: {
...initialValue,
to_space_id: spaceId,
},
});
}, [initialValue, spaceId]);
const handleDelete = useCallback(() => {
deleteIntelligence({
name: initialValue.name || '',
projectId: projectInfo?.id || '',
});
}, [initialValue, projectInfo]);
const canDelete = useProjectAuth(
EProjectPermission.DELETE,
projectId,
spaceId,
);
return version ? (
<Tag prefixIcon={<IconCozEye />}>{I18n.t('app_ide_viewing_archive')}</Tag>
) : (
<div className="flex items-center justify-end grow gap-[8px] overflow-hidden">
{modalContextHolder}
{projectRoles.length ? (
<>
{modalDelete}
<LeftContent>
<LeftContentButtons />
{IS_OVERSEA ? <MonetizeConfig /> : null}
</LeftContent>
<Divider layout="vertical" className="first:hidden" />
<PublishButton
spaceId={spaceId}
projectId={projectId}
hasPublished={Boolean(Number(projectInfo?.publish_time))}
/>
<Popover
trigger="click"
className="rounded-[8px]"
content={
<Menu>
<Menu.Item
className="min-w-[190px] h-[32px] rounded-[4px]"
onClick={handleCopy}
>
{I18n.t('project_ide_duplicate')}
</Menu.Item>
{/* Tooltip disableFocusListener 失效,等待后续修复完成 */}
{canDelete ? (
<Menu.Item
className="min-w-[190px] h-[32px] rounded-[4px]"
onClick={handleDelete}
>
{I18n.t('project_ide_delete_project')}
</Menu.Item>
) : (
<Tooltip
position="left"
content={I18n.t('project_delete_permission_tooltips')}
>
<Menu.Item
disabled={true}
className="min-w-[190px] h-[32px] rounded-[4px]"
onClick={handleDelete}
>
{I18n.t('project_ide_delete_project')}
</Menu.Item>
</Tooltip>
)}
</Menu>
}
>
<IconButton icon={<IconCozMore />} color="secondary" />
</Popover>
</>
) : (
<Button onClick={handleCopy}>
{I18n.t('project_ide_create_duplicate')}
</Button>
)}
</div>
);
};
/**
* 为了给左侧一个容器用于计算整体宽度以便实现宽度不够时自动隐藏付费配置文案
* 同时还要兼顾左侧没有任何内容时自动隐藏 divider 的 first:hidden 写法
*/
function LeftContent({ children }: PropsWithChildren) {
return IS_OVERSEA ? (
<CollapsibleIconButtonGroup gap={8}>{children}</CollapsibleIconButtonGroup>
) : (
children
);
}

View File

@@ -0,0 +1,98 @@
/*
* 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, { useState } from 'react';
import { useRequest } from 'ahooks';
import {
MonetizeConfigPanel,
type MonetizeConfigValue,
} from '@coze-studio/components/monetize';
import { CollapsibleIconButton } from '@coze-studio/components/collapsible-icon-button';
import { I18n } from '@coze-arch/i18n';
import { IconCozWallet } from '@coze-arch/coze-design/icons';
import { Popover } from '@coze-arch/coze-design';
import {
BotMonetizationRefreshPeriod,
MonetizationEntityType,
} from '@coze-arch/bot-api/benefit';
import { benefitApi } from '@coze-arch/bot-api';
import { ProjectRoleType, useProjectRole } from '@coze-common/auth';
import { useProjectId } from '@coze-project-ide/framework';
export function MonetizeConfig() {
const projectId = useProjectId();
const myRoles = useProjectRole(projectId);
const [monetizeConfig, setMonetizeConfig] = useState<MonetizeConfigValue>({
isOn: true,
freeCount: 0,
refreshCycle: BotMonetizationRefreshPeriod.Never,
});
const { data, loading } = useRequest(
() =>
benefitApi.PublicGetBotMonetizationConfig({
entity_id: projectId,
entity_type: MonetizationEntityType.Project,
}),
{
onSuccess: res => {
setMonetizeConfig({
isOn: res.data?.is_enable ?? true,
freeCount: res.data?.free_chat_allowance_count ?? 0,
refreshCycle:
res.data?.refresh_period ?? BotMonetizationRefreshPeriod.Never,
});
},
},
);
/** loading 时展示为激活态(默认值) */
const btnDisplayOn = loading ? true : monetizeConfig.isOn;
return (
<Popover
key={loading || !data?.data ? 'custom' : 'click'}
trigger={loading || !data?.data ? 'custom' : 'click'}
autoAdjustOverflow={true}
content={
<MonetizeConfigPanel
disabled={!myRoles.includes(ProjectRoleType.Owner)}
value={monetizeConfig}
onChange={setMonetizeConfig}
onDebouncedChange={val => {
benefitApi.PublicSaveBotDraftMonetizationConfig({
entity_id: projectId,
entity_type: MonetizationEntityType.Project,
is_enable: val.isOn,
free_chat_allowance_count: val.freeCount,
refresh_period: val.refreshCycle,
});
}}
/>
}
>
<CollapsibleIconButton
itemKey={Symbol.for('monetize-btn')}
icon={<IconCozWallet className="text-[16px]" />}
text={
btnDisplayOn ? I18n.t('monetization_on') : I18n.t('monetization_off')
}
color={btnDisplayOn ? 'highlight' : 'secondary'}
/>
</Popover>
);
}

View File

@@ -0,0 +1,133 @@
/*
* 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, { useCallback } from 'react';
import { I18n } from '@coze-arch/i18n';
import { useProjectAuth, EProjectPermission } from '@coze-common/auth';
import { useUpdateProjectModal } from '@coze-studio/project-entity-adapter';
import {
useSpaceId,
useProjectId,
useCommitVersion,
} from '@coze-project-ide/framework';
import {
IconCozEdit,
IconCozCheckMarkCircleFillPalette,
} from '@coze-arch/coze-design/icons';
import {
CozAvatar,
Typography,
Skeleton,
IconButton,
Toast,
Popover,
} from '@coze-arch/coze-design';
import { useProjectInfo } from '../../../hooks';
import { InfoContent } from './info-content';
import styles from './styles.module.less';
const { Title: COZTitle } = Typography;
export const ProjectInfo = () => {
const {
loading,
initialValue,
projectInfo,
updateProjectInfo,
publishInfo,
ownerInfo,
} = useProjectInfo();
const spaceId = useSpaceId();
const projectId = useProjectId();
const { version } = useCommitVersion();
const { modalContextHolder, openModal } = useUpdateProjectModal({
onSuccess: () => {
updateProjectInfo();
// 更新 info 信息
Toast.success(I18n.t('project_ide_toast_edit_success'));
},
});
const canAuthEdit = useProjectAuth(
EProjectPermission.EDIT_INFO,
projectId || '',
spaceId || '',
);
/**
* 可编辑判断:
* 1. 有编辑权限
* 2. 非预览态
*/
const canEdit = canAuthEdit && !version;
// 打开 project 编辑弹窗
const handleEditProject = useCallback(() => {
openModal({
initialValue,
});
}, [initialValue]);
const hasPublished = publishInfo?.has_published;
return loading ? (
<Skeleton.Title style={{ width: 24, height: 24 }} />
) : (
<div className={styles['project-info']}>
<Popover
content={
<InfoContent
projectInfo={projectInfo}
publishInfo={publishInfo}
ownerInfo={ownerInfo}
spaceId={spaceId}
/>
}
>
<CozAvatar type="bot" size="small" src={projectInfo?.icon_url} />
{hasPublished ? (
<div className={styles['check-icon']}>
<IconCozCheckMarkCircleFillPalette color="green" />
</div>
) : null}
</Popover>
<COZTitle
ellipsis={{
showTooltip: {
opts: { content: projectInfo?.name },
},
}}
className={styles.title}
fontSize="16px"
style={{ maxWidth: 320 }}
>
{projectInfo?.name}
</COZTitle>
{/* 权限判断 */}
{canEdit ? (
<IconButton
color="secondary"
icon={<IconCozEdit />}
onClick={handleEditProject}
/>
) : null}
{modalContextHolder}
</div>
);
};

View File

@@ -0,0 +1,97 @@
/*
* 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 dayjs from 'dayjs';
import {
type IntelligenceBasicInfo,
type IntelligencePublishInfo,
} from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { useSpace } from '@coze-arch/foundation-sdk';
import { IconCozCheckMarkCircleFillPalette } from '@coze-arch/coze-design/icons';
import { CozAvatar, Tag } from '@coze-arch/coze-design';
import { type User } from '@coze-arch/bot-api/intelligence_api';
import styles from './styles.module.less';
const formatTime = (time?: string) => {
const timeNumber = Number(time);
if (isNaN(timeNumber)) {
return '-';
}
return dayjs.unix(timeNumber).format('YYYY-MM-DD HH:mm:ss');
};
export const InfoContent = ({
spaceId,
projectInfo,
publishInfo,
ownerInfo,
}: {
spaceId: string;
projectInfo?: IntelligenceBasicInfo;
publishInfo?: IntelligencePublishInfo;
ownerInfo?: User;
}) => {
const space = useSpace(spaceId);
if (!projectInfo) {
return null;
}
const createTime = formatTime(projectInfo?.create_time);
return (
<div className={styles.content}>
<CozAvatar type="bot" size="xl" src={projectInfo?.icon_url} />
<div className={styles.title}>{projectInfo?.name}</div>
<div className={styles.description}>{projectInfo?.description}</div>
<div className={styles['tag-container']}>
{space ? (
<Tag
className={styles.tag}
color="primary"
prefixIcon={<CozAvatar size="mini" src={space.icon_url} />}
>
{space.name}
</Tag>
) : null}
{publishInfo?.has_published ? (
<Tag
className={styles.tag}
color="green"
prefixIcon={<IconCozCheckMarkCircleFillPalette />}
>
{I18n.t('Published_1')}
</Tag>
) : null}
</div>
{ownerInfo ? (
<div className={styles['owner-container']}>
<CozAvatar size="micro" src={ownerInfo?.avatar_url} />
<div>{ownerInfo?.nickname}</div>
<div>@{ownerInfo?.user_unique_name}</div>
</div>
) : null}
<div className={styles.time}>
{I18n.t('project_ide_info_created_on', {
time: createTime,
})}
</div>
</div>
);
};

View File

@@ -0,0 +1,100 @@
.project-info {
display: flex;
align-items: center;
.title {
margin: 0 4px 0 8px;
font-size: 16px;
font-weight: 500;
line-height: 22px;
color: var(--coz-fg-plus);
}
.check-icon {
position: absolute;
top: 31px;
left: 54px;
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
background-color: white;
border-radius: 50%;
}
}
.icon {
&&& {
background-color: var(--coz-bg-plus);
&:hover {
background-color: var(--coz-bg-6);
}
&:active {
background-color: var(--coz-bg-8);
}
}
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 252px;
max-width: 320px;
padding: 16px;
.title{
margin: 16px 0 2px;
font-size: 16px;
font-weight: 500;
line-height: 22px;
word-break: break-all;
}
.description{
font-size: 14px;
line-height: 20px;
word-break: break-all;
}
.tag-container {
display: flex;
column-gap: 4px;
margin-top: 8px;
font-weight: 500;
.tag {
padding: 2px 6px;
}
}
.owner-container {
display: flex;
column-gap: 4px;
align-items: center;
margin-top: 24px;
font-size: 12px;
color: var(--coz-fg-secondary);
}
.time {
margin-top: 6px;
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-secondary);
word-break: break-all;
}
}

View File

@@ -0,0 +1,49 @@
.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
height: fit-content;
.banner {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 36px;
}
}
.top-bar {
display: flex;
align-items: center;
width: 100%;
height: 56px;
.left-col {
display: flex;
column-gap: 4px;
align-items: center;
height: 100%;
padding-left: 4px;
}
.middle-col {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.right-col {
display: flex;
column-gap: 8px;
align-items: center;
justify-content: flex-end;
height: 100%;
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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 { UIBuilder as RawUIBuilder } from '@coze-project-ide/ui-adapter';
import { useProjectInfo } from '../../hooks';
export const UIBuilder = () => {
const { projectInfo } = useProjectInfo();
return <RawUIBuilder projectInfo={projectInfo} />;
};

View File

@@ -0,0 +1,162 @@
/*
* 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, { useCallback } from 'react';
import {
useIDEService,
ShortcutsService,
CommandRegistry,
Command,
} from '@coze-project-ide/framework';
import { I18n } from '@coze-arch/i18n';
import { IconCozDocument } from '@coze-arch/coze-design/icons';
import { Image, Button } from '@coze-arch/coze-design';
import EnWorkflowFrame from '@/assets/en-workflow-frame.png';
import EnUIBuilderFrame from '@/assets/en-ui-builder-frame.png';
import EnKnowledgeFrame from '@/assets/en-knowledge-frame.png';
import CnWorkflowFrame from '@/assets/cn-workflow-frame.png';
import CnUIBuilderFrame from '@/assets/cn-ui-builder-frame.png';
import CnKnowledgeFrame from '@/assets/cn-knowledge-frame.png';
import { FullScreenButton } from '../toolbar/full-screen-button';
import { SidebarExpand } from '../sidebar-expand';
import { ShortcutItem } from './shortcut-item';
import styles from './styles.module.less';
// coze 快捷键需要绑定 starling 文案。没有绑定文案的暂时不展示
// 避免添加快捷键导致新增误展示
const SHOW_SHORTCUTS: string[] = [
Command.Default.VIEW_CLOSE_ALL_WIDGET,
Command.Default.VIEW_CLOSE_CURRENT_WIDGET,
Command.Default.VIEW_CLOSE_OTHER_WIDGET,
];
export const WidgetDefaultRenderer = () => {
const shortcutsService = useIDEService<ShortcutsService>(ShortcutsService);
const commandRegistry = useIDEService<CommandRegistry>(CommandRegistry);
const shortcutsList = shortcutsService.shortcutsHandlers
.filter(shortcut => SHOW_SHORTCUTS.includes(shortcut.commandId))
.map(shortcut => ({
key: shortcut.commandId,
label:
commandRegistry.getCommand(shortcut.commandId)?.label ||
shortcut.commandId,
keybinding: shortcutsService.getShortcutByCommandId(shortcut.commandId),
}));
const handleWorkflowDoc = useCallback(() => {
window.open('/docs/guides/build_project_in_projectide');
}, []);
const handleUIBuilderDoc = useCallback(() => {
window.open('/docs/guides/build_ui_interface');
}, []);
const handleDatabaseDoc = useCallback(() => {
window.open('/docs/guides/add_resources_to_project');
}, []);
return (
<div className={styles['default-container']}>
<div className={styles['icon-expand']}>
<SidebarExpand />
</div>
<div className={styles['full-screen']}>
<FullScreenButton />
</div>
<div className={styles.title}>{I18n.t('project_ide_welcome_title')}</div>
<div className={styles['sub-title']}>
{I18n.t('project_ide_welcome_describe')}
</div>
<div className={styles.gallery}>
<div className={styles['gallery-block']}>
<Image
preview={false}
src={IS_OVERSEA ? EnWorkflowFrame : CnWorkflowFrame}
width={320}
height={160}
/>
<div className={styles['gallery-title']}>
{I18n.t('project_ide_welcome_workflow_title')}
</div>
<div className={styles['gallery-description']}>
{I18n.t('project_ide_welcome_workflow_describe')}
</div>
<Button
className={styles['doc-search']}
icon={<IconCozDocument />}
color="primary"
onClick={handleWorkflowDoc}
>
{I18n.t('project_ide_view_document')}
</Button>
</div>
{IS_OVERSEA || IS_OPEN_SOURCE ? null : (
<div className={styles['gallery-block']}>
<Image
preview={false}
src={IS_OVERSEA ? EnUIBuilderFrame : CnUIBuilderFrame}
width={320}
height={160}
/>
<div className={styles['gallery-title']}>
{I18n.t('project_ide_welcome_ui_builder_title')}
</div>
<div className={styles['gallery-description']}>
{I18n.t('project_ide_welcome_ui_builder_describe')}
</div>
<Button
className={styles['doc-search']}
icon={<IconCozDocument />}
color="primary"
onClick={handleUIBuilderDoc}
>
{I18n.t('project_ide_view_document')}
</Button>
</div>
)}
<div className={styles['gallery-block']}>
<Image
preview={false}
src={IS_OVERSEA ? EnKnowledgeFrame : CnKnowledgeFrame}
width={320}
height={160}
/>
<div className={styles['gallery-title']}>
{I18n.t('project_ide_welcome_db_title')}
</div>
<div className={styles['gallery-description']}>
{I18n.t('project_ide_welcome_db_describ')}
</div>
<Button
className={styles['doc-search']}
icon={<IconCozDocument />}
color="primary"
onClick={handleDatabaseDoc}
>
{I18n.t('project_ide_view_document')}
</Button>
</div>
</div>
<div className={styles['shortcuts-list']}>
{shortcutsList.map(item => (
<ShortcutItem key={item.key} item={item} />
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,46 @@
/*
* 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 styles from './styles.module.less';
export const ShortcutItem = ({
item,
}: {
item: {
key: string;
label: string;
keybinding: string[][];
};
}) => {
const { key, label, keybinding } = item;
return (
<div className={styles['shortcut-item']} key={key}>
<div className={styles.label}>{label}</div>
<div className={styles.keybinding}>
{keybinding.map(bindings =>
bindings.map(binding => (
<div key={binding} className={styles['keybinding-block']}>
{binding}
</div>
)),
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,35 @@
.shortcut-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
.label {
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
.keybinding {
display: flex;
.keybinding-block {
display: flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
margin-left: 6px;
font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 9px;
font-weight: 800;
color: var(--coz-fg-secondary);
background: var(--coz-mg-primary);
border: 0.5px solid var(--coz-stroke-primary);
border-radius: 4px;
}
}
}

View File

@@ -0,0 +1,126 @@
.default-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 3%);
img {
pointer-events: none;
}
.title {
font-size: 28px;
font-weight: 700;
line-height: 36px;
}
.sub-title {
margin-bottom: 40px;
font-size: 14px;
line-height: 20px;
color: var(--coz-fg-secondary, rgba(6, 7, 9, 50%));
}
.gallery {
display: flex;
gap: 12px;
justify-content: center;
width: 100%;
.gallery-block {
cursor: pointer;
display: flex;
flex-direction: column;
width: 320px;
.gallery-image {
flex-shrink: 0;
border: 1px solid var(--coz-stroke-primary);
border-radius: 12px;
}
.gallery-title {
margin: 12px 0 8px;
font-size: 16px;
font-weight: 500;
font-weight: 22px;
}
.gallery-description {
font-size: 14px;
font-weight: 20px;
color: var(--coz-fg-secondary, rgba(6, 7, 9, 5%));
}
.doc-search {
margin-top: 12px;
visibility: hidden;
}
}
}
.gallery-block:hover > .doc-search {
visibility: visible;
}
.item {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
width: 320px;
height: 48px;
margin-bottom: 16px;
padding: 8px 12px 8px 8px;
background-color: var(--coz-bg-max);
border: 1px solid var(--coz-stroke-primary);
border-radius: 8px;
&:hover {
background-color: var(--coz-bg-6);
}
&:active {
background-color: var(--coz-bg-8);
}
.item-pre {
display: flex;
align-items: center;
}
.item-text {
margin-left: 8px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
}
.icon-expand {
position: absolute;
top: 6px;
left: 8px;
}
.full-screen {
position: absolute;
top: 6px;
right: 8px;
}
.shortcuts-list {
display: flex;
flex-direction: column;
width: 320px;
margin-top: 48px;
}
}

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 React from 'react';
import { type ReactWidget } from '@coze-project-ide/framework';
interface WidgetFallbackProps {
widget: ReactWidget;
}
export const WidgetFallback: React.FC<WidgetFallbackProps> = ({ widget }) => (
<div>Widget error: {widget.id}</div>
);

View File

@@ -0,0 +1,118 @@
/*
* 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, { useMemo, useState, useCallback } from 'react';
import cls from 'classnames';
import {
IconCozCrossFill,
IconCozWarningCircleFill,
} from '@coze-arch/coze-design/icons';
import { Typography, Loading, Skeleton } from '@coze-arch/coze-design';
import {
type TitlePropsType,
DISABLE_HANDLE_EVENT,
Command,
type ProjectIDEWidget,
} from '@coze-project-ide/framework';
import styles from './styles.module.less';
export const WidgetTitle: React.FC<TitlePropsType> = ({
commandRegistry,
title,
widget,
uiState,
registry,
}) => {
const [tabHovered, setTabHovered] = useState(false);
const renderIcon = useMemo(() => {
if (!registry?.renderIcon || typeof registry?.renderIcon !== 'function') {
return null;
}
return registry.renderIcon((widget as ProjectIDEWidget).context);
}, [registry]);
const renderTitle = useMemo(() => {
if (tabHovered) {
return (
<IconCozCrossFill
className="coz-fg-secondary"
style={{ fontSize: 16 }}
onClick={e => {
e.stopPropagation();
commandRegistry.executeCommand(
Command.Default.VIEW_SAVING_WIDGET_CLOSE_CONFIRM,
[widget?.title],
);
}}
/>
);
}
// 没有标题还在骨架屏阶段
if (!title) {
return null;
} else if (uiState === 'saving') {
return <Loading size="mini" loading={true} />;
} else if (uiState === 'error') {
return <IconCozWarningCircleFill className="text-lg coz-fg-hglt-red" />;
}
return null;
}, [uiState, widget, tabHovered, commandRegistry, title]);
const handleTabHover = useCallback(() => {
setTabHovered(true);
}, []);
const handleTabBlur = useCallback(() => {
setTabHovered(false);
}, []);
return (
<div
className={styles['title-container']}
onMouseOver={handleTabHover}
onMouseLeave={handleTabBlur}
>
{uiState === 'loading' || !title ? (
<div className={styles['widget-title']}>
<div className={styles['title-label']}>
<Skeleton.Title style={{ width: '100px' }} />
</div>
<div className={cls(styles['close-icon'], DISABLE_HANDLE_EVENT)}>
{renderTitle}
</div>
</div>
) : (
<div className={styles['widget-title']}>
<div className={styles['title-label']}>
<div className={styles['label-icon']}>{renderIcon}</div>
<div className={styles['label-text']}>
<Typography.Text ellipsis={{ showTooltip: true }}>
{title}
</Typography.Text>
</div>
</div>
<div className={cls(styles['close-icon'], DISABLE_HANDLE_EVENT)}>
{renderTitle}
</div>
</div>
)}
</div>
);
};
export const widgetTitleRender = props => <WidgetTitle {...props} />;

View File

@@ -0,0 +1,43 @@
.title-container {
margin: 0 8px;
}
.widget-title {
display: flex;
align-items: center;
justify-content: space-between;
:global {
.semi-spin.coz-loading-wrapper {
line-height: 0;
}
}
}
.title-label {
display: flex;
flex-grow: 1;
flex-shrink: 1;
column-gap: 8px;
align-items: center;
width: 0;
}
.label-icon {
display: flex;
align-items: center;
}
.label-text {
flex-grow: 1;
flex-shrink: 1;
width: 0;
line-height: 20px;
}
.close-icon {
cursor: pointer;
display: flex;
align-items: center;
}