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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,18 @@
/*
* 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 { WorkflowTooltip } from './workflow-tooltip';
export { WorkflowWidgetIcon } from './workflow-widget-icon';

View File

@@ -0,0 +1,4 @@
.image {
width: 160px;
border-radius: var(--coze-8);
}

View File

@@ -0,0 +1,84 @@
/*
* 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, { type FC, useMemo } from 'react';
import { I18n } from '@coze-arch/i18n';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
import { Image, useTheme } from '@coze-arch/coze-design';
import workflowLightImg from './assets/workflow-light.jpg';
import workflowDarkImg from './assets/workflow-dark.jpg';
import chatflowLightImg from './assets/chatflow-light.jpg';
import chatflowDarkImg from './assets/chatflow-dark.jpg';
export interface WorkflowTooltipProps {
flowMode: WorkflowMode;
}
const ILLUSTRATION_IMG_URL = {
workflow: {
dark: workflowDarkImg,
light: workflowLightImg,
},
chatflow: {
dark: chatflowDarkImg,
light: chatflowLightImg,
},
};
export const WorkflowTooltip: FC<WorkflowTooltipProps> = ({ flowMode }) => {
const { theme } = useTheme();
const imgUrl = useMemo(() => {
switch (flowMode) {
case WorkflowMode.ChatFlow:
return (
ILLUSTRATION_IMG_URL.chatflow[theme] ||
ILLUSTRATION_IMG_URL.chatflow.light
);
default:
return (
ILLUSTRATION_IMG_URL.workflow[theme] ||
ILLUSTRATION_IMG_URL.workflow.light
);
}
}, [theme, flowMode]);
return (
<div className="flex flex-col gap-1">
<Image
src={imgUrl}
crossOrigin="anonymous"
imgStyle={{
width: 200,
minHeight: 120,
borderRadius: '7.5px',
border: '1px solid var(--coz-stroke-primary)',
}}
preview={false}
/>
<div className="px-2 pt-1 pb-2">
<p className="text-14 font-medium coz-fg-primary leading-5">
{flowMode === WorkflowMode.Workflow ? 'Workflow' : 'Chatflow'}
</p>
<span className="text-[12px] coz-fg-primary leading-4">
{flowMode === WorkflowMode.Workflow
? I18n.t('wf_chatflow_02')
: I18n.t('wf_chatflow_01')}
</span>
</div>
</div>
);
};

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 FC, useEffect, useState, useMemo } from 'react';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
import { type WidgetContext } from '@coze-project-ide/framework';
import { WORKFLOW_SUB_TYPE_ICON_MAP } from '../constants';
interface WorkflowWidgetIconProps {
context: WidgetContext;
}
export const WorkflowWidgetIcon: FC<WorkflowWidgetIconProps> = ({
context,
}) => {
const { widget } = context;
const [iconType, setIconType] = useState<string>(
widget.getIconType() || String(WorkflowMode.Workflow),
);
const icon = useMemo(() => WORKFLOW_SUB_TYPE_ICON_MAP[iconType], [iconType]);
useEffect(() => {
const disposable = widget.onIconTypeChanged(_iconType =>
setIconType(_iconType),
);
return () => disposable?.dispose?.();
}, []);
return icon;
};

View File

@@ -0,0 +1,24 @@
/*
* 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 { IconCozChat, IconCozWorkflow } from '@coze-arch/coze-design/icons';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
export const WORKFLOW_SUB_TYPE_ICON_MAP = {
[WorkflowMode.Workflow]: <IconCozWorkflow />,
[WorkflowMode.ChatFlow]: <IconCozChat />,
};

View File

@@ -0,0 +1,49 @@
.skeleton-container {
display: flex;
flex-direction: column;
row-gap: 16px;
align-items: center;
width: 100%;
height: 100%;
padding: 28px 97px;
.skeleton-item {
display: flex;
column-gap: 12px;
width: 100%;
.skeleton-column {
display: flex;
flex: 1;
flex-direction: column;
row-gap: 8px;
}
.skeleton-avatar {
flex-shrink: 0;
width: 32px;
height: 32px;
}
.skeleton-name {
width: 64px;
height: 13px;
border-radius: 12px;
}
.skeleton-content {
flex-shrink: 0;
width: 100%;
height: 102px;
border-radius: 12px;
}
.skeleton-content-mini {
flex-shrink: 0;
width: 50%;
height: 60px;
border-radius: 12px;
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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, { Suspense, lazy, useMemo } from 'react';
import { userStoreService } from '@coze-studio/user-store';
import { I18n } from '@coze-arch/i18n';
import { IconCozIllusAdd } from '@coze-arch/coze-design/illustrations';
import { EmptyState } from '@coze-arch/coze-design';
import { CreateEnv } from '@coze-arch/bot-api/workflow_api';
import type { IProject } from '@coze-studio/open-chat';
import { useIDEGlobalStore } from '@coze-project-ide/framework';
import { DISABLED_CONVERSATION } from '../constants';
import { useSkeleton } from './use-skeleton';
const LazyBuilderChat = lazy(async () => {
const { BuilderChat } = await import('@coze-studio/open-chat');
return { default: BuilderChat };
});
export interface ChatHistoryProps {
/**
* 会话 id
*/
conversationId?: string;
/**
* 会话名称
*/
conversationName: string;
/**
* 渠道 id
*/
connectorId: string;
/**
* 创建会话的环境
*/
createEnv: CreateEnv;
}
export const ChatHistory: React.FC<ChatHistoryProps> = ({
conversationId,
conversationName,
connectorId,
createEnv,
}) => {
const userInfo = userStoreService.getUserInfo();
const renderLoading = useSkeleton();
const projectInfo = useIDEGlobalStore(
store => store.projectInfo?.projectInfo,
);
const innerProjectInfo = useMemo<IProject>(
() => ({
id: projectInfo?.id || '',
conversationId,
connectorId,
conversationName,
name: conversationName || projectInfo?.name,
iconUrl: projectInfo?.icon_url,
type: 'app',
mode: createEnv === CreateEnv.Draft ? 'draft' : 'release',
caller: createEnv === CreateEnv.Draft ? 'CANVAS' : undefined,
}),
[projectInfo, conversationId, connectorId, conversationName, createEnv],
);
const chatUserInfo = {
id: userInfo?.user_id_str || '',
name: userInfo?.name || '',
avatar: userInfo?.avatar_url || '',
};
if (
!innerProjectInfo.id ||
!conversationName ||
(conversationId === DISABLED_CONVERSATION && createEnv !== CreateEnv.Draft)
) {
return (
<EmptyState
size="full_screen"
icon={<IconCozIllusAdd />}
title={I18n.t('wf_chatflow_61')}
description={I18n.t('wf_chatflow_62')}
/>
);
}
return (
<Suspense fallback={null}>
<LazyBuilderChat
workflow={{}}
project={innerProjectInfo}
areaUi={{
// 只看会话记录,不可操作
isDisabled: true,
isNeedClearContext: false,
input: {
isShow: false,
},
renderLoading,
uiTheme: 'chatFlow',
}}
userInfo={chatUserInfo}
auth={{
type: 'internal',
}}
></LazyBuilderChat>
</Suspense>
);
};

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 React, { useCallback } from 'react';
import { Skeleton } from '@coze-arch/coze-design';
import styles from './index.module.less';
export const useSkeleton = () => {
const renderLoading = useCallback(
() => (
<Skeleton
style={{ width: '100%', height: '100%' }}
placeholder={
<div className={styles['skeleton-container']}>
<div className={styles['skeleton-item']}>
<Skeleton.Avatar className={styles['skeleton-avatar']} />
<div className={styles['skeleton-column']}>
<Skeleton.Title className={styles['skeleton-name']} />
<Skeleton.Image className={styles['skeleton-content']} />
</div>
</div>
<div className={styles['skeleton-item']}>
<Skeleton.Avatar className={styles['skeleton-avatar']} />
<Skeleton.Image className={styles['skeleton-content-mini']} />
</div>
<div className={styles['skeleton-item']}>
<Skeleton.Avatar className={styles['skeleton-avatar']} />
<div className={styles['skeleton-column']}>
<Skeleton.Title className={styles['skeleton-name']} />
<Skeleton.Image className={styles['skeleton-content']} />
</div>
</div>
</div>
}
active
loading={true}
/>
),
[],
);
return renderLoading;
};

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 { I18n } from '@coze-arch/i18n';
// 默认会话 unique_id
export const DEFAULT_UNIQUE_ID = '0';
export const DEFAULT_CONVERSATION_NAME = 'Default';
export const MAX_LIMIT = 1000;
export enum ErrorCode {
DUPLICATE = 'duplicate',
EXCEED_MAX_LENGTH = 'exceed-max-length',
}
export const MAX_INPUT_LEN = 200;
/**
* 调试的渠道 id
*/
export const DEBUG_CONNECTOR_ID = '_10000010';
export const DEFAULT_CONNECTOR = {
connectorId: DEBUG_CONNECTOR_ID,
connectorName: I18n.t('workflow_saved_database'),
};
export const COZE_CONNECTOR_ID = '10000010';
export const API_CONNECTOR_ID = '1024';
export const CHAT_SDK_CONNECTOR_ID = '999';
export const COZE_CONNECTOR_IDS = [COZE_CONNECTOR_ID, '10000122', '10000129'];
/**
* 不存在的会话
*/
export const DISABLED_CONVERSATION = '0';
/**
* 只展示这些线上渠道,其他的后端不支持 @qiangshunliang
*/
export const ALLOW_CONNECTORS = [
COZE_CONNECTOR_ID,
API_CONNECTOR_ID,
CHAT_SDK_CONNECTOR_ID,
];

View File

@@ -0,0 +1,99 @@
/*
* 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 { IconCozWarningCircleFill } from '@coze-arch/coze-design/icons';
import { Input, Tooltip } from '@coze-arch/coze-design';
import { ErrorCode } from '../constants';
import s from './index.module.less';
export const EditInput = ({
ref,
defaultValue,
loading,
onBlur,
onValidate,
}: {
ref?: React.Ref<HTMLInputElement>;
/**
* 默认值
*/
defaultValue?: string;
/**
* loading
*/
loading: boolean;
/**
* 失焦 / 回车后执行的行为
*/
onBlur?: (input?: string, error?: ErrorCode) => void;
/**
* 校验函数,返回 true 标识校验通过
*/
onValidate?: (input: string) => ErrorCode | undefined;
}) => {
const [input, setInput] = useState(defaultValue);
const [error, setError] = useState<ErrorCode | undefined>(undefined);
const handleCreateSession = () => {
onBlur?.(input, error);
setInput('');
};
const handleValidateName = (_input: string) => {
setInput(_input);
const validateRes = onValidate?.(_input);
if (validateRes) {
setError(validateRes);
} else {
setError(undefined);
}
};
const renderError = useMemo(() => {
if (error === ErrorCode.DUPLICATE) {
return I18n.t('wf_chatflow_109');
} else if (error === ErrorCode.EXCEED_MAX_LENGTH) {
return I18n.t('wf_chatflow_116');
}
}, [error]);
return (
<Input
ref={ref}
className={s.input}
size="small"
loading={loading}
autoFocus
onChange={handleValidateName}
placeholder={'Please enter'}
defaultValue={defaultValue}
error={Boolean(error)}
suffix={
error ? (
<Tooltip content={renderError} position="right">
<IconCozWarningCircleFill className="coz-fg-hglt-red absolute right-1 text-[13px]" />
</Tooltip>
) : null
}
onBlur={handleCreateSession}
onEnterPress={handleCreateSession}
/>
);
};

View File

@@ -0,0 +1,134 @@
.page-container {
display: flex;
width: 100%;
height: calc(100% - 54px);
background: var(--coz-bg-plus);
border-radius: 0 0 8px 8px;
.chat-list-container {
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
flex-shrink: 0;
width: 260px;
height: 100%;
border-right: 1px solid var(--coz-stroke-primary);
.test-run-title-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-left: 8px;
}
.title {
margin-bottom: 4px;
padding: 12px 12px 0;
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: var(--coz-fg-plus);
}
.description {
margin-bottom: 12px;
padding: 0 12px;
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-secondary);
}
.new-chat-button {
margin-bottom: 12px;
}
.chat-item {
cursor: pointer;
overflow: hidden;
display: flex;
flex-shrink: 0;
column-gap: 4px;
align-items: center;
width: 100%;
height: 28px;
margin-bottom: 2px;
padding: 4px 8px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--coz-fg-secondary);
border-radius: 6px;
.icons {
display: none;
flex-grow: 1;
column-gap: 4px;
align-items: center;
justify-content: flex-end;
width: 0;
}
&:hover {
background-color: var(--coz-mg-primary);
.icons {
display: flex;
}
}
}
.chat-item-activate {
background-color: var(--coz-mg-primary);
}
.chat-item-editing {
padding: 2px;
}
}
.chat-area {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
}
// ui 特殊兼容样式,白底 @maijinning.888
// 设计稿地址https://www.figma.com/design/e4X3MThLYyo1Fhjcg8uhpr/Workflow-%26-Imageflow?node-id=7133-217329&node-type=instance&m=dev
.input {
/* stylelint-disable-next-line declaration-no-important */
background-color: #FFF !important;
:global {
.semi-input {
padding: 0 4px;
}
}
}
.new-list {
overflow-y: auto;
flex-grow: 1;
height: 0;
}

View File

@@ -0,0 +1,215 @@
/*
* 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 { Element } from 'react-scroll';
import { useLocation } from 'react-router-dom';
import React, {
useMemo,
useCallback,
useState,
useRef,
useEffect,
} from 'react';
import { I18n } from '@coze-arch/i18n';
import {
type ProjectConversation,
CreateEnv,
} from '@coze-arch/bot-api/workflow_api';
import {
CONVERSATION_URI,
getURIPathByPathname,
} from '@coze-project-ide/framework';
import { StaticChatList } from '../static-chat-list';
import {
useCreateChat,
useUpdateChat,
useDeleteChat,
useBatchDelete,
useConversationListWithConnector,
} from '../hooks';
import { DynamicChatList } from '../dynamic-chat-list';
import { ErrorCode, MAX_INPUT_LEN } from '../constants';
import { ChatHistory } from '../chat-history';
import { EditInput } from './edit-input';
import styles from './index.module.less';
interface ConversationContentProps {
connectorId: string;
createEnv: CreateEnv;
canEdit?: boolean;
}
// eslint-disable-next-line @coze-arch/max-line-per-function
export const ConversationContent: React.FC<ConversationContentProps> = ({
connectorId,
createEnv,
canEdit,
}) => {
const inputRef = useRef<HTMLInputElement>(null);
// 顶部的创建弹窗
const [inputVisible, setInputVisible] = useState(false);
const [activateChat, setActivateChat] = useState<
ProjectConversation | undefined
>();
const { pathname } = useLocation();
const { staticList, dynamicList, fetch } = useConversationListWithConnector({
connector_id: connectorId,
create_env: createEnv,
});
const { loading: createLoading, handleCreateChat } = useCreateChat({
manualRefresh: () => fetch(),
});
const { handleUpdateChat, loading: updateLoading } = useUpdateChat({
manualRefresh: () => fetch(),
});
const { handleDelete, modalDom } = useDeleteChat({
staticList,
manualRefresh: () => fetch(),
setActivateChat,
});
const handleSelectChat = useCallback((chatItem?: ProjectConversation) => {
setActivateChat(chatItem);
}, []);
const { batchDelete } = useBatchDelete({
connectorId,
createEnv,
manualRefresh: () => fetch(),
setActivateChat,
});
useEffect(() => {
// 初始化选中接口返回数据。conversationId 可能为 0 要展示空
if (!activateChat && staticList?.length) {
handleSelectChat(staticList[0]);
}
}, [staticList]);
const handleCreateInput = useCallback(() => {
setInputVisible(true);
}, []);
const handleValidateName = (_input: string) => {
if (_input?.length > MAX_INPUT_LEN) {
return ErrorCode.EXCEED_MAX_LENGTH;
}
if (staticList.some(item => item.conversation_name === _input)) {
return ErrorCode.DUPLICATE;
}
return undefined;
};
const handleCreateSession = async (input?: string, error?: ErrorCode) => {
if (!input) {
setInputVisible(false);
return;
}
if (!error) {
await handleCreateChat(input);
}
setInputVisible(false);
};
const conversationName = useMemo(() => {
if (
createEnv !== CreateEnv.Draft &&
activateChat?.release_conversation_name
) {
return activateChat?.release_conversation_name;
}
return activateChat?.conversation_name || '';
}, [createEnv, activateChat]);
useEffect(() => {
if (inputVisible) {
inputRef.current?.scrollIntoView();
}
}, [inputVisible]);
const renderCreateInput = () =>
inputVisible ? (
<EditInput
ref={inputRef}
loading={createLoading}
onBlur={handleCreateSession}
onValidate={handleValidateName}
/>
) : null;
useEffect(() => {
// 判断会话页是否显示,实现切换tab时刷新列表效果
const value = getURIPathByPathname(pathname);
if (value && CONVERSATION_URI.displayName === value) {
fetch();
}
}, [pathname]);
useEffect(() => {
fetch();
setActivateChat(undefined);
}, [connectorId, createEnv]);
return (
<div className={styles['page-container']}>
<div className={styles['chat-list-container']}>
<div className={styles.title}>{I18n.t('wf_chatflow_101')}</div>
<div className={styles.description}>
{createEnv === CreateEnv.Release
? I18n.t('wf_chatflow_102')
: I18n.t('workflow_chatflow_testrun_conversation_des')}
</div>
<div className={styles['new-list']} id="conversation-list">
<Element name="static" />
<StaticChatList
canEdit={canEdit}
list={staticList}
activateChat={activateChat}
updateLoading={updateLoading}
onUpdate={handleUpdateChat}
onDelete={handleDelete}
onValidate={handleValidateName}
onSelectChat={handleSelectChat}
renderCreateInput={renderCreateInput}
handleCreateInput={handleCreateInput}
/>
<Element name="dynamic" />
<DynamicChatList
list={dynamicList}
canEdit={canEdit}
activateChat={activateChat}
onDelete={handleDelete}
onBatchDelete={batchDelete}
onSelectChat={handleSelectChat}
/>
</div>
</div>
<div className={styles['chat-area']}>
<ChatHistory
createEnv={createEnv}
connectorId={connectorId}
conversationId={activateChat?.conversation_id}
conversationName={conversationName}
/>
</div>
{modalDom}
</div>
);
};

View File

@@ -0,0 +1,49 @@
/* stylelint-disable declaration-no-important */
.list-container {
padding: 0 12px;
.empty-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 300px;
}
&.in-batch {
padding-bottom: 56px;
}
}
.title {
top: 28px;
bottom: 0;
&.is-bottom {
border-top: 1px solid var(--coz-stroke-primary);
}
}
.batch-wrap {
position: absolute;
z-index: 2;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
display: flex;
column-gap: 5px;
align-items: center;
padding: 5px;
background-color: var(--coz-bg-max);
border: 1px solid var(--coz-stroke-primary);
border-radius: 10px;
box-shadow: var(--coz-shadow-default);
}
.is-batch-selected {
background-color: var(--coz-mg-hglt-secondary)!important;
}

View File

@@ -0,0 +1,270 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import { scroller } from 'react-scroll';
import React, { useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import { useInViewport } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import {
IconCozEmpty,
IconCozTrashCan,
IconCozListDisorder,
IconCozCross,
} from '@coze-arch/coze-design/icons';
import {
EmptyState,
IconButton,
Button,
Typography,
Tooltip,
Checkbox,
Popconfirm,
} from '@coze-arch/coze-design';
import { useFlags } from '@coze-arch/bot-flags';
import { type ProjectConversation } from '@coze-arch/bot-api/workflow_api';
import { TitleWithTooltip } from '../title-with-tooltip';
import commonStyle from '../conversation-content/index.module.less';
import styles from './index.module.less';
const { Text } = Typography;
const ChatItem: React.FC<{
chat: ProjectConversation;
canEdit?: boolean;
isActivate: boolean;
isInBatch: boolean;
isInBatchSelected: boolean;
onBatchSelectChange: (data: ProjectConversation) => void;
onActivate: (data: ProjectConversation) => void;
onDelete: (data: ProjectConversation) => void;
}> = ({
chat,
isActivate,
isInBatch,
isInBatchSelected,
canEdit,
onBatchSelectChange,
onActivate,
onDelete,
}) => {
const showActivate = isActivate && !isInBatch;
const canDeleteOperate = canEdit && !isInBatch;
const handleClick = () => {
if (isInBatch) {
onBatchSelectChange(chat);
} else {
onActivate(chat);
}
};
return (
<div
className={classNames(
commonStyle['chat-item'],
showActivate && commonStyle['chat-item-activate'],
isInBatchSelected && styles['is-batch-selected'],
)}
onClick={handleClick}
>
{isInBatch ? <Checkbox checked={isInBatchSelected} /> : null}
<Text ellipsis={{ showTooltip: true }}>{chat.conversation_name}</Text>
{canDeleteOperate ? (
<div className={commonStyle.icons}>
<IconButton
size="small"
color="secondary"
icon={<IconCozTrashCan />}
onClick={e => {
e.stopPropagation();
onDelete(chat);
}}
/>
</div>
) : null}
</div>
);
};
export const DynamicChatList = ({
canEdit,
list,
activateChat,
onDelete,
onBatchDelete,
onSelectChat,
}: {
canEdit?: boolean;
list: ProjectConversation[];
activateChat?: ProjectConversation;
onDelete: (chatItem: ProjectConversation) => Promise<void>;
onBatchDelete: (ids: string[]) => Promise<void>;
onSelectChat: (chatItem: ProjectConversation) => void;
}) => {
const [FLAGS] = useFlags();
const [inBatch, setInBatch] = useState(false);
const [batchSelected, setBatchSelected] = useState<
Record<string, ProjectConversation | undefined>
>({});
const dynamicTopRef = useRef(null);
const [inDynamicTopViewport] = useInViewport(dynamicTopRef);
const batchSelectedList = useMemo(
() =>
Object.values(batchSelected).filter((i): i is ProjectConversation => !!i),
[batchSelected],
);
const canBatchOperate =
!!canEdit &&
!!list?.length &&
!inBatch &&
// 社区版暂不支持该功能
FLAGS['bot.automation.conversation_batch_delete'];
const exitBatch = () => {
setInBatch(false);
setBatchSelected({});
};
const handleBatchSelectChange = (item: ProjectConversation) => {
const key = item.unique_id || '';
const next = batchSelected[key] ? undefined : item;
setBatchSelected({
...batchSelected,
[item.unique_id || '']: next,
});
};
const handleBatchDelete = async (items: ProjectConversation[]) => {
const ids = items.map(i => i.unique_id).filter((i): i is string => !!i);
await onBatchDelete(ids);
// 删除成功后退出批量操作模式
exitBatch();
};
return (
<>
<TitleWithTooltip
className={classNames(
styles.title,
!inDynamicTopViewport && styles['is-bottom'],
)}
title={I18n.t('project_conversation_list_dynamic_title')}
tooltip={I18n.t('wf_chatflow_44')}
extra={
canBatchOperate && (
<Tooltip
content={I18n.t(
'project_conversation_list_operate_batch_tooltip',
)}
>
<IconButton
icon={<IconCozListDisorder />}
size="small"
color="secondary"
onClick={() => setInBatch(true)}
/>
</Tooltip>
)
}
onClick={() =>
scroller.scrollTo('dynamic', {
duration: 200,
smooth: true,
containerId: 'conversation-list',
})
}
/>
<div ref={dynamicTopRef} />
<div
className={classNames(
styles['list-container'],
inBatch && styles['in-batch'],
)}
>
{list?.length ? (
list.map(data => (
<ChatItem
chat={data}
canEdit={canEdit}
isActivate={data.unique_id === activateChat?.unique_id}
isInBatch={inBatch}
isInBatchSelected={!!batchSelected[data.unique_id || '']}
onBatchSelectChange={handleBatchSelectChange}
onActivate={onSelectChat}
onDelete={onDelete}
/>
))
) : (
<div className={styles['empty-container']}>
<EmptyState
size="default"
icon={<IconCozEmpty />}
title={I18n.t('wf_chatflow_41')}
description={I18n.t('wf_chatflow_42')}
/>
</div>
)}
{inBatch ? (
<div className={styles['batch-wrap']}>
<Popconfirm
title={I18n.t('project_conversation_list_batch_delete_tooltip')}
okText={I18n.t('delete_title')}
cancelText={I18n.t('cancel')}
content={I18n.t(
'project_conversation_list_batch_delete_tooltip_context',
{
len: batchSelectedList.length,
},
)}
onConfirm={() => {
handleBatchDelete(batchSelectedList);
}}
>
<Button size="small" disabled={!batchSelectedList.length}>
{I18n.t('project_conversation_list_batch_delete_btn', {
len: batchSelectedList.length,
})}
</Button>
</Popconfirm>
<Popconfirm
title={I18n.t('filebox_0040')}
okText={I18n.t('filebox_0040')}
cancelText={I18n.t('cancel')}
content={I18n.t(
'project_conversation_list_delete_all_tooltip_context',
)}
okButtonColor="red"
onConfirm={() => {
handleBatchDelete(list);
}}
>
<Button size="small" color="redhglt">
{I18n.t('url_add_008')}
</Button>
</Popconfirm>
<IconButton
size="small"
color="secondary"
icon={<IconCozCross />}
onClick={exitBatch}
/>
</div>
) : null}
</div>
</>
);
};

View File

@@ -0,0 +1,22 @@
/*
* 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 { useDeleteChat } from './use-delete-chat';
export { useUpdateChat } from './use-update-chat';
export { useCreateChat } from './use-create-chat';
export { useConversationListWithConnector } from './use-conversation-list';
export { useConnectorList } from './use-connector-list';
export { useBatchDelete } from './use-batch-delete';

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import {
CreateEnv,
type ProjectConversation,
} from '@coze-arch/bot-api/workflow_api';
import { workflowApi } from '@coze-arch/bot-api';
import { useIDEGlobalStore } from '@coze-project-ide/framework';
interface UseBatchDeleteOptions {
connectorId: string;
createEnv: CreateEnv;
manualRefresh: () => void;
setActivateChat: (_chat: ProjectConversation | undefined) => void;
}
export const useBatchDelete = (options: UseBatchDeleteOptions) => {
const { spaceId, projectId } = useIDEGlobalStore(store => ({
spaceId: store.spaceId,
projectId: store.projectId,
}));
const batchDelete = async (ids: string[]) => {
const isDraft = options.createEnv === CreateEnv.Draft;
const res = await workflowApi.BatchDeleteProjectConversation({
space_id: spaceId,
project_id: projectId,
unique_id_list: ids,
draft_mode: isDraft,
connector_id: isDraft ? '' : options.connectorId,
});
if (res.Success) {
Toast.success(I18n.t('wf_chatflow_112'));
options.manualRefresh();
options.setActivateChat(undefined);
} else {
Toast.error(I18n.t('wf_chatflow_151'));
}
};
return {
batchDelete,
};
};

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 { useEffect, useState, useMemo } from 'react';
import { useMemoizedFn } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import { CreateEnv } from '@coze-arch/bot-api/workflow_api';
import { intelligenceApi } from '@coze-arch/bot-api';
import {
useProjectId,
useListenMessageEvent,
CONVERSATION_URI,
type MessageEvent,
} from '@coze-project-ide/framework';
import {
DEFAULT_CONNECTOR,
DEBUG_CONNECTOR_ID,
COZE_CONNECTOR_ID,
COZE_CONNECTOR_IDS,
ALLOW_CONNECTORS,
} from '../constants';
interface Connector {
connectorId: string;
connectorName?: string;
}
export const useConnectorList = () => {
const projectId = useProjectId();
const [connectorList, setConnectorList] = useState<Connector[]>([
DEFAULT_CONNECTOR,
]);
const [activeKey, setActiveKey] = useState(DEBUG_CONNECTOR_ID);
const createEnv = useMemo(() => {
if (activeKey === DEBUG_CONNECTOR_ID) {
return CreateEnv.Draft;
}
return CreateEnv.Release;
}, [activeKey]);
const fetch = async () => {
const res = await intelligenceApi.GetProjectPublishedConnector({
project_id: projectId,
});
const data = res.data || [];
let noCoze = true;
const next = data
.reduce((prev, current) => {
if (!current.id) {
return prev;
}
if (COZE_CONNECTOR_IDS.includes(current.id)) {
if (noCoze) {
prev.push({
connectorId: COZE_CONNECTOR_ID,
connectorName: I18n.t('platform_name'),
});
noCoze = false;
}
} else {
prev.push({
connectorId: current.id,
connectorName: current.name,
});
}
return prev;
}, [] as Connector[])
.filter(i => ALLOW_CONNECTORS.includes(i.connectorId));
setConnectorList([DEFAULT_CONNECTOR, ...next]);
};
const handleTabChange = (v: string) => {
setActiveKey(v);
};
const listener = useMemoizedFn((e: MessageEvent) => {
if (e.name === 'tab' && e.data?.value === 'testrun') {
setActiveKey(DEBUG_CONNECTOR_ID);
}
});
useListenMessageEvent(CONVERSATION_URI, listener);
useEffect(() => {
fetch();
}, []);
return {
connectorList,
activeKey,
createEnv,
onTabChange: handleTabChange,
};
};

View File

@@ -0,0 +1,94 @@
/*
* 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 } from 'react';
import {
type ProjectConversation,
type ListProjectConversationRequest,
CreateMethod,
CreateEnv,
} from '@coze-arch/bot-api/workflow_api';
import { workflowApi } from '@coze-arch/bot-api';
import { useIDEGlobalStore } from '@coze-project-ide/framework';
export const MAX_LIMIT = 1000;
type ListProjectConversationDefParams = Pick<
ListProjectConversationRequest,
'create_env' | 'create_method'
> & {
connector_id: string;
};
type ConversationListWithConnectorParams = Pick<
ListProjectConversationDefParams,
'connector_id' | 'create_env'
>;
const useConversationList = (params: ListProjectConversationDefParams) => {
const { spaceId, projectId, version } = useIDEGlobalStore(store => ({
spaceId: store.spaceId,
projectId: store.projectId,
version: store.version,
}));
const [list, setList] = useState<ProjectConversation[]>([]);
const fetch = async () => {
const staticList = await workflowApi.ListProjectConversationDef({
space_id: spaceId,
project_id: projectId,
project_version: version,
create_method: CreateMethod.ManualCreate,
create_env: CreateEnv.Release,
limit: MAX_LIMIT,
...params,
});
setList(staticList.data || []);
};
return {
list,
fetch,
};
};
export const useConversationListWithConnector = (
params: ConversationListWithConnectorParams,
) => {
// 静态
const { list: staticList, fetch: fetchStatic } = useConversationList({
create_method: CreateMethod.ManualCreate,
...params,
});
// 动态
const { list: dynamicList, fetch: fetchDynamic } = useConversationList({
create_method: CreateMethod.NodeCreate,
...params,
});
const fetch = () => {
fetchStatic();
fetchDynamic();
};
return {
staticList,
dynamicList,
fetch,
};
};

View File

@@ -0,0 +1,53 @@
/*
* 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 } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { workflowApi } from '@coze-arch/bot-api';
import { useIDEGlobalStore } from '@coze-project-ide/framework';
export const useCreateChat = ({
manualRefresh,
}: {
manualRefresh: () => void;
}) => {
const { spaceId, projectId } = useIDEGlobalStore(store => ({
spaceId: store.spaceId,
projectId: store.projectId,
}));
const [loading, setLoading] = useState(false);
const handleCreateChat = async (input: string) => {
try {
setLoading(true);
const res = await workflowApi.CreateProjectConversationDef({
space_id: spaceId,
project_id: projectId,
conversation_name: input,
});
if (res?.code === 0) {
Toast.success(I18n.t('wf_chatflow_111'));
manualRefresh();
} else {
Toast.error(I18n.t('wf_chatflow_112'));
}
} finally {
setLoading(false);
}
};
return { loading, handleCreateChat };
};

View File

@@ -0,0 +1,72 @@
.modal {
:global {
.semi-modal {
.semi-modal-footer {
margin-top: 32px;
}
.semi-modal-content {
padding: 24px;
}
}
}
}
.content-container {
display: flex;
flex-direction: column;
.content-text {
margin-top: 16px;
font-size: 14px;
line-height: 20px;
color: var(--coz-fg-secondary);
}
.rebind-chat {
margin-top: 24px;
.rebind-title {
margin-bottom: 4px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: var(--coz-fg-primary);
}
.rebind-icon {
margin-right: 2px;
}
.rebind-text {
width: 50%;
}
.rebind-desc {
margin-bottom: 18px;
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-secondary);
}
.rebind-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 6px;
}
}
}
.rebind-select {
.option-text-wrapper {
overflow: hidden;
}
:global {
.option-text-wrapper {
overflow: hidden;
}
}
}

View File

@@ -0,0 +1,202 @@
/*
* 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 } from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozChat } from '@coze-arch/coze-design/icons';
import { Modal, Select, Typography, Toast } from '@coze-arch/coze-design';
import {
type Workflow,
type ProjectConversation,
} from '@coze-arch/bot-api/workflow_api';
import { workflowApi } from '@coze-arch/bot-api';
import { useIDEGlobalStore } from '@coze-project-ide/framework';
import { DEFAULT_UNIQUE_ID, DEFAULT_CONVERSATION_NAME } from '../../constants';
import s from './index.module.less';
const { Text } = Typography;
// eslint-disable-next-line @coze-arch/max-line-per-function
export const useDeleteChat = ({
staticList,
manualRefresh,
setActivateChat,
}: {
staticList: ProjectConversation[];
manualRefresh: () => void;
setActivateChat: (_chat: ProjectConversation | undefined) => void;
}) => {
const { spaceId, projectId } = useIDEGlobalStore(store => ({
spaceId: store.spaceId,
projectId: store.projectId,
}));
const [visible, setVisible] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const [replace, setReplace] = useState<Workflow[]>([]);
const [chat, setChat] = useState<ProjectConversation | undefined>(undefined);
// keyworkflowIdvalueconversationId
const [rebindReplace, setRebindReplace] = useState<Record<string, string>>(
{},
);
const optionList = staticList
.filter(item => item.unique_id !== chat?.unique_id)
.map(item => ({
label: (
<Text style={{ width: '100%' }} ellipsis={{ showTooltip: true }}>
{item.conversation_name}
</Text>
),
value: item.unique_id,
conversationId: item.conversation_id,
}));
/**
* 给外部的 check用作 replace 请求
*/
const handleDelete = async (_chat?: ProjectConversation) => {
setChat(_chat);
const res = await workflowApi.DeleteProjectConversationDef({
space_id: spaceId,
project_id: projectId,
check_only: true,
unique_id: _chat?.unique_id || '',
});
if (res.need_replace) {
setReplace(res.need_replace);
const rebindInit = {};
res.need_replace.forEach(_replace => {
if (_replace.workflow_id) {
rebindInit[_replace.workflow_id] = DEFAULT_CONVERSATION_NAME;
}
});
setRebindReplace(rebindInit);
} else {
setReplace([]);
}
setVisible(true);
};
const handleModalOk = async () => {
setDeleteLoading(true);
try {
const res = await workflowApi.DeleteProjectConversationDef({
space_id: spaceId,
project_id: projectId,
unique_id: chat?.unique_id || '',
replace: rebindReplace,
});
if (res.success) {
setReplace([]);
setVisible(false);
Toast.success(I18n.t('wf_chatflow_112'));
// 删除成功后刷新列表
manualRefresh();
setActivateChat(undefined);
} else {
Toast.error(I18n.t('wf_chatflow_151'));
}
} finally {
setDeleteLoading(false);
}
};
const handleSelectChange = (
workflowId?: string,
conversationName?: string,
) => {
if (workflowId && conversationName) {
const newBind = {
...rebindReplace,
[workflowId]: conversationName,
};
setRebindReplace(newBind);
}
};
const modalDom = useMemo(() => {
const dom = (
<div className={s['rebind-chat']}>
<div className={s['rebind-title']}>{I18n.t('wf_chatflow_53')}</div>
<div className={s['rebind-desc']}>{I18n.t('wf_chatflow_54')}</div>
{replace.map(item => {
const { name } = item;
return (
<div className={s['rebind-item']}>
<IconCozChat className={s['rebind-icon']} />
<Text
ellipsis={{ showTooltip: true }}
className={s['rebind-text']}
>
{name}
</Text>
<Select
dropdownClassName={s['rebind-select']}
style={{ width: '50%' }}
dropdownStyle={{ width: 220 }}
size="small"
defaultValue={DEFAULT_UNIQUE_ID}
optionList={optionList}
onChange={value => {
const selectItem = staticList.find(
option => option.unique_id === value,
);
handleSelectChange(
item.workflow_id,
selectItem?.conversation_name,
);
}}
/>
</div>
);
})}
</div>
);
return (
<Modal
visible={visible}
width={480}
type="dialog"
title={I18n.t('wf_chatflow_51')}
className={s.modal}
okText={I18n.t('wf_chatflow_55')}
cancelText={I18n.t('wf_chatflow_56')}
onCancel={() => setVisible(false)}
okButtonColor="red"
okButtonProps={{
loading: deleteLoading,
}}
onOk={handleModalOk}
>
<div className={s['content-container']}>
<div className={s['content-text']}>{I18n.t('wf_chatflow_52')}</div>
{replace?.length ? dom : null}
</div>
</Modal>
);
}, [chat, replace, visible, optionList]);
return {
handleDelete,
modalDom,
};
};

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import { workflowApi } from '@coze-arch/bot-api';
import { useIDEGlobalStore } from '@coze-project-ide/framework';
export const useUpdateChat = ({
manualRefresh,
}: {
manualRefresh: () => void;
}) => {
const { spaceId, projectId } = useIDEGlobalStore(store => ({
spaceId: store.spaceId,
projectId: store.projectId,
}));
const [loading, setLoading] = useState(false);
const handleUpdateChat = async (
uniqueId: string,
conversationName: string,
) => {
try {
setLoading(true);
await workflowApi.UpdateProjectConversationDef({
space_id: spaceId,
project_id: projectId,
unique_id: uniqueId,
conversation_name: conversationName,
});
manualRefresh();
} finally {
setLoading(false);
}
};
return { loading, handleUpdateChat };
};

View File

@@ -0,0 +1,10 @@
.connector-tab {
display: flex;
align-items: center;
height: 54px;
padding: 0 12px;
background: var(--coz-bg-plus);
border-bottom: 1px solid var(--coz-stroke-primary);
}

View File

@@ -0,0 +1,75 @@
/*
* 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 } from 'react';
import { TabBar } from '@coze-arch/coze-design';
import { useProjectRole } from '@coze-common/auth';
import {
useProjectId,
useCommitVersion,
useCurrentWidgetContext,
} from '@coze-project-ide/framework';
import { useConnectorList } from './hooks';
import { ConversationContent } from './conversation-content';
import { DEBUG_CONNECTOR_ID, COZE_CONNECTOR_ID } from './constants';
import css from './main.module.less';
const Conversation = () => {
const projectId = useProjectId();
const { version: commitVersion } = useCommitVersion();
const { widget: uiWidget } = useCurrentWidgetContext();
const { connectorList, activeKey, createEnv, onTabChange } =
useConnectorList();
const projectRoles = useProjectRole(projectId);
const readonly = !projectRoles?.length || !!commitVersion;
useEffect(() => {
uiWidget.setUIState('normal');
}, []);
return (
<>
<TabBar
type="text"
mode="select"
className={css['connector-tab']}
activeKey={activeKey}
onTabClick={onTabChange}
>
{connectorList.map(connector => (
<TabBar.TabPanel
tab={connector.connectorName}
itemKey={connector.connectorId}
/>
))}
</TabBar>
<ConversationContent
canEdit={!readonly}
connectorId={
activeKey === DEBUG_CONNECTOR_ID ? COZE_CONNECTOR_ID : activeKey
}
createEnv={createEnv}
/>
</>
);
};
export default Conversation;

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 React from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozChatSetting } from '@coze-arch/coze-design/icons';
import {
LayoutPanelType,
withLazyLoad,
type WidgetRegistry,
type WidgetContext,
} from '@coze-project-ide/framework';
export const ConversationRegistry: WidgetRegistry = {
match: /(\/session.*|\/conversation.*)/,
area: LayoutPanelType.MAIN_PANEL,
load: (ctx: WidgetContext) =>
Promise.resolve().then(() => {
ctx.widget.setTitle(I18n.t('wf_chatflow_101'));
ctx.widget.setUIState('normal');
}),
renderContent() {
const Component = withLazyLoad(() => import('./main'));
return <Component />;
},
renderIcon() {
return <IconCozChatSetting />;
},
};

View File

@@ -0,0 +1,7 @@
.list-container {
padding: 0 12px;
}
.title {
top: 0;
}

View File

@@ -0,0 +1,173 @@
/*
* 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 { scroller } from 'react-scroll';
import React, { useState } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import {
IconCozEdit,
IconCozPlus,
IconCozTrashCan,
} from '@coze-arch/coze-design/icons';
import { IconButton, Typography } from '@coze-arch/coze-design';
import { type ProjectConversation } from '@coze-arch/bot-api/workflow_api';
import { TitleWithTooltip } from '../title-with-tooltip';
import commonStyles from '../conversation-content/index.module.less';
import { EditInput } from '../conversation-content/edit-input';
import { DEFAULT_UNIQUE_ID, type ErrorCode } from '../constants';
import s from './index.module.less';
const { Text } = Typography;
export const StaticChatList = ({
canEdit,
list,
activateChat,
updateLoading,
onUpdate,
onDelete,
onValidate,
onSelectChat,
renderCreateInput,
handleCreateInput,
}: {
canEdit?: boolean;
list: ProjectConversation[];
activateChat?: ProjectConversation;
updateLoading: boolean;
onUpdate: (uniqueId: string, conversationName: string) => void;
onDelete: (chatItem: ProjectConversation) => Promise<void>;
onValidate: (_input: string) => ErrorCode | undefined;
onSelectChat: (chatItem: ProjectConversation) => void;
renderCreateInput: () => React.ReactNode;
handleCreateInput?: () => void;
}) => {
// 存储 session_id
const [editingUniqueId, setEditingUniqueId] = useState('');
const handleEditSession = (inputStr?: string, error?: ErrorCode) => {
if (!error) {
onUpdate(editingUniqueId, inputStr || '');
}
setEditingUniqueId('');
};
const handleSessionVisible = (_uniqueId?: string) => {
setEditingUniqueId(_uniqueId || '');
};
/**
* ux @wangwenbo.me 设计default 放在首位,
* 剩余的接口返回按照创建先后顺序倒序排序(后创建的放前边)
*/
return (
<>
<TitleWithTooltip
className={s.title}
title={I18n.t('project_conversation_list_static_title')}
tooltip={I18n.t('wf_chatflow_104')}
extra={
canEdit && (
<IconButton
icon={<IconCozPlus />}
color="highlight"
size="small"
onClick={handleCreateInput}
/>
)
}
onClick={() =>
scroller.scrollTo('static', {
duration: 200,
smooth: true,
containerId: 'conversation-list',
})
}
/>
<div className={s['list-container']}>
<div
className={classNames(
commonStyles['chat-item'],
activateChat?.unique_id === list[0]?.unique_id &&
commonStyles['chat-item-activate'],
)}
key={list[0]?.unique_id}
onClick={() => onSelectChat(list[0])}
>
<Text ellipsis={{ showTooltip: true }}>
{list[0]?.conversation_name}
</Text>
</div>
{renderCreateInput()}
{list.slice(1).map(item => (
<div
className={classNames(
commonStyles['chat-item'],
activateChat?.unique_id === item.unique_id &&
commonStyles['chat-item-activate'],
editingUniqueId === item.unique_id &&
commonStyles['chat-item-editing'],
)}
key={item.unique_id}
onClick={() => onSelectChat(item)}
>
{editingUniqueId === item.unique_id ? (
<EditInput
loading={updateLoading}
defaultValue={item.conversation_name}
onBlur={handleEditSession}
onValidate={onValidate}
/>
) : (
<Text ellipsis={{ showTooltip: true }}>
{item.conversation_name}
</Text>
)}
{editingUniqueId === item.unique_id ||
item.unique_id === DEFAULT_UNIQUE_ID ||
!canEdit ? null : (
<div className={commonStyles.icons}>
<IconButton
size="small"
color="secondary"
icon={<IconCozEdit />}
onClick={e => {
e.stopPropagation();
handleSessionVisible(item.unique_id);
}}
/>
{/* 默认会话不可删除 */}
<IconButton
size="small"
color="secondary"
icon={<IconCozTrashCan />}
onClick={e => {
e.stopPropagation();
onDelete(item);
}}
/>
</div>
)}
</div>
))}
</div>
</>
);
};

View File

@@ -0,0 +1,34 @@
.title-container {
cursor: pointer;
position: sticky;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
height: 28px;
margin-bottom: 2px;
padding: 0 8px;
font-size: 14px;
font-weight: 500;
font-style: normal;
line-height: 20px;
color: var(--coz-fg-secondary);
background-color: var(--coz-bg-plus);
.title-with-tip {
display: flex;
column-gap: 4px;
align-items: center;
}
}
.extra {
display: flex;
column-gap: 4px;
align-items: center;
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import cls from 'classnames';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tooltip } from '@coze-arch/coze-design';
import s from './index.module.less';
interface TitleWithTooltipProps {
title: React.ReactNode;
tooltip?: React.ReactNode;
extra?: React.ReactNode;
className?: string;
onClick?: () => void;
}
export const TitleWithTooltip: React.FC<TitleWithTooltipProps> = ({
title,
tooltip,
extra,
className,
onClick,
}) => (
<div className={cls(s['title-container'], className)} onClick={onClick}>
<div className={s['title-with-tip']}>
{title}
<Tooltip content={tooltip}>
<IconCozInfoCircle />
</Tooltip>
</div>
<div className={s.extra} onClick={e => e.stopPropagation()}>
{extra}
</div>
</div>
);

View File

@@ -0,0 +1,47 @@
/*
* 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 { ProjectResourceActionKey } from '@coze-arch/bot-api/plugin_develop';
export const workflowActions = [
{
key: ProjectResourceActionKey.Rename,
enable: true,
},
{
key: ProjectResourceActionKey.Copy,
enable: true,
},
{
key: ProjectResourceActionKey.MoveToLibrary,
enable: false,
hint: '不能移动到资源库',
},
{
key: ProjectResourceActionKey.CopyToLibrary,
enable: true,
hint: '复制到资源库',
},
{
// 切换为 chatflow
key: ProjectResourceActionKey.SwitchToChatflow,
enable: true,
},
{
key: ProjectResourceActionKey.Delete,
enable: true,
},
];

View File

@@ -0,0 +1,18 @@
/*
* 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 { useProjectApi } from './use-project-api';
export { useListenWFMessageEvent } from './use-listen-message-event';

View File

@@ -0,0 +1,56 @@
/*
* 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 {
useIDENavigate,
getURIByResource,
useProjectIDEServices,
} from '@coze-project-ide/framework';
import { usePrimarySidebarStore } from '@coze-project-ide/biz-components';
import { I18n } from '@coze-arch/i18n';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
import { workflowApi } from '@coze-arch/bot-api';
import { Toast } from '@coze-arch/coze-design';
export const useChangeFlowMode = () => {
const refetch = usePrimarySidebarStore(state => state.refetch);
const navigate = useIDENavigate();
const { view } = useProjectIDEServices();
return async (
flowMode: WorkflowMode,
workflowId: string,
spaceId: string,
) => {
await workflowApi.UpdateWorkflowMeta({
workflow_id: workflowId,
space_id: spaceId,
flow_mode: flowMode,
});
Toast.success(
I18n.t('wf_chatflow_123', {
Chatflow: I18n.t(
flowMode === WorkflowMode.ChatFlow ? 'wf_chatflow_76' : 'Workflow',
),
}),
);
await refetch();
const uri = getURIByResource('workflow', workflowId);
const widgetContext = view.getWidgetContextFromURI(uri);
const widgetOpened = Boolean(widgetContext?.widget);
// 已经打开的 widget加 refresh 参数刷新,未打开的直接打开会刷新
navigate(`/workflow/${workflowId}${widgetOpened ? '?refresh=true' : ''}`);
};
};

View File

@@ -0,0 +1,87 @@
/*
* 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 } from 'react';
import {
useWorkflowModal,
WorkflowModalFrom,
type WorkFlowModalModeProps,
WorkflowCategory,
} from '@coze-workflow/components';
import {
BizResourceTypeEnum,
useOpenResource,
usePrimarySidebarStore,
useResourceCopyDispatch,
} from '@coze-project-ide/biz-components';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
import { resource_resource_common } from '@coze-arch/bot-api/plugin_develop';
import { useNameValidators } from './use-name-validators';
export const useImportLibraryWorkflow = ({
projectId,
}: {
projectId: string;
}): {
modal: ReactNode;
importLibrary: () => void;
} => {
const refetch = usePrimarySidebarStore(state => state.refetch);
const openResource = useOpenResource();
const importResource = useResourceCopyDispatch();
const onImport: WorkFlowModalModeProps['onImport'] = async item => {
try {
close();
console.log('[ResourceFolder]import library workflow>>>', item);
await importResource({
scene:
resource_resource_common.ResourceCopyScene.CopyResourceFromLibrary,
res_id: item.workflow_id,
res_type: resource_resource_common.ResType.Workflow,
project_id: projectId,
res_name: item.name,
});
} catch (e) {
console.error('[ResourceFolder]import library workflow error>>>', e);
}
};
const nameValidators = useNameValidators();
const { node, open, close } = useWorkflowModal({
from: WorkflowModalFrom.ProjectImportLibrary,
flowMode: WorkflowMode.Workflow,
hiddenExplore: true,
hiddenCreate: true,
hiddenWorkflowCategories: [
WorkflowCategory.Example,
WorkflowCategory.Project,
],
projectId,
onImport,
nameValidators,
onCreateSuccess: async ({ workflowId }) => {
close();
await refetch();
openResource({
resourceType: BizResourceTypeEnum.Workflow,
resourceId: workflowId,
});
},
});
return { modal: node, importLibrary: open };
};

View File

@@ -0,0 +1,48 @@
/*
* 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 RefObject } from 'react';
import { useMemoizedFn } from 'ahooks';
import { type WorkflowPlaygroundRef } from '@coze-workflow/playground';
import {
useListenMessageEvent,
type URI,
type MessageEvent,
} from '@coze-project-ide/framework';
export const useListenWFMessageEvent = (
uri: URI,
ref: RefObject<WorkflowPlaygroundRef>,
) => {
const listener = useMemoizedFn((e: MessageEvent) => {
if (e.name === 'process' && e.data?.executeId && ref.current) {
ref.current.getProcess({ executeId: e.data.executeId });
} else if (e.name === 'debug' && ref.current) {
const { nodeId, executeId, subExecuteId } = e?.data || {};
if (nodeId) {
setTimeout(() => {
ref.current?.scrollToNode(nodeId);
}, 1000);
}
if (executeId) {
ref.current.showTestRunResult(executeId, subExecuteId);
}
}
});
useListenMessageEvent(uri, listener);
};

View File

@@ -0,0 +1,55 @@
/*
* 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 RefObject } from 'react';
import {
useResourceList,
type BizResourceType,
} from '@coze-project-ide/biz-components';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { CustomError } from '@coze-arch/bot-error';
export const useNameValidators = ({
currentResourceRef,
}: {
currentResourceRef?: RefObject<BizResourceType | undefined>;
} = {}): Array<{
validator: (rules: unknown[], value: string) => boolean | Error;
}> => {
const { workflowResource } = useResourceList();
return [
{
validator(_, value) {
// 过滤掉当前资源
const otherResource = currentResourceRef?.current
? workflowResource.filter(
r => r.res_id !== currentResourceRef?.current?.res_id,
)
: workflowResource;
if (otherResource.map(item => item.name).includes(value)) {
return new CustomError(
REPORT_EVENTS.formValidation,
I18n.t('project_resource_sidebar_warning_label_exists', {
label: value,
}),
);
}
return true;
},
},
];
};

View File

@@ -0,0 +1,47 @@
/*
* 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 { useMemoizedFn } from 'ahooks';
import { type ProjectApi } from '@coze-workflow/playground';
import {
useSendMessageEvent,
useIDENavigate,
useCurrentWidgetContext,
useIDEGlobalContext,
} from '@coze-project-ide/framework';
/**
* 注入到 workflow 内 project api 的能力。
* 注:非响应式
*/
export const useProjectApi = () => {
const { sendOpen } = useSendMessageEvent();
const { widget: uiWidget } = useCurrentWidgetContext();
const navigate = useIDENavigate();
const ideGlobalContext = useIDEGlobalContext();
const getProjectAPI = useMemoizedFn(() => {
const api: ProjectApi = {
navigate,
ideGlobalStore: ideGlobalContext,
setWidgetUIState: (status: string) => uiWidget.setUIState(status as any),
sendMsgOpenWidget: sendOpen,
};
return api;
});
return getProjectAPI;
};

View File

@@ -0,0 +1,41 @@
/*
* 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 RefObject } from 'react';
import { type WorkflowPlaygroundRef } from '@coze-workflow/playground';
import {
useIDEParams,
useIDENavigate,
useCurrentWidget,
getURLByURI,
type ProjectIDEWidget,
} from '@coze-project-ide/framework';
export const useRefresh = (ref: RefObject<WorkflowPlaygroundRef>) => {
const widget = useCurrentWidget<ProjectIDEWidget>();
const params = useIDEParams();
const navigate = useIDENavigate();
useEffect(() => {
if (params.refresh) {
ref.current?.reload();
navigate(getURLByURI(widget.uri!.removeQueryObject('refresh')), {
replace: true,
});
}
}, [params.refresh, ref, widget, navigate]);
};

View File

@@ -0,0 +1,55 @@
/*
* 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 BizResourceType,
useResourceCopyDispatch,
} from '@coze-project-ide/biz-components';
import { resource_resource_common } from '@coze-arch/bot-api/plugin_develop';
export interface ResourceOperationProps {
projectId: string;
}
export const useResourceOperation = ({ projectId }: ResourceOperationProps) => {
const copyDispatch = useResourceCopyDispatch();
return async ({
scene,
resource,
}: {
scene: resource_resource_common.ResourceCopyScene;
resource?: BizResourceType;
}) => {
try {
console.log(
`[ResourceFolder]workflow resource copy dispatch, scene ${scene}>>>`,
resource,
);
await copyDispatch({
scene,
res_id: resource?.id,
res_type: resource_resource_common.ResType.Workflow,
project_id: projectId,
res_name: resource?.name || '',
});
} catch (e) {
console.error(
`[ResourceFolder]workflow resource copy dispatch, scene ${scene} error>>>`,
e,
);
}
};
};

View File

@@ -0,0 +1,274 @@
/*
* 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, {
type ReactNode,
useCallback,
useMemo,
useRef,
type RefObject,
} from 'react';
import {
useCreateWorkflowModal,
WorkflowModalFrom,
} from '@coze-workflow/components';
import {
type ResourceFolderProps,
type ResourceType,
useProjectId,
useSpaceId,
} from '@coze-project-ide/framework';
import {
BizResourceContextMenuBtnType,
type BizResourceType,
BizResourceTypeEnum,
type ResourceFolderCozeProps,
useOpenResource,
usePrimarySidebarStore,
} from '@coze-project-ide/biz-components';
import { I18n } from '@coze-arch/i18n';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
import { ResourceCopyScene } from '@coze-arch/bot-api/plugin_develop';
import { workflowApi } from '@coze-arch/bot-api';
import { Toast } from '@coze-arch/coze-design';
import { WORKFLOW_SUB_TYPE_ICON_MAP } from '@/constants';
import { WorkflowTooltip } from '@/components';
import { useResourceOperation } from './use-resource-operation';
import { useNameValidators } from './use-name-validators';
import { useImportLibraryWorkflow } from './use-import-library-workflow';
import { useChangeFlowMode } from './use-change-flow-mode';
type UseWorkflowResourceReturn = Pick<
ResourceFolderCozeProps,
| 'onCustomCreate'
| 'onDelete'
| 'onChangeName'
| 'onAction'
| 'createResourceConfig'
| 'iconRender'
> & { modals: ReactNode };
// eslint-disable-next-line @coze-arch/max-line-per-function
export const useWorkflowResource = (): UseWorkflowResourceReturn => {
const refetch = usePrimarySidebarStore(state => state.refetch);
const spaceId = useSpaceId();
const projectId = useProjectId();
const openResource = useOpenResource();
const currentResourceRef = useRef<BizResourceType>();
const nameValidators = useNameValidators({
currentResourceRef: currentResourceRef as RefObject<
BizResourceType | undefined
>,
});
const {
createWorkflowModal,
workflowModal: templateWorkflowModal,
openCreateModal,
handleEditWorkflow,
} = useCreateWorkflowModal({
from: WorkflowModalFrom.ProjectAddWorkflowResource,
spaceId,
projectId,
hiddenTemplateEntry: true,
nameValidators,
refreshPage: () => {
currentResourceRef.current = undefined;
refetch?.();
},
onCreateSuccess: async ({ workflowId }) => {
await refetch?.();
openResource({
resourceType: BizResourceTypeEnum.Workflow,
resourceId: workflowId,
});
},
});
const onCustomCreate: ResourceFolderCozeProps['onCustomCreate'] = (
resourceType,
subType,
) => {
console.log('[ResourceFolder]on custom create>>>', resourceType, subType);
openCreateModal(subType as WorkflowMode);
};
const onChangeName: ResourceFolderProps['onChangeName'] = useCallback(
async changeNameEvent => {
try {
console.log('[ResourceFolder]on change name>>>', changeNameEvent);
const resp = await workflowApi.UpdateWorkflowMeta({
space_id: spaceId,
workflow_id: changeNameEvent.id,
name: changeNameEvent.name,
});
console.log('[ResourceFolder]rename workflow response>>>', resp);
} catch (e) {
console.log('[ResourceFolder]rename workflow error>>>', e);
} finally {
refetch();
}
},
[refetch, spaceId],
);
const updateDesc = useCallback(
async (resource?: BizResourceType) => {
if (!resource?.res_id) {
return;
}
currentResourceRef.current = resource;
const resp = await workflowApi.GetWorkflowDetail({
space_id: spaceId,
workflow_ids: [resource?.res_id],
});
const workflowInfo = resp?.data?.[0];
if (!workflowInfo) {
return;
}
handleEditWorkflow({
space_id: workflowInfo.space_id,
workflow_id: workflowInfo.workflow_id,
url: workflowInfo.icon,
icon_uri: workflowInfo.icon_uri,
name: workflowInfo.name,
desc: workflowInfo.desc,
});
},
[spaceId, handleEditWorkflow],
);
const onDelete = useCallback(
async (resources: ResourceType[]) => {
try {
console.log('[ResourceFolder]on delete>>>', resources);
console.log('delete start>>>', Date.now());
const resp = await workflowApi.BatchDeleteWorkflow({
space_id: spaceId,
workflow_id_list: resources
.filter(r => r.type === BizResourceTypeEnum.Workflow)
.map(r => r.id),
});
console.log('delete end>>>', Date.now());
Toast.success(I18n.t('Delete_success'));
refetch().then(() => console.log('refetch end>>>', Date.now()));
console.log('[ResourceFolder]delete workflow response>>>', resp);
} catch (e) {
console.log('[ResourceFolder]delete workflow error>>>', e);
Toast.error(I18n.t('Delete_failed'));
}
},
[refetch, spaceId],
);
const { modal: workflowModal, importLibrary } = useImportLibraryWorkflow({
projectId,
});
const changeFlowMode = useChangeFlowMode();
const resourceOperation = useResourceOperation({ projectId });
const onAction = (
action: BizResourceContextMenuBtnType,
resource?: BizResourceType,
) => {
switch (action) {
case BizResourceContextMenuBtnType.ImportLibraryResource:
return importLibrary();
case BizResourceContextMenuBtnType.DuplicateResource:
return resourceOperation({
scene: ResourceCopyScene.CopyProjectResource,
resource,
});
case BizResourceContextMenuBtnType.MoveToLibrary:
return resourceOperation({
scene: ResourceCopyScene.MoveResourceToLibrary,
resource,
});
case BizResourceContextMenuBtnType.CopyToLibrary:
return resourceOperation({
scene: ResourceCopyScene.CopyResourceToLibrary,
resource,
});
case BizResourceContextMenuBtnType.UpdateDesc:
return updateDesc(resource);
case BizResourceContextMenuBtnType.SwitchToChatflow:
return changeFlowMode(
WorkflowMode.ChatFlow,
resource?.res_id ?? '',
spaceId,
);
case BizResourceContextMenuBtnType.SwitchToWorkflow:
return changeFlowMode(
WorkflowMode.Workflow,
resource?.res_id ?? '',
spaceId,
);
default:
console.warn('[WorkflowResource]unsupported action>>>', action);
break;
}
};
const createResourceConfig = useMemo(
() =>
[
{
icon: WORKFLOW_SUB_TYPE_ICON_MAP[WorkflowMode.Workflow],
label: I18n.t('project_resource_sidebar_create_new_resource', {
resource: I18n.t('library_resource_type_workflow'),
}),
subType: WorkflowMode.Workflow,
tooltip: <WorkflowTooltip flowMode={WorkflowMode.Workflow} />,
},
// 社区版本暂不支持对话流
IS_OPEN_SOURCE
? null
: {
icon: WORKFLOW_SUB_TYPE_ICON_MAP[WorkflowMode.ChatFlow],
label: I18n.t('project_resource_sidebar_create_new_resource', {
resource: I18n.t('wf_chatflow_76'),
}),
subType: WorkflowMode.ChatFlow,
tooltip: <WorkflowTooltip flowMode={WorkflowMode.ChatFlow} />,
},
].filter(Boolean) as ResourceFolderCozeProps['createResourceConfig'],
[],
);
const iconRender: ResourceFolderCozeProps['iconRender'] = useMemo(
() =>
({ resource }) => (
<>
{
WORKFLOW_SUB_TYPE_ICON_MAP[
resource.res_sub_type || WorkflowMode.Workflow
]
}
</>
),
[],
);
return {
onChangeName,
onAction,
onDelete,
onCustomCreate,
createResourceConfig,
iconRender,
modals: [workflowModal, createWorkflowModal, templateWorkflowModal],
};
};

View File

@@ -0,0 +1,31 @@
/*
* 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 {
ResourceRefTooltip,
usePluginDetail,
LinkNode,
navigateResource,
} from '@coze-workflow/playground';
export { WorkflowWidgetRegistry } from './registry';
/**
* 会话管理 registry
*/
export { ConversationRegistry } from './conversation/registry';
export { useWorkflowResource } from './hooks/use-workflow-resource';
export { WorkflowTooltip, WorkflowWidgetIcon } from './components';
export { WORKFLOW_SUB_TYPE_ICON_MAP } from './constants';

View File

@@ -0,0 +1,124 @@
/*
* 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, useRef } from 'react';
import {
WorkflowPlayground,
type WorkflowGlobalStateEntity,
type WorkflowPlaygroundRef,
} from '@coze-workflow/playground';
import type { WsMessageProps } from '@coze-project-ide/framework/src/types';
import {
useSpaceId,
useProjectId,
useCommitVersion,
useCurrentWidgetContext,
useCurrentWidget,
type ProjectIDEWidget,
useWsListener,
} from '@coze-project-ide/framework';
import {
CustomResourceFolderShortcutService,
usePrimarySidebarStore,
} from '@coze-project-ide/biz-components';
import { useFlags } from '@coze-arch/bot-flags';
import { useRefresh } from './hooks/use-refresh';
import { useProjectApi, useListenWFMessageEvent } from './hooks';
const Main = () => {
const workflowRef = useRef<WorkflowPlaygroundRef>(null);
const spaceId = useSpaceId();
const projectId = useProjectId();
const { version: commitVersion } = useCommitVersion();
const [FLAGS] = useFlags();
const refetchProjectResourceList = usePrimarySidebarStore(
state => state.refetch,
);
const { uri, widget: uiWidget } = useCurrentWidgetContext();
const widget = useCurrentWidget<ProjectIDEWidget>();
const workflowId = useMemo(() => uri?.displayName, [uri]);
const getProjectApi = useProjectApi();
const handleInit = useCallback(
(workflowState: WorkflowGlobalStateEntity) => {
const name = workflowState.info?.name;
if (name) {
uiWidget.setTitle(name);
uiWidget.setUIState('normal');
}
uiWidget.setIconType(String(workflowState.flowMode));
},
[uiWidget],
);
const handleReload = () => {
widget.refresh();
widget.context.widget.setUIState('loading');
};
useWsListener((props: WsMessageProps) => {
if (!FLAGS['bot.automation.project_multi_tab']) {
return;
}
workflowRef.current?.onResourceChange(props, handleReload);
});
useEffect(() => {
const disposable = uiWidget.onFocus(() => {
workflowRef.current?.triggerFitView();
workflowRef.current?.loadGlobalVariables();
});
return () => {
disposable?.dispose?.();
};
}, []);
useRefresh(workflowRef);
useListenWFMessageEvent(uri!, workflowRef);
if (!spaceId || !workflowId) {
return null;
}
return (
<WorkflowPlayground
ref={workflowRef}
spaceId={spaceId}
workflowId={workflowId}
projectCommitVersion={commitVersion}
renderHeader={() => null}
onInit={handleInit}
projectId={projectId}
getProjectApi={getProjectApi}
// parentContainer={widget.container}
className="project-ide-workflow-playground"
refetchProjectResourceList={refetchProjectResourceList}
renameProjectResource={(resourceId: string) => {
const shortcutService = widget.container.get(
CustomResourceFolderShortcutService,
);
shortcutService.renameResource(resourceId);
}}
/>
);
};
export default Main;

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 React from 'react';
import {
LayoutPanelType,
withLazyLoad,
type WidgetRegistry,
} from '@coze-project-ide/framework';
import { WorkflowWidgetIcon } from './components';
export const WorkflowWidgetRegistry: WidgetRegistry = {
match: /\/workflow\/.*/,
area: LayoutPanelType.MAIN_PANEL,
renderContent() {
const Component = withLazyLoad(() => import('./main'));
return <Component />;
},
renderIcon(ctx) {
return <WorkflowWidgetIcon context={ctx} />;
},
onFocus(ctx) {
ctx.widget.onFocusEmitter.fire();
},
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />