feat: Support for Chat Flow & Agent Support for binding a single chat flow (#765)

Co-authored-by: Yu Yang <72337138+tomasyu985@users.noreply.github.com>
Co-authored-by: zengxiaohui <csu.zengxiaohui@gmail.com>
Co-authored-by: lijunwen.gigoo <lijunwen.gigoo@bytedance.com>
Co-authored-by: lvxinyu.1117 <lvxinyu.1117@bytedance.com>
Co-authored-by: liuyunchao.0510 <liuyunchao.0510@bytedance.com>
Co-authored-by: haozhenfei <37089575+haozhenfei@users.noreply.github.com>
Co-authored-by: July <jiangxujin@bytedance.com>
Co-authored-by: tecvan-fe <fanwenjie.fe@bytedance.com>
This commit is contained in:
Zhj
2025-08-28 21:53:32 +08:00
committed by GitHub
parent bbc615a18e
commit d70101c979
503 changed files with 48036 additions and 3427 deletions

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { type PropsWithChildren } from 'react';
import { forwardRef, type PropsWithChildren } from 'react';
import classNames from 'classnames';
@@ -25,14 +25,16 @@ interface ActionBarHoverContainerProps {
style?: React.CSSProperties;
}
export const ActionBarHoverContainer: React.FC<
export const ActionBarHoverContainer = forwardRef<
HTMLDivElement,
PropsWithChildren<ActionBarHoverContainerProps>
> = ({ children, style }) => (
>(({ children, style }, ref) => (
<div
data-testid="chat-area.answer-action.hover-action-bar"
className={classNames(s.container, ['coz-stroke-primary', 'coz-bg-max'])}
style={style}
ref={ref}
>
{children}
</div>
);
));

View File

@@ -34,15 +34,27 @@ import { useTooltipTrigger } from '../../hooks/use-tooltip-trigger';
type CopyTextMessageProps = Omit<
ComponentProps<typeof IconButton>,
'icon' | 'iconSize' | 'onClick'
>;
> & {
isMustGroupLastAnswerMessage?: boolean;
isUseExternalContent?: boolean;
externalContent?: string;
};
export const CopyTextMessage: React.FC<
PropsWithChildren<CopyTextMessageProps>
> = ({ className, ...props }) => {
> = ({
className,
isMustGroupLastAnswerMessage = true,
isUseExternalContent = false,
externalContent,
...props
}) => {
const { reporter } = useChatArea();
const { message, meta } = useMessageBoxContext();
const { content } = message;
const content = isUseExternalContent
? externalContent || ''
: message.content;
const [isCopySuccessful, setIsCopySuccessful] = useState<boolean>(false);
const trigger = useTooltipTrigger('hover');
@@ -87,7 +99,7 @@ export const CopyTextMessage: React.FC<
return null;
}
if (!meta.isGroupLastAnswerMessage) {
if (!meta.isGroupLastAnswerMessage && isMustGroupLastAnswerMessage) {
return null;
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.build.json",
"compilerOptions": {
"noEmit": true,
"declaration": false,
"declarationMap": false,
"sourceMap": false,
"outDir": null,
"tsBuildInfoFile": null
},
"exclude": ["dist", "node_modules", "__tests__"]
}

View File

@@ -9,6 +9,9 @@
},
{
"path": "./tsconfig.misc.json"
},
{
"path": "./tsconfig.dev.json"
}
],
"exclude": ["**/*"]

View File

@@ -222,6 +222,7 @@ export const ChatArea = forwardRef<ChatAreaRef, ChatAreaProps>((props, ref) => {
selectable,
showClearContextDivider,
messageWidth,
messageMaxWidth,
readonly,
uiKitChatInputButtonConfig,
uikitChatInputButtonStatus,
@@ -237,6 +238,7 @@ export const ChatArea = forwardRef<ChatAreaRef, ChatAreaProps>((props, ref) => {
isOnboardingCentered,
fileLimit,
stopRespondOverrideWaiting,
isMiniScreen,
} = props;
const getScrollViewRef = useRef<() => ScrollViewController>(null);
const {
@@ -278,6 +280,7 @@ export const ChatArea = forwardRef<ChatAreaRef, ChatAreaProps>((props, ref) => {
enableSelectOnboarding,
showClearContextDivider,
messageWidth,
messageMaxWidth,
readonly: readonly || isClearMessageHistoryLock,
uiKitChatInputButtonConfig,
theme,
@@ -304,6 +307,7 @@ export const ChatArea = forwardRef<ChatAreaRef, ChatAreaProps>((props, ref) => {
onboardingSuggestionsShowMode,
showBackground,
stopRespondOverrideWaiting,
isMiniScreen,
}}
>
<ChatAreaMain

View File

@@ -28,13 +28,6 @@ import {
import { nanoid } from 'nanoid';
import classNames from 'classnames';
import {
useUIKitCustomComponent,
ChatInput as UIKitChatInput,
type InputRefObject,
} from '@coze-common/chat-uikit';
import { safeAsyncThrow } from '@coze-common/chat-area-utils';
import { I18n } from '@coze-arch/i18n';
import {
type IChatInputProps,
UploadType,
@@ -42,6 +35,13 @@ import {
MAX_FILE_MBYTE,
Layout,
} from '@coze-common/chat-uikit-shared';
import {
useUIKitCustomComponent,
ChatInput as UIKitChatInput,
type InputRefObject,
} from '@coze-common/chat-uikit';
import { safeAsyncThrow } from '@coze-common/chat-area-utils';
import { I18n } from '@coze-arch/i18n';
import { BatchUploadFileList } from '../batch-upload-file-list';
import { getSendMultimodalMessageStrategy } from '../../utils/message';
@@ -158,6 +158,7 @@ export const ChatInput: <T extends OverrideProps>(
wrapperClassName,
inputNativeCallbacks,
safeAreaClassName,
...restInputProps
} = useChatInputProps();
const showBackground = useShowBackGround();
@@ -395,6 +396,7 @@ export const ChatInput: <T extends OverrideProps>(
showBackground={showBackground}
limitFileCount={fileLimit}
onPaste={handlePaste}
{...restInputProps}
{...componentProps}
/>
<div

View File

@@ -39,6 +39,7 @@ import { usePreference } from '../../../context/preference';
import s from './index.module.less';
import './index.less';
import { usePluginCustomComponents } from '../../../plugin/hooks/use-plugin-custom-components';
const BuiltinMessageGroupWrapper: ComponentTypesMap['messageGroupWrapper'] = ({
children,
@@ -135,6 +136,18 @@ export const MessageGroupWrapper: React.FC<
const showContextDividerWithOnboarding =
showClearContextDividerByPreference && showContextDivider;
const customMessageGroupFooterPlugin =
usePluginCustomComponents('MessageGroupFooter').at(0);
const renderFooter = () => {
const usedFooter = customMessageGroupFooterPlugin;
if (!usedFooter) {
return null;
}
const { Component } = usedFooter;
return <Component messageGroup={messageGroup} />;
};
return (
<>
@@ -152,6 +165,7 @@ export const MessageGroupWrapper: React.FC<
isSendingMessage={isSendingMessage}
messageGroup={messageGroup}
>
{renderFooter?.()}
{isLatest ? (
<>
{!showContextDividerWithOnboarding && <SuggestionInChat />}

View File

@@ -16,6 +16,10 @@
import { type MouseEvent } from 'react';
import {
type IEventCallbacks,
type IOnLinkClickParams,
} from '@coze-common/chat-uikit-shared';
import { type MessageBoxTheme } from '@coze-common/chat-uikit';
import {
type ClearMessageContextParams,
@@ -25,7 +29,6 @@ import {
type ChatCoreError,
type GetHistoryMessageResponse,
} from '@coze-common/chat-core';
import { type IOnLinkClickParams } from '@coze-common/chat-uikit-shared';
import {
type Message as BuiltInMessage,
@@ -150,6 +153,7 @@ export interface ChatAreaMessageEventMap {
event: MouseEvent<Element, globalThis.MouseEvent>,
) => void;
onBeforeStopResponding: () => void;
onCopyUpload: IEventCallbacks['onCopyUpload'];
}
export type ChatAreaEventCallback = Partial<ChatAreaLifeCycleEventMap> &

View File

@@ -22,4 +22,5 @@ export const defaultConfigs: ChatAreaConfigs = {
ignoreMessageConfigList: [],
groupUserMessage: false,
uploadPlugin: UploadPlugin,
isShowFunctionCallBox: true,
};

View File

@@ -106,6 +106,7 @@ export const allIgnorableMessageTypes = [
export interface ChatAreaConfigs {
ignoreMessageConfigList: IgnoreMessageType[];
showFunctionCallDetail: boolean;
isShowFunctionCallBox: boolean;
// Whether to group user messages (merge avatars)
groupUserMessage: boolean;
uploadPlugin: typeof UploadPlugin;

View File

@@ -23,7 +23,8 @@ import {
type OnBeforeSubmit = IChatInputProps['onBeforeSubmit'];
export interface ChatInputProps {
export interface ChatInputProps
extends Pick<IChatInputProps, 'leftActions' | 'rightSlot'> {
/**
* {@link OnBeforeSubmit}
*/

View File

@@ -26,6 +26,7 @@ const getDefaultCopywriting = (): CopywritingContextInterface => ({
textareaBottomTips: '',
clearContextDividerText: '',
clearContextTooltipContent: '',
audioButtonTooltipContent: '',
});
export const CopywritingContext = createContext<CopywritingContextInterface>(

View File

@@ -19,4 +19,5 @@ export interface CopywritingContextInterface {
textareaBottomTips: string;
clearContextDividerText: string;
clearContextTooltipContent: string;
audioButtonTooltipContent: string;
}

View File

@@ -17,9 +17,9 @@
import { createContext, type PropsWithChildren, useContext } from 'react';
import { isUndefined, merge, omitBy } from 'lodash-es';
import { Layout } from '@coze-common/chat-uikit-shared';
import { type MakeValueUndefinable } from '@coze-common/chat-area-utils';
import { SuggestedQuestionsShowMode } from '@coze-arch/bot-api/developer_api';
import { Layout } from '@coze-common/chat-uikit-shared';
import {
type PreferenceContextInterface,
@@ -49,6 +49,7 @@ const getDefaultPreference = (): Required<PreferenceContextInterface> => ({
selectable: false,
showClearContextDivider: true,
messageWidth: '100%',
messageMaxWidth: '',
readonly: false,
uiKitChatInputButtonConfig: {
isSendButtonVisible: true,
@@ -68,6 +69,7 @@ const getDefaultPreference = (): Required<PreferenceContextInterface> => ({
forceShowOnboardingMessage: false,
showStopRespond: true,
layout: Layout.PC,
isMiniScreen: false,
isOnboardingCentered: false,
stopRespondOverrideWaiting: undefined,
});

View File

@@ -96,6 +96,10 @@ export interface PreferenceContextInterface {
* message list width
*/
messageWidth: string;
/**
* message list max width
*/
messageMaxWidth: string;
/**
* Is it read-only?
*/
@@ -156,6 +160,11 @@ export interface PreferenceContextInterface {
*/
layout: Layout;
/**
* Whether to enable mini screen mode
*/
isMiniScreen: boolean;
/**
* Whether to force the stop reply button to be displayed
*/

View File

@@ -17,6 +17,7 @@
import { type PropsWithChildren, useRef, useEffect } from 'react';
import { UploadController } from '../../service/upload-controller';
import { useChatAreaContext } from '../../hooks/context/use-chat-area-context';
import {
UploadControllerContext,
type UploadControllerContextProps,
@@ -25,12 +26,16 @@ import {
export const UploadControllerProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
const { configs } = useChatAreaContext();
const uploadControllerMap = useRef<
UploadControllerContextProps['uploadControllerMap']
>({});
const createControllerAndUpload: UploadControllerContextProps['createControllerAndUpload'] =
param => {
uploadControllerMap.current[param.fileId] = new UploadController(param);
uploadControllerMap.current[param.fileId] = new UploadController({
...param,
multiUploadPlugin: configs.uploadPlugin,
});
};
const cancelUploadById: UploadControllerContextProps['cancelUploadById'] =
id => {

View File

@@ -301,3 +301,5 @@ export {
} from '@coze-common/chat-core';
export { type OnboardingSelectChangeCallback } from './context/chat-area-context/chat-area-callback';
export { ChatInputArea } from './components/chat-input';
export { type ChatMessage } from '@coze-arch/bot-api/developer_api';
export { useBuiltinButtonStatus } from './hooks/uikit/use-builtin-button-status';

View File

@@ -16,6 +16,7 @@
import { type ComponentType } from 'react';
import { type MessageGroup } from '../../../store/types';
import { type MessageBoxProps } from '../../../components/types';
import {
type CustomSendMessageBox,
@@ -44,6 +45,8 @@ export interface CustomComponent {
MessageBox: ComponentType<MessageBoxProps>;
MessageBoxFooter: CustomMessageBoxFooter;
MessageBoxHoverSlot: ComponentType;
MessageGroupFooter: ComponentType<{ messageGroup: MessageGroup }>;
UIKitMessageBoxPlugin: ComponentType<CustomUiKitMessageBoxProps>;
UIKitOnBoardingPlugin: ComponentType;
}

View File

@@ -21,6 +21,7 @@ export interface UploadControllerProps {
fileId: string;
file: File;
userId: string;
multiUploadPlugin?: typeof UploadPlugin;
onProgress: (event: EventPayloadMap['progress'], fileId: string) => void;
onComplete: (event: EventPayloadMap['complete'], fileId: string) => void;
onError: (event: EventPayloadMap['error'], fileId: string) => void;
@@ -39,9 +40,10 @@ export class UploadController {
onComplete,
onError,
onReady,
multiUploadPlugin = UploadPlugin,
}: UploadControllerProps) {
this.fileId = fileId;
this.uploadPlugin = new UploadPlugin({
this.uploadPlugin = new multiUploadPlugin({
file,
userId,
type: isImage(file) ? 'image' : 'object',

View File

@@ -42,7 +42,6 @@ import { MessageManager } from '@/message/message-manager';
import { ChunkProcessor, PreSendLocalMessageFactory } from '@/message';
import { HttpChunk } from '@/channel/http-chunk';
import { type TokenManager } from '../credential';
import {
type ChatASRParams,
type BreakMessageParams,
@@ -70,6 +69,7 @@ import { MessageManagerService } from './services/message-manager-service';
import { HttpChunkService } from './services/http-chunk-service';
import { CreateMessageService } from './services/create-message-service';
import { ReportEventsTracer, SlardarEvents } from './events/slardar-events';
import { type TokenManager } from '../credential';
export default class ChatSDK {
private static instances: Map<string, ChatSDK> = new Map();
@@ -523,4 +523,10 @@ export default class ChatSDK {
}
return this.messageManagerService.chatASR(params);
}
updateConversationId(conversationId: string) {
this.conversation_id = conversationId;
this.messageManagerService.conversation_id = conversationId;
this.preSendLocalMessageFactory.conversation_id = conversationId;
}
}

View File

@@ -137,7 +137,7 @@ export class RequestManager {
// Execute incoming unified hooks
const onCommonAfterResponse = async (
response: AxiosResponse,
hooksName: 'onAfterResponse' | 'onErrrorResponse' = 'onAfterResponse',
hooksName: 'onAfterResponse' | 'onErrorResponse' = 'onAfterResponse',
): Promise<AxiosResponse> => {
// eslint-disable-next-line @typescript-eslint/naming-convention -- temporary variable, quite normal
let _response: AxiosResponse | Promise<AxiosResponse> = response;
@@ -154,7 +154,7 @@ export class RequestManager {
// Execute hooks for each scene
const onSceneAfterResponse = async (
response: AxiosResponse,
hooksName: 'onAfterResponse' | 'onErrrorResponse' = 'onAfterResponse',
hooksName: 'onAfterResponse' | 'onErrorResponse' = 'onAfterResponse',
): Promise<AxiosResponse> => {
const { scenes } = this.mergedBaseOptions;
// eslint-disable-next-line @typescript-eslint/naming-convention -- temporary variable, quite normal
@@ -188,9 +188,9 @@ export class RequestManager {
// eslint-disable-next-line @typescript-eslint/naming-convention -- temporary variable, quite normal
const _response = await onCommonAfterResponse(
response,
'onErrrorResponse',
'onErrorResponse',
);
return await onSceneAfterResponse(_response, 'onErrrorResponse');
return await onSceneAfterResponse(_response, 'onErrorResponse');
},
);
}

View File

@@ -66,7 +66,7 @@ interface Hooks {
onGetMessageStreamParser?: (
requestMessageRawBody: Record<string, unknown>,
) => FetchSteamConfig<ParsedEvent>['streamParser'];
onErrrorResponse?: Array<(response: AxiosResponse) => Promise<AxiosResponse>>;
onErrorResponse?: Array<(response: AxiosResponse) => Promise<AxiosResponse>>;
}
export enum RequestScene {

View File

@@ -127,10 +127,15 @@ export interface IChatInputProps {
leftActions?: ReactNode;
/**
* Right Slot
* Right Actions
*/
rightActions?: ReactNode;
/**
* right slot
*/
rightSlot?: ReactNode;
/**
* Custom send button
*/

View File

@@ -16,16 +16,16 @@
import { type FC } from 'react';
import {
FILE_TYPE_CONFIG,
FileTypeEnum,
} from '@coze-common/chat-core/shared/const';
import { Toast, Upload } from '@coze-arch/coze-design';
import {
type IChatUploadCopywritingConfig,
DEFAULT_MAX_FILE_SIZE,
UploadType,
} from '@coze-common/chat-uikit-shared';
import {
FILE_TYPE_CONFIG,
FileTypeEnum,
} from '@coze-common/chat-core/shared/const';
import { Toast, Upload } from '@coze-arch/coze-design';
interface IChatUploadProps {
/**
@@ -132,6 +132,7 @@ export const ChatUpload: FC<IChatUploadProps> = props => {
onFileChange={handleUpload}
disabled={isDisabled}
multiple={limitFileCount > 1}
uploadTrigger={'custom'}
>
{children}
</Upload>

View File

@@ -40,7 +40,8 @@ export const MessageBox: FC<
messageBubbleClassname,
messageBubbleWrapperClassname,
messageBoxWraperClassname,
messageBoxWrapperClassname,
messageHoverWrapperClassName,
messageErrorWrapperClassname,
isHoverShowUserInfo,
@@ -69,7 +70,8 @@ export const MessageBox: FC<
classname={classname}
messageBubbleWrapperClassname={messageBubbleWrapperClassname}
messageBubbleClassname={messageBubbleClassname}
messageBoxWraperClassname={messageBoxWraperClassname}
messageBoxWrapperClassname={messageBoxWrapperClassname}
messageHoverWrapperClassName={messageHoverWrapperClassName}
messageErrorWrapperClassname={messageErrorWrapperClassname}
isHoverShowUserInfo={isHoverShowUserInfo}
layout={layout}

View File

@@ -24,13 +24,13 @@ import {
import classnames from 'classnames';
import { useClickAway, useHover, useUpdateEffect } from 'ahooks';
import { ErrorBoundary } from '@coze-arch/logger';
import {
Layout,
UIKitEvents,
useUiKitEventCenter,
} from '@coze-common/chat-uikit-shared';
import { useEventCallback } from '@coze-common/chat-hooks';
import { ErrorBoundary } from '@coze-arch/logger';
import { Avatar, Typography } from '@coze-arch/coze-design';
import { UserLabel, UserName } from '../user-label';
@@ -67,7 +67,8 @@ export const MessageBoxWrap: FC<
classname,
messageBubbleClassname,
messageBubbleWrapperClassname,
messageBoxWraperClassname,
messageBoxWrapperClassname,
messageHoverWrapperClassName,
messageErrorWrapperClassname,
isHoverShowUserInfo = true,
layout,
@@ -165,7 +166,7 @@ export const MessageBoxWrap: FC<
// chat-uikit-message-box-container chat-uikit-message-box-container-pc
className={classnames(
messageBoxContainerVariants({ isMobileLayout }),
messageBoxWraperClassname,
messageBoxWrapperClassname,
)}
>
<div
@@ -292,6 +293,17 @@ export const MessageBoxWrap: FC<
>
{right}
</div>
{isHovering || hoverContentVisible ? (
<div
// chat-uikit-message-box-container__message__message-box__hover-container
className={classnames(
'absolute right-[-12px] bottom-[-20px]',
messageHoverWrapperClassName,
)}
>
{hoverContent}
</div>
) : null}
</div>
{/* Please read the refreshContainerWidthConditionally above before changing the style of this dom */}
<div
@@ -301,14 +313,6 @@ export const MessageBoxWrap: FC<
>
{renderFooter?.(refreshContainerWidthConditionally)}
</div>
{isHovering || hoverContentVisible ? (
<div
// chat-uikit-message-box-container__message__message-box__hover-container
className="absolute right-[-12px] bottom-[-20px]"
>
{hoverContent}
</div>
) : null}
</div>
</div>
</div>

View File

@@ -91,7 +91,8 @@ interface MessageBoxBasicProps {
classname?: string;
messageBubbleWrapperClassname?: string;
messageBoxWraperClassname?: string; // Direct father style of message box
messageBoxWrapperClassname?: string; // Direct father style of message box
messageHoverWrapperClassName?: string; // Direct hover style of message box
messageBubbleClassname?: string; // Message The style of the message bubble
messageErrorWrapperClassname?: string; // Message wrong father style
isHoverShowUserInfo?: boolean; // Whether to display user details when hovering
@@ -160,7 +161,9 @@ export interface MessageBoxWrapProps {
contentTime: number | undefined;
classname?: string;
messageBoxWraperClassname?: string; // Direct father style of message box
messageBoxWrapperClassname?: string; // Direct father style of message box
messageHoverWrapperClassName?: string; // Direct hover style of message box
messageBubbleClassname?: string; // Message The style of the message bubble
messageBubbleWrapperClassname?: string; // Message message bubble father style
messageErrorWrapperClassname?: string; // Message wrong father style

View File

@@ -54,7 +54,6 @@ export const TextContent: FC<IMessageContentProps> = props => {
} = props;
const MdBoxLazy = LazyCozeMdBox;
const contentRef = useRef<HTMLDivElement | null>(null);
const { content } = message;
if (!isText(content)) {
@@ -63,7 +62,6 @@ export const TextContent: FC<IMessageContentProps> = props => {
const isStreaming = !message.is_finish;
const text = content.slice(0, message.broken_pos ?? Infinity);
return (
<div
className="chat-uikit-text-content"

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 { memo, useMemo } from 'react';
import { isEqual, isFunction, omitBy } from 'lodash-es';
import { extractChatflowMessage } from './utils';
import { type ChatflowNodeData, type RenderNodeEntryProps } from './type';
import { QuestionNodeRender } from './question-node-render';
import { InputNodeRender } from './input-node-render';
const BaseComponent: React.FC<RenderNodeEntryProps> = ({
message,
...restProps
}) => {
const chatflowNodeData: ChatflowNodeData | undefined = useMemo(
() => extractChatflowMessage(message),
[message],
);
if (!chatflowNodeData) {
return null;
}
if (chatflowNodeData.card_type === 'INPUT') {
return (
<InputNodeRender
data={chatflowNodeData}
message={message}
{...restProps}
/>
);
} else if (chatflowNodeData.card_type === 'QUESTION') {
return (
<QuestionNodeRender
data={chatflowNodeData}
message={message}
{...restProps}
/>
);
} else {
return 'content type is not supported';
}
};
export const WorkflowRenderEntry = memo(BaseComponent, (prevProps, nextProps) =>
isEqual(omitBy(prevProps, isFunction), omitBy(nextProps, isFunction)),
);

View File

@@ -0,0 +1,102 @@
/*
* 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 { produce } from 'immer';
import {
type IEventCallbacks,
type IMessage,
} from '@coze-common/chat-uikit-shared';
import { I18n } from '@coze-arch/i18n';
import { Button, Input, Space, Typography } from '@coze-arch/coze-design';
import { type ChatflowNodeData } from './type';
import { NodeWrapperUI } from './node-wrapper-ui';
export const InputNodeRender = ({
data,
onCardSendMsg,
readonly,
isDisable,
message,
}: {
data: ChatflowNodeData;
onCardSendMsg?: IEventCallbacks['onCardSendMsg'];
readonly?: boolean;
isDisable?: boolean;
message: IMessage;
}) => {
const [inputData, setInputData] = useState<Record<string, string>>({});
const [hasSend, setHasSend] = useState(false);
const disabled = readonly || isDisable || hasSend;
return (
<NodeWrapperUI>
<Space spacing={12} vertical className="w-full">
{data.input_card_data?.map((item, index) => (
<Space
align="start"
className="w-full"
spacing={6}
vertical
key={item?.name + index}
>
<Typography.Text ellipsis className="text-lg !font-medium">
{item?.name}
</Typography.Text>
<Input
disabled={disabled || hasSend}
value={inputData[item.name]}
onChange={value => {
setInputData(
produce(draft => {
draft[item.name] = value;
}),
);
}}
/>
</Space>
))}
<Button
className="w-full"
disabled={disabled}
onClick={() => {
if (disabled) {
return;
}
setHasSend(true);
onCardSendMsg?.({
message,
extra: {
msg:
data.input_card_data
?.map(item => `${item.name}:${inputData[item.name] || ''}`)
.join('\n') || '',
mentionList: message.sender_id
? [{ id: message.sender_id }]
: [],
},
});
}}
>
{I18n.t('workflow_detail_title_testrun_submit')}
</Button>
</Space>
</NodeWrapperUI>
);
};

View File

@@ -0,0 +1,23 @@
/*
* 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 PropsWithChildren } from 'react';
export const NodeWrapperUI: React.FC<PropsWithChildren> = ({ children }) => (
<div className="overflow-hidden w-full min-w-[282px] max-w-[546px] p-[16px] coz-bg-primary">
{children}
</div>
);

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type IEventCallbacks,
type IMessage,
} from '@coze-common/chat-uikit-shared';
import { Button, Space, Typography } from '@coze-arch/coze-design';
import { type ChatflowNodeData } from './type';
import { NodeWrapperUI } from './node-wrapper-ui';
export const QuestionNodeRender = ({
data,
onCardSendMsg,
readonly,
isDisable,
message,
}: {
data: ChatflowNodeData;
onCardSendMsg?: IEventCallbacks['onCardSendMsg'];
readonly?: boolean;
isDisable?: boolean;
message: IMessage;
}) => {
const disabled = readonly || isDisable;
return (
<NodeWrapperUI>
<Space className="w-full" vertical spacing={12} align="start">
<Typography.Text ellipsis className="text-18px">
{data.question_card_data?.Title}
</Typography.Text>
<Space className="w-full" vertical spacing={16}>
{data.question_card_data?.Options?.map((option, index) => (
<Button
key={option.name + index}
className="w-full"
color="primary"
disabled={disabled}
onClick={() =>
onCardSendMsg?.({
message,
extra: { msg: option.name, mentionList: [] },
})
}
>
{option.name}
</Button>
))}
</Space>
</Space>
</NodeWrapperUI>
);
};

View File

@@ -0,0 +1,54 @@
/*
* 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 IEventCallbacks,
type IMessage,
} from '@coze-common/chat-uikit-shared';
interface RenderNodeBaseProps extends Pick<IEventCallbacks, 'onCardSendMsg'> {
isDisable: boolean | undefined;
readonly: boolean | undefined;
}
export interface RenderNodeEntryProps extends RenderNodeBaseProps {
message: IMessage;
}
export interface ChatflowNodeData {
card_type: 'QUESTION' | 'INPUT';
input_card_data?: {
type: string;
name: string;
}[];
question_card_data?: {
// eslint-disable-next-line @typescript-eslint/naming-convention
Title: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
Options: { name: string }[];
};
}
export interface ChatflowNodeData {
card_type: 'QUESTION' | 'INPUT';
input_card_data?: {
type: string;
name: string;
}[];
question_card_data?: {
// eslint-disable-next-line @typescript-eslint/naming-convention
Title: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
Options: { name: string }[];
};
}

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 { type IMessage } from '@coze-common/chat-uikit-shared';
import { safeJSONParse } from '@coze-common/chat-uikit';
import { type ChatflowNodeData } from './type';
export const extractChatflowMessage = (message: IMessage) => {
if (message.content_type === 'card') {
const contentStruct = safeJSONParse(message.content) as {
x_properties: {
workflow_card_info: string;
};
};
const workflowDataStr = contentStruct?.x_properties?.workflow_card_info;
if (workflowDataStr) {
const cardData = safeJSONParse(workflowDataStr) as ChatflowNodeData;
if (cardData?.card_type === 'QUESTION' && cardData?.question_card_data) {
return cardData;
}
if (cardData?.card_type === 'INPUT' && cardData?.input_card_data) {
return cardData;
}
}
}
};

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 { ContentBoxType } from '@coze-common/chat-uikit-shared';
import {
ContentBox,
type EnhancedContentConfig,
ContentType,
} from '@coze-common/chat-uikit';
import {
PluginScopeContextProvider,
usePluginCustomComponents,
type ComponentTypesMap,
} from '@coze-common/chat-area';
import { WorkflowRenderEntry } from './components';
const defaultEnable = (value?: boolean) => {
if (typeof value === 'undefined') {
return true;
}
return value;
};
export const ChatFlowRender: ComponentTypesMap['contentBox'] = props => {
const customTextMessageInnerTopSlotList = usePluginCustomComponents(
'TextMessageInnerTopSlot',
);
const enhancedContentConfigList: EnhancedContentConfig[] = [
{
rule: ({ contentType, contentConfigs }) => {
const isCardEnable = defaultEnable(
contentConfigs?.[ContentBoxType.CARD]?.enable,
);
return contentType === ContentType.Card && isCardEnable;
},
render: ({ message, eventCallbacks, options }) => {
const { isCardDisabled, readonly } = options;
const { onCardSendMsg } = eventCallbacks ?? {};
return (
<WorkflowRenderEntry
message={message}
onCardSendMsg={onCardSendMsg}
readonly={readonly}
isDisable={isCardDisabled}
/>
);
},
},
];
return (
<ContentBox
enhancedContentConfigList={enhancedContentConfigList}
multimodalTextContentAddonTop={
<>
{customTextMessageInnerTopSlotList.map(
// eslint-disable-next-line @typescript-eslint/naming-convention -- matches the expected naming
({ pluginName, Component }, index) => (
<PluginScopeContextProvider
pluginName={pluginName}
key={pluginName}
>
<Component key={index} message={props.message} />
</PluginScopeContextProvider>
),
)}
</>
}
{...props}
/>
);
};

View File

@@ -15,3 +15,4 @@
*/
export { WorkflowRender } from './components/workflow-render';
export { ChatFlowRender } from './components/chat-flow-render';

View File

@@ -48,5 +48,6 @@ export const createChatBackgroundPlugin = () => {
};
return {
ChatBackgroundPlugin,
chatBackgroundEvent,
};
};

View File

@@ -117,7 +117,9 @@ export const useSendUseToolMessage = () => {
componentsFormValues: Record<string, TValue>;
options?: SendMessageOptions;
onBeforeSendTemplateShortcut?: (
params: OnBeforeSendTemplateShortcutParams,
params: OnBeforeSendTemplateShortcutParams & {
shortcut: ShortCutCommand;
},
) => OnBeforeSendTemplateShortcutParams;
withoutComponentsList?: boolean;
}) => {
@@ -170,6 +172,7 @@ export const useSendUseToolMessage = () => {
const handledParams = onBeforeSendTemplateShortcut?.({
message: cloneDeep(message),
options: cloneDeep(options),
shortcut,
}) || {
message,
options,

View File

@@ -18,10 +18,10 @@
import { type CSSProperties, type FC, useRef, useState } from 'react';
import cls from 'classnames';
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
import { useMessageWidth } from '@coze-common/chat-area';
import { OverflowList, Popover } from '@coze-arch/bot-semi';
import { SendType } from '@coze-arch/bot-api/playground_api';
import { type ShortCutCommand } from '@coze-agent-ide/tool-config';
import {
enableSendTypePanelHideTemplate,
@@ -53,7 +53,9 @@ interface ChatShortCutBarProps {
wrapperStyle?: CSSProperties;
toolTipFooterSlot?: React.ReactNode;
onBeforeSendTemplateShortcut?: (
params: OnBeforeSendTemplateShortcutParams,
params: OnBeforeSendTemplateShortcutParams & {
shortcut: ShortCutCommand;
},
) => OnBeforeSendTemplateShortcutParams;
onBeforeSendTextMessage?: (
params: OnBeforeSendQueryShortcutParams,

View File

@@ -19,7 +19,7 @@ import { exhaustiveCheckSimple } from '@coze-common/chat-area-utils';
import { type UIMode } from '../shortcut-bar/types';
export const getUIModeByBizScene: (props: {
bizScene: 'debug' | 'store' | 'home' | 'agentApp';
bizScene: 'debug' | 'store' | 'home' | 'agentApp' | 'websdk';
showBackground: boolean;
}) => UIMode = ({ bizScene, showBackground }) => {
if (bizScene === 'agentApp') {
@@ -32,7 +32,7 @@ export const getUIModeByBizScene: (props: {
return 'white';
}
if (bizScene === 'store' || bizScene === 'debug') {
if (bizScene === 'store' || bizScene === 'debug' || bizScene === 'websdk') {
if (showBackground) {
return 'blur';
}

View File

@@ -41,6 +41,7 @@ export const EditorFullInputInner = forwardRef<EditorHandle, EditorInputProps>(
...restProps
} = props;
const [value, setValue] = useState(propsValue);
const [isComposing, setIsComposing] = useState(false);
// Create a mutable reference to store the latest value
const valueRef = useRef(value);
@@ -104,8 +105,16 @@ export const EditorFullInputInner = forwardRef<EditorHandle, EditorInputProps>(
value={value}
onChange={v => {
setValue(v);
if (isComposing) {
return;
}
propsOnChange?.(v);
}}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={e => {
setIsComposing(false);
propsOnChange?.(e.currentTarget.value);
}}
/>
);
},